@brandlab-digital/conformance 1.0.0-rc.2 → 1.0.0-rc.21
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 +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 +9 -1
- package/dist/project-config-v3.d.ts.map +1 -1
- package/dist/project-config-v3.js +246 -37
- 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 +9 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +290 -44
- package/dist/project.js.map +1 -1
- package/dist/release-catalog.d.ts.map +1 -1
- package/dist/release-catalog.js +1405 -0
- package/dist/release-catalog.js.map +1 -1
- package/dist/release-contract-compatibility.d.ts +133 -0
- package/dist/release-contract-compatibility.d.ts.map +1 -1
- package/dist/release-contract-compatibility.js +133 -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 +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 +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/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,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 declare an explicit defineLocationRouteDecision result.",
|
|
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)
|
|
@@ -935,7 +1087,7 @@ function addImportContractIssues(root, config, usage, issues) {
|
|
|
935
1087
|
requireStrictUsage({
|
|
936
1088
|
moduleName: "@brandlab-digital/site-core/locations",
|
|
937
1089
|
surface: "location-proof",
|
|
938
|
-
exports: ["defineLocationRouteQuality"],
|
|
1090
|
+
exports: ["defineLocationRouteDecision", "defineLocationRouteQuality"],
|
|
939
1091
|
}, usage, issues);
|
|
940
1092
|
}
|
|
941
1093
|
const contactEvidence = detectContactSurfaceEvidence(root, usage);
|
|
@@ -962,16 +1114,30 @@ function addImportContractIssues(root, config, usage, issues) {
|
|
|
962
1114
|
}));
|
|
963
1115
|
}
|
|
964
1116
|
if (contactRequired) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1117
|
+
requireAnyStrictUsage([
|
|
1118
|
+
{
|
|
1119
|
+
moduleName: "@brandlab-digital/site-next/contact/server",
|
|
1120
|
+
surface: "contact-production",
|
|
1121
|
+
exports: ["createBrandlabProductionContactRuntime"],
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
moduleName: "@brandlab-digital/site-next/contact/production",
|
|
1125
|
+
surface: "contact-production",
|
|
1126
|
+
exports: ["createBrandlabProductionContactRuntime"],
|
|
1127
|
+
},
|
|
1128
|
+
], usage, issues);
|
|
1129
|
+
requireAnyStrictUsage([
|
|
1130
|
+
{
|
|
1131
|
+
moduleName: "@brandlab-digital/site-next/contact",
|
|
1132
|
+
surface: "contact-client",
|
|
1133
|
+
exports: ["createContactHttpSubmissionController"],
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
moduleName: "@brandlab-digital/site-next/contact/client",
|
|
1137
|
+
surface: "contact-client",
|
|
1138
|
+
exports: ["createContactHttpSubmissionController"],
|
|
1139
|
+
},
|
|
1140
|
+
], usage, issues);
|
|
975
1141
|
}
|
|
976
1142
|
if (config.capabilities.blogAdapter && config.capabilities.blogAdapter !== "none") {
|
|
977
1143
|
const adapterExport = config.capabilities.blogAdapter === "mdx"
|
|
@@ -984,16 +1150,37 @@ function addImportContractIssues(root, config, usage, issues) {
|
|
|
984
1150
|
}, usage, issues);
|
|
985
1151
|
}
|
|
986
1152
|
if (config.capabilities.reviewsProvider === "google-places") {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1153
|
+
if (contractVersion === "project-config-v3") {
|
|
1154
|
+
requireStrictUsage({
|
|
1155
|
+
moduleName: "@brandlab-digital/site-next/google-reviews",
|
|
1156
|
+
surface: "google-reviews-route",
|
|
1157
|
+
exports: ["createGooglePlaceReviewsRouteResponse"],
|
|
1158
|
+
}, usage, issues);
|
|
1159
|
+
requireStrictUsage({
|
|
1160
|
+
moduleName: "@brandlab-digital/site-next/google-reviews",
|
|
1161
|
+
surface: "google-reviews-display",
|
|
1162
|
+
exports: ["GooglePlaceReviews"],
|
|
1163
|
+
}, usage, issues);
|
|
1164
|
+
}
|
|
1165
|
+
else if (config.capabilities.reviewsMode === "link-only") {
|
|
1166
|
+
requireStrictUsage({
|
|
1167
|
+
moduleName: "@brandlab-digital/site-next/google-reviews/ui",
|
|
1168
|
+
surface: "google-reviews-link",
|
|
1169
|
+
exports: ["GoogleReviewsLink"],
|
|
1170
|
+
}, usage, issues);
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
requireStrictUsage({
|
|
1174
|
+
moduleName: "@brandlab-digital/site-next/google-reviews/server",
|
|
1175
|
+
surface: "google-reviews-production",
|
|
1176
|
+
exports: ["createBrandlabGooglePlaceReviewsProductionRuntime"],
|
|
1177
|
+
}, usage, issues);
|
|
1178
|
+
requireStrictUsage({
|
|
1179
|
+
moduleName: "@brandlab-digital/site-next/google-reviews/client",
|
|
1180
|
+
surface: "google-reviews-display",
|
|
1181
|
+
exports: ["GooglePlaceReviewsClient"],
|
|
1182
|
+
}, usage, issues);
|
|
1183
|
+
}
|
|
997
1184
|
}
|
|
998
1185
|
else if (config.capabilities.reviewsProvider === "project-owned") {
|
|
999
1186
|
requireStrictUsage({
|
|
@@ -1002,7 +1189,7 @@ function addImportContractIssues(root, config, usage, issues) {
|
|
|
1002
1189
|
exports: ["preparePublicReviews"],
|
|
1003
1190
|
}, usage, issues);
|
|
1004
1191
|
}
|
|
1005
|
-
else if (usage.imports.
|
|
1192
|
+
else if ([...usage.imports].some((name) => name.startsWith("@brandlab-digital/site-next/google-reviews"))) {
|
|
1006
1193
|
issues.push(configIssue({
|
|
1007
1194
|
ruleId: "brandlab-config-v3-google-reviews-capability",
|
|
1008
1195
|
surface: "google-reviews",
|
|
@@ -1069,7 +1256,8 @@ function detectNextPackageBackedEvidence(root, usage) {
|
|
|
1069
1256
|
function detectContactSurfaceEvidence(root, usage) {
|
|
1070
1257
|
const evidence = new Set();
|
|
1071
1258
|
for (const moduleName of usage.imports) {
|
|
1072
|
-
if (moduleName
|
|
1259
|
+
if (moduleName === "@brandlab-digital/site-next/contact" ||
|
|
1260
|
+
moduleName.startsWith("@brandlab-digital/site-next/contact/")) {
|
|
1073
1261
|
evidence.add(`shared contact import ${moduleName}`);
|
|
1074
1262
|
}
|
|
1075
1263
|
}
|
|
@@ -1153,6 +1341,13 @@ function requireStrictUsage(requirement, usage, issues) {
|
|
|
1153
1341
|
}));
|
|
1154
1342
|
}
|
|
1155
1343
|
}
|
|
1344
|
+
function requireAnyStrictUsage(requirements, usage, issues) {
|
|
1345
|
+
const selected = requirements.find((requirement) => usage.imports.has(requirement.moduleName)) ??
|
|
1346
|
+
requirements[0];
|
|
1347
|
+
if (!selected)
|
|
1348
|
+
return;
|
|
1349
|
+
requireStrictUsage(selected, usage, issues);
|
|
1350
|
+
}
|
|
1156
1351
|
function frameworkOwnerExpectation(owner) {
|
|
1157
1352
|
if (owner === "metadata") {
|
|
1158
1353
|
return "The strict metadata builder reaches metadata or generateMetadata in an App Router page or layout entrypoint.";
|
|
@@ -1328,7 +1523,8 @@ function collectProjectImports(root, issues) {
|
|
|
1328
1523
|
return;
|
|
1329
1524
|
executableImports.add(binding.moduleName);
|
|
1330
1525
|
addExportUsage(executableExports, binding.moduleName, binding.exportName);
|
|
1331
|
-
const isSharedContactClientController = binding.moduleName === "@brandlab-digital/site-next/contact
|
|
1526
|
+
const isSharedContactClientController = (binding.moduleName === "@brandlab-digital/site-next/contact" ||
|
|
1527
|
+
binding.moduleName === "@brandlab-digital/site-next/contact/client") &&
|
|
1332
1528
|
binding.exportName === "createContactHttpSubmissionController";
|
|
1333
1529
|
if (isOutputOwned(node) ||
|
|
1334
1530
|
(isSharedContactClientController && isContactClientRuntimeOwned(node))) {
|
|
@@ -2752,11 +2948,14 @@ function isRuntimeIdentifierReference(identifier) {
|
|
|
2752
2948
|
}
|
|
2753
2949
|
return true;
|
|
2754
2950
|
}
|
|
2755
|
-
function addProtectedImplementationIssues(root, issues) {
|
|
2951
|
+
function addProtectedImplementationIssues(root, issues, contractVersion) {
|
|
2756
2952
|
for (const file of walkProjectRuntimeFiles(root)) {
|
|
2757
2953
|
const relativePath = normalizePath(relative(root, file));
|
|
2758
2954
|
const source = readFileSync(file, "utf8");
|
|
2759
2955
|
for (const rule of PROTECTED_LOCAL_PATTERNS) {
|
|
2956
|
+
if ("minimumContract" in rule &&
|
|
2957
|
+
!configurationContractIncludes(contractVersion, rule.minimumContract))
|
|
2958
|
+
continue;
|
|
2760
2959
|
const match = rule.pattern.exec(source);
|
|
2761
2960
|
if (!match || match.index === undefined)
|
|
2762
2961
|
continue;
|
|
@@ -2766,9 +2965,11 @@ function addProtectedImplementationIssues(root, issues) {
|
|
|
2766
2965
|
surface: rule.surface,
|
|
2767
2966
|
expected: rule.expected,
|
|
2768
2967
|
actual: `A protected local implementation was found in ${relativePath}.`,
|
|
2769
|
-
remediation:
|
|
2770
|
-
?
|
|
2771
|
-
:
|
|
2968
|
+
remediation: "remediation" in rule
|
|
2969
|
+
? rule.remediation
|
|
2970
|
+
: rule.ruleId === "brandlab-config-v3-url-derived-breadcrumbs"
|
|
2971
|
+
? "Use BrandlabRouteBreadcrumbs with the typed route catalog; keep path labels only as a development fallback."
|
|
2972
|
+
: "Keep only the thin project binding and consume the shared runtime.",
|
|
2772
2973
|
severity: "severity" in rule ? rule.severity : "blocker",
|
|
2773
2974
|
path: relativePath,
|
|
2774
2975
|
line,
|
|
@@ -2776,6 +2977,14 @@ function addProtectedImplementationIssues(root, issues) {
|
|
|
2776
2977
|
}
|
|
2777
2978
|
}
|
|
2778
2979
|
}
|
|
2980
|
+
function configurationContractIncludes(actual, minimum) {
|
|
2981
|
+
const order = {
|
|
2982
|
+
"project-config-v3": 3,
|
|
2983
|
+
"project-config-v4": 4,
|
|
2984
|
+
"project-config-v5": 5,
|
|
2985
|
+
};
|
|
2986
|
+
return order[actual] >= order[minimum];
|
|
2987
|
+
}
|
|
2779
2988
|
function walkSourceFiles(root) {
|
|
2780
2989
|
if (!existsSync(root))
|
|
2781
2990
|
return [];
|