@rathnasgala2/template 2.0.0 → 2.2.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.
Files changed (42) hide show
  1. package/README.md +211 -137
  2. package/contracts/render-policy.jcs +1 -1
  3. package/contracts/theme-styling-contract.jcs +1 -1
  4. package/package.json +5 -6
  5. package/src/core/errors.js +13 -13
  6. package/src/core/index.js +130 -75
  7. package/src/core/internal/appearance/base-layer.js +180 -0
  8. package/src/core/internal/appearance/bootstrap-script.js +11 -11
  9. package/src/core/internal/appearance/contract.js +59 -44
  10. package/src/core/internal/appearance/controller-markup.js +7 -8
  11. package/src/core/internal/appearance/styling-contract.js +132 -29
  12. package/src/core/internal/canonical-jcs.js +0 -0
  13. package/src/core/internal/content-security.js +0 -0
  14. package/src/core/internal/eleventy-render.js +52 -35
  15. package/src/core/internal/feeds.js +15 -15
  16. package/src/core/internal/fs-walk.js +2 -2
  17. package/src/core/internal/media/avif-probe.js +4 -4
  18. package/src/core/internal/media/font-policy.js +2 -2
  19. package/src/core/internal/media/jpeg-codec.js +0 -0
  20. package/src/core/internal/media/limits.js +32 -34
  21. package/src/core/internal/media/pipeline.js +4 -4
  22. package/src/core/internal/media/png-codec.js +7 -7
  23. package/src/core/internal/media/resize.js +2 -2
  24. package/src/core/internal/media/sniff.js +4 -4
  25. package/src/core/internal/media/theme-svg-sanitizer.js +278 -0
  26. package/src/core/internal/media/webp-probe.js +3 -3
  27. package/src/core/internal/messages.js +8 -8
  28. package/src/core/internal/page-kinds.js +50 -51
  29. package/src/core/internal/render-policy-content.js +57 -21
  30. package/src/core/internal/route-labels.js +5 -6
  31. package/src/core/internal/route.js +58 -20
  32. package/src/core/internal/search-index.js +17 -16
  33. package/src/core/internal/semver-range.js +84 -0
  34. package/src/core/internal/seo.js +24 -24
  35. package/src/core/internal/sitemap.js +11 -12
  36. package/src/core/internal/skeleton.js +65 -46
  37. package/src/core/internal/source-inventory.js +4 -5
  38. package/src/core/internal/text-direction.js +1 -2
  39. package/src/core/internal/theme-assets.js +314 -68
  40. package/src/core/internal/uuid.js +4 -5
  41. package/src/core/manifest.js +0 -0
  42. package/types/index.d.ts +22 -8
package/README.md CHANGED
@@ -5,14 +5,14 @@ portable author repository format. It is one of the three author-facing S2
5
5
  artifacts, alongside one `@rathnasgala2/theme-*` package and
6
6
  `@rathnasgala2/publish-action`.
7
7
 
8
- This package is JavaScript ESM on Node 24, authored without TypeScript sources
9
- (DEC-094): every export carries JSDoc types, checked by `tsc --checkJs --noEmit`
10
- against a hand-maintained `.d.ts` surface.
8
+ This package is JavaScript ESM on Node 24, authored without TypeScript sources:
9
+ every export carries JSDoc types, checked by `tsc --checkJs --noEmit` against a
10
+ hand-maintained `.d.ts` surface.
11
11
 
12
12
  ## Status
13
13
 
14
- S2-T02's repository scaffold (exact pins, `src/core/`-only layout, module-tree
15
- absence gate, quality-gate tooling) plus S2-T03's renderer adapter:
14
+ This repository's scaffold (exact pins, `src/core/`-only layout, module-tree
15
+ absence gate, quality-gate tooling) plus its renderer adapter:
16
16
  `renderPublication(buildInput, options)`, exported from `src/core/index.js`, is
17
17
  the package's one documented entry point. It:
18
18
 
@@ -29,7 +29,7 @@ the package's one documented entry point. It:
29
29
  `urn:gala:schema:artifact-manifest:2.0.0` instance describing the candidate
30
30
  output directory.
31
31
 
32
- S2-T05 adds the media pipeline under `src/core/internal/media/`, run from
32
+ The media pipeline lives under `src/core/internal/media/`, run from
33
33
  `renderPublication` after the Eleventy route listing so its output is never
34
34
  mistaken for an HTML route:
35
35
 
@@ -40,8 +40,8 @@ mistaken for an HTML route:
40
40
  entropy decode (a documented scope decision — see
41
41
  `webp-probe.js`/`avif-probe.js`). Every format shares one set of resource
42
42
  ceilings in `limits.js` (raster dimension, pixel count, decoded-buffer and
43
- source-byte caps reused from DEC-097's own closed binary-asset decoder bounds,
44
- plus this renderer's own documented per-publication ceilings).
43
+ source-byte caps reused from the theme package's own closed binary-asset
44
+ decoder bounds, plus this renderer's own documented per-publication ceilings).
45
45
  - **Metadata strip, orientation.** PNG/JPEG derivatives are always re-encoded
46
46
  from decoded pixels, never byte-copied, so no ancillary/EXIF metadata survives
47
47
  into a derivative; JPEG's EXIF `Orientation` tag is read and applied
@@ -55,7 +55,7 @@ mistaken for an HTML route:
55
55
  unconditionally as an image reference (`MEDIA_SVG_REJECTED`) — SVG is never
56
56
  processed as author media.
57
57
  - **Font policy.** `appearance.fontAssets` entries must be bounded WOFF2 (exact
58
- signature, DEC-097's 4,194,304-byte font cap); a remote font reference cannot
58
+ signature, a fixed 4,194,304-byte font cap); a remote font reference cannot
59
59
  enter `build-input` in the first place (`resolvedFile` is always a
60
60
  repository-relative path), so "no remote font is ever retrieved" holds
61
61
  structurally.
@@ -63,17 +63,16 @@ mistaken for an HTML route:
63
63
  mismatch, SVG, unrecognized format, invalid font — is a `MediaPipelineError`
64
64
  with a stable `reasonCode`, thrown before the manifest is assembled.
65
65
 
66
- `options.sourceDirectory` (new in S2-T05) is the caller-mounted, read-only
67
- repository source tree `build-input`'s `resolvedFile` references point into;
68
- every referenced file's bytes are read from there and verified against
69
- `build-input`'s declared `sourceDigest` before being decoded.
70
-
71
- S2-T04 adds the complete output-security pipeline
72
- (`src/core/internal/content-security.js`) on top of that adapter. **Division of
73
- labour** (DEC-097 section 5: normalization replaces an authored body path with a
74
- `renderableBody` carrying "the deterministic policy output/digest";
75
- `build-input.schema.json` fixes `renderableBody.bodyMediaType` as
76
- `const: "text/html"`): a `renderableBody.body` inside a validated
66
+ `options.sourceDirectory` is the caller-mounted, read-only repository source
67
+ tree `build-input`'s `resolvedFile` references point into; every referenced
68
+ file's bytes are read from there and verified against `build-input`'s declared
69
+ `sourceDigest` before being decoded.
70
+
71
+ The complete output-security pipeline (`src/core/internal/content-security.js`)
72
+ sits on top of that adapter. **Division of labour** (normalization replaces an
73
+ authored body path with a `renderableBody` carrying "the deterministic policy
74
+ output/digest"; `build-input.schema.json` fixes `renderableBody.bodyMediaType`
75
+ as `const: "text/html"`): a `renderableBody.body` inside a validated
77
76
  `build-input:2.0.0` instance is **already** render-policy-conformant HTML,
78
77
  produced once upstream of this renderer. This package therefore exposes two
79
78
  distinct public entry points:
@@ -115,23 +114,37 @@ highlighter grammar/token-class catalog) both surfaces above read from,
115
114
  generated deterministically by `scripts/generate-contracts.mjs` from
116
115
  `src/core/internal/render-policy-content.js` — the single source of truth, so
117
116
  the published contract and the enforced policy cannot drift apart. Every
118
- rendered page also carries the exact per-artifact CSP baseline as a
117
+ rendered page also carries the meta-safe CSP baseline as a
119
118
  `<meta http-equiv="Content-Security-Policy">` tag (byte-identical on every
120
119
  route, since S2 materializes no module package/configuration/output/runtime).
121
120
 
121
+ **TPL-H4 fix — meta vs. header-only directives.** The CSP specification
122
+ explicitly ignores `frame-ancestors` (and `report-uri`/`sandbox`, neither used
123
+ here) when a policy is delivered via `<meta http-equiv>`; the `<meta>` tag above
124
+ therefore never includes `frame-ancestors`, and
125
+ `contentSecurityPolicyHeaderOnlyDirectives()` (`internal/content-security.js`)
126
+ returns the one directive that is not: `frame-ancestors 'none'`. Clickjacking
127
+ protection for a published artifact is a **hosting-layer requirement**: the
128
+ adapter/deployment serving this artifact must add
129
+ `Content-Security-Policy: frame-ancestors 'none'` (or an equivalent
130
+ `X-Frame-Options` header) as a real HTTP response header. This cannot yet be
131
+ carried in `artifact-manifest:2.0.0`'s own `declarativeHeaders` field, which
132
+ `@rathnasgala2/schemas` currently fixes to `const: []`; surfacing it there is a
133
+ schema-repo change, out of this repository's scope.
134
+
122
135
  `img[src]` is restricted to repository-relative paths only in this pipeline;
123
- full manifest-coverage verification of media references is the S2-T05 media
124
- pipeline's job (immediately above). A handful of manifest fields (`builder`,
136
+ full manifest-coverage verification of media references is the media pipeline's
137
+ job (immediately above). A handful of manifest fields (`builder`,
125
138
  `sourceIdentity.repository`, `workflowIdentity`, `buildToolVersions`,
126
139
  `excludedInputs`) describe facts `build-input` does not itself carry;
127
140
  `renderPublication` accepts them as an explicit `options.provenance` bundle
128
141
  rather than fabricating them — see `src/core/manifest.js`'s module
129
- documentation. `contracts/theme-styling-contract.jcs` is S2-T12's real, closed
130
- `templateStylingContract` catalog (see the S2-T12 section below).
142
+ documentation. `contracts/theme-styling-contract.jcs` is the real, closed
143
+ `templateStylingContract` catalog (see the theme styling contract section
144
+ below).
131
145
 
132
- S2-T06 adds the core semantic skeleton, page kinds and navigation renderer on
133
- top of the S2-T04 output-security pipeline every generated page still passes
134
- through:
146
+ The core semantic skeleton, page kinds and navigation renderer sit on top of the
147
+ output-security pipeline every generated page still passes through:
135
148
 
136
149
  - **`src/core/internal/skeleton.js`** renders the one landmark contract every
137
150
  page kind shares, through its one `renderPageBody` composition: a skip link
@@ -144,13 +157,13 @@ through:
144
157
  template attribution line). Core owns nine versioned semantic slots
145
158
  (`header-actions`, `article-preamble`, `article-end`, `footer-profile`,
146
159
  `footer-auxiliary`, `account-intent`, `conversation`, `newsletter`,
147
- `edition-selector`) plus the collapsed `article-footer-ad` slot (DEC-060):
148
- every slot is one `data-gala-slot="<name>"` attribute on an otherwise empty,
149
- non-landmark `<div>`, and the collapsed slot additionally carries `hidden`. S2
160
+ `edition-selector`) plus the collapsed `article-footer-ad` slot: every slot is
161
+ one `data-gala-slot="<name>"` attribute on an otherwise empty, non-landmark
162
+ `<div>`, and the collapsed slot additionally carries `hidden`. This renderer
150
163
  has no module system, so every slot except `footer-profile` (which always
151
- wraps the publication's own footer card) and `header-actions` (S2-T07's
152
- appearance control, core content rather than a module) renders empty. This
153
- module also renders breadcrumbs and deterministic pagination controls
164
+ wraps the publication's own footer card) and `header-actions` (the appearance
165
+ control, core content rather than a module) renders empty. This module also
166
+ renders breadcrumbs and deterministic pagination controls
154
167
  (`rel="prev"`/`rel="next"`, an `aria-current="page"` status).
155
168
  - **`src/core/internal/page-kinds.js`** builds every generated page kind from a
156
169
  validated `build-input:2.0.0`: `profile` (the optional publication profile),
@@ -162,10 +175,10 @@ through:
162
175
  page per distinct authored value, plus a `/tags`/`/series` root page linking
163
176
  to each), and `archive` (one listing page per UTC calendar year, plus a
164
177
  `/archive` root page). The `error` page kind (`renderErrorPageBody`) is
165
- exported as a pure function for a future `404.html` caller (S2-T08, out of
166
- this task's scope), not wired into any generated route here. Listing page size
167
- (10), sort tie-break, and the tag/series/archive root pages are documented
168
- scoped decisions the brief text this task read does not fix verbatim.
178
+ exported as a pure function for the `404.html` caller (see below), not wired
179
+ into any generated route here. Listing page size (10), sort tie-break, and the
180
+ tag/series/archive root pages are documented scoped decisions this renderer's
181
+ own governing brief does not fix verbatim.
169
182
  - **`src/core/internal/messages.js`** is the message catalog every user-visible
170
183
  chrome string above is looked up from (never an inline literal), keyed by
171
184
  BCP-47 tag with a fixed `en` fallback — the only catalog S2 authors;
@@ -186,19 +199,19 @@ every page (a scoped decision: navigation/footer text does not flip language
186
199
  page to page); each page's own `<html lang>`/`dir` still reflects that
187
200
  individual page's own content language.
188
201
 
189
- S2-T07 adds the Light/Dark/System appearance controller
190
- (`src/core/internal/appearance/`), the only browser bootstrap S2 emits (brief S2
191
- section 3, "Module absence and CSP equality"):
202
+ The Light/Dark/System appearance controller (`src/core/internal/appearance/`) is
203
+ the only browser bootstrap this renderer emits ("Module absence and CSP
204
+ equality"):
192
205
 
193
206
  - **`contract.js`** is the one module every other appearance-related file
194
- imports every name/value from: the two DEC-097-fixed root attributes
207
+ imports every name/value from: the two fixed root attributes
195
208
  (`data-gala-publication-root` presence, `data-gala-resolved-color-mode`
196
209
  exact-value `"light"`/`"dark"`), the template-internal
197
210
  `data-gala-color-mode-selection` bookkeeping attribute, the three canonical
198
211
  mode values (`light`/`dark`/`system`), the versioned local storage key
199
212
  (`gala:appearance:color-mode:v1`), the control's `id`, the bootstrap script's
200
213
  fixed output path/media type and the `color-scheme` meta content — the
201
- contract S2-T12's published theme styling catalog targets.
214
+ contract the published theme styling catalog targets.
202
215
  - **`controller-markup.js`** renders a native `<select>` (a brief-admitted
203
216
  pattern; needs no `aria-pressed`/`aria-checked` bookkeeping of its own, unlike
204
217
  a custom button-group or radio-group would) bound to a visible `<label>`, with
@@ -225,29 +238,47 @@ section 3, "Module absence and CSP equality"):
225
238
  directory and enters it into the manifest as an ordinary `manifestAsset` row
226
239
  (`{path, byteLength, sha256}` plus its fixed
227
240
  `application/javascript; charset=utf-8` media type), after the Eleventy route
228
- listing — exactly like an S2-T05 media derivative — so it is never mistaken
229
- for an HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout
230
- carries the `data-gala-publication-root` attribute, the
241
+ listing — exactly like a media derivative — so it is never mistaken for an
242
+ HTML route. `src/core/internal/eleventy-render.js`'s skeleton layout carries
243
+ the `data-gala-publication-root` attribute, the
231
244
  `<meta name="color-scheme" content="light dark">` tag and the one
232
245
  `<script src>` reference, all fixed literals from `contract.js`, so every
233
- route's `<head>` is still byte-identical (S2 materializes no module
246
+ route's `<head>` is still byte-identical (this renderer materializes no module
234
247
  package/configuration/output/runtime).
235
248
  - With JavaScript disabled, the control still renders (a native `<select>`,
236
- fully keyboard-operable and usable with no script at all) and the resolved
237
- root attribute is simply never set; `<meta name="color-scheme">` alone already
238
- carries `prefers-color-scheme` through to user-agent styling and the initial
239
- paint independent of any script.
240
-
241
- S2-T12 replaces `contracts/theme-styling-contract.jcs`'s S2-T02 scaffold
242
- placeholder with the real, closed `templateStylingContract` catalog DEC-097
243
- section 4 defines, and wires the selected theme package's stylesheets into every
244
- generated page:
249
+ fully keyboard-operable and usable with no script at all). The resolved root
250
+ attribute is server-rendered as the fixed `light` default, so a no-JS reader,
251
+ an archival crawler, or a load where the bootstrap script fails still gets a
252
+ fully themed page instead of unstyled UA-default HTML; a scripted reader's
253
+ phase 1 bootstrap always overwrites this attribute with the reader's real
254
+ stored selection/system preference before first paint.
255
+ `<meta name="color-scheme">` also carries `prefers-color-scheme` through to
256
+ user-agent styling (form controls, scrollbars) independent of any script.
257
+
258
+ **Authoring a theme:** see [`docs/theme-authoring.md`](docs/theme-authoring.md)
259
+ for the full guide — how a theme is a delta over the template-owned `gala-base`
260
+ layer, the closed 35-token catalog, the closed five-member pseudo-class catalog,
261
+ passive-asset/SVG admission, the `theme.json` digest chain, and `templateRange`.
262
+ Run `node scripts/scaffold-theme.mjs <directory>` to write a minimal,
263
+ already-conformant theme package skeleton rather than hand-typing one.
264
+
265
+ The theme styling contract replaces `contracts/theme-styling-contract.jcs`'s
266
+ original scaffold placeholder with the real, closed `templateStylingContract`
267
+ catalog this renderer's own internal styling decision defines, and wires the
268
+ selected theme package's stylesheets into every generated page:
245
269
 
246
270
  - **`src/core/internal/appearance/styling-contract.js`** is the one reviewed
247
271
  source module `scripts/generate-contracts.mjs` emits the contract from: the
248
- ordered four-layer catalog (`gala-tokens`/`gala-components`/`gala-utilities`/
249
- `gala-print`), the publication-root/resolved-palette selectors, the closed
250
- type/class/id/attribute leaf catalogs, and exactly 64 `publicThemeSlotHooks` —
272
+ ordered five-layer catalog (`gala-base`/`gala-tokens`/`gala-components`/
273
+ `gala-utilities`/`gala-print` — contract 2.1.0: `gala-base` is the template's
274
+ own layer, carrying the reset/type-scale/focus-ring defaults documented below
275
+ `internal/appearance/base-layer.js`, always ordered first so a theme's own
276
+ layers can override it), the publication-root/ resolved-palette selectors, the
277
+ closed type/class/id/attribute leaf catalogs, a closed five-member
278
+ pseudo-class catalog (`hover`, `focus-visible`, `active`, `visited`,
279
+ `disabled` — contract 2.1.0; see that module's own documentation for why each
280
+ is admitted and why the token catalog's `color-focus`/`color-link-visited` had
281
+ no reachable application before it), and exactly 64 `publicThemeSlotHooks` —
251
282
  one per catalog leaf, so every leaf a theme could validly select is a named,
252
283
  documented hook and no catalog member is orphaned. Every leaf is drawn from
253
284
  what this renderer actually renders: 28 type-selector hooks (every
@@ -255,16 +286,16 @@ generated page:
255
286
  `internal/page-kinds.js` and the markdown-it CommonMark pipeline can produce),
256
287
  15 class-selector hooks (the base Prism `.token` class plus one
257
288
  `.language-<grammar>` hook per admitted highlight grammar — fine-grained
258
- per-token-kind classes are a documented S2 scope exclusion), 2 id-selector
259
- hooks (`#main-content`, the appearance `<select>`'s fixed id) and 19
260
- attribute-value hooks (one per `data-gala-slot` value, one per the new
261
- `data-gala-page-kind` value). `catalogDigest` is
289
+ per-token-kind classes are a documented scope exclusion), 2 id-selector hooks
290
+ (`#main-content`, the appearance `<select>`'s fixed id) and 19 attribute-value
291
+ hooks (one per `data-gala-slot` value, one per the `data-gala-page-kind`
292
+ value). `catalogDigest` is
262
293
  `SHA256(UTF8("GALA-TEMPLATE-STYLING-CONTRACT-V2\0") || JCS(...))`, matching
263
- DEC-097 section 8 and the digest profile `@rathnasgala2/schemas`' own internal
264
- `templateStylingContract` profile uses (that function is not exported through
265
- the package's public API, so this module reimplements the same
266
- domain-separated formula rather than importing it).
267
- `assertTemplateStylingContractShape` is this repository's own structural
294
+ the same domain-separated digest construction and the digest profile
295
+ `@rathnasgala2/schemas`' own internal `templateStylingContract` profile uses
296
+ (that function is not exported through the package's public API, so this
297
+ module reimplements the same domain-separated formula rather than importing
298
+ it). `assertTemplateStylingContractShape` is this repository's own structural
268
299
  self-validator (root/palette attribute rows, fixed layer/selector/composition
269
300
  constants, hook count/uniqueness bound, `catalogDigest` recomputation); no
270
301
  `urn:gala:schema:template-styling-contract` schema ID is registered in
@@ -286,50 +317,74 @@ generated page:
286
317
  `@rathnasgala2/schemas`' own exported `validateGalaDocument`.
287
318
  - **`src/core/internal/theme-assets.js`** is the optional theme-asset
288
319
  integration layer: given `options.themeDirectory` (an extracted theme package
289
- directory, shaped exactly like the S2-T13 package file set — `theme.json`,
290
- `tokens.css`, `components.css`, an optional `utilities.css`, `print.css`,
291
- `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies the declared
292
- stylesheets and any declared non-CSS passive assets into `assets/theme/`,
293
- enters each as an ordinary `{path, byteLength, sha256}` `manifestAsset` row,
294
- and renders the ordered `<link>` markup every generated page's `<head>`
295
- inserts (`print.css` alone carries `media="print"`). It validates the theme's
296
- own declared `stylesheets` list against the two admitted shapes (with or
297
- without `utilities.css`) and that `cssLayers` byte-equals the corresponding
298
- layer projection, but does **not** re-implement DEC-097's full CSS Syntax
299
- Module admission grammar — that is the shared `theme-release.yml` conformance
300
- runner's job (S2-T11), run once per theme release, never per publication
320
+ directory, shaped exactly like the theme package's own file set —
321
+ `theme.json`, `tokens.css`, `components.css`, an optional `utilities.css`,
322
+ `print.css`, `package.json`, `LICENSE`, `README.md`, `assets/*`), it copies
323
+ the declared stylesheets and any declared non-CSS passive assets into
324
+ `assets/theme/`, enters each as an ordinary `{path, byteLength, sha256}`
325
+ `manifestAsset` row, and renders the ordered `<link>` markup every generated
326
+ page's `<head>` inserts (`print.css` alone carries `media="print"`). It
327
+ validates the theme's own declared `stylesheets` list against the two admitted
328
+ shapes (with or without `utilities.css`) and that `cssLayers` byte-equals the
329
+ corresponding layer projection, but does **not** re-implement the full CSS
330
+ Syntax Module admission grammar — that is the shared `theme-release.yml`
331
+ conformance runner's job, run once per theme release, never per publication
301
332
  build. `renderPublication` falls back to the pre-existing fixed
302
333
  `assets/theme/print.css` link when no `options.themeDirectory` is supplied
303
- (every caller before S2-T12, and this repository's own tests that do not
304
- exercise theme integration). Minimal in-repo fixture theme packages for both
334
+ (every caller that predates theme integration, and this repository's own tests
335
+ that do not exercise it). Minimal in-repo fixture theme packages for both
305
336
  stylesheet-list shapes live under `test/fixtures/theme-fixture-full/` and
306
337
  `test/fixtures/theme-fixture-minimal/` — deliberately not real, conformant
307
- theme CSS (authoring one is S2-T13's task); they exist only to exercise the
308
- copy/link/manifest mechanism.
338
+ theme CSS (authoring one is a theme package's own task); they exist only to
339
+ exercise the copy/link/manifest mechanism.
340
+
341
+ Hardening at consume time (a theme package is lower-trust supply-chain input
342
+ than the repository owner's own authored content, so it is held to at least the
343
+ same bar): `theme.json` is schema-validated against
344
+ `urn:gala:schema:theme-contract:2.0.0` before anything else, and its
345
+ `contractVersion`/`stylingContractDigest`/ `templateRange` are checked against
346
+ this renderer's own published styling contract and version
347
+ (`internal/semver-range.js`: a deliberately closed exact-version/caret-range
348
+ subset). Every declared file's `byteLength`/ `sha256` is verified against the
349
+ bytes actually read, and `theme.json.budgets` is enforced. Every passive asset's
350
+ bytes are sniffed (never trusted from the declared `mediaType`) against a closed
351
+ allowlist: raster formats are admitted directly, and SVG is admitted only after
352
+ `internal/media/theme-svg-sanitizer.js`'s closed grammar rejects any
353
+ `<script>`/event-handler/external-reference construct outright. **Iconography is
354
+ in scope and expected** (TPL-H6 decision): this sanitizer is what makes a
355
+ theme-declared SVG icon mark safe to publish.
356
+
357
+ - **`baseUrl` path prefix (2.2.0).** `buildInput.baseUrl` may carry a path (a
358
+ GitHub project site, `https://owner.github.io/g9/`); it is not origin-only.
359
+ `internal/route.js#derivePublicBasePath(baseUrl, basePath)` joins that path
360
+ with `basePath` into the one public base path every emitted URL uses (route
361
+ and asset hrefs, the appearance script, stylesheet links, canonical/`og:url`,
362
+ feed, sitemap and search-index URLs). Output file paths still use `basePath`
363
+ alone, because the artifact is deployed at the hosting site root.
309
364
  - **basePath fix (LOCAL-7 follow-up; independent-review finding B2).** Before
310
- S2-T12, the appearance bootstrap script's `<script src>` and the social-image
311
- `og:image`/`twitter:image` URL were both root-absolute, unprefixed by the
312
- publication's own `basePath` — a non-root `basePath` publication would 404
313
- them. `internal/appearance/contract.js`'s new
365
+ theme integration existed, the appearance bootstrap script's `<script src>`
366
+ and the social-image `og:image`/`twitter:image` URL were both root-absolute,
367
+ unprefixed by the publication's own `basePath` — a non-root `basePath`
368
+ publication would 404 them. `internal/appearance/contract.js`'s new
314
369
  `appearanceBootstrapScriptHref(basePath)` and `internal/seo.js`'s
315
370
  `resolveSocialImageUrl` (now taking `basePath`) both join through
316
371
  `internal/route.js`'s shared `joinBasePathAndRoute`/`projectFixedAssetPath`,
317
372
  exactly like the pre-existing print-stylesheet convention (now folded into
318
373
  `internal/theme-assets.js`) already did for _hrefs_. Independent review
319
374
  (finding B2) found that this was only half the fix: the _physical files_ this
320
- renderer writes for every asset class — theme CSS/passive assets, every S2-T05
321
- media derivative, and the S2-T07 appearance bootstrap script — were still
322
- written, and entered into the manifest, at their bare **unprefixed** paths.
323
- Since the local-directory adapter serves a `manifestAsset.path` verbatim, a
324
- non-root `basePath` publication's `<link>`/`<script src>`/`og:image` href
325
- would point at `<basePath>/assets/...` while the actual file sat at the
326
- unprefixed `assets/...` — a 404 in practice, not just a theoretical href
327
- mismatch. `src/core/index.js` now joins `basePath` into the physical
328
- destination path and the `manifestAsset.path` for media derivatives
329
- (`joinedMediaAssets`, computed with the same `projectFixedAssetPath` every
330
- other fixed-name generated asset already uses) and the appearance bootstrap
331
- script (`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its
332
- joined output path once per file and derives both the `<link href>` and the
375
+ renderer writes for every asset class — theme CSS/passive assets, every media
376
+ derivative, and the appearance bootstrap script — were still written, and
377
+ entered into the manifest, at their bare **unprefixed** paths. Since the
378
+ local-directory adapter serves a `manifestAsset.path` verbatim, a non-root
379
+ `basePath` publication's `<link>`/`<script src>`/`og:image` href would point
380
+ at `<basePath>/assets/...` while the actual file sat at the unprefixed
381
+ `assets/...` — a 404 in practice, not just a theoretical href mismatch.
382
+ `src/core/index.js` now joins `basePath` into the physical destination path
383
+ and the `manifestAsset.path` for media derivatives (`joinedMediaAssets`,
384
+ computed with the same `projectFixedAssetPath` every other fixed-name
385
+ generated asset already uses) and the appearance bootstrap script
386
+ (`joinedAppearanceScriptPath`); `internal/theme-assets.js` computes its joined
387
+ output path once per file and derives both the `<link href>` and the
333
388
  file/manifest path from that exact same value, so they cannot drift apart.
334
389
  `test/theme-styling-contract.test.js` carries a dedicated non-root,
335
390
  multi-segment `basePath` (`/blog/2024`) fixture test asserting every one of
@@ -356,14 +411,15 @@ generated page:
356
411
 
357
412
  ### Media pipeline scope decisions (documented, not silently assumed)
358
413
 
359
- Neither the S2 brief nor DEC-097 fixes exact derivative widths, output
360
- formats/quality, or a per-publication media byte/count ceiling for _author_
361
- media (DEC-097's own numeric bounds are stated for the closed
362
- `gala-theme-binary-assets-v2` _theme passive-asset_ profile). This renderer
363
- therefore documents its own choices in `src/core/internal/media/limits.js`
364
- rather than inventing undocumented magic numbers:
414
+ Neither this renderer's own governing brief nor its internal styling decision
415
+ fixes exact derivative widths, output formats/quality, or a per-publication
416
+ media byte/count ceiling for _author_ media (those numeric bounds are stated
417
+ only for the closed `gala-theme-binary-assets-v2` _theme passive-asset_
418
+ profile). This renderer therefore documents its own choices in
419
+ `src/core/internal/media/limits.js` rather than inventing undocumented magic
420
+ numbers:
365
421
 
366
- - decode/resource ceilings reuse DEC-097's theme binary-asset numbers
422
+ - decode/resource ceilings reuse the theme binary-asset profile's numbers
367
423
  (dimension, pixel-count, decoded-buffer, source-byte caps), since the
368
424
  decode-bomb risk they defend against is identical regardless of asset class;
369
425
  - responsive derivative widths (`320/640/960/1280/1920`), JPEG derivative
@@ -375,8 +431,8 @@ rather than inventing undocumented magic numbers:
375
431
  proportion to this task, and is flagged as a follow-up rather than attempted
376
432
  partially.
377
433
 
378
- S2-T08 adds every remaining "Required generated output" from brief S2 section 3
379
- on top of S2-T06's page kinds:
434
+ Every remaining "Required generated output" from this renderer's own governing
435
+ brief sits on top of the page kinds above:
380
436
 
381
437
  - **Feeds** (`src/core/internal/feeds.js`): deterministic Atom 1.0
382
438
  (`/feed/atom.xml`) and RSS 2.0 (`/feed/rss.xml`) documents over the exact
@@ -384,7 +440,8 @@ on top of S2-T06's page kinds:
384
440
  `internal/page-kinds.js`'s `index` page kind paginates (the shared
385
441
  `selectPublishedArticles` export), every item URL absolute against
386
442
  `buildInput.baseUrl`, bounded to the most recent 50 entries (documented scoped
387
- decision — neither the brief nor DEC-097 fixes a feed item count).
443
+ decision — neither the brief nor this renderer's own styling decision fixes a
444
+ feed item count).
388
445
  - **Sitemap** (`src/core/internal/sitemap.js`): `/sitemap.xml` over every page
389
446
  `internal/page-kinds.js` generated that this renderer did not itself mark
390
447
  `noindex` (unlisted content), deterministically sorted by each entry's own
@@ -394,9 +451,10 @@ on top of S2-T06's page kinds:
394
451
  `/search-index.json`, a deterministic JSON document indexing every published
395
452
  `article`/`page` record's title, description, a bounded plain-text excerpt,
396
453
  tags and timestamps. Its shape is a documented scoped decision (neither the
397
- brief nor DEC-097 fixes one). **No runtime search JavaScript accompanies it**
398
- — the Light/Dark/System appearance controller (S2-T07) remains the only
399
- browser bootstrap this renderer ever emits in all of S2 (acceptance test C).
454
+ brief nor this renderer's own styling decision fixes one). **No runtime search
455
+ JavaScript accompanies it** — the Light/Dark/System appearance controller
456
+ remains the only browser bootstrap this renderer ever emits (acceptance test
457
+ C).
400
458
  - **SEO/Open-Graph/Twitter/localization metadata**: every generated page's
401
459
  `<head>` now carries a viewport meta, an optional description, an optional
402
460
  `robots` directive (`noindex, follow` for `status: "unlisted"` content and for
@@ -406,16 +464,15 @@ on top of S2-T06's page kinds:
406
464
  `<link>`s. **hreflang** is a documented scope decision, not a silent omission:
407
465
  `build-input:2.0.0` has no translation/locale-variant linkage between distinct
408
466
  `content[]` records (see `internal/seo.js`'s module documentation), so no
409
- `<link rel="alternate" hreflang="...">` is ever emitted in S2.
410
- - **`404.html`**: the S2-T06 `error` page kind's pure `renderErrorPageBody`, now
411
- actually wired into `renderPublication` at DEC-097's exact `errorDocumentKey`
467
+ `<link rel="alternate" hreflang="...">` is ever emitted by this renderer.
468
+ - **`404.html`**: the `error` page kind's pure `renderErrorPageBody`, now
469
+ actually wired into `renderPublication` at the fixed `errorDocumentKey`
412
470
  (`internal/route.js#errorDocumentPath`) — `404.html` at the root, or
413
471
  `<basePath>/404.html` otherwise.
414
- - **Redirect documents**: unchanged from S2-T03
472
+ - **Redirect documents**: unchanged from the renderer adapter
415
473
  (`internal/route.js#renderRedirectDocument`) — every authored redirect was
416
474
  already an ordinary generated artifact with manifest status fixed at `200`;
417
- this task's own scope note is documenting that fact here, not re-implementing
418
- it.
475
+ this is documenting that fact here, not re-implementing it.
419
476
  - **Print CSS hookup** (`src/core/internal/print-stylesheet.js`): every page's
420
477
  `<head>` carries a `<link rel="stylesheet" media="print">` at the documented
421
478
  conventional path `<basePath>/assets/theme/print.css`. A selected theme's own
@@ -430,11 +487,16 @@ by the route's own fixed output path.
430
487
 
431
488
  ## Prerequisites
432
489
 
433
- - Node.js 24.18.0
434
- - npm 11.16.0
490
+ - Node.js >= 24.0.0 (this repository's own development/CI toolchain is Node.js
491
+ 24.18.0)
492
+ - npm >= 11.0.0 (this repository's own development/CI toolchain is npm 11.16.0)
435
493
 
436
- Both are enforced by `package.json` `engines`/`devEngines`; `.nvmrc` and
437
- `.node-version` carry the same pin.
494
+ `package.json`'s published `engines` field declares the supported _range_
495
+ (TPL-H5 fix: an exact pin published in the tarball rejects every consumer not on
496
+ that precise patch version, including under a consumer's own `engine-strict`
497
+ setting). This repository's own exact toolchain is pinned separately in
498
+ `devEngines` (evaluated only for this project root, never for a consumer) and in
499
+ `.nvmrc`/`.node-version`.
438
500
 
439
501
  ## Commands
440
502
 
@@ -443,11 +505,15 @@ npm install
443
505
  npm run verify
444
506
  ```
445
507
 
446
- `npm run verify` runs, in order: Prettier format check, ESLint,
508
+ `npm run verify` (this description is kept in sync with `package.json`'s own
509
+ `verify` script string by `test/readme-verify-description.test.js`, not
510
+ hand-maintained) runs, in order: Prettier format check, ESLint,
447
511
  `tsc --checkJs --noEmit`, the dependency-cruiser
448
512
  architecture/module-tree-absence gate, `jscpd` duplication scan, the
449
- `contracts/*.jcs` canonical-form check, `node --test`, the license inventory
450
- check, and `cyclonedx-npm` SBOM generation plus presence check.
513
+ `contracts/*.jcs` canonical-form check, the no-local-schema-pin check,
514
+ `node --test`, the license inventory check, the SBOM currency check
515
+ (`sbom:check` compares `sbom.cdx.json` against the current lockfile — it is not
516
+ a presence check), the workflow-pin check, and `npm audit --audit-level=high`.
451
517
 
452
518
  Individual gates:
453
519
 
@@ -458,21 +524,25 @@ npm run lint # ESLint flat config
458
524
  npm run typecheck # tsc --checkJs --noEmit against types/index.d.ts
459
525
  npm run architecture # dependency-cruiser: no src/modules/, no circular deps
460
526
  npm run duplication # jscpd, 3% / 50 tokens
461
- npm run contracts:generate # (re)write contracts/*.jcs placeholders
527
+ npm run contracts:generate # (re)write contracts/*.jcs from their reviewed source modules
462
528
  npm run contracts:check # fail if committed contracts drifted
529
+ npm run schema-pin:check # fail on a file: @rathnasgala2/schemas specifier
463
530
  npm test # node --test
464
531
  npm run licenses:generate # (re)write THIRD_PARTY_LICENSES.json
465
532
  npm run licenses:check # fail if it drifted from package-lock.json
466
533
  npm run sbom:generate # cyclonedx-npm -> sbom.cdx.json
467
- npm run sbom:check # fail if sbom.cdx.json is missing
534
+ npm run sbom:check # fail if sbom.cdx.json is stale against package-lock.json
535
+ npm run workflows:check # fail if a GitHub Actions ref is not SHA-pinned
536
+ npm run workflows:drift # fail if workflow files diverge from their own baseline
537
+ npm run audit # npm audit --audit-level=high
468
538
  ```
469
539
 
470
540
  ## Package layout
471
541
 
472
542
  ```text
473
543
  src/core/ # the only source root
474
- contracts/render-policy.jcs # normalized-body policy identity (S2-T04, real content)
475
- contracts/theme-styling-contract.jcs # published styling catalog (S2-T12, real and closed)
544
+ contracts/render-policy.jcs # normalized-body policy identity (real content)
545
+ contracts/theme-styling-contract.jcs # published styling catalog (real and closed)
476
546
  ```
477
547
 
478
548
  No `src/modules/` tree, module import edge, registration stub, module
@@ -481,7 +551,7 @@ configuration, module output or module runtime is ever admitted beside
481
551
  `only-src-core-is-a-source-root` rule both enforce this; `interactions`,
482
552
  `whitelabel`, `newsletter` and `prism` have no module directory, package,
483
553
  configuration schema, stub, output, runtime code, network target or
484
- compatibility promise in this MVP (DEC-097 section 2).
554
+ compatibility promise in this MVP.
485
555
 
486
556
  ## Consuming `@rathnasgala2/schemas`
487
557
 
@@ -508,8 +578,12 @@ and `artifact-manifest` are byte-identical from 2.6.1 to 2.8.0.
508
578
 
509
579
  ## Governing documents
510
580
 
511
- - [Slice brief S2: author-owned publication](../../orchestration/slice-briefs/S2-author-owned-publication.md),
512
- section 3 (`@rathnasgala2/template`)
513
- - [DEC-094](../../orchestration/decisions/DEC-094-javascript-esm-public-packages.md):
514
- JavaScript ESM for public packages
515
- - [WORKSPACE.md](../../orchestration/WORKSPACE.md), sections 4-8 and 13
581
+ The internal specifications this package is built against live outside this
582
+ repository and have no public URL, so they are named here as plain text rather
583
+ than as dead links:
584
+
585
+ - Slice brief S2: author-owned publication, section 3 (`@rathnasgala2/template`)
586
+ (internal)
587
+ - The internal decision record establishing JavaScript ESM for public packages
588
+ (internal)
589
+ - WORKSPACE.md, sections 4-8 and 13 (internal)
@@ -1 +1 @@
1
- {"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}
1
+ {"contentSecurityPolicy":"default-src 'none'; base-uri 'none'; object-src 'none'; frame-ancestors 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","contentSecurityPolicyHeaderOnlyDirectives":["frame-ancestors 'none'"],"contentSecurityPolicyMeta":"default-src 'none'; base-uri 'none'; object-src 'none'; form-action 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'none'; media-src 'self'; manifest-src 'self'; worker-src 'none'","highlighter":{"grammars":["bash","css","diff","java","javascript","json","jsx","markdown","markup","python","sql","tsx","typescript","yaml"],"package":"@11ty/eleventy-plugin-syntaxhighlight","tokenClasses":["atrule","attr-equals","attr-name","attr-value","bold","boolean","builtin","cdata","char","class-name","comment","constant","content","coord","deleted","deleted-sign","doctype","entity","for-or-select","function","function-variable","important","inserted","inserted-sign","interpolation","interpolation-punctuation","italic","key","keyword","namespace","null","number","operator","prefix","prolog","property","punctuation","regex","rule","selector","shebang","string","symbol","tag","template-punctuation","template-string","title","token","url","variable"],"version":"5.0.2"},"markdownIt":{"options":{"html":false,"linkify":false,"typographer":false},"package":"markdown-it","version":"14.3.1"},"name":"gala-render-policy","sanitizer":{"allowedAttributes":{"a":["href","rel"],"code":["class"],"h1":["id"],"h2":["id"],"h3":["id"],"h4":["id"],"h5":["id"],"h6":["id"],"img":["src","alt"],"ol":["start"],"pre":["class"],"span":["class"]},"allowedTags":["p","br","hr","h1","h2","h3","h4","h5","h6","ul","ol","li","blockquote","pre","code","strong","em","b","i","u","s","del","ins","sub","sup","mark","small","wbr","a","img","table","thead","tbody","tr","th","td","dl","dt","dd","figure","figcaption","span"],"allowedUrlSchemes":["https","http","mailto"],"package":"sanitize-html","version":"2.17.7"},"version":"2.0.0"}