@brandlab-digital/conformance 0.1.0-alpha.8 → 1.0.0-rc.1
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/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/locations.d.ts +63 -0
- package/dist/locations.d.ts.map +1 -0
- package/dist/locations.js +105 -0
- package/dist/locations.js.map +1 -0
- package/dist/project-config.d.ts +31 -0
- package/dist/project-config.d.ts.map +1 -0
- package/dist/project-config.js +917 -0
- package/dist/project-config.js.map +1 -0
- package/dist/project.d.ts +67 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +1039 -0
- package/dist/project.js.map +1 -0
- package/dist/release-catalog.d.ts +4 -0
- package/dist/release-catalog.d.ts.map +1 -0
- package/dist/release-catalog.js +256 -0
- package/dist/release-catalog.js.map +1 -0
- package/dist/release-manifest.d.ts +119 -0
- package/dist/release-manifest.d.ts.map +1 -0
- package/dist/release-manifest.js +445 -0
- package/dist/release-manifest.js.map +1 -0
- package/dist/schema-hygiene.d.ts +25 -0
- package/dist/schema-hygiene.d.ts.map +1 -0
- package/dist/schema-hygiene.js +19 -0
- package/dist/schema-hygiene.js.map +1 -0
- package/dist/vitest.d.ts +17 -5
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +2414 -129
- package/dist/vitest.js.map +1 -1
- package/package.json +35 -1
package/dist/project.js
ADDED
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { BRANDLAB_FORBIDDEN_SOURCE_PATTERNS, evaluateBrandlabArchitecture, evaluateSharedPackageVersionSpecs, findForbiddenBrandlabPatterns, } from "./vitest.js";
|
|
4
|
+
import { DEFAULT_BRANDLAB_RELEASE_CATALOG, } from "./release-catalog.js";
|
|
5
|
+
import { inspectBrandlabConfigV2, } from "./project-config.js";
|
|
6
|
+
import { BRANDLAB_SHARED_PACKAGE_NAMES, compareBrandlabSemver, digestBrandlabAuthority, isBrandlabSemver, validateBrandlabReleaseCatalog, validateBrandlabRuntimeReleaseCatalog, validateBrandlabRuntimeReleaseCatalogAgainstManifests, validateBrandlabSupportPolicy, } from "./release-manifest.js";
|
|
7
|
+
export const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v1";
|
|
8
|
+
export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "control-plane-v1";
|
|
9
|
+
export const BRANDLAB_ARCHITECTURE_EVALUATOR_CONTRACT = "architecture-v3";
|
|
10
|
+
const LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT = "architecture-v2";
|
|
11
|
+
const LEGACY_CONFIGURATION_CONTRACT = "legacy-config-v1";
|
|
12
|
+
const CONFIGURATION_V2_CONTRACT = "project-config-v2";
|
|
13
|
+
const RUNTIME_PACKAGE_FIELDS = [
|
|
14
|
+
["next", "next"],
|
|
15
|
+
["react", "react"],
|
|
16
|
+
["react-dom", "reactDom"],
|
|
17
|
+
["typescript", "typescript"],
|
|
18
|
+
];
|
|
19
|
+
export function evaluateBrandlabProject(options) {
|
|
20
|
+
const root = resolve(options.root);
|
|
21
|
+
const mode = options.mode ?? "local";
|
|
22
|
+
const releaseCatalog = options.releaseCatalog ?? DEFAULT_BRANDLAB_RELEASE_CATALOG;
|
|
23
|
+
const supportPolicy = options.supportPolicy;
|
|
24
|
+
validateBrandlabRuntimeReleaseCatalog(releaseCatalog);
|
|
25
|
+
if (options.releaseManifests) {
|
|
26
|
+
validateBrandlabReleaseCatalog(options.releaseManifests);
|
|
27
|
+
validateBrandlabRuntimeReleaseCatalogAgainstManifests(releaseCatalog, options.releaseManifests);
|
|
28
|
+
}
|
|
29
|
+
validateBrandlabSupportPolicy(supportPolicy, releaseCatalog);
|
|
30
|
+
const packageJson = readPackageJson(root);
|
|
31
|
+
const packageLock = readPackageLock(root);
|
|
32
|
+
const detectedRelease = detectInstalledBrandlabRelease(packageJson);
|
|
33
|
+
const installedRelease = options.installedRelease === undefined
|
|
34
|
+
? detectedRelease
|
|
35
|
+
: options.installedRelease;
|
|
36
|
+
const targetRelease = options.targetRelease ?? null;
|
|
37
|
+
const releaseByVersion = new Map(releaseCatalog.map((manifest) => [manifest.releaseVersion, manifest]));
|
|
38
|
+
const installedManifest = installedRelease
|
|
39
|
+
? releaseByVersion.get(installedRelease) ?? null
|
|
40
|
+
: null;
|
|
41
|
+
const targetManifest = targetRelease
|
|
42
|
+
? releaseByVersion.get(targetRelease) ?? null
|
|
43
|
+
: null;
|
|
44
|
+
const releaseManifestByVersion = new Map((options.releaseManifests ?? []).map((manifest) => [manifest.releaseVersion, manifest]));
|
|
45
|
+
const findings = [];
|
|
46
|
+
addReleaseMaintenanceFindings({
|
|
47
|
+
findings,
|
|
48
|
+
packageJson,
|
|
49
|
+
packageLock,
|
|
50
|
+
installedRelease,
|
|
51
|
+
detectedRelease,
|
|
52
|
+
installedManifest,
|
|
53
|
+
installedAttestation: installedRelease
|
|
54
|
+
? releaseManifestByVersion.get(installedRelease) ?? null
|
|
55
|
+
: null,
|
|
56
|
+
supportPolicy,
|
|
57
|
+
});
|
|
58
|
+
addMigrationFindings({
|
|
59
|
+
findings,
|
|
60
|
+
installedRelease,
|
|
61
|
+
targetRelease,
|
|
62
|
+
targetManifest,
|
|
63
|
+
supportPolicy,
|
|
64
|
+
packageJson,
|
|
65
|
+
packageLock,
|
|
66
|
+
});
|
|
67
|
+
const architectureFingerprint = evaluateApplicableArchitectureContracts({
|
|
68
|
+
root,
|
|
69
|
+
findings,
|
|
70
|
+
installedManifest,
|
|
71
|
+
targetManifest,
|
|
72
|
+
targetRelease,
|
|
73
|
+
architecture: options.architecture,
|
|
74
|
+
});
|
|
75
|
+
const sourceRoot = join(root, "src");
|
|
76
|
+
if (existsSync(sourceRoot)) {
|
|
77
|
+
for (const match of findForbiddenBrandlabPatterns({
|
|
78
|
+
roots: [sourceRoot],
|
|
79
|
+
patterns: BRANDLAB_FORBIDDEN_SOURCE_PATTERNS,
|
|
80
|
+
})) {
|
|
81
|
+
findings.push({
|
|
82
|
+
ruleId: `brandlab-source-${slugify(match.patternName)}`,
|
|
83
|
+
surface: surfaceForRule(match.patternName),
|
|
84
|
+
classification: "maintenance",
|
|
85
|
+
severity: "blocker",
|
|
86
|
+
contract: {
|
|
87
|
+
name: "source-safety",
|
|
88
|
+
version: installedManifest?.contracts.architecture ?? "unversioned",
|
|
89
|
+
},
|
|
90
|
+
evidenceKind: "static",
|
|
91
|
+
path: normalizePath(relative(root, match.file)),
|
|
92
|
+
line: match.line,
|
|
93
|
+
expected: `No ${match.patternName} source pattern`,
|
|
94
|
+
actual: `A prohibited source pattern was detected at line ${match.line}.`,
|
|
95
|
+
remediation: `Remove the ${match.patternName} implementation and use the supported shared-platform path.`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const sortedFindings = findings.sort(compareFindings);
|
|
100
|
+
const authorityDigest = digestBrandlabAuthority({
|
|
101
|
+
releaseCatalog,
|
|
102
|
+
supportPolicy,
|
|
103
|
+
releaseManifests: options.releaseManifests ?? null,
|
|
104
|
+
});
|
|
105
|
+
const contractDigest = digestBrandlabAuthority({
|
|
106
|
+
installed: installedManifest?.contracts ?? null,
|
|
107
|
+
target: targetManifest?.contracts ?? null,
|
|
108
|
+
evaluator: BRANDLAB_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
109
|
+
});
|
|
110
|
+
const implementationFingerprint = digestBrandlabAuthority({
|
|
111
|
+
packageJson: packageJson ?? null,
|
|
112
|
+
packageLock: packageLock.fingerprint,
|
|
113
|
+
brandlabConfig: readOptionalText(join(root, "brandlab.config.ts")),
|
|
114
|
+
architecture: architectureFingerprint,
|
|
115
|
+
evidence: sortedFindings.map(({ ruleId, classification, severity, path, line }) => ({
|
|
116
|
+
ruleId,
|
|
117
|
+
classification,
|
|
118
|
+
severity,
|
|
119
|
+
path: path ?? null,
|
|
120
|
+
line: line ?? null,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
schemaVersion: BRANDLAB_CONFORMANCE_SCHEMA_VERSION,
|
|
125
|
+
evaluatorVersion: BRANDLAB_CONFORMANCE_EVALUATOR_VERSION,
|
|
126
|
+
authorityDigest,
|
|
127
|
+
contractDigest,
|
|
128
|
+
implementationFingerprint,
|
|
129
|
+
mode,
|
|
130
|
+
installedRelease,
|
|
131
|
+
targetRelease,
|
|
132
|
+
findings: sortedFindings,
|
|
133
|
+
maintenance: summarize(sortedFindings, "maintenance"),
|
|
134
|
+
migration: summarize(sortedFindings, "migration"),
|
|
135
|
+
advisory: summarize(sortedFindings, "advisory"),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function shouldFailBrandlabConformance(result) {
|
|
139
|
+
if (result.maintenance.blockers > 0)
|
|
140
|
+
return true;
|
|
141
|
+
return result.mode === "preflight" && result.migration.blockers > 0;
|
|
142
|
+
}
|
|
143
|
+
export function detectInstalledBrandlabRelease(packageJson) {
|
|
144
|
+
if (!packageJson)
|
|
145
|
+
return null;
|
|
146
|
+
const versions = BRANDLAB_SHARED_PACKAGE_NAMES.map((packageName) => packageJson.dependencies?.[packageName] ??
|
|
147
|
+
packageJson.devDependencies?.[packageName] ??
|
|
148
|
+
null);
|
|
149
|
+
if (versions.some((version) => !version || !isBrandlabSemver(version)))
|
|
150
|
+
return null;
|
|
151
|
+
return new Set(versions).size === 1 ? versions[0] : null;
|
|
152
|
+
}
|
|
153
|
+
export function readBrandlabArchitectureConfig(root) {
|
|
154
|
+
const source = readOptionalText(join(root, "brandlab.config.ts"));
|
|
155
|
+
if (!source)
|
|
156
|
+
return null;
|
|
157
|
+
const architectureSource = extractDelimitedProperty(source, "architecture", "{", "}");
|
|
158
|
+
if (!architectureSource)
|
|
159
|
+
return null;
|
|
160
|
+
const architecture = {};
|
|
161
|
+
assignString(architecture, "profile", extractStringProperty(architectureSource, "profile"));
|
|
162
|
+
assignString(architecture, "status", extractStringProperty(architectureSource, "status"));
|
|
163
|
+
assignString(architecture, "targetProfile", extractStringProperty(architectureSource, "targetProfile"));
|
|
164
|
+
assignString(architecture, "migrationRationale", extractStringProperty(architectureSource, "migrationRationale"));
|
|
165
|
+
assignNumber(architecture, "staticRouteFileLimit", extractNumberProperty(architectureSource, "staticRouteFileLimit"));
|
|
166
|
+
assignNumber(architecture, "largeRouteFileLineLimit", extractNumberProperty(architectureSource, "largeRouteFileLineLimit"));
|
|
167
|
+
assignNumber(architecture, "routeFileContentCharLimit", extractNumberProperty(architectureSource, "routeFileContentCharLimit"));
|
|
168
|
+
const routeFamiliesSource = extractDelimitedProperty(architectureSource, "routeFamilies", "[", "]");
|
|
169
|
+
if (routeFamiliesSource) {
|
|
170
|
+
architecture.routeFamilies = extractObjectBodies(routeFamiliesSource).map((routeFamily) => ({
|
|
171
|
+
name: extractStringProperty(routeFamily, "name") ?? undefined,
|
|
172
|
+
model: extractStringProperty(routeFamily, "model") ?? undefined,
|
|
173
|
+
route: extractStringProperty(routeFamily, "route") ?? undefined,
|
|
174
|
+
dataSource: extractStringProperty(routeFamily, "dataSource") ?? undefined,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
const contentOwnershipSource = extractDelimitedProperty(architectureSource, "contentOwnership", "{", "}");
|
|
178
|
+
if (contentOwnershipSource) {
|
|
179
|
+
architecture.contentOwnership = {
|
|
180
|
+
routeFiles: extractStringProperty(contentOwnershipSource, "routeFiles") ?? undefined,
|
|
181
|
+
pageData: extractStringProperty(contentOwnershipSource, "pageData") ?? undefined,
|
|
182
|
+
componentCopy: extractStringProperty(contentOwnershipSource, "componentCopy") ?? undefined,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const topologySource = extractDelimitedProperty(architectureSource, "topology", "{", "}");
|
|
186
|
+
if (topologySource) {
|
|
187
|
+
architecture.topology = {
|
|
188
|
+
allowedRootDirs: extractStringArray(topologySource, "allowedRootDirs"),
|
|
189
|
+
allowedComponentDirs: extractStringArray(topologySource, "allowedComponentDirs"),
|
|
190
|
+
allowedLibDirs: extractStringArray(topologySource, "allowedLibDirs"),
|
|
191
|
+
allowedTypeFiles: extractStringArray(topologySource, "allowedTypeFiles"),
|
|
192
|
+
legacyAllowedRootDirs: extractStringArray(topologySource, "legacyAllowedRootDirs"),
|
|
193
|
+
legacyAllowedComponentDirs: extractStringArray(topologySource, "legacyAllowedComponentDirs"),
|
|
194
|
+
legacyAllowedLibDirs: extractStringArray(topologySource, "legacyAllowedLibDirs"),
|
|
195
|
+
legacyAllowedTypeFiles: extractStringArray(topologySource, "legacyAllowedTypeFiles"),
|
|
196
|
+
targetTopology: extractStringProperty(topologySource, "targetTopology") ?? undefined,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return architecture;
|
|
200
|
+
}
|
|
201
|
+
function addReleaseMaintenanceFindings(options) {
|
|
202
|
+
const packageJson = options.packageJson;
|
|
203
|
+
if (!packageJson) {
|
|
204
|
+
options.findings.push(releaseFinding({
|
|
205
|
+
ruleId: "brandlab-release-package-json-missing",
|
|
206
|
+
severity: "blocker",
|
|
207
|
+
expected: "A readable project package.json",
|
|
208
|
+
actual: "package.json is missing or invalid.",
|
|
209
|
+
remediation: "Restore a valid package.json before evaluating Brandlab conformance.",
|
|
210
|
+
}));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const installedRelease = options.installedRelease;
|
|
214
|
+
if (!installedRelease) {
|
|
215
|
+
options.findings.push(releaseFinding({
|
|
216
|
+
ruleId: "brandlab-release-installed-version-unresolved",
|
|
217
|
+
severity: "blocker",
|
|
218
|
+
expected: "Four exact, internally aligned Brandlab package versions",
|
|
219
|
+
actual: "The installed Brandlab release could not be resolved.",
|
|
220
|
+
remediation: "Pin all four Brandlab packages to one exact supported release.",
|
|
221
|
+
}));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!options.installedManifest) {
|
|
225
|
+
options.findings.push(releaseFinding({
|
|
226
|
+
ruleId: "brandlab-release-unsupported",
|
|
227
|
+
severity: "blocker",
|
|
228
|
+
expected: "An installed release with an embedded runtime contract",
|
|
229
|
+
actual: `${options.installedRelease} has no runtime release contract.`,
|
|
230
|
+
remediation: "Use a supported exact release or add and verify its external manifest and runtime projection.",
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
addInstalledPackageLockFindings({
|
|
234
|
+
findings: options.findings,
|
|
235
|
+
packageJson,
|
|
236
|
+
packageLock: options.packageLock,
|
|
237
|
+
installedRelease,
|
|
238
|
+
installedAttestation: options.installedAttestation,
|
|
239
|
+
});
|
|
240
|
+
const versionIssues = evaluateSharedPackageVersionSpecs({
|
|
241
|
+
packageJson,
|
|
242
|
+
expectedVersion: installedRelease,
|
|
243
|
+
packageNames: BRANDLAB_SHARED_PACKAGE_NAMES,
|
|
244
|
+
});
|
|
245
|
+
for (const issue of versionIssues) {
|
|
246
|
+
options.findings.push(releaseFinding({
|
|
247
|
+
ruleId: `brandlab-release-package-${issue.issue}`,
|
|
248
|
+
severity: "blocker",
|
|
249
|
+
expected: `${issue.packageName}@${installedRelease}`,
|
|
250
|
+
actual: `${issue.packageName} is ${issue.installedVersion ?? "missing"}.`,
|
|
251
|
+
remediation: "Restore exact lockstep package pins; do not use ranges, tags or mixed releases.",
|
|
252
|
+
path: "package.json",
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
if (options.detectedRelease && options.detectedRelease !== installedRelease) {
|
|
256
|
+
options.findings.push(releaseFinding({
|
|
257
|
+
ruleId: "brandlab-release-declared-installed-mismatch",
|
|
258
|
+
severity: "blocker",
|
|
259
|
+
expected: `Detected package release ${installedRelease}`,
|
|
260
|
+
actual: `package.json resolves to ${options.detectedRelease}.`,
|
|
261
|
+
remediation: "Use the detected exact release or correct all four package pins.",
|
|
262
|
+
path: "package.json",
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
const lifecycle = options.supportPolicy.releases[installedRelease]?.lifecycle;
|
|
266
|
+
if (options.installedManifest && !lifecycle) {
|
|
267
|
+
options.findings.push(releaseFinding({
|
|
268
|
+
ruleId: "brandlab-release-not-operationally-supported",
|
|
269
|
+
severity: "blocker",
|
|
270
|
+
expected: "An installed release listed in mutable support policy",
|
|
271
|
+
actual: `${installedRelease} has a runtime contract but is not approved for operational use.`,
|
|
272
|
+
remediation: "Use the current supported release or supply the synthetic candidate policy only inside packed release fixtures.",
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
if (lifecycle === "security-blocked" || lifecycle === "retired") {
|
|
276
|
+
options.findings.push(releaseFinding({
|
|
277
|
+
ruleId: `brandlab-release-${lifecycle}`,
|
|
278
|
+
severity: "blocker",
|
|
279
|
+
expected: "A current or tolerated installed release",
|
|
280
|
+
actual: `${installedRelease} is ${lifecycle}.`,
|
|
281
|
+
remediation: "Prepare an owner-approved migration to a supported release.",
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
if (options.installedManifest) {
|
|
285
|
+
addInstalledRuntimeFindings({
|
|
286
|
+
findings: options.findings,
|
|
287
|
+
packageJson,
|
|
288
|
+
packageLock: options.packageLock,
|
|
289
|
+
manifest: options.installedManifest,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function addMigrationFindings(options) {
|
|
294
|
+
if (!options.targetRelease) {
|
|
295
|
+
if (options.installedRelease &&
|
|
296
|
+
options.installedRelease !== options.supportPolicy.currentReleaseVersion) {
|
|
297
|
+
options.findings.push({
|
|
298
|
+
...releaseFinding({
|
|
299
|
+
ruleId: "brandlab-release-upgrade-available",
|
|
300
|
+
severity: "info",
|
|
301
|
+
expected: "No migration is active until a target release is approved.",
|
|
302
|
+
actual: `${options.supportPolicy.currentReleaseVersion} is current; ${options.installedRelease} remains installed.`,
|
|
303
|
+
remediation: "Keep monitoring the tolerated exact pin or select a separately approved target release.",
|
|
304
|
+
}),
|
|
305
|
+
classification: "advisory",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!options.targetManifest) {
|
|
311
|
+
options.findings.push({
|
|
312
|
+
...releaseFinding({
|
|
313
|
+
ruleId: "brandlab-migration-target-unsupported",
|
|
314
|
+
severity: "blocker",
|
|
315
|
+
expected: "A target release with an embedded runtime contract",
|
|
316
|
+
actual: `${options.targetRelease} has no runtime release contract.`,
|
|
317
|
+
remediation: "Select a manifest-backed runtime contract before preparing migration work.",
|
|
318
|
+
}),
|
|
319
|
+
classification: "migration",
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const targetLifecycle = options.supportPolicy.releases[options.targetRelease]?.lifecycle;
|
|
324
|
+
if (!targetLifecycle) {
|
|
325
|
+
options.findings.push({
|
|
326
|
+
...releaseFinding({
|
|
327
|
+
ruleId: "brandlab-migration-target-not-approved",
|
|
328
|
+
severity: "blocker",
|
|
329
|
+
expected: "A target release explicitly approved in mutable support policy",
|
|
330
|
+
actual: `${options.targetRelease} has a runtime contract but no approved support-policy lifecycle.`,
|
|
331
|
+
remediation: "Keep the installed release unchanged until the candidate release and project migration are separately approved.",
|
|
332
|
+
}),
|
|
333
|
+
classification: "migration",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (targetLifecycle === "security-blocked" || targetLifecycle === "retired") {
|
|
337
|
+
options.findings.push({
|
|
338
|
+
...releaseFinding({
|
|
339
|
+
ruleId: `brandlab-migration-target-${targetLifecycle}`,
|
|
340
|
+
severity: "blocker",
|
|
341
|
+
expected: "A current or tolerated target release",
|
|
342
|
+
actual: `${options.targetRelease} is ${targetLifecycle}.`,
|
|
343
|
+
remediation: "Select a current or tolerated target release.",
|
|
344
|
+
}),
|
|
345
|
+
classification: "migration",
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!options.installedRelease || options.installedRelease === options.targetRelease)
|
|
350
|
+
return;
|
|
351
|
+
const comparison = compareBrandlabSemver(options.targetRelease, options.installedRelease);
|
|
352
|
+
options.findings.push({
|
|
353
|
+
...releaseFinding({
|
|
354
|
+
ruleId: comparison < 0
|
|
355
|
+
? "brandlab-migration-downgrade-review"
|
|
356
|
+
: "brandlab-migration-target-selected",
|
|
357
|
+
severity: comparison < 0 ? "blocker" : "info",
|
|
358
|
+
expected: comparison < 0
|
|
359
|
+
? "A separately documented rollback decision"
|
|
360
|
+
: "Target-release migration evidence",
|
|
361
|
+
actual: `${options.installedRelease} -> ${options.targetRelease}`,
|
|
362
|
+
remediation: comparison < 0
|
|
363
|
+
? "Use the immutable rollback manifest and record explicit owner approval."
|
|
364
|
+
: "Address migration findings and preserve the installed release until target proof passes.",
|
|
365
|
+
}),
|
|
366
|
+
classification: "migration",
|
|
367
|
+
});
|
|
368
|
+
addTargetRuntimeFindings({
|
|
369
|
+
findings: options.findings,
|
|
370
|
+
packageJson: options.packageJson,
|
|
371
|
+
packageLock: options.packageLock,
|
|
372
|
+
targetManifest: options.targetManifest,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function addInstalledPackageLockFindings(options) {
|
|
376
|
+
const { packageLock } = options;
|
|
377
|
+
if (packageLock.state !== "valid") {
|
|
378
|
+
const detail = packageLock.state === "missing"
|
|
379
|
+
? "package-lock.json is missing."
|
|
380
|
+
: packageLock.state === "unsupported"
|
|
381
|
+
? `package-lock.json uses unsupported lockfileVersion ${String(packageLock.lockfileVersion)}.`
|
|
382
|
+
: "package-lock.json is invalid or has no npm root package entry.";
|
|
383
|
+
options.findings.push(releaseFinding({
|
|
384
|
+
ruleId: `brandlab-release-package-lock-${packageLock.state}`,
|
|
385
|
+
severity: "blocker",
|
|
386
|
+
expected: "A valid npm package-lock.json (lockfile v2 or v3) with root and resolved package evidence",
|
|
387
|
+
actual: detail,
|
|
388
|
+
remediation: "Regenerate the npm lockfile from the exact supported package declarations and review the resulting diff.",
|
|
389
|
+
path: "package-lock.json",
|
|
390
|
+
}));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const packageName of BRANDLAB_SHARED_PACKAGE_NAMES) {
|
|
394
|
+
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
395
|
+
const lockedSpec = lockedRootSpec(packageLock, packageName);
|
|
396
|
+
const resolvedPackage = packageLock.resolvedPackages.get(packageName);
|
|
397
|
+
if (options.packageJson.dependencies?.[packageName] !== undefined &&
|
|
398
|
+
options.packageJson.devDependencies?.[packageName] !== undefined) {
|
|
399
|
+
options.findings.push(releaseFinding({
|
|
400
|
+
ruleId: "brandlab-release-package-duplicate-declaration",
|
|
401
|
+
severity: "blocker",
|
|
402
|
+
expected: `${packageName} declared exactly once`,
|
|
403
|
+
actual: `${packageName} appears in both dependencies and devDependencies.`,
|
|
404
|
+
remediation: "Keep each Brandlab package in one appropriate dependency section.",
|
|
405
|
+
path: "package.json",
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
if (packageLock.rootDependencies[packageName] !== undefined &&
|
|
409
|
+
packageLock.rootDevDependencies[packageName] !== undefined) {
|
|
410
|
+
options.findings.push(releaseFinding({
|
|
411
|
+
ruleId: "brandlab-release-lock-duplicate-declaration",
|
|
412
|
+
severity: "blocker",
|
|
413
|
+
expected: `${packageName} recorded once in the lockfile root`,
|
|
414
|
+
actual: `${packageName} appears in both root dependency sections.`,
|
|
415
|
+
remediation: "Regenerate the lockfile from the single package.json declaration.",
|
|
416
|
+
path: "package-lock.json",
|
|
417
|
+
}));
|
|
418
|
+
}
|
|
419
|
+
if (lockedSpec !== options.installedRelease || lockedSpec !== declared) {
|
|
420
|
+
options.findings.push(releaseFinding({
|
|
421
|
+
ruleId: "brandlab-release-lock-root-spec-mismatch",
|
|
422
|
+
severity: "blocker",
|
|
423
|
+
expected: `${packageName}@${options.installedRelease} in package.json and the lockfile root`,
|
|
424
|
+
actual: `package.json=${declared ?? "missing"}; lockfile=${lockedSpec ?? "missing"}.`,
|
|
425
|
+
remediation: "Reinstall from the exact lockstep package declarations so root lock specifications match package.json.",
|
|
426
|
+
path: "package-lock.json",
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
if (!resolvedPackage ||
|
|
430
|
+
resolvedPackage.version !== options.installedRelease ||
|
|
431
|
+
resolvedPackage.linked ||
|
|
432
|
+
!resolvedPackage.resolved ||
|
|
433
|
+
!resolvedPackage.integrity) {
|
|
434
|
+
options.findings.push(releaseFinding({
|
|
435
|
+
ruleId: "brandlab-release-lock-resolution-mismatch",
|
|
436
|
+
severity: "blocker",
|
|
437
|
+
expected: `${packageName} resolved to immutable ${options.installedRelease}`,
|
|
438
|
+
actual: !resolvedPackage
|
|
439
|
+
? "No resolved node_modules package entry exists."
|
|
440
|
+
: `version=${resolvedPackage.version ?? "missing"}; linked=${String(resolvedPackage.linked)}; resolved=${resolvedPackage.resolved ? "present" : "missing"}; integrity=${resolvedPackage.integrity ? "present" : "missing"}.`,
|
|
441
|
+
remediation: "Produce a clean lockfile with a non-workspace resolved package, source and integrity at the declared exact release.",
|
|
442
|
+
path: "package-lock.json",
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
const expectedIntegrity = options.installedAttestation?.packages[packageName].registry.integrity;
|
|
446
|
+
if (expectedIntegrity && resolvedPackage?.integrity !== expectedIntegrity) {
|
|
447
|
+
options.findings.push(releaseFinding({
|
|
448
|
+
ruleId: "brandlab-release-lock-integrity-mismatch",
|
|
449
|
+
severity: "blocker",
|
|
450
|
+
expected: `${packageName} lock integrity matching the immutable release manifest`,
|
|
451
|
+
actual: resolvedPackage?.integrity
|
|
452
|
+
? "The lock integrity differs from verified registry evidence."
|
|
453
|
+
: "The lock integrity is missing.",
|
|
454
|
+
remediation: "Recreate the lock from the verified registry release and investigate unexpected package content.",
|
|
455
|
+
path: "package-lock.json",
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function addInstalledRuntimeFindings(options) {
|
|
461
|
+
for (const [packageName, runtimeField] of RUNTIME_PACKAGE_FIELDS) {
|
|
462
|
+
const expected = options.manifest.runtime[runtimeField];
|
|
463
|
+
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
464
|
+
if (declared !== expected) {
|
|
465
|
+
options.findings.push(runtimeFinding({
|
|
466
|
+
ruleId: `brandlab-runtime-${slugify(packageName)}-declaration`,
|
|
467
|
+
expected: `${packageName}@${expected} declared exactly`,
|
|
468
|
+
actual: `${packageName} is ${declared ?? "missing"}.`,
|
|
469
|
+
remediation: `Use the ${options.manifest.releaseVersion} embedded runtime contract.`,
|
|
470
|
+
path: "package.json",
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
if (options.packageLock.state !== "valid")
|
|
474
|
+
continue;
|
|
475
|
+
const lockedSpec = lockedRootSpec(options.packageLock, packageName);
|
|
476
|
+
const resolved = options.packageLock.resolvedPackages.get(packageName);
|
|
477
|
+
if (lockedSpec !== expected || lockedSpec !== declared) {
|
|
478
|
+
options.findings.push(runtimeFinding({
|
|
479
|
+
ruleId: `brandlab-runtime-${slugify(packageName)}-lock-spec`,
|
|
480
|
+
expected: `${packageName}@${expected} in the lockfile root`,
|
|
481
|
+
actual: `package.json=${declared ?? "missing"}; lockfile=${lockedSpec ?? "missing"}.`,
|
|
482
|
+
remediation: "Regenerate package-lock.json from the exact runtime declaration.",
|
|
483
|
+
path: "package-lock.json",
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
if (!resolved || resolved.version !== expected || resolved.linked) {
|
|
487
|
+
options.findings.push(runtimeFinding({
|
|
488
|
+
ruleId: `brandlab-runtime-${slugify(packageName)}-resolution`,
|
|
489
|
+
expected: `${packageName} resolved to ${expected}`,
|
|
490
|
+
actual: !resolved
|
|
491
|
+
? "No resolved node_modules package entry exists."
|
|
492
|
+
: `version=${resolved.version ?? "missing"}; linked=${String(resolved.linked)}.`,
|
|
493
|
+
remediation: "Reinstall the immutable runtime baseline and preserve the reviewed lockfile.",
|
|
494
|
+
path: "package-lock.json",
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const nodeEngine = options.packageJson.engines?.node ?? null;
|
|
499
|
+
if (nodeEngine !== options.manifest.runtime.node) {
|
|
500
|
+
options.findings.push(runtimeFinding({
|
|
501
|
+
ruleId: "brandlab-runtime-node-declaration",
|
|
502
|
+
expected: `engines.node=${options.manifest.runtime.node}`,
|
|
503
|
+
actual: `engines.node=${nodeEngine ?? "missing"}.`,
|
|
504
|
+
remediation: "Restore the installed release's Node engine declaration.",
|
|
505
|
+
path: "package.json",
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
if (options.packageLock.state === "valid" &&
|
|
509
|
+
options.packageLock.rootEngines.node !== options.manifest.runtime.node) {
|
|
510
|
+
options.findings.push(runtimeFinding({
|
|
511
|
+
ruleId: "brandlab-runtime-node-lock-spec",
|
|
512
|
+
expected: `lockfile root engines.node=${options.manifest.runtime.node}`,
|
|
513
|
+
actual: `lockfile root engines.node=${options.packageLock.rootEngines.node ?? "missing"}.`,
|
|
514
|
+
remediation: "Regenerate package-lock.json after restoring the supported Node engine.",
|
|
515
|
+
path: "package-lock.json",
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
if (options.packageJson.packageManager !== options.manifest.runtime.packageManager) {
|
|
519
|
+
options.findings.push(runtimeFinding({
|
|
520
|
+
ruleId: "brandlab-runtime-package-manager",
|
|
521
|
+
expected: `packageManager=${options.manifest.runtime.packageManager}`,
|
|
522
|
+
actual: `packageManager=${options.packageJson.packageManager ?? "missing"}.`,
|
|
523
|
+
remediation: "Restore the installed release's exact npm package-manager declaration.",
|
|
524
|
+
path: "package.json",
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function addTargetRuntimeFindings(options) {
|
|
529
|
+
if (!options.packageJson)
|
|
530
|
+
return;
|
|
531
|
+
const packageMismatches = [];
|
|
532
|
+
for (const packageName of BRANDLAB_SHARED_PACKAGE_NAMES) {
|
|
533
|
+
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
534
|
+
const locked = lockedRootSpec(options.packageLock, packageName);
|
|
535
|
+
const resolved = options.packageLock.resolvedPackages.get(packageName)?.version ?? null;
|
|
536
|
+
if (declared !== options.targetManifest.releaseVersion ||
|
|
537
|
+
locked !== options.targetManifest.releaseVersion ||
|
|
538
|
+
resolved !== options.targetManifest.releaseVersion) {
|
|
539
|
+
packageMismatches.push(`${packageName}(declared=${declared ?? "missing"},locked=${locked ?? "missing"},resolved=${resolved ?? "missing"})`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (packageMismatches.length > 0) {
|
|
543
|
+
options.findings.push(migrationRuntimeFinding({
|
|
544
|
+
ruleId: "brandlab-migration-package-provenance",
|
|
545
|
+
expected: `All four packages declared and locked at ${options.targetManifest.releaseVersion}`,
|
|
546
|
+
actual: packageMismatches.join("; "),
|
|
547
|
+
remediation: "Apply the target exact pins in an approved migration branch and regenerate the reviewed lockfile.",
|
|
548
|
+
}));
|
|
549
|
+
}
|
|
550
|
+
const runtimeMismatches = [];
|
|
551
|
+
for (const [packageName, runtimeField] of RUNTIME_PACKAGE_FIELDS) {
|
|
552
|
+
const expected = options.targetManifest.runtime[runtimeField];
|
|
553
|
+
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
554
|
+
const locked = lockedRootSpec(options.packageLock, packageName);
|
|
555
|
+
const resolved = options.packageLock.resolvedPackages.get(packageName)?.version ?? null;
|
|
556
|
+
if (declared !== expected || locked !== expected || resolved !== expected) {
|
|
557
|
+
runtimeMismatches.push(`${packageName}(expected=${expected},declared=${declared ?? "missing"},locked=${locked ?? "missing"},resolved=${resolved ?? "missing"})`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (options.packageJson.engines?.node !== options.targetManifest.runtime.node) {
|
|
561
|
+
runtimeMismatches.push(`node(expected=${options.targetManifest.runtime.node},actual=${options.packageJson.engines?.node ?? "missing"})`);
|
|
562
|
+
}
|
|
563
|
+
if (options.packageJson.packageManager !== options.targetManifest.runtime.packageManager) {
|
|
564
|
+
runtimeMismatches.push(`packageManager(expected=${options.targetManifest.runtime.packageManager},actual=${options.packageJson.packageManager ?? "missing"})`);
|
|
565
|
+
}
|
|
566
|
+
if (runtimeMismatches.length > 0) {
|
|
567
|
+
options.findings.push(migrationRuntimeFinding({
|
|
568
|
+
ruleId: "brandlab-migration-runtime-baseline",
|
|
569
|
+
expected: `The ${options.targetManifest.releaseVersion} immutable runtime baseline`,
|
|
570
|
+
actual: runtimeMismatches.join("; "),
|
|
571
|
+
remediation: "Apply and lock the target runtime only inside the separately approved migration.",
|
|
572
|
+
}));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function runtimeFinding(options) {
|
|
576
|
+
return {
|
|
577
|
+
ruleId: options.ruleId,
|
|
578
|
+
surface: "runtime",
|
|
579
|
+
classification: "maintenance",
|
|
580
|
+
severity: "blocker",
|
|
581
|
+
contract: { name: "runtime", version: "installed-runtime-contract" },
|
|
582
|
+
evidenceKind: "static",
|
|
583
|
+
path: options.path,
|
|
584
|
+
expected: options.expected,
|
|
585
|
+
actual: options.actual,
|
|
586
|
+
remediation: options.remediation,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function migrationRuntimeFinding(options) {
|
|
590
|
+
return {
|
|
591
|
+
ruleId: options.ruleId,
|
|
592
|
+
surface: "runtime",
|
|
593
|
+
classification: "migration",
|
|
594
|
+
severity: "blocker",
|
|
595
|
+
contract: { name: "runtime", version: "target-runtime-contract" },
|
|
596
|
+
evidenceKind: "static",
|
|
597
|
+
path: "package-lock.json",
|
|
598
|
+
expected: options.expected,
|
|
599
|
+
actual: options.actual,
|
|
600
|
+
remediation: options.remediation,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function evaluateApplicableArchitectureContracts(options) {
|
|
604
|
+
const installedConfiguration = options.installedManifest?.contracts.configuration ?? null;
|
|
605
|
+
const targetConfiguration = options.targetManifest?.contracts.configuration ?? null;
|
|
606
|
+
const hasDistinctTarget = Boolean(options.targetRelease &&
|
|
607
|
+
options.targetRelease !== options.installedManifest?.releaseVersion);
|
|
608
|
+
let configV2Inspection = null;
|
|
609
|
+
const inspectConfigV2 = () => {
|
|
610
|
+
configV2Inspection ??= inspectBrandlabConfigV2(options.root);
|
|
611
|
+
return configV2Inspection;
|
|
612
|
+
};
|
|
613
|
+
let legacyReport = null;
|
|
614
|
+
const inspectLegacy = () => {
|
|
615
|
+
if (legacyReport)
|
|
616
|
+
return legacyReport;
|
|
617
|
+
const architecture = options.architecture === undefined
|
|
618
|
+
? readBrandlabArchitectureConfig(options.root)
|
|
619
|
+
: options.architecture;
|
|
620
|
+
legacyReport = evaluateBrandlabArchitecture({ root: options.root, architecture });
|
|
621
|
+
return legacyReport;
|
|
622
|
+
};
|
|
623
|
+
if (installedConfiguration === CONFIGURATION_V2_CONTRACT) {
|
|
624
|
+
const inspection = inspectConfigV2();
|
|
625
|
+
for (const issue of inspection.issues) {
|
|
626
|
+
options.findings.push(mapConfigV2Issue(issue, "maintenance", installedConfiguration));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else if (installedConfiguration === LEGACY_CONFIGURATION_CONTRACT) {
|
|
630
|
+
const report = inspectLegacy();
|
|
631
|
+
const installedArchitecture = options.installedManifest?.contracts.architecture ?? null;
|
|
632
|
+
for (const issue of report.issues) {
|
|
633
|
+
const classification = installedArchitecture === LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT ||
|
|
634
|
+
issue.code === "brandlab-architecture-config-missing"
|
|
635
|
+
? "maintenance"
|
|
636
|
+
: "advisory";
|
|
637
|
+
if (classification === "advisory" &&
|
|
638
|
+
hasDistinctTarget &&
|
|
639
|
+
options.targetManifest?.contracts.architecture === LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
options.findings.push(mapArchitectureIssue(issue, classification, {
|
|
643
|
+
contractVersion: classification === "maintenance"
|
|
644
|
+
? installedArchitecture
|
|
645
|
+
: LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
646
|
+
}));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
else if (!options.installedManifest) {
|
|
650
|
+
// Unsupported or unadopted projects still benefit from architecture
|
|
651
|
+
// diagnosis, but no current evaluator may retroactively become their
|
|
652
|
+
// installed maintenance contract. Keep this lane advisory until a
|
|
653
|
+
// supported target release is explicitly selected.
|
|
654
|
+
const configSource = readOptionalText(join(options.root, "brandlab.config.ts"));
|
|
655
|
+
if (/\bdefineBrandlabProject\s*\(/.test(configSource ?? "")) {
|
|
656
|
+
for (const issue of inspectConfigV2().issues) {
|
|
657
|
+
options.findings.push(mapConfigV2Issue(issue, "advisory", CONFIGURATION_V2_CONTRACT));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
for (const issue of inspectLegacy().issues) {
|
|
662
|
+
options.findings.push(mapArchitectureIssue(issue, "advisory", {
|
|
663
|
+
contractVersion: LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
664
|
+
}));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (hasDistinctTarget && targetConfiguration === CONFIGURATION_V2_CONTRACT) {
|
|
669
|
+
for (const issue of inspectConfigV2().issues) {
|
|
670
|
+
options.findings.push(mapConfigV2Issue(issue, "migration", targetConfiguration));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
else if (hasDistinctTarget &&
|
|
674
|
+
targetConfiguration === LEGACY_CONFIGURATION_CONTRACT &&
|
|
675
|
+
options.targetManifest?.contracts.architecture === LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT &&
|
|
676
|
+
options.installedManifest?.contracts.architecture !== LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT) {
|
|
677
|
+
for (const issue of inspectLegacy().issues) {
|
|
678
|
+
options.findings.push(mapArchitectureIssue(issue, "migration", {
|
|
679
|
+
contractVersion: LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
680
|
+
}));
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const inspectedLegacy = legacyReport;
|
|
684
|
+
const inspectedConfigV2 = configV2Inspection;
|
|
685
|
+
return {
|
|
686
|
+
evaluator: BRANDLAB_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
687
|
+
installedContract: options.installedManifest?.contracts.architecture ?? null,
|
|
688
|
+
targetContract: options.targetManifest?.contracts.architecture ?? null,
|
|
689
|
+
legacy: inspectedLegacy
|
|
690
|
+
? {
|
|
691
|
+
evaluator: LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
692
|
+
profile: inspectedLegacy.profile,
|
|
693
|
+
status: inspectedLegacy.status,
|
|
694
|
+
pages: inspectedLegacy.pageFileCount,
|
|
695
|
+
}
|
|
696
|
+
: null,
|
|
697
|
+
configV2: inspectedConfigV2
|
|
698
|
+
? {
|
|
699
|
+
projectId: inspectedConfigV2.config?.projectId ?? null,
|
|
700
|
+
profile: inspectedConfigV2.config?.profile ?? null,
|
|
701
|
+
imports: [...inspectedConfigV2.imports].sort(),
|
|
702
|
+
executableImports: [...inspectedConfigV2.executableImports].sort(),
|
|
703
|
+
}
|
|
704
|
+
: null,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function mapArchitectureIssue(issue, classification, options) {
|
|
708
|
+
const rawSeverity = issue.severity === "error"
|
|
709
|
+
? "blocker"
|
|
710
|
+
: issue.severity === "warning"
|
|
711
|
+
? "warning"
|
|
712
|
+
: "info";
|
|
713
|
+
const severity = classification === "advisory" && rawSeverity === "blocker"
|
|
714
|
+
? "warning"
|
|
715
|
+
: rawSeverity;
|
|
716
|
+
return {
|
|
717
|
+
ruleId: issue.code,
|
|
718
|
+
surface: surfaceForRule(issue.code),
|
|
719
|
+
classification,
|
|
720
|
+
severity,
|
|
721
|
+
contract: {
|
|
722
|
+
name: "architecture",
|
|
723
|
+
version: options.contractVersion ?? LEGACY_ARCHITECTURE_EVALUATOR_CONTRACT,
|
|
724
|
+
},
|
|
725
|
+
evidenceKind: "static",
|
|
726
|
+
path: issue.path,
|
|
727
|
+
expected: issue.fix ?? "The project matches its applicable Brandlab architecture contract.",
|
|
728
|
+
actual: issue.detail,
|
|
729
|
+
remediation: issue.fix ?? "Apply the supported shared-platform implementation for this rule.",
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function mapConfigV2Issue(issue, classification, contractVersion) {
|
|
733
|
+
const severity = classification === "advisory" && issue.severity === "blocker"
|
|
734
|
+
? "warning"
|
|
735
|
+
: issue.severity;
|
|
736
|
+
return {
|
|
737
|
+
ruleId: issue.ruleId,
|
|
738
|
+
surface: issue.surface,
|
|
739
|
+
classification,
|
|
740
|
+
severity,
|
|
741
|
+
contract: {
|
|
742
|
+
name: "configuration",
|
|
743
|
+
version: contractVersion ?? "project-config-v2",
|
|
744
|
+
},
|
|
745
|
+
evidenceKind: issue.evidenceKind,
|
|
746
|
+
path: issue.path,
|
|
747
|
+
line: issue.line,
|
|
748
|
+
expected: issue.expected,
|
|
749
|
+
actual: issue.actual,
|
|
750
|
+
remediation: issue.remediation,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function releaseFinding(options) {
|
|
754
|
+
return {
|
|
755
|
+
ruleId: options.ruleId,
|
|
756
|
+
surface: "release",
|
|
757
|
+
classification: "maintenance",
|
|
758
|
+
severity: options.severity,
|
|
759
|
+
contract: { name: "release-authority", version: "v3" },
|
|
760
|
+
evidenceKind: "static",
|
|
761
|
+
path: options.path,
|
|
762
|
+
expected: options.expected,
|
|
763
|
+
actual: options.actual,
|
|
764
|
+
remediation: options.remediation,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
function summarize(findings, classification) {
|
|
768
|
+
const relevant = findings.filter((finding) => finding.classification === classification);
|
|
769
|
+
const blockers = relevant.filter((finding) => finding.severity === "blocker").length;
|
|
770
|
+
const warnings = relevant.filter((finding) => finding.severity === "warning").length;
|
|
771
|
+
const info = relevant.filter((finding) => finding.severity === "info").length;
|
|
772
|
+
return {
|
|
773
|
+
status: blockers > 0 ? "blocked" : warnings > 0 ? "warning" : "pass",
|
|
774
|
+
total: relevant.length,
|
|
775
|
+
blockers,
|
|
776
|
+
warnings,
|
|
777
|
+
info,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function readPackageJson(root) {
|
|
781
|
+
const source = readOptionalText(join(root, "package.json"));
|
|
782
|
+
if (!source)
|
|
783
|
+
return null;
|
|
784
|
+
try {
|
|
785
|
+
return JSON.parse(source);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function readPackageLock(root) {
|
|
792
|
+
const path = join(root, "package-lock.json");
|
|
793
|
+
const source = readOptionalText(path);
|
|
794
|
+
if (!source) {
|
|
795
|
+
return emptyPackageLockInspection("missing");
|
|
796
|
+
}
|
|
797
|
+
let parsed;
|
|
798
|
+
try {
|
|
799
|
+
parsed = JSON.parse(source);
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
return emptyPackageLockInspection("invalid", {
|
|
803
|
+
digest: digestBrandlabAuthority(source),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
if (!isObjectRecord(parsed)) {
|
|
807
|
+
return emptyPackageLockInspection("invalid", {
|
|
808
|
+
digest: digestBrandlabAuthority(parsed),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
const lockfileVersion = typeof parsed.lockfileVersion === "number"
|
|
812
|
+
? parsed.lockfileVersion
|
|
813
|
+
: null;
|
|
814
|
+
if (lockfileVersion !== 2 && lockfileVersion !== 3) {
|
|
815
|
+
return emptyPackageLockInspection("unsupported", {
|
|
816
|
+
lockfileVersion,
|
|
817
|
+
digest: digestBrandlabAuthority(parsed),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (!isObjectRecord(parsed.packages) || !isObjectRecord(parsed.packages[""])) {
|
|
821
|
+
return emptyPackageLockInspection("invalid", {
|
|
822
|
+
lockfileVersion,
|
|
823
|
+
digest: digestBrandlabAuthority(parsed),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
const rootPackage = parsed.packages[""];
|
|
827
|
+
const rootDependencies = stringRecord(rootPackage.dependencies);
|
|
828
|
+
const rootDevDependencies = stringRecord(rootPackage.devDependencies);
|
|
829
|
+
const rootEngines = stringRecord(rootPackage.engines);
|
|
830
|
+
const resolvedPackages = new Map();
|
|
831
|
+
for (const packageName of [...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name)]) {
|
|
832
|
+
const entry = parsed.packages[`node_modules/${packageName}`];
|
|
833
|
+
if (!isObjectRecord(entry))
|
|
834
|
+
continue;
|
|
835
|
+
resolvedPackages.set(packageName, {
|
|
836
|
+
version: typeof entry.version === "string" ? entry.version : null,
|
|
837
|
+
resolved: typeof entry.resolved === "string" ? entry.resolved : null,
|
|
838
|
+
integrity: typeof entry.integrity === "string" ? entry.integrity : null,
|
|
839
|
+
linked: entry.link === true,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
state: "valid",
|
|
844
|
+
lockfileVersion,
|
|
845
|
+
rootDependencies,
|
|
846
|
+
rootDevDependencies,
|
|
847
|
+
rootEngines,
|
|
848
|
+
resolvedPackages,
|
|
849
|
+
fingerprint: {
|
|
850
|
+
state: "valid",
|
|
851
|
+
lockfileVersion,
|
|
852
|
+
root: {
|
|
853
|
+
packages: Object.fromEntries([...BRANDLAB_SHARED_PACKAGE_NAMES, ...RUNTIME_PACKAGE_FIELDS.map(([name]) => name)]
|
|
854
|
+
.map((packageName) => [packageName, lockedRootSpec({
|
|
855
|
+
state: "valid",
|
|
856
|
+
lockfileVersion,
|
|
857
|
+
rootDependencies,
|
|
858
|
+
rootDevDependencies,
|
|
859
|
+
rootEngines,
|
|
860
|
+
resolvedPackages,
|
|
861
|
+
fingerprint: {},
|
|
862
|
+
}, packageName)])),
|
|
863
|
+
node: rootEngines.node ?? null,
|
|
864
|
+
},
|
|
865
|
+
resolved: Object.fromEntries([...resolvedPackages.entries()].map(([packageName, entry]) => [
|
|
866
|
+
packageName,
|
|
867
|
+
{ version: entry.version, linked: entry.linked },
|
|
868
|
+
])),
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function emptyPackageLockInspection(state, extra = {}) {
|
|
873
|
+
return {
|
|
874
|
+
state,
|
|
875
|
+
lockfileVersion: typeof extra.lockfileVersion === "number" ? extra.lockfileVersion : null,
|
|
876
|
+
rootDependencies: {},
|
|
877
|
+
rootDevDependencies: {},
|
|
878
|
+
rootEngines: {},
|
|
879
|
+
resolvedPackages: new Map(),
|
|
880
|
+
fingerprint: { state, ...extra },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function declaredPackageSpec(packageJson, packageName) {
|
|
884
|
+
return packageJson.dependencies?.[packageName] ??
|
|
885
|
+
packageJson.devDependencies?.[packageName] ??
|
|
886
|
+
null;
|
|
887
|
+
}
|
|
888
|
+
function lockedRootSpec(packageLock, packageName) {
|
|
889
|
+
return packageLock.rootDependencies[packageName] ??
|
|
890
|
+
packageLock.rootDevDependencies[packageName] ??
|
|
891
|
+
null;
|
|
892
|
+
}
|
|
893
|
+
function stringRecord(value) {
|
|
894
|
+
if (!isObjectRecord(value))
|
|
895
|
+
return {};
|
|
896
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
897
|
+
}
|
|
898
|
+
function isObjectRecord(value) {
|
|
899
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
900
|
+
}
|
|
901
|
+
function readOptionalText(path) {
|
|
902
|
+
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
903
|
+
}
|
|
904
|
+
function extractDelimitedProperty(source, propertyName, openChar, closeChar) {
|
|
905
|
+
const propertyMatch = source.match(new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:`));
|
|
906
|
+
if (!propertyMatch || propertyMatch.index === undefined)
|
|
907
|
+
return null;
|
|
908
|
+
const start = source.indexOf(openChar, propertyMatch.index + propertyMatch[0].length);
|
|
909
|
+
if (start === -1)
|
|
910
|
+
return null;
|
|
911
|
+
return extractDelimitedBody(source, start, openChar, closeChar);
|
|
912
|
+
}
|
|
913
|
+
function extractDelimitedBody(source, start, openChar, closeChar) {
|
|
914
|
+
let depth = 0;
|
|
915
|
+
let quote = null;
|
|
916
|
+
let escaped = false;
|
|
917
|
+
for (let index = start; index < source.length; index += 1) {
|
|
918
|
+
const char = source[index];
|
|
919
|
+
if (quote) {
|
|
920
|
+
if (escaped)
|
|
921
|
+
escaped = false;
|
|
922
|
+
else if (char === "\\")
|
|
923
|
+
escaped = true;
|
|
924
|
+
else if (char === quote)
|
|
925
|
+
quote = null;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (char === '"' || char === "'" || char === "`")
|
|
929
|
+
quote = char;
|
|
930
|
+
else if (char === openChar)
|
|
931
|
+
depth += 1;
|
|
932
|
+
else if (char === closeChar) {
|
|
933
|
+
depth -= 1;
|
|
934
|
+
if (depth === 0)
|
|
935
|
+
return source.slice(start + 1, index);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
function extractObjectBodies(source) {
|
|
941
|
+
const bodies = [];
|
|
942
|
+
let quote = null;
|
|
943
|
+
let escaped = false;
|
|
944
|
+
let arrayDepth = 0;
|
|
945
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
946
|
+
const char = source[index];
|
|
947
|
+
if (quote) {
|
|
948
|
+
if (escaped)
|
|
949
|
+
escaped = false;
|
|
950
|
+
else if (char === "\\")
|
|
951
|
+
escaped = true;
|
|
952
|
+
else if (char === quote)
|
|
953
|
+
quote = null;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
957
|
+
quote = char;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (char === "[")
|
|
961
|
+
arrayDepth += 1;
|
|
962
|
+
else if (char === "]")
|
|
963
|
+
arrayDepth -= 1;
|
|
964
|
+
else if (char === "{" && arrayDepth === 0) {
|
|
965
|
+
const body = extractDelimitedBody(source, index, "{", "}");
|
|
966
|
+
if (body !== null) {
|
|
967
|
+
bodies.push(body);
|
|
968
|
+
index += body.length + 1;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return bodies;
|
|
973
|
+
}
|
|
974
|
+
function extractStringProperty(source, propertyName) {
|
|
975
|
+
return source.match(new RegExp("\\b" + escapeRegExp(propertyName) + "\\s*:\\s*['\"`]([^'\"`]+)['\"`]", "m"))?.[1] ?? null;
|
|
976
|
+
}
|
|
977
|
+
function extractNumberProperty(source, propertyName) {
|
|
978
|
+
const raw = source.match(new RegExp(`\\b${escapeRegExp(propertyName)}\\s*:\\s*(\\d+)`, "m"))?.[1];
|
|
979
|
+
return raw ? Number(raw) : null;
|
|
980
|
+
}
|
|
981
|
+
function extractStringArray(source, propertyName) {
|
|
982
|
+
const arraySource = extractDelimitedProperty(source, propertyName, "[", "]");
|
|
983
|
+
if (arraySource === null)
|
|
984
|
+
return undefined;
|
|
985
|
+
return [...arraySource.matchAll(/['\"]([^'\"]+)['\"]/g)].map((match) => match[1]);
|
|
986
|
+
}
|
|
987
|
+
function assignString(target, key, value) {
|
|
988
|
+
if (value !== null)
|
|
989
|
+
target[key] = value;
|
|
990
|
+
}
|
|
991
|
+
function assignNumber(target, key, value) {
|
|
992
|
+
if (value !== null)
|
|
993
|
+
target[key] = value;
|
|
994
|
+
}
|
|
995
|
+
function surfaceForRule(ruleId) {
|
|
996
|
+
for (const surface of [
|
|
997
|
+
"contact",
|
|
998
|
+
"metadata",
|
|
999
|
+
"schema",
|
|
1000
|
+
"sitemap",
|
|
1001
|
+
"breadcrumb",
|
|
1002
|
+
"analytics",
|
|
1003
|
+
"accessibility",
|
|
1004
|
+
"runtime",
|
|
1005
|
+
"release",
|
|
1006
|
+
]) {
|
|
1007
|
+
if (ruleId.toLowerCase().includes(surface))
|
|
1008
|
+
return surface;
|
|
1009
|
+
}
|
|
1010
|
+
if (/api key|secret|credential|recaptcha/i.test(ruleId))
|
|
1011
|
+
return "security";
|
|
1012
|
+
return "architecture";
|
|
1013
|
+
}
|
|
1014
|
+
function slugify(value) {
|
|
1015
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1016
|
+
}
|
|
1017
|
+
function normalizePath(value) {
|
|
1018
|
+
return value.split(/[/\\]/).join("/");
|
|
1019
|
+
}
|
|
1020
|
+
function escapeRegExp(value) {
|
|
1021
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1022
|
+
}
|
|
1023
|
+
function compareFindings(first, second) {
|
|
1024
|
+
const classificationOrder = {
|
|
1025
|
+
maintenance: 0,
|
|
1026
|
+
migration: 1,
|
|
1027
|
+
advisory: 2,
|
|
1028
|
+
};
|
|
1029
|
+
const severityOrder = {
|
|
1030
|
+
blocker: 0,
|
|
1031
|
+
warning: 1,
|
|
1032
|
+
info: 2,
|
|
1033
|
+
};
|
|
1034
|
+
return (classificationOrder[first.classification] - classificationOrder[second.classification] ||
|
|
1035
|
+
severityOrder[first.severity] - severityOrder[second.severity] ||
|
|
1036
|
+
first.ruleId.localeCompare(second.ruleId) ||
|
|
1037
|
+
(first.path ?? "").localeCompare(second.path ?? ""));
|
|
1038
|
+
}
|
|
1039
|
+
//# sourceMappingURL=project.js.map
|