Skip to content

T-7EJO-extract-corpus-depgraph-module

Status: closed/done · Impact: medium · Complexity: medium

The cross-entity corpus loader, depends_on target resolution, per-entity-type “satisfied” predicate, and the dependency-graph (edge-build + cycle detection) live inside plugin/lib/model/entities/task/ops/next.ts and are duplicated — with a tasks-only resolution gap — inside plugin/lib/model/ops/audit.ts. Two independent cycle detectors and two corpus walks drift apart and each must be fixed twice. Extract this machinery into one model-level module both ops import, killing the duplication and closing audit’s cross-entity resolution gap — the sibling follow-up T-0015 explicitly deferred (“audit.ts cross-entity / bare-id depends_on resolution. Same gap, sibling follow-up”).

LocationRole today
plugin/lib/model/entities/task/ops/next.tsThe sdlc task next op. Defines loadCorpus/DIR_TYPE (cross-entity {basename → {type,status}} index), SATISFIED_BY_TYPE/isSatisfied (per-type satisfied bands), resolveTarget (bare-id ↔ basename), buildDependencyIndex (forward/reverse edges, skipping closed targets), and detectCycle (3-color DFS, returns one cycle’s sorted members). All cross-entity, not task-specific — only the sort-key/liftSortKeys policy is genuinely task-local.
plugin/lib/model/ops/audit.tsThe entities audit op. Carries its own duplicate corpus walk, adjacency build, and findCycles (all elementary cycles, rendered as a -> b -> a paths), and resolves depends_on targets only against docs/planning/tasks/ — a cross-entity target is wrongly reported as a broken edge (the does not resolve to a file under docs/planning/tasks/ finding). This is the gap T-0015 deferred.
plugin/lib/model/entities/task/ops/resolve.tsTask-only file resolver (resolveTaskFile); shares the bare-id/basename matching convention but does not satisfy the cross-entity case.
plugin/lib/services/orchestrator/ops/watch.tsIn-process consumer: imports nextOp and calls nextOp.handler for the dispatchable set. Depends transitively on the machinery being moved; must keep working unchanged.
plugin/lib/util/wikilinks.tsunwrapWikilink — used by both ops to read depends_on entries. Stays a leaf util.
plugin/lib/model/entities/task/ops/tests/next-golden.test.tsGolden tests pinning corpus loading, the satisfied-band ⊆ status-enum assertion, cycle detection, and lift.
plugin/skills/entities-audit/tests/Audit fixtures (cycle-of-two, cycle-of-three, linear-chain, …) that exercise audit’s dependency-graph + cycle reporting end to end.

One new module — plugin/lib/model/corpus/ — owns the corpus + dependency-graph substrate:

  • loader.tsloadCorpus, DIR_TYPE, CorpusEntry, and the shared readFrontmatter helper (moved out of next.ts).
  • resolve.tsresolveTarget(target, basenames) (exact basename, then unique <id>- prefix; ambiguous/none → null).
  • satisfied.tsSATISFIED_BY_TYPE, SatisfiedBand, isSatisfied.
  • graph.ts — a generic edge-builder buildEdges({ nodes, targetsOf, resolve, skip? }) → { forwardEdges, reverseEdges, unresolved } and a single cycle primitive findCycles(nodes, forwardEdges) → string[][] (all elementary cycles, deterministic order). next.ts consumes findCycles(...)[0] (sorted members) for its single-cycle contract; audit.ts consumes the full list for its multi-cycle report.
  • index.ts — re-exports; tests/ — unit tests for each file.

next.ts keeps only the task-sort policy (SortKey, sortKeyFromFm, comparators, liftSortKeys, unsatisfiedTargets wiring) and its op descriptor; it imports everything else from the module. audit.ts resolves depends_on cross-entity through the module and detects cycles via findCycles, retiring its local copies. No task next output changes; audit gains correct cross-entity resolution.

  1. Create plugin/lib/model/corpus/ and move loadCorpus/DIR_TYPE/ CorpusEntry/readFrontmatter (→ loader.ts), resolveTarget (→ resolve.ts), and SATISFIED_BY_TYPE/SatisfiedBand/isSatisfied (→ satisfied.ts) verbatim from next.ts. Re-export from index.ts.
  2. Generalize the graph code into graph.ts: buildEdges takes the node set, a targetsOf(node) (raw depends_on reader), a resolve(raw) (the module’s resolveTarget bound to the corpus basenames), and an optional skip(target) predicate (next passes “status starts with closed/”; audit passes none). Implement findCycles as the all-elementary-cycles finder (reuse audit’s canonicalize/dedup so audit output is preserved); deterministic iteration over sorted nodes.
  3. Rewrite next.ts to import from the module: replace buildDependencyIndex with buildEdges, and detectCycle with findCycles(...)[0] → sorted members (or null). Leave liftSortKeys, sort keys, unsatisfiedTargets, and the op descriptor in place. Target: next-golden.test.ts passes with only import-path edits.
  4. Rewrite audit’s depends_on resolution (the cross-file graph section ending in the tasks-only does not resolve… finding) and findCycles call site to use the module: resolve targets against the full corpus, flag only genuinely-absent targets as broken, and detect cycles via the shared findCycles. Keep audit’s finding-render contract (a -> b -> a) intact.
  5. Relocate the satisfied-band ⊆ status-enum assertion to corpus/tests/satisfied.test.ts (or re-point it at the moved constant) so an enum change that breaks a band still fails. Add corpus/tests/ coverage for loader/resolve/graph.
  6. Add an entities-audit fixture proving cross-entity resolution (a task whose depends_on names a decision) no longer flags a broken edge. Update any audit golden that asserted the old tasks-only behavior.
  7. Sweep doc-comments / definition.md that name next.ts as the home of the corpus loader or satisfied bands; re-point them at plugin/lib/model/corpus/.
LocationKindChange
plugin/lib/model/corpus/loader.tsnewloadCorpus, DIR_TYPE, CorpusEntry, readFrontmatter moved from next.ts
plugin/lib/model/corpus/resolve.tsnewresolveTarget moved from next.ts
plugin/lib/model/corpus/satisfied.tsnewSATISFIED_BY_TYPE, SatisfiedBand, isSatisfied moved from next.ts
plugin/lib/model/corpus/graph.tsnewgeneric buildEdges + all-cycles findCycles (unifies next.detectCycle + audit.findCycles)
plugin/lib/model/corpus/index.tsnewre-exports for the module
plugin/lib/model/corpus/tests/newunit tests for loader/resolve/satisfied/graph, incl. the band ⊆ enum assertion
plugin/lib/model/entities/task/ops/next.ts#loadCorpusmodifydelete moved fns; import from the corpus module; keep sort/lift policy + op descriptor
plugin/lib/model/ops/audit.ts#findCyclesmodifyadopt cross-entity resolver + shared findCycles; retire local copies; preserve render contract
plugin/lib/model/entities/task/ops/tests/next-golden.test.tsmodifyimport-path updates; move/re-point the band ⊆ enum assertion
plugin/skills/entities-audit/tests/modifyadd a cross-entity-edge fixture; refresh any golden asserting tasks-only resolution
plugin/lib/model/entities/task/definition.mdmodifyre-point any pointer naming next.ts as the corpus/satisfied-band home
  • AC-1: plugin/lib/model/corpus/ exports loadCorpus, resolveTarget, SATISFIED_BY_TYPE/isSatisfied, buildEdges, and findCycles; neither next.ts nor audit.ts defines its own corpus loader, target resolver, or cycle detector — both import from the module.
  • AC-2: running bun test against plugin/lib/model/entities/task/ops/tests/next-golden.test.ts passes with only import-path edits; every non-cycle task next fixture yields byte-identical ordered and skipped_blocked output to pre-change.
  • AC-3: a fixture with a task whose depends_on names a non-task entity (e.g. a decision) produces no broken-edge finding from entities audit; only a genuinely-absent target is flagged.
  • AC-4: the cycle-of-two, cycle-of-three, and linear-chain fixtures under plugin/skills/entities-audit/tests/ produce the same cycle / no-cycle verdicts and the same a -> b -> a render as pre-change.
  • AC-5: the satisfied-band ⊆ live-status-enum assertion runs against the moved SATISFIED_BY_TYPE and still fails if any band escapes its schema enum.
  • AC-6: bun test plugin/ is green and the typecheck (bun run typecheck path) is clean.
  • AC-7: git grep -nE 'loadCorpus|SATISFIED_BY_TYPE|DIR_TYPE' -- plugin docs site shows these symbols defined only under plugin/lib/model/corpus/; all other hits are imports or prose pointing at the module, none naming next.ts as their home.
  • The liftSortKeys / sort-key policy. The dependency-lift and the five-key sort tuple stay in next.ts; only the corpus + graph substrate moves. Extracting a generic schedule kernel (lift + order, generic over key type) is a separate, later follow-up gated on a second algorithmic consumer.
  • Changing cycle-finding semantics. Cycles are still detected and reported by both ops; this unifies the implementation, not the behavior.
  • Time-based / recurring task scheduling (B-1NN4-scheduled-tasks) — a different sense of “schedule”; unrelated.
  • Broader audit.ts refactor beyond the depends_on resolution + cycle call sites (the schema/frontmatter/prose checks are untouched).
  • Migrating resolve.ts’s resolveTaskFile into the module; it is a task-file-on-disk resolver with a different contract.
  • none — built entirely on shipped code (T-0015 merged in PR #484).

Surfaced 2026-06-27 reviewing PR #484 / T-0015 for a possible “graph schedule library” extraction. T-0015 shipped the cross-entity corpus loader + satisfied-band predicate under task next but explicitly deferred audit’s matching cross-entity/bare-id depends_on resolution as a sibling follow-up. The review found audit independently re-implements the corpus walk and cycle detection (findCycles) with a tasks-only resolution gap — making the extraction a concrete de-duplication with a ready second consumer, not speculative generality.

Captured by /sdlc:task-work on 2026-06-28. PR: pending.

  • AC-1: auto — git grep confirms loadCorpus / resolveTarget / SATISFIED_BY_TYPE / isSatisfied / buildEdges / findCycles defined only under plugin/lib/model/corpus/ (re-exported via index.ts); next.ts and audit.ts import them and define none locally.
  • AC-2: auto — bun test next-golden.test.ts (11 pass / 0 fail); byte-exact ordered / skipped_blocked goldens untouched, only the import paths and the AC-5-mandated band-assertion relocation changed.
  • AC-3: auto — new fixture plugin/skills/entities-audit/tests/fixtures/cross-entity-edge/ (a task whose depends_on names a decision, plus a genuinely-absent ghost target) drives run_evals.test.ts; the cross-entity edge is not flagged, only the ghost is.
  • AC-4: auto — the cycle-of-two / cycle-of-three / linear-chain cases in run_evals.test.ts pass unchanged, incl. the exact a -> b -> a render (10 pass / 0 fail).
  • AC-5: auto — the band ⊆ live-status-enum invariant relocated to corpus/tests/satisfied.test.ts, importing the moved SATISFIED_BY_TYPE and statusEnumFor; bun test plugin/lib/model/corpus (26 pass / 0 fail).
  • AC-6: auto — bunx tsc --noEmit clean; targeted suites green. Full bun test plugin/ is green except one pre-existing environmental failure (a task-auto-define test that does a real git fetch origin and fails on sandbox auth) that is in the origin/main baseline and unrelated to this change.
  • AC-7: auto — git grep -nE 'loadCorpus|SATISFIED_BY_TYPE|DIR_TYPE' -- plugin docs site: definitions only under corpus/; every other hit is an import/use or the T-7EJO spec prose and its generated site/ mirror, none naming next.ts as the home.
  • The deterministic readiness gate (task gap-report) and the baseline-then-diff quality flow ran end to end without intervention; the sub-agent landed the extraction in six focused commits and left the worktree clean.
  • The golden-test contract (next-golden.test.ts) made AC-2 a binary check — byte-exact ordered output proved the refactor was behavior-preserving with no manual diffing.
  • bun test’s dashboard list table-row prints a raw PID in the path column that the quality-baseline normalizer does not scrub (it scrubs <PID> / <PORT> / <TMPDIR> elsewhere) — so that row differs every run (10737 / 18165 / 25129) and always shows as new-drift: under baseline-gating, defeating subtraction. Extend the normalizer to scrub the PID column of dashboard list output (T-BQRU territory). → T-BQRU-quality-normalize-ports-pids-timings
  • The rumdl aggregate summary lines (Issues: Found N issues in M files, Run rumdl fmt to fix N) are captured as findings, so any change in the total count — including a legitimate reduction — diffs as new-drift: even when no individual finding is introduced. The baseline diff should drop rumdl’s aggregate/summary lines and gate only on per-file finding lines. → T-BCNP-quality-gate-ignores-summary-and-corpus-lines
  • start_task.ts’s appended ## Post-mortem stub uses underscore emphasis, which flips a task file’s MD049 majority style and retroactively flags any pre-existing asterisk emphasis in the spec prose (here schedule kernel) as new drift. Either emit the stub with no emphasis or have task creation pre-normalize emphasis style so the stub cannot strand author prose. → T-AVRD-post-mortem-stub-md049-safe-emphasis

← Back to Tasks