shapeup-sdlc 3.18.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.claude/settings.local.example.json +2 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +115 -56
  4. package/README.md +101 -143
  5. package/SECURITY.md +16 -25
  6. package/agents/orient.md +14 -0
  7. package/agents/qa-edge-hunter.md +13 -0
  8. package/agents/spec-evaluator.md +14 -0
  9. package/agents/task-executor.md +14 -0
  10. package/bin/init.mjs +13 -16
  11. package/bin/lib/grant.mjs +8 -19
  12. package/commands/build.md +9 -14
  13. package/commands/eval.md +3 -3
  14. package/commands/hammer.md +9 -5
  15. package/commands/retro.md +12 -11
  16. package/commands/scopes.md +1 -12
  17. package/commands/ship.md +45 -88
  18. package/commands/wire.md +1 -8
  19. package/hooks/dispatch-receipt.mjs +94 -8
  20. package/hooks/hooks.json +0 -41
  21. package/hooks/lib/decision.mjs +6 -9
  22. package/hooks/lib/tier.mjs +50 -0
  23. package/hooks/sandbox-guard.mjs +140 -64
  24. package/kernel/gate.mjs +203 -22
  25. package/kernel/harness.mjs +20 -67
  26. package/kernel/init/run.mjs +175 -523
  27. package/kernel/lib/contract.mjs +9 -93
  28. package/kernel/lib/floor.mjs +64 -0
  29. package/kernel/lib/paths.mjs +39 -112
  30. package/kernel/order.mjs +157 -0
  31. package/kernel/probe/digest.mjs +2 -38
  32. package/kernel/schemas/domain.schema.json +146 -1125
  33. package/kernel/schemas/work-order.schema.json +8 -4
  34. package/kernel/schemas/work-result.schema.json +1 -1
  35. package/kernel/verify/envelope.mjs +2 -2
  36. package/kernel/verify/skills.mjs +27 -8
  37. package/kernel/verify/t0.mjs +2 -2
  38. package/package.json +4 -2
  39. package/skills/building/SKILL.md +144 -0
  40. package/skills/building/resources/gates.md +76 -0
  41. package/skills/building/resources/guidance.md +23 -0
  42. package/skills/{translator/references/preservation-rules.md → building/resources/intake-preservation.md} +1 -1
  43. package/skills/building/resources/intake.md +157 -0
  44. package/skills/building/resources/orders.md +69 -0
  45. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/doc-schemas.md +1 -1
  46. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/task-generation.md +13 -13
  47. package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/test-surface.md +6 -7
  48. package/skills/building/resources/plan-scopes.md +95 -0
  49. package/skills/building/resources/plan-spec.md +102 -0
  50. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/_index.tmpl.md +1 -1
  51. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/assess-report.tmpl.md +10 -11
  52. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/run-state.tmpl.md +4 -4
  53. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/scope-summary.tmpl.md +2 -2
  54. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/synthesis.tmpl.md +4 -4
  55. package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/usecase.tmpl.md +2 -2
  56. package/skills/building/resources/plan-wire.md +82 -0
  57. package/skills/building/resources/plan.md +98 -0
  58. package/skills/building/resources/retro-research.md +60 -0
  59. package/skills/building/resources/retro-scan.md +49 -0
  60. package/skills/building/resources/retro.md +225 -0
  61. package/skills/building/resources/rounds.md +78 -0
  62. package/skills/building/resources/stop.md +127 -0
  63. package/skills/orient/SKILL.md +37 -37
  64. package/skills/qa-edge-hunter/SKILL.md +14 -14
  65. package/skills/shapeup/SKILL.md +5 -5
  66. package/skills/shapeup/resources/breadboarding.md +1 -1
  67. package/skills/shapeup/resources/context-compaction.md +5 -5
  68. package/skills/shapeup/resources/shaping.md +1 -1
  69. package/skills/spec-evaluator/SKILL.md +17 -21
  70. package/skills/spec-evaluator/references/dimension-contract.md +1 -1
  71. package/skills/spec-evaluator/references/dimensions/completeness.md +3 -3
  72. package/skills/spec-evaluator/references/dimensions/spec-conformance.md +1 -1
  73. package/skills/spec-evaluator/references/dimensions/test-surface-conformance.md +5 -5
  74. package/skills/task-executor/SKILL.md +11 -15
  75. package/commands/hill.md +0 -10
  76. package/hooks/gate-intake.mjs +0 -109
  77. package/hooks/gate-zerowork.mjs +0 -349
  78. package/hooks/tier-guard.mjs +0 -162
  79. package/kernel/compile.mjs +0 -1419
  80. package/kernel/init/fit.mjs +0 -199
  81. package/kernel/init/run-args.mjs +0 -206
  82. package/kernel/lib/breadboard.mjs +0 -165
  83. package/kernel/probe/attempts.mjs +0 -161
  84. package/kernel/probe/concurrency.mjs +0 -535
  85. package/kernel/probe/eval.mjs +0 -405
  86. package/kernel/probe/hunt.mjs +0 -104
  87. package/kernel/probe/leg.mjs +0 -229
  88. package/kernel/probe/owner.mjs +0 -142
  89. package/kernel/probe/requirements.mjs +0 -328
  90. package/kernel/probe/resume.mjs +0 -1110
  91. package/kernel/probe/rounds.mjs +0 -115
  92. package/kernel/probe/staged.mjs +0 -83
  93. package/kernel/probe/stats.mjs +0 -477
  94. package/kernel/probe/t0.mjs +0 -115
  95. package/kernel/reduce/board.mjs +0 -350
  96. package/kernel/reduce/graph.mjs +0 -482
  97. package/kernel/reduce/hill.mjs +0 -322
  98. package/kernel/reduce/ingest.mjs +0 -767
  99. package/kernel/reduce/leftovers.mjs +0 -159
  100. package/kernel/reduce/ship.mjs +0 -529
  101. package/kernel/reduce/snapshot.mjs +0 -291
  102. package/kernel/reduce/verdict.mjs +0 -172
  103. package/kernel/report/export.mjs +0 -384
  104. package/kernel/report/facts.mjs +0 -234
  105. package/kernel/verify/budget.mjs +0 -153
  106. package/kernel/verify/build.mjs +0 -428
  107. package/kernel/verify/dispatch.mjs +0 -114
  108. package/kernel/verify/spec.mjs +0 -975
  109. package/kernel/verify/trace.mjs +0 -604
  110. package/skills/ba-pitch-analyzer/SKILL.md +0 -204
  111. package/skills/coach/SKILL.md +0 -397
  112. package/skills/hill-chart/SKILL.md +0 -154
  113. package/skills/hill-chart/assets/dashboard.template.html +0 -1432
  114. package/skills/scope-architect/SKILL.md +0 -174
  115. package/skills/scope-hammer/SKILL.md +0 -231
  116. package/skills/solution-architect/SKILL.md +0 -158
  117. package/skills/tech-lead/SKILL.md +0 -151
  118. package/skills/tech-lead/references/gates.md +0 -605
  119. package/skills/tech-lead/references/protocol.md +0 -889
  120. package/skills/tech-lead/references/tiny-lane.md +0 -53
  121. package/skills/tech-lead/workflows/shapeup-run.js +0 -2187
  122. package/skills/translator/SKILL.md +0 -258
  123. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/contract-patterns.md +0 -0
  124. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ddd-patterns.md +0 -0
  125. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/integration-analysis.md +0 -0
  126. /package/skills/{ba-pitch-analyzer/references → building/resources/plan-references}/ux-behavior-patterns.md +0 -0
  127. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/api-feasibility.tmpl.md +0 -0
  128. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/be-service.contract.tmpl.md +0 -0
  129. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/offline-storage.contract.tmpl.md +0 -0
  130. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/contracts/third-party-api.contract.tmpl.md +0 -0
  131. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/context-map.tmpl.md +0 -0
  132. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/event-choreography.tmpl.md +0 -0
  133. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/migration-plan.tmpl.md +0 -0
  134. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/cross-context/team-handoff.tmpl.md +0 -0
  135. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/domain-model.tmpl.md +0 -0
  136. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/feedback.tmpl.md +0 -0
  137. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/integration.tmpl.md +0 -0
  138. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-board.tmpl.md +0 -0
  139. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task-spike.tmpl.md +0 -0
  140. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/task.tmpl.md +0 -0
  141. /package/skills/{ba-pitch-analyzer/assets/templates → building/resources/plan-templates}/ux-behavior.tmpl.md +0 -0
@@ -1,229 +0,0 @@
1
- // probe leg — "did this scope's work actually reach the board?"
2
- //
3
- // CONTRACT. A bounded, read-only query over one scope's orders, results and leg-completion rows for
4
- // one round. Prints `{closed, scope_id, round, orders: [...], unapplied: [...]}` on stdout; exits 0
5
- // when every order that produced a result has been ingested, 1 when at least one has not, 2 on a bad
6
- // argv. Writes nothing.
7
- //
8
- // WHY IT EXISTS. `probe t0` answers "is the T0 verdict green", and the BUILD round's confirm stage
9
- // asked only that. A green T0 says the worker's fixtures ran and passed; it says nothing about
10
- // whether the WorkResult was applied — and this is a state a real run reaches. A build leg can write
11
- // its code, a green T0 verdict, a kept trial row and its WorkResult to disk, and never run step 3 of
12
- // its own script, `reduce ingest`. It reports green; a confirm stage that re-verifies only the T0
13
- // artifact agrees, and the round walks on. That scope's task is left `pending` with zero acceptance
14
- // criteria ticked while its code sits finished on disk, beside a sibling scope whose task is `done`.
15
- //
16
- // Nothing has to be wrong with the order, the receipt or the result for this to happen — ingesting the
17
- // same file afterwards succeeds and ticks the criteria. The single writer of shared state simply never
18
- // ran, and the board GATE L2 reads as "100% ✅" silently disagrees with a scope that is genuinely
19
- // finished. Under fan-out it gets worse rather than better: N legs can each drop the step
20
- // independently, and the only symptom is a board that lags reality.
21
- //
22
- // WHY A LEG ROW IS THE RIGHT EVIDENCE. The row is appended BY `reduce ingest`. Its presence is
23
- // therefore proof the writer ran, and it is not something the leg can assert about itself — the same
24
- // reason the dispatch receipt is written by the hook layer rather than by the sub-agent making the
25
- // call. "The worker says it ingested" is exactly the class of claim this pipeline does not accept.
26
-
27
- import { existsSync, readdirSync, readFileSync } from "node:fs";
28
- import { join, resolve } from "node:path";
29
- import { runArgs } from "../lib/argv.mjs";
30
- import { ordersDir, resultsDir, legLedger, dispatchReceipts, readRunId } from "../lib/paths.mjs";
31
-
32
- /** `<scope>-r<round>-a<attempt>.json` — the only address a build order is written under. */
33
- const BUILD_ORDER = /^(.+)-r(\d+)-a(\d+)\.json$/;
34
-
35
- /**
36
- * Every leg-completion row on disk, tolerant of a torn last line.
37
- * @param {string} path - The leg ledger.
38
- * @returns {object[]} Parsed rows; an unparsable line is skipped rather than fatal.
39
- */
40
- export function readLegs(path) {
41
- if (!existsSync(path)) return [];
42
- return readFileSync(path, "utf8").split("\n").filter(Boolean)
43
- .map((l) => { try { return JSON.parse(l); } catch { return null; } })
44
- .filter(Boolean);
45
- }
46
-
47
- /**
48
- * Which of one scope's orders in one round produced a result, and which of those were applied.
49
- *
50
- * An order with no result has not finished and is not a finding — the leg may still be running, or
51
- * may have died, which the round already treats as a spent attempt. What this reports is the
52
- * narrower and stranger case: a result on disk that the single writer never applied.
53
- *
54
- * @param {string} cwd - Project root.
55
- * @param {string} slug - Feature slug.
56
- * @param {string} scopeId - Scope contract id.
57
- * @param {number} round - Build round.
58
- * @returns {{closed: boolean, orders: object[], unapplied: object[]}} `closed` is false only when a
59
- * result exists that was never ingested; a scope with no results at all reports `closed: false`
60
- * with an empty `orders` list, so a caller can tell "nothing ran" from "nothing was applied".
61
- */
62
- /**
63
- * Every order the run compiled, with whether its result landed and whether the single writer
64
- * applied it — planning orders (`analyze.json`) and build orders (`alpha-r1-a1.json`) alike.
65
- *
66
- * Why the generic form exists: the per-scope reading below was the only reader of the leg ledger
67
- * in the run loop, and it was asked in one place, behind the checks that decide a scope is green.
68
- * Measured on a live run: five dispatches, five results, three leg rows — and the planning phase
69
- * whose result nobody applied walked on, because its post-condition checked the artifact the
70
- * worker wrote directly and never asked whether the writer ran. This is the question, asked of any
71
- * order by name.
72
- *
73
- * @param {string} cwd - Project root.
74
- * @param {string} slug - Feature slug.
75
- * @returns {{order:string, name:string, order_id:(string|null), has_result:boolean, applied:boolean}[]}
76
- */
77
- export function legsOf(cwd, slug) {
78
- const oDir = ordersDir(cwd, slug);
79
- const rDir = resultsDir(cwd, slug);
80
- const ingested = new Set(readLegs(legLedger(cwd, slug))
81
- .filter((r) => r.ingested_at)
82
- .map((r) => String(r.order_id)));
83
- // Dispatch receipts are the hook layer's record that a Skill was invoked against an order —
84
- // scoped to this run, since receipts over one slug accumulate across runs and order ids repeat.
85
- const runId = readRunId(cwd, slug);
86
- const receipted = new Set(readLegs(dispatchReceipts(cwd, slug))
87
- .filter((r) => r?.order_id && (!runId || !r.run_id || r.run_id === runId))
88
- .map((r) => String(r.order_id)));
89
- const out = [];
90
- for (const f of (existsSync(oDir) ? readdirSync(oDir) : []).filter((x) => x.endsWith(".json")).sort()) {
91
- let orderId = null;
92
- try { orderId = JSON.parse(readFileSync(join(oDir, f), "utf8")).order_id ?? null; } catch { /* unreadable — reported as unapplied */ }
93
- out.push({
94
- order: join(oDir, f),
95
- name: f.replace(/\.json$/, ""),
96
- order_id: orderId,
97
- has_receipt: orderId !== null && receipted.has(orderId),
98
- has_result: existsSync(join(rDir, f)),
99
- applied: orderId !== null && ingested.has(orderId),
100
- });
101
- }
102
- return out;
103
- }
104
-
105
- /**
106
- * One order by its file stem — `analyze`, `wire`, `alpha-r1-a1` — and whether its leg closed.
107
- * @param {string} cwd - Project root.
108
- * @param {string} slug - Feature slug.
109
- * @param {string} name - The order file's stem.
110
- * @returns {{closed:boolean, found:boolean, order:(string|null), order_id:(string|null), has_result:boolean, applied:boolean}}
111
- */
112
- export function orderLegState(cwd, slug, name) {
113
- const o = legsOf(cwd, slug).find((x) => x.name === name);
114
- if (!o) return { closed: false, found: false, order: null, order_id: null, has_receipt: false, has_result: false, applied: false };
115
- return { closed: o.applied, found: true, ...o };
116
- }
117
-
118
- /**
119
- * Orders that were dispatched — a receipt says a Skill ran against them — and never answered:
120
- * no WorkResult on disk. Measured on a live run: the first dispatch wrote its four artifacts and
121
- * no envelope, every later phase read the artifacts, and the run closed `shipped` with the order
122
- * still open by construction — the export's own dispatch row said `answered: false` and nothing
123
- * upstream of it had noticed. Distinct from {@link openLegs}: that is work nobody applied; this is
124
- * a leg that never came back.
125
- * @param {string} cwd - Project root.
126
- * @param {string} slug - Feature slug.
127
- * @returns {{order:string, name:string, order_id:(string|null)}[]}
128
- */
129
- export function unansweredOrders(cwd, slug) {
130
- return legsOf(cwd, slug).filter((o) => o.has_receipt && !o.has_result);
131
- }
132
-
133
- /**
134
- * The results on disk that no leg row applied — finished work the board never saw.
135
- * @param {string} cwd - Project root.
136
- * @param {string} slug - Feature slug.
137
- * @returns {{order:string, name:string, order_id:(string|null)}[]}
138
- */
139
- export function openLegs(cwd, slug) {
140
- return legsOf(cwd, slug).filter((o) => o.has_result && !o.applied);
141
- }
142
-
143
- export function legState(cwd, slug, scopeId, round) {
144
- const oDir = ordersDir(cwd, slug);
145
- const rDir = resultsDir(cwd, slug);
146
- const legs = readLegs(legLedger(cwd, slug));
147
- const ingested = new Set(legs
148
- .filter((r) => Number(r.round) === round && r.scope_id === scopeId && r.ingested_at)
149
- .map((r) => String(r.order_id)));
150
-
151
- const orders = [];
152
- for (const f of (existsSync(oDir) ? readdirSync(oDir) : []).sort()) {
153
- const m = f.match(BUILD_ORDER);
154
- if (!m || m[1] !== scopeId || Number(m[2]) !== round) continue;
155
- // The order id is read off the order rather than rebuilt from the filename: the filename is the
156
- // path the round knows, and the id is the key every record joins on, and they are allowed to be
157
- // written by different steps.
158
- let orderId = null;
159
- try { orderId = JSON.parse(readFileSync(join(oDir, f), "utf8")).order_id ?? null; } catch { /* unreadable — reported below */ }
160
- orders.push({
161
- order: join(oDir, f),
162
- order_id: orderId,
163
- attempt: Number(m[3]),
164
- has_result: existsSync(join(rDir, f)),
165
- applied: orderId !== null && ingested.has(orderId),
166
- });
167
- }
168
- const unapplied = orders.filter((o) => o.has_result && !o.applied);
169
- return { closed: orders.some((o) => o.applied) && unapplied.length === 0, orders, unapplied };
170
- }
171
-
172
- export const ARGV_SPEC = {
173
- usage: "harness.mjs probe leg --slug <slug> (--scope <scope-id> --round N | --order <stem> | --open) [--cwd <dir>]",
174
- _: { arity: 0, max: 0, name: "(no positional operands)" },
175
- slug: { type: "str", required: true },
176
- scope: { type: "str" },
177
- round: { type: "int", min: 1 },
178
- order: { type: "str" },
179
- open: { type: "flag" },
180
- cwd: { type: "path" },
181
- };
182
-
183
- /**
184
- * Report whether one scope's results for one round reached the board.
185
- *
186
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
187
- * @returns {void} Exits 0 when closed, 1 when a result was never applied — the shape a caller can
188
- * branch on without parsing prose.
189
- */
190
- export function cli(rawArgv) {
191
- const args = runArgs(ARGV_SPEC, rawArgv);
192
- const cwd = resolve(args.cwd || process.cwd());
193
- // `--open`: every result nothing applied, across the whole run, any phase. Exit 0 when none.
194
- if (args.open) {
195
- const open = openLegs(cwd, args.slug);
196
- const unanswered = unansweredOrders(cwd, args.slug);
197
- console.log(JSON.stringify({
198
- closed: open.length === 0 && unanswered.length === 0,
199
- open_total: open.length, open: open.map((o) => o.order), open_ids: open.map((o) => o.order_id),
200
- unanswered_total: unanswered.length, unanswered: unanswered.map((o) => o.order), unanswered_ids: unanswered.map((o) => o.order_id),
201
- }));
202
- process.exit(open.length === 0 && unanswered.length === 0 ? 0 : 1);
203
- }
204
- // `--order <stem>`: one order by file stem (`analyze`, `alpha-r1-a1`). Exit 0 when its leg closed.
205
- if (args.order) {
206
- const s = orderLegState(cwd, args.slug, args.order);
207
- console.log(JSON.stringify(s));
208
- process.exit(s.closed ? 0 : 1);
209
- }
210
- if (!args.scope || !args.round) {
211
- console.error("probe leg: pass --scope <id> --round N, or --order <stem>, or --open");
212
- process.exit(2);
213
- }
214
- const s = legState(cwd, args.slug, args.scope, args.round);
215
- console.log(JSON.stringify({
216
- closed: s.closed,
217
- scope_id: args.scope,
218
- round: args.round,
219
- // The counts travel with the verdict: `closed: false` over zero orders means the leg has not run,
220
- // and over three orders means its work is on disk and unapplied. Those need different answers
221
- // from the caller, so they must not read the same.
222
- orders_total: s.orders.length,
223
- results_total: s.orders.filter((o) => o.has_result).length,
224
- applied_total: s.orders.filter((o) => o.applied).length,
225
- orders: s.orders,
226
- unapplied: s.unapplied.map((o) => o.order),
227
- }));
228
- process.exit(s.closed ? 0 : 1);
229
- }
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env node
2
- // probe owner — which scope owns a path, derived from the contracts and nothing else.
3
- //
4
- // WHY THIS IS A QUERY AND NOT A SENTENCE. GATE H's census stated ownership facts without reading
5
- // the contracts: a ship report said no scope owned the app's page files, while the composition
6
- // root's committed substrate listed `pages/**` in plain sight. The real gap was elsewhere (a route
7
- // table naming files nobody had written), and the report pointed the PO at a scope cut that was
8
- // not the problem. A census that narrates ownership from memory is a census that can be wrong in
9
- // exactly the way that looks most authoritative.
10
- //
11
- // Ownership already has ONE mechanical definition in this plugin — the substrate the sandbox hook
12
- // enforces, and the election `harness compile` runs to address a cited bug to the scope that may
13
- // write the file (`electOwner`). This probe exposes that same answer on the command line, so a
14
- // worker that needs to say "no scope owns X" can cite the query instead of asserting it.
15
- //
16
- // With no `--path`, the input set is what a census would ask about anyway: every engine and
17
- // entry-point call site the wiring map names, plus the profile's entry point. A row with no
18
- // writers is an UNOWNED SEAM — the one kind of gap the disjointness lint cannot see, since DISJOINT
19
- // fails two owners and nothing fails zero.
20
- //
21
- // Usage:
22
- // node "${CLAUDE_PLUGIN_ROOT}/kernel/harness.mjs" probe owner --slug <slug> [--path <p>]... [--format json|table] [--cwd <dir>]
23
- //
24
- // Exit code: 0 = answered, 2 = bad argv or no readable scope contracts.
25
-
26
- import { existsSync } from "node:fs";
27
- import { resolve, join } from "node:path";
28
- import { runArgs, isMain } from "../lib/argv.mjs";
29
- import { wiringMap, projectProfile } from "../lib/paths.mjs";
30
- import { readContract, WIRING_MAP, PROJECT_PROFILE } from "../lib/contract.mjs";
31
- import { scopeSubstrates, electOwner, bugLocations } from "../compile.mjs";
32
- import { matchesAny } from "../../hooks/sandbox-guard.mjs";
33
-
34
- /**
35
- * Ownership of one repo-relative path under a set of scope substrates.
36
- *
37
- * @param {string} path - Repo-relative POSIX path.
38
- * @param {Array<{scope_id:string, allowed:string[], shared:string[]}>} scopes - From
39
- * {@link scopeSubstrates}.
40
- * @param {string} [cwd] - Project root; when given, `exists` says whether the path is on disk.
41
- * @returns {{path:string, owner:(string|null), writers:string[], shared_with:string[], exists:(boolean|null)}}
42
- * `owner` is the elected fixer (exclusive first, lowest id among equals); `writers` every scope
43
- * whose substrate admits the path; `shared_with` the writers that declare it shared. Empty
44
- * `writers` means no scope may write the file — an unowned path. `exists` is the other half a
45
- * census needs: a seam that is owned but not on disk is a route to a file nobody wrote.
46
- */
47
- export function ownership(path, scopes, cwd = null) {
48
- const rel = String(path).replace(/^\.\//, "");
49
- const shared = (scopes || []).filter((s) => matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
50
- // `writers` is admits-the-path, the same union the sandbox fence composes (allowed ++ shared) —
51
- // a path declared only in a contract's `shared` list is still a scope this path may write, and
52
- // must read as owned rather than UNOWNED. `shared_with` (below) stays the narrower subset.
53
- const writers = (scopes || []).filter((s) => matchesAny(rel, s.allowed) || matchesAny(rel, s.shared || [])).map((s) => s.scope_id).sort();
54
- return { path: rel, owner: electOwner(rel, scopes), writers, shared_with: shared, exists: cwd ? existsSync(join(cwd, rel)) : null };
55
- }
56
-
57
- /**
58
- * The paths a census asks about when none are given: the wiring map's engines and entry call
59
- * sites, and the profile's entry point. Only tokens that look like files are kept — an entry call
60
- * site is prose around a path (`src/server.ts — POST /checkout route`), and the path is what the
61
- * substrate can answer for.
62
- *
63
- * @param {string} cwd - Project root.
64
- * @param {string} slug - Feature slug.
65
- * @returns {Array<{path:string, cited_by:string}>} Distinct paths with where each was named.
66
- */
67
- export function seamPaths(cwd, slug) {
68
- const out = [];
69
- const seen = new Set();
70
- const add = (p, by) => { if (p && !seen.has(p)) { seen.add(p); out.push({ path: p, cited_by: by }); } };
71
- const wm = wiringMap(cwd, slug);
72
- if (existsSync(wm)) {
73
- const entries = readContract(wm, WIRING_MAP)?.contract?.entries || [];
74
- for (const e of entries) {
75
- for (const p of bugLocations({ location: e.engine })) add(p, `${e.use_case || "?"} engine`);
76
- for (const p of bugLocations({ location: e.entry_call_site })) add(p, `${e.use_case || "?"} entry_call_site`);
77
- }
78
- }
79
- const pp = projectProfile(cwd, slug);
80
- if (existsSync(pp)) {
81
- const ep = readContract(pp, PROJECT_PROFILE)?.contract?.entry_point;
82
- for (const p of bugLocations({ location: ep })) add(p, "project-profile entry_point");
83
- }
84
- return out;
85
- }
86
-
87
- /**
88
- * Render the report as a fixed-width table for a human reading a gate block.
89
- * @param {object} r - The report {@link cli} prints.
90
- * @returns {string} One header line and one row per path.
91
- */
92
- export function renderTable(r) {
93
- const rows = r.paths.map((p) => [p.path, p.owner || "—", p.writers.join(",") || "UNOWNED", p.exists === null ? "?" : p.exists ? "yes" : "MISSING", p.cited_by || ""]);
94
- const head = ["path", "owner", "writers", "on disk", "cited by"];
95
- const w = head.map((h, i) => Math.max(h.length, ...rows.map((row) => String(row[i]).length)));
96
- const line = (row) => row.map((c, i) => String(c).padEnd(w[i])).join(" ").trimEnd();
97
- return [line(head), line(w.map((n) => "-".repeat(n))), ...rows.map(line),
98
- "", `${r.scopes} scope(s) read · ${r.unowned.length} unowned path(s)${r.unowned.length ? `: ${r.unowned.join(", ")}` : ""}` +
99
- ` · ${r.missing.length} not on disk${r.missing.length ? `: ${r.missing.join(", ")}` : ""}`].join("\n");
100
- }
101
-
102
- export const ARGV_SPEC = {
103
- usage: "harness.mjs probe owner --slug <slug> [--path <p>]... [--format json|table] [--cwd <dir>]",
104
- _: { arity: 0, max: 0, name: "(no positional operands)" },
105
- slug: { type: "str", required: true },
106
- path: { type: "str", multiple: true },
107
- format: { type: "enum", values: ["json", "table"], default: "json" },
108
- cwd: { type: "path" },
109
- };
110
-
111
- /**
112
- * Answer "which scope owns this path" from the committed contracts.
113
- *
114
- * @param {string[]} rawArgv - The subcommand's own arguments (harness.mjs strips the verb words).
115
- * @returns {Promise<void>} Exits 0 with the report, 2 when no scope contract can be read.
116
- */
117
- export async function cli(rawArgv) {
118
- const args = runArgs(ARGV_SPEC, rawArgv);
119
- const cwd = resolve(args.cwd || process.cwd());
120
- const scopes = scopeSubstrates(cwd, args.slug);
121
- if (!scopes.length) {
122
- console.error(`probe owner: no readable scope contract for ${args.slug} — ownership is a property of the contracts, and there are none to read`);
123
- process.exit(2);
124
- }
125
- const asked = (args.path || []).map((p) => ({ path: p, cited_by: "--path" }));
126
- const inputs = asked.length ? asked : seamPaths(cwd, args.slug);
127
- const paths = inputs.map((i) => ({ ...ownership(i.path, scopes, cwd), cited_by: i.cited_by }));
128
- const report = {
129
- slug: args.slug,
130
- scopes: scopes.length,
131
- source: asked.length ? "--path" : "wiring-map + project-profile",
132
- paths,
133
- unowned: paths.filter((p) => p.writers.length === 0).map((p) => p.path),
134
- // Owned but absent: the wiring names a file nobody wrote — the gap a census reads as
135
- // "unowned" when it does not check the disk.
136
- missing: paths.filter((p) => p.exists === false).map((p) => p.path),
137
- };
138
- console.log(args.format === "table" ? renderTable(report) : JSON.stringify(report, null, 2));
139
- process.exit(0);
140
- }
141
-
142
- if (isMain(import.meta.url)) cli(process.argv.slice(2));