peaks-loop 4.0.51 → 4.0.52
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 +20 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/baseline-commands.js +11 -1
- package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
- package/dist/cli/commands/codegraph-command-runtime.js +72 -0
- package/dist/cli/commands/codegraph-commands.d.ts +2 -11
- package/dist/cli/commands/codegraph-commands.js +173 -228
- package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
- package/dist/cli/commands/codegraph-status-command.js +299 -0
- package/dist/cli/commands/core/memory-command.js +6 -2
- package/dist/cli/commands/job-commands.js +121 -30
- package/dist/cli/commands/project-commands.js +13 -3
- package/dist/cli/commands/request-commands.js +19 -8
- package/dist/cli/commands/slice-commands.js +2 -2
- package/dist/services/artifacts/artifact-prerequisites.js +23 -1
- package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
- package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
- package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
- package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
- package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
- package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
- package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
- package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
- package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
- package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
- package/dist/services/codegraph/codegraph-service.d.ts +54 -0
- package/dist/services/codegraph/codegraph-service.js +84 -1
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
- package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/types.d.ts +25 -0
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
- package/dist/services/memory/project-memory-service/index.d.ts +5 -3
- package/dist/services/memory/project-memory-service/index.js +2 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
- package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
- package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
- package/dist/services/memory/project-memory-service/types.d.ts +86 -0
- package/dist/services/slice/slice-check-types.d.ts +1 -1
- package/dist/services/workspace/runtime-layout.d.ts +91 -0
- package/dist/services/workspace/runtime-layout.js +148 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
- package/package.json +6 -6
- package/scripts/clean-dist.mjs +15 -3
- package/scripts/sync-version.mjs +26 -4
- package/skills/bee/peaks-prd/SKILL.md +1 -1
- package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
- package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +3 -3
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/project-memory-loading.md +19 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +1 -1
|
@@ -1,184 +1,127 @@
|
|
|
1
1
|
// src/services/codegraph/codegraph-exclude-repair.ts
|
|
2
2
|
//
|
|
3
3
|
// Slice S2 of `2026-09-12-codegraph-exclude-integrity` — the WRITE path.
|
|
4
|
+
// WIDENED by slice-002 of `2026-09-16-codegraph-index-integrity` from one
|
|
5
|
+
// axis to BOTH config axes; the file name and the entry point's name are
|
|
6
|
+
// retained because three seams import them by name and the exclude axis is
|
|
7
|
+
// still the entry condition. See the ordering note below — the widening is
|
|
8
|
+
// not a second feature bolted on, it is what makes the exclude repair
|
|
9
|
+
// correct.
|
|
4
10
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
11
|
+
// The reconcilers compute; this module applies. Two callers are allowed to
|
|
12
|
+
// reach it and nothing else is:
|
|
7
13
|
//
|
|
8
14
|
// 1. `peaks codegraph init` — after a fresh upstream init (which
|
|
9
|
-
// always writes upstream's 99-rule default template
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
+
// always writes upstream's 99-rule default `exclude` template AND
|
|
16
|
+
// its 32-entry default `include` template), so a brand new clone /
|
|
17
|
+
// new machine ends up with a complete index instead of silently
|
|
18
|
+
// dropping tracked source files.
|
|
19
|
+
// 2. `peaks codegraph repair-exclude` (exclude axis, incremental
|
|
20
|
+
// rebuild) and `peaks codegraph repair-index` (both axes, FORCED
|
|
21
|
+
// rebuild) — the explicit repair for a workspace that was
|
|
22
|
+
// initialized before this shipped, or whose config drifted.
|
|
15
23
|
//
|
|
16
24
|
// `status` and the doctor check deliberately do NOT live here: they
|
|
17
|
-
// read
|
|
25
|
+
// read the integrity inspectors and never write.
|
|
26
|
+
//
|
|
27
|
+
// ── WHY THE INCLUDE AXIS IS PART OF *THIS* REPAIR ─────────────────────
|
|
28
|
+
//
|
|
29
|
+
// Slice-001's RD measured the reason with a real test failure: an
|
|
30
|
+
// `exclude` rule that blocks ONLY a file `include` already drops
|
|
31
|
+
// reconciles COMPLETELY CLEAN today — `include: ['**/*.ts']` with
|
|
32
|
+
// `exclude: ['**/tool.mjs']` reports `gap:false, rulesToRemove:[]` even
|
|
33
|
+
// though that rule is actively harmful. The moment `include` is widened
|
|
34
|
+
// (which is exactly what repairing the include axis does) the rule starts
|
|
35
|
+
// biting, and the exclude reconciliation is the only thing that can see it.
|
|
36
|
+
//
|
|
37
|
+
// So the ORDER IS LOAD-BEARING and is enforced structurally rather than by
|
|
38
|
+
// comment: `repairCodegraphExcludeFromProject` reconciles `exclude` against
|
|
39
|
+
// the NORMALIZED include list, never against the on-disk one. Reconciling
|
|
40
|
+
// first would bless a rule that the same run was about to make harmful —
|
|
41
|
+
// the repair would report success over a config it had just broken.
|
|
18
42
|
//
|
|
19
43
|
// Safety posture (the file being edited belongs to a THIRD-PARTY tool):
|
|
20
|
-
// - `rulesToRemove` comes from
|
|
21
|
-
// actually blocks at least one git-tracked source
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
44
|
+
// - `rulesToRemove` comes from the exclude reconciler, which only ever
|
|
45
|
+
// lists a rule that actually blocks at least one git-tracked source
|
|
46
|
+
// file AFTER include normalization. A rule that matches nothing
|
|
47
|
+
// tracked is never dropped.
|
|
48
|
+
// - `includePatternsToAdd` comes from the include reconciler, which only
|
|
49
|
+
// ever appends a bare-extension pattern for an extension upstream's
|
|
50
|
+
// extractor supports and its own template omits. User entries are
|
|
51
|
+
// never removed, reordered or rewritten.
|
|
52
|
+
// - Nothing is written when both lists are empty — the config bytes, and
|
|
53
|
+
// its mtime, are untouched.
|
|
25
54
|
// - The original bytes are copied to `config.json.bak` before the
|
|
26
|
-
// rewrite, so a rollback is byte-exact
|
|
27
|
-
//
|
|
28
|
-
// `renameSync
|
|
55
|
+
// rewrite, so a rollback is byte-exact *whenever that copy is THIS
|
|
56
|
+
// writer's own*: the backup is written through the same CSPRNG-named
|
|
57
|
+
// temp + `renameSync` as the config, and the write REFUSES a link at
|
|
58
|
+
// the backup path (see `writeConfigBackup`). The path is fixed and
|
|
59
|
+
// therefore guessable, so a repo that commits
|
|
60
|
+
// `.codegraph/config.json.bak` as a symlink or a hard link would
|
|
61
|
+
// otherwise redirect the copy into an arbitrary file — a
|
|
62
|
+
// write-what-where with attacker-chosen content, reachable with no
|
|
63
|
+
// explicit command. `.codegraph/` receives no gitignore coverage in a
|
|
64
|
+
// consumer project (`config.json.bak` matches neither the peaks-loop
|
|
65
|
+
// snippet nor upstream's own `.codegraph/.gitignore`), so both files
|
|
66
|
+
// are committable.
|
|
67
|
+
// - The DIRECTORY both paths live in is contained: `assertCodegraphDirContained`
|
|
68
|
+
// refuses when `<projectRoot>/.codegraph` resolves (junction or symlink)
|
|
69
|
+
// outside the canonical project root, so neither the read nor either
|
|
70
|
+
// write can be redirected into another project's `.codegraph/`
|
|
71
|
+
// (security R1). Containment is the predicate, not link-ness — see the
|
|
72
|
+
// function's own note on why an in-project link is allowed.
|
|
73
|
+
// - Both axes move in ONE rewrite through a same-directory temp file plus
|
|
74
|
+
// `renameSync`, so the config is never observed half-written, there is
|
|
75
|
+
// never a moment where `include` is widened but `exclude` still holds
|
|
76
|
+
// the rules the widened list just made harmful, and one backup covers
|
|
77
|
+
// both.
|
|
29
78
|
// - Every other key of the config survives verbatim, in its original
|
|
30
|
-
// position; only `exclude`
|
|
31
|
-
import { randomBytes } from 'node:crypto';
|
|
32
|
-
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
79
|
+
// position; only `include` and `exclude` change.
|
|
33
80
|
import { join } from 'node:path';
|
|
34
|
-
import {
|
|
81
|
+
import { CODEGRAPH_CONFIG_FILENAME, filterAdmittedTrackedFiles, readCodegraphProjectInputs, reconcileCodegraphExclude } from './codegraph-exclude-reconciler.js';
|
|
82
|
+
import { normalizeCodegraphInclude, upstreamUnnamedIncludeExtensions } from './codegraph-include-reconciler.js';
|
|
83
|
+
import { upstreamSupportsPath } from './codegraph-index-integrity.js';
|
|
35
84
|
import { CODEGRAPH_DIR_NAME, createCodegraphInvocation, executeCodegraphInvocation } from './codegraph-service.js';
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
}
|
|
85
|
+
import { applyCodegraphConfigRepair, CODEGRAPH_CONFIG_BACKUP_SUFFIX, repairCodegraphExclude, repairCodegraphInclude } from './codegraph-config-repair-writer.js';
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
// Public surface, unchanged
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
89
|
+
// Re-exported so every existing `codegraph-exclude-repair.js` import site
|
|
90
|
+
// keeps resolving: the extraction moved the definitions, not the contract.
|
|
91
|
+
export { applyCodegraphConfigRepair, CODEGRAPH_CONFIG_BACKUP_SUFFIX, repairCodegraphExclude, repairCodegraphInclude };
|
|
163
92
|
function errorMessage(error) {
|
|
164
93
|
return error instanceof Error ? error.message : String(error);
|
|
165
94
|
}
|
|
95
|
+
function configPathOf(projectRoot) {
|
|
96
|
+
return join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME);
|
|
97
|
+
}
|
|
166
98
|
function emptyRepairReport(warning) {
|
|
167
99
|
return {
|
|
168
100
|
applied: false,
|
|
169
101
|
rulesRemoved: [],
|
|
102
|
+
includePatternsAdded: [],
|
|
170
103
|
filesRecovered: 0,
|
|
104
|
+
includeFilesRecovered: 0,
|
|
105
|
+
includeAdmittedAfter: 0,
|
|
171
106
|
trackedSourceCount: 0,
|
|
172
107
|
configPath: '',
|
|
173
108
|
backupPath: null,
|
|
174
109
|
reindexed: false,
|
|
110
|
+
forcedRebuild: false,
|
|
175
111
|
warning
|
|
176
112
|
};
|
|
177
113
|
}
|
|
178
114
|
/**
|
|
179
|
-
* Reconcile `projectRoot`'s
|
|
180
|
-
*
|
|
181
|
-
* actually land in it.
|
|
115
|
+
* Reconcile `projectRoot`'s config against its tracked files — normalize
|
|
116
|
+
* `include`, then drop the `exclude` rules that block tracked source files
|
|
117
|
+
* — and rebuild the index so the recovered files actually land in it.
|
|
118
|
+
*
|
|
119
|
+
* ORDER IS LOAD-BEARING. `exclude` is reconciled against the NORMALIZED
|
|
120
|
+
* include list, never the on-disk one: an `exclude` rule that blocks only a
|
|
121
|
+
* file `include` drops reconciles clean today and starts biting the moment
|
|
122
|
+
* `include` is widened, so reconciling before normalizing would let this
|
|
123
|
+
* run report success over a config it had just made worse. See the module
|
|
124
|
+
* header.
|
|
182
125
|
*
|
|
183
126
|
* This is the ONE shared "after upstream init" self-heal: the CLI's
|
|
184
127
|
* fresh `peaks codegraph init`, the pre-dispatch preflight and the
|
|
@@ -192,75 +135,186 @@ function emptyRepairReport(warning) {
|
|
|
192
135
|
* the never-throwing `refreshCodegraphAfterSlice`.
|
|
193
136
|
*/
|
|
194
137
|
export async function repairCodegraphExcludeFromProject(projectRoot, runner, options = {}) {
|
|
195
|
-
|
|
138
|
+
const forcedRebuild = options.reindex === 'force';
|
|
139
|
+
let trackedFiles;
|
|
140
|
+
let config;
|
|
141
|
+
let includePlan;
|
|
196
142
|
try {
|
|
197
|
-
|
|
143
|
+
// Read once, in the one place that owns the readers. The exclude
|
|
144
|
+
// reconciler is then fed the NORMALIZED include list below, which is
|
|
145
|
+
// what makes the ordering guarantee structural: there is no code path
|
|
146
|
+
// here that reconciles against the on-disk include.
|
|
147
|
+
({ trackedFiles, config } = readCodegraphProjectInputs(projectRoot));
|
|
148
|
+
// INSIDE the guard on purpose. The adapter loads upstream's own
|
|
149
|
+
// `types.js` / `extraction/grammars.js` out of the installed package and
|
|
150
|
+
// throws if the pinned install is damaged or its `dist/` layout moved;
|
|
151
|
+
// this function's contract is "never throws", and the alternative is
|
|
152
|
+
// that `repair-exclude` / `repair-index` escape to the CLI's generic
|
|
153
|
+
// UNHANDLED_ERROR (and `init` prints FAILURE for an init that already
|
|
154
|
+
// succeeded). A failure here is reported as a warning like every other
|
|
155
|
+
// one.
|
|
156
|
+
includePlan = normalizeCodegraphInclude({
|
|
157
|
+
include: config.include,
|
|
158
|
+
candidateExtensions: upstreamUnnamedIncludeExtensions()
|
|
159
|
+
});
|
|
198
160
|
}
|
|
199
161
|
catch (error) {
|
|
200
162
|
return emptyRepairReport(`codegraph exclude reconcile skipped: ${errorMessage(error)}`);
|
|
201
163
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
164
|
+
// AFTER normalization, never before. `admitted` here is the normalized
|
|
165
|
+
// admission set, so a rule that the widening would make harmful is
|
|
166
|
+
// visible to this reconciliation in the SAME run that widens.
|
|
167
|
+
const reconcile = reconcileCodegraphExclude({
|
|
168
|
+
trackedFiles,
|
|
169
|
+
include: includePlan.include,
|
|
170
|
+
exclude: config.exclude
|
|
171
|
+
});
|
|
172
|
+
// The numerator is the normalized admission count, which the reconciliation
|
|
173
|
+
// just computed — it is not re-filtered here.
|
|
174
|
+
const includeAdmittedAfter = reconcile.trackedSourceCount;
|
|
175
|
+
// The denominator is a DIFFERENT measurement over the same file list: an
|
|
176
|
+
// extension decision per tracked file (`upstreamSupportsPath`, the same
|
|
177
|
+
// oracle the index axis uses), not a glob match. Two independent
|
|
178
|
+
// measurements can disagree, which is the whole point — a ratio whose
|
|
179
|
+
// numerator and denominator are the same expression cannot report the
|
|
180
|
+
// shortfall it exists to report. It is also cheap: no glob is compiled.
|
|
181
|
+
//
|
|
182
|
+
// This replaced a full `filterAdmittedTrackedFiles(trackedFiles,
|
|
183
|
+
// config.include)` pass whose only consumer was the on-disk "was N"
|
|
184
|
+
// trailer of a user-facing sentence. Perf measured that second pass at
|
|
185
|
+
// 11.73 ms of the repair entry's +14.13 ms (83 %), on ALL THREE automatic
|
|
186
|
+
// seams and both repair commands, including no-ops; the trailer was not
|
|
187
|
+
// worth it. Measured again here on this repo (2,104 tracked files x 32
|
|
188
|
+
// include rules, paired runs): the removed pass 9.78 ms median, this
|
|
189
|
+
// extension check 0.18 ms — and it reports the same 1,240 the status line
|
|
190
|
+
// reports as the denominator, which is the point of using one oracle.
|
|
191
|
+
const trackedSourceCount = trackedFiles.filter((file) => upstreamSupportsPath(file)).length;
|
|
192
|
+
// A1 (2026-09-17): the include axis' own file count, measured as a DELTA
|
|
193
|
+
// against the ON-DISK include list — `includeAdmittedAfter` is the count
|
|
194
|
+
// for the NORMALIZED list, so the difference is exactly the tracked files
|
|
195
|
+
// this run's appends newly admit. Never negative: `includePlan.include` is
|
|
196
|
+
// a superset of `config.include` and admission is monotone in the rule
|
|
197
|
+
// list, so the before-count cannot exceed the after-count.
|
|
198
|
+
//
|
|
199
|
+
// GATED on `includePlan.changed`, which is exact rather than an
|
|
200
|
+
// optimization: when nothing was appended the normalized list IS the
|
|
201
|
+
// on-disk list, so the delta is 0 without measuring anything. The pass
|
|
202
|
+
// this gate keeps off the hot path is the one slice-002 deleted for cost
|
|
203
|
+
// (see the `trackedSourceCount` note above) — the difference is that THIS
|
|
204
|
+
// pass runs only on the one run per project that actually widens
|
|
205
|
+
// `include`, where a repair is about to spend seconds rebuilding an index.
|
|
206
|
+
const includeFilesRecovered = includePlan.changed
|
|
207
|
+
? includeAdmittedAfter -
|
|
208
|
+
filterAdmittedTrackedFiles(trackedFiles, config.include).length
|
|
209
|
+
: 0;
|
|
210
|
+
const nothingToRepair = includePlan.addedPatterns.length === 0 && reconcile.rulesToRemove.length === 0;
|
|
211
|
+
const unchanged = {
|
|
212
|
+
applied: false,
|
|
213
|
+
rulesRemoved: [],
|
|
214
|
+
includePatternsAdded: [],
|
|
215
|
+
filesRecovered: 0,
|
|
216
|
+
includeFilesRecovered: 0,
|
|
217
|
+
includeAdmittedAfter,
|
|
218
|
+
trackedSourceCount,
|
|
219
|
+
configPath: configPathOf(projectRoot),
|
|
220
|
+
backupPath: null,
|
|
221
|
+
reindexed: false,
|
|
222
|
+
forcedRebuild: false,
|
|
223
|
+
warning: null
|
|
224
|
+
};
|
|
225
|
+
// Nothing to write AND no rebuild was asked for: the config bytes and
|
|
226
|
+
// their mtime are untouched, and no upstream process is spawned.
|
|
227
|
+
//
|
|
228
|
+
// A FORCED rebuild is exempt on purpose: `repair-index`'s contract is
|
|
229
|
+
// "make the index match the repository", and a clean reconciliation does
|
|
230
|
+
// NOT prove the index is complete — the gate's coverage verdict is
|
|
231
|
+
// admission-only (a file `include` admits that upstream skipped for
|
|
232
|
+
// `maxFileSize` or on an extraction error is not visible to it at all;
|
|
233
|
+
// slice-001 recorded that as a known limitation). So the operator who
|
|
234
|
+
// asked for a forced rebuild gets one.
|
|
235
|
+
if (nothingToRepair && !forcedRebuild) {
|
|
236
|
+
return unchanged;
|
|
220
237
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
238
|
+
let outcome = null;
|
|
239
|
+
if (!nothingToRepair) {
|
|
240
|
+
try {
|
|
241
|
+
outcome = applyCodegraphConfigRepair(projectRoot, {
|
|
242
|
+
rulesToRemove: reconcile.rulesToRemove,
|
|
243
|
+
includePatternsToAdd: includePlan.addedPatterns
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return emptyRepairReport(`codegraph exclude repair failed for ${configPathOf(projectRoot)}: ${errorMessage(error)}`);
|
|
248
|
+
}
|
|
249
|
+
// The plan said there was work and the writer found none (a config that
|
|
250
|
+
// changed underneath us, or a rule/pattern already applied). Report the
|
|
251
|
+
// read-side numbers and write nothing further.
|
|
252
|
+
if (!outcome.applied) {
|
|
253
|
+
return unchanged;
|
|
254
|
+
}
|
|
232
255
|
}
|
|
233
256
|
const base = {
|
|
234
|
-
applied:
|
|
235
|
-
rulesRemoved: outcome
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
applied: outcome !== null,
|
|
258
|
+
rulesRemoved: outcome?.removedRules ?? [],
|
|
259
|
+
includePatternsAdded: outcome?.addedIncludePatterns ?? [],
|
|
260
|
+
filesRecovered: reconcile.excludedTrackedCount,
|
|
261
|
+
includeFilesRecovered,
|
|
262
|
+
includeAdmittedAfter,
|
|
263
|
+
trackedSourceCount,
|
|
264
|
+
configPath: configPathOf(projectRoot),
|
|
265
|
+
backupPath: outcome?.backupPath ?? null
|
|
240
266
|
};
|
|
241
267
|
if (options.reindex === false) {
|
|
242
268
|
// The caller indexes immediately after this returns, so doing it
|
|
243
269
|
// here too would index the same tree twice for no extra coverage.
|
|
244
|
-
return { ...base, reindexed: false, warning: null };
|
|
270
|
+
return { ...base, reindexed: false, forcedRebuild: false, warning: null };
|
|
245
271
|
}
|
|
246
|
-
// The recovered files are on disk but not in the index yet
|
|
247
|
-
//
|
|
272
|
+
// The recovered files are on disk but not in the index yet, and (under
|
|
273
|
+
// `'force'`) rows for files that are gone may still be in it. Rebuild now
|
|
274
|
+
// — that is the whole point of repairing.
|
|
275
|
+
const repairedSummary = describeRepair(outcome);
|
|
248
276
|
try {
|
|
249
|
-
const result = await executeCodegraphInvocation(createCodegraphInvocation({
|
|
277
|
+
const result = await executeCodegraphInvocation(createCodegraphInvocation({
|
|
278
|
+
subcommand: 'index',
|
|
279
|
+
project: projectRoot,
|
|
280
|
+
quiet: true,
|
|
281
|
+
...(forcedRebuild ? { force: true } : {})
|
|
282
|
+
}), runner);
|
|
250
283
|
if (result.exitCode !== 0) {
|
|
284
|
+
// `forcedRebuild` reports what WAS ATTEMPTED, not what completed:
|
|
285
|
+
// upstream's `index --force` runs `cg.clear()` BEFORE `indexAll()`, so
|
|
286
|
+
// a forced run that failed has already deleted the rows. Hardcoding
|
|
287
|
+
// `false` here (as this did) made the envelope unable to distinguish
|
|
288
|
+
// "no forced purge ever ran" from "the forced purge ran and the
|
|
289
|
+
// rebuild then failed" — the one case where a JSON consumer most needs
|
|
290
|
+
// to know. `reindexed: false` carries "did not complete".
|
|
251
291
|
return {
|
|
252
292
|
...base,
|
|
253
293
|
reindexed: false,
|
|
254
|
-
|
|
294
|
+
forcedRebuild,
|
|
295
|
+
warning: `codegraph config repaired (${repairedSummary}) but the follow-up index failed (exit ${String(result.exitCode)}); run \`peaks codegraph index --project <root>\`.`
|
|
255
296
|
};
|
|
256
297
|
}
|
|
257
|
-
return { ...base, reindexed: true, warning: null };
|
|
298
|
+
return { ...base, reindexed: true, forcedRebuild, warning: null };
|
|
258
299
|
}
|
|
259
300
|
catch (error) {
|
|
301
|
+
// Same reasoning as the non-zero exit above: the invocation may have
|
|
302
|
+
// reached upstream's `clear()` before the failure.
|
|
260
303
|
return {
|
|
261
304
|
...base,
|
|
262
305
|
reindexed: false,
|
|
263
|
-
|
|
306
|
+
forcedRebuild,
|
|
307
|
+
warning: `codegraph config repaired (${repairedSummary}) but the follow-up index could not run: ${errorMessage(error)}`
|
|
264
308
|
};
|
|
265
309
|
}
|
|
266
310
|
}
|
|
311
|
+
// "2 exclude rule(s) removed, 5 include pattern(s) added" — the trailing
|
|
312
|
+
// half of every degraded-path warning above. Written once so the four
|
|
313
|
+
// warnings cannot drift from each other, and it names BOTH axes because
|
|
314
|
+
// either may be the one that changed.
|
|
315
|
+
function describeRepair(outcome) {
|
|
316
|
+
if (outcome === null || !outcome.applied) {
|
|
317
|
+
return 'nothing written';
|
|
318
|
+
}
|
|
319
|
+
return `${outcome.removedRules.length} exclude rule(s) removed, ${outcome.addedIncludePatterns.length} include pattern(s) added`;
|
|
320
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CodegraphIncludeNormalizePlan = {
|
|
2
|
+
readonly changed: boolean;
|
|
3
|
+
readonly include: readonly string[];
|
|
4
|
+
readonly addedPatterns: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export declare function normalizeCodegraphInclude(input: {
|
|
7
|
+
readonly include: readonly string[];
|
|
8
|
+
readonly candidateExtensions: readonly string[];
|
|
9
|
+
}): CodegraphIncludeNormalizePlan;
|
|
10
|
+
export declare function upstreamUnnamedIncludeExtensions(): readonly string[];
|