@brandlab-digital/conformance 3.14.0 → 3.15.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 (108) hide show
  1. package/README.md +60 -3
  2. package/dist/current-release.js +5 -5
  3. package/dist/current-release.js.map +1 -1
  4. package/dist/engineering.d.ts.map +1 -1
  5. package/dist/engineering.js +18 -2
  6. package/dist/engineering.js.map +1 -1
  7. package/dist/estate-snapshot.d.ts +54 -0
  8. package/dist/estate-snapshot.d.ts.map +1 -0
  9. package/dist/estate-snapshot.js +164 -0
  10. package/dist/estate-snapshot.js.map +1 -0
  11. package/dist/estate.d.ts +10 -0
  12. package/dist/estate.d.ts.map +1 -1
  13. package/dist/estate.js +36 -16
  14. package/dist/estate.js.map +1 -1
  15. package/dist/media-snapshot.d.ts +53 -0
  16. package/dist/media-snapshot.d.ts.map +1 -0
  17. package/dist/media-snapshot.js +66 -0
  18. package/dist/media-snapshot.js.map +1 -0
  19. package/dist/site-system/checks.d.ts +2 -2
  20. package/dist/site-system/checks.js +2 -2
  21. package/dist/site-system/checks.js.map +1 -1
  22. package/dist/site-system/config.d.ts +13 -0
  23. package/dist/site-system/config.d.ts.map +1 -1
  24. package/dist/site-system/config.js +30 -1
  25. package/dist/site-system/config.js.map +1 -1
  26. package/dist/site-system/index.d.ts +1 -1
  27. package/dist/site-system/index.d.ts.map +1 -1
  28. package/dist/site-system/index.js +1 -1
  29. package/dist/site-system/index.js.map +1 -1
  30. package/dist/site-system/journeys/conversion.d.ts.map +1 -1
  31. package/dist/site-system/journeys/conversion.js +15 -2
  32. package/dist/site-system/journeys/conversion.js.map +1 -1
  33. package/dist/site-system/journeys/menu.d.ts.map +1 -1
  34. package/dist/site-system/journeys/menu.js +24 -1
  35. package/dist/site-system/journeys/menu.js.map +1 -1
  36. package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
  37. package/dist/site-system/journeys/page-helpers.js +1 -1
  38. package/dist/site-system/journeys/page-helpers.js.map +1 -1
  39. package/dist/site-system/journeys/plan.d.ts +13 -3
  40. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  41. package/dist/site-system/journeys/plan.js +12 -2
  42. package/dist/site-system/journeys/plan.js.map +1 -1
  43. package/dist/site-system/journeys/shell.d.ts +1 -1
  44. package/dist/site-system/journeys/shell.js +2 -2
  45. package/dist/site-system/journeys/shell.js.map +1 -1
  46. package/dist/site-system/report.d.ts.map +1 -1
  47. package/dist/site-system/report.js +24 -4
  48. package/dist/site-system/report.js.map +1 -1
  49. package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
  50. package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
  51. package/dist/site-system/rules/form/c7-thank-you.js +26 -3
  52. package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
  53. package/dist/site-system/rules/form/index.d.ts.map +1 -1
  54. package/dist/site-system/rules/form/index.js +11 -1
  55. package/dist/site-system/rules/form/index.js.map +1 -1
  56. package/dist/site-system/rules/full/common.d.ts +2 -0
  57. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  58. package/dist/site-system/rules/full/common.js.map +1 -1
  59. package/dist/site-system/rules/full/estate.d.ts +10 -2
  60. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  61. package/dist/site-system/rules/full/estate.js +35 -5
  62. package/dist/site-system/rules/full/estate.js.map +1 -1
  63. package/dist/site-system/rules/full/geography.d.ts +7 -0
  64. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  65. package/dist/site-system/rules/full/geography.js +9 -0
  66. package/dist/site-system/rules/full/geography.js.map +1 -1
  67. package/dist/site-system/rules/full/index.d.ts +2 -1
  68. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  69. package/dist/site-system/rules/full/index.js +5 -1
  70. package/dist/site-system/rules/full/index.js.map +1 -1
  71. package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
  72. package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
  73. package/dist/site-system/rules/full/l10-location-copy.js +5 -2
  74. package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
  75. package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
  76. package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
  77. package/dist/site-system/rules/full/l3-neighbours.js +4 -1
  78. package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
  79. package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
  80. package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
  81. package/dist/site-system/rules/full/l4-local-copy.js +8 -3
  82. package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
  83. package/dist/site-system/rules/full/l9-data-contract.js +2 -2
  84. package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
  85. package/dist/site-system/rules/full/links.d.ts +3 -1
  86. package/dist/site-system/rules/full/links.d.ts.map +1 -1
  87. package/dist/site-system/rules/full/links.js +15 -5
  88. package/dist/site-system/rules/full/links.js.map +1 -1
  89. package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
  90. package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
  91. package/dist/site-system/rules/full/route-kinds.js +52 -0
  92. package/dist/site-system/rules/full/route-kinds.js.map +1 -0
  93. package/dist/site-system/rules/full/strategy.d.ts +2 -0
  94. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  95. package/dist/site-system/rules/full/strategy.js +86 -14
  96. package/dist/site-system/rules/full/strategy.js.map +1 -1
  97. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  98. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  99. package/dist/site-system/rules/sections/k2-skin-coverage.js +9 -2
  100. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  101. package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
  102. package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
  103. package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
  104. package/dist/site-system-e2e.d.ts +1 -1
  105. package/dist/site-system-e2e.d.ts.map +1 -1
  106. package/dist/site-system-e2e.js +17 -4
  107. package/dist/site-system-e2e.js.map +1 -1
  108. package/package.json +10 -2
@@ -0,0 +1,54 @@
1
+ import type { SiteEstatePlan, SiteReleaseWave, SiteStrategy } from "@brandlab-digital/site-core/strategy";
2
+ /** Where the snapshot lives, relative to the project root. */
3
+ export declare const ESTATE_PLAN_SNAPSHOT_FILE = "src/data/estate-plan.generated.json";
4
+ /** The folders hashed by default: the snapshot is stale once any file in them changes. */
5
+ export declare const ESTATE_PLAN_SNAPSHOT_SOURCES: readonly string[];
6
+ export declare const ESTATE_PLAN_SNAPSHOT_KIND = "brandlab-estate-plan-snapshot";
7
+ export declare const ESTATE_PLAN_SNAPSHOT_VERSION = 1;
8
+ export interface EstatePlanSnapshot {
9
+ readonly kind: typeof ESTATE_PLAN_SNAPSHOT_KIND;
10
+ readonly version: typeof ESTATE_PLAN_SNAPSHOT_VERSION;
11
+ /** The folders hashed and the hash of their files when the snapshot was written. */
12
+ readonly sources: {
13
+ readonly paths: readonly string[];
14
+ readonly sha256: string;
15
+ };
16
+ readonly waves?: readonly SiteReleaseWave[];
17
+ readonly estate?: SiteEstatePlan;
18
+ }
19
+ export interface EstatePlanSnapshotOptions {
20
+ readonly projectRoot: string;
21
+ /** Folders (or files) relative to the project root the plan is computed from. Defaults to `src/data`. */
22
+ readonly sources?: readonly string[];
23
+ }
24
+ /**
25
+ * The waves and estate plan in their canonical form: waves in date order (stable), each wave's routes sorted, the
26
+ * plan's lists sorted and its keys in a fixed order. Two plans that mean the same thing serialise the same.
27
+ */
28
+ export declare function canonicalEstatePlan(strategy: Pick<SiteStrategy, "waves" | "estate">): Pick<EstatePlanSnapshot, "waves" | "estate">;
29
+ /**
30
+ * The hash of the files the plan is computed from: every code and data file under `sources` (tests and generated
31
+ * files left out), by path and content in path order. The ready gate recomputes it to tell a stale snapshot.
32
+ */
33
+ export declare function estatePlanSourceHash(projectRoot: string, sources?: readonly string[]): string;
34
+ /** The snapshot for a strategy record: its waves and estate plan in canonical form, with the sources' hash. */
35
+ export declare function estatePlanSnapshot(strategy: Pick<SiteStrategy, "waves" | "estate">, options: EstatePlanSnapshotOptions): EstatePlanSnapshot;
36
+ /** The snapshot as the file holds it: two-space JSON and a final newline. */
37
+ export declare function serialiseEstatePlanSnapshot(snapshot: EstatePlanSnapshot): string;
38
+ /**
39
+ * Writes `src/data/estate-plan.generated.json` when its content differs; `changed` says whether it did. Run it from the
40
+ * site's tests or build so the snapshot follows the strategy record, and commit the file.
41
+ */
42
+ export declare function writeEstatePlanSnapshot(strategy: Pick<SiteStrategy, "waves" | "estate">, options: EstatePlanSnapshotOptions): {
43
+ readonly path: string;
44
+ readonly changed: boolean;
45
+ readonly snapshot: EstatePlanSnapshot;
46
+ };
47
+ /** Writes a snapshot file when its text differs; `changed` says whether it did. Shared with the media snapshot. */
48
+ export declare function writeSnapshotFile(projectRoot: string, file: string, text: string): {
49
+ readonly path: string;
50
+ readonly changed: boolean;
51
+ };
52
+ /** The folder list a snapshot records, checked and sorted. */
53
+ export declare function snapshotSourcePaths(sources?: readonly string[]): string[];
54
+ //# sourceMappingURL=estate-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estate-snapshot.d.ts","sourceRoot":"","sources":["../src/estate-snapshot.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AAE1G,8DAA8D;AAC9D,eAAO,MAAM,yBAAyB,wCAAwC,CAAC;AAC/E,0FAA0F;AAC1F,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAAgC,CAAC;AAC3F,eAAO,MAAM,yBAAyB,kCAAkC,CAAC;AACzE,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAM9C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,OAAO,yBAAyB,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,4BAA4B,CAAC;IACtD,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,yGAAyG;IACzG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAUD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,QAAQ,CAAC,CAsClI;AA8BD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,SAAS,MAAM,EAAiC,GAAG,MAAM,CAU3H;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC,EAAE,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAQ3I;AAED,6EAA6E;AAC7E,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAEhF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAC,EAChD,OAAO,EAAE,yBAAyB,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAA;CAAE,CAG7F;AAED,mHAAmH;AACnH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAOvI;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,SAAS,MAAM,EAAiC,GAAG,MAAM,EAAE,CAEvG"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The estate plan snapshot (3.15.0): a site whose waves or estate plan are computed (from its location catalogue,
3
+ * for example) records them as plain JSON, so the ready gate's LOC-INDEX can read them without running the site's
4
+ * code. The snapshot is deterministic (sorted routes and keys, no timestamp) and carries a hash of the source files it
5
+ * was written from (`src/data` by default), so the gate fails a snapshot older than those files.
6
+ *
7
+ * The site writes it from its own tests, where its module aliases resolve:
8
+ *
9
+ * // tests/data/estatePlanSnapshot.test.ts
10
+ * import { writeEstatePlanSnapshot } from '@brandlab-digital/conformance/estate-snapshot'
11
+ * import { siteStrategy } from '@/data/strategy'
12
+ *
13
+ * it('records the estate plan for the ready gate', () => {
14
+ * const { changed } = writeEstatePlanSnapshot(siteStrategy, { projectRoot: process.cwd() })
15
+ * if (process.env.CI) expect(changed).toBe(false)
16
+ * })
17
+ *
18
+ * Node only: it reads and writes files.
19
+ */
20
+ import { createHash } from "node:crypto";
21
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
22
+ import { dirname, isAbsolute, join, normalize, relative, sep } from "node:path";
23
+ /** Where the snapshot lives, relative to the project root. */
24
+ export const ESTATE_PLAN_SNAPSHOT_FILE = "src/data/estate-plan.generated.json";
25
+ /** The folders hashed by default: the snapshot is stale once any file in them changes. */
26
+ export const ESTATE_PLAN_SNAPSHOT_SOURCES = Object.freeze(["src/data"]);
27
+ export const ESTATE_PLAN_SNAPSHOT_KIND = "brandlab-estate-plan-snapshot";
28
+ export const ESTATE_PLAN_SNAPSHOT_VERSION = 1;
29
+ /** The source files hashed: code and data, never tests or generated files. */
30
+ const SOURCE_FILE = /\.(?:ts|tsx|mts|cts|js|mjs|cjs|json|mdx?|csv)$/;
31
+ const SKIPPED_FILE = /(?:\.generated\.json|\.(?:test|spec)\.[a-z]+)$/;
32
+ function sortedUnique(values) {
33
+ return [...new Set(values)].sort();
34
+ }
35
+ function sortedRecord(record) {
36
+ return Object.fromEntries(Object.entries(record).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
37
+ }
38
+ /**
39
+ * The waves and estate plan in their canonical form: waves in date order (stable), each wave's routes sorted, the
40
+ * plan's lists sorted and its keys in a fixed order. Two plans that mean the same thing serialise the same.
41
+ */
42
+ export function canonicalEstatePlan(strategy) {
43
+ const result = {};
44
+ if (strategy.waves !== undefined) {
45
+ result.waves = [...strategy.waves]
46
+ .map((wave, index) => ({ wave, index }))
47
+ .sort((left, right) => (left.wave.releaseOn < right.wave.releaseOn ? -1 : left.wave.releaseOn > right.wave.releaseOn ? 1 : left.index - right.index))
48
+ .map(({ wave }) => ({
49
+ releaseOn: wave.releaseOn,
50
+ ...(wave.kind !== undefined ? { kind: wave.kind } : {}),
51
+ routes: sortedUnique(wave.routes),
52
+ }));
53
+ }
54
+ const plan = strategy.estate;
55
+ if (plan !== undefined) {
56
+ const paths = plan.paths;
57
+ const slugs = plan.slugs;
58
+ result.estate = {
59
+ indexedNow: sortedUnique(plan.indexedNow),
60
+ ...(paths !== undefined
61
+ ? {
62
+ paths: {
63
+ ...(paths.region !== undefined ? { region: paths.region } : {}),
64
+ ...(paths.suburb !== undefined ? { suburb: paths.suburb } : {}),
65
+ ...(paths.serviceInSuburb !== undefined ? { serviceInSuburb: sortedUnique(paths.serviceInSuburb) } : {}),
66
+ },
67
+ }
68
+ : {}),
69
+ ...(slugs !== undefined
70
+ ? {
71
+ slugs: {
72
+ ...(slugs.regions !== undefined ? { regions: sortedRecord(slugs.regions) } : {}),
73
+ ...(slugs.suburbs !== undefined ? { suburbs: sortedRecord(slugs.suburbs) } : {}),
74
+ },
75
+ }
76
+ : {}),
77
+ };
78
+ }
79
+ return result;
80
+ }
81
+ function assertSourcePath(path) {
82
+ if (typeof path !== "string" || path === "" || isAbsolute(path)) {
83
+ throw new Error(`The snapshot source "${String(path)}" must be a folder or file inside the project.`);
84
+ }
85
+ const clean = normalize(path).split(sep).join("/").replace(/\/$/, "");
86
+ if (clean === "." || clean === ".." || clean.startsWith("../")) {
87
+ throw new Error(`The snapshot source "${path}" must be a folder or file inside the project.`);
88
+ }
89
+ return clean;
90
+ }
91
+ function collectFiles(root, path, files) {
92
+ const absolute = join(root, path);
93
+ if (!existsSync(absolute))
94
+ return;
95
+ if (statSync(absolute).isFile()) {
96
+ if (!SKIPPED_FILE.test(path))
97
+ files.push(path);
98
+ return;
99
+ }
100
+ for (const name of readdirSync(absolute)) {
101
+ if (name === "node_modules" || name.startsWith("."))
102
+ continue;
103
+ const child = `${path}/${name}`;
104
+ // statSync follows symbolic links, so a linked file or folder is hashed by what it points to.
105
+ const stats = statSync(join(root, child), { throwIfNoEntry: false });
106
+ if (stats?.isDirectory())
107
+ collectFiles(root, child, files);
108
+ else if (stats?.isFile() && SOURCE_FILE.test(name) && !SKIPPED_FILE.test(name))
109
+ files.push(child);
110
+ }
111
+ }
112
+ /**
113
+ * The hash of the files the plan is computed from: every code and data file under `sources` (tests and generated
114
+ * files left out), by path and content in path order. The ready gate recomputes it to tell a stale snapshot.
115
+ */
116
+ export function estatePlanSourceHash(projectRoot, sources = ESTATE_PLAN_SNAPSHOT_SOURCES) {
117
+ const files = [];
118
+ for (const source of sortedUnique(sources.map(assertSourcePath)))
119
+ collectFiles(projectRoot, source, files);
120
+ const hash = createHash("sha256");
121
+ for (const file of sortedUnique(files)) {
122
+ hash.update(`${file}\n`);
123
+ hash.update(readFileSync(join(projectRoot, file)));
124
+ hash.update("\n");
125
+ }
126
+ return hash.digest("hex");
127
+ }
128
+ /** The snapshot for a strategy record: its waves and estate plan in canonical form, with the sources' hash. */
129
+ export function estatePlanSnapshot(strategy, options) {
130
+ const paths = sortedUnique((options.sources ?? ESTATE_PLAN_SNAPSHOT_SOURCES).map(assertSourcePath));
131
+ return {
132
+ kind: ESTATE_PLAN_SNAPSHOT_KIND,
133
+ version: ESTATE_PLAN_SNAPSHOT_VERSION,
134
+ sources: { paths, sha256: estatePlanSourceHash(options.projectRoot, paths) },
135
+ ...canonicalEstatePlan(strategy),
136
+ };
137
+ }
138
+ /** The snapshot as the file holds it: two-space JSON and a final newline. */
139
+ export function serialiseEstatePlanSnapshot(snapshot) {
140
+ return `${JSON.stringify(snapshot, null, 2)}\n`;
141
+ }
142
+ /**
143
+ * Writes `src/data/estate-plan.generated.json` when its content differs; `changed` says whether it did. Run it from the
144
+ * site's tests or build so the snapshot follows the strategy record, and commit the file.
145
+ */
146
+ export function writeEstatePlanSnapshot(strategy, options) {
147
+ const snapshot = estatePlanSnapshot(strategy, options);
148
+ return { ...writeSnapshotFile(options.projectRoot, ESTATE_PLAN_SNAPSHOT_FILE, serialiseEstatePlanSnapshot(snapshot)), snapshot };
149
+ }
150
+ /** Writes a snapshot file when its text differs; `changed` says whether it did. Shared with the media snapshot. */
151
+ export function writeSnapshotFile(projectRoot, file, text) {
152
+ const path = join(projectRoot, file);
153
+ const current = existsSync(path) ? readFileSync(path, "utf8") : null;
154
+ if (current === text)
155
+ return { path: relative(projectRoot, path), changed: false };
156
+ mkdirSync(dirname(path), { recursive: true });
157
+ writeFileSync(path, text);
158
+ return { path: relative(projectRoot, path), changed: true };
159
+ }
160
+ /** The folder list a snapshot records, checked and sorted. */
161
+ export function snapshotSourcePaths(sources = ESTATE_PLAN_SNAPSHOT_SOURCES) {
162
+ return sortedUnique(sources.map(assertSourcePath));
163
+ }
164
+ //# sourceMappingURL=estate-snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"estate-snapshot.js","sourceRoot":"","sources":["../src/estate-snapshot.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The estate plan snapshot (3.15.0): a site whose waves or estate plan are computed (from its location catalogue,\n * for example) records them as plain JSON, so the ready gate's LOC-INDEX can read them without running the site's\n * code. The snapshot is deterministic (sorted routes and keys, no timestamp) and carries a hash of the source files it\n * was written from (`src/data` by default), so the gate fails a snapshot older than those files.\n *\n * The site writes it from its own tests, where its module aliases resolve:\n *\n * // tests/data/estatePlanSnapshot.test.ts\n * import { writeEstatePlanSnapshot } from '@brandlab-digital/conformance/estate-snapshot'\n * import { siteStrategy } from '@/data/strategy'\n *\n * it('records the estate plan for the ready gate', () => {\n * const { changed } = writeEstatePlanSnapshot(siteStrategy, { projectRoot: process.cwd() })\n * if (process.env.CI) expect(changed).toBe(false)\n * })\n *\n * Node only: it reads and writes files.\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, normalize, relative, sep } from \"node:path\";\nimport type { SiteEstatePlan, SiteReleaseWave, SiteStrategy } from \"@brandlab-digital/site-core/strategy\";\n\n/** Where the snapshot lives, relative to the project root. */\nexport const ESTATE_PLAN_SNAPSHOT_FILE = \"src/data/estate-plan.generated.json\";\n/** The folders hashed by default: the snapshot is stale once any file in them changes. */\nexport const ESTATE_PLAN_SNAPSHOT_SOURCES: readonly string[] = Object.freeze([\"src/data\"]);\nexport const ESTATE_PLAN_SNAPSHOT_KIND = \"brandlab-estate-plan-snapshot\";\nexport const ESTATE_PLAN_SNAPSHOT_VERSION = 1;\n\n/** The source files hashed: code and data, never tests or generated files. */\nconst SOURCE_FILE = /\\.(?:ts|tsx|mts|cts|js|mjs|cjs|json|mdx?|csv)$/;\nconst SKIPPED_FILE = /(?:\\.generated\\.json|\\.(?:test|spec)\\.[a-z]+)$/;\n\nexport interface EstatePlanSnapshot {\n readonly kind: typeof ESTATE_PLAN_SNAPSHOT_KIND;\n readonly version: typeof ESTATE_PLAN_SNAPSHOT_VERSION;\n /** The folders hashed and the hash of their files when the snapshot was written. */\n readonly sources: { readonly paths: readonly string[]; readonly sha256: string };\n readonly waves?: readonly SiteReleaseWave[];\n readonly estate?: SiteEstatePlan;\n}\n\nexport interface EstatePlanSnapshotOptions {\n readonly projectRoot: string;\n /** Folders (or files) relative to the project root the plan is computed from. Defaults to `src/data`. */\n readonly sources?: readonly string[];\n}\n\nfunction sortedUnique(values: readonly string[]): string[] {\n return [...new Set(values)].sort();\n}\n\nfunction sortedRecord(record: Readonly<Record<string, string>>): Record<string, string> {\n return Object.fromEntries(Object.entries(record).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));\n}\n\n/**\n * The waves and estate plan in their canonical form: waves in date order (stable), each wave's routes sorted, the\n * plan's lists sorted and its keys in a fixed order. Two plans that mean the same thing serialise the same.\n */\nexport function canonicalEstatePlan(strategy: Pick<SiteStrategy, \"waves\" | \"estate\">): Pick<EstatePlanSnapshot, \"waves\" | \"estate\"> {\n const result: { waves?: SiteReleaseWave[]; estate?: SiteEstatePlan } = {};\n if (strategy.waves !== undefined) {\n result.waves = [...strategy.waves]\n .map((wave, index) => ({ wave, index }))\n .sort((left, right) => (left.wave.releaseOn < right.wave.releaseOn ? -1 : left.wave.releaseOn > right.wave.releaseOn ? 1 : left.index - right.index))\n .map(({ wave }) => ({\n releaseOn: wave.releaseOn,\n ...(wave.kind !== undefined ? { kind: wave.kind } : {}),\n routes: sortedUnique(wave.routes),\n }));\n }\n const plan = strategy.estate;\n if (plan !== undefined) {\n const paths = plan.paths;\n const slugs = plan.slugs;\n result.estate = {\n indexedNow: sortedUnique(plan.indexedNow),\n ...(paths !== undefined\n ? {\n paths: {\n ...(paths.region !== undefined ? { region: paths.region } : {}),\n ...(paths.suburb !== undefined ? { suburb: paths.suburb } : {}),\n ...(paths.serviceInSuburb !== undefined ? { serviceInSuburb: sortedUnique(paths.serviceInSuburb) } : {}),\n },\n }\n : {}),\n ...(slugs !== undefined\n ? {\n slugs: {\n ...(slugs.regions !== undefined ? { regions: sortedRecord(slugs.regions) } : {}),\n ...(slugs.suburbs !== undefined ? { suburbs: sortedRecord(slugs.suburbs) } : {}),\n },\n }\n : {}),\n };\n }\n return result;\n}\n\nfunction assertSourcePath(path: string): string {\n if (typeof path !== \"string\" || path === \"\" || isAbsolute(path)) {\n throw new Error(`The snapshot source \"${String(path)}\" must be a folder or file inside the project.`);\n }\n const clean = normalize(path).split(sep).join(\"/\").replace(/\\/$/, \"\");\n if (clean === \".\" || clean === \"..\" || clean.startsWith(\"../\")) {\n throw new Error(`The snapshot source \"${path}\" must be a folder or file inside the project.`);\n }\n return clean;\n}\n\nfunction collectFiles(root: string, path: string, files: string[]): void {\n const absolute = join(root, path);\n if (!existsSync(absolute)) return;\n if (statSync(absolute).isFile()) {\n if (!SKIPPED_FILE.test(path)) files.push(path);\n return;\n }\n for (const name of readdirSync(absolute)) {\n if (name === \"node_modules\" || name.startsWith(\".\")) continue;\n const child = `${path}/${name}`;\n // statSync follows symbolic links, so a linked file or folder is hashed by what it points to.\n const stats = statSync(join(root, child), { throwIfNoEntry: false });\n if (stats?.isDirectory()) collectFiles(root, child, files);\n else if (stats?.isFile() && SOURCE_FILE.test(name) && !SKIPPED_FILE.test(name)) files.push(child);\n }\n}\n\n/**\n * The hash of the files the plan is computed from: every code and data file under `sources` (tests and generated\n * files left out), by path and content in path order. The ready gate recomputes it to tell a stale snapshot.\n */\nexport function estatePlanSourceHash(projectRoot: string, sources: readonly string[] = ESTATE_PLAN_SNAPSHOT_SOURCES): string {\n const files: string[] = [];\n for (const source of sortedUnique(sources.map(assertSourcePath))) collectFiles(projectRoot, source, files);\n const hash = createHash(\"sha256\");\n for (const file of sortedUnique(files)) {\n hash.update(`${file}\\n`);\n hash.update(readFileSync(join(projectRoot, file)));\n hash.update(\"\\n\");\n }\n return hash.digest(\"hex\");\n}\n\n/** The snapshot for a strategy record: its waves and estate plan in canonical form, with the sources' hash. */\nexport function estatePlanSnapshot(strategy: Pick<SiteStrategy, \"waves\" | \"estate\">, options: EstatePlanSnapshotOptions): EstatePlanSnapshot {\n const paths = sortedUnique((options.sources ?? ESTATE_PLAN_SNAPSHOT_SOURCES).map(assertSourcePath));\n return {\n kind: ESTATE_PLAN_SNAPSHOT_KIND,\n version: ESTATE_PLAN_SNAPSHOT_VERSION,\n sources: { paths, sha256: estatePlanSourceHash(options.projectRoot, paths) },\n ...canonicalEstatePlan(strategy),\n };\n}\n\n/** The snapshot as the file holds it: two-space JSON and a final newline. */\nexport function serialiseEstatePlanSnapshot(snapshot: EstatePlanSnapshot): string {\n return `${JSON.stringify(snapshot, null, 2)}\\n`;\n}\n\n/**\n * Writes `src/data/estate-plan.generated.json` when its content differs; `changed` says whether it did. Run it from the\n * site's tests or build so the snapshot follows the strategy record, and commit the file.\n */\nexport function writeEstatePlanSnapshot(\n strategy: Pick<SiteStrategy, \"waves\" | \"estate\">,\n options: EstatePlanSnapshotOptions\n): { readonly path: string; readonly changed: boolean; readonly snapshot: EstatePlanSnapshot } {\n const snapshot = estatePlanSnapshot(strategy, options);\n return { ...writeSnapshotFile(options.projectRoot, ESTATE_PLAN_SNAPSHOT_FILE, serialiseEstatePlanSnapshot(snapshot)), snapshot };\n}\n\n/** Writes a snapshot file when its text differs; `changed` says whether it did. Shared with the media snapshot. */\nexport function writeSnapshotFile(projectRoot: string, file: string, text: string): { readonly path: string; readonly changed: boolean } {\n const path = join(projectRoot, file);\n const current = existsSync(path) ? readFileSync(path, \"utf8\") : null;\n if (current === text) return { path: relative(projectRoot, path), changed: false };\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, text);\n return { path: relative(projectRoot, path), changed: true };\n}\n\n/** The folder list a snapshot records, checked and sorted. */\nexport function snapshotSourcePaths(sources: readonly string[] = ESTATE_PLAN_SNAPSHOT_SOURCES): string[] {\n return sortedUnique(sources.map(assertSourcePath));\n}\n"]}
package/dist/estate.d.ts CHANGED
@@ -212,6 +212,16 @@ export declare function pageQualityRunFromEnv(env?: NodeJS.ProcessEnv): {
212
212
  reason: string;
213
213
  directory?: string;
214
214
  };
215
+ /**
216
+ * The internal pages a page's server HTML links, once each, without the page itself. Only anchors count: an area named
217
+ * as plain text (a `Coverage` entry with no `href`, 3.15.0) is no link.
218
+ */
219
+ export declare function pageLinkTargets(html: string, path: string, origin: string, base?: string): Set<string>;
220
+ /**
221
+ * Indexable pages that link a noindex location page (D4: list a place whose page waits for its wave as plain text,
222
+ * as site-core's `coverageEntries` does). `linksFrom` maps each page to the pages it links.
223
+ */
224
+ export declare function noindexLinkFindings(linksFrom: ReadonlyMap<string, ReadonlySet<string>>, routes: readonly Pick<BrandlabEstateRoute, 'path' | 'indexing' | 'kind'>[]): BrandlabPageQualityFinding[];
215
225
  /**
216
226
  * Absolute (`https://host`) and protocol-relative (`//host`) destinations leave
217
227
  * the site; browsers also read `/\host` and `\\host` as protocol-relative.
@@ -1 +1 @@
1
- {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../src/estate.ts"],"names":[],"mappings":"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"}
package/dist/estate.js CHANGED
@@ -659,26 +659,14 @@ export function defineBrandlabPageQualityTests(options) {
659
659
  const place = placeOfRoute(route)?.slug;
660
660
  if (indexable && place)
661
661
  similarityPages.push({ path: route.path, place, blocks: textBlocks(html).map(geography.maskPlaces) });
662
- const targets = new Set();
663
- for (const href of extractLinks(html)) {
664
- const path = internalPath(href, expectedCanonical, origin);
665
- if (path && path !== normalisePath(route.path))
666
- targets.add(path);
667
- }
668
- linksFrom.set(normalisePath(route.path), targets);
662
+ linksFrom.set(normalisePath(route.path), pageLinkTargets(html, route.path, origin, expectedCanonical));
669
663
  });
670
664
  // Internal links: every indexable page needs a link from another page, and indexable pages link no noindex location page.
671
665
  const inbound = new Map();
672
- for (const [from, targets] of linksFrom) {
673
- const source = routeByPath.get(from);
674
- for (const target of targets) {
666
+ for (const targets of linksFrom.values())
667
+ for (const target of targets)
675
668
  inbound.set(target, (inbound.get(target) ?? 0) + 1);
676
- const linked = routeByPath.get(target);
677
- if (source?.indexing === 'index' && linked && linked.indexing !== 'index' && LOCATION_KINDS.has(linked.kind ?? '')) {
678
- findings.push({ path: from, kind: 'noindex-link', detail: `links to ${target}, a noindex location page (list unindexed places as plain text)` });
679
- }
680
- }
681
- }
669
+ findings.push(...noindexLinkFindings(linksFrom, routes));
682
670
  for (const route of routes) {
683
671
  const path = normalisePath(route.path);
684
672
  if (route.indexing === 'index' && route.kind !== 'home' && path !== '/' && linksFrom.has(path) && !inbound.get(path)) {
@@ -789,6 +777,38 @@ function inspectStructuredData(route, payloads, businessId) {
789
777
  failures.push(`${route.path}: JSON-LD does not reference ${businessId}`);
790
778
  return failures;
791
779
  }
780
+ /**
781
+ * The internal pages a page's server HTML links, once each, without the page itself. Only anchors count: an area named
782
+ * as plain text (a `Coverage` entry with no `href`, 3.15.0) is no link.
783
+ */
784
+ export function pageLinkTargets(html, path, origin, base = new URL(path, `${origin}/`).href) {
785
+ const targets = new Set();
786
+ for (const href of extractLinks(html)) {
787
+ const target = internalPath(href, base, origin);
788
+ if (target && target !== normalisePath(path))
789
+ targets.add(target);
790
+ }
791
+ return targets;
792
+ }
793
+ /**
794
+ * Indexable pages that link a noindex location page (D4: list a place whose page waits for its wave as plain text,
795
+ * as site-core's `coverageEntries` does). `linksFrom` maps each page to the pages it links.
796
+ */
797
+ export function noindexLinkFindings(linksFrom, routes) {
798
+ const routeByPath = new Map(routes.map((route) => [normalisePath(route.path), route]));
799
+ const findings = [];
800
+ for (const [from, targets] of linksFrom) {
801
+ if (routeByPath.get(from)?.indexing !== 'index')
802
+ continue;
803
+ for (const target of targets) {
804
+ const linked = routeByPath.get(target);
805
+ if (linked && linked.indexing !== 'index' && LOCATION_KINDS.has(linked.kind ?? '')) {
806
+ findings.push({ path: from, kind: 'noindex-link', detail: `links to ${target}, a noindex location page (list unindexed places as plain text)` });
807
+ }
808
+ }
809
+ }
810
+ return findings;
811
+ }
792
812
  function extractLinks(html) {
793
813
  return [...html.matchAll(/<a\b[^>]*\bhref=["']([^"']+)["']/gi)].map((match) => decodeXml(match[1]));
794
814
  }