peaks-loop 4.0.42 → 4.0.44
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 +59 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/api-diff-commands.d.ts +16 -0
- package/dist/cli/commands/api-diff-commands.js +55 -0
- package/dist/cli/commands/audit-commands.d.ts +16 -3
- package/dist/cli/commands/audit-commands.js +84 -31
- package/dist/cli/commands/codegraph-commands.js +191 -6
- package/dist/cli/commands/final-review-commands.d.ts +34 -10
- package/dist/cli/commands/final-review-commands.js +130 -34
- package/dist/cli/commands/job-commands.js +4 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/share-commands.d.ts +49 -0
- package/dist/cli/commands/share-commands.js +114 -14
- package/dist/cli/commands/test-commands.d.ts +60 -3
- package/dist/cli/commands/test-commands.js +125 -7
- package/dist/services/audit/audit-goal-service.js +38 -3
- package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
- package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
- package/dist/services/codegraph/codegraph-service.d.ts +0 -1
- package/dist/services/codegraph/codegraph-service.js +5 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
- package/dist/services/doctor/doctor-service/types.d.ts +47 -0
- package/dist/services/final-review/final-review-service.d.ts +154 -0
- package/dist/services/final-review/final-review-service.js +621 -7
- package/dist/services/final-review/index.d.ts +1 -1
- package/dist/services/final-review/index.js +1 -1
- package/dist/services/llm/anthropic-runner.d.ts +87 -0
- package/dist/services/llm/anthropic-runner.js +171 -0
- package/dist/services/llm/stub-runner.d.ts +11 -0
- package/dist/services/llm/stub-runner.js +33 -0
- package/dist/services/prd/handoff-auto-regen.js +0 -1
- package/dist/services/prd/handoff-service.d.ts +9 -1
- package/dist/services/prd/handoff-service.js +48 -6
- package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
- package/dist/services/scan/api-diff-openapi.d.ts +32 -0
- package/dist/services/scan/api-diff-openapi.js +359 -0
- package/dist/services/scan/api-diff-recorded.d.ts +96 -0
- package/dist/services/scan/api-diff-recorded.js +577 -0
- package/dist/services/scan/api-diff-service.d.ts +34 -0
- package/dist/services/scan/api-diff-service.js +407 -0
- package/dist/services/scan/api-diff-types.d.ts +116 -0
- package/dist/services/scan/api-diff-types.js +46 -0
- package/dist/services/scan/archetype-service.js +27 -1
- package/dist/services/scan/existing-system-service.js +17 -4
- package/dist/services/scan/hook-convention-service.d.ts +26 -0
- package/dist/services/scan/hook-convention-service.js +562 -0
- package/dist/services/scan/scan-types.d.ts +47 -0
- package/dist/services/session/caller-binding-service.d.ts +28 -0
- package/dist/services/session/caller-binding-service.js +10 -2
- package/dist/services/session/caller-id-types.d.ts +12 -2
- package/dist/services/session/index.d.ts +2 -2
- package/dist/services/session/index.js +2 -2
- package/dist/services/session/session-binding-bridge.js +11 -6
- package/dist/services/session/session-manager.d.ts +33 -1
- package/dist/services/session/session-manager.js +84 -25
- package/dist/services/skills/skill-presence-service.d.ts +17 -3
- package/dist/services/skills/skill-presence-service.js +23 -3
- package/package.json +7 -5
- package/skills/bee/peaks-rd/SKILL.md +11 -3
- package/skills/peaks-code/references/existing-system-extraction.md +5 -1
- package/skills/peaks-code/references/frontend-only-mode.md +48 -6
- package/skills/peaks-code/references/project-scan-checklist.md +20 -1
- package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
- package/skills/peaks-final-review/SKILL.md +43 -32
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1. Auto-detects the framework from package.json (devDependencies +
|
|
8
8
|
* dependencies) via detectTestFramework().
|
|
9
|
-
* 2.
|
|
9
|
+
* 2. Resolves the project-LOCAL runner binary (node_modules) and spawns
|
|
10
|
+
* that, so the command works where the runner is not on PATH — notably
|
|
11
|
+
* Windows, where node_modules/.bin/vitest.cmd is not spawnable without
|
|
12
|
+
* a shell. PATH is a last resort and is reported, not silent.
|
|
13
|
+
* 3. Spawns the framework's CLI with --cache enabled (overriding any
|
|
10
14
|
* --no-cache in the consumer's `test` script). The user can
|
|
11
15
|
* opt back into no-cache via `peaks test --no-cache` or
|
|
12
16
|
* `peaks test --passthrough`.
|
|
13
|
-
*
|
|
17
|
+
* 4. Skips tests where (fileMtime, fileSha256) is unchanged AND the
|
|
14
18
|
* previous run status was 'passed' (per-test fingerprint cache at
|
|
15
19
|
* `<projectRoot>/.peaks/_runtime/test-cache/<hash>.json`).
|
|
16
|
-
*
|
|
20
|
+
* 5. Exits 0 on all-pass / all-skip; exits 1 on any failure.
|
|
17
21
|
*
|
|
18
22
|
* The CLI is invoked by USER (not just by skill) per slice 2.5.0
|
|
19
23
|
* sub-fix B (G16) — a documented exception to the
|
|
@@ -30,6 +34,8 @@
|
|
|
30
34
|
* peaks test --framework <name> — force a specific framework
|
|
31
35
|
*/
|
|
32
36
|
import { spawn } from 'node:child_process';
|
|
37
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
38
|
+
import { dirname, join, resolve } from 'node:path';
|
|
33
39
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
34
40
|
import { getErrorMessage } from '../cli-helpers.js';
|
|
35
41
|
import { clearTestCache, detectTestFramework } from '../../services/test-cache/test-cache-service.js';
|
|
@@ -81,10 +87,120 @@ export function buildRunnerArgv(framework, patterns, options) {
|
|
|
81
87
|
// mocha — no built-in --cache flag; we just pass the patterns.
|
|
82
88
|
return [...patterns];
|
|
83
89
|
}
|
|
84
|
-
|
|
90
|
+
/** `<pkg>/package.json#bin`, resolved to the absolute JS entry it points at. */
|
|
91
|
+
function readBinEntry(pkgJsonPath, name, read) {
|
|
92
|
+
let pkg = {};
|
|
93
|
+
try {
|
|
94
|
+
pkg = JSON.parse(read(pkgJsonPath));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Not fatal: fall through to the `.bin` shim below, and the not-found
|
|
98
|
+
// error still names this package.json in `searched`.
|
|
99
|
+
pkg = {};
|
|
100
|
+
}
|
|
101
|
+
const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[name];
|
|
102
|
+
if (typeof rel !== 'string' || rel.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
return resolve(dirname(pkgJsonPath), rel);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Convert a resolved executable + argv into a form `spawn` can launch with
|
|
108
|
+
* `shell: false`.
|
|
109
|
+
*
|
|
110
|
+
* A Windows `.cmd`/`.bat` shim cannot be spawned directly (spawn → EINVAL).
|
|
111
|
+
* The obvious fix, `spawn(shim, argv, { shell: true })`, re-splits argv inside
|
|
112
|
+
* cmd.exe: a pattern `tests/a b/x.test.ts` arrives as THREE args (measured).
|
|
113
|
+
* Invoking cmd.exe ourselves with `/d /s /c` keeps argv intact.
|
|
114
|
+
*/
|
|
115
|
+
function toSpawnable(exe, argv, platform, env) {
|
|
116
|
+
if (platform === 'win32' && /\.(?:cmd|bat)$/i.test(exe)) {
|
|
117
|
+
return { command: env.ComSpec ?? 'cmd.exe', args: ['/d', '/s', '/c', exe, ...argv] };
|
|
118
|
+
}
|
|
119
|
+
return { command: exe, args: argv };
|
|
120
|
+
}
|
|
121
|
+
/** Spawnable PATH hits, in preference order (`where`/`which` avoided so this
|
|
122
|
+
* stays in-process and testable). */
|
|
123
|
+
function resolveFromPath(name, platform, env, exists) {
|
|
124
|
+
const dirs = (env.PATH ?? '').split(platform === 'win32' ? ';' : ':').filter((d) => d.length > 0);
|
|
125
|
+
const exts = platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
126
|
+
for (const dir of dirs) {
|
|
127
|
+
for (const ext of exts) {
|
|
128
|
+
const candidate = join(dir, name + ext);
|
|
129
|
+
if (exists(candidate))
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the consumer project's LOCAL runner, and the form of it that
|
|
137
|
+
* `spawn` can actually launch on this platform.
|
|
138
|
+
*
|
|
139
|
+
* Probed, in order (every probe is reported when nothing is found):
|
|
140
|
+
* 1. `<root>/node_modules/.bin/<runner>` (+ `.cmd`/`.exe` on Windows) —
|
|
141
|
+
* the project-local runner the command documents.
|
|
142
|
+
* 2. `<root>/node_modules/<runner>/package.json` → its `bin` JS entry.
|
|
143
|
+
* 3. PATH — last resort only; the caller prints a visible notice.
|
|
144
|
+
*
|
|
145
|
+
* Between 1 and 2 the **JS entry** wins: `spawn` runs it as
|
|
146
|
+
* `node <entry> …`, which is identical on Windows and POSIX and never
|
|
147
|
+
* routes argv through a shell. The `.cmd` shim is only a fallback because
|
|
148
|
+
* it needs cmd.exe to launch it (see `toSpawnable`).
|
|
149
|
+
*/
|
|
150
|
+
export function resolveRunner(framework, argv, projectRoot, deps = {}) {
|
|
151
|
+
const platform = deps.platform ?? process.platform;
|
|
152
|
+
const exists = deps.existsSync ?? existsSync;
|
|
153
|
+
const read = deps.readFileSync ?? ((path) => readFileSync(path, 'utf8'));
|
|
154
|
+
const env = deps.env ?? process.env;
|
|
155
|
+
const nodeExec = deps.nodeExecPath ?? process.execPath;
|
|
156
|
+
const searched = [];
|
|
157
|
+
// 1. Project-local `.bin` shim.
|
|
158
|
+
const binDir = join(projectRoot, 'node_modules', '.bin');
|
|
159
|
+
const shimExts = platform === 'win32' ? ['.cmd', '.exe'] : [''];
|
|
160
|
+
let shim = null;
|
|
161
|
+
for (const ext of shimExts) {
|
|
162
|
+
const candidate = join(binDir, framework + ext);
|
|
163
|
+
searched.push(candidate);
|
|
164
|
+
if (shim === null && exists(candidate))
|
|
165
|
+
shim = candidate;
|
|
166
|
+
}
|
|
167
|
+
// 2. Project-local package entry.
|
|
168
|
+
const pkgJsonPath = join(projectRoot, 'node_modules', framework, 'package.json');
|
|
169
|
+
searched.push(pkgJsonPath);
|
|
170
|
+
const entry = exists(pkgJsonPath) ? readBinEntry(pkgJsonPath, framework, read) : null;
|
|
171
|
+
if (entry !== null && exists(entry)) {
|
|
172
|
+
return { ok: true, command: nodeExec, args: [entry, ...argv], via: `local ${entry}`, fromPath: false };
|
|
173
|
+
}
|
|
174
|
+
if (shim !== null) {
|
|
175
|
+
return { ok: true, ...toSpawnable(shim, argv, platform, env), via: `local ${shim}`, fromPath: false };
|
|
176
|
+
}
|
|
177
|
+
// 3. PATH — last resort.
|
|
178
|
+
const onPath = resolveFromPath(framework, platform, env, exists);
|
|
179
|
+
searched.push(`PATH lookup for "${framework}"`);
|
|
180
|
+
if (onPath !== null) {
|
|
181
|
+
return { ok: true, ...toSpawnable(onPath, argv, platform, env), via: `PATH: ${onPath}`, fromPath: true };
|
|
182
|
+
}
|
|
183
|
+
return { ok: false, searched };
|
|
184
|
+
}
|
|
185
|
+
/** Actionable message for the no-runner case — never a raw ENOENT. */
|
|
186
|
+
export function formatRunnerNotFound(framework, searched) {
|
|
187
|
+
return [
|
|
188
|
+
`RUNNER_NOT_FOUND: no ${framework} runner found for this project. Looked for:`,
|
|
189
|
+
...searched.map((p) => ` - ${p}`),
|
|
190
|
+
`Install it (e.g. \`npm i -D ${framework}\`) or pass --framework <name>.`
|
|
191
|
+
].join('\n');
|
|
192
|
+
}
|
|
193
|
+
export function runRunner(framework, argv, projectRoot, deps = {}) {
|
|
194
|
+
const resolution = resolveRunner(framework, argv, projectRoot, deps);
|
|
195
|
+
if (!resolution.ok) {
|
|
196
|
+
return Promise.reject(new Error(formatRunnerNotFound(framework, resolution.searched)));
|
|
197
|
+
}
|
|
198
|
+
const notice = resolution.fromPath
|
|
199
|
+
? `[peaks test] no local ${framework} under ${join(projectRoot, 'node_modules')}; falling back to ${resolution.via}\n`
|
|
200
|
+
: null;
|
|
85
201
|
return new Promise((resolveRun, reject) => {
|
|
86
|
-
const
|
|
87
|
-
const proc =
|
|
202
|
+
const spawnFn = deps.spawnFn ?? spawn;
|
|
203
|
+
const proc = spawnFn(resolution.command, resolution.args, {
|
|
88
204
|
cwd: projectRoot,
|
|
89
205
|
env: process.env,
|
|
90
206
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
@@ -95,7 +211,7 @@ function runRunner(framework, argv, projectRoot) {
|
|
|
95
211
|
proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
96
212
|
proc.on('error', (err) => reject(err));
|
|
97
213
|
proc.on('close', (code) => {
|
|
98
|
-
resolveRun({ code: code ?? 0, stdout, stderr });
|
|
214
|
+
resolveRun({ code: code ?? 0, stdout, stderr, notice });
|
|
99
215
|
});
|
|
100
216
|
});
|
|
101
217
|
}
|
|
@@ -173,6 +289,8 @@ export function registerTestCommands(program, _io) {
|
|
|
173
289
|
});
|
|
174
290
|
// Stream the runner's output to the user.
|
|
175
291
|
const result = await runRunner(framework, argv, projectRoot);
|
|
292
|
+
if (result.notice !== null)
|
|
293
|
+
process.stderr.write(result.notice);
|
|
176
294
|
process.stdout.write(result.stdout);
|
|
177
295
|
process.stderr.write(result.stderr);
|
|
178
296
|
if (result.code !== 0) {
|
|
@@ -31,19 +31,39 @@ const REQUIRED_DIMENSIONS = [
|
|
|
31
31
|
'alternatives',
|
|
32
32
|
'constraints'
|
|
33
33
|
];
|
|
34
|
+
/**
|
|
35
|
+
* The allowed values for the three constrained fields.
|
|
36
|
+
*
|
|
37
|
+
* These are stated in the prompt AND enforced below: a prompt alone is a
|
|
38
|
+
* request, not a gate. Before this, the prompt named only `severity` with no
|
|
39
|
+
* allowed values, the model answered `severity: "high"`, and the gate passed
|
|
40
|
+
* it — leaving every downstream consumer that switches on
|
|
41
|
+
* `info | concern | blocker` holding a value it has never seen.
|
|
42
|
+
*/
|
|
43
|
+
const SEVERITIES = ['info', 'concern', 'blocker'];
|
|
44
|
+
const EFFORTS = ['small', 'medium', 'large', 'epic'];
|
|
45
|
+
const CONFIDENCES = ['high', 'medium', 'low'];
|
|
34
46
|
const SYSTEM_PROMPT = `You are auditing a software development need. Produce a structured JSON response with EXACTLY these fields:
|
|
35
47
|
- summary (1-2 sentence summary of the need)
|
|
36
48
|
- audit (array of EXACTLY 6 objects, one per dimension: correctness, completeness, scope, risks, alternatives, constraints; each with dimension, finding, severity)
|
|
37
49
|
- proposedGoal (what success looks like)
|
|
38
50
|
- successCriteria (list of acceptance criteria)
|
|
39
|
-
- roughEffort (small | medium | large | epic)
|
|
40
|
-
- confidence (high | medium | low)
|
|
51
|
+
- roughEffort (one of exactly: small | medium | large | epic)
|
|
52
|
+
- confidence (one of exactly: high | medium | low)
|
|
41
53
|
- rationale (one paragraph tying audit to goal)
|
|
42
54
|
|
|
55
|
+
severity is one of exactly: info | concern | blocker. Do not invent other values.
|
|
56
|
+
Any value outside the lists above is rejected and the whole response is discarded — a value that is merely plausible is not acceptable.
|
|
57
|
+
|
|
43
58
|
Output ONLY valid JSON, no prose.`;
|
|
44
59
|
export async function auditGoal(input, llmRunner) {
|
|
45
60
|
const userPrompt = `Need: ${input.need}\n\nAudit this need across the 6 dimensions and propose a goal.`;
|
|
46
|
-
|
|
61
|
+
// 8000, not 2000: the reply is a six-dimension JSON audit, and a reasoning
|
|
62
|
+
// model shares this budget with its own thinking blocks — measured against
|
|
63
|
+
// the session's provider, a 2000-token cap returns `stop_reason: max_tokens`
|
|
64
|
+
// with the JSON cut off mid-string. That is still ONE bounded call; it is
|
|
65
|
+
// just one the model can finish.
|
|
66
|
+
const response = await llmRunner.call(SYSTEM_PROMPT, userPrompt, { maxTokens: 8000 });
|
|
47
67
|
let parsed;
|
|
48
68
|
try {
|
|
49
69
|
parsed = JSON.parse(response.output);
|
|
@@ -59,6 +79,21 @@ export async function auditGoal(input, llmRunner) {
|
|
|
59
79
|
if (missing.length > 0) {
|
|
60
80
|
throw new IncompleteAuditError(`Missing required audit dimensions: ${missing.join(', ')}`);
|
|
61
81
|
}
|
|
82
|
+
// Coverage is not validity: a reply can carry all six dimensions and still
|
|
83
|
+
// hand downstream consumers enum values that do not exist. An out-of-enum
|
|
84
|
+
// value fails here — it is never coerced into a valid one, because silently
|
|
85
|
+
// repairing the model's output is what makes the enum decorative.
|
|
86
|
+
for (const entry of parsed.audit) {
|
|
87
|
+
if (!SEVERITIES.includes(entry.severity)) {
|
|
88
|
+
throw new IncompleteAuditError(`Invalid severity for dimension "${entry.dimension}": ${JSON.stringify(entry.severity)}. Allowed values: ${SEVERITIES.join(', ')}.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!EFFORTS.includes(parsed.roughEffort)) {
|
|
92
|
+
throw new IncompleteAuditError(`Invalid roughEffort: ${JSON.stringify(parsed.roughEffort)}. Allowed values: ${EFFORTS.join(', ')}.`);
|
|
93
|
+
}
|
|
94
|
+
if (!CONFIDENCES.includes(parsed.confidence)) {
|
|
95
|
+
throw new IncompleteAuditError(`Invalid confidence: ${JSON.stringify(parsed.confidence)}. Allowed values: ${CONFIDENCES.join(', ')}.`);
|
|
96
|
+
}
|
|
62
97
|
return parsed;
|
|
63
98
|
}
|
|
64
99
|
function isAuditGoalOutput(value) {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import { existsSync, statSync } from 'node:fs';
|
|
39
39
|
import { join } from 'node:path';
|
|
40
40
|
import { CODEGRAPH_DIR_NAME, CODEGRAPH_MARKER_NAME, createCodegraphInvocation, executeCodegraphInvocation, isCodegraphInitialized, } from './codegraph-service.js';
|
|
41
|
+
import { repairCodegraphExcludeFromProject } from './codegraph-exclude-repair.js';
|
|
41
42
|
/**
|
|
42
43
|
* True when `<projectRoot>/.codegraph/` exists and is a directory.
|
|
43
44
|
* Pure fs probe; never throws.
|
|
@@ -107,6 +108,17 @@ export async function refreshCodegraphAfterSlice(projectRoot, runner) {
|
|
|
107
108
|
note: `auto codegraph refresh self-heal init failed (exit ${String(initResult.exitCode)}): ${firstMeaningfulLine(initResult.stderr || initResult.stdout)}`,
|
|
108
109
|
};
|
|
109
110
|
}
|
|
111
|
+
// That init just wrote upstream's 99-rule default `exclude`
|
|
112
|
+
// template, some of which block tracked source files — the same
|
|
113
|
+
// self-heal the CLI's `peaks codegraph init` performs, via the
|
|
114
|
+
// same shared helper. Skipped, this path would stamp the
|
|
115
|
+
// peaks-loop marker over an incomplete index that no later
|
|
116
|
+
// `init` (it would no-op) could ever repair.
|
|
117
|
+
//
|
|
118
|
+
// Never throws (the helper catches everything), and
|
|
119
|
+
// `reindex: false` because the index call below covers the
|
|
120
|
+
// recovered files.
|
|
121
|
+
await repairCodegraphExcludeFromProject(projectRoot, runner, { reindex: false });
|
|
110
122
|
}
|
|
111
123
|
const invocation = createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true });
|
|
112
124
|
const result = await executeCodegraphInvocation(invocation, runner);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type CodegraphExcludeViolation } from './codegraph-exclude-reconciler.js';
|
|
2
|
+
/**
|
|
3
|
+
* Exit code `peaks codegraph status` uses when the index is
|
|
4
|
+
* demonstrably incomplete. Distinct from the upstream pass-through
|
|
5
|
+
* exit code and from `CODEGRAPH_INIT_CONFLICT_EXIT_CODE` (73) so a CI
|
|
6
|
+
* job can tell "codegraph said no" apart from "peaks-loop found a
|
|
7
|
+
* tracked source file the index silently dropped".
|
|
8
|
+
*/
|
|
9
|
+
export declare const CODEGRAPH_INTEGRITY_EXIT_CODE = 74;
|
|
10
|
+
export type CodegraphExcludeRuleImpact = {
|
|
11
|
+
/** The `exclude` rule that must be dropped. */
|
|
12
|
+
readonly rule: string;
|
|
13
|
+
/** Distinct tracked source files this single rule blocks. */
|
|
14
|
+
readonly blockedCount: number;
|
|
15
|
+
};
|
|
16
|
+
export type CodegraphExcludeIntegrityReport = {
|
|
17
|
+
/** Absolute path of the reconciled `.codegraph/config.json`. */
|
|
18
|
+
readonly configPath: string;
|
|
19
|
+
/** True when at least one tracked source file is blocked. */
|
|
20
|
+
readonly gap: boolean;
|
|
21
|
+
/** Tracked files that pass the config's `include` filter at all. */
|
|
22
|
+
readonly trackedSourceCount: number;
|
|
23
|
+
/** Distinct tracked source files blocked by at least one rule. */
|
|
24
|
+
readonly excludedTrackedCount: number;
|
|
25
|
+
/** One entry per (file, rule) pair — see S1's reconciler. */
|
|
26
|
+
readonly violations: readonly CodegraphExcludeViolation[];
|
|
27
|
+
/** Rules that must be dropped; empty exactly when `gap` is false. */
|
|
28
|
+
readonly rulesToRemove: readonly string[];
|
|
29
|
+
/** Per-rule blocked-file counts, in `rulesToRemove` order. */
|
|
30
|
+
readonly ruleImpacts: readonly CodegraphExcludeRuleImpact[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* True when `<projectRoot>/.codegraph/config.json` exists, i.e. when an
|
|
34
|
+
* exclude list is actually in play here. Both consumers use this to
|
|
35
|
+
* stay SILENT on a project that never ran `peaks codegraph init`: there
|
|
36
|
+
* is no exclusion to report, and a missing config is not a finding.
|
|
37
|
+
*
|
|
38
|
+
* Note the difference from `isCodegraphInitialized` in
|
|
39
|
+
* `codegraph-service.ts`, which probes `codegraph.db` (upstream's own
|
|
40
|
+
* definition of "initialized"). The exclude list is written by upstream
|
|
41
|
+
* init, so its presence is the narrower question this module asks.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isCodegraphExcludeConfigPresent(projectRoot: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Read the project's git-tracked files + `.codegraph/config.json` and
|
|
46
|
+
* fold the reconciliation into a report.
|
|
47
|
+
*
|
|
48
|
+
* Throws (never silently degrades) when the project is not a git work
|
|
49
|
+
* tree, when the config is missing, or when it is malformed — callers
|
|
50
|
+
* that must stay alive (doctor, `status`) catch and surface the reason.
|
|
51
|
+
*/
|
|
52
|
+
export declare function inspectCodegraphExcludeIntegrity(projectRoot: string): CodegraphExcludeIntegrityReport;
|
|
53
|
+
/**
|
|
54
|
+
* Human-readable detail lines for a gapped report: the headline count,
|
|
55
|
+
* then the offending rules, then a sample of the blocked files so an
|
|
56
|
+
* operator can point at a concrete path without re-running anything.
|
|
57
|
+
*
|
|
58
|
+
* Returns an empty array for a clean report — the caller decides
|
|
59
|
+
* whether "clean" is worth printing at all.
|
|
60
|
+
*/
|
|
61
|
+
export declare function renderCodegraphExcludeIntegrityLines(report: CodegraphExcludeIntegrityReport): readonly string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/services/codegraph/codegraph-exclude-integrity.ts
|
|
2
|
+
//
|
|
3
|
+
// Slice S2 of `2026-09-12-codegraph-exclude-integrity` — the READ-ONLY
|
|
4
|
+
// integrity report shared by `peaks codegraph status` and the
|
|
5
|
+
// `capability:codegraph-exclude-integrity` doctor check.
|
|
6
|
+
//
|
|
7
|
+
// S1's reconciler computes the raw truth (`violations` /
|
|
8
|
+
// `rulesToRemove`). This module folds it into a verdict-shaped report
|
|
9
|
+
// that both consumers can render and gate on, so neither of them has
|
|
10
|
+
// to re-derive "is there a gap, how big, and which rules cause it".
|
|
11
|
+
//
|
|
12
|
+
// It NEVER writes `.codegraph/config.json`. The only write path is
|
|
13
|
+
// `codegraph-exclude-repair.ts`, reachable from `peaks codegraph init`
|
|
14
|
+
// (fresh-initialization self-heal) and the explicit
|
|
15
|
+
// `peaks codegraph repair-exclude` command. Read stays read.
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { CODEGRAPH_CONFIG_FILENAME, reconcileCodegraphExcludeFromProject } from './codegraph-exclude-reconciler.js';
|
|
19
|
+
import { CODEGRAPH_DIR_NAME } from './codegraph-service.js';
|
|
20
|
+
/**
|
|
21
|
+
* Exit code `peaks codegraph status` uses when the index is
|
|
22
|
+
* demonstrably incomplete. Distinct from the upstream pass-through
|
|
23
|
+
* exit code and from `CODEGRAPH_INIT_CONFLICT_EXIT_CODE` (73) so a CI
|
|
24
|
+
* job can tell "codegraph said no" apart from "peaks-loop found a
|
|
25
|
+
* tracked source file the index silently dropped".
|
|
26
|
+
*/
|
|
27
|
+
export const CODEGRAPH_INTEGRITY_EXIT_CODE = 74;
|
|
28
|
+
/** How many offending rules and blocked files we name on the human path. */
|
|
29
|
+
const MAX_REPORTED_RULES = 10;
|
|
30
|
+
const MAX_REPORTED_FILES = 10;
|
|
31
|
+
/**
|
|
32
|
+
* True when `<projectRoot>/.codegraph/config.json` exists, i.e. when an
|
|
33
|
+
* exclude list is actually in play here. Both consumers use this to
|
|
34
|
+
* stay SILENT on a project that never ran `peaks codegraph init`: there
|
|
35
|
+
* is no exclusion to report, and a missing config is not a finding.
|
|
36
|
+
*
|
|
37
|
+
* Note the difference from `isCodegraphInitialized` in
|
|
38
|
+
* `codegraph-service.ts`, which probes `codegraph.db` (upstream's own
|
|
39
|
+
* definition of "initialized"). The exclude list is written by upstream
|
|
40
|
+
* init, so its presence is the narrower question this module asks.
|
|
41
|
+
*/
|
|
42
|
+
export function isCodegraphExcludeConfigPresent(projectRoot) {
|
|
43
|
+
return existsSync(join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the project's git-tracked files + `.codegraph/config.json` and
|
|
47
|
+
* fold the reconciliation into a report.
|
|
48
|
+
*
|
|
49
|
+
* Throws (never silently degrades) when the project is not a git work
|
|
50
|
+
* tree, when the config is missing, or when it is malformed — callers
|
|
51
|
+
* that must stay alive (doctor, `status`) catch and surface the reason.
|
|
52
|
+
*/
|
|
53
|
+
export function inspectCodegraphExcludeIntegrity(projectRoot) {
|
|
54
|
+
const result = reconcileCodegraphExcludeFromProject(projectRoot);
|
|
55
|
+
const blockedCounts = new Map();
|
|
56
|
+
for (const violation of result.violations) {
|
|
57
|
+
blockedCounts.set(violation.matchedRule, (blockedCounts.get(violation.matchedRule) ?? 0) + 1);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
configPath: join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME),
|
|
61
|
+
gap: result.excludedTrackedCount > 0,
|
|
62
|
+
trackedSourceCount: result.trackedSourceCount,
|
|
63
|
+
excludedTrackedCount: result.excludedTrackedCount,
|
|
64
|
+
violations: result.violations,
|
|
65
|
+
rulesToRemove: result.rulesToRemove,
|
|
66
|
+
ruleImpacts: result.rulesToRemove.map((rule) => ({ rule, blockedCount: blockedCounts.get(rule) ?? 0 }))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Human-readable detail lines for a gapped report: the headline count,
|
|
71
|
+
* then the offending rules, then a sample of the blocked files so an
|
|
72
|
+
* operator can point at a concrete path without re-running anything.
|
|
73
|
+
*
|
|
74
|
+
* Returns an empty array for a clean report — the caller decides
|
|
75
|
+
* whether "clean" is worth printing at all.
|
|
76
|
+
*/
|
|
77
|
+
export function renderCodegraphExcludeIntegrityLines(report) {
|
|
78
|
+
if (!report.gap) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const lines = [
|
|
82
|
+
`[FAIL] codegraph index is incomplete: ${report.excludedTrackedCount} of ${report.trackedSourceCount} tracked source files are excluded by ${report.rulesToRemove.length} rule(s).`
|
|
83
|
+
];
|
|
84
|
+
for (const impact of report.ruleImpacts.slice(0, MAX_REPORTED_RULES)) {
|
|
85
|
+
lines.push(` rule ${impact.rule} blocks ${impact.blockedCount} tracked file(s)`);
|
|
86
|
+
}
|
|
87
|
+
if (report.ruleImpacts.length > MAX_REPORTED_RULES) {
|
|
88
|
+
lines.push(` … and ${report.ruleImpacts.length - MAX_REPORTED_RULES} more rule(s)`);
|
|
89
|
+
}
|
|
90
|
+
for (const violation of report.violations.slice(0, MAX_REPORTED_FILES)) {
|
|
91
|
+
lines.push(` excluded: ${violation.path} <- ${violation.matchedRule}`);
|
|
92
|
+
}
|
|
93
|
+
if (report.violations.length > MAX_REPORTED_FILES) {
|
|
94
|
+
lines.push(` … and ${report.violations.length - MAX_REPORTED_FILES} more file/rule pair(s)`);
|
|
95
|
+
}
|
|
96
|
+
lines.push('Run `peaks codegraph repair-exclude --project <root>` to drop these rules, back up the config, and rebuild the index.');
|
|
97
|
+
return lines;
|
|
98
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function matchesCodegraphGlob(filePath: string, pattern: string): boolean;
|
|
2
|
+
export type CodegraphExcludeReconcileInput = {
|
|
3
|
+
readonly trackedFiles: readonly string[];
|
|
4
|
+
readonly include: readonly string[];
|
|
5
|
+
readonly exclude: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
export type CodegraphExcludeViolation = {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly matchedRule: string;
|
|
10
|
+
};
|
|
11
|
+
export type CodegraphExcludeReconcileResult = {
|
|
12
|
+
readonly violations: readonly CodegraphExcludeViolation[];
|
|
13
|
+
readonly rulesToRemove: readonly string[];
|
|
14
|
+
readonly trackedSourceCount: number;
|
|
15
|
+
readonly excludedTrackedCount: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function reconcileCodegraphExclude(input: CodegraphExcludeReconcileInput): CodegraphExcludeReconcileResult;
|
|
18
|
+
export declare const CODEGRAPH_CONFIG_FILENAME = "config.json";
|
|
19
|
+
export type CodegraphExcludeConfig = {
|
|
20
|
+
readonly include: readonly string[];
|
|
21
|
+
readonly exclude: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
export declare function readTrackedFiles(projectRoot: string): readonly string[];
|
|
24
|
+
export declare function assertStringArray(value: unknown, field: string, configPath: string): readonly string[];
|
|
25
|
+
export declare function readCodegraphExcludeConfig(projectRoot: string): CodegraphExcludeConfig;
|
|
26
|
+
export declare function reconcileCodegraphExcludeFromProject(projectRoot: string): CodegraphExcludeReconcileResult;
|