@homepages/template-kit 0.5.2 → 0.6.0-dev-20260717012648

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,39 @@
1
1
  # @homepages/template-kit
2
2
 
3
+ ## 0.6.0-dev-20260717012648
4
+
5
+ ### Minor Changes
6
+
7
+ - 30458a9: Fixture facts now carry the canonical address shape.
8
+
9
+ `PropertyFacts["address"]` is an object (`display`, `street`, `street_no_type`, `city`,
10
+ `state`, `postal`, optional `unit`) instead of a string, and `address_lines` is removed.
11
+ Fill-specs seed from address sub-parts such as `${facts.address.street_no_type}` and
12
+ `${facts.address.city}`, which no scenario previously exposed — so fixtures had to
13
+ hand-derive them, and a fixture could not state what a correct fill-spec would produce.
14
+
15
+ Migration: a slot bound straight to `facts.address` must become `facts.address.display` —
16
+ otherwise the slot receives an object and React throws `Objects are not valid as a React
17
+ child`. Read any other sub-part off `facts.address` directly. For address LINES, use
18
+ `propertyMetrics(s).addressLine1` / `.addressLine2` — they derive from the sub-parts the
19
+ way the publishing pipeline does, so they need no change at the call site.
20
+
21
+ Two values move, both in the `for-rent-condo` scenario, which carries `unit: "Unit 4"`:
22
+ `facts.address.display` is `"845 Pearl Street, Unit 4, Denver, CO 80203"` (the string it
23
+ replaces read `"845 Pearl Street, Denver, CO 80203"`), and the derivation folds the unit
24
+ into line 1, so `propertyMetrics(forRentCondo).addressLine1` is `"845 Pearl Street, Unit
25
+ 4"` (was `"845 Pearl Street"`). Snapshots over that scenario will show both diffs.
26
+ `addressLine2` is unchanged, and in `luxury-multi-unit` and `sparse-single-family`
27
+ `display` holds exactly the text the string did.
28
+
29
+ Also: the fixture-vs-canonical facts check now treats `videos` as an iterable array
30
+ field, not a scalar — matching how the rest of the fill pipeline already reads it. If
31
+ you write a fill-spec against `videos`, expect array semantics.
32
+
33
+ ### Patch Changes
34
+
35
+ - b0688c5: `check` now reports when a local kit overlay is active, naming the overlaid version and the version your workspace actually pins. Being overlaid while you develop is correct, so the report never affects the result — but a passing run under an overlay does not reflect your declared kit version, and that is worth seeing before you merge.
36
+
3
37
  ## 0.5.2
4
38
 
5
39
  ## 0.5.1
@@ -9,6 +9,11 @@ function formatOne(d) {
9
9
  }
10
10
  function formatHuman(report) {
11
11
  const parts = [];
12
+ if (report.linkedKit !== void 0) {
13
+ const { kitVersion, linkedFrom, pin } = report.linkedKit;
14
+ parts.push(`⚠ linked kit ${kitVersion} from ${linkedFrom} — your pin is ${pin}.\n`);
15
+ parts.push(" This result does not reflect your pin. Run `template-kit unlink` before merging.\n");
16
+ }
12
17
  for (const [key, t] of Object.entries(report.templates)) {
13
18
  if (t.ok) {
14
19
  parts.push(`✓ ${key}\n`);
@@ -1,4 +1,5 @@
1
1
  import { KIT_VERSION } from "../../index.js";
2
+ import { readLinkMarker } from "../link/overlay.js";
2
3
  import { formatHuman, toJson } from "./diagnostics.js";
3
4
  import { loadEsbuild } from "./resolve-tool.js";
4
5
  import { resetLoadCache } from "./loader.js";
@@ -9,7 +10,7 @@ import { sizeStage } from "./stages/size.js";
9
10
  import { treeStage } from "./stages/tree.js";
10
11
  import { typecheckStage } from "./stages/typecheck.js";
11
12
  import { validateStage } from "./stages/validate/index.js";
12
- import { findWorkspaceRoot, selectTemplates, templateFromCwd } from "./workspace.js";
13
+ import { declaredKitPin, findWorkspaceRoot, selectTemplates, templateFromCwd } from "./workspace.js";
13
14
  import { parseArgs } from "node:util";
14
15
 
15
16
  //#region src/cli/check/index.ts
@@ -71,9 +72,15 @@ async function runCheck(argv, cwd) {
71
72
  ok: workspaceDiagnostics.length === 0,
72
73
  diagnostics: workspaceDiagnostics
73
74
  };
75
+ const marker = await readLinkMarker(root);
74
76
  const report = {
75
77
  ok: workspace.ok && Object.values(templates).every((t) => t.ok),
76
78
  kitVersion: KIT_VERSION,
79
+ ...marker === void 0 ? {} : { linkedKit: {
80
+ linkedFrom: marker.linkedFrom,
81
+ kitVersion: marker.kitVersion,
82
+ pin: await declaredKitPin(root)
83
+ } },
77
84
  templates,
78
85
  workspace
79
86
  };
@@ -1,8 +1,8 @@
1
1
  import { loadEsbuild } from "./resolve-tool.js";
2
2
  import { ASSET_LOADERS, ASSET_NAMES, ASSET_PUBLIC_PATH } from "./css.js";
3
3
  import { join, relative } from "node:path";
4
- import { createHash } from "node:crypto";
5
4
  import { mkdir, rm, stat } from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
6
6
  import { pathToFileURL } from "node:url";
7
7
 
8
8
  //#region src/cli/check/loader.ts
@@ -2,6 +2,7 @@
2
2
  const FACTS_ARRAY_FIELDS = /* @__PURE__ */ new Set([
3
3
  "units",
4
4
  "photos",
5
+ "videos",
5
6
  "floor_plans",
6
7
  "pois",
7
8
  "contacts"
@@ -1,5 +1,5 @@
1
1
  import { dirname, join, resolve } from "node:path";
2
- import { readdir, stat } from "node:fs/promises";
2
+ import { readFile, readdir, stat } from "node:fs/promises";
3
3
 
4
4
  //#region src/cli/check/workspace.ts
5
5
  const isDir = async (p) => {
@@ -26,6 +26,19 @@ async function findWorkspaceRoot(from) {
26
26
  dir = parent;
27
27
  }
28
28
  }
29
+ /**
30
+ * The kit version the workspace DECLARES in package.json — deliberately not what is
31
+ * installed. The link overlay never touches package.json, so declared-vs-installed is
32
+ * precisely the gap the check warning exists to surface.
33
+ */
34
+ async function declaredKitPin(root) {
35
+ try {
36
+ const raw = await readFile(join(root, "package.json"), "utf8");
37
+ return JSON.parse(raw).dependencies?.["@homepages/template-kit"] ?? "unknown";
38
+ } catch {
39
+ return "unknown";
40
+ }
41
+ }
29
42
  async function discoverTemplates(root) {
30
43
  const templatesDir = join(root, "templates");
31
44
  const entries = await readdir(templatesDir, { withFileTypes: true });
@@ -83,4 +96,4 @@ function templateFromCwd(root, cwd) {
83
96
  }
84
97
 
85
98
  //#endregion
86
- export { discoverTemplates, findWorkspaceRoot, selectTemplates, templateFromCwd };
99
+ export { declaredKitPin, discoverTemplates, findWorkspaceRoot, selectTemplates, templateFromCwd };
@@ -1,5 +1,5 @@
1
- import { findWorkspaceRoot } from "../check/workspace.js";
2
1
  import { KIT_PKG_SUBPATH, findKitRoot, overlayKit, readLinkMarker } from "./overlay.js";
2
+ import { findWorkspaceRoot } from "../check/workspace.js";
3
3
  import { join } from "node:path";
4
4
  import { parseArgs, promisify } from "node:util";
5
5
  import { readFile } from "node:fs/promises";
@@ -11,7 +11,7 @@ const heroImage = luxuryMultiUnit.photos[19]!; // exterior facade shot
11
11
  const fixtures: FixtureModule = {
12
12
  base: {
13
13
  slots: {
14
- headline: luxuryMultiUnit.facts.address,
14
+ headline: luxuryMultiUnit.facts.address.display,
15
15
  hero_image: {
16
16
  photo_id: heroImage.id,
17
17
  url: heroImage.url,
@@ -1,6 +1,6 @@
1
1
  import { join, relative, sep } from "node:path";
2
- import { createHash } from "node:crypto";
3
2
  import { readFile, readdir } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
4
 
5
5
  //#region src/cli/pack/collect.ts
6
6
  const toPosix = (p) => p.split(sep).join("/");
@@ -1,7 +1,7 @@
1
1
  import { loadEsbuild } from "../check/resolve-tool.js";
2
2
  import { join } from "node:path";
3
- import { createHash } from "node:crypto";
4
3
  import { mkdir, readFile, rm, stat } from "node:fs/promises";
4
+ import { createHash } from "node:crypto";
5
5
  import { pathToFileURL } from "node:url";
6
6
 
7
7
  //#region src/cli/theme/load-theme.ts
@@ -22,10 +22,23 @@ function poisByCategory(s) {
22
22
  for (const poi of s.pois) out[poi.category].push(poi);
23
23
  return out;
24
24
  }
25
+ function addressLineParts(a) {
26
+ const street = a.street.trim();
27
+ const unit = (a.unit ?? "").trim();
28
+ const city = a.city.trim();
29
+ const state = a.state.trim();
30
+ const postal = a.postal.trim();
31
+ const streetLine = unit && street ? `${street}, ${unit}` : street;
32
+ const cityStateZip = [city, [state, postal].filter(Boolean).join(" ")].filter(Boolean).join(", ");
33
+ return {
34
+ line1: streetLine.length > 0 ? streetLine : null,
35
+ line2: cityStateZip.length > 0 ? cityStateZip : null
36
+ };
37
+ }
25
38
  function propertyMetrics(s) {
26
39
  const units = s.facts.units;
27
- const lines = s.facts.address_lines ?? [s.facts.address, ""];
28
40
  const primary = [...units].sort((a, b) => a.position - b.position)[0] ?? null;
41
+ const lines = addressLineParts(s.facts.address);
29
42
  const fmt = (name) => {
30
43
  const v = TRANSFORMS[name].impl({ units });
31
44
  return v == null ? null : String(v);
@@ -41,8 +54,8 @@ function propertyMetrics(s) {
41
54
  bathsMax: fmt("units.bathsMax"),
42
55
  sqftMax: fmt("units.sqftMax"),
43
56
  priceStart: fmt("units.priceStart"),
44
- addressLine1: lines[0] ?? "",
45
- addressLine2: lines[1] ?? ""
57
+ addressLine1: lines.line1 ?? "",
58
+ addressLine2: lines.line2 ?? ""
46
59
  };
47
60
  }
48
61
 
@@ -792,8 +792,14 @@ const sampleAmenities = [
792
792
  }
793
793
  ];
794
794
  const sampleProperty = {
795
- address: "101 Queensway, Jamaica Plain, MA 02130",
796
- address_lines: ["101 Queensway", "Jamaica Plain, MA 02130"],
795
+ address: {
796
+ display: "101 Queensway, Jamaica Plain, MA 02130",
797
+ street: "101 Queensway",
798
+ street_no_type: "101 Queensway",
799
+ city: "Jamaica Plain",
800
+ state: "MA",
801
+ postal: "02130"
802
+ },
797
803
  coordinates: {
798
804
  lat: 42.31,
799
805
  lng: -71.11
@@ -314,8 +314,15 @@ const contacts = [{
314
314
  }
315
315
  }];
316
316
  const facts = {
317
- address: "845 Pearl Street, Denver, CO 80203",
318
- address_lines: ["845 Pearl Street", "Denver, CO 80203"],
317
+ address: {
318
+ display: "845 Pearl Street, Unit 4, Denver, CO 80203",
319
+ street: "845 Pearl Street",
320
+ street_no_type: "845 Pearl",
321
+ unit: "Unit 4",
322
+ city: "Denver",
323
+ state: "CO",
324
+ postal: "80203"
325
+ },
319
326
  coordinates: {
320
327
  lat: 39.7273,
321
328
  lng: -104.9808
@@ -183,8 +183,14 @@ const contacts = [{
183
183
  }
184
184
  }];
185
185
  const facts = {
186
- address: "212 Tuscan Road, Maplewood, NJ 07040",
187
- address_lines: ["212 Tuscan Road", "Maplewood, NJ 07040"],
186
+ address: {
187
+ display: "212 Tuscan Road, Maplewood, NJ 07040",
188
+ street: "212 Tuscan Road",
189
+ street_no_type: "212 Tuscan",
190
+ city: "Maplewood",
191
+ state: "NJ",
192
+ postal: "07040"
193
+ },
188
194
  coordinates: {
189
195
  lat: 40.7315,
190
196
  lng: -74.2739
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.5.2";
2
+ var version = "0.6.0-dev-20260717012648";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -26,9 +26,18 @@ type UnitRow = {
26
26
  position: number;
27
27
  floor_plans: FloorplanAssignment[];
28
28
  };
29
+ type Address = {
30
+ display: string;
31
+ street: string;
32
+ street_no_type: string;
33
+ city: string;
34
+ state: string;
35
+ postal: string;
36
+ /** Address Line 2 (apt/suite/unit, e.g. "Unit 4B"); absent when none. */
37
+ unit?: string;
38
+ };
29
39
  type PropertyFacts = {
30
- address: string;
31
- address_lines?: string[];
40
+ address: Address;
32
41
  lat?: number | null;
33
42
  lon?: number | null;
34
43
  coordinates?: Coordinates | null;
@@ -41,4 +50,4 @@ type PropertyFacts = {
41
50
  contacts?: Array<Record<string, unknown>>;
42
51
  };
43
52
  //#endregion
44
- export { Coordinates, FloorplanAssignment, PropertyFacts, UnitRow };
53
+ export { Address, Coordinates, FloorplanAssignment, PropertyFacts, UnitRow };
@@ -1,5 +1,5 @@
1
1
  import { ResponsiveImage } from "../contracts/image-descriptor.js";
2
- import { Coordinates, FloorplanAssignment, PropertyFacts, UnitRow } from "./property-facts.js";
2
+ import { Address, Coordinates, FloorplanAssignment, PropertyFacts, UnitRow } from "./property-facts.js";
3
3
  import { VerificationWidth } from "../design-system/breakpoints.js";
4
4
  //#region src/schema/runtime-values.d.ts
5
5
  type FrameConfig = {
@@ -67,7 +67,7 @@ const sparse = scenarios.sparseSingleFamily;
67
67
  const fixtures: FixtureModule = {
68
68
  base: {
69
69
  slots: {
70
- headline: luxury.facts.address,
70
+ headline: luxury.facts.address.display,
71
71
  summary: "A 12-unit mansard Victorian moments from the Emerald Necklace.",
72
72
  },
73
73
  options: { align: "center" },
@@ -75,7 +75,7 @@ const fixtures: FixtureModule = {
75
75
  states: {
76
76
  sparse: {
77
77
  slots: {
78
- headline: sparse.facts.address,
78
+ headline: sparse.facts.address.display,
79
79
  summary:
80
80
  "A classic center-hall colonial with an updated kitchen and original " +
81
81
  "hardwood floors throughout.",
@@ -46,7 +46,7 @@ production — `fixtures.ts` mirrors that:
46
46
 
47
47
  | Slot's fill-spec source | How the fixture fills it |
48
48
  |---|---|
49
- | direct (a fact) | read it off the scenario — `s.facts.address`, `primaryContact(s).fields.phone` |
49
+ | direct (a fact) | read it off the scenario — `s.facts.address.display`, `primaryContact(s).fields.phone` |
50
50
  | derived | a helper — `propertyMetrics(s)`, `poisByCategory(s)`, `contactCards(s)` |
51
51
  | ai | a short sample fill **grounded in the scenario's facts** — a headline about *this* property, never lorem ipsum or invented facts |
52
52
 
@@ -229,7 +229,7 @@ export default fixtures;
229
229
  ### Build on a scenario, not on lorem ipsum
230
230
 
231
231
  Preview is only as honest as the data behind it, so the kit ships three complete,
232
- property-facts-shaped scenarios to build fixtures from — real addresses, unit counts, and
232
+ property-shaped scenarios to build fixtures from — real addresses, unit counts, and
233
233
  contacts, the cases that actually break a layout. Pick the one whose breadth matches the
234
234
  state you're exercising:
235
235
 
@@ -260,7 +260,7 @@ rule says where to find it, keyed to the same `source` / `produced_by` a slot de
260
260
 
261
261
  | Slot's fill-spec source | How the fixture fills it |
262
262
  |---|---|
263
- | direct (a fact) | read it off the scenario — `s.facts.address`, `primaryContact(s).fields.phone` |
263
+ | direct (a fact) | read it off the scenario — `s.facts.address.display`, `primaryContact(s).fields.phone` |
264
264
  | derived | a helper — `propertyMetrics(s)`, `poisByCategory(s)`, `contactCards(s)` |
265
265
  | ai | a short sample fill **grounded in the scenario's facts** — a headline about *this* property, never lorem ipsum or invented facts |
266
266
 
@@ -292,6 +292,34 @@ a sibling "brand" object — read them off `primaryContact(s).fields` or `s.cont
292
292
  See [Pick a scenario](recipes/pick-a-scenario.md) for the full picker and helper
293
293
  cheat-sheet.
294
294
 
295
+ ### What a scenario is, and is not
296
+
297
+ A scenario is the **fixture view** of a property, not the fact bundle the AI-fill pipeline
298
+ reads. The difference matters when you write a `fill-spec.ts`:
299
+
300
+ - Scenarios carry values that only exist *after* fill and render — `UnitRow.description`
301
+ and `features` are AI-written copy; a floor plan's `url` and `alt` are attached when the
302
+ image is served. Nothing upstream produces these; they are here so your renderer has
303
+ something real to draw.
304
+ - Scenarios hoist some facts **out** of `facts` and onto the scenario itself, in the shapes
305
+ a renderer wants: `photos` → `s.photos` (`PhotoFixture[]`), `pois` → `s.pois`
306
+ (`PoiFixture[]`), and the fact bundle's `tours` → `s.media` — a single `MediaFixture`
307
+ **object** (`video_url`, `tour_3d_url`), not an array. Floor plans hoist too, but onto
308
+ each unit: `s.facts.units[*].floor_plans` (`FloorplanAssignment[]`).
309
+ - `contacts` is **duplicated, not hoisted**. `s.contacts` holds the render-shaped
310
+ `ContactFixture[]` your renderer draws, and `facts.contacts` still exists as well — so a
311
+ fill-spec that reads or iterates `facts.contacts` has a real counterpart in every
312
+ scenario.
313
+ - `facts.address` is the exception: it mirrors the real address contract field for field
314
+ (`display`, `street`, `street_no_type`, `city`, `state`, `postal`, and an optional
315
+ `unit`). A fill-spec that seeds from `${facts.address.city}` has a real counterpart in
316
+ every scenario. Note `street_no_type` drops the street-type suffix — "845 Pearl" for
317
+ "845 Pearl Street" — so never seed one from the other.
318
+
319
+ Address lines are **derived, never stored**: use `propertyMetrics(s).addressLine1` /
320
+ `addressLine2`, which compose the sub-parts exactly as the publishing pipeline does. Don't
321
+ hand-assemble or split them — a fixture must state the value the real pipeline bakes.
322
+
295
323
  ## Variants — rendering differently based on the facts
296
324
 
297
325
  A variant axis picks a case from the property's facts, before any content is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homepages/template-kit",
3
- "version": "0.5.2",
3
+ "version": "0.6.0-dev-20260717012648",
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",