@holmes-lab/holmes-kit 0.1.8 → 0.1.10
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 +99 -0
- package/README.md +48 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/test-platform.d.ts +25 -0
- package/dist/holmes/cli/test-platform.js +38 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
- package/dist/holmes/cpg/cpg-scanner.js +53 -1
- package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
- package/dist/holmes/cpg/forbidden-edges.js +140 -0
- package/dist/holmes/cpg/hash-cache.js +13 -5
- package/dist/holmes/cpg/language-parser-walk.js +70 -4
- package/dist/holmes/cpg/proposed-content.d.ts +51 -0
- package/dist/holmes/cpg/proposed-content.js +72 -0
- package/dist/holmes/cpg/required-calls.d.ts +62 -0
- package/dist/holmes/cpg/required-calls.js +93 -0
- package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
- package/dist/holmes/guardrail/cspec-change.js +70 -0
- package/dist/holmes/guardrail/risk-classifier.js +122 -0
- package/dist/holmes/guardrail/write-target.d.ts +42 -0
- package/dist/holmes/guardrail/write-target.js +69 -18
- package/dist/holmes/hooks/pre-tool-use.js +90 -5
- package/dist/holmes/hooks/stop.d.ts +17 -0
- package/dist/holmes/hooks/stop.js +39 -2
- package/dist/holmes/mcp/handlers.d.ts +41 -0
- package/dist/holmes/mcp/handlers.js +173 -3
- package/dist/holmes/mcp/tool-schemas.js +12 -0
- package/dist/holmes/project/dependencies.d.ts +15 -0
- package/dist/holmes/project/dependencies.js +58 -0
- package/dist/holmes/project/json-state.d.ts +24 -0
- package/dist/holmes/project/json-state.js +30 -0
- package/dist/holmes/reverse/scan.js +8 -1
- package/dist/holmes/review/scope.d.ts +29 -0
- package/dist/holmes/review/scope.js +44 -0
- package/dist/holmes/rtm/test-scope.d.ts +44 -0
- package/dist/holmes/rtm/test-scope.js +92 -2
- package/dist/holmes/server/dashboard.d.ts +77 -0
- package/dist/holmes/server/dashboard.js +703 -183
- package/dist/holmes/spec/approval-blockers.d.ts +21 -5
- package/dist/holmes/spec/approval-blockers.js +49 -6
- package/dist/holmes/spec/legacy-format.d.ts +14 -0
- package/dist/holmes/spec/legacy-format.js +15 -1
- package/dist/holmes/spec/nonfunctional.d.ts +70 -0
- package/dist/holmes/spec/nonfunctional.js +119 -0
- package/dist/holmes/spec/spec-parser.d.ts +25 -0
- package/dist/holmes/spec/spec-parser.js +46 -2
- package/dist/holmes/spec/spec-types.d.ts +4 -1
- package/dist/holmes/spec/spec-types.js +13 -1
- package/dist/holmes/testing/effects.d.ts +54 -0
- package/dist/holmes/testing/effects.js +107 -0
- package/package.json +3 -2
- package/playbooks/promote-slice/PLAYBOOK.md +20 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-238
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.observeEffects = observeEffects;
|
|
5
|
+
exports.expectPure = expectPure;
|
|
6
|
+
/**
|
|
7
|
+
* Every filesystem READ surface, found by prefix rather than listed by name.
|
|
8
|
+
*
|
|
9
|
+
* Enumerating method names would let a new one through, and a pass reads as "no access". Judging the
|
|
10
|
+
* surface whole is the direction this repository has already taken three times: tool names in
|
|
11
|
+
* `write-target.ts`, shell verbs in `risk-classifier.ts`, and dependency declaration surfaces in
|
|
12
|
+
* `project/dependencies.ts`. Prefix matching also means a future Node release's new read method is
|
|
13
|
+
* covered the day it appears.
|
|
14
|
+
*/
|
|
15
|
+
const READ_PREFIXES = /^(read|stat|lstat|fstat|exists|open|access|opendir|realpath|watch|glob)/;
|
|
16
|
+
/**
|
|
17
|
+
* The CommonJS `fs` module object, NOT an `import * as fs` namespace.
|
|
18
|
+
*
|
|
19
|
+
* The namespace an ES import produces exposes getter-only properties: assigning to one throws
|
|
20
|
+
* `TypeError: Cannot set property access of #<Object> which has only a getter`. Measured while
|
|
21
|
+
* writing this — the standalone probe worked because it used `require`, and the first version of
|
|
22
|
+
* this module did not, so every case failed at the first patch. The module object is what the code
|
|
23
|
+
* under test actually calls through.
|
|
24
|
+
*/
|
|
25
|
+
const fsModule = () =>
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
27
|
+
require('node:fs');
|
|
28
|
+
/**
|
|
29
|
+
* Run `fn` and report what it touched.
|
|
30
|
+
*
|
|
31
|
+
* @implements A-SPEC-238
|
|
32
|
+
* The body's exception is RETURNED in `threw`, not re-thrown. "Why did it fail" and "what did it
|
|
33
|
+
* touch" are different pieces of information and a caller usually needs both; re-throwing would
|
|
34
|
+
* restore the globals but discard the observation.
|
|
35
|
+
*
|
|
36
|
+
* WHAT THIS DOES NOT SEE. Network calls and child processes — no claim in this repository's
|
|
37
|
+
* `## Non-Functional` sections mentions them, and offering coverage would recreate the
|
|
38
|
+
* "documented but unenforced" state. Asynchronous work that lands after `fn` returns: every function
|
|
39
|
+
* making a purity claim here is synchronous. Reads through a handle obtained before observation
|
|
40
|
+
* began. And a grammar loaded once per process — measured 2026-08-23, `extractSymbols` performs 26
|
|
41
|
+
* filesystem calls on the FIRST call in a process and zero on every call after, including the first
|
|
42
|
+
* call for a different language. That is one-time lazy loading, not per-call I/O, and this tool
|
|
43
|
+
* cannot tell them apart: warm the callee first.
|
|
44
|
+
*/
|
|
45
|
+
function observeEffects(fn) {
|
|
46
|
+
const target = fsModule();
|
|
47
|
+
const surface = Object.keys(target).filter((k) => READ_PREFIXES.test(k) && typeof target[k] === 'function');
|
|
48
|
+
const originals = {};
|
|
49
|
+
const unwatched = [];
|
|
50
|
+
const calls = [];
|
|
51
|
+
for (const name of surface) {
|
|
52
|
+
const original = target[name];
|
|
53
|
+
try {
|
|
54
|
+
target[name] = function (...args) {
|
|
55
|
+
calls.push(name);
|
|
56
|
+
return original.apply(target, args);
|
|
57
|
+
};
|
|
58
|
+
// Assignment can silently no-op on a non-writable property, which would look like a wrapped
|
|
59
|
+
// method that never fires. Confirm it took.
|
|
60
|
+
if (target[name] === original) {
|
|
61
|
+
unwatched.push(name);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
originals[name] = original;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
unwatched.push(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const realNow = Date.now;
|
|
71
|
+
const realRandom = Math.random;
|
|
72
|
+
let clock = 0;
|
|
73
|
+
let random = 0;
|
|
74
|
+
Date.now = function () { clock++; return realNow.call(Date); };
|
|
75
|
+
Math.random = function () { random++; return realRandom.call(Math); };
|
|
76
|
+
let threw;
|
|
77
|
+
let didThrow = false;
|
|
78
|
+
try {
|
|
79
|
+
fn();
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
threw = e;
|
|
83
|
+
didThrow = true;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
// Restoration is in `finally` because a tool that patches globals and fails to put them back
|
|
87
|
+
// poisons every test that runs afterwards — and that poisoning surfaces far from its cause.
|
|
88
|
+
for (const name of Object.keys(originals))
|
|
89
|
+
target[name] = originals[name];
|
|
90
|
+
Date.now = realNow;
|
|
91
|
+
Math.random = realRandom;
|
|
92
|
+
}
|
|
93
|
+
const out = { fs: calls, clock, random, unwatched };
|
|
94
|
+
return didThrow ? { ...out, threw } : out;
|
|
95
|
+
}
|
|
96
|
+
/** Assert that `fn` touched nothing, naming what it touched when it did. */
|
|
97
|
+
function expectPure(fn) {
|
|
98
|
+
const e = observeEffects(fn);
|
|
99
|
+
const touched = [
|
|
100
|
+
...e.fs.map((m) => `fs.${m}`),
|
|
101
|
+
...(e.clock ? [`Date.now ×${e.clock}`] : []),
|
|
102
|
+
...(e.random ? [`Math.random ×${e.random}`] : []),
|
|
103
|
+
];
|
|
104
|
+
if (touched.length > 0) {
|
|
105
|
+
throw new Error(`expected no effects, observed: ${[...new Set(touched)].join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
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.1.
|
|
4
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"test": "npm run typecheck && jest",
|
|
24
24
|
"mcp": "node bin/holmes-mcp.js",
|
|
25
25
|
"release": "npm run build && node scripts/make-release.js",
|
|
26
|
-
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
26
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
27
|
+
"prepublishOnly": "node scripts/verify-release.js"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|
|
29
30
|
"ase",
|
|
@@ -107,6 +107,25 @@ depends_on:
|
|
|
107
107
|
승인 후에 발견된 것은 **새 리비전**이지 승인 취소가 아니다. 되돌리는 비용이 승인을 미루게 만들면,
|
|
108
108
|
게이트는 품질 장치가 아니라 지연 장치가 된다.
|
|
109
109
|
|
|
110
|
+
## 문서를 물리는 법 — `spec_retire`
|
|
111
|
+
|
|
112
|
+
승인이 권한을 주는 행위라면 폐기는 거두는 행위다. 그래서 기본 문턱이 낮다 — 잘못 만든 초안을
|
|
113
|
+
치우는 데 대역외 열쇠를 요구하면 일상 작업이 막힌다. **다만 문턱은 행위의 이름이 아니라 결과의
|
|
114
|
+
위험을 따른다.** 두 경우는 폐기가 곧 게이트 열기다:
|
|
115
|
+
|
|
116
|
+
- **봉인된 문서.** 코드 게이트는 "대상 A-SPEC을 `depends_on`에 담은 approved T-SPEC"을 요구한다.
|
|
117
|
+
그 T-SPEC을 폐기하면 요구가 사라진다. 그래서 `approved_digest`를 가진 문서의 폐기는
|
|
118
|
+
`spec_approve`와 **같은** `HOLMES_APPROVAL`을 요구하며 fail-closed다.
|
|
119
|
+
- **활성 문서가 의존하는 문서.** `approved` 자식이 하나라도 있으면 거부하고, 문면이 그 자식을
|
|
120
|
+
id로 지목한다. 그 사슬은 지금 무언가를 지탱하고 있다.
|
|
121
|
+
|
|
122
|
+
사유는 `retired_reason`에 남는다 — 비어 있으면 적지 않고, `superseded_by`가 이미 답이면 묻지
|
|
123
|
+
않는다. 멱등이다: 이미 `outdated`인 문서를 다시 폐기하면 아무것도 쓰지 않는다. 두 번째 호출이
|
|
124
|
+
첫 번째의 사유를 덮어쓰면 기록이 아니라 낙서다.
|
|
125
|
+
|
|
126
|
+
> `outdated`는 `SPEC_STATUSES`에 처음부터 있었지만 이를 쓰는 코드가 0곳, 그 상태인 문서도
|
|
127
|
+
> 0건이었다(실측 2026-08-21). 도달할 수 없는 상태는 선언이 아니라 장식이다.
|
|
128
|
+
|
|
110
129
|
## 흔한 오해
|
|
111
130
|
|
|
112
131
|
| 오해 | 사실 |
|
|
@@ -116,6 +135,7 @@ depends_on:
|
|
|
116
135
|
| "A-SPEC을 승인했으니 코드를 쓸 수 있다" | 테스트는 그렇다. 코드는 T-SPEC이 남았다 |
|
|
117
136
|
| "T-SPEC 승인했는데 막힌다 = 게이트 버그" | `depends_on` 누락. 메시지가 동일해서 구별되지 않는다 |
|
|
118
137
|
| "일단 승인하고 검증은 나중에" | 통과한 채 깨진 그래프가 남는다. 4단계 참조 |
|
|
138
|
+
| "폐기는 `status: outdated`로 손편집한다" | `spec_retire(id, reason)`이 유일한 경로다. 봉인된 문서는 승인 열쇠를 요구하고, approved 의존자가 있으면 거부한다 |
|
|
119
139
|
| "`missing`이 'approved A-SPEC'이니 A-SPEC을 승인하면 된다" | `AUTHOR_TSPEC`이면 아무거나 하나, `WRITE_TEST`면 대상 그것. 같은 문자열이 두 가지를 뜻한다 |
|
|
120
140
|
|
|
121
141
|
## 검증
|