@brandlab-digital/conformance 1.0.0-rc.2 → 1.0.0-rc.20

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 (44) hide show
  1. package/dist/analytics-contract.d.ts +20 -0
  2. package/dist/analytics-contract.d.ts.map +1 -0
  3. package/dist/analytics-contract.js +211 -0
  4. package/dist/analytics-contract.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +13 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/project-config-v3.d.ts +9 -1
  10. package/dist/project-config-v3.d.ts.map +1 -1
  11. package/dist/project-config-v3.js +210 -24
  12. package/dist/project-config-v3.js.map +1 -1
  13. package/dist/project-config.d.ts +1 -1
  14. package/dist/project-config.d.ts.map +1 -1
  15. package/dist/project-config.js +1 -1
  16. package/dist/project-config.js.map +1 -1
  17. package/dist/project-documentation.d.ts +28 -0
  18. package/dist/project-documentation.d.ts.map +1 -0
  19. package/dist/project-documentation.js +398 -0
  20. package/dist/project-documentation.js.map +1 -0
  21. package/dist/project.d.ts +4 -1
  22. package/dist/project.d.ts.map +1 -1
  23. package/dist/project.js +263 -15
  24. package/dist/project.js.map +1 -1
  25. package/dist/release-catalog.d.ts.map +1 -1
  26. package/dist/release-catalog.js +1356 -0
  27. package/dist/release-catalog.js.map +1 -1
  28. package/dist/release-contract-compatibility.d.ts +126 -0
  29. package/dist/release-contract-compatibility.d.ts.map +1 -1
  30. package/dist/release-contract-compatibility.js +126 -0
  31. package/dist/release-contract-compatibility.js.map +1 -1
  32. package/dist/release-manifest.d.ts +1 -0
  33. package/dist/release-manifest.d.ts.map +1 -1
  34. package/dist/release-manifest.js +7 -0
  35. package/dist/release-manifest.js.map +1 -1
  36. package/dist/rendered-content.d.ts +18 -0
  37. package/dist/rendered-content.d.ts.map +1 -0
  38. package/dist/rendered-content.js +80 -0
  39. package/dist/rendered-content.js.map +1 -0
  40. package/dist/source-safety-contracts.d.ts +1 -1
  41. package/dist/source-safety-contracts.d.ts.map +1 -1
  42. package/dist/source-safety-contracts.js +5 -1
  43. package/dist/source-safety-contracts.js.map +1 -1
  44. package/package.json +19 -2
@@ -7,6 +7,8 @@ import { createHash } from "node:crypto";
7
7
  import { extname, join, relative } from "node:path";
8
8
  import * as ts from "typescript";
9
9
  export const BRANDLAB_CONFIG_V3_RULESET_ID = "project-config-v3@1.0.0-rc.2";
10
+ export const BRANDLAB_CONFIG_V4_RULESET_ID = "project-config-v4@1.0.0-rc.12";
11
+ export const BRANDLAB_CONFIG_V5_RULESET_ID = "project-config-v5@1.0.0-rc.19";
10
12
  export const BRANDLAB_CONFIG_V3_PROFILES = [
11
13
  "next-local-service",
12
14
  "next-root-level",
@@ -32,6 +34,8 @@ const LOCATION_STRATEGIES = new Set([
32
34
  "multi-location",
33
35
  ]);
34
36
  const REVIEWS_PROVIDERS = new Set(["google-places", "project-owned"]);
37
+ const GOOGLE_REVIEWS_MODES = new Set(["link-only", "summary", "full"]);
38
+ const MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT = 100_000;
35
39
  const ROOT_CONFIG_KEYS = new Set([
36
40
  "projectId",
37
41
  "profile",
@@ -181,6 +185,54 @@ const FORBIDDEN_CANDIDATE_EXPORTS = [
181
185
  },
182
186
  ];
183
187
  const PROTECTED_LOCAL_PATTERNS = [
188
+ {
189
+ ruleId: "brandlab-config-v3-circular-business-evidence",
190
+ minimumContract: "project-config-v5",
191
+ surface: "business",
192
+ pattern: /\b(?:existing-production-config|repository-config|source-code):/i,
193
+ expected: "Verified business facts cite independent evidence, not the repository value being verified.",
194
+ remediation: "Use structured official-registry, GBP, client-document, first-party-record or public-authority evidence; otherwise mark the fact unknown.",
195
+ },
196
+ {
197
+ ruleId: "brandlab-config-v3-service-area-public-address",
198
+ minimumContract: "project-config-v5",
199
+ surface: "business",
200
+ pattern: /\bmodel\s*:\s*["']service-area["'][\s\S]{0,1200}\baddressVisibility\s*:\s*["']public["']|\baddressVisibility\s*:\s*["']public["'][\s\S]{0,1200}\bmodel\s*:\s*["']service-area["']/i,
201
+ expected: "Customer-location-only service-area businesses keep address facts hidden.",
202
+ remediation: "Use model service-area, addressVisibility hidden and an unknown or not-applicable address fact.",
203
+ },
204
+ {
205
+ ruleId: "brandlab-config-v3-synthetic-review-proof",
206
+ minimumContract: "project-config-v5",
207
+ surface: "location-proof",
208
+ pattern: /\bregion-review:[^"'`\n]*[\s\S]{0,2400}\b(?:review\?\.\w+\s*\|\||trust\s+\w+|customer feedback across)\b/i,
209
+ expected: "Review proof exists only when an approved review record is bound to business.proof.",
210
+ remediation: "Remove fallback review proof and use verified review evidence or no review proof.",
211
+ },
212
+ {
213
+ ruleId: "brandlab-config-v3-local-programmatic-proof-derivation",
214
+ minimumContract: "project-config-v5",
215
+ surface: "location-proof",
216
+ pattern: /\bfunction\s+getProofLevelFromAssetIds\s*\(/,
217
+ expected: "Programmatic proof levels are derived by @brandlab-digital/site-core/locations.",
218
+ remediation: "Remove the project-local proof classifier and call defineLocationRouteQuality with business.proof.",
219
+ },
220
+ {
221
+ ruleId: "brandlab-config-v3-hardcoded-search-demand",
222
+ minimumContract: "project-config-v5",
223
+ surface: "location-quality",
224
+ pattern: /\bsearchDemand\s*:\s*["']medium["']/,
225
+ expected: "Search demand is observed from evidence or explicitly unknown.",
226
+ remediation: "Replace the assumed medium value with unknown or an evidence-backed observed record.",
227
+ },
228
+ {
229
+ ruleId: "brandlab-config-v3-hidden-focusable-content",
230
+ minimumContract: "project-config-v5",
231
+ surface: "accessibility",
232
+ pattern: /\baria-hidden\s*=\s*(?:\{\s*true\s*\}|["']true["'])[\s\S]{0,1200}<\s*(?:a|button|input|select|textarea)\b/i,
233
+ expected: "Collapsed or aria-hidden content contains no focusable controls.",
234
+ remediation: "Remove collapsed controls from the DOM or make every descendant unfocusable until the content is visible.",
235
+ },
184
236
  {
185
237
  ruleId: "brandlab-config-v3-local-contact-transport",
186
238
  surface: "contact",
@@ -193,6 +245,34 @@ const PROTECTED_LOCAL_PATTERNS = [
193
245
  pattern: /\b(?:class|function|const|let|var)\s+(?:ContactFormEmail|renderContactEmail)\b/,
194
246
  expected: "Contact email rendering is owned by @brandlab-digital/site-next/contact/email.",
195
247
  },
248
+ {
249
+ ruleId: "brandlab-config-v3-local-google-reviews-fetch",
250
+ minimumContract: "project-config-v4",
251
+ surface: "google-reviews",
252
+ pattern: /\bplaces\.googleapis\.com\/v1\/places\b/,
253
+ expected: "Google Places fetching is owned by @brandlab-digital/site-next/google-reviews/server.",
254
+ },
255
+ {
256
+ ruleId: "brandlab-config-v3-local-google-reviews-renderer",
257
+ minimumContract: "project-config-v4",
258
+ surface: "google-reviews",
259
+ pattern: /@brandlab-digital\/site-core\/google-reviews[\s\S]{0,12000}\breviews\.map\s*\(/,
260
+ expected: "Google review content and attribution are rendered by the shared policy-complete UI.",
261
+ },
262
+ {
263
+ ruleId: "brandlab-config-v3-hardcoded-google-review-total",
264
+ minimumContract: "project-config-v4",
265
+ surface: "google-reviews",
266
+ pattern: /["'`][^"'`\n]{0,100}\b\d{1,6}\s+Google\s+(?:reviews?|ratings?)\b/i,
267
+ expected: "Rendered Google rating and review totals come from a live no-store shared snapshot.",
268
+ },
269
+ {
270
+ ruleId: "brandlab-config-v3-public-google-review-cache",
271
+ minimumContract: "project-config-v4",
272
+ surface: "google-reviews",
273
+ pattern: /\b(?:googleReviews?|userRatingCount)\b[\s\S]{0,2000}\b(?:s-maxage|stale-while-revalidate)\b|\b(?:s-maxage|stale-while-revalidate)\b[\s\S]{0,2000}\b(?:googleReviews?|userRatingCount)\b/i,
274
+ expected: "Google Places content is returned with private no-store headers and is never publicly cached.",
275
+ },
196
276
  {
197
277
  ruleId: "brandlab-config-v3-local-lead-event",
198
278
  surface: "analytics",
@@ -207,7 +287,8 @@ const PROTECTED_LOCAL_PATTERNS = [
207
287
  expected: "Breadcrumb labels and hierarchy derive from the semantic route catalog.",
208
288
  },
209
289
  ];
210
- export function inspectBrandlabConfigV3(root) {
290
+ export function inspectBrandlabConfigV3(root, options = {}) {
291
+ const contractVersion = options.contractVersion ?? "project-config-v5";
211
292
  const issues = [];
212
293
  const configPath = join(root, "brandlab.config.ts");
213
294
  const usage = collectProjectImports(root, issues);
@@ -280,11 +361,11 @@ export function inspectBrandlabConfigV3(root) {
280
361
  }));
281
362
  return { ...usage, config: null, issues };
282
363
  }
283
- const config = validateConfigValue(value, issues);
364
+ const config = validateConfigValue(value, issues, contractVersion);
284
365
  if (config) {
285
- addImportContractIssues(root, config, usage, issues);
366
+ addImportContractIssues(root, config, usage, issues, contractVersion);
286
367
  }
287
- addProtectedImplementationIssues(root, issues);
368
+ addProtectedImplementationIssues(root, issues, contractVersion);
288
369
  return {
289
370
  config,
290
371
  imports,
@@ -382,7 +463,7 @@ function propertyName(name) {
382
463
  }
383
464
  return null;
384
465
  }
385
- function validateConfigValue(value, issues) {
466
+ function validateConfigValue(value, issues, contractVersion) {
386
467
  if (!isRecord(value)) {
387
468
  issues.push(configIssue({
388
469
  ruleId: "brandlab-config-v3-object",
@@ -537,7 +618,7 @@ function validateConfigValue(value, issues) {
537
618
  }));
538
619
  }
539
620
  else {
540
- validateCapabilities(capabilities, issues);
621
+ validateCapabilities(capabilities, issues, contractVersion);
541
622
  }
542
623
  validateSitemap(value.sitemap, issues);
543
624
  validateExceptions(value.exceptions, issues);
@@ -549,6 +630,13 @@ function validateConfigValue(value, issues) {
549
630
  const locations = isRecord(safeCapabilities.locations) ? safeCapabilities.locations : null;
550
631
  const reviews = isRecord(safeCapabilities.reviews) ? safeCapabilities.reviews : null;
551
632
  const reviewsProvider = typeof reviews?.provider === "string" ? reviews.provider : null;
633
+ const reviewsMode = typeof reviews?.mode === "string" ? reviews.mode : null;
634
+ const reviewsRouteIds = Array.isArray(reviews?.routeIds)
635
+ ? reviews.routeIds.filter((entry) => typeof entry === "string")
636
+ : [];
637
+ const reviewsMonthlyRequestLimit = typeof reviews?.monthlyRequestLimit === "number"
638
+ ? reviews.monthlyRequestLimit
639
+ : null;
552
640
  if (reviewsProvider === "google-places" && !families.includes("legal")) {
553
641
  issues.push(configIssue({
554
642
  ruleId: "brandlab-config-v3-google-places-legal-family",
@@ -571,6 +659,9 @@ function validateConfigValue(value, issues) {
571
659
  contactEnabled: contact !== null,
572
660
  locationStrategy: typeof locations?.strategy === "string" ? locations.strategy : null,
573
661
  reviewsProvider,
662
+ reviewsMode,
663
+ reviewsRouteIds,
664
+ reviewsMonthlyRequestLimit,
574
665
  },
575
666
  };
576
667
  }
@@ -647,7 +738,7 @@ function validateAliases(value, issues) {
647
738
  }));
648
739
  }
649
740
  }
650
- function validateCapabilities(capabilities, issues) {
741
+ function validateCapabilities(capabilities, issues, contractVersion) {
651
742
  addUnknownKeyIssues(capabilities, new Set(["blog", "contact", "locations", "reviews"]), new Set(), "capabilities", issues);
652
743
  if (capabilities.blog !== undefined) {
653
744
  if (!isRecord(capabilities.blog)) {
@@ -717,7 +808,10 @@ function validateCapabilities(capabilities, issues) {
717
808
  addShapeIssue("reviews", issues);
718
809
  }
719
810
  else {
720
- addUnknownKeyIssues(capabilities.reviews, new Set(["provider"]), new Set(), "capabilities.reviews", issues);
811
+ addUnknownKeyIssues(capabilities.reviews, capabilities.reviews.provider === "google-places" &&
812
+ contractVersion !== "project-config-v3"
813
+ ? new Set(["provider", "mode", "routeIds", "monthlyRequestLimit"])
814
+ : new Set(["provider"]), new Set(), "capabilities.reviews", issues);
721
815
  if (typeof capabilities.reviews.provider !== "string" ||
722
816
  !REVIEWS_PROVIDERS.has(capabilities.reviews.provider)) {
723
817
  issues.push(configIssue({
@@ -728,6 +822,64 @@ function validateCapabilities(capabilities, issues) {
728
822
  path: "brandlab.config.ts",
729
823
  }));
730
824
  }
825
+ if (capabilities.reviews.provider === "google-places" &&
826
+ contractVersion !== "project-config-v3") {
827
+ if (typeof capabilities.reviews.mode !== "string" ||
828
+ !GOOGLE_REVIEWS_MODES.has(capabilities.reviews.mode)) {
829
+ issues.push(configIssue({
830
+ ruleId: "brandlab-config-v3-google-reviews-mode",
831
+ expected: `One Google reviews mode: ${[...GOOGLE_REVIEWS_MODES].join(", ")}.`,
832
+ actual: `reviews.mode is ${JSON.stringify(capabilities.reviews.mode)}.`,
833
+ remediation: "Select link-only, summary or full explicitly.",
834
+ path: "brandlab.config.ts",
835
+ }));
836
+ }
837
+ const routeIds = Array.isArray(capabilities.reviews.routeIds)
838
+ ? capabilities.reviews.routeIds.filter((entry) => typeof entry === "string")
839
+ : [];
840
+ if (!Array.isArray(capabilities.reviews.routeIds) ||
841
+ routeIds.length === 0 ||
842
+ routeIds.length !== capabilities.reviews.routeIds.length ||
843
+ routeIds.some((routeId) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeId))) {
844
+ issues.push(configIssue({
845
+ ruleId: "brandlab-config-v3-google-reviews-routes",
846
+ expected: "At least one unique lowercase kebab-case review placement route ID.",
847
+ actual: `reviews.routeIds is ${JSON.stringify(capabilities.reviews.routeIds)}.`,
848
+ remediation: "Declare only the route IDs where Google review proof is intentionally displayed.",
849
+ path: "brandlab.config.ts",
850
+ }));
851
+ }
852
+ else {
853
+ addDuplicateIssue(routeIds, "brandlab-config-v3-google-reviews-route-duplicate", "reviews.routeIds", issues);
854
+ }
855
+ if (capabilities.reviews.mode === "link-only") {
856
+ if (capabilities.reviews.monthlyRequestLimit !== undefined) {
857
+ issues.push(configIssue({
858
+ ruleId: "brandlab-config-v3-google-reviews-link-only-quota",
859
+ expected: "No API request limit for link-only review proof.",
860
+ actual: "reviews.monthlyRequestLimit is configured for link-only mode.",
861
+ remediation: "Remove monthlyRequestLimit because link-only mode never calls Places.",
862
+ path: "brandlab.config.ts",
863
+ }));
864
+ }
865
+ }
866
+ else if (capabilities.reviews.mode === "summary" ||
867
+ capabilities.reviews.mode === "full") {
868
+ const limit = capabilities.reviews.monthlyRequestLimit;
869
+ if (typeof limit !== "number" ||
870
+ !Number.isSafeInteger(limit) ||
871
+ limit < 1 ||
872
+ limit > MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT) {
873
+ issues.push(configIssue({
874
+ ruleId: "brandlab-config-v3-google-reviews-quota",
875
+ expected: `monthlyRequestLimit from 1 to ${MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT}.`,
876
+ actual: `reviews.monthlyRequestLimit is ${JSON.stringify(limit)}.`,
877
+ remediation: "Set an explicit project-level monthly request ceiling.",
878
+ path: "brandlab.config.ts",
879
+ }));
880
+ }
881
+ }
882
+ }
731
883
  }
732
884
  }
733
885
  }
@@ -905,7 +1057,7 @@ function isIsoDate(value) {
905
1057
  return false;
906
1058
  return !Number.isNaN(Date.parse(value));
907
1059
  }
908
- function addImportContractIssues(root, config, usage, issues) {
1060
+ function addImportContractIssues(root, config, usage, issues, contractVersion) {
909
1061
  if (config.profile === "legacy-php") {
910
1062
  const nextEvidence = detectNextPackageBackedEvidence(root, usage);
911
1063
  if (nextEvidence.length === 0)
@@ -984,16 +1136,37 @@ function addImportContractIssues(root, config, usage, issues) {
984
1136
  }, usage, issues);
985
1137
  }
986
1138
  if (config.capabilities.reviewsProvider === "google-places") {
987
- requireStrictUsage({
988
- moduleName: "@brandlab-digital/site-next/google-reviews",
989
- surface: "google-reviews-route",
990
- exports: ["createGooglePlaceReviewsRouteResponse"],
991
- }, usage, issues);
992
- requireStrictUsage({
993
- moduleName: "@brandlab-digital/site-next/google-reviews",
994
- surface: "google-reviews-display",
995
- exports: ["GooglePlaceReviews"],
996
- }, usage, issues);
1139
+ if (contractVersion === "project-config-v3") {
1140
+ requireStrictUsage({
1141
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1142
+ surface: "google-reviews-route",
1143
+ exports: ["createGooglePlaceReviewsRouteResponse"],
1144
+ }, usage, issues);
1145
+ requireStrictUsage({
1146
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1147
+ surface: "google-reviews-display",
1148
+ exports: ["GooglePlaceReviews"],
1149
+ }, usage, issues);
1150
+ }
1151
+ else if (config.capabilities.reviewsMode === "link-only") {
1152
+ requireStrictUsage({
1153
+ moduleName: "@brandlab-digital/site-next/google-reviews/ui",
1154
+ surface: "google-reviews-link",
1155
+ exports: ["GoogleReviewsLink"],
1156
+ }, usage, issues);
1157
+ }
1158
+ else {
1159
+ requireStrictUsage({
1160
+ moduleName: "@brandlab-digital/site-next/google-reviews/server",
1161
+ surface: "google-reviews-production",
1162
+ exports: ["createBrandlabGooglePlaceReviewsProductionRuntime"],
1163
+ }, usage, issues);
1164
+ requireStrictUsage({
1165
+ moduleName: "@brandlab-digital/site-next/google-reviews/client",
1166
+ surface: "google-reviews-display",
1167
+ exports: ["GooglePlaceReviewsClient"],
1168
+ }, usage, issues);
1169
+ }
997
1170
  }
998
1171
  else if (config.capabilities.reviewsProvider === "project-owned") {
999
1172
  requireStrictUsage({
@@ -1002,7 +1175,7 @@ function addImportContractIssues(root, config, usage, issues) {
1002
1175
  exports: ["preparePublicReviews"],
1003
1176
  }, usage, issues);
1004
1177
  }
1005
- else if (usage.imports.has("@brandlab-digital/site-next/google-reviews")) {
1178
+ else if ([...usage.imports].some((name) => name.startsWith("@brandlab-digital/site-next/google-reviews"))) {
1006
1179
  issues.push(configIssue({
1007
1180
  ruleId: "brandlab-config-v3-google-reviews-capability",
1008
1181
  surface: "google-reviews",
@@ -2752,11 +2925,14 @@ function isRuntimeIdentifierReference(identifier) {
2752
2925
  }
2753
2926
  return true;
2754
2927
  }
2755
- function addProtectedImplementationIssues(root, issues) {
2928
+ function addProtectedImplementationIssues(root, issues, contractVersion) {
2756
2929
  for (const file of walkProjectRuntimeFiles(root)) {
2757
2930
  const relativePath = normalizePath(relative(root, file));
2758
2931
  const source = readFileSync(file, "utf8");
2759
2932
  for (const rule of PROTECTED_LOCAL_PATTERNS) {
2933
+ if ("minimumContract" in rule &&
2934
+ !configurationContractIncludes(contractVersion, rule.minimumContract))
2935
+ continue;
2760
2936
  const match = rule.pattern.exec(source);
2761
2937
  if (!match || match.index === undefined)
2762
2938
  continue;
@@ -2766,9 +2942,11 @@ function addProtectedImplementationIssues(root, issues) {
2766
2942
  surface: rule.surface,
2767
2943
  expected: rule.expected,
2768
2944
  actual: `A protected local implementation was found in ${relativePath}.`,
2769
- remediation: rule.ruleId === "brandlab-config-v3-url-derived-breadcrumbs"
2770
- ? "Use BrandlabRouteBreadcrumbs with the typed route catalog; keep path labels only as a development fallback."
2771
- : "Keep only the thin project binding and consume the shared runtime.",
2945
+ remediation: "remediation" in rule
2946
+ ? rule.remediation
2947
+ : rule.ruleId === "brandlab-config-v3-url-derived-breadcrumbs"
2948
+ ? "Use BrandlabRouteBreadcrumbs with the typed route catalog; keep path labels only as a development fallback."
2949
+ : "Keep only the thin project binding and consume the shared runtime.",
2772
2950
  severity: "severity" in rule ? rule.severity : "blocker",
2773
2951
  path: relativePath,
2774
2952
  line,
@@ -2776,6 +2954,14 @@ function addProtectedImplementationIssues(root, issues) {
2776
2954
  }
2777
2955
  }
2778
2956
  }
2957
+ function configurationContractIncludes(actual, minimum) {
2958
+ const order = {
2959
+ "project-config-v3": 3,
2960
+ "project-config-v4": 4,
2961
+ "project-config-v5": 5,
2962
+ };
2963
+ return order[actual] >= order[minimum];
2964
+ }
2779
2965
  function walkSourceFiles(root) {
2780
2966
  if (!existsSync(root))
2781
2967
  return [];