@brandlab-digital/conformance 3.11.0 → 3.12.1
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/dist/current-release.js +6 -6
- package/dist/current-release.js.map +1 -1
- package/dist/ready-cli.d.ts.map +1 -1
- package/dist/ready-cli.js +1 -0
- package/dist/ready-cli.js.map +1 -1
- package/dist/site-system/checks.d.ts +1 -1
- package/dist/site-system/checks.js +1 -1
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/rule-modules.d.ts.map +1 -1
- package/dist/site-system/rule-modules.js +5 -0
- package/dist/site-system/rule-modules.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +37 -0
- package/dist/site-system/rules/full/common.d.ts.map +1 -0
- package/dist/site-system/rules/full/common.js +51 -0
- package/dist/site-system/rules/full/common.js.map +1 -0
- package/dist/site-system/rules/full/e10-phrases.d.ts +14 -0
- package/dist/site-system/rules/full/e10-phrases.d.ts.map +1 -0
- package/dist/site-system/rules/full/e10-phrases.js +69 -0
- package/dist/site-system/rules/full/e10-phrases.js.map +1 -0
- package/dist/site-system/rules/full/estate.d.ts +118 -0
- package/dist/site-system/rules/full/estate.d.ts.map +1 -0
- package/dist/site-system/rules/full/estate.js +447 -0
- package/dist/site-system/rules/full/estate.js.map +1 -0
- package/dist/site-system/rules/full/geography.d.ts +60 -0
- package/dist/site-system/rules/full/geography.d.ts.map +1 -0
- package/dist/site-system/rules/full/geography.js +111 -0
- package/dist/site-system/rules/full/geography.js.map +1 -0
- package/dist/site-system/rules/full/index.d.ts +33 -0
- package/dist/site-system/rules/full/index.d.ts.map +1 -0
- package/dist/site-system/rules/full/index.js +126 -0
- package/dist/site-system/rules/full/index.js.map +1 -0
- package/dist/site-system/rules/full/k12-manifests.d.ts +19 -0
- package/dist/site-system/rules/full/k12-manifests.d.ts.map +1 -0
- package/dist/site-system/rules/full/k12-manifests.js +130 -0
- package/dist/site-system/rules/full/k12-manifests.js.map +1 -0
- package/dist/site-system/rules/full/l10-location-copy.d.ts +15 -0
- package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -0
- package/dist/site-system/rules/full/l10-location-copy.js +59 -0
- package/dist/site-system/rules/full/l10-location-copy.js.map +1 -0
- package/dist/site-system/rules/full/l3-neighbours.d.ts +16 -0
- package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -0
- package/dist/site-system/rules/full/l3-neighbours.js +78 -0
- package/dist/site-system/rules/full/l3-neighbours.js.map +1 -0
- package/dist/site-system/rules/full/l4-local-copy.d.ts +27 -0
- package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -0
- package/dist/site-system/rules/full/l4-local-copy.js +150 -0
- package/dist/site-system/rules/full/l4-local-copy.js.map +1 -0
- package/dist/site-system/rules/full/l9-data-contract.d.ts +15 -0
- package/dist/site-system/rules/full/l9-data-contract.d.ts.map +1 -0
- package/dist/site-system/rules/full/l9-data-contract.js +65 -0
- package/dist/site-system/rules/full/l9-data-contract.js.map +1 -0
- package/dist/site-system/rules/full/links.d.ts +23 -0
- package/dist/site-system/rules/full/links.d.ts.map +1 -0
- package/dist/site-system/rules/full/links.js +131 -0
- package/dist/site-system/rules/full/links.js.map +1 -0
- package/dist/site-system/rules/full/loc-index.d.ts +16 -0
- package/dist/site-system/rules/full/loc-index.d.ts.map +1 -0
- package/dist/site-system/rules/full/loc-index.js +59 -0
- package/dist/site-system/rules/full/loc-index.js.map +1 -0
- package/dist/site-system/rules/full/s4-route-files.d.ts +4 -0
- package/dist/site-system/rules/full/s4-route-files.d.ts.map +1 -0
- package/dist/site-system/rules/full/s4-route-files.js +125 -0
- package/dist/site-system/rules/full/s4-route-files.js.map +1 -0
- package/dist/site-system/rules/full/strategy.d.ts +43 -0
- package/dist/site-system/rules/full/strategy.d.ts.map +1 -0
- package/dist/site-system/rules/full/strategy.js +147 -0
- package/dist/site-system/rules/full/strategy.js.map +1 -0
- package/dist/site-system/runners.d.ts +2 -0
- package/dist/site-system/runners.d.ts.map +1 -1
- package/dist/site-system/runners.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The places the full tier reasons about: registry suburbs with their region and centre, and the regions. They come
|
|
3
|
+
* from the site's installed site-core geography at run time (the conformance package compiles without site-core), or
|
|
4
|
+
* from `defineFullGeography` in a test.
|
|
5
|
+
*/
|
|
6
|
+
import { createPlaceMask } from "../../../page-standard.js";
|
|
7
|
+
export function defineFullGeography(input) {
|
|
8
|
+
const suburbs = new Map();
|
|
9
|
+
const centres = new Map();
|
|
10
|
+
for (const suburb of input.suburbs) {
|
|
11
|
+
if (suburbs.has(suburb.slug))
|
|
12
|
+
continue;
|
|
13
|
+
suburbs.set(suburb.slug, { slug: suburb.slug, name: suburb.name, region: suburb.region, ...(suburb.council ? { council: suburb.council } : {}) });
|
|
14
|
+
if (suburb.centre)
|
|
15
|
+
centres.set(suburb.slug, suburb.centre);
|
|
16
|
+
}
|
|
17
|
+
const regions = new Map();
|
|
18
|
+
for (const region of input.regions) {
|
|
19
|
+
if (!regions.has(region.slug))
|
|
20
|
+
regions.set(region.slug, { slug: region.slug, name: region.name, names: [...new Set([region.name, ...(region.names ?? [])])] });
|
|
21
|
+
}
|
|
22
|
+
const names = new Map();
|
|
23
|
+
for (const suburb of suburbs.values())
|
|
24
|
+
names.set(suburb.name.toLowerCase(), suburb.slug);
|
|
25
|
+
for (const region of regions.values())
|
|
26
|
+
for (const name of region.names)
|
|
27
|
+
if (!names.has(name.toLowerCase()))
|
|
28
|
+
names.set(name.toLowerCase(), region.slug);
|
|
29
|
+
const alternation = [...names.keys()]
|
|
30
|
+
.filter((name) => name.length >= 3)
|
|
31
|
+
.sort((left, right) => right.length - left.length)
|
|
32
|
+
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+"));
|
|
33
|
+
const pattern = alternation.length > 0 ? new RegExp(`\\b(?:${alternation.join("|")})\\b`, "gi") : null;
|
|
34
|
+
return {
|
|
35
|
+
centreOf: input.centreOf ?? ((slug) => centres.get(slug)),
|
|
36
|
+
suburbs,
|
|
37
|
+
regions,
|
|
38
|
+
mask: createPlaceMask([...names.keys()]),
|
|
39
|
+
names,
|
|
40
|
+
placesIn: (text) => {
|
|
41
|
+
if (!pattern)
|
|
42
|
+
return [];
|
|
43
|
+
const found = new Set();
|
|
44
|
+
for (const match of text.matchAll(pattern)) {
|
|
45
|
+
const slug = names.get(match[0].toLowerCase().replace(/\s+/g, " "));
|
|
46
|
+
if (slug)
|
|
47
|
+
found.add(slug);
|
|
48
|
+
}
|
|
49
|
+
return [...found];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// A variable specifier, so the package compiles without site-core; it resolves to the site's installed copy.
|
|
54
|
+
const GEOGRAPHY_MODULE = "@brandlab-digital/site-core/geography";
|
|
55
|
+
let loaded;
|
|
56
|
+
/** The site's installed site-core geography (3.4.0 or later). */
|
|
57
|
+
export function loadFullGeography() {
|
|
58
|
+
loaded ??= import(GEOGRAPHY_MODULE).then((module) => {
|
|
59
|
+
if (typeof module.getSuburbCentre !== "function")
|
|
60
|
+
throw new Error("the installed site-core geography has no suburb centres (3.4.0 or later is needed)");
|
|
61
|
+
const registries = [module.sydneyGeography, module.centralCoastGeography];
|
|
62
|
+
return defineFullGeography({
|
|
63
|
+
suburbs: registries.flatMap((registry) => [...registry.suburbs, ...registry.nonSuburbLocalities].map((place) => ({ slug: place.slug, name: place.name, region: place.regionSlug, council: place.councilSlug }))),
|
|
64
|
+
regions: registries.flatMap((registry) => [
|
|
65
|
+
...registry.regions.map((region) => ({ slug: region.slug, name: region.name, names: [region.seoName, ...(region.alternateNames ?? [])] })),
|
|
66
|
+
...registry.subRegions.map((region) => ({ slug: region.slug, name: region.name })),
|
|
67
|
+
]),
|
|
68
|
+
centreOf: module.getSuburbCentre,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return loaded;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The place a path names: its last segment, or the end of its last segment after a hyphen (`/tree-removal-ryde`),
|
|
75
|
+
* when that is a registry suburb or region (`{region}-area` included). The longest match wins.
|
|
76
|
+
*/
|
|
77
|
+
export function placeOfPath(path, geography) {
|
|
78
|
+
const segments = path.split("/");
|
|
79
|
+
const last = segments.pop() ?? "";
|
|
80
|
+
if (!last)
|
|
81
|
+
return undefined;
|
|
82
|
+
const parent = segments.join("/");
|
|
83
|
+
const kindOf = (slug) => {
|
|
84
|
+
if (geography.suburbs.has(slug))
|
|
85
|
+
return "suburb";
|
|
86
|
+
if (geography.regions.has(slug))
|
|
87
|
+
return "region";
|
|
88
|
+
if (slug.endsWith("-area") && geography.regions.has(slug.slice(0, -5)))
|
|
89
|
+
return "region";
|
|
90
|
+
return undefined;
|
|
91
|
+
};
|
|
92
|
+
const whole = kindOf(last);
|
|
93
|
+
if (whole)
|
|
94
|
+
return { slug: regionSlug(last, geography), kind: whole, stem: `${parent}/{place}` };
|
|
95
|
+
const parts = last.split("-");
|
|
96
|
+
for (let start = 1; start < parts.length; start += 1) {
|
|
97
|
+
const slug = parts.slice(start).join("-");
|
|
98
|
+
const kind = kindOf(slug);
|
|
99
|
+
if (kind)
|
|
100
|
+
return { slug: regionSlug(slug, geography), kind, stem: `${parent}/${parts.slice(0, start).join("-")}-{place}` };
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function regionSlug(slug, geography) {
|
|
105
|
+
return !geography.suburbs.has(slug) && !geography.regions.has(slug) && slug.endsWith("-area") ? slug.slice(0, -5) : slug;
|
|
106
|
+
}
|
|
107
|
+
/** A place's display name, for messages and for matching it in copy. */
|
|
108
|
+
export function placeName(slug, geography) {
|
|
109
|
+
return geography.suburbs.get(slug)?.name ?? geography.regions.get(slug)?.name ?? slug;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=geography.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geography.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/geography.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The places the full tier reasons about: registry suburbs with their region and centre, and the regions. They come\n * from the site's installed site-core geography at run time (the conformance package compiles without site-core), or\n * from `defineFullGeography` in a test.\n */\nimport { createPlaceMask, type SuburbCentre, type SuburbCentreLookup } from \"../../../page-standard.js\";\n\nexport interface FullGeographySuburb {\n readonly slug: string;\n readonly name: string;\n /** The registry region the suburb belongs to. */\n readonly region: string;\n /** The suburb's council, when known. */\n readonly council?: string;\n}\n\nexport interface FullGeographyRegion {\n readonly slug: string;\n readonly name: string;\n /** Every name the region goes by (name, search name and alternates). */\n readonly names: readonly string[];\n}\n\nexport interface FullGeography {\n readonly centreOf: SuburbCentreLookup;\n readonly suburbs: ReadonlyMap<string, FullGeographySuburb>;\n /** Regions and sub-regions, by slug. */\n readonly regions: ReadonlyMap<string, FullGeographyRegion>;\n /** Masks every place name and postcode in lowercase text, so name-swapped copy compares as the same. */\n readonly mask: (text: string) => string;\n /** Lowercase place names (suburbs and regions) and their slugs. */\n readonly names: ReadonlyMap<string, string>;\n /** The slugs of the places a piece of copy names, longest name first (\"North Ryde\" before \"Ryde\"). */\n readonly placesIn: (text: string) => string[];\n}\n\nexport interface FullGeographyInput {\n readonly suburbs: readonly (FullGeographySuburb & { readonly centre?: SuburbCentre })[];\n readonly regions: readonly { readonly slug: string; readonly name: string; readonly names?: readonly string[] }[];\n readonly centreOf?: SuburbCentreLookup;\n}\n\nexport function defineFullGeography(input: FullGeographyInput): FullGeography {\n const suburbs = new Map<string, FullGeographySuburb>();\n const centres = new Map<string, SuburbCentre>();\n for (const suburb of input.suburbs) {\n if (suburbs.has(suburb.slug)) continue;\n suburbs.set(suburb.slug, { slug: suburb.slug, name: suburb.name, region: suburb.region, ...(suburb.council ? { council: suburb.council } : {}) });\n if (suburb.centre) centres.set(suburb.slug, suburb.centre);\n }\n const regions = new Map<string, FullGeographyRegion>();\n for (const region of input.regions) {\n if (!regions.has(region.slug)) regions.set(region.slug, { slug: region.slug, name: region.name, names: [...new Set([region.name, ...(region.names ?? [])])] });\n }\n const names = new Map<string, string>();\n for (const suburb of suburbs.values()) names.set(suburb.name.toLowerCase(), suburb.slug);\n for (const region of regions.values()) for (const name of region.names) if (!names.has(name.toLowerCase())) names.set(name.toLowerCase(), region.slug);\n const alternation = [...names.keys()]\n .filter((name) => name.length >= 3)\n .sort((left, right) => right.length - left.length)\n .map((name) => name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\s+/g, \"\\\\s+\"));\n const pattern = alternation.length > 0 ? new RegExp(`\\\\b(?:${alternation.join(\"|\")})\\\\b`, \"gi\") : null;\n return {\n centreOf: input.centreOf ?? ((slug) => centres.get(slug)),\n suburbs,\n regions,\n mask: createPlaceMask([...names.keys()]),\n names,\n placesIn: (text) => {\n if (!pattern) return [];\n const found = new Set<string>();\n for (const match of text.matchAll(pattern)) {\n const slug = names.get(match[0].toLowerCase().replace(/\\s+/g, \" \"));\n if (slug) found.add(slug);\n }\n return [...found];\n },\n };\n}\n\ninterface RegistryLike {\n suburbs: readonly { slug: string; name: string; regionSlug: string; councilSlug: string }[];\n nonSuburbLocalities: readonly { slug: string; name: string; regionSlug: string; councilSlug: string }[];\n regions: readonly { slug: string; name: string; seoName: string; alternateNames?: readonly string[] }[];\n subRegions: readonly { slug: string; name: string }[];\n}\n\n// A variable specifier, so the package compiles without site-core; it resolves to the site's installed copy.\nconst GEOGRAPHY_MODULE: string = \"@brandlab-digital/site-core/geography\";\nlet loaded: Promise<FullGeography> | undefined;\n\n/** The site's installed site-core geography (3.4.0 or later). */\nexport function loadFullGeography(): Promise<FullGeography> {\n loaded ??= (import(GEOGRAPHY_MODULE) as Promise<{\n getSuburbCentre?: SuburbCentreLookup;\n sydneyGeography: RegistryLike;\n centralCoastGeography: RegistryLike;\n }>).then((module) => {\n if (typeof module.getSuburbCentre !== \"function\") throw new Error(\"the installed site-core geography has no suburb centres (3.4.0 or later is needed)\");\n const registries = [module.sydneyGeography, module.centralCoastGeography];\n return defineFullGeography({\n suburbs: registries.flatMap((registry) =>\n [...registry.suburbs, ...registry.nonSuburbLocalities].map((place) => ({ slug: place.slug, name: place.name, region: place.regionSlug, council: place.councilSlug }))),\n regions: registries.flatMap((registry) => [\n ...registry.regions.map((region) => ({ slug: region.slug, name: region.name, names: [region.seoName, ...(region.alternateNames ?? [])] })),\n ...registry.subRegions.map((region) => ({ slug: region.slug, name: region.name })),\n ]),\n centreOf: module.getSuburbCentre,\n });\n });\n return loaded;\n}\n\nexport interface PathPlace {\n readonly slug: string;\n readonly kind: \"suburb\" | \"region\";\n /** The path with the place replaced by `{place}`: pages sharing a stem are one family's pages. */\n readonly stem: string;\n}\n\n/**\n * The place a path names: its last segment, or the end of its last segment after a hyphen (`/tree-removal-ryde`),\n * when that is a registry suburb or region (`{region}-area` included). The longest match wins.\n */\nexport function placeOfPath(path: string, geography: FullGeography): PathPlace | undefined {\n const segments = path.split(\"/\");\n const last = segments.pop() ?? \"\";\n if (!last) return undefined;\n const parent = segments.join(\"/\");\n const kindOf = (slug: string): PathPlace[\"kind\"] | undefined => {\n if (geography.suburbs.has(slug)) return \"suburb\";\n if (geography.regions.has(slug)) return \"region\";\n if (slug.endsWith(\"-area\") && geography.regions.has(slug.slice(0, -5))) return \"region\";\n return undefined;\n };\n const whole = kindOf(last);\n if (whole) return { slug: regionSlug(last, geography), kind: whole, stem: `${parent}/{place}` };\n const parts = last.split(\"-\");\n for (let start = 1; start < parts.length; start += 1) {\n const slug = parts.slice(start).join(\"-\");\n const kind = kindOf(slug);\n if (kind) return { slug: regionSlug(slug, geography), kind, stem: `${parent}/${parts.slice(0, start).join(\"-\")}-{place}` };\n }\n return undefined;\n}\n\nfunction regionSlug(slug: string, geography: FullGeography): string {\n return !geography.suburbs.has(slug) && !geography.regions.has(slug) && slug.endsWith(\"-area\") ? slug.slice(0, -5) : slug;\n}\n\n/** A place's display name, for messages and for matching it in copy. */\nexport function placeName(slug: string, geography: FullGeography): string {\n return geography.suburbs.get(slug)?.name ?? geography.regions.get(slug)?.name ?? slug;\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
2
|
+
import type { SiteSystemRenderedInput } from "../../runners.js";
|
|
3
|
+
import { type FullContext } from "./common.js";
|
|
4
|
+
import { type FullGeography } from "./geography.js";
|
|
5
|
+
export { FULL_LISTED_FAILURES, type FullContext } from "./common.js";
|
|
6
|
+
export { headingPhrase } from "./e10-phrases.js";
|
|
7
|
+
export { readEstate, type Estate, type EstatePage, type LocationCopy, type ManifestData } from "./estate.js";
|
|
8
|
+
export { defineFullGeography, loadFullGeography, placeOfPath, type FullGeography, type FullGeographyInput } from "./geography.js";
|
|
9
|
+
export { K12_FAMILIES, isDataBackedReason } from "./k12-manifests.js";
|
|
10
|
+
export { L3_MAX_LIST, L3_MIN_LIST, L3_TOLERANCE_KM } from "./l3-neighbours.js";
|
|
11
|
+
export { L4_MIN_ROTATION_PAGES, L4_MIN_TEMPLATE_WORDS } from "./l4-local-copy.js";
|
|
12
|
+
export { yearStamp } from "./l10-location-copy.js";
|
|
13
|
+
export { L6_MIN_INBOUND, L6_MIN_PAGE_TYPES, L7_MAX_DEPTH, clickDepths } from "./links.js";
|
|
14
|
+
export { sydneyDay } from "./loc-index.js";
|
|
15
|
+
export { FULL_STRATEGY_FILE, WAVE_MAX_GAP_DAYS, WAVE_MIN_GAP_DAYS, matchesWaveRoute, normalisePhrase, readFullStrategy, waveScheduleProblems } from "./strategy.js";
|
|
16
|
+
/** The full-tier checks this module runs, in the order their findings are returned. */
|
|
17
|
+
export declare const FULL_TIER_CHECK_IDS: readonly ["K12", "E5", "E10", "L3", "L4", "L6", "L7", "L8", "L9", "L10", "S4", "LOC-INDEX"];
|
|
18
|
+
export interface FullTierRuleOptions {
|
|
19
|
+
/** The day waves are judged on. Defaults to now. */
|
|
20
|
+
readonly asOf?: Date;
|
|
21
|
+
/** The build directory; defaults to `input.buildDir`, then `<projectRoot>/.next`. */
|
|
22
|
+
readonly buildDir?: string;
|
|
23
|
+
/** Places for a test; a real run loads the site's site-core geography. Null runs without places. */
|
|
24
|
+
readonly geography?: FullGeography | null;
|
|
25
|
+
/** Canonical section ids per family for a test; a real run loads site-core's `defaultSectionOrder`. */
|
|
26
|
+
readonly defaultOrder?: FullContext["defaultOrder"];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Runs the full-tier rules over the built estate at `projectRoot`. A site that declares a lower tier (or none) gets no
|
|
30
|
+
* findings: the report shows the full checks as not run, which never blocks below the full tier.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runFullTierRules(input: SiteSystemRenderedInput, options?: FullTierRuleOptions): Promise<readonly SiteSystemFinding[]>;
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/index.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAWvE,OAAO,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAClI,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAEpK,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,6FAAqI,CAAC;AAEtK,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC;IACrB,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oGAAoG;IACpG,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC1C,uGAAuG;IACvG,QAAQ,CAAC,YAAY,CAAC,EAAE,WAAW,CAAC,cAAc,CAAC,CAAC;CACrD;AAoCD;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,uBAAuB,EAC9B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CA+CvC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full-tier rules (release 3.12.0): page families, location estates, section manifests, links and the local data
|
|
3
|
+
* contract. They read the production build's route catalogue and pages (`.next`), the project's strategy record and
|
|
4
|
+
* source, and the site's installed site-core (geography and section orders), and return findings for the ready report:
|
|
5
|
+
* K12 (families), E5, E10, L3, L4, L6, L7, L8, L9, L10 (location), S4 and the location indexing guard. The runner has
|
|
6
|
+
* the signature the ready gate registers.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { readSiteSystemProjectConfig } from "../../config.js";
|
|
10
|
+
import {} from "./common.js";
|
|
11
|
+
import { checkE10 } from "./e10-phrases.js";
|
|
12
|
+
import { readEstate } from "./estate.js";
|
|
13
|
+
import { loadFullGeography } from "./geography.js";
|
|
14
|
+
import { checkK12 } from "./k12-manifests.js";
|
|
15
|
+
import { checkL3 } from "./l3-neighbours.js";
|
|
16
|
+
import { checkL4 } from "./l4-local-copy.js";
|
|
17
|
+
import { checkL9 } from "./l9-data-contract.js";
|
|
18
|
+
import { checkL10 } from "./l10-location-copy.js";
|
|
19
|
+
import { checkE5, checkL6, checkL7, checkL8, inboundLinks } from "./links.js";
|
|
20
|
+
import { checkLocationIndexing, sydneyDay } from "./loc-index.js";
|
|
21
|
+
import { checkS4 } from "./s4-route-files.js";
|
|
22
|
+
import { readFullStrategy } from "./strategy.js";
|
|
23
|
+
export { FULL_LISTED_FAILURES } from "./common.js";
|
|
24
|
+
export { headingPhrase } from "./e10-phrases.js";
|
|
25
|
+
export { readEstate } from "./estate.js";
|
|
26
|
+
export { defineFullGeography, loadFullGeography, placeOfPath } from "./geography.js";
|
|
27
|
+
export { K12_FAMILIES, isDataBackedReason } from "./k12-manifests.js";
|
|
28
|
+
export { L3_MAX_LIST, L3_MIN_LIST, L3_TOLERANCE_KM } from "./l3-neighbours.js";
|
|
29
|
+
export { L4_MIN_ROTATION_PAGES, L4_MIN_TEMPLATE_WORDS } from "./l4-local-copy.js";
|
|
30
|
+
export { yearStamp } from "./l10-location-copy.js";
|
|
31
|
+
export { L6_MIN_INBOUND, L6_MIN_PAGE_TYPES, L7_MAX_DEPTH, clickDepths } from "./links.js";
|
|
32
|
+
export { sydneyDay } from "./loc-index.js";
|
|
33
|
+
export { FULL_STRATEGY_FILE, WAVE_MAX_GAP_DAYS, WAVE_MIN_GAP_DAYS, matchesWaveRoute, normalisePhrase, readFullStrategy, waveScheduleProblems } from "./strategy.js";
|
|
34
|
+
/** The full-tier checks this module runs, in the order their findings are returned. */
|
|
35
|
+
export const FULL_TIER_CHECK_IDS = ["K12", "E5", "E10", "L3", "L4", "L6", "L7", "L8", "L9", "L10", "S4", "LOC-INDEX"];
|
|
36
|
+
// Variable specifiers, so the package compiles without site-core; they resolve to the site's installed copy.
|
|
37
|
+
const SECTION_ORDER_MODULE = "@brandlab-digital/site-core/section-order";
|
|
38
|
+
async function loadDefaultOrder() {
|
|
39
|
+
try {
|
|
40
|
+
const module = (await import(SECTION_ORDER_MODULE));
|
|
41
|
+
const defaults = module.defaultSectionOrder;
|
|
42
|
+
if (typeof defaults !== "function")
|
|
43
|
+
return "the installed site-core has no defaultSectionOrder (3.11.0 or later is needed)";
|
|
44
|
+
return (family, trade) => {
|
|
45
|
+
try {
|
|
46
|
+
const order = defaults(family, trade);
|
|
47
|
+
return { cap: order.cap, entries: order.entries.map((entry) => ({ id: entry.id, requirement: entry.requirement })) };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return `site-core's section orders could not be imported: ${error instanceof Error ? error.message : String(error)}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The declared tier and the representative service route; null tier when the config cannot be read in full. */
|
|
59
|
+
function projectSettings(projectRoot) {
|
|
60
|
+
try {
|
|
61
|
+
const config = readSiteSystemProjectConfig(projectRoot);
|
|
62
|
+
// A config the gate cannot read in full could hide a full tier, so the checks run.
|
|
63
|
+
return { tier: config.complete ? (config.siteSystem?.tier ?? "none") : null, service: config.representativeRoutes["service"] };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { tier: null, service: undefined };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs the full-tier rules over the built estate at `projectRoot`. A site that declares a lower tier (or none) gets no
|
|
71
|
+
* findings: the report shows the full checks as not run, which never blocks below the full tier.
|
|
72
|
+
*/
|
|
73
|
+
export async function runFullTierRules(input, options = {}) {
|
|
74
|
+
const settings = projectSettings(input.projectRoot);
|
|
75
|
+
if (settings.tier !== null && settings.tier !== "full")
|
|
76
|
+
return [];
|
|
77
|
+
let geography = null;
|
|
78
|
+
let geographyProblem = "";
|
|
79
|
+
if (options.geography !== undefined) {
|
|
80
|
+
geography = options.geography;
|
|
81
|
+
if (!geography)
|
|
82
|
+
geographyProblem = "no geography was supplied";
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
try {
|
|
86
|
+
geography = await loadFullGeography();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
geographyProblem = `site-core's geography could not be imported: ${error instanceof Error ? error.message : String(error)}`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const estate = readEstate({
|
|
93
|
+
pages: input.pages,
|
|
94
|
+
buildDir: options.buildDir ?? input.buildDir ?? join(input.projectRoot, ".next"),
|
|
95
|
+
geography,
|
|
96
|
+
...(settings.service ? { seeds: { [settings.service]: "service" } } : {}),
|
|
97
|
+
});
|
|
98
|
+
const today = sydneyDay(options.asOf ?? new Date());
|
|
99
|
+
const context = {
|
|
100
|
+
estate,
|
|
101
|
+
geography,
|
|
102
|
+
geographyProblem,
|
|
103
|
+
strategy: readFullStrategy(input.projectRoot, today),
|
|
104
|
+
projectRoot: input.projectRoot,
|
|
105
|
+
today,
|
|
106
|
+
defaultOrder: options.defaultOrder ?? (await loadDefaultOrder()),
|
|
107
|
+
};
|
|
108
|
+
const inbound = inboundLinks(estate);
|
|
109
|
+
const findings = [
|
|
110
|
+
...checkK12(context),
|
|
111
|
+
...checkE5(context, inbound),
|
|
112
|
+
...checkE10(context),
|
|
113
|
+
...checkL3(context),
|
|
114
|
+
...checkL4(context),
|
|
115
|
+
...checkL6(context, inbound),
|
|
116
|
+
...checkL7(context),
|
|
117
|
+
...checkL8(context),
|
|
118
|
+
...checkL9(context),
|
|
119
|
+
...checkL10(context),
|
|
120
|
+
];
|
|
121
|
+
// The copy is only for L3, L4 and L10; let it go before the source scan.
|
|
122
|
+
for (const page of estate.pages)
|
|
123
|
+
page.copy = undefined;
|
|
124
|
+
return [...findings, ...checkS4(context), ...checkLocationIndexing(context)];
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The full-tier rules (release 3.12.0): page families, location estates, section manifests, links and the local data\n * contract. They read the production build's route catalogue and pages (`.next`), the project's strategy record and\n * source, and the site's installed site-core (geography and section orders), and return findings for the ready report:\n * K12 (families), E5, E10, L3, L4, L6, L7, L8, L9, L10 (location), S4 and the location indexing guard. The runner has\n * the signature the ready gate registers.\n */\nimport { join } from \"node:path\";\nimport { readSiteSystemProjectConfig } from \"../../config.js\";\nimport type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport type { SiteSystemRenderedInput } from \"../../runners.js\";\nimport { type FullContext } from \"./common.js\";\nimport { checkE10 } from \"./e10-phrases.js\";\nimport { readEstate } from \"./estate.js\";\nimport { loadFullGeography, type FullGeography } from \"./geography.js\";\nimport { checkK12 } from \"./k12-manifests.js\";\nimport { checkL3 } from \"./l3-neighbours.js\";\nimport { checkL4 } from \"./l4-local-copy.js\";\nimport { checkL9 } from \"./l9-data-contract.js\";\nimport { checkL10 } from \"./l10-location-copy.js\";\nimport { checkE5, checkL6, checkL7, checkL8, inboundLinks } from \"./links.js\";\nimport { checkLocationIndexing, sydneyDay } from \"./loc-index.js\";\nimport { checkS4 } from \"./s4-route-files.js\";\nimport { readFullStrategy } from \"./strategy.js\";\n\nexport { FULL_LISTED_FAILURES, type FullContext } from \"./common.js\";\nexport { headingPhrase } from \"./e10-phrases.js\";\nexport { readEstate, type Estate, type EstatePage, type LocationCopy, type ManifestData } from \"./estate.js\";\nexport { defineFullGeography, loadFullGeography, placeOfPath, type FullGeography, type FullGeographyInput } from \"./geography.js\";\nexport { K12_FAMILIES, isDataBackedReason } from \"./k12-manifests.js\";\nexport { L3_MAX_LIST, L3_MIN_LIST, L3_TOLERANCE_KM } from \"./l3-neighbours.js\";\nexport { L4_MIN_ROTATION_PAGES, L4_MIN_TEMPLATE_WORDS } from \"./l4-local-copy.js\";\nexport { yearStamp } from \"./l10-location-copy.js\";\nexport { L6_MIN_INBOUND, L6_MIN_PAGE_TYPES, L7_MAX_DEPTH, clickDepths } from \"./links.js\";\nexport { sydneyDay } from \"./loc-index.js\";\nexport { FULL_STRATEGY_FILE, WAVE_MAX_GAP_DAYS, WAVE_MIN_GAP_DAYS, matchesWaveRoute, normalisePhrase, readFullStrategy, waveScheduleProblems } from \"./strategy.js\";\n\n/** The full-tier checks this module runs, in the order their findings are returned. */\nexport const FULL_TIER_CHECK_IDS = [\"K12\", \"E5\", \"E10\", \"L3\", \"L4\", \"L6\", \"L7\", \"L8\", \"L9\", \"L10\", \"S4\", \"LOC-INDEX\"] as const satisfies readonly SiteSystemCheckId[];\n\nexport interface FullTierRuleOptions {\n /** The day waves are judged on. Defaults to now. */\n readonly asOf?: Date;\n /** The build directory; defaults to `input.buildDir`, then `<projectRoot>/.next`. */\n readonly buildDir?: string;\n /** Places for a test; a real run loads the site's site-core geography. Null runs without places. */\n readonly geography?: FullGeography | null;\n /** Canonical section ids per family for a test; a real run loads site-core's `defaultSectionOrder`. */\n readonly defaultOrder?: FullContext[\"defaultOrder\"];\n}\n\n// Variable specifiers, so the package compiles without site-core; they resolve to the site's installed copy.\nconst SECTION_ORDER_MODULE: string = \"@brandlab-digital/site-core/section-order\";\n\nasync function loadDefaultOrder(): Promise<FullContext[\"defaultOrder\"]> {\n try {\n const module = (await import(SECTION_ORDER_MODULE)) as {\n defaultSectionOrder?: (family: string, trade?: string) => { cap: number; entries: readonly { id: string; requirement: string }[] };\n };\n const defaults = module.defaultSectionOrder;\n if (typeof defaults !== \"function\") return \"the installed site-core has no defaultSectionOrder (3.11.0 or later is needed)\";\n return (family, trade) => {\n try {\n const order = defaults(family, trade);\n return { cap: order.cap, entries: order.entries.map((entry) => ({ id: entry.id, requirement: entry.requirement })) };\n } catch {\n return undefined;\n }\n };\n } catch (error) {\n return `site-core's section orders could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n}\n\n/** The declared tier and the representative service route; null tier when the config cannot be read in full. */\nfunction projectSettings(projectRoot: string): { tier: string | null; service: string | undefined } {\n try {\n const config = readSiteSystemProjectConfig(projectRoot);\n // A config the gate cannot read in full could hide a full tier, so the checks run.\n return { tier: config.complete ? (config.siteSystem?.tier ?? \"none\") : null, service: config.representativeRoutes[\"service\"] };\n } catch {\n return { tier: null, service: undefined };\n }\n}\n\n/**\n * Runs the full-tier rules over the built estate at `projectRoot`. A site that declares a lower tier (or none) gets no\n * findings: the report shows the full checks as not run, which never blocks below the full tier.\n */\nexport async function runFullTierRules(\n input: SiteSystemRenderedInput,\n options: FullTierRuleOptions = {},\n): Promise<readonly SiteSystemFinding[]> {\n const settings = projectSettings(input.projectRoot);\n if (settings.tier !== null && settings.tier !== \"full\") return [];\n let geography: FullGeography | null = null;\n let geographyProblem = \"\";\n if (options.geography !== undefined) {\n geography = options.geography;\n if (!geography) geographyProblem = \"no geography was supplied\";\n } else {\n try {\n geography = await loadFullGeography();\n } catch (error) {\n geographyProblem = `site-core's geography could not be imported: ${error instanceof Error ? error.message : String(error)}`;\n }\n }\n const estate = readEstate({\n pages: input.pages,\n buildDir: options.buildDir ?? input.buildDir ?? join(input.projectRoot, \".next\"),\n geography,\n ...(settings.service ? { seeds: { [settings.service]: \"service\" } } : {}),\n });\n const today = sydneyDay(options.asOf ?? new Date());\n const context: FullContext = {\n estate,\n geography,\n geographyProblem,\n strategy: readFullStrategy(input.projectRoot, today),\n projectRoot: input.projectRoot,\n today,\n defaultOrder: options.defaultOrder ?? (await loadDefaultOrder()),\n };\n const inbound = inboundLinks(estate);\n const findings = [\n ...checkK12(context),\n ...checkE5(context, inbound),\n ...checkE10(context),\n ...checkL3(context),\n ...checkL4(context),\n ...checkL6(context, inbound),\n ...checkL7(context),\n ...checkL8(context),\n ...checkL9(context),\n ...checkL10(context),\n ];\n // The copy is only for L3, L4 and L10; let it go before the source scan.\n for (const page of estate.pages) page.copy = undefined;\n return [...findings, ...checkS4(context), ...checkLocationIndexing(context)];\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K12 on every family (Site System v2 §3.3, §13.2): each page of a family carries one section manifest
|
|
3
|
+
* (`<script type="application/json" data-bl-manifest>`, written by SectionSequence). Every required section renders
|
|
4
|
+
* or is skipped with a reason drawn from its data (`no-…`), never the sequencer's `not-provided`, `cap` or
|
|
5
|
+
* `duplicate`; every conditional section that does not render says its data is absent; the planned order is the
|
|
6
|
+
* family's canonical one or the alternative recorded in strategy.ts; and the page renders exactly the sections the
|
|
7
|
+
* manifest lists, in that order. A page over its cap (`cap-exceeded`) is a warning, not a failure. The contact page
|
|
8
|
+
* has its own check (K12-contact).
|
|
9
|
+
*/
|
|
10
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
11
|
+
import { type FullContext } from "./common.js";
|
|
12
|
+
import type { EstatePage, ManifestData } from "./estate.js";
|
|
13
|
+
/** The families that carry a manifest (site-core BRANDLAB_PAGE_FAMILIES without the contact page). */
|
|
14
|
+
export declare const K12_FAMILIES: readonly ["home", "service", "service-in-suburb", "region", "suburb", "cost-guide", "services-hub", "locations-hub", "projects-hub", "project", "gallery", "reviews", "about", "faqs", "guides-hub", "guide", "council", "emergency"];
|
|
15
|
+
/** As site-core's isDataBackedSkipReason: the section's own data is absent, or its content is in the hero checklist. */
|
|
16
|
+
export declare function isDataBackedReason(reason: string): boolean;
|
|
17
|
+
export declare function manifestProblems(page: EstatePage, manifest: ManifestData, context: FullContext): string[];
|
|
18
|
+
export declare function checkK12(context: FullContext): SiteSystemFinding[];
|
|
19
|
+
//# sourceMappingURL=k12-manifests.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k12-manifests.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/k12-manifests.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAyD,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACtG,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE5D,sGAAsG;AACtG,eAAO,MAAM,YAAY,uOAmBf,CAAC;AAEX,wHAAwH;AACxH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE1D;AAMD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM,EAAE,CA8DzG;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CA+BlE"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { fail, listOf, notApplicable, outcome, plural, warning } from "./common.js";
|
|
2
|
+
/** The families that carry a manifest (site-core BRANDLAB_PAGE_FAMILIES without the contact page). */
|
|
3
|
+
export const K12_FAMILIES = [
|
|
4
|
+
"home",
|
|
5
|
+
"service",
|
|
6
|
+
"service-in-suburb",
|
|
7
|
+
"region",
|
|
8
|
+
"suburb",
|
|
9
|
+
"cost-guide",
|
|
10
|
+
"services-hub",
|
|
11
|
+
"locations-hub",
|
|
12
|
+
"projects-hub",
|
|
13
|
+
"project",
|
|
14
|
+
"gallery",
|
|
15
|
+
"reviews",
|
|
16
|
+
"about",
|
|
17
|
+
"faqs",
|
|
18
|
+
"guides-hub",
|
|
19
|
+
"guide",
|
|
20
|
+
"council",
|
|
21
|
+
"emergency",
|
|
22
|
+
];
|
|
23
|
+
/** As site-core's isDataBackedSkipReason: the section's own data is absent, or its content is in the hero checklist. */
|
|
24
|
+
export function isDataBackedReason(reason) {
|
|
25
|
+
return reason === "merge-into-hero-checklist" || /^no-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(reason);
|
|
26
|
+
}
|
|
27
|
+
function sameList(left, right) {
|
|
28
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
29
|
+
}
|
|
30
|
+
export function manifestProblems(page, manifest, context) {
|
|
31
|
+
const problems = [];
|
|
32
|
+
const where = page.path;
|
|
33
|
+
const planned = manifest.planned.map((entry) => entry.id);
|
|
34
|
+
if (!K12_FAMILIES.includes(manifest.family)) {
|
|
35
|
+
return [`The manifest on ${where} names "${manifest.family}", which is not a page family that carries one.`];
|
|
36
|
+
}
|
|
37
|
+
if (manifest.order === "recorded") {
|
|
38
|
+
const recorded = context.strategy.recordedOrders.get(manifest.family);
|
|
39
|
+
if (!recorded)
|
|
40
|
+
problems.push(`The manifest on ${where} says its order is recorded, but strategy.ts records no order for ${manifest.family} pages.`);
|
|
41
|
+
else if (!sameList(planned, recorded))
|
|
42
|
+
problems.push(`The sections planned on ${where} (${planned.join(", ")}) are not the order recorded for ${manifest.family} pages (${recorded.join(", ")}).`);
|
|
43
|
+
}
|
|
44
|
+
const canonical = typeof context.defaultOrder === "string" ? undefined : context.defaultOrder(manifest.family, context.strategy.trade);
|
|
45
|
+
if (canonical) {
|
|
46
|
+
const ids = canonical.entries.map((entry) => entry.id);
|
|
47
|
+
if (manifest.order !== "recorded" && !sameList(planned, ids)) {
|
|
48
|
+
problems.push(`The sections planned on ${where} (${planned.join(", ")}) are not the canonical order for ${manifest.family} pages (${ids.join(", ")}).`);
|
|
49
|
+
}
|
|
50
|
+
// Each planned section keeps the requirement the family gives it, whatever the order.
|
|
51
|
+
const requirementOf = new Map(canonical.entries.map((entry) => [entry.id, entry.requirement]));
|
|
52
|
+
for (const entry of manifest.planned) {
|
|
53
|
+
const expected = requirementOf.get(entry.id);
|
|
54
|
+
if (expected !== undefined && expected !== entry.requirement) {
|
|
55
|
+
problems.push(`The manifest on ${where} marks "${entry.id}" as ${entry.requirement}; on ${manifest.family} pages it is ${expected}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (manifest.cap !== canonical.cap)
|
|
59
|
+
problems.push(`The manifest on ${where} gives a cap of ${manifest.cap}; ${manifest.family} pages have a cap of ${canonical.cap}.`);
|
|
60
|
+
}
|
|
61
|
+
if (manifest.flags.includes("cap-exceeded") !== manifest.counted > manifest.cap) {
|
|
62
|
+
problems.push(`The manifest on ${where} counts ${manifest.counted} sections against a cap of ${manifest.cap} but ${manifest.flags.includes("cap-exceeded") ? "flags" : "does not flag"} cap-exceeded.`);
|
|
63
|
+
}
|
|
64
|
+
// What renders follows the plan, and the page shows exactly what the manifest lists.
|
|
65
|
+
let position = -1;
|
|
66
|
+
for (const entry of manifest.rendered) {
|
|
67
|
+
const index = planned.indexOf(entry.id, position + 1);
|
|
68
|
+
if (index === -1) {
|
|
69
|
+
problems.push(`The manifest on ${where} lists "${entry.id}" as rendered out of its planned place.`);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
position = index;
|
|
73
|
+
}
|
|
74
|
+
const listed = manifest.rendered.map((entry) => entry.section);
|
|
75
|
+
if (!sameList(page.sections, listed)) {
|
|
76
|
+
problems.push(`${where} renders the sections ${page.sections.join(", ") || "(none)"}, but its manifest lists ${listed.join(", ") || "(none)"}.`);
|
|
77
|
+
}
|
|
78
|
+
const renderedIds = new Set(manifest.rendered.map((entry) => entry.id));
|
|
79
|
+
for (const entry of manifest.planned) {
|
|
80
|
+
if (entry.requirement !== "R" && entry.requirement !== "C")
|
|
81
|
+
continue;
|
|
82
|
+
if (renderedIds.has(entry.id))
|
|
83
|
+
continue;
|
|
84
|
+
const reasons = manifest.skipped.filter((skip) => skip.id === entry.id).map((skip) => skip.reason);
|
|
85
|
+
if (reasons.some(isDataBackedReason))
|
|
86
|
+
continue;
|
|
87
|
+
const reason = reasons[0] ?? "no reason recorded";
|
|
88
|
+
problems.push(entry.requirement === "R"
|
|
89
|
+
? `${where} skips its required "${entry.id}" section without a reason drawn from its data (${reason}).`
|
|
90
|
+
: `${where} leaves out its conditional "${entry.id}" section without saying its data is absent (${reason}).`);
|
|
91
|
+
}
|
|
92
|
+
return problems;
|
|
93
|
+
}
|
|
94
|
+
export function checkK12(context) {
|
|
95
|
+
const failures = [];
|
|
96
|
+
let checked = 0;
|
|
97
|
+
if (typeof context.defaultOrder === "string") {
|
|
98
|
+
failures.push(fail("K12", undefined, `The canonical section orders could not be loaded (${context.defaultOrder}), so default orders were not compared.`));
|
|
99
|
+
}
|
|
100
|
+
for (const problem of context.strategy.problems.orders)
|
|
101
|
+
failures.push(fail("K12", "src/data/strategy.ts", problem));
|
|
102
|
+
for (const page of context.estate.pages) {
|
|
103
|
+
if (!K12_FAMILIES.includes(page.family ?? ""))
|
|
104
|
+
continue;
|
|
105
|
+
checked += 1;
|
|
106
|
+
const state = page.manifest;
|
|
107
|
+
if (state.kind === "none") {
|
|
108
|
+
failures.push(fail("K12", page.path, `${page.path} is a ${page.family} page with no section manifest; its sections go through SectionSequence.`));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (state.kind === "many") {
|
|
112
|
+
failures.push(fail("K12", page.path, `${page.path} carries ${state.count} section manifests; a page has one.`));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (state.kind === "invalid") {
|
|
116
|
+
failures.push(fail("K12", page.path, `The section manifest on ${page.path} ${state.reason}.`));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const problem of manifestProblems(page, state.manifest, context))
|
|
120
|
+
failures.push(fail("K12", page.path, problem));
|
|
121
|
+
if (state.manifest.flags.includes("cap-exceeded")) {
|
|
122
|
+
failures.push(warning("K12", page.path, `${page.path} renders ${state.manifest.counted} sections against a cap of ${state.manifest.cap}, because its required and conditional sections all have data.`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (checked === 0)
|
|
126
|
+
return [...failures, notApplicable("K12", "No page of a family that carries a section manifest was found.")];
|
|
127
|
+
const families = [...new Set(context.estate.pages.flatMap((page) => (K12_FAMILIES.includes(page.family ?? "") ? [page.family] : [])))];
|
|
128
|
+
return outcome("K12", failures, `${plural(checked, "page")} across ${listOf(families)} carry a section manifest with every required and conditional section accounted for, in the canonical or recorded order.`);
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=k12-manifests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"k12-manifests.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/k12-manifests.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * K12 on every family (Site System v2 §3.3, §13.2): each page of a family carries one section manifest\n * (`<script type=\"application/json\" data-bl-manifest>`, written by SectionSequence). Every required section renders\n * or is skipped with a reason drawn from its data (`no-…`), never the sequencer's `not-provided`, `cap` or\n * `duplicate`; every conditional section that does not render says its data is absent; the planned order is the\n * family's canonical one or the alternative recorded in strategy.ts; and the page renders exactly the sections the\n * manifest lists, in that order. A page over its cap (`cap-exceeded`) is a warning, not a failure. The contact page\n * has its own check (K12-contact).\n */\nimport type { SiteSystemFinding } from \"../../checks.js\";\nimport { fail, listOf, notApplicable, outcome, plural, warning, type FullContext } from \"./common.js\";\nimport type { EstatePage, ManifestData } from \"./estate.js\";\n\n/** The families that carry a manifest (site-core BRANDLAB_PAGE_FAMILIES without the contact page). */\nexport const K12_FAMILIES = [\n \"home\",\n \"service\",\n \"service-in-suburb\",\n \"region\",\n \"suburb\",\n \"cost-guide\",\n \"services-hub\",\n \"locations-hub\",\n \"projects-hub\",\n \"project\",\n \"gallery\",\n \"reviews\",\n \"about\",\n \"faqs\",\n \"guides-hub\",\n \"guide\",\n \"council\",\n \"emergency\",\n] as const;\n\n/** As site-core's isDataBackedSkipReason: the section's own data is absent, or its content is in the hero checklist. */\nexport function isDataBackedReason(reason: string): boolean {\n return reason === \"merge-into-hero-checklist\" || /^no-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(reason);\n}\n\nfunction sameList(left: readonly string[], right: readonly string[]): boolean {\n return left.length === right.length && left.every((item, index) => item === right[index]);\n}\n\nexport function manifestProblems(page: EstatePage, manifest: ManifestData, context: FullContext): string[] {\n const problems: string[] = [];\n const where = page.path;\n const planned = manifest.planned.map((entry) => entry.id);\n\n if (!(K12_FAMILIES as readonly string[]).includes(manifest.family)) {\n return [`The manifest on ${where} names \"${manifest.family}\", which is not a page family that carries one.`];\n }\n if (manifest.order === \"recorded\") {\n const recorded = context.strategy.recordedOrders.get(manifest.family);\n if (!recorded) problems.push(`The manifest on ${where} says its order is recorded, but strategy.ts records no order for ${manifest.family} pages.`);\n else if (!sameList(planned, recorded)) problems.push(`The sections planned on ${where} (${planned.join(\", \")}) are not the order recorded for ${manifest.family} pages (${recorded.join(\", \")}).`);\n }\n const canonical = typeof context.defaultOrder === \"string\" ? undefined : context.defaultOrder(manifest.family, context.strategy.trade);\n if (canonical) {\n const ids = canonical.entries.map((entry) => entry.id);\n if (manifest.order !== \"recorded\" && !sameList(planned, ids)) {\n problems.push(`The sections planned on ${where} (${planned.join(\", \")}) are not the canonical order for ${manifest.family} pages (${ids.join(\", \")}).`);\n }\n // Each planned section keeps the requirement the family gives it, whatever the order.\n const requirementOf = new Map(canonical.entries.map((entry) => [entry.id, entry.requirement]));\n for (const entry of manifest.planned) {\n const expected = requirementOf.get(entry.id);\n if (expected !== undefined && expected !== entry.requirement) {\n problems.push(`The manifest on ${where} marks \"${entry.id}\" as ${entry.requirement}; on ${manifest.family} pages it is ${expected}.`);\n }\n }\n if (manifest.cap !== canonical.cap) problems.push(`The manifest on ${where} gives a cap of ${manifest.cap}; ${manifest.family} pages have a cap of ${canonical.cap}.`);\n }\n if (manifest.flags.includes(\"cap-exceeded\") !== manifest.counted > manifest.cap) {\n problems.push(`The manifest on ${where} counts ${manifest.counted} sections against a cap of ${manifest.cap} but ${manifest.flags.includes(\"cap-exceeded\") ? \"flags\" : \"does not flag\"} cap-exceeded.`);\n }\n\n // What renders follows the plan, and the page shows exactly what the manifest lists.\n let position = -1;\n for (const entry of manifest.rendered) {\n const index = planned.indexOf(entry.id, position + 1);\n if (index === -1) {\n problems.push(`The manifest on ${where} lists \"${entry.id}\" as rendered out of its planned place.`);\n break;\n }\n position = index;\n }\n const listed = manifest.rendered.map((entry) => entry.section);\n if (!sameList(page.sections, listed)) {\n problems.push(`${where} renders the sections ${page.sections.join(\", \") || \"(none)\"}, but its manifest lists ${listed.join(\", \") || \"(none)\"}.`);\n }\n\n const renderedIds = new Set(manifest.rendered.map((entry) => entry.id));\n for (const entry of manifest.planned) {\n if (entry.requirement !== \"R\" && entry.requirement !== \"C\") continue;\n if (renderedIds.has(entry.id)) continue;\n const reasons = manifest.skipped.filter((skip) => skip.id === entry.id).map((skip) => skip.reason);\n if (reasons.some(isDataBackedReason)) continue;\n const reason = reasons[0] ?? \"no reason recorded\";\n problems.push(\n entry.requirement === \"R\"\n ? `${where} skips its required \"${entry.id}\" section without a reason drawn from its data (${reason}).`\n : `${where} leaves out its conditional \"${entry.id}\" section without saying its data is absent (${reason}).`,\n );\n }\n return problems;\n}\n\nexport function checkK12(context: FullContext): SiteSystemFinding[] {\n const failures: SiteSystemFinding[] = [];\n let checked = 0;\n if (typeof context.defaultOrder === \"string\") {\n failures.push(fail(\"K12\", undefined, `The canonical section orders could not be loaded (${context.defaultOrder}), so default orders were not compared.`));\n }\n for (const problem of context.strategy.problems.orders) failures.push(fail(\"K12\", \"src/data/strategy.ts\", problem));\n for (const page of context.estate.pages) {\n if (!(K12_FAMILIES as readonly string[]).includes(page.family ?? \"\")) continue;\n checked += 1;\n const state = page.manifest;\n if (state.kind === \"none\") {\n failures.push(fail(\"K12\", page.path, `${page.path} is a ${page.family} page with no section manifest; its sections go through SectionSequence.`));\n continue;\n }\n if (state.kind === \"many\") {\n failures.push(fail(\"K12\", page.path, `${page.path} carries ${state.count} section manifests; a page has one.`));\n continue;\n }\n if (state.kind === \"invalid\") {\n failures.push(fail(\"K12\", page.path, `The section manifest on ${page.path} ${state.reason}.`));\n continue;\n }\n for (const problem of manifestProblems(page, state.manifest, context)) failures.push(fail(\"K12\", page.path, problem));\n if (state.manifest.flags.includes(\"cap-exceeded\")) {\n failures.push(warning(\"K12\", page.path, `${page.path} renders ${state.manifest.counted} sections against a cap of ${state.manifest.cap}, because its required and conditional sections all have data.`));\n }\n }\n if (checked === 0) return [...failures, notApplicable(\"K12\", \"No page of a family that carries a section manifest was found.\")];\n const families = [...new Set(context.estate.pages.flatMap((page) => ((K12_FAMILIES as readonly string[]).includes(page.family ?? \"\") ? [page.family!] : [])))];\n return outcome(\"K12\", failures, `${plural(checked, \"page\")} across ${listOf(families)} carry a section manifest with every required and conditional section accounted for, in the canonical or recorded order.`);\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L10 for location copy (Site System v2 §4.4 step 4, banned on location pages): no kilometre figures, no "km from"
|
|
3
|
+
* and no "based in" anywhere in a suburb, service-in-suburb or region page's content (verbatim reviews aside), and no
|
|
4
|
+
* year stamp posing as local detail: a year in a line that names a place, outside a "Month Year" job or review date,
|
|
5
|
+
* a price table and the proof strip. The patterns are the shell's (L10-hero), so the two checks never disagree.
|
|
6
|
+
*/
|
|
7
|
+
import type { SiteSystemFinding } from "../../checks.js";
|
|
8
|
+
import { type FullContext } from "./common.js";
|
|
9
|
+
/**
|
|
10
|
+
* The first year stamp in a line, or null. A number after a month is a date, and a number straight after a place
|
|
11
|
+
* name ("Abbotsford 2046", "Ryde NSW 2112") is a postcode; `isPlace` says whether some words are a place name.
|
|
12
|
+
*/
|
|
13
|
+
export declare function yearStamp(line: string, isPlace?: (words: string) => boolean): string | null;
|
|
14
|
+
export declare function checkL10(context: FullContext): SiteSystemFinding[];
|
|
15
|
+
//# sourceMappingURL=l10-location-copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"l10-location-copy.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/l10-location-copy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AASrF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAqB,GAAG,MAAM,GAAG,IAAI,CASxG;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,iBAAiB,EAAE,CAwBlE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BASED_IN, KM_FIGURE, KM_FROM } from "../shell/location-wording.js";
|
|
2
|
+
import { fail, notApplicable, outcome, plural } from "./common.js";
|
|
3
|
+
import { isLocationFamily } from "./estate.js";
|
|
4
|
+
const MONTH = "(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)";
|
|
5
|
+
/** A year from 1800 to 2099, or a decade ("1950s"), not part of a price, a longer number or a postcode. */
|
|
6
|
+
const YEAR = /(?<![$\d.,/:-])\b(1[89]\d\d|20\d\d)(?:'?s)?\b(?![.,]?\d)/gi;
|
|
7
|
+
const BEFORE_YEAR_ALLOWED = new RegExp(`(?:${MONTH}\\.?|nsw|postcode|©|\\(c\\))\\s*$`, "i");
|
|
8
|
+
const NO_YEAR_SECTIONS = new Set(["reviews", "nearby-jobs", "price-guide", "proof-strip"]);
|
|
9
|
+
/**
|
|
10
|
+
* The first year stamp in a line, or null. A number after a month is a date, and a number straight after a place
|
|
11
|
+
* name ("Abbotsford 2046", "Ryde NSW 2112") is a postcode; `isPlace` says whether some words are a place name.
|
|
12
|
+
*/
|
|
13
|
+
export function yearStamp(line, isPlace = () => false) {
|
|
14
|
+
for (const match of line.matchAll(YEAR)) {
|
|
15
|
+
const before = line.slice(0, match.index);
|
|
16
|
+
if (BEFORE_YEAR_ALLOWED.test(before))
|
|
17
|
+
continue;
|
|
18
|
+
const words = before.replace(/[,\s]+$/, "").split(/\s+/);
|
|
19
|
+
if ([1, 2, 3, 4].some((count) => words.length >= count && isPlace(words.slice(-count).join(" "))))
|
|
20
|
+
continue;
|
|
21
|
+
return match[0];
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function checkL10(context) {
|
|
26
|
+
const geography = context.geography;
|
|
27
|
+
const failures = [];
|
|
28
|
+
let checked = 0;
|
|
29
|
+
for (const page of context.estate.pages) {
|
|
30
|
+
if (!isLocationFamily(page.family) || !page.copy)
|
|
31
|
+
continue;
|
|
32
|
+
checked += 1;
|
|
33
|
+
// One example of each kind of problem.
|
|
34
|
+
const found = new Map();
|
|
35
|
+
const isPlace = (words) => geography?.names.has(words.toLowerCase()) === true;
|
|
36
|
+
for (const line of page.copy.lines) {
|
|
37
|
+
if (line.section === "reviews")
|
|
38
|
+
continue;
|
|
39
|
+
const km = KM_FIGURE.exec(line.text) ?? KM_FROM.exec(line.text);
|
|
40
|
+
if (km && !found.has("km"))
|
|
41
|
+
found.set("km", `a kilometre figure ("${km[0]}")`);
|
|
42
|
+
if (BASED_IN.test(line.text))
|
|
43
|
+
found.set("based", `"based in"`);
|
|
44
|
+
if (NO_YEAR_SECTIONS.has(line.section) || !geography || found.has("year"))
|
|
45
|
+
continue;
|
|
46
|
+
const year = yearStamp(line.text, isPlace);
|
|
47
|
+
if (year && geography.placesIn(line.text).length > 0)
|
|
48
|
+
found.set("year", `a year stamp as local detail ("${year}" in "${line.text.slice(0, 80)}")`);
|
|
49
|
+
}
|
|
50
|
+
if (found.size > 0)
|
|
51
|
+
failures.push(fail("L10", page.path, `The location copy on ${page.path} has ${[...found.values()].join(" and ")}, which location pages do not carry.`));
|
|
52
|
+
}
|
|
53
|
+
if (checked === 0)
|
|
54
|
+
return [notApplicable("L10", "No location page was read, so there is no location copy to check.")];
|
|
55
|
+
if (!geography)
|
|
56
|
+
failures.push(fail("L10", undefined, `The place names could not be read (${context.geographyProblem}), so year stamps posing as local detail could not be checked.`));
|
|
57
|
+
return outcome("L10", failures, `No kilometre figures, "km from", "based in" or year stamps as local detail in the copy of ${plural(checked, "location page")}.`);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=l10-location-copy.js.map
|