@wxxb789/anc 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/astro.config.mjs +255 -0
  4. package/bin/anc.mjs +798 -0
  5. package/package.json +93 -0
  6. package/public/_headers +45 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/favicon.svg +9 -0
  9. package/public/fonts/Temml.woff2 +0 -0
  10. package/scripts/build-snapshot.js +69 -0
  11. package/scripts/build-wasm.js +67 -0
  12. package/scripts/build-workspace.js +50 -0
  13. package/scripts/copy-snapshot.js +48 -0
  14. package/scripts/copy-wasm.js +57 -0
  15. package/scripts/emit-redirects.js +64 -0
  16. package/scripts/generate-corpus.js +421 -0
  17. package/scripts/init-repository.js +249 -0
  18. package/scripts/load-config.js +975 -0
  19. package/scripts/markdown-to-artifact.js +1286 -0
  20. package/scripts/preview-site.js +364 -0
  21. package/scripts/publish-set-review.js +157 -0
  22. package/scripts/resolve-links.js +640 -0
  23. package/scripts/run-pagefind.js +125 -0
  24. package/scripts/scan-residue.js +1016 -0
  25. package/scripts/scan-secrets.js +268 -0
  26. package/scripts/seed-gitignore.js +410 -0
  27. package/scripts/snapshot-rows.js +205 -0
  28. package/scripts/validate-content.js +95 -0
  29. package/scripts/vendor-provenance.js +307 -0
  30. package/scripts/verify-output-inventory.js +307 -0
  31. package/scripts/write-report.js +351 -0
  32. package/scripts/write-snapshot.js +136 -0
  33. package/src/components/CollectionExplorer.astro +149 -0
  34. package/src/components/FacetIndex.astro +45 -0
  35. package/src/components/LinkedNotes.astro +94 -0
  36. package/src/components/NoteGraph.astro +434 -0
  37. package/src/components/NoteList.astro +54 -0
  38. package/src/components/SiteMetadata.astro +143 -0
  39. package/src/components/TableOfContents.astro +69 -0
  40. package/src/components/TagBrowser.astro +70 -0
  41. package/src/layouts/Layout.astro +305 -0
  42. package/src/lib/artifact-source.js +72 -0
  43. package/src/lib/build-bindings.js +11 -0
  44. package/src/lib/collection-navigation.js +145 -0
  45. package/src/lib/content.js +92 -0
  46. package/src/lib/diagram-mode.js +18 -0
  47. package/src/lib/graph-layout.js +219 -0
  48. package/src/lib/graph-selection.js +84 -0
  49. package/src/lib/graph.js +42 -0
  50. package/src/lib/link-resolution.js +389 -0
  51. package/src/lib/markdown.js +1469 -0
  52. package/src/lib/math-mode.js +31 -0
  53. package/src/lib/math-output.js +131 -0
  54. package/src/lib/math.js +165 -0
  55. package/src/lib/mermaid-environment.js +475 -0
  56. package/src/lib/mermaid-render.js +769 -0
  57. package/src/lib/preview-model.js +96 -0
  58. package/src/lib/relations.js +242 -0
  59. package/src/lib/rendered-marker.js +55 -0
  60. package/src/lib/route-path.js +192 -0
  61. package/src/lib/routes.js +342 -0
  62. package/src/lib/schema.js +578 -0
  63. package/src/lib/site.js +578 -0
  64. package/src/lib/snapshot-contract.js +216 -0
  65. package/src/lib/snapshot-operations.js +147 -0
  66. package/src/lib/snapshot-queries.js +216 -0
  67. package/src/lib/snapshot-reader.js +210 -0
  68. package/src/lib/snapshot.js +233 -0
  69. package/src/lib/sqlite-warning.js +22 -0
  70. package/src/lib/sqlite.js +14 -0
  71. package/src/lib/tag-browser-model.js +18 -0
  72. package/src/lib/translations.js +533 -0
  73. package/src/lib/wasm-asset.js +47 -0
  74. package/src/lib/worker-protocol.js +93 -0
  75. package/src/pages/404.astro +35 -0
  76. package/src/pages/about.astro +100 -0
  77. package/src/pages/collections/[slug].astro +36 -0
  78. package/src/pages/collections/index.astro +33 -0
  79. package/src/pages/graph.astro +64 -0
  80. package/src/pages/index.astro +33 -0
  81. package/src/pages/notes/[slug].astro +509 -0
  82. package/src/pages/privacy.astro +121 -0
  83. package/src/pages/private.astro +59 -0
  84. package/src/pages/recent.astro +33 -0
  85. package/src/pages/robots.txt.js +15 -0
  86. package/src/pages/rss.xml.js +21 -0
  87. package/src/pages/sitemap.xml.js +20 -0
  88. package/src/pages/tags/[tag].astro +56 -0
  89. package/src/pages/tags/index.astro +37 -0
  90. package/src/scripts/code-copy.js +45 -0
  91. package/src/scripts/diagram-disabled.js +16 -0
  92. package/src/scripts/diagram.js +322 -0
  93. package/src/scripts/graph-client.js +222 -0
  94. package/src/scripts/graph-draw.js +166 -0
  95. package/src/scripts/link-preview.js +334 -0
  96. package/src/scripts/math-disabled.js +18 -0
  97. package/src/scripts/math.js +133 -0
  98. package/src/scripts/preferences.js +123 -0
  99. package/src/scripts/search-dialog.js +651 -0
  100. package/src/scripts/snapshot-client.js +157 -0
  101. package/src/scripts/snapshot-worker.js +341 -0
  102. package/src/scripts/tag-browser.js +153 -0
  103. package/src/scripts/theme-init.js +35 -0
  104. package/src/scripts/toc-active.js +38 -0
  105. package/src/styles/code.css +152 -0
  106. package/src/styles/diagram.css +149 -0
  107. package/src/styles/global.css +1739 -0
  108. package/src/styles/graph.css +317 -0
  109. package/src/styles/math.css +376 -0
  110. package/src/styles/tokens.css +171 -0
  111. package/tsconfig.json +25 -0
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Build gate for the generated content artifact.
3
+ *
4
+ * Runs before `astro build` so an invalid or privacy-violating artifact fails
5
+ * with a precise message instead of a bundler stack trace.
6
+ *
7
+ * Everything downstream of `astro build` that can *throw* is exercised here
8
+ * too. The build is a `&&` chain — validate, snapshot, build, redirects, Pagefind
9
+ * — so a throw in a later link leaves a `dist/` that is already written and now
10
+ * permanently incomplete, which `pnpm run preview` serves happily. Running those
11
+ * computations against the same artifact first means the failure happens while
12
+ * `dist/` is still the last known good build.
13
+ */
14
+ import { fileURLToPath } from 'node:url';
15
+ import { createHash } from 'node:crypto';
16
+ import { REDIRECT_RULES, collectionFacets, renderRedirects, tagFacets } from "../src/lib/routes.js";
17
+ import { ARTIFACT_PATH, loadArtifact, readArtifact } from "../src/lib/artifact-source.js";
18
+ /**
19
+ * The artifact's content version: a hash of its bytes, so anything generated
20
+ * from it can be tied back to the exact input (requirements section 20). The
21
+ * artifact is already public, so its digest discloses nothing further.
22
+ *
23
+ * Exported because `emit-redirects.ts` stamps the same value into the file it
24
+ * writes, and two independent hashes of the same bytes is one rename away from
25
+ * two different stamps.
26
+ */
27
+ export function contentVersion(source) {
28
+ return `sha256:${createHash('sha256').update(source).digest('hex')}`;
29
+ }
30
+ /**
31
+ * Run every artifact-derived computation that `astro build` and the steps after
32
+ * it would otherwise run for the first time, and discard the results.
33
+ *
34
+ * `tagFacets` and `collectionFacets` throw on a label with no URL-safe route key
35
+ * and on two labels colliding onto one key. Astro reaches them inside
36
+ * `getStaticPaths()`, which is after `dist/` has been opened, so the failure
37
+ * lands on a half-written directory. `renderRedirects` runs later still, in a
38
+ * separate `&&` link after `astro build` has finished writing — and if it
39
+ * throws, the chain stops before `pagefind --site dist` ever runs, leaving a
40
+ * `dist/` with no search index that `pnpm run preview` serves happily.
41
+ *
42
+ * Running all three here moves every one of those failures ahead of the first
43
+ * write, so a bad artifact leaves the previous good build intact.
44
+ *
45
+ * The results are deliberately unused: this is a gate, and recomputing what the
46
+ * pages compute would make it a second source of truth for the route set.
47
+ * `renderRedirects` has no failure path today — the rule set is empty and the
48
+ * Cloudflare rule-count ceiling went with the legacy `/<slug>/` rules. It is
49
+ * called anyway, because what this guards is the *ordering*: the emitter is a
50
+ * separate link in the `&&` chain that runs after `dist/` is written, so the
51
+ * day it can fail again is the day the guarantee is needed, and a call that
52
+ * cannot throw costs a microsecond.
53
+ */
54
+ export function checkDerivedRoutes(artifact, version) {
55
+ tagFacets(artifact.entries);
56
+ collectionFacets(artifact.entries);
57
+ renderRedirects(REDIRECT_RULES, { schema: artifact.version, content: version });
58
+ }
59
+ /**
60
+ * Every gate this script runs, as one call, so a caller that is not the shell
61
+ * enforces exactly what the shell does.
62
+ *
63
+ * `bin/anc.mjs` runs the same chain in-process, and the failure
64
+ * mode of letting it restate the steps is that the packaged build validates less
65
+ * than this repository's own — a gate enforced on us and not on the people we
66
+ * ship to. Calling one function means there is no second list to drift.
67
+ *
68
+ * @throws {ContentValidationError} or whichever error the derived-route
69
+ * computations raise, unchanged: the caller decides how to report it.
70
+ */
71
+ export function validateBuildInputs(artifactPath = ARTIFACT_PATH) {
72
+ const artifact = loadArtifact(artifactPath);
73
+ checkDerivedRoutes(artifact, contentVersion(readArtifact(artifactPath)));
74
+ return artifact;
75
+ }
76
+ /**
77
+ * @param artifactPath Artifact to validate, repository-relative. Defaults to
78
+ * whichever one this build selected — `src/data/content.json` unless
79
+ * `CONTENT_ARTIFACT` names another. A caller passes a path so it can gate a
80
+ * candidate artifact without writing over `src/data/content.json`, which is
81
+ * producer-owned generated content.
82
+ */
83
+ function main(artifactPath = ARTIFACT_PATH) {
84
+ try {
85
+ const artifact = validateBuildInputs(artifactPath);
86
+ console.log(`content ok: version=${artifact.version} entries=${artifact.entries.length}`);
87
+ return 0;
88
+ }
89
+ catch (error) {
90
+ console.error(error instanceof Error ? error.message : String(error));
91
+ return 1;
92
+ }
93
+ }
94
+ if (process.argv[1] === fileURLToPath(import.meta.url))
95
+ process.exit(main(process.argv[2]));
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Build-minted provenance for JavaScript chunks containing dependency code only.
3
+ *
4
+ * The final residue scan must keep reading every first-party chunk, while the
5
+ * selected client runtimes emit dependency chunks containing byte sequences
6
+ * that resemble residue. A basename cannot prove which is which. The bundler can: its chunk
7
+ * interface carries every resolved module id before the bytes are written.
8
+ *
9
+ * The manifest lives beside, never inside, the output directory. It therefore
10
+ * reaches the scanner in the next process without becoming a public asset. It
11
+ * contains final output names only — never module ids or host paths.
12
+ */
13
+ import { createHash } from 'node:crypto';
14
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { dirname, join, resolve, sep, win32 } from 'node:path';
17
+ import { BuildFailure } from "./write-report.js";
18
+ export const VENDOR_PROVENANCE_VERSION = 2;
19
+ export const VENDOR_STATE_SUFFIX = '.publish-state';
20
+ export const VENDOR_PROVENANCE_FILE = 'vendored-chunks.json';
21
+ export const CLIENT_ASSET_PREFIX = '_astro/';
22
+ function slash(path) {
23
+ return path.replaceAll('\\', '/').replace(/\/$/, '');
24
+ }
25
+ function canonical(path) {
26
+ // A Windows absolute module ID stays absolute when inspected on POSIX; the
27
+ // host's resolve() would otherwise prefix cwd to e.g. Q:/repo/module.js.
28
+ const windowsPath = /^[a-z]:[\\/]/i.test(path) || path.startsWith('\\\\');
29
+ if (windowsPath && process.platform !== 'win32') {
30
+ return slash(win32.normalize(path)).toLowerCase();
31
+ }
32
+ const absolute = resolve(path);
33
+ const real = slash(existsSync(absolute) ? realpathSync(absolute) : absolute);
34
+ // Distinct case-sensitive POSIX directories must not receive the same grant.
35
+ return process.platform === 'win32' ? real.toLowerCase() : real;
36
+ }
37
+ /** Package owners measured in the selected Mermaid and Temml client bundles. */
38
+ export const CLIENT_RUNTIME_PACKAGES = new Set([
39
+ '@braintree/sanitize-url',
40
+ '@iconify/utils',
41
+ '@mermaid-js/parser',
42
+ '@upsetjs/venn.js',
43
+ 'cose-base',
44
+ 'cytoscape',
45
+ 'cytoscape-cose-bilkent',
46
+ 'cytoscape-fcose',
47
+ 'd3',
48
+ 'd3-array',
49
+ 'd3-axis',
50
+ 'd3-brush',
51
+ 'd3-color',
52
+ 'd3-dispatch',
53
+ 'd3-ease',
54
+ 'd3-format',
55
+ 'd3-hierarchy',
56
+ 'd3-interpolate',
57
+ 'd3-path',
58
+ 'd3-sankey',
59
+ 'd3-scale',
60
+ 'd3-scale-chromatic',
61
+ 'd3-selection',
62
+ 'd3-shape',
63
+ 'd3-time',
64
+ 'd3-time-format',
65
+ 'd3-timer',
66
+ 'd3-transition',
67
+ 'd3-zoom',
68
+ 'dagre-d3-es',
69
+ 'dayjs',
70
+ 'dompurify',
71
+ 'elkjs',
72
+ 'es-toolkit',
73
+ 'internmap',
74
+ 'katex',
75
+ 'khroma',
76
+ 'layout-base',
77
+ 'lodash-es',
78
+ 'marked',
79
+ 'mermaid',
80
+ 'roughjs',
81
+ 'stylis',
82
+ 'temml',
83
+ 'ts-dedent',
84
+ 'uuid',
85
+ ]);
86
+ /** The one generated module measured inside an otherwise-vendored Rolldown chunk. */
87
+ const TRUSTED_BUNDLER_MODULES = new Set(['\0rolldown/runtime.js']);
88
+ function packageAt(entry) {
89
+ let directory = dirname(entry);
90
+ while (true) {
91
+ const manifestPath = join(directory, 'package.json');
92
+ if (existsSync(manifestPath)) {
93
+ try {
94
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
95
+ if (typeof manifest.name === 'string')
96
+ return { root: directory, manifest, manifestPath };
97
+ }
98
+ catch (error) {
99
+ return refuse('could not read runtime package metadata: ' + String(error));
100
+ }
101
+ }
102
+ const parent = dirname(directory);
103
+ if (parent === directory)
104
+ return refuse('could not locate package metadata for a client runtime module');
105
+ directory = parent;
106
+ }
107
+ }
108
+ /**
109
+ * Canonical roots in the installed Mermaid/Temml dependency closure that this
110
+ * build is allowed to classify as vendored. Package names remain an explicit
111
+ * review list; resolution supplies the stronger fact that a module came from
112
+ * the exact package instance these runtimes load, not a same-named external tree.
113
+ */
114
+ export function clientRuntimePackageRoots(packageRoot) {
115
+ const roots = new Set();
116
+ const visited = new Set();
117
+ const visit = (name, importer, required) => {
118
+ let entry;
119
+ try {
120
+ entry = createRequire(importer).resolve(name);
121
+ }
122
+ catch (error) {
123
+ // Some browser-only packages deliberately expose no Node main. The package
124
+ // manager link beside the importing package still identifies the exact
125
+ // installed instance Vite resolves; use its manifest, never a name search
126
+ // elsewhere in the tree.
127
+ const linkedManifest = join(dirname(dirname(importer)), ...name.split('/'), 'package.json');
128
+ if (existsSync(linkedManifest))
129
+ entry = linkedManifest;
130
+ else {
131
+ if (!required)
132
+ return;
133
+ return refuse('could not resolve selected client runtime ' + name + ': ' + String(error));
134
+ }
135
+ }
136
+ const resolved = packageAt(entry);
137
+ const packageRoot = canonical(resolved.root);
138
+ if (visited.has(packageRoot))
139
+ return;
140
+ visited.add(packageRoot);
141
+ if (resolved.manifest.name !== undefined && CLIENT_RUNTIME_PACKAGES.has(resolved.manifest.name)) {
142
+ roots.add(packageRoot);
143
+ }
144
+ for (const dependency of Object.keys(resolved.manifest.dependencies ?? {})) {
145
+ visit(dependency, resolved.manifestPath, false);
146
+ }
147
+ for (const dependency of Object.keys(resolved.manifest.optionalDependencies ?? {})) {
148
+ visit(dependency, resolved.manifestPath, false);
149
+ }
150
+ };
151
+ const importer = join(resolve(packageRoot), 'package.json');
152
+ visit('mermaid', importer, true);
153
+ visit('temml', importer, true);
154
+ return [...roots].sort();
155
+ }
156
+ /** Every non-virtual module must be inside one exact resolved runtime root. */
157
+ export function isVendoredChunk(moduleIds, trustedRoots) {
158
+ if (moduleIds.length === 0)
159
+ return false;
160
+ let packages = 0;
161
+ for (const id of moduleIds) {
162
+ const withoutQuery = id.split('?')[0];
163
+ if (withoutQuery.startsWith('\0')) {
164
+ if (!TRUSTED_BUNDLER_MODULES.has(withoutQuery))
165
+ return false;
166
+ continue;
167
+ }
168
+ const modulePath = canonical(withoutQuery);
169
+ if (!trustedRoots.some((root) => modulePath === root || modulePath.startsWith(root + '/')))
170
+ return false;
171
+ packages += 1;
172
+ }
173
+ return packages > 0;
174
+ }
175
+ /** Final client chunk names whose module ownership is dependency-only. */
176
+ export function vendoredClientChunks(chunks, trustedRoots) {
177
+ return [...new Set(chunks
178
+ .filter((chunk) => chunk.fileName.startsWith(CLIENT_ASSET_PREFIX))
179
+ .filter((chunk) => isVendoredChunk(chunk.moduleIds, trustedRoots))
180
+ .map((chunk) => chunk.fileName))].sort();
181
+ }
182
+ /**
183
+ * Vite/Rolldown adapter that writes exact provenance after client chunks exist.
184
+ *
185
+ * Astro invokes output hooks for more than one bundle. State is isolated by
186
+ * output directory and only a bundle carrying client JavaScript can write, so a
187
+ * later server bundle cannot erase the client result. Repeated client outputs
188
+ * union rather than replace their observations.
189
+ */
190
+ export function vendorProvenancePlugin(packageRoot) {
191
+ const byOutDir = new Map();
192
+ let trustedRoots;
193
+ return {
194
+ name: 'publish:vendor-provenance',
195
+ apply: 'build',
196
+ enforce: 'post',
197
+ writeBundle(options, bundle) {
198
+ const clientChunks = Object.values(bundle).filter((output) => output.type === 'chunk' &&
199
+ output.fileName.startsWith(CLIENT_ASSET_PREFIX) &&
200
+ Array.isArray(output.moduleIds));
201
+ if (clientChunks.length === 0)
202
+ return;
203
+ if (options.dir === undefined)
204
+ return refuse('the client bundle declares no output directory');
205
+ const outDir = resolve(options.dir);
206
+ const observed = byOutDir.get(outDir) ?? new Map();
207
+ byOutDir.set(outDir, observed);
208
+ for (const chunk of clientChunks) {
209
+ observed.set(chunk.fileName, { fileName: chunk.fileName, moduleIds: chunk.moduleIds });
210
+ }
211
+ trustedRoots ??= clientRuntimePackageRoots(packageRoot);
212
+ writeVendorProvenance(outDir, vendoredClientChunks([...observed.values()], trustedRoots));
213
+ },
214
+ };
215
+ }
216
+ /** The non-public, output-specific manifest paired with one output directory. */
217
+ export function vendorProvenancePath(outDir) {
218
+ return join(resolve(outDir) + VENDOR_STATE_SUFFIX, VENDOR_PROVENANCE_FILE);
219
+ }
220
+ function refuse(detail) {
221
+ throw new BuildFailure('vendor-provenance-invalid', 'vendored chunk provenance is invalid — rebuild the site before scanning it', detail);
222
+ }
223
+ function validateChunkPath(path) {
224
+ if (typeof path !== 'string' || !/^_astro\/[^/]+\.js$/.test(path)) {
225
+ return refuse('vendored chunk provenance carries an invalid output name: ' + JSON.stringify(path));
226
+ }
227
+ return path;
228
+ }
229
+ function sha256(path) {
230
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
231
+ }
232
+ function validateGrant(value) {
233
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
234
+ return refuse('vendored chunk provenance carries a non-object grant');
235
+ }
236
+ const grant = value;
237
+ if (Object.keys(grant).sort().join(',') !== 'file,sha256') {
238
+ return refuse('vendored chunk provenance grant does not have exactly file and sha256');
239
+ }
240
+ const file = validateChunkPath(grant['file']);
241
+ if (typeof grant['sha256'] !== 'string' || !/^[a-f0-9]{64}$/.test(grant['sha256'])) {
242
+ return refuse('vendored chunk provenance carries an invalid SHA-256 digest');
243
+ }
244
+ return { file, sha256: grant['sha256'] };
245
+ }
246
+ /** Write one complete, deterministic, content-bound manifest. */
247
+ export function writeVendorProvenance(outDir, chunks) {
248
+ const path = vendorProvenancePath(outDir);
249
+ const files = [...new Set(chunks.map(validateChunkPath))].sort();
250
+ const grants = files.map((file) => {
251
+ const candidate = join(outDir, ...file.split('/'));
252
+ if (!existsSync(candidate) || !statSync(candidate).isFile()) {
253
+ return refuse('cannot grant a vendored chunk that is not in this output');
254
+ }
255
+ return { file, sha256: sha256(candidate) };
256
+ });
257
+ const body = { version: VENDOR_PROVENANCE_VERSION, chunks: grants };
258
+ mkdirSync(dirname(path), { recursive: true });
259
+ const temporary = path + '.' + process.pid + '.tmp';
260
+ writeFileSync(temporary, JSON.stringify(body, null, 2) + '\n', 'utf8');
261
+ renameSync(temporary, path);
262
+ }
263
+ export function readVendorProvenance(root, options = {}) {
264
+ const path = options.path ?? vendorProvenancePath(root);
265
+ if (!existsSync(path)) {
266
+ if (options.required === true)
267
+ return refuse('the client build wrote no vendored chunk provenance');
268
+ return new Set();
269
+ }
270
+ let parsed;
271
+ try {
272
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
273
+ }
274
+ catch (error) {
275
+ return refuse('could not parse ' + path + ': ' + (error instanceof Error ? error.message : String(error)));
276
+ }
277
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) ||
278
+ Object.keys(parsed).sort().join(',') !== 'chunks,version') {
279
+ return refuse(path + ' does not have exactly the version and chunks keys');
280
+ }
281
+ const record = parsed;
282
+ if (record['version'] !== VENDOR_PROVENANCE_VERSION || !Array.isArray(record['chunks'])) {
283
+ return refuse(path + ' has an unsupported version or a non-array chunks value');
284
+ }
285
+ const grants = record['chunks'].map(validateGrant);
286
+ const files = grants.map((grant) => grant.file);
287
+ if (new Set(files).size !== files.length || [...files].sort().some((value, index) => value !== files[index])) {
288
+ return refuse(path + ' chunk names are duplicated or not sorted');
289
+ }
290
+ if (options.required === true && grants.length === 0) {
291
+ return refuse(path + ' grants no chunks to a client diagram build');
292
+ }
293
+ for (const grant of grants) {
294
+ const candidate = join(root, ...grant.file.split('/'));
295
+ if (!existsSync(candidate) || !statSync(candidate).isFile()) {
296
+ return refuse(path + ' names a file that is not in the built site');
297
+ }
298
+ const relative = candidate.slice(resolve(root).length + 1);
299
+ if (relative.split(sep).join('/') !== grant.file) {
300
+ return refuse(path + ' names a file that does not resolve inside the built site');
301
+ }
302
+ if (sha256(candidate) !== grant.sha256) {
303
+ return refuse(path + ' does not describe the bytes in this build');
304
+ }
305
+ }
306
+ return new Set(files);
307
+ }