@homepages/template-kit 0.2.0 → 0.3.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 (74) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +61 -17
  3. package/dist/base.css +9 -0
  4. package/dist/cli/check/config.js +37 -0
  5. package/dist/cli/check/css.js +117 -0
  6. package/dist/cli/check/diagnostics.js +32 -0
  7. package/dist/cli/check/index.js +87 -0
  8. package/dist/cli/check/loader.js +142 -0
  9. package/dist/cli/check/paths.js +15 -0
  10. package/dist/cli/check/relativize.js +18 -0
  11. package/dist/cli/check/render-invariants.js +24 -0
  12. package/dist/cli/check/resolve-tool.js +38 -0
  13. package/dist/cli/check/stages/deps.js +337 -0
  14. package/dist/cli/check/stages/lint.js +46 -0
  15. package/dist/cli/check/stages/render.js +101 -0
  16. package/dist/cli/check/stages/size.js +158 -0
  17. package/dist/cli/check/stages/tree.js +207 -0
  18. package/dist/cli/check/stages/typecheck.js +46 -0
  19. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  20. package/dist/cli/check/stages/validate/facts.js +18 -0
  21. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  22. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  23. package/dist/cli/check/stages/validate/index.js +66 -0
  24. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  25. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  26. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  27. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  28. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  29. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  30. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  31. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  32. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  33. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  34. package/dist/cli/check/workspace.js +86 -0
  35. package/dist/cli/theme/generate.js +43 -0
  36. package/dist/cli/theme/index.js +33 -0
  37. package/dist/cli/theme/load-theme.js +69 -0
  38. package/dist/cli.js +22 -4
  39. package/dist/contracts/fill-treatments.js +47 -0
  40. package/dist/design-system/theme.d.ts +30 -300
  41. package/dist/design-system/theme.js +112 -90
  42. package/dist/eslint/rules/no-hex.js +78 -6
  43. package/dist/eslint.js +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +2 -2
  46. package/dist/package.js +1 -1
  47. package/dist/primitives/Image.js +1 -1
  48. package/dist/schema/fill-spec.d.ts +80 -590
  49. package/dist/schema/fixture-schema.d.ts +5 -81
  50. package/dist/schema/manifest.d.ts +39 -475
  51. package/dist/schema/resolve-section-ref.js +10 -0
  52. package/dist/schema/rows.js +10 -0
  53. package/dist/schema/section-nav.js +9 -0
  54. package/dist/schema/section-schema.d.ts +51 -437
  55. package/dist/schema/slot-types.d.ts +12 -16
  56. package/dist/styles.css +31 -88
  57. package/docs/INDEX.md +4 -3
  58. package/docs/check.md +121 -0
  59. package/docs/eslint.md +18 -8
  60. package/docs/llms.txt +21 -9
  61. package/docs/primitives.md +5 -0
  62. package/docs/rules/INDEX.md +11 -4
  63. package/docs/rules/bundle-binary-asset.md +102 -0
  64. package/docs/rules/fixtures-invalid.md +96 -0
  65. package/docs/rules/license-denied.md +11 -3
  66. package/docs/rules/manifest-invalid.md +99 -0
  67. package/docs/rules/no-hex.md +76 -10
  68. package/docs/rules/no-templates.md +87 -0
  69. package/docs/rules/parse-error.md +76 -0
  70. package/docs/rules/schema-invalid.md +96 -0
  71. package/docs/rules/size-section-css.md +2 -2
  72. package/docs/theme-and-css.md +155 -90
  73. package/package.json +4 -9
  74. package/tsconfig.json +1 -1
@@ -0,0 +1,96 @@
1
+ ---
2
+ purpose: The template-kit/fixtures-invalid authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md, schema-invalid.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/fixtures-invalid
8
+
9
+ > `fixtures.ts` is the one contract file nothing else parses for you. This is that parse.
10
+
11
+ ## Rule
12
+
13
+ The default export of `fixtures.ts` must satisfy the fixture-module shape:
14
+
15
+ ```ts
16
+ {
17
+ base: {
18
+ slots: { /* every required slot, with a valid value */ },
19
+ options: { /* optional */ },
20
+ anchors: { /* optional */ },
21
+ variants: { /* optional */ },
22
+ },
23
+ option_matrix: [ /* optional: { label?, options } entries */ ],
24
+ states: { /* optional: named scenario → { label?, slots?, options?, anchors?, variants? } */ },
25
+ }
26
+ ```
27
+
28
+ `base` is required and must be an object holding a `slots` record — every other key is
29
+ optional. Each violation names the exact path inside the module that's wrong (`"base"`,
30
+ `"base.slots"`, `"states.empty-address"`, …) and what shape was expected there instead.
31
+
32
+ ## Reason
33
+
34
+ `schema.ts` and `fill-spec.ts` are both Zod-parsed on the way into every other check, so a
35
+ malformed one is caught immediately. `fixtures.ts` used to be the exception: nothing
36
+ touched it until the render stage read it directly, so a wrong shape there surfaced as a
37
+ raw crash mid-render, blamed on whatever line happened to dereference the missing field —
38
+ never on the fixture that was actually wrong.
39
+
40
+ This gate exists to move that failure to where the mistake was made, with a message about
41
+ the fixture module instead of a stack trace from three files away.
42
+
43
+ ## Fix
44
+
45
+ Read the message's path — it points at the exact key that's wrong — and bring that key's
46
+ shape in line with what's documented above.
47
+
48
+ ### Before
49
+
50
+ ```ts
51
+ // sections/hero/fixtures.ts
52
+ import type { FixtureModule } from "@homepages/template-kit";
53
+
54
+ const fixtures: FixtureModule = {
55
+ base: {
56
+ slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
57
+ },
58
+ states: {
59
+ "no-photo": { slots: { hero_image: null }, label: 12 },
60
+ // ^ label must be a string
61
+ },
62
+ };
63
+
64
+ export default fixtures;
65
+ ```
66
+
67
+ ```text
68
+ Section "hero": fixtures.ts — "states.no-photo.label": Expected string, received number.
69
+ ```
70
+
71
+ ### After
72
+
73
+ ```ts
74
+ // sections/hero/fixtures.ts
75
+ import type { FixtureModule } from "@homepages/template-kit";
76
+
77
+ const fixtures: FixtureModule = {
78
+ base: {
79
+ slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
80
+ },
81
+ states: {
82
+ "no-photo": { slots: { hero_image: null }, label: "No hero photo" },
83
+ },
84
+ };
85
+
86
+ export default fixtures;
87
+ ```
88
+
89
+ ## See also
90
+
91
+ - [The schema system](../schema-system.md) — `fixtures.ts`'s place among the section's
92
+ four contract files.
93
+ - [`schema-invalid`](schema-invalid.md) — the sibling gate on `schema.ts` and
94
+ `fill-spec.ts`.
95
+ - [`bundle-incomplete`](bundle-incomplete.md) — reported instead of this id when
96
+ `fixtures.ts` is missing outright, rather than present and wrong.
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, audit-severity.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/license-denied
8
8
 
@@ -31,8 +31,16 @@ Two cases people expect to slide through, and neither does:
31
31
  - **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
32
32
  permissive one — by default, no rights are granted at all.
33
33
 
34
- **Scope: the production tree only.** `devDependencies` are not evaluated — your bundler,
35
- your test runner, and your formatter do not ship to anyone.
34
+ **Scope: the production tree only.** "Production tree" means exactly what
35
+ `npm ci --omit=dev` installs. `devDependencies` are not evaluated your bundler, your
36
+ test runner, and your formatter do not ship to anyone.
37
+
38
+ **An optional peer you have anyway counts.** If a production package declares an
39
+ optional `peerDependency` and your workspace has that package for any reason (even as a
40
+ `devDependency`), npm resolves the peer and keeps it in the `--omit=dev` install — so it
41
+ is in the production tree and its license is checked, transitively. An optional peer you
42
+ do **not** have is never installed and is not evaluated. Run `npm ci --omit=dev` and look
43
+ at the resulting `node_modules` if you want the exact set the check sees.
36
44
 
37
45
  **There is no per-package override.** No allowlist entry you can add, no waiver comment. By
38
46
  design.
@@ -0,0 +1,99 @@
1
+ ---
2
+ purpose: The template-kit/manifest-invalid authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md, schema-invalid.md, no-templates.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/manifest-invalid
8
+
9
+ > `manifest.json` is the one file that says which sections make a page, and in what
10
+ > order. Every check on it lands under this id.
11
+
12
+ ## Rule
13
+
14
+ A template folder (`templates/<key>/`) must have a `manifest.json` at its root, and that
15
+ file must:
16
+
17
+ - **exist** — a template folder with none at all reports this once, naming the missing
18
+ file;
19
+ - **parse as JSON, and satisfy the manifest shape** — `key`, `name`, `deliverable_type`,
20
+ and an ordered `sections` array of section instances (`section`, `instance_id`,
21
+ `options`, `locked`, `required`, `reorderable`);
22
+ - **agree with its own folder** — the manifest's `key` must match the template folder
23
+ name it lives in;
24
+ - **reference real sections** — every `sections[].section` must resolve to a section this
25
+ workspace actually has;
26
+ - **override only what's real and overridable** — an instance's `options` may only name an
27
+ option the target section declares, only when that option is not
28
+ `template_overridable: false`, and only with a value that actually differs from the
29
+ section's own default;
30
+ - **satisfy the whole-composition nav contract** — the cross-section rules that need the
31
+ full, resolved section set rather than any one section in isolation.
32
+
33
+ Every violation collects into the same run; a manifest with several wrong references
34
+ reports all of them together.
35
+
36
+ ## Reason
37
+
38
+ A section's own four files describe that section in isolation — they say nothing about
39
+ which page it appears on, in what order, or which of its options a specific template has
40
+ chosen to lock in. `manifest.json` is where that composition is declared, and it is the
41
+ only place a downstream consumer (the publisher, the fill pipeline, the editor) can go to
42
+ learn a template's actual shape. A manifest that references a section that doesn't exist,
43
+ or restates a default as if it were an override, is a template whose real composition
44
+ nobody can trust the manifest to describe.
45
+
46
+ ## Fix
47
+
48
+ Read the message: it names the template, the section instance, and what's wrong.
49
+ Missing entirely, add one. Otherwise, fix the field the message points at.
50
+
51
+ ### Before
52
+
53
+ ```text
54
+ templates/acme-modern/
55
+ manifest.json
56
+ sections/
57
+ hero/
58
+ ```
59
+
60
+ ```json
61
+ // templates/acme-modern/manifest.json
62
+ {
63
+ "key": "acme-classic",
64
+ "name": "Acme Classic",
65
+ "deliverable_type": "single_property_website",
66
+ "sections": [
67
+ { "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
68
+ ],
69
+ "reconcile": []
70
+ }
71
+ ```
72
+
73
+ ```text
74
+ Template "acme-modern": manifest key "acme-classic" does not match the template folder name "acme-modern".
75
+ ```
76
+
77
+ ### After
78
+
79
+ ```json
80
+ // templates/acme-modern/manifest.json
81
+ {
82
+ "key": "acme-modern",
83
+ "name": "Acme Modern",
84
+ "deliverable_type": "single_property_website",
85
+ "sections": [
86
+ { "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
87
+ ],
88
+ "reconcile": []
89
+ }
90
+ ```
91
+
92
+ ## See also
93
+
94
+ - [The schema system](../schema-system.md#manifestjson--composing-sections-into-a-template) —
95
+ what a manifest declares and the type it's validated against.
96
+ - [`schema-invalid`](schema-invalid.md) — the same kind of cross-reference check, one
97
+ level down, on a single section's own contract files.
98
+ - [`no-templates`](no-templates.md) — reported instead when the workspace has no
99
+ `templates/<key>/` folder to check at all.
@@ -2,7 +2,7 @@
2
2
  purpose: The template-kit/no-hex authoring rule — what it enforces, why, and how to fix it.
3
3
  status: living
4
4
  related: [INDEX.md, server-vs-client.md, ../theme-and-css.md]
5
- updated: 2026-07-14
5
+ updated: 2026-07-15
6
6
  ---
7
7
  # template-kit/no-hex
8
8
 
@@ -11,11 +11,20 @@ updated: 2026-07-14
11
11
 
12
12
  ## Rule
13
13
 
14
- Two shapes are banned:
14
+ This rule ships as a **warning**, not an error: a section spending theme tokens is
15
+ recommended, not gated. A hard-coded colour squiggles in your editor, but it does not
16
+ fail `template-kit check` (the check gates on errors only, so a warning never reaches it)
17
+ and so does not block ingest either. The guidance below is still the right thing to do —
18
+ it only changes what the linter does with a violation, not whether the violation is
19
+ worth fixing.
15
20
 
16
- - **A CSS colour function**, anywhere in a string or template literal:
17
- `oklch(…)`, `oklab(…)`, `rgb(…)`, `rgba(…)`, `hsl(…)`, `hsla(…)`, `color-mix(…)`.
18
- No context test nobody spells an anchor id `rgb(...)`.
21
+ The rule flags two shapes:
22
+
23
+ - **A CSS colour function that bakes a colour**, anywhere in a string or template
24
+ literal: `oklch(…)`, `oklab(…)`, `rgb(…)`, `rgba(…)`, `hsl(…)`, `hsla(…)`,
25
+ `color-mix(…)`. No context test — nobody spells an anchor id `rgb(...)`. **One
26
+ carve-out:** a `color-mix()` whose colour arguments are all theme-derived is legal —
27
+ see below.
19
28
  - **A hex literal** (3, 4, 6, or 8 digits) **in a colour context**. A colour context is
20
29
  a Tailwind arbitrary-value bracket that resolves to a colour:
21
30
  - a colour property — `[color:#fff]`, `[background:#fff]`, `[background-color:#fff]`,
@@ -35,6 +44,40 @@ anchor ids that happen to be spelled from hex digits.
35
44
  Every hex-shaped token in a string is checked, not just the first — an out-of-context
36
45
  token early in a `className` cannot shadow a genuine colour later in it.
37
46
 
47
+ ### The `color-mix()` carve-out
48
+
49
+ A `color-mix()` carries **no palette of its own** when every one of its colour arguments
50
+ is theme-derived — a `var(--…)` reference, `transparent`, `currentColor`, or a nested
51
+ `color-mix()` judged on these same terms. It composes a colour out of the theme the
52
+ section was handed, so it is as theme-agnostic as `bg-surface` and cannot break when
53
+ another template reuses the section. It is legal:
54
+
55
+ ```
56
+ hover:bg-[color-mix(in_srgb,var(--tr-color-surface)_92%,var(--tr-color-ink))]
57
+ bg-[color-mix(in_srgb,var(--tr-color-ink)_50%,transparent)] ← a translucent tint:
58
+ overlay, scrim, hover wash
59
+ ```
60
+
61
+ A mix weight (`92%`) is not a colour; only the colour arguments are tested.
62
+
63
+ `transparent` and `currentColor` are the only palette-free keywords: `transparent` names
64
+ the *absence* of a colour, and `currentColor` defers to the inherited (theme-driven)
65
+ `color`. Every other CSS named colour (`white`, `black`, `rebeccapurple`, …) is a baked
66
+ palette exactly as a hex is. So these report:
67
+
68
+ ```
69
+ color-mix(in srgb, var(--tr-color-primary) 80%, #fff) ← a hex literal
70
+ color-mix(in srgb, var(--tr-color-primary) 80%, white) ← a CSS named colour
71
+ color-mix(in srgb, var(--tr-color-accent, #fff) 80%, …) ← a var FALLBACK is a baked
72
+ colour the moment the
73
+ property is undefined
74
+ ```
75
+
76
+ The carve-out is `color-mix()` **only** — it is the one colour function that *composes* a
77
+ colour rather than stating one. The other six take raw channel values, so they bake a
78
+ colour by construction: `rgb(0 0 0)`, `oklch(0.7 0.1 20)`, and even
79
+ `rgb(var(--r) var(--g) var(--b))` report.
80
+
38
81
  ## Reason
39
82
 
40
83
  A section carries **zero palette of its own**. It is theme-agnostic, and must look
@@ -49,10 +92,14 @@ not a palette boundary.
49
92
 
50
93
  ## Fix
51
94
 
52
- Use a semantic token utility (`bg-surface-alt`, `text-ink`). If the value is genuinely
53
- brand decoration with no token, add it to the **template's** `theme.ts` palette and use
54
- the `--tr-color-<name>` utility it compiles to — which keeps the value in one place
55
- instead of in one section.
95
+ Use one of your theme's colour utilities. If the value is genuinely brand decoration with
96
+ no token yet, add it to `colors` in the **template's** `theme.ts` the key `lagoon`
97
+ compiles to `--tr-color-lagoon` and the `bg-lagoon` / `text-lagoon` utilities — which
98
+ keeps the value in one place instead of in one section.
99
+
100
+ If you need a colour the theme does not name — a hover, active, or pressed step — derive
101
+ it from the tokens the theme *does* name with `color-mix()`, rather than reaching for a
102
+ literal to lighten or darken with.
56
103
 
57
104
  ### Before
58
105
 
@@ -90,8 +137,27 @@ export function Renderer({ slots }: Props) {
90
137
  }
91
138
  ```
92
139
 
140
+ ### A state colour, derived
141
+
142
+ Lightening a hover step by mixing in a hard-coded white is both the violation and the
143
+ bug: under a dark theme it lightens a button the theme wanted dark. Mix toward a colour
144
+ the **theme** names, and the step follows whatever theme the section is handed.
145
+
146
+ ```tsx
147
+ // Before — `#fff` is a palette this section invented.
148
+ <button className="bg-primary hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,#fff)]">
149
+ {slots.cta}
150
+ </button>
151
+
152
+ // After — every colour argument is a theme var.
153
+ <button className="bg-primary hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,var(--tr-color-surface))]">
154
+ {slots.cta}
155
+ </button>
156
+ ```
157
+
93
158
  ## See also
94
159
 
95
- - [Theme and CSS](../theme-and-css.md#color-roles) — the token system and the colour roles.
160
+ - [Theme and CSS](../theme-and-css.md#tokentheme--your-templates-design-system) — the
161
+ token system, and how a `colors` entry becomes a utility.
96
162
  - [Server vs client](server-vs-client.md) — why the other bans stop at the hydration
97
163
  boundary and this one does not.
@@ -0,0 +1,87 @@
1
+ ---
2
+ purpose: The template-kit/no-templates authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, manifest-invalid.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/no-templates
8
+
9
+ > A run that gates nothing must never report success. This is what a workspace with no
10
+ > template to check reports instead of a silent pass.
11
+
12
+ ## Rule
13
+
14
+ `templates/` must hold at least one template folder — for the run you asked for:
15
+
16
+ - `template-kit check` (no argument, from the workspace root): `templates/` has no
17
+ folder in it at all.
18
+ - `template-kit check --all`: same — no template folder exists to check.
19
+ - `template-kit check <name>` (or a bare `check` run from inside a template's own
20
+ folder): that one named template does not exist. (A folder that exists but is
21
+ missing its `manifest.json` is a different, more specific failure —
22
+ [`manifest-invalid`](manifest-invalid.md) — because a folder under `templates/` is a
23
+ template whatever else is wrong with it.)
24
+
25
+ This is checked once per run, scoped to the workspace, not to any one template.
26
+
27
+ ## Reason
28
+
29
+ `every()` over an empty set is vacuously true. A `check` run that selected zero templates
30
+ and reported "check passed" would exit 0 having run not one gate — indistinguishable from
31
+ a workspace that passed every rule, when in truth no rule ran at all. Anything that
32
+ re-runs this command on your behalf (an ingest pipeline, a CI step) would accept that
33
+ silence as a clean submission.
34
+
35
+ Reporting a diagnostic instead means "nothing to check" is a failure your run surfaces,
36
+ not a false green light.
37
+
38
+ ## Fix
39
+
40
+ Add a template: `templates/<key>/` with a `manifest.json` declaring its section
41
+ composition, and a `sections/` folder holding the sections it composes. If you meant to
42
+ check a specific template and named it wrong, or ran `check` from a folder that isn't a
43
+ template folder, correct the name or your working directory instead.
44
+
45
+ ### Before
46
+
47
+ ```text
48
+ my-workspace/
49
+ package.json
50
+ package-lock.json
51
+ templates/ ← empty
52
+ ```
53
+
54
+ ```text
55
+ $ template-kit check
56
+ ✗ workspace — 1 problem(s)
57
+ [template-kit/no-templates] This workspace has no template to check: templates/ holds no template folder.
58
+ fix: Add a template: templates/<key>/ with a manifest.json declaring its section composition and a sections/ folder holding the sections it composes.
59
+ ```
60
+
61
+ ### After
62
+
63
+ ```text
64
+ my-workspace/
65
+ package.json
66
+ package-lock.json
67
+ templates/
68
+ acme-modern/
69
+ manifest.json
70
+ sections/
71
+ hero/
72
+ Renderer.tsx
73
+ schema.ts
74
+ fill-spec.ts
75
+ fixtures.ts
76
+ ```
77
+
78
+ ```text
79
+ $ template-kit check
80
+ ✓ acme-modern
81
+ check passed
82
+ ```
83
+
84
+ ## See also
85
+
86
+ - [`manifest-invalid`](manifest-invalid.md) — the check that takes over once a template
87
+ folder exists, on whether its `manifest.json` is present and correct.
@@ -0,0 +1,76 @@
1
+ ---
2
+ purpose: The template-kit/parse-error authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, typecheck.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/parse-error
8
+
9
+ > A file so broken it never became an AST. Every other rule needs one; this is what's
10
+ > reported when there isn't one to have an opinion about.
11
+
12
+ ## Rule
13
+
14
+ Every other lint rule runs against a parsed file — an AST it can walk and ask questions
15
+ of. When a `.ts`/`.tsx` file cannot even be parsed (invalid syntax: an unclosed brace, a
16
+ stray token, a JSX tag that never closes), there is no AST for any rule to run against,
17
+ and no rule id of its own to report. This id is what's reported in that one case instead:
18
+ the parser's own message, relocated to the file and line it names.
19
+
20
+ This is the only diagnostic under this id — it is not a rule you can violate by writing
21
+ working code a particular way, the way `no-hex` or `no-inline-style` are. It exists so
22
+ that "the file didn't parse" is a rule-id and a fix hint, not silence or a raw crash.
23
+
24
+ ## Reason
25
+
26
+ A stage never aborts the run — an author fixing one file's problem must see every other
27
+ file's problems in the same run, not discover them one release at a time. A parser
28
+ exception is the one case where that promise is hardest to keep: there is genuinely
29
+ nothing else to check about a file that doesn't parse. Reporting it under its own id,
30
+ rather than dropping it or letting it throw, keeps that promise intact.
31
+
32
+ You may see the **same file** flagged here and under
33
+ [`typecheck`](typecheck.md): the lint stage and the typecheck stage each parse the
34
+ workspace independently, and a syntax error fails both parsers, so both stages report it,
35
+ each in their own words. Fixing the syntax error clears both.
36
+
37
+ ## Fix
38
+
39
+ Read the message — it is the parser's own error, naming the file and the line. Fix the
40
+ syntax at that location.
41
+
42
+ ### Before
43
+
44
+ ```tsx
45
+ // sections/hero/Renderer.tsx
46
+ export function Renderer() {
47
+ return (
48
+ <Section>
49
+ <h1>{headline}</h1>
50
+ </Section
51
+ );
52
+ }
53
+ ```
54
+
55
+ ```text
56
+ sections/hero/Renderer.tsx:6
57
+ Parsing error: '>' expected.
58
+ ```
59
+
60
+ ### After
61
+
62
+ ```tsx
63
+ // sections/hero/Renderer.tsx
64
+ export function Renderer() {
65
+ return (
66
+ <Section>
67
+ <h1>{headline}</h1>
68
+ </Section>
69
+ );
70
+ }
71
+ ```
72
+
73
+ ## See also
74
+
75
+ - [`typecheck`](typecheck.md) — the sibling failure one layer up: a file that parses but
76
+ does not type-check.
@@ -0,0 +1,96 @@
1
+ ---
2
+ purpose: The template-kit/schema-invalid authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md, fixtures-invalid.md, manifest-invalid.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/schema-invalid
8
+
9
+ > `schema.ts` and `fill-spec.ts` are a matched pair. This is every check that makes sure
10
+ > they actually agree.
11
+
12
+ ## Rule
13
+
14
+ A section's `schema.ts` and `fill-spec.ts` must, together:
15
+
16
+ - **parse** — each is `as const satisfies` its shape (`SectionSchemaShape`,
17
+ `FillSpecShape`); a value that doesn't satisfy it fails here with the parse error;
18
+ - **cross-reference correctly** — every `fill-spec.ts` decision produces a slot that
19
+ actually exists in `schema.ts`, every `reads` entry names a real, author-declarable
20
+ facts key, every option's `affects_decisions`/`affects_slots` names a real decision or
21
+ slot;
22
+ - **satisfy the closed per-type contracts** — an `object_list` slot's `item_order`
23
+ covers every field, a `select` slot's cases match its transform, a group's members are
24
+ slots that exist, and the rest of the per-slot-type rules the schema system documents.
25
+
26
+ One violation is one diagnostic; a section with several wrong things reports all of them
27
+ in the same run, not one per fix.
28
+
29
+ This is the id you also see when a section **cannot be built or parsed at all** — a
30
+ syntax error in `schema.ts`, a missing export, an import that doesn't resolve. The
31
+ message in that case is the underlying build error, relocated to the file it points at.
32
+
33
+ ## Reason
34
+
35
+ `fill-spec.ts` is not free-standing — every decision it declares exists to fill a slot
36
+ `schema.ts` declared, and every fact it reads must be one the platform actually exposes.
37
+ A decision that produces a slot no schema declares, or reads a fact that doesn't exist,
38
+ is not a style problem: the fill pipeline would either drop the output on the floor or
39
+ crash trying to read a fact key that isn't there. Catching the mismatch here, at author
40
+ time, is the difference between a red check and a listing that silently fails to fill.
41
+
42
+ ## Fix
43
+
44
+ Read the message: it names the section, the decision or slot in question, and what's
45
+ wrong with it. Fix whichever side is actually stale — the schema, or the fill-spec.
46
+
47
+ ### Before
48
+
49
+ ```ts
50
+ // sections/facts/schema.ts
51
+ export const schema = {
52
+ meta: { displayName: "Facts" },
53
+ slots: {
54
+ year_built: { type: "text", required: false, produced_by: "year_built_text" },
55
+ },
56
+ options: {},
57
+ } as const satisfies SectionSchemaShape;
58
+ ```
59
+
60
+ ```ts
61
+ // sections/facts/fill-spec.ts
62
+ export const fillSpec = {
63
+ reads: [],
64
+ decisions: [
65
+ { id: "year_built_text", type: "text-block", produces: ["year_built_summary"], length_cap: 20 },
66
+ // ^ typo — schema.ts declares "year_built"
67
+ ],
68
+ } as const satisfies FillSpecShape;
69
+ ```
70
+
71
+ ```text
72
+ Section "facts": fill-spec decision "year_built_text" produces unknown slot "year_built_summary".
73
+ ```
74
+
75
+ ### After
76
+
77
+ ```ts
78
+ // sections/facts/fill-spec.ts
79
+ export const fillSpec = {
80
+ reads: [],
81
+ decisions: [
82
+ { id: "year_built_text", type: "text-block", produces: ["year_built"], length_cap: 20 },
83
+ ],
84
+ } as const satisfies FillSpecShape;
85
+ ```
86
+
87
+ ## See also
88
+
89
+ - [The schema system](../schema-system.md) — the full authoring contract for `schema.ts`
90
+ and `fill-spec.ts`.
91
+ - [`fixtures-invalid`](fixtures-invalid.md) — the sibling gate on the third contract
92
+ file, `fixtures.ts`.
93
+ - [`manifest-invalid`](manifest-invalid.md) — the same kind of cross-reference check, one
94
+ level up, on a template composing several sections.
95
+ - [`bundle-incomplete`](bundle-incomplete.md) — reported instead of this id when a
96
+ contract file is missing outright, rather than present and wrong.
@@ -46,8 +46,8 @@ Three moves, in order of how often they work:
46
46
  stylesheet plus opt-in modules, *and* a fat "everything" bundle that is the one you get
47
47
  from copying the quickstart. Take the core, plus only the modules you use.
48
48
  2. **Cut the webfont.** If the package's stylesheet drags in a font for its icons, and you
49
- are using three of those icons, replace them with inline SVG — or with a family the page's
50
- theme already declares (`--tr-font-sans`, `--tr-font-serif`), which costs nothing
49
+ are using three of those icons, replace them with inline SVG — or with a family the
50
+ template's theme already declares in `fonts` (`--tr-font-<name>`), which costs nothing
51
51
  because it is already loaded.
52
52
  3. **Drop the package and hand-write the residue.** If you use one component from a large
53
53
  widget library, its stylesheet is paying for components you never render.