@homepages/template-kit 0.6.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @homepages/template-kit
2
2
 
3
+ ## 0.7.0
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
+
3
51
  ## 0.6.0
4
52
 
5
53
  ### Minor Changes
@@ -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];
@@ -238,6 +238,8 @@ const DERIVED_INPUT_FIELDS = [
238
238
  const DECISION_TYPES = [
239
239
  "image-assign",
240
240
  "image-collection",
241
+ "video-assign",
242
+ "video-collection",
241
243
  "text-block",
242
244
  "text-seed",
243
245
  "list-extract",
@@ -665,6 +665,10 @@ const sampleContacts = [
665
665
  {
666
666
  platform: "x",
667
667
  url: "https://x.com/okanemarketing"
668
+ },
669
+ {
670
+ platform: "facebook",
671
+ url: "https://facebook.com/okanemarketing/"
668
672
  }
669
673
  ]
670
674
  }
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.6.0";
2
+ var version = "0.7.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -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">;
@@ -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-16
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-15
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 include
48
- `image-assign`, `image-collection`, `text-seed`, `list-extract`, and
49
- `structured-list`, each producing a differently-shaped slot value. This
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-14
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 set to its empty value
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-16
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 (`null` for a scalar or an
25
- image, `[]` for a list) — derived from the schema, not declared by you.
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
@@ -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-16
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, with a real value.
205
- `states` adds named states, each overriding part of it use them for the
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.6.0",
3
+ "version": "0.7.0",
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": {