@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,370 @@
|
|
|
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.createReviewEvidenceHandlers = createReviewEvidenceHandlers;
|
|
37
|
+
// @implements A-SPEC-617, A-SPEC-100.2, A-SPEC-102.1, A-SPEC-133, A-SPEC-135, A-SPEC-141, A-SPEC-157, A-SPEC-160, A-SPEC-189, A-SPEC-191, A-SPEC-244, A-SPEC-245, A-SPEC-263.1, A-SPEC-497.1
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const findings_1 = require("../../review/findings");
|
|
40
|
+
const review_targets_1 = require("../../review/review-targets");
|
|
41
|
+
const elicit_approval_1 = require("../elicit-approval");
|
|
42
|
+
const basis_1 = require("../basis");
|
|
43
|
+
const ledger_store_1 = require("../../governance/ledger-store");
|
|
44
|
+
const ledger_lock_1 = require("../../governance/ledger-lock");
|
|
45
|
+
const approval_grants_1 = require("../../governance/approval-grants");
|
|
46
|
+
const provenance_chain_1 = require("../../governance/provenance-chain");
|
|
47
|
+
function createReviewEvidenceHandlers(context) {
|
|
48
|
+
return {
|
|
49
|
+
async review_record(a) {
|
|
50
|
+
// @implements A-SPEC-189 §8 (round 11) — §15 shut the door the merge gate READS through and
|
|
51
|
+
// left the one it is WRITTEN through wide open. Measured: `review_record` joined the caller's
|
|
52
|
+
// `root` straight into a path and created `.ax/ledger/findings.jsonl` in any directory on the
|
|
53
|
+
// machine, answering `{recorded: N}` — while `review_status`, bound to this server's project,
|
|
54
|
+
// could never see those findings. An open critical filed at the wrong root is worse than none:
|
|
55
|
+
// it reads as recorded and gates nothing. The two doors now ask the same question.
|
|
56
|
+
const foreignWrite = context.foreignRootReason(a.root);
|
|
57
|
+
if (foreignWrite !== null)
|
|
58
|
+
throw context.refusal(`review_record: ${foreignWrite}`);
|
|
59
|
+
// MCP input is unvalidated JSON: an out-of-enum severity or status
|
|
60
|
+
// (e.g. a typo like "blocker") would land in the ledger as a stray
|
|
61
|
+
// value that review_status's counts never match, so it counts toward
|
|
62
|
+
// neither its own bucket nor `blocked` — a mislabeled critical could
|
|
63
|
+
// silently pass the gate. Validate up front so the gate's counts stay
|
|
64
|
+
// trustworthy.
|
|
65
|
+
const validSeverities = new Set(['critical', 'important', 'minor']);
|
|
66
|
+
const validStatuses = new Set(['open', 'resolved']);
|
|
67
|
+
for (const f of a.findings) {
|
|
68
|
+
if (!validSeverities.has(f.severity)) {
|
|
69
|
+
throw context.refusal(`review_record: finding ${f.id} has invalid severity ${JSON.stringify(f.severity)} (must be one of critical|important|minor)`);
|
|
70
|
+
}
|
|
71
|
+
if (!validStatuses.has(f.status)) {
|
|
72
|
+
throw context.refusal(`review_record: finding ${f.id} has invalid status ${JSON.stringify(f.status)} (must be one of open|resolved)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// @implements A-SPEC-191 (§4e) — the ledger's meaning is guarded at RECORD time, where the
|
|
76
|
+
// collision actually happens. Measured: conventional ids (C1…) recur across review rounds, so
|
|
77
|
+
// one round's resolve of "C1" silently lifted the gate over ANOTHER round's unfixed critical;
|
|
78
|
+
// and a severity-downgrade re-open cleared ART-7 with no resolution at all. Both die at one
|
|
79
|
+
// door: an id whose latest state is open cannot be opened again — resolve first, then
|
|
80
|
+
// re-record. Lifting an open CRITICAL requires the out-of-band approval channel (ART-5's
|
|
81
|
+
// principle): the blocked party must not be able to pull its own teeth in-band. A resolved
|
|
82
|
+
// record for an id never opened lifts nothing and stays approval-free (found-and-fixed audit
|
|
83
|
+
// records keep working).
|
|
84
|
+
// The guard and the append hold ONE lock (round-6: the read→judge→append section was a
|
|
85
|
+
// cross-process TOCTOU — two sessions both recorded open under the same id, silencing an
|
|
86
|
+
// open critical at both readers; consumeNonceExclusively already earned this discipline).
|
|
87
|
+
const findingsFile = context.boundFindingsLedger(a.root);
|
|
88
|
+
// @implements A-SPEC-263.1 — elicitation happens BEFORE the lock: the human may take up to
|
|
89
|
+
// the wiring's timeout to answer, and holding the findings ledger lock for that long starves
|
|
90
|
+
// every other writer (the lock callback is synchronous by design). This is a best-effort
|
|
91
|
+
// PRE-SCAN over an unlocked snapshot: for each finding that LOOKS like an open-critical lift
|
|
92
|
+
// not covered by env/grant, ask now and carry the grant into the lock as data. TOCTOU folds
|
|
93
|
+
// fail-closed — state moved so the lift is no longer needed → the grant is simply unused;
|
|
94
|
+
// state moved so a lift IS needed that the snapshot missed → no grant → the standard refusal.
|
|
95
|
+
const elicitGrants = new Map();
|
|
96
|
+
// A DENY at pre-scan is carried as data too (round-1): throwing here judged a stale snapshot —
|
|
97
|
+
// a finding concurrently resolved by someone else no longer needs a lift inside the lock, and
|
|
98
|
+
// the human's "no" must not abort a batch that never needed the question. The deny bites only
|
|
99
|
+
// at the in-lock site, and only if the lift is ACTUALLY needed there.
|
|
100
|
+
const elicitDenials = new Map();
|
|
101
|
+
// @implements A-SPEC-497.1 — an expiry is carried as data like a denial: it bites only at the
|
|
102
|
+
// in-lock refusal site, where it prefixes the standard message so the agent can tell the user
|
|
103
|
+
// a session dialog died (instead of the anonymous "no approval" the old null-fold produced).
|
|
104
|
+
const elicitExpiries = new Map();
|
|
105
|
+
if (context.hasElicitor()) {
|
|
106
|
+
try {
|
|
107
|
+
const snapshot = new Map();
|
|
108
|
+
for (const f of new findings_1.FindingsLedger(findingsFile).list())
|
|
109
|
+
snapshot.set(f.id, f);
|
|
110
|
+
for (const f of a.findings) {
|
|
111
|
+
const last = snapshot.get(f.id);
|
|
112
|
+
if (!(f.status === 'resolved' && last?.status === 'open' && last.severity === 'critical'))
|
|
113
|
+
continue;
|
|
114
|
+
const raw0 = process.env.HOLMES_APPROVAL;
|
|
115
|
+
let env0;
|
|
116
|
+
try {
|
|
117
|
+
env0 = raw0 ? JSON.parse(raw0) : undefined;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
env0 = undefined;
|
|
121
|
+
}
|
|
122
|
+
if (context.resolveHandlerApproval(a.root, env0, { kind: 'review-resolve', target: f.id }, new Date().toISOString()) !== undefined)
|
|
123
|
+
continue; // an open door never summons a human
|
|
124
|
+
const out = await context.tryElicit('review-resolve', f.id, `열린 치명 발견 ${f.id} 의 해소 기록`);
|
|
125
|
+
if (out.kind === 'answered' && out.decision.granted)
|
|
126
|
+
elicitGrants.set(f.id, context.elicitApproval(out.decision.reason));
|
|
127
|
+
else if (out.kind === 'answered')
|
|
128
|
+
elicitDenials.set(f.id, out.decision.reason ?? '(사유 없음)');
|
|
129
|
+
else if (out.kind === 'expired')
|
|
130
|
+
elicitExpiries.set(f.id, out.waitedMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// A pre-scan failure is never worse than no channel: fall through with no grants.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
(0, ledger_lock_1.withLedgerLock)(findingsFile, () => {
|
|
138
|
+
const ledger = new findings_1.FindingsLedger(findingsFile);
|
|
139
|
+
const latest = new Map();
|
|
140
|
+
const liftedCriticals = [];
|
|
141
|
+
const envLiftedCriticals = []; // lifts the ENV approval authorized (round-1: nonce charges only these)
|
|
142
|
+
const grantConsumptions = []; // spent AFTER the batch validates (round-2)
|
|
143
|
+
const elicitationLifts = []; // ledgered per lift (round-5: the promised audit trace)
|
|
144
|
+
for (const f of ledger.list())
|
|
145
|
+
latest.set(f.id, f);
|
|
146
|
+
for (const f of a.findings) {
|
|
147
|
+
if (!f.id || !f.id.trim()) {
|
|
148
|
+
throw context.refusal('review_record: finding id 가 비어 있습니다 — 모든 발견은 원장에서 유일하게 식별될 id 가 필요합니다');
|
|
149
|
+
}
|
|
150
|
+
// Round-5: pre-round-4 ads declared `message`, but the ledger whitelist persists only
|
|
151
|
+
// `summary` — clients following the OLD ad lost their text with zero signal. A pointed
|
|
152
|
+
// refusal turns that silent loss into the one-line fix it needs.
|
|
153
|
+
// Round-9: `=== undefined` let `summary: null` through, so the ledger dropped the text
|
|
154
|
+
// and the caller saw success — the very silence this guard exists to end. Absence is
|
|
155
|
+
// null OR undefined here, as everywhere else in this validator's vocabulary.
|
|
156
|
+
if ('message' in f && (f.summary === undefined || f.summary === null)) {
|
|
157
|
+
throw context.refusal(`review_record: finding ${f.id} 의 message 는 폐지된 광고 키입니다 — 원장은 summary 만 보존합니다. 같은 내용을 summary 로 보내십시오`);
|
|
158
|
+
}
|
|
159
|
+
const last = latest.get(f.id);
|
|
160
|
+
if (f.status === 'open' && last?.status === 'open') {
|
|
161
|
+
throw context.refusal(`review_record: id ${f.id} 는 이미 미해소(open) 상태입니다 — 다른 발견이면 새 id 를 쓰고, 같은 발견의 갱신이면 먼저 resolved 를 기록한 뒤 재기록하십시오 (id 충돌이 남의 critical 을 침묵시키는 것을 막는 문입니다)`);
|
|
162
|
+
}
|
|
163
|
+
if (f.status === 'resolved' && last?.status === 'open' && last.severity === 'critical') {
|
|
164
|
+
// @implements A-SPEC-191 §11 — checked BEFORE any approval is resolved or consumed
|
|
165
|
+
// (round-2): with the guard after resolution, a single-use elicitation grant (or grant
|
|
166
|
+
// file) was spent on the FIRST lift, so the second occurrence saw "no approval" and
|
|
167
|
+
// surfaced a bogus set-HOLMES_APPROVAL message plus a queue entry for an act a human
|
|
168
|
+
// already decided. The double-lift is named honestly on EVERY channel, and nothing
|
|
169
|
+
// single-use is touched by a batch that dies here.
|
|
170
|
+
if (liftedCriticals.includes(f.id)) {
|
|
171
|
+
throw context.refusal(`review_record: 한 배치에서 같은 발견(${f.id})의 치명 해소를 두 번 들 수 없습니다 — 승인은 행위마다 필요합니다`);
|
|
172
|
+
}
|
|
173
|
+
const raw = process.env.HOLMES_APPROVAL;
|
|
174
|
+
let approval;
|
|
175
|
+
try {
|
|
176
|
+
approval = raw ? JSON.parse(raw) : undefined;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
approval = undefined;
|
|
180
|
+
}
|
|
181
|
+
// covers, not merely well-formed (round-2): A-SPEC-133 built the seam so one token is
|
|
182
|
+
// not a master key — an EXPIRED or elsewhere-scoped approval must not lift a critical.
|
|
183
|
+
// An unscoped {actor,token,rationale} stays the operator's session key (unchanged).
|
|
184
|
+
const nowTs = new Date().toISOString();
|
|
185
|
+
let rrResolved = context.resolveHandlerApproval(a.root, approval, { kind: 'review-resolve', target: f.id }, nowTs);
|
|
186
|
+
// @implements A-SPEC-263.1 — the second (and last) elicitable kind. The HUMAN was asked
|
|
187
|
+
// BEFORE the lock (pre-scan above — a lock must not wait on a person); in here the grant
|
|
188
|
+
// is plain data, consumed synchronously and at most once per id.
|
|
189
|
+
if (rrResolved === undefined) {
|
|
190
|
+
const g = elicitGrants.get(f.id);
|
|
191
|
+
if (g) {
|
|
192
|
+
elicitGrants.delete(f.id);
|
|
193
|
+
rrResolved = { approval: g, source: 'elicitation' };
|
|
194
|
+
elicitationLifts.push({ id: f.id, approval: g });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (rrResolved === undefined) {
|
|
198
|
+
// A pre-scan DENY surfaces HERE — only when the lift is genuinely needed at lock time
|
|
199
|
+
// (round-1). The human decided, so the answer is the message and nothing is queued.
|
|
200
|
+
const denied = elicitDenials.get(f.id);
|
|
201
|
+
if (denied !== undefined) {
|
|
202
|
+
throw context.refusal(`review_record: 세션에서 거부됨 — ${denied}. 사유를 해소한 뒤 다시 기록하십시오.`);
|
|
203
|
+
}
|
|
204
|
+
// @implements A-SPEC-497.1 — an expired dialog leads the refusal by name; silence
|
|
205
|
+
// keeps the pre-elicitation face.
|
|
206
|
+
const expiredMs = elicitExpiries.get(f.id);
|
|
207
|
+
throw context.refusal((expiredMs !== undefined ? (0, elicit_approval_1.expiredNotice)(expiredMs) : '')
|
|
208
|
+
+ `review_record: id ${f.id} 의 열린 치명 발견을 해소하는 기록은 이 행위를 덮는 유효한 대역외 승인이 필요합니다 — 차단당한 쪽이 스스로 이빨을 뽑을 수 없어야 하고, 만료·다른 범위의 승인은 덮지 않습니다. HOLMES_APPROVAL='{"actor":"<you>","token":"<any>","rationale":"<why fixed>"}' (범위를 쓰면 kind "review-resolve") 를 서버 환경에 설정하고 다시 기록하십시오`
|
|
209
|
+
+ context.refusalQueueHint(a.root, { kind: 'review-resolve', target: f.id, why: '열린 치명 발견의 해소 기록' }));
|
|
210
|
+
}
|
|
211
|
+
// Grant-file consumption is DEFERRED past the loop (round-2): consuming here burned the
|
|
212
|
+
// single-use file when a LATER finding in the batch failed validation — nothing was
|
|
213
|
+
// recorded, yet the operator's legitimate grant was gone and the retry refused. Same
|
|
214
|
+
// harm class the env nonce fixed in its round-4; collected now, spent only once the
|
|
215
|
+
// whole batch has validated (directly before the append, like the nonce block).
|
|
216
|
+
if (rrResolved.source === 'grant' && rrResolved.root && rrResolved.approval.nonce) {
|
|
217
|
+
grantConsumptions.push({ root: rrResolved.root, nonce: rrResolved.approval.nonce });
|
|
218
|
+
}
|
|
219
|
+
liftedCriticals.push(f.id);
|
|
220
|
+
// The nonce block below enforces single-use on the ENV approval — so it must key on the
|
|
221
|
+
// lifts the ENV approval actually authorized (round-1): before elicitation existed,
|
|
222
|
+
// reaching it implied env/grant coverage, but an elicitation-authorized lift flows past
|
|
223
|
+
// it with a possibly unrelated env token in the environment. Charging THAT token burned
|
|
224
|
+
// an innocent nonce, misattributed the ledger line, and a stale env nonce made a
|
|
225
|
+
// human-approved lift permanently refusable.
|
|
226
|
+
if (rrResolved.source === 'env')
|
|
227
|
+
envLiftedCriticals.push(f.id);
|
|
228
|
+
}
|
|
229
|
+
latest.set(f.id, f); // 한 배치 안의 순서도 기록 순서다
|
|
230
|
+
}
|
|
231
|
+
// A-SPEC-133 promises nonce = single-use "consumed on the ledger, a replay is denied".
|
|
232
|
+
// Consumed AFTER the whole batch validates and ONCE per call (round-4: consuming inside
|
|
233
|
+
// the loop burned the grant when a LATER row failed validation — the audit trail asserted
|
|
234
|
+
// an act that never happened, the retry was refused, and two resolves in one batch would
|
|
235
|
+
// have double-spent). Consumption directly precedes the append; the only failure between
|
|
236
|
+
// them is the append itself, which throws loudly.
|
|
237
|
+
// ONE NONCE, ONE ACT — on the grant channel too (round-4): the round-2 deferral removed the
|
|
238
|
+
// arity the in-loop spend had accidentally enforced, so a wide-scoped single-use grant
|
|
239
|
+
// backed N lifts in one batch while the same lifts split across calls refused after the
|
|
240
|
+
// first (the §13 batch-shape dependence). Checked BEFORE anything is spent (round-3 order
|
|
241
|
+
// doctrine: every throwable validation precedes every single-use spend).
|
|
242
|
+
{
|
|
243
|
+
const liftsPerNonce = new Map();
|
|
244
|
+
for (const g of grantConsumptions)
|
|
245
|
+
liftsPerNonce.set(g.nonce, (liftsPerNonce.get(g.nonce) ?? 0) + 1);
|
|
246
|
+
for (const [, n] of liftsPerNonce) {
|
|
247
|
+
if (n > 1) {
|
|
248
|
+
throw context.refusal(`review_record: 단일 사용 승인(grant)은 한 건의 치명 해소만 authorize 합니다 — 이 배치는 한 grant 로 ${n}건을 듭니다. 한 건씩 보내십시오`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// The elicitation channel's AUDIT TRACE (round-5, repositioned round-6): the trust model
|
|
253
|
+
// admits review-resolve at a boundary closer than env/grant BECAUSE every grant is ledgered
|
|
254
|
+
// under an `elicitation:<client>` actor. This append is throwable I/O (chain lock
|
|
255
|
+
// contention, disk faults), so it stands BEFORE every single-use spend — round-5 placed it
|
|
256
|
+
// between the env-nonce spend and the findings append, and a lock-contended mixed batch
|
|
257
|
+
// burned the operator's env nonce with nothing recorded (the §7 doctrine regression). A
|
|
258
|
+
// failure HERE burns nothing; an orphaned event left when a LATER step throws stays honest,
|
|
259
|
+
// because it records the AUTHORIZATION, which already happened at pre-scan.
|
|
260
|
+
if (elicitationLifts.length > 0) {
|
|
261
|
+
const chain = new ledger_store_1.FileLedgerStore(path.join(context.projectRootOf(a.root), '.ax', 'ledger'));
|
|
262
|
+
for (const lift of elicitationLifts) {
|
|
263
|
+
chain.append({
|
|
264
|
+
ts: new Date().toISOString(), actor: lift.approval.actor, kind: 'review-resolve-authorized',
|
|
265
|
+
summary: `elicitation grant authorized lifting open critical ${lift.id}`.slice(0, 200),
|
|
266
|
+
inputs: [lift.id], rationale: lift.approval.rationale,
|
|
267
|
+
authorization: (0, provenance_chain_1.authorizationRef)(lift.approval.actor, lift.approval.token),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Keyed on the lifts the ENV approval AUTHORIZED, not on the ambient env var (round-1):
|
|
272
|
+
// an elicitation- or grant-authorized lift must not charge — or be blocked by — an env
|
|
273
|
+
// token that never covered the act. When env authorized nothing, its nonce is untouched.
|
|
274
|
+
if (envLiftedCriticals.length > 0) {
|
|
275
|
+
const raw = process.env.HOLMES_APPROVAL;
|
|
276
|
+
let approval;
|
|
277
|
+
try {
|
|
278
|
+
approval = raw ? JSON.parse(raw) : undefined;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
approval = undefined;
|
|
282
|
+
}
|
|
283
|
+
if ((0, provenance_chain_1.blankNonce)(approval?.nonce)) {
|
|
284
|
+
throw context.refusal('review_record: 승인이 단일 사용(nonce)을 선언했으나 값이 비어 있습니다 — 1회성을 집행할 수 없어 거부합니다');
|
|
285
|
+
}
|
|
286
|
+
// @implements A-SPEC-191 §13 — 1회용 승인은 한 번의 행위를 authorize 한다. 9라운드
|
|
287
|
+
// 실측: 서로 다른 id 의 치명 해소 N 건이 한 배치에서 nonce 한 장으로 통과했고,
|
|
288
|
+
// 같은 세 건을 세 호출로 나누면 두 번째부터 거부됐다 — 판정이 묶음 방식에 의존했다.
|
|
289
|
+
if ((0, provenance_chain_1.nonceDeclared)(approval?.nonce) && envLiftedCriticals.length > 1) {
|
|
290
|
+
throw context.refusal(`review_record: 단일 사용 승인(nonce)은 한 건의 치명 해소만 authorize 합니다 — 이 배치는 ${envLiftedCriticals.length}건(${envLiftedCriticals.join(', ')})을 듭니다. 한 건씩 보내십시오`);
|
|
291
|
+
}
|
|
292
|
+
if ((0, provenance_chain_1.nonceDeclared)(approval?.nonce)) {
|
|
293
|
+
// @implements A-SPEC-191 §10 — the same bound anchor as risk_check: two consumers must
|
|
294
|
+
// spend into ONE ledger, or a nonce spent here reopens the gate there (and back).
|
|
295
|
+
const ledgerFile = context.boundNonceLedger();
|
|
296
|
+
if (ledgerFile === null) {
|
|
297
|
+
throw context.refusal('review_record: 단일 사용 승인(nonce)을 기록할 프로젝트 원장을 확정할 수 없습니다 — 1회성을 보장할 수 없어 거부합니다');
|
|
298
|
+
}
|
|
299
|
+
const won = (0, provenance_chain_1.consumeNonceExclusively)(String(approval.nonce), ledgerFile, {
|
|
300
|
+
ts: new Date().toISOString(), actor: approval.actor, kind: 'nonce-consumed',
|
|
301
|
+
summary: `consumed single-use approval for: review-resolve ${envLiftedCriticals.join(', ')}`.slice(0, 200),
|
|
302
|
+
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(approval.nonce))], rationale: approval.rationale,
|
|
303
|
+
authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
|
|
304
|
+
}, { isNonceConsumed: (0, ledger_store_1.nonceConsumedIn)(ledgerFile) });
|
|
305
|
+
if (!won) {
|
|
306
|
+
throw context.refusal(`review_record: 단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 거부됩니다. 새 승인을 발급받으십시오`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Deferred single-use spends — LAST, after every throwable validation including the
|
|
311
|
+
// env-nonce block above (round-3: placed before it, a mixed batch's env-nonce refusal
|
|
312
|
+
// burned the grant with nothing recorded — the exact harm the round-2 deferral was written
|
|
313
|
+
// to fix, reintroduced through the env throw path). Only the append itself follows, and it
|
|
314
|
+
// fails loudly.
|
|
315
|
+
for (const g of grantConsumptions)
|
|
316
|
+
(0, approval_grants_1.consumeGrantFile)(g.root, g.nonce);
|
|
317
|
+
// @implements A-SPEC-157 — the server's OWN observation, not anything the caller sent.
|
|
318
|
+
// @implements A-SPEC-160 — the ONLY caller that reads the on-disk build, so the divergence
|
|
319
|
+
// marker can be sealed. Append INSIDE the same lock as the guard (round-6 TOCTOU).
|
|
320
|
+
ledger.record(a.findings, (0, basis_1.basisDigest)(context.basisFor(a.root, true)));
|
|
321
|
+
});
|
|
322
|
+
return { recorded: a.findings.length };
|
|
323
|
+
},
|
|
324
|
+
async review_status(a) {
|
|
325
|
+
// @implements A-SPEC-191 §15 — the merge gate must not answer about a project it did not read.
|
|
326
|
+
const foreign = context.foreignRootReason(a.root);
|
|
327
|
+
if (foreign !== null)
|
|
328
|
+
throw context.refusal(`review_status: ${foreign}`);
|
|
329
|
+
// @implements A-SPEC-191 (§4f) — counted on the LATEST record per id, the same folding the
|
|
330
|
+
// constitution's ART-7 uses. Line-based counting could never decrease on an append-only
|
|
331
|
+
// ledger, so the canonical exit (a resolved line) left `blocked: true` forever while the Stop
|
|
332
|
+
// gate passed — two official verdicts about one ledger, disagreeing permanently (measured).
|
|
333
|
+
const all = new findings_1.FindingsLedger(context.boundFindingsLedger(a.root)).list();
|
|
334
|
+
const latest = new Map();
|
|
335
|
+
// Severity of the LATEST open row per id — a forever-sticky "ever critical" set resurrected
|
|
336
|
+
// a properly-closed critical when a later minor re-finding got a diverged resolve (r6).
|
|
337
|
+
const lastOpenCritical = new Map();
|
|
338
|
+
for (const f of all) {
|
|
339
|
+
latest.set(f.id, f);
|
|
340
|
+
if (f.status === 'open')
|
|
341
|
+
lastOpenCritical.set(f.id, f.severity === 'critical');
|
|
342
|
+
}
|
|
343
|
+
// SAME ledger meaning as ART-7 (round-3: the two official readers disagreed): a resolution
|
|
344
|
+
// sealed on a diverged build does not close a critical here either. What it keeps open is
|
|
345
|
+
// the CRITICAL it failed to close — bucketing by the resolved line's own severity let a
|
|
346
|
+
// severity-downgraded diverged resolve report blocked:false while the Stop gate blocked
|
|
347
|
+
// (round-4 HIGH), and a severity-less row minted an "undefined" bucket.
|
|
348
|
+
const counts = { critical: 0, important: 0, minor: 0 };
|
|
349
|
+
for (const f of latest.values()) {
|
|
350
|
+
if (f.status === 'open') {
|
|
351
|
+
if (Object.prototype.hasOwnProperty.call(counts, f.severity))
|
|
352
|
+
counts[f.severity]++; // in-체크는 프로토타입 키(toString)로 새었다(r6)
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (f.status === 'resolved' && lastOpenCritical.get(f.id) === true && (0, basis_1.basisDiverged)(f.basis))
|
|
356
|
+
counts.critical++;
|
|
357
|
+
}
|
|
358
|
+
// @implements A-SPEC-135
|
|
359
|
+
// Surface outstanding P4 routing signals: specs whose approved content moved, with the anchored
|
|
360
|
+
// files awaiting targeted review. This is a backlog to route human attention, NOT a hard block —
|
|
361
|
+
// the review layer decides severity — so it does not touch `blocked`.
|
|
362
|
+
// @implements A-SPEC-189 §12 (round 12) — the findings moved onto the bound project in §8 and
|
|
363
|
+
// this line stayed on the caller's raw root, so the SAME project answered with or without a
|
|
364
|
+
// P4 routing backlog depending on how its root was spelled (a subdirectory passes the
|
|
365
|
+
// project-level foreign check). One question, one tree.
|
|
366
|
+
const reviewNeeded = (0, review_targets_1.readReviewNeeded)(path.join(path.dirname(context.boundFindingsLedger(a.root)), 'provenance.jsonl'));
|
|
367
|
+
return { blocked: counts.critical > 0 || counts.important > 0, open: counts, reviewNeeded };
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Spec } from '../../spec/spec-parser';
|
|
2
|
+
import { ScannedFile } from '../../cpg/cpg-scanner';
|
|
3
|
+
import { ChangeSourceInfo } from '../../project/change-source';
|
|
4
|
+
import { ContentSource } from '../../context/bundler';
|
|
5
|
+
export interface ReviewQueryContext {
|
|
6
|
+
listSpecs(): Promise<Spec[]>;
|
|
7
|
+
assertStoreReachable(tool: string, root: unknown): void;
|
|
8
|
+
foreignRootReason(root?: string): string | null;
|
|
9
|
+
refusal(reason: string): Error;
|
|
10
|
+
projectRootOf(root: string): string;
|
|
11
|
+
cachedScan(root: string, repoRoot: string): ScannedFile[];
|
|
12
|
+
buildContentSource(specs: Spec[], scanned: ScannedFile[]): ContentSource;
|
|
13
|
+
deriveChangedContext(root: string, args: {
|
|
14
|
+
base?: string;
|
|
15
|
+
head?: string;
|
|
16
|
+
since?: string;
|
|
17
|
+
}, tool: string): Promise<{
|
|
18
|
+
root: string;
|
|
19
|
+
specs: Spec[];
|
|
20
|
+
scanned: ScannedFile[];
|
|
21
|
+
changedFiles: string[];
|
|
22
|
+
changedSymbols: string[];
|
|
23
|
+
changeSource: ChangeSourceInfo;
|
|
24
|
+
scopeFallback?: 'full';
|
|
25
|
+
anchorImpactedSpecs: string[];
|
|
26
|
+
changedTestFiles: string[];
|
|
27
|
+
unresolvedFiles: string[];
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createReviewQueryHandlers(context: ReviewQueryContext): {
|
|
31
|
+
context_bundle(a: {
|
|
32
|
+
root: string;
|
|
33
|
+
seedId: string;
|
|
34
|
+
budget: number;
|
|
35
|
+
}): Promise<import("../../context/bundler").ContextBundle>;
|
|
36
|
+
review_scope(a: {
|
|
37
|
+
root: string;
|
|
38
|
+
base?: string;
|
|
39
|
+
head?: string;
|
|
40
|
+
since?: string;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
scopeFallback?: "full" | undefined;
|
|
43
|
+
unscannedChangedFiles: string[];
|
|
44
|
+
testScope: import("../../rtm/test-scope").TestScope;
|
|
45
|
+
changeSource: ChangeSourceInfo;
|
|
46
|
+
changedSymbols: string[];
|
|
47
|
+
impactedSpecs: string[];
|
|
48
|
+
acceptanceCriteria: Array<{
|
|
49
|
+
specId: string;
|
|
50
|
+
criteria: string;
|
|
51
|
+
}>;
|
|
52
|
+
unrequestedSymbols: string[];
|
|
53
|
+
coverageGaps: string[];
|
|
54
|
+
obligationGaps: string[];
|
|
55
|
+
}>;
|
|
56
|
+
review_prepare(a: {
|
|
57
|
+
root: string;
|
|
58
|
+
base?: string;
|
|
59
|
+
head?: string;
|
|
60
|
+
since?: string;
|
|
61
|
+
budget?: number;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
scopeFallback?: "full" | undefined;
|
|
64
|
+
package: import("../../review/package").ReviewPackage;
|
|
65
|
+
unscannedChangedFiles: string[];
|
|
66
|
+
changeSource: ChangeSourceInfo;
|
|
67
|
+
}>;
|
|
68
|
+
};
|
|
@@ -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
|
+
};
|