@holmes-lab/holmes-kit 0.18.0 → 0.19.2
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 +149 -0
- package/README.md +3 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +22 -0
- package/dist/holmes/cli/agents.js +76 -1
- package/dist/holmes/cli/approve.js +6 -1
- package/dist/holmes/cli/doctor.d.ts +36 -1
- package/dist/holmes/cli/doctor.js +182 -35
- package/dist/holmes/cli/index.js +7 -1
- package/dist/holmes/cli/init.js +12 -0
- package/dist/holmes/cli/native-deps.d.ts +65 -0
- package/dist/holmes/cli/native-deps.js +131 -0
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +68 -0
- package/dist/holmes/cpg/arch-observe.d.ts +15 -0
- package/dist/holmes/cpg/arch-observe.js +19 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
- package/dist/holmes/cpg/cpg-scanner.js +27 -3
- package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
- package/dist/holmes/cpg/cycle-detect.js +251 -0
- package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
- package/dist/holmes/cpg/cycle-observation.js +146 -0
- package/dist/holmes/cpg/scan-cache.d.ts +1 -1
- package/dist/holmes/cpg/scanned-file.d.ts +36 -0
- package/dist/holmes/cpg/scanned-file.js +2 -0
- package/dist/holmes/governance/approval-queue.d.ts +23 -4
- package/dist/holmes/governance/approval-queue.js +44 -6
- package/dist/holmes/governance/constitution.d.ts +20 -0
- package/dist/holmes/governance/constitution.js +17 -0
- package/dist/holmes/governance/ledger-store.d.ts +9 -0
- package/dist/holmes/governance/ledger-store.js +47 -0
- package/dist/holmes/governance/provenance-chain.d.ts +16 -1
- package/dist/holmes/governance/provenance-chain.js +5 -3
- package/dist/holmes/hooks/pre-tool-use.js +3 -1
- package/dist/holmes/hooks/stop.d.ts +29 -0
- package/dist/holmes/hooks/stop.js +119 -3
- package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
- package/dist/holmes/mcp/defuse-bound.js +8 -0
- package/dist/holmes/mcp/handlers.d.ts +7 -0
- package/dist/holmes/mcp/handlers.js +132 -6
- package/dist/holmes/mcp/history-admission.d.ts +15 -0
- package/dist/holmes/mcp/history-admission.js +37 -0
- package/dist/holmes/mcp/maintenance-analyze.d.ts +45 -0
- package/dist/holmes/mcp/maintenance-analyze.js +117 -9
- package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
- package/dist/holmes/mcp/maintenance-evidence.js +71 -4
- package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
- package/dist/holmes/project/install-scripts-policy.js +131 -0
- package/dist/holmes/project/npx-bin.d.ts +6 -0
- package/dist/holmes/project/npx-bin.js +10 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/failed-test-names.d.ts +19 -0
- package/dist/holmes/review/failed-test-names.js +43 -0
- package/dist/holmes/review/paired-power.d.ts +14 -0
- package/dist/holmes/review/paired-power.js +57 -0
- package/dist/holmes/review/replay-corpus.d.ts +11 -0
- package/dist/holmes/review/replay-corpus.js +34 -0
- package/dist/holmes/review/run-replay.d.ts +23 -0
- package/dist/holmes/review/run-replay.js +90 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/review/test-runner.d.ts +27 -0
- package/dist/holmes/review/test-runner.js +59 -3
- package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
- package/dist/holmes/rtm/defuse-symbols.js +91 -0
- package/dist/holmes/rtm/incremental.js +5 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +32 -5
- package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/spec/legacy-fields.d.ts +2 -0
- package/dist/holmes/spec/legacy-fields.js +9 -0
- package/dist/holmes/spec/legacy-format.d.ts +1 -1
- package/dist/holmes/spec/legacy-format.js +4 -1
- package/dist/holmes/spec/spec-parser.js +5 -3
- package/docs/install-guide.md +54 -5
- package/package.json +4 -1
- package/playbooks/author-slice/PLAYBOOK.md +14 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
|
@@ -0,0 +1,146 @@
|
|
|
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.CYCLE_LIST_CAP = void 0;
|
|
37
|
+
exports.buildCycleObservation = buildCycleObservation;
|
|
38
|
+
exports.appendCycleObservation = appendCycleObservation;
|
|
39
|
+
exports.readCycleObservations = readCycleObservations;
|
|
40
|
+
// @implements A-SPEC-578.1
|
|
41
|
+
// The observation ledger the cycle ratchet's promotion criterion was already waiting on.
|
|
42
|
+
//
|
|
43
|
+
// `stop.ts` says promotion to `strict` "waits on the observation ledger answering the false-positive
|
|
44
|
+
// rate, which is the same path impactAdvisory and anchorDensity took". Measured 2026-09-09: those two
|
|
45
|
+
// siblings had been appending to `<name>.<replica>.jsonl` all along, and this one wrote a single line
|
|
46
|
+
// to stderr and nothing to disk — 0 records against their 12 and 7. A criterion waiting on data
|
|
47
|
+
// nobody collects never fires. This is the missing half, and it is deliberately the SIBLINGS' shape
|
|
48
|
+
// rather than a better one: a second convention for the same job is the next drift point.
|
|
49
|
+
//
|
|
50
|
+
// `cycle-detect.ts` stays pure (zero imports) — the same split `rtm/anchor-density.ts` uses against
|
|
51
|
+
// the rule it observes.
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const replica_id_1 = require("../governance/replica-id");
|
|
55
|
+
const cycle_detect_1 = require("./cycle-detect");
|
|
56
|
+
/**
|
|
57
|
+
* How many cycles one record lists before it starts counting instead.
|
|
58
|
+
*
|
|
59
|
+
* A record must not grow with the tree: a repository with a thousand cycles would otherwise write a
|
|
60
|
+
* thousand-entry line every turn, and the ledger this exists to make readable would be the thing
|
|
61
|
+
* that makes it unreadable. What is dropped is COUNTED, never silently cut.
|
|
62
|
+
*/
|
|
63
|
+
exports.CYCLE_LIST_CAP = 50;
|
|
64
|
+
/**
|
|
65
|
+
* Build the record. PURE — the clock is an argument, so a test can pin it and two callers cannot
|
|
66
|
+
* disagree about what "now" was.
|
|
67
|
+
*
|
|
68
|
+
* A CLEAN run produces a record too. That is the whole design: a false-positive RATE is violations
|
|
69
|
+
* over chances, and a ledger that only speaks when something is wrong records the numerator and
|
|
70
|
+
* throws the denominator away.
|
|
71
|
+
*/
|
|
72
|
+
function buildCycleObservation(ev, ts) {
|
|
73
|
+
const listed = ev.current.slice(0, exports.CYCLE_LIST_CAP);
|
|
74
|
+
// The ratchet's own predicate decides what counts as a violation — reimplementing the filter here
|
|
75
|
+
// would be a second rule for one question, which is how the two quietly disagree.
|
|
76
|
+
const violations = (0, cycle_detect_1.cycleRatchetViolations)(listed, ev.allowed).map((v) => v.key);
|
|
77
|
+
return {
|
|
78
|
+
ts,
|
|
79
|
+
mode: ev.mode,
|
|
80
|
+
cycles: listed.map((c) => ({ key: (0, cycle_detect_1.cycleKey)(c.files), files: [...c.files].sort(), runtime: c.runtime, edges: c.edges.length })),
|
|
81
|
+
cyclesOmitted: Math.max(0, ev.current.length - listed.length),
|
|
82
|
+
violations,
|
|
83
|
+
allowed: ev.allowed.length,
|
|
84
|
+
scope: { judged: [...ev.scope.judged], unavailable: [...ev.scope.unavailable] },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const OBSERVATION_FILE_RE = /^cycle-observations\.([^.]+)\.jsonl$/;
|
|
88
|
+
/**
|
|
89
|
+
* Append one record. Never throws, and never creates `.ax` where governance was not opted into
|
|
90
|
+
* (A-SPEC-191 §25 — the same refusal the approval queue makes).
|
|
91
|
+
*
|
|
92
|
+
* A failure returns `false` and changes nothing else: this is an OBSERVATION, and an observation
|
|
93
|
+
* that could alter a verdict would be a gate wearing a different name.
|
|
94
|
+
*/
|
|
95
|
+
function appendCycleObservation(root, rec) {
|
|
96
|
+
try {
|
|
97
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
98
|
+
return false;
|
|
99
|
+
let replica = 'local';
|
|
100
|
+
try {
|
|
101
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
102
|
+
}
|
|
103
|
+
catch { /* keep the fallback */ }
|
|
104
|
+
const file = path.join(root, '.ax', 'ledger', `cycle-observations.${replica}.jsonl`);
|
|
105
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
106
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Every replica's records, merged. A corrupt line is skipped, never fatal. */
|
|
114
|
+
function readCycleObservations(root) {
|
|
115
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = fs.readdirSync(dir).filter((n) => OBSERVATION_FILE_RE.test(n)).sort();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
let text;
|
|
126
|
+
try {
|
|
127
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const line of text.split('\n')) {
|
|
133
|
+
const s = line.trim();
|
|
134
|
+
if (!s)
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
const r = JSON.parse(s);
|
|
138
|
+
if (r && typeof r === 'object' && typeof r.ts === 'string' && Array.isArray(r.cycles) && Array.isArray(r.violations)) {
|
|
139
|
+
out.push(r);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CodeSymbol, CodeEdge, DataFlowFacts } from './language-parser';
|
|
2
|
+
export interface ScannedFile {
|
|
3
|
+
path: string;
|
|
4
|
+
/** Repo-root-relative POSIX path — provenance key used by RtmGraph.removeBySource. */
|
|
5
|
+
sourcePath: string;
|
|
6
|
+
symbols: CodeSymbol[];
|
|
7
|
+
/**
|
|
8
|
+
* Call/import relations recovered from this file, with `from` a qualified name defined HERE and
|
|
9
|
+
* `to` a bare callee name or module path. Resolution to graph node ids happens in rtm-builder,
|
|
10
|
+
* where the whole scan is visible; a single file cannot know where a callee lives.
|
|
11
|
+
*
|
|
12
|
+
* Optional so a cache entry written by an older scanner still loads — such an entry simply carries
|
|
13
|
+
* no edges, which degrades to the previous anchor-only behaviour rather than crashing.
|
|
14
|
+
*/
|
|
15
|
+
edges?: CodeEdge[];
|
|
16
|
+
/**
|
|
17
|
+
* Def-use facts, present ONLY when the scanner was constructed with `{ dataflow: true }` AND the
|
|
18
|
+
* file's language has a data-flow walk.
|
|
19
|
+
*
|
|
20
|
+
* @implements A-SPEC-140.1
|
|
21
|
+
* Opt-in by measurement, not taste: the scan already parses each file twice (symbols, edges), and
|
|
22
|
+
* an unconditional third parse would regress the REQ-139 cold-scan baseline by ~50% to produce a
|
|
23
|
+
* signal only the taint pass reads. `undefined` therefore means "not extracted" — for the option
|
|
24
|
+
* being off, for a language with no walk, or for a cache entry from an older writer. It never
|
|
25
|
+
* means "extracted, found nothing"; that is `{defs:[],calls:[],params:[],returns:[]}`.
|
|
26
|
+
*/
|
|
27
|
+
dataflow?: DataFlowFacts;
|
|
28
|
+
implementsSpecs: string[];
|
|
29
|
+
/**
|
|
30
|
+
* `@implements` tags shaped like a well-formed governed spec id (REQ/H-SPEC/
|
|
31
|
+
* C-SPEC/T-SPEC-NNN) instead of an A-SPEC anchor — a wrong-kind anchor that
|
|
32
|
+
* would otherwise be silently dropped (audit finding D). Populated
|
|
33
|
+
* alongside `implementsSpecs`, never overlapping with it.
|
|
34
|
+
*/
|
|
35
|
+
unanchoredImplements: string[];
|
|
36
|
+
}
|
|
@@ -109,8 +109,9 @@ export declare function approvalRequestId(kind: string, target: string): string;
|
|
|
109
109
|
* waiting.
|
|
110
110
|
*/
|
|
111
111
|
export declare function foldQueue(lines: string[], opts?: {
|
|
112
|
-
now
|
|
113
|
-
ttlMs
|
|
112
|
+
now?: number;
|
|
113
|
+
ttlMs?: number;
|
|
114
|
+
includeAllKinds?: boolean;
|
|
114
115
|
}): QueueState;
|
|
115
116
|
/**
|
|
116
117
|
* Append a request event. Fire-and-forget.
|
|
@@ -126,10 +127,28 @@ export declare function enqueueApprovalRequest(root: string, req: {
|
|
|
126
127
|
why: string;
|
|
127
128
|
reasonBytes?: number;
|
|
128
129
|
}): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The same act, told in full: what happened and why.
|
|
132
|
+
*
|
|
133
|
+
* @implements A-SPEC-576.1
|
|
134
|
+
* Nine modules read this one, and all of them want the boolean — so the boolean stays and this is
|
|
135
|
+
* the shape underneath it, rather than a breaking change rippling through nine call sites for the
|
|
136
|
+
* benefit of the one caller that wants to distinguish a duplicate from a refusal.
|
|
137
|
+
*/
|
|
138
|
+
export declare function enqueueApprovalRequestDetailed(root: string, req: {
|
|
139
|
+
kind: string;
|
|
140
|
+
target: string;
|
|
141
|
+
why: string;
|
|
142
|
+
reasonBytes?: number;
|
|
143
|
+
}): {
|
|
144
|
+
written: boolean;
|
|
145
|
+
reason: 'written' | 'duplicate' | 'no-project' | 'unwritable';
|
|
146
|
+
};
|
|
129
147
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
130
148
|
export declare function readQueue(root: string, opts?: {
|
|
131
|
-
now
|
|
132
|
-
ttlMs
|
|
149
|
+
now?: number;
|
|
150
|
+
ttlMs?: number;
|
|
151
|
+
includeAllKinds?: boolean;
|
|
133
152
|
}): QueueState;
|
|
134
153
|
/**
|
|
135
154
|
* The refusal-message suffix pointing the operator at the review CLI.
|
|
@@ -40,6 +40,7 @@ exports.readRefusals = readRefusals;
|
|
|
40
40
|
exports.approvalRequestId = approvalRequestId;
|
|
41
41
|
exports.foldQueue = foldQueue;
|
|
42
42
|
exports.enqueueApprovalRequest = enqueueApprovalRequest;
|
|
43
|
+
exports.enqueueApprovalRequestDetailed = enqueueApprovalRequestDetailed;
|
|
43
44
|
exports.readQueue = readQueue;
|
|
44
45
|
exports.queueHint = queueHint;
|
|
45
46
|
// @implements A-SPEC-244
|
|
@@ -169,6 +170,16 @@ function foldQueue(lines, opts) {
|
|
|
169
170
|
malformedLines++;
|
|
170
171
|
break;
|
|
171
172
|
}
|
|
173
|
+
// @implements A-SPEC-576.1 — the READER applies the writer's predicate.
|
|
174
|
+
// REQ-563 routed gate refusals away from the inbox and deliberately did NOT rewrite the
|
|
175
|
+
// 2,192 lines already written; append-only is the rule this ledger is worth something for.
|
|
176
|
+
// The two decisions together left the inbox permanently 99.93% noise (measured here:
|
|
177
|
+
// 1,418 `shell` entries around a single real decision) because only the writer changed.
|
|
178
|
+
// The same constant does both jobs, so there is no second list to drift.
|
|
179
|
+
// `decisions` and `holds` are built from the OTHER events and stay complete — the hold
|
|
180
|
+
// question and the denial reason a refused `shell` request needs still reach `queueHint`.
|
|
181
|
+
if (!opts?.includeAllKinds && !exports.DECISION_KINDS.has(String(e.kind ?? '')))
|
|
182
|
+
break;
|
|
172
183
|
const prev = pending.get(id);
|
|
173
184
|
const ts = typeof e.ts === 'string' ? e.ts : '';
|
|
174
185
|
if (prev) {
|
|
@@ -223,7 +234,9 @@ function foldQueue(lines, opts) {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
const all = [...pending.values()];
|
|
226
|
-
|
|
237
|
+
const ttl = typeof opts?.ttlMs === 'number' && typeof opts?.now === 'number'
|
|
238
|
+
? { ttlMs: opts.ttlMs, now: opts.now } : null;
|
|
239
|
+
if (ttl === null)
|
|
227
240
|
return { pending: all, expired: [], malformedLines, decisions, holds };
|
|
228
241
|
// @implements A-SPEC-507.1 — strict excess only, and an unparseable lastTs stays ACTIVE: a
|
|
229
242
|
// clockless entry must never be silently hidden by a clock it does not carry.
|
|
@@ -231,7 +244,7 @@ function foldQueue(lines, opts) {
|
|
|
231
244
|
const active = [];
|
|
232
245
|
for (const p of all) {
|
|
233
246
|
const last = Date.parse(p.lastTs);
|
|
234
|
-
(Number.isFinite(last) && last +
|
|
247
|
+
(Number.isFinite(last) && last + ttl.ttlMs < ttl.now ? expired : active).push(p);
|
|
235
248
|
}
|
|
236
249
|
return { pending: active, expired, malformedLines, decisions, holds };
|
|
237
250
|
}
|
|
@@ -244,6 +257,18 @@ function foldQueue(lines, opts) {
|
|
|
244
257
|
* caller uses the boolean only to decide whether to print the review hint.
|
|
245
258
|
*/
|
|
246
259
|
function enqueueApprovalRequest(root, req) {
|
|
260
|
+
return enqueueApprovalRequestDetailed(root, req).written;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The same act, told in full: what happened and why.
|
|
264
|
+
*
|
|
265
|
+
* @implements A-SPEC-576.1
|
|
266
|
+
* Nine modules read this one, and all of them want the boolean — so the boolean stays and this is
|
|
267
|
+
* the shape underneath it, rather than a breaking change rippling through nine call sites for the
|
|
268
|
+
* benefit of the one caller that wants to distinguish a duplicate from a refusal.
|
|
269
|
+
*/
|
|
270
|
+
function enqueueApprovalRequestDetailed(root, req) {
|
|
271
|
+
let reason = 'unwritable';
|
|
247
272
|
try {
|
|
248
273
|
// @implements A-SPEC-244
|
|
249
274
|
// Only under an EXISTING .ax. The constitution suite (§25a) caught the first cut creating
|
|
@@ -251,7 +276,7 @@ function enqueueApprovalRequest(root, req) {
|
|
|
251
276
|
// marker where governance was never opted into, the exact defect A-SPEC-191 §25 exists to stop.
|
|
252
277
|
// An ungoverned directory gets no queue and no hint; it is not part of the system.
|
|
253
278
|
if (!fs.existsSync(path.join(root, '.ax')))
|
|
254
|
-
return false;
|
|
279
|
+
return { written: false, reason: 'no-project' };
|
|
255
280
|
// @implements A-SPEC-563.1 — kind routing: only decision-seeking kinds enter the tracked inbox;
|
|
256
281
|
// a gate refusal (shell, or any future kind — fail-safe toward a clean inbox) goes to this
|
|
257
282
|
// machine's LOCAL refusal log, raw target and all, where the approve fallback can still find it.
|
|
@@ -301,12 +326,25 @@ function enqueueApprovalRequest(root, req) {
|
|
|
301
326
|
// not a regular file, so a link — dangling or not — is refused. A path that truly does not exist
|
|
302
327
|
// throws ENOENT and is created, which is the ordinary first-write case.
|
|
303
328
|
if (!isPlainFile(file))
|
|
304
|
-
return false;
|
|
329
|
+
return { written: false, reason: 'unwritable' };
|
|
330
|
+
// @implements A-SPEC-576.1 — idempotency, on the INBOX only.
|
|
331
|
+
// Measured on this ledger: 2,203 `requested` lines carry 1,427 distinct ids, and 774 of the 776
|
|
332
|
+
// duplicates are one id — a `rm -rf /` test fixture re-filed 774 times across nine days. An
|
|
333
|
+
// outstanding question does not become more answerable by being asked again.
|
|
334
|
+
// The refusal log is deliberately left alone: it is a LOG, where each refusal is its own event
|
|
335
|
+
// and A-SPEC-564.2 counts them. Deduplicating a log erases the measurement it exists for.
|
|
336
|
+
// "Already asked" means still pending — a request re-filed AFTER a decision is a new question,
|
|
337
|
+
// and folding already drops decided ids from `pending`.
|
|
338
|
+
if (exports.DECISION_KINDS.has(req.kind)
|
|
339
|
+
&& readQueue(root, { includeAllKinds: true }).pending.some((p) => p.id === event.id)) {
|
|
340
|
+
return { written: false, reason: 'duplicate' };
|
|
341
|
+
}
|
|
305
342
|
fs.appendFileSync(file, JSON.stringify(event) + '\n');
|
|
306
|
-
|
|
343
|
+
reason = 'written';
|
|
344
|
+
return { written: true, reason };
|
|
307
345
|
}
|
|
308
346
|
catch {
|
|
309
|
-
return false;
|
|
347
|
+
return { written: false, reason };
|
|
310
348
|
}
|
|
311
349
|
}
|
|
312
350
|
/**
|
|
@@ -44,6 +44,26 @@ export interface ConstitutionContext {
|
|
|
44
44
|
severity?: string;
|
|
45
45
|
summary?: string;
|
|
46
46
|
}[];
|
|
47
|
+
/**
|
|
48
|
+
* @implements A-SPEC-574.4
|
|
49
|
+
* Import cycles in the CODE graph, supplied by the hook — the constitution stays pure and does no
|
|
50
|
+
* I/O, exactly as ART-4's evidence and ART-6's findings are supplied.
|
|
51
|
+
*
|
|
52
|
+
* Absent means SILENT: a project whose hook could not scan is not a project without cycles, and
|
|
53
|
+
* inventing a clean verdict from missing data is the failure mode this repository names most
|
|
54
|
+
* often. `scope.unavailable` carries the languages whose imports could not be resolved, so the
|
|
55
|
+
* report can say what it did NOT judge instead of implying zero.
|
|
56
|
+
*/
|
|
57
|
+
cycles?: {
|
|
58
|
+
current: import('../cpg/cycle-detect').Cycle[];
|
|
59
|
+
/** Cycle keys a C-SPEC named as accepted. The escape is a name, never a threshold. */
|
|
60
|
+
allowed: string[];
|
|
61
|
+
mode: 'strict' | 'track' | 'off';
|
|
62
|
+
scope: {
|
|
63
|
+
judged: string[];
|
|
64
|
+
unavailable: string[];
|
|
65
|
+
};
|
|
66
|
+
};
|
|
47
67
|
/** A-SPEC id -> count of real test cases in anchored test files (H1 syntactic lower bound). */
|
|
48
68
|
testCasesByAspec?: Record<string, number>;
|
|
49
69
|
/**
|
|
@@ -6,6 +6,8 @@ exports.verifyConstitution = verifyConstitution;
|
|
|
6
6
|
const basis_1 = require("../mcp/basis");
|
|
7
7
|
const spec_types_1 = require("../spec/spec-types");
|
|
8
8
|
const rtm_check_1 = require("../rtm/rtm-check");
|
|
9
|
+
// @implements A-SPEC-574.4 — the same detector the design-time advisory calls.
|
|
10
|
+
const cycle_detect_1 = require("../cpg/cycle-detect");
|
|
9
11
|
const validator_1 = require("../spec/validator");
|
|
10
12
|
/**
|
|
11
13
|
* @implements A-SPEC-534.2
|
|
@@ -54,6 +56,21 @@ function verifyConstitution(ctx) {
|
|
|
54
56
|
for (const issue of (0, rtm_check_1.rtmCheck)(governed)) {
|
|
55
57
|
v.push({ article: 'ART-2', detail: `RTM ${issue.kind} [${issue.id}]: ${issue.detail}` });
|
|
56
58
|
}
|
|
59
|
+
// @implements A-SPEC-574.4
|
|
60
|
+
// ART-2's sibling on the OTHER graph. The spec graph has been required to be acyclic since early
|
|
61
|
+
// on; the code graph never was, in the same repository. Shipped in `track` first — the posture
|
|
62
|
+
// that records without blocking — because a gate whose false-positive rate nobody has measured
|
|
63
|
+
// gets promoted by evidence, not by enthusiasm (the impactAdvisory / anchorDensity lifecycle).
|
|
64
|
+
// STRICT ONLY here, exactly like ART-8: `track` must record without blocking, and a violation
|
|
65
|
+
// emitted from this function IS a block. The hook computes the track list separately.
|
|
66
|
+
if (ctx.cycles !== undefined && ctx.cycles.mode === 'strict') {
|
|
67
|
+
const scope = ctx.cycles.scope.unavailable.length > 0
|
|
68
|
+
? ` (judged: ${ctx.cycles.scope.judged.join(', ') || 'none'}; NOT judged: ${ctx.cycles.scope.unavailable.join(', ')})`
|
|
69
|
+
: '';
|
|
70
|
+
for (const found of (0, cycle_detect_1.cycleRatchetViolations)(ctx.cycles.current, ctx.cycles.allowed)) {
|
|
71
|
+
v.push({ article: 'ART-2', detail: `code ${found.detail}${scope}` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
57
74
|
// ART-3: spec validity (type rules + 4-quadrant GWT via validateSpec)
|
|
58
75
|
for (const s of governed) {
|
|
59
76
|
const r = (0, validator_1.validateSpec)(s, resolve);
|
|
@@ -112,3 +112,12 @@ export declare class MemoryLedgerStore implements LedgerStore {
|
|
|
112
112
|
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
113
113
|
isNonceConsumed(nonce: string): boolean;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The cross-replica nonce check, bound to one ledger file, ready to hand to
|
|
117
|
+
* `consumeNonceExclusively`.
|
|
118
|
+
*
|
|
119
|
+
* Lives HERE, on the store side, because that is the direction the dependency should point: the
|
|
120
|
+
* chain layer asks a question the caller answers, instead of reaching into the store through a lazy
|
|
121
|
+
* `require()` to dodge an import cycle. One helper rather than three copies at the call sites.
|
|
122
|
+
*/
|
|
123
|
+
export declare function nonceConsumedIn(ledgerFile: string): (nonce: string) => boolean;
|
|
@@ -1,9 +1,44 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.MemoryLedgerStore = exports.FileLedgerStore = void 0;
|
|
37
|
+
exports.nonceConsumedIn = nonceConsumedIn;
|
|
4
38
|
// @implements A-SPEC-150
|
|
5
39
|
const provenance_chain_1 = require("./provenance-chain");
|
|
6
40
|
const provenance_ledger_1 = require("./provenance-ledger");
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
7
42
|
/**
|
|
8
43
|
* Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
|
|
9
44
|
*
|
|
@@ -104,3 +139,15 @@ class MemoryLedgerStore {
|
|
|
104
139
|
}
|
|
105
140
|
}
|
|
106
141
|
exports.MemoryLedgerStore = MemoryLedgerStore;
|
|
142
|
+
// @implements A-SPEC-574.3
|
|
143
|
+
/**
|
|
144
|
+
* The cross-replica nonce check, bound to one ledger file, ready to hand to
|
|
145
|
+
* `consumeNonceExclusively`.
|
|
146
|
+
*
|
|
147
|
+
* Lives HERE, on the store side, because that is the direction the dependency should point: the
|
|
148
|
+
* chain layer asks a question the caller answers, instead of reaching into the store through a lazy
|
|
149
|
+
* `require()` to dodge an import cycle. One helper rather than three copies at the call sites.
|
|
150
|
+
*/
|
|
151
|
+
function nonceConsumedIn(ledgerFile) {
|
|
152
|
+
return (nonce) => new FileLedgerStore(path.dirname(ledgerFile)).isNonceConsumed(nonce);
|
|
153
|
+
}
|
|
@@ -177,5 +177,20 @@ export declare const PROVENANCE_FILE: string;
|
|
|
177
177
|
* decision, not this function's: an authorization gate must deny, while a record-keeping append must
|
|
178
178
|
* not flip a decision that was already made.
|
|
179
179
|
*/
|
|
180
|
-
export declare function consumeNonceExclusively(nonce: string, ledgerFile: string, body: Omit<ProvenanceBody, 'seq'>, opts
|
|
180
|
+
export declare function consumeNonceExclusively(nonce: string, ledgerFile: string, body: Omit<ProvenanceBody, 'seq'>, opts: LockOptions & {
|
|
181
|
+
/**
|
|
182
|
+
* @implements A-SPEC-574.3
|
|
183
|
+
* Whether this nonce was already consumed, ACROSS every replica chain (A-SPEC-148's rule).
|
|
184
|
+
*
|
|
185
|
+
* Injected, because the chain layer has no business knowing about the store layer — it used to
|
|
186
|
+
* reach for `FileLedgerStore` through a lazy `require()`, and that workaround existed only to
|
|
187
|
+
* dodge the import cycle it created. The caller owns the store; the caller passes the question.
|
|
188
|
+
*
|
|
189
|
+
* REQUIRED, and that is the point. The first cut made it optional with a `?? false` default,
|
|
190
|
+
* and `ledger-race` went red immediately: the second consume of the same nonce SUCCEEDED. A
|
|
191
|
+
* default of "nothing is spent" turns REQ-141's whole reason for existing into an opt-in. A
|
|
192
|
+
* missing security check has to be a compile error, not a quiet pass.
|
|
193
|
+
*/
|
|
194
|
+
isNonceConsumed: (nonce: string) => boolean;
|
|
195
|
+
}): boolean;
|
|
181
196
|
export {};
|
|
@@ -427,8 +427,10 @@ function consumeNonceExclusively(nonce, ledgerFile, body, opts) {
|
|
|
427
427
|
// across machines there is no shared filesystem to lock, so two machines racing the same nonce
|
|
428
428
|
// can both pass and the duplicate is DETECTED afterwards in the merged ledger rather than
|
|
429
429
|
// prevented. Preventing it needs a shared authority, which is out of this slice's scope.
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
// @implements A-SPEC-574.3 — the check comes from the caller. Absent, nothing is known to be
|
|
431
|
+
// spent: a caller that does not supply the store is asking for an append, not for exclusion,
|
|
432
|
+
// and inventing a false "already consumed" here would silently refuse a legitimate action.
|
|
433
|
+
const spent = opts.isNonceConsumed(nonce);
|
|
432
434
|
if (spent)
|
|
433
435
|
return false;
|
|
434
436
|
chain.appendInsideLock(body);
|
|
@@ -436,5 +438,5 @@ function consumeNonceExclusively(nonce, ledgerFile, body, opts) {
|
|
|
436
438
|
// Default the stale-break recorder to the chain's own: breaking a lock silently is forbidden
|
|
437
439
|
// here for exactly the same reason it is in `append()`, and leaving it undefined made this
|
|
438
440
|
// path — the SECURITY path — the one place a lock could be stolen without a trace.
|
|
439
|
-
}, { ...opts, onStaleBreak: opts
|
|
441
|
+
}, { ...opts, onStaleBreak: opts.onStaleBreak ?? chain.staleBreakRecorder() });
|
|
440
442
|
}
|
|
@@ -72,6 +72,8 @@ const governance_history_1 = require("../guardrail/governance-history");
|
|
|
72
72
|
const rtm_check_1 = require("../rtm/rtm-check");
|
|
73
73
|
const ledger_store_1 = require("../governance/ledger-store");
|
|
74
74
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
75
|
+
// @implements A-SPEC-574.3 — the caller owns the store and hands the chain the question.
|
|
76
|
+
const ledger_store_2 = require("../governance/ledger-store");
|
|
75
77
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
76
78
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
77
79
|
const approval_grants_1 = require("../governance/approval-grants");
|
|
@@ -491,7 +493,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
491
493
|
ts: nowTs, actor: acting.actor, kind: 'nonce-consumed',
|
|
492
494
|
summary: `consumed single-use approval for: ${(0, provenance_chain_1.redactTarget)('command', command)}`.slice(0, 200),
|
|
493
495
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(acting.nonce)], rationale: acting.rationale, authorization: (0, provenance_chain_1.authorizationRef)(acting.actor, acting.token),
|
|
494
|
-
});
|
|
496
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
495
497
|
}
|
|
496
498
|
catch (err) {
|
|
497
499
|
// FAIL CLOSED. A gate that cannot prove exclusivity must not grant — degrading to an
|
|
@@ -95,6 +95,20 @@ export interface StopEvidence {
|
|
|
95
95
|
ts: string;
|
|
96
96
|
}>>;
|
|
97
97
|
redFirstMode?: 'strict' | 'track' | 'off';
|
|
98
|
+
/**
|
|
99
|
+
* @implements A-SPEC-574.4
|
|
100
|
+
* Code-graph cycles, scanned by this hook. Absent = not scanned, which the constitution treats as
|
|
101
|
+
* silence rather than as "none" — a project whose scan failed has not been cleared.
|
|
102
|
+
*/
|
|
103
|
+
cycles?: {
|
|
104
|
+
current: import('../cpg/cycle-detect').Cycle[];
|
|
105
|
+
allowed: string[];
|
|
106
|
+
mode: 'strict' | 'track' | 'off';
|
|
107
|
+
scope: {
|
|
108
|
+
judged: string[];
|
|
109
|
+
unavailable: string[];
|
|
110
|
+
};
|
|
111
|
+
};
|
|
98
112
|
}
|
|
99
113
|
/**
|
|
100
114
|
* @implements A-SPEC-534.4
|
|
@@ -160,6 +174,21 @@ export declare function rolledBackLedgers(root: string): string[] | undefined;
|
|
|
160
174
|
* Returns the reason to block with, or null when there is nothing to report.
|
|
161
175
|
*/
|
|
162
176
|
export declare function governanceLostPreflight(specsDir: string, projectRoot?: string): string | null;
|
|
177
|
+
/**
|
|
178
|
+
* One line per ARTICLE, each under its own name.
|
|
179
|
+
*
|
|
180
|
+
* This used to be a single line reading `ART-8 RED-first (track)` for everything in `tracked` —
|
|
181
|
+
* and the cycle ratchet pushes ART-2 findings into that same array, so an import cycle was
|
|
182
|
+
* reported to the operator as a RED-first violation. Two observers sharing one sentence means the
|
|
183
|
+
* sentence is wrong for at least one of them.
|
|
184
|
+
*
|
|
185
|
+
* An article with no label still speaks, under its bare name: a new observer that says nothing is
|
|
186
|
+
* worse than one that says something plain.
|
|
187
|
+
*/
|
|
188
|
+
export declare function trackedLines(tracked?: {
|
|
189
|
+
article: string;
|
|
190
|
+
detail: string;
|
|
191
|
+
}[]): string[];
|
|
163
192
|
export declare function evaluateStop(specs: Spec[], evidence?: StopEvidence): {
|
|
164
193
|
block: boolean;
|
|
165
194
|
reason?: string;
|