@homepages/template-kit 0.6.0-dev-20260717012648 → 0.7.0-dev-20260717151640
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.
- package/CHANGELOG.md +49 -1
- package/dist/contracts/slot-catalog.d.ts +1 -1
- package/dist/contracts/slot-catalog.js +2 -0
- package/dist/fixtures/sample-property.js +4 -0
- package/dist/package.js +1 -1
- package/dist/schema/fill-spec.d.ts +32 -0
- package/dist/schema/fill-spec.js +20 -0
- package/guide/recipes/collection-slot.md +3 -3
- package/guide/recipes/fill-spec-decision.md +8 -6
- package/guide/rules/determinism-drift.md +6 -5
- package/guide/rules/render-invariant.md +4 -3
- package/guide/schema-system.md +24 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
# @homepages/template-kit
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.7.0-dev-20260717151640
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b55052d: The `luxuryMultiUnit` primary contact now carries a `facebook` social.
|
|
8
|
+
|
|
9
|
+
`"facebook"` was in the socials type union but on no fixture contact, so a slot resolving
|
|
10
|
+
`socials.find((s) => s.platform === "facebook")?.url ?? null` returned `null` in every
|
|
11
|
+
fixture of every scenario. Its populated branch rendered nowhere, and nullability
|
|
12
|
+
synthesis could not fill the gap from the other side — it nulls slots the base fixture
|
|
13
|
+
populates, never the reverse, and skips a slot already empty in base. A section could
|
|
14
|
+
break its Facebook link and every fixture would still look correct.
|
|
15
|
+
|
|
16
|
+
Okane Marketing's James Donovan (`luxuryMultiUnit`'s primary contact) now carries
|
|
17
|
+
`facebook` alongside `instagram`, `linkedin`, and `x`, so all four platforms behave
|
|
18
|
+
alike: populated in base, nulled by synthesis. Breadth belongs to the richest scenario's
|
|
19
|
+
primary contact by design — the other five contacts keep partial social sets, so sections
|
|
20
|
+
still render thin social rows.
|
|
21
|
+
|
|
22
|
+
Snapshots will show the diff wherever a fixture resolves a facebook slot off that contact
|
|
23
|
+
— it moves from `null` to `"https://facebook.com/okanemarketing/"`. Any assertion
|
|
24
|
+
expecting that slot to be null needs updating; a section that gates on the value now
|
|
25
|
+
renders its populated branch.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- e5b447f: Fill-specs can now declare `video-assign` and `video-collection` decisions.
|
|
30
|
+
|
|
31
|
+
The `video` and `video_collection` slot types have had nothing able to fill them: AI
|
|
32
|
+
fill could pool photos but not videos. These two decisions mirror `image-assign` and
|
|
33
|
+
`image-collection`, pooling from `property_facts.videos[]` instead of `photos[]` — a
|
|
34
|
+
video is picked by tags, caption, and duration, the same text-only contract as images
|
|
35
|
+
(no bytes ever reach the selection model).
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
{ id: "hero_video", type: "video-assign", tag_query: "exterior",
|
|
39
|
+
selection_prompt: "The most cinematic establishing shot.", produces: ["video"] }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both accept an optional `tag_query` (omit to pool across every video) and a
|
|
43
|
+
`selection_prompt`; `video-collection` adds `count: { min, max }` and produces one
|
|
44
|
+
ordered array slot. Two fields from the image side are deliberately absent:
|
|
45
|
+
`from_fact` (no fact names a video upload — videos only reach a slot by selection)
|
|
46
|
+
and `mode: "walkthrough"` (sequencing a room-by-room tour presumes a photo-sized
|
|
47
|
+
pool). Neither implements `source_filter`.
|
|
48
|
+
|
|
49
|
+
Additive: existing fill-specs are unaffected.
|
|
50
|
+
|
|
51
|
+
## 0.6.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
|
6
54
|
|
|
@@ -229,7 +229,7 @@ declare function collectionForSlot(slot: {
|
|
|
229
229
|
}): CollectionField | null;
|
|
230
230
|
declare const DERIVED_INPUT_FIELDS: readonly ["units", "address_line_parts", "contact_address_parts", "coordinates", "clock"];
|
|
231
231
|
type DerivedInputName = (typeof DERIVED_INPUT_FIELDS)[number];
|
|
232
|
-
declare const DECISION_TYPES: readonly ["image-assign", "image-collection", "text-block", "text-seed", "list-extract", "structured-list"];
|
|
232
|
+
declare const DECISION_TYPES: readonly ["image-assign", "image-collection", "video-assign", "video-collection", "text-block", "text-seed", "list-extract", "structured-list"];
|
|
233
233
|
type DecisionType = (typeof DECISION_TYPES)[number];
|
|
234
234
|
declare const FORMATTER_NAMES: readonly ["currency", "number"];
|
|
235
235
|
type FormatterName = (typeof FORMATTER_NAMES)[number];
|
package/dist/package.js
CHANGED
|
@@ -21,6 +21,22 @@ declare const Decision: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
21
21
|
}, z.core.$strict>>;
|
|
22
22
|
selection_prompt: z.ZodString;
|
|
23
23
|
produces: z.ZodArray<z.ZodString>;
|
|
24
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
type: z.ZodLiteral<"video-assign">;
|
|
27
|
+
tag_query: z.ZodOptional<z.ZodString>;
|
|
28
|
+
selection_prompt: z.ZodOptional<z.ZodString>;
|
|
29
|
+
produces: z.ZodArray<z.ZodString>;
|
|
30
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
type: z.ZodLiteral<"video-collection">;
|
|
33
|
+
tag_query: z.ZodOptional<z.ZodString>;
|
|
34
|
+
count: z.ZodOptional<z.ZodObject<{
|
|
35
|
+
min: z.ZodNumber;
|
|
36
|
+
max: z.ZodNumber;
|
|
37
|
+
}, z.core.$strict>>;
|
|
38
|
+
selection_prompt: z.ZodString;
|
|
39
|
+
produces: z.ZodArray<z.ZodString>;
|
|
24
40
|
}, z.core.$strict>, z.ZodObject<{
|
|
25
41
|
id: z.ZodString;
|
|
26
42
|
type: z.ZodLiteral<"text-block">;
|
|
@@ -110,6 +126,22 @@ declare const FillSpec: z.ZodObject<{
|
|
|
110
126
|
}, z.core.$strict>>;
|
|
111
127
|
selection_prompt: z.ZodString;
|
|
112
128
|
produces: z.ZodArray<z.ZodString>;
|
|
129
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
130
|
+
id: z.ZodString;
|
|
131
|
+
type: z.ZodLiteral<"video-assign">;
|
|
132
|
+
tag_query: z.ZodOptional<z.ZodString>;
|
|
133
|
+
selection_prompt: z.ZodOptional<z.ZodString>;
|
|
134
|
+
produces: z.ZodArray<z.ZodString>;
|
|
135
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
136
|
+
id: z.ZodString;
|
|
137
|
+
type: z.ZodLiteral<"video-collection">;
|
|
138
|
+
tag_query: z.ZodOptional<z.ZodString>;
|
|
139
|
+
count: z.ZodOptional<z.ZodObject<{
|
|
140
|
+
min: z.ZodNumber;
|
|
141
|
+
max: z.ZodNumber;
|
|
142
|
+
}, z.core.$strict>>;
|
|
143
|
+
selection_prompt: z.ZodString;
|
|
144
|
+
produces: z.ZodArray<z.ZodString>;
|
|
113
145
|
}, z.core.$strict>, z.ZodObject<{
|
|
114
146
|
id: z.ZodString;
|
|
115
147
|
type: z.ZodLiteral<"text-block">;
|
package/dist/schema/fill-spec.js
CHANGED
|
@@ -22,6 +22,24 @@ const ImageCollectionDecision = z.object({
|
|
|
22
22
|
selection_prompt: z.string(),
|
|
23
23
|
produces: z.array(z.string()).min(1)
|
|
24
24
|
}).strict();
|
|
25
|
+
const VideoAssignDecision = z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
type: z.literal("video-assign"),
|
|
28
|
+
tag_query: z.string().optional(),
|
|
29
|
+
selection_prompt: z.string().optional(),
|
|
30
|
+
produces: z.array(z.string()).min(1)
|
|
31
|
+
}).strict();
|
|
32
|
+
const VideoCollectionDecision = z.object({
|
|
33
|
+
id: z.string(),
|
|
34
|
+
type: z.literal("video-collection"),
|
|
35
|
+
tag_query: z.string().optional(),
|
|
36
|
+
count: z.object({
|
|
37
|
+
min: z.number().int().nonnegative(),
|
|
38
|
+
max: z.number().int().positive()
|
|
39
|
+
}).strict().optional(),
|
|
40
|
+
selection_prompt: z.string(),
|
|
41
|
+
produces: z.array(z.string()).min(1)
|
|
42
|
+
}).strict();
|
|
25
43
|
const TextBlockDecision = z.object({
|
|
26
44
|
id: z.string(),
|
|
27
45
|
type: z.literal("text-block"),
|
|
@@ -84,6 +102,8 @@ const StructuredListDecision = z.object({
|
|
|
84
102
|
const Decision = z.discriminatedUnion("type", [
|
|
85
103
|
ImageAssignDecision,
|
|
86
104
|
ImageCollectionDecision,
|
|
105
|
+
VideoAssignDecision,
|
|
106
|
+
VideoCollectionDecision,
|
|
87
107
|
TextBlockDecision,
|
|
88
108
|
TextSeedDecision,
|
|
89
109
|
ListExtractDecision,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: Add a repeating list whose items the editor selects and edits one at a time.
|
|
3
3
|
status: living
|
|
4
4
|
related: [../primitives.md, ../vocabulary.md, ../schema-system.md, bind-property-fact.md, pick-a-scenario.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# A collection slot — a repeating, per-item list
|
|
8
8
|
|
|
@@ -96,7 +96,7 @@ Verify with [the author loop](../quickstart.md#the-author-loop).
|
|
|
96
96
|
(`object_list` / `image_collection` / `video_collection` / `poi_list` / `list`) and the
|
|
97
97
|
`direct()` List sources.
|
|
98
98
|
- [The schema system](../schema-system.md#fill-spects--how-ai-fills-the-slots) — the
|
|
99
|
-
`produced_by` decision types (`structured-list`, `list-extract`, `image-collection
|
|
100
|
-
for a collection AI fills instead of binding to a fact.
|
|
99
|
+
`produced_by` decision types (`structured-list`, `list-extract`, `image-collection`,
|
|
100
|
+
`video-collection`) for a collection AI fills instead of binding to a fact.
|
|
101
101
|
- [Bind a slot to a property fact](bind-property-fact.md) — the `direct()` pattern this
|
|
102
102
|
extends from a scalar to a whole array.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: Have AI write a slot's content.
|
|
3
3
|
status: living
|
|
4
4
|
related: [../schema-system.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# Have AI write a slot's content
|
|
8
8
|
|
|
@@ -44,12 +44,14 @@ summary: {
|
|
|
44
44
|
},
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`text-block` is one of several decision types — the others
|
|
48
|
-
`image-assign`, `
|
|
49
|
-
`structured-list`, each producing a differently-shaped slot
|
|
50
|
-
recipe covers the common text case; see [the schema
|
|
47
|
+
`text-block` is one of several decision types — the others are `image-assign`,
|
|
48
|
+
`image-collection`, `video-assign`, `video-collection`, `text-seed`,
|
|
49
|
+
`list-extract`, and `structured-list`, each producing a differently-shaped slot
|
|
50
|
+
value. This recipe covers the common text case; see [the schema
|
|
51
51
|
system](../schema-system.md#fill-spects--how-ai-fills-the-slots) for the
|
|
52
|
-
full decision-type contract before reaching for one of the others
|
|
52
|
+
full decision-type contract before reaching for one of the others, and
|
|
53
|
+
[Video slots](../schema-system.md#video-slots) for what the `video-*` pair
|
|
54
|
+
deliberately does not take.
|
|
53
55
|
|
|
54
56
|
## Commands
|
|
55
57
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
purpose: The template-kit/determinism-drift authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
|
-
related: [INDEX.md, server-vs-client.md]
|
|
5
|
-
updated: 2026-07-
|
|
4
|
+
related: [INDEX.md, server-vs-client.md, ../schema-system.md]
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# template-kit/determinism-drift
|
|
8
8
|
|
|
@@ -18,12 +18,13 @@ The corpus is broader than the fixtures you wrote:
|
|
|
18
18
|
|
|
19
19
|
- your **`base`** fixture;
|
|
20
20
|
- each of your **`states`** fixtures;
|
|
21
|
-
- one **synthesized** fixture per **optional** slot, with that slot
|
|
22
|
-
(`null` for a scalar or an image, `[]` for a list).
|
|
21
|
+
- one **synthesized** fixture per **optional** slot your `base` populates, with that slot
|
|
22
|
+
set to its empty value (`null` for a scalar or an image, `[]` for a list).
|
|
23
23
|
|
|
24
24
|
You do not declare that last set. Nullability coverage is derived from the schema — from
|
|
25
25
|
`(type, required)` — so a nullable branch cannot be forgotten by forgetting to write a
|
|
26
|
-
fixture for it.
|
|
26
|
+
fixture for it. It only runs one way, though: a slot your `base` leaves empty is skipped,
|
|
27
|
+
so [`base` should fill every slot it can](../schema-system.md#fixturests--content-for-local-preview).
|
|
27
28
|
|
|
28
29
|
## Reason
|
|
29
30
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The template-kit/render-invariant authoring rule — what it enforces, why, and how to fix it.
|
|
3
3
|
status: living
|
|
4
4
|
related: [INDEX.md, ../primitives.md, ../schema-system.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# template-kit/render-invariant
|
|
8
8
|
|
|
@@ -21,8 +21,9 @@ output is checked for three defects:
|
|
|
21
21
|
3. **An `<img>` whose `src` is absent, empty, `"undefined"`, or `"null"`.**
|
|
22
22
|
|
|
23
23
|
The corpus is your `base` fixture, each of your `states` fixtures, and one **synthesized**
|
|
24
|
-
fixture per optional slot with that slot set to its empty value
|
|
25
|
-
image, `[]` for a list) — derived from the schema, not declared
|
|
24
|
+
fixture per optional slot **your `base` populates**, with that slot set to its empty value
|
|
25
|
+
(`null` for a scalar or an image, `[]` for a list) — derived from the schema, not declared
|
|
26
|
+
by you. A slot already empty in `base` is skipped, having no filled branch to null.
|
|
26
27
|
|
|
27
28
|
The check is deliberately biased toward **false negatives**: it would rather miss a bug than
|
|
28
29
|
invent one. Everything it reports is a real defect, so there is nothing here to argue with
|
package/guide/schema-system.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
purpose: The section authoring contract — schema.ts, fill-spec.ts, fixtures.ts, and the closed source/transform vocabulary.
|
|
3
3
|
status: living
|
|
4
4
|
related: [primitives.md, theme-and-css.md, vocabulary.md, recipes/pick-a-scenario.md]
|
|
5
|
-
updated: 2026-07-
|
|
5
|
+
updated: 2026-07-17
|
|
6
6
|
---
|
|
7
7
|
# The schema system
|
|
8
8
|
|
|
@@ -110,6 +110,21 @@ single progressive MP4 rendition — there is no `responsive` ladder and no `mob
|
|
|
110
110
|
to pass along, unlike an image. Read `url`, `poster`, `alt`, and the intrinsic
|
|
111
111
|
`width`/`height`, and hand them to `Video`.
|
|
112
112
|
|
|
113
|
+
AI fills a `video` slot with a `video-assign` decision and a `video_collection` with
|
|
114
|
+
`video-collection` — the video twins of `image-assign` / `image-collection`, pooling the
|
|
115
|
+
property's videos rather than its photos. They are separate decision types rather than a
|
|
116
|
+
filter on the image ones so a decision's candidate pool is legible from its `type` alone.
|
|
117
|
+
Both take an optional `tag_query` (omit it to pool across every video) and a
|
|
118
|
+
`selection_prompt`; selection reads only text — tags, caption, duration — never the video
|
|
119
|
+
itself. Two fields the image side has are absent by design:
|
|
120
|
+
|
|
121
|
+
- **No `from_fact`.** Nothing in the [closed vocabulary](vocabulary.md#direct-sources)
|
|
122
|
+
names a video upload the way `headshot` names an image one, so a video reaches a slot
|
|
123
|
+
only by selection.
|
|
124
|
+
- **No `mode`.** The photo side's `walkthrough` sequences every photo into a room-by-room
|
|
125
|
+
tour, which presumes a pool large enough to order; a property carries a handful of
|
|
126
|
+
videos.
|
|
127
|
+
|
|
113
128
|
### Grouping slots into one editor card
|
|
114
129
|
|
|
115
130
|
`meta.groups` collapses several slots into a single card in the editor sidebar. Each
|
|
@@ -201,8 +216,14 @@ export const fillSpec = {
|
|
|
201
216
|
|
|
202
217
|
## `fixtures.ts` — content for local preview
|
|
203
218
|
|
|
204
|
-
`base` is one fully-specified baseline: every required slot
|
|
205
|
-
|
|
219
|
+
`base` is one fully-specified baseline: every required slot with a real value —
|
|
220
|
+
and every optional slot the scenario can plausibly fill, too. Coverage runs one
|
|
221
|
+
way: the checks synthesize an empty fixture for each optional slot your `base`
|
|
222
|
+
populates, but nothing populates a slot your `base` leaves empty. A slot empty
|
|
223
|
+
in `base` has its filled branch rendered by no fixture at all, so a section can
|
|
224
|
+
break that branch with every check green.
|
|
225
|
+
|
|
226
|
+
`states` adds named states, each overriding part of `base` — use them for the
|
|
206
227
|
structural cases your section is designed to handle (a missing logo, a long
|
|
207
228
|
headline, an empty list).
|
|
208
229
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homepages/template-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-dev-20260717151640",
|
|
4
4
|
"description": "Authoring kit for HomePages marketing-section templates: schema system, contract primitives, theme tokens, and the template-kit CLI.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"check": "npm run typecheck && npm run lint && npm run build && npm run size:islands && npm run test && npm run lint:pkg && npm run verify:consumer && npm run check:scaffolder",
|
|
75
75
|
"check:scaffolder": "npm run typecheck -w create-homepages-workspace && npm run build -w create-homepages-workspace && npm run test -w create-homepages-workspace",
|
|
76
76
|
"changeset": "changeset",
|
|
77
|
+
"version": "changeset version && npm install --package-lock-only",
|
|
77
78
|
"release": "changeset publish"
|
|
78
79
|
},
|
|
79
80
|
"peerDependencies": {
|