- Status: Proposed implementation.
- Owner: Jon.
- Sources checked:
hooks/depcruiser-authored-graph-contract-simplified/index.mdx,.../v3-hooks-aligned-runtime.mdx,E:/hooks/depcruiser-migrations/(current files),E:/hooks/_docs/depcruiser-migrations-design-and-verification.mdand_supplement.md,E:/hooks/config.json,E:/hooks/_core/config-model.mjs,E:/hooks/quality-completion-gate/quality-verify-manifest.json,E:/hooks/.state/.
This page proposes the P0 depcruiser runtime as one buildable cut. It keeps the simplified hooks-aligned file footprint and specifies three enforcement properties the runtime must hold: node completion is declared as data in graph.contract.json; guarded target edits are gated on an approval hash, not on the current node label; and the active migration state is written only by runtime subcommands, never by hand.
Files The Proposed Plan Would Create
| # | File | New or exists | What it does |
|---|---|---|---|
| 1 | depcruiser-migrations/depcruiser-migrations.mjs | New (replaces closure-gate.mjs) | PreToolUse gate plus CLI subcommands arm, advance, disarm, status. Resolves the active node and decides allow/deny on edit tools. |
| 2 | depcruiser-migrations/graph.contract.json | New | The lifecycle as data: each node lists its requiredOutputs and a completion predicate; transitions lists allowed edges including the rework back-edges. |
| 3 | depcruiser-migrations/lib/state-store.mjs | New | Atomic read/write of active.json (temp file + rename), append to events.jsonl, and active-state schema validation. |
| 4 | depcruiser-migrations/lib/path-scope.mjs | New | Repo-relative path resolution, guarded prefix/file matching, and apply_patch header path extraction. |
| 5 | depcruiser-migrations/verify-active-closure.mjs | New | Reads active.json; if armed runs target depcruise, tsc --noEmit in targetCwd, then strict reconcile; gates on its own exit code. |
| 6 | depcruiser-migrations/generate-closure-manifest.mjs | Exists | Builds source-manifest.json from the donor trace with every module verdict set to TBD. |
| 7 | depcruiser-migrations/validate-closure-manifest.mjs | Exists, strengthen | Verdicts mode plus strict reconcile on normalized repo-relative paths with a target_extras allowlist; blocks rather than warns. |
| 8 | depcruiser-migrations/dependency-cruiser.cjs | Exists | Dependency-cruiser rule config (tsPreCompilationDeps, no-circular, no-unresolvable). |
| 9 | depcruiser-migrations/initialize-depcruiser-migrations/SKILL.md | Exists, update | Human workflow; calls arm, then drives the worker through the nodes via advance. |
| 10 | depcruiser-migrations/tests/depcruiser-migrations.test.mjs | New | One focused test file: state validation, guarded matching, apply_patch extraction, the approval-hash gate, and strict reconcile cases. |
| 11 | depcruiser-migrations/README.md | Exists, update | Bundle overview and the node lifecycle. |
Retire after the replacement passes its tests, in one controlled commit: closure-gate.mjs, closure-gate-core.mjs, closure-gate.register.json.
Runtime State And Evidence (created by the runtime, not authored)
| # | Path | What it holds |
|---|---|---|
| 1 | E:/hooks/.state/depcruiser-migrations/active.json | The single armed migration: ids, donor/target roots and entries, guardedPrefixes, guardedFiles, currentNode, approvedManifestHash, evidenceDir, status. |
| 2 | E:/hooks/.state/depcruiser-migrations/events.jsonl | Append-only audit: arm, advance, verify, approve, disarm. |
| 3 | <targetRepo>/_migrations/<source>/<name>/ | Migration evidence: before-edit.donor.json, source-manifest.json, adjudication.json, approval.json, after-edit.target.json. |
Graph Contract (data)
{
"version": 1,
"nodes": [
{ "id": "discovery", "requiredOutputs": ["active.json"], "predicate": "activeStateValid" },
{ "id": "manifest_generation", "requiredOutputs": ["before-edit.donor.json", "source-manifest.json"], "predicate": "manifestHasAllModules" },
{ "id": "adjudication", "requiredOutputs": ["adjudication.json"], "predicate": "noTbdVerdicts" },
{ "id": "approval", "requiredOutputs": ["approval.json"], "predicate": "approvedHashMatchesManifest" },
{ "id": "implementation", "requiredOutputs": [], "predicate": "allApprovedTargetsExist" },
{ "id": "closure_verification","requiredOutputs": ["after-edit.target.json"], "predicate": "verifyActiveClosureExit0" },
{ "id": "final_approval", "requiredOutputs": [], "predicate": "finalApprovalEventRecorded" },
{ "id": "disarm", "requiredOutputs": [], "predicate": "activeStateCleared" }
],
"transitions": [
["discovery", "manifest_generation"],
["manifest_generation", "adjudication"],
["adjudication", "approval"],
["approval", "implementation"],
["implementation", "closure_verification"],
["closure_verification", "final_approval"],
["final_approval", "disarm"],
["closure_verification", "implementation"],
["closure_verification", "adjudication"]
]
}The runtime reads this file to know each node's required outputs and completion predicate. Advancement is decided by artifact existence plus predicate result, never by turn count. The two back-edges from closure_verification carry in-scope debugging (to implementation) and scope changes that need re-approval (to adjudication).
Runtime Behavior, Node By Node
discovery
- Initiated by: the user invokes the
initialize-depcruiser-migrationsskill naming a donor entry and a target entry; the skill callsdepcruiser-migrations arm. - Runtime does: verifies donor and target repos exist; detects each package manager by lockfile; checks
dependency-cruiserin both and installs it only when missing; creates the target_migrations/<source>/<name>/directory; writesactive.jsonwithstatus: "armed",currentNode: "discovery". - Files written:
active.json; anarmrow inevents.jsonl. - Worker (AI): runs the checks through
arm; reports what it found. - User: nothing after the initial invocation.
- Completes when:
active.jsonvalidates anddependency-cruiseris available in both repos.
manifest_generation
- Initiated by:
advanceafter discovery completes. - Runtime does: runs the donor
depcruiseJSON trace from the donor entry intobefore-edit.donor.json; runsgenerate-closure-manifest.mjsto producesource-manifest.jsonwith every moduleverdict: "TBD", plusport_order,imported_by, andexternal_deps. - Files written:
before-edit.donor.json,source-manifest.json. - Worker (AI): runs the trace and the generator.
- User: nothing.
- Completes when:
source-manifest.jsonexists, parses, and lists every donor module with averdictfield.
adjudication
- Initiated by: automatically once the manifest exists.
- Runtime does: holds the node open across as many turns as needed; accepts the worker's writes to
adjudication.json. - Files written:
adjudication.json. - Worker (AI): proposes a
port,adapter,cut, ortarget_extrasdecision for every module, with atarget_pathfor eachport/adapter, and records them inadjudication.json. - User: discusses and decides the verdicts; this is a conversation, not a file edit.
- Completes when:
adjudication.jsonresolves every donor module (noTBD), everyport/adapterhas atarget_path, everytarget_extrasentry has a reason, andreadyForApprovalistrue.
approval
- Initiated by: the worker once adjudication is complete.
- Runtime does: computes
approvedManifestHash = sha256(source-manifest.json + adjudication.json); on the user's grant, writesapproval.jsoncarrying that hash and copies it intoactive.json. - Files written:
approval.json; updatedactive.json; anapproverow inevents.jsonl. - Worker (AI): presents the finalized adjudication summary and requests approval; writes
approval.jsononly after the user approves. - User: approves the adjudicated interpretation. This is the first gate.
- Completes when:
approval.jsonexists and itsapprovedManifestHashequals the current manifest hash.
implementation
- Initiated by: after approval.
- Runtime does: for every edit-tool call, the gate allows it only if all gate invariants below hold; otherwise it denies and injects the current node and required action.
- Files written: target source files (the ported/adapted modules).
- Worker (AI): edits only the approved
port/adaptertarget paths. - User: nothing; the gate enforces scope.
- Completes when: every approved
port/adaptertarget_pathexists on disk.
closure_verification
- Initiated by:
advanceonce the approved targets exist. - Runtime does: runs
verify-active-closure.mjs— targetdepcruisetrace intoafter-edit.target.json,tsc --noEmitintargetCwd, then strict reconcile (target equals ports plus adapters, nocutpaths present, every extra declared intarget_extras, no dependency-cruisersummary.error). Gates on the exit code. - Files written:
after-edit.target.json; averifyrow inevents.jsonl. - Worker (AI): runs the verifier; on failure debugs within the approved scope.
- User: nothing, unless the fix needs a new path, dependency, or changed verdict — then the graph returns to
adjudicationand re-approval. - Completes when:
verify-active-closure.mjsexits 0.
final_approval
- Initiated by: after closure verification passes.
- Runtime does: records the user's final approval as an event. P0 verifies code closure, not browser parity.
- Files written: a
final-approvalrow inevents.jsonl. - Worker (AI): presents the reconcile result, the source manifest, and the target trace.
- User: gives the final approval. This is the second gate.
- Completes when: the
final-approvalevent is recorded.
disarm
- Initiated by:
disarmafter final approval. - Runtime does: clears
active.json(or setsstatus: "disarmed") atomically and appends adisarmevent; the gate returns to dormant. - Files written: removed or rewritten
active.json; adisarmrow inevents.jsonl. - Worker (AI): runs the
disarmsubcommand with a reason. - User: nothing.
- Completes when: no armed
active.jsonremains, so the gate allows edits again.
Gate Invariants
The PreToolUse decision for Edit, Write, MultiEdit, NotebookEdit, and apply_patch:
- No
active.json→ allow (no migration armed). active.jsoninvalid, or pastexpiresAt→ deny guarded edits with a repair instruction.- Edit path outside
targetRoot, or not underguardedPrefixes/guardedFiles→ allow. - Guarded edit while
approval.jsonis missing or itsapprovedManifestHashdoes not equal the current manifest hash → deny, inject the current node. - Guarded edit to a path that is not an approved
port/adaptertarget_path→ deny. apply_patch→ extract every header path and apply rules 3–5 to each.
The gate always exits 0; failPolicy: open covers script crashes, but a known-invalid armed state is an intentional deny.
Config Entry
Added only after depcruiser-migrations.mjs exists. No config.schema.json regeneration is required because hook settings is a generic object and no hook-specific schema is introduced.
{
"id": "depcruiser-migrations",
"name": "Depcruiser Migrations",
"description": "PreToolUse gate and CLI for governed dependency-cruiser migrations.",
"category": "gate",
"event": "PreToolUse",
"match": { "tools": ["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch"] },
"script": { "path": "depcruiser-migrations/depcruiser-migrations.mjs", "runtime": "node" },
"scope": { "projects": ["*"], "paths": ["**"] },
"enabled": false,
"failPolicy": "open",
"settings": {
"stateDir": "E:/hooks/.state/depcruiser-migrations",
"editTools": ["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch"]
}
}Implementation Sequence
depcruiser-migrations.mjs+lib/state-store.mjs+lib/path-scope.mjs: unarmed allow, invalid-state deny, guarded-path matching,apply_patchpath extraction.graph.contract.jsonwith the eight nodes and predicates.- Strengthen
validate-closure-manifest.mjsto repo-relative, blocking reconcile withtarget_extras. verify-active-closure.mjs.- One focused test file (state, matching,
apply_patch, approval-hash gate, reconcile). - Add the disabled config hook entry; run
_core/validate-runtime-hooks.mjs(no schema regen). - Retire the
closure-gate*files only after the new runtime passes.