@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,342 @@
1
+ /**
2
+ * The public route model, and the versioned redirect map derived from it.
3
+ *
4
+ * This module is the in-repo record of every public route the site owns. It is
5
+ * deliberately pure: it imports no artifact and holds no state, so every
6
+ * function here can be exercised against synthetic fixtures, and so a client
7
+ * script may import a route helper without pulling the content artifact into
8
+ * the browser bundle. Pages pass `entries` in; this module never reaches for it.
9
+ *
10
+ * Two properties are load-bearing:
11
+ *
12
+ * 1. **Route keys are stable.** A note's route is a pure function of its slug,
13
+ * and a tag's route key is a pure function of that one tag's text. Nothing
14
+ * here disambiguates by position or by corpus size, so publishing a new note
15
+ * or a new tag can never silently move an existing public URL.
16
+ * 2. **Grouping is never invented.** Tags and collections come from artifact
17
+ * fields. When the artifact carries none, the facet lists are empty and the
18
+ * index routes say so; no grouping is derived from anything else.
19
+ */
20
+ import { slug as slugify } from 'github-slugger';
21
+ import { INVISIBLE_CODE_POINTS, NOTES_SEGMENT, SLUG_CHARACTERS, WITHHELD_ROUTE, noteRoute, noteSlugFromPath, } from "./route-path.js";
22
+ /**
23
+ * Re-exported so pages have one route module to import. The definitions live in
24
+ * `route-path.ts` because `link-preview.ts` needs them in the browser, and
25
+ * importing this module there would ship the whole site map as dead code.
26
+ */
27
+ export { NOTES_SEGMENT, WITHHELD_ROUTE, noteRoute, noteSlugFromPath };
28
+ /** The top-level segment tag pages live under. Reserved by TK-01. */
29
+ export const TAGS_SEGMENT = 'tags';
30
+ /** The top-level segment collection pages live under. Reserved by TK-01. */
31
+ export const COLLECTIONS_SEGMENT = 'collections';
32
+ /** The site-wide graph route. Reserved by TK-01, filled by TK-17. */
33
+ export const GRAPH_SEGMENT = 'graph';
34
+ export function tagRoute(key) {
35
+ return `/${TAGS_SEGMENT}/${key}/`;
36
+ }
37
+ export function collectionRoute(key) {
38
+ return `/${COLLECTIONS_SEGMENT}/${key}/`;
39
+ }
40
+ /**
41
+ * The vocabulary a public route key may use.
42
+ *
43
+ * The note-slug character class from `route-path.ts` plus `_`. A tag is free
44
+ * text in any script: `笔记`, `हिन्दी`, and `tiếng việt` must all produce a
45
+ * readable key. Combining marks are admitted for exactly that reason — Devanagari
46
+ * vowel signs and Vietnamese tone marks are `\p{M}`, and excluding them would
47
+ * reject those languages outright.
48
+ *
49
+ * What it excludes is what a public route segment must never carry: a leading or
50
+ * trailing hyphen, a doubled hyphen, and anything that is not a letter, digit,
51
+ * mark, or underscore.
52
+ *
53
+ * Underscore is a word character here, not a separator, so it is neither
54
+ * trimmed nor collapsed: `snake_case_tag` and `_ops_` are readable keys a
55
+ * reader would recognize, and `github-slugger` preserves them from the label
56
+ * rather than manufacturing them from punctuation. The hyphen rules exist
57
+ * because a hyphen is what the slugger *substitutes* for stripped characters,
58
+ * which is how a gap becomes a separator nobody wrote.
59
+ */
60
+ const ROUTE_KEY = new RegExp(`^[${SLUG_CHARACTERS}_]+(?:-[${SLUG_CHARACTERS}_]+)*$`, 'u');
61
+ /**
62
+ * At least one visible letter or digit. A key of only combining marks satisfies
63
+ * {@link ROUTE_KEY} but is an invisible public URL.
64
+ */
65
+ const ROUTE_KEY_SUBSTANCE = /[\p{L}\p{N}]/u;
66
+ /**
67
+ * Characters a renderer draws as nothing: the emoji variation selector U+FE0F,
68
+ * the zero-width joiner, soft hyphens, and the bidi controls.
69
+ *
70
+ * U+FE0F is why this is a separate rule rather than a clause of
71
+ * {@link ROUTE_KEY}: it is a *nonspacing mark*, so `\p{M}` admits it, and
72
+ * `⚠️ warning` would otherwise key a route whose first character is invisible.
73
+ */
74
+ const INVISIBLE_IN_KEY = INVISIBLE_CODE_POINTS;
75
+ /** Whether a string is usable, as-is, as a public route segment. */
76
+ export function isRouteKey(key) {
77
+ return ROUTE_KEY.test(key) && ROUTE_KEY_SUBSTANCE.test(key);
78
+ }
79
+ /**
80
+ * A label's public route key: slugified, then cleaned into the route vocabulary.
81
+ *
82
+ * `github-slugger` strips emoji and most punctuation *without* closing the gap
83
+ * they leave, so `🌱 seedling` becomes `-seedling`, `seedling 🌱` becomes
84
+ * `seedling-`, and `Ops & SRE` becomes `ops--sre` — a leading, a trailing, and a
85
+ * doubled separator, all of which were emittable public URLs. A tag of `---`
86
+ * survived whole and routed to `/tags/---/`.
87
+ *
88
+ * Cleaning rather than rejecting, because an emoji tag is ordinary in a digital
89
+ * garden and `🌱 seedling` has one obvious, readable answer. What matters is not
90
+ * that the key is unaltered but that it stays a **pure function of this one
91
+ * label**: nothing here reads another tag, a position, or a corpus size, so
92
+ * publishing a new tag can never move an existing tag's URL. That is the same
93
+ * property that rules out a numeric disambiguating suffix.
94
+ *
95
+ * Unicode is normalized to NFC first, so a label typed with a combining accent
96
+ * and one typed with the precomposed character produce the same key rather than
97
+ * two indistinguishable public URLs. That is also a build-correctness matter:
98
+ * these keys become directory names, and macOS normalizes filenames, so the two
99
+ * forms would collide on disk on one platform and not on another.
100
+ *
101
+ * When cleaning leaves nothing addressable — `---`, `...`, `½` — there is no
102
+ * answer to invent, and {@link facets} fails the build.
103
+ */
104
+ export function routeKey(label) {
105
+ return slugify(label.normalize('NFC'))
106
+ .normalize('NFC')
107
+ .replace(INVISIBLE_IN_KEY, '')
108
+ .replace(/-+/g, '-')
109
+ .replace(/^-|-$/g, '');
110
+ }
111
+ /**
112
+ * Whether two labels are spellings of the same thing rather than a collision.
113
+ *
114
+ * Compared after the same normalizations {@link routeKey} applies, because the
115
+ * two questions have to agree: if `café` and `café` produce one key, they
116
+ * must also count as one label, or the merge that key implies becomes a build
117
+ * failure the author cannot meaningfully fix — the two are indistinguishable on screen.
118
+ *
119
+ * Case folding is what makes `Gardening` and `gardening` one tag. It is
120
+ * `toLowerCase`, which is ASCII-shaped: `straße` and `STRASSE` stay separate.
121
+ * They also produce different keys, so the outcome is two pages rather than a
122
+ * wrong merge — the safe direction.
123
+ */
124
+ function sameLabel(a, b) {
125
+ return a.normalize('NFC').toLowerCase() === b.normalize('NFC').toLowerCase();
126
+ }
127
+ /** Actionable advice shared by both facet failures. */
128
+ const CHANGE_SOURCE_LABEL = 'Change the tag or collection label in the source Markdown and rebuild. ' +
129
+ 'Do not edit the generated artifact by hand.';
130
+ /**
131
+ * Total order over notes: title, then the unique slug so no tie is left open.
132
+ *
133
+ * The one presentation comparator for note lists: facets, related notes, and
134
+ * collection neighbours all sort with it. It compares with JavaScript `<`
135
+ * (UTF-16 code units) because it is a display order; the canonical *slug* order
136
+ * that SQLite IDs and cursors follow is `compareSlugs` in `route-path.ts`.
137
+ */
138
+ export function byTitleThenSlug(a, b) {
139
+ if (a.title !== b.title)
140
+ return a.title < b.title ? -1 : 1;
141
+ return a.slug < b.slug ? -1 : 1;
142
+ }
143
+ /**
144
+ * Group entries by one metadata field.
145
+ *
146
+ * Labels differing only by case are one facet: `Gardening` and `gardening` are
147
+ * the same tag, and splitting them would publish two pages saying the same
148
+ * thing. The surviving label is the lexicographically smallest, so it does not
149
+ * depend on the order entries appear in the artifact.
150
+ *
151
+ * Labels that genuinely differ but collapse to the same route key are a build
152
+ * failure rather than a silent merge: `C++` and `C#` both slug to `c`, and
153
+ * quietly folding them would put one tag's notes on the other tag's page. The
154
+ * alternative — disambiguating with a numeric suffix — would make an existing
155
+ * tag's public URL depend on what other tags exist, which breaks the stable
156
+ * identity requirement. Failing names both labels so the author can fix it.
157
+ *
158
+ * Both failures name {@link CHANGE_SOURCE_LABEL}, so the message points to the
159
+ * source Markdown rather than to generated output.
160
+ */
161
+ function facets(entries, labelsOf, keyOf, what, base) {
162
+ const groups = new Map();
163
+ for (const entry of entries) {
164
+ for (const label of labelsOf(entry)) {
165
+ const key = keyOf(label);
166
+ if (!isRouteKey(key)) {
167
+ throw new Error(`${what} ${JSON.stringify(label)} has no URL-safe route key under ${base}: ` +
168
+ `it reduces to ${JSON.stringify(key)}, which is not an addressable public route segment. ` +
169
+ CHANGE_SOURCE_LABEL);
170
+ }
171
+ const existing = groups.get(key);
172
+ if (existing === undefined) {
173
+ groups.set(key, { key, label, entries: [entry] });
174
+ continue;
175
+ }
176
+ if (sameLabel(existing.label, label)) {
177
+ // The smallest label wins, so which spelling survives does not depend on
178
+ // the order entries appear in the artifact.
179
+ if (label < existing.label)
180
+ existing.label = label;
181
+ }
182
+ else {
183
+ throw new Error(`${what}s ${JSON.stringify(existing.label)} and ${JSON.stringify(label)} ` +
184
+ `both route to ${base}${key}/. ` +
185
+ CHANGE_SOURCE_LABEL);
186
+ }
187
+ // One entry carrying two spellings of the same tag (`Gardening` and
188
+ // `gardening`) is one note on that page, not two. TK-01's duplicate check
189
+ // compares exact strings, so it admits the pair; the case merge is what
190
+ // turns them into one facet, and this is what stops the note being listed
191
+ // and counted twice on it. Entries arrive grouped by the outer loop, so
192
+ // checking the last one is enough.
193
+ if (existing.entries.at(-1) !== entry)
194
+ existing.entries.push(entry);
195
+ }
196
+ }
197
+ return [...groups.values()]
198
+ .sort((a, b) => (a.key < b.key ? -1 : 1))
199
+ .map((facet) => ({ ...facet, entries: [...facet.entries].sort(byTitleThenSlug) }));
200
+ }
201
+ /**
202
+ * Tag facets, keyed by a slugified form of the tag text.
203
+ *
204
+ * Slugified rather than percent-encoded: a tag may be any text TK-01 admits,
205
+ * including mixed zh-CN and English, and a percent-encoded route key would be
206
+ * unreadable and awkward as a filename in static output.
207
+ */
208
+ export function tagFacets(entries) {
209
+ return facets(entries, (entry) => entry.tags ?? [], routeKey, 'tag', `/${TAGS_SEGMENT}/`);
210
+ }
211
+ /** Collection facets. TK-01 already validates `collection` as a slug, so it is its own key. */
212
+ export function collectionFacets(entries) {
213
+ return facets(entries, (entry) => (entry.collection === undefined ? [] : [entry.collection]), (label) => label, 'collection', `/${COLLECTIONS_SEGMENT}/`);
214
+ }
215
+ /**
216
+ * The instant `/recent/` orders by: `updated` when present, else `created`.
217
+ *
218
+ * Parsed rather than compared as text, because an artifact may mix a date-only
219
+ * value with a timestamp carrying an explicit offset, and `2026-08-06T12:00+08:00`
220
+ * sorts before `2026-08-06T05:00:00Z` lexically while being the later instant.
221
+ *
222
+ * An unparseable value is reported as undated rather than as `NaN`. TK-01 has
223
+ * already proven both fields parse, so this is unreachable through the real
224
+ * loader — but `NaN !== NaN`, so leaving it would send a comparator down its
225
+ * numeric branch and return `NaN`, which makes the sort order
226
+ * implementation-defined. This module is pure and fixture-driven; it must not
227
+ * depend on a caller having validated first.
228
+ */
229
+ export function noteTimestamp(entry) {
230
+ const value = entry.updated ?? entry.created;
231
+ if (value === undefined)
232
+ return undefined;
233
+ const parsed = Date.parse(value);
234
+ return Number.isNaN(parsed) ? undefined : parsed;
235
+ }
236
+ /**
237
+ * Notes in `/recent/` order: most recently updated first.
238
+ *
239
+ * The documented fallback for the artifact as it stands today, which carries
240
+ * neither `updated` nor `created`: undated notes follow every dated note, in
241
+ * ascending slug order. Slug is unique, so the result is a total order and two
242
+ * builds of the same artifact produce the same page.
243
+ */
244
+ export function recentFirst(entries) {
245
+ return [...entries].sort((a, b) => {
246
+ const left = noteTimestamp(a);
247
+ const right = noteTimestamp(b);
248
+ if (left !== right) {
249
+ if (left === undefined)
250
+ return 1;
251
+ if (right === undefined)
252
+ return -1;
253
+ return right - left;
254
+ }
255
+ return a.slug < b.slug ? -1 : 1;
256
+ });
257
+ }
258
+ /**
259
+ * Every public path this site has stranded, and where it goes now.
260
+ *
261
+ * **Empty, and that is the correct state.** TK-04 generated a pair of rules per
262
+ * note migrating `/<slug>/` to `/notes/<slug>/`, but `/<slug>/` was never
263
+ * publicly served: it was introduced at `3831ad0` and superseded at `04f8d9c`,
264
+ * entirely within unpushed history, with no `site:` configured and no canonical
265
+ * URL ever emitted. Redirecting from a URL nobody could hold is two rules per
266
+ * note of pure cost, and it took a rule limit, a cycle walk, and forty lines of
267
+ * comment with it. TK-12 deleted the rules and kept the mechanism.
268
+ *
269
+ * A rename that genuinely orphans a public URL adds its rule here. Owner
270
+ * decision 3 — renames are delete-and-recreate, old URLs may 404 — means that
271
+ * may never happen, which is why this is a literal rather than a derivation.
272
+ *
273
+ * ponytail: the deleted machinery included a `REDIRECT_LIMIT = 2000` guard for
274
+ * Cloudflare Pages' rule ceiling, which existed because the old rules were
275
+ * *derived* — two per note, so a large corpus could cross it without anyone
276
+ * writing a line. A hand-written literal cannot: reaching 2,000 entries here
277
+ * means typing 2,000 entries. Reinstate the check if rules ever become derived
278
+ * again.
279
+ */
280
+ export const REDIRECT_RULES = [];
281
+ /**
282
+ * The rules as a Cloudflare Pages `_redirects` file.
283
+ *
284
+ * Format: `[source] [destination] [code]`, one rule per line, `#` for comments.
285
+ * Emitted into the build output rather than committed, so it cannot drift from
286
+ * the artifact it describes; `tests/built-routes.test.ts` asserts the built file
287
+ * is exactly this projection.
288
+ *
289
+ * The header carries the schema and content version of the artifact the map was
290
+ * generated from, per requirements section 20 ("Every artifact records
291
+ * `schema_version` and `content_version`") — a deployed map is otherwise
292
+ * impossible to tie back to the artifact that produced it. Both are comments,
293
+ * which Cloudflare ignores, and both are public values: a schema number and a
294
+ * hash of already-published content.
295
+ */
296
+ export function renderRedirects(rules, version) {
297
+ return [
298
+ '# Generated from the content artifact at build time. Do not edit by hand.',
299
+ `# schema_version: ${version.schema}`,
300
+ `# content_version: ${version.content}`,
301
+ ...rules.map((rule) => `${rule.from} ${rule.to} ${rule.status}`),
302
+ '',
303
+ ].join('\n');
304
+ }
305
+ /**
306
+ * Every route that exists for any artifact, including an empty one, with the
307
+ * label the footer site map gives it.
308
+ *
309
+ * This list is why the site has no orphan route: the footer renders all of it
310
+ * on every page, and the footer survives reader mode, which hides the header's
311
+ * secondary links. Note, tag, and collection routes are absent because they
312
+ * depend on the artifact — each is linked from the index that owns it.
313
+ *
314
+ * `404` is deliberately absent: it is the host's fallback for an unmatched
315
+ * request, so it is reachable by definition and must not be linked as though it
316
+ * were a destination a reader could choose.
317
+ */
318
+ export const SITE_MAP = [
319
+ { href: '/', label: 'navHome' },
320
+ { href: '/recent/', label: 'navRecent' },
321
+ { href: `/${TAGS_SEGMENT}/`, label: 'navTags' },
322
+ { href: `/${COLLECTIONS_SEGMENT}/`, label: 'navCollections' },
323
+ { href: `/${GRAPH_SEGMENT}/`, label: 'navGraph' },
324
+ { href: '/about/', label: 'navAbout' },
325
+ { href: '/privacy/', label: 'navPrivacy' },
326
+ ];
327
+ /** The routes every build emits regardless of what the artifact contains. */
328
+ export const FIXED_ROUTES = SITE_MAP.map((item) => item.href);
329
+ /**
330
+ * Header navigation: the routes a reader moves between while reading.
331
+ *
332
+ * `/#notes` rather than a `/notes/` index — the home page already lists every
333
+ * published note, and requirements section 9.1 defines no `/notes/` route, so a
334
+ * second full listing would be one more page saying the same thing. About and
335
+ * Privacy are read once, so they live in the footer site map only.
336
+ */
337
+ export const PRIMARY_NAV = [
338
+ { href: '/#notes', label: 'navNotes' },
339
+ { href: '/recent/', label: 'navRecent' },
340
+ { href: `/${TAGS_SEGMENT}/`, label: 'navTags' },
341
+ { href: `/${COLLECTIONS_SEGMENT}/`, label: 'navCollections' },
342
+ ];