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
|
@@ -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
|
-
|
|
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';
|