codex-context-map 0.1.0

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.
@@ -0,0 +1,314 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <meta
7
+ http-equiv="Content-Security-Policy"
8
+ content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; base-uri 'none'; form-action 'none'; object-src 'none'"
9
+ />
10
+ <meta
11
+ name="description"
12
+ content="An offline visual debugger for Codex AGENTS.md discovery. Explore instruction inheritance, hidden overrides, byte limits, and directory differences. No account or API key."
13
+ />
14
+ <meta name="theme-color" content="#f7f9f8" />
15
+ <meta
16
+ property="og:title"
17
+ content="Codex Context Map — Know what your agent reads"
18
+ />
19
+ <meta
20
+ property="og:description"
21
+ content="A local, interactive map of AGENTS.md inheritance, overrides and truncation. Zero runtime dependencies."
22
+ />
23
+ <title>Codex Context Map — Know what your agent reads</title>
24
+ <style>
25
+ /* STYLE */
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <a class="skip-link" href="#main">Skip to instruction map</a>
30
+ <header class="topbar">
31
+ <a href="#" class="brand" aria-label="Codex Context Map home"
32
+ ><span class="brand-icon" aria-hidden="true">⌘</span
33
+ ><span>Codex <b>Context Map</b></span
34
+ ><span class="version">v0.1</span></a
35
+ >
36
+ <nav aria-label="Main">
37
+ <span class="local-badge"
38
+ ><i></i><span data-i18n="local">Runs on your device</span></span
39
+ ><button id="language" class="quiet" aria-label="Switch language">
40
+ 中文</button
41
+ ><a
42
+ class="github-link"
43
+ href="https://github.com/Han-1413141/codex-context-map"
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ >GitHub <span aria-hidden="true">↗</span></a
47
+ >
48
+ </nav>
49
+ </header>
50
+ <section class="intro">
51
+ <div>
52
+ <div class="eyebrow" data-i18n="eyebrow">
53
+ THE MISSING MAP FOR AGENTS.md
54
+ </div>
55
+ <h1 data-i18n="headline">Know what your agent reads<span>.</span></h1>
56
+ <p data-i18n="subhead">
57
+ Trace the instructions. Find the override. See the cutoff.
58
+ </p>
59
+ </div>
60
+ <div class="quickstart">
61
+ <span data-i18n="quickstart">MAP YOUR OWN PROJECT</span
62
+ ><button id="copy-command" title="Copy CLI command">
63
+ <code>npx codex-context-map . --html context-map.html</code
64
+ ><span aria-hidden="true">⧉</span></button
65
+ ><small data-i18n="quickhint"
66
+ >No account. No API key. No runtime dependencies.</small
67
+ >
68
+ </div>
69
+ </section>
70
+ <div class="workspace-toolbar">
71
+ <div>
72
+ <span class="workspace-symbol" aria-hidden="true">▦</span
73
+ ><strong id="project-name">northstar</strong
74
+ ><span id="workspace-kind" class="tag"></span>
75
+ </div>
76
+ <div class="toolbar-actions">
77
+ <button id="reset" class="quiet" data-i18n="demo">Demo</button
78
+ ><button id="import-json" class="quiet" data-i18n="importJson">
79
+ Open snapshot</button
80
+ ><button id="export" class="quiet" data-i18n="export">
81
+ Export JSON</button
82
+ ><button id="import-folder" class="primary" data-i18n="importFolder">
83
+ Open project folder
84
+ </button>
85
+ </div>
86
+ </div>
87
+ <div
88
+ id="status"
89
+ class="status-message"
90
+ role="status"
91
+ aria-live="polite"
92
+ ></div>
93
+ <div class="workspace">
94
+ <aside class="sidebar" aria-label="Project directories">
95
+ <div class="side-label">
96
+ <span data-i18n="directories">DIRECTORIES</span
97
+ ><span id="dir-count"></span>
98
+ </div>
99
+ <label class="search-wrap"
100
+ ><span aria-hidden="true">⌕</span
101
+ ><input
102
+ id="search"
103
+ type="search"
104
+ placeholder="Find a directory…"
105
+ aria-label="Find a directory"
106
+ /></label>
107
+ <div id="tree" class="tree" aria-label="Launch directory"></div>
108
+ <p id="tree-limit" class="small-note"></p>
109
+ <div class="legend">
110
+ <span
111
+ ><i class="dot teal"></i
112
+ ><span data-i18n="hasRules">Has instructions</span></span
113
+ ><span
114
+ ><i class="dot amber"></i
115
+ ><span data-i18n="hasOverride">Has an override</span></span
116
+ ><span
117
+ ><i class="dot gray"></i
118
+ ><span data-i18n="inherits">No local instructions</span></span
119
+ >
120
+ </div>
121
+ <div class="privacy-note">
122
+ <span aria-hidden="true">◈</span>
123
+ <div>
124
+ <b data-i18n="privacyTitle">Your files stay here.</b>
125
+ <p data-i18n="privacy">
126
+ Only instruction files are read. Nothing is uploaded or saved in
127
+ browser storage.
128
+ </p>
129
+ </div>
130
+ </div>
131
+ </aside>
132
+ <main id="main">
133
+ <div class="path-row">
134
+ <div>
135
+ <span class="section-kicker" data-i18n="launchFrom"
136
+ >SIMULATE LAUNCHING CODEX FROM</span
137
+ >
138
+ <h2 id="current-path"></h2>
139
+ </div>
140
+ <div class="view-toggle" role="group" aria-label="View">
141
+ <button id="map-view" class="selected" data-i18n="map">
142
+ Instruction map</button
143
+ ><button id="compare-view" data-i18n="compare">
144
+ Compare directories
145
+ </button>
146
+ </div>
147
+ </div>
148
+ <div class="scenarios" id="scenarios">
149
+ <span data-i18n="try">TRY A SCENARIO</span
150
+ ><button data-scenario="override" data-i18n="scenarioOverride">
151
+ 01 · Hidden override</button
152
+ ><button data-scenario="budget" data-i18n="scenarioBudget">
153
+ 02 · Budget cutoff</button
154
+ ><button data-scenario="empty" data-i18n="scenarioEmpty">
155
+ 03 · Empty override
156
+ </button>
157
+ </div>
158
+ <div class="metrics">
159
+ <div>
160
+ <span data-i18n="loaded">FILES CONTRIBUTING</span
161
+ ><strong id="metric-loaded"></strong
162
+ ><small data-i18n="loadedHint">in discovery order</small>
163
+ </div>
164
+ <div>
165
+ <span data-i18n="shadowed">FILES SHADOWED</span
166
+ ><strong id="metric-shadowed"></strong
167
+ ><small data-i18n="shadowedHint">same-directory alternatives</small>
168
+ </div>
169
+ <div class="budget-metric">
170
+ <span data-i18n="budgetLabel">PROJECT BYTE BUDGET</span
171
+ ><strong id="metric-budget"></strong>
172
+ <div class="meter"><div id="meter-fill"></div></div>
173
+ </div>
174
+ </div>
175
+ <div class="config-row">
176
+ <label
177
+ ><span data-i18n="limit">Byte limit</span
178
+ ><input
179
+ id="budget"
180
+ type="number"
181
+ min="0"
182
+ max="1048576"
183
+ step="1"
184
+ aria-label="Project byte limit" /></label
185
+ ><button
186
+ id="reset-budget"
187
+ class="text-button"
188
+ data-i18n="defaultLimit"
189
+ >
190
+ Reset to 32 KiB
191
+ </button>
192
+ <details class="settings">
193
+ <summary data-i18n="fallbackSettings">Fallback filenames</summary>
194
+ <div>
195
+ <label for="fallbacks" data-i18n="fallbackLabel"
196
+ >Ordered, comma-separated filenames</label
197
+ ><input
198
+ id="fallbacks"
199
+ placeholder="TEAM_GUIDE.md, .agents.md"
200
+ /><button id="apply-fallbacks" class="primary" data-i18n="apply">
201
+ Apply
202
+ </button>
203
+ <p data-i18n="fallbackHint">
204
+ Set names before opening a folder. Re-import to read newly added
205
+ filenames.
206
+ </p>
207
+ </div>
208
+ </details>
209
+ <span class="model-hint" data-i18n="budgetHint"
210
+ >Content bytes · global guidance counted separately</span
211
+ >
212
+ </div>
213
+ <div id="findings" class="findings" aria-live="polite"></div>
214
+ <section id="map-panel">
215
+ <div class="section-heading">
216
+ <h3 data-i18n="chain">Instruction chain</h3>
217
+ <span data-i18n="chainHint"
218
+ >Broader guidance → closer guidance</span
219
+ >
220
+ </div>
221
+ <div id="chain" class="chain"></div>
222
+ <details id="ignored-panel" class="ignored-panel">
223
+ <summary id="ignored-title"></summary>
224
+ <div id="ignored"></div>
225
+ </details>
226
+ </section>
227
+ <section id="compare-panel" hidden>
228
+ <div class="compare-heading">
229
+ <h3 data-i18n="compareTo">Compare with</h3>
230
+ <select
231
+ id="compare-target"
232
+ aria-label="Comparison directory"
233
+ ></select>
234
+ </div>
235
+ <div id="comparison"></div>
236
+ </section>
237
+ <section class="preview">
238
+ <div class="preview-toolbar">
239
+ <div>
240
+ <span class="code-symbol" aria-hidden="true">&lt;/&gt;</span>
241
+ <h3 data-i18n="preview">Assembled instruction preview</h3>
242
+ </div>
243
+ <button id="copy-preview" class="quiet" data-i18n="copy">
244
+ Copy text
245
+ </button>
246
+ </div>
247
+ <div
248
+ id="preview-content"
249
+ class="code-preview"
250
+ tabindex="0"
251
+ aria-label="Assembled instruction text"
252
+ ></div>
253
+ <div class="preview-footer">
254
+ <span id="preview-footnote"></span
255
+ ><span data-i18n="readOnly">READ ONLY</span>
256
+ </div>
257
+ </section>
258
+ <details class="boundaries">
259
+ <summary data-i18n="boundaries">
260
+ What this map can and cannot tell you
261
+ </summary>
262
+ <p data-i18n="boundaryText">
263
+ This is a static discovery preview for one trusted project. It does
264
+ not inspect a running session, resolve semantic conflicts, or
265
+ include system prompts, skills, MCP, thread/account instructions or
266
+ extra environments. Set the budget, fallback names and root to match
267
+ your setup. A later file is added to the chain; it does not erase an
268
+ earlier file.
269
+ </p>
270
+ <p data-i18n="boundaryImport">
271
+ Folder import treats the selected folder as an explicit project
272
+ root. The CLI detects the nearest .git marker, including nested
273
+ repositories, and skips symlinks with a warning. Browser folder
274
+ pickers do not expose symlink metadata. Global files are only read
275
+ by the CLI with --global.
276
+ </p>
277
+ <p>
278
+ <a
279
+ href="https://github.com/Han-1413141/codex-context-map/blob/main/docs/semantics.md"
280
+ target="_blank"
281
+ rel="noopener noreferrer"
282
+ data-i18n="semanticsLink"
283
+ >Read the versioned model and its limits ↗</a
284
+ >
285
+ </p>
286
+ </details>
287
+ </main>
288
+ </div>
289
+ <footer class="page-footer">
290
+ <span
291
+ >Codex Context Map <span class="footer-dot">/</span> MIT License
292
+ <span class="footer-dot">/</span>
293
+ <span data-i18n="independent"
294
+ >Independent project. Not affiliated with OpenAI.</span
295
+ ></span
296
+ ><a
297
+ href="https://github.com/Han-1413141/codex-context-map/issues"
298
+ target="_blank"
299
+ rel="noopener noreferrer"
300
+ data-i18n="feedback"
301
+ >Found a mismatch? Report it ↗</a
302
+ >
303
+ </footer>
304
+ <input id="folder-input" type="file" webkitdirectory multiple hidden />
305
+ <input id="json-input" type="file" accept=".json,application/json" hidden />
306
+ <noscript
307
+ >This interactive map requires JavaScript. Your instruction files are
308
+ processed locally, without network requests.</noscript
309
+ >
310
+ <script>
311
+ /* SCRIPT */
312
+ </script>
313
+ </body>
314
+ </html>
package/src/core.js ADDED
@@ -0,0 +1,314 @@
1
+ /** Pure discovery model shared by the CLI and the offline browser report. */
2
+ export const ENGINE_REF = "50d77959bf927293c4b5ddcca81d05331ae582ea";
3
+ export const DEFAULT_BUDGET = 32768;
4
+ export const MAX_BUDGET = 1048576;
5
+ export const MAX_FILES = 2000;
6
+ export const MAX_DIRECTORIES = 12000;
7
+ export const MAX_SNAPSHOT_BYTES = 8 * 1048576;
8
+ const encoder = new TextEncoder();
9
+ const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
10
+ // Rust str::trim uses Unicode White_Space; JavaScript trim also removes BOM.
11
+ const hasText = (text) =>
12
+ /[^\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/u.test(
13
+ text,
14
+ );
15
+
16
+ export function byteLength(text) {
17
+ return encoder.encode(text).length;
18
+ }
19
+
20
+ export function normalizePath(input) {
21
+ if (
22
+ typeof input !== "string" ||
23
+ input.length > 4096 ||
24
+ /[\x00-\x1f\x7f]/.test(input)
25
+ )
26
+ throw new Error("Invalid relative path.");
27
+ const portable = input.replace(/\\/g, "/");
28
+ if (portable.startsWith("/") || /^[a-z]:/i.test(portable))
29
+ throw new Error("Paths must be relative to the project root.");
30
+ const path = portable.replace(/^\.\//, "").replace(/\/$/, "");
31
+ if (path === "" || path === ".") return ".";
32
+ if (
33
+ path.startsWith("/") ||
34
+ /^[a-z]:/i.test(path) ||
35
+ path.split("/").some((p) => !p || p === ".." || p === ".")
36
+ ) {
37
+ throw new Error(
38
+ "Paths must be relative to the project root, without .. segments.",
39
+ );
40
+ }
41
+ return path;
42
+ }
43
+
44
+ export function parentPath(path) {
45
+ const i = path.lastIndexOf("/");
46
+ return i < 0 ? "." : path.slice(0, i);
47
+ }
48
+ export function baseName(path) {
49
+ return path.split("/").at(-1);
50
+ }
51
+ export function ancestors(path) {
52
+ path = normalizePath(path);
53
+ return path === "."
54
+ ? ["."]
55
+ : [
56
+ ".",
57
+ ...path
58
+ .split("/")
59
+ .map((_, i, parts) => parts.slice(0, i + 1).join("/")),
60
+ ];
61
+ }
62
+
63
+ export function candidateNames(fallbackNames = []) {
64
+ if (!Array.isArray(fallbackNames) || fallbackNames.length > 30)
65
+ throw new Error("Use at most 30 fallback filenames.");
66
+ for (const name of fallbackNames) {
67
+ if (
68
+ typeof name !== "string" ||
69
+ !name.trim() ||
70
+ name === "." ||
71
+ name === ".." ||
72
+ /[/\\:\x00-\x1f\x7f]/.test(name)
73
+ ) {
74
+ throw new Error(
75
+ "Fallbacks must be plain filenames, such as TEAM_GUIDE.md.",
76
+ );
77
+ }
78
+ }
79
+ return [...new Set(["AGENTS.override.md", "AGENTS.md", ...fallbackNames])];
80
+ }
81
+
82
+ export function validateBudget(value) {
83
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_BUDGET)
84
+ throw new Error("Budget must be an integer from 0 to 1048576 bytes.");
85
+ return value;
86
+ }
87
+
88
+ /** Validate untrusted snapshots before they reach the model or DOM. */
89
+ export function validateSnapshot(input) {
90
+ if (
91
+ !input ||
92
+ input.schemaVersion !== 1 ||
93
+ !Array.isArray(input.files) ||
94
+ !Array.isArray(input.directories)
95
+ ) {
96
+ throw new Error(
97
+ "Expected a Codex Context Map snapshot (schemaVersion: 1).",
98
+ );
99
+ }
100
+ if (
101
+ input.files.length > MAX_FILES ||
102
+ input.directories.length > MAX_DIRECTORIES
103
+ )
104
+ throw new Error("Snapshot exceeds the file or directory limit.");
105
+ const settings = {
106
+ maxBytes: validateBudget(input.settings?.maxBytes ?? DEFAULT_BUDGET),
107
+ fallbackNames: input.settings?.fallbackNames ?? [],
108
+ };
109
+ candidateNames(settings.fallbackNames);
110
+ const ids = new Set();
111
+ let size = 0;
112
+ const files = input.files.map((file) => {
113
+ if (!file || typeof file.content !== "string")
114
+ throw new Error("Every instruction file needs text content.");
115
+ const path = normalizePath(file.path);
116
+ if (path === ".") throw new Error("An instruction file needs a filename.");
117
+ const scope = file.scope ?? "project";
118
+ if (!["global", "project"].includes(scope))
119
+ throw new Error("Unknown file scope.");
120
+ if (
121
+ scope === "global" &&
122
+ !["AGENTS.md", "AGENTS.override.md"].includes(path)
123
+ )
124
+ throw new Error("Invalid global instruction filename.");
125
+ const id = `${scope}:${path}`;
126
+ if (ids.has(id)) throw new Error(`Duplicate instruction file: ${path}`);
127
+ ids.add(id);
128
+ size += byteLength(file.content);
129
+ if (size > MAX_SNAPSHOT_BYTES || byteLength(file.content) > MAX_BUDGET)
130
+ throw new Error("Instruction content exceeds the import size limit.");
131
+ return { path, scope, content: file.content };
132
+ });
133
+ const directories = new Set([".", ...input.directories.map(normalizePath)]);
134
+ for (const f of files.filter((f) => f.scope === "project"))
135
+ for (const p of ancestors(parentPath(f.path))) directories.add(p);
136
+ if (directories.size > MAX_DIRECTORIES)
137
+ throw new Error("Too many directories.");
138
+ const defaultTarget = normalizePath(input.defaultTarget ?? ".");
139
+ if (!directories.has(defaultTarget))
140
+ throw new Error("The selected directory is missing from this snapshot.");
141
+ const projectRoots = (input.projectRoots ?? ["."]).map(normalizePath);
142
+ if (projectRoots.some((p) => !directories.has(p)))
143
+ throw new Error("Project root is missing from the directory list.");
144
+ return {
145
+ schemaVersion: 1,
146
+ engineRef: ENGINE_REF,
147
+ projectName: String(input.projectName ?? "project")
148
+ .replace(/[\x00-\x1f\x7f]/g, "")
149
+ .slice(0, 120),
150
+ defaultTarget,
151
+ settings,
152
+ projectRoots: [...new Set(projectRoots)],
153
+ directories: [...directories].sort(),
154
+ files,
155
+ warnings: Array.isArray(input.warnings)
156
+ ? input.warnings.slice(0, 100).map((w) => String(w).slice(0, 500))
157
+ : [],
158
+ };
159
+ }
160
+
161
+ /** Simulate one trusted project's initial discovery. Does not evaluate prose. */
162
+ export function resolveContext(
163
+ snapshot,
164
+ target = snapshot.defaultTarget,
165
+ options = {},
166
+ ) {
167
+ target = normalizePath(target);
168
+ if (!snapshot.directories.includes(target))
169
+ throw new Error(`Unknown directory: ${target}`);
170
+ const budget = validateBudget(options.maxBytes ?? snapshot.settings.maxBytes);
171
+ const names = candidateNames(
172
+ options.fallbackNames ?? snapshot.settings.fallbackNames,
173
+ );
174
+ const allAncestors = ancestors(target);
175
+ const root = [...allAncestors]
176
+ .reverse()
177
+ .find((p) => snapshot.projectRoots.includes(p));
178
+ const chain =
179
+ root === undefined
180
+ ? [target]
181
+ : allAncestors.slice(allAncestors.indexOf(root));
182
+ const chainSet = new Set(chain);
183
+ const records = snapshot.files.map((f) => ({
184
+ ...f,
185
+ id: `${f.scope}:${f.path}`,
186
+ directory: f.scope === "global" ? "$CODEX_HOME" : parentPath(f.path),
187
+ bytes: byteLength(f.content),
188
+ usedBytes: 0,
189
+ includedText: "",
190
+ truncated: false,
191
+ status: "outside",
192
+ reason: "outside",
193
+ }));
194
+ const selected = [];
195
+ const findings = [];
196
+ const globals = records.filter((f) => f.scope === "global");
197
+ let globalChosen;
198
+ for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
199
+ const file = globals.find((f) => f.path === name);
200
+ if (!file) continue;
201
+ if (globalChosen) {
202
+ file.status = "shadowed";
203
+ file.reason = "higher-priority";
204
+ file.shadowedBy = globalChosen.id;
205
+ } else if (!hasText(file.content)) {
206
+ file.status = "empty";
207
+ file.reason = "empty-global";
208
+ } else {
209
+ globalChosen = file;
210
+ file.status = "loaded";
211
+ file.reason = "global";
212
+ file.usedBytes = file.bytes;
213
+ file.includedText = file.content;
214
+ selected.push(file);
215
+ }
216
+ }
217
+ let remaining = budget;
218
+ let selectedBytes = 0;
219
+ for (const directory of chain) {
220
+ const here = records.filter(
221
+ (f) => f.scope === "project" && f.directory === directory,
222
+ );
223
+ const candidates = names
224
+ .map((name) => here.find((f) => baseName(f.path) === name))
225
+ .filter(Boolean);
226
+ for (const file of here.filter((f) => !candidates.includes(f))) {
227
+ file.status = "unrecognized";
228
+ file.reason = "not-configured";
229
+ }
230
+ const chosen = candidates[0];
231
+ if (!chosen) continue;
232
+ for (const file of candidates.slice(1)) {
233
+ file.status = "shadowed";
234
+ file.reason = "higher-priority";
235
+ file.shadowedBy = chosen.id;
236
+ }
237
+ selectedBytes += chosen.bytes;
238
+ if (remaining === 0) {
239
+ chosen.status = "excluded";
240
+ chosen.reason = "budget";
241
+ selected.push(chosen);
242
+ continue;
243
+ }
244
+ const data = encoder.encode(chosen.content).slice(0, remaining);
245
+ const text = decoder.decode(data);
246
+ chosen.truncated = data.length < chosen.bytes;
247
+ if (!hasText(text)) {
248
+ chosen.status = "empty";
249
+ chosen.reason = "empty-project";
250
+ if (candidates.length > 1)
251
+ findings.push({ type: "empty-override", path: chosen.path });
252
+ } else {
253
+ chosen.usedBytes = data.length;
254
+ chosen.includedText = text;
255
+ remaining -= data.length;
256
+ chosen.status = data.length < chosen.bytes ? "partial" : "loaded";
257
+ chosen.reason = chosen.status === "partial" ? "budget" : "selected";
258
+ }
259
+ selected.push(chosen);
260
+ }
261
+ for (const file of records) {
262
+ if (file.truncated || file.status === "excluded")
263
+ findings.push({
264
+ type: file.status === "excluded" ? "excluded" : "partial",
265
+ path: file.path,
266
+ });
267
+ if (file.scope === "project" && !chainSet.has(file.directory))
268
+ file.reason = "outside";
269
+ }
270
+ const projectText = selected
271
+ .filter((f) => f.scope === "project" && f.includedText)
272
+ .map((f) => f.includedText)
273
+ .join("\n\n");
274
+ const globalText = globalChosen?.includedText ?? "";
275
+ const text =
276
+ globalText && projectText
277
+ ? `${globalText}\n\n--- project-doc ---\n\n${projectText}`
278
+ : globalText || projectText;
279
+ return {
280
+ target,
281
+ budget,
282
+ usedBytes: budget - remaining,
283
+ selectedBytes,
284
+ globalBytes: globalChosen?.usedBytes ?? 0,
285
+ chain,
286
+ selected,
287
+ records,
288
+ findings,
289
+ text,
290
+ loadedCount: selected.filter((f) => f.usedBytes > 0).length,
291
+ shadowedCount: records.filter((f) => f.status === "shadowed").length,
292
+ hasCutoff: selected.some((f) => f.truncated || f.status === "excluded"),
293
+ };
294
+ }
295
+
296
+ export function compareContexts(before, after) {
297
+ const a = new Map(
298
+ before.selected.filter((f) => f.usedBytes > 0).map((f) => [f.id, f]),
299
+ );
300
+ const b = new Map(
301
+ after.selected.filter((f) => f.usedBytes > 0).map((f) => [f.id, f]),
302
+ );
303
+ return {
304
+ added: [...b.values()].filter((f) => !a.has(f.id)).map((f) => f.path),
305
+ removed: [...a.values()].filter((f) => !b.has(f.id)).map((f) => f.path),
306
+ changed: [...b.values()]
307
+ .filter((f) => a.has(f.id) && a.get(f.id).includedText !== f.includedText)
308
+ .map((f) => f.path),
309
+ shared: [...b.values()]
310
+ .filter((f) => a.has(f.id) && a.get(f.id).includedText === f.includedText)
311
+ .map((f) => f.path),
312
+ byteDelta: after.usedBytes - before.usedBytes,
313
+ };
314
+ }
package/src/demo.js ADDED
@@ -0,0 +1,60 @@
1
+ import { validateSnapshot } from "./core.js";
2
+
3
+ export function createDemo() {
4
+ const files = [
5
+ [
6
+ "AGENTS.md",
7
+ "# Northstar workspace\n\nWe build a small commerce platform with a web storefront and payment services.\n\n## Working agreements\n- Keep each change focused on the requested behavior.\n- Use pnpm for JavaScript packages.\n- Run the relevant tests before opening a pull request.\n- Explain behavior changes in the pull request description.\n- Keep credentials out of source control and examples.\n\n## Structure\n- apps/web contains the storefront.\n- services/payments handles payment orchestration.\n- docs contains developer-facing documentation.\n",
8
+ ],
9
+ [
10
+ "apps/AGENTS.md",
11
+ "# Application conventions\n\n- Use TypeScript for new application code.\n- Build accessible controls with visible keyboard focus.\n- Test loading, empty, success, and error states.\n- Prefer existing components before adding a new dependency.\n",
12
+ ],
13
+ [
14
+ "apps/web/AGENTS.md",
15
+ "# Storefront\n\n- Run pnpm --filter web test for this package.\n- Keep checkout components under src/checkout.\n- Preserve cart state when navigating between product pages.\n- Format monetary amounts using the currency locale.\n",
16
+ ],
17
+ [
18
+ "services/AGENTS.md",
19
+ "# Service conventions\n\n- Keep HTTP handlers small and validate request bodies at the boundary.\n- Use structured logs without secrets or personal payment details.\n- Document failure modes next to retry logic.\n- Every background job must support idempotent retries.\n",
20
+ ],
21
+ [
22
+ "services/payments/AGENTS.md",
23
+ "# Payments (base)\n\n- Use the old mock gateway.\n- Run pnpm test for the payment package.\n\nThis file is shadowed by AGENTS.override.md in the same directory.\n",
24
+ ],
25
+ [
26
+ "services/payments/AGENTS.override.md",
27
+ "# Payments migration\n\n## Gateway contract\n- Use the sandbox adapter for the new gateway.\n- Verify webhook signatures before parsing business events.\n- Store an idempotency key before sending a payment request.\n- Treat a gateway timeout as an unknown result; reconcile before retrying.\n\n## Tests\n- Run pnpm --filter payments test:contract for this migration.\n- Cover duplicate webhook delivery and out-of-order events.\n- Check the refund flow with both partial and complete refunds.\n\n## Release checks\n- Verify that cancellation does not create a second charge.\n- Keep migration notes alongside the adapter.\n- Record the contract-test result in the pull request.\n\nCritical instructions at the bottom can disappear when the byte budget is exhausted.\n",
28
+ ],
29
+ [
30
+ "docs/TEAM_GUIDE.md",
31
+ "# Documentation\n\n- Use runnable examples.\n- Link to the official API reference.\n- Keep the English and Chinese quickstarts in sync.\n",
32
+ ],
33
+ [
34
+ "tools/lab/AGENTS.md",
35
+ "# Lab conventions\n\n- Use Python 3.12 for experimental scripts.\n- Keep experiments isolated from the application packages.\n",
36
+ ],
37
+ ["tools/lab/AGENTS.override.md", ""],
38
+ ].map(([path, content]) => ({ path, content, scope: "project" }));
39
+ return validateSnapshot({
40
+ schemaVersion: 1,
41
+ projectName: "northstar",
42
+ defaultTarget: "services/payments",
43
+ projectRoots: ["."],
44
+ settings: { maxBytes: 32768, fallbackNames: ["TEAM_GUIDE.md"] },
45
+ directories: [
46
+ ".",
47
+ "apps",
48
+ "apps/web",
49
+ "apps/web/src",
50
+ "apps/web/src/checkout",
51
+ "services",
52
+ "services/payments",
53
+ "services/search",
54
+ "docs",
55
+ "tools",
56
+ "tools/lab",
57
+ ],
58
+ files,
59
+ });
60
+ }