@brandlab-digital/conformance 1.0.0-rc.11 → 1.0.0-rc.13

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.
@@ -1,4 +1,6 @@
1
1
  export declare const BRANDLAB_CONFIG_V3_RULESET_ID = "project-config-v3@1.0.0-rc.2";
2
+ export declare const BRANDLAB_CONFIG_V4_RULESET_ID = "project-config-v4@1.0.0-rc.12";
3
+ export type BrandlabInspectableConfigurationContract = "project-config-v3" | "project-config-v4";
2
4
  export declare const BRANDLAB_CONFIG_V3_PROFILES: readonly ["next-local-service", "next-root-level", "next-static-legacy", "legacy-php"];
3
5
  export interface InspectedBrandlabProjectConfig {
4
6
  projectId: string;
@@ -14,6 +16,9 @@ export interface InspectedBrandlabProjectConfig {
14
16
  contactEnabled: boolean;
15
17
  locationStrategy: string | null;
16
18
  reviewsProvider: string | null;
19
+ reviewsMode: string | null;
20
+ reviewsRouteIds: readonly string[];
21
+ reviewsMonthlyRequestLimit: number | null;
17
22
  };
18
23
  }
19
24
  export interface BrandlabConfigV3Issue {
@@ -37,7 +42,9 @@ export interface BrandlabConfigV3Inspection {
37
42
  frameworkOwnedExports: ReadonlyMap<string, ReadonlySet<string>>;
38
43
  issues: BrandlabConfigV3Issue[];
39
44
  }
40
- export declare function inspectBrandlabConfigV3(root: string): BrandlabConfigV3Inspection;
45
+ export declare function inspectBrandlabConfigV3(root: string, options?: {
46
+ contractVersion?: BrandlabInspectableConfigurationContract;
47
+ }): BrandlabConfigV3Inspection;
41
48
  /** SHA-256 evidence for the exact canonical serializer output produced at runtime. */
42
49
  export declare function digestBrandlabPlatformContractSerialization(serialized: string): string;
43
50
  //# sourceMappingURL=project-config-v3.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-config-v3.d.ts","sourceRoot":"","sources":["../src/project-config-v3.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,6BAA6B,iCAAiC,CAAC;AAE5E,eAAO,MAAM,2BAA2B,wFAK9B,CAAC;AA6CX,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,EAAE;QACN,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,mBAAmB,EAAE;QACnB,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KACxD,CAAC;IACF,YAAY,EAAE;QACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,cAAc,EAAE,OAAO,CAAC;QACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;KAChC,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACzC,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,SAAS,CAAC;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,8BAA8B,GAAG,IAAI,CAAC;IAC9C,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,iBAAiB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,qBAAqB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AA2JD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,0BAA0B,CA4GhF;AAs4FD,sFAAsF;AACtF,wBAAgB,2CAA2C,CAAC,UAAU,EAAE,MAAM,UAe7E"}
1
+ {"version":3,"file":"project-config-v3.d.ts","sourceRoot":"","sources":["../src/project-config-v3.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,6BAA6B,iCAAiC,CAAC;AAC5E,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAC7E,MAAM,MAAM,wCAAwC,GAChD,mBAAmB,GACnB,mBAAmB,CAAC;AAExB,eAAO,MAAM,2BAA2B,wFAK9B,CAAC;AA+CX,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,EAAE;QACN,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;KAC7B,CAAC;IACF,mBAAmB,EAAE;QACnB,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;KACxD,CAAC;IACF,YAAY,EAAE;QACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,cAAc,EAAE,OAAO,CAAC;QACxB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;QACnC,0BAA0B,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3C,CAAC;CACH;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACzC,YAAY,EAAE,QAAQ,GAAG,aAAa,GAAG,SAAS,CAAC;IACnD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,8BAA8B,GAAG,IAAI,CAAC;IAC9C,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7B,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,iBAAiB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5D,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,qBAAqB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,MAAM,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAuLD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,wCAAwC,CAAA;CAAO,GAC3E,0BAA0B,CA6G5B;AA6/FD,sFAAsF;AACtF,wBAAgB,2CAA2C,CAAC,UAAU,EAAE,MAAM,UAe7E"}
@@ -7,6 +7,7 @@ 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";
10
11
  export const BRANDLAB_CONFIG_V3_PROFILES = [
11
12
  "next-local-service",
12
13
  "next-root-level",
@@ -32,6 +33,8 @@ const LOCATION_STRATEGIES = new Set([
32
33
  "multi-location",
33
34
  ]);
34
35
  const REVIEWS_PROVIDERS = new Set(["google-places", "project-owned"]);
36
+ const GOOGLE_REVIEWS_MODES = new Set(["link-only", "summary", "full"]);
37
+ const MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT = 100_000;
35
38
  const ROOT_CONFIG_KEYS = new Set([
36
39
  "projectId",
37
40
  "profile",
@@ -193,6 +196,34 @@ const PROTECTED_LOCAL_PATTERNS = [
193
196
  pattern: /\b(?:class|function|const|let|var)\s+(?:ContactFormEmail|renderContactEmail)\b/,
194
197
  expected: "Contact email rendering is owned by @brandlab-digital/site-next/contact/email.",
195
198
  },
199
+ {
200
+ ruleId: "brandlab-config-v3-local-google-reviews-fetch",
201
+ minimumContract: "project-config-v4",
202
+ surface: "google-reviews",
203
+ pattern: /\bplaces\.googleapis\.com\/v1\/places\b/,
204
+ expected: "Google Places fetching is owned by @brandlab-digital/site-next/google-reviews/server.",
205
+ },
206
+ {
207
+ ruleId: "brandlab-config-v3-local-google-reviews-renderer",
208
+ minimumContract: "project-config-v4",
209
+ surface: "google-reviews",
210
+ pattern: /@brandlab-digital\/site-core\/google-reviews[\s\S]{0,12000}\breviews\.map\s*\(/,
211
+ expected: "Google review content and attribution are rendered by the shared policy-complete UI.",
212
+ },
213
+ {
214
+ ruleId: "brandlab-config-v3-hardcoded-google-review-total",
215
+ minimumContract: "project-config-v4",
216
+ surface: "google-reviews",
217
+ pattern: /["'`][^"'`\n]{0,100}\b\d{1,6}\s+Google\s+(?:reviews?|ratings?)\b/i,
218
+ expected: "Rendered Google rating and review totals come from a live no-store shared snapshot.",
219
+ },
220
+ {
221
+ ruleId: "brandlab-config-v3-public-google-review-cache",
222
+ minimumContract: "project-config-v4",
223
+ surface: "google-reviews",
224
+ 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,
225
+ expected: "Google Places content is returned with private no-store headers and is never publicly cached.",
226
+ },
196
227
  {
197
228
  ruleId: "brandlab-config-v3-local-lead-event",
198
229
  surface: "analytics",
@@ -207,7 +238,8 @@ const PROTECTED_LOCAL_PATTERNS = [
207
238
  expected: "Breadcrumb labels and hierarchy derive from the semantic route catalog.",
208
239
  },
209
240
  ];
210
- export function inspectBrandlabConfigV3(root) {
241
+ export function inspectBrandlabConfigV3(root, options = {}) {
242
+ const contractVersion = options.contractVersion ?? "project-config-v4";
211
243
  const issues = [];
212
244
  const configPath = join(root, "brandlab.config.ts");
213
245
  const usage = collectProjectImports(root, issues);
@@ -280,11 +312,11 @@ export function inspectBrandlabConfigV3(root) {
280
312
  }));
281
313
  return { ...usage, config: null, issues };
282
314
  }
283
- const config = validateConfigValue(value, issues);
315
+ const config = validateConfigValue(value, issues, contractVersion);
284
316
  if (config) {
285
- addImportContractIssues(root, config, usage, issues);
317
+ addImportContractIssues(root, config, usage, issues, contractVersion);
286
318
  }
287
- addProtectedImplementationIssues(root, issues);
319
+ addProtectedImplementationIssues(root, issues, contractVersion);
288
320
  return {
289
321
  config,
290
322
  imports,
@@ -382,7 +414,7 @@ function propertyName(name) {
382
414
  }
383
415
  return null;
384
416
  }
385
- function validateConfigValue(value, issues) {
417
+ function validateConfigValue(value, issues, contractVersion) {
386
418
  if (!isRecord(value)) {
387
419
  issues.push(configIssue({
388
420
  ruleId: "brandlab-config-v3-object",
@@ -537,7 +569,7 @@ function validateConfigValue(value, issues) {
537
569
  }));
538
570
  }
539
571
  else {
540
- validateCapabilities(capabilities, issues);
572
+ validateCapabilities(capabilities, issues, contractVersion);
541
573
  }
542
574
  validateSitemap(value.sitemap, issues);
543
575
  validateExceptions(value.exceptions, issues);
@@ -549,6 +581,13 @@ function validateConfigValue(value, issues) {
549
581
  const locations = isRecord(safeCapabilities.locations) ? safeCapabilities.locations : null;
550
582
  const reviews = isRecord(safeCapabilities.reviews) ? safeCapabilities.reviews : null;
551
583
  const reviewsProvider = typeof reviews?.provider === "string" ? reviews.provider : null;
584
+ const reviewsMode = typeof reviews?.mode === "string" ? reviews.mode : null;
585
+ const reviewsRouteIds = Array.isArray(reviews?.routeIds)
586
+ ? reviews.routeIds.filter((entry) => typeof entry === "string")
587
+ : [];
588
+ const reviewsMonthlyRequestLimit = typeof reviews?.monthlyRequestLimit === "number"
589
+ ? reviews.monthlyRequestLimit
590
+ : null;
552
591
  if (reviewsProvider === "google-places" && !families.includes("legal")) {
553
592
  issues.push(configIssue({
554
593
  ruleId: "brandlab-config-v3-google-places-legal-family",
@@ -571,6 +610,9 @@ function validateConfigValue(value, issues) {
571
610
  contactEnabled: contact !== null,
572
611
  locationStrategy: typeof locations?.strategy === "string" ? locations.strategy : null,
573
612
  reviewsProvider,
613
+ reviewsMode,
614
+ reviewsRouteIds,
615
+ reviewsMonthlyRequestLimit,
574
616
  },
575
617
  };
576
618
  }
@@ -647,7 +689,7 @@ function validateAliases(value, issues) {
647
689
  }));
648
690
  }
649
691
  }
650
- function validateCapabilities(capabilities, issues) {
692
+ function validateCapabilities(capabilities, issues, contractVersion) {
651
693
  addUnknownKeyIssues(capabilities, new Set(["blog", "contact", "locations", "reviews"]), new Set(), "capabilities", issues);
652
694
  if (capabilities.blog !== undefined) {
653
695
  if (!isRecord(capabilities.blog)) {
@@ -717,7 +759,10 @@ function validateCapabilities(capabilities, issues) {
717
759
  addShapeIssue("reviews", issues);
718
760
  }
719
761
  else {
720
- addUnknownKeyIssues(capabilities.reviews, new Set(["provider"]), new Set(), "capabilities.reviews", issues);
762
+ addUnknownKeyIssues(capabilities.reviews, capabilities.reviews.provider === "google-places" &&
763
+ contractVersion === "project-config-v4"
764
+ ? new Set(["provider", "mode", "routeIds", "monthlyRequestLimit"])
765
+ : new Set(["provider"]), new Set(), "capabilities.reviews", issues);
721
766
  if (typeof capabilities.reviews.provider !== "string" ||
722
767
  !REVIEWS_PROVIDERS.has(capabilities.reviews.provider)) {
723
768
  issues.push(configIssue({
@@ -728,6 +773,64 @@ function validateCapabilities(capabilities, issues) {
728
773
  path: "brandlab.config.ts",
729
774
  }));
730
775
  }
776
+ if (capabilities.reviews.provider === "google-places" &&
777
+ contractVersion === "project-config-v4") {
778
+ if (typeof capabilities.reviews.mode !== "string" ||
779
+ !GOOGLE_REVIEWS_MODES.has(capabilities.reviews.mode)) {
780
+ issues.push(configIssue({
781
+ ruleId: "brandlab-config-v3-google-reviews-mode",
782
+ expected: `One Google reviews mode: ${[...GOOGLE_REVIEWS_MODES].join(", ")}.`,
783
+ actual: `reviews.mode is ${JSON.stringify(capabilities.reviews.mode)}.`,
784
+ remediation: "Select link-only, summary or full explicitly.",
785
+ path: "brandlab.config.ts",
786
+ }));
787
+ }
788
+ const routeIds = Array.isArray(capabilities.reviews.routeIds)
789
+ ? capabilities.reviews.routeIds.filter((entry) => typeof entry === "string")
790
+ : [];
791
+ if (!Array.isArray(capabilities.reviews.routeIds) ||
792
+ routeIds.length === 0 ||
793
+ routeIds.length !== capabilities.reviews.routeIds.length ||
794
+ routeIds.some((routeId) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(routeId))) {
795
+ issues.push(configIssue({
796
+ ruleId: "brandlab-config-v3-google-reviews-routes",
797
+ expected: "At least one unique lowercase kebab-case review placement route ID.",
798
+ actual: `reviews.routeIds is ${JSON.stringify(capabilities.reviews.routeIds)}.`,
799
+ remediation: "Declare only the route IDs where Google review proof is intentionally displayed.",
800
+ path: "brandlab.config.ts",
801
+ }));
802
+ }
803
+ else {
804
+ addDuplicateIssue(routeIds, "brandlab-config-v3-google-reviews-route-duplicate", "reviews.routeIds", issues);
805
+ }
806
+ if (capabilities.reviews.mode === "link-only") {
807
+ if (capabilities.reviews.monthlyRequestLimit !== undefined) {
808
+ issues.push(configIssue({
809
+ ruleId: "brandlab-config-v3-google-reviews-link-only-quota",
810
+ expected: "No API request limit for link-only review proof.",
811
+ actual: "reviews.monthlyRequestLimit is configured for link-only mode.",
812
+ remediation: "Remove monthlyRequestLimit because link-only mode never calls Places.",
813
+ path: "brandlab.config.ts",
814
+ }));
815
+ }
816
+ }
817
+ else if (capabilities.reviews.mode === "summary" ||
818
+ capabilities.reviews.mode === "full") {
819
+ const limit = capabilities.reviews.monthlyRequestLimit;
820
+ if (typeof limit !== "number" ||
821
+ !Number.isSafeInteger(limit) ||
822
+ limit < 1 ||
823
+ limit > MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT) {
824
+ issues.push(configIssue({
825
+ ruleId: "brandlab-config-v3-google-reviews-quota",
826
+ expected: `monthlyRequestLimit from 1 to ${MAXIMUM_GOOGLE_REVIEWS_MONTHLY_REQUEST_LIMIT}.`,
827
+ actual: `reviews.monthlyRequestLimit is ${JSON.stringify(limit)}.`,
828
+ remediation: "Set an explicit project-level monthly request ceiling.",
829
+ path: "brandlab.config.ts",
830
+ }));
831
+ }
832
+ }
833
+ }
731
834
  }
732
835
  }
733
836
  }
@@ -905,7 +1008,7 @@ function isIsoDate(value) {
905
1008
  return false;
906
1009
  return !Number.isNaN(Date.parse(value));
907
1010
  }
908
- function addImportContractIssues(root, config, usage, issues) {
1011
+ function addImportContractIssues(root, config, usage, issues, contractVersion) {
909
1012
  if (config.profile === "legacy-php") {
910
1013
  const nextEvidence = detectNextPackageBackedEvidence(root, usage);
911
1014
  if (nextEvidence.length === 0)
@@ -984,16 +1087,37 @@ function addImportContractIssues(root, config, usage, issues) {
984
1087
  }, usage, issues);
985
1088
  }
986
1089
  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);
1090
+ if (contractVersion === "project-config-v3") {
1091
+ requireStrictUsage({
1092
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1093
+ surface: "google-reviews-route",
1094
+ exports: ["createGooglePlaceReviewsRouteResponse"],
1095
+ }, usage, issues);
1096
+ requireStrictUsage({
1097
+ moduleName: "@brandlab-digital/site-next/google-reviews",
1098
+ surface: "google-reviews-display",
1099
+ exports: ["GooglePlaceReviews"],
1100
+ }, usage, issues);
1101
+ }
1102
+ else if (config.capabilities.reviewsMode === "link-only") {
1103
+ requireStrictUsage({
1104
+ moduleName: "@brandlab-digital/site-next/google-reviews/ui",
1105
+ surface: "google-reviews-link",
1106
+ exports: ["GoogleReviewsLink"],
1107
+ }, usage, issues);
1108
+ }
1109
+ else {
1110
+ requireStrictUsage({
1111
+ moduleName: "@brandlab-digital/site-next/google-reviews/server",
1112
+ surface: "google-reviews-production",
1113
+ exports: ["createBrandlabGooglePlaceReviewsProductionRuntime"],
1114
+ }, usage, issues);
1115
+ requireStrictUsage({
1116
+ moduleName: "@brandlab-digital/site-next/google-reviews/client",
1117
+ surface: "google-reviews-display",
1118
+ exports: ["GooglePlaceReviewsClient"],
1119
+ }, usage, issues);
1120
+ }
997
1121
  }
998
1122
  else if (config.capabilities.reviewsProvider === "project-owned") {
999
1123
  requireStrictUsage({
@@ -1002,7 +1126,7 @@ function addImportContractIssues(root, config, usage, issues) {
1002
1126
  exports: ["preparePublicReviews"],
1003
1127
  }, usage, issues);
1004
1128
  }
1005
- else if (usage.imports.has("@brandlab-digital/site-next/google-reviews")) {
1129
+ else if ([...usage.imports].some((name) => name.startsWith("@brandlab-digital/site-next/google-reviews"))) {
1006
1130
  issues.push(configIssue({
1007
1131
  ruleId: "brandlab-config-v3-google-reviews-capability",
1008
1132
  surface: "google-reviews",
@@ -2752,11 +2876,15 @@ function isRuntimeIdentifierReference(identifier) {
2752
2876
  }
2753
2877
  return true;
2754
2878
  }
2755
- function addProtectedImplementationIssues(root, issues) {
2879
+ function addProtectedImplementationIssues(root, issues, contractVersion) {
2756
2880
  for (const file of walkProjectRuntimeFiles(root)) {
2757
2881
  const relativePath = normalizePath(relative(root, file));
2758
2882
  const source = readFileSync(file, "utf8");
2759
2883
  for (const rule of PROTECTED_LOCAL_PATTERNS) {
2884
+ if ("minimumContract" in rule &&
2885
+ rule.minimumContract === "project-config-v4" &&
2886
+ contractVersion !== "project-config-v4")
2887
+ continue;
2760
2888
  const match = rule.pattern.exec(source);
2761
2889
  if (!match || match.index === undefined)
2762
2890
  continue;