@holmes-lab/holmes-kit 0.26.1 → 0.27.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 +65 -0
- package/README.md +9 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/attest.d.ts +15 -0
- package/dist/holmes/cli/attest.js +168 -0
- package/dist/holmes/cli/index.d.ts +1 -0
- package/dist/holmes/cli/index.js +20 -1
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +16 -2
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +238 -66
- package/dist/holmes/mcp/handlers.d.ts +5 -0
- package/dist/holmes/mcp/handlers.js +2 -0
- package/dist/holmes/mcp/tool-schemas.js +13 -0
- package/dist/holmes/rtm/traced-files.d.ts +83 -0
- package/dist/holmes/rtm/traced-files.js +207 -0
- package/dist/holmes/spec/entity-store.js +28 -6
- package/dist/holmes/spec/id-collision.js +4 -1
- package/dist/holmes/spec/remote-spec-refs.d.ts +9 -0
- package/dist/holmes/spec/remote-spec-refs.js +88 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.27.0] - 2026-09-20
|
|
9
|
+
|
|
10
|
+
A repository can now say, as a number anyone can reproduce, how much of it is governed.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`holmes-kit attest` — how many source files trace to an APPROVED spec** (A-SPEC-702). The request
|
|
14
|
+
was a "generated under holmes-kit" header in every source file. That was declined: a header is a
|
|
15
|
+
claim that can be pasted and keeps saying "governed" after an ungoverned edit, it brands files that
|
|
16
|
+
belong to the consumer, the same sentence in every file is measured retrieval noise, and line 1
|
|
17
|
+
already carries the `implements` anchor a gate verifies. What was missing is a repository-level
|
|
18
|
+
statement: the spec-side census lived only inside the dashboard and the file-side one did not exist.
|
|
19
|
+
`attest` adds **no new judgement** — the denominator is what the scanner returned, the numerator is
|
|
20
|
+
anchors ∩ approved ids (a draft anchor is not a trace), the spec axis is the existing link census —
|
|
21
|
+
and prints where the number is true: commit and a digest of the approved specs. `+dirty` is set only
|
|
22
|
+
when a path that *decides* the number is uncommitted (a scanned source or a spec), so writing the
|
|
23
|
+
badge does not make the badge a lie. `--exclude a,b` names trees that are not yours (vendored);
|
|
24
|
+
exclusions match at a path boundary, a prefix meaning "everything" is dropped, and they are echoed in
|
|
25
|
+
every output. What cannot be measured is `n/a` with the reason, never `0%` or `100%`; percentages are
|
|
26
|
+
floored, so `100` appears only when every file is traced; ten or fewer untraced files are named.
|
|
27
|
+
`--badge` writes a self-contained SVG (`.ax/badges/traced.svg` — no href, font, script or fetch).
|
|
28
|
+
`--readme` rewrites **only** the region between `<!-- holmes-kit:attest:begin -->` and
|
|
29
|
+
`<!-- holmes-kit:attest:end -->`: a README without the markers is never written (placing them is the
|
|
30
|
+
consent), a half, doubled or reversed marker is refused with exit 1, and a CRLF README stays CRLF.
|
|
31
|
+
The numbers never change the exit code — this is a statement, not a gate. CLI only, so it is the
|
|
32
|
+
same in every harness and adds no MCP schema cost. Measured on this repository: 396/614 (64.4%) as
|
|
33
|
+
scanned; 386/386 once `reference/` (212 vendored files), `dist-tools/` (build output that carried
|
|
34
|
+
anchors and had been counting as traced source) and `docs/goals/probes/` are named as excluded.
|
|
35
|
+
|
|
36
|
+
## [0.26.2] - 2026-09-20
|
|
37
|
+
|
|
38
|
+
The last stretch of the number-collision repair: the plan `doctor` has been printing since 0.26.1 can
|
|
39
|
+
now be executed — the plan a person saw, under one approval, before the merge.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **`spec_reconcile` — the seen plan, one approval, before the merge** (A-SPEC-700.2). 0.26.1 detected a
|
|
43
|
+
spec number another machine took and printed the plan; executing it was still a person calling
|
|
44
|
+
`spec_renumber` once per family. `spec_reconcile({operation:'plan'})` is read-only: it chains the
|
|
45
|
+
existing collectors, collision detector, planner and `planRenumber` — no new judgement — and returns
|
|
46
|
+
the moves, how many documents / `depends_on` / anchors each rewrites, the re-seal order, the prose
|
|
47
|
+
candidates, and a `target` derived from the plan's content alone (no clock, no nonce: the same state
|
|
48
|
+
plans to the same target). `apply` takes that plan back, **rebuilds it inside the store hold**, and
|
|
49
|
+
refuses `plan-changed` if it is no longer the one that was seen — a fetch in between may have given
|
|
50
|
+
the destination away. It then demands one `config-write` approval bound to `target`, and runs every
|
|
51
|
+
move through the same path `spec_renumber` uses. Found while building it: `spec_renumber`'s own apply
|
|
52
|
+
asks for **no approval at all**, although it rewrites approved documents and source anchors; this
|
|
53
|
+
tool is the first path that honours REQ-700's "one approval" constraint, and the standalone tool is
|
|
54
|
+
unchanged (a separate decision). It never seals — `spec_approve` stays the only sealer, and the order
|
|
55
|
+
is returned. Prose is reported, never rewritten. No remote, or no git, is an answer (`moves: []` with
|
|
56
|
+
a note), not a refusal. Measured cost: the tool adds ≈414 schema tokens (41 tools ≈ 10,724).
|
|
57
|
+
An adversarial pass **run, not read**: forged `moves` under a true `target` are never read (what runs
|
|
58
|
+
is the recomputed plan); a grant is spent once and a refused apply spends nothing; a second move that
|
|
59
|
+
stops reports `moved` / `remaining` and planning again yields only the rest. It found one defect,
|
|
60
|
+
fixed here: with two colliding families that depend on each other, the re-seal order named a document
|
|
61
|
+
a sibling move had already renamed.
|
|
62
|
+
- `doctor`'s collision advice now ends by naming the tool: `… then re-seal with spec_approve, and merge
|
|
63
|
+
(spec_reconcile plan → apply runs every move under one approval)`.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- **A finished entity operation is no longer read as "interrupted" or "vanished"** (A-SPEC-701). The
|
|
67
|
+
pending-operation scan read each `journal.json` twice while retirement is a single rename, so a scan
|
|
68
|
+
racing a retirement could see the journal on the first read and not the second, and refuse
|
|
69
|
+
`recovery-required` for work that had just completed — measured as the Linux CI red on
|
|
70
|
+
`entity-store-boundaries` at a commit that changed no code. The scan reads once and skips an
|
|
71
|
+
operation whose directory has retired; the set of legal outcomes is not widened.
|
|
72
|
+
|
|
8
73
|
## [0.26.1] - 2026-09-20
|
|
9
74
|
|
|
10
75
|
Three judgements that were computed and reached nobody now reach a person, and a spec number another
|
package/README.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
> **"No Spec, No Code"** — Deterministic Agentic Software Engineering (ASE) harness with causal traceability.
|
|
8
8
|
|
|
9
|
+
<!-- holmes-kit:attest:begin -->
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
**386/386** source files (100%) trace to an approved spec · **629/680** approved A-SPECs (92.5%) are anchored from code.
|
|
13
|
+
Measured at `173b99650442` (specs `sha256:215f84d4e9f4`) — reproduce with `holmes-kit attest --exclude dist-tools/,docs/goals/probes/,reference/`.
|
|
14
|
+
<!-- holmes-kit:attest:end -->
|
|
15
|
+
|
|
9
16
|
---
|
|
10
17
|
|
|
11
18
|
## 🕵️ Philosophy & Vision: Beyond Code Generation
|
|
@@ -16,6 +23,8 @@
|
|
|
16
23
|
|
|
17
24
|
### 🛡️ Currently Supported Features (Production Features)
|
|
18
25
|
|
|
26
|
+
- 📏 **Say how much of the repository is governed — as a number, not a label** *(new in 0.27.0)*: `holmes-kit attest` reports how many scanned source files are anchored to an **approved** spec, beside how many approved A-SPECs are anchored from code, and says where that is true (commit, plus a digest of the approved specs) so anyone can reproduce it. No new judgement: the scanner's files, the anchors, the existing link census. A per-file "generated under holmes-kit" header was declined — it is a claim that survives an ungoverned edit, and line 1 already carries the anchor a gate verifies. `--exclude a,b` names vendored trees at a path boundary and is echoed in every output; what cannot be measured is `n/a`, never `0%`; `100` appears only when every file is traced. `--badge` writes a self-contained local SVG, and `--readme` rewrites **only** the region between the `holmes-kit:attest` markers — a README without them is never touched, and a half marker is refused. A statement, not a gate: the numbers never change the exit code.
|
|
27
|
+
- 🤝 **The collision plan runs under one approval** *(new in 0.26.2)*: 0.26.1 saw a spec number another machine took and printed the plan; carrying it out was still one `spec_renumber` call per family, and that tool's apply asks for no approval although it rewrites approved documents and source anchors. `spec_reconcile` `plan` is read-only and returns the moves, what each rewrites, the re-seal order, the prose it will **not** touch, and a `target` derived from the plan's content alone. `apply` rebuilds the plan inside the store hold, refuses `plan-changed` if a fetch moved it since you looked, demands one `config-write` approval bound to that `target`, and runs every move through the path `spec_renumber` already uses. Forged `moves` are never read — what runs is the recomputed plan. It never seals: `spec_approve` stays the only sealer. No remote is an answer, not a refusal.
|
|
19
28
|
- 🔢 **A spec number another machine took is seen before the merge** *(new in 0.26.1)*: two checkouts allocated `REQ-694` thirteen minutes apart, neither could see the other, and it surfaced only as an add/add conflict after the push was refused — a rebuilt slice and five out-of-band approvals. `doctor --target` now reads each remote-tracking ref **as last fetched** (no network) and judges only the documents each side *added* since they parted, so a spec one side merely edited is divergence, not an alarm. On a collision the advice is the plan: `Before merging — move 901 → 903: spec_renumber(oldBase=901, newBase=903); then re-seal`. The side that moves is the one not published yet, the destination is free on both sides, and "published" means equal content — never path, because a spec's path is its number.
|
|
20
29
|
- 🔁 **The cycle ratchet finally speaks — and takes a named exception** *(new in 0.26.1)*: since the commit that introduced it, the Stop hook computed the code cycles and called the verdict without them, so the `ART-2` line had never been printed; every unit test passed because each handed the evidence in directly. It is emitted now, bounded (five cycles, five members each, the rest counted — one vendored cycle here has 46 members), and `ax.config.json` gains `architecture.cycleIgnore`: prefixes for trees that are not yours to fix. A cycle is excepted only when every member lies under one, `reference` does not pardon `reference-impl/`, a broken config excepts nothing, and excepted cycles stay counted in the ledger.
|
|
21
30
|
- 🧬 **Declare your architecture, in every language the graph can follow** *(new in 0.26.0)*: a C-SPEC's `## Forbidden Edges` takes a third rule kind beside `import` and `call` — `- inherits src/app -x-> ConcreteBase`, an exact match enforced at the pre-edit gate. Nothing is inferred: the rule reads the graph's `inherits` edges and never asks which class is "abstract", which is what makes it hold in Go, where interface satisfaction cannot be inferred at all. A project that already breaks the rule it wants adopts it with a **baseline that expires** — `- allow until 2026-09-27 : inherits src/legacy/a.ts -> LegacyBase`, exact on kind, file and target, never a prefix. Past its date the allowance still holds, because an expiry that blocked would turn the rule into a barrier to adoption; it is **reported** instead, carrying `(reconfirmed n since <date>)` so a twelfth renewal cannot hide.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
4ec4b788-mu9vw1zt
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AttestReport } from '../rtm/traced-files';
|
|
2
|
+
export interface AttestOptions {
|
|
3
|
+
root: string;
|
|
4
|
+
exclude: string[];
|
|
5
|
+
json: boolean;
|
|
6
|
+
badge: boolean;
|
|
7
|
+
readme: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface AttestIo {
|
|
10
|
+
stdout(s: string): void;
|
|
11
|
+
stderr(s: string): void;
|
|
12
|
+
}
|
|
13
|
+
export declare const BADGE_REL: string;
|
|
14
|
+
export declare function buildAttestReport(root: string, exclude: string[]): Promise<AttestReport>;
|
|
15
|
+
export declare function runAttest(o: AttestOptions, io: AttestIo): Promise<number>;
|
|
@@ -0,0 +1,168 @@
|
|
|
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.BADGE_REL = void 0;
|
|
37
|
+
exports.buildAttestReport = buildAttestReport;
|
|
38
|
+
exports.runAttest = runAttest;
|
|
39
|
+
// @implements A-SPEC-702
|
|
40
|
+
/**
|
|
41
|
+
* `holmes-kit attest` — the surface that calls `rtm/traced-files`.
|
|
42
|
+
*
|
|
43
|
+
* Reads: one scan, the spec store, git (optional — REQ-128). Writes: AT MOST two files, and only
|
|
44
|
+
* when asked: the badge (`--badge`) and the region between the README markers (`--readme`). A README
|
|
45
|
+
* without markers is never written: placing the markers is the consent. Nothing reaches the network,
|
|
46
|
+
* and the measured numbers never change the exit code — this is a statement, not a gate.
|
|
47
|
+
*/
|
|
48
|
+
const fs = __importStar(require("node:fs"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const node_child_process_1 = require("node:child_process");
|
|
51
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
52
|
+
const spec_store_1 = require("../spec/spec-store");
|
|
53
|
+
const root_1 = require("../project/root");
|
|
54
|
+
const test_scope_1 = require("../rtm/test-scope");
|
|
55
|
+
const file_anchors_1 = require("../rtm/file-anchors");
|
|
56
|
+
const link_census_1 = require("../rtm/link-census");
|
|
57
|
+
const traced_files_1 = require("../rtm/traced-files");
|
|
58
|
+
exports.BADGE_REL = path.join('.ax', 'badges', 'traced.svg');
|
|
59
|
+
const SPECS_REL = path.join('.ax', 'specs');
|
|
60
|
+
const git = (root, args) => {
|
|
61
|
+
try {
|
|
62
|
+
return (0, node_child_process_1.execFileSync)('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)(process.env) }).toString('utf8');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* `+dirty` only when something that DECIDES the number is uncommitted: a scanned source file or a
|
|
70
|
+
* spec. The badge and the README are outputs of this very command, and a session ledger is always
|
|
71
|
+
* moving — counting them would make a committed badge say `+dirty` forever.
|
|
72
|
+
*/
|
|
73
|
+
function decidingPathsDirty(root, scanned) {
|
|
74
|
+
const out = git(root, ['status', '--porcelain', '-z', '--untracked-files=all']);
|
|
75
|
+
if (out === undefined)
|
|
76
|
+
return false;
|
|
77
|
+
const specs = SPECS_REL.split(path.sep).join('/') + '/';
|
|
78
|
+
for (const entry of out.split('\0')) {
|
|
79
|
+
if (entry.length < 4)
|
|
80
|
+
continue;
|
|
81
|
+
const p = entry.slice(3);
|
|
82
|
+
if (p.startsWith(specs) || scanned.has(p))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
async function buildAttestReport(root, exclude) {
|
|
88
|
+
const scanned = new cpg_scanner_1.CpgScanner().scan(root, root);
|
|
89
|
+
const specs = await new spec_store_1.LocalMarkdownRepository(path.join(root, SPECS_REL)).list();
|
|
90
|
+
const approved = specs.filter((s) => s.status === 'approved');
|
|
91
|
+
const files = scanned.map((f) => ({ path: f.sourcePath.split(path.sep).join('/'), anchors: f.implementsSpecs ?? [] }));
|
|
92
|
+
// The spec axis, the way the dashboard computes it (A-SPEC-655). `weakMentions` stays empty: it
|
|
93
|
+
// only splits the REASON a spec is unlinked, and this command reports the count.
|
|
94
|
+
const excludes = (0, traced_files_1.normalizeExcludes)(exclude);
|
|
95
|
+
const kept = files.filter((f) => !excludes.some((e) => f.path.startsWith(e)));
|
|
96
|
+
const census = (0, link_census_1.linkCensus)({
|
|
97
|
+
approvedIds: approved.filter((s) => s.id.startsWith('A-SPEC')).map((s) => s.id),
|
|
98
|
+
implemented: new Set(kept.flatMap((f) => f.anchors)),
|
|
99
|
+
testAnchors: (0, test_scope_1.scanTestAnchors)(root),
|
|
100
|
+
fileAnchors: (0, file_anchors_1.scanFileAnchors)(root),
|
|
101
|
+
weakMentions: {},
|
|
102
|
+
});
|
|
103
|
+
const commit = git(root, ['rev-parse', 'HEAD'])?.trim();
|
|
104
|
+
return {
|
|
105
|
+
files: (0, traced_files_1.tracedFiles)({ files, approvedIds: approved.map((s) => s.id), exclude }),
|
|
106
|
+
specs: { total: census.total, linked: census.codeLinked },
|
|
107
|
+
provenance: (0, traced_files_1.attestProvenance)({
|
|
108
|
+
commit,
|
|
109
|
+
dirty: commit ? decidingPathsDirty(root, new Set(files.map((f) => f.path))) : false,
|
|
110
|
+
approved: approved.map((s) => ({ id: s.id, digest: String(s.frontmatter.approved_digest ?? '') })),
|
|
111
|
+
exclude,
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function runAttest(o, io) {
|
|
116
|
+
if (!fs.existsSync(o.root) || !fs.statSync(o.root).isDirectory()) {
|
|
117
|
+
io.stderr(`attest: ${o.root} is not a directory\n`);
|
|
118
|
+
return 2;
|
|
119
|
+
}
|
|
120
|
+
const report = await buildAttestReport(o.root, o.exclude);
|
|
121
|
+
io.stdout(o.json ? (0, traced_files_1.renderAttestJson)(report) : (0, traced_files_1.renderAttestText)(report));
|
|
122
|
+
let code = 0;
|
|
123
|
+
const note = (s) => { if (!o.json)
|
|
124
|
+
io.stdout(s);
|
|
125
|
+
else
|
|
126
|
+
io.stderr(s); };
|
|
127
|
+
if (o.badge) {
|
|
128
|
+
const file = path.join(o.root, exports.BADGE_REL);
|
|
129
|
+
const svg = (0, traced_files_1.renderAttestSvg)(report);
|
|
130
|
+
let same = false;
|
|
131
|
+
try {
|
|
132
|
+
same = fs.readFileSync(file, 'utf8') === svg;
|
|
133
|
+
}
|
|
134
|
+
catch { /* absent */ }
|
|
135
|
+
if (!same) {
|
|
136
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
137
|
+
fs.writeFileSync(file, svg);
|
|
138
|
+
}
|
|
139
|
+
note(`badge ${exports.BADGE_REL.split(path.sep).join('/')}${same ? ' (unchanged)' : ' written'}\n`);
|
|
140
|
+
}
|
|
141
|
+
if (o.readme) {
|
|
142
|
+
const file = path.join(o.root, 'README.md');
|
|
143
|
+
let text;
|
|
144
|
+
try {
|
|
145
|
+
text = fs.readFileSync(file, 'utf8');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
text = undefined;
|
|
149
|
+
}
|
|
150
|
+
if (text === undefined)
|
|
151
|
+
note('readme README.md not found — nothing written\n');
|
|
152
|
+
else {
|
|
153
|
+
const r = (0, traced_files_1.replaceMarkerRegion)(text, (0, traced_files_1.renderAttestMarker)(report));
|
|
154
|
+
if (r.ok) {
|
|
155
|
+
if (r.changed)
|
|
156
|
+
fs.writeFileSync(file, r.text);
|
|
157
|
+
note(`readme marker region ${r.changed ? 'updated' : 'unchanged'}\n`);
|
|
158
|
+
}
|
|
159
|
+
else if (r.reason === 'no-marker')
|
|
160
|
+
note('readme no marker in README.md — nothing written (add the begin/end markers to opt in)\n');
|
|
161
|
+
else {
|
|
162
|
+
io.stderr('attest: README.md has an unbalanced holmes-kit:attest marker (each must appear exactly once, begin first) — nothing written\n');
|
|
163
|
+
code = 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return code;
|
|
168
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Commands:
|
|
5
5
|
* init [--mode guardrail|governed] [--target dir] [--dry-run] [--remove] [--force] ...
|
|
6
6
|
* doctor [--target dir] [--json]
|
|
7
|
+
* attest [--target dir] [--exclude a,b] [--json] [--badge] [--readme]
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* The installed package root. From `dist/holmes/cli/index.js` that is three levels up. Node resolves
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -55,6 +55,7 @@ const colophon_1 = require("./colophon");
|
|
|
55
55
|
* Commands:
|
|
56
56
|
* init [--mode guardrail|governed] [--target dir] [--dry-run] [--remove] [--force] ...
|
|
57
57
|
* doctor [--target dir] [--json]
|
|
58
|
+
* attest [--target dir] [--exclude a,b] [--json] [--badge] [--readme]
|
|
58
59
|
*/
|
|
59
60
|
/**
|
|
60
61
|
* The installed package root. From `dist/holmes/cli/index.js` that is three levels up. Node resolves
|
|
@@ -78,6 +79,8 @@ const KNOWN_FLAGS = {
|
|
|
78
79
|
init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent', 'mcp-launcher', 'autonomy', 'no-autonomy'],
|
|
79
80
|
autonomy: ['help', 'target', 'for'],
|
|
80
81
|
doctor: ['help', 'target', 'json'],
|
|
82
|
+
// @implements A-SPEC-702 — a statement, not a gate: how many source files trace to an approved spec.
|
|
83
|
+
attest: ['help', 'target', 'exclude', 'json', 'badge', 'readme'],
|
|
81
84
|
skills: ['help', 'target'],
|
|
82
85
|
ci: ['help', 'target', 'specs-dir', 'json'],
|
|
83
86
|
serve: ['help', 'target', 'port'],
|
|
@@ -119,7 +122,7 @@ class UnknownFlagError extends Error {
|
|
|
119
122
|
* what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
|
|
120
123
|
* --force). A boolean flag must never consume a token.
|
|
121
124
|
*/
|
|
122
|
-
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
|
|
125
|
+
const BOOLEAN_FLAGS = new Set(['badge', 'readme', 'help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
|
|
123
126
|
function parseFlags(argv, known) {
|
|
124
127
|
const flags = {};
|
|
125
128
|
const positionals = [];
|
|
@@ -239,6 +242,14 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
|
239
242
|
--target <dir> also verify that target's wiring points at this install
|
|
240
243
|
--json machine-readable output
|
|
241
244
|
|
|
245
|
+
holmes-kit attest [flags] say how many source files trace to an APPROVED spec (a statement, not a gate)
|
|
246
|
+
--target <dir> the project to measure (default: current directory)
|
|
247
|
+
--exclude <a,b> path prefixes that are not this project's source (vendored trees); echoed in the output
|
|
248
|
+
--json machine-readable output
|
|
249
|
+
--badge write a self-contained SVG to .ax/badges/traced.svg
|
|
250
|
+
--readme update ONLY the region between the holmes-kit:attest markers in README.md;
|
|
251
|
+
a README without the markers is never written
|
|
252
|
+
|
|
242
253
|
holmes-kit upgrade [flags] install the latest and re-pin every recorded workspace (one command)
|
|
243
254
|
--dry-run show the plan (current→latest, workspaces to re-pin), change nothing
|
|
244
255
|
--yes skip the confirmation prompt (required in a non-interactive shell)
|
|
@@ -376,6 +387,14 @@ async function main(argv) {
|
|
|
376
387
|
}
|
|
377
388
|
return reported.code;
|
|
378
389
|
}
|
|
390
|
+
// @implements A-SPEC-702 — reads one scan and the spec store; writes only the badge and the README's
|
|
391
|
+
// marker region, and only when asked. The numbers never change the exit code.
|
|
392
|
+
if (cmd === 'attest') {
|
|
393
|
+
const { runAttest } = require('./attest');
|
|
394
|
+
const target = typeof flags.target === 'string' ? path.resolve(flags.target) : process.cwd();
|
|
395
|
+
const exclude = typeof flags.exclude === 'string' ? flags.exclude.split(',') : [];
|
|
396
|
+
return runAttest({ root: target, exclude, json: flags.json === true, badge: flags.badge === true, readme: flags.readme === true }, { stdout: (t) => process.stdout.write(t), stderr: (t) => process.stderr.write(t) });
|
|
397
|
+
}
|
|
379
398
|
if (cmd === 'doctor') {
|
|
380
399
|
// A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
|
|
381
400
|
// (round-6) — the typo is the diagnosis, so say it.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SpecStore } from '../../spec/spec-store';
|
|
2
2
|
import { Approval } from '../../guardrail/risk-gate';
|
|
3
|
+
import { type RenumberPlan } from '../../spec/renumber';
|
|
3
4
|
export interface SpecLifecycleContext {
|
|
4
5
|
store: SpecStore;
|
|
5
6
|
resolveLedgerRoot(root?: string): {
|
|
@@ -64,6 +65,19 @@ export declare function createSpecLifecycleHandlers(context: SpecLifecycleContex
|
|
|
64
65
|
dependents: string[];
|
|
65
66
|
reason?: undefined;
|
|
66
67
|
}>;
|
|
68
|
+
/**
|
|
69
|
+
* @implements A-SPEC-700.2
|
|
70
|
+
* The plan a person SAW, executed under ONE approval, before the merge. `plan` reads; `apply`
|
|
71
|
+
* rebuilds the plan, refuses `plan-changed` if it is not the one that was seen, demands a
|
|
72
|
+
* `config-write` approval bound to the plan's `target`, then runs every move through the SAME
|
|
73
|
+
* path `spec_renumber` uses, inside one store hold. It never seals: `spec_approve` is the only
|
|
74
|
+
* sealer, so `approveOrder` is returned and the caller runs it.
|
|
75
|
+
*/
|
|
76
|
+
spec_reconcile(a: {
|
|
77
|
+
root?: string;
|
|
78
|
+
operation?: string;
|
|
79
|
+
plan?: unknown;
|
|
80
|
+
}): Promise<Record<string, unknown>>;
|
|
67
81
|
spec_renumber(a: {
|
|
68
82
|
root?: string;
|
|
69
83
|
oldBase: string;
|
|
@@ -79,13 +93,13 @@ export declare function createSpecLifecycleHandlers(context: SpecLifecycleContex
|
|
|
79
93
|
} | {
|
|
80
94
|
ok: boolean;
|
|
81
95
|
dryRun: boolean;
|
|
82
|
-
plan:
|
|
96
|
+
plan: RenumberPlan;
|
|
83
97
|
movedSpecs?: undefined;
|
|
84
98
|
} | {
|
|
85
99
|
ok: boolean;
|
|
86
100
|
dryRun: boolean;
|
|
87
101
|
movedSpecs: number;
|
|
88
|
-
plan:
|
|
102
|
+
plan: RenumberPlan;
|
|
89
103
|
}>;
|
|
90
104
|
/**
|
|
91
105
|
* @implements A-SPEC-538.1
|