@holmes-lab/holmes-kit 0.3.5 → 0.3.7
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 +48 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.d.ts +13 -1
- package/dist/holmes/cli/approve.js +35 -2
- package/dist/holmes/cli/index.js +9 -3
- package/dist/holmes/cpg/consistency-lints.d.ts +26 -0
- package/dist/holmes/cpg/consistency-lints.js +67 -0
- package/dist/holmes/cpg/cpg-scanner.js +8 -8
- package/dist/holmes/governance/approval-queue.d.ts +16 -2
- package/dist/holmes/governance/approval-queue.js +17 -6
- package/dist/holmes/guardrail/anchors.js +12 -2
- package/dist/holmes/mcp/handlers.d.ts +22 -0
- package/dist/holmes/mcp/handlers.js +42 -2
- package/dist/holmes/mcp/tool-schemas.js +2 -2
- package/dist/holmes/rtm/anchor-ids.d.ts +15 -0
- package/dist/holmes/rtm/anchor-ids.js +39 -0
- package/dist/holmes/rtm/test-scope.js +15 -4
- package/dist/holmes/spec/approval-blockers.d.ts +10 -0
- package/dist/holmes/spec/approval-blockers.js +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ 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.7] - 2026-09-01
|
|
9
|
+
|
|
10
|
+
Field-incident hardening: consistency warning lints over the code graph, and an approval queue
|
|
11
|
+
that stops burying the one approval that matters.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Consistency lints, opt-in** (A-SPEC-506.1): `cpg_scan` accepts `lints: true` and adds a
|
|
16
|
+
warning-signal field (Python, with an honest `limits` envelope): `dynamicRef` — a literal
|
|
17
|
+
`getattr`/`hasattr` name absent from the scanned symbol census (field incident: 49
|
|
18
|
+
owner-approved deletions silently no-op'ed against a nonexistent method) — and
|
|
19
|
+
`asyncBlocking` — a known-blocking call inside an `async def`, unwrapped on its line (field
|
|
20
|
+
incident: scheduler starvation while the same file's other call sites used `to_thread`). No
|
|
21
|
+
gate consumes these; the default scan result is unchanged.
|
|
22
|
+
- **Approval-queue staleness** (A-SPEC-507.1): the `approve` list sorts by latest activity and
|
|
23
|
+
folds entries whose last activity exceeded `--stale-hours` (default 24; `--all` unfolds,
|
|
24
|
+
`--stale-hours 0` disables) — measured: 51 pending rows, mostly week-old residue, buried the
|
|
25
|
+
one live approval at [32]. The ledger deletes nothing; a re-filed request revives itself; and
|
|
26
|
+
gate-path folds take no clock at all (value-identical without the view options).
|
|
27
|
+
|
|
28
|
+
<!-- @implements A-SPEC-209 -->
|
|
29
|
+
## [0.3.6] - 2026-09-01
|
|
30
|
+
|
|
31
|
+
Traceability hardening: comma-listed anchors stop silently losing ids, the claim gate stops
|
|
32
|
+
false-flagging test fixtures, and boilerplate acceptance criteria can no longer be sealed.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **Comma-listed anchors** (A-SPEC-503.1, field-measured +410 lost rtm edges): all set-semantics
|
|
37
|
+
anchor consumers — coverage (`extractAnchors`), graph (`implementsSpecs`/wrong-kind warning) and
|
|
38
|
+
the claim gate — now parse the full `@implements ID, ID, …` list through ONE shared parser. A
|
|
39
|
+
wrong-kind id in second position is finally visible to the warning; prose mentions after the
|
|
40
|
+
list stay non-anchors; single-id behavior is value-identical.
|
|
41
|
+
- **Claim-gate preprocessing asymmetry** (A-SPEC-504.1): the gate judged raw text while the
|
|
42
|
+
scanner stripped string literals, so anchor-shaped FIXTURE strings were refused as unapproved
|
|
43
|
+
claims (measured twice) — breeding a string-concatenation evasion habit. Both surfaces now share
|
|
44
|
+
`stripStringLiterals`; a string-anchor is a claim to nobody, exactly as it is an anchor to
|
|
45
|
+
nobody (no protection lost — pinned as a test).
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **Acceptance-substance sealing gate** (A-SPEC-505.1, dogfooded: 18 boilerplate-criteria REQs
|
|
50
|
+
sealed in one day): `spec_approve` now refuses a REQ whose Success Criteria are absent or
|
|
51
|
+
entirely boilerplate, with cause and remedy in the refusal. Act-time placement — the 154/428
|
|
52
|
+
legacy approved REQs measured in this repo are untouched. The `spec_slice_init` REQ template no
|
|
53
|
+
longer generates that very boilerplate (empty Success Criteria scaffold: fill before sealing).
|
|
54
|
+
|
|
7
55
|
<!-- @implements A-SPEC-209 -->
|
|
8
56
|
## [0.3.5] - 2026-09-01
|
|
9
57
|
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
eb3164c-mthu5jwj
|
|
@@ -21,7 +21,19 @@ export declare function holdRequest(root: string, id: string, question: string,
|
|
|
21
21
|
* when, and any standing question. Deliberately NO scope grammar anywhere — the moment the screen
|
|
22
22
|
* teaches syntax, humans start typing it.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The list-view stale flags, parsed once: `--stale-hours <h>` (default 24; 0 disables folding —
|
|
26
|
+
* `--ttl` was already taken by the grant-lifetime flag) and `--all` to unfold. Returning null
|
|
27
|
+
* means "no clock": readQueue is then called without opts and the fold stays value-identical.
|
|
28
|
+
*/
|
|
29
|
+
export declare function staleView(flags: Record<string, unknown>): {
|
|
30
|
+
ttlMs: number;
|
|
31
|
+
showAll: boolean;
|
|
32
|
+
} | null;
|
|
33
|
+
export declare function renderPending(state: QueueState, view?: {
|
|
34
|
+
staleHours: number;
|
|
35
|
+
showAll: boolean;
|
|
36
|
+
}): string;
|
|
25
37
|
/**
|
|
26
38
|
* @implements A-SPEC-262.1
|
|
27
39
|
* What a decision line must say. Round-1: `✓ 승인 — <expires> 까지 유효` named NOTHING, so an index
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.grantRequest = grantRequest;
|
|
37
37
|
exports.denyRequest = denyRequest;
|
|
38
38
|
exports.holdRequest = holdRequest;
|
|
39
|
+
exports.staleView = staleView;
|
|
39
40
|
exports.renderPending = renderPending;
|
|
40
41
|
exports.subjectCells = subjectCells;
|
|
41
42
|
exports.subjectRoom = subjectRoom;
|
|
@@ -236,7 +237,20 @@ function holdRequest(root, id, question, actor) {
|
|
|
236
237
|
* when, and any standing question. Deliberately NO scope grammar anywhere — the moment the screen
|
|
237
238
|
* teaches syntax, humans start typing it.
|
|
238
239
|
*/
|
|
239
|
-
|
|
240
|
+
// @implements A-SPEC-507.1
|
|
241
|
+
/**
|
|
242
|
+
* The list-view stale flags, parsed once: `--stale-hours <h>` (default 24; 0 disables folding —
|
|
243
|
+
* `--ttl` was already taken by the grant-lifetime flag) and `--all` to unfold. Returning null
|
|
244
|
+
* means "no clock": readQueue is then called without opts and the fold stays value-identical.
|
|
245
|
+
*/
|
|
246
|
+
function staleView(flags) {
|
|
247
|
+
const raw = flags['stale-hours'];
|
|
248
|
+
const hours = typeof raw === 'string' ? Number(raw) : 24;
|
|
249
|
+
if (!Number.isFinite(hours) || hours <= 0)
|
|
250
|
+
return null;
|
|
251
|
+
return { ttlMs: hours * 3_600_000, showAll: flags.all === true };
|
|
252
|
+
}
|
|
253
|
+
function renderPending(state, view) {
|
|
240
254
|
if (state.pending.length === 0) {
|
|
241
255
|
// Round-7: this early return made the malformed-line row below UNREACHABLE in exactly the case
|
|
242
256
|
// it exists for — a wholly corrupted queue printed "no requests waiting" and exited 0, which is
|
|
@@ -249,7 +263,13 @@ function renderPending(state) {
|
|
|
249
263
|
return '승인 대기 중인 요청이 없습니다.';
|
|
250
264
|
}
|
|
251
265
|
const lines = [`◆ 승인 대기 ${state.pending.length}건`, ''];
|
|
252
|
-
|
|
266
|
+
// @implements A-SPEC-507.1 — the LIST VIEW sorts newest-activity-first and folds stale entries
|
|
267
|
+
// (C4: 51 rows, mostly week-old residue, buried the one live approval at [32]). Only when a view
|
|
268
|
+
// is passed: callers without one keep the legacy insertion order byte-for-byte.
|
|
269
|
+
const shown = view
|
|
270
|
+
? [...state.pending].sort((a, b) => (Date.parse(b.lastTs) || 0) - (Date.parse(a.lastTs) || 0))
|
|
271
|
+
: state.pending;
|
|
272
|
+
shown.forEach((p, i) => {
|
|
253
273
|
// @implements A-SPEC-262.1 — every field here is AGENT-CONTROLLED (the target is the command it
|
|
254
274
|
// was blocked on). Round-1 forged two extra rows and hid the real ones behind an ANSI conceal,
|
|
255
275
|
// and the operator granted a `curl … | sh` they never saw. The template owns the line structure.
|
|
@@ -266,6 +286,19 @@ function renderPending(state) {
|
|
|
266
286
|
lines.push(` id: ${(0, screen_safe_1.rowField)(p.id, 40)}`);
|
|
267
287
|
lines.push('');
|
|
268
288
|
});
|
|
289
|
+
if (view && state.expired.length > 0) {
|
|
290
|
+
if (view.showAll) {
|
|
291
|
+
// Folding is not concealment: --all shows every expired entry under a template-owned prefix.
|
|
292
|
+
for (const p of state.expired) {
|
|
293
|
+
lines.push(`[만료] ${subjectCells('[만료] ', p)}`);
|
|
294
|
+
lines.push(` 마지막 활동: ${(0, screen_safe_1.rowField)(p.lastTs, 28)} · id: ${(0, screen_safe_1.rowField)(p.id, 40)}`);
|
|
295
|
+
lines.push('');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
lines.push(`⏳ 만료로 접힘 ${state.expired.length}건 (마지막 활동 ${view.staleHours}h 초과) — --all 로 표시`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
269
302
|
if (state.malformedLines > 0)
|
|
270
303
|
lines.push(`(큐에 읽을 수 없는 줄 ${state.malformedLines}건 — 손상 여부를 확인하십시오)`);
|
|
271
304
|
return lines.join('\n');
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -787,8 +787,14 @@ async function main(argv) {
|
|
|
787
787
|
process.stdout.write(r.ok ? `✓ 보류 — ${ref.subject('✓ 보류 — ')}\n${ref.detail('질문은 다음 거부 문면에')}\n` : `✗ ${r.reason}\n`);
|
|
788
788
|
return r.ok ? 0 : 1;
|
|
789
789
|
}
|
|
790
|
+
// @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
|
|
791
|
+
// here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
|
|
792
|
+
const { staleView } = require('./approve');
|
|
793
|
+
const stale = staleView(flags);
|
|
794
|
+
const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
|
|
795
|
+
const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
|
|
790
796
|
if (flags.list) {
|
|
791
|
-
process.stdout.write(renderPending(readQueue(root)) + '\n');
|
|
797
|
+
process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
|
|
792
798
|
return 0;
|
|
793
799
|
}
|
|
794
800
|
// @implements A-SPEC-262.2 — the RESIDENT surface. Validate --poll-ms first (so a bad value is
|
|
@@ -833,9 +839,9 @@ async function main(argv) {
|
|
|
833
839
|
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
834
840
|
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
835
841
|
if (!process.stdin.isTTY) {
|
|
836
|
-
const state = readQueue(root);
|
|
842
|
+
const state = readQueue(root, queueOpts);
|
|
837
843
|
const hint = renderNonTtyHint(state);
|
|
838
|
-
process.stdout.write(renderPending(state) + (hint ? '\n\n' + hint : '') + '\n');
|
|
844
|
+
process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
|
|
839
845
|
return 0;
|
|
840
846
|
}
|
|
841
847
|
// @implements A-SPEC-262.1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consistency lints over what the repo already KNOWS — two field incidents, one gap:
|
|
3
|
+
*
|
|
4
|
+
* P-G `getattr(mem, "delete_fact", None)` referenced a symbol no class defines; the optional
|
|
5
|
+
* default hid the absence and 49 owner-approved deletions silently no-op'ed. The symbol
|
|
6
|
+
* census (CPG scan) had the answer all along — nobody asked it.
|
|
7
|
+
* P-J an `async def` body called sqlite directly and starved the scheduler, while the SAME
|
|
8
|
+
* file's other call sites wrapped the call in `to_thread` — the correct convention was one
|
|
9
|
+
* screen away.
|
|
10
|
+
*
|
|
11
|
+
* WARNING SIGNALS ONLY: no gate consumes these (association proposes, determinism judges — the
|
|
12
|
+
* standing principle). Text heuristics with honest limits (LINT_LIMITS): literal names only,
|
|
13
|
+
* same-line wrapping exemption only, Python only — both incidents were Python; extensions need
|
|
14
|
+
* their own field evidence, like every pattern list in this repository.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LINT_LIMITS: readonly string[];
|
|
17
|
+
/** P-G: literal getattr/hasattr names absent from the repo's symbol census. */
|
|
18
|
+
export declare function dynamicRefFindings(code: string, defined: ReadonlySet<string>): Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
line: number;
|
|
21
|
+
}>;
|
|
22
|
+
/** P-J: known-blocking calls inside an async def's indentation block, unwrapped on their line. */
|
|
23
|
+
export declare function asyncBlockingFindings(code: string): Array<{
|
|
24
|
+
pattern: string;
|
|
25
|
+
line: number;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-506.1
|
|
3
|
+
/**
|
|
4
|
+
* Consistency lints over what the repo already KNOWS — two field incidents, one gap:
|
|
5
|
+
*
|
|
6
|
+
* P-G `getattr(mem, "delete_fact", None)` referenced a symbol no class defines; the optional
|
|
7
|
+
* default hid the absence and 49 owner-approved deletions silently no-op'ed. The symbol
|
|
8
|
+
* census (CPG scan) had the answer all along — nobody asked it.
|
|
9
|
+
* P-J an `async def` body called sqlite directly and starved the scheduler, while the SAME
|
|
10
|
+
* file's other call sites wrapped the call in `to_thread` — the correct convention was one
|
|
11
|
+
* screen away.
|
|
12
|
+
*
|
|
13
|
+
* WARNING SIGNALS ONLY: no gate consumes these (association proposes, determinism judges — the
|
|
14
|
+
* standing principle). Text heuristics with honest limits (LINT_LIMITS): literal names only,
|
|
15
|
+
* same-line wrapping exemption only, Python only — both incidents were Python; extensions need
|
|
16
|
+
* their own field evidence, like every pattern list in this repository.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.LINT_LIMITS = void 0;
|
|
20
|
+
exports.dynamicRefFindings = dynamicRefFindings;
|
|
21
|
+
exports.asyncBlockingFindings = asyncBlockingFindings;
|
|
22
|
+
exports.LINT_LIMITS = [
|
|
23
|
+
'리터럴 이름 인자만 판정한다 — getattr(obj, 변수)는 판정 불가로 건너뛴다',
|
|
24
|
+
'차단 패턴 목록은 실사고·명백성 기반 최소 집합이다(time.sleep·requests.동사·sqlite3.·subprocess.run·urllib.request.urlopen) — 확장은 새 실측으로만',
|
|
25
|
+
'to_thread/run_in_executor 면제는 같은 행에서만 인식한다 — 여러 줄 래핑은 오탐될 수 있다',
|
|
26
|
+
'.py 텍스트 휴리스틱이다 — 비파이썬 언어와 문자열 속 코드는 보지 않는다',
|
|
27
|
+
];
|
|
28
|
+
const DYNAMIC_REF_RE = /\b(?:getattr|hasattr)\(\s*[^,()]+,\s*['"](\w+)['"]/g;
|
|
29
|
+
/** P-G: literal getattr/hasattr names absent from the repo's symbol census. */
|
|
30
|
+
function dynamicRefFindings(code, defined) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const m of code.matchAll(DYNAMIC_REF_RE)) {
|
|
33
|
+
const name = m[1];
|
|
34
|
+
if (defined.has(name))
|
|
35
|
+
continue;
|
|
36
|
+
out.push({ name, line: code.slice(0, m.index).split('\n').length });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const BLOCKING = ['time.sleep(', 'requests.get(', 'requests.post(', 'requests.put(',
|
|
41
|
+
'requests.delete(', 'requests.request(', 'sqlite3.', 'subprocess.run(', 'urllib.request.urlopen'];
|
|
42
|
+
const EXEMPT_RE = /to_thread|run_in_executor/;
|
|
43
|
+
/** P-J: known-blocking calls inside an async def's indentation block, unwrapped on their line. */
|
|
44
|
+
function asyncBlockingFindings(code) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const lines = code.split('\n');
|
|
47
|
+
let asyncIndent = null; // indentation of the enclosing `async def`, when inside one
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
const indent = line.length - line.trimStart().length;
|
|
51
|
+
const isBlank = line.trim() === '';
|
|
52
|
+
if (asyncIndent !== null && !isBlank && indent <= asyncIndent)
|
|
53
|
+
asyncIndent = null; // dedent ends the block
|
|
54
|
+
const def = /^(\s*)async\s+def\b/.exec(line);
|
|
55
|
+
if (def) {
|
|
56
|
+
asyncIndent = def[1].length;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (asyncIndent === null || isBlank || EXEMPT_RE.test(line))
|
|
60
|
+
continue;
|
|
61
|
+
for (const pattern of BLOCKING) {
|
|
62
|
+
if (line.includes(pattern))
|
|
63
|
+
out.push({ pattern, line: i + 1 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -106,12 +106,8 @@ function isVendorDir(dir) {
|
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
// anchor. Deliberately requires the full "KIND-NNN" shape (kind + '-' +
|
|
112
|
-
// 3+ digits) so prose/comments like `@implements A-SPEC refs` (no id number)
|
|
113
|
-
// or a bare word never match — only a genuine wrong-kind spec-id does.
|
|
114
|
-
const WRONG_KIND_IMPL = /@implements\s+((?:REQ|H-SPEC|C-SPEC|T-SPEC)-\d{3,}(?:\.\d+)?)/g;
|
|
109
|
+
// Anchor parsing (incl. wrong-kind detection) moved to the shared comma-list parser in
|
|
110
|
+
// rtm/anchor-ids (A-SPEC-503.1) — the local single-capture regexes dropped every id after a comma.
|
|
115
111
|
/** Single source of truth for which file extensions CpgScanner ingests (REQ-124 gate 2). */
|
|
116
112
|
exports.SCANNABLE_EXTENSIONS = ['.ts', '.mts', '.cts', '.tsx', '.jsx', '.js', '.mjs', '.cjs', '.py', '.cs', '.java', '.go', '.rs', '.cpp', '.cc', '.cxx', '.hpp', '.hh', '.h'];
|
|
117
113
|
const SCANNABLE_RE = /\.(ts|mts|cts|tsx|jsx|js|mjs|cjs|py|cs|java|go|rs|cpp|cc|cxx|hpp|hh|h)$/;
|
|
@@ -122,6 +118,7 @@ var test_files_1 = require("./test-files");
|
|
|
122
118
|
Object.defineProperty(exports, "TEST_FILE_PATTERNS", { enumerable: true, get: function () { return test_files_1.TEST_FILE_PATTERNS; } });
|
|
123
119
|
Object.defineProperty(exports, "isTestFile", { enumerable: true, get: function () { return test_files_1.isTestFile; } });
|
|
124
120
|
const test_files_2 = require("./test-files");
|
|
121
|
+
const anchor_ids_1 = require("../rtm/anchor-ids");
|
|
125
122
|
// .ts/.mts/.cts can contain TS type-assertion syntax (`<T>x`, arrow-generics)
|
|
126
123
|
// that the 'tsx' grammar misreads as JSX, so they must stay on 'typescript'.
|
|
127
124
|
// Everything else in SCANNABLE_EXTENSIONS (.tsx/.jsx/.js/.mjs/.cjs) never uses
|
|
@@ -212,8 +209,11 @@ class CpgScanner {
|
|
|
212
209
|
// symbol walk, and disambiguating a bare `.h` between C and C++
|
|
213
210
|
// is out of scope for this slice.
|
|
214
211
|
const lang = /\.py$/.test(e.name) ? 'python' : (/\.cs$/.test(e.name) ? 'csharp' : (/\.java$/.test(e.name) ? 'java' : (/\.go$/.test(e.name) ? 'go' : (/\.rs$/.test(e.name) ? 'rust' : (/\.(cpp|cc|cxx|hpp|hh|h)$/.test(e.name) ? 'cpp' : (TSX_GRAMMAR_RE.test(e.name) ? 'tsx' : 'typescript'))))));
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
// @implements A-SPEC-503.1 — comma-listed anchors: the old single-capture regexes
|
|
213
|
+
// dropped every id after the first (C12), and a wrong-kind id in second position was
|
|
214
|
+
// invisible to the warning. Both consumers now ride the shared list parser.
|
|
215
|
+
const implementsSpecs = (0, anchor_ids_1.anchorSpecIds)(code);
|
|
216
|
+
const unanchoredImplements = (0, anchor_ids_1.wrongKindAnchorIds)(code);
|
|
217
217
|
const sourcePath = (0, source_path_1.toSourcePath)(repoRoot, path.resolve(p));
|
|
218
218
|
// @implements A-SPEC-140.1
|
|
219
219
|
// The third parse happens only when asked for. `undefined` from the parser (a language
|
|
@@ -31,6 +31,14 @@ export interface PendingRequest {
|
|
|
31
31
|
}
|
|
32
32
|
export interface QueueState {
|
|
33
33
|
pending: PendingRequest[];
|
|
34
|
+
/**
|
|
35
|
+
* @implements A-SPEC-507.1
|
|
36
|
+
* Pending entries whose last activity exceeded the caller's TTL — populated ONLY when the fold
|
|
37
|
+
* was given a clock ({now, ttlMs}), so gate-path folds never depend on when you look. C4
|
|
38
|
+
* measured the harm of agelessness: 51 entries, mostly week-old residue, buried the one live
|
|
39
|
+
* approval at [32]. Expiry is a display-layer judgment; the ledger keeps every event.
|
|
40
|
+
*/
|
|
41
|
+
expired: PendingRequest[];
|
|
34
42
|
/** Broken lines are counted, never swallowed: an empty-looking queue must be distinguishable from a corrupted one. */
|
|
35
43
|
malformedLines: number;
|
|
36
44
|
/**
|
|
@@ -64,7 +72,10 @@ export declare function approvalRequestId(kind: string, target: string): string;
|
|
|
64
72
|
* entry's pending state: an old CLI reading a new queue must not silently mis-report what is
|
|
65
73
|
* waiting.
|
|
66
74
|
*/
|
|
67
|
-
export declare function foldQueue(lines: string[]
|
|
75
|
+
export declare function foldQueue(lines: string[], opts?: {
|
|
76
|
+
now: number;
|
|
77
|
+
ttlMs: number;
|
|
78
|
+
}): QueueState;
|
|
68
79
|
/**
|
|
69
80
|
* Append a request event. Fire-and-forget.
|
|
70
81
|
*
|
|
@@ -79,7 +90,10 @@ export declare function enqueueApprovalRequest(root: string, req: {
|
|
|
79
90
|
why: string;
|
|
80
91
|
}): boolean;
|
|
81
92
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
82
|
-
export declare function readQueue(root: string
|
|
93
|
+
export declare function readQueue(root: string, opts?: {
|
|
94
|
+
now: number;
|
|
95
|
+
ttlMs: number;
|
|
96
|
+
}): QueueState;
|
|
83
97
|
/**
|
|
84
98
|
* The refusal-message suffix pointing the operator at the review CLI.
|
|
85
99
|
*
|
|
@@ -83,7 +83,7 @@ function approvalRequestId(kind, target) {
|
|
|
83
83
|
* entry's pending state: an old CLI reading a new queue must not silently mis-report what is
|
|
84
84
|
* waiting.
|
|
85
85
|
*/
|
|
86
|
-
function foldQueue(lines) {
|
|
86
|
+
function foldQueue(lines, opts) {
|
|
87
87
|
const pending = new Map();
|
|
88
88
|
const decisions = {};
|
|
89
89
|
let malformedLines = 0;
|
|
@@ -155,7 +155,18 @@ function foldQueue(lines) {
|
|
|
155
155
|
break;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
|
-
|
|
158
|
+
const all = [...pending.values()];
|
|
159
|
+
if (!opts)
|
|
160
|
+
return { pending: all, expired: [], malformedLines, decisions };
|
|
161
|
+
// @implements A-SPEC-507.1 — strict excess only, and an unparseable lastTs stays ACTIVE: a
|
|
162
|
+
// clockless entry must never be silently hidden by a clock it does not carry.
|
|
163
|
+
const expired = [];
|
|
164
|
+
const active = [];
|
|
165
|
+
for (const p of all) {
|
|
166
|
+
const last = Date.parse(p.lastTs);
|
|
167
|
+
(Number.isFinite(last) && last + opts.ttlMs < opts.now ? expired : active).push(p);
|
|
168
|
+
}
|
|
169
|
+
return { pending: active, expired, malformedLines, decisions };
|
|
159
170
|
}
|
|
160
171
|
/**
|
|
161
172
|
* Append a request event. Fire-and-forget.
|
|
@@ -223,7 +234,7 @@ function isPlainFile(file) {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
226
|
-
function readQueue(root) {
|
|
237
|
+
function readQueue(root, opts) {
|
|
227
238
|
const file = path.join(root, exports.QUEUE_RELPATH);
|
|
228
239
|
try {
|
|
229
240
|
// TYPE BEFORE READ. `readFileSync` on a FIFO blocks inside open(2) forever — no throw, no
|
|
@@ -232,12 +243,12 @@ function readQueue(root) {
|
|
|
232
243
|
// hook, the stop hook and the MCP handlers, not just the CLI. `approve-context.ts` has guarded
|
|
233
244
|
// this since the 2026-08-24 hang; the queue reader, which far more code depends on, did not.
|
|
234
245
|
if (!fs.lstatSync(file).isFile())
|
|
235
|
-
return { pending: [], malformedLines: 1, decisions: {} };
|
|
246
|
+
return { pending: [], expired: [], malformedLines: 1, decisions: {} };
|
|
236
247
|
const raw = fs.readFileSync(file, 'utf8');
|
|
237
|
-
return foldQueue(raw.split('\n'));
|
|
248
|
+
return foldQueue(raw.split('\n'), opts);
|
|
238
249
|
}
|
|
239
250
|
catch {
|
|
240
|
-
return { pending: [], malformedLines: 0, decisions: {} };
|
|
251
|
+
return { pending: [], expired: [], malformedLines: 0, decisions: {} };
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
/**
|
|
@@ -15,9 +15,19 @@ exports.newlyClaimed = newlyClaimed;
|
|
|
15
15
|
* keeps LLM judgement out of governance. What a change CLAIMS, though, is right there in the text —
|
|
16
16
|
* that is the decidable subset, and this is it.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
// @implements A-SPEC-503.1 — comma-list support with the gate's OWN lenient token grammar kept
|
|
19
|
+
// (any `*-SPEC-*` shape claims; the shared parser's strict kinds must not narrow a gate). The old
|
|
20
|
+
// single capture let the second id of a listed claim slip past `newlyClaimed` unjudged (C12).
|
|
21
|
+
const anchor_ids_1 = require("../rtm/anchor-ids");
|
|
22
|
+
const ANCHOR_TOKEN = String.raw `[A-Za-z]+-SPEC-[\w.]+`;
|
|
23
|
+
const ANCHOR_RE = new RegExp(String.raw `@implements\s+(${ANCHOR_TOKEN}(?:[ \t]*,[ \t]*${ANCHOR_TOKEN})*)`, 'g');
|
|
19
24
|
function claimedAnchors(content) {
|
|
20
|
-
|
|
25
|
+
// @implements A-SPEC-504.1 — judge on the SAME literal-stripped text the coverage scanner uses:
|
|
26
|
+
// a string-anchor is an anchor to nobody, so treating it as a claim produced only false-positive
|
|
27
|
+
// refusals (measured twice on legitimate fixtures) and bred the '@'+'implements' evasion habit.
|
|
28
|
+
// Losing it costs no protection — there is no "claimed but unjudged" state to protect against.
|
|
29
|
+
const ids = [...(0, anchor_ids_1.stripStringLiterals)(content).matchAll(ANCHOR_RE)].flatMap((m) => m[1].split(/[ \t]*,[ \t]*/));
|
|
30
|
+
return [...new Set(ids)];
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
33
|
* The anchors this write ADDS, relative to what the file already had.
|
|
@@ -272,6 +272,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
272
272
|
}>;
|
|
273
273
|
cpg_scan(a: {
|
|
274
274
|
root: string;
|
|
275
|
+
lints?: boolean;
|
|
275
276
|
}): Promise<{
|
|
276
277
|
files: number;
|
|
277
278
|
symbols: number;
|
|
@@ -280,6 +281,27 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
280
281
|
reason: string;
|
|
281
282
|
}[];
|
|
282
283
|
skippedCount: number;
|
|
284
|
+
} | {
|
|
285
|
+
lints: {
|
|
286
|
+
dynamicRef: {
|
|
287
|
+
file: string;
|
|
288
|
+
name: string;
|
|
289
|
+
line: number;
|
|
290
|
+
}[];
|
|
291
|
+
asyncBlocking: {
|
|
292
|
+
file: string;
|
|
293
|
+
pattern: string;
|
|
294
|
+
line: number;
|
|
295
|
+
}[];
|
|
296
|
+
limits: string[];
|
|
297
|
+
};
|
|
298
|
+
files: number;
|
|
299
|
+
symbols: number;
|
|
300
|
+
skipped: {
|
|
301
|
+
file: string;
|
|
302
|
+
reason: string;
|
|
303
|
+
}[];
|
|
304
|
+
skippedCount: number;
|
|
283
305
|
}>;
|
|
284
306
|
/**
|
|
285
307
|
* @implements A-SPEC-138
|
|
@@ -131,6 +131,7 @@ 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
133
|
const anchor_comment_1 = require("../rtm/anchor-comment");
|
|
134
|
+
const consistency_lints_1 = require("../cpg/consistency-lints");
|
|
134
135
|
const approval_queue_1 = require("../governance/approval-queue");
|
|
135
136
|
const approval_grants_1 = require("../governance/approval-grants");
|
|
136
137
|
const spec_digest_1 = require("../spec/spec-digest");
|
|
@@ -1295,6 +1296,16 @@ function makeRawHandlers(store, opts) {
|
|
|
1295
1296
|
if (stubs.length > 0) {
|
|
1296
1297
|
return { ok: false, reason: (0, approval_blockers_1.placeholderMessage)(stubs) };
|
|
1297
1298
|
}
|
|
1299
|
+
// @implements A-SPEC-505.1 — acceptance substance, judged at the ACT on the same candidate
|
|
1300
|
+
// the seal would freeze, like the placeholder gate above (154/428 approved REQs here are
|
|
1301
|
+
// non-stated legacy; a validateSpec predicate would brick them all). Judged HERE and not
|
|
1302
|
+
// before the approval channels: an earlier extra read shifts the optimistic-concurrency
|
|
1303
|
+
// window and lets a mid-approval edit get sealed (store-integrity contract: edits win,
|
|
1304
|
+
// approvals lose). The post-grant refusal it costs is the standing property of every
|
|
1305
|
+
// act-time blocker in this block (breaking_change, placeholder) — one ordering, one truth.
|
|
1306
|
+
const unactionable = (0, approval_blockers_1.unactionableCriteriaBlocker)(candidate);
|
|
1307
|
+
if (unactionable)
|
|
1308
|
+
return { ok: false, reason: unactionable };
|
|
1298
1309
|
// Parents-first: a sealed child snapshotting an unsealed parent would pin nothing. This loop
|
|
1299
1310
|
// exists to COLLECT the digests; the refusal inside it is now a backstop, because
|
|
1300
1311
|
// `parentBlockers` above already returns for the same condition with the same sentence. Kept
|
|
@@ -1623,7 +1634,37 @@ function makeRawHandlers(store, opts) {
|
|
|
1623
1634
|
// with casualties" by counts alone. Bounded listing (50), complete count.
|
|
1624
1635
|
const { scanned, skipped } = cachedScanWithReport(root);
|
|
1625
1636
|
const symbols = scanned.reduce((n, f) => n + f.symbols.length, 0);
|
|
1626
|
-
|
|
1637
|
+
const base = { files: scanned.length, symbols, skipped: skipped.slice(0, 50), skippedCount: skipped.length };
|
|
1638
|
+
if (!a.lints)
|
|
1639
|
+
return base;
|
|
1640
|
+
// @implements A-SPEC-506.1 — opt-in consistency lints (P-G/P-J field incidents): WARNING
|
|
1641
|
+
// signals only, additive field only, Python only; the default path above is byte-identical.
|
|
1642
|
+
const defined = new Set();
|
|
1643
|
+
for (const f of scanned)
|
|
1644
|
+
for (const s of f.symbols) {
|
|
1645
|
+
defined.add(s.name);
|
|
1646
|
+
const last = s.name.split('.').pop();
|
|
1647
|
+
if (last)
|
|
1648
|
+
defined.add(last);
|
|
1649
|
+
}
|
|
1650
|
+
const dynamicRef = [];
|
|
1651
|
+
const asyncBlocking = [];
|
|
1652
|
+
for (const f of scanned) {
|
|
1653
|
+
if (!f.sourcePath.endsWith('.py'))
|
|
1654
|
+
continue;
|
|
1655
|
+
let code;
|
|
1656
|
+
try {
|
|
1657
|
+
code = fs.readFileSync(path.join(root, f.sourcePath), 'utf8');
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
for (const hit of (0, consistency_lints_1.dynamicRefFindings)(code, defined))
|
|
1663
|
+
dynamicRef.push({ file: f.sourcePath, ...hit });
|
|
1664
|
+
for (const hit of (0, consistency_lints_1.asyncBlockingFindings)(code))
|
|
1665
|
+
asyncBlocking.push({ file: f.sourcePath, ...hit });
|
|
1666
|
+
}
|
|
1667
|
+
return { ...base, lints: { dynamicRef, asyncBlocking, limits: [...consistency_lints_1.LINT_LIMITS] } };
|
|
1627
1668
|
},
|
|
1628
1669
|
/**
|
|
1629
1670
|
* @implements A-SPEC-138
|
|
@@ -2841,7 +2882,6 @@ ${a.objective}
|
|
|
2841
2882
|
- Standard project governance rules.
|
|
2842
2883
|
|
|
2843
2884
|
## Success Criteria
|
|
2844
|
-
- Implementation completed and verified by tests.
|
|
2845
2885
|
|
|
2846
2886
|
## Out of Scope
|
|
2847
2887
|
- Unrelated feature changes.
|
|
@@ -171,10 +171,10 @@ exports.TOOL_SCHEMAS = {
|
|
|
171
171
|
},
|
|
172
172
|
},
|
|
173
173
|
cpg_scan: {
|
|
174
|
-
description: 'Scan the repository tree with tree-sitter across the 8 supported languages; returns { files, symbols } counts plus the skip report ({ skipped, skippedCount }) naming any claimed file the scan could not ingest — an empty report distinguishes a clean tree from one with casualties.',
|
|
174
|
+
description: 'Scan the repository tree with tree-sitter across the 8 supported languages; returns { files, symbols } counts plus the skip report ({ skipped, skippedCount }) naming any claimed file the scan could not ingest — an empty report distinguishes a clean tree from one with casualties. Pass lints:true to ALSO receive opt-in consistency WARNING signals (Python only, additive `lints` field with its own `limits`): dynamicRef (a literal getattr/hasattr name absent from the scanned symbol census) and asyncBlocking (a known-blocking call inside an async def, unwrapped on its line). Signals route a review; no gate consumes them.',
|
|
175
175
|
inputSchema: {
|
|
176
176
|
type: 'object',
|
|
177
|
-
properties: { root: ROOT },
|
|
177
|
+
properties: { root: ROOT, lints: { type: 'boolean', description: 'Include the consistency-lint warning signals (default false — the base result is unchanged without it).' } },
|
|
178
178
|
required: ['root'],
|
|
179
179
|
},
|
|
180
180
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE comma-list grammar for anchor lines — C12, dogfooded: four parsers each captured only
|
|
3
|
+
* the first id of a comma-listed anchor (`… <marker> A-SPEC-…110, …219`), so every id after the
|
|
4
|
+
* comma silently vanished from coverage, graph edges (+410 recovered in the field by
|
|
5
|
+
* hand-normalizing 14 files) and claim gating. The list ends at the comma chain: a prose mention
|
|
6
|
+
* after the ids (`… (see another id)`) is NOT a claim, which is what keeps this widening from
|
|
7
|
+
* turning commentary into governance.
|
|
8
|
+
*/
|
|
9
|
+
export declare function stripStringLiterals(text: string): string;
|
|
10
|
+
/** Every anchor-marker occurrence's comma-list, as an array of ids per occurrence. */
|
|
11
|
+
export declare function implementsIdLists(text: string): string[][];
|
|
12
|
+
/** Flattened A-SPEC ids across all lists — the graph/coverage consumers' shape. */
|
|
13
|
+
export declare function anchorSpecIds(text: string): string[];
|
|
14
|
+
/** Flattened wrong-kind ids (a governed id anchored where only A-SPEC belongs) — anywhere in a list. */
|
|
15
|
+
export declare function wrongKindAnchorIds(text: string): string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-503.1
|
|
3
|
+
/**
|
|
4
|
+
* The ONE comma-list grammar for anchor lines — C12, dogfooded: four parsers each captured only
|
|
5
|
+
* the first id of a comma-listed anchor (`… <marker> A-SPEC-…110, …219`), so every id after the
|
|
6
|
+
* comma silently vanished from coverage, graph edges (+410 recovered in the field by
|
|
7
|
+
* hand-normalizing 14 files) and claim gating. The list ends at the comma chain: a prose mention
|
|
8
|
+
* after the ids (`… (see another id)`) is NOT a claim, which is what keeps this widening from
|
|
9
|
+
* turning commentary into governance.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.stripStringLiterals = stripStringLiterals;
|
|
13
|
+
exports.implementsIdLists = implementsIdLists;
|
|
14
|
+
exports.anchorSpecIds = anchorSpecIds;
|
|
15
|
+
exports.wrongKindAnchorIds = wrongKindAnchorIds;
|
|
16
|
+
// @implements A-SPEC-504.1
|
|
17
|
+
// The ONE string-literal preprocessing every anchor-judging surface shares. Measured 2026-09-01:
|
|
18
|
+
// the coverage scanner stripped literals while the claim gate judged raw text, so a legitimate
|
|
19
|
+
// fixture string was refused twice as an "unapproved claim" — and that friction bred the
|
|
20
|
+
// '@'+'implements' evasion habit. A string-anchor is an anchor to nobody and must be a claim to
|
|
21
|
+
// nobody; one definition here keeps the two judgments from drifting apart again.
|
|
22
|
+
const STRINGS_RE = /'(?:\\.|[^'\\\n])*'|"(?:\\.|[^"\\\n])*"|`(?:\\.|[^`\\])*`/g;
|
|
23
|
+
function stripStringLiterals(text) {
|
|
24
|
+
return text.replace(STRINGS_RE, '""');
|
|
25
|
+
}
|
|
26
|
+
const ID = String.raw `(?:REQ|A-SPEC|H-SPEC|C-SPEC|T-SPEC)-\d{3,}(?:\.\d+)?`;
|
|
27
|
+
const LIST_RE = new RegExp(String.raw `@implements[ \t]+(${ID}(?:[ \t]*,[ \t]*${ID})*)`, 'g');
|
|
28
|
+
/** Every anchor-marker occurrence's comma-list, as an array of ids per occurrence. */
|
|
29
|
+
function implementsIdLists(text) {
|
|
30
|
+
return [...text.matchAll(LIST_RE)].map((m) => m[1].split(/[ \t]*,[ \t]*/));
|
|
31
|
+
}
|
|
32
|
+
/** Flattened A-SPEC ids across all lists — the graph/coverage consumers' shape. */
|
|
33
|
+
function anchorSpecIds(text) {
|
|
34
|
+
return [...new Set(implementsIdLists(text).flat().filter((id) => id.startsWith('A-SPEC-')))];
|
|
35
|
+
}
|
|
36
|
+
/** Flattened wrong-kind ids (a governed id anchored where only A-SPEC belongs) — anywhere in a list. */
|
|
37
|
+
function wrongKindAnchorIds(text) {
|
|
38
|
+
return [...new Set(implementsIdLists(text).flat().filter((id) => !id.startsWith('A-SPEC-')))];
|
|
39
|
+
}
|
|
@@ -41,6 +41,7 @@ exports.extractAnchors = extractAnchors;
|
|
|
41
41
|
exports.scanTestAnchors = scanTestAnchors;
|
|
42
42
|
exports.computeTestScope = computeTestScope;
|
|
43
43
|
// @implements A-SPEC-121.2
|
|
44
|
+
const anchor_ids_1 = require("./anchor-ids");
|
|
44
45
|
const fs = __importStar(require("node:fs"));
|
|
45
46
|
const path = __importStar(require("node:path"));
|
|
46
47
|
const spec_types_1 = require("../spec/spec-types");
|
|
@@ -256,12 +257,22 @@ function countTestCases(source, filePath) {
|
|
|
256
257
|
// every id merely MENTIONED in it — a live run credited 50 cases to the nonexistent A-SPEC-999 from
|
|
257
258
|
// a fixture string, and injecting one comment into any test file re-credited it to an arbitrary
|
|
258
259
|
// spec). Both the fixture-string and the mid-code-line injection vectors die here.
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
// @implements A-SPEC-503.1 — the line must still BE a comment line, but the ids on it come from
|
|
261
|
+
// the shared comma-list parser: the old single-capture regex silently dropped every id after the
|
|
262
|
+
// first comma (C12 — +410 rtm edges recovered in the field only by hand-normalizing 14 files).
|
|
263
|
+
const IMPL_LINE_RE = /^[ \t]*(?:\/\/|\/\*|\*|#)[^\n]*?@implements[ \t]/gm;
|
|
261
264
|
/** A-SPEC ids anchored by a file, counting only standalone comment-line markers outside literals. */
|
|
262
265
|
function extractAnchors(source) {
|
|
263
|
-
|
|
264
|
-
|
|
266
|
+
// @implements A-SPEC-504.1 — the literal-stripping moved to the shared definition the claim
|
|
267
|
+
// gate now uses too; a private copy here is how the two judgments drifted apart in the first place.
|
|
268
|
+
const noStrings = (0, anchor_ids_1.stripStringLiterals)(source);
|
|
269
|
+
const ids = [];
|
|
270
|
+
for (const line of noStrings.split('\n')) {
|
|
271
|
+
IMPL_LINE_RE.lastIndex = 0;
|
|
272
|
+
if (IMPL_LINE_RE.test(line))
|
|
273
|
+
ids.push(...(0, anchor_ids_1.anchorSpecIds)(line));
|
|
274
|
+
}
|
|
275
|
+
return [...new Set(ids)];
|
|
265
276
|
}
|
|
266
277
|
/**
|
|
267
278
|
* @implements A-SPEC-269
|
|
@@ -76,3 +76,13 @@ export declare const blockerClause: (label: string, blockers: string[]) => strin
|
|
|
76
76
|
* Never throws: a refusal that says less is recoverable, a hook that dies is not.
|
|
77
77
|
*/
|
|
78
78
|
export declare function blockerSummary(spec: Spec | undefined, resolve: (id: string) => Spec | null, specId?: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* The acceptance-substance gate, standing at the SEALING ACT like the placeholder gate above and
|
|
81
|
+
* for that gate's measured reason (A-SPEC-182): a static predicate in validateSpec would turn the
|
|
82
|
+
* already-approved corpus into violations — measured 2026-09-01, 154/428 approved REQs here are
|
|
83
|
+
* non-stated legacy — and brick the harness. Dogfooded origin: 18 REQs sealed in one day whose
|
|
84
|
+
* entire Success Criteria read "implementation completed and verified by tests", while the
|
|
85
|
+
* detector (acceptanceQuality) sat wired only into the post-hoc analyzer. The detector is
|
|
86
|
+
* CONSUMED, not copied — one judgment, two surfaces.
|
|
87
|
+
*/
|
|
88
|
+
export declare function unactionableCriteriaBlocker(spec: Spec): string | null;
|
|
@@ -5,6 +5,9 @@ exports.placeholderSections = placeholderSections;
|
|
|
5
5
|
exports.parentBlockers = parentBlockers;
|
|
6
6
|
exports.approvalBlockers = approvalBlockers;
|
|
7
7
|
exports.blockerSummary = blockerSummary;
|
|
8
|
+
exports.unactionableCriteriaBlocker = unactionableCriteriaBlocker;
|
|
9
|
+
// @implements A-SPEC-182
|
|
10
|
+
const acceptance_quality_1 = require("./acceptance-quality");
|
|
8
11
|
const validator_1 = require("./validator");
|
|
9
12
|
const breaking_change_1 = require("./breaking-change");
|
|
10
13
|
const spec_digest_1 = require("./spec-digest");
|
|
@@ -245,3 +248,23 @@ function blockerSummary(spec, resolve, specId) {
|
|
|
245
248
|
return null;
|
|
246
249
|
}
|
|
247
250
|
}
|
|
251
|
+
// @implements A-SPEC-505.1
|
|
252
|
+
/**
|
|
253
|
+
* The acceptance-substance gate, standing at the SEALING ACT like the placeholder gate above and
|
|
254
|
+
* for that gate's measured reason (A-SPEC-182): a static predicate in validateSpec would turn the
|
|
255
|
+
* already-approved corpus into violations — measured 2026-09-01, 154/428 approved REQs here are
|
|
256
|
+
* non-stated legacy — and brick the harness. Dogfooded origin: 18 REQs sealed in one day whose
|
|
257
|
+
* entire Success Criteria read "implementation completed and verified by tests", while the
|
|
258
|
+
* detector (acceptanceQuality) sat wired only into the post-hoc analyzer. The detector is
|
|
259
|
+
* CONSUMED, not copied — one judgment, two surfaces.
|
|
260
|
+
*/
|
|
261
|
+
function unactionableCriteriaBlocker(spec) {
|
|
262
|
+
if (spec.type !== 'REQ')
|
|
263
|
+
return null;
|
|
264
|
+
const quality = (0, acceptance_quality_1.acceptanceQuality)(spec);
|
|
265
|
+
if (quality === 'stated')
|
|
266
|
+
return null;
|
|
267
|
+
return quality === 'absent'
|
|
268
|
+
? `REQ ${spec.id}의 Success Criteria가 비어 있습니다 — 관측 가능한 기준을 채운 뒤 승인하십시오`
|
|
269
|
+
: `REQ ${spec.id}의 Success Criteria가 전량 보일러플레이트입니다('구현·테스트 완료'는 충족 정의가 아닙니다) — 각 항목이 검증 동사·측정 대상을 갖게 고친 뒤 승인하십시오`;
|
|
270
|
+
}
|
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.7",
|
|
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",
|