@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.
Files changed (48) 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 +3 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +91 -20
  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 +246 -37
  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 +9 -2
  22. package/dist/project.d.ts.map +1 -1
  23. package/dist/project.js +290 -44
  24. package/dist/project.js.map +1 -1
  25. package/dist/release-catalog.d.ts.map +1 -1
  26. package/dist/release-catalog.js +1405 -0
  27. package/dist/release-catalog.js.map +1 -1
  28. package/dist/release-contract-compatibility.d.ts +133 -0
  29. package/dist/release-contract-compatibility.d.ts.map +1 -1
  30. package/dist/release-contract-compatibility.js +133 -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 +38 -0
  37. package/dist/rendered-content.d.ts.map +1 -0
  38. package/dist/rendered-content.js +149 -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/dist/verification-policy.d.ts +99 -0
  45. package/dist/verification-policy.d.ts.map +1 -0
  46. package/dist/verification-policy.js +219 -0
  47. package/dist/verification-policy.js.map +1 -0
  48. package/package.json +20 -2
package/dist/project.js CHANGED
@@ -2,20 +2,35 @@ 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";
13
+ import { applyBrandlabVerificationPolicy, BRANDLAB_VERIFICATION_POLICY_VERSION, summarizeVerificationFindings, } from "./verification-policy.js";
11
14
  export const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v1";
12
- export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "control-plane-v1";
15
+ export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "simplified-v1";
13
16
  export const BRANDLAB_ARCHITECTURE_DISPATCH_RULESET_ID = "version-aware-architecture-dispatch-v1";
14
17
  const LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT = "architecture-v2";
15
18
  const LEGACY_CONFIGURATION_CONTRACT = "legacy-config-v1";
16
19
  const CONFIGURATION_V2_CONTRACT = "project-config-v2";
17
20
  const CONFIGURATION_V3_CONTRACT = "project-config-v3";
21
+ const CONFIGURATION_V4_CONTRACT = "project-config-v4";
22
+ const CONFIGURATION_V5_CONTRACT = "project-config-v5";
18
23
  const SEO_V3_CONTRACT = "seo-v3";
24
+ const SEO_V4_CONTRACT = "seo-v4";
25
+ const ARCHITECTURE_V5_CONTRACT = "architecture-v5";
26
+ const ANALYTICS_V3_CONTRACT = "analytics-v3";
27
+ const ANALYTICS_V4_CONTRACT = "analytics-v4";
28
+ function isSeoV3Compatible(contract) {
29
+ return contract === SEO_V3_CONTRACT || contract === SEO_V4_CONTRACT;
30
+ }
31
+ function isAnalyticsV3Compatible(contract) {
32
+ return contract === ANALYTICS_V3_CONTRACT || contract === ANALYTICS_V4_CONTRACT;
33
+ }
19
34
  const RUNTIME_PACKAGE_FIELDS = [
20
35
  ["next", "next"],
21
36
  ["react", "react"],
@@ -59,7 +74,6 @@ export function evaluateBrandlabProject(options) {
59
74
  installedAttestation: installedRelease
60
75
  ? releaseManifestByVersion.get(installedRelease) ?? null
61
76
  : null,
62
- releaseAttestationsRequired: options.releaseManifests !== undefined,
63
77
  supportPolicy,
64
78
  });
65
79
  addMigrationFindings({
@@ -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,11 +201,14 @@ 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
  }
165
208
  const sortedFindings = findings.sort(compareFindings);
209
+ const policyResult = applyBrandlabVerificationPolicy(sortedFindings);
210
+ const operationalFindings = policyResult.operationalFindings.sort(compareFindings);
211
+ const diagnosticFindings = policyResult.diagnostics.sort(compareFindings);
166
212
  const authorityDigest = digestBrandlabAuthority({
167
213
  releaseCatalog,
168
214
  supportPolicy,
@@ -172,14 +218,25 @@ export function evaluateBrandlabProject(options) {
172
218
  installed: installedManifest?.contracts ?? null,
173
219
  target: targetManifest?.contracts ?? null,
174
220
  evaluator: BRANDLAB_ARCHITECTURE_DISPATCH_RULESET_ID,
221
+ verificationPolicy: BRANDLAB_VERIFICATION_POLICY_VERSION,
175
222
  rulesets: {
176
223
  configurationV2: BRANDLAB_CONFIG_V2_RULESET_ID,
177
224
  configurationV3: BRANDLAB_CONFIG_V3_RULESET_ID,
225
+ configurationV4: BRANDLAB_CONFIG_V4_RULESET_ID,
226
+ configurationV5: BRANDLAB_CONFIG_V5_RULESET_ID,
178
227
  seoV3: BRANDLAB_SCHEMA_HYGIENE_V3_RULESET_ID,
179
228
  installedSourceSafety: installedSourceSafety?.id ?? null,
180
229
  targetSourceSafety: targetSourceSafety?.id ?? null,
181
230
  installedLegacyArchitecture: legacyArchitectureRulesetId(installedManifest?.contracts.architecture),
182
231
  targetLegacyArchitecture: legacyArchitectureRulesetId(targetManifest?.contracts.architecture),
232
+ projectDocumentation: installedManifest?.contracts.architecture === ARCHITECTURE_V5_CONTRACT ||
233
+ targetManifest?.contracts.architecture === ARCHITECTURE_V5_CONTRACT
234
+ ? BRANDLAB_PROJECT_DOCUMENTATION_RULESET_ID
235
+ : null,
236
+ analyticsV3: isAnalyticsV3Compatible(installedManifest?.contracts.analytics) ||
237
+ isAnalyticsV3Compatible(targetManifest?.contracts.analytics)
238
+ ? BRANDLAB_ANALYTICS_V3_RULESET_ID
239
+ : null,
183
240
  },
184
241
  });
185
242
  const implementationFingerprint = digestBrandlabAuthority({
@@ -187,7 +244,16 @@ export function evaluateBrandlabProject(options) {
187
244
  packageLock: packageLock.fingerprint,
188
245
  brandlabConfig: readOptionalText(join(root, "brandlab.config.ts")),
189
246
  architecture: architectureFingerprint,
190
- evidence: sortedFindings.map(({ ruleId, classification, severity, path, line }) => ({
247
+ documentation: documentationFingerprint,
248
+ analytics: analyticsFingerprint,
249
+ evidence: operationalFindings.map(({ ruleId, classification, severity, path, line }) => ({
250
+ ruleId,
251
+ classification,
252
+ severity,
253
+ path: path ?? null,
254
+ line: line ?? null,
255
+ })),
256
+ diagnostics: diagnosticFindings.map(({ ruleId, classification, severity, path, line }) => ({
191
257
  ruleId,
192
258
  classification,
193
259
  severity,
@@ -205,11 +271,98 @@ export function evaluateBrandlabProject(options) {
205
271
  installedRelease,
206
272
  targetRelease,
207
273
  findings: sortedFindings,
274
+ operationalFindings,
275
+ diagnostics: {
276
+ findings: diagnosticFindings,
277
+ maintenance: summarizeVerificationFindings(diagnosticFindings, "maintenance"),
278
+ migration: summarizeVerificationFindings(diagnosticFindings, "migration"),
279
+ advisory: summarizeVerificationFindings(diagnosticFindings, "advisory"),
280
+ },
281
+ verification: policyResult.verification,
208
282
  maintenance: summarize(sortedFindings, "maintenance"),
209
283
  migration: summarize(sortedFindings, "migration"),
210
284
  advisory: summarize(sortedFindings, "advisory"),
211
285
  };
212
286
  }
287
+ function evaluateApplicableProjectDocumentationContracts(options) {
288
+ const installedArchitecture = options.installedManifest?.contracts.architecture ?? null;
289
+ const targetArchitecture = options.targetManifest?.contracts.architecture ?? null;
290
+ if (installedArchitecture !== ARCHITECTURE_V5_CONTRACT &&
291
+ targetArchitecture !== ARCHITECTURE_V5_CONTRACT) {
292
+ return null;
293
+ }
294
+ const report = inspectBrandlabProjectDocumentation(options.root);
295
+ if (installedArchitecture === ARCHITECTURE_V5_CONTRACT) {
296
+ for (const issue of report.issues) {
297
+ options.findings.push(mapProjectDocumentationIssue(issue, "maintenance"));
298
+ }
299
+ }
300
+ else if (targetArchitecture === ARCHITECTURE_V5_CONTRACT) {
301
+ for (const issue of report.issues) {
302
+ options.findings.push(mapProjectDocumentationIssue(issue, "migration"));
303
+ }
304
+ }
305
+ return {
306
+ rulesetId: report.rulesetId,
307
+ files: report.files,
308
+ applicableRuleIds: report.issues.map((issue) => issue.ruleId).sort(),
309
+ };
310
+ }
311
+ function mapProjectDocumentationIssue(issue, classification) {
312
+ return {
313
+ ruleId: issue.ruleId,
314
+ surface: issue.surface,
315
+ classification,
316
+ severity: issue.severity,
317
+ contract: { name: "architecture", version: ARCHITECTURE_V5_CONTRACT },
318
+ evidenceKind: issue.evidenceKind,
319
+ ...(issue.path ? { path: issue.path } : {}),
320
+ ...(issue.line ? { line: issue.line } : {}),
321
+ expected: issue.expected,
322
+ actual: issue.actual,
323
+ remediation: issue.remediation,
324
+ };
325
+ }
326
+ function evaluateApplicableAnalyticsContracts(options) {
327
+ const installedAnalytics = options.installedManifest?.contracts.analytics ?? null;
328
+ const targetAnalytics = options.targetManifest?.contracts.analytics ?? null;
329
+ if (!isAnalyticsV3Compatible(installedAnalytics) &&
330
+ !isAnalyticsV3Compatible(targetAnalytics)) {
331
+ return null;
332
+ }
333
+ const inspection = inspectBrandlabAnalyticsV3(options.root);
334
+ if (isAnalyticsV3Compatible(installedAnalytics)) {
335
+ for (const issue of inspection.issues) {
336
+ options.findings.push(mapAnalyticsV3Issue(issue, "maintenance", installedAnalytics));
337
+ }
338
+ }
339
+ else if (isAnalyticsV3Compatible(targetAnalytics)) {
340
+ for (const issue of inspection.issues) {
341
+ options.findings.push(mapAnalyticsV3Issue(issue, "migration", targetAnalytics));
342
+ }
343
+ }
344
+ return {
345
+ rulesetId: inspection.rulesetId,
346
+ websiteCallsDeclared: inspection.websiteCallsDeclared,
347
+ leadDiagnosticsDeclared: inspection.leadDiagnosticsDeclared,
348
+ applicableRuleIds: inspection.issues.map((issue) => issue.ruleId).sort(),
349
+ };
350
+ }
351
+ function mapAnalyticsV3Issue(issue, classification, contractVersion) {
352
+ return {
353
+ ruleId: issue.ruleId,
354
+ surface: issue.surface,
355
+ classification,
356
+ severity: issue.severity,
357
+ contract: { name: "analytics", version: contractVersion },
358
+ evidenceKind: issue.evidenceKind,
359
+ ...(issue.path ? { path: issue.path } : {}),
360
+ ...(issue.line ? { line: issue.line } : {}),
361
+ expected: issue.expected,
362
+ actual: issue.actual,
363
+ remediation: issue.remediation,
364
+ };
365
+ }
213
366
  function addSourcePatternFindings(options) {
214
367
  for (const match of findForbiddenBrandlabPatterns({
215
368
  roots: [options.sourceRoot],
@@ -231,9 +384,9 @@ function addSourcePatternFindings(options) {
231
384
  }
232
385
  }
233
386
  export function shouldFailBrandlabConformance(result) {
234
- if (result.maintenance.blockers > 0)
387
+ if (result.verification.maintenance.blockers > 0)
235
388
  return true;
236
- return result.mode === "preflight" && result.migration.blockers > 0;
389
+ return result.mode === "preflight" && result.verification.migration.blockers > 0;
237
390
  }
238
391
  export function detectInstalledBrandlabRelease(packageJson) {
239
392
  if (!packageJson)
@@ -325,17 +478,6 @@ function addReleaseMaintenanceFindings(options) {
325
478
  remediation: "Use a supported exact release or add and verify its external manifest and runtime projection.",
326
479
  }));
327
480
  }
328
- if (options.releaseAttestationsRequired &&
329
- options.installedManifest &&
330
- !options.installedAttestation) {
331
- options.findings.push(releaseFinding({
332
- ruleId: "brandlab-release-attestation-missing",
333
- severity: "blocker",
334
- expected: `Immutable registry attestation for ${installedRelease}`,
335
- actual: "The operational release-manifest set does not attest the installed release.",
336
- remediation: "Load the installed release manifest referenced by support policy before accepting package provenance.",
337
- }));
338
- }
339
481
  addInstalledPackageLockFindings({
340
482
  findings: options.findings,
341
483
  packageJson,
@@ -550,16 +692,6 @@ function addInstalledPackageLockFindings(options) {
550
692
  }
551
693
  const registryAttestation = options.installedAttestation?.packages[packageName].registry ?? null;
552
694
  const expectedIntegrity = registryAttestation?.integrity ?? null;
553
- if (registryAttestation &&
554
- (registryAttestation.state !== "verified" || !expectedIntegrity)) {
555
- options.findings.push(releaseFinding({
556
- ruleId: "brandlab-release-registry-attestation-unverified",
557
- severity: "blocker",
558
- expected: `${packageName} with verified immutable npm registry integrity`,
559
- actual: `The ${options.installedRelease} release manifest does not contain verified registry integrity for ${packageName}.`,
560
- remediation: "Do not adopt this release until publication evidence is finalized and independently verified.",
561
- }));
562
- }
563
695
  if (expectedIntegrity && resolvedPackage?.integrity !== expectedIntegrity) {
564
696
  options.findings.push(releaseFinding({
565
697
  ruleId: "brandlab-release-lock-integrity-mismatch",
@@ -572,9 +704,8 @@ function addInstalledPackageLockFindings(options) {
572
704
  path: "package-lock.json",
573
705
  }));
574
706
  }
575
- if (options.installedAttestation &&
576
- (!resolvedPackage?.resolved ||
577
- !isApprovedBrandlabRegistryResolution(resolvedPackage.resolved, packageName, options.installedRelease))) {
707
+ if (!resolvedPackage?.resolved ||
708
+ !isApprovedBrandlabRegistryResolution(resolvedPackage.resolved, packageName, options.installedRelease)) {
578
709
  options.findings.push(releaseFinding({
579
710
  ruleId: "brandlab-release-lock-registry-source-mismatch",
580
711
  severity: "blocker",
@@ -674,6 +805,13 @@ function addInstalledRuntimeFindings(options) {
674
805
  path: "package.json",
675
806
  }));
676
807
  }
808
+ addSharpRuntimeFindings({
809
+ findings: options.findings,
810
+ packageJson: options.packageJson,
811
+ packageLock: options.packageLock,
812
+ manifest: options.manifest,
813
+ classification: "maintenance",
814
+ });
677
815
  }
678
816
  function addTargetRuntimeFindings(options) {
679
817
  if (!options.packageJson)
@@ -713,6 +851,16 @@ function addTargetRuntimeFindings(options) {
713
851
  if (options.packageJson.packageManager !== options.targetManifest.runtime.packageManager) {
714
852
  runtimeMismatches.push(`packageManager(expected=${options.targetManifest.runtime.packageManager},actual=${options.packageJson.packageManager ?? "missing"})`);
715
853
  }
854
+ const targetSharp = options.targetManifest.runtime.sharp;
855
+ if (targetSharp) {
856
+ const actualOverride = options.packageJson.overrides?.sharp ?? null;
857
+ const sharpResolutions = options.packageLock.sharpResolutions;
858
+ if (actualOverride !== targetSharp ||
859
+ sharpResolutions.length !== 1 ||
860
+ sharpResolutions[0]?.version !== targetSharp) {
861
+ runtimeMismatches.push(`sharp(expected=${targetSharp},override=${actualOverride ?? "missing"},resolutions=${formatSharpResolutions(sharpResolutions)})`);
862
+ }
863
+ }
716
864
  if (runtimeMismatches.length > 0) {
717
865
  options.findings.push(migrationRuntimeFinding({
718
866
  ruleId: "brandlab-migration-runtime-baseline",
@@ -722,6 +870,65 @@ function addTargetRuntimeFindings(options) {
722
870
  }));
723
871
  }
724
872
  }
873
+ function addSharpRuntimeFindings(options) {
874
+ const expected = options.manifest.runtime.sharp;
875
+ if (!expected)
876
+ return;
877
+ const actualOverride = options.packageJson.overrides?.sharp ?? null;
878
+ if (actualOverride !== expected) {
879
+ options.findings.push(runtimeFinding({
880
+ ruleId: "brandlab-runtime-sharp-override",
881
+ expected: `overrides.sharp=${expected}`,
882
+ actual: `overrides.sharp=${actualOverride ?? "missing"}.`,
883
+ remediation: "Add the release-owned Sharp override so Next.js and site-next use one security-patched native runtime.",
884
+ path: "package.json",
885
+ }));
886
+ }
887
+ if (options.packageLock.state !== "valid")
888
+ return;
889
+ if (options.packageLock.sharpResolutions.length !== 1 ||
890
+ options.packageLock.sharpResolutions[0]?.version !== expected) {
891
+ options.findings.push(runtimeFinding({
892
+ ruleId: "brandlab-runtime-sharp-resolution",
893
+ expected: `one sharp@${expected} lockfile resolution`,
894
+ actual: formatSharpResolutions(options.packageLock.sharpResolutions),
895
+ remediation: "Regenerate package-lock.json after applying the exact release-owned Sharp override.",
896
+ path: "package-lock.json",
897
+ }));
898
+ }
899
+ }
900
+ function addContactUploadFilenameFindings(options) {
901
+ if (options.manifest.contracts.contact !== "contact-v4" &&
902
+ !options.manifest.capabilities.includes("client-safe-contact-upload-filenames")) {
903
+ return;
904
+ }
905
+ const path = "src/lib/contact/imageUploads.ts";
906
+ const source = readOptionalText(join(options.root, path));
907
+ // Raw File objects already retain their reviewed names. This rule is only
908
+ // relevant when a project owns a client-side image processing adapter.
909
+ if (!source || /\bnormalizeContactUploadFile\s*\(/.test(source))
910
+ return;
911
+ options.findings.push({
912
+ ruleId: "brandlab-contact-upload-filename-normalization",
913
+ surface: "contact",
914
+ classification: options.classification,
915
+ severity: "blocker",
916
+ contract: {
917
+ name: "contact",
918
+ version: options.manifest.contracts.contact,
919
+ },
920
+ evidenceKind: "static",
921
+ path,
922
+ expected: "Compressed contact images are wrapped with normalizeContactUploadFile before FormData submission.",
923
+ actual: "The contact upload module does not use the shared filename normalizer.",
924
+ remediation: "Use normalizeContactUploadFile(original, processed) from @brandlab-digital/site-next/contact so anonymous browser Blobs retain a valid reviewed filename.",
925
+ });
926
+ }
927
+ function formatSharpResolutions(resolutions) {
928
+ return resolutions.length === 0
929
+ ? "none"
930
+ : resolutions.map((entry) => `${entry.path}=${entry.version ?? "missing"}`).join(", ");
931
+ }
725
932
  function runtimeFinding(options) {
726
933
  return {
727
934
  ruleId: options.ruleId,
@@ -760,10 +967,14 @@ function evaluateApplicableArchitectureContracts(options) {
760
967
  configV2Inspection ??= inspectImmutableBrandlabConfigV2(options.root);
761
968
  return configV2Inspection;
762
969
  };
763
- let configV3Inspection = null;
764
- const inspectConfigV3 = () => {
765
- configV3Inspection ??= inspectBrandlabConfigV3(options.root);
766
- return configV3Inspection;
970
+ const configV3Inspections = new Map();
971
+ const inspectConfigV3 = (contractVersion) => {
972
+ const cached = configV3Inspections.get(contractVersion);
973
+ if (cached)
974
+ return cached;
975
+ const inspection = inspectBrandlabConfigV3(options.root, { contractVersion });
976
+ configV3Inspections.set(contractVersion, inspection);
977
+ return inspection;
767
978
  };
768
979
  const legacyInspections = new Map();
769
980
  let legacyArchitectureInput;
@@ -793,7 +1004,19 @@ function evaluateApplicableArchitectureContracts(options) {
793
1004
  }
794
1005
  }
795
1006
  else if (installedConfiguration === CONFIGURATION_V3_CONTRACT) {
796
- const inspection = inspectConfigV3();
1007
+ const inspection = inspectConfigV3(CONFIGURATION_V3_CONTRACT);
1008
+ for (const issue of inspection.issues) {
1009
+ options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
1010
+ }
1011
+ }
1012
+ else if (installedConfiguration === CONFIGURATION_V4_CONTRACT) {
1013
+ const inspection = inspectConfigV3(CONFIGURATION_V4_CONTRACT);
1014
+ for (const issue of inspection.issues) {
1015
+ options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
1016
+ }
1017
+ }
1018
+ else if (installedConfiguration === CONFIGURATION_V5_CONTRACT) {
1019
+ const inspection = inspectConfigV3(CONFIGURATION_V5_CONTRACT);
797
1020
  for (const issue of inspection.issues) {
798
1021
  options.findings.push(mapConfigIssue(issue, "maintenance", options.installedManifest?.contracts));
799
1022
  }
@@ -826,9 +1049,9 @@ function evaluateApplicableArchitectureContracts(options) {
826
1049
  // supported target release is explicitly selected.
827
1050
  const configSource = readOptionalText(join(options.root, "brandlab.config.ts"));
828
1051
  if (/\bdefineBrandlabProject\s*\(/.test(configSource ?? "")) {
829
- for (const issue of inspectConfigV3().issues) {
1052
+ for (const issue of inspectConfigV3(CONFIGURATION_V5_CONTRACT).issues) {
830
1053
  options.findings.push(mapConfigIssue(issue, "advisory", {
831
- configuration: CONFIGURATION_V3_CONTRACT,
1054
+ configuration: CONFIGURATION_V5_CONTRACT,
832
1055
  }));
833
1056
  }
834
1057
  }
@@ -846,7 +1069,17 @@ function evaluateApplicableArchitectureContracts(options) {
846
1069
  }
847
1070
  }
848
1071
  else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V3_CONTRACT) {
849
- for (const issue of inspectConfigV3().issues) {
1072
+ for (const issue of inspectConfigV3(CONFIGURATION_V3_CONTRACT).issues) {
1073
+ options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
1074
+ }
1075
+ }
1076
+ else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V4_CONTRACT) {
1077
+ for (const issue of inspectConfigV3(CONFIGURATION_V4_CONTRACT).issues) {
1078
+ options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
1079
+ }
1080
+ }
1081
+ else if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V5_CONTRACT) {
1082
+ for (const issue of inspectConfigV3(CONFIGURATION_V5_CONTRACT).issues) {
850
1083
  options.findings.push(mapConfigIssue(issue, "migration", options.targetManifest?.contracts));
851
1084
  }
852
1085
  }
@@ -867,7 +1100,9 @@ function evaluateApplicableArchitectureContracts(options) {
867
1100
  }
868
1101
  }
869
1102
  const inspectedConfigV2 = configV2Inspection;
870
- const inspectedConfigV3 = configV3Inspection;
1103
+ const inspectedConfigV3 = (configV3Inspections.get(CONFIGURATION_V4_CONTRACT) ??
1104
+ configV3Inspections.get(CONFIGURATION_V3_CONTRACT) ??
1105
+ null);
871
1106
  return {
872
1107
  evaluator: BRANDLAB_ARCHITECTURE_DISPATCH_RULESET_ID,
873
1108
  installedContract: options.installedManifest?.contracts.architecture ?? null,
@@ -1050,7 +1285,7 @@ function readPackageLock(root) {
1050
1285
  const rootDevDependencies = stringRecord(rootPackage.devDependencies);
1051
1286
  const rootEngines = stringRecord(rootPackage.engines);
1052
1287
  const resolvedPackages = new Map();
1053
- for (const packageName of [...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name)]) {
1288
+ for (const packageName of [...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name), "sharp"]) {
1054
1289
  const entry = parsed.packages[`node_modules/${packageName}`];
1055
1290
  if (!isObjectRecord(entry))
1056
1291
  continue;
@@ -1061,6 +1296,13 @@ function readPackageLock(root) {
1061
1296
  linked: entry.link === true,
1062
1297
  });
1063
1298
  }
1299
+ const sharpResolutions = Object.entries(parsed.packages)
1300
+ .filter(([packagePath]) => packagePath === "node_modules/sharp" || packagePath.endsWith("/node_modules/sharp"))
1301
+ .map(([packagePath, entry]) => ({
1302
+ path: packagePath,
1303
+ version: isObjectRecord(entry) && typeof entry.version === "string" ? entry.version : null,
1304
+ }))
1305
+ .sort((left, right) => left.path.localeCompare(right.path));
1064
1306
  return {
1065
1307
  state: "valid",
1066
1308
  lockfileVersion,
@@ -1068,6 +1310,7 @@ function readPackageLock(root) {
1068
1310
  rootDevDependencies,
1069
1311
  rootEngines,
1070
1312
  resolvedPackages,
1313
+ sharpResolutions,
1071
1314
  fingerprint: {
1072
1315
  state: "valid",
1073
1316
  lockfileVersion,
@@ -1080,6 +1323,7 @@ function readPackageLock(root) {
1080
1323
  rootDevDependencies,
1081
1324
  rootEngines,
1082
1325
  resolvedPackages,
1326
+ sharpResolutions,
1083
1327
  fingerprint: {},
1084
1328
  }, packageName)])),
1085
1329
  node: rootEngines.node ?? null,
@@ -1088,6 +1332,7 @@ function readPackageLock(root) {
1088
1332
  packageName,
1089
1333
  { version: entry.version, linked: entry.linked },
1090
1334
  ])),
1335
+ sharp: sharpResolutions,
1091
1336
  },
1092
1337
  };
1093
1338
  }
@@ -1099,6 +1344,7 @@ function emptyPackageLockInspection(state, extra = {}) {
1099
1344
  rootDevDependencies: {},
1100
1345
  rootEngines: {},
1101
1346
  resolvedPackages: new Map(),
1347
+ sharpResolutions: [],
1102
1348
  fingerprint: { state, ...extra },
1103
1349
  };
1104
1350
  }