# 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:'` (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. ## Dry-run preview `diff_against_hub(..., hub_edges=[...], edge_targets={...})` previews edges with the same pure planner receiving uses (`plan_source_edges` + `classify_source_edges`). `hub_edges` are the rows whose from-workplan belongs to the repository (with `source_owner`); `edge_targets` maps a target record id to its fleet-wide non-retired candidate ids. A target the hub lacks but this projection declares resolves to the id the receipt would create. The diff gains `edges` (`create`, `remove`, `retain`, `unresolved`, `external`) and `edge_counts`; only `create`/`remove` make it non-clean. Without `hub_edges` the output is unchanged (no `edges` key). Duplicate keys: several rows may share one edge key (an owned row plus an API-authored duplicate). The classifier works on the row list, so an external duplicate can neither hide an owned row from removal nor turn a satisfied declaration into an external report (fixed 2026-10-04; the first implementation keyed rows by edge key and let the last row win). ## 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. - 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.