@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.
- package/dist/analytics-contract.d.ts +20 -0
- package/dist/analytics-contract.d.ts.map +1 -0
- package/dist/analytics-contract.js +211 -0
- package/dist/analytics-contract.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -3
- package/dist/cli.js.map +1 -1
- package/dist/project-config-v3.d.ts +9 -1
- package/dist/project-config-v3.d.ts.map +1 -1
- package/dist/project-config-v3.js +210 -24
- 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 +398 -0
- package/dist/project-documentation.js.map +1 -0
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +263 -15
- package/dist/project.js.map +1 -1
- package/dist/release-catalog.d.ts.map +1 -1
- package/dist/release-catalog.js +1356 -0
- package/dist/release-catalog.js.map +1 -1
- package/dist/release-contract-compatibility.d.ts +126 -0
- package/dist/release-contract-compatibility.d.ts.map +1 -1
- package/dist/release-contract-compatibility.js +126 -0
- 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 +18 -0
- package/dist/rendered-content.d.ts.map +1 -0
- package/dist/rendered-content.js +80 -0
- package/dist/rendered-content.js.map +1 -0
- package/dist/source-safety-contracts.d.ts +1 -1
- package/dist/source-safety-contracts.d.ts.map +1 -1
- package/dist/source-safety-contracts.js +5 -1
- package/dist/source-safety-contracts.js.map +1 -1
- 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,
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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.
|
|
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:
|
|
2770
|
-
?
|
|
2771
|
-
:
|
|
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 [];
|