@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.
Files changed (44) 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 +211 -0
  4. package/dist/analytics-contract.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +13 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/project-config-v3.d.ts +9 -1
  10. package/dist/project-config-v3.d.ts.map +1 -1
  11. package/dist/project-config-v3.js +210 -24
  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 +398 -0
  20. package/dist/project-documentation.js.map +1 -0
  21. package/dist/project.d.ts +4 -1
  22. package/dist/project.d.ts.map +1 -1
  23. package/dist/project.js +263 -15
  24. package/dist/project.js.map +1 -1
  25. package/dist/release-catalog.d.ts.map +1 -1
  26. package/dist/release-catalog.js +1356 -0
  27. package/dist/release-catalog.js.map +1 -1
  28. package/dist/release-contract-compatibility.d.ts +126 -0
  29. package/dist/release-contract-compatibility.d.ts.map +1 -1
  30. package/dist/release-contract-compatibility.js +126 -0
  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 +18 -0
  37. package/dist/rendered-content.d.ts.map +1 -0
  38. package/dist/rendered-content.js +80 -0
  39. package/dist/rendered-content.js.map +1 -0
  40. package/dist/source-safety-contracts.d.ts +1 -1
  41. package/dist/source-safety-contracts.d.ts.map +1 -1
  42. package/dist/source-safety-contracts.js +5 -1
  43. package/dist/source-safety-contracts.js.map +1 -1
  44. package/package.json +19 -2
package/dist/project.js CHANGED
@@ -2,12 +2,14 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, relative, resolve } from "node:path";
3
3
  import { evaluateSharedPackageVersionSpecs, findForbiddenBrandlabPatterns, } from "./vitest.js";
4
4
  import { DEFAULT_BRANDLAB_RELEASE_CATALOG, } from "./release-catalog.js";
5
- import { BRANDLAB_CONFIG_V3_RULESET_ID, inspectBrandlabConfigV3, } from "./project-config-v3.js";
5
+ import { BRANDLAB_CONFIG_V3_RULESET_ID, BRANDLAB_CONFIG_V4_RULESET_ID, BRANDLAB_CONFIG_V5_RULESET_ID, inspectBrandlabConfigV3, } from "./project-config-v3.js";
6
6
  import { BRANDLAB_CONFIG_V2_RULESET_ID, inspectBrandlabConfigV2 as inspectImmutableBrandlabConfigV2, } from "./project-config-v2-contract.js";
7
7
  import { BRANDLAB_DEPRECATED_SCHEMA_SOURCE_PATTERNS, BRANDLAB_SCHEMA_HYGIENE_V3_RULESET_ID, } from "./schema-hygiene.js";
8
8
  import { BRANDLAB_SOURCE_SAFETY_V2_PATTERNS, BRANDLAB_SOURCE_SAFETY_V2_RULESET_ID, sourceSafetyRulesetForArchitecture, targetOnlySourceSafetyPatterns, } from "./source-safety-contracts.js";
9
9
  import { inspectLegacyArchitectureContract, legacyArchitectureRulesetId, targetOnlyLegacyArchitectureIssues, } from "./legacy-architecture-contracts.js";
10
10
  import { BRANDLAB_SHARED_PACKAGE_NAMES, compareBrandlabSemver, digestBrandlabAuthority, isBrandlabSemver, validateBrandlabReleaseCatalog, validateBrandlabRuntimeReleaseCatalog, validateBrandlabRuntimeReleaseManifestSubsetAgainstCatalog, validateBrandlabSupportPolicy, } from "./release-manifest.js";
11
+ import { BRANDLAB_PROJECT_DOCUMENTATION_RULESET_ID, inspectBrandlabProjectDocumentation, } from "./project-documentation.js";
12
+ import { BRANDLAB_ANALYTICS_V3_RULESET_ID, inspectBrandlabAnalyticsV3, } from "./analytics-contract.js";
11
13
  export const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v1";
12
14
  export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "control-plane-v1";
13
15
  export const BRANDLAB_ARCHITECTURE_DISPATCH_RULESET_ID = "version-aware-architecture-dispatch-v1";
@@ -15,7 +17,19 @@ const LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT = "architecture-v2";
15
17
  const LEGACY_CONFIGURATION_CONTRACT = "legacy-config-v1";
16
18
  const CONFIGURATION_V2_CONTRACT = "project-config-v2";
17
19
  const CONFIGURATION_V3_CONTRACT = "project-config-v3";
20
+ const CONFIGURATION_V4_CONTRACT = "project-config-v4";
21
+ const CONFIGURATION_V5_CONTRACT = "project-config-v5";
18
22
  const SEO_V3_CONTRACT = "seo-v3";
23
+ const SEO_V4_CONTRACT = "seo-v4";
24
+ const ARCHITECTURE_V5_CONTRACT = "architecture-v5";
25
+ const ANALYTICS_V3_CONTRACT = "analytics-v3";
26
+ const ANALYTICS_V4_CONTRACT = "analytics-v4";
27
+ function isSeoV3Compatible(contract) {
28
+ return contract === SEO_V3_CONTRACT || contract === SEO_V4_CONTRACT;
29
+ }
30
+ function isAnalyticsV3Compatible(contract) {
31
+ return contract === ANALYTICS_V3_CONTRACT || contract === ANALYTICS_V4_CONTRACT;
32
+ }
19
33
  const RUNTIME_PACKAGE_FIELDS = [
20
34
  ["next", "next"],
21
35
  ["react", "react"],
@@ -79,6 +93,35 @@ export function evaluateBrandlabProject(options) {
79
93
  targetRelease,
80
94
  architecture: options.architecture,
81
95
  });
96
+ const documentationFingerprint = evaluateApplicableProjectDocumentationContracts({
97
+ root,
98
+ findings,
99
+ installedManifest,
100
+ targetManifest,
101
+ });
102
+ const analyticsFingerprint = evaluateApplicableAnalyticsContracts({
103
+ root,
104
+ findings,
105
+ installedManifest,
106
+ targetManifest,
107
+ });
108
+ if (installedManifest) {
109
+ addContactUploadFilenameFindings({
110
+ root,
111
+ findings,
112
+ manifest: installedManifest,
113
+ classification: "maintenance",
114
+ });
115
+ }
116
+ if (targetManifest &&
117
+ targetManifest.releaseVersion !== installedManifest?.releaseVersion) {
118
+ addContactUploadFilenameFindings({
119
+ root,
120
+ findings,
121
+ manifest: targetManifest,
122
+ classification: "migration",
123
+ });
124
+ }
82
125
  const sourceRoot = join(root, "src");
83
126
  const installedSourceSafety = sourceSafetyRulesetForArchitecture(installedManifest?.contracts.architecture);
84
127
  const targetSourceSafety = sourceSafetyRulesetForArchitecture(targetManifest?.contracts.architecture);
@@ -140,17 +183,17 @@ export function evaluateBrandlabProject(options) {
140
183
  },
141
184
  });
142
185
  }
143
- if (installedManifest?.contracts.seo === SEO_V3_CONTRACT) {
186
+ if (isSeoV3Compatible(installedManifest?.contracts.seo)) {
144
187
  addSourcePatternFindings({
145
188
  root,
146
189
  sourceRoot,
147
190
  findings,
148
191
  patterns: BRANDLAB_DEPRECATED_SCHEMA_SOURCE_PATTERNS,
149
192
  classification: "maintenance",
150
- contract: { name: "seo", version: SEO_V3_CONTRACT },
193
+ contract: { name: "seo", version: installedManifest.contracts.seo },
151
194
  });
152
195
  }
153
- if (targetManifest?.contracts.seo === SEO_V3_CONTRACT &&
196
+ if (isSeoV3Compatible(targetManifest?.contracts.seo) &&
154
197
  targetManifest.releaseVersion !== installedManifest?.releaseVersion) {
155
198
  addSourcePatternFindings({
156
199
  root,
@@ -158,7 +201,7 @@ export function evaluateBrandlabProject(options) {
158
201
  findings,
159
202
  patterns: BRANDLAB_DEPRECATED_SCHEMA_SOURCE_PATTERNS,
160
203
  classification: "migration",
161
- contract: { name: "seo", version: SEO_V3_CONTRACT },
204
+ contract: { name: "seo", version: targetManifest.contracts.seo },
162
205
  });
163
206
  }
164
207
  }
@@ -175,11 +218,21 @@ export function evaluateBrandlabProject(options) {
175
218
  rulesets: {
176
219
  configurationV2: BRANDLAB_CONFIG_V2_RULESET_ID,
177
220
  configurationV3: BRANDLAB_CONFIG_V3_RULESET_ID,
221
+ configurationV4: BRANDLAB_CONFIG_V4_RULESET_ID,
222
+ configurationV5: BRANDLAB_CONFIG_V5_RULESET_ID,
178
223
  seoV3: BRANDLAB_SCHEMA_HYGIENE_V3_RULESET_ID,
179
224
  installedSourceSafety: installedSourceSafety?.id ?? null,
180
225
  targetSourceSafety: targetSourceSafety?.id ?? null,
181
226
  installedLegacyArchitecture: legacyArchitectureRulesetId(installedManifest?.contracts.architecture),
182
227
  targetLegacyArchitecture: legacyArchitectureRulesetId(targetManifest?.contracts.architecture),
228
+ projectDocumentation: installedManifest?.contracts.architecture === ARCHITECTURE_V5_CONTRACT ||
229
+ targetManifest?.contracts.architecture === ARCHITECTURE_V5_CONTRACT
230
+ ? BRANDLAB_PROJECT_DOCUMENTATION_RULESET_ID
231
+ : null,
232
+ analyticsV3: isAnalyticsV3Compatible(installedManifest?.contracts.analytics) ||
233
+ isAnalyticsV3Compatible(targetManifest?.contracts.analytics)
234
+ ? BRANDLAB_ANALYTICS_V3_RULESET_ID
235
+ : null,
183
236
  },
184
237
  });
185
238
  const implementationFingerprint = digestBrandlabAuthority({
@@ -187,6 +240,8 @@ export function evaluateBrandlabProject(options) {
187
240
  packageLock: packageLock.fingerprint,
188
241
  brandlabConfig: readOptionalText(join(root, "brandlab.config.ts")),
189
242
  architecture: architectureFingerprint,
243
+ documentation: documentationFingerprint,
244
+ analytics: analyticsFingerprint,
190
245
  evidence: sortedFindings.map(({ ruleId, classification, severity, path, line }) => ({
191
246
  ruleId,
192
247
  classification,
@@ -210,6 +265,85 @@ export function evaluateBrandlabProject(options) {
210
265
  advisory: summarize(sortedFindings, "advisory"),
211
266
  };
212
267
  }
268
+ function evaluateApplicableProjectDocumentationContracts(options) {
269
+ const installedArchitecture = options.installedManifest?.contracts.architecture ?? null;
270
+ const targetArchitecture = options.targetManifest?.contracts.architecture ?? null;
271
+ if (installedArchitecture !== ARCHITECTURE_V5_CONTRACT &&
272
+ targetArchitecture !== ARCHITECTURE_V5_CONTRACT) {
273
+ return null;
274
+ }
275
+ const report = inspectBrandlabProjectDocumentation(options.root);
276
+ if (installedArchitecture === ARCHITECTURE_V5_CONTRACT) {
277
+ for (const issue of report.issues) {
278
+ options.findings.push(mapProjectDocumentationIssue(issue, "maintenance"));
279
+ }
280
+ }
281
+ else if (targetArchitecture === ARCHITECTURE_V5_CONTRACT) {
282
+ for (const issue of report.issues) {
283
+ options.findings.push(mapProjectDocumentationIssue(issue, "migration"));
284
+ }
285
+ }
286
+ return {
287
+ rulesetId: report.rulesetId,
288
+ files: report.files,
289
+ applicableRuleIds: report.issues.map((issue) => issue.ruleId).sort(),
290
+ };
291
+ }
292
+ function mapProjectDocumentationIssue(issue, classification) {
293
+ return {
294
+ ruleId: issue.ruleId,
295
+ surface: issue.surface,
296
+ classification,
297
+ severity: issue.severity,
298
+ contract: { name: "architecture", version: ARCHITECTURE_V5_CONTRACT },
299
+ evidenceKind: issue.evidenceKind,
300
+ ...(issue.path ? { path: issue.path } : {}),
301
+ ...(issue.line ? { line: issue.line } : {}),
302
+ expected: issue.expected,
303
+ actual: issue.actual,
304
+ remediation: issue.remediation,
305
+ };
306
+ }
307
+ function evaluateApplicableAnalyticsContracts(options) {
308
+ const installedAnalytics = options.installedManifest?.contracts.analytics ?? null;
309
+ const targetAnalytics = options.targetManifest?.contracts.analytics ?? null;
310
+ if (!isAnalyticsV3Compatible(installedAnalytics) &&
311
+ !isAnalyticsV3Compatible(targetAnalytics)) {
312
+ return null;
313
+ }
314
+ const inspection = inspectBrandlabAnalyticsV3(options.root);
315
+ if (isAnalyticsV3Compatible(installedAnalytics)) {
316
+ for (const issue of inspection.issues) {
317
+ options.findings.push(mapAnalyticsV3Issue(issue, "maintenance", installedAnalytics));
318
+ }
319
+ }
320
+ else if (isAnalyticsV3Compatible(targetAnalytics)) {
321
+ for (const issue of inspection.issues) {
322
+ options.findings.push(mapAnalyticsV3Issue(issue, "migration", targetAnalytics));
323
+ }
324
+ }
325
+ return {
326
+ rulesetId: inspection.rulesetId,
327
+ websiteCallsDeclared: inspection.websiteCallsDeclared,
328
+ leadDiagnosticsDeclared: inspection.leadDiagnosticsDeclared,
329
+ applicableRuleIds: inspection.issues.map((issue) => issue.ruleId).sort(),
330
+ };
331
+ }
332
+ function mapAnalyticsV3Issue(issue, classification, contractVersion) {
333
+ return {
334
+ ruleId: issue.ruleId,
335
+ surface: issue.surface,
336
+ classification,
337
+ severity: issue.severity,
338
+ contract: { name: "analytics", version: contractVersion },
339
+ evidenceKind: issue.evidenceKind,
340
+ ...(issue.path ? { path: issue.path } : {}),
341
+ ...(issue.line ? { line: issue.line } : {}),
342
+ expected: issue.expected,
343
+ actual: issue.actual,
344
+ remediation: issue.remediation,
345
+ };
346
+ }
213
347
  function addSourcePatternFindings(options) {
214
348
  for (const match of findForbiddenBrandlabPatterns({
215
349
  roots: [options.sourceRoot],
@@ -674,6 +808,13 @@ function addInstalledRuntimeFindings(options) {
674
808
  path: "package.json",
675
809
  }));
676
810
  }
811
+ addSharpRuntimeFindings({
812
+ findings: options.findings,
813
+ packageJson: options.packageJson,
814
+ packageLock: options.packageLock,
815
+ manifest: options.manifest,
816
+ classification: "maintenance",
817
+ });
677
818
  }
678
819
  function addTargetRuntimeFindings(options) {
679
820
  if (!options.packageJson)
@@ -713,6 +854,16 @@ function addTargetRuntimeFindings(options) {
713
854
  if (options.packageJson.packageManager !== options.targetManifest.runtime.packageManager) {
714
855
  runtimeMismatches.push(`packageManager(expected=${options.targetManifest.runtime.packageManager},actual=${options.packageJson.packageManager ?? "missing"})`);
715
856
  }
857
+ const targetSharp = options.targetManifest.runtime.sharp;
858
+ if (targetSharp) {
859
+ const actualOverride = options.packageJson.overrides?.sharp ?? null;
860
+ const sharpResolutions = options.packageLock.sharpResolutions;
861
+ if (actualOverride !== targetSharp ||
862
+ sharpResolutions.length !== 1 ||
863
+ sharpResolutions[0]?.version !== targetSharp) {
864
+ runtimeMismatches.push(`sharp(expected=${targetSharp},override=${actualOverride ?? "missing"},resolutions=${formatSharpResolutions(sharpResolutions)})`);
865
+ }
866
+ }
716
867
  if (runtimeMismatches.length > 0) {
717
868
  options.findings.push(migrationRuntimeFinding({
718
869
  ruleId: "brandlab-migration-runtime-baseline",
@@ -722,6 +873,64 @@ function addTargetRuntimeFindings(options) {
722
873
  }));
723
874
  }
724
875
  }
876
+ function addSharpRuntimeFindings(options) {
877
+ const expected = options.manifest.runtime.sharp;
878
+ if (!expected)
879
+ return;
880
+ const actualOverride = options.packageJson.overrides?.sharp ?? null;
881
+ if (actualOverride !== expected) {
882
+ options.findings.push(runtimeFinding({
883
+ ruleId: "brandlab-runtime-sharp-override",
884
+ expected: `overrides.sharp=${expected}`,
885
+ actual: `overrides.sharp=${actualOverride ?? "missing"}.`,
886
+ remediation: "Add the release-owned Sharp override so Next.js and site-next use one security-patched native runtime.",
887
+ path: "package.json",
888
+ }));
889
+ }
890
+ if (options.packageLock.state !== "valid")
891
+ return;
892
+ if (options.packageLock.sharpResolutions.length !== 1 ||
893
+ options.packageLock.sharpResolutions[0]?.version !== expected) {
894
+ options.findings.push(runtimeFinding({
895
+ ruleId: "brandlab-runtime-sharp-resolution",
896
+ expected: `one sharp@${expected} lockfile resolution`,
897
+ actual: formatSharpResolutions(options.packageLock.sharpResolutions),
898
+ remediation: "Regenerate package-lock.json after applying the exact release-owned Sharp override.",
899
+ path: "package-lock.json",
900
+ }));
901
+ }
902
+ }
903
+ function addContactUploadFilenameFindings(options) {
904
+ if (!options.manifest.capabilities.includes("client-safe-contact-upload-filenames")) {
905
+ return;
906
+ }
907
+ const path = "src/lib/contact/imageUploads.ts";
908
+ const source = readOptionalText(join(options.root, path));
909
+ // Raw File objects already retain their reviewed names. This rule is only
910
+ // relevant when a project owns a client-side image processing adapter.
911
+ if (!source || /\bnormalizeContactUploadFile\s*\(/.test(source))
912
+ return;
913
+ options.findings.push({
914
+ ruleId: "brandlab-contact-upload-filename-normalization",
915
+ surface: "contact",
916
+ classification: options.classification,
917
+ severity: "blocker",
918
+ contract: {
919
+ name: "contact",
920
+ version: options.manifest.contracts.contact,
921
+ },
922
+ evidenceKind: "static",
923
+ path,
924
+ expected: "Compressed contact images are wrapped with normalizeContactUploadFile before FormData submission.",
925
+ actual: "The contact upload module does not use the shared filename normalizer.",
926
+ remediation: "Use normalizeContactUploadFile(original, processed) from @brandlab-digital/site-next/contact/client so anonymous browser Blobs retain a valid reviewed filename.",
927
+ });
928
+ }
929
+ function formatSharpResolutions(resolutions) {
930
+ return resolutions.length === 0
931
+ ? "none"
932
+ : resolutions.map((entry) => `${entry.path}=${entry.version ?? "missing"}`).join(", ");
933
+ }
725
934
  function runtimeFinding(options) {
726
935
  return {
727
936
  ruleId: options.ruleId,
@@ -760,10 +969,14 @@ function evaluateApplicableArchitectureContracts(options) {
760
969
  configV2Inspection ??= inspectImmutableBrandlabConfigV2(options.root);
761
970
  return configV2Inspection;
762
971
  };
763
- let configV3Inspection = null;
764
- const inspectConfigV3 = () => {
765
- configV3Inspection ??= inspectBrandlabConfigV3(options.root);
766
- return configV3Inspection;
972
+ const configV3Inspections = new Map();
973
+ const inspectConfigV3 = (contractVersion) => {
974
+ const cached = configV3Inspections.get(contractVersion);
975
+ if (cached)
976
+ return cached;
977
+ const inspection = inspectBrandlabConfigV3(options.root, { contractVersion });
978
+ configV3Inspections.set(contractVersion, inspection);
979
+ return inspection;
767
980
  };
768
981
  const legacyInspections = new Map();
769
982
  let legacyArchitectureInput;
@@ -793,7 +1006,19 @@ function evaluateApplicableArchitectureContracts(options) {
793
1006
  }
794
1007
  }
795
1008
  else if (installedConfiguration === CONFIGURATION_V3_CONTRACT) {
796
- const inspection = inspectConfigV3();
1009
+ const inspection = inspectConfigV3(CONFIGURATION_V3_CONTRACT);
1010
+ for (const issue of inspection.issues) {
1011
+ options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
1012
+ }
1013
+ }
1014
+ else if (installedConfiguration === CONFIGURATION_V4_CONTRACT) {
1015
+ const inspection = inspectConfigV3(CONFIGURATION_V4_CONTRACT);
1016
+ for (const issue of inspection.issues) {
1017
+ options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
1018
+ }
1019
+ }
1020
+ else if (installedConfiguration === CONFIGURATION_V5_CONTRACT) {
1021
+ const inspection = inspectConfigV3(CONFIGURATION_V5_CONTRACT);
797
1022
  for (const issue of inspection.issues) {
798
1023
  options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
799
1024
  }
@@ -826,9 +1051,9 @@ function evaluateApplicableArchitectureContracts(options) {
826
1051
  // supported target release is explicitly selected.
827
1052
  const configSource = readOptionalText(join(options.root, "brandlab.config.ts"));
828
1053
  if (/\bdefineBrandlabProject\s*\(/.test(configSource ?? "")) {
829
- for (const issue of inspectConfigV3().issues) {
1054
+ for (const issue of inspectConfigV3(CONFIGURATION_V5_CONTRACT).issues) {
830
1055
  options.findings.push(mapConfigIssue(issue, "advisory", {
831
- configuration: CONFIGURATION_V3_CONTRACT,
1056
+ configuration: CONFIGURATION_V5_CONTRACT,
832
1057
  }));
833
1058
  }
834
1059
  }
@@ -846,7 +1071,17 @@ function evaluateApplicableArchitectureContracts(options) {
846
1071
  }
847
1072
  }
848
1073
  else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V3_CONTRACT) {
849
- for (const issue of inspectConfigV3().issues) {
1074
+ for (const issue of inspectConfigV3(CONFIGURATION_V3_CONTRACT).issues) {
1075
+ options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
1076
+ }
1077
+ }
1078
+ else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V4_CONTRACT) {
1079
+ for (const issue of inspectConfigV3(CONFIGURATION_V4_CONTRACT).issues) {
1080
+ options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
1081
+ }
1082
+ }
1083
+ else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V5_CONTRACT) {
1084
+ for (const issue of inspectConfigV3(CONFIGURATION_V5_CONTRACT).issues) {
850
1085
  options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
851
1086
  }
852
1087
  }
@@ -867,7 +1102,9 @@ function evaluateApplicableArchitectureContracts(options) {
867
1102
  }
868
1103
  }
869
1104
  const inspectedConfigV2 = configV2Inspection;
870
- const inspectedConfigV3 = configV3Inspection;
1105
+ const inspectedConfigV3 = (configV3Inspections.get(CONFIGURATION_V4_CONTRACT) ??
1106
+ configV3Inspections.get(CONFIGURATION_V3_CONTRACT) ??
1107
+ null);
871
1108
  return {
872
1109
  evaluator: BRANDLAB_ARCHITECTURE_DISPATCH_RULESET_ID,
873
1110
  installedContract: options.installedManifest?.contracts.architecture ?? null,
@@ -1050,7 +1287,7 @@ function readPackageLock(root) {
1050
1287
  const rootDevDependencies = stringRecord(rootPackage.devDependencies);
1051
1288
  const rootEngines = stringRecord(rootPackage.engines);
1052
1289
  const resolvedPackages = new Map();
1053
- for (const packageName of [...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name)]) {
1290
+ for (const packageName of [...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name), "sharp"]) {
1054
1291
  const entry = parsed.packages[`node_modules/${packageName}`];
1055
1292
  if (!isObjectRecord(entry))
1056
1293
  continue;
@@ -1061,6 +1298,13 @@ function readPackageLock(root) {
1061
1298
  linked: entry.link === true,
1062
1299
  });
1063
1300
  }
1301
+ const sharpResolutions = Object.entries(parsed.packages)
1302
+ .filter(([packagePath]) => packagePath === "node_modules/sharp" || packagePath.endsWith("/node_modules/sharp"))
1303
+ .map(([packagePath, entry]) => ({
1304
+ path: packagePath,
1305
+ version: isObjectRecord(entry) && typeof entry.version === "string" ? entry.version : null,
1306
+ }))
1307
+ .sort((left, right) => left.path.localeCompare(right.path));
1064
1308
  return {
1065
1309
  state: "valid",
1066
1310
  lockfileVersion,
@@ -1068,6 +1312,7 @@ function readPackageLock(root) {
1068
1312
  rootDevDependencies,
1069
1313
  rootEngines,
1070
1314
  resolvedPackages,
1315
+ sharpResolutions,
1071
1316
  fingerprint: {
1072
1317
  state: "valid",
1073
1318
  lockfileVersion,
@@ -1080,6 +1325,7 @@ function readPackageLock(root) {
1080
1325
  rootDevDependencies,
1081
1326
  rootEngines,
1082
1327
  resolvedPackages,
1328
+ sharpResolutions,
1083
1329
  fingerprint: {},
1084
1330
  }, packageName)])),
1085
1331
  node: rootEngines.node ?? null,
@@ -1088,6 +1334,7 @@ function readPackageLock(root) {
1088
1334
  packageName,
1089
1335
  { version: entry.version, linked: entry.linked },
1090
1336
  ])),
1337
+ sharp: sharpResolutions,
1091
1338
  },
1092
1339
  };
1093
1340
  }
@@ -1099,6 +1346,7 @@ function emptyPackageLockInspection(state, extra = {}) {
1099
1346
  rootDevDependencies: {},
1100
1347
  rootEngines: {},
1101
1348
  resolvedPackages: new Map(),
1349
+ sharpResolutions: [],
1102
1350
  fingerprint: { state, ...extra },
1103
1351
  };
1104
1352
  }