peaks-loop 4.0.50 → 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.
Files changed (127) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/baseline-commands.js +11 -1
  5. package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
  6. package/dist/cli/commands/codegraph-command-runtime.js +72 -0
  7. package/dist/cli/commands/codegraph-commands.d.ts +2 -11
  8. package/dist/cli/commands/codegraph-commands.js +173 -228
  9. package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
  10. package/dist/cli/commands/codegraph-status-command.js +299 -0
  11. package/dist/cli/commands/core/memory-command.js +6 -2
  12. package/dist/cli/commands/job-commands.js +121 -30
  13. package/dist/cli/commands/project-commands.js +13 -3
  14. package/dist/cli/commands/request-commands.js +19 -8
  15. package/dist/cli/commands/share-commands.js +85 -18
  16. package/dist/cli/commands/slice-commands.js +2 -2
  17. package/dist/services/artifacts/artifact-prerequisites.js +23 -1
  18. package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
  19. package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
  20. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
  21. package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
  22. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
  23. package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
  24. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
  25. package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
  26. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
  27. package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
  28. package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
  29. package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
  30. package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
  31. package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
  32. package/dist/services/codegraph/codegraph-service.d.ts +54 -0
  33. package/dist/services/codegraph/codegraph-service.js +84 -1
  34. package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
  35. package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
  36. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
  37. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
  38. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
  39. package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
  40. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  41. package/dist/services/doctor/doctor-service/types.d.ts +25 -0
  42. package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
  43. package/dist/services/ide/adapters/codex-adapter.js +1 -2
  44. package/dist/services/ide/adapters/cursor-adapter.js +1 -2
  45. package/dist/services/ide/adapters/hermes-adapter.js +1 -2
  46. package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
  47. package/dist/services/ide/adapters/qoder-adapter.js +1 -2
  48. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
  49. package/dist/services/ide/adapters/trae-adapter.js +1 -2
  50. package/dist/services/ide/adapters/zcode-adapter.js +0 -1
  51. package/dist/services/ide/ide-types.d.ts +0 -2
  52. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
  53. package/dist/services/memory/project-memory-service/index.d.ts +5 -3
  54. package/dist/services/memory/project-memory-service/index.js +2 -2
  55. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
  56. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
  57. package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
  58. package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
  59. package/dist/services/memory/project-memory-service/types.d.ts +86 -0
  60. package/dist/services/slice/slice-check-types.d.ts +1 -1
  61. package/dist/services/workspace/runtime-layout.d.ts +91 -0
  62. package/dist/services/workspace/runtime-layout.js +148 -0
  63. package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
  64. package/package.json +6 -6
  65. package/scripts/clean-dist.mjs +15 -3
  66. package/scripts/sync-version.mjs +26 -4
  67. package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
  68. package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
  69. package/skills/bee/peaks-prd/SKILL.md +4 -4
  70. package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
  71. package/skills/bee/peaks-prd/references/workflow.md +1 -1
  72. package/skills/bee/peaks-qa/SKILL.md +6 -6
  73. package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
  74. package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
  75. package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
  76. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
  77. package/skills/bee/peaks-rd/SKILL.md +2 -2
  78. package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
  79. package/skills/bee/peaks-rd/references/external-references.md +1 -1
  80. package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
  81. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
  82. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
  83. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
  84. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  85. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
  86. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  87. package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
  88. package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
  89. package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
  90. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
  91. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  92. package/skills/bee/peaks-sc/SKILL.md +1 -1
  93. package/skills/bee/peaks-security-audit/SKILL.md +3 -3
  94. package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
  95. package/skills/bee/peaks-txt/SKILL.md +3 -3
  96. package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
  97. package/skills/bee/peaks-ui/SKILL.md +1 -1
  98. package/skills/peaks-audit/SKILL.md +1 -1
  99. package/skills/peaks-code/SKILL.md +9 -9
  100. package/skills/peaks-code/references/context-governance.md +1 -1
  101. package/skills/peaks-code/references/dag-orchestrator.md +3 -4
  102. package/skills/peaks-code/references/external-references.md +1 -1
  103. package/skills/peaks-code/references/external-skill-invocation.md +2 -2
  104. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  105. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  106. package/skills/peaks-code/references/gstack-integration.md +1 -1
  107. package/skills/peaks-code/references/micro-cycle.md +1 -1
  108. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  109. package/skills/peaks-code/references/project-memory-loading.md +19 -1
  110. package/skills/peaks-code/references/project-scan-checklist.md +1 -1
  111. package/skills/peaks-code/references/resume-detection.md +1 -1
  112. package/skills/peaks-code/references/runbook.md +3 -3
  113. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  114. package/skills/peaks-code/references/startup-sequence.md +16 -16
  115. package/skills/peaks-code/references/step-11-memory-sediment.md +3 -3
  116. package/skills/peaks-code/references/sub-agent-dispatch.md +7 -6
  117. package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
  118. package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
  119. package/skills/peaks-code/references/worktree-governance.md +1 -1
  120. package/skills/peaks-final-review/SKILL.md +3 -3
  121. package/skills/peaks-ide/references/audit-log-helper.md +5 -4
  122. package/skills/peaks-resume/SKILL.md +1 -1
  123. package/skills/peaks-slice-decompose/SKILL.md +4 -4
  124. package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
  125. package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
  126. package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
  127. package/skills/peaks-solo/SKILL.md +1 -2
@@ -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
- // S1 computes `rulesToRemove`; this module applies it. Two callers are
6
- // allowed to reach it and nothing else is:
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), 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
+ // 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 `codegraph-exclude-integrity.ts` and never write.
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 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.
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
- // - The rewrite itself goes through a same-directory temp file plus
28
- // `renameSync`, so the config is never observed half-written.
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` changes.
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 { assertStringArray, CODEGRAPH_CONFIG_FILENAME } from './codegraph-exclude-reconciler.js';
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 { 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
- }
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 exclude list, drop the rules that block
180
- * tracked source files, and rebuild the index so the recovered files
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
- let integrity;
138
+ const forcedRebuild = options.reindex === 'force';
139
+ let trackedFiles;
140
+ let config;
141
+ let includePlan;
196
142
  try {
197
- integrity = inspectCodegraphExcludeIntegrity(projectRoot);
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
- 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)}`);
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
- 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
- };
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: true,
235
- rulesRemoved: outcome.removedRules,
236
- filesRecovered: integrity.excludedTrackedCount,
237
- trackedSourceCount: integrity.trackedSourceCount,
238
- configPath: outcome.configPath,
239
- backupPath: outcome.backupPath
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. Rebuild
247
- // it now — that is the whole point of repairing at init time.
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({ subcommand: 'index', project: projectRoot, quiet: true }), runner);
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
- 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>\`.`
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
- warning: `codegraph exclude repaired (${outcome.removedRules.length} rule(s) removed) but the follow-up index could not run: ${errorMessage(error)}`
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[];