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,322 @@
|
|
|
1
|
+
// src/services/codegraph/codegraph-config-repair-writer.ts
|
|
2
|
+
//
|
|
3
|
+
// The pure repair plans (`repairCodegraphExclude` for the exclude axis,
|
|
4
|
+
// `repairCodegraphInclude` for the include axis) and the writer that applies
|
|
5
|
+
// them to `<projectRoot>/.codegraph/config.json` in ONE atomic rewrite, plus
|
|
6
|
+
// the byte-exact `config.json.bak` copy it keeps for rollback.
|
|
7
|
+
//
|
|
8
|
+
// Extracted verbatim from `codegraph-exclude-repair.ts` (rid
|
|
9
|
+
// 2026-09-17-oversize-followup, G1 — the 800-line file-size cap). Every moved
|
|
10
|
+
// line is byte-identical and no behaviour changed; `codegraph-exclude-repair.ts`
|
|
11
|
+
// re-exports this module's public surface, so every existing import site
|
|
12
|
+
// (`applyCodegraphConfigRepair`, `repairCodegraphExclude`,
|
|
13
|
+
// `repairCodegraphInclude`, `CODEGRAPH_CONFIG_BACKUP_SUFFIX`, and the three
|
|
14
|
+
// plan/outcome types) keeps working unchanged.
|
|
15
|
+
//
|
|
16
|
+
// The dependency edge is ONE-WAY and must stay that way: this module must not
|
|
17
|
+
// import `codegraph-exclude-repair.ts`, which imports this one.
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { chmodSync, lstatSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { assertStringArray, CODEGRAPH_CONFIG_FILENAME } from './codegraph-exclude-reconciler.js';
|
|
22
|
+
import { assertCodegraphDirContained, CODEGRAPH_DIR_NAME } from './codegraph-service.js';
|
|
23
|
+
/** Suffix of the byte-exact pre-repair copy kept next to the config. */
|
|
24
|
+
export const CODEGRAPH_CONFIG_BACKUP_SUFFIX = '.bak';
|
|
25
|
+
/**
|
|
26
|
+
* Pure: given the current `exclude` list and the rules to drop, return
|
|
27
|
+
* the new list. No fs, no clock, no serialization.
|
|
28
|
+
*
|
|
29
|
+
* A rule named in `rulesToRemove` but absent from `exclude` is NOT
|
|
30
|
+
* invented — the result is a subset of the input, so a caller can
|
|
31
|
+
* never add a rule by accident. Removing an already-absent rule is a
|
|
32
|
+
* no-op, which is what makes the whole repair idempotent: feeding the
|
|
33
|
+
* repaired list back in yields `changed: false`.
|
|
34
|
+
*/
|
|
35
|
+
export function repairCodegraphExclude(input) {
|
|
36
|
+
const removable = new Set(input.rulesToRemove);
|
|
37
|
+
// De-duplicated, order-preserving. A config that lists the same rule
|
|
38
|
+
// twice would otherwise be counted twice here, and this array is what
|
|
39
|
+
// the caller reports to the user as "rules removed".
|
|
40
|
+
const removedRules = [...new Set(input.exclude.filter((rule) => removable.has(rule)))];
|
|
41
|
+
if (removedRules.length === 0) {
|
|
42
|
+
return { changed: false, exclude: input.exclude, removedRules: [] };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
changed: true,
|
|
46
|
+
exclude: input.exclude.filter((rule) => !removable.has(rule)),
|
|
47
|
+
removedRules
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Pure: given the current `include` list and the patterns to append, return
|
|
52
|
+
* the new list. The mirror of `repairCodegraphExclude`, in the other
|
|
53
|
+
* direction — a SUPERSET operation instead of a subset one.
|
|
54
|
+
*
|
|
55
|
+
* A pattern already present is not appended twice: the include reconciler
|
|
56
|
+
* already guarantees that, but this function is the writer's own last line
|
|
57
|
+
* of defence, and a duplicated glob in a third-party config would be a
|
|
58
|
+
* visible defect even though it changes no matching behaviour.
|
|
59
|
+
*/
|
|
60
|
+
export function repairCodegraphInclude(input) {
|
|
61
|
+
const existing = new Set(input.include);
|
|
62
|
+
const additions = input.patternsToAdd.filter((pattern) => !existing.has(pattern));
|
|
63
|
+
if (additions.length === 0) {
|
|
64
|
+
return { changed: false, include: input.include, addedPatterns: [] };
|
|
65
|
+
}
|
|
66
|
+
return { changed: true, include: [...input.include, ...additions], addedPatterns: additions };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Detect the indentation the config already uses so the rewrite keeps
|
|
70
|
+
* the file's shape instead of reformatting a third-party tool's file.
|
|
71
|
+
*
|
|
72
|
+
* A single-line (minified) config has no indentation to copy: `0` tells
|
|
73
|
+
* `JSON.stringify` to emit compact JSON, so the file comes back as the
|
|
74
|
+
* one-liner it went in as. `0` is NOT the same as "not set" here — an
|
|
75
|
+
* omitted indent would also be compact, but returning a number keeps the
|
|
76
|
+
* intent explicit at the call site.
|
|
77
|
+
*
|
|
78
|
+
* Falls back to two spaces when the file spans lines but has no indented
|
|
79
|
+
* member.
|
|
80
|
+
*/
|
|
81
|
+
function detectIndent(text) {
|
|
82
|
+
if (!text.trimEnd().includes('\n')) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
const match = /\n([ \t]+)"/.exec(text);
|
|
86
|
+
return match?.[1] ?? 2;
|
|
87
|
+
}
|
|
88
|
+
function serializeConfig(config, originalText) {
|
|
89
|
+
const body = JSON.stringify(config, null, detectIndent(originalText));
|
|
90
|
+
return originalText.endsWith('\n') ? `${body}\n` : body;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Write `content` to `filePath` atomically: same-directory temp file,
|
|
94
|
+
* then `renameSync` over the target.
|
|
95
|
+
*
|
|
96
|
+
* The file being written belongs to a THIRD-PARTY tool, so a crash or a
|
|
97
|
+
* full disk mid-`writeFileSync` must never leave a half-written config
|
|
98
|
+
* behind. `rename` within one directory is atomic, so a reader sees
|
|
99
|
+
* either the old bytes or the new ones, never a prefix of the new ones.
|
|
100
|
+
* The temp file lives next to the target (same directory ⇒ same
|
|
101
|
+
* filesystem ⇒ the rename cannot degrade to a cross-device copy) and is
|
|
102
|
+
* removed if the write or the rename fails.
|
|
103
|
+
*
|
|
104
|
+
* N5: the temp name carries the pid plus 6 random bytes. A FIXED
|
|
105
|
+
* `${filePath}.tmp` is single-writer only, and this writer has three
|
|
106
|
+
* reachable concurrent callers — a fresh `peaks codegraph init` (which
|
|
107
|
+
* repairs then indexes), the pre-dispatch preflight, and the post-slice
|
|
108
|
+
* autorefresh. Two overlapping writers sharing one temp name let one
|
|
109
|
+
* `renameSync` publish a file the other was still writing, which is
|
|
110
|
+
* exactly the half-written config the temp file exists to prevent. The
|
|
111
|
+
* suffix keeps the temp in the SAME directory, so the rename stays
|
|
112
|
+
* within one filesystem and therefore stays atomic.
|
|
113
|
+
*/
|
|
114
|
+
function writeConfigAtomic(filePath, content, mode) {
|
|
115
|
+
const tempPath = `${filePath}.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`;
|
|
116
|
+
try {
|
|
117
|
+
writeFileSync(tempPath, content, 'utf8');
|
|
118
|
+
if (mode !== undefined) {
|
|
119
|
+
// The mode is applied to the TEMP, before the rename, so the
|
|
120
|
+
// published file has it in the same atomic step — a `chmod` after
|
|
121
|
+
// the rename would leave a window in which the file exists at the
|
|
122
|
+
// process umask instead of the original's mode.
|
|
123
|
+
chmodSync(tempPath, mode);
|
|
124
|
+
// Windows cannot REPLACE a read-only destination: `renameSync` over
|
|
125
|
+
// one throws EPERM (measured, `2026-09-17-codegraph-msg-and-refresh`).
|
|
126
|
+
// The only destination this branch ever sees read-only is this
|
|
127
|
+
// writer's own previous `.bak`, which under A4 carries the original
|
|
128
|
+
// config's mode — so a read-only config would make the SECOND repair
|
|
129
|
+
// fail at the rename. Clear the bit and let the rename plus the mode
|
|
130
|
+
// above put it back. A destination that is not a regular file is left
|
|
131
|
+
// alone: `writeConfigBackup`'s link/directory guard has already
|
|
132
|
+
// refused those before this function is reached.
|
|
133
|
+
const existing = lstatSync(filePath, { throwIfNoEntry: false });
|
|
134
|
+
if (existing !== undefined && existing.isFile() && (existing.mode & 0o200) === 0) {
|
|
135
|
+
chmodSync(filePath, existing.mode | 0o200);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
renameSync(tempPath, filePath);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
rmSync(tempPath, { force: true });
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Copy the config's ORIGINAL bytes to `config.json.bak`, refusing to write
|
|
147
|
+
* through a link that already occupies that path.
|
|
148
|
+
*
|
|
149
|
+
* The backup path is FIXED (`<configPath>.bak`) and therefore guessable, and
|
|
150
|
+
* `.codegraph/config.json.bak` is committable in a consumer project. So a
|
|
151
|
+
* repository can ship that path as a link to an arbitrary file of the
|
|
152
|
+
* attacker's choosing plus a `config.json` that merely OMITS an extension —
|
|
153
|
+
* which is the DEFAULT state of every config written before the include axis
|
|
154
|
+
* existed — and any repair-seam run (a fresh `peaks codegraph init`, the
|
|
155
|
+
* pre-dispatch preflight, the post-slice autorefresh, or either explicit
|
|
156
|
+
* repair verb) would write the whole attacker-authored config file through
|
|
157
|
+
* that link. Arbitrary file overwrite, as the invoking user, with no
|
|
158
|
+
* operator action. `writeFileSync`'s default `'w'` flag is
|
|
159
|
+
* `O_WRONLY|O_CREAT|O_TRUNC`: it follows a symlink and it truncates a hard
|
|
160
|
+
* link's shared inode, so it is the wrong primitive here.
|
|
161
|
+
*
|
|
162
|
+
* Two independent refusals, because either one alone leaves a case open:
|
|
163
|
+
*
|
|
164
|
+
* 1. `lstat` (never `stat`) rejects a SYMBOLIC LINK — `stat` would follow
|
|
165
|
+
* it and report the victim's regular-file type — and a HARD LINK, which
|
|
166
|
+
* `lstat` cannot distinguish by type but the link count exposes
|
|
167
|
+
* (`nlink > 1` means another name shares this inode, so writing here
|
|
168
|
+
* writes to that file too). Both are refusals, not repairs: silently
|
|
169
|
+
* replacing someone else's link is not this writer's decision to make.
|
|
170
|
+
* 2. The copy goes through `writeConfigAtomic` (CSPRNG temp + `renameSync`),
|
|
171
|
+
* so even an entry that appears between the `lstat` and the write is
|
|
172
|
+
* REPLACED rather than written through — `rename` never follows the
|
|
173
|
+
* destination's link, and it never truncates the destination's inode.
|
|
174
|
+
* That also makes the backup atomic, which the module header's
|
|
175
|
+
* byte-exact-rollback promise needs and a bare `writeFileSync` did not
|
|
176
|
+
* provide.
|
|
177
|
+
*
|
|
178
|
+
* What is NOT refused: a regular file at the backup path with a link count
|
|
179
|
+
* of 1. That is this writer's own previous backup (a project may legitimately
|
|
180
|
+
* drift and be repaired twice), and replacing it atomically is exactly what
|
|
181
|
+
* the `rename` in (2) does. `O_EXCL` alone would have refused that legitimate
|
|
182
|
+
* case along with the attack, which is why the guard is a link test rather
|
|
183
|
+
* than an existence test.
|
|
184
|
+
*
|
|
185
|
+
* `writeFileSync` there is called only after parsing and planning have both
|
|
186
|
+
* succeeded, so a refusal leaves the config BYTES UNTOUCHED — the throw
|
|
187
|
+
* propagates out of `applyCodegraphConfigRepair` before the rewrite.
|
|
188
|
+
*
|
|
189
|
+
* A4 (`2026-09-17-codegraph-msg-and-refresh`, the L1 half left over from
|
|
190
|
+
* slice-002's S1): the copy carries the ORIGINAL CONFIG'S MODE, because the
|
|
191
|
+
* mode is part of what a rollback restores. The `.bak` exists so an operator
|
|
192
|
+
* can put the previous config back with a `mv`, and a restore that hands
|
|
193
|
+
* back a file whose permissions were decided by this process's umask is not
|
|
194
|
+
* a restore: a `0600` config would come back as `0644` — group- and
|
|
195
|
+
* world-readable, a permission the project never granted it.
|
|
196
|
+
*
|
|
197
|
+
* The mode is read with `statSync` on the config (not `lstatSync`, and not
|
|
198
|
+
* from the caller): the caller has just read that file's bytes, so it
|
|
199
|
+
* exists, and a config that is itself a symlink is a case upstream's own
|
|
200
|
+
* reader follows — the permissions that matter are the ones the operator
|
|
201
|
+
* sees on the config.
|
|
202
|
+
*
|
|
203
|
+
* NOT applied to the config rewrite beside it, deliberately: that path
|
|
204
|
+
* (`writeConfigAtomic(configPath, …)` with no mode) keeps the process
|
|
205
|
+
* default, which is what upstream's own `init` produces. Stated rather than
|
|
206
|
+
* left silent — see this batch's RD artifact, section A4.
|
|
207
|
+
*/
|
|
208
|
+
function writeConfigBackup(configPath, originalText) {
|
|
209
|
+
const backupPath = `${configPath}${CODEGRAPH_CONFIG_BACKUP_SUFFIX}`;
|
|
210
|
+
const existing = lstatSync(backupPath, { throwIfNoEntry: false });
|
|
211
|
+
if (existing !== undefined &&
|
|
212
|
+
(existing.isSymbolicLink() || existing.isDirectory() || existing.nlink > 1)) {
|
|
213
|
+
const kind = existing.isSymbolicLink()
|
|
214
|
+
? 'a symbolic link'
|
|
215
|
+
: existing.isDirectory()
|
|
216
|
+
? 'a directory'
|
|
217
|
+
: `a hard link (link count ${String(existing.nlink)})`;
|
|
218
|
+
// "through" for a link (the bytes land in whatever it points at), "at" for
|
|
219
|
+
// a directory (there is nowhere for them to land).
|
|
220
|
+
const preposition = existing.isDirectory() ? 'at' : 'through';
|
|
221
|
+
throw new Error(`codegraph config backup ${backupPath}: refusing to write ${preposition} ${kind} occupying ` +
|
|
222
|
+
'this path. Another file or directory shares it, so a backup written here would overwrite ' +
|
|
223
|
+
'that. Remove it (or point `peaks` at a project root whose `.codegraph/` it owns) and re-run.');
|
|
224
|
+
}
|
|
225
|
+
// A4: the ORIGINAL config's mode travels with the copy. `& 0o777` drops
|
|
226
|
+
// the file-type bits `statSync` packs above the permission bits — `chmod`
|
|
227
|
+
// takes permission bits only.
|
|
228
|
+
writeConfigAtomic(backupPath, originalText, statSync(configPath).mode & 0o777);
|
|
229
|
+
return backupPath;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Apply BOTH config repairs to `<projectRoot>/.codegraph/config.json` in one
|
|
233
|
+
* atomic rewrite.
|
|
234
|
+
*
|
|
235
|
+
* No-op (and no write, no mtime change, no backup) when both lists are
|
|
236
|
+
* empty. Otherwise: back up the original bytes to `config.json.bak`, then
|
|
237
|
+
* rewrite the file with `exclude` reduced by exactly the rules that were
|
|
238
|
+
* both requested and present, and `include` extended by exactly the
|
|
239
|
+
* patterns that were both requested and absent.
|
|
240
|
+
*
|
|
241
|
+
* ONE rewrite, not two. A caller that widened `include` in one write and
|
|
242
|
+
* dropped the newly-offending `exclude` rules in a second would leave a
|
|
243
|
+
* window in which the config on disk is worse than it started (the widened
|
|
244
|
+
* include admits a file that a surviving rule then hides from the index),
|
|
245
|
+
* and would need two backups to stay rollback-exact. One rewrite through
|
|
246
|
+
* the same-directory temp file has neither property.
|
|
247
|
+
*
|
|
248
|
+
* Throws only on real fs/parse failures and on the containment refusal
|
|
249
|
+
* above — the caller decides whether that is fatal (`repair-exclude` /
|
|
250
|
+
* `repair-index` → non-zero exit) or a surfaced warning (`init` → keep
|
|
251
|
+
* going, the init itself already succeeded). Both are refusals BEFORE
|
|
252
|
+
* anything is written, so a throw can never half-apply.
|
|
253
|
+
*/
|
|
254
|
+
export function applyCodegraphConfigRepair(projectRoot, repair) {
|
|
255
|
+
if (repair.rulesToRemove.length === 0 && repair.includePatternsToAdd.length === 0) {
|
|
256
|
+
return {
|
|
257
|
+
applied: false,
|
|
258
|
+
reason: 'nothing-to-repair',
|
|
259
|
+
removedRules: [],
|
|
260
|
+
addedIncludePatterns: []
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// Containment FIRST — before even reading (S12 / security R1). The `.bak`
|
|
264
|
+
// guard below protects a FILE path; this protects the DIRECTORY it lives
|
|
265
|
+
// in, so `<root>/.codegraph` as a junction cannot redirect the rewrite (or
|
|
266
|
+
// the parsed read) into another project. Called for its refusal only: the
|
|
267
|
+
// write target stays derived from the caller's `projectRoot`, so this
|
|
268
|
+
// verb's canonicalization remains where L2/S5 put it, at the CLI edge.
|
|
269
|
+
assertCodegraphDirContained(projectRoot);
|
|
270
|
+
const configPath = join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME);
|
|
271
|
+
const originalText = readFileSync(configPath, 'utf8');
|
|
272
|
+
const parsed = JSON.parse(originalText);
|
|
273
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
274
|
+
throw new Error(`codegraph config ${configPath}: expected a JSON object`);
|
|
275
|
+
}
|
|
276
|
+
const record = parsed;
|
|
277
|
+
const exclude = assertStringArray(record.exclude, 'exclude', configPath);
|
|
278
|
+
// An ABSENT `include` key is tolerated, and is NOT the same as a malformed
|
|
279
|
+
// one: this writer predates the include axis and callers exist whose
|
|
280
|
+
// config carries only `exclude` (a hand-written minimal file; the S2
|
|
281
|
+
// hardening fixtures are exactly that). Throwing here would make the
|
|
282
|
+
// whole repair fail — including the exclude half that used to succeed —
|
|
283
|
+
// so the include axis simply has nothing to extend, and the key is never
|
|
284
|
+
// invented. A key that is PRESENT but is not an array of strings is still
|
|
285
|
+
// an error: that is a genuinely malformed config, and silently ignoring it
|
|
286
|
+
// is how a wrong `include` would survive a repair that reported success.
|
|
287
|
+
const include = record.include === undefined ? [] : assertStringArray(record.include, 'include', configPath);
|
|
288
|
+
const excludePlan = repairCodegraphExclude({ exclude, rulesToRemove: repair.rulesToRemove });
|
|
289
|
+
const includePlan = repairCodegraphInclude({ include, patternsToAdd: repair.includePatternsToAdd });
|
|
290
|
+
if (!excludePlan.changed && !includePlan.changed) {
|
|
291
|
+
return {
|
|
292
|
+
applied: false,
|
|
293
|
+
reason: 'nothing-to-repair',
|
|
294
|
+
removedRules: [],
|
|
295
|
+
addedIncludePatterns: []
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const backupPath = writeConfigBackup(configPath, originalText);
|
|
299
|
+
// Spread first, then replace the keys that changed — every other key keeps
|
|
300
|
+
// its original value AND its original position in the serialized object.
|
|
301
|
+
// `include` is only written back when it actually changed, so a config
|
|
302
|
+
// without that key does not gain one from a repair that did not touch it.
|
|
303
|
+
const nextRecord = { ...record, exclude: excludePlan.exclude };
|
|
304
|
+
if (includePlan.changed) {
|
|
305
|
+
nextRecord.include = includePlan.include;
|
|
306
|
+
}
|
|
307
|
+
writeConfigAtomic(configPath, serializeConfig(nextRecord, originalText));
|
|
308
|
+
return {
|
|
309
|
+
applied: true,
|
|
310
|
+
configPath,
|
|
311
|
+
backupPath,
|
|
312
|
+
removedRules: excludePlan.removedRules,
|
|
313
|
+
// The patterns that ACTUALLY landed (a caller-supplied pattern that was
|
|
314
|
+
// already present is not an addition), so the report cannot overstate
|
|
315
|
+
// what changed on disk.
|
|
316
|
+
addedIncludePatterns: includePlan.addedPatterns,
|
|
317
|
+
excludeCountBefore: exclude.length,
|
|
318
|
+
excludeCountAfter: excludePlan.exclude.length,
|
|
319
|
+
includeCountBefore: include.length,
|
|
320
|
+
includeCountAfter: includePlan.include.length
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CodegraphExcludeViolation } from './codegraph-exclude-reconciler.js';
|
|
1
|
+
import { type CodegraphExcludeViolation, type ReadCodegraphExcludeConfig, type ReadTrackedFiles } from './codegraph-exclude-reconciler.js';
|
|
2
2
|
/**
|
|
3
3
|
* Exit code `peaks codegraph status` uses when the index is
|
|
4
4
|
* demonstrably incomplete. Distinct from the upstream pass-through
|
|
@@ -48,8 +48,26 @@ export declare function isCodegraphExcludeConfigPresent(projectRoot: string): bo
|
|
|
48
48
|
* Throws (never silently degrades) when the project is not a git work
|
|
49
49
|
* tree, when the config is missing, or when it is malformed — callers
|
|
50
50
|
* that must stay alive (doctor, `status`) catch and surface the reason.
|
|
51
|
+
*
|
|
52
|
+
* `inputs` is the optional shared-read seam (perf audit F1): a caller
|
|
53
|
+
* that runs BOTH codegraph axes in one process reads
|
|
54
|
+
* `readCodegraphProjectInputs(projectRoot)` once and passes it here and
|
|
55
|
+
* to `inspectCodegraphIndexIntegrity`, so the `git ls-files` spawn, the
|
|
56
|
+
* config read and the `include` glob compilation happen once instead of
|
|
57
|
+
* twice. Omitted (the default) the readers run exactly as they always
|
|
58
|
+
* did, so this is additive for every existing caller.
|
|
59
|
+
*
|
|
60
|
+
* Both fields are read-marked (code review R4-1): a caller may hand over a
|
|
61
|
+
* value that came out of the readers, never one it built itself. An
|
|
62
|
+
* explicit `[]` used to be accepted and silently meant "nothing is
|
|
63
|
+
* tracked", turning a real gap into a clean report; it is now a compile
|
|
64
|
+
* error, and a run-time throw for a caller the type system cannot see.
|
|
65
|
+
* `undefined` still means "read it yourself" — unchanged.
|
|
51
66
|
*/
|
|
52
|
-
export declare function inspectCodegraphExcludeIntegrity(projectRoot: string
|
|
67
|
+
export declare function inspectCodegraphExcludeIntegrity(projectRoot: string, inputs?: {
|
|
68
|
+
readonly trackedFiles?: ReadTrackedFiles | undefined;
|
|
69
|
+
readonly config?: ReadCodegraphExcludeConfig | undefined;
|
|
70
|
+
}): CodegraphExcludeIntegrityReport;
|
|
53
71
|
/**
|
|
54
72
|
* Human-readable detail lines for a gapped report: the headline count,
|
|
55
73
|
* then the offending rules, then a sample of the blocked files so an
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// `peaks codegraph repair-exclude` command. Read stays read.
|
|
16
16
|
import { existsSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
-
import { CODEGRAPH_CONFIG_FILENAME,
|
|
18
|
+
import { CODEGRAPH_CONFIG_FILENAME, resolveSharedConfig, resolveSharedTrackedFiles, reconcileCodegraphExclude } from './codegraph-exclude-reconciler.js';
|
|
19
19
|
import { CODEGRAPH_DIR_NAME } from './codegraph-service.js';
|
|
20
20
|
/**
|
|
21
21
|
* Exit code `peaks codegraph status` uses when the index is
|
|
@@ -49,9 +49,30 @@ export function isCodegraphExcludeConfigPresent(projectRoot) {
|
|
|
49
49
|
* Throws (never silently degrades) when the project is not a git work
|
|
50
50
|
* tree, when the config is missing, or when it is malformed — callers
|
|
51
51
|
* that must stay alive (doctor, `status`) catch and surface the reason.
|
|
52
|
+
*
|
|
53
|
+
* `inputs` is the optional shared-read seam (perf audit F1): a caller
|
|
54
|
+
* that runs BOTH codegraph axes in one process reads
|
|
55
|
+
* `readCodegraphProjectInputs(projectRoot)` once and passes it here and
|
|
56
|
+
* to `inspectCodegraphIndexIntegrity`, so the `git ls-files` spawn, the
|
|
57
|
+
* config read and the `include` glob compilation happen once instead of
|
|
58
|
+
* twice. Omitted (the default) the readers run exactly as they always
|
|
59
|
+
* did, so this is additive for every existing caller.
|
|
60
|
+
*
|
|
61
|
+
* Both fields are read-marked (code review R4-1): a caller may hand over a
|
|
62
|
+
* value that came out of the readers, never one it built itself. An
|
|
63
|
+
* explicit `[]` used to be accepted and silently meant "nothing is
|
|
64
|
+
* tracked", turning a real gap into a clean report; it is now a compile
|
|
65
|
+
* error, and a run-time throw for a caller the type system cannot see.
|
|
66
|
+
* `undefined` still means "read it yourself" — unchanged.
|
|
52
67
|
*/
|
|
53
|
-
export function inspectCodegraphExcludeIntegrity(projectRoot) {
|
|
54
|
-
const
|
|
68
|
+
export function inspectCodegraphExcludeIntegrity(projectRoot, inputs = {}) {
|
|
69
|
+
const trackedFiles = resolveSharedTrackedFiles(inputs.trackedFiles, projectRoot);
|
|
70
|
+
const config = resolveSharedConfig(inputs.config, projectRoot);
|
|
71
|
+
const result = reconcileCodegraphExclude({
|
|
72
|
+
trackedFiles,
|
|
73
|
+
include: config.include,
|
|
74
|
+
exclude: config.exclude
|
|
75
|
+
});
|
|
55
76
|
const blockedCounts = new Map();
|
|
56
77
|
for (const violation of result.violations) {
|
|
57
78
|
blockedCounts.set(violation.matchedRule, (blockedCounts.get(violation.matchedRule) ?? 0) + 1);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare function matchesCodegraphGlob(filePath: string, pattern: string): boolean;
|
|
2
|
+
export type CompiledCodegraphGlobs = {
|
|
3
|
+
readonly matchesAny: (filePath: string) => boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function compileCodegraphGlobs(patterns: readonly string[]): CompiledCodegraphGlobs;
|
|
2
6
|
export type CodegraphExcludeReconcileInput = {
|
|
3
7
|
readonly trackedFiles: readonly string[];
|
|
4
8
|
readonly include: readonly string[];
|
|
@@ -14,13 +18,30 @@ export type CodegraphExcludeReconcileResult = {
|
|
|
14
18
|
readonly trackedSourceCount: number;
|
|
15
19
|
readonly excludedTrackedCount: number;
|
|
16
20
|
};
|
|
21
|
+
export declare function filterAdmittedTrackedFiles(trackedFiles: readonly string[], include: readonly string[]): readonly string[];
|
|
17
22
|
export declare function reconcileCodegraphExclude(input: CodegraphExcludeReconcileInput): CodegraphExcludeReconcileResult;
|
|
18
23
|
export declare const CODEGRAPH_CONFIG_FILENAME = "config.json";
|
|
19
24
|
export type CodegraphExcludeConfig = {
|
|
20
25
|
readonly include: readonly string[];
|
|
21
26
|
readonly exclude: readonly string[];
|
|
22
27
|
};
|
|
23
|
-
|
|
28
|
+
declare const READ_PROVENANCE: unique symbol;
|
|
29
|
+
type ReadProvenance = {
|
|
30
|
+
readonly [READ_PROVENANCE]: true;
|
|
31
|
+
};
|
|
32
|
+
/** A tracked-file list that provably came out of `readTrackedFiles`. */
|
|
33
|
+
export type ReadTrackedFiles = readonly string[] & ReadProvenance;
|
|
34
|
+
/** A config that provably came out of `readCodegraphExcludeConfig`. */
|
|
35
|
+
export type ReadCodegraphExcludeConfig = CodegraphExcludeConfig & ReadProvenance;
|
|
36
|
+
export declare function readTrackedFiles(projectRoot: string): ReadTrackedFiles;
|
|
24
37
|
export declare function assertStringArray(value: unknown, field: string, configPath: string): readonly string[];
|
|
25
|
-
export declare function readCodegraphExcludeConfig(projectRoot: string):
|
|
38
|
+
export declare function readCodegraphExcludeConfig(projectRoot: string): ReadCodegraphExcludeConfig;
|
|
39
|
+
export type CodegraphProjectInputs = {
|
|
40
|
+
readonly trackedFiles: ReadTrackedFiles;
|
|
41
|
+
readonly config: ReadCodegraphExcludeConfig;
|
|
42
|
+
};
|
|
43
|
+
export declare function readCodegraphProjectInputs(projectRoot: string): CodegraphProjectInputs;
|
|
44
|
+
export declare function resolveSharedTrackedFiles(supplied: ReadTrackedFiles | undefined, projectRoot: string): ReadTrackedFiles;
|
|
45
|
+
export declare function resolveSharedConfig(supplied: ReadCodegraphExcludeConfig | undefined, projectRoot: string): ReadCodegraphExcludeConfig;
|
|
26
46
|
export declare function reconcileCodegraphExcludeFromProject(projectRoot: string): CodegraphExcludeReconcileResult;
|
|
47
|
+
export {};
|
|
@@ -120,6 +120,39 @@ export function matchesCodegraphGlob(filePath, pattern) {
|
|
|
120
120
|
}
|
|
121
121
|
return compileGlob(pattern).match(normalizePath(filePath));
|
|
122
122
|
}
|
|
123
|
+
export function compileCodegraphGlobs(patterns) {
|
|
124
|
+
const rules = compileRules(patterns);
|
|
125
|
+
return {
|
|
126
|
+
matchesAny: (filePath) => {
|
|
127
|
+
const normalizedPath = normalizePath(filePath);
|
|
128
|
+
return rules.some((rule) => rule.match(normalizedPath));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// The project-relative paths of the tracked files the config's `include`
|
|
133
|
+
// globs admit, normalized. This is the ONE place the "would the index
|
|
134
|
+
// ingest this path" question is answered, so this reconciler and the
|
|
135
|
+
// index-integrity inspector cannot drift apart on glob semantics: the
|
|
136
|
+
// `include`-axis gap is computed as a set difference against exactly the
|
|
137
|
+
// set this function returns.
|
|
138
|
+
//
|
|
139
|
+
// Pure: no fs, no spawn, no clock. Compiling per rule (not per file x
|
|
140
|
+
// rule) keeps it linear in the number of tracked files.
|
|
141
|
+
//
|
|
142
|
+
// Unmatchable `include` entries are dropped before compiling (see
|
|
143
|
+
// `isUnmatchableRule`). An empty `include` entry admits nothing, which is
|
|
144
|
+
// the same verdict as an explicitly empty `include` list.
|
|
145
|
+
export function filterAdmittedTrackedFiles(trackedFiles, include) {
|
|
146
|
+
const includeRules = compileRules(include);
|
|
147
|
+
const admitted = [];
|
|
148
|
+
for (const candidate of trackedFiles) {
|
|
149
|
+
const normalizedPath = normalizePath(candidate);
|
|
150
|
+
if (includeRules.some((rule) => rule.match(normalizedPath))) {
|
|
151
|
+
admitted.push(normalizedPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return admitted;
|
|
155
|
+
}
|
|
123
156
|
// Reconcile the codegraph `exclude` list against the set of git-tracked
|
|
124
157
|
// source files. Pure: no fs, no spawn, no clock.
|
|
125
158
|
//
|
|
@@ -131,15 +164,8 @@ export function reconcileCodegraphExclude(input) {
|
|
|
131
164
|
// `isUnmatchableRule`. An empty `include` entry admits nothing, which
|
|
132
165
|
// is the same verdict as an explicitly empty `include` list, so it
|
|
133
166
|
// needs no special case here.
|
|
134
|
-
const includeRules = compileRules(input.include);
|
|
135
167
|
const excludeRules = compileRules(input.exclude);
|
|
136
|
-
const trackedSourceFiles =
|
|
137
|
-
for (const candidate of input.trackedFiles) {
|
|
138
|
-
const normalizedPath = normalizePath(candidate);
|
|
139
|
-
if (includeRules.some((rule) => rule.match(normalizedPath))) {
|
|
140
|
-
trackedSourceFiles.push(normalizedPath);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
168
|
+
const trackedSourceFiles = filterAdmittedTrackedFiles(input.trackedFiles, input.include);
|
|
143
169
|
const violations = [];
|
|
144
170
|
const offendingRules = new Set();
|
|
145
171
|
const blockedFiles = new Set();
|
|
@@ -165,6 +191,59 @@ export function reconcileCodegraphExclude(input) {
|
|
|
165
191
|
// ─────────────────────────────────────────────────────────────────────
|
|
166
192
|
// Upstream `CONFIG_FILENAME` inside `<projectRoot>/.codegraph/`.
|
|
167
193
|
export const CODEGRAPH_CONFIG_FILENAME = 'config.json';
|
|
194
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
+
// Read provenance — why the shared-input seam refuses a fabricated value
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
197
|
+
// Code review R4-1. The shared-input seam (perf F1) hands an inspector the
|
|
198
|
+
// ALREADY-READ inputs. Before this block, `trackedFiles?: readonly string[]`
|
|
199
|
+
// could not tell "field omitted" (read it yourself) from "field supplied as
|
|
200
|
+
// `[]`" (silently: nothing is tracked) — and the second one turned a real
|
|
201
|
+
// gap into a CLEAN verdict on BOTH axes:
|
|
202
|
+
//
|
|
203
|
+
// E(root, { trackedFiles: [] })
|
|
204
|
+
// exclude axis: gap true -> false, trackedSourceCount 1 -> 0, violations [] -> dropped
|
|
205
|
+
// index axis: includeGap ['scripts/tool.mjs'] -> []
|
|
206
|
+
//
|
|
207
|
+
// That is a silent false pass inside the guard built to prevent silent false
|
|
208
|
+
// passes. Emptiness alone cannot be the discriminator — a repository that
|
|
209
|
+
// genuinely tracks nothing has a legitimately EMPTY list, and rejecting that
|
|
210
|
+
// would turn `peaks codegraph status` on such a repo from "clean" into a
|
|
211
|
+
// spurious warning. Provenance can discriminate, so that is what we key on.
|
|
212
|
+
//
|
|
213
|
+
// Every value this module's readers produce is marked, non-enumerably, with
|
|
214
|
+
// this symbol at the ONE place that owns the readers; the seam accepts only
|
|
215
|
+
// marked values. A hand-built `[]` is therefore unreachable at compile time
|
|
216
|
+
// (the brand is a required property) AND loud at run time (a JS caller or a
|
|
217
|
+
// cast trips the assertion instead of silently reporting clean).
|
|
218
|
+
//
|
|
219
|
+
// `Symbol.for` rather than `Symbol()` so the mark survives the same module
|
|
220
|
+
// being loaded twice (dual ESM/CJS evaluation of a linked package).
|
|
221
|
+
const READ_PROVENANCE = Symbol.for('peaks-loop.codegraph.read-provenance');
|
|
222
|
+
// `enumerable: false` on purpose: the mark is provenance, not content. A
|
|
223
|
+
// `JSON.stringify`, a spread or a deep-equality assertion over the list must
|
|
224
|
+
// not see it, so branding stays invisible to every existing consumer.
|
|
225
|
+
function markAsRead(value) {
|
|
226
|
+
Object.defineProperty(value, READ_PROVENANCE, {
|
|
227
|
+
value: true,
|
|
228
|
+
enumerable: false,
|
|
229
|
+
configurable: false,
|
|
230
|
+
writable: false
|
|
231
|
+
});
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
// The run-time half of the brand. Reached only by a caller the type system
|
|
235
|
+
// could not police (JS, a cast, or a future `as` in a hurry), so it must be
|
|
236
|
+
// loud and must say what the silent alternative would have done.
|
|
237
|
+
function assertReadProvenance(value, field, producer) {
|
|
238
|
+
if (!Object.prototype.hasOwnProperty.call(value, READ_PROVENANCE)) {
|
|
239
|
+
throw new Error(`codegraph shared inputs: "${field}" did not come from ${producer}(), so it is not a ` +
|
|
240
|
+
'value this module can trust. A hand-built value is indistinguishable from a real read, ' +
|
|
241
|
+
'and an empty one means "nothing is tracked / nothing is admitted" — which reports a ' +
|
|
242
|
+
'real gap as CLEAN on both codegraph axes. Pass the result of ' +
|
|
243
|
+
'readCodegraphProjectInputs(projectRoot) verbatim, or omit the field to read from disk.');
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
168
247
|
// Project-relative paths of every git-tracked file, exactly as
|
|
169
248
|
// `git ls-files` reports them. Why git and not an fs walk: the index
|
|
170
249
|
// must cover what git tracks (see the anti-fake-green contract in
|
|
@@ -173,17 +252,20 @@ export const CODEGRAPH_CONFIG_FILENAME = 'config.json';
|
|
|
173
252
|
//
|
|
174
253
|
// Throws when `projectRoot` is not inside a git work tree — callers
|
|
175
254
|
// decide whether that is fatal; this function never swallows it.
|
|
255
|
+
//
|
|
256
|
+
// Returns a READ-MARKED list: the seam helpers below accept only marked
|
|
257
|
+
// values, so a fabricated `[]` cannot be passed off as this read's result.
|
|
176
258
|
export function readTrackedFiles(projectRoot) {
|
|
177
259
|
const stdout = execFileSync('git', ['-C', projectRoot, 'ls-files'], {
|
|
178
260
|
encoding: 'utf8',
|
|
179
261
|
maxBuffer: 64 * 1024 * 1024,
|
|
180
262
|
windowsHide: true
|
|
181
263
|
});
|
|
182
|
-
return stdout
|
|
264
|
+
return markAsRead(stdout
|
|
183
265
|
.split('\n')
|
|
184
266
|
.map((line) => line.trim())
|
|
185
267
|
.filter((line) => line.length > 0)
|
|
186
|
-
.map((line) => normalizePath(line));
|
|
268
|
+
.map((line) => normalizePath(line)));
|
|
187
269
|
}
|
|
188
270
|
// Exported for S2's repair writer, which re-validates `exclude` on the
|
|
189
271
|
// way out so the read and write paths agree on what a valid config is.
|
|
@@ -195,7 +277,7 @@ export function assertStringArray(value, field, configPath) {
|
|
|
195
277
|
}
|
|
196
278
|
// Read `<projectRoot>/.codegraph/config.json` and return just the two
|
|
197
279
|
// glob lists the reconciler needs. Read-only: this module never writes
|
|
198
|
-
// that file.
|
|
280
|
+
// that file. Returns a READ-MARKED config — see `readTrackedFiles`.
|
|
199
281
|
export function readCodegraphExcludeConfig(projectRoot) {
|
|
200
282
|
const configPath = join(projectRoot, CODEGRAPH_DIR_NAME, CODEGRAPH_CONFIG_FILENAME);
|
|
201
283
|
const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
@@ -203,11 +285,40 @@ export function readCodegraphExcludeConfig(projectRoot) {
|
|
|
203
285
|
throw new Error(`codegraph config ${configPath}: expected a JSON object`);
|
|
204
286
|
}
|
|
205
287
|
const record = parsed;
|
|
206
|
-
return {
|
|
288
|
+
return markAsRead({
|
|
207
289
|
include: assertStringArray(record.include, 'include', configPath),
|
|
208
290
|
exclude: assertStringArray(record.exclude, 'exclude', configPath)
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// Read both shared inputs exactly once, in the one place that owns the
|
|
294
|
+
// readers. Callers that need both axes (the CLI, and anything added later)
|
|
295
|
+
// should call this and pass the result to each inspector rather than
|
|
296
|
+
// letting each inspector read for itself.
|
|
297
|
+
export function readCodegraphProjectInputs(projectRoot) {
|
|
298
|
+
return {
|
|
299
|
+
trackedFiles: readTrackedFiles(projectRoot),
|
|
300
|
+
config: readCodegraphExcludeConfig(projectRoot)
|
|
209
301
|
};
|
|
210
302
|
}
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
304
|
+
// Seam resolution — what each inspector's shared-input seam resolves through
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
306
|
+
// OMITTED (or `undefined`) always means "read it yourself": that is the
|
|
307
|
+
// original, seam-free behaviour and it must stay bit-identical. SUPPLIED
|
|
308
|
+
// means "here is a value you already read" — and the only way to prove that
|
|
309
|
+
// is the read mark, which is why an unmarked value is refused rather than
|
|
310
|
+
// used. An empty marked list is perfectly legal (a repo that tracks nothing
|
|
311
|
+
// really does have none); an empty unmarked one is the false clean.
|
|
312
|
+
export function resolveSharedTrackedFiles(supplied, projectRoot) {
|
|
313
|
+
return supplied === undefined
|
|
314
|
+
? readTrackedFiles(projectRoot)
|
|
315
|
+
: assertReadProvenance(supplied, 'trackedFiles', 'readTrackedFiles');
|
|
316
|
+
}
|
|
317
|
+
export function resolveSharedConfig(supplied, projectRoot) {
|
|
318
|
+
return supplied === undefined
|
|
319
|
+
? readCodegraphExcludeConfig(projectRoot)
|
|
320
|
+
: assertReadProvenance(supplied, 'config', 'readCodegraphExcludeConfig');
|
|
321
|
+
}
|
|
211
322
|
// Read-only entry point: resolve the project's tracked files + codegraph
|
|
212
323
|
// config from disk and reconcile them. S2's repair path consumes this
|
|
213
324
|
// result and writes the reduced `exclude` list back; S1 only computes.
|