peaks-loop 4.0.43 → 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 +32 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- 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/share-commands.d.ts +49 -0
- package/dist/cli/commands/share-commands.js +114 -14
- 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/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/types.d.ts +27 -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/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/package.json +7 -5
- package/skills/peaks-final-review/SKILL.md +43 -32
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type CodegraphProcessRunner } from './codegraph-service.js';
|
|
2
|
+
/** Suffix of the byte-exact pre-repair copy kept next to the config. */
|
|
3
|
+
export declare const CODEGRAPH_CONFIG_BACKUP_SUFFIX = ".bak";
|
|
4
|
+
export type CodegraphExcludeRepairPlan = {
|
|
5
|
+
/** True when at least one rule would actually be dropped. */
|
|
6
|
+
readonly changed: boolean;
|
|
7
|
+
/** The `exclude` array after the removal. */
|
|
8
|
+
readonly exclude: readonly string[];
|
|
9
|
+
/** Rules actually present in `exclude` and dropped, in config order. */
|
|
10
|
+
readonly removedRules: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Pure: given the current `exclude` list and the rules to drop, return
|
|
14
|
+
* the new list. No fs, no clock, no serialization.
|
|
15
|
+
*
|
|
16
|
+
* A rule named in `rulesToRemove` but absent from `exclude` is NOT
|
|
17
|
+
* invented — the result is a subset of the input, so a caller can
|
|
18
|
+
* never add a rule by accident. Removing an already-absent rule is a
|
|
19
|
+
* no-op, which is what makes the whole repair idempotent: feeding the
|
|
20
|
+
* repaired list back in yields `changed: false`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function repairCodegraphExclude(input: {
|
|
23
|
+
readonly exclude: readonly string[];
|
|
24
|
+
readonly rulesToRemove: readonly string[];
|
|
25
|
+
}): CodegraphExcludeRepairPlan;
|
|
26
|
+
export type CodegraphExcludeRepairOutcome = {
|
|
27
|
+
readonly applied: false;
|
|
28
|
+
readonly reason: 'no-rules-to-remove';
|
|
29
|
+
readonly removedRules: readonly string[];
|
|
30
|
+
} | {
|
|
31
|
+
readonly applied: true;
|
|
32
|
+
readonly configPath: string;
|
|
33
|
+
readonly backupPath: string;
|
|
34
|
+
readonly removedRules: readonly string[];
|
|
35
|
+
readonly excludeCountBefore: number;
|
|
36
|
+
readonly excludeCountAfter: number;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Apply the repair to `<projectRoot>/.codegraph/config.json`.
|
|
40
|
+
*
|
|
41
|
+
* No-op (and no write, no mtime change, no backup) when
|
|
42
|
+
* `rulesToRemove` is empty. Otherwise: back up the original bytes to
|
|
43
|
+
* `config.json.bak`, then rewrite the file with `exclude` reduced by
|
|
44
|
+
* exactly the rules that were both requested and present.
|
|
45
|
+
*
|
|
46
|
+
* Throws only on real fs/parse failures — the caller decides whether
|
|
47
|
+
* that is fatal (`repair-exclude` → non-zero exit) or a surfaced
|
|
48
|
+
* warning (`init` → keep going, the init itself already succeeded).
|
|
49
|
+
*/
|
|
50
|
+
export declare function applyCodegraphExcludeRepair(projectRoot: string, rulesToRemove: readonly string[]): CodegraphExcludeRepairOutcome;
|
|
51
|
+
export type CodegraphExcludeRepairReport = {
|
|
52
|
+
/** True when the config was actually rewritten. */
|
|
53
|
+
readonly applied: boolean;
|
|
54
|
+
/** Rules dropped from `exclude`. */
|
|
55
|
+
readonly rulesRemoved: readonly string[];
|
|
56
|
+
/** Distinct tracked source files the dropped rules had been hiding. */
|
|
57
|
+
readonly filesRecovered: number;
|
|
58
|
+
/** Tracked files the config's `include` filter admits at all. */
|
|
59
|
+
readonly trackedSourceCount: number;
|
|
60
|
+
/** Absolute path of the rewritten config (empty when nothing was written). */
|
|
61
|
+
readonly configPath: string;
|
|
62
|
+
/** Byte-exact rollback copy (null when nothing was written). */
|
|
63
|
+
readonly backupPath: string | null;
|
|
64
|
+
/** True when the post-repair `codegraph index` finished successfully. */
|
|
65
|
+
readonly reindexed: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Non-null when the step could not run to completion (no git repo,
|
|
68
|
+
* missing config, malformed config, upstream index failure, …).
|
|
69
|
+
* Always reported, never swallowed — but never thrown either, so a
|
|
70
|
+
* successful `peaks codegraph init` is never undone by it.
|
|
71
|
+
*/
|
|
72
|
+
readonly warning: string | null;
|
|
73
|
+
};
|
|
74
|
+
export type CodegraphExcludeRepairOptions = {
|
|
75
|
+
/**
|
|
76
|
+
* When `false`, skip the follow-up `codegraph index` rebuild and
|
|
77
|
+
* return `reindexed: false` (with no warning — nothing went wrong).
|
|
78
|
+
*
|
|
79
|
+
* Set by callers that run their OWN index immediately after this
|
|
80
|
+
* step: the pre-dispatch preflight and the post-slice autorefresh
|
|
81
|
+
* both call `init` → `index`, and an index is expensive enough
|
|
82
|
+
* (5-30 s on this repo) that a fresh init must not pay for it twice.
|
|
83
|
+
*/
|
|
84
|
+
readonly reindex?: boolean;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Reconcile `projectRoot`'s exclude list, drop the rules that block
|
|
88
|
+
* tracked source files, and rebuild the index so the recovered files
|
|
89
|
+
* actually land in it.
|
|
90
|
+
*
|
|
91
|
+
* This is the ONE shared "after upstream init" self-heal: the CLI's
|
|
92
|
+
* fresh `peaks codegraph init`, the pre-dispatch preflight and the
|
|
93
|
+
* post-slice autorefresh all call it, so a fresh clone cannot end up
|
|
94
|
+
* with a peaks-loop marker stamped over an incomplete index that no
|
|
95
|
+
* later `init` would ever repair.
|
|
96
|
+
*
|
|
97
|
+
* Never throws. Every failure becomes a populated `warning` field, so
|
|
98
|
+
* the caller can surface it without the step being able to break the
|
|
99
|
+
* command it is attached to — including the fail-soft preflight and
|
|
100
|
+
* the never-throwing `refreshCodegraphAfterSlice`.
|
|
101
|
+
*/
|
|
102
|
+
export declare function repairCodegraphExcludeFromProject(projectRoot: string, runner?: CodegraphProcessRunner, options?: CodegraphExcludeRepairOptions): Promise<CodegraphExcludeRepairReport>;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/services/codegraph/codegraph-exclude-repair.ts
|
|
2
|
+
//
|
|
3
|
+
// Slice S2 of `2026-09-12-codegraph-exclude-integrity` — the WRITE path.
|
|
4
|
+
//
|
|
5
|
+
// S1 computes `rulesToRemove`; this module applies it. Two callers are
|
|
6
|
+
// allowed to reach it and nothing else is:
|
|
7
|
+
//
|
|
8
|
+
// 1. `peaks codegraph init` — after a fresh upstream init (which
|
|
9
|
+
// always writes upstream's 99-rule default template), so a brand
|
|
10
|
+
// new clone / new machine ends up with a complete index instead
|
|
11
|
+
// of silently dropping tracked source files.
|
|
12
|
+
// 2. `peaks codegraph repair-exclude` — the explicit repair for a
|
|
13
|
+
// workspace that was initialized before this slice shipped, or
|
|
14
|
+
// whose config drifted.
|
|
15
|
+
//
|
|
16
|
+
// `status` and the doctor check deliberately do NOT live here: they
|
|
17
|
+
// read `codegraph-exclude-integrity.ts` and never write.
|
|
18
|
+
//
|
|
19
|
+
// Safety posture (the file being edited belongs to a THIRD-PARTY tool):
|
|
20
|
+
// - `rulesToRemove` comes from S1, which only ever lists a rule that
|
|
21
|
+
// actually blocks at least one git-tracked source file. A rule that
|
|
22
|
+
// matches nothing tracked is never dropped.
|
|
23
|
+
// - Nothing is written when `rulesToRemove` is empty — the config
|
|
24
|
+
// bytes, and its mtime, are untouched.
|
|
25
|
+
// - The original bytes are copied to `config.json.bak` before the
|
|
26
|
+
// rewrite, so a rollback is byte-exact.
|
|
27
|
+
// - The rewrite itself goes through a same-directory temp file plus
|
|
28
|
+
// `renameSync`, so the config is never observed half-written.
|
|
29
|
+
// - Every other key of the config survives verbatim, in its original
|
|
30
|
+
// position; only `exclude` changes.
|
|
31
|
+
import { randomBytes } from 'node:crypto';
|
|
32
|
+
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { assertStringArray, CODEGRAPH_CONFIG_FILENAME } from './codegraph-exclude-reconciler.js';
|
|
35
|
+
import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, executeCodegraphInvocation } from './codegraph-service.js';
|
|
36
|
+
import { inspectCodegraphExcludeIntegrity } from './codegraph-exclude-integrity.js';
|
|
37
|
+
/** Suffix of the byte-exact pre-repair copy kept next to the config. */
|
|
38
|
+
export const CODEGRAPH_CONFIG_BACKUP_SUFFIX = '.bak';
|
|
39
|
+
/**
|
|
40
|
+
* Pure: given the current `exclude` list and the rules to drop, return
|
|
41
|
+
* the new list. No fs, no clock, no serialization.
|
|
42
|
+
*
|
|
43
|
+
* A rule named in `rulesToRemove` but absent from `exclude` is NOT
|
|
44
|
+
* invented — the result is a subset of the input, so a caller can
|
|
45
|
+
* never add a rule by accident. Removing an already-absent rule is a
|
|
46
|
+
* no-op, which is what makes the whole repair idempotent: feeding the
|
|
47
|
+
* repaired list back in yields `changed: false`.
|
|
48
|
+
*/
|
|
49
|
+
export function repairCodegraphExclude(input) {
|
|
50
|
+
const removable = new Set(input.rulesToRemove);
|
|
51
|
+
// De-duplicated, order-preserving. A config that lists the same rule
|
|
52
|
+
// twice would otherwise be counted twice here, and this array is what
|
|
53
|
+
// the caller reports to the user as "rules removed".
|
|
54
|
+
const removedRules = [...new Set(input.exclude.filter((rule) => removable.has(rule)))];
|
|
55
|
+
if (removedRules.length === 0) {
|
|
56
|
+
return { changed: false, exclude: input.exclude, removedRules: [] };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
changed: true,
|
|
60
|
+
exclude: input.exclude.filter((rule) => !removable.has(rule)),
|
|
61
|
+
removedRules
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect the indentation the config already uses so the rewrite keeps
|
|
66
|
+
* the file's shape instead of reformatting a third-party tool's file.
|
|
67
|
+
*
|
|
68
|
+
* A single-line (minified) config has no indentation to copy: `0` tells
|
|
69
|
+
* `JSON.stringify` to emit compact JSON, so the file comes back as the
|
|
70
|
+
* one-liner it went in as. `0` is NOT the same as "not set" here — an
|
|
71
|
+
* omitted indent would also be compact, but returning a number keeps the
|
|
72
|
+
* intent explicit at the call site.
|
|
73
|
+
*
|
|
74
|
+
* Falls back to two spaces when the file spans lines but has no indented
|
|
75
|
+
* member.
|
|
76
|
+
*/
|
|
77
|
+
function detectIndent(text) {
|
|
78
|
+
if (!text.trimEnd().includes('\n')) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const match = /\n([ \t]+)"/.exec(text);
|
|
82
|
+
return match?.[1] ?? 2;
|
|
83
|
+
}
|
|
84
|
+
function serializeConfig(config, originalText) {
|
|
85
|
+
const body = JSON.stringify(config, null, detectIndent(originalText));
|
|
86
|
+
return originalText.endsWith('\n') ? `${body}\n` : body;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write `content` to `filePath` atomically: same-directory temp file,
|
|
90
|
+
* then `renameSync` over the target.
|
|
91
|
+
*
|
|
92
|
+
* The file being written belongs to a THIRD-PARTY tool, so a crash or a
|
|
93
|
+
* full disk mid-`writeFileSync` must never leave a half-written config
|
|
94
|
+
* behind. `rename` within one directory is atomic, so a reader sees
|
|
95
|
+
* either the old bytes or the new ones, never a prefix of the new ones.
|
|
96
|
+
* The temp file lives next to the target (same directory ⇒ same
|
|
97
|
+
* filesystem ⇒ the rename cannot degrade to a cross-device copy) and is
|
|
98
|
+
* removed if the write or the rename fails.
|
|
99
|
+
*
|
|
100
|
+
* N5: the temp name carries the pid plus 6 random bytes. A FIXED
|
|
101
|
+
* `${filePath}.tmp` is single-writer only, and this writer has three
|
|
102
|
+
* reachable concurrent callers — a fresh `peaks codegraph init` (which
|
|
103
|
+
* repairs then indexes), the pre-dispatch preflight, and the post-slice
|
|
104
|
+
* autorefresh. Two overlapping writers sharing one temp name let one
|
|
105
|
+
* `renameSync` publish a file the other was still writing, which is
|
|
106
|
+
* exactly the half-written config the temp file exists to prevent. The
|
|
107
|
+
* suffix keeps the temp in the SAME directory, so the rename stays
|
|
108
|
+
* within one filesystem and therefore stays atomic.
|
|
109
|
+
*/
|
|
110
|
+
function writeConfigAtomic(filePath, content) {
|
|
111
|
+
const tempPath = `${filePath}.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`;
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(tempPath, content, 'utf8');
|
|
114
|
+
renameSync(tempPath, filePath);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
rmSync(tempPath, { force: true });
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Apply the repair to `<projectRoot>/.codegraph/config.json`.
|
|
123
|
+
*
|
|
124
|
+
* No-op (and no write, no mtime change, no backup) when
|
|
125
|
+
* `rulesToRemove` is empty. Otherwise: back up the original bytes to
|
|
126
|
+
* `config.json.bak`, then rewrite the file with `exclude` reduced by
|
|
127
|
+
* exactly the rules that were both requested and present.
|
|
128
|
+
*
|
|
129
|
+
* Throws only on real fs/parse failures — the caller decides whether
|
|
130
|
+
* that is fatal (`repair-exclude` → non-zero exit) or a surfaced
|
|
131
|
+
* warning (`init` → keep going, the init itself already succeeded).
|
|
132
|
+
*/
|
|
133
|
+
export function applyCodegraphExcludeRepair(projectRoot, rulesToRemove) {
|
|
134
|
+
if (rulesToRemove.length === 0) {
|
|
135
|
+
return { applied: false, reason: 'no-rules-to-remove', removedRules: [] };
|
|
136
|
+
}
|
|
137
|
+
const configPath = join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME);
|
|
138
|
+
const originalText = readFileSync(configPath, 'utf8');
|
|
139
|
+
const parsed = JSON.parse(originalText);
|
|
140
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
141
|
+
throw new Error(`codegraph config ${configPath}: expected a JSON object`);
|
|
142
|
+
}
|
|
143
|
+
const record = parsed;
|
|
144
|
+
const exclude = assertStringArray(record.exclude, 'exclude', configPath);
|
|
145
|
+
const plan = repairCodegraphExclude({ exclude, rulesToRemove });
|
|
146
|
+
if (!plan.changed) {
|
|
147
|
+
return { applied: false, reason: 'no-rules-to-remove', removedRules: [] };
|
|
148
|
+
}
|
|
149
|
+
const backupPath = `${configPath}${CODEGRAPH_CONFIG_BACKUP_SUFFIX}`;
|
|
150
|
+
writeFileSync(backupPath, originalText, 'utf8');
|
|
151
|
+
// Spread first, then replace `exclude` — every other key keeps its
|
|
152
|
+
// original value AND its original position in the serialized object.
|
|
153
|
+
writeConfigAtomic(configPath, serializeConfig({ ...record, exclude: plan.exclude }, originalText));
|
|
154
|
+
return {
|
|
155
|
+
applied: true,
|
|
156
|
+
configPath,
|
|
157
|
+
backupPath,
|
|
158
|
+
removedRules: plan.removedRules,
|
|
159
|
+
excludeCountBefore: exclude.length,
|
|
160
|
+
excludeCountAfter: plan.exclude.length
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function errorMessage(error) {
|
|
164
|
+
return error instanceof Error ? error.message : String(error);
|
|
165
|
+
}
|
|
166
|
+
function emptyRepairReport(warning) {
|
|
167
|
+
return {
|
|
168
|
+
applied: false,
|
|
169
|
+
rulesRemoved: [],
|
|
170
|
+
filesRecovered: 0,
|
|
171
|
+
trackedSourceCount: 0,
|
|
172
|
+
configPath: '',
|
|
173
|
+
backupPath: null,
|
|
174
|
+
reindexed: false,
|
|
175
|
+
warning
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Reconcile `projectRoot`'s exclude list, drop the rules that block
|
|
180
|
+
* tracked source files, and rebuild the index so the recovered files
|
|
181
|
+
* actually land in it.
|
|
182
|
+
*
|
|
183
|
+
* This is the ONE shared "after upstream init" self-heal: the CLI's
|
|
184
|
+
* fresh `peaks codegraph init`, the pre-dispatch preflight and the
|
|
185
|
+
* post-slice autorefresh all call it, so a fresh clone cannot end up
|
|
186
|
+
* with a peaks-loop marker stamped over an incomplete index that no
|
|
187
|
+
* later `init` would ever repair.
|
|
188
|
+
*
|
|
189
|
+
* Never throws. Every failure becomes a populated `warning` field, so
|
|
190
|
+
* the caller can surface it without the step being able to break the
|
|
191
|
+
* command it is attached to — including the fail-soft preflight and
|
|
192
|
+
* the never-throwing `refreshCodegraphAfterSlice`.
|
|
193
|
+
*/
|
|
194
|
+
export async function repairCodegraphExcludeFromProject(projectRoot, runner, options = {}) {
|
|
195
|
+
let integrity;
|
|
196
|
+
try {
|
|
197
|
+
integrity = inspectCodegraphExcludeIntegrity(projectRoot);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return emptyRepairReport(`codegraph exclude reconcile skipped: ${errorMessage(error)}`);
|
|
201
|
+
}
|
|
202
|
+
if (integrity.rulesToRemove.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
applied: false,
|
|
205
|
+
rulesRemoved: [],
|
|
206
|
+
filesRecovered: 0,
|
|
207
|
+
trackedSourceCount: integrity.trackedSourceCount,
|
|
208
|
+
configPath: integrity.configPath,
|
|
209
|
+
backupPath: null,
|
|
210
|
+
reindexed: false,
|
|
211
|
+
warning: null
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
let outcome;
|
|
215
|
+
try {
|
|
216
|
+
outcome = applyCodegraphExcludeRepair(projectRoot, integrity.rulesToRemove);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return emptyRepairReport(`codegraph exclude repair failed for ${integrity.configPath}: ${errorMessage(error)}`);
|
|
220
|
+
}
|
|
221
|
+
if (!outcome.applied) {
|
|
222
|
+
return {
|
|
223
|
+
applied: false,
|
|
224
|
+
rulesRemoved: [],
|
|
225
|
+
filesRecovered: 0,
|
|
226
|
+
trackedSourceCount: integrity.trackedSourceCount,
|
|
227
|
+
configPath: integrity.configPath,
|
|
228
|
+
backupPath: null,
|
|
229
|
+
reindexed: false,
|
|
230
|
+
warning: null
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const base = {
|
|
234
|
+
applied: true,
|
|
235
|
+
rulesRemoved: outcome.removedRules,
|
|
236
|
+
filesRecovered: integrity.excludedTrackedCount,
|
|
237
|
+
trackedSourceCount: integrity.trackedSourceCount,
|
|
238
|
+
configPath: outcome.configPath,
|
|
239
|
+
backupPath: outcome.backupPath
|
|
240
|
+
};
|
|
241
|
+
if (options.reindex === false) {
|
|
242
|
+
// The caller indexes immediately after this returns, so doing it
|
|
243
|
+
// here too would index the same tree twice for no extra coverage.
|
|
244
|
+
return { ...base, reindexed: false, warning: null };
|
|
245
|
+
}
|
|
246
|
+
// The recovered files are on disk but not in the index yet. Rebuild
|
|
247
|
+
// it now — that is the whole point of repairing at init time.
|
|
248
|
+
try {
|
|
249
|
+
const result = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'index', project: projectRoot, quiet: true }), runner);
|
|
250
|
+
if (result.exitCode !== 0) {
|
|
251
|
+
return {
|
|
252
|
+
...base,
|
|
253
|
+
reindexed: false,
|
|
254
|
+
warning: `codegraph exclude repaired (${outcome.removedRules.length} rule(s) removed) but the follow-up index failed (exit ${String(result.exitCode)}); run \`peaks codegraph index --project <root>\`.`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { ...base, reindexed: true, warning: null };
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
reindexed: false,
|
|
263
|
+
warning: `codegraph exclude repaired (${outcome.removedRules.length} rule(s) removed) but the follow-up index could not run: ${errorMessage(error)}`
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { mkdirSync } from 'node:fs';
|
|
34
34
|
import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, defaultCodegraphInitGuard, executeCodegraphInvocation, isCodegraphInitialized, writeCodegraphMarker, } from './codegraph-service.js';
|
|
35
35
|
import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
|
|
36
|
+
import { repairCodegraphExcludeFromProject } from './codegraph-exclude-repair.js';
|
|
36
37
|
/** Cap for the directory histogram in the rendered structure block. */
|
|
37
38
|
export const CODEGRAPH_STRUCTURE_MAX_DIRS = 40;
|
|
38
39
|
/** Cap for bare root files listed in the rendered structure block. */
|
|
@@ -208,6 +209,17 @@ export async function buildCodegraphPreflightBlock(projectRoot, runner) {
|
|
|
208
209
|
catch {
|
|
209
210
|
// Best-effort: a marker-write failure must not undo the init.
|
|
210
211
|
}
|
|
212
|
+
// Upstream init just wrote its 99-rule default `exclude` template,
|
|
213
|
+
// some of which block tracked source files. Without this, a fresh
|
|
214
|
+
// clone is initialized HERE, by the first dispatch's preflight —
|
|
215
|
+
// marker stamped over an incomplete index — and every later
|
|
216
|
+
// `peaks codegraph init` hits `noop-already-peaks-loop`, so the
|
|
217
|
+
// CLI's own self-heal becomes unreachable. Same shared helper, so
|
|
218
|
+
// all three init landing sites behave identically.
|
|
219
|
+
//
|
|
220
|
+
// Never throws, and `reindex: false` because the index call below
|
|
221
|
+
// covers the recovered files anyway.
|
|
222
|
+
await repairCodegraphExcludeFromProject(projectRoot, processRunner, { reindex: false });
|
|
211
223
|
}
|
|
212
224
|
catch (error) {
|
|
213
225
|
return { available: false, note: `codegraph init unavailable: ${errorMessage(error)}` };
|
|
@@ -13,7 +13,11 @@ const NUMERIC_FLAG_NAMES = ['limit', 'maxDepth'];
|
|
|
13
13
|
const COMMON_OPTION_KEYS = ['subcommand', 'project'];
|
|
14
14
|
const ALLOWED_OPTIONS_BY_SUBCOMMAND = {
|
|
15
15
|
status: [],
|
|
16
|
-
init
|
|
16
|
+
// Upstream `init` takes NO flags. The `--yes` we used to whitelist
|
|
17
|
+
// here was passed straight through to `@colbymchenry/codegraph`, which
|
|
18
|
+
// rejects it with CODEGRAPH_COMMAND_FAILED — and upstream init never
|
|
19
|
+
// prompts, so there was nothing for it to answer.
|
|
20
|
+
init: [],
|
|
17
21
|
index: ['force', 'quiet'],
|
|
18
22
|
query: ['search', 'json', 'limit'],
|
|
19
23
|
files: ['json', 'maxDepth'],
|
|
@@ -120,9 +124,6 @@ function buildCommandArgs(options, projectRoot) {
|
|
|
120
124
|
if (options.subcommand === 'affected') {
|
|
121
125
|
args.push(...buildAffectedFileArgs(projectRoot, options.files));
|
|
122
126
|
}
|
|
123
|
-
if (options.yes === true) {
|
|
124
|
-
args.push('--yes');
|
|
125
|
-
}
|
|
126
127
|
if (options.force === true) {
|
|
127
128
|
args.push('--force');
|
|
128
129
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check: codegraph exclude integrity (`capability:codegraph-exclude-integrity`).
|
|
3
|
+
*
|
|
4
|
+
* The companion to `capability:codegraph`. That check answers "is the
|
|
5
|
+
* upstream package resolvable at the pinned version"; this one answers
|
|
6
|
+
* "does the project's `.codegraph/config.json` exclude rules silently
|
|
7
|
+
* drop source files git tracks".
|
|
8
|
+
*
|
|
9
|
+
* Why it exists: upstream ships a default `exclude` template matched by
|
|
10
|
+
* *directory name*, and `config.json`'s `exclude` array replaces those
|
|
11
|
+
* defaults wholesale. Any default rule whose directory name collides
|
|
12
|
+
* with a real source directory drops tracked files from the index while
|
|
13
|
+
* `peaks codegraph status` still printed `[OK] Index is up to date`.
|
|
14
|
+
*
|
|
15
|
+
* Read-only by construction — it consumes the same inspector
|
|
16
|
+
* `peaks codegraph status` gates on and never writes the config.
|
|
17
|
+
*
|
|
18
|
+
* Failure posture:
|
|
19
|
+
* - codegraph not initialized in the inspected root (no
|
|
20
|
+
* `.codegraph/config.json`) → `ok: true`; there is nothing to
|
|
21
|
+
* reconcile and a fresh clone must not fail the doctor.
|
|
22
|
+
* - a confirmed gap → `ok: false` (blocking; the index is provably
|
|
23
|
+
* incomplete and the fix is one command).
|
|
24
|
+
* - could not evaluate (not a git work tree, malformed config) →
|
|
25
|
+
* `ok: false, severity: 'warning'` so the doctor reports the blind
|
|
26
|
+
* spot without flipping the exit code on an unrelated failure.
|
|
27
|
+
*/
|
|
28
|
+
import type { DoctorCheckPlugin } from '../types.js';
|
|
29
|
+
export declare const check: DoctorCheckPlugin;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check: codegraph exclude integrity (`capability:codegraph-exclude-integrity`).
|
|
3
|
+
*
|
|
4
|
+
* The companion to `capability:codegraph`. That check answers "is the
|
|
5
|
+
* upstream package resolvable at the pinned version"; this one answers
|
|
6
|
+
* "does the project's `.codegraph/config.json` exclude rules silently
|
|
7
|
+
* drop source files git tracks".
|
|
8
|
+
*
|
|
9
|
+
* Why it exists: upstream ships a default `exclude` template matched by
|
|
10
|
+
* *directory name*, and `config.json`'s `exclude` array replaces those
|
|
11
|
+
* defaults wholesale. Any default rule whose directory name collides
|
|
12
|
+
* with a real source directory drops tracked files from the index while
|
|
13
|
+
* `peaks codegraph status` still printed `[OK] Index is up to date`.
|
|
14
|
+
*
|
|
15
|
+
* Read-only by construction — it consumes the same inspector
|
|
16
|
+
* `peaks codegraph status` gates on and never writes the config.
|
|
17
|
+
*
|
|
18
|
+
* Failure posture:
|
|
19
|
+
* - codegraph not initialized in the inspected root (no
|
|
20
|
+
* `.codegraph/config.json`) → `ok: true`; there is nothing to
|
|
21
|
+
* reconcile and a fresh clone must not fail the doctor.
|
|
22
|
+
* - a confirmed gap → `ok: false` (blocking; the index is provably
|
|
23
|
+
* incomplete and the fix is one command).
|
|
24
|
+
* - could not evaluate (not a git work tree, malformed config) →
|
|
25
|
+
* `ok: false, severity: 'warning'` so the doctor reports the blind
|
|
26
|
+
* spot without flipping the exit code on an unrelated failure.
|
|
27
|
+
*/
|
|
28
|
+
import { getErrorMessage } from 'peaks-loop-shared/result';
|
|
29
|
+
import { inspectCodegraphExcludeIntegrity, isCodegraphExcludeConfigPresent } from '../../../codegraph/codegraph-exclude-integrity.js';
|
|
30
|
+
const CHECK_ID = 'capability:codegraph-exclude-integrity';
|
|
31
|
+
/** How many rules / offending files the message names before eliding. */
|
|
32
|
+
const MAX_NAMED = 5;
|
|
33
|
+
function defaultProbe() {
|
|
34
|
+
const projectRoot = process.cwd();
|
|
35
|
+
// No config → codegraph was never initialized here, so no exclude
|
|
36
|
+
// list is in play and there is nothing to report.
|
|
37
|
+
return isCodegraphExcludeConfigPresent(projectRoot)
|
|
38
|
+
? inspectCodegraphExcludeIntegrity(projectRoot)
|
|
39
|
+
: null;
|
|
40
|
+
}
|
|
41
|
+
function renderGapMessage(excludedTrackedCount, trackedSourceCount, rulesToRemove, violations) {
|
|
42
|
+
const namedRules = rulesToRemove.slice(0, MAX_NAMED).join(', ');
|
|
43
|
+
const elidedRules = rulesToRemove.length > MAX_NAMED ? `, … (+${rulesToRemove.length - MAX_NAMED})` : '';
|
|
44
|
+
const namedFiles = violations
|
|
45
|
+
.slice(0, MAX_NAMED)
|
|
46
|
+
.map((violation) => `${violation.path} <- ${violation.matchedRule}`)
|
|
47
|
+
.join('; ');
|
|
48
|
+
const elidedFiles = violations.length > MAX_NAMED ? `; … (+${violations.length - MAX_NAMED})` : '';
|
|
49
|
+
return `codegraph index is incomplete: ${excludedTrackedCount} of ${trackedSourceCount} tracked source files are blocked by ${rulesToRemove.length} exclude rule(s) [${namedRules}${elidedRules}]. Blocked: ${namedFiles}${elidedFiles}. Run \`peaks codegraph repair-exclude --project <root>\` to drop them and rebuild the index.`;
|
|
50
|
+
}
|
|
51
|
+
function run({ options }) {
|
|
52
|
+
const probe = options.codegraphIntegrityProbe ?? defaultProbe;
|
|
53
|
+
let report;
|
|
54
|
+
try {
|
|
55
|
+
report = probe();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return [{
|
|
59
|
+
id: CHECK_ID,
|
|
60
|
+
ok: false,
|
|
61
|
+
severity: 'warning',
|
|
62
|
+
message: `codegraph exclude integrity could not be evaluated: ${getErrorMessage(error)}`
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
if (report === null) {
|
|
66
|
+
return [{
|
|
67
|
+
id: CHECK_ID,
|
|
68
|
+
ok: true,
|
|
69
|
+
message: 'codegraph is not initialized in this project (no .codegraph/config.json); the exclude list is not in play yet'
|
|
70
|
+
}];
|
|
71
|
+
}
|
|
72
|
+
if (!report.gap) {
|
|
73
|
+
return [{
|
|
74
|
+
id: CHECK_ID,
|
|
75
|
+
ok: true,
|
|
76
|
+
message: `codegraph exclude list drops no tracked source file (${report.trackedSourceCount} tracked source file(s) admitted by include)`
|
|
77
|
+
}];
|
|
78
|
+
}
|
|
79
|
+
return [{
|
|
80
|
+
id: CHECK_ID,
|
|
81
|
+
ok: false,
|
|
82
|
+
message: renderGapMessage(report.excludedTrackedCount, report.trackedSourceCount, report.rulesToRemove, report.violations)
|
|
83
|
+
}];
|
|
84
|
+
}
|
|
85
|
+
export const check = {
|
|
86
|
+
name: 'codegraph-exclude-integrity',
|
|
87
|
+
run
|
|
88
|
+
};
|
|
@@ -42,6 +42,7 @@ import { check as workspaceInit } from './checks/workspace-init.js';
|
|
|
42
42
|
import { check as statuslineInstall } from './checks/statusline-install.js';
|
|
43
43
|
import { check as statuslineRuntime } from './checks/statusline-runtime.js';
|
|
44
44
|
import { check as codegraphCapability } from './checks/codegraph-capability.js';
|
|
45
|
+
import { check as codegraphExcludeIntegrity } from './checks/codegraph-exclude-integrity.js';
|
|
45
46
|
import { check as distSourceVersion } from './checks/dist-source-version.js';
|
|
46
47
|
import { check as multiBinaryDrift } from './checks/multi-binary-drift.js';
|
|
47
48
|
import { check as workspaceLayout } from './checks/workspace-layout.js';
|
|
@@ -70,6 +71,7 @@ export const PLUGINS = [
|
|
|
70
71
|
statuslineInstall, // id "statusline:install"
|
|
71
72
|
statuslineRuntime, // id "statusline:runtime"
|
|
72
73
|
codegraphCapability, // id "capability:codegraph"
|
|
74
|
+
codegraphExcludeIntegrity, // id "capability:codegraph-exclude-integrity"
|
|
73
75
|
distSourceVersion, // id "build:dist-version-matches-source"
|
|
74
76
|
multiBinaryDrift, // id "build:multi-binary-drift"
|
|
75
77
|
workspaceLayout, // id "build:workspace-layout-canonical"
|
|
@@ -75,6 +75,25 @@ export type CodegraphCapabilityProbe = {
|
|
|
75
75
|
*/
|
|
76
76
|
managedPath: CodegraphManagedPathInfo | null;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Structural shape of the codegraph exclude-integrity report the
|
|
80
|
+
* `capability:codegraph-exclude-integrity` check gates on. Declared
|
|
81
|
+
* structurally (rather than imported from the codegraph service) to
|
|
82
|
+
* keep this type module dependency-free — the default probe returns a
|
|
83
|
+
* `CodegraphExcludeIntegrityReport`, which is assignable here.
|
|
84
|
+
*/
|
|
85
|
+
export type CodegraphExcludeIntegrityProbe = {
|
|
86
|
+
readonly configPath: string;
|
|
87
|
+
readonly gap: boolean;
|
|
88
|
+
readonly trackedSourceCount: number;
|
|
89
|
+
readonly excludedTrackedCount: number;
|
|
90
|
+
readonly rulesToRemove: readonly string[];
|
|
91
|
+
/** One entry per (file, rule) pair. */
|
|
92
|
+
readonly violations: readonly {
|
|
93
|
+
readonly path: string;
|
|
94
|
+
readonly matchedRule: string;
|
|
95
|
+
}[];
|
|
96
|
+
};
|
|
78
97
|
export type DistVersionComparison = {
|
|
79
98
|
dist: string | null;
|
|
80
99
|
source: string;
|
|
@@ -229,6 +248,14 @@ export type DoctorOptions = {
|
|
|
229
248
|
* `process.cwd()`.
|
|
230
249
|
*/
|
|
231
250
|
codegraphManagedPathProbe?: () => CodegraphManagedPathInfo | null;
|
|
251
|
+
/**
|
|
252
|
+
* Optional override for the `capability:codegraph-exclude-integrity`
|
|
253
|
+
* check. Returns the integrity report, or `null` when codegraph is
|
|
254
|
+
* not initialized in the inspected root (nothing to reconcile). When
|
|
255
|
+
* omitted, the check inspects `process.cwd()`. Throwing is allowed
|
|
256
|
+
* and reported as a non-blocking warning.
|
|
257
|
+
*/
|
|
258
|
+
codegraphIntegrityProbe?: () => CodegraphExcludeIntegrityProbe | null;
|
|
232
259
|
skillPresenceProbe?: () => DoctorSkillPresence | null;
|
|
233
260
|
skillPresenceFreshnessThresholdMs?: number;
|
|
234
261
|
statusLineInstalledProbe?: () => boolean;
|