@brandlab-digital/conformance 1.0.0-rc.3 → 1.0.0-rc.30

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 (47) 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 +252 -0
  4. package/dist/analytics-contract.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +91 -20
  8. package/dist/cli.js.map +1 -1
  9. package/dist/project-config-v3.d.ts +8 -1
  10. package/dist/project-config-v3.d.ts.map +1 -1
  11. package/dist/project-config-v3.js +193 -31
  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 +234 -0
  20. package/dist/project-documentation.js.map +1 -0
  21. package/dist/project.d.ts +9 -2
  22. package/dist/project.d.ts.map +1 -1
  23. package/dist/project.js +327 -53
  24. package/dist/project.js.map +1 -1
  25. package/dist/release-catalog.d.ts.map +1 -1
  26. package/dist/release-catalog.js +1785 -0
  27. package/dist/release-catalog.js.map +1 -1
  28. package/dist/release-contract-compatibility.d.ts +10 -57
  29. package/dist/release-contract-compatibility.d.ts.map +1 -1
  30. package/dist/release-contract-compatibility.js +115 -61
  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 +38 -0
  37. package/dist/rendered-content.d.ts.map +1 -0
  38. package/dist/rendered-content.js +149 -0
  39. package/dist/rendered-content.js.map +1 -0
  40. package/dist/source-safety-contracts.d.ts.map +1 -1
  41. package/dist/source-safety-contracts.js +4 -0
  42. package/dist/source-safety-contracts.js.map +1 -1
  43. package/dist/verification-policy.d.ts +99 -0
  44. package/dist/verification-policy.d.ts.map +1 -0
  45. package/dist/verification-policy.js +219 -0
  46. package/dist/verification-policy.js.map +1 -0
  47. package/package.json +20 -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,7 @@ 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"]);
35
38
  const ROOT_CONFIG_KEYS = new Set([
36
39
  "projectId",
37
40
  "profile",
@@ -181,6 +184,54 @@ const FORBIDDEN_CANDIDATE_EXPORTS = [
181
184
  },
182
185
  ];
183
186
  const PROTECTED_LOCAL_PATTERNS = [
187
+ {
188
+ ruleId: "brandlab-config-v3-circular-business-evidence",
189
+ minimumContract: "project-config-v5",
190
+ surface: "business",
191
+ pattern: /\b(?:existing-production-config|repository-config|source-code):/i,
192
+ expected: "Verified business facts cite independent evidence, not the repository value being verified.",
193
+ remediation: "Use structured official-registry, GBP, client-document, first-party-record or public-authority evidence; otherwise mark the fact unknown.",
194
+ },
195
+ {
196
+ ruleId: "brandlab-config-v3-service-area-public-address",
197
+ minimumContract: "project-config-v5",
198
+ surface: "business",
199
+ 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,
200
+ expected: "Customer-location-only service-area businesses keep address facts hidden.",
201
+ remediation: "Use model service-area, addressVisibility hidden and an unknown or not-applicable address fact.",
202
+ },
203
+ {
204
+ ruleId: "brandlab-config-v3-synthetic-review-proof",
205
+ minimumContract: "project-config-v5",
206
+ surface: "location-proof",
207
+ pattern: /\bregion-review:[^"'`\n]*[\s\S]{0,2400}\b(?:review\?\.\w+\s*\|\||trust\s+\w+|customer feedback across)\b/i,
208
+ expected: "Review proof exists only when an approved review record is bound to business.proof.",
209
+ remediation: "Remove fallback review proof and use verified review evidence or no review proof.",
210
+ },
211
+ {
212
+ ruleId: "brandlab-config-v3-local-programmatic-proof-derivation",
213
+ minimumContract: "project-config-v5",
214
+ surface: "location-proof",
215
+ pattern: /\bfunction\s+getProofLevelFromAssetIds\s*\(/,
216
+ expected: "Programmatic proof levels are derived by @brandlab-digital/site-core/locations.",
217
+ remediation: "Remove the project-local proof classifier and declare an explicit defineLocationRouteDecision result.",
218
+ },
219
+ {
220
+ ruleId: "brandlab-config-v3-hardcoded-search-demand",
221
+ minimumContract: "project-config-v5",
222
+ surface: "location-quality",
223
+ pattern: /\bsearchDemand\s*:\s*["']medium["']/,
224
+ expected: "Search demand is observed from evidence or explicitly unknown.",
225
+ remediation: "Replace the assumed medium value with unknown or an evidence-backed observed record.",
226
+ },
227
+ {
228
+ ruleId: "brandlab-config-v3-hidden-focusable-content",
229
+ minimumContract: "project-config-v5",
230
+ surface: "accessibility",
231
+ pattern: /\baria-hidden\s*=\s*(?:\{\s*true\s*\}|["']true["'])[\s\S]{0,1200}<\s*(?:a|button|input|select|textarea)\b/i,
232
+ expected: "Collapsed or aria-hidden content contains no focusable controls.",
233
+ remediation: "Remove collapsed controls from the DOM or make every descendant unfocusable until the content is visible.",
234
+ },
184
235
  {
185
236
  ruleId: "brandlab-config-v3-local-contact-transport",
186
237
  surface: "contact",
@@ -191,7 +242,35 @@ const PROTECTED_LOCAL_PATTERNS = [
191
242
  ruleId: "brandlab-config-v3-local-contact-email",
192
243
  surface: "contact",
193
244
  pattern: /\b(?:class|function|const|let|var)\s+(?:ContactFormEmail|renderContactEmail)\b/,
194
- expected: "Contact email rendering is owned by @brandlab-digital/site-next/contact/email.",
245
+ expected: "Contact email rendering is owned by @brandlab-digital/site-next/contact/server.",
246
+ },
247
+ {
248
+ ruleId: "brandlab-config-v3-local-google-reviews-fetch",
249
+ minimumContract: "project-config-v4",
250
+ surface: "google-reviews",
251
+ pattern: /\bplaces\.googleapis\.com\/v1\/places\b/,
252
+ expected: "Google Places fetching is owned by @brandlab-digital/site-next/google-reviews/server.",
253
+ },
254
+ {
255
+ ruleId: "brandlab-config-v3-local-google-reviews-renderer",
256
+ minimumContract: "project-config-v4",
257
+ surface: "google-reviews",
258
+ pattern: /@brandlab-digital\/site-core\/google-reviews[\s\S]{0,12000}\breviews\.map\s*\(/,
259
+ expected: "Google review content and attribution are rendered by the shared policy-complete UI.",
260
+ },
261
+ {
262
+ ruleId: "brandlab-config-v3-hardcoded-google-review-total",
263
+ minimumContract: "project-config-v4",
264
+ surface: "google-reviews",
265
+ pattern: /["'`][^"'`\n]{0,100}\b\d{1,6}\s+Google\s+(?:reviews?|ratings?)\b/i,
266
+ expected: "Rendered Google rating and review totals come from a live no-store shared snapshot.",
267
+ },
268
+ {
269
+ ruleId: "brandlab-config-v3-public-google-review-cache",
270
+ minimumContract: "project-config-v4",
271
+ surface: "google-reviews",
272
+ 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,
273
+ expected: "Google Places content is returned with private no-store headers and is never publicly cached.",
195
274
  },
196
275
  {
197
276
  ruleId: "brandlab-config-v3-local-lead-event",
@@ -207,7 +286,8 @@ const PROTECTED_LOCAL_PATTERNS = [
207
286
  expected: "Breadcrumb labels and hierarchy derive from the semantic route catalog.",
208
287
  },
209
288
  ];
210
- export function inspectBrandlabConfigV3(root) {
289
+ export function inspectBrandlabConfigV3(root, options = {}) {
290
+ const contractVersion = options.contractVersion ?? "project-config-v5";
211
291
  const issues = [];
212
292
  const configPath = join(root, "brandlab.config.ts");
213
293
  const usage = collectProjectImports(root, issues);
@@ -280,11 +360,11 @@ export function inspectBrandlabConfigV3(root) {
280
360
  }));
281
361
  return { ...usage, config: null, issues };
282
362
  }
283
- const config = validateConfigValue(value, issues);
363
+ const config = validateConfigValue(value, issues, contractVersion);
284
364
  if (config) {
285
- addImportContractIssues(root, config, usage, issues);
365
+ addImportContractIssues(root, config, usage, issues, contractVersion);
286
366
  }
287
- addProtectedImplementationIssues(root, issues);
367
+ addProtectedImplementationIssues(root, issues, contractVersion);
288
368
  return {
289
369
  config,
290
370
  imports,
@@ -382,7 +462,7 @@ function propertyName(name) {
382
462
  }
383
463
  return null;
384
464
  }
385
- function validateConfigValue(value, issues) {
465
+ function validateConfigValue(value, issues, contractVersion) {
386
466
  if (!isRecord(value)) {
387
467
  issues.push(configIssue({
388
468
  ruleId: "brandlab-config-v3-object",
@@ -537,7 +617,7 @@ function validateConfigValue(value, issues) {
537
617
  }));
538
618
  }
539
619
  else {
540
- validateCapabilities(capabilities, issues);
620
+ validateCapabilities(capabilities, issues, contractVersion);
541
621
  }
542
622
  validateSitemap(value.sitemap, issues);
543
623
  validateExceptions(value.exceptions, issues);
@@ -549,6 +629,10 @@ function validateConfigValue(value, issues) {
549
629
  const locations = isRecord(safeCapabilities.locations) ? safeCapabilities.locations : null;
550
630
  const reviews = isRecord(safeCapabilities.reviews) ? safeCapabilities.reviews : null;
551
631
  const reviewsProvider = typeof reviews?.provider === "string" ? reviews.provider : null;
632
+ const reviewsMode = typeof reviews?.mode === "string" ? reviews.mode : null;
633
+ const reviewsRouteIds = Array.isArray(reviews?.routeIds)
634
+ ? reviews.routeIds.filter((entry) => typeof entry === "string")
635
+ : [];
552
636
  if (reviewsProvider === "google-places" && !families.includes("legal")) {
553
637
  issues.push(configIssue({
554
638
  ruleId: "brandlab-config-v3-google-places-legal-family",
@@ -571,6 +655,8 @@ function validateConfigValue(value, issues) {
571
655
  contactEnabled: contact !== null,
572
656
  locationStrategy: typeof locations?.strategy === "string" ? locations.strategy : null,
573
657
  reviewsProvider,
658
+ reviewsMode,
659
+ reviewsRouteIds,
574
660
  },
575
661
  };
576
662
  }
@@ -647,7 +733,7 @@ function validateAliases(value, issues) {
647
733
  }));
648
734
  }
649
735
  }
650
- function validateCapabilities(capabilities, issues) {
736
+ function validateCapabilities(capabilities, issues, contractVersion) {
651
737
  addUnknownKeyIssues(capabilities, new Set(["blog", "contact", "locations", "reviews"]), new Set(), "capabilities", issues);
652
738
  if (capabilities.blog !== undefined) {
653
739
  if (!isRecord(capabilities.blog)) {
@@ -717,7 +803,10 @@ function validateCapabilities(capabilities, issues) {
717
803
  addShapeIssue("reviews", issues);
718
804
  }
719
805
  else {
720
- addUnknownKeyIssues(capabilities.reviews, new Set(["provider"]), new Set(), "capabilities.reviews", issues);
806
+ addUnknownKeyIssues(capabilities.reviews, capabilities.reviews.provider === "google-places" &&
807
+ contractVersion !== "project-config-v3"
808
+ ? new Set(["provider", "mode", "routeIds", "monthlyRequestLimit"])
809
+ : new Set(["provider"]), new Set(), "capabilities.reviews", issues);
721
810
  if (typeof capabilities.reviews.provider !== "string" ||
722
811
  !REVIEWS_PROVIDERS.has(capabilities.reviews.provider)) {
723
812
  issues.push(configIssue({
@@ -728,6 +817,37 @@ function validateCapabilities(capabilities, issues) {
728
817
  path: "brandlab.config.ts",
729
818
  }));
730
819
  }
820
+ if (capabilities.reviews.provider === "google-places" &&
821
+ contractVersion !== "project-config-v3") {
822
+ if (typeof capabilities.reviews.mode !== "string" ||
823
+ !GOOGLE_REVIEWS_MODES.has(capabilities.reviews.mode)) {
824
+ issues.push(configIssue({
825
+ ruleId: "brandlab-config-v3-google-reviews-mode",
826
+ expected: `One Google reviews mode: ${[...GOOGLE_REVIEWS_MODES].join(", ")}.`,
827
+ actual: `reviews.mode is ${JSON.stringify(capabilities.reviews.mode)}.`,
828
+ remediation: "Select link-only, summary or full explicitly.",
829
+ path: "brandlab.config.ts",
830
+ }));
831
+ }
832
+ const routeIds = Array.isArray(capabilities.reviews.routeIds)
833
+ ? capabilities.reviews.routeIds.filter((entry) => typeof entry === "string")
834
+ : [];
835
+ if (!Array.isArray(capabilities.reviews.routeIds) ||
836
+ routeIds.length === 0 ||
837
+ routeIds.length !== capabilities.reviews.routeIds.length ||
838
+ routeIds.some((routeId) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeId))) {
839
+ issues.push(configIssue({
840
+ ruleId: "brandlab-config-v3-google-reviews-routes",
841
+ expected: "At least one unique lowercase kebab-case review placement route ID.",
842
+ actual: `reviews.routeIds is ${JSON.stringify(capabilities.reviews.routeIds)}.`,
843
+ remediation: "Declare only the route IDs where Google review proof is intentionally displayed.",
844
+ path: "brandlab.config.ts",
845
+ }));
846
+ }
847
+ else {
848
+ addDuplicateIssue(routeIds, "brandlab-config-v3-google-reviews-route-duplicate", "reviews.routeIds", issues);
849
+ }
850
+ }
731
851
  }
732
852
  }
733
853
  }
@@ -905,7 +1025,7 @@ function isIsoDate(value) {
905
1025
  return false;
906
1026
  return !Number.isNaN(Date.parse(value));
907
1027
  }
908
- function addImportContractIssues(root, config, usage, issues) {
1028
+ function addImportContractIssues(root, config, usage, issues, contractVersion) {
909
1029
  if (config.profile === "legacy-php") {
910
1030
  const nextEvidence = detectNextPackageBackedEvidence(root, usage);
911
1031
  if (nextEvidence.length === 0)
@@ -935,7 +1055,7 @@ function addImportContractIssues(root, config, usage, issues) {
935
1055
  requireStrictUsage({
936
1056
  moduleName: "@brandlab-digital/site-core/locations",
937
1057
  surface: "location-proof",
938
- exports: ["defineLocationRouteQuality"],
1058
+ exports: ["defineLocationRouteDecision", "defineLocationRouteQuality"],
939
1059
  }, usage, issues);
940
1060
  }
941
1061
  const contactEvidence = detectContactSurfaceEvidence(root, usage);
@@ -963,12 +1083,12 @@ function addImportContractIssues(root, config, usage, issues) {
963
1083
  }
964
1084
  if (contactRequired) {
965
1085
  requireStrictUsage({
966
- moduleName: "@brandlab-digital/site-next/contact/production",
1086
+ moduleName: "@brandlab-digital/site-next/contact/server",
967
1087
  surface: "contact-production",
968
- exports: ["createBrandlabProductionContactRuntime"],
1088
+ exports: ["createBrandlabContactRuntime"],
969
1089
  }, usage, issues);
970
1090
  requireStrictUsage({
971
- moduleName: "@brandlab-digital/site-next/contact/client",
1091
+ moduleName: "@brandlab-digital/site-next/contact",
972
1092
  surface: "contact-client",
973
1093
  exports: ["createContactHttpSubmissionController"],
974
1094
  }, usage, issues);
@@ -984,16 +1104,37 @@ function addImportContractIssues(root, config, usage, issues) {
984
1104
  }, usage, issues);
985
1105
  }
986
1106
  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);
1107
+ if (contractVersion === "project-config-v3") {
1108
+ requireStrictUsage({
1109
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1110
+ surface: "google-reviews-route",
1111
+ exports: ["createGooglePlaceReviewsRouteResponse"],
1112
+ }, usage, issues);
1113
+ requireStrictUsage({
1114
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1115
+ surface: "google-reviews-display",
1116
+ exports: ["GooglePlaceReviews"],
1117
+ }, usage, issues);
1118
+ }
1119
+ else if (config.capabilities.reviewsMode === "link-only") {
1120
+ requireStrictUsage({
1121
+ moduleName: "@brandlab-digital/site-next/google-reviews/ui",
1122
+ surface: "google-reviews-link",
1123
+ exports: ["GoogleReviewsLink"],
1124
+ }, usage, issues);
1125
+ }
1126
+ else {
1127
+ requireStrictUsage({
1128
+ moduleName: "@brandlab-digital/site-next/google-reviews/server",
1129
+ surface: "google-reviews-production",
1130
+ exports: ["createBrandlabGooglePlaceReviewsProductionRuntime"],
1131
+ }, usage, issues);
1132
+ requireStrictUsage({
1133
+ moduleName: "@brandlab-digital/site-next/google-reviews/client",
1134
+ surface: "google-reviews-display",
1135
+ exports: ["GooglePlaceReviewsClient"],
1136
+ }, usage, issues);
1137
+ }
997
1138
  }
998
1139
  else if (config.capabilities.reviewsProvider === "project-owned") {
999
1140
  requireStrictUsage({
@@ -1002,7 +1143,7 @@ function addImportContractIssues(root, config, usage, issues) {
1002
1143
  exports: ["preparePublicReviews"],
1003
1144
  }, usage, issues);
1004
1145
  }
1005
- else if (usage.imports.has("@brandlab-digital/site-next/google-reviews")) {
1146
+ else if ([...usage.imports].some((name) => name.startsWith("@brandlab-digital/site-next/google-reviews"))) {
1006
1147
  issues.push(configIssue({
1007
1148
  ruleId: "brandlab-config-v3-google-reviews-capability",
1008
1149
  surface: "google-reviews",
@@ -1069,7 +1210,8 @@ function detectNextPackageBackedEvidence(root, usage) {
1069
1210
  function detectContactSurfaceEvidence(root, usage) {
1070
1211
  const evidence = new Set();
1071
1212
  for (const moduleName of usage.imports) {
1072
- if (moduleName.startsWith("@brandlab-digital/site-next/contact/")) {
1213
+ if (moduleName === "@brandlab-digital/site-next/contact" ||
1214
+ moduleName.startsWith("@brandlab-digital/site-next/contact/")) {
1073
1215
  evidence.add(`shared contact import ${moduleName}`);
1074
1216
  }
1075
1217
  }
@@ -1153,6 +1295,13 @@ function requireStrictUsage(requirement, usage, issues) {
1153
1295
  }));
1154
1296
  }
1155
1297
  }
1298
+ function requireAnyStrictUsage(requirements, usage, issues) {
1299
+ const selected = requirements.find((requirement) => usage.imports.has(requirement.moduleName)) ??
1300
+ requirements[0];
1301
+ if (!selected)
1302
+ return;
1303
+ requireStrictUsage(selected, usage, issues);
1304
+ }
1156
1305
  function frameworkOwnerExpectation(owner) {
1157
1306
  if (owner === "metadata") {
1158
1307
  return "The strict metadata builder reaches metadata or generateMetadata in an App Router page or layout entrypoint.";
@@ -1328,7 +1477,7 @@ function collectProjectImports(root, issues) {
1328
1477
  return;
1329
1478
  executableImports.add(binding.moduleName);
1330
1479
  addExportUsage(executableExports, binding.moduleName, binding.exportName);
1331
- const isSharedContactClientController = binding.moduleName === "@brandlab-digital/site-next/contact/client" &&
1480
+ const isSharedContactClientController = binding.moduleName === "@brandlab-digital/site-next/contact" &&
1332
1481
  binding.exportName === "createContactHttpSubmissionController";
1333
1482
  if (isOutputOwned(node) ||
1334
1483
  (isSharedContactClientController && isContactClientRuntimeOwned(node))) {
@@ -2752,11 +2901,14 @@ function isRuntimeIdentifierReference(identifier) {
2752
2901
  }
2753
2902
  return true;
2754
2903
  }
2755
- function addProtectedImplementationIssues(root, issues) {
2904
+ function addProtectedImplementationIssues(root, issues, contractVersion) {
2756
2905
  for (const file of walkProjectRuntimeFiles(root)) {
2757
2906
  const relativePath = normalizePath(relative(root, file));
2758
2907
  const source = readFileSync(file, "utf8");
2759
2908
  for (const rule of PROTECTED_LOCAL_PATTERNS) {
2909
+ if ("minimumContract" in rule &&
2910
+ !configurationContractIncludes(contractVersion, rule.minimumContract))
2911
+ continue;
2760
2912
  const match = rule.pattern.exec(source);
2761
2913
  if (!match || match.index === undefined)
2762
2914
  continue;
@@ -2766,9 +2918,11 @@ function addProtectedImplementationIssues(root, issues) {
2766
2918
  surface: rule.surface,
2767
2919
  expected: rule.expected,
2768
2920
  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.",
2921
+ remediation: "remediation" in rule
2922
+ ? rule.remediation
2923
+ : rule.ruleId === "brandlab-config-v3-url-derived-breadcrumbs"
2924
+ ? "Use BrandlabRouteBreadcrumbs with the typed route catalog; keep path labels only as a development fallback."
2925
+ : "Keep only the thin project binding and consume the shared runtime.",
2772
2926
  severity: "severity" in rule ? rule.severity : "blocker",
2773
2927
  path: relativePath,
2774
2928
  line,
@@ -2776,6 +2930,14 @@ function addProtectedImplementationIssues(root, issues) {
2776
2930
  }
2777
2931
  }
2778
2932
  }
2933
+ function configurationContractIncludes(actual, minimum) {
2934
+ const order = {
2935
+ "project-config-v3": 3,
2936
+ "project-config-v4": 4,
2937
+ "project-config-v5": 5,
2938
+ };
2939
+ return order[actual] >= order[minimum];
2940
+ }
2779
2941
  function walkSourceFiles(root) {
2780
2942
  if (!existsSync(root))
2781
2943
  return [];