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
@@ -0,0 +1,471 @@
1
+ // src/services/codegraph/codegraph-index-integrity.ts
2
+ //
3
+ // Slice-001 of `2026-09-16-codegraph-index-integrity` — the READ-ONLY
4
+ // integrity report for the two index defects the exclude gate cannot see.
5
+ // Shared by `peaks codegraph status` and the
6
+ // `capability:codegraph-index-integrity` doctor check.
7
+ //
8
+ // The exclude gate (`codegraph-exclude-integrity.ts`) answers "do the
9
+ // config's `exclude` rules block a tracked source file". It runs the
10
+ // `include` filter FIRST and only reconciles `exclude` against the
11
+ // survivors, and it never looks at the index contents. So two classes of
12
+ // defect are structurally invisible to it:
13
+ //
14
+ // ① INCLUDE-AXIS GAP — a git-tracked file whose extension the upstream
15
+ // extractor supports, but which the config's `include` globs do not
16
+ // admit, is silently absent from the index. Upstream's default
17
+ // `include` template has no `**/*.mjs` / `**/*.cjs`, so every `.mjs`
18
+ // in a repo is dropped while `status` still says the index is fine.
19
+ //
20
+ // ② STALENESS — the index holds `files` rows for paths that no longer
21
+ // exist on disk (deleted or renamed upstream). Incremental `index`
22
+ // never deletes them, so the graph keeps answering about files that
23
+ // are gone.
24
+ //
25
+ // Both are DETECTION ONLY here. Repair is slice-002; this module NEVER
26
+ // writes `.codegraph/config.json` and never invokes the upstream binary.
27
+ // Its only IO is: read git's tracked-file list, read the config, read the
28
+ // index's `files` table, and probe path existence.
29
+ //
30
+ // "Read-only" is exact, not approximate: the index db is opened
31
+ // `readonly: true` and its bytes/mtime are never written, but a read-only
32
+ // open of a WAL database still creates/updates SQLite's own
33
+ // `codegraph.db-shm` / `codegraph.db-wal` sidecars in `.codegraph/`
34
+ // (gitignored, untracked, and required by SQLite itself). See
35
+ // `readIndexedFilePaths`.
36
+ //
37
+ // SEVERITY (user decision 2026-09-16, option C): a detected gap is
38
+ // ADVISORY by default — it reports as a warning and the command exits 0,
39
+ // so upgrading peaks-loop cannot red-light a downstream project's CI. A
40
+ // project may opt in to blocking with `PEAKS_CODEGRAPH_INDEX_STRICT=1`.
41
+ // "Could not evaluate" is NOT advisory: it has its own exit code, because
42
+ // a gate that cannot read its input must never report "fine".
43
+ //
44
+ // Genericity (binding): no hardcoded extension list, no hardcoded rule
45
+ // names, no project-specific paths. "Which files would upstream ingest" is
46
+ // answered by calling upstream's OWN `detectLanguage` + `isLanguageSupported`
47
+ // — the same two functions `extraction/index.js` calls — so this gate
48
+ // tracks upstream instead of drifting from a list we would have to maintain.
49
+ import { createRequire } from 'node:module';
50
+ import { existsSync, readdirSync } from 'node:fs';
51
+ import { dirname, join } from 'node:path';
52
+ import Database from 'better-sqlite3';
53
+ import { normalizePath } from '../../shared/path-utils.js';
54
+ import { CODEGRAPH_DB_NAME, CODEGRAPH_DIR_NAME } from './codegraph-service.js';
55
+ import { CODEGRAPH_CONFIG_FILENAME, filterAdmittedTrackedFiles, resolveSharedConfig, resolveSharedTrackedFiles } from './codegraph-exclude-reconciler.js';
56
+ /**
57
+ * Exit code `peaks codegraph status` uses when the index itself is
58
+ * incomplete or stale — distinct from `CODEGRAPH_INTEGRITY_EXIT_CODE` (74,
59
+ * the exclude-rule gate) and from `CODEGRAPH_INIT_CONFLICT_EXIT_CODE` (73).
60
+ *
61
+ * Why distinct rather than reusing 74: the two gates have different
62
+ * REMEDIATIONS. 74 means "these `exclude` rules must be dropped", and a CI
63
+ * job keyed on 74 already knows to run `repair-exclude`. 75 means "the
64
+ * index content does not match the repository — a supported tracked file
65
+ * is missing from it, or it holds rows for files that are gone". Folding
66
+ * that into 74 would silently re-point an existing consumer at a different
67
+ * fix. When BOTH gates fire, 74 wins (the exclude gap is the upstream
68
+ * cause; repairing it and rebuilding also clears staleness).
69
+ *
70
+ * NOTE (2026-09-16 policy change): 75 is only reachable in STRICT mode.
71
+ * See `isCodegraphIndexStrictMode`.
72
+ */
73
+ export const CODEGRAPH_INDEX_INTEGRITY_EXIT_CODE = 75;
74
+ /**
75
+ * Exit code for "the index axis could not be measured at all" — the db is
76
+ * present but unreadable, or its schema no longer has the `files` table
77
+ * (an upstream bump), or `git ls-files` / the config read failed.
78
+ *
79
+ * Distinct from BOTH 0 and 75 on purpose, and it applies in every mode
80
+ * including the advisory default:
81
+ *
82
+ * - 0 would say "I checked and it is fine". It does not.
83
+ * - 75 would say "I checked and the index does not cover the
84
+ * repository", which asserts a measurement that never happened.
85
+ *
86
+ * Why it is not merely advisory (the user's option C covers `gap`, not
87
+ * this): option C exists so that upgrading peaks-loop cannot turn a
88
+ * *healthy* project red — the three downstream triggers it names (a
89
+ * tracked `.mjs`, an un-purged dead row, a deliberately narrowed
90
+ * `include`) are all `gap`-class. "Could not read my own input" is never a
91
+ * healthy-project condition: it requires `.codegraph/codegraph.db` to be
92
+ * present but unreadable, and `.codegraph/` is gitignored, so a downstream
93
+ * CI checkout normally has no index at all and reports `not-applicable`
94
+ * (exit 0, no output). This is the exact failure class
95
+ * `codegraph-exclude-reconciler.ts:76-82` documents as the reason that
96
+ * module exists.
97
+ */
98
+ export const CODEGRAPH_INDEX_UNEVALUABLE_EXIT_CODE = 76;
99
+ /**
100
+ * The one command that repairs what this axis detects (slice-002). Exported
101
+ * as a constant rather than spelled out at each site so the human line, the
102
+ * doctor message and the JSON `nextActions` cannot name three different
103
+ * things: the renderer below, the doctor check and the CLI envelope all
104
+ * interpolate this value.
105
+ *
106
+ * It repairs BOTH axes in one run — `include` normalization, `exclude` rule
107
+ * drops against the normalized include, then a FORCED index rebuild (the
108
+ * only upstream path that drops rows for files deleted in an earlier
109
+ * commit; see `CodegraphExcludeRepairOptions.reindex`).
110
+ */
111
+ export const CODEGRAPH_REPAIR_INDEX_COMMAND = 'peaks codegraph repair-index --project <root>';
112
+ /**
113
+ * Opt-in switch for the user's option C decision (advisory by default,
114
+ * blocking on request). Set `PEAKS_CODEGRAPH_INDEX_STRICT=1` (or `true`)
115
+ * to make a detected index gap block: `status` exits 75 and the doctor
116
+ * check loses its `severity: 'warning'` tag and flips the doctor exit code.
117
+ *
118
+ * Why an environment variable rather than a config key or a CLI flag:
119
+ *
120
+ * - ONE mechanism covers both consumers. `peaks codegraph status` and
121
+ * `peaks doctor` are separate command surfaces; a CLI flag would have
122
+ * to be threaded through the doctor's option plumbing as well, and the
123
+ * default doctor probe takes no arguments.
124
+ * - CI is where the need lives, and CI sets environment variables.
125
+ * - A config key would have to live in the gitignored
126
+ * `.codegraph/config.json` (upstream's file, which this slice must
127
+ * never write) — so it would not survive a clone and could not
128
+ * configure a CI job at all.
129
+ * - It writes no state, so it does not weaken the read-only contract.
130
+ *
131
+ * Discoverability: the advisory warning text names this variable
132
+ * verbatim, so an operator who wants blocking is told how to get it at
133
+ * the moment they see the finding.
134
+ */
135
+ export const CODEGRAPH_INDEX_STRICT_ENV_VAR = 'PEAKS_CODEGRAPH_INDEX_STRICT';
136
+ /** True when the project opted in to blocking index-gap verdicts. */
137
+ export function isCodegraphIndexStrictMode(env = process.env) {
138
+ return env[CODEGRAPH_INDEX_STRICT_ENV_VAR] === '1' || env[CODEGRAPH_INDEX_STRICT_ENV_VAR] === 'true';
139
+ }
140
+ /**
141
+ * Fold the inspected report (null when there is no index to inspect) and
142
+ * the caught failure (null when nothing threw) into one verdict. This is
143
+ * the single place the four outcomes are distinguished, so a caller
144
+ * cannot re-derive them differently.
145
+ */
146
+ export function resolveCodegraphIndexIntegrityVerdict(report, warning) {
147
+ if (warning !== null) {
148
+ return 'not-evaluated';
149
+ }
150
+ if (report === null) {
151
+ return 'not-applicable';
152
+ }
153
+ return report.gap ? 'gap' : 'clean';
154
+ }
155
+ /**
156
+ * The exit code the index axis contributes, or `null` when it
157
+ * contributes none (clean / not-applicable → the caller's exit code is
158
+ * left alone).
159
+ */
160
+ export function codegraphIndexIntegrityExitCode(verdict, strict) {
161
+ if (verdict === 'not-evaluated') {
162
+ return CODEGRAPH_INDEX_UNEVALUABLE_EXIT_CODE;
163
+ }
164
+ if (verdict === 'gap' && strict) {
165
+ return CODEGRAPH_INDEX_INTEGRITY_EXIT_CODE;
166
+ }
167
+ return null;
168
+ }
169
+ /** How many offending paths we name per axis on the human path. */
170
+ const MAX_REPORTED_PATHS = 10;
171
+ /**
172
+ * Pure fold over already-resolved data. Both axes are set differences, so
173
+ * neither can be satisfied by a self-consistency assertion: axis ① compares
174
+ * git's tracked set against the `include` matcher, axis ② compares the
175
+ * index's own rows against the filesystem.
176
+ */
177
+ export function inspectCodegraphIndexIntegrityFrom(input) {
178
+ // Reuse the exclude reconciler's matcher so the two axes cannot disagree
179
+ // about what `include` admits (AC6) — this is the same function the
180
+ // exclude gate filters with, not a second implementation of it.
181
+ const admitted = filterAdmittedTrackedFiles(input.trackedFiles, input.include);
182
+ const admittedSet = new Set(admitted);
183
+ // The `include` filter is a path test, not an extension test, so a file
184
+ // it drops could be dropped for a directory reason rather than an
185
+ // extension reason. Only the extension axis is this gate's business:
186
+ // upstream would not ingest a markdown file either way, and reporting it
187
+ // as a gap would be a false positive.
188
+ const supportedTracked = input.trackedFiles
189
+ .map((file) => normalizePath(file))
190
+ .filter((file) => input.supportsPath(file));
191
+ const includeGap = supportedTracked.filter((file) => !admittedSet.has(file));
192
+ const deadRows = input.indexedPaths
193
+ .map((indexedPath) => normalizePath(indexedPath))
194
+ .filter((indexedPath) => !input.pathExists(indexedPath));
195
+ return {
196
+ configPath: input.configPath,
197
+ databasePath: input.databasePath,
198
+ gap: includeGap.length > 0 || deadRows.length > 0,
199
+ trackedSourceCount: supportedTracked.length,
200
+ admittedTrackedCount: supportedTracked.length - includeGap.length,
201
+ includeGap,
202
+ indexedFileCount: input.indexedPaths.length,
203
+ deadRows
204
+ };
205
+ }
206
+ let cachedUpstreamGrammars = null;
207
+ /**
208
+ * Load upstream's own `grammars` module and return the two functions
209
+ * `extraction/index.js` itself calls to decide whether to parse a file.
210
+ *
211
+ * Why reach into upstream's internals rather than ship a list: a hardcoded
212
+ * extension list drifts from upstream silently, and a list derived from
213
+ * `EXTENSION_MAP` would still be a re-derivation of a decision upstream
214
+ * already implements. Calling the decision is exact by construction. The
215
+ * module path is resolved from the package's own `package.json` (the same
216
+ * seam `codegraph-service.ts` uses for the binary), so it follows whichever
217
+ * `@colbymchenry/codegraph` instance this install actually runs.
218
+ *
219
+ * Node's `require` cache makes the second and later loads free; the module
220
+ * body only defines tables and functions (grammar WASM loading is a
221
+ * separate, explicitly-invoked `initGrammars`).
222
+ */
223
+ function loadUpstreamGrammars() {
224
+ if (cachedUpstreamGrammars === null) {
225
+ const require = createRequire(import.meta.url);
226
+ const packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json');
227
+ const grammarsPath = join(dirname(packageJsonPath), 'dist', 'extraction', 'grammars.js');
228
+ cachedUpstreamGrammars = require(grammarsPath);
229
+ }
230
+ return cachedUpstreamGrammars;
231
+ }
232
+ /** The upstream decision, verbatim: detect the language, then ask if it has a grammar. */
233
+ export function upstreamSupportsPath(filePath) {
234
+ const grammars = loadUpstreamGrammars();
235
+ return grammars.isLanguageSupported(grammars.detectLanguage(filePath));
236
+ }
237
+ /* ──────────────────────────────────────────────────────────────────────
238
+ * Boundary adapters — READ ONLY
239
+ * ────────────────────────────────────────────────────────────────────── */
240
+ /**
241
+ * The project-relative paths recorded in the index's `files` table.
242
+ *
243
+ * Opened `readonly: true` so the gate can never mutate the index's
244
+ * CONTENT, and `fileMustExist: true` so a missing/not-yet-initialized db
245
+ * throws instead of silently creating an empty one (which would report a
246
+ * 100% stale index).
247
+ *
248
+ * Read-only, exactly stated: opening a WAL database (`journal_mode = wal`
249
+ * is what upstream `dist/db/index.js` sets, and what this repo's own index
250
+ * uses) read-only makes SQLite create/update the gitignored
251
+ * `codegraph.db-shm` / `codegraph.db-wal` sidecars next to it, because a
252
+ * read-only WAL reader still needs the shared-memory index. The db file
253
+ * itself is never opened for writing (a write through this handle returns
254
+ * `SQLITE_READONLY`) and its bytes and mtime are unchanged. The sidecars
255
+ * are ignored by `.codegraph/.gitignore`, so no tracked repo state moves.
256
+ *
257
+ * Throws when the schema has no `files` table — an upstream schema change
258
+ * must be loud, not a silent zero-row pass.
259
+ */
260
+ function readIndexedFilePaths(projectRoot) {
261
+ const databasePath = join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_DB_NAME);
262
+ let db;
263
+ try {
264
+ db = new Database(databasePath, { readonly: true, fileMustExist: true });
265
+ }
266
+ catch (error) {
267
+ // sqlite's own message ("unable to open database file") does not name
268
+ // the path, and this text reaches the operator as the `status` warning
269
+ // line. Name it, matching the config reader's convention.
270
+ throw new Error(`codegraph index ${databasePath}: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ try {
273
+ // The SELECT is inside the `try` but OUTSIDE a `catch`, deliberately:
274
+ // a query failure is not an open failure (upstream schema drift gives
275
+ // "no such table: files"), and sqlite's message for it names neither
276
+ // the database nor the cause. Left raw it reaches the operator as
277
+ // `no such table: files` with no indication of WHICH file, so it is
278
+ // wrapped with the same path context the open failure carries.
279
+ return queryIndexedPaths(db, databasePath);
280
+ }
281
+ finally {
282
+ db.close();
283
+ }
284
+ }
285
+ function queryIndexedPaths(db, databasePath) {
286
+ let rows;
287
+ try {
288
+ rows = db.prepare('SELECT path FROM files').all();
289
+ }
290
+ catch (error) {
291
+ throw new Error(`codegraph index ${databasePath}: ${error instanceof Error ? error.message : String(error)}`);
292
+ }
293
+ return rows.map((row) => normalizePath(row.path));
294
+ }
295
+ /**
296
+ * Project-relative index path → does that file still exist on disk?
297
+ *
298
+ * THE AUTHORITY. Whatever else changes around it, this is the only
299
+ * predicate that decides "absent from disk", and it is preserved verbatim
300
+ * so the answer cannot drift.
301
+ *
302
+ * Two cheaper predicates are WRONG here, and both have been proposed:
303
+ *
304
+ * - `git ls-files` MEMBERSHIP is not this question. An orchestrator
305
+ * scratch script used it and reported 11 dead rows where the disk says
306
+ * 4: a project's own uncommitted-but-present files are absent from
307
+ * `git ls-files`, so a tracked-only predicate calls live files dead.
308
+ * The predicate is "absent from disk", full stop.
309
+ * - A directory walk that answers `true` for every name `readdir`
310
+ * reports is not this question either: `readdir` lists a BROKEN
311
+ * symlink, and `existsSync` on it is false.
312
+ */
313
+ function codegraphIndexPathExists(projectRoot, projectRelativePath) {
314
+ return existsSync(join(projectRoot, projectRelativePath));
315
+ }
316
+ /**
317
+ * Per-call, bounded replacement for the per-row `existsSync` above.
318
+ *
319
+ * WHY: the fold calls `pathExists` once per index row (`inspectCodegraph
320
+ * IndexIntegrityFrom`), so the gate is O(rows) full-path stats at ~13.5 µs
321
+ * each — 35 ms at 1.2k rows, 1,923 ms at 100x. It is unbounded in the size
322
+ * of the index. What is replaced is the COST, not the authority: a row is
323
+ * confirmed PRESENT by looking its basename up in one cached listing of its
324
+ * parent directory, so the number of filesystem calls is bounded by the
325
+ * number of DISTINCT DIRECTORIES the index names — which is bounded by the
326
+ * repository, not by the index.
327
+ *
328
+ * A listing may only answer `true`, and only for an entry whose dirent is
329
+ * positively a plain file or a plain directory. For exactly those entries,
330
+ * "the name is in `readdir`" and "`existsSync` on the joined path is true"
331
+ * cannot disagree. Everything else falls through to
332
+ * `codegraphIndexPathExists` — the old implementation, verbatim — so the
333
+ * classes this cache cannot decide are decided exactly as they always were:
334
+ *
335
+ * - a symlink or junction entry (`isSymbolicLink()`, Windows included:
336
+ * a junction reports `isDirectory() === false`), because a BROKEN link
337
+ * is listed by `readdir` and is absent from disk;
338
+ * - a case-differing path on a case-insensitive filesystem — the listing
339
+ * carries the on-disk spelling, so `SRC/OK.TS` misses and the stat
340
+ * decides (true on Windows, false on Linux, exactly as before);
341
+ * - a path naming a directory, or any non-file special entry;
342
+ * - a parent that is missing, unreadable, or not a directory at all
343
+ * (`readdir` throws → `null`, memoized so the failure costs one call);
344
+ * - a trailing-slash or empty row, and a project root that does not exist.
345
+ *
346
+ * The cache lives for ONE `inspectCodegraphIndexIntegrity` call, so a
347
+ * second command re-reads the tree and cannot be served a stale listing.
348
+ * Within a call, a listing taken before a concurrent delete is the only
349
+ * divergence from the old code — both implementations race the filesystem,
350
+ * and neither holds a snapshot.
351
+ */
352
+ export function createCodegraphIndexPathExists() {
353
+ const listings = new Map();
354
+ const listDirectory = (absoluteDirectory) => {
355
+ const cached = listings.get(absoluteDirectory);
356
+ if (cached !== undefined) {
357
+ return cached;
358
+ }
359
+ let listing;
360
+ try {
361
+ listing = new Set(readdirSync(absoluteDirectory, { withFileTypes: true })
362
+ .filter((entry) => entry.isFile() || entry.isDirectory())
363
+ .map((entry) => entry.name));
364
+ }
365
+ catch {
366
+ listing = null;
367
+ }
368
+ listings.set(absoluteDirectory, listing);
369
+ return listing;
370
+ };
371
+ return (projectRoot, projectRelativePath) => {
372
+ // Split at the LAST separator, so `parent` + `basename` re-join to the
373
+ // same path `join(projectRoot, projectRelativePath)` produces after
374
+ // normalization — including repeated separators.
375
+ const separatorIndex = projectRelativePath.lastIndexOf('/');
376
+ const basename = projectRelativePath.slice(separatorIndex + 1);
377
+ if (basename.length > 0) {
378
+ const parent = separatorIndex === -1 ? '' : projectRelativePath.slice(0, separatorIndex);
379
+ const listing = listDirectory(join(projectRoot, parent));
380
+ if (listing !== null && listing.has(basename)) {
381
+ return true;
382
+ }
383
+ }
384
+ return codegraphIndexPathExists(projectRoot, projectRelativePath);
385
+ };
386
+ }
387
+ /**
388
+ * Read-only entry point: resolve git's tracked files, the config's
389
+ * `include` globs and the index's own rows from disk, then fold them into
390
+ * one report. Throws (never silently degrades) when the project is not a
391
+ * git work tree, the config is missing/malformed, or the index is absent —
392
+ * callers that must stay alive (`status`, doctor) catch and surface it.
393
+ */
394
+ export function inspectCodegraphIndexIntegrity(projectRoot, deps = {}) {
395
+ const readIndexed = deps.readIndexedPaths ?? readIndexedFilePaths;
396
+ const supports = deps.supportsPath ?? upstreamSupportsPath;
397
+ // Perf audit F1/D2: one bounded resolver per call, so the directory
398
+ // listings are shared by every row of THIS inspection and are re-read by
399
+ // the next one. A caller that injects `pathExists` keeps its own.
400
+ const exists = deps.pathExists ?? createCodegraphIndexPathExists();
401
+ // Already-read inputs win; otherwise read them here exactly as before.
402
+ // (A caller that also runs the exclude axis should pass
403
+ // `readCodegraphProjectInputs(projectRoot)` to both — see the deps type.)
404
+ // Only a read-marked value can win: an unmarked one throws here rather
405
+ // than being mistaken for a real read (code review R4-1).
406
+ const config = resolveSharedConfig(deps.config, projectRoot);
407
+ const trackedFiles = resolveSharedTrackedFiles(deps.trackedFiles, projectRoot);
408
+ return inspectCodegraphIndexIntegrityFrom({
409
+ configPath: join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME),
410
+ databasePath: join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_DB_NAME),
411
+ trackedFiles,
412
+ include: config.include,
413
+ indexedPaths: readIndexed(projectRoot),
414
+ supportsPath: supports,
415
+ pathExists: (projectRelativePath) => exists(projectRoot, projectRelativePath)
416
+ });
417
+ }
418
+ /**
419
+ * Human-readable detail lines for a gapped report, grouped by axis so an
420
+ * operator can tell which defect they have (and whether they have both).
421
+ *
422
+ * Returns an empty array for a clean report — the caller decides whether
423
+ * "clean" is worth printing at all.
424
+ *
425
+ * `blocking` is the user's option C switch, and it changes the TAG, not
426
+ * the finding: advisory (the default) prints `[WARN]` and the command
427
+ * exits 0; strict prints `[FAIL]` and the command exits 75. The verdict
428
+ * word is chosen here rather than by the caller so the two channels
429
+ * cannot disagree about the same report.
430
+ *
431
+ * The remediation sentence NAMES the real command (`peaks codegraph
432
+ * repair-index`). Slice-001 deliberately named none, because naming a
433
+ * command that does not exist yet reproduces the exact failure this gate
434
+ * exists to prevent (an operator following a hint into "command not
435
+ * found"). Slice-002 shipped that command, so the obligation recorded in
436
+ * slice-001's design decision 4 is discharged here — and the name is
437
+ * exported as a constant so the renderer, the doctor message and the
438
+ * error envelope cannot drift from the command that is actually
439
+ * registered.
440
+ */
441
+ export function renderCodegraphIndexIntegrityLines(report, blocking) {
442
+ if (!report.gap) {
443
+ return [];
444
+ }
445
+ const lines = [
446
+ `${blocking ? '[FAIL]' : '[WARN]'} codegraph index does not cover the repository: ${report.includeGap.length} supported tracked file(s) are not admitted by the config's include globs, and ${report.deadRows.length} index row(s) point at files that no longer exist.`
447
+ ];
448
+ if (report.includeGap.length > 0) {
449
+ lines.push(` include gap: ${report.admittedTrackedCount} of ${report.trackedSourceCount} extractor-supported tracked file(s) are admitted by include`);
450
+ for (const filePath of report.includeGap.slice(0, MAX_REPORTED_PATHS)) {
451
+ lines.push(` not admitted: ${filePath}`);
452
+ }
453
+ if (report.includeGap.length > MAX_REPORTED_PATHS) {
454
+ lines.push(` … and ${report.includeGap.length - MAX_REPORTED_PATHS} more not-admitted file(s)`);
455
+ }
456
+ }
457
+ if (report.deadRows.length > 0) {
458
+ lines.push(` stale rows: ${report.deadRows.length} of ${report.indexedFileCount} indexed file(s) are gone from disk`);
459
+ for (const filePath of report.deadRows.slice(0, MAX_REPORTED_PATHS)) {
460
+ lines.push(` stale: ${filePath}`);
461
+ }
462
+ if (report.deadRows.length > MAX_REPORTED_PATHS) {
463
+ lines.push(` … and ${report.deadRows.length - MAX_REPORTED_PATHS} more stale row(s)`);
464
+ }
465
+ }
466
+ if (!blocking) {
467
+ lines.push(` advisory: this does not fail the command. Set ${CODEGRAPH_INDEX_STRICT_ENV_VAR}=1 to make it block (exit ${CODEGRAPH_INDEX_INTEGRITY_EXIT_CODE}).`);
468
+ }
469
+ lines.push(` fix: run \`${CODEGRAPH_REPAIR_INDEX_COMMAND}\` — it appends the missing include pattern(s), re-checks the exclude rules against the widened list, and rebuilds the index from scratch (that rebuild is what drops the stale rows).`);
470
+ return lines;
471
+ }
@@ -37,6 +37,20 @@ export type CodegraphExecutionResult = {
37
37
  stderr: string;
38
38
  };
39
39
  export type CodegraphProcessRunner = (invocation: CodegraphInvocation) => Promise<CodegraphExecutionResult>;
40
+ /**
41
+ * Resolve `--project` to an existing directory, CANONICALIZED through
42
+ * `realpathSync.native`.
43
+ *
44
+ * Exported because the CLI's repair verbs used to hand-roll `resolve()` +
45
+ * `statSync().isDirectory()` and skip the canonicalization, so the config /
46
+ * backup paths they reported and wrote were alias-dependent: a symlinked or
47
+ * short-named project root produced paths naming a different directory from
48
+ * the one every other codegraph command (all of which spawn through
49
+ * `createCodegraphInvocation`, i.e. through this function) reports on.
50
+ * Callers that need the same root upstream will be spawned with must call
51
+ * this rather than re-implement it.
52
+ */
53
+ export declare function resolveProjectRoot(project: string): string;
40
54
  export declare function createCodegraphInvocation(options: CodegraphInvocationOptions): CodegraphInvocation;
41
55
  export declare function executeCodegraphInvocation(invocation: CodegraphInvocation, runner?: CodegraphProcessRunner): Promise<CodegraphExecutionResult>;
42
56
  /**
@@ -76,6 +90,46 @@ export type ResolvedCodegraphLocation = {
76
90
  * absolute data-dir path. Pure path computation; no fs IO.
77
91
  */
78
92
  export declare function resolveCodegraphProjectRoot(projectRoot: string): ResolvedCodegraphLocation;
93
+ /**
94
+ * Resolve `<projectRoot>/.codegraph/` and REFUSE it when the directory it
95
+ * actually names is not contained by the canonical project root. Returns the
96
+ * canonicalized directory on success.
97
+ *
98
+ * Slice-002 S12 (security R1). The H1 fix refuses a link planted at the
99
+ * backup FILE path, but the DIRECTORY that path lives in was never checked:
100
+ * with `<root>/.codegraph` as a junction (or a symlink) to another directory,
101
+ * every existing probe passed — `defaultCodegraphInitGuard` probes with
102
+ * `statSync`, which follows — and `applyCodegraphConfigRepair` rewrote
103
+ * `<linked>/config.json` and created `<linked>/config.json.bak`. That is a
104
+ * write outside the project, into a directory chosen by whoever committed
105
+ * the link, and `.codegraph/` has no gitignore coverage in a consumer
106
+ * project (the same argument that made H1 reachable). Its bound is real but
107
+ * not a containment argument: `applyCodegraphConfigRepair` parses and
108
+ * validates the target as a codegraph config before it writes anything, so
109
+ * the clean chain is cross-project config tampering rather than an arbitrary
110
+ * overwrite. Slice-001's audit recorded "all write targets are constants
111
+ * under `projectRoot`" as CLEAN; this check is what makes that true.
112
+ *
113
+ * The predicate is CONTAINMENT, deliberately not link-ness. A link that
114
+ * resolves to a directory still inside the project root is allowed, because
115
+ * refusing every link would fail closed on a state a user may legitimately
116
+ * have (a derived `.codegraph/` kept inside the repo). What is refused is a
117
+ * `.codegraph` that ESCAPES the root, which no legitimate layout needs.
118
+ *
119
+ * Both sides go through the repo's own canonicalization rather than a second
120
+ * hand-rolled comparison — `resolveProjectRoot`'s `realpathSync.native` (the
121
+ * M1 fix) and `isInsidePath` from `shared/path-utils.ts`. `realpath` is what
122
+ * makes a junction resolve at all (verified on Windows: `lstat` reports a
123
+ * junction as a symlink and `realpathSync.native` returns its target), and
124
+ * it is the normalization every codegraph invocation already spawns under.
125
+ *
126
+ * An ABSENT directory is returned as-is, not refused: nothing exists to
127
+ * contain yet, and the write this guard precedes cannot create it — a
128
+ * `<root>/.codegraph/config.json` whose directory does not resolve fails
129
+ * with ENOENT before anything is written. The read the writer does first
130
+ * would throw ENOENT anyway.
131
+ */
132
+ export declare function assertCodegraphDirContained(projectRoot: string): string;
79
133
  export type CodegraphInitGuardResult = {
80
134
  status: 'fresh';
81
135
  codegraphDir: string;
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module';
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
5
5
  import { getSessionId, getSessionDir } from '../session/index.js';
6
+ import { isInsidePath } from '../../shared/path-utils.js';
6
7
  const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
7
8
  const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
8
9
  const CODEGRAPH_EXECUTABLE = process.execPath;
@@ -35,7 +36,20 @@ function assertSupportedSubcommand(subcommand) {
35
36
  throw new Error(`Unsupported codegraph subcommand: ${subcommand}`);
36
37
  }
37
38
  }
38
- function resolveProjectRoot(project) {
39
+ /**
40
+ * Resolve `--project` to an existing directory, CANONICALIZED through
41
+ * `realpathSync.native`.
42
+ *
43
+ * Exported because the CLI's repair verbs used to hand-roll `resolve()` +
44
+ * `statSync().isDirectory()` and skip the canonicalization, so the config /
45
+ * backup paths they reported and wrote were alias-dependent: a symlinked or
46
+ * short-named project root produced paths naming a different directory from
47
+ * the one every other codegraph command (all of which spawn through
48
+ * `createCodegraphInvocation`, i.e. through this function) reports on.
49
+ * Callers that need the same root upstream will be spawned with must call
50
+ * this rather than re-implement it.
51
+ */
52
+ export function resolveProjectRoot(project) {
39
53
  const projectRoot = resolve(project);
40
54
  try {
41
55
  if (!statSync(projectRoot).isDirectory()) {
@@ -202,6 +216,75 @@ export function resolveCodegraphProjectRoot(projectRoot) {
202
216
  codegraphDir: join(projectRoot, CODEGRAPH_DIR_NAME)
203
217
  };
204
218
  }
219
+ /**
220
+ * Resolve `<projectRoot>/.codegraph/` and REFUSE it when the directory it
221
+ * actually names is not contained by the canonical project root. Returns the
222
+ * canonicalized directory on success.
223
+ *
224
+ * Slice-002 S12 (security R1). The H1 fix refuses a link planted at the
225
+ * backup FILE path, but the DIRECTORY that path lives in was never checked:
226
+ * with `<root>/.codegraph` as a junction (or a symlink) to another directory,
227
+ * every existing probe passed — `defaultCodegraphInitGuard` probes with
228
+ * `statSync`, which follows — and `applyCodegraphConfigRepair` rewrote
229
+ * `<linked>/config.json` and created `<linked>/config.json.bak`. That is a
230
+ * write outside the project, into a directory chosen by whoever committed
231
+ * the link, and `.codegraph/` has no gitignore coverage in a consumer
232
+ * project (the same argument that made H1 reachable). Its bound is real but
233
+ * not a containment argument: `applyCodegraphConfigRepair` parses and
234
+ * validates the target as a codegraph config before it writes anything, so
235
+ * the clean chain is cross-project config tampering rather than an arbitrary
236
+ * overwrite. Slice-001's audit recorded "all write targets are constants
237
+ * under `projectRoot`" as CLEAN; this check is what makes that true.
238
+ *
239
+ * The predicate is CONTAINMENT, deliberately not link-ness. A link that
240
+ * resolves to a directory still inside the project root is allowed, because
241
+ * refusing every link would fail closed on a state a user may legitimately
242
+ * have (a derived `.codegraph/` kept inside the repo). What is refused is a
243
+ * `.codegraph` that ESCAPES the root, which no legitimate layout needs.
244
+ *
245
+ * Both sides go through the repo's own canonicalization rather than a second
246
+ * hand-rolled comparison — `resolveProjectRoot`'s `realpathSync.native` (the
247
+ * M1 fix) and `isInsidePath` from `shared/path-utils.ts`. `realpath` is what
248
+ * makes a junction resolve at all (verified on Windows: `lstat` reports a
249
+ * junction as a symlink and `realpathSync.native` returns its target), and
250
+ * it is the normalization every codegraph invocation already spawns under.
251
+ *
252
+ * An ABSENT directory is returned as-is, not refused: nothing exists to
253
+ * contain yet, and the write this guard precedes cannot create it — a
254
+ * `<root>/.codegraph/config.json` whose directory does not resolve fails
255
+ * with ENOENT before anything is written. The read the writer does first
256
+ * would throw ENOENT anyway.
257
+ */
258
+ export function assertCodegraphDirContained(projectRoot) {
259
+ const canonicalRoot = resolveProjectRoot(projectRoot);
260
+ const codegraphDir = join(canonicalRoot, CODEGRAPH_DIR_NAME);
261
+ if (!existsSync(codegraphDir)) {
262
+ return codegraphDir;
263
+ }
264
+ let canonicalDir;
265
+ try {
266
+ canonicalDir = realpathSync.native(codegraphDir);
267
+ }
268
+ catch (error) {
269
+ // Containment cannot be VERIFIED, so it is not asserted. An opaque
270
+ // errno here would read as a filesystem glitch rather than the refusal
271
+ // it is.
272
+ const detail = error instanceof Error ? error.message : String(error);
273
+ throw new Error(`codegraph directory ${codegraphDir}: refusing to write through it — the directory could not ` +
274
+ `be canonicalized (${detail}), so containment under the project root cannot be verified.`);
275
+ }
276
+ // Equality is refused as well as "outside": a `.codegraph` resolving to the
277
+ // project root itself is technically contained, but it would place
278
+ // `config.json` and `config.json.bak` AT the root, overwriting two files
279
+ // that belong to the user.
280
+ if (canonicalDir === canonicalRoot || !isInsidePath(canonicalDir, canonicalRoot)) {
281
+ throw new Error(`codegraph directory ${codegraphDir}: refusing to write through it — it resolves to ` +
282
+ `${canonicalDir}, which is not inside the project root ${canonicalRoot}. A junction or ` +
283
+ 'symbolic link here redirects every codegraph config write outside the project. Remove ' +
284
+ 'the link (or point `peaks` at the project that owns that directory) and re-run.');
285
+ }
286
+ return canonicalDir;
287
+ }
205
288
  export class CodegraphInitConflictError extends Error {
206
289
  codegraphDir;
207
290
  code = 'CODEGRAPH_INIT_CONFLICT';