diff_against_hub accepts hub_edges/edge_targets and reports edges to create, remove, retain, unresolved and external, using the same pure planner that receiving now calls (plan_source_edges + classify_source_edges). The classifier works on a row list, fixing a collapse where an external duplicate of an owned edge key could hide the owned row from removal. 7 new pure tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
101 lines
5.7 KiB
Markdown
101 lines
5.7 KiB
Markdown
# 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.
|