@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.
- package/dist/analytics-contract.d.ts +20 -0
- package/dist/analytics-contract.d.ts.map +1 -0
- package/dist/analytics-contract.js +252 -0
- package/dist/analytics-contract.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -20
- package/dist/cli.js.map +1 -1
- package/dist/project-config-v3.d.ts +8 -1
- package/dist/project-config-v3.d.ts.map +1 -1
- package/dist/project-config-v3.js +193 -31
- package/dist/project-config-v3.js.map +1 -1
- package/dist/project-config.d.ts +1 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +1 -1
- package/dist/project-config.js.map +1 -1
- package/dist/project-documentation.d.ts +28 -0
- package/dist/project-documentation.d.ts.map +1 -0
- package/dist/project-documentation.js +234 -0
- package/dist/project-documentation.js.map +1 -0
- package/dist/project.d.ts +9 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +327 -53
- package/dist/project.js.map +1 -1
- package/dist/release-catalog.d.ts.map +1 -1
- package/dist/release-catalog.js +1785 -0
- package/dist/release-catalog.js.map +1 -1
- package/dist/release-contract-compatibility.d.ts +10 -57
- package/dist/release-contract-compatibility.d.ts.map +1 -1
- package/dist/release-contract-compatibility.js +115 -61
- package/dist/release-contract-compatibility.js.map +1 -1
- package/dist/release-manifest.d.ts +1 -0
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +7 -0
- package/dist/release-manifest.js.map +1 -1
- package/dist/rendered-content.d.ts +38 -0
- package/dist/rendered-content.d.ts.map +1 -0
- package/dist/rendered-content.js +149 -0
- package/dist/rendered-content.js.map +1 -0
- package/dist/source-safety-contracts.d.ts.map +1 -1
- package/dist/source-safety-contracts.js +4 -0
- package/dist/source-safety-contracts.js.map +1 -1
- package/dist/verification-policy.d.ts +99 -0
- package/dist/verification-policy.d.ts.map +1 -0
- package/dist/verification-policy.js +219 -0
- package/dist/verification-policy.js.map +1 -0
- 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/
|
|
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,
|
|
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/
|
|
1086
|
+
moduleName: "@brandlab-digital/site-next/contact/server",
|
|
967
1087
|
surface: "contact-production",
|
|
968
|
-
exports: ["
|
|
1088
|
+
exports: ["createBrandlabContactRuntime"],
|
|
969
1089
|
}, usage, issues);
|
|
970
1090
|
requireStrictUsage({
|
|
971
|
-
moduleName: "@brandlab-digital/site-next/contact
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
2770
|
-
?
|
|
2771
|
-
:
|
|
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 [];
|