@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,509 @@
1
+ ---
2
+ /**
3
+ * The note page: requirements section 9.2's thirteen-element anatomy.
4
+ *
5
+ * Elements 1, 2, and part of 13 come from `Layout.astro` — the skip link, the
6
+ * header with its search trigger, and the printed canonical route. Everything
7
+ * else is here, in the order the requirements list them, and every region is
8
+ * either rendered below or left as a named, empty slot with the ticket that
9
+ * owns it. **Do not fill a slot from another ticket**: the slot is the contract.
10
+ *
11
+ * | § 9.2 element | Where |
12
+ * | --- | --- |
13
+ * | 1 skip link | `Layout.astro` |
14
+ * | 2 header and search trigger | `Layout.astro` |
15
+ * | 3 breadcrumbs | here |
16
+ * | 4 title and public metadata | here |
17
+ * | 5 optional summary | here |
18
+ * | 6 table of contents | here, via `TableOfContents.astro` |
19
+ * | 7 sanitized article content | here |
20
+ * | 8 outgoing public links | here |
21
+ * | 9 static backlinks | here |
22
+ * | 10 related-note fallback | here, derived by `relations.ts` |
23
+ * | 11 local-graph island | here, via `NoteGraph.astro`, laid out by `graph.ts` |
24
+ * | 12 previous/next or collection navigation | here |
25
+ * | 13 footer with provenance and canonical URL | here; the absolute URL is TK-08 |
26
+ *
27
+ * Reading and navigation need no JavaScript. The table of contents is server
28
+ * rendered, the breadcrumbs are static, and code-copy is a progressive control
29
+ * inserted only when scripting runs.
30
+ */
31
+ import Layout from '../../layouts/Layout.astro';
32
+ import LinkedNotes from '../../components/LinkedNotes.astro';
33
+ import NoteGraph from '../../components/NoteGraph.astro';
34
+ import TableOfContents from '../../components/TableOfContents.astro';
35
+ import { entries, getEntry, tagFacets, tagRouteForLabel } from '../../lib/content.js';
36
+ import { hasDrawableGraph, localGraph } from '../../lib/graph.js';
37
+ import { renderMarkdown } from '../../lib/markdown.js';
38
+ import {
39
+ collectionNeighbours,
40
+ hasTagPeer,
41
+ notesForSlugs,
42
+ relatedNotes,
43
+ } from '../../lib/relations.js';
44
+ import { GRAPH_SEGMENT, collectionRoute, noteRoute } from '../../lib/routes.js';
45
+ import { canonicalUrl, TITLE_SUFFIX } from '../../lib/site.js';
46
+ import { translate, NAV_LANGUAGE, partLanguage } from '../../lib/translations.js';
47
+ // Loaded as a URL rather than imported, so the stylesheet becomes its own
48
+ // content-hashed asset and the `<link>` below can be emitted conditionally.
49
+ // Importing it would merge it into the layout's sheet and ship it everywhere,
50
+ // which is precisely what owner decision 7 rules out.
51
+ import codeStylesheet from '../../styles/code.css?url';
52
+ import mathStylesheet from '../../styles/math.css?url';
53
+ import diagramStylesheet from '../../styles/diagram.css?url';
54
+ import graphStylesheet from '../../styles/graph.css?url';
55
+ import { DIAGRAM_MODE } from '../../lib/diagram-mode.js';
56
+ import { MATH_MODE } from '../../lib/math-mode.js';
57
+
58
+ export function getStaticPaths() {
59
+ return entries.map((entry) => ({ params: { slug: entry.slug }, props: { entry } }));
60
+ }
61
+
62
+ const { entry } = Astro.props;
63
+
64
+ /**
65
+ * **The one route that is a document**, and therefore the whole point of TK-16.
66
+ *
67
+ * Every other page on this site aggregates many entries or none and resolves its
68
+ * chrome from the navigation language. This one renders exactly one artifact
69
+ * entry, so it resolves from that entry's own `language` — and the result is
70
+ * that a zh-CN note renders Chinese chrome under `<html lang="zh-CN">` while an
71
+ * English note in the same build renders English chrome. `translate` falls back
72
+ * to the navigation language for an entry carrying no `language`, which is the
73
+ * published corpus's single note.
74
+ */
75
+ const t = translate(entry.language);
76
+
77
+ /**
78
+ * This page's language as an actual tag, which is what `<html lang>` carries and
79
+ * what a listed foreign title is compared against.
80
+ *
81
+ * `entry.language` is optional and `translate` resolves that to the navigation
82
+ * language; this resolves it to the same value so that a note declaring none and
83
+ * a note declaring `en` mark exactly the same set of foreign titles. Passing
84
+ * `entry.language` raw would compare `undefined` against `'en'` and put a
85
+ * redundant `lang="en"` on every English title on the published corpus.
86
+ */
87
+ const lang = entry.language ?? NAV_LANGUAGE;
88
+ // The renderer takes this mapping rather than knowing the route shape itself,
89
+ // so `/notes/<slug>/` is defined in exactly one place. Returning undefined for
90
+ // an unpublished slug leaves that href untouched rather than pointing it at a
91
+ // route that does not exist.
92
+ //
93
+ // `pageTitle` is what lets the anatomy put the title above the article: the
94
+ // producer writes it into the body as a leading `# Title`, and the renderer
95
+ // drops that one duplicate rather than the page rendering it twice. A body that
96
+ // keeps some *other* `h1` is left alone and fails the "exactly one h1" gate,
97
+ // which is the honest outcome — see `RenderOptions.pageTitle`.
98
+ const { html, toc, hasCode, hasMath, hasMermaid } = await renderMarkdown(entry.markdown, {
99
+ pageTitle: entry.title,
100
+ routeForSlug: (slug) => (getEntry(slug) ? noteRoute(slug) : undefined),
101
+ // The two strings the renderer writes into the article that are this
102
+ // repository's words rather than the author's: a heading anchor's accessible
103
+ // name and an untitled diagram's caption. They are chrome, so they follow the
104
+ // document like every other chrome string — a Chinese article had English
105
+ // accessible names on every heading before TK-16 threaded this through.
106
+ chrome: t,
107
+ });
108
+
109
+ /**
110
+ * `Home / Notes / <collection> / <title>`, from public fields only.
111
+ *
112
+ * `collection` is validated as a flat slug and there is no hierarchy behind it,
113
+ * so the trail is at most four deep. TK-05c owns the collection tree; when it
114
+ * lands, this gains ancestors and nothing else changes. The `/#notes` target is
115
+ * the home page's own listing — requirements section 9.1 defines no `/notes/`
116
+ * index, and `PRIMARY_NAV` already points there.
117
+ *
118
+ * Every segment is a fixed route or an artifact field. Nothing reads a path, so
119
+ * a breadcrumb can never disclose a private folder name.
120
+ */
121
+ const collection = entry.collection;
122
+ const breadcrumbs: { label: string; href?: string }[] = [
123
+ { label: t.navHome, href: '/' },
124
+ { label: t.navNotes, href: '/#notes' },
125
+ ...(collection === undefined ? [] : [{ label: collection, href: collectionRoute(collection) }]),
126
+ { label: entry.title },
127
+ ];
128
+
129
+ /**
130
+ * The calendar date a reader sees, from an artifact date.
131
+ *
132
+ * The first ten characters, not a parsed and reformatted `Date`. TK-01 has
133
+ * already validated both fields as ISO 8601, so the prefix *is* the calendar
134
+ * date exactly as the producer wrote it — and taking it verbatim avoids the two
135
+ * ways formatting gets this wrong: a date-only value parsed as UTC midnight and
136
+ * rendered in the build machine's zone shows the previous day west of
137
+ * Greenwich, and a locale format would be a chrome string TK-16 then has to
138
+ * translate. `YYYY-MM-DD` is unambiguous in both of this site's languages.
139
+ *
140
+ * The `<time datetime>` attribute carries the full precise value, so nothing
141
+ * the artifact expressed is lost.
142
+ */
143
+ function displayDate(value: string): string {
144
+ return value.slice(0, 10);
145
+ }
146
+
147
+ const canonicalRoute = noteRoute(entry.slug);
148
+
149
+ /**
150
+ * The three relationship surfaces, resolved once.
151
+ *
152
+ * `outgoing` and `backlinks` are the artifact's authored edges; `related` is
153
+ * derived and says so on the page. All three are computed here rather than in
154
+ * the template so that the markup below reads as the anatomy it is.
155
+ *
156
+ * **The incompleteness this used to state is gone, and the heading still says
157
+ * "Links to".** The producer derived `outgoing` from wikilinks only, so a plain
158
+ * Markdown link rendered a live `<a>` with no edge behind it. Since TK-27 one
159
+ * traversal produces the rewritten href *and* the edge from a single
160
+ * resolution (`scripts/resolve-links.ts`), across all five link forms — so that
161
+ * direction of disagreement is now unrepresentable rather than merely untested.
162
+ *
163
+ * The heading is unchanged deliberately, because "every link on this page"
164
+ * would be a lie in three ways the data still cannot support. An external link
165
+ * is an anchor a reader can click and is never an edge. A link that resolved to
166
+ * nothing, and one to a note the user withheld, are both on the page as the
167
+ * author's own words — degraded to text rather than deleted — and neither
168
+ * reaches anything here. And a link to this page's own heading renders as a
169
+ * real anchor inside the article while contributing no edge, because an entry
170
+ * may not link to itself. "Links to" describes the list that is actually there:
171
+ * the published notes this one reaches.
172
+ *
173
+ * What did change is the *empty* state, from possibly-false to true.
174
+ * `outgoingEmpty` reads "This note links to no other published note", and
175
+ * before TK-27 that sentence was printed for a note whose body was full of
176
+ * Markdown-syntax links to published notes. Now it can only be printed when it
177
+ * holds. `tests/backlink-surfaces.test.ts` gates it over a built site, which
178
+ * nothing did before — an empty state that states a fact was the one region
179
+ * here with no gate on the fact.
180
+ */
181
+ const outgoing = notesForSlugs(entry.outgoing, getEntry);
182
+ const backlinks = notesForSlugs(entry.backlinks, getEntry);
183
+ const facets = tagFacets();
184
+ const related = relatedNotes(entry, entries, facets);
185
+
186
+ /**
187
+ * Why the related list is empty, when it is.
188
+ *
189
+ * Two different facts produce the same empty list and a reader deserves to be
190
+ * told which: a note with no tag-sharer anywhere, and a note whose every
191
+ * tag-sharer is already rendered in the two sections above. Saying "nothing
192
+ * shares a tag" in the second case is a claim the artifact contradicts, which
193
+ * is precisely what an honest empty state must not do.
194
+ *
195
+ * `hasTagPeer` runs only for an empty list, which is the only case
196
+ * `LinkedNotes` renders the message; a page with related notes skips a second
197
+ * full scan of the facet index.
198
+ */
199
+ const relatedEmpty =
200
+ related.length === 0 && !hasTagPeer(entry, entries, facets)
201
+ ? t.relatedNoTagPeer
202
+ : t.relatedAlreadyListed;
203
+
204
+ /**
205
+ * The pager: its two links and the name it announces, or nothing at all.
206
+ *
207
+ * One value rather than a neighbour pair plus a separate label, so the template
208
+ * has a single condition to branch on. Two conditions is how a pager ends up
209
+ * rendered as an empty `<nav>` for the one shape neither corpus contains — a
210
+ * note alone in its collection. `collectionNeighbours` returns no neighbour for
211
+ * that case and `tests/relations.test.ts` pins it; this keeps the page from
212
+ * being able to disagree.
213
+ *
214
+ * Naming the collection matters because a page carries several `<nav>` elements
215
+ * and a screen reader lists them by name.
216
+ */
217
+ const neighbours = collectionNeighbours(entry, entries);
218
+ const pager =
219
+ collection !== undefined && (neighbours.previous !== undefined || neighbours.next !== undefined)
220
+ ? { ...neighbours, label: t.moreInCollection(collection) }
221
+ : undefined;
222
+
223
+ /**
224
+ * § 9.2.11's neighbourhood, laid out during the build.
225
+ *
226
+ * The same edges the two lists above render, drawn as a figure — plus the edges
227
+ * *between* those neighbours, which is what makes it a graph rather than a
228
+ * picture of a list. `checkCorpus` has already proven every one of them
229
+ * resolves, which is why the layout is affordable here and structurally
230
+ * unavailable to a whole-vault renderer.
231
+ */
232
+ const graph = localGraph(entry, getEntry);
233
+ ---
234
+
235
+ <Layout
236
+ title={`${entry.title}${TITLE_SUFFIX}`}
237
+ description={entry.excerpt}
238
+ lang={entry.language}
239
+ >
240
+ {/*
241
+ Owner decision 7, the live case. Each flag is true only when the body
242
+ contains the construct, so each sheet reaches only the pages that render
243
+ one. Three conditional `<link>` elements rather than one merged sheet:
244
+ merging Mermaid's per-diagram-type stylesheets was measured at 6,354 B gzip
245
+ against a 972 B median for a single type, and the build-time renderer
246
+ removes the need for either — `diagram.css` is 14 rules because the theming
247
+ is flattened onto the SVG itself.
248
+
249
+ `hasMath` also gates the ~9 KB Temml stylesheet and, through its
250
+ `unicode-range`, the 9.4 KB `Temml.woff2` — which a browser fetches only for
251
+ an expression that actually uses `\mathscr`.
252
+ */}
253
+ {hasCode && <link slot="head" rel="stylesheet" href={codeStylesheet} />}
254
+ {hasMath && <link slot="head" rel="stylesheet" href={mathStylesheet} />}
255
+ {hasMermaid && <link slot="head" rel="stylesheet" href={diagramStylesheet} />}
256
+ {/* The graph sheet reaches only a note that actually draws one — a note with
257
+ no neighbourhood renders a sentence and requests nothing. The published
258
+ corpus's single note is exactly that case. `hasDrawableGraph` is the same
259
+ predicate the component branches on, so the stylesheet and the figure
260
+ cannot disagree about whether there is a graph. */}
261
+ {hasDrawableGraph(graph) && <link slot="head" rel="stylesheet" href={graphStylesheet} />}
262
+ {/*
263
+ Client mode only, and only on a page that has a diagram.
264
+
265
+ Astro bundles a script tag **eagerly**: the `hasMermaid` condition decides
266
+ whether the tag is rendered, not whether the module is built. Keeping
267
+ Mermaid out of `dist/` in build-time mode is therefore done in
268
+ `astro.config.mjs`, which resolves `src/scripts/diagram.ts` to an empty stub
269
+ unless client mode is selected. Both halves are needed — this one so a
270
+ diagram-free page requests nothing, that one so a diagram-free *build*
271
+ contains nothing. `tests/math-and-diagrams.test.ts` asserts both over
272
+ `dist/`.
273
+
274
+ `DIAGRAM_MODE` is checked here as well, so build-time mode emits no tag at
275
+ all rather than one pointing at a 20-byte empty chunk. It is not what keeps
276
+ Mermaid out — the resolver is — but a request for nothing is still a
277
+ request.
278
+ */}
279
+ {hasMermaid && DIAGRAM_MODE === 'client' && (
280
+ <script>
281
+ import '../../scripts/diagram.js';
282
+ </script>
283
+ )}
284
+ {/*
285
+ The same arrangement for math, and it needs both halves for the same reason
286
+ the diagram one does: this condition keeps a math-free page from requesting
287
+ anything, and `astro.config.mjs`'s resolver keeps a build-time *build* from
288
+ containing Temml at all. `clientRuntimePlugin` is parameterised over the two
289
+ runtimes rather than duplicated.
290
+
291
+ `hasMath` needs no change to serve this: it is set in `mathPlugin` before the
292
+ mode branch, so it means "this page has an expression" in either mode — which
293
+ is what both the stylesheet above and this tag ask of it.
294
+ */}
295
+ {hasMath && MATH_MODE === 'client' && (
296
+ <script>
297
+ import '../../scripts/math.js';
298
+ </script>
299
+ )}
300
+
301
+ <header class="note-header">
302
+ {/* § 9.2.3 — the trail. Public fields only; never a private folder name. */}
303
+ <nav class="breadcrumbs" aria-label={t.breadcrumbLabel}>
304
+ <ol>
305
+ {breadcrumbs.map((crumb) => (
306
+ <li>
307
+ {crumb.href === undefined ? (
308
+ <span aria-current="page">{crumb.label}</span>
309
+ ) : (
310
+ <a href={crumb.href}>{crumb.label}</a>
311
+ )}
312
+ </li>
313
+ ))}
314
+ </ol>
315
+ </nav>
316
+
317
+ {/* § 9.2.4 — the title, and the public metadata the artifact actually
318
+ carries. Every row below is optional in the contract, so each is
319
+ conditional: a note with none of them renders a title and nothing else,
320
+ which is the published corpus today. */}
321
+ <h1 class="note-title">{entry.title}</h1>
322
+
323
+ {(entry.created !== undefined ||
324
+ entry.updated !== undefined ||
325
+ collection !== undefined ||
326
+ (entry.tags?.length ?? 0) > 0 ||
327
+ (entry.aliases?.length ?? 0) > 0) && (
328
+ <dl class="note-meta">
329
+ {entry.created !== undefined && (
330
+ <div>
331
+ <dt>{t.metaPublished}</dt>
332
+ <dd><time datetime={entry.created}>{displayDate(entry.created)}</time></dd>
333
+ </div>
334
+ )}
335
+ {entry.updated !== undefined && (
336
+ <div>
337
+ <dt>{t.metaUpdated}</dt>
338
+ <dd><time datetime={entry.updated}>{displayDate(entry.updated)}</time></dd>
339
+ </div>
340
+ )}
341
+ {collection !== undefined && (
342
+ <div>
343
+ <dt>{t.metaCollection}</dt>
344
+ <dd><a href={collectionRoute(collection)}>{collection}</a></dd>
345
+ </div>
346
+ )}
347
+ {entry.tags !== undefined && entry.tags.length > 0 && (
348
+ <div>
349
+ <dt>{t.metaTags}</dt>
350
+ <dd>
351
+ <ul class="note-tags">
352
+ {entry.tags.map((tag) => (
353
+ <li><a href={tagRouteForLabel(tag)}>{tag}</a></li>
354
+ ))}
355
+ </ul>
356
+ </dd>
357
+ </div>
358
+ )}
359
+ {/* Pagefind searches metadata outside `data-pagefind-body`, so aliases
360
+ remain findable without becoming the body-derived result excerpt. */}
361
+ {entry.aliases !== undefined && entry.aliases.length > 0 && (
362
+ <div>
363
+ <dt>{t.metaAliases}</dt>
364
+ <dd>
365
+ <ul class="note-aliases" data-pagefind-meta="aliases">
366
+ {entry.aliases.map((alias) => <li>{alias}</li>)}
367
+ </ul>
368
+ </dd>
369
+ </div>
370
+ )}
371
+ </dl>
372
+ )}
373
+
374
+ {/* § 9.2.5 — the optional summary.
375
+ `description` verbatim, with no comparison against `excerpt`: the two
376
+ are separate artifact fields, the excerpt is never rendered on this page
377
+ (it is the `<meta name="description">` and the card body elsewhere), and
378
+ a note whose author wrote the same sentence into both still asked for a
379
+ summary. Suppressing on equality was tried and was a product decision
380
+ this ticket has no reason to make. */}
381
+ {entry.description !== undefined && <p class="note-summary">{entry.description}</p>}
382
+ </header>
383
+
384
+ {/* § 9.2.6 — the table of contents, on pages above `TOC_MIN_HEADINGS`. The
385
+ renderer returns an empty `toc` below the threshold, so the threshold
386
+ lives in one place and this is only the rendering decision. */}
387
+ {toc.length > 0 && <TableOfContents entries={toc} isRoot lang={entry.language} />}
388
+
389
+ {/* § 9.2.7 — the sanitized article body. */}
390
+ <article
391
+ class="prose"
392
+ data-pagefind-body
393
+ data-code-copy={t.codeCopy}
394
+ data-code-copied={t.codeCopied}
395
+ data-code-copy-failed={t.codeCopyFailed}
396
+ >
397
+ <div set:html={html} />
398
+ </article>
399
+
400
+ {/* § 9.2.8 — outgoing public links: the notes this one links to, by title.
401
+ Every target is a published entry — `checkCorpus` proves it — so this can
402
+ never be a dead link or a private title. */}
403
+ <LinkedNotes
404
+ region="outgoing"
405
+ heading={t.outgoingHeading}
406
+ entries={outgoing}
407
+ empty={t.outgoingEmpty}
408
+ lang={lang}
409
+ />
410
+
411
+ {/* § 9.2.9 — static backlinks. Requirements section 13.1 asks for five
412
+ things: source title, source route, an optional source heading, a bounded
413
+ sanitized context excerpt, and a stable sort order. Three are rendered.
414
+ The heading and the context excerpt are not, because the artifact carries
415
+ one slug per edge and nothing about *where* in the source that edge is —
416
+ no heading, no offset, no surrounding text.
417
+
418
+ `ContentEntry.excerpt` is deliberately not substituted for the missing
419
+ one. It is the source note's own summary — the same string the card, the
420
+ `<meta name="description">`, and the hover preview all use — and it is
421
+ derived from the note's opening, not from the passage that links here.
422
+ Printed under a backlink it would read as the context of the link while
423
+ being unrelated to it, which is the fabrication the empty states exist to
424
+ avoid: a wrong answer presented in the shape of a right one is worse than
425
+ no answer. A real context excerpt needs a per-edge field; that is TK-19's
426
+ producer contract work. */}
427
+ <LinkedNotes
428
+ region="backlinks"
429
+ heading={t.backlinksHeading}
430
+ entries={backlinks}
431
+ empty={t.backlinksEmpty}
432
+ lang={lang}
433
+ />
434
+
435
+ {/* § 9.2.10 — the related-note fallback. Derived, not authored, and the
436
+ markup says so: the rule is stated below the heading so a reader is never
437
+ left to guess whether the owner put these here. `relations.ts` documents
438
+ the rule and the alternatives it was chosen over. */}
439
+ <LinkedNotes
440
+ region="related"
441
+ heading={t.relatedHeading}
442
+ entries={related}
443
+ empty={relatedEmpty}
444
+ derivation={t.relatedDerivation}
445
+ lang={lang}
446
+ />
447
+
448
+ {/* § 9.2.11 — the local-graph neighborhood: build-time inline SVG with real
449
+ `<a>` elements plus the equivalent table section 17 requires. TK-21's
450
+ interactive layer must not be stubbed in here — this is the baseline that
451
+ survives when that enhancement fails. */}
452
+ <NoteGraph
453
+ graph={graph}
454
+ region="note-graph"
455
+ heading={t.graphHeading}
456
+ derivation={t.graphLocalDerivation}
457
+ empty={t.graphEmpty}
458
+ bounded={t.graphBoundedLocal}
459
+ expand={{
460
+ href: `/${GRAPH_SEGMENT}/`,
461
+ text: t.graphExpandLocal,
462
+ label: t.graphExpandLocalLabel,
463
+ }}
464
+ lang={lang}
465
+ explore={{ scope: 'local', centerSlug: entry.slug }}
466
+ />
467
+
468
+ {/* § 9.2.12 — collection navigation, "where meaningful": only inside a
469
+ collection that has another note, and in the same order
470
+ `/collections/<key>/` lists them. A note with no collection, or the only
471
+ note in one, renders nothing rather than a pager with two dead ends. */}
472
+ {pager !== undefined && (
473
+ <nav class="collection-pager" aria-label={pager.label}>
474
+ {/* The link carries the destination's own title, so it is a meaningful
475
+ link out of context — "Previous" alone would be four identical link
476
+ names on a screen reader's link list. The direction word is a
477
+ separate element rather than part of the link text for the same
478
+ reason — and the title carries its own `lang` when the neighbour is in
479
+ another language, which the direction word never is. */}
480
+ {pager.previous !== undefined && (
481
+ <a href={noteRoute(pager.previous.slug)} rel="prev">
482
+ <span class="pager-direction">{t.pagerPrevious}</span>
483
+ <span
484
+ class="pager-title"
485
+ lang={partLanguage(pager.previous.language, lang)}
486
+ >{pager.previous.title}</span>
487
+ </a>
488
+ )}
489
+ {pager.next !== undefined && (
490
+ <a class="pager-next" href={noteRoute(pager.next.slug)} rel="next">
491
+ <span class="pager-direction">{t.pagerNext}</span>
492
+ <span
493
+ class="pager-title"
494
+ lang={partLanguage(pager.next.language, lang)}
495
+ >{pager.next.title}</span>
496
+ </a>
497
+ )}
498
+ </nav>
499
+ )}
500
+
501
+ {/* § 9.2.13 — the footer: public provenance and the canonical route. */}
502
+ <footer class="note-footer">
503
+ <p>{t.noteProvenance}</p>
504
+ {/* TK-08 configured `site:`, so this is the full canonical URL rather than
505
+ the route TK-05a could honestly emit. A reader who copies it from a
506
+ printed or quoted page gets an address that resolves. */}
507
+ <p class="note-canonical">{t.canonicalUrlLine(canonicalUrl(Astro.site, canonicalRoute))}</p>
508
+ </footer>
509
+ </Layout>
@@ -0,0 +1,121 @@
1
+ ---
2
+ /**
3
+ * What this site collects, which is nothing.
4
+ *
5
+ * **Rewritten by TK-31 for one false claim and one unsupportable one.** The
6
+ * previous version ended with "Every page here was individually reviewed and
7
+ * approved for publication. Material that was not approved is not represented
8
+ * on this site in any form." The first sentence describes the retired owner-specific bridge
9
+ * and is false of a build from a directory of Markdown; the second is *true*
10
+ * and had been resting on the first, so it read as a consequence of a review
11
+ * process rather than of what the build actually does.
12
+ *
13
+ * That distinction matters more here than on `/about/`. A privacy page is the
14
+ * document a reader consults to decide whether to trust the rest, so a claim in
15
+ * it that the tool cannot keep is worse than no page: it converts a technical
16
+ * guarantee into a promise about someone's diligence, and a reader has no way
17
+ * to tell which they were given.
18
+ *
19
+ * **Every claim below is a property this build enforces**, and each is scoped to
20
+ * what is actually held. `tests/built-output.test.ts` asserts every `<script>`
21
+ * in `dist/` is same-origin and none is inline; `public/_headers` sets a CSP
22
+ * whose `script-src`, `style-src`, `font-src`, and `connect-src` are all
23
+ * `'self'`. The absence of cookies and of analytics follows from there being no
24
+ * server and no such script.
25
+ *
26
+ * **`img-src` is the one directive that is not `'self'`** — it permits `https:`,
27
+ * so a note whose author embedded a remote image renders it, and that request
28
+ * reaches a third party. Measured, not inferred. The page therefore says
29
+ * "script, stylesheet, and font" where it once said "every asset", and names the
30
+ * image case separately: the earlier wording was a claim this build does not
31
+ * keep, on the page a reader consults to calibrate trust.
32
+ *
33
+ * The other thing this page cannot speak for is the host that serves the files,
34
+ * so it says who that is and stops rather than promising on their behalf.
35
+ *
36
+ * See `about.astro` for why this is still a shipped page rather than a note
37
+ * `init` seeds into the user's own repository.
38
+ *
39
+ * **The Content section was rewritten on 2026-08-17**, for the same reason the
40
+ * paragraphs above it were: it said an excluded note "produces no page, no
41
+ * search entry, and no link", and the last clause stopped being true when the
42
+ * owner made links to withheld notes live. It now says what a reader can
43
+ * actually observe — the link exists, it carries the note's own path as often
44
+ * as not, and it leads to a page that says the note is not published.
45
+ */
46
+ import Layout from '../layouts/Layout.astro';
47
+ import { DEFAULT_NAV_LANGUAGE } from '../lib/translations.js';
48
+ import { TITLE_SUFFIX } from '../lib/site.js';
49
+ ---
50
+
51
+ {/* The prose below is English whatever the site language, so the page says so. */}
52
+ <Layout
53
+ title={`Privacy${TITLE_SUFFIX}`}
54
+ lang={DEFAULT_NAV_LANGUAGE}
55
+ description="No analytics, no comments, no third-party tracking."
56
+ >
57
+ <article class="prose">
58
+ <h1>Privacy</h1>
59
+
60
+ <h2>What this site does not do</h2>
61
+
62
+ <p>
63
+ There are no analytics on this site. No page view, click, scroll, reading time, or search
64
+ query is measured, recorded, or reported anywhere.
65
+ </p>
66
+
67
+ <p>
68
+ There are no comments, reactions, accounts, or sign-in. There is nothing to submit and no
69
+ form to fill in.
70
+ </p>
71
+
72
+ <p>
73
+ No third-party tracking runs here. There are no advertising or analytics scripts, no
74
+ tracking pixels, no social widgets, and no embedded players. Every script, stylesheet, and
75
+ font a page loads comes from this site's own address.
76
+ </p>
77
+
78
+ <p>
79
+ Images are the exception, and only where a note's author linked one. A note that embeds an
80
+ image hosted elsewhere renders that image from wherever it lives, so displaying it tells that
81
+ host you loaded the page — the same as following a link there. Nothing on this site does that
82
+ on its own; it happens only where the writing itself points outward.
83
+ </p>
84
+
85
+ <p>
86
+ This site sets no cookies. Your theme and reader-mode choices are stored by your own browser,
87
+ on your own device, and are never sent anywhere. Clearing your browser's site data removes
88
+ them.
89
+ </p>
90
+
91
+ <h2>Search</h2>
92
+
93
+ <p>
94
+ Search runs entirely inside your browser against a search index served as a static file.
95
+ Nothing you type is transmitted to this site or to anyone else.
96
+ </p>
97
+
98
+ <h2>What is unavoidable</h2>
99
+
100
+ <p>
101
+ Requesting a page from any website necessarily reveals your IP address and browser details to
102
+ whoever serves it. This site is served by a static hosting provider, which may keep
103
+ operational request logs of its own under its own policy. Nothing beyond that is collected by
104
+ this site, which has no server of its own to collect it with.
105
+ </p>
106
+
107
+ <h2>Content</h2>
108
+
109
+ <p>
110
+ This site is generated from a set of notes, and a note excluded from it produces no page, no
111
+ search entry, and no content here. None of what it says appears. What does still appear is
112
+ the words a published note used to refer to it: a link pointing at an excluded note keeps its
113
+ own text — often the note's name and the path it had — and leads to a page saying the note is
114
+ not published. See <a href="/about/">About</a> for how that works.
115
+ </p>
116
+
117
+ <p>
118
+ If any of this changes, this page changes with it.
119
+ </p>
120
+ </article>
121
+ </Layout>