state-hub/docs/forge-dependency-edge-receiving.md
tegwick fceb5f27f0 Receive source-owned depends_on edges through Forge projection (STATE-WP-0094-T05)
Derive frontmatter and task-block depends_on edges from the pinned commit and
reconcile workplan_dependencies under an explicit ownership contract: only
forge:<repo> rows are created or removed, external relations are preserved,
missing/ambiguous/self targets are recorded and hold owned edges, failures
roll back, and repeat receipts are noop. Additive migration b3c4d5e6f7a8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 671493@bnt-lap001
Assistant-Session: 60d7b11f-78f9-4731-9367-918511b2762d
2026-10-04 12:54:57 +02:00

4.8 KiB

Forge dependency-edge receiving (STATE-WP-0094-T05)

Status: source implementation landed 2026-10-04; not deployed. Deployment and any receiving schedule need separate founder authorization.

Problem

docs/owner-return-handoff.md ("T05 graph freshness assessment") records that normal Forge receiving (api/services/forge_projection.py) did not derive depends_on edges or reconcile workplan_dependencies. Only C-20 created edges, from a workstation checkout, and nothing ever removed an edge whose declaration disappeared. Adding a projection that "replaces every dependency row" would delete externally authored relations, so receiving needs an explicit ownership contract first.

Contract

  1. Source. Edges are derived only from the immutable commit that receiving already pins (expected_commit = derived commit), from the same files as the rest of the projection:
    • workplan frontmatter depends_on, depends_on_workplans, depends_on_tasks - split like C-20: ids ending -T\d{2,3} are task targets (starts_after), all others workplan targets (blocks);
    • task-block depends_on - the waiting task is the from side (from_task_id). As in C-20, task-block edges of closed workplans (finished/archived) and closed tasks (done/cancel) are history and not desired.
    • A bare T## target is qualified with the declaring workplan id.
    • Bound historical workplans (historical_preservation) declare no edges.
    • A declaration that is a mapping or nested list refuses the whole derive (ForgeDeriveError); it is never dropped.
  2. Ownership. A row is source-owned iff workplan_dependencies.source_owner = 'forge:<repo-slug>' (additive, nullable column, migration b3c4d5e6f7a8) and its from-workplan belongs to that repository. source_commit records the commit that introduced it. Receiving creates rows only with that owner and removes only rows with that owner. Rows with NULL or any other owner (API, MCP, C-20 fix path, other tools) are externally authored and are never modified or removed.
  3. Already present externally. When a declared edge already exists as an external row (same from workplan, from task, target and relationship), it is preserved, not adopted, and reported in external_edges. Adoption is a deliberate future decision (see risks).
  4. Target resolution (cross-repository). Fleet-wide, excluding projection-retired workplans and their tasks: workplans by ADR-007 derived UUID or record-id slug; tasks by derived UUID or record_id. Exactly one candidate resolves. Targets created in the same receipt resolve, because resolution runs after the workplan/task writes are flushed.
  5. Missing / ambiguous / invalid targets. Zero candidates = missing, more than one = ambiguous (candidates listed; no winner chosen), a self edge = self, a waiting task the hub lacks = from-task-missing. Each is recorded in unresolved_edges; nothing is silently dropped. The receipt itself still applies (a target in a not-yet-received repository must not block the source repository's workplan projection).
  6. Removal. Only source-owned rows whose key is no longer declared are removed - including rows of workplans retired in the same acknowledged receipt. If the from side (workplan or waiting task) has any unresolved declaration, its owned rows are retained and a note is emitted, because the unresolved target may be one of them.
  7. Transactional failure. Edge reconciliation runs inside the same session as the workplan/task projection and flushes at the end. Any error propagates and the endpoint commits nothing; a refused receipt returns before edges are touched.
  8. Repeat noop. Owned rows are not rewritten when unchanged, so the same commit received twice reports status: noop with zero created/removed edges. (unresolved_edges is still reported every time.)

Outcome schema additions (state-hub.projection-reset.v1): created_edges, removed_edges, unresolved_edges, external_edges lists and their counts.

Coexistence with C-20

C-20 keys existence on (from task, target, relationship) regardless of owner, so it does not duplicate a source-owned edge; edges its fix path creates are external to receiving. Existing hub edges therefore stay external until an explicit adoption decision.

Open risks

  • Pre-existing C-20/API edges are never adopted, so a removed declaration does not remove an edge C-20 created earlier; adoption needs its own decision.
  • diff_against_hub (dry run) does not yet preview edge changes.
  • Ambiguity by slug for workplans is mostly prevented by the global slug index; task record_id ambiguity across workplans is the realistic case.
  • Not deployed; no evidence of scheduled freshness. T05 stays in progress.