@homepages/template-kit 0.0.1 → 0.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 (121) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +77 -11
  3. package/dist/base.css +64 -0
  4. package/dist/browser.d.ts +26 -0
  5. package/dist/browser.js +55 -0
  6. package/dist/cli.js +1 -1
  7. package/dist/contracts/image-descriptor.d.ts +17 -0
  8. package/dist/contracts/markers.d.ts +20 -0
  9. package/dist/contracts/markers.js +21 -0
  10. package/dist/contracts/slot-catalog.d.ts +237 -0
  11. package/dist/contracts/slot-catalog.js +247 -0
  12. package/dist/contracts/transforms.d.ts +108 -0
  13. package/dist/contracts/transforms.js +117 -0
  14. package/dist/design-system/breakpoints.d.ts +5 -0
  15. package/dist/design-system/breakpoints.js +14 -0
  16. package/dist/design-system/theme.d.ts +308 -0
  17. package/dist/design-system/theme.js +102 -0
  18. package/dist/eslint/is-client-file.js +79 -0
  19. package/dist/eslint/rules/image-bare-needs-reason.js +43 -0
  20. package/dist/eslint/rules/no-client-directive-in-contract.js +31 -0
  21. package/dist/eslint/rules/no-client-runtime-in-server.js +103 -0
  22. package/dist/eslint/rules/no-css-import-from-render-path.js +29 -0
  23. package/dist/eslint/rules/no-hex.js +116 -0
  24. package/dist/eslint/rules/no-inline-style.js +27 -0
  25. package/dist/eslint/rules/no-nondeterminism.js +48 -0
  26. package/dist/eslint/rules/no-raw-element.js +37 -0
  27. package/dist/eslint/rules/props-from-schema.js +37 -0
  28. package/dist/eslint/rules/serializable-island-props.js +112 -0
  29. package/dist/eslint/rules/slot-marker-literal.js +38 -0
  30. package/dist/eslint.d.ts +4 -8
  31. package/dist/eslint.js +75 -11
  32. package/dist/fixtures/sample-property.d.ts +67 -0
  33. package/dist/fixtures/sample-property.js +707 -0
  34. package/dist/fixtures.d.ts +2 -0
  35. package/dist/fixtures.js +3 -0
  36. package/dist/index.d.ts +28 -12
  37. package/dist/index.js +34 -2
  38. package/dist/island-runtime.d.ts +30 -0
  39. package/dist/island-runtime.js +73 -0
  40. package/dist/islands/contract.d.ts +56 -0
  41. package/dist/islands/contract.js +71 -0
  42. package/dist/islands/marker.d.ts +16 -0
  43. package/dist/islands/marker.js +18 -0
  44. package/dist/package.js +5 -0
  45. package/dist/primitives/Image.d.ts +49 -0
  46. package/dist/primitives/Image.js +95 -0
  47. package/dist/primitives/Section.d.ts +22 -0
  48. package/dist/primitives/Section.js +26 -0
  49. package/dist/primitives/Slot.d.ts +50 -0
  50. package/dist/primitives/Slot.js +39 -0
  51. package/dist/primitives/SlotGroup.d.ts +38 -0
  52. package/dist/primitives/SlotGroup.js +24 -0
  53. package/dist/primitives/SlotItem.d.ts +43 -0
  54. package/dist/primitives/SlotItem.js +38 -0
  55. package/dist/primitives/index.d.ts +7 -0
  56. package/dist/primitives/picture-sources.d.ts +22 -0
  57. package/dist/primitives/picture-sources.js +52 -0
  58. package/dist/rules/registry.js +16 -0
  59. package/dist/schema/fill-spec.d.ts +692 -0
  60. package/dist/schema/fill-spec.js +98 -0
  61. package/dist/schema/fixture-schema.d.ts +143 -0
  62. package/dist/schema/fixture-schema.js +113 -0
  63. package/dist/schema/index.d.ts +16 -0
  64. package/dist/schema/manifest.d.ts +527 -0
  65. package/dist/schema/manifest.js +73 -0
  66. package/dist/schema/property-facts.d.ts +44 -0
  67. package/dist/schema/resolve-variants.d.ts +11 -0
  68. package/dist/schema/resolve-variants.js +15 -0
  69. package/dist/schema/runtime-values.d.ts +45 -0
  70. package/dist/schema/section-nav.d.ts +7 -0
  71. package/dist/schema/section-props.d.ts +69 -0
  72. package/dist/schema/section-schema.d.ts +533 -0
  73. package/dist/schema/section-schema.js +72 -0
  74. package/dist/schema/slot-types.d.ts +117 -0
  75. package/dist/schema/slot-types.js +181 -0
  76. package/dist/schema/source.d.ts +13 -0
  77. package/dist/schema/source.js +12 -0
  78. package/dist/schema/synthesize-nullable.d.ts +18 -0
  79. package/dist/schema/synthesize-nullable.js +47 -0
  80. package/dist/styles.css +131 -9
  81. package/docs/INDEX.md +7 -4
  82. package/docs/eslint.md +55 -0
  83. package/docs/islands.md +150 -0
  84. package/docs/llms.txt +33 -3
  85. package/docs/primitives.md +214 -0
  86. package/docs/rules/INDEX.md +58 -0
  87. package/docs/rules/audit-severity.md +91 -0
  88. package/docs/rules/bundle-incomplete.md +117 -0
  89. package/docs/rules/css-reason.md +94 -0
  90. package/docs/rules/determinism-drift.md +112 -0
  91. package/docs/rules/image-bare-needs-reason.md +117 -0
  92. package/docs/rules/license-denied.md +106 -0
  93. package/docs/rules/lockfile-missing.md +68 -0
  94. package/docs/rules/lockfile-stale.md +88 -0
  95. package/docs/rules/missing-slot-marker.md +140 -0
  96. package/docs/rules/no-bare-css-import.md +128 -0
  97. package/docs/rules/no-client-directive-in-contract.md +111 -0
  98. package/docs/rules/no-client-runtime-in-server.md +143 -0
  99. package/docs/rules/no-css-import-from-render-path.md +118 -0
  100. package/docs/rules/no-hex.md +97 -0
  101. package/docs/rules/no-inline-style.md +121 -0
  102. package/docs/rules/no-nondeterminism.md +100 -0
  103. package/docs/rules/no-raw-element.md +100 -0
  104. package/docs/rules/props-from-schema.md +119 -0
  105. package/docs/rules/render-invariant.md +165 -0
  106. package/docs/rules/serializable-island-props.md +146 -0
  107. package/docs/rules/server-vs-client.md +105 -0
  108. package/docs/rules/sidebar-order.md +109 -0
  109. package/docs/rules/single-react.md +97 -0
  110. package/docs/rules/size-renderer-bundle.md +109 -0
  111. package/docs/rules/size-section-css.md +95 -0
  112. package/docs/rules/slot-group-marker.md +127 -0
  113. package/docs/rules/slot-marker-literal.md +148 -0
  114. package/docs/rules/typecheck.md +103 -0
  115. package/docs/rules/unlayered-fence.md +113 -0
  116. package/docs/schema-system.md +240 -0
  117. package/docs/theme-and-css.md +225 -0
  118. package/package.json +30 -6
  119. package/tsconfig.base.json +16 -0
  120. package/tsconfig.json +2 -13
  121. package/dist/src-dZr5N30y.js +0 -35
@@ -0,0 +1,117 @@
1
+ ---
2
+ purpose: The template-kit/bundle-incomplete authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../schema-system.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/bundle-incomplete
8
+
9
+ > A section folder is **yours**. Its only structural requirement is that four contract
10
+ > files exist at its root.
11
+
12
+ ## Rule
13
+
14
+ A section folder must contain these four, **as files, at the folder root**:
15
+
16
+ | File | Declares |
17
+ |---|---|
18
+ | `schema.ts` | the slots and options the section has |
19
+ | `fill-spec.ts` | how AI fills those slots |
20
+ | `fixtures.ts` | sample content for local preview |
21
+ | `Renderer.tsx` | the markup |
22
+
23
+ Each missing file is one violation, so a folder holding only a `Renderer.tsx` reports
24
+ three times.
25
+
26
+ Two near-misses do **not** satisfy it:
27
+
28
+ - a **directory** named `schema.ts/` — the check wants a file;
29
+ - a file at `components/schema.ts` — the four are looked for at the folder root, not
30
+ searched for down the tree.
31
+
32
+ Everything else in the folder is unconstrained. `components/`, `hooks/`, `utils/`, any
33
+ nesting, any number of support modules, islands, stylesheets, enhancers: the check has no
34
+ opinion about them, and neither does the rest of the kit.
35
+
36
+ ## Reason
37
+
38
+ The four files are the platform's entire interface to a section. The publisher reads
39
+ `schema.ts` to know what the section contains, the fill pipeline reads `fill-spec.ts` to
40
+ fill it, preview reads `fixtures.ts`, and `Renderer.tsx` is the markup. A folder missing
41
+ one of them cannot be loaded as a section.
42
+
43
+ That freedom has one non-obvious consequence, worth knowing before it surprises you:
44
+ **every file in the folder is part of the section's identity.** The content hash covers
45
+ the whole folder, not just the four — so editing a helper, a util, or even a comment
46
+ produces a new section version on the next publish, exactly as editing `Renderer.tsx`
47
+ would.
48
+
49
+ ## Fix
50
+
51
+ Create the missing file at the folder root. A section that fills nothing still needs a
52
+ `fill-spec.ts`; a section with no designed states still needs a `fixtures.ts`. Declaring
53
+ "nothing to fill" is a declaration.
54
+
55
+ ### Before
56
+
57
+ ```text
58
+ sections/hero/
59
+ Renderer.tsx
60
+ schema.ts
61
+ components/
62
+ Headline.tsx
63
+ ```
64
+
65
+ Two violations: `fill-spec.ts` and `fixtures.ts` are missing. `components/` is not one of
66
+ them — it was never a problem.
67
+
68
+ ### After
69
+
70
+ ```text
71
+ sections/hero/
72
+ Renderer.tsx
73
+ schema.ts
74
+ fill-spec.ts ← added
75
+ fixtures.ts ← added
76
+ components/
77
+ Headline.tsx
78
+ ```
79
+
80
+ ```ts
81
+ // sections/hero/fill-spec.ts
82
+ import type { FillSpecShape } from "@homepages/template-kit";
83
+
84
+ export const fillSpec = {
85
+ reads: [],
86
+ decisions: [
87
+ {
88
+ id: "headline_text",
89
+ type: "text-block",
90
+ produces: ["headline"],
91
+ length_cap: 60,
92
+ structure: "single_sentence",
93
+ voice_prompt: "Confident premium-listing voice. Title Case.",
94
+ },
95
+ ],
96
+ } as const satisfies FillSpecShape;
97
+ ```
98
+
99
+ ```ts
100
+ // sections/hero/fixtures.ts
101
+ import type { FixtureModule } from "@homepages/template-kit";
102
+
103
+ const fixtures: FixtureModule = {
104
+ base: {
105
+ slots: { headline: "A mansard Victorian in the heart of Jamaica Plain" },
106
+ },
107
+ };
108
+
109
+ export default fixtures;
110
+ ```
111
+
112
+ ## See also
113
+
114
+ - [The schema system](../schema-system.md) — what each of the four files declares, and
115
+ the types they satisfy.
116
+ - [`missing-slot-marker`](missing-slot-marker.md) — the check that runs once the four
117
+ exist: every editable slot in `schema.ts` has a marker in the JSX.
@@ -0,0 +1,94 @@
1
+ ---
2
+ purpose: The template-kit/css-reason authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../theme-and-css.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/css-reason
8
+
9
+ > Hand-written CSS is the escape hatch from Tailwind, so every stylesheet must **open**
10
+ > with a comment saying what it escapes for.
11
+
12
+ ## Rule
13
+
14
+ Every hand-written `.css` file — a section's `styles.css`, any other `.css` in a section
15
+ folder, and the template's own `styles.css` — must **begin** with a block comment
16
+ containing the token `css-reason:`.
17
+
18
+ Three ways to get this wrong:
19
+
20
+ - **The comment is not first.** The token must be in the file's *leading* comment.
21
+ Mentioning `css-reason:` further down, after a rule, does not pass.
22
+ - **The comment is a `//` line.** `//` does not open a comment in CSS. It is not a comment
23
+ at all — it is a parse error waiting to happen, and it does not satisfy this check.
24
+ - **The file is in a subdirectory and you assumed it was exempt.** CSS is collected **by
25
+ path, not by import**: the build walks the whole section folder. A stylesheet at
26
+ `sections/map/styles/markers.css` is gated — and published — exactly like `styles.css`.
27
+
28
+ The token is all that is checked, but the sentence after it is the point. Write it for the
29
+ next author.
30
+
31
+ ## Reason
32
+
33
+ Tailwind is the default, and it is the default for a reason: a utility class is scoped to
34
+ the element it sits on, participates in the cascade layers the platform already orders,
35
+ and cannot leak into another section on the same page. Hand-written CSS gives all of that
36
+ up.
37
+
38
+ There is exactly one thing utilities cannot do: style **DOM that Tailwind's scanner never
39
+ sees**. Markup an enhancer builds at runtime has no class in your source for the scanner
40
+ to find, and a third-party widget's own DOM was never yours to put classes on. Those are
41
+ the cases hand-CSS exists for.
42
+
43
+ We cannot cheaply prove that a given rule is *inexpressible* as a utility — so the check
44
+ enforces the half of the bar that is checkable: **you have to say which un-scannable DOM
45
+ you are styling.** A stylesheet that cannot name one is almost always a stylesheet whose
46
+ rules should have been utility classes, and a reasonless file leaves the next author
47
+ unable to tell the difference.
48
+
49
+ ## Fix
50
+
51
+ Make the first thing in the file a block comment that names the DOM Tailwind cannot see.
52
+ If you find you cannot name one, that is the answer: delete the CSS and use utility
53
+ classes on the markup.
54
+
55
+ ### Before
56
+
57
+ ```css
58
+ /* sections/neighborhood-map/styles.css */
59
+ .tr-map-marker {
60
+ display: grid;
61
+ place-items: center;
62
+ border-radius: var(--tr-radius-full);
63
+ background: var(--tr-color-primary);
64
+ }
65
+ ```
66
+
67
+ ### After
68
+
69
+ ```css
70
+ /* css-reason: styles the marker DOM the neighborhood-map enhancer builds at runtime —
71
+ the nodes are created by the map library after hydration, so Tailwind's scanner never
72
+ sees a class to compile. */
73
+ .tr-map-marker {
74
+ display: grid;
75
+ place-items: center;
76
+ border-radius: var(--tr-radius-full);
77
+ background: var(--tr-color-primary);
78
+ }
79
+ ```
80
+
81
+ And the case where the fix is not a comment at all — this rule *is* expressible as
82
+ utilities, so the stylesheet goes away and the classes move onto the element:
83
+
84
+ ```tsx
85
+ <div className="grid place-items-center rounded-full bg-primary" />
86
+ ```
87
+
88
+ ## See also
89
+
90
+ - [Theme and CSS](../theme-and-css.md) — the token namespace utilities compile from, and
91
+ the layer order hand-CSS lands in.
92
+ - [`no-bare-css-import`](no-bare-css-import.md) — the other constraint on a hand-written
93
+ stylesheet: no `@import`, ever.
94
+ - [`unlayered-fence`](unlayered-fence.md) — the escape hatch *within* the escape hatch.
@@ -0,0 +1,112 @@
1
+ ---
2
+ purpose: The template-kit/determinism-drift authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, server-vs-client.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/determinism-drift
8
+
9
+ > The same fixture, rendered twice, must produce **byte-identical** HTML. This is the
10
+ > runtime back-stop to `no-nondeterminism` — it catches what a linter cannot see.
11
+
12
+ ## Rule
13
+
14
+ Every fixture in the section's invariant corpus is server-rendered **twice**, and the two
15
+ outputs are compared byte for byte. Any difference is a violation.
16
+
17
+ The corpus is broader than the fixtures you wrote:
18
+
19
+ - your **`base`** fixture;
20
+ - each of your **`states`** fixtures;
21
+ - one **synthesized** fixture per **optional** slot, with that slot set to its empty value
22
+ (`null` for a scalar or an image, `[]` for a list).
23
+
24
+ You do not declare that last set. Nullability coverage is derived from the schema — from
25
+ `(type, required)` — so a nullable branch cannot be forgotten by forgetting to write a
26
+ fixture for it.
27
+
28
+ ## Reason
29
+
30
+ [`no-nondeterminism`](no-nondeterminism.md) bans the clock and randomness **in your code**.
31
+ A linter reads one file at a time and can only report on what it can see, so it stops at
32
+ your source. This check runs the render.
33
+
34
+ That gap is where the real bugs live, and they arrive **through a package**. A helper that
35
+ mints a fresh id per call. A library that iterates a hash and hands back a different order
36
+ each time. Nothing in your Renderer looks nondeterministic — you never wrote `Math.random()`
37
+ — but the output moves.
38
+
39
+ It matters because the publisher and the editor **both** render the section and must agree.
40
+ Byte-identical output is what makes a section's HTML content-addressable: the same content
41
+ must hash to the same page. A section whose HTML changes on every render re-publishes
42
+ forever and never converges.
43
+
44
+ ## Fix
45
+
46
+ If the nondeterminism is your own: compute the value at **fill time** and pass it in as a
47
+ slot value, or move the markup into a `"use client"` island — an island renders in the
48
+ browser after hydration, and its output is nobody's content hash.
49
+
50
+ If the nondeterminism came from a package: replace it, or give it a deterministic input.
51
+ An id generator gets a stable id derived from props. A collection gets sorted before you
52
+ map it.
53
+
54
+ ### Before
55
+
56
+ ```tsx
57
+ // sections/facts/Renderer.tsx
58
+ import { Section, Slot } from "@homepages/template-kit";
59
+ import { nanoid } from "nanoid";
60
+
61
+ import type { Props } from "./schema.js";
62
+
63
+ export function Renderer({ slots, nav }: Props) {
64
+ const descriptionId = nanoid(); // ← a new id on every render; the HTML differs each time
65
+
66
+ return (
67
+ <Section id={nav.selfAnchor}>
68
+ <Slot id="headline" as="h1" textLeaf>
69
+ <span aria-describedby={descriptionId}>{slots.headline}</span>
70
+ </Slot>
71
+ <p id={descriptionId}>{slots.summary}</p>
72
+ </Section>
73
+ );
74
+ }
75
+ ```
76
+
77
+ Nothing here reads the clock or a random source, so no lint rule fires. The section still
78
+ renders differently every single time.
79
+
80
+ ### After
81
+
82
+ The id is derived from something the section was **given**, so it is the same on every
83
+ render:
84
+
85
+ ```tsx
86
+ // sections/facts/Renderer.tsx
87
+ import { Section, Slot } from "@homepages/template-kit";
88
+
89
+ import type { Props } from "./schema.js";
90
+
91
+ export function Renderer({ slots, nav }: Props) {
92
+ const descriptionId = `${nav.selfAnchor}-summary`;
93
+
94
+ return (
95
+ <Section id={nav.selfAnchor}>
96
+ <Slot id="headline" as="h1" textLeaf>
97
+ <span aria-describedby={descriptionId}>{slots.headline}</span>
98
+ </Slot>
99
+ <p id={descriptionId}>{slots.summary}</p>
100
+ </Section>
101
+ );
102
+ }
103
+ ```
104
+
105
+ ## See also
106
+
107
+ - [`no-nondeterminism`](no-nondeterminism.md) — the same ban, enforced in your editor, on the
108
+ code a linter can see.
109
+ - [Server vs client](server-vs-client.md) — why a server-rendered file must be a pure
110
+ function of its props, and why an island is not.
111
+ - [`render-invariant`](render-invariant.md) — the other check that runs over this same
112
+ fixture corpus.
@@ -0,0 +1,117 @@
1
+ ---
2
+ purpose: The template-kit/image-bare-needs-reason authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, ../primitives.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/image-bare-needs-reason
8
+
9
+ > `<Image bare>` needs a `// bare: <reason>` comment above it. **A reason cannot be spent
10
+ > twice** — one comment documents exactly one element.
11
+
12
+ ## Rule
13
+
14
+ Every `<Image bare …>` must have a qualifying reason comment. A comment qualifies when
15
+ all three hold:
16
+
17
+ 1. **It contains `bare:`** — e.g. `// bare: the logo is chrome; the parent already sizes it.`
18
+ 2. **It sits strictly above the element, within 5 lines.** Above, not beside and not below.
19
+ 3. **It has not already been spent** on an earlier `<Image bare>`.
20
+
21
+ Both comment forms count — a `//` line comment and a `{/* … */}` JSX comment. The rule
22
+ reads the raw comment stream rather than hopping up the AST, so proximity survives
23
+ arbitrary wrapping: a container `<div>`, a `.map()` callback, a parenthesized `return`, an
24
+ assignment to a `const`. If it *looks* directly above the element, it counts.
25
+
26
+ **The surprising part: a reason cannot be spent twice.** One comment above a row of
27
+ sibling bare logos documents only the **first** of them; the second still reports. Each
28
+ bare image is its own decision, so each gets its own sentence. (A comment written *between*
29
+ two bare images is fine for the second — it starts after the first element ends.)
30
+
31
+ ## Reason
32
+
33
+ Framed is `<Image>`'s default, and the frame is load-bearing: a wrapper `div` that holds
34
+ the aspect-ratio box, carries the slot marker, and renders a neutral fallback rect when
35
+ `src` is empty.
36
+
37
+ `bare` drops all of it — one `<img>`, no wrapper. So a bare image with no source is
38
+ **nothing at all**: the layout collapses, and whatever sat below it jumps up the page. A
39
+ missing image is a supported state on this platform (a thin listing simply has no photo),
40
+ so "the source is always there" is an assumption, not a fact.
41
+
42
+ `bare` is still the right call sometimes — a logo whose parent already establishes the
43
+ box, a slide inside its own aspect-ratio parent. The rule does not ban it; it makes you
44
+ **say why the parent guarantees the box**, so the next author (or the next agent) can
45
+ tell whether the frame is safe to restore.
46
+
47
+ ## Fix
48
+
49
+ Put a `// bare: <reason>` comment immediately above the element. A reason is legitimate in
50
+ either of two ways: it can name what **guarantees the layout box** without `<Image>`'s
51
+ frame (a parent that already establishes the size, a slide inside its own aspect-ratio
52
+ container) — or it can name something about the **image's own content** that makes the
53
+ frame the wrong choice, such as a transparent PNG that framing would letterbox. If
54
+ neither holds, drop `bare` and let `<Image>` frame it.
55
+
56
+ ### Before
57
+
58
+ ```tsx
59
+ import { Image, Section } from "@homepages/template-kit";
60
+
61
+ import type { Props } from "./schema.js";
62
+
63
+ export function Renderer({ slots }: Props) {
64
+ return (
65
+ <Section>
66
+ <div className="h-12 w-32">
67
+ <Image bare src={slots.brand_logo.url} alt={slots.brand_logo.alt} className="object-contain" />
68
+ </div>
69
+ </Section>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### After
75
+
76
+ ```tsx
77
+ import { Image, Section } from "@homepages/template-kit";
78
+
79
+ import type { Props } from "./schema.js";
80
+
81
+ export function Renderer({ slots }: Props) {
82
+ return (
83
+ <Section>
84
+ <div className="h-12 w-32">
85
+ {/* bare: the logo is chrome; the parent already sizes it. */}
86
+ <Image bare src={slots.brand_logo.url} alt={slots.brand_logo.alt} className="object-contain" />
87
+ </div>
88
+ </Section>
89
+ );
90
+ }
91
+ ```
92
+
93
+ Two bare images need two comments — the first one's reason is spent:
94
+
95
+ ```tsx
96
+ <div className="flex gap-4">
97
+ {/* bare: the primary logo is chrome; the row sizes it. */}
98
+ <Image bare src={slots.logo_a.url} alt={slots.logo_a.alt} className="h-8 object-contain" />
99
+ {/* bare: the partner logo is chrome; the row sizes it. */}
100
+ <Image bare src={slots.logo_b.url} alt={slots.logo_b.alt} className="h-8 object-contain" />
101
+ </div>
102
+ ```
103
+
104
+ A content-based reason is written the same way — only the sentence changes, because it's
105
+ justifying a different thing (the image, not the layout around it):
106
+
107
+ ```tsx
108
+ {/* bare: a transparent PNG badge; Image's frame would letterbox its transparent
109
+ padding against the section background. */}
110
+ <Image bare src={slots.certification_badge.url} alt={slots.certification_badge.alt} className="h-10" />
111
+ ```
112
+
113
+ ## See also
114
+
115
+ - [Contract primitives: `Image`](../primitives.md#image--the-defensive-image-primitive) —
116
+ framed vs bare, and what the frame does for you.
117
+ - [`no-raw-element`](no-raw-element.md) — why `<img>` goes through `<Image>` at all.
@@ -0,0 +1,106 @@
1
+ ---
2
+ purpose: The template-kit/license-denied authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, audit-severity.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/license-denied
8
+
9
+ > Every package in your **production** tree must carry a license from a fixed allowlist.
10
+ > There is no per-package override.
11
+
12
+ ## Rule
13
+
14
+ Each package in the production dependency tree must declare a license on this list:
15
+
16
+ | Allowed |
17
+ |---|
18
+ | `MIT` |
19
+ | `ISC` |
20
+ | `Apache-2.0` |
21
+ | `BSD-2-Clause` |
22
+ | `BSD-3-Clause` |
23
+ | `0BSD` |
24
+ | `Unlicense` |
25
+ | `CC0-1.0` |
26
+
27
+ Two cases people expect to slide through, and neither does:
28
+
29
+ - **`SEE LICENSE IN <file>` is a denial**, not a pass. The check cannot read the file, so
30
+ it cannot clear the terms.
31
+ - **A missing `license` field is a denial**, not a pass. An unlicensed package is not a
32
+ permissive one — by default, no rights are granted at all.
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.
36
+
37
+ **There is no per-package override.** No allowlist entry you can add, no waiver comment. By
38
+ design.
39
+
40
+ ## Reason
41
+
42
+ Your template's production dependencies are compiled into a page we host, and serve, on
43
+ behalf of a paying customer. Their license terms become terms we are shipping under. A
44
+ license we cannot clear in advance is a license we cannot ship — so the check is a
45
+ gate rather than a warning, and the list is the set we have already cleared.
46
+
47
+ ## Fix
48
+
49
+ **Replace the package** with an equivalent whose license is on the list. That is the only
50
+ fix, and for the overwhelming majority of what a section needs there is an MIT or Apache-2.0
51
+ alternative.
52
+
53
+ One legitimate adjacent fix: if the package is genuinely **build-time only** — it never
54
+ reaches the published bundle — then it belongs in `devDependencies` anyway, and
55
+ `devDependencies` are not evaluated. Be honest about this. If your published page imports
56
+ it, it is a production dependency no matter which stanza it is listed under, and moving it
57
+ is a bug in your `package.json` rather than a fix.
58
+
59
+ ### Before
60
+
61
+ ```jsonc
62
+ // package.json
63
+ {
64
+ "dependencies": {
65
+ "react": "^19.0.0",
66
+ "react-dom": "^19.0.0",
67
+ "fancy-charts": "^3.2.0" // "license": "GPL-3.0-only" → denied
68
+ },
69
+ "devDependencies": {
70
+ "typescript": "^5.7.2"
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### After
76
+
77
+ ```jsonc
78
+ // package.json
79
+ {
80
+ "dependencies": {
81
+ "react": "^19.0.0",
82
+ "react-dom": "^19.0.0",
83
+ "lightweight-charts": "^4.2.0" // "license": "Apache-2.0" → allowed
84
+ },
85
+ "devDependencies": {
86
+ "typescript": "^5.7.2"
87
+ }
88
+ }
89
+ ```
90
+
91
+ ```bash
92
+ npm uninstall fancy-charts
93
+ npm install lightweight-charts
94
+ ```
95
+
96
+ Confirm what a package declares before you commit to it:
97
+
98
+ ```bash
99
+ npm view <package> license
100
+ ```
101
+
102
+ ## See also
103
+
104
+ - [`audit-severity`](audit-severity.md) — the other gate on the packages you ship.
105
+ - [`size-renderer-bundle`](size-renderer-bundle.md) — the third: what those packages cost the
106
+ page in bytes.
@@ -0,0 +1,68 @@
1
+ ---
2
+ purpose: The template-kit/lockfile-missing authoring rule — what it enforces, why, and how to fix it.
3
+ status: living
4
+ related: [INDEX.md, lockfile-stale.md]
5
+ updated: 2026-07-14
6
+ ---
7
+ # template-kit/lockfile-missing
8
+
9
+ > Your workspace must have a committed `package-lock.json`. Without one, your submission
10
+ > cannot be installed at all.
11
+
12
+ ## Rule
13
+
14
+ There must be a `package-lock.json` at the **workspace root**.
15
+
16
+ This is checked once per run, not once per template. A workspace has one `package.json` and
17
+ one lockfile, however many templates and sections live inside it — so this violation is
18
+ reported once and fixed once.
19
+
20
+ A `yarn.lock` or a `pnpm-lock.yaml` does not satisfy it. The install is `npm ci`, and
21
+ `npm ci` reads `package-lock.json`.
22
+
23
+ ## Reason
24
+
25
+ The platform installs your submission with **`npm ci`** — the reproducible install, which
26
+ takes the tree the lockfile pins and does not resolve anything itself. It requires a
27
+ lockfile, and refuses to run without one.
28
+
29
+ That is the whole point of the requirement: the tree that is built, bundled, audited, and
30
+ published is byte-for-byte the tree you tested against. No lockfile, no reproducible tree,
31
+ no build — and "works on my machine" would otherwise be the only thing standing behind a
32
+ page a customer is paying for.
33
+
34
+ ## Fix
35
+
36
+ Run `npm install` and **commit the lockfile**. If it is being ignored, stop ignoring it —
37
+ that is the single most common cause of this violation, and the ignore rule is almost always
38
+ inherited from a template `.gitignore` that was written for an application, not a package.
39
+
40
+ ### Before
41
+
42
+ ```gitignore
43
+ # .gitignore
44
+ node_modules/
45
+ dist/
46
+ package-lock.json ← the lockfile never gets committed
47
+ ```
48
+
49
+ `npm ci` on the checked-out submission: `The 'npm ci' command can only install with an
50
+ existing package-lock.json`.
51
+
52
+ ### After
53
+
54
+ ```gitignore
55
+ # .gitignore
56
+ node_modules/
57
+ dist/
58
+ ```
59
+
60
+ ```bash
61
+ npm install # regenerates package-lock.json from package.json
62
+ git add package-lock.json
63
+ ```
64
+
65
+ ## See also
66
+
67
+ - [`lockfile-stale`](lockfile-stale.md) — the lockfile exists but no longer reproduces the
68
+ tree `package.json` asks for.