@holmes-lab/holmes-kit 0.19.6 → 0.20.1

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 (139) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +9 -2
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/doctor-check.d.ts +15 -0
  13. package/dist/holmes/cli/doctor-check.js +2 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -8
  15. package/dist/holmes/cli/doctor.js +12 -3
  16. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  17. package/dist/holmes/cli/gitignore-merge.js +6 -2
  18. package/dist/holmes/cli/index.js +176 -139
  19. package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
  20. package/dist/holmes/cli/npx-cache-check.js +130 -0
  21. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  22. package/dist/holmes/cli/playbook-skills.js +10 -54
  23. package/dist/holmes/cli/probe-process.d.ts +17 -2
  24. package/dist/holmes/cli/probe-process.js +44 -11
  25. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  26. package/dist/holmes/governance/approval-grants.js +55 -1
  27. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  28. package/dist/holmes/governance/approval-queue.js +91 -13
  29. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  30. package/dist/holmes/governance/display-id-aliases.js +86 -0
  31. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  32. package/dist/holmes/governance/ledger-store.js +49 -3
  33. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  34. package/dist/holmes/governance/ledger-timeline.js +11 -1
  35. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  36. package/dist/holmes/governance/provenance-chain.js +13 -2
  37. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  38. package/dist/holmes/governance/provenance-ledger.js +69 -16
  39. package/dist/holmes/governance/session-context.d.ts +3 -0
  40. package/dist/holmes/governance/session-context.js +26 -8
  41. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  42. package/dist/holmes/guardrail/write-target.js +2 -1
  43. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  44. package/dist/holmes/hooks/stop.js +14 -3
  45. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  46. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  47. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  48. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  49. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  50. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  51. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  52. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  53. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  54. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  55. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  56. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  57. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  58. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  59. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  60. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  61. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  62. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  63. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  64. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  65. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  66. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  67. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  68. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  69. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
  70. package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
  71. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  72. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  73. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  74. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  75. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  76. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  77. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  78. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  79. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  80. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  81. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  82. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  83. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  84. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  85. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  86. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  87. package/dist/holmes/mcp/handlers.d.ts +661 -424
  88. package/dist/holmes/mcp/handlers.js +270 -3251
  89. package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
  90. package/dist/holmes/mcp/server-specs-dir.js +71 -0
  91. package/dist/holmes/mcp/server.js +12 -2
  92. package/dist/holmes/mcp/tool-schemas.js +43 -2
  93. package/dist/holmes/project/execution-context.d.ts +17 -0
  94. package/dist/holmes/project/execution-context.js +121 -0
  95. package/dist/holmes/project/installer-markers.d.ts +34 -0
  96. package/dist/holmes/project/installer-markers.js +65 -0
  97. package/dist/holmes/project/root.d.ts +12 -1
  98. package/dist/holmes/project/root.js +22 -3
  99. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  100. package/dist/holmes/project/workspace-identity.js +181 -0
  101. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  102. package/dist/holmes/review/test-outcomes.js +23 -5
  103. package/dist/holmes/review/test-runner.d.ts +18 -0
  104. package/dist/holmes/review/test-runner.js +136 -5
  105. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  106. package/dist/holmes/rtm/graph-store.js +244 -3
  107. package/dist/holmes/rtm/incremental.d.ts +1 -0
  108. package/dist/holmes/rtm/incremental.js +12 -3
  109. package/dist/holmes/rtm/localize.js +7 -0
  110. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  111. package/dist/holmes/rtm/rtm-graph.js +13 -0
  112. package/dist/holmes/rtm/test-scope.js +4 -1
  113. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  114. package/dist/holmes/semantic/vector-cache.js +155 -19
  115. package/dist/holmes/spec/approval-status.d.ts +10 -0
  116. package/dist/holmes/spec/approval-status.js +7 -3
  117. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  118. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  119. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  120. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  121. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  122. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  123. package/dist/holmes/spec/entity-integration.d.ts +216 -0
  124. package/dist/holmes/spec/entity-integration.js +760 -0
  125. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  126. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  127. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  128. package/dist/holmes/spec/entity-renumber.js +156 -0
  129. package/dist/holmes/spec/entity-store.d.ts +135 -0
  130. package/dist/holmes/spec/entity-store.js +1053 -0
  131. package/dist/holmes/spec/entity-transaction.d.ts +105 -0
  132. package/dist/holmes/spec/entity-transaction.js +741 -0
  133. package/dist/holmes/spec/renumber.d.ts +58 -0
  134. package/dist/holmes/spec/renumber.js +200 -1
  135. package/dist/holmes/spec/spec-store.d.ts +3 -2
  136. package/dist/holmes/spec/spec-store.js +23 -2
  137. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  138. package/dist/holmes/spec/transition-policy.js +135 -0
  139. package/package.json +5 -2
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createRiskAssessmentHandlers = createRiskAssessmentHandlers;
37
+ // @implements A-SPEC-607, A-SPEC-100.2, A-SPEC-128
38
+ // @implements A-SPEC-642
39
+ const path = __importStar(require("node:path"));
40
+ const spec_parser_1 = require("../../spec/spec-parser");
41
+ const rtm_graph_1 = require("../../rtm/rtm-graph");
42
+ const rtm_builder_1 = require("../../rtm/rtm-builder");
43
+ const risk_classifier_1 = require("../../guardrail/risk-classifier");
44
+ const risk_gate_1 = require("../../guardrail/risk-gate");
45
+ const cspec_change_1 = require("../../guardrail/cspec-change");
46
+ const provenance_chain_1 = require("../../governance/provenance-chain");
47
+ const ledger_store_1 = require("../../governance/ledger-store");
48
+ const decision_ledger_1 = require("../../guardrail/decision-ledger");
49
+ function createRiskAssessmentHandlers(context) {
50
+ return {
51
+ // @implements A-SPEC-125.4
52
+ async risk_check(a) {
53
+ const action = { ...a.action };
54
+ if (a.root && a.changedSymbols && a.changedSymbols.length) {
55
+ // @implements A-SPEC-189 §14 (round 13) — the derivation was computed and discarded, so the
56
+ // advertised "a subdirectory resolves up to it" held for `taint_scan` and no one else.
57
+ const root = context.projectRootOf(a.root);
58
+ const specs = await context.listSpecs();
59
+ const scanned = context.cachedScan(root, root);
60
+ const g = new rtm_graph_1.RtmGraph();
61
+ try {
62
+ (0, rtm_builder_1.buildRtm)(specs, scanned, g);
63
+ const impacted = (0, rtm_builder_1.impactedBy)(g, a.changedSymbols);
64
+ action.blastRadius = { impactedSpecCount: impacted.length, hitsFoundational: impacted.some((id) => id.startsWith('SPEC:REQ-')) };
65
+ }
66
+ finally {
67
+ g.close(); // release native SQLite handle even if build/query throws
68
+ }
69
+ }
70
+ // @implements A-SPEC-225 — the flag that raises this action's risk is COMPUTED here, not
71
+ // taken from the party being judged. Measured 2026-08-22: nothing in the repository ever set
72
+ // `cspecConstraintChange`, so the hard-hitl branch guarding C-SPEC constraints could only fire
73
+ // if the actor changing them volunteered that they had. The precedent is one field away —
74
+ // `blastRadius` above is derived from the store rather than accepted from the caller — and the
75
+ // principle is the one `findings.ts` states for `basis`: a judge that stamps its own evidence
76
+ // turns the field into self-report.
77
+ //
78
+ // OR, never assignment: a caller may raise its own risk (that is their business) but may not
79
+ // lower the server's verdict.
80
+ const cspecId = (0, cspec_change_1.cspecTargetId)(typeof action.target === 'string' ? action.target : '');
81
+ if (cspecId !== null) {
82
+ const stored = (await context.listSpecs()).find((sp) => sp.id === cspecId);
83
+ const current = stored ? (0, spec_parser_1.serializeSpec)(stored) : null;
84
+ const proposed = typeof action.stagedContent === 'string' ? action.stagedContent : null;
85
+ action.cspecConstraintChange = (0, cspec_change_1.cspecConstraintChanged)(current, proposed) || action.cspecConstraintChange === true;
86
+ }
87
+ const assessment = (0, risk_classifier_1.assessRisk)(action);
88
+ // @implements A-SPEC-133 — coverage (scope + expiry), not mere well-formedness, decides whether
89
+ // this approval authorizes THIS action. An out-of-scope or expired token no longer unblocks.
90
+ const coverTarget = { kind: action.kind, target: typeof action.target === 'string' ? action.target : String(action.target ?? '') };
91
+ let gate = (0, risk_gate_1.riskGate)(assessment, a.approval, a.enforcement, { action: coverTarget, now: a.ts });
92
+ const covers = (0, risk_gate_1.approvalCovers)(a.approval, coverTarget, a.ts);
93
+ // @implements A-SPEC-133 — the hard-hitl branch HONORS a nonce by SPENDING it (r7-191 HIGH:
94
+ // this consumer accepted single-use approvals and spent nothing — 'N-RISK' unblocked three
95
+ // times; a replayed approval must deny exactly as an absent one would).
96
+ // @implements A-SPEC-191 §13 — 승인이 실제로 문을 여는 경우에만 승인의 하자를 따진다.
97
+ // 9라운드 실측: 승인이 아예 필요 없던 낮은 판정까지 환경에 놓인 토큰의 모양 때문에
98
+ // 차단됐다 — 승인을 쥔 쪽이 안 쥔 쪽보다 나빠지는 역전이다.
99
+ const wouldBlockWithout = (0, risk_gate_1.riskGate)(assessment, undefined, a.enforcement, { action: coverTarget, now: a.ts }).blocked;
100
+ if (!gate.blocked && covers && wouldBlockWithout && (0, provenance_chain_1.blankNonce)(a.approval?.nonce)) {
101
+ gate = { ...gate, blocked: true, reasons: [...gate.reasons, '승인이 단일 사용(nonce)을 선언했으나 값이 비어 있거나 문자열이 아닙니다 — 1회성을 집행할 수 없어 거부합니다'] };
102
+ }
103
+ // 묶인 프로젝트가 없으면 개방을 기록할 수 없다 — 기록되지 않는 개방은 개방하지 않는다.
104
+ if (!gate.blocked && covers && wouldBlockWithout && context.boundNonceLedger() === null) {
105
+ gate = { ...gate, blocked: true, reasons: [...gate.reasons, '개방을 기록할 프로젝트 원장을 확정할 수 없습니다 — 기록 없는 개방은 하지 않습니다(.ax 를 가진 프로젝트에 바인딩된 서버에서 호출하십시오)'] };
106
+ }
107
+ // @implements A-SPEC-642 — an opening is refused, and nothing spent, while the merged ledger
108
+ // holds an unreconciled cross-replica double-spend; the reason names the nonce and the act.
109
+ if (!gate.blocked && covers && wouldBlockWithout) {
110
+ const bound = context.boundNonceLedger();
111
+ const conflict = bound === null ? null : (0, ledger_store_1.ledgerConflictReason)(path.dirname(bound));
112
+ if (conflict)
113
+ gate = { ...gate, blocked: true, reasons: [...gate.reasons, conflict] };
114
+ }
115
+ if (assessment.level === 'hard-hitl' && !gate.blocked && covers && (0, provenance_chain_1.nonceDeclared)(a.approval?.nonce)) {
116
+ const ledgerFile = context.boundNonceLedger();
117
+ let won = false;
118
+ try {
119
+ won = ledgerFile !== null && (0, provenance_chain_1.consumeNonceExclusively)(String(a.approval.nonce), ledgerFile, {
120
+ ts: a.ts, actor: a.approval.actor, kind: 'nonce-consumed',
121
+ summary: `consumed single-use approval for: ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 200),
122
+ inputs: [(0, provenance_chain_1.nonceFingerprint)(String(a.approval.nonce))], rationale: a.approval.rationale,
123
+ authorization: (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token),
124
+ }, { isNonceConsumed: (0, ledger_store_1.nonceConsumedIn)(ledgerFile) });
125
+ }
126
+ catch {
127
+ won = false;
128
+ } // 배타 확보 실패 = 허용 불가(fail-closed)
129
+ if (!won) {
130
+ gate = { ...gate, blocked: true, reasons: [...gate.reasons, ledgerFile === null
131
+ ? '단일 사용 승인(nonce)을 기록할 프로젝트 원장을 확정할 수 없습니다 — 1회성을 보장할 수 없어 거부합니다(.ax 를 가진 프로젝트에 바인딩된 서버에서 호출하십시오)'
132
+ : '단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 부재와 같이 거부됩니다'] };
133
+ }
134
+ }
135
+ // Honest provenance: a confirm-level action that was NOT blocked but received no COVERING
136
+ // approval must NOT be recorded as 'approved' (nobody authorized it for this action) — it
137
+ // 'proceeded unconfirmed'. Only a genuine covering out-of-band approval yields 'approved'.
138
+ const decision = gate.blocked
139
+ ? 'blocked'
140
+ : covers
141
+ ? 'approved'
142
+ : gate.requiresApproval
143
+ ? 'proceeded-unconfirmed'
144
+ : 'auto';
145
+ const evt = { ts: a.ts, actor: a.actor ?? a.approval?.actor ?? 'unknown', action, level: assessment.level, decision, rationale: a.rationale ?? a.approval?.rationale ?? '', reasons: assessment.reasons };
146
+ // @implements A-SPEC-191 §10 — a bound server files its decisions in the project it is bound
147
+ // to. r8: the audit line for a nonce-replay opening landed in the caller's scratch directory,
148
+ // so the project whose gate was opened held no trace of it.
149
+ const bound = context.boundNonceLedger();
150
+ const decisionsFile = bound !== null
151
+ ? path.join(path.dirname(bound), 'decisions.jsonl')
152
+ : path.join(a.root ?? '.', '.ax', 'ledger', 'decisions.jsonl');
153
+ // @implements A-SPEC-191 §11 — an audit write must not swallow a verdict the caller already
154
+ // earned (the hook has said this since N3). The nonce is spent for an opening that DOES reach
155
+ // the caller; a disk failure here loses the record, not the decision.
156
+ try {
157
+ new decision_ledger_1.DecisionLedger(decisionsFile).record([evt]);
158
+ }
159
+ catch { /* 기록 실패가 이미 계산된 판정을 뒤집지 않는다 */ }
160
+ // @implements A-SPEC-133 — 'Every gate opening records a provenance approved-action entry.'
161
+ // r8: the promise was kept in the hook and broken here — a covering approval that unblocked a
162
+ // hard-hitl risk_check left only `nonce-consumed` (and nothing at all when it carried no
163
+ // nonce), so the master key's use was invisible in the chain the audit reads.
164
+ const wouldBlock = wouldBlockWithout;
165
+ const bnl = context.boundNonceLedger();
166
+ if (!gate.blocked && covers && wouldBlock && bnl !== null) {
167
+ try {
168
+ new provenance_chain_1.ProvenanceChain(bnl).append({
169
+ ts: a.ts, actor: a.approval?.actor ?? 'unknown', kind: 'approved-action',
170
+ summary: `approved action: risk_check ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 300),
171
+ inputs: ['risk_check', (0, provenance_chain_1.redactTarget)('command', coverTarget.target), (0, provenance_chain_1.approvalMarkers)(a.approval)],
172
+ rationale: a.approval?.rationale ?? '',
173
+ authorization: a.approval ? (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token) : undefined,
174
+ });
175
+ }
176
+ catch { /* 감사 기록 실패가 판정을 바꾸지 않는다 */ }
177
+ }
178
+ return { assessment, gate };
179
+ },
180
+ };
181
+ }
@@ -0,0 +1,72 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { SpecStore } from '../../spec/spec-store';
3
+ export interface SliceOrchestrationContext {
4
+ listSpecs(): Promise<Spec[]>;
5
+ unreadableAmong(listed: ReadonlyArray<{
6
+ id: string;
7
+ }>, created: readonly string[]): string[];
8
+ rawHandlersForStore(store: SpecStore): {
9
+ spec_approve(a: {
10
+ root: string;
11
+ id: string;
12
+ }): Promise<unknown>;
13
+ spec_slice_approve(a: {
14
+ root: string;
15
+ sliceName: string;
16
+ }): Promise<unknown>;
17
+ };
18
+ }
19
+ /**
20
+ * @implements A-SPEC-652.1 — the T-SPEC that belongs to an A-SPEC, by the mirror rule the
21
+ * author-slice playbook states (the chain inherits the parent's number, dot included): the
22
+ * mirror id first, then a T-SPEC that declares `depends_on` on this exact A-SPEC, then — for an
23
+ * UNDOTTED id only — the old base-number match that kept legacy `T-SPEC-1291`-style ids working.
24
+ * Measured 2026-09-14: `spec_slice_approve('A-SPEC-651.2')` sealed `T-SPEC-651.1`, because the old
25
+ * `find` accepted any T-SPEC whose base number matched and .1 came first.
26
+ */
27
+ export declare function tspecFor(aspec: Spec, specs: Spec[]): Spec | undefined;
28
+ export declare function createSliceOrchestrationHandlers(context: SliceOrchestrationContext): {
29
+ spec_slice_init(a: {
30
+ root?: string;
31
+ sliceName: string;
32
+ title: string;
33
+ objective: string;
34
+ filesToTouch: string[];
35
+ }): Promise<{
36
+ ok: boolean;
37
+ reason: string;
38
+ existing: string[];
39
+ specsCreated: string[];
40
+ unreadable: string[];
41
+ } | {
42
+ ok: boolean;
43
+ specsCreated: string[];
44
+ unreadable: string[];
45
+ reason?: undefined;
46
+ existing?: undefined;
47
+ }>;
48
+ spec_slice_approve(a: {
49
+ root?: string;
50
+ sliceName: string;
51
+ }): Promise<{
52
+ ok: boolean;
53
+ approvedSpecs: string[];
54
+ alreadySealed: string[];
55
+ refused: {
56
+ id: string;
57
+ reason: string;
58
+ }[];
59
+ }>;
60
+ spec_remediate(a: {
61
+ root?: string;
62
+ targetFile?: string;
63
+ aspecId?: string;
64
+ }): Promise<{
65
+ ok: boolean;
66
+ actionsTaken: string[];
67
+ refused: {
68
+ id: string;
69
+ reason: string;
70
+ }[];
71
+ }>;
72
+ };
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.tspecFor = tspecFor;
37
+ exports.createSliceOrchestrationHandlers = createSliceOrchestrationHandlers;
38
+ // @implements A-SPEC-609, A-SPEC-100.2, A-SPEC-156, A-SPEC-203, A-SPEC-204, A-SPEC-298, A-SPEC-299, A-SPEC-536.1, A-SPEC-546.1, A-SPEC-565.2, A-SPEC-587.2, A-SPEC-501.1
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const spec_store_1 = require("../../spec/spec-store");
42
+ const root_1 = require("../../project/root");
43
+ const yaml_scalar_1 = require("../../spec/yaml-scalar");
44
+ const draft_1 = require("../../reverse/draft");
45
+ const anchor_comment_1 = require("../../rtm/anchor-comment");
46
+ /**
47
+ * @implements A-SPEC-652.1 — the T-SPEC that belongs to an A-SPEC, by the mirror rule the
48
+ * author-slice playbook states (the chain inherits the parent's number, dot included): the
49
+ * mirror id first, then a T-SPEC that declares `depends_on` on this exact A-SPEC, then — for an
50
+ * UNDOTTED id only — the old base-number match that kept legacy `T-SPEC-1291`-style ids working.
51
+ * Measured 2026-09-14: `spec_slice_approve('A-SPEC-651.2')` sealed `T-SPEC-651.1`, because the old
52
+ * `find` accepted any T-SPEC whose base number matched and .1 came first.
53
+ */
54
+ function tspecFor(aspec, specs) {
55
+ const number = aspec.id.replace(/^A-SPEC-/, '');
56
+ const mirror = specs.find((s) => s.type === 'T-SPEC' && s.id === `T-SPEC-${number}`);
57
+ if (mirror)
58
+ return mirror;
59
+ const dependent = specs.filter((s) => s.type === 'T-SPEC' && s.dependsOn.includes(aspec.id)).sort((a, b) => a.id.localeCompare(b.id))[0];
60
+ if (dependent)
61
+ return dependent;
62
+ if (number.includes('.'))
63
+ return undefined;
64
+ // Undotted only: the old base-number match (`T-SPEC-700.1` for `A-SPEC-700`) and the pre-REQ-187
65
+ // convention that appended one digit (`T-SPEC-1291` for `A-SPEC-129`).
66
+ const mainId = (id) => id.split('.')[0].replace(/\D/g, '');
67
+ const legacy = new RegExp(`^T-SPEC-${number}\\d$`);
68
+ return specs.find((s) => s.type === 'T-SPEC' && (mainId(s.id) === number || legacy.test(s.id)));
69
+ }
70
+ function createSliceOrchestrationHandlers(context) {
71
+ return {
72
+ async spec_slice_init(a) {
73
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
74
+ const specsDir = path.join(root, '.ax', 'specs');
75
+ const existing = await context.listSpecs();
76
+ // @implements A-SPEC-546.1 — align with the sibling reverse-draft path (nextIdBase, floor 100)
77
+ // instead of a hard-coded 200 floor, so a fresh project starts at REQ-100, not REQ-201. Existing
78
+ // projects are unchanged: nextIdBase returns max(existing)+1, which dominates the floor.
79
+ // @implements A-SPEC-651.5 — the number is never below EITHER store's max: the bound store's
80
+ // (the contract above) and the store the files are written to. Measured 2026-09-14: the bound
81
+ // store was empty, the id fell to 100 and the sealed REQ-100.md of the target was overwritten.
82
+ const targetIds = fs.existsSync(specsDir) ? (await new spec_store_1.LocalMarkdownRepository(specsDir).list()).map((s) => s.id) : [];
83
+ const nextId = (0, draft_1.nextIdBase)([...existing.map((s) => s.id), ...targetIds]);
84
+ const reqId = `REQ-${nextId}`;
85
+ const hspecId = `H-SPEC-${nextId}`;
86
+ const aspecId = `A-SPEC-${nextId}`;
87
+ const tspecId = `T-SPEC-${nextId}`;
88
+ const reqContent = `---
89
+ source:
90
+ - kind: user-request
91
+ ref: ${(0, yaml_scalar_1.yamlScalar)(a.title, 4)}
92
+ retrieved: ${new Date().toISOString().split('T')[0]}
93
+ note: ${(0, yaml_scalar_1.yamlScalar)(a.objective, 4)}
94
+ created: ${new Date().toISOString()}
95
+ id: ${reqId}
96
+ type: REQ
97
+ title: ${(0, yaml_scalar_1.yamlScalar)(a.title)}
98
+ status: draft
99
+ depends_on: []
100
+ ---
101
+
102
+ ## Problem / Need
103
+ ${a.objective}
104
+
105
+ ## Desired Outcome
106
+ ${a.objective}
107
+
108
+ ## Constraints
109
+ - Standard project governance rules.
110
+
111
+ ## Success Criteria
112
+
113
+ ## Out of Scope
114
+ - Unrelated feature changes.
115
+ `;
116
+ const hspecContent = `---
117
+ created: ${new Date().toISOString()}
118
+ id: ${hspecId}
119
+ type: H-SPEC
120
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Functional Specification for ${a.title}`)}
121
+ status: draft
122
+ req_type: functional
123
+ owner: me
124
+ depends_on:
125
+ - ${reqId}
126
+ ---
127
+
128
+ ## Intent
129
+ Implement ${a.title}.
130
+
131
+ ## Scope (In / Out)
132
+ In Scope: ${a.title}.
133
+ Out of Scope: None.
134
+
135
+ ## Design Overview
136
+ High level design for ${a.title}.
137
+
138
+ ## Interfaces / Contracts
139
+ - Target files: ${a.filesToTouch.join(', ')}
140
+
141
+ ## Acceptance Criteria
142
+ - Code written and tests passing.
143
+
144
+ ## Non-Functional
145
+ - Performance and stability maintained.
146
+
147
+ ## Assumptions
148
+ - Environment configured properly.
149
+
150
+ ## Open Questions
151
+ - None.
152
+ `;
153
+ const aspecContent = `---
154
+ created: ${new Date().toISOString()}
155
+ id: ${aspecId}
156
+ type: A-SPEC
157
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Architecture Specification for ${a.title}`)}
158
+ status: draft
159
+ slice: ${(0, yaml_scalar_1.yamlScalar)(a.sliceName)}
160
+ priority: P1
161
+ independent_test: true
162
+ depends_on:
163
+ - ${hspecId}
164
+ breaking_change: 'none'
165
+ harness_impact: 'none: TODO — 3하네스(claude/codex/agy) 영향 검토 후 기술'
166
+ os_impact: 'none: TODO — 3OS(windows/mac/linux) 영향 검토 후 기술'
167
+ ---
168
+
169
+ ## Objective
170
+ ${a.objective}
171
+
172
+ ## Component Design
173
+ 1. Target Component:
174
+ - Modifies ${a.filesToTouch.join(', ')}.
175
+
176
+ ## Inputs / Outputs
177
+ - Inputs: Tool calls / developer modifications.
178
+ - Outputs: Working implementation.
179
+
180
+ ## Behavior
181
+ - Implements desired behavior cleanly.
182
+
183
+ ## Test Points
184
+ - Unit tests in test suite.
185
+
186
+ ## Files to Touch
187
+ ${a.filesToTouch.map((f) => `- ${f}`).join('\n')}
188
+
189
+ ## Done When
190
+ - All tests pass 100%.
191
+ `;
192
+ const tspecContent = `---
193
+ coverage:
194
+ normal: true
195
+ corner: true
196
+ negative: true
197
+ boundary: true
198
+ id: ${tspecId}
199
+ type: T-SPEC
200
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Test Specification for ${a.title}`)}
201
+ status: draft
202
+ depends_on:
203
+ - ${aspecId}
204
+ ---
205
+
206
+ ## Normal Cases
207
+ - Given valid inputs
208
+ When operation is performed
209
+ Then correct behavior is observed
210
+
211
+ ## Corner Cases
212
+ - Given edge cases
213
+ When operation is performed
214
+ Then system handles gracefully
215
+
216
+ ## Negative Cases
217
+ - Given invalid inputs
218
+ When operation is performed
219
+ Then appropriate error is returned
220
+
221
+ ## Boundary Cases
222
+ - Given boundary conditions
223
+ When operation is performed
224
+ Then boundary limits are respected
225
+ `;
226
+ // @implements A-SPEC-651.5 — all or nothing, judged BEFORE the first byte: a target path that
227
+ // already exists (readable or not — the number cannot see an unparseable file) refuses the
228
+ // whole chain and names the files, so a sealed document is never replaced by a stub.
229
+ const targets = [
230
+ ['01_req', `${reqId}.md`], ['02_h-spec/functional', `${hspecId}.md`], ['03_a-spec', `${aspecId}.md`], ['05_t-spec', `${tspecId}.md`],
231
+ ];
232
+ const existingFiles = targets.filter(([dir, name]) => fs.existsSync(path.join(specsDir, dir, name))).map(([dir, name]) => `${dir}/${name}`);
233
+ if (existingFiles.length > 0) {
234
+ return { ok: false, reason: `spec_slice_init: ${existingFiles.length} target file(s) already exist — nothing written`, existing: existingFiles, specsCreated: [], unreadable: [] };
235
+ }
236
+ fs.mkdirSync(path.join(specsDir, '01_req'), { recursive: true });
237
+ fs.mkdirSync(path.join(specsDir, '02_h-spec', 'functional'), { recursive: true });
238
+ fs.mkdirSync(path.join(specsDir, '03_a-spec'), { recursive: true });
239
+ fs.mkdirSync(path.join(specsDir, '05_t-spec'), { recursive: true });
240
+ fs.writeFileSync(path.join(specsDir, '01_req', `${reqId}.md`), reqContent);
241
+ fs.writeFileSync(path.join(specsDir, '02_h-spec', 'functional', `${hspecId}.md`), hspecContent);
242
+ fs.writeFileSync(path.join(specsDir, '03_a-spec', `${aspecId}.md`), aspecContent);
243
+ fs.writeFileSync(path.join(specsDir, '05_t-spec', `${tspecId}.md`), tspecContent);
244
+ // @implements A-SPEC-299
245
+ // Read back what was just written. S-298 fixed one cause of unparseable frontmatter; this
246
+ // closes the class. `list()` silently skips documents it cannot parse, so "created" and
247
+ // "present in the store" can diverge without anything throwing — which is exactly how a
248
+ // requirement went missing on 2026-08-28 while the tool reported four specs created.
249
+ const specsCreated = [reqId, hspecId, aspecId, tspecId];
250
+ const unreadable = context.unreadableAmong(await new spec_store_1.LocalMarkdownRepository(specsDir).list(), specsCreated);
251
+ // The files stay on disk on purpose: deleting the evidence turns a diagnosable bug into a
252
+ // mystery, and a half-written slice is something a person should see.
253
+ return { ok: unreadable.length === 0, specsCreated, unreadable };
254
+ },
255
+ async spec_slice_approve(a) {
256
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
257
+ const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
258
+ const specs = await rawStore.list();
259
+ const inSlice = specs.filter((s) => {
260
+ if (s.frontmatter?.slice === a.sliceName)
261
+ return true;
262
+ if (s.id === a.sliceName)
263
+ return true;
264
+ return false;
265
+ });
266
+ const idsToApprove = [];
267
+ // @implements A-SPEC-652.1 — a dotted A-SPEC with no mirror and no dependent T-SPEC is named
268
+ // in `refused` AFTER the rest of the chain is sealed: the code gate stays shut without a
269
+ // T-SPEC, and a silent `ok:true` would read as "the slice is approved".
270
+ let missingTspec;
271
+ if (inSlice.length > 0) {
272
+ const aspec = inSlice.find((s) => s.type === 'A-SPEC') ?? inSlice[0];
273
+ const mainId = (id) => id.split('.')[0].replace(/\D/g, '');
274
+ const req = specs.find((s) => aspec.dependsOn.includes(s.id) || mainId(s.id) === mainId(aspec.id));
275
+ const hspec = specs.find((s) => s.type === 'H-SPEC' && (s.dependsOn.includes(req?.id ?? '') || mainId(s.id) === mainId(aspec.id)));
276
+ const tspec = tspecFor(aspec, specs);
277
+ if (req)
278
+ idsToApprove.push(req.id);
279
+ if (hspec)
280
+ idsToApprove.push(hspec.id);
281
+ idsToApprove.push(aspec.id);
282
+ if (tspec)
283
+ idsToApprove.push(tspec.id);
284
+ else if (aspec.type === 'A-SPEC' && aspec.id.includes('.'))
285
+ missingTspec = `T-SPEC-${aspec.id.replace(/^A-SPEC-/, '')}`;
286
+ }
287
+ else {
288
+ const matches = specs.filter((s) => s.id.includes(a.sliceName));
289
+ idsToApprove.push(...matches.map((s) => s.id));
290
+ }
291
+ const rawHandlers = context.rawHandlersForStore(rawStore);
292
+ const approvedSpecs = [];
293
+ const alreadySealed = [];
294
+ const refused = [];
295
+ if (idsToApprove.length === 0) {
296
+ // Reporting this as a success is how a caller ends up believing an unapproved slice was
297
+ // approved. Measured 2026-08-28: a broken-frontmatter REQ made the whole slice invisible
298
+ // here, and the tool answered `ok: true, approvedSpecs: []`.
299
+ return { ok: false, approvedSpecs, alreadySealed, refused: [{ id: a.sliceName, reason: 'no specs matched this slice name' }] };
300
+ }
301
+ // @implements A-SPEC-587.2 — a sealed spec is SKIPPED, not re-sealed: re-approving asked the
302
+ // human the same question again (same request id back in the queue) and stopped the chain
303
+ // there, so the H-SPEC was never even attempted (measured 2026-09-10, three slices).
304
+ const sealed = (id) => {
305
+ const s = specs.find((x) => x.id === id);
306
+ return s?.status === 'approved' && typeof s.frontmatter?.approved_digest === 'string';
307
+ };
308
+ for (const id of idsToApprove) {
309
+ if (sealed(id)) {
310
+ alreadySealed.push(id);
311
+ continue;
312
+ }
313
+ const res = (await rawHandlers.spec_approve({ root, id }));
314
+ if (res.approved || res.ok) {
315
+ approvedSpecs.push(id);
316
+ continue;
317
+ }
318
+ const findings = Array.isArray(res.findings)
319
+ ? res.findings.map((f) => f.message).filter(Boolean).join('; ')
320
+ : '';
321
+ refused.push({ id, reason: findings ? `${res.reason ?? 'refused'}: ${findings}` : (res.reason ?? 'refused') });
322
+ // The chain is topological: approving a child whose parent was refused would either fail
323
+ // again or, worse, succeed against an unsealed parent. Stop and say what is left.
324
+ break;
325
+ }
326
+ for (const id of idsToApprove.slice(approvedSpecs.length + alreadySealed.length + refused.length)) {
327
+ refused.push({ id, reason: 'not attempted — an earlier spec in the chain was refused' });
328
+ }
329
+ if (missingTspec !== undefined) {
330
+ const aspecId = `A-SPEC-${missingTspec.replace(/^T-SPEC-/, '')}`;
331
+ refused.push({ id: missingTspec, reason: `no T-SPEC mirrors ${aspecId} or depends on it — write ${missingTspec} first` });
332
+ }
333
+ // `ok` means the slice is approved — freshly or already. Anything less is not a success.
334
+ return { ok: refused.length === 0 && approvedSpecs.length + alreadySealed.length > 0, approvedSpecs, alreadySealed, refused };
335
+ },
336
+ async spec_remediate(a) {
337
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
338
+ const actionsTaken = [];
339
+ if (a.targetFile && a.aspecId) {
340
+ const fullPath = path.isAbsolute(a.targetFile) ? a.targetFile : path.join(root, a.targetFile);
341
+ if (fs.existsSync(fullPath)) {
342
+ // @implements A-SPEC-501.1 — BUG-4 (dogfooded twice): the old unconditional
343
+ // `// @implements` broke Python/YAML targets outright AND produced anchors the scanner
344
+ // could not read. The syntax comes from the one map the scanner is aligned with, an
345
+ // unknown syntax is an honest refusal, and the containment check is form-agnostic so a
346
+ // `#` anchor is not double-injected.
347
+ const anchorTag = (0, anchor_comment_1.anchorLineFor)(fullPath, a.aspecId);
348
+ const content = fs.readFileSync(fullPath, 'utf8');
349
+ if (anchorTag === null) {
350
+ actionsTaken.push(`앵커 주입 거부: ${path.basename(fullPath)} — 이 확장자의 주석 문법을 모릅니다. 파일 관례에 맞는 주석으로 1행에 '@implements ${a.aspecId}'를 직접 추가하십시오`);
351
+ }
352
+ else if (!content.includes(`@implements ${a.aspecId}`)) {
353
+ fs.writeFileSync(fullPath, `${anchorTag}\n${content}`);
354
+ actionsTaken.push(`Injected ${anchorTag} on line 1 of ${a.targetFile}`);
355
+ }
356
+ }
357
+ }
358
+ const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
359
+ const rawHandlers = context.rawHandlersForStore(rawStore);
360
+ const approveRes = await rawHandlers.spec_slice_approve({ root, sliceName: a.aspecId ?? 'slice' });
361
+ if (approveRes.approvedSpecs?.length > 0) {
362
+ actionsTaken.push(`Approved slice specs: ${approveRes.approvedSpecs.join(', ')}`);
363
+ }
364
+ // Same false-PASS as spec_slice_approve had, one layer up: this handler's whole purpose is to
365
+ // approve, so reporting success when approval was refused sends the caller into code with an
366
+ // unapproved slice behind it.
367
+ const refused = approveRes.refused ?? [];
368
+ for (const r of refused)
369
+ actionsTaken.push(`Refused ${r.id}: ${r.reason}`);
370
+ return { ok: refused.length === 0, actionsTaken, refused };
371
+ },
372
+ };
373
+ }