@holmes-lab/holmes-kit 0.1.6 → 0.1.8
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 +14 -0
- package/README.md +13 -9
- package/bin/holmes-mcp.js +0 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +50 -0
- package/dist/holmes/cli/ci-gate.d.ts +18 -0
- package/dist/holmes/cli/doctor.d.ts +44 -0
- package/dist/holmes/cli/gitignore-merge.d.ts +18 -0
- package/dist/holmes/cli/governed-precondition.d.ts +27 -0
- package/dist/holmes/cli/index.d.ts +14 -0
- package/dist/holmes/cli/index.js +23 -1
- package/dist/holmes/cli/init.d.ts +69 -0
- package/dist/holmes/cli/interactive-prompt.d.ts +9 -0
- package/dist/holmes/cli/playbook-skills.d.ts +129 -0
- package/dist/holmes/cli/roles-readme.d.ts +12 -0
- package/dist/holmes/cli/serve.d.ts +14 -0
- package/dist/holmes/cli/serve.js +70 -0
- package/dist/holmes/cli/settings-merge.d.ts +66 -0
- package/dist/holmes/config/config.d.ts +13 -0
- package/dist/holmes/context/bundler.d.ts +40 -0
- package/dist/holmes/context/render.d.ts +9 -0
- package/dist/holmes/context/tiers.d.ts +54 -0
- package/dist/holmes/context/tokens.d.ts +2 -0
- package/dist/holmes/cpg/ast-mutation.d.ts +31 -0
- package/dist/holmes/cpg/ast-mutation.js +126 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +69 -0
- package/dist/holmes/cpg/dynamic-cpg.d.ts +18 -0
- package/dist/holmes/cpg/dynamic-cpg.js +82 -0
- package/dist/holmes/cpg/hash-cache.d.ts +21 -0
- package/dist/holmes/cpg/language-parser-walk.d.ts +31 -0
- package/dist/holmes/cpg/language-parser-worker.d.ts +1 -0
- package/dist/holmes/cpg/language-parser.d.ts +116 -0
- package/dist/holmes/cpg/program-slicing.d.ts +21 -0
- package/dist/holmes/cpg/program-slicing.js +123 -0
- package/dist/holmes/cpg/scan-cache.d.ts +21 -0
- package/dist/holmes/cpg/source-path.d.ts +2 -0
- package/dist/holmes/cpg/test-files.d.ts +36 -0
- package/dist/holmes/governance/constitution-debt.d.ts +3 -0
- package/dist/holmes/governance/constitution-report.d.ts +23 -0
- package/dist/holmes/governance/constitution.d.ts +61 -0
- package/dist/holmes/governance/identity.d.ts +28 -0
- package/dist/holmes/governance/ledger-lock.d.ts +55 -0
- package/dist/holmes/governance/ledger-store.conformance.d.ts +11 -0
- package/dist/holmes/governance/ledger-store.d.ts +114 -0
- package/dist/holmes/governance/progress-ledger.d.ts +25 -0
- package/dist/holmes/governance/provenance-chain.d.ts +154 -0
- package/dist/holmes/governance/provenance-ledger.d.ts +102 -0
- package/dist/holmes/governance/provenance-schema.d.ts +21 -0
- package/dist/holmes/governance/replica-id.d.ts +20 -0
- package/dist/holmes/governance/role-policy.d.ts +52 -0
- package/dist/holmes/governance/trust-score.d.ts +32 -0
- package/dist/holmes/guardrail/anchors.d.ts +8 -0
- package/dist/holmes/guardrail/blind-spots.d.ts +32 -0
- package/dist/holmes/guardrail/decision-ledger.d.ts +29 -0
- package/dist/holmes/guardrail/executable-artifact.d.ts +1 -0
- package/dist/holmes/guardrail/governance-history.d.ts +15 -0
- package/dist/holmes/guardrail/phase.d.ts +27 -0
- package/dist/holmes/guardrail/risk-classifier.d.ts +52 -0
- package/dist/holmes/guardrail/risk-gate.d.ts +77 -0
- package/dist/holmes/guardrail/risk-types.d.ts +48 -0
- package/dist/holmes/guardrail/tspec-state.d.ts +71 -0
- package/dist/holmes/guardrail/write-target.d.ts +77 -0
- package/dist/holmes/hooks/adapters/antigravity.d.ts +42 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +144 -0
- package/dist/holmes/hooks/stop.d.ts +91 -0
- package/dist/holmes/mcp/basis.d.ts +100 -0
- package/dist/holmes/mcp/handlers.d.ts +482 -0
- package/dist/holmes/mcp/server.d.ts +2 -0
- package/dist/holmes/mcp/stdio-client.d.ts +51 -0
- package/dist/holmes/mcp/supervisor.d.ts +48 -0
- package/dist/holmes/mcp/tool-schemas.d.ts +12 -0
- package/dist/holmes/mcp/validate-args.d.ts +27 -0
- package/dist/holmes/messages/registry.d.ts +23 -0
- package/dist/holmes/project/baseline.d.ts +66 -0
- package/dist/holmes/project/change-source.d.ts +89 -0
- package/dist/holmes/project/ignore.d.ts +33 -0
- package/dist/holmes/project/root.d.ts +34 -0
- package/dist/holmes/reverse/anchor.d.ts +47 -0
- package/dist/holmes/reverse/cluster.d.ts +45 -0
- package/dist/holmes/reverse/draft.d.ts +33 -0
- package/dist/holmes/reverse/dynamic-wiring.d.ts +18 -0
- package/dist/holmes/reverse/scan.d.ts +103 -0
- package/dist/holmes/reverse/surface.d.ts +91 -0
- package/dist/holmes/reverse/test-map.d.ts +91 -0
- package/dist/holmes/review/coverage.d.ts +21 -0
- package/dist/holmes/review/findings.d.ts +49 -0
- package/dist/holmes/review/package.d.ts +26 -0
- package/dist/holmes/review/review-targets.d.ts +28 -0
- package/dist/holmes/review/scope.d.ts +13 -0
- package/dist/holmes/review/test-evidence.d.ts +31 -0
- package/dist/holmes/review/test-runner.d.ts +141 -0
- package/dist/holmes/rtm/dataflow-taint.d.ts +81 -0
- package/dist/holmes/rtm/gap-analyzer.d.ts +28 -0
- package/dist/holmes/rtm/git-changes.d.ts +33 -0
- package/dist/holmes/rtm/heatmap.d.ts +21 -0
- package/dist/holmes/rtm/incremental.d.ts +38 -0
- package/dist/holmes/rtm/localize.d.ts +36 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +101 -0
- package/dist/holmes/rtm/rtm-check.d.ts +13 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +88 -0
- package/dist/holmes/rtm/taint.d.ts +35 -0
- package/dist/holmes/rtm/test-scope.d.ts +85 -0
- package/dist/holmes/server/dashboard.d.ts +30 -0
- package/dist/holmes/server/dashboard.js +1758 -0
- package/dist/holmes/spec/approval-blockers.d.ts +62 -0
- package/dist/holmes/spec/breaking-change.d.ts +42 -0
- package/dist/holmes/spec/legacy-format.d.ts +93 -0
- package/dist/holmes/spec/spec-digest.d.ts +29 -0
- package/dist/holmes/spec/spec-parser.d.ts +12 -0
- package/dist/holmes/spec/spec-store.conformance.d.ts +10 -0
- package/dist/holmes/spec/spec-store.d.ts +130 -0
- package/dist/holmes/spec/spec-types.d.ts +150 -0
- package/dist/holmes/spec/validator.d.ts +65 -0
- package/package.json +5 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export interface CodeSymbol {
|
|
2
|
+
name: string;
|
|
3
|
+
kind: 'function' | 'class' | 'method';
|
|
4
|
+
qualifiedName: string;
|
|
5
|
+
startLine: number;
|
|
6
|
+
endLine: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A call/import relation recovered from a parse tree.
|
|
10
|
+
*
|
|
11
|
+
* @implements A-SPEC-136
|
|
12
|
+
* WIRED END-TO-END. `CpgScanner.scan` populates `ScannedFile.edges` via `extractEdges`;
|
|
13
|
+
* `addCallEdges` resolves each bare callee name to a graph edge under a precision-over-recall rule
|
|
14
|
+
* (same-file first, else the single unique definition across the scan, else nothing); and
|
|
15
|
+
* `impactedBy` traverses `callersOf`, so a change to an unanchored callee reaches its caller's spec.
|
|
16
|
+
* Measured: the self-repo carries ~2260 `calls` edges. Only `calls` edges are traversed for impact;
|
|
17
|
+
* `imports` are recorded but advisory.
|
|
18
|
+
*
|
|
19
|
+
* Traceability remains a file-level `@implements` claim as the FLOOR — a call the walk cannot resolve
|
|
20
|
+
* leaves a gap L6 surfaces, never a wrong edge (a wrong edge poisons every impact answer through it).
|
|
21
|
+
* Call-edge extraction covers TS/JS and Python (bespoke walks, for their import handling) and
|
|
22
|
+
* Go/Rust/Java/C#/C++ (table-driven `EDGE_CONFIG` + `walkEdges` in language-parser-walk.js). The
|
|
23
|
+
* corrected wiring pin lives in rtm-builder.test.ts ('call edges ARE scanned, resolved, and
|
|
24
|
+
* traversed'); a prior version of this note wrongly said edges were unwired and misled an audit.
|
|
25
|
+
*/
|
|
26
|
+
export interface CodeEdge {
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
rel: 'calls' | 'imports';
|
|
30
|
+
}
|
|
31
|
+
export type TsLang = 'typescript' | 'tsx' | 'python' | 'csharp' | 'java' | 'go' | 'rust' | 'cpp';
|
|
32
|
+
/**
|
|
33
|
+
* Def-use FACTS for one file — what a taint fixpoint needs, with no analysis applied yet.
|
|
34
|
+
*
|
|
35
|
+
* @implements A-SPEC-140.1
|
|
36
|
+
* `fn` is always the enclosing definition's QUALIFIED name, built by the same rule as
|
|
37
|
+
* `CodeEdge.from`, so a fact can be joined to the call graph. `calls[].args` is ORDERED: position is
|
|
38
|
+
* what binds an argument to a parameter, and `params[].index` is the parameter's DECLARATION index —
|
|
39
|
+
* a parameter whose identifier cannot be read is omitted but still consumes its index, so the
|
|
40
|
+
* remaining ones keep their true positions.
|
|
41
|
+
*
|
|
42
|
+
* Every field reports what the tree literally says. A construct the walk cannot read produces NO
|
|
43
|
+
* fact rather than a guessed one (precision over recall): the cost is a propagation hop that does
|
|
44
|
+
* not happen, which is a disclosed false negative, versus a fabricated flow, which is a false
|
|
45
|
+
* positive nobody can distinguish from a real finding.
|
|
46
|
+
*/
|
|
47
|
+
export interface DataFlowFacts {
|
|
48
|
+
defs: {
|
|
49
|
+
fn: string;
|
|
50
|
+
name: string;
|
|
51
|
+
expr: Expr;
|
|
52
|
+
line: number;
|
|
53
|
+
}[];
|
|
54
|
+
calls: {
|
|
55
|
+
fn: string;
|
|
56
|
+
callee: string;
|
|
57
|
+
args: Expr[];
|
|
58
|
+
line: number;
|
|
59
|
+
}[];
|
|
60
|
+
params: {
|
|
61
|
+
fn: string;
|
|
62
|
+
index: number;
|
|
63
|
+
name: string;
|
|
64
|
+
}[];
|
|
65
|
+
returns: {
|
|
66
|
+
fn: string;
|
|
67
|
+
expr: Expr;
|
|
68
|
+
line: number;
|
|
69
|
+
}[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* An expression as STRUCTURE rather than text.
|
|
73
|
+
*
|
|
74
|
+
* @implements A-SPEC-140.1
|
|
75
|
+
* Text alone would force the propagation pass to ask "does this mention a tainted variable?"
|
|
76
|
+
* lexically, and two ordinary lines break that: `const note = "raw user input";` falsely taints
|
|
77
|
+
* `note` because the literal contains the word, and `exec(sanitize(raw))` looks tainted because the
|
|
78
|
+
* argument text contains `raw` even though it was sanitized inline. Both are exactly the false
|
|
79
|
+
* positives REQ-140 exists to remove, so the structure is recovered once, from the tree.
|
|
80
|
+
*/
|
|
81
|
+
export interface Expr {
|
|
82
|
+
/** Source text, trimmed and bounded (200 chars) — for display, and for the inherently textual source/sink patterns. */
|
|
83
|
+
text: string;
|
|
84
|
+
/** Identifiers READ, deduped, first-appearance order. A property name after a dot is not a read (`req.body.cmd` → `['req']`). */
|
|
85
|
+
refs: string[];
|
|
86
|
+
/** Bare names INVOKED, deduped, first-appearance order (`sanitize(raw)` → `['sanitize']`), by the edge walk's last-segment rule. */
|
|
87
|
+
callees: string[];
|
|
88
|
+
}
|
|
89
|
+
export interface LanguageParser {
|
|
90
|
+
extractSymbols(code: string, lang?: TsLang): CodeSymbol[];
|
|
91
|
+
extractEdges(code: string, lang?: TsLang): CodeEdge[];
|
|
92
|
+
/** `undefined` — never an empty fact set — for a language with no data-flow walk: not analysed ≠ nothing found. */
|
|
93
|
+
extractDataFlow(code: string, lang?: TsLang): DataFlowFacts | undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Thrown for RPC/infrastructure failures on the jest worker_thread path
|
|
97
|
+
* (timeout, dead worker, desynchronized response) — as opposed to a genuine
|
|
98
|
+
* per-file parse exception. Callers such as CpgScanner use this to avoid
|
|
99
|
+
* silently treating a systemic worker failure as "just this file is
|
|
100
|
+
* unparseable".
|
|
101
|
+
*/
|
|
102
|
+
export declare class TreeSitterInfraError extends Error {
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a language has a data-flow walk at all.
|
|
106
|
+
*
|
|
107
|
+
* @implements A-SPEC-140.1
|
|
108
|
+
* Read straight from the walk's own table so the two can never drift: a caller that guessed the
|
|
109
|
+
* list would eventually claim "analysed, nothing found" for a language nothing ever looked at.
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasDataFlowWalk(lang: TsLang): boolean;
|
|
112
|
+
export declare class TreeSitterTsParser implements LanguageParser {
|
|
113
|
+
extractSymbols(code: string, lang?: TsLang): CodeSymbol[];
|
|
114
|
+
extractEdges(code: string, lang?: TsLang): CodeEdge[];
|
|
115
|
+
extractDataFlow(code: string, lang?: TsLang): DataFlowFacts | undefined;
|
|
116
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ScannedFile } from './cpg-scanner';
|
|
2
|
+
export interface SliceCriterion {
|
|
3
|
+
file: string;
|
|
4
|
+
symbolName?: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
direction?: 'backward' | 'forward' | 'both';
|
|
7
|
+
}
|
|
8
|
+
export interface SliceResult {
|
|
9
|
+
criterion: SliceCriterion;
|
|
10
|
+
slicedNodeIds: Set<string>;
|
|
11
|
+
totalOriginalNodes: number;
|
|
12
|
+
prunedNodesCount: number;
|
|
13
|
+
pruningRatioPct: number;
|
|
14
|
+
slicedFiles: ScannedFile[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Perform precise program slicing over CPG AST nodes and call edges to prune noise and isolate affected nodes.
|
|
18
|
+
*
|
|
19
|
+
* @implements A-SPEC-217
|
|
20
|
+
*/
|
|
21
|
+
export declare function sliceProgram(scannedFiles: ScannedFile[] | null | undefined, criterion: SliceCriterion | null | undefined): SliceResult;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sliceProgram = sliceProgram;
|
|
4
|
+
function isFileMatch(sfSource, sfFull, normTargetFile) {
|
|
5
|
+
if (!normTargetFile)
|
|
6
|
+
return true;
|
|
7
|
+
if (sfSource !== '' && (sfSource === normTargetFile || sfSource.endsWith('/' + normTargetFile)))
|
|
8
|
+
return true;
|
|
9
|
+
if (sfFull !== '' && (sfFull === normTargetFile || sfFull.endsWith('/' + normTargetFile)))
|
|
10
|
+
return true;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
function getSymbolId(sf, sym) {
|
|
14
|
+
if (sym.qualifiedName)
|
|
15
|
+
return sym.qualifiedName;
|
|
16
|
+
const sfSource = (sf.sourcePath || '').split(/[\\/]/).filter(Boolean).join('/');
|
|
17
|
+
const sfFull = (sf.path || '').split(/[\\/]/).filter(Boolean).join('/');
|
|
18
|
+
return `${sfSource || sfFull}#${sym.name}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Perform precise program slicing over CPG AST nodes and call edges to prune noise and isolate affected nodes.
|
|
22
|
+
*
|
|
23
|
+
* @implements A-SPEC-217
|
|
24
|
+
*/
|
|
25
|
+
function sliceProgram(scannedFiles, criterion) {
|
|
26
|
+
const safeFiles = Array.isArray(scannedFiles) ? scannedFiles : [];
|
|
27
|
+
const safeCriterion = criterion || { file: '' };
|
|
28
|
+
const direction = safeCriterion.direction || 'both';
|
|
29
|
+
let totalOriginalNodes = 0;
|
|
30
|
+
// Build symbol lookup maps and adjacency graph
|
|
31
|
+
const nodeMap = new Map();
|
|
32
|
+
const forwardEdges = new Map();
|
|
33
|
+
const backwardEdges = new Map();
|
|
34
|
+
const normTargetFile = (safeCriterion.file || '').split(/[\\/]/).filter(Boolean).join('/');
|
|
35
|
+
const seedNodeIds = new Set();
|
|
36
|
+
for (const sf of safeFiles) {
|
|
37
|
+
if (!sf)
|
|
38
|
+
continue;
|
|
39
|
+
const sfSource = (sf.sourcePath || '').split(/[\\/]/).filter(Boolean).join('/');
|
|
40
|
+
const sfFull = (sf.path || '').split(/[\\/]/).filter(Boolean).join('/');
|
|
41
|
+
const fileMatches = isFileMatch(sfSource, sfFull, normTargetFile);
|
|
42
|
+
const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
|
|
43
|
+
for (const sym of symbols) {
|
|
44
|
+
totalOriginalNodes++;
|
|
45
|
+
const id = getSymbolId(sf, sym);
|
|
46
|
+
nodeMap.set(id, { file: sf, name: sym.name, qualifiedName: id, line: sym.startLine });
|
|
47
|
+
if (!forwardEdges.has(id))
|
|
48
|
+
forwardEdges.set(id, new Set());
|
|
49
|
+
if (!backwardEdges.has(id))
|
|
50
|
+
backwardEdges.set(id, new Set());
|
|
51
|
+
if (fileMatches) {
|
|
52
|
+
let nameMatch = !safeCriterion.symbolName || sym.name === safeCriterion.symbolName || sym.qualifiedName === safeCriterion.symbolName;
|
|
53
|
+
let lineMatch = safeCriterion.line === undefined || (safeCriterion.line >= sym.startLine && safeCriterion.line <= sym.endLine);
|
|
54
|
+
if (nameMatch && lineMatch) {
|
|
55
|
+
seedNodeIds.add(id);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const edges = Array.isArray(sf.edges) ? sf.edges : [];
|
|
60
|
+
for (const e of edges) {
|
|
61
|
+
if (e && e.from && e.to) {
|
|
62
|
+
if (!forwardEdges.has(e.from))
|
|
63
|
+
forwardEdges.set(e.from, new Set());
|
|
64
|
+
forwardEdges.get(e.from).add(e.to);
|
|
65
|
+
if (!backwardEdges.has(e.to))
|
|
66
|
+
backwardEdges.set(e.to, new Set());
|
|
67
|
+
backwardEdges.get(e.to).add(e.from);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Traversal for transitive closure
|
|
72
|
+
const slicedNodeIds = new Set();
|
|
73
|
+
const queue = Array.from(seedNodeIds);
|
|
74
|
+
for (const id of seedNodeIds) {
|
|
75
|
+
slicedNodeIds.add(id);
|
|
76
|
+
}
|
|
77
|
+
while (queue.length > 0) {
|
|
78
|
+
const curr = queue.shift();
|
|
79
|
+
if (direction === 'forward' || direction === 'both') {
|
|
80
|
+
const nextForward = forwardEdges.get(curr);
|
|
81
|
+
if (nextForward) {
|
|
82
|
+
for (const nextId of nextForward) {
|
|
83
|
+
if (!slicedNodeIds.has(nextId) && nodeMap.has(nextId)) {
|
|
84
|
+
slicedNodeIds.add(nextId);
|
|
85
|
+
queue.push(nextId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (direction === 'backward' || direction === 'both') {
|
|
91
|
+
const nextBackward = backwardEdges.get(curr);
|
|
92
|
+
if (nextBackward) {
|
|
93
|
+
for (const prevId of nextBackward) {
|
|
94
|
+
if (!slicedNodeIds.has(prevId) && nodeMap.has(prevId)) {
|
|
95
|
+
slicedNodeIds.add(prevId);
|
|
96
|
+
queue.push(prevId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const prunedNodesCount = totalOriginalNodes - slicedNodeIds.size;
|
|
103
|
+
const pruningRatioPct = totalOriginalNodes > 0 ? Math.round((prunedNodesCount / totalOriginalNodes) * 100) : 0;
|
|
104
|
+
// Filter scanned files to include only sliced symbols
|
|
105
|
+
const slicedFiles = safeFiles.map((sf) => {
|
|
106
|
+
if (!sf)
|
|
107
|
+
return sf;
|
|
108
|
+
const symbols = Array.isArray(sf.symbols) ? sf.symbols : [];
|
|
109
|
+
const filteredSymbols = symbols.filter((sym) => {
|
|
110
|
+
const id = getSymbolId(sf, sym);
|
|
111
|
+
return slicedNodeIds.has(id);
|
|
112
|
+
});
|
|
113
|
+
return { ...sf, symbols: filteredSymbols };
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
criterion: safeCriterion,
|
|
117
|
+
slicedNodeIds,
|
|
118
|
+
totalOriginalNodes,
|
|
119
|
+
prunedNodesCount,
|
|
120
|
+
pruningRatioPct,
|
|
121
|
+
slicedFiles,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ScannedFile } from './cpg-scanner';
|
|
2
|
+
export declare const SCAN_CACHE_VERSION = 3;
|
|
3
|
+
export declare class ScanFileCache {
|
|
4
|
+
private readonly dir;
|
|
5
|
+
private readonly file;
|
|
6
|
+
private entries;
|
|
7
|
+
private seen;
|
|
8
|
+
private dirty;
|
|
9
|
+
hits: number;
|
|
10
|
+
misses: number;
|
|
11
|
+
constructor(dir: string);
|
|
12
|
+
static hashOf(content: string): string;
|
|
13
|
+
/** Cached scan result iff the stored hash matches this content. Marks the path as seen. */
|
|
14
|
+
get(sourcePath: string, contentHash: string): Omit<ScannedFile, 'path'> | undefined;
|
|
15
|
+
put(sourcePath: string, contentHash: string, file: Omit<ScannedFile, 'path'>): void;
|
|
16
|
+
/**
|
|
17
|
+
* Persist once per scan. Mark-and-sweep: entries whose path was NOT seen this scan belong to
|
|
18
|
+
* deleted/renamed files — pruned so the cache cannot grow unboundedly. No-op when nothing changed.
|
|
19
|
+
*/
|
|
20
|
+
save(scannedScopeDir?: string): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What counts as a TEST file, per ecosystem — the single table both the scanner and the phase gate
|
|
3
|
+
* read.
|
|
4
|
+
*
|
|
5
|
+
* It lives in its own module, apart from the scanner, because two very different consumers need it
|
|
6
|
+
* and only one of them can afford the scanner's dependencies. `CpgScanner` uses it to keep test
|
|
7
|
+
* symbols out of the production graph; `classifyAction` (the PreToolUse phase gate, which runs on
|
|
8
|
+
* every Write/Edit and is measured in tens of milliseconds) uses it to tell WRITE_TEST from
|
|
9
|
+
* WRITE_CODE. When the two disagree about what a test is, the harness contradicts itself: measured on
|
|
10
|
+
* a real Python target, `tests/test_briefing.py` was excluded from the graph as a test AND gated as
|
|
11
|
+
* production code, so editing it demanded a stricter chain than the code it tests.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here imports anything, and that is deliberate — it is the property that lets the hook use
|
|
14
|
+
* it without pulling tree-sitter into a latency-critical path.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Test-file conventions, one entry per ecosystem, matched against the path RELATIVE to the project
|
|
18
|
+
* root (POSIX-separated).
|
|
19
|
+
*
|
|
20
|
+
* Test code must never enter the production graph: its symbols would be counted as production, which
|
|
21
|
+
* corrupts change-impact, clustering, and any reverse-engineered spec drafted from a scan. The
|
|
22
|
+
* previous rules covered TS/JS, Python and Go only, so — measured — `AppTest.java`, `AppTests.cs`,
|
|
23
|
+
* `app_test.rs`, `tests/mod.rs` and `app_test.cpp` all leaked through.
|
|
24
|
+
*
|
|
25
|
+
* Kept as one exported table rather than scattered constants so the per-language coverage is
|
|
26
|
+
* auditable at a glance and testable as a unit; `isTestFile` is the only entry point.
|
|
27
|
+
*/
|
|
28
|
+
export declare const TEST_FILE_PATTERNS: ReadonlyArray<readonly [string, RegExp]>;
|
|
29
|
+
/**
|
|
30
|
+
* True when a PROJECT-ROOT-RELATIVE POSIX path is a test file by any ecosystem's convention.
|
|
31
|
+
*
|
|
32
|
+
* The path must be relative to the project, not absolute: several rules key on a directory segment,
|
|
33
|
+
* so a repository that merely lives under some `/…/test/` directory on disk would otherwise read as
|
|
34
|
+
* one big test tree.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isTestFile(relPosixPath: string): boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders constitution violations as text a reader can act on.
|
|
3
|
+
*
|
|
4
|
+
* This exists because of a measured failure of diagnosis, not a failure of logic. The suite's
|
|
5
|
+
* corpus assertions compare arrays of violation objects, so a failure printed an opaque diff —
|
|
6
|
+
* `expect([]).toEqual([6 items])`. Twice that failure was recorded as an unidentified intermittent
|
|
7
|
+
* and the cause was never found; the 2026-08-08 run reproduced it eleven times on demand and showed
|
|
8
|
+
* it was never random at all, but a true report about the working tree at the moment it was read.
|
|
9
|
+
*
|
|
10
|
+
* Pure and exported rather than assembled inside the test, because an unverified diagnostic is wrong
|
|
11
|
+
* exactly when it is needed. Extracting it lets a mutation prove the message really names the spec.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConstitutionViolation {
|
|
14
|
+
article?: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cap on listed violations. A corpus-wide regression across 300+ specs would otherwise bury the
|
|
19
|
+
* terminal in hundreds of lines, which reads as noise and gets skipped — the same outcome as
|
|
20
|
+
* printing nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_LISTED = 20;
|
|
23
|
+
export declare function describeViolations(violations: ConstitutionViolation[]): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
|
+
/**
|
|
3
|
+
* L1 — Governance CONSTITUTION (target-architecture §7 L1).
|
|
4
|
+
*
|
|
5
|
+
* The inviolable principles are defined ONCE, here, as named ARTICLES — deterministic, pure checks —
|
|
6
|
+
* and RE-VERIFIED at every phase boundary by the hooks (the Stop gate calls verifyConstitution each
|
|
7
|
+
* turn; the PreToolUse gates enforce ART-1/ART-5 per-action). This is the spec-kit "constitution" +
|
|
8
|
+
* bkit "BeforePhase re-verification" pattern, realized as CODE, not prompt text: an article cannot be
|
|
9
|
+
* rationalized away by a model because it is executed, not read.
|
|
10
|
+
*
|
|
11
|
+
* Articles (violations carry the article id so remediation is addressable):
|
|
12
|
+
* ART-1 No-Spec-No-Code — enforced per-action by the PreToolUse phase gate (not re-checked here;
|
|
13
|
+
* listed for completeness of the constitution document).
|
|
14
|
+
* ART-2 RTM integrity — the governed spec graph has no orphans/cycles/dangling/duplicates.
|
|
15
|
+
* ART-3 Spec validity — every governed spec passes its type rules (incl. 4-quadrant GWT).
|
|
16
|
+
* ART-4 Coverage honesty — an approved T-SPEC is backed by REAL anchored test cases (evidence).
|
|
17
|
+
* ART-5 Approval out-of-band — spec approval/config writes require HOLMES_APPROVAL (enforced
|
|
18
|
+
* per-action by the PreToolUse gate; listed for completeness).
|
|
19
|
+
*
|
|
20
|
+
* Pure: no I/O, no clocks. The hooks supply loaded specs + evidence.
|
|
21
|
+
*/
|
|
22
|
+
export interface ConstitutionViolation {
|
|
23
|
+
article: string;
|
|
24
|
+
detail: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ConstitutionContext {
|
|
27
|
+
specs: Spec[];
|
|
28
|
+
/**
|
|
29
|
+
* @implements A-SPEC-160
|
|
30
|
+
* Recorded review findings, for ART-6. Supplied by the hook, like ART-4's evidence — the
|
|
31
|
+
* constitution stays pure and does no I/O.
|
|
32
|
+
*
|
|
33
|
+
* @implements A-SPEC-191
|
|
34
|
+
* `severity` rides along for ART-7. The ledger always carried it; this type narrowed it away, so
|
|
35
|
+
* the constitution could not legally read the one field that says "this must not ship". Optional:
|
|
36
|
+
* pre-REQ-157 records never had it, and absence stays silent (the A-SPEC-146 trap). `summary`
|
|
37
|
+
* rides along too (§4d) — a violation that names only an opaque id sends the operator to grep.
|
|
38
|
+
*/
|
|
39
|
+
findings?: {
|
|
40
|
+
id: string;
|
|
41
|
+
status: 'open' | 'resolved';
|
|
42
|
+
basis?: string;
|
|
43
|
+
severity?: string;
|
|
44
|
+
summary?: string;
|
|
45
|
+
}[];
|
|
46
|
+
/** A-SPEC id -> count of real test cases in anchored test files (H1 syntactic lower bound). */
|
|
47
|
+
testCasesByAspec?: Record<string, number>;
|
|
48
|
+
/**
|
|
49
|
+
* A-SPEC id -> cases that ACTUALLY EXECUTED in a recorded PASSING test run.
|
|
50
|
+
*
|
|
51
|
+
* STRICTLY ADDITIVE (adversarial review C1/C3): this can only ADD violations, never satisfy ART-4
|
|
52
|
+
* on its own. The record lives in an unsigned, gitignored file an agent can rewrite, and its
|
|
53
|
+
* freshness stamp (git HEAD) does not move for uncommitted work — so letting it OVERRIDE the
|
|
54
|
+
* syntactic bound made forgery and staleness able to SUPPRESS real violations (measured: 16 -> 0).
|
|
55
|
+
* As an additive signal both failure modes are defused: forging it unlocks nothing, and a stale
|
|
56
|
+
* record cannot hide a missing test suite because the syntactic bound is still enforced.
|
|
57
|
+
*/
|
|
58
|
+
executedByAspec?: Record<string, number>;
|
|
59
|
+
}
|
|
60
|
+
export declare const ARTICLES: Record<string, string>;
|
|
61
|
+
export declare function verifyConstitution(ctx: ConstitutionContext): ConstitutionViolation[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is acting — the port a server-backed identity will eventually fill.
|
|
3
|
+
*
|
|
4
|
+
* `proven` is the load-bearing field. A LOCAL role is a claim the operator made when launching the
|
|
5
|
+
* agent; nothing verified it. Recording that plainly is what keeps a later reader from assuming a
|
|
6
|
+
* local role was checked by something, and it reserves the exact slot where a server answers
|
|
7
|
+
* `proven: true` for "this really is the Reviewer".
|
|
8
|
+
*/
|
|
9
|
+
export interface Identity {
|
|
10
|
+
actor?: string;
|
|
11
|
+
role?: string;
|
|
12
|
+
/** Always false locally. A server-backed provider is where this becomes true. */
|
|
13
|
+
proven: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface IdentityProvider {
|
|
16
|
+
current(): Identity;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reads the claim from the process environment — the same out-of-band discipline `HOLMES_APPROVAL`
|
|
20
|
+
* established, and for the same reason: a session cannot set its own environment, so it cannot claim
|
|
21
|
+
* its own role. A role carried in the tool payload would be self-assertion, which is precisely what
|
|
22
|
+
* ART-5 forbids on the spec axis.
|
|
23
|
+
*/
|
|
24
|
+
export declare class EnvIdentityProvider implements IdentityProvider {
|
|
25
|
+
private readonly env;
|
|
26
|
+
constructor(env?: NodeJS.ProcessEnv);
|
|
27
|
+
current(): Identity;
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process mutual exclusion for one ledger file.
|
|
3
|
+
*
|
|
4
|
+
* WHY this exists, measured rather than assumed. `ProvenanceChain.append()` was read-then-append and
|
|
5
|
+
* the single-use approval gate was check-then-act, so two agents running at once could both observe
|
|
6
|
+
* "nonce not consumed" and both proceed. Two real processes synchronized on a file barrier, 20
|
|
7
|
+
* trials: the single-use approval was spent TWICE 20/20 times, and the tamper-evident chain reported
|
|
8
|
+
* `ok:false` 17/20 times — reporting tampering that never happened. Sequential behaviour was, and
|
|
9
|
+
* remains, correct; the failure only appears when the critical sections overlap, which is precisely
|
|
10
|
+
* the case a harness with "agentic" in its name promises to support.
|
|
11
|
+
*
|
|
12
|
+
* `fs.mkdirSync` is the primitive: atomic on every platform this ships to, `EEXIST` when held, and
|
|
13
|
+
* with none of the file-vs-`O_EXCL` subtleties. A DIRECTORY rather than a lock FILE because a
|
|
14
|
+
* zero-byte file is what a cleanup script or `git clean` deletes mid-hold, while a directory holds
|
|
15
|
+
* its own metadata and cannot be removed non-empty by accident.
|
|
16
|
+
*
|
|
17
|
+
* LIMIT, stated rather than implied: single host, single filesystem with atomic `mkdir`. Networked
|
|
18
|
+
* filesystems (NFS, shared volumes) are out of scope and this does not pretend to cover them.
|
|
19
|
+
*/
|
|
20
|
+
/** Thrown when exclusion cannot be obtained within the acquire budget. Distinguishable on purpose. */
|
|
21
|
+
export declare class LedgerLockError extends Error {
|
|
22
|
+
readonly holmesRefusal = true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A hold older than this is treated as abandoned and broken.
|
|
26
|
+
*
|
|
27
|
+
* 30 seconds against a critical section that reads a few hundred bytes and appends one line —
|
|
28
|
+
* microseconds of real work, so this is roughly five orders of magnitude of headroom and cannot fire
|
|
29
|
+
* on a live holder that is merely slow. The number exists because a `kill -9`'d agent must not wedge
|
|
30
|
+
* every other agent forever.
|
|
31
|
+
*/
|
|
32
|
+
export declare const STALE_MS = 30000;
|
|
33
|
+
/** Default budget for waiting on someone else's hold before failing. */
|
|
34
|
+
export declare const ACQUIRE_MS = 5000;
|
|
35
|
+
export declare function lockDirFor(ledgerFile: string): string;
|
|
36
|
+
export interface LockOptions {
|
|
37
|
+
acquireMs?: number;
|
|
38
|
+
staleMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Records the breaking of a stale hold. Injected so the lock module does not depend on the
|
|
41
|
+
* provenance chain that depends on IT — `withLedgerLock` is the lower layer.
|
|
42
|
+
*/
|
|
43
|
+
onStaleBreak?: (info: {
|
|
44
|
+
pid?: number;
|
|
45
|
+
ageMs: number;
|
|
46
|
+
ledgerFile: string;
|
|
47
|
+
}) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run `fn` while holding exclusion for `ledgerFile`.
|
|
51
|
+
*
|
|
52
|
+
* The hold is released in `finally`, including when `fn` throws, and a release problem never masks
|
|
53
|
+
* `fn`'s own error — the body's failure is the one the caller needs to see.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withLedgerLock<T>(ledgerFile: string, fn: () => T, opts?: LockOptions): T;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { LedgerStore } from './ledger-store';
|
|
2
|
+
/**
|
|
3
|
+
* The ONE conformance suite every `LedgerStore` adapter must pass.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately a single exported function rather than a copy per adapter: two copies drift, and
|
|
6
|
+
* "both adapters pass" quietly stops meaning anything. Everything here is expressed against the
|
|
7
|
+
* CONTRACT — no paths, no files, no knowledge of how an adapter stores anything. If a property
|
|
8
|
+
* cannot be written that way, that is itself the finding: the contract has leaked its
|
|
9
|
+
* implementation, and finding that now is the whole reason a second adapter exists.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ledgerStoreConformance(name: string, make: () => LedgerStore): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ProvenanceEvent } from './provenance-chain';
|
|
2
|
+
import { LedgerEvent } from './provenance-ledger';
|
|
3
|
+
/**
|
|
4
|
+
* The ledger as a PORT — ADR-001's deferred half, applied to the one store whose shape was derived
|
|
5
|
+
* from real use rather than guessed at.
|
|
6
|
+
*
|
|
7
|
+
* ADR-001 decided the ports now because "an interface changed later causes core-wide rework, while
|
|
8
|
+
* an adapter can wait". Measured on 2026-08-07: seven of the eight declared ports do not exist, and
|
|
9
|
+
* the one that does carries the very `read/write/list` shape that ADR called insufficient. This is
|
|
10
|
+
* the first to land, and it starts from `ProvenanceLedger` — five operations that five production
|
|
11
|
+
* call sites already use — so it avoids the cost that ADR itself listed as the downside: designing a
|
|
12
|
+
* contract for something nobody calls.
|
|
13
|
+
*
|
|
14
|
+
* DECLARED, with reasons:
|
|
15
|
+
* - `replicaId()` — the filename carries it locally, but a server adapter has no filenames. On the
|
|
16
|
+
* contract it becomes independent of storage.
|
|
17
|
+
* - append-only — free from a file append, an explicit promise for anything else.
|
|
18
|
+
* - `isNonceConsumed` spanning ALL chains — REQ-148's rule. An adapter storing chains separately
|
|
19
|
+
* can get this wrong, so the contract has to require it rather than one implementation happening
|
|
20
|
+
* to do it.
|
|
21
|
+
*
|
|
22
|
+
* NOT DECLARED, and why: transactions, batching, pagination, RBAC. No caller needs them, they cannot
|
|
23
|
+
* be verified by the two adapters, and an unverifiable contract clause is worth less than a comment.
|
|
24
|
+
* ADR-001 named speculative port design as the cost of deciding early; this is where that cost is
|
|
25
|
+
* refused.
|
|
26
|
+
*/
|
|
27
|
+
export interface LedgerStore {
|
|
28
|
+
/** This writer's identity — the one used for events this store appends. */
|
|
29
|
+
replicaId(): string;
|
|
30
|
+
/** Known chains by IDENTITY, never by location: a path here is a filesystem assumption a server adapter could not satisfy. */
|
|
31
|
+
chains(): {
|
|
32
|
+
replicaId: string | null;
|
|
33
|
+
id: string;
|
|
34
|
+
}[];
|
|
35
|
+
/** Every event across every chain, deterministically ordered. */
|
|
36
|
+
loadAll(): LedgerEvent[];
|
|
37
|
+
/** Per-chain verification; one broken chain fails the whole store and is named. */
|
|
38
|
+
verifyAll(): {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
broken: {
|
|
41
|
+
replicaId: string | null;
|
|
42
|
+
detail?: string;
|
|
43
|
+
}[];
|
|
44
|
+
};
|
|
45
|
+
/** Append to this writer's chain. Never modifies or removes an existing event. */
|
|
46
|
+
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
47
|
+
/** Spent on ANY chain — see the interface note. */
|
|
48
|
+
isNonceConsumed(nonce: string): boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Local adapter — DELEGATES to `ProvenanceLedger` rather than re-implementing it.
|
|
52
|
+
*
|
|
53
|
+
* That is load-bearing, not stylistic. REQ-141's per-file lock, REQ-147's read compatibility with the
|
|
54
|
+
* pre-split ledger, and REQ-148's replica split all live inside `ProvenanceLedger`. An adapter that
|
|
55
|
+
* wrote files itself would satisfy every conformance test while silently dropping all three, and
|
|
56
|
+
* nothing in this suite would notice — which is why a structural test pins the delegation too.
|
|
57
|
+
*/
|
|
58
|
+
export declare class FileLedgerStore implements LedgerStore {
|
|
59
|
+
private readonly dir;
|
|
60
|
+
private readonly givenId?;
|
|
61
|
+
private readonly inner;
|
|
62
|
+
private cachedId;
|
|
63
|
+
/**
|
|
64
|
+
* The replica id is resolved LAZILY and the root derivation is delegated.
|
|
65
|
+
*
|
|
66
|
+
* Both were regressions I reintroduced here: resolving in the constructor made a mere read create
|
|
67
|
+
* `.ax/state/` (REQ-148 had fixed precisely that in ProvenanceLedger), and deriving the project
|
|
68
|
+
* root with a local regex duplicated a rule that already exists — two copies of one rule drift.
|
|
69
|
+
* The contract now demands the no-footprint property so the NEXT adapter cannot lose it either.
|
|
70
|
+
*/
|
|
71
|
+
constructor(dir: string, givenId?: string | undefined);
|
|
72
|
+
replicaId(): string;
|
|
73
|
+
chains(): {
|
|
74
|
+
replicaId: string | null;
|
|
75
|
+
id: string;
|
|
76
|
+
}[];
|
|
77
|
+
loadAll(): LedgerEvent[];
|
|
78
|
+
verifyAll(): {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
broken: {
|
|
81
|
+
replicaId: string | null;
|
|
82
|
+
detail?: string;
|
|
83
|
+
}[];
|
|
84
|
+
};
|
|
85
|
+
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
86
|
+
isNonceConsumed(nonce: string): boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* In-memory adapter — the instrument that makes the contract testable.
|
|
90
|
+
*
|
|
91
|
+
* Uses NO filesystem, deliberately. An in-memory store backed by a temp directory would be the file
|
|
92
|
+
* adapter in disguise and would prove nothing about coupling. It reuses the pure chain functions
|
|
93
|
+
* (`chainNext`, `verifyChain`) because those are the format, not the storage.
|
|
94
|
+
*/
|
|
95
|
+
export declare class MemoryLedgerStore implements LedgerStore {
|
|
96
|
+
private readonly id;
|
|
97
|
+
private readonly byReplica;
|
|
98
|
+
constructor(id?: string);
|
|
99
|
+
replicaId(): string;
|
|
100
|
+
chains(): {
|
|
101
|
+
replicaId: string | null;
|
|
102
|
+
id: string;
|
|
103
|
+
}[];
|
|
104
|
+
loadAll(): LedgerEvent[];
|
|
105
|
+
verifyAll(): {
|
|
106
|
+
ok: boolean;
|
|
107
|
+
broken: {
|
|
108
|
+
replicaId: string | null;
|
|
109
|
+
detail?: string;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
append(body: Omit<ProvenanceEvent, 'prevHash' | 'hash' | 'seq'>): ProvenanceEvent;
|
|
113
|
+
isNonceConsumed(nonce: string): boolean;
|
|
114
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 — append-only PROGRESS ledger + checkpoints (target-architecture §7 L4).
|
|
3
|
+
*
|
|
4
|
+
* Complements the decision ledger (guardrail/decision-ledger.ts, same L4 family): decisions record
|
|
5
|
+
* WHY, this ledger records WHAT HAPPENED — slices completed, findings, checkpoints — as append-only
|
|
6
|
+
* JSONL under .ax/ledger/progress.jsonl (gitignored, like all .ax derived state). Append-only means
|
|
7
|
+
* an agent cannot rewrite history it doesn't like; a checkpoint entry pins the git HEAD + suite count
|
|
8
|
+
* so "where were we / what was green" survives compaction and session loss.
|
|
9
|
+
*
|
|
10
|
+
* Pure half: formatProgressEntry / parseProgressLog (deterministic — the caller supplies the
|
|
11
|
+
* timestamp, this module never reads a clock). I/O half: appendProgress / readProgress (fail-open).
|
|
12
|
+
*/
|
|
13
|
+
export type ProgressKind = 'slice' | 'finding' | 'checkpoint' | 'note';
|
|
14
|
+
export interface ProgressEntry {
|
|
15
|
+
ts: string;
|
|
16
|
+
kind: ProgressKind;
|
|
17
|
+
summary: string;
|
|
18
|
+
refs?: string[];
|
|
19
|
+
suite?: number;
|
|
20
|
+
head?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function formatProgressEntry(e: ProgressEntry): string;
|
|
23
|
+
export declare function parseProgressLog(text: string): ProgressEntry[];
|
|
24
|
+
export declare function appendProgress(root: string, entry: ProgressEntry): boolean;
|
|
25
|
+
export declare function readProgress(root: string, lastN?: number): ProgressEntry[];
|