@holmes-lab/holmes-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/holmes-hook-antigravity.js +31 -0
- package/bin/holmes-kit.js +23 -0
- package/bin/holmes-mcp.js +34 -0
- package/bin/holmes-stop-antigravity.js +29 -0
- package/dist/.build-id +1 -0
- package/dist/holmes/cli/agents.js +168 -0
- package/dist/holmes/cli/doctor.js +625 -0
- package/dist/holmes/cli/gitignore-merge.js +84 -0
- package/dist/holmes/cli/governed-precondition.js +157 -0
- package/dist/holmes/cli/index.js +384 -0
- package/dist/holmes/cli/init.js +462 -0
- package/dist/holmes/cli/playbook-skills.js +711 -0
- package/dist/holmes/cli/roles-readme.js +134 -0
- package/dist/holmes/cli/settings-merge.js +122 -0
- package/dist/holmes/config/config.js +70 -0
- package/dist/holmes/context/bundler.js +114 -0
- package/dist/holmes/context/render.js +29 -0
- package/dist/holmes/context/tiers.js +110 -0
- package/dist/holmes/context/tokens.js +8 -0
- package/dist/holmes/cpg/cpg-scanner.js +213 -0
- package/dist/holmes/cpg/hash-cache.js +86 -0
- package/dist/holmes/cpg/language-parser-walk.js +917 -0
- package/dist/holmes/cpg/language-parser-worker.js +81 -0
- package/dist/holmes/cpg/language-parser.js +234 -0
- package/dist/holmes/cpg/scan-cache.js +108 -0
- package/dist/holmes/cpg/source-path.js +44 -0
- package/dist/holmes/cpg/test-files.js +84 -0
- package/dist/holmes/governance/constitution-debt.js +73 -0
- package/dist/holmes/governance/constitution-report.js +25 -0
- package/dist/holmes/governance/constitution.js +129 -0
- package/dist/holmes/governance/identity.js +30 -0
- package/dist/holmes/governance/ledger-lock.js +165 -0
- package/dist/holmes/governance/ledger-store.conformance.js +90 -0
- package/dist/holmes/governance/ledger-store.js +106 -0
- package/dist/holmes/governance/progress-ledger.js +83 -0
- package/dist/holmes/governance/provenance-chain.js +365 -0
- package/dist/holmes/governance/provenance-ledger.js +0 -0
- package/dist/holmes/governance/provenance-schema.js +47 -0
- package/dist/holmes/governance/replica-id.js +106 -0
- package/dist/holmes/governance/role-policy.js +137 -0
- package/dist/holmes/governance/trust-score.js +43 -0
- package/dist/holmes/guardrail/anchors.js +31 -0
- package/dist/holmes/guardrail/blind-spots.js +38 -0
- package/dist/holmes/guardrail/decision-ledger.js +107 -0
- package/dist/holmes/guardrail/executable-artifact.js +129 -0
- package/dist/holmes/guardrail/governance-history.js +101 -0
- package/dist/holmes/guardrail/phase.js +169 -0
- package/dist/holmes/guardrail/risk-classifier.js +450 -0
- package/dist/holmes/guardrail/risk-gate.js +160 -0
- package/dist/holmes/guardrail/risk-types.js +6 -0
- package/dist/holmes/guardrail/tspec-state.js +392 -0
- package/dist/holmes/guardrail/write-target.js +224 -0
- package/dist/holmes/hooks/adapters/antigravity.js +194 -0
- package/dist/holmes/hooks/pre-tool-use.js +1262 -0
- package/dist/holmes/hooks/stop.js +416 -0
- package/dist/holmes/mcp/basis.js +162 -0
- package/dist/holmes/mcp/handlers.js +1831 -0
- package/dist/holmes/mcp/server.js +71 -0
- package/dist/holmes/mcp/stdio-client.js +165 -0
- package/dist/holmes/mcp/supervisor.js +178 -0
- package/dist/holmes/mcp/tool-schemas.js +394 -0
- package/dist/holmes/mcp/validate-args.js +281 -0
- package/dist/holmes/messages/registry.js +50 -0
- package/dist/holmes/project/baseline.js +210 -0
- package/dist/holmes/project/change-source.js +233 -0
- package/dist/holmes/project/ignore.js +145 -0
- package/dist/holmes/project/root.js +113 -0
- package/dist/holmes/reverse/anchor.js +162 -0
- package/dist/holmes/reverse/cluster.js +187 -0
- package/dist/holmes/reverse/draft.js +151 -0
- package/dist/holmes/reverse/dynamic-wiring.js +47 -0
- package/dist/holmes/reverse/scan.js +194 -0
- package/dist/holmes/reverse/surface.js +154 -0
- package/dist/holmes/reverse/test-map.js +263 -0
- package/dist/holmes/review/coverage.js +33 -0
- package/dist/holmes/review/findings.js +123 -0
- package/dist/holmes/review/package.js +40 -0
- package/dist/holmes/review/review-targets.js +92 -0
- package/dist/holmes/review/scope.js +57 -0
- package/dist/holmes/review/test-evidence.js +77 -0
- package/dist/holmes/review/test-runner.js +572 -0
- package/dist/holmes/rtm/dataflow-taint.js +262 -0
- package/dist/holmes/rtm/gap-analyzer.js +27 -0
- package/dist/holmes/rtm/git-changes.js +72 -0
- package/dist/holmes/rtm/incremental.js +45 -0
- package/dist/holmes/rtm/localize.js +100 -0
- package/dist/holmes/rtm/rtm-builder.js +191 -0
- package/dist/holmes/rtm/rtm-check.js +89 -0
- package/dist/holmes/rtm/rtm-graph.js +232 -0
- package/dist/holmes/rtm/taint.js +92 -0
- package/dist/holmes/rtm/test-scope.js +336 -0
- package/dist/holmes/spec/approval-blockers.js +204 -0
- package/dist/holmes/spec/breaking-change.js +89 -0
- package/dist/holmes/spec/legacy-format.js +87 -0
- package/dist/holmes/spec/spec-digest.js +71 -0
- package/dist/holmes/spec/spec-parser.js +106 -0
- package/dist/holmes/spec/spec-store.conformance.js +118 -0
- package/dist/holmes/spec/spec-store.js +331 -0
- package/dist/holmes/spec/spec-types.js +177 -0
- package/dist/holmes/spec/validator.js +280 -0
- package/package.json +76 -0
- package/playbooks/adopt/PLAYBOOK.md +125 -0
- package/playbooks/author-slice/PLAYBOOK.md +119 -0
- package/playbooks/promote-slice/PLAYBOOK.md +134 -0
|
@@ -0,0 +1,416 @@
|
|
|
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.MAX_CONSECUTIVE_BLOCKS = void 0;
|
|
37
|
+
exports.governanceLostPreflight = governanceLostPreflight;
|
|
38
|
+
exports.evaluateStop = evaluateStop;
|
|
39
|
+
exports.stopDebtAction = stopDebtAction;
|
|
40
|
+
exports.decideStopGuard = decideStopGuard;
|
|
41
|
+
exports.__setWiredSpecsForTest = __setWiredSpecsForTest;
|
|
42
|
+
exports.readGuardCount = readGuardCount;
|
|
43
|
+
exports.writeGuardCount = writeGuardCount;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const node_child_process_1 = require("node:child_process");
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const test_scope_1 = require("../rtm/test-scope");
|
|
48
|
+
const constitution_1 = require("../governance/constitution");
|
|
49
|
+
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
50
|
+
const test_evidence_1 = require("../review/test-evidence");
|
|
51
|
+
const pre_tool_use_1 = require("./pre-tool-use");
|
|
52
|
+
const governance_history_1 = require("../guardrail/governance-history");
|
|
53
|
+
const constitution_debt_1 = require("../governance/constitution-debt");
|
|
54
|
+
/**
|
|
55
|
+
* @implements A-SPEC-175
|
|
56
|
+
* The turn-boundary answer to "this project is governed but its specs are gone".
|
|
57
|
+
*
|
|
58
|
+
* Extracted so it is reachable by a test: the runtime block below only runs under
|
|
59
|
+
* `require.main === module`, and a check that lives only there is a check nothing can mutate against.
|
|
60
|
+
* Returns the reason to block with, or null when there is nothing to report.
|
|
61
|
+
*/
|
|
62
|
+
function governanceLostPreflight(specsDir, projectRoot) {
|
|
63
|
+
if (fs.existsSync(specsDir))
|
|
64
|
+
return null;
|
|
65
|
+
// @implements A-SPEC-191 §18 (round 11) — the project is not "two directories up from the spec
|
|
66
|
+
// store". That arithmetic assumed a two-segment layout (`.ax/specs`), and `init --specs-dir specs`
|
|
67
|
+
// is an explicitly supported one-segment value: there `../..` names the project's PARENT, so the
|
|
68
|
+
// A-SPEC-175 protection looked for governance history in the wrong tree — silent where it should
|
|
69
|
+
// block, and blocking where it should be silent. The caller knows the root; it passes it.
|
|
70
|
+
const root = projectRoot ?? path.resolve(specsDir, '..', '..');
|
|
71
|
+
return (0, governance_history_1.hasGovernanceHistory)(root) ? governance_history_1.GOVERNANCE_LOST_HINT : null;
|
|
72
|
+
}
|
|
73
|
+
function evaluateStop(specs, evidence) {
|
|
74
|
+
// L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
|
|
75
|
+
// inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
|
|
76
|
+
// The articles live in ONE place (governance/constitution.ts); this gate merely executes them.
|
|
77
|
+
const violations = (0, constitution_1.verifyConstitution)({ specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings });
|
|
78
|
+
const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
|
|
79
|
+
// Tampered audit trail blocks finishing — wires the provenance chain's verify into the live gate
|
|
80
|
+
// (review finding: verify() previously had no product caller, so the chain's guarantee was inert).
|
|
81
|
+
if (evidence?.provenance && !evidence.provenance.ok) {
|
|
82
|
+
problems.push(`[ART-2] provenance chain broken at seq ${evidence.provenance.brokenAt}: ${evidence.provenance.detail}`);
|
|
83
|
+
}
|
|
84
|
+
// @implements A-SPEC-191 (§4a) — an existing-but-unreadable findings ledger is not a clean turn:
|
|
85
|
+
// it may hold an open critical, and a "clean" verdict here would also CLEAR standing ART-7 debt.
|
|
86
|
+
if (evidence?.findingsUnreadable) {
|
|
87
|
+
problems.push('[ART-7] findings 원장을 읽을 수 없습니다 — 열린 치명 발견의 존재를 확인할 수 없는 턴은 깨끗한 턴이 아닙니다 (원장 파일의 권한·형식을 복구하십시오)');
|
|
88
|
+
}
|
|
89
|
+
if (problems.length === 0)
|
|
90
|
+
return { block: false };
|
|
91
|
+
const shown = problems.slice(0, 20);
|
|
92
|
+
const more = problems.length > shown.length ? `\n…and ${problems.length - shown.length} more` : '';
|
|
93
|
+
// @implements A-SPEC-134 — the distinct articles feed the constitution-debt state on a cap-yield.
|
|
94
|
+
const articles = [...new Set(violations.map((x) => x.article)
|
|
95
|
+
.concat(evidence?.provenance && !evidence.provenance.ok ? ['ART-2'] : [])
|
|
96
|
+
.concat(evidence?.findingsUnreadable ? ['ART-7'] : []))];
|
|
97
|
+
return {
|
|
98
|
+
block: true,
|
|
99
|
+
articles,
|
|
100
|
+
reason: `[Holmes-Kit] constitution gate: ${problems.length} article violation(s) must be ` +
|
|
101
|
+
`fixed before finishing:\n${shown.join('\n')}${more}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* @implements A-SPEC-134
|
|
106
|
+
* Pure decision for the constitution-debt state at a turn boundary. Replaces the Stop gate's silent
|
|
107
|
+
* cap-yield: a clean turn clears any standing debt; a cap-yield (the gate giving up its block after
|
|
108
|
+
* MAX_CONSECUTIVE_BLOCKS) RECORDS the unresolved articles as debt so new code is blocked until the
|
|
109
|
+
* constitution is clean again; a normal block (still within the cap) touches nothing.
|
|
110
|
+
*/
|
|
111
|
+
function stopDebtAction(evaluation, guard) {
|
|
112
|
+
if (!evaluation.block)
|
|
113
|
+
return { action: 'clear', articles: [] }; // clean turn → debt paid
|
|
114
|
+
if (guard.capped)
|
|
115
|
+
return { action: 'write', articles: evaluation.articles ?? [] }; // gave up → debt owed
|
|
116
|
+
return { action: 'none', articles: [] }; // still blocking within cap
|
|
117
|
+
}
|
|
118
|
+
// ---- H2: stop loop-guard with a bounded consecutive-block cap ----------------------------------
|
|
119
|
+
// The old guard exited unconditionally when stop_hook_active was set, making the gate a ONE-SHOT
|
|
120
|
+
// speed bump (block once, then the very next stop sails through with the problems unfixed —
|
|
121
|
+
// adversarial review F7). Now consecutive blocked stops are COUNTED in a state file: the gate keeps
|
|
122
|
+
// blocking while problems persist, up to MAX_CONSECUTIVE_BLOCKS, then yields with a loud warning
|
|
123
|
+
// (bounded, so an unfixable state cannot infinite-loop the session). Any clean stop resets the count.
|
|
124
|
+
exports.MAX_CONSECUTIVE_BLOCKS = 3;
|
|
125
|
+
function decideStopGuard(wantsBlock, priorConsecutiveBlocks, cap = exports.MAX_CONSECUTIVE_BLOCKS) {
|
|
126
|
+
if (!wantsBlock)
|
|
127
|
+
return { block: false, nextCount: 0, capped: false };
|
|
128
|
+
if (priorConsecutiveBlocks >= cap)
|
|
129
|
+
return { block: false, nextCount: 0, capped: true }; // yield, reset
|
|
130
|
+
return { block: true, nextCount: priorConsecutiveBlocks + 1, capped: false };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* @implements A-SPEC-191 §14 (round 10)
|
|
134
|
+
* WHERE this hook keeps its state, and it is the project — not the shell.
|
|
135
|
+
*
|
|
136
|
+
* Measured: with a raw relative path, running a session from a subdirectory made this hook create
|
|
137
|
+
* `<cwd>/.ax/ledger/stop-guard.json` on every CLEAN turn. That directory is a project marker, so the
|
|
138
|
+
* NEXT tool call's walk-up stopped there, and the PreToolUse gate then classified nearly the whole
|
|
139
|
+
* real project as "outside the project" — roles policy, .mcp.json, the ledger, spec promotion and
|
|
140
|
+
* ordinary source writes all flipped from deny to allow, one turn after the session started. The
|
|
141
|
+
* round-9 anchor fix went into PreToolUse only; this hook quietly undid it.
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* @implements A-SPEC-191 §19 (round 11, 3rd)
|
|
145
|
+
* The project this turn belongs to, or NULL when there is none to be found.
|
|
146
|
+
*
|
|
147
|
+
* Round-10 walked up for a `.ax` marker, and `resolveProjectRoot` returns its INPUT when the walk
|
|
148
|
+
* finds nothing. So on the first-class `--specs-dir docs/specs` deployment — where `.ax` may not
|
|
149
|
+
* exist at all (its contents are derived state the generated .gitignore ignores, and `.ax/roles` is
|
|
150
|
+
* optional) — a clean turn in a subdirectory wrote `<sub>/.ax/ledger/stop-guard.json` and thereby
|
|
151
|
+
* MINTED a project marker. From then on the nearest-marker walk stopped there: the real root could
|
|
152
|
+
* later gain its own `.ax` and still lose, and measured, that subdirectory session had No-Spec-No-Code
|
|
153
|
+
* and config-write open forever. §15(b) excused this on the premise that "nothing above is a
|
|
154
|
+
* project"; the premise was false — a real project above simply had no marker to find.
|
|
155
|
+
*
|
|
156
|
+
* So: look for the marker, then for the wired spec directory (the deployment's own evidence of where
|
|
157
|
+
* the project is), and if neither answers, say so. A hook that cannot find the project does not
|
|
158
|
+
* invent one; it keeps no state and falls back to the stateless one-shot guard.
|
|
159
|
+
*/
|
|
160
|
+
function findProjectRoot(specsDir) {
|
|
161
|
+
try {
|
|
162
|
+
const { resolveProjectRoot } = require('../project/root');
|
|
163
|
+
const r = resolveProjectRoot(process.cwd());
|
|
164
|
+
if (r.marker !== 'given')
|
|
165
|
+
return r.root;
|
|
166
|
+
}
|
|
167
|
+
catch { /* fall through to the specs-dir walk */ }
|
|
168
|
+
if (specsDir && !path.isAbsolute(specsDir)) {
|
|
169
|
+
let dir = process.cwd();
|
|
170
|
+
for (let hop = 0; hop < 64; hop++) {
|
|
171
|
+
if (fs.existsSync(path.join(dir, specsDir)))
|
|
172
|
+
return dir;
|
|
173
|
+
const parent = path.dirname(dir);
|
|
174
|
+
if (parent === dir)
|
|
175
|
+
break;
|
|
176
|
+
dir = parent;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// @implements A-SPEC-191 §21 (round 11) — `dirname(dirname(specsDir))` is the same two-segment
|
|
180
|
+
// arithmetic §18 deleted from `governanceLostPreflight`, and it came back on the absolute branch.
|
|
181
|
+
// On the first-class one-segment deployment (`--specs-dir specs`) it names the project's PARENT,
|
|
182
|
+
// and §19's own minting then plants `.ax/ledger/stop-guard.json` there — measured: a project at
|
|
183
|
+
// `<outer>/proj` (marker and all) had the anchor moved to `<outer>` by one clean turn. Cut a tail
|
|
184
|
+
// only after confirming it is there; otherwise ask the marker, and if nothing answers, say so.
|
|
185
|
+
if (specsDir && path.isAbsolute(specsDir) && fs.existsSync(specsDir)) {
|
|
186
|
+
const norm = specsDir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
187
|
+
if (/\/\.ax\/specs$/i.test(norm))
|
|
188
|
+
return path.dirname(path.dirname(norm));
|
|
189
|
+
try {
|
|
190
|
+
const { resolveProjectRoot } = require('../project/root');
|
|
191
|
+
const r = resolveProjectRoot(norm);
|
|
192
|
+
if (r.marker !== 'given')
|
|
193
|
+
return r.root;
|
|
194
|
+
}
|
|
195
|
+
catch { /* 표지가 없으면 짓지 않는다 */ }
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
/** The wired spec directory for this run, set once by the CLI so the lookups above can use it. */
|
|
201
|
+
let WIRED_SPECS;
|
|
202
|
+
/** Test seam for the same reason as the two exports below — the CLI sets this on a real run. */
|
|
203
|
+
function __setWiredSpecsForTest(v) { WIRED_SPECS = v; }
|
|
204
|
+
function stopProjectRoot() {
|
|
205
|
+
return findProjectRoot(WIRED_SPECS) ?? process.cwd();
|
|
206
|
+
}
|
|
207
|
+
/** null when no project could be found — nothing is written, nothing is minted. */
|
|
208
|
+
function guardStatePath() {
|
|
209
|
+
const root = findProjectRoot(WIRED_SPECS);
|
|
210
|
+
return root === null ? null : path.join(root, '.ax', 'ledger', 'stop-guard.json');
|
|
211
|
+
}
|
|
212
|
+
const GUARD_STATE = () => guardStatePath() ?? path.join(process.cwd(), '.ax', 'ledger', 'stop-guard.json');
|
|
213
|
+
// State is keyed by SESSION so two concurrent sessions in one repo don't share/reset each other's
|
|
214
|
+
// count (adversarial finding MED-5). A different session id starts from 0.
|
|
215
|
+
//
|
|
216
|
+
// @implements A-SPEC-191 §18 (round 11) — keyed for READING is not keyed for STORING. The file held
|
|
217
|
+
// ONE {sessionId, consecutiveBlocks} cell, so two sessions taking turns each read 0 (the id does not
|
|
218
|
+
// match) and wrote 1 (overwriting the other) — the count could never reach the cap and the Stop gate
|
|
219
|
+
// blocked FOREVER. That is precisely the unbounded loop H2 exists to prevent, on the ordinary
|
|
220
|
+
// two-sessions-in-one-repo case, and round-10 widened it from same-cwd to project-wide by moving
|
|
221
|
+
// every session into a single project-level file. One cell per session, oldest evicted.
|
|
222
|
+
const GUARD_SESSIONS_MAX = 32;
|
|
223
|
+
function readGuardState() {
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(fs.readFileSync(GUARD_STATE(), 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function readGuardCount(sessionId) {
|
|
232
|
+
const s = readGuardState();
|
|
233
|
+
const entry = s.sessions?.[sessionId];
|
|
234
|
+
if (entry && Number.isInteger(entry.n) && entry.n >= 0)
|
|
235
|
+
return entry.n;
|
|
236
|
+
// Pre-§18 files carried a single cell; honour it for the session that owns it so an upgrade in
|
|
237
|
+
// mid-session does not silently reset a count the operator is already living with.
|
|
238
|
+
if (s.sessionId === sessionId && Number.isInteger(s.consecutiveBlocks) && (s.consecutiveBlocks ?? -1) >= 0) {
|
|
239
|
+
return s.consecutiveBlocks;
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
// Returns whether the state actually persisted. If it did NOT, the caller must fall back to the
|
|
244
|
+
// stateless one-shot guard (stop_hook_active) — otherwise an unwritable state file would make the
|
|
245
|
+
// count always read 0 and the gate block FOREVER (adversarial finding MED-4: the exact infinite
|
|
246
|
+
// loop H2 exists to prevent).
|
|
247
|
+
/**
|
|
248
|
+
* Exported for the §19 race discriminator: the persistence layer was the untested half (round-11),
|
|
249
|
+
* and a test that can only reach it through a spawned hook cannot pin what the lock does.
|
|
250
|
+
*/
|
|
251
|
+
function writeGuardCount(sessionId, n) {
|
|
252
|
+
// @implements A-SPEC-191 §19 — no project, no state. Minting `<cwd>/.ax` to hold a counter is how
|
|
253
|
+
// the gate lost the project in the first place; returning false puts the caller on the stateless
|
|
254
|
+
// one-shot guard, which is the documented fallback for "the state did not persist".
|
|
255
|
+
const target = guardStatePath();
|
|
256
|
+
if (target === null)
|
|
257
|
+
return false;
|
|
258
|
+
try {
|
|
259
|
+
// @implements A-SPEC-191 §19 — read-modify-write under the SAME lock the findings ledger uses.
|
|
260
|
+
// Round-10 moved every session into one project-level file and round-11 gave each its own cell,
|
|
261
|
+
// but the merge was unguarded: two hooks overlapping meant the later writer dropped the other's
|
|
262
|
+
// cell, so a session's count kept resetting and the cap never fired — measured 12/12 consecutive
|
|
263
|
+
// blocks with three concurrent sessions, which is exactly the unbounded loop H2 exists to stop.
|
|
264
|
+
const { withLedgerLock } = require('../governance/ledger-lock');
|
|
265
|
+
return withLedgerLock(target, () => {
|
|
266
|
+
const prev = readGuardState();
|
|
267
|
+
const sessions = { ...(prev.sessions ?? {}) };
|
|
268
|
+
sessions[sessionId] = { n, at: Date.now() };
|
|
269
|
+
// Bounded: a long-lived repository must not accumulate a cell per session forever. The oldest
|
|
270
|
+
// are dropped, never the caller's own.
|
|
271
|
+
const ordered = Object.entries(sessions).sort((a, b) => b[1].at - a[1].at).slice(0, GUARD_SESSIONS_MAX);
|
|
272
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
273
|
+
fs.writeFileSync(target, JSON.stringify({ sessions: Object.fromEntries(ordered) }));
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// CLI entry: Claude Code passes Stop-hook JSON on stdin. Fail-open (never crash a turn).
|
|
282
|
+
if (require.main === module) {
|
|
283
|
+
let buf = '';
|
|
284
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
285
|
+
process.stdin.on('end', () => {
|
|
286
|
+
try {
|
|
287
|
+
const input = JSON.parse(buf || '{}');
|
|
288
|
+
const sessionId = typeof input.session_id === 'string' ? input.session_id : 'unknown-session';
|
|
289
|
+
// @implements A-SPEC-190 (round 8) — same wiring channel as the PreToolUse hook.
|
|
290
|
+
const { wiredSpecsDir } = require('./pre-tool-use');
|
|
291
|
+
// @implements A-SPEC-191 §16 (round 11) — the INPUTS move with the outputs, or the hook judges
|
|
292
|
+
// one tree and writes the verdict onto another. Round-10 moved the state paths to the project
|
|
293
|
+
// and left the spec path relative to the shell: a subdirectory turn then saw an EMPTY project,
|
|
294
|
+
// called the turn clean, and applied that clean verdict to the REAL root — deleting its
|
|
295
|
+
// constitution-debt record and reopening WRITE_CODE. Before round 10 the same miss was inert
|
|
296
|
+
// (it wrote to cwd too); the repair is what turned an inert miss into active laundering.
|
|
297
|
+
const wired = wiredSpecsDir(process.argv, process.env);
|
|
298
|
+
// The wiring is also EVIDENCE of where the project is (§19): a deployment that keeps its specs
|
|
299
|
+
// in `docs/specs` says so on this hook's own command line, and that is a marker no `.ax`
|
|
300
|
+
// gitignore can erase. Set before any path is resolved.
|
|
301
|
+
WIRED_SPECS = wired;
|
|
302
|
+
const specsDir = path.isAbsolute(wired) ? wired : path.join(stopProjectRoot(), wired);
|
|
303
|
+
// @implements A-SPEC-175
|
|
304
|
+
// BEFORE the constitution runs, because with the spec tree gone there are no articles to check
|
|
305
|
+
// and this hook simply exited 0 in silence — measured 2026-08-12, while the PreToolUse escape
|
|
306
|
+
// hatch justified itself by claiming this hook re-verifies. It does, but only when there is
|
|
307
|
+
// something to verify; a project whose ledger records approvals and whose specs have vanished
|
|
308
|
+
// is exactly the case that fell between the two.
|
|
309
|
+
const lost = governanceLostPreflight(specsDir, stopProjectRoot());
|
|
310
|
+
if (lost) {
|
|
311
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason: lost }));
|
|
312
|
+
process.stderr.write(`${lost}\n`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const specs = (0, pre_tool_use_1.readSpecsSync)(specsDir);
|
|
316
|
+
// H1 evidence: count EXECUTABLE test cases per anchored A-SPEC (countTestCases strips comments/
|
|
317
|
+
// strings — sham-resistant). A scan FAILURE yields `undefined` (ART-4 skipped this turn, true
|
|
318
|
+
// fail-open); a SUCCESSFUL scan that finds nothing yields `{}` and DOES enforce — all evidence
|
|
319
|
+
// vanishing while approved T-SPECs exist is a genuine violation state, not a hiccup.
|
|
320
|
+
let testCasesByAspec;
|
|
321
|
+
try {
|
|
322
|
+
testCasesByAspec = {};
|
|
323
|
+
for (const [file, ids] of Object.entries((0, test_scope_1.scanTestAnchors)(stopProjectRoot()))) {
|
|
324
|
+
let n = 0;
|
|
325
|
+
// Pass the path: the counter picks the test framework from the extension, and without it
|
|
326
|
+
// every non-JS suite counts zero and ART-4 reports "no tests" for a project that has them.
|
|
327
|
+
try {
|
|
328
|
+
n = (0, test_scope_1.countTestCases)(fs.readFileSync(file, 'utf8'), file);
|
|
329
|
+
}
|
|
330
|
+
catch { /* unreadable file: skip */ }
|
|
331
|
+
for (const id of ids)
|
|
332
|
+
testCasesByAspec[id] = (testCasesByAspec[id] ?? 0) + n;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
testCasesByAspec = undefined;
|
|
337
|
+
}
|
|
338
|
+
// EXECUTION evidence (ART-4 upgrade): usable only when produced at the CURRENT git HEAD, so a
|
|
339
|
+
// stale record can never vouch for changed code. Absent/stale -> syntactic fallback.
|
|
340
|
+
let executedByAspec;
|
|
341
|
+
try {
|
|
342
|
+
const head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: stopProjectRoot(), stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
343
|
+
const ev = (0, test_evidence_1.readTestEvidence)(stopProjectRoot());
|
|
344
|
+
if ((0, test_evidence_1.isFresh)(ev, head))
|
|
345
|
+
executedByAspec = ev.executedByAspec;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
executedByAspec = undefined;
|
|
349
|
+
}
|
|
350
|
+
// Provenance-chain verification (fail-open: a verify error skips the check, never crashes).
|
|
351
|
+
let provenance;
|
|
352
|
+
// @implements A-SPEC-148
|
|
353
|
+
// Verify EVERY replica chain: after the split a single file is only part of the ledger, and a
|
|
354
|
+
// broken chain elsewhere must still block finishing. `ok` collapses the per-chain results in
|
|
355
|
+
// the safe direction — one bad chain means the audit trail is not intact.
|
|
356
|
+
try {
|
|
357
|
+
const { FileLedgerStore } = require('../governance/ledger-store');
|
|
358
|
+
// Same rule for the tamper check (round-11): verifying the shell's `.ax/ledger` let a forged
|
|
359
|
+
// chain in the real project pass unseen from a subdirectory.
|
|
360
|
+
const r = new FileLedgerStore(path.join(stopProjectRoot(), path.dirname(provenance_chain_1.PROVENANCE_FILE))).verifyAll();
|
|
361
|
+
provenance = r.ok ? { ok: true } : { ok: false, detail: r.broken.map((b) => `${b.replicaId ?? 'legacy'}: ${b.detail ?? 'broken'}`).join('; ') };
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
provenance = undefined;
|
|
365
|
+
}
|
|
366
|
+
// @implements A-SPEC-160 — supply the findings ledger so ART-6 can see judgements sealed on a
|
|
367
|
+
// diverged server. An article nobody feeds is a dead article; ADR-014's discipline applies to
|
|
368
|
+
// the constitution too.
|
|
369
|
+
let findings;
|
|
370
|
+
let findingsUnreadable = false;
|
|
371
|
+
try {
|
|
372
|
+
const { FindingsLedger } = require('../review/findings');
|
|
373
|
+
findings = new FindingsLedger(path.join(stopProjectRoot(), '.ax', 'ledger', 'findings.jsonl')).list();
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
findingsUnreadable = true;
|
|
377
|
+
} // @implements A-SPEC-191 (§4a) — list() absorbs ENOENT; a throw means the ledger EXISTS and cannot be read, which must block, not launder
|
|
378
|
+
const out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable });
|
|
379
|
+
const guard = decideStopGuard(out.block, readGuardCount(sessionId));
|
|
380
|
+
const persisted = writeGuardCount(sessionId, guard.nextCount);
|
|
381
|
+
// @implements A-SPEC-134 — the cap-yield is no longer silent: a clean turn clears any debt, a
|
|
382
|
+
// give-up records the unresolved articles so WRITE_CODE is blocked until the constitution is
|
|
383
|
+
// clean again. Fail-open: a debt-state write error never changes the stop verdict.
|
|
384
|
+
try {
|
|
385
|
+
const debt = stopDebtAction(out, guard);
|
|
386
|
+
if (debt.action === 'clear')
|
|
387
|
+
(0, constitution_debt_1.clearDebt)(stopProjectRoot());
|
|
388
|
+
else if (debt.action === 'write')
|
|
389
|
+
(0, constitution_debt_1.writeDebt)(stopProjectRoot(), debt.articles);
|
|
390
|
+
}
|
|
391
|
+
catch { /* debt bookkeeping must never break the gate */ }
|
|
392
|
+
if (guard.block && !persisted && input.stop_hook_active) {
|
|
393
|
+
// State cannot persist -> the counter can never advance. Degrade to the stateless one-shot
|
|
394
|
+
// guard instead of blocking forever: stop_hook_active means we already blocked once.
|
|
395
|
+
process.stderr.write(`[Holmes-Kit] governance gate yielding (guard state unwritable, one-shot fallback) — issues remain UNRESOLVED:\n${out.reason ?? ''}\n`);
|
|
396
|
+
process.exitCode = 0;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (guard.block) {
|
|
400
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason: out.reason }));
|
|
401
|
+
process.stderr.write(`${out.reason}\n`);
|
|
402
|
+
}
|
|
403
|
+
else if (guard.capped) {
|
|
404
|
+
process.stderr.write(`[Holmes-Kit] governance gate YIELDING after ${exports.MAX_CONSECUTIVE_BLOCKS} consecutive blocks — issues remain UNRESOLVED:\n${out.reason ?? ''}\n`);
|
|
405
|
+
}
|
|
406
|
+
// exitCode, NOT process.exit(): exit() tears the process down before large async stdout
|
|
407
|
+
// writes flush — a >64KB block decision reached the hook runner as TRUNCATED (invalid) JSON
|
|
408
|
+
// and the block silently became a pass (round-3). Natural exit drains the pipe first.
|
|
409
|
+
process.exitCode = 0;
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
process.stderr.write(`[Holmes-Kit Stop Hook] Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
413
|
+
process.exitCode = 0; // fail-open — exitCode 로 파이프를 비운 뒤 자연 종료(잘림 방지)
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
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.BASIS_DIVERGED = exports.MAX_BUILD_LEN = exports.MAX_DIGEST_LEN = void 0;
|
|
37
|
+
exports.basisDiverged = basisDiverged;
|
|
38
|
+
exports.loadedBuildId = loadedBuildId;
|
|
39
|
+
exports.collectBasis = collectBasis;
|
|
40
|
+
exports.basisDigest = basisDigest;
|
|
41
|
+
exports.withBasis = withBasis;
|
|
42
|
+
// @implements A-SPEC-156
|
|
43
|
+
const fs = __importStar(require("node:fs"));
|
|
44
|
+
const path = __importStar(require("node:path"));
|
|
45
|
+
/**
|
|
46
|
+
* There is deliberately NO graph axis. `RtmGraph` is in-memory only (ADR-011 — a derived index,
|
|
47
|
+
* rebuilt every time), so the graph is a FUNCTION of specs and scan and cannot diverge from them
|
|
48
|
+
* independently. Including it would mean either an always-`?` field or a rebuild per call: token
|
|
49
|
+
* cost with no information in the first case, a constant-I/O violation in the second.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Cap on the digest. A basis rides on every one of the tools, so its size multiplies across a
|
|
53
|
+
* session — this bound is what keeps "carry the basis" compatible with REQ-122's requirement that
|
|
54
|
+
* the harness be net-token-positive, rather than opposed to it.
|
|
55
|
+
*/
|
|
56
|
+
exports.MAX_DIGEST_LEN = 64;
|
|
57
|
+
/**
|
|
58
|
+
* Cap on the build id alone. Truncating the whole digest at the end would eat the trailing axes, so
|
|
59
|
+
* a long build id could make `x:` disappear entirely and the digest would read as if that axis had
|
|
60
|
+
* never existed — the exact silent-omission failure `?` exists to prevent. Bounding the variable
|
|
61
|
+
* part instead keeps the structure intact whatever the stamp looks like.
|
|
62
|
+
*/
|
|
63
|
+
exports.MAX_BUILD_LEN = 32;
|
|
64
|
+
/**
|
|
65
|
+
* @implements A-SPEC-160
|
|
66
|
+
* Appended when the loaded and on-disk builds are BOTH known and differ.
|
|
67
|
+
*
|
|
68
|
+
* This is an observation, not a verdict, and the distinction matters: REQ-156 deliberately refused to
|
|
69
|
+
* let the server decide whether it was stale, because that logic would be running inside the stale
|
|
70
|
+
* build. Reporting that two measured values disagree is a fact; what to do about it is ART-6's call.
|
|
71
|
+
*
|
|
72
|
+
* An unknown disk build never carries the marker. Reading absence as divergence would fire on every
|
|
73
|
+
* tool but `review_record` — none of the others read disk — and an article that fires constantly is
|
|
74
|
+
* one people route around within a day.
|
|
75
|
+
*/
|
|
76
|
+
exports.BASIS_DIVERGED = '/!';
|
|
77
|
+
/** Reads the marker back out of a sealed digest. */
|
|
78
|
+
function basisDiverged(digest) {
|
|
79
|
+
return typeof digest === 'string' && digest.endsWith(exports.BASIS_DIVERGED);
|
|
80
|
+
}
|
|
81
|
+
const BUILD_ID_FILE = path.join('dist', '.build-id');
|
|
82
|
+
/**
|
|
83
|
+
* The build identity of the code that is RUNNING. Read once at start-up by the server, never per
|
|
84
|
+
* call: hashing `dist` on every request buys I/O for no new information, and mtime changes on a
|
|
85
|
+
* checkout or a copy, which would manufacture divergence that is not there.
|
|
86
|
+
*
|
|
87
|
+
* An unstamped build is reported as `unknown` rather than guessed. Absence is a fact too, and a
|
|
88
|
+
* fabricated id would be worse than none — it would look like agreement.
|
|
89
|
+
*/
|
|
90
|
+
function loadedBuildId(root) {
|
|
91
|
+
try {
|
|
92
|
+
const raw = fs.readFileSync(path.join(root, BUILD_ID_FILE), 'utf8').trim();
|
|
93
|
+
if (raw === '')
|
|
94
|
+
return 'unknown';
|
|
95
|
+
return raw.length <= exports.MAX_BUILD_LEN ? raw : `${raw.slice(0, exports.MAX_BUILD_LEN - 1)}~`;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return 'unknown';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Runs one axis, converting any failure into absence. */
|
|
102
|
+
const attempt = (f) => {
|
|
103
|
+
if (!f)
|
|
104
|
+
return undefined;
|
|
105
|
+
try {
|
|
106
|
+
return f();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Basis is diagnostic. If collecting it could fail a request, adding it would make the harness
|
|
110
|
+
// LESS available than before — the exact opposite of the point.
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
function collectBasis(ctx) {
|
|
115
|
+
const basis = { loadedBuild: ctx.loadedBuild ?? (ctx.root ? loadedBuildId(ctx.root) : 'unknown') };
|
|
116
|
+
const scanFp = attempt(ctx.scanFp);
|
|
117
|
+
if (scanFp !== undefined)
|
|
118
|
+
basis.scanFp = scanFp;
|
|
119
|
+
const specs = attempt(ctx.specs);
|
|
120
|
+
if (specs !== undefined)
|
|
121
|
+
basis.specs = specs;
|
|
122
|
+
// The on-disk value is deliberately NOT compared here. If the server rendered the "am I stale?"
|
|
123
|
+
// verdict, that logic would itself be running inside the stale build — the exact circle that let
|
|
124
|
+
// today's defect answer confidently. Both values are exposed; the caller draws the conclusion.
|
|
125
|
+
if (ctx.root)
|
|
126
|
+
basis.diskBuild = loadedBuildId(ctx.root);
|
|
127
|
+
return basis;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `b:<build>/s:<scanFp>/x:<specs>`
|
|
131
|
+
*
|
|
132
|
+
* Short by design: a caller compares the value against another and learns that something diverged
|
|
133
|
+
* without needing to interpret any field. An unmeasured axis renders as `?` rather than being
|
|
134
|
+
* dropped — omitting it would let a reader take "these two matched" for agreement when the third
|
|
135
|
+
* was never measured at all.
|
|
136
|
+
*/
|
|
137
|
+
function basisDigest(b) {
|
|
138
|
+
const diverged = b.diskBuild !== undefined && b.diskBuild !== b.loadedBuild ? exports.BASIS_DIVERGED : '';
|
|
139
|
+
const digest = `b:${b.loadedBuild}/s:${b.scanFp ?? '?'}/x:${b.specs ?? '?'}`;
|
|
140
|
+
const capped = digest.length <= exports.MAX_DIGEST_LEN ? digest : `${digest.slice(0, exports.MAX_DIGEST_LEN - 1)}~`;
|
|
141
|
+
// The marker is appended AFTER capping so a long build id can never truncate it away — losing it
|
|
142
|
+
// would silently downgrade a divergent record to an agreeing one, which is the one direction this
|
|
143
|
+
// must never fail in.
|
|
144
|
+
return capped + diverged;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Attaches a basis to a handler's result.
|
|
148
|
+
*
|
|
149
|
+
* Exported and pure so the contract is testable without a handler that exercises each branch. The
|
|
150
|
+
* array guard in particular has no live caller today — every tool returns an object — and an
|
|
151
|
+
* untested guard is one a later change deletes for free. Spreading an array into an object yields
|
|
152
|
+
* `{0:…, 1:…, basis}`, silently reshaping a response into something no caller can read, so the
|
|
153
|
+
* branch has to survive on its own merits rather than on nobody having tried it yet.
|
|
154
|
+
*/
|
|
155
|
+
function withBasis(fn, basisOf) {
|
|
156
|
+
return async (a) => {
|
|
157
|
+
const out = await fn(a);
|
|
158
|
+
if (out === null || typeof out !== 'object' || Array.isArray(out))
|
|
159
|
+
return out;
|
|
160
|
+
return { ...out, basis: basisDigest(basisOf(a)) };
|
|
161
|
+
};
|
|
162
|
+
}
|