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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +108 -0
- package/bin/codex-context-map.js +148 -0
- package/package.json +52 -0
- package/site/app.js +581 -0
- package/site/style.css +1226 -0
- package/site/template.html +314 -0
- package/src/core.js +314 -0
- package/src/demo.js +60 -0
- package/src/report.js +25 -0
- package/src/scan.js +170 -0
|
@@ -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"></></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
|
+
}
|