@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.
- package/CHANGELOG.md +143 -0
- package/README.md +9 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +91 -13
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +121 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +12 -1
- package/dist/holmes/project/root.js +22 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +136 -5
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/rtm/test-scope.js +4 -1
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- 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
|
+
}
|