@holmes-lab/holmes-kit 0.3.3 → 0.3.5
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 +40 -0
- package/README.md +42 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/mcp/handlers.js +11 -2
- package/dist/holmes/review/excerpt-align.d.ts +14 -0
- package/dist/holmes/review/excerpt-align.js +51 -0
- package/dist/holmes/review/python-env.d.ts +7 -0
- package/dist/holmes/review/python-env.js +76 -0
- package/dist/holmes/review/run-replay.d.ts +2 -0
- package/dist/holmes/review/run-replay.js +6 -1
- package/dist/holmes/review/test-runner.d.ts +6 -0
- package/dist/holmes/review/test-runner.js +9 -2
- package/dist/holmes/rtm/anchor-comment.d.ts +9 -0
- package/dist/holmes/rtm/anchor-comment.js +34 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
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
|
+
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.3.5] - 2026-09-01
|
|
9
|
+
|
|
10
|
+
Two P0 dogfooding fixes: the self-healer no longer breaks what it heals, and Python evidence
|
|
11
|
+
finally flows on venv projects.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **spec_remediate language-blind `//` injection** (A-SPEC-501.1, dogfooded twice): the anchor is
|
|
16
|
+
now injected in the file's own comment syntax (`#` for py/yml/sh/toml + Makefile/Dockerfile,
|
|
17
|
+
`//` for the C family — byte-identical to before, `/* */` for CSS), and a file whose syntax the
|
|
18
|
+
tool does not know is **refused with guidance** instead of broken. Every injected form is
|
|
19
|
+
round-trip verified against the real anchor scanner, so injector and scanner can no longer
|
|
20
|
+
drift apart.
|
|
21
|
+
- **test_run on Python venv projects** (A-SPEC-502.1): the interpreter resolves from evidence —
|
|
22
|
+
`HOLMES_PYTHON` first, then a `.venv`/`venv` carrying `pyvenv.cfg`, then the old `python3`
|
|
23
|
+
fallback (byte-identical when no venv exists). A `conftest.py` in the scoped file list widens
|
|
24
|
+
to its directory (the fixture's actual blast radius) instead of collection-ERRORing the whole
|
|
25
|
+
run — measured: the same tree passed 1995 cases manually while every scoped run failed.
|
|
26
|
+
|
|
27
|
+
<!-- @implements A-SPEC-209 -->
|
|
28
|
+
## [0.3.4] - 2026-09-01
|
|
29
|
+
|
|
30
|
+
Docs: a friendly, accurate setup guide for the opt-in semantic tiers.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- README Quickstart step 4: enabling the semantic layer — tier `local` (install
|
|
35
|
+
`@xenova/transformers`, no egress) and tier `cloud` (`gemini-embedding-001`): get a key from
|
|
36
|
+
Google AI Studio, store it out-of-tree via `holmes-kit semantic-key set` (stdin-only, 0600 /
|
|
37
|
+
macOS keychain, value never printed), env-var alternatives for CI
|
|
38
|
+
(`HOLMES_SEMANTIC_API_KEY` / `GEMINI_API_KEY` / `GOOGLE_API_KEY`), revocation via
|
|
39
|
+
`semantic-key unset`, and the security stance (no project-tree key source; agent
|
|
40
|
+
self-consent is gated).
|
|
41
|
+
|
|
42
|
+
### Internal (measurement plumbing, no default-path change)
|
|
43
|
+
|
|
44
|
+
- `alignExcerpt` request-aligned excerptor + `dumpBodies.mode` (A-SPEC-500.1) — feeds the
|
|
45
|
+
judgment-precision experiment REQ-500; default excerpts stay byte-identical.
|
|
46
|
+
|
|
7
47
|
<!-- @implements A-SPEC-209 -->
|
|
8
48
|
## [0.3.3] - 2026-08-31
|
|
9
49
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,46 @@ npx holmes-kit doctor
|
|
|
117
117
|
```
|
|
118
118
|
*If everything is green, your project is governed and ready for AI pair-programming!*
|
|
119
119
|
|
|
120
|
+
### 4. Optional: Enable the Semantic Layer (Tier `local` / Tier `cloud`)
|
|
121
|
+
|
|
122
|
+
Out of the box Holmes-Kit runs tier **`none`** — lexical + citation + graph search, **zero
|
|
123
|
+
egress**. Two opt-in tiers raise recall on requests your vocabulary can't reach (measured on 305
|
|
124
|
+
traceability cases — see the feature list above):
|
|
125
|
+
|
|
126
|
+
**Tier `local` — no egress, no account.** Install the optional embedding runtime next to
|
|
127
|
+
holmes-kit and the local model (`bge-m3`) is picked up automatically:
|
|
128
|
+
```bash
|
|
129
|
+
npm install --save-dev @xenova/transformers
|
|
130
|
+
npx holmes-kit doctor # → semantic tier: local (no egress)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Tier `cloud` — highest recall, explicit consent (`gemini-embedding-001`).** Setting a key IS
|
|
134
|
+
the consent act: with a key present, repository-derived text is sent to Google's embedding API.
|
|
135
|
+
|
|
136
|
+
1. Get a Gemini API key (Google AI Studio → <https://aistudio.google.com/apikey>; the free tier
|
|
137
|
+
is enough to try it).
|
|
138
|
+
2. Store it **outside your project tree** with the built-in command — the key rides **stdin,
|
|
139
|
+
never argv**, lands in `~/.holmes/credentials.json` with `0600` permissions, and no output
|
|
140
|
+
ever contains the value:
|
|
141
|
+
```bash
|
|
142
|
+
npx holmes-kit semantic-key set # hidden prompt on a TTY; or: echo "$KEY" | npx holmes-kit semantic-key set
|
|
143
|
+
npx holmes-kit semantic-key status # shows the key's SOURCE only, never the value
|
|
144
|
+
npx holmes-kit doctor # → semantic tier: cloud (egress: on)
|
|
145
|
+
```
|
|
146
|
+
On macOS the key prefers the system keychain; elsewhere the `0600` file is the store.
|
|
147
|
+
Environment variables also work and take precedence (`HOLMES_SEMANTIC_API_KEY` dedicated, or
|
|
148
|
+
the ecosystem-compatible `GEMINI_API_KEY` / `GOOGLE_API_KEY`) — useful for CI. Prefer
|
|
149
|
+
`semantic-key set` on workstations: it keeps the key out of shell history, `.env` files, and
|
|
150
|
+
the repository.
|
|
151
|
+
3. To revoke consent at any time:
|
|
152
|
+
```bash
|
|
153
|
+
npx holmes-kit semantic-key unset # clears the stored key; tier falls back to local/none
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> 🔒 **Never** commit a key, pass it as a CLI argument, or put it in a file inside the project
|
|
157
|
+
> tree. Holmes-Kit's credential chain has **no project-tree source by design**, and agents are
|
|
158
|
+
> gated from setting keys on their own — consent stays a human act.
|
|
159
|
+
|
|
120
160
|
---
|
|
121
161
|
|
|
122
162
|
## 🔄 Daily Workflow (How It Works)
|
|
@@ -193,11 +233,11 @@ Holmes-Kit embeds native AST & Code Property Graph (D-CPG) analyzers to track ca
|
|
|
193
233
|
| **macOS** | Apple Silicon (arm64) / Intel (x64) | ✅ Tier 1 | macOS 12+ (Full hook enforcement) |
|
|
194
234
|
| **Linux** | x86_64 / arm64 | ✅ Tier 1 | Ubuntu, Debian, Fedora, Arch, RHEL |
|
|
195
235
|
| **Windows (WSL2)** | x86_64 | ✅ Tier 1 | WSL2 Ubuntu/Debian recommended |
|
|
196
|
-
| **Windows Native** | x86_64 | ✅ Tier 1 | Windows 10/11 (Node.js 20+; prebuilt natives, no build tools needed in the common case). **Field-validated 2026-08-31** on a real user machine: registry install, natives (better-sqlite3 + 7 tree-sitter grammars), both OS gates, MCP handshake (30 tools), interactive init TUI, out-of-band approval channel (doctor 25 PASS; the 4 false FAILs it also showed were doctor's own win32 spawn bug, fixed in 0.3.2). See
|
|
236
|
+
| **Windows Native** | x86_64 | ✅ Tier 1 | Windows 10/11 (Node.js 20+; prebuilt natives, no build tools needed in the common case). **Field-validated 2026-08-31** on a real user machine: registry install, natives (better-sqlite3 + 7 tree-sitter grammars), both OS gates, MCP handshake (30 tools), interactive init TUI, out-of-band approval channel (doctor 25 PASS; the 4 false FAILs it also showed were doctor's own win32 spawn bug, fixed in 0.3.2). See ADR-015 (platform tier is decided by executed verification — internal decision record) for tier criteria and residual risks (NTFS 8.3 names, reserved device names, 260-char paths; no Windows CI yet) |
|
|
197
237
|
|
|
198
238
|
> **Runtime Requirement**: Node.js `>= 20.0.0` (LTS recommended)
|
|
199
239
|
>
|
|
200
|
-
> **How a tier is decided**: by the verification that actually executes, not by declaration. A platform is Tier 1 only while its gate verdicts are exercised by the suite; if that stops being true it is demoted and the demotion is recorded. See
|
|
240
|
+
> **How a tier is decided**: by the verification that actually executes, not by declaration. A platform is Tier 1 only while its gate verdicts are exercised by the suite; if that stops being true it is demoted and the demotion is recorded. See ADR-015 (platform tier is decided by executed verification — internal decision record).
|
|
201
241
|
|
|
202
242
|
---
|
|
203
243
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f98587b-mthg333w
|
|
@@ -130,6 +130,7 @@ const package_1 = require("../review/package");
|
|
|
130
130
|
const risk_classifier_1 = require("../guardrail/risk-classifier");
|
|
131
131
|
const risk_gate_1 = require("../guardrail/risk-gate");
|
|
132
132
|
const elicit_approval_1 = require("./elicit-approval");
|
|
133
|
+
const anchor_comment_1 = require("../rtm/anchor-comment");
|
|
133
134
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
134
135
|
const approval_grants_1 = require("../governance/approval-grants");
|
|
135
136
|
const spec_digest_1 = require("../spec/spec-digest");
|
|
@@ -3037,9 +3038,17 @@ depends_on:
|
|
|
3037
3038
|
if (a.targetFile && a.aspecId) {
|
|
3038
3039
|
const fullPath = path.isAbsolute(a.targetFile) ? a.targetFile : path.join(root, a.targetFile);
|
|
3039
3040
|
if (fs.existsSync(fullPath)) {
|
|
3041
|
+
// @implements A-SPEC-501.1 — BUG-4 (dogfooded twice): the old unconditional
|
|
3042
|
+
// `// @implements` broke Python/YAML targets outright AND produced anchors the scanner
|
|
3043
|
+
// could not read. The syntax comes from the one map the scanner is aligned with, an
|
|
3044
|
+
// unknown syntax is an honest refusal, and the containment check is form-agnostic so a
|
|
3045
|
+
// `#` anchor is not double-injected.
|
|
3046
|
+
const anchorTag = (0, anchor_comment_1.anchorLineFor)(fullPath, a.aspecId);
|
|
3040
3047
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
3041
|
-
|
|
3042
|
-
|
|
3048
|
+
if (anchorTag === null) {
|
|
3049
|
+
actionsTaken.push(`앵커 주입 거부: ${path.basename(fullPath)} — 이 확장자의 주석 문법을 모릅니다. 파일 관례에 맞는 주석으로 1행에 '@implements ${a.aspecId}'를 직접 추가하십시오`);
|
|
3050
|
+
}
|
|
3051
|
+
else if (!content.includes(`@implements ${a.aspecId}`)) {
|
|
3043
3052
|
fs.writeFileSync(fullPath, `${anchorTag}\n${content}`);
|
|
3044
3053
|
actionsTaken.push(`Injected ${anchorTag} on line 1 of ${a.targetFile}`);
|
|
3045
3054
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-aligned excerpting for the judgment-measurement dump. The excerpt ladder
|
|
3
|
+
* (S-505→S-516) showed excerpt information content DOMINATES the agent-judge's precision, and
|
|
4
|
+
* the shipped head-slice starves the judge exactly when the relevant region sits mid-file —
|
|
5
|
+
* imports and module prose fill the window and the judge degrades to filename guessing.
|
|
6
|
+
*
|
|
7
|
+
* The alignment is deliberately dumb and PURE: slide a chars-wide window at half-stride and keep
|
|
8
|
+
* the one containing the most DISTINCT request tokens (presence, not frequency — one dense
|
|
9
|
+
* mention must not outvote three different matched concepts). Ties keep the EARLIEST window and
|
|
10
|
+
* zero overlap falls back to the head slice, so where there is no signal the behavior is
|
|
11
|
+
* byte-identical to the status quo. Every return is a verbatim substring — the judge must only
|
|
12
|
+
* ever see real code.
|
|
13
|
+
*/
|
|
14
|
+
export declare function alignExcerpt(text: string, request: string, chars: number): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-500.1
|
|
3
|
+
/**
|
|
4
|
+
* Request-aligned excerpting for the judgment-measurement dump. The excerpt ladder
|
|
5
|
+
* (S-505→S-516) showed excerpt information content DOMINATES the agent-judge's precision, and
|
|
6
|
+
* the shipped head-slice starves the judge exactly when the relevant region sits mid-file —
|
|
7
|
+
* imports and module prose fill the window and the judge degrades to filename guessing.
|
|
8
|
+
*
|
|
9
|
+
* The alignment is deliberately dumb and PURE: slide a chars-wide window at half-stride and keep
|
|
10
|
+
* the one containing the most DISTINCT request tokens (presence, not frequency — one dense
|
|
11
|
+
* mention must not outvote three different matched concepts). Ties keep the EARLIEST window and
|
|
12
|
+
* zero overlap falls back to the head slice, so where there is no signal the behavior is
|
|
13
|
+
* byte-identical to the status quo. Every return is a verbatim substring — the judge must only
|
|
14
|
+
* ever see real code.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.alignExcerpt = alignExcerpt;
|
|
18
|
+
const tokensOf = (request) => {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const m of request.toLowerCase().match(/[a-z0-9_$가-힣]+/g) ?? []) {
|
|
21
|
+
if (m.length >= 3)
|
|
22
|
+
seen.add(m);
|
|
23
|
+
}
|
|
24
|
+
return [...seen];
|
|
25
|
+
};
|
|
26
|
+
function alignExcerpt(text, request, chars) {
|
|
27
|
+
if (text.length <= chars)
|
|
28
|
+
return text;
|
|
29
|
+
const tokens = tokensOf(request);
|
|
30
|
+
const head = text.slice(0, chars);
|
|
31
|
+
if (tokens.length === 0)
|
|
32
|
+
return head;
|
|
33
|
+
const stride = Math.ceil(chars / 2);
|
|
34
|
+
let best = head;
|
|
35
|
+
let bestCount = 0;
|
|
36
|
+
for (let at = 0; at < text.length; at += stride) {
|
|
37
|
+
const window = text.slice(at, at + chars);
|
|
38
|
+
const lower = window.toLowerCase();
|
|
39
|
+
let count = 0;
|
|
40
|
+
for (const t of tokens)
|
|
41
|
+
if (lower.includes(t))
|
|
42
|
+
count++;
|
|
43
|
+
if (count > bestCount) {
|
|
44
|
+
bestCount = count;
|
|
45
|
+
best = window;
|
|
46
|
+
}
|
|
47
|
+
if (at + chars >= text.length)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return bestCount === 0 ? head : best;
|
|
51
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface PythonEnvDeps {
|
|
2
|
+
env: NodeJS.ProcessEnv;
|
|
3
|
+
exists: (p: string) => boolean;
|
|
4
|
+
platform: NodeJS.Platform;
|
|
5
|
+
}
|
|
6
|
+
export declare function pythonFor(cwd: string, deps: PythonEnvDeps): string;
|
|
7
|
+
export declare function pytestTargets(files: string[]): string[];
|
|
@@ -0,0 +1,76 @@
|
|
|
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.pythonFor = pythonFor;
|
|
37
|
+
exports.pytestTargets = pytestTargets;
|
|
38
|
+
// @implements A-SPEC-502.1
|
|
39
|
+
/**
|
|
40
|
+
* The two pure transforms that stand between planTestRun and runPytest's execFileSync — BUG-2,
|
|
41
|
+
* dogfooded on a real .venv project, where both halves of the evidence system idled:
|
|
42
|
+
*
|
|
43
|
+
* WHICH python: PATH's python3 was the system interpreter (no pytest), so last-green was never
|
|
44
|
+
* recorded and ART-4 debt piled 12 entries deep. The interpreter now resolves from EVIDENCE —
|
|
45
|
+
* pyvenv.cfg, the same proof containsPyvenvCfg uses in the scanner — never from a directory name,
|
|
46
|
+
* with HOLMES_PYTHON as the explicit operator escape hatch above all detection.
|
|
47
|
+
*
|
|
48
|
+
* WHAT arguments: conftest.py is a legitimate TEST FILE for anchor/coverage purposes, but not a
|
|
49
|
+
* runnable target — passed verbatim it collection-ERRORs every scoped run (measured: the same
|
|
50
|
+
* tree's manual `pytest tests/` was green, 1995 cases). A conftest argument WIDENS to its
|
|
51
|
+
* directory — the fixture's actual blast radius — because dropping it would verify less and
|
|
52
|
+
* keeping it verbatim verifies nothing.
|
|
53
|
+
*/
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
function pythonFor(cwd, deps) {
|
|
56
|
+
const explicit = deps.env.HOLMES_PYTHON;
|
|
57
|
+
if (typeof explicit === 'string' && explicit.trim() !== '')
|
|
58
|
+
return explicit;
|
|
59
|
+
for (const dir of ['.venv', 'venv']) {
|
|
60
|
+
if (deps.exists(path.join(cwd, dir, 'pyvenv.cfg'))) {
|
|
61
|
+
return deps.platform === 'win32'
|
|
62
|
+
? path.join(cwd, dir, 'Scripts', 'python.exe')
|
|
63
|
+
: path.join(cwd, dir, 'bin', 'python');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return 'python3';
|
|
67
|
+
}
|
|
68
|
+
function pytestTargets(files) {
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
const target = path.basename(f) === 'conftest.py' ? path.dirname(f) : f;
|
|
72
|
+
if (!out.includes(target))
|
|
73
|
+
out.push(target);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
@@ -241,6 +241,8 @@ export declare function runReplay(corpus: ReplayCorpus, limit: number, opts?: {
|
|
|
241
241
|
chars: number;
|
|
242
242
|
/** @implements A-SPEC-490 — drop leading license blocks before excerpting; default false keeps S-507 bytes. */
|
|
243
243
|
skipLicense?: boolean;
|
|
244
|
+
/** @implements A-SPEC-500.1 — 'aligned' excerpts the request-densest window instead of the head; default 'head' keeps prior bytes. */
|
|
245
|
+
mode?: 'head' | 'aligned';
|
|
244
246
|
};
|
|
245
247
|
}): Promise<ReplayResult>;
|
|
246
248
|
/**
|
|
@@ -64,6 +64,7 @@ const localize_1 = require("../rtm/localize");
|
|
|
64
64
|
// @implements A-SPEC-483 — content-level verification: the candidate's BODY, read from the
|
|
65
65
|
// materialized parent tree.
|
|
66
66
|
const content_verify_1 = require("./content-verify");
|
|
67
|
+
const excerpt_align_1 = require("./excerpt-align");
|
|
67
68
|
// @implements A-SPEC-467 — the associative-recall arm: measured beside the product, never inside it.
|
|
68
69
|
const assoc_arm_1 = require("../assoc/assoc-arm");
|
|
69
70
|
// @implements A-SPEC-470 — the impact axis's Phase-B external yardstick, always scored.
|
|
@@ -265,7 +266,11 @@ async function runReplay(corpus, limit, opts = {}) {
|
|
|
265
266
|
const raw = fs.readFileSync(path.join(dest, f), 'utf8');
|
|
266
267
|
// @implements A-SPEC-490 — the informative excerpt: license blocks first, then cut.
|
|
267
268
|
const text = opts.dumpBodies.skipLicense === true ? (0, content_verify_1.skipLicenseHeader)(raw) : raw;
|
|
268
|
-
|
|
269
|
+
// @implements A-SPEC-500.1 — 'aligned' picks the request-densest window; the
|
|
270
|
+
// default stays the head slice, byte-identical to the pre-mode path.
|
|
271
|
+
bodies[f] = opts.dumpBodies.mode === 'aligned'
|
|
272
|
+
? (0, excerpt_align_1.alignExcerpt)(text, c.subject, opts.dumpBodies.chars)
|
|
273
|
+
: text.slice(0, opts.dumpBodies.chars);
|
|
269
274
|
}
|
|
270
275
|
catch { /* unreadable candidate: omitted */ }
|
|
271
276
|
}
|
|
@@ -75,6 +75,12 @@ export declare function parseJUnitXmlCounts(xml: string, ext?: string): Record<s
|
|
|
75
75
|
* there), and a repo with no local jest could never yield meaningful suite evidence anyway.
|
|
76
76
|
*/
|
|
77
77
|
export declare function jestEntry(cwd: string): string | null;
|
|
78
|
+
/** Run the pytest half of a plan, taking execution evidence from pytest's built-in JUnit XML. */
|
|
79
|
+
export declare function runPytest(files: string[], mode: TestRunPlan['mode'], cwd: string): {
|
|
80
|
+
passed: boolean;
|
|
81
|
+
tail: string;
|
|
82
|
+
executed: Record<string, number>;
|
|
83
|
+
};
|
|
78
84
|
/**
|
|
79
85
|
* @implements A-SPEC-137.2
|
|
80
86
|
* Executed-test count from `cargo test` text output. cargo prints one `test <name> ... ok|FAILED|
|
|
@@ -39,6 +39,7 @@ exports.parseGoTestJson = parseGoTestJson;
|
|
|
39
39
|
exports.parseExecutedCounts = parseExecutedCounts;
|
|
40
40
|
exports.parseJUnitXmlCounts = parseJUnitXmlCounts;
|
|
41
41
|
exports.jestEntry = jestEntry;
|
|
42
|
+
exports.runPytest = runPytest;
|
|
42
43
|
exports.parseCargoTest = parseCargoTest;
|
|
43
44
|
exports.runCargo = runCargo;
|
|
44
45
|
exports.runGradle = runGradle;
|
|
@@ -50,6 +51,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
50
51
|
const fs = __importStar(require("node:fs"));
|
|
51
52
|
const os = __importStar(require("node:os"));
|
|
52
53
|
const path = __importStar(require("node:path"));
|
|
54
|
+
const python_env_1 = require("./python-env");
|
|
53
55
|
function planTestRun(scope) {
|
|
54
56
|
if (scope.tier === 'full') {
|
|
55
57
|
return { mode: 'full', testFiles: [], reason: 'full regression — run the entire suite' };
|
|
@@ -241,6 +243,7 @@ function runJest(files, mode, cwd) {
|
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
/** Run the pytest half of a plan, taking execution evidence from pytest's built-in JUnit XML. */
|
|
246
|
+
// @implements A-SPEC-502.1 — exported for the wiring test (a fake venv python capturing argv).
|
|
244
247
|
function runPytest(files, mode, cwd) {
|
|
245
248
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'holmes-pytest-'));
|
|
246
249
|
const report = path.join(dir, 'report.xml');
|
|
@@ -253,8 +256,12 @@ function runPytest(files, mode, cwd) {
|
|
|
253
256
|
// attribute per testcase. pytest's default since v6 is xunit2, which carries only `classname` —
|
|
254
257
|
// measured: a green 4-passed/1-skipped run yielded ZERO per-file evidence under the default, so
|
|
255
258
|
// the tier gate would have had nothing to verify against.
|
|
259
|
+
// @implements A-SPEC-502.1 — BUG-2, both halves: the interpreter resolves from pyvenv.cfg
|
|
260
|
+
// evidence (HOLMES_PYTHON above all, python3 fallback byte-identical to the old path), and a
|
|
261
|
+
// conftest.py argument widens to its directory instead of collection-ERRORing the whole run.
|
|
256
262
|
const args = ['-m', 'pytest', '-q', '-o', 'junit_family=xunit1', `--junit-xml=${report}`,
|
|
257
|
-
...(mode === 'scoped' ? ['--', ...files] : [])];
|
|
263
|
+
...(mode === 'scoped' ? ['--', ...(0, python_env_1.pytestTargets)(files)] : [])];
|
|
264
|
+
const python = (0, python_env_1.pythonFor)(cwd, { env: process.env, exists: fs.existsSync, platform: process.platform });
|
|
258
265
|
const read = () => {
|
|
259
266
|
try {
|
|
260
267
|
return parseJUnitXmlCounts(fs.readFileSync(report, 'utf8'));
|
|
@@ -264,7 +271,7 @@ function runPytest(files, mode, cwd) {
|
|
|
264
271
|
}
|
|
265
272
|
};
|
|
266
273
|
try {
|
|
267
|
-
const out = (0, node_child_process_1.execFileSync)(
|
|
274
|
+
const out = (0, node_child_process_1.execFileSync)(python, args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
268
275
|
return { passed: true, tail: tailOf(out), executed: read() };
|
|
269
276
|
}
|
|
270
277
|
catch (e) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one comment-syntax truth the anchor INJECTOR shares with the anchor SCANNER. BUG-4
|
|
3
|
+
* (dogfooded twice): spec_remediate wrote `// @implements` into every file type — a YAML workflow
|
|
4
|
+
* became a parse error, a Python golden file a SyntaxError, and the anchor was not even legible to
|
|
5
|
+
* extractAnchors. Every form emitted here matches the scanner's comment prefixes (`//`, `/*`, `#`);
|
|
6
|
+
* a syntax this map does not know returns null and the caller must REFUSE to write — an honest
|
|
7
|
+
* refusal beats a broken file, and beats an anchor the gate cannot read.
|
|
8
|
+
*/
|
|
9
|
+
export declare function anchorLineFor(filePath: string, aspecId: string): string | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-501.1
|
|
3
|
+
/**
|
|
4
|
+
* The one comment-syntax truth the anchor INJECTOR shares with the anchor SCANNER. BUG-4
|
|
5
|
+
* (dogfooded twice): spec_remediate wrote `// @implements` into every file type — a YAML workflow
|
|
6
|
+
* became a parse error, a Python golden file a SyntaxError, and the anchor was not even legible to
|
|
7
|
+
* extractAnchors. Every form emitted here matches the scanner's comment prefixes (`//`, `/*`, `#`);
|
|
8
|
+
* a syntax this map does not know returns null and the caller must REFUSE to write — an honest
|
|
9
|
+
* refusal beats a broken file, and beats an anchor the gate cannot read.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.anchorLineFor = anchorLineFor;
|
|
13
|
+
const SLASH = new Set(['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'java', 'c', 'h', 'cc', 'cpp',
|
|
14
|
+
'hpp', 'cs', 'go', 'rs', 'swift', 'kt', 'kts', 'scala', 'dart']);
|
|
15
|
+
const HASH = new Set(['py', 'rb', 'sh', 'bash', 'zsh', 'fish', 'yml', 'yaml', 'toml', 'pl', 'pm',
|
|
16
|
+
'r', 'jl', 'cmake', 'mk']);
|
|
17
|
+
const CSS = new Set(['css', 'scss', 'less']);
|
|
18
|
+
const HASH_BASENAMES = new Set(['Makefile', 'Dockerfile', 'Rakefile', 'Gemfile']);
|
|
19
|
+
function anchorLineFor(filePath, aspecId) {
|
|
20
|
+
const basename = filePath.slice(filePath.lastIndexOf('/') + 1);
|
|
21
|
+
if (HASH_BASENAMES.has(basename))
|
|
22
|
+
return `# @implements ${aspecId}`;
|
|
23
|
+
const dot = basename.lastIndexOf('.');
|
|
24
|
+
if (dot <= 0)
|
|
25
|
+
return null;
|
|
26
|
+
const ext = basename.slice(dot + 1).toLowerCase();
|
|
27
|
+
if (SLASH.has(ext))
|
|
28
|
+
return `// @implements ${aspecId}`;
|
|
29
|
+
if (HASH.has(ext))
|
|
30
|
+
return `# @implements ${aspecId}`;
|
|
31
|
+
if (CSS.has(ext))
|
|
32
|
+
return `/* @implements ${aspecId} */`;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.5",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|