@holmes-lab/holmes-kit 0.19.5 → 0.20.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 (128) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/README.md +17 -1
  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/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createReviewQueryHandlers = createReviewQueryHandlers;
4
+ const rtm_graph_1 = require("../../rtm/rtm-graph");
5
+ const rtm_builder_1 = require("../../rtm/rtm-builder");
6
+ const test_scope_1 = require("../../rtm/test-scope");
7
+ const bundler_1 = require("../../context/bundler");
8
+ const scope_1 = require("../../review/scope");
9
+ const package_1 = require("../../review/package");
10
+ const coverage_1 = require("../../review/coverage");
11
+ function createReviewQueryHandlers(context) {
12
+ return {
13
+ async context_bundle(a) {
14
+ context.assertStoreReachable('context_bundle', a.root); // @implements A-SPEC-433
15
+ const foreign = context.foreignRootReason(a.root);
16
+ if (foreign)
17
+ throw context.refusal(foreign);
18
+ const root = context.projectRootOf(a.root);
19
+ const specs = await context.listSpecs();
20
+ const scanned = context.cachedScan(root, root);
21
+ const content = context.buildContentSource(specs, scanned);
22
+ const g = new rtm_graph_1.RtmGraph();
23
+ try {
24
+ (0, rtm_builder_1.buildRtm)(specs, scanned, g);
25
+ return new bundler_1.ContextBundler(g, content).getContextBundle(a.seedId, a.budget);
26
+ }
27
+ finally {
28
+ g.close(); // release native SQLite handle even if bundling throws
29
+ }
30
+ },
31
+ async review_scope(a) {
32
+ context.assertStoreReachable('review_scope', a.root); // @implements A-SPEC-419
33
+ // Guard+diff+scan+changedFiles/changedSymbols prologue: see
34
+ // deriveChangedContext (shared with review_prepare/rtm_reindex).
35
+ const { root, specs, scanned, changedFiles, changedSymbols, changeSource, scopeFallback, anchorImpactedSpecs, changedTestFiles, unresolvedFiles } = await context.deriveChangedContext(a.root, a, 'review_scope');
36
+ const g = new rtm_graph_1.RtmGraph();
37
+ try {
38
+ (0, rtm_builder_1.buildRtm)(specs, scanned, g);
39
+ const scope = (0, scope_1.computeReviewScope)(g, specs, changedSymbols);
40
+ // Intelligent regression test-scope (target-arch §5). Test files are scanned SEPARATELY for
41
+ // their @implements anchors (the CPG scan excludes tests), coverage gaps feed the safety
42
+ // fallback, and impactedSpecs are the graph's SPEC:-prefixed ids (stripped inside).
43
+ // @implements A-SPEC-130 — the same widened set as test_run, so review packaging and test
44
+ // scoping cannot disagree about what a change touches.
45
+ const widenedImpacted = [...new Set([...scope.impactedSpecs, ...anchorImpactedSpecs])];
46
+ const testScope = (0, test_scope_1.computeTestScope)(widenedImpacted, (0, test_scope_1.scanTestAnchors)(root), specs, scope.coverageGaps, undefined, { changedTestFiles, unresolvedFiles, architectureTouched: (0, test_scope_1.architectureGoverningFiles)(changedFiles), obligationGaps: scope.obligationGaps });
47
+ // Honest signal (REQ-124 gate 2a): which changed files the CpgScanner
48
+ // did NOT ingest (e.g. non-.ts files), computed from the same
49
+ // changedFiles/scanned already derived above — no new git/scan calls.
50
+ const { unscanned: unscannedChangedFiles } = (0, coverage_1.partitionChangedFiles)(changedFiles, scanned.map((f) => f.sourcePath));
51
+ return { ...scope, unscannedChangedFiles, testScope, changeSource, ...(scopeFallback ? { scopeFallback } : {}) };
52
+ }
53
+ finally {
54
+ g.close(); // release native SQLite handle even if scope computation throws
55
+ }
56
+ },
57
+ async review_prepare(a) {
58
+ context.assertStoreReachable('review_prepare', a.root); // @implements A-SPEC-419
59
+ // Guard+diff+scan+changedFiles/changedSymbols prologue: see
60
+ // deriveChangedContext (shared with review_scope/rtm_reindex).
61
+ const { root, specs, scanned, changedFiles, changedSymbols, changeSource, scopeFallback } = await context.deriveChangedContext(a.root, a, 'review_prepare');
62
+ // ContentSource factory shared with context_bundle: see buildContentSource.
63
+ const content = context.buildContentSource(specs, scanned);
64
+ const g = new rtm_graph_1.RtmGraph();
65
+ try {
66
+ (0, rtm_builder_1.buildRtm)(specs, scanned, g);
67
+ const pkg = (0, package_1.assembleReviewPackage)(g, specs, changedSymbols, content, a.budget ?? 4000);
68
+ // Honest signal (REQ-124 gate 2a): which changed files the CpgScanner
69
+ // did NOT ingest, computed from the same changedFiles/scanned already
70
+ // derived above — no new git/scan calls.
71
+ const { unscanned: unscannedChangedFiles } = (0, coverage_1.partitionChangedFiles)(changedFiles, scanned.map((f) => f.sourcePath));
72
+ return { package: pkg, unscannedChangedFiles, changeSource, ...(scopeFallback ? { scopeFallback } : {}) };
73
+ }
74
+ finally {
75
+ g.close(); // release native SQLite handle even if assembly throws
76
+ }
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,25 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { ScannedFile } from '../../cpg/cpg-scanner';
3
+ import { Approval, Enforcement } from '../../guardrail/risk-gate';
4
+ import { RiskAction } from '../../guardrail/risk-types';
5
+ export interface RiskAssessmentContext {
6
+ listSpecs(): Promise<Spec[]>;
7
+ projectRootOf(root: string): string;
8
+ cachedScan(root: string, repoRoot?: string): ScannedFile[];
9
+ boundNonceLedger(): string | null;
10
+ }
11
+ export declare function createRiskAssessmentHandlers(context: RiskAssessmentContext): {
12
+ risk_check(a: {
13
+ root?: string;
14
+ action: RiskAction;
15
+ approval?: Approval;
16
+ changedSymbols?: string[];
17
+ enforcement?: Enforcement;
18
+ ts: string;
19
+ actor?: string;
20
+ rationale?: string;
21
+ }): Promise<{
22
+ assessment: import("../../guardrail/risk-types").RiskAssessment;
23
+ gate: import("../../guardrail/risk-gate").GateResult;
24
+ }>;
25
+ };
@@ -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,55 @@
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
+ export declare function createSliceOrchestrationHandlers(context: SliceOrchestrationContext): {
20
+ spec_slice_init(a: {
21
+ root?: string;
22
+ sliceName: string;
23
+ title: string;
24
+ objective: string;
25
+ filesToTouch: string[];
26
+ }): Promise<{
27
+ ok: boolean;
28
+ specsCreated: string[];
29
+ unreadable: string[];
30
+ }>;
31
+ spec_slice_approve(a: {
32
+ root?: string;
33
+ sliceName: string;
34
+ }): Promise<{
35
+ ok: boolean;
36
+ approvedSpecs: string[];
37
+ alreadySealed: string[];
38
+ refused: {
39
+ id: string;
40
+ reason: string;
41
+ }[];
42
+ }>;
43
+ spec_remediate(a: {
44
+ root?: string;
45
+ targetFile?: string;
46
+ aspecId?: string;
47
+ }): Promise<{
48
+ ok: boolean;
49
+ actionsTaken: string[];
50
+ refused: {
51
+ id: string;
52
+ reason: string;
53
+ }[];
54
+ }>;
55
+ };
@@ -0,0 +1,324 @@
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.createSliceOrchestrationHandlers = createSliceOrchestrationHandlers;
37
+ // @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
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const spec_store_1 = require("../../spec/spec-store");
41
+ const root_1 = require("../../project/root");
42
+ const yaml_scalar_1 = require("../../spec/yaml-scalar");
43
+ const draft_1 = require("../../reverse/draft");
44
+ const anchor_comment_1 = require("../../rtm/anchor-comment");
45
+ function createSliceOrchestrationHandlers(context) {
46
+ return {
47
+ async spec_slice_init(a) {
48
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
49
+ const specsDir = path.join(root, '.ax', 'specs');
50
+ const existing = await context.listSpecs();
51
+ // @implements A-SPEC-546.1 — align with the sibling reverse-draft path (nextIdBase, floor 100)
52
+ // instead of a hard-coded 200 floor, so a fresh project starts at REQ-100, not REQ-201. Existing
53
+ // projects are unchanged: nextIdBase returns max(existing)+1, which dominates the floor.
54
+ const nextId = (0, draft_1.nextIdBase)(existing.map((s) => s.id));
55
+ const reqId = `REQ-${nextId}`;
56
+ const hspecId = `H-SPEC-${nextId}`;
57
+ const aspecId = `A-SPEC-${nextId}`;
58
+ const tspecId = `T-SPEC-${nextId}`;
59
+ const reqContent = `---
60
+ source:
61
+ - kind: user-request
62
+ ref: ${(0, yaml_scalar_1.yamlScalar)(a.title, 4)}
63
+ retrieved: ${new Date().toISOString().split('T')[0]}
64
+ note: ${(0, yaml_scalar_1.yamlScalar)(a.objective, 4)}
65
+ created: ${new Date().toISOString()}
66
+ id: ${reqId}
67
+ type: REQ
68
+ title: ${(0, yaml_scalar_1.yamlScalar)(a.title)}
69
+ status: draft
70
+ depends_on: []
71
+ ---
72
+
73
+ ## Problem / Need
74
+ ${a.objective}
75
+
76
+ ## Desired Outcome
77
+ ${a.objective}
78
+
79
+ ## Constraints
80
+ - Standard project governance rules.
81
+
82
+ ## Success Criteria
83
+
84
+ ## Out of Scope
85
+ - Unrelated feature changes.
86
+ `;
87
+ const hspecContent = `---
88
+ created: ${new Date().toISOString()}
89
+ id: ${hspecId}
90
+ type: H-SPEC
91
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Functional Specification for ${a.title}`)}
92
+ status: draft
93
+ req_type: functional
94
+ owner: me
95
+ depends_on:
96
+ - ${reqId}
97
+ ---
98
+
99
+ ## Intent
100
+ Implement ${a.title}.
101
+
102
+ ## Scope (In / Out)
103
+ In Scope: ${a.title}.
104
+ Out of Scope: None.
105
+
106
+ ## Design Overview
107
+ High level design for ${a.title}.
108
+
109
+ ## Interfaces / Contracts
110
+ - Target files: ${a.filesToTouch.join(', ')}
111
+
112
+ ## Acceptance Criteria
113
+ - Code written and tests passing.
114
+
115
+ ## Non-Functional
116
+ - Performance and stability maintained.
117
+
118
+ ## Assumptions
119
+ - Environment configured properly.
120
+
121
+ ## Open Questions
122
+ - None.
123
+ `;
124
+ const aspecContent = `---
125
+ created: ${new Date().toISOString()}
126
+ id: ${aspecId}
127
+ type: A-SPEC
128
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Architecture Specification for ${a.title}`)}
129
+ status: draft
130
+ slice: ${(0, yaml_scalar_1.yamlScalar)(a.sliceName)}
131
+ priority: P1
132
+ independent_test: true
133
+ depends_on:
134
+ - ${hspecId}
135
+ breaking_change: 'none'
136
+ harness_impact: 'none: TODO — 3하네스(claude/codex/agy) 영향 검토 후 기술'
137
+ os_impact: 'none: TODO — 3OS(windows/mac/linux) 영향 검토 후 기술'
138
+ ---
139
+
140
+ ## Objective
141
+ ${a.objective}
142
+
143
+ ## Component Design
144
+ 1. Target Component:
145
+ - Modifies ${a.filesToTouch.join(', ')}.
146
+
147
+ ## Inputs / Outputs
148
+ - Inputs: Tool calls / developer modifications.
149
+ - Outputs: Working implementation.
150
+
151
+ ## Behavior
152
+ - Implements desired behavior cleanly.
153
+
154
+ ## Test Points
155
+ - Unit tests in test suite.
156
+
157
+ ## Files to Touch
158
+ ${a.filesToTouch.map((f) => `- ${f}`).join('\n')}
159
+
160
+ ## Done When
161
+ - All tests pass 100%.
162
+ `;
163
+ const tspecContent = `---
164
+ coverage:
165
+ normal: true
166
+ corner: true
167
+ negative: true
168
+ boundary: true
169
+ id: ${tspecId}
170
+ type: T-SPEC
171
+ title: ${(0, yaml_scalar_1.yamlScalar)(`Test Specification for ${a.title}`)}
172
+ status: draft
173
+ depends_on:
174
+ - ${aspecId}
175
+ ---
176
+
177
+ ## Normal Cases
178
+ - Given valid inputs
179
+ When operation is performed
180
+ Then correct behavior is observed
181
+
182
+ ## Corner Cases
183
+ - Given edge cases
184
+ When operation is performed
185
+ Then system handles gracefully
186
+
187
+ ## Negative Cases
188
+ - Given invalid inputs
189
+ When operation is performed
190
+ Then appropriate error is returned
191
+
192
+ ## Boundary Cases
193
+ - Given boundary conditions
194
+ When operation is performed
195
+ Then boundary limits are respected
196
+ `;
197
+ fs.mkdirSync(path.join(specsDir, '01_req'), { recursive: true });
198
+ fs.mkdirSync(path.join(specsDir, '02_h-spec', 'functional'), { recursive: true });
199
+ fs.mkdirSync(path.join(specsDir, '03_a-spec'), { recursive: true });
200
+ fs.mkdirSync(path.join(specsDir, '05_t-spec'), { recursive: true });
201
+ fs.writeFileSync(path.join(specsDir, '01_req', `${reqId}.md`), reqContent);
202
+ fs.writeFileSync(path.join(specsDir, '02_h-spec', 'functional', `${hspecId}.md`), hspecContent);
203
+ fs.writeFileSync(path.join(specsDir, '03_a-spec', `${aspecId}.md`), aspecContent);
204
+ fs.writeFileSync(path.join(specsDir, '05_t-spec', `${tspecId}.md`), tspecContent);
205
+ // @implements A-SPEC-299
206
+ // Read back what was just written. S-298 fixed one cause of unparseable frontmatter; this
207
+ // closes the class. `list()` silently skips documents it cannot parse, so "created" and
208
+ // "present in the store" can diverge without anything throwing — which is exactly how a
209
+ // requirement went missing on 2026-08-28 while the tool reported four specs created.
210
+ const specsCreated = [reqId, hspecId, aspecId, tspecId];
211
+ const unreadable = context.unreadableAmong(await new spec_store_1.LocalMarkdownRepository(specsDir).list(), specsCreated);
212
+ // The files stay on disk on purpose: deleting the evidence turns a diagnosable bug into a
213
+ // mystery, and a half-written slice is something a person should see.
214
+ return { ok: unreadable.length === 0, specsCreated, unreadable };
215
+ },
216
+ async spec_slice_approve(a) {
217
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
218
+ const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
219
+ const specs = await rawStore.list();
220
+ const inSlice = specs.filter((s) => {
221
+ if (s.frontmatter?.slice === a.sliceName)
222
+ return true;
223
+ if (s.id === a.sliceName)
224
+ return true;
225
+ return false;
226
+ });
227
+ const idsToApprove = [];
228
+ if (inSlice.length > 0) {
229
+ const aspec = inSlice.find((s) => s.type === 'A-SPEC') ?? inSlice[0];
230
+ const mainId = (id) => id.split('.')[0].replace(/\D/g, '');
231
+ const req = specs.find((s) => aspec.dependsOn.includes(s.id) || mainId(s.id) === mainId(aspec.id));
232
+ const hspec = specs.find((s) => s.type === 'H-SPEC' && (s.dependsOn.includes(req?.id ?? '') || mainId(s.id) === mainId(aspec.id)));
233
+ const tspec = specs.find((s) => s.type === 'T-SPEC' && (s.dependsOn.includes(aspec.id) || mainId(s.id) === mainId(aspec.id)));
234
+ if (req)
235
+ idsToApprove.push(req.id);
236
+ if (hspec)
237
+ idsToApprove.push(hspec.id);
238
+ idsToApprove.push(aspec.id);
239
+ if (tspec)
240
+ idsToApprove.push(tspec.id);
241
+ }
242
+ else {
243
+ const matches = specs.filter((s) => s.id.includes(a.sliceName));
244
+ idsToApprove.push(...matches.map((s) => s.id));
245
+ }
246
+ const rawHandlers = context.rawHandlersForStore(rawStore);
247
+ const approvedSpecs = [];
248
+ const alreadySealed = [];
249
+ const refused = [];
250
+ if (idsToApprove.length === 0) {
251
+ // Reporting this as a success is how a caller ends up believing an unapproved slice was
252
+ // approved. Measured 2026-08-28: a broken-frontmatter REQ made the whole slice invisible
253
+ // here, and the tool answered `ok: true, approvedSpecs: []`.
254
+ return { ok: false, approvedSpecs, alreadySealed, refused: [{ id: a.sliceName, reason: 'no specs matched this slice name' }] };
255
+ }
256
+ // @implements A-SPEC-587.2 — a sealed spec is SKIPPED, not re-sealed: re-approving asked the
257
+ // human the same question again (same request id back in the queue) and stopped the chain
258
+ // there, so the H-SPEC was never even attempted (measured 2026-09-10, three slices).
259
+ const sealed = (id) => {
260
+ const s = specs.find((x) => x.id === id);
261
+ return s?.status === 'approved' && typeof s.frontmatter?.approved_digest === 'string';
262
+ };
263
+ for (const id of idsToApprove) {
264
+ if (sealed(id)) {
265
+ alreadySealed.push(id);
266
+ continue;
267
+ }
268
+ const res = (await rawHandlers.spec_approve({ root, id }));
269
+ if (res.approved || res.ok) {
270
+ approvedSpecs.push(id);
271
+ continue;
272
+ }
273
+ const findings = Array.isArray(res.findings)
274
+ ? res.findings.map((f) => f.message).filter(Boolean).join('; ')
275
+ : '';
276
+ refused.push({ id, reason: findings ? `${res.reason ?? 'refused'}: ${findings}` : (res.reason ?? 'refused') });
277
+ // The chain is topological: approving a child whose parent was refused would either fail
278
+ // again or, worse, succeed against an unsealed parent. Stop and say what is left.
279
+ break;
280
+ }
281
+ for (const id of idsToApprove.slice(approvedSpecs.length + alreadySealed.length + refused.length)) {
282
+ refused.push({ id, reason: 'not attempted — an earlier spec in the chain was refused' });
283
+ }
284
+ // `ok` means the slice is approved — freshly or already. Anything less is not a success.
285
+ return { ok: refused.length === 0 && approvedSpecs.length + alreadySealed.length > 0, approvedSpecs, alreadySealed, refused };
286
+ },
287
+ async spec_remediate(a) {
288
+ const root = a.root ? (0, root_1.resolveProjectRoot)(a.root).root : process.cwd();
289
+ const actionsTaken = [];
290
+ if (a.targetFile && a.aspecId) {
291
+ const fullPath = path.isAbsolute(a.targetFile) ? a.targetFile : path.join(root, a.targetFile);
292
+ if (fs.existsSync(fullPath)) {
293
+ // @implements A-SPEC-501.1 — BUG-4 (dogfooded twice): the old unconditional
294
+ // `// @implements` broke Python/YAML targets outright AND produced anchors the scanner
295
+ // could not read. The syntax comes from the one map the scanner is aligned with, an
296
+ // unknown syntax is an honest refusal, and the containment check is form-agnostic so a
297
+ // `#` anchor is not double-injected.
298
+ const anchorTag = (0, anchor_comment_1.anchorLineFor)(fullPath, a.aspecId);
299
+ const content = fs.readFileSync(fullPath, 'utf8');
300
+ if (anchorTag === null) {
301
+ actionsTaken.push(`앵커 주입 거부: ${path.basename(fullPath)} — 이 확장자의 주석 문법을 모릅니다. 파일 관례에 맞는 주석으로 1행에 '@implements ${a.aspecId}'를 직접 추가하십시오`);
302
+ }
303
+ else if (!content.includes(`@implements ${a.aspecId}`)) {
304
+ fs.writeFileSync(fullPath, `${anchorTag}\n${content}`);
305
+ actionsTaken.push(`Injected ${anchorTag} on line 1 of ${a.targetFile}`);
306
+ }
307
+ }
308
+ }
309
+ const rawStore = new spec_store_1.LocalMarkdownRepository(path.join(root, '.ax', 'specs'));
310
+ const rawHandlers = context.rawHandlersForStore(rawStore);
311
+ const approveRes = await rawHandlers.spec_slice_approve({ root, sliceName: a.aspecId ?? 'slice' });
312
+ if (approveRes.approvedSpecs?.length > 0) {
313
+ actionsTaken.push(`Approved slice specs: ${approveRes.approvedSpecs.join(', ')}`);
314
+ }
315
+ // Same false-PASS as spec_slice_approve had, one layer up: this handler's whole purpose is to
316
+ // approve, so reporting success when approval was refused sends the caller into code with an
317
+ // unapproved slice behind it.
318
+ const refused = approveRes.refused ?? [];
319
+ for (const r of refused)
320
+ actionsTaken.push(`Refused ${r.id}: ${r.reason}`);
321
+ return { ok: refused.length === 0, actionsTaken, refused };
322
+ },
323
+ };
324
+ }