jwc-global
  • 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.md and _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

#FileNew or existsWhat it does
1depcruiser-migrations/depcruiser-migrations.mjsNew (replaces closure-gate.mjs)PreToolUse gate plus CLI subcommands arm, advance, disarm, status. Resolves the active node and decides allow/deny on edit tools.
2depcruiser-migrations/graph.contract.jsonNewThe lifecycle as data: each node lists its requiredOutputs and a completion predicate; transitions lists allowed edges including the rework back-edges.
3depcruiser-migrations/lib/state-store.mjsNewAtomic read/write of active.json (temp file + rename), append to events.jsonl, and active-state schema validation.
4depcruiser-migrations/lib/path-scope.mjsNewRepo-relative path resolution, guarded prefix/file matching, and apply_patch header path extraction.
5depcruiser-migrations/verify-active-closure.mjsNewReads active.json; if armed runs target depcruise, tsc --noEmit in targetCwd, then strict reconcile; gates on its own exit code.
6depcruiser-migrations/generate-closure-manifest.mjsExistsBuilds source-manifest.json from the donor trace with every module verdict set to TBD.
7depcruiser-migrations/validate-closure-manifest.mjsExists, strengthenVerdicts mode plus strict reconcile on normalized repo-relative paths with a target_extras allowlist; blocks rather than warns.
8depcruiser-migrations/dependency-cruiser.cjsExistsDependency-cruiser rule config (tsPreCompilationDeps, no-circular, no-unresolvable).
9depcruiser-migrations/initialize-depcruiser-migrations/SKILL.mdExists, updateHuman workflow; calls arm, then drives the worker through the nodes via advance.
10depcruiser-migrations/tests/depcruiser-migrations.test.mjsNewOne focused test file: state validation, guarded matching, apply_patch extraction, the approval-hash gate, and strict reconcile cases.
11depcruiser-migrations/README.mdExists, updateBundle 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)

#PathWhat it holds
1E:/hooks/.state/depcruiser-migrations/active.jsonThe single armed migration: ids, donor/target roots and entries, guardedPrefixes, guardedFiles, currentNode, approvedManifestHash, evidenceDir, status.
2E:/hooks/.state/depcruiser-migrations/events.jsonlAppend-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-migrations skill naming a donor entry and a target entry; the skill calls depcruiser-migrations arm.
  • Runtime does: verifies donor and target repos exist; detects each package manager by lockfile; checks dependency-cruiser in both and installs it only when missing; creates the target _migrations/<source>/<name>/ directory; writes active.json with status: "armed", currentNode: "discovery".
  • Files written: active.json; an arm row in events.jsonl.
  • Worker (AI): runs the checks through arm; reports what it found.
  • User: nothing after the initial invocation.
  • Completes when: active.json validates and dependency-cruiser is available in both repos.

manifest_generation

  • Initiated by: advance after discovery completes.
  • Runtime does: runs the donor depcruise JSON trace from the donor entry into before-edit.donor.json; runs generate-closure-manifest.mjs to produce source-manifest.json with every module verdict: "TBD", plus port_order, imported_by, and external_deps.
  • Files written: before-edit.donor.json, source-manifest.json.
  • Worker (AI): runs the trace and the generator.
  • User: nothing.
  • Completes when: source-manifest.json exists, parses, and lists every donor module with a verdict field.

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, or target_extras decision for every module, with a target_path for each port/adapter, and records them in adjudication.json.
  • User: discusses and decides the verdicts; this is a conversation, not a file edit.
  • Completes when: adjudication.json resolves every donor module (no TBD), every port/adapter has a target_path, every target_extras entry has a reason, and readyForApproval is true.

approval

  • Initiated by: the worker once adjudication is complete.
  • Runtime does: computes approvedManifestHash = sha256(source-manifest.json + adjudication.json); on the user's grant, writes approval.json carrying that hash and copies it into active.json.
  • Files written: approval.json; updated active.json; an approve row in events.jsonl.
  • Worker (AI): presents the finalized adjudication summary and requests approval; writes approval.json only after the user approves.
  • User: approves the adjudicated interpretation. This is the first gate.
  • Completes when: approval.json exists and its approvedManifestHash equals 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/adapter target paths.
  • User: nothing; the gate enforces scope.
  • Completes when: every approved port/adapter target_path exists on disk.

closure_verification

  • Initiated by: advance once the approved targets exist.
  • Runtime does: runs verify-active-closure.mjs — target depcruise trace into after-edit.target.json, tsc --noEmit in targetCwd, then strict reconcile (target equals ports plus adapters, no cut paths present, every extra declared in target_extras, no dependency-cruiser summary.error). Gates on the exit code.
  • Files written: after-edit.target.json; a verify row in events.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 adjudication and re-approval.
  • Completes when: verify-active-closure.mjs exits 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-approval row in events.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-approval event is recorded.

disarm

  • Initiated by: disarm after final approval.
  • Runtime does: clears active.json (or sets status: "disarmed") atomically and appends a disarm event; the gate returns to dormant.
  • Files written: removed or rewritten active.json; a disarm row in events.jsonl.
  • Worker (AI): runs the disarm subcommand with a reason.
  • User: nothing.
  • Completes when: no armed active.json remains, so the gate allows edits again.

Gate Invariants

The PreToolUse decision for Edit, Write, MultiEdit, NotebookEdit, and apply_patch:

  1. No active.json → allow (no migration armed).
  2. active.json invalid, or past expiresAt → deny guarded edits with a repair instruction.
  3. Edit path outside targetRoot, or not under guardedPrefixes/guardedFiles → allow.
  4. Guarded edit while approval.json is missing or its approvedManifestHash does not equal the current manifest hash → deny, inject the current node.
  5. Guarded edit to a path that is not an approved port/adapter target_path → deny.
  6. 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

  1. depcruiser-migrations.mjs + lib/state-store.mjs + lib/path-scope.mjs: unarmed allow, invalid-state deny, guarded-path matching, apply_patch path extraction.
  2. graph.contract.json with the eight nodes and predicates.
  3. Strengthen validate-closure-manifest.mjs to repo-relative, blocking reconcile with target_extras.
  4. verify-active-closure.mjs.
  5. One focused test file (state, matching, apply_patch, approval-hash gate, reconcile).
  6. Add the disabled config hook entry; run _core/validate-runtime-hooks.mjs (no schema regen).
  7. Retire the closure-gate* files only after the new runtime passes.