state-hub/docs/forge-dependency-edge-receiving.md

102 lines
5.7 KiB
Markdown
Raw Normal View History

# 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.
## 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.