@holmes-lab/holmes-kit 0.19.3 → 0.19.4
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 +137 -0
- package/README.md +22 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +8 -0
- package/dist/holmes/cli/agents.js +26 -2
- package/dist/holmes/cli/codex-toml.d.ts +10 -0
- package/dist/holmes/cli/codex-toml.js +76 -12
- package/dist/holmes/cli/doctor.d.ts +19 -0
- package/dist/holmes/cli/doctor.js +107 -42
- package/dist/holmes/cli/index.js +13 -0
- package/dist/holmes/cli/init.js +10 -3
- package/dist/holmes/cli/native-deps.js +4 -1
- package/dist/holmes/cli/playbook-skills.js +6 -4
- package/dist/holmes/cli/probe-process.d.ts +8 -0
- package/dist/holmes/cli/probe-process.js +73 -0
- package/dist/holmes/cli/spawn-spec.js +3 -1
- package/dist/holmes/cli/test-platform.d.ts +37 -0
- package/dist/holmes/cli/test-platform.js +126 -1
- package/dist/holmes/governance/approval-grants.js +26 -3
- package/dist/holmes/governance/autonomy.d.ts +17 -1
- package/dist/holmes/governance/autonomy.js +37 -5
- package/dist/holmes/mcp/handlers.d.ts +30 -5
- package/dist/holmes/mcp/handlers.js +111 -13
- package/dist/holmes/mcp/spec-id-guard.d.ts +1 -1
- package/dist/holmes/mcp/spec-id-guard.js +9 -13
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/project/install-scripts-policy.d.ts +16 -2
- package/dist/holmes/project/install-scripts-policy.js +16 -2
- package/dist/holmes/review/point-in-time-replay.js +43 -3
- package/dist/holmes/rtm/graph-store.d.ts +2 -0
- package/dist/holmes/rtm/graph-store.js +14 -0
- package/dist/holmes/rtm/rtm-graph.js +42 -30
- package/dist/holmes/semantic/credentials.js +86 -9
- package/dist/holmes/semantic/embedder.js +6 -39
- package/dist/holmes/semantic/local-model.d.ts +30 -0
- package/dist/holmes/semantic/local-model.js +92 -0
- package/dist/holmes/semantic/model-cache.d.ts +8 -0
- package/dist/holmes/semantic/model-cache.js +67 -0
- package/dist/holmes/semantic/tier.d.ts +7 -0
- package/dist/holmes/semantic/tier.js +9 -3
- package/dist/holmes/spec/renumber.d.ts +72 -0
- package/dist/holmes/spec/renumber.js +341 -0
- package/dist/holmes/spec/spec-id.d.ts +9 -0
- package/dist/holmes/spec/spec-id.js +23 -0
- package/docs/install-guide.md +90 -2
- package/package.json +6 -3
- package/scripts/install.ps1 +30 -27
|
@@ -132,11 +132,34 @@ function resolveApproval(root, envApproval, action, now) {
|
|
|
132
132
|
* The nonce is flattened to a basename so a traversal-shaped nonce cannot reach outside the dir.
|
|
133
133
|
*/
|
|
134
134
|
function consumeGrantFile(root, nonce) {
|
|
135
|
+
// @implements A-SPEC-596 — address the grant the SAME way `readGrants` does: by the nonce INSIDE
|
|
136
|
+
// the file, never by a path built from it. Building the path meant a grant whose filename differed
|
|
137
|
+
// from its nonce was honoured (reading scans, and never compares the two) yet never spent, with
|
|
138
|
+
// `force: true` swallowing the miss — a single-use grant that stayed spendable forever. Scanning
|
|
139
|
+
// also makes traversal structurally impossible: no nonce becomes a path, so none can point out.
|
|
140
|
+
const dir = path.join(root, exports.GRANTS_RELDIR);
|
|
141
|
+
const wanted = String(nonce);
|
|
142
|
+
let removed = 0;
|
|
135
143
|
try {
|
|
136
|
-
const name
|
|
137
|
-
|
|
144
|
+
for (const name of fs.readdirSync(dir).filter((n) => n.endsWith('.json'))) {
|
|
145
|
+
const file = path.join(dir, name);
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
148
|
+
if (String(parsed?.nonce) !== wanted)
|
|
149
|
+
continue; // someone else's grant is not ours to spend
|
|
150
|
+
fs.rmSync(file, { force: true });
|
|
151
|
+
removed++;
|
|
152
|
+
}
|
|
153
|
+
catch { /* corrupt or vanished: it was never honoured either, so it is not ours to spend */ }
|
|
154
|
+
}
|
|
138
155
|
}
|
|
139
156
|
catch {
|
|
140
|
-
|
|
157
|
+
// No directory, or it cannot be read: there is nothing to spend, which is an ordinary state.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (removed === 0) {
|
|
161
|
+
// Consumption runs AFTER the act succeeded, so this must never throw — but it must not be silent
|
|
162
|
+
// either: an unspent single-use grant is still spendable, and nobody would know.
|
|
163
|
+
process.stderr.write(`[Holmes-Kit] approval grant was used but no grant file matched it — the grant may still be spendable. Remove it by hand from ${exports.GRANTS_RELDIR}.\n`);
|
|
141
164
|
}
|
|
142
165
|
}
|
|
@@ -9,7 +9,23 @@ export declare function isHighRiskPath(p: string): boolean;
|
|
|
9
9
|
* parent-aware rules (kept in the signature so callers wire it once); the current bound is decided
|
|
10
10
|
* from the spec itself.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @implements A-SPEC-587
|
|
14
|
+
* The A-SPECs whose chain leads up to `spec`: one hop for an H-SPEC (A-SPECs that depend on it),
|
|
15
|
+
* two for a REQ (through its H-SPECs). Anything else has no downstream A-SPECs. Pure; `depends_on`
|
|
16
|
+
* is the only edge this repository's chain uses.
|
|
17
|
+
*/
|
|
18
|
+
export declare function descendantAspecs(spec: Spec, all: Spec[]): Spec[];
|
|
19
|
+
/**
|
|
20
|
+
* @implements A-SPEC-587
|
|
21
|
+
* The autonomy verdict for approving THIS spec. `resolveParent` is accepted for future parent-aware
|
|
22
|
+
* rules. `descendants` — the A-SPECs beneath a REQ/H-SPEC (see `descendantAspecs`) — is what lets an
|
|
23
|
+
* upstream document be graded by the code it will admit: a REQ or H-SPEC carries no code of its
|
|
24
|
+
* own, so its risk IS the risk its A-SPECs declare (REQ-587, the owner's line: only irreversible,
|
|
25
|
+
* architectural or high-risk changes ask a human). Every downstream A-SPEC auto → auto; any hitl,
|
|
26
|
+
* or no A-SPEC at all (unknown scope) → human. The two-argument call keeps the old answer.
|
|
27
|
+
*/
|
|
28
|
+
export declare function specApprovalAutonomy(spec: Spec, resolveParent: (id: string) => Spec | null, descendants?: Spec[]): ApprovalAutonomy;
|
|
13
29
|
export declare const SESSION_AUTONOMY_MARKER: readonly [".ax", "state", "autonomy.json"];
|
|
14
30
|
export declare function sessionAutonomyActive(root: string, now: string): boolean;
|
|
15
31
|
/** Whether autonomous approval is enabled — the out-of-band env switch, OR a valid session envelope
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SESSION_AUTONOMY_MARKER = exports.AUTONOMY_ENV = void 0;
|
|
37
37
|
exports.isHighRiskPath = isHighRiskPath;
|
|
38
|
+
exports.descendantAspecs = descendantAspecs;
|
|
38
39
|
exports.specApprovalAutonomy = specApprovalAutonomy;
|
|
39
40
|
exports.sessionAutonomyActive = sessionAutonomyActive;
|
|
40
41
|
exports.projectDefaultAutonomy = projectDefaultAutonomy;
|
|
@@ -48,8 +49,9 @@ exports.releaseAutonomy = releaseAutonomy;
|
|
|
48
49
|
// the MCP elicitation TUI. The governance chain (REQ→H→A→T→CPG + RTM + taint + phase gate) already
|
|
49
50
|
// forces quality structurally, so a low/mid-risk spec self-approving under an explicitly-enabled
|
|
50
51
|
// autonomous mode is a small, bounded relaxation (REQ-532). The bound is what this module owns: the
|
|
51
|
-
// governance-critical specs — gate behaviour, architecture, taint/security boundaries
|
|
52
|
-
// upstream REQ/H-SPEC
|
|
52
|
+
// governance-critical specs — gate behaviour, architecture (C-SPEC/ADR), taint/security boundaries —
|
|
53
|
+
// never leave the human channel. An upstream REQ/H-SPEC is graded by the A-SPECs beneath it
|
|
54
|
+
// (REQ-587): all auto → auto; any hitl, or none yet → human.
|
|
53
55
|
//
|
|
54
56
|
// PURE: the spec, a parent resolver, and (separately) the env are the only inputs; the wiring layer
|
|
55
57
|
// (A-SPEC-532.2) injects process.env and the elicitor.
|
|
@@ -126,15 +128,45 @@ const AUTO_GRADES = new Set(['none', 'persisted-artifact', 'derived-artifact', '
|
|
|
126
128
|
* parent-aware rules (kept in the signature so callers wire it once); the current bound is decided
|
|
127
129
|
* from the spec itself.
|
|
128
130
|
*/
|
|
129
|
-
|
|
131
|
+
/**
|
|
132
|
+
* @implements A-SPEC-587
|
|
133
|
+
* The A-SPECs whose chain leads up to `spec`: one hop for an H-SPEC (A-SPECs that depend on it),
|
|
134
|
+
* two for a REQ (through its H-SPECs). Anything else has no downstream A-SPECs. Pure; `depends_on`
|
|
135
|
+
* is the only edge this repository's chain uses.
|
|
136
|
+
*/
|
|
137
|
+
function descendantAspecs(spec, all) {
|
|
138
|
+
const dependsOn = (s, id) => Array.isArray(s.dependsOn) && s.dependsOn.includes(id);
|
|
139
|
+
if (spec.type === 'H-SPEC')
|
|
140
|
+
return all.filter((s) => s.type === 'A-SPEC' && dependsOn(s, spec.id));
|
|
141
|
+
if (spec.type === 'REQ') {
|
|
142
|
+
const hs = all.filter((s) => s.type === 'H-SPEC' && dependsOn(s, spec.id));
|
|
143
|
+
return all.filter((s) => s.type === 'A-SPEC' && hs.some((h) => dependsOn(s, h.id)));
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @implements A-SPEC-587
|
|
149
|
+
* The autonomy verdict for approving THIS spec. `resolveParent` is accepted for future parent-aware
|
|
150
|
+
* rules. `descendants` — the A-SPECs beneath a REQ/H-SPEC (see `descendantAspecs`) — is what lets an
|
|
151
|
+
* upstream document be graded by the code it will admit: a REQ or H-SPEC carries no code of its
|
|
152
|
+
* own, so its risk IS the risk its A-SPECs declare (REQ-587, the owner's line: only irreversible,
|
|
153
|
+
* architectural or high-risk changes ask a human). Every downstream A-SPEC auto → auto; any hitl,
|
|
154
|
+
* or no A-SPEC at all (unknown scope) → human. The two-argument call keeps the old answer.
|
|
155
|
+
*/
|
|
156
|
+
function specApprovalAutonomy(spec, resolveParent, descendants) {
|
|
130
157
|
switch (spec.type) {
|
|
131
158
|
case 'T-SPEC':
|
|
132
159
|
return 'auto'; // tests are low risk
|
|
133
160
|
case 'REQ':
|
|
134
|
-
case 'H-SPEC':
|
|
161
|
+
case 'H-SPEC': {
|
|
162
|
+
const below = (descendants ?? []).filter((s) => s.type === 'A-SPEC');
|
|
163
|
+
if (below.length === 0)
|
|
164
|
+
return 'hitl'; // unknown scope stays human
|
|
165
|
+
return below.every((a) => specApprovalAutonomy(a, resolveParent) === 'auto') ? 'auto' : 'hitl';
|
|
166
|
+
}
|
|
135
167
|
case 'C-SPEC':
|
|
136
168
|
case 'ADR': // @implements A-SPEC-571.1 — a decision is the human's to seal
|
|
137
|
-
return 'hitl'; //
|
|
169
|
+
return 'hitl'; // architecture / structural constraint — the owner's exception
|
|
138
170
|
case 'A-SPEC': {
|
|
139
171
|
const grade = breakingGrade(spec);
|
|
140
172
|
if (grade === null || !AUTO_GRADES.has(grade))
|
|
@@ -183,6 +183,30 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
183
183
|
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
184
184
|
* drift detection compares against — would silently go stale.
|
|
185
185
|
*/
|
|
186
|
+
spec_renumber(a: {
|
|
187
|
+
root?: string;
|
|
188
|
+
oldBase: string;
|
|
189
|
+
newBase: string;
|
|
190
|
+
dryRun?: boolean;
|
|
191
|
+
}): Promise<{
|
|
192
|
+
ok: boolean;
|
|
193
|
+
reason: string;
|
|
194
|
+
dryRun?: undefined;
|
|
195
|
+
plan?: undefined;
|
|
196
|
+
movedSpecs?: undefined;
|
|
197
|
+
} | {
|
|
198
|
+
ok: boolean;
|
|
199
|
+
dryRun: boolean;
|
|
200
|
+
plan: import("../spec/renumber").RenumberPlan;
|
|
201
|
+
reason?: undefined;
|
|
202
|
+
movedSpecs?: undefined;
|
|
203
|
+
} | {
|
|
204
|
+
ok: boolean;
|
|
205
|
+
dryRun: boolean;
|
|
206
|
+
movedSpecs: number;
|
|
207
|
+
plan: import("../spec/renumber").RenumberPlan;
|
|
208
|
+
reason?: undefined;
|
|
209
|
+
}>;
|
|
186
210
|
spec_unseal(a: {
|
|
187
211
|
root?: string;
|
|
188
212
|
id: string;
|
|
@@ -579,15 +603,15 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
579
603
|
head?: string;
|
|
580
604
|
since?: string;
|
|
581
605
|
}): Promise<{
|
|
582
|
-
semanticWarm?: {
|
|
583
|
-
tier: string;
|
|
584
|
-
computed: number;
|
|
585
|
-
cached: number;
|
|
586
|
-
} | undefined;
|
|
587
606
|
changed: number;
|
|
588
607
|
nodes: number;
|
|
589
608
|
edges: number;
|
|
590
609
|
changeSource: ChangeSourceInfo;
|
|
610
|
+
semanticWarm?: {
|
|
611
|
+
tier: string;
|
|
612
|
+
computed: number;
|
|
613
|
+
cached: number;
|
|
614
|
+
};
|
|
591
615
|
}>;
|
|
592
616
|
context_bundle(a: {
|
|
593
617
|
root: string;
|
|
@@ -779,6 +803,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
779
803
|
}): Promise<{
|
|
780
804
|
ok: boolean;
|
|
781
805
|
approvedSpecs: string[];
|
|
806
|
+
alreadySealed: string[];
|
|
782
807
|
refused: {
|
|
783
808
|
id: string;
|
|
784
809
|
reason: string;
|
|
@@ -38,6 +38,7 @@ exports.isHandlerRefusal = isHandlerRefusal;
|
|
|
38
38
|
exports.unreadableAmong = unreadableAmong;
|
|
39
39
|
exports.makeHandlers = makeHandlers;
|
|
40
40
|
exports.collectDecisions = collectDecisions;
|
|
41
|
+
// @implements A-SPEC-599
|
|
41
42
|
// @implements A-SPEC-293
|
|
42
43
|
// @implements A-SPEC-292
|
|
43
44
|
// @implements A-SPEC-290
|
|
@@ -255,6 +256,7 @@ const ignore_1 = require("../project/ignore");
|
|
|
255
256
|
// @implements A-SPEC-126
|
|
256
257
|
const scan_1 = require("../reverse/scan");
|
|
257
258
|
const draft_1 = require("../reverse/draft");
|
|
259
|
+
const renumber_1 = require("../spec/renumber");
|
|
258
260
|
const adr_refs_1 = require("../spec/adr-refs");
|
|
259
261
|
const spec_id_guard_1 = require("./spec-id-guard");
|
|
260
262
|
const anchor_1 = require("../reverse/anchor");
|
|
@@ -376,7 +378,7 @@ function specStoreBlindReason(store, root, loaded) {
|
|
|
376
378
|
// Compared through realpath: on macOS a temp root is `/var/...` while the same directory resolves
|
|
377
379
|
// to `/private/var/...`, and a plain string compare calls the correctly-bound store foreign.
|
|
378
380
|
const real = (p) => { try {
|
|
379
|
-
return
|
|
381
|
+
return (0, root_2.canonicalPath)(p);
|
|
380
382
|
}
|
|
381
383
|
catch {
|
|
382
384
|
return path.resolve(p);
|
|
@@ -779,10 +781,11 @@ function makeRawHandlers(store, opts) {
|
|
|
779
781
|
// rides the existing seal path, but its actor names `autonomous:<client>` so an audit can tell a
|
|
780
782
|
// self-approved seal from a human-approved (elicitation) or operator (env/grant) one. Single-use
|
|
781
783
|
// by construction — it exists only inside this call, persisted nowhere.
|
|
782
|
-
const autonomousApproval = () => ({
|
|
784
|
+
const autonomousApproval = (derivedFrom) => ({
|
|
783
785
|
actor: `autonomous:${opts?.clientName?.() ?? 'unknown'}`,
|
|
784
786
|
token: crypto.randomUUID(),
|
|
785
|
-
|
|
787
|
+
// @implements A-SPEC-587 — an upstream grade is DERIVED; the audit line names the A-SPECs it came from.
|
|
788
|
+
rationale: `autonomous grant (${'HOLMES_AUTONOMOUS_APPROVAL'} enabled, spec grade auto${derivedFrom && derivedFrom.length > 0 ? ` — derived from ${derivedFrom.join(', ')}` : ''})`,
|
|
786
789
|
});
|
|
787
790
|
/**
|
|
788
791
|
* Where the audit record for a governance act belongs — resolved BEFORE the act writes anything.
|
|
@@ -1241,6 +1244,38 @@ function makeRawHandlers(store, opts) {
|
|
|
1241
1244
|
* APPROVED spec depends on the target, because that dependent's `parent_digests` — the snapshot
|
|
1242
1245
|
* drift detection compares against — would silently go stale.
|
|
1243
1246
|
*/
|
|
1247
|
+
// @implements A-SPEC-255 — the WIRING only. The judgment is `planRenumber`, which is pure and
|
|
1248
|
+
// tested directly; this handler adds no rules of its own. Re-sealing is deliberately absent:
|
|
1249
|
+
// `spec_approve` is the only sealer, so the plan reports the two ORDERS and the caller runs them.
|
|
1250
|
+
// @implements A-SPEC-255 — WIRING only. The judgment lives in `planRenumber`, which is pure and
|
|
1251
|
+
// tested directly; nothing here adds a rule. Re-sealing is deliberately absent: `spec_approve`
|
|
1252
|
+
// is the only sealer (a second sealer becomes a second truth), so the plan reports the two
|
|
1253
|
+
// ORDERS and the caller runs them. `dryRun` defaults to true — a renumber is read before it runs.
|
|
1254
|
+
async spec_renumber(a) {
|
|
1255
|
+
const specsRoot = store.specsRoot;
|
|
1256
|
+
if (typeof specsRoot !== 'string')
|
|
1257
|
+
return { ok: false, reason: '파일 스토어에 묶인 서버에서만 리넘버할 수 있습니다.' };
|
|
1258
|
+
const projectRoot = path.resolve(specsRoot, '..', '..');
|
|
1259
|
+
const plan = (0, renumber_1.planRenumber)({
|
|
1260
|
+
specs: (0, renumber_1.readSpecsForRenumber)(specsRoot),
|
|
1261
|
+
sources: (0, renumber_1.readSourcesForRenumber)(projectRoot),
|
|
1262
|
+
oldBase: String(a.oldBase), newBase: String(a.newBase),
|
|
1263
|
+
});
|
|
1264
|
+
if (plan.refusal)
|
|
1265
|
+
return { ok: false, reason: plan.refusal };
|
|
1266
|
+
if (a.dryRun !== false)
|
|
1267
|
+
return { ok: true, dryRun: true, plan };
|
|
1268
|
+
const movedSpecs = (0, renumber_1.applyRenumber)(specsRoot, { ...plan, anchors: [] });
|
|
1269
|
+
(0, renumber_1.applyRenumber)(projectRoot, { ...plan, moves: [], dependsOn: [], slices: [] });
|
|
1270
|
+
new ledger_store_1.FileLedgerStore(path.join(projectRoot, '.ax', 'ledger')).append({
|
|
1271
|
+
ts: new Date().toISOString(),
|
|
1272
|
+
actor: 'spec_renumber',
|
|
1273
|
+
kind: 'spec-renumbered',
|
|
1274
|
+
summary: `renumbered base ${a.oldBase} -> ${a.newBase}: ${movedSpecs} spec(s), ${plan.anchors.length} anchored file(s), ${plan.proseCandidates.length} prose candidate(s) left for a human`,
|
|
1275
|
+
inputs: plan.moves.map((m) => `${m.oldId}->${m.newId}`),
|
|
1276
|
+
});
|
|
1277
|
+
return { ok: true, dryRun: false, movedSpecs, plan };
|
|
1278
|
+
},
|
|
1244
1279
|
async spec_unseal(a) {
|
|
1245
1280
|
const all = await store.list();
|
|
1246
1281
|
if (all.filter((s) => s.id === a.id).length > 1) {
|
|
@@ -1526,9 +1561,14 @@ function makeRawHandlers(store, opts) {
|
|
|
1526
1561
|
// @implements A-SPEC-553.1 — autonomy is the out-of-band env switch OR a valid, non-expired
|
|
1527
1562
|
// session envelope marker under this project's `.ax/state/` (which an agent cannot write).
|
|
1528
1563
|
const autonomyOn = (0, autonomy_1.autonomousApprovalEnabled)(process.env, a.root, new Date().toISOString());
|
|
1564
|
+
// @implements A-SPEC-587 — an upstream REQ/H-SPEC is graded by the A-SPECs beneath it, so
|
|
1565
|
+
// the store is consulted for its descendants (only then: the list is a cost the auto-grade
|
|
1566
|
+
// A-SPEC/T-SPEC path does not pay). Unknown scope (none yet) keeps the human answer.
|
|
1567
|
+
const upstream = target.spec.type === 'REQ' || target.spec.type === 'H-SPEC';
|
|
1568
|
+
const below = autonomyOn && upstream ? (0, autonomy_1.descendantAspecs)(target.spec, await store.list()) : undefined;
|
|
1529
1569
|
if (autonomyOn
|
|
1530
|
-
&& (0, autonomy_1.specApprovalAutonomy)(target.spec, resolver([target.spec])) === 'auto') {
|
|
1531
|
-
approveResolved = { approval: autonomousApproval(), source: 'autonomous' };
|
|
1570
|
+
&& (0, autonomy_1.specApprovalAutonomy)(target.spec, resolver([target.spec]), below) === 'auto') {
|
|
1571
|
+
approveResolved = { approval: autonomousApproval(below?.map((s) => s.id)), source: 'autonomous' };
|
|
1532
1572
|
}
|
|
1533
1573
|
else if (autonomyOn) {
|
|
1534
1574
|
// @implements A-SPEC-551.1 — hitl-grade spec under autonomy: the in-session elicitation
|
|
@@ -2765,7 +2805,7 @@ function makeRawHandlers(store, opts) {
|
|
|
2765
2805
|
graphSchema: RTM_GRAPH_SCHEMA,
|
|
2766
2806
|
extractorVersion: RTM_EXTRACTOR_VERSION,
|
|
2767
2807
|
sourceCommit: head,
|
|
2768
|
-
specFingerprint:
|
|
2808
|
+
specFingerprint: (0, graph_store_1.specFingerprint)(specs),
|
|
2769
2809
|
scanDigest: (0, graph_store_1.scanDigest)(scanned),
|
|
2770
2810
|
});
|
|
2771
2811
|
const g = opened.graph;
|
|
@@ -2863,8 +2903,26 @@ function makeRawHandlers(store, opts) {
|
|
|
2863
2903
|
deleted: changes.deleted,
|
|
2864
2904
|
renamed: changes.renamed,
|
|
2865
2905
|
};
|
|
2866
|
-
|
|
2906
|
+
let head = '';
|
|
2867
2907
|
try {
|
|
2908
|
+
head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)(), stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
2909
|
+
}
|
|
2910
|
+
catch { /* non-git */ }
|
|
2911
|
+
const graphPath = path.join(root, '.ax', 'rtm.sqlite');
|
|
2912
|
+
const tempGraphPath = `${graphPath}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
2913
|
+
const opened = (0, graph_store_1.openReusableGraph)(tempGraphPath, {
|
|
2914
|
+
graphSchema: RTM_GRAPH_SCHEMA,
|
|
2915
|
+
extractorVersion: RTM_EXTRACTOR_VERSION,
|
|
2916
|
+
sourceCommit: head,
|
|
2917
|
+
specFingerprint: (0, graph_store_1.specFingerprint)(specs),
|
|
2918
|
+
scanDigest: (0, graph_store_1.scanDigest)(scanned),
|
|
2919
|
+
});
|
|
2920
|
+
const g = opened.graph;
|
|
2921
|
+
let result;
|
|
2922
|
+
try {
|
|
2923
|
+
// Explicit reindex is a persistence operation: discard the previous contents, rebuild the
|
|
2924
|
+
// complete graph, then commit the matching basis before releasing the file (A-SPEC-589).
|
|
2925
|
+
g.clear();
|
|
2868
2926
|
(0, rtm_builder_1.buildRtm)(specs, scanned, g);
|
|
2869
2927
|
// Scaffold note: `g` was just full-built at HEAD, so applying the
|
|
2870
2928
|
// diff on top of it is currently an idempotent no-op in practice
|
|
@@ -2877,6 +2935,7 @@ function makeRawHandlers(store, opts) {
|
|
|
2877
2935
|
// graph equals what a full rebuild would have produced. Measured: without this, rtm_reindex
|
|
2878
2936
|
// returned a graph with no call edges at all.
|
|
2879
2937
|
(0, incremental_1.applyIncremental)(g, changesToApply, { repoRoot: root, specs, scanOne, allScanned: () => scanned });
|
|
2938
|
+
opened.commitBasis();
|
|
2880
2939
|
// @implements A-SPEC-478 — the semantic cache WARMING lives here, in the explicit heavy
|
|
2881
2940
|
// operation, so the ranking hot path only ever LOOKS UP vectors. Idempotent through the
|
|
2882
2941
|
// cache; a tier of none (the shipped default) computes nothing.
|
|
@@ -2891,12 +2950,39 @@ function makeRawHandlers(store, opts) {
|
|
|
2891
2950
|
}
|
|
2892
2951
|
}
|
|
2893
2952
|
catch { /* warming is best-effort; reindex's own result is unaffected */ }
|
|
2894
|
-
|
|
2953
|
+
result = { changed, nodes: g.nodeCount(), edges: g.edgeCount(), changeSource,
|
|
2895
2954
|
...(semanticWarm !== undefined ? { semanticWarm } : {}) };
|
|
2896
2955
|
}
|
|
2897
2956
|
finally {
|
|
2898
2957
|
g.close(); // release native SQLite handle even if build/apply throws
|
|
2899
2958
|
}
|
|
2959
|
+
// Publish only after the complete temporary graph is closed. Rename the old file aside so a
|
|
2960
|
+
// Windows reader either keeps the old complete graph or sees the new complete graph; it never
|
|
2961
|
+
// observes a partially built database or a basis that belongs to another file (A-SPEC-589).
|
|
2962
|
+
const backupPath = `${graphPath}.bak-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
2963
|
+
const hadOld = fs.existsSync(graphPath);
|
|
2964
|
+
try {
|
|
2965
|
+
if (hadOld)
|
|
2966
|
+
fs.renameSync(graphPath, backupPath);
|
|
2967
|
+
fs.renameSync(tempGraphPath, graphPath);
|
|
2968
|
+
if (hadOld)
|
|
2969
|
+
fs.rmSync(backupPath, { force: true });
|
|
2970
|
+
}
|
|
2971
|
+
catch (error) {
|
|
2972
|
+
try {
|
|
2973
|
+
if (!fs.existsSync(graphPath) && fs.existsSync(backupPath))
|
|
2974
|
+
fs.renameSync(backupPath, graphPath);
|
|
2975
|
+
}
|
|
2976
|
+
catch { /* preserve the publication error; the old path is reported as unavailable */ }
|
|
2977
|
+
throw error;
|
|
2978
|
+
}
|
|
2979
|
+
finally {
|
|
2980
|
+
try {
|
|
2981
|
+
fs.rmSync(tempGraphPath, { force: true });
|
|
2982
|
+
}
|
|
2983
|
+
catch { /* already renamed or retained for diagnosis */ }
|
|
2984
|
+
}
|
|
2985
|
+
return result;
|
|
2900
2986
|
},
|
|
2901
2987
|
async context_bundle(a) {
|
|
2902
2988
|
assertSpecStoreReachable('context_bundle', store, a.root); // @implements A-SPEC-433
|
|
@@ -3779,14 +3865,26 @@ depends_on:
|
|
|
3779
3865
|
}
|
|
3780
3866
|
const rawHandlers = makeRawHandlers(rawStore);
|
|
3781
3867
|
const approvedSpecs = [];
|
|
3868
|
+
const alreadySealed = [];
|
|
3782
3869
|
const refused = [];
|
|
3783
3870
|
if (idsToApprove.length === 0) {
|
|
3784
3871
|
// Reporting this as a success is how a caller ends up believing an unapproved slice was
|
|
3785
3872
|
// approved. Measured 2026-08-28: a broken-frontmatter REQ made the whole slice invisible
|
|
3786
3873
|
// here, and the tool answered `ok: true, approvedSpecs: []`.
|
|
3787
|
-
return { ok: false, approvedSpecs, refused: [{ id: a.sliceName, reason: 'no specs matched this slice name' }] };
|
|
3788
|
-
}
|
|
3874
|
+
return { ok: false, approvedSpecs, alreadySealed, refused: [{ id: a.sliceName, reason: 'no specs matched this slice name' }] };
|
|
3875
|
+
}
|
|
3876
|
+
// @implements A-SPEC-587.2 — a sealed spec is SKIPPED, not re-sealed: re-approving asked the
|
|
3877
|
+
// human the same question again (same request id back in the queue) and stopped the chain
|
|
3878
|
+
// there, so the H-SPEC was never even attempted (measured 2026-09-10, three slices).
|
|
3879
|
+
const sealed = (id) => {
|
|
3880
|
+
const s = specs.find((x) => x.id === id);
|
|
3881
|
+
return s?.status === 'approved' && typeof s.frontmatter?.approved_digest === 'string';
|
|
3882
|
+
};
|
|
3789
3883
|
for (const id of idsToApprove) {
|
|
3884
|
+
if (sealed(id)) {
|
|
3885
|
+
alreadySealed.push(id);
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3790
3888
|
const res = (await rawHandlers.spec_approve({ root, id }));
|
|
3791
3889
|
if (res.approved || res.ok) {
|
|
3792
3890
|
approvedSpecs.push(id);
|
|
@@ -3800,11 +3898,11 @@ depends_on:
|
|
|
3800
3898
|
// again or, worse, succeed against an unsealed parent. Stop and say what is left.
|
|
3801
3899
|
break;
|
|
3802
3900
|
}
|
|
3803
|
-
for (const id of idsToApprove.slice(approvedSpecs.length + refused.length)) {
|
|
3901
|
+
for (const id of idsToApprove.slice(approvedSpecs.length + alreadySealed.length + refused.length)) {
|
|
3804
3902
|
refused.push({ id, reason: 'not attempted — an earlier spec in the chain was refused' });
|
|
3805
3903
|
}
|
|
3806
|
-
// `ok` means the slice is approved. Anything less is not a success.
|
|
3807
|
-
return { ok: refused.length === 0 && approvedSpecs.length > 0, approvedSpecs, refused };
|
|
3904
|
+
// `ok` means the slice is approved — freshly or already. Anything less is not a success.
|
|
3905
|
+
return { ok: refused.length === 0 && approvedSpecs.length + alreadySealed.length > 0, approvedSpecs, alreadySealed, refused };
|
|
3808
3906
|
},
|
|
3809
3907
|
async spec_remediate(a) {
|
|
3810
3908
|
const root = a.root ? (0, root_2.resolveProjectRoot)(a.root).root : process.cwd();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 현재 max 를 한 칸 넘게 뛸 수 없다. 막으면 max+1 은 다시 오염되지 않고 넘버링이 영구히 예측 가능하다.
|
|
8
8
|
*/
|
|
9
9
|
/** id 의 base 숫자. "REQ-1403"→1403, "A-SPEC-250.1"→250(서브번호 무시), 레거시 "H-SPEC-050"→50. 못 뽑으면 null. */
|
|
10
|
-
export
|
|
10
|
+
export { specIdBase } from '../spec/spec-id';
|
|
11
11
|
export type IdVerdict = {
|
|
12
12
|
ok: true;
|
|
13
13
|
} | {
|
|
@@ -9,19 +9,15 @@
|
|
|
9
9
|
* 현재 max 를 한 칸 넘게 뛸 수 없다. 막으면 max+1 은 다시 오염되지 않고 넘버링이 영구히 예측 가능하다.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.specIdBase =
|
|
12
|
+
exports.specIdBase = void 0;
|
|
13
13
|
exports.sequentialIdVerdict = sequentialIdVerdict;
|
|
14
14
|
/** id 의 base 숫자. "REQ-1403"→1403, "A-SPEC-250.1"→250(서브번호 무시), 레거시 "H-SPEC-050"→50. 못 뽑으면 null. */
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return null;
|
|
22
|
-
const n = parseInt(m[1], 10);
|
|
23
|
-
return Number.isNaN(n) ? null : n;
|
|
24
|
-
}
|
|
15
|
+
// @implements A-SPEC-255 — the parser moved DOWN to the spec layer so `spec/renumber.ts` can share it
|
|
16
|
+
// without importing mcp (C-SPEC-224 forbids that direction). Re-exported so existing callers and the
|
|
17
|
+
// A-SPEC-252 tests keep their import path.
|
|
18
|
+
var spec_id_1 = require("../spec/spec-id");
|
|
19
|
+
Object.defineProperty(exports, "specIdBase", { enumerable: true, get: function () { return spec_id_1.specIdBase; } });
|
|
20
|
+
const spec_id_2 = require("../spec/spec-id");
|
|
25
21
|
/**
|
|
26
22
|
* 새 id 의 base 가 코퍼스 max base 를 한 칸 넘게 뛰면 거부한다.
|
|
27
23
|
*
|
|
@@ -37,13 +33,13 @@ function idSpaceOf(id) {
|
|
|
37
33
|
return /^ADR-/.test(id.trim()) ? 'ADR' : 'functional';
|
|
38
34
|
}
|
|
39
35
|
function sequentialIdVerdict(newId, existingIds) {
|
|
40
|
-
const base = specIdBase(newId);
|
|
36
|
+
const base = (0, spec_id_2.specIdBase)(newId);
|
|
41
37
|
if (base === null)
|
|
42
38
|
return { ok: true };
|
|
43
39
|
// @implements A-SPEC-571.1 — compare only within the same number space.
|
|
44
40
|
const space = idSpaceOf(newId);
|
|
45
41
|
const bases = (existingIds ?? []).filter((id) => idSpaceOf(id) === space)
|
|
46
|
-
.map(specIdBase).filter((n) => n !== null);
|
|
42
|
+
.map(spec_id_2.specIdBase).filter((n) => n !== null);
|
|
47
43
|
if (bases.length === 0)
|
|
48
44
|
return { ok: true };
|
|
49
45
|
const maxBase = Math.max(...bases);
|
|
@@ -101,6 +101,19 @@ exports.TOOL_SCHEMAS = {
|
|
|
101
101
|
required: ['id'],
|
|
102
102
|
},
|
|
103
103
|
},
|
|
104
|
+
spec_renumber: {
|
|
105
|
+
description: "Move a whole spec FAMILY (REQ/H-SPEC/A-SPEC/T-SPEC sharing one base, dot-suffixed members included) from one number to another, and report what a machine must not touch. The tool rewrites only places with a definite grammar: filenames, frontmatter `id`, every `depends_on` in the store, `slice` tags, and source `@implements` anchors. Prose — comments, test titles, CHANGELOG entries, `source.ref` citations — is REPORTED as candidates with file and line, never substituted: measured 2026-08-23, a bulk regex doing exactly that turned REQ-253's own source citation into a reference to itself, while a hand renumber left seven prose sites behind including a comment in the shipped install.ps1. Re-sealing is NOT performed here; `spec_approve` remains the only sealer. Instead the plan returns two orders, which are opposites because the two acts refuse in opposite directions: `unsealOrder` is child-first (spec_unseal refuses while an approved child depends on the target) and `approveOrder` is parent-first (spec_approve refuses while a parent is unsealed). A spec whose `depends_on` is empty appears in neither, because specDigest does not hash `id` and its seal survives the move. Refuses outright when the destination base is already claimed or the source base does not exist, emptying every list so a partial plan cannot become a partial move. `dryRun` defaults to TRUE.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
oldBase: str('The base number to move FROM, digits only (e.g. "596").'),
|
|
110
|
+
newBase: str('The base number to move TO, digits only (e.g. "599"). Must be unclaimed.'),
|
|
111
|
+
dryRun: { type: 'boolean', description: 'Default true — return the plan without touching anything. Pass false to apply.' },
|
|
112
|
+
root: str('Optional when the server is bound to a file store; if supplied it must resolve to the SAME project.'),
|
|
113
|
+
},
|
|
114
|
+
required: ['oldBase', 'newBase'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
104
117
|
spec_unseal: {
|
|
105
118
|
description: "The inverse of spec_approve: return ONE sealed (approved) spec to an editable `status: draft`, clearing `approved_digest` and `parent_digests` in a single ACT, and record `spec-unsealed` in the provenance ledger. Un-sealing WITHDRAWS a seal, so it requires the same out-of-band HOLMES_APPROVAL as spec_approve/spec_retire's sealed path (fail-closed): un-sealing an approved T-SPEC removes the code gate's demand, which unguarded would be an approval bypass. Refuses when an APPROVED spec depends on the target — that dependent's parent_digests would silently go stale — naming the blockers. Idempotent: un-sealing a spec that is already draft (never sealed) writes nothing and returns unsealed:false. Written only at the version this act read; a concurrent edit wins and the un-seal is refused for retry. Distinct from spec_retire (which withdraws authority to status:outdated); un-seal keeps the spec alive and editable.",
|
|
106
119
|
inputSchema: {
|
|
@@ -12,8 +12,22 @@
|
|
|
12
12
|
* This module is the pure re-statement of that rule: no I/O, no process, no platform — the same
|
|
13
13
|
* verdict on every OS, shared by the test that pins package.json and by doctor (A-SPEC-580).
|
|
14
14
|
*/
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Dependencies whose install script must actually RUN for the module to load.
|
|
17
|
+
*
|
|
18
|
+
* A-SPEC-595, measured 2026-09-10: `sharp@0.32.6` belongs here on EVERY platform, not just the
|
|
19
|
+
* Windows machine that first hit it. `npm pack sharp@0.32.6` yields a tarball with zero entries
|
|
20
|
+
* matching `.node`, `prebuilds/` or `vendor/`; its declared hook is
|
|
21
|
+
* `(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile
|
|
22
|
+
* && node-gyp rebuild && node install/dll-copy)`; the lockfile records `hasInstallScript: true`;
|
|
23
|
+
* and this repository's macOS tree holds `build/Release/sharp-darwin-arm64v8.node` and
|
|
24
|
+
* `vendor/8.14.5` only because that script ran. (sharp 0.33+ moved to platform packages that DO
|
|
25
|
+
* ship prebuilds — a version bump makes this entry a re-measurement, not a given.)
|
|
26
|
+
*
|
|
27
|
+
* This list is what doctor reads, and `allowScripts` is a root-manifest policy that does not
|
|
28
|
+
* transfer to consumers — so a name missing here is a name no consumer is ever told to approve.
|
|
29
|
+
*/
|
|
30
|
+
export declare const NATIVE_INSTALL_SCRIPT_DEPS: readonly ["better-sqlite3", "sharp"];
|
|
17
31
|
/** Native dependencies that ship prebuilds and load with the script blocked (measured). */
|
|
18
32
|
export declare const PREBUILT_NATIVE_DEPS: readonly ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python", "tree-sitter-c-sharp", "tree-sitter-java", "tree-sitter-go", "tree-sitter-rust", "tree-sitter-cpp"];
|
|
19
33
|
export type AllowScripts = Record<string, boolean>;
|
|
@@ -20,8 +20,22 @@ exports.policyVerdict = policyVerdict;
|
|
|
20
20
|
* This module is the pure re-statement of that rule: no I/O, no process, no platform — the same
|
|
21
21
|
* verdict on every OS, shared by the test that pins package.json and by doctor (A-SPEC-580).
|
|
22
22
|
*/
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Dependencies whose install script must actually RUN for the module to load.
|
|
25
|
+
*
|
|
26
|
+
* A-SPEC-595, measured 2026-09-10: `sharp@0.32.6` belongs here on EVERY platform, not just the
|
|
27
|
+
* Windows machine that first hit it. `npm pack sharp@0.32.6` yields a tarball with zero entries
|
|
28
|
+
* matching `.node`, `prebuilds/` or `vendor/`; its declared hook is
|
|
29
|
+
* `(node install/libvips && node install/dll-copy && prebuild-install) || (node install/can-compile
|
|
30
|
+
* && node-gyp rebuild && node install/dll-copy)`; the lockfile records `hasInstallScript: true`;
|
|
31
|
+
* and this repository's macOS tree holds `build/Release/sharp-darwin-arm64v8.node` and
|
|
32
|
+
* `vendor/8.14.5` only because that script ran. (sharp 0.33+ moved to platform packages that DO
|
|
33
|
+
* ship prebuilds — a version bump makes this entry a re-measurement, not a given.)
|
|
34
|
+
*
|
|
35
|
+
* This list is what doctor reads, and `allowScripts` is a root-manifest policy that does not
|
|
36
|
+
* transfer to consumers — so a name missing here is a name no consumer is ever told to approve.
|
|
37
|
+
*/
|
|
38
|
+
exports.NATIVE_INSTALL_SCRIPT_DEPS = ['better-sqlite3', 'sharp'];
|
|
25
39
|
/** Native dependencies that ship prebuilds and load with the script blocked (measured). */
|
|
26
40
|
exports.PREBUILT_NATIVE_DEPS = [
|
|
27
41
|
'tree-sitter', 'tree-sitter-typescript', 'tree-sitter-python', 'tree-sitter-c-sharp',
|
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.materializeTreeAt = materializeTreeAt;
|
|
37
37
|
exports.verifyTreeFidelity = verifyTreeFidelity;
|
|
38
|
+
// @implements A-SPEC-599
|
|
38
39
|
// @implements A-SPEC-297
|
|
39
40
|
const fs = __importStar(require("node:fs"));
|
|
40
41
|
const os = __importStar(require("node:os"));
|
|
@@ -56,12 +57,14 @@ function git(repoRoot, args) {
|
|
|
56
57
|
}
|
|
57
58
|
/** `path mode sha` triples the commit records for the pathspec, keyed by repo-relative path. */
|
|
58
59
|
function blobsAt(repoRoot, commitish, pathspec) {
|
|
59
|
-
const out = git(repoRoot, ['ls-tree', '-
|
|
60
|
+
const out = git(repoRoot, ['ls-tree', '-rz', `${commitish}^{tree}`, '--', pathspec]);
|
|
60
61
|
const blobs = new Map();
|
|
61
|
-
for (const line of out.split('\
|
|
62
|
+
for (const line of out.split('\0')) {
|
|
62
63
|
if (line.trim() === '')
|
|
63
64
|
continue;
|
|
64
|
-
const
|
|
65
|
+
const tab = line.indexOf('\t');
|
|
66
|
+
const meta = line.slice(0, tab);
|
|
67
|
+
const file = line.slice(tab + 1);
|
|
65
68
|
const parts = meta.split(/\s+/);
|
|
66
69
|
if (parts[1] !== 'blob')
|
|
67
70
|
continue; // submodules and trees are not files we can compare
|
|
@@ -119,6 +122,43 @@ function materializeTreeAt(repoRoot, commitish, pathspec, destDir, opts) {
|
|
|
119
122
|
const tar = (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'archive', '--format=tar', commitish, '--', pathspec], { maxBuffer: 512 * 1024 * 1024 });
|
|
120
123
|
fs.writeFileSync(tarPath, tar);
|
|
121
124
|
(0, node_child_process_1.execFileSync)('tar', ['-x', '-C', destDir, '-f', tarPath], { maxBuffer: 512 * 1024 * 1024 });
|
|
125
|
+
// Archive preserves modes/layout, but applies EOL/export substitutions. Read raw objects in
|
|
126
|
+
// one batch so fidelity is independent of user config and committed attributes alike.
|
|
127
|
+
const refsPath = path.join(path.dirname(tarPath), 'objects.txt');
|
|
128
|
+
fs.writeFileSync(refsPath, [...blobs.values()].join('\n') + '\n');
|
|
129
|
+
const refs = fs.openSync(refsPath, 'r');
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = (0, node_child_process_1.execFileSync)('git', ['-C', repoRoot, 'cat-file', '--batch'], {
|
|
133
|
+
stdio: [refs, 'pipe', 'pipe'], maxBuffer: 512 * 1024 * 1024,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
fs.closeSync(refs);
|
|
138
|
+
}
|
|
139
|
+
let offset = 0;
|
|
140
|
+
for (const [file, sha] of blobs) {
|
|
141
|
+
const end = raw.indexOf(10, offset);
|
|
142
|
+
const header = raw.subarray(offset, end).toString('ascii').split(' ');
|
|
143
|
+
const size = Number(header[2]);
|
|
144
|
+
if (end < offset || header[0] !== sha || header[1] !== 'blob' || !Number.isSafeInteger(size)
|
|
145
|
+
|| size < 0 || end + 1 + size >= raw.length)
|
|
146
|
+
throw new Error('invalid cat-file response');
|
|
147
|
+
const bytes = raw.subarray(end + 1, end + 1 + size);
|
|
148
|
+
offset = end + 1 + size + 1;
|
|
149
|
+
const target = path.join(destDir, file);
|
|
150
|
+
// Keep archive's symlink representation; never follow it to overwrite a target.
|
|
151
|
+
try {
|
|
152
|
+
if (fs.lstatSync(target).isSymbolicLink())
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error.code !== 'ENOENT')
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
160
|
+
fs.writeFileSync(target, bytes);
|
|
161
|
+
}
|
|
122
162
|
}
|
|
123
163
|
catch (err) {
|
|
124
164
|
return { ok: false, reason: `archive-or-extract-failed: could not materialize ${commitish}: ${err.message}` };
|
|
@@ -27,6 +27,8 @@ export interface GraphBasis {
|
|
|
27
27
|
*/
|
|
28
28
|
scanDigest: string;
|
|
29
29
|
}
|
|
30
|
+
/** Deterministic content fingerprint for every spec field consumed by buildRtm. */
|
|
31
|
+
export declare function specFingerprint(specs: readonly unknown[]): string;
|
|
30
32
|
/**
|
|
31
33
|
* Content address of a scan: every file's path and its symbols/edges, sorted so the digest depends
|
|
32
34
|
* on the scan's CONTENT and not on the order the scanner happened to walk the tree in.
|