@brandlab-digital/conformance 2.1.0 → 3.1.0

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 (67) hide show
  1. package/dist/analytics-e2e.d.ts +52 -0
  2. package/dist/analytics-e2e.d.ts.map +1 -0
  3. package/dist/analytics-e2e.js +189 -0
  4. package/dist/analytics-e2e.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 +40 -3
  8. package/dist/cli.js.map +1 -1
  9. package/dist/contact-e2e.d.ts +43 -0
  10. package/dist/contact-e2e.d.ts.map +1 -0
  11. package/dist/contact-e2e.js +193 -0
  12. package/dist/contact-e2e.js.map +1 -0
  13. package/dist/current-release.js +7 -7
  14. package/dist/current-release.js.map +1 -1
  15. package/dist/e2e-support.d.ts +42 -0
  16. package/dist/e2e-support.d.ts.map +1 -0
  17. package/dist/e2e-support.js +126 -0
  18. package/dist/e2e-support.js.map +1 -0
  19. package/dist/engineering.d.ts +14 -0
  20. package/dist/engineering.d.ts.map +1 -1
  21. package/dist/engineering.js +446 -50
  22. package/dist/engineering.js.map +1 -1
  23. package/dist/estate.d.ts +105 -0
  24. package/dist/estate.d.ts.map +1 -0
  25. package/dist/estate.js +391 -0
  26. package/dist/estate.js.map +1 -0
  27. package/dist/families-e2e.d.ts +45 -0
  28. package/dist/families-e2e.d.ts.map +1 -0
  29. package/dist/families-e2e.js +248 -0
  30. package/dist/families-e2e.js.map +1 -0
  31. package/dist/glob.d.ts +21 -0
  32. package/dist/glob.d.ts.map +1 -0
  33. package/dist/glob.js +138 -0
  34. package/dist/glob.js.map +1 -0
  35. package/dist/knip.d.ts +47 -0
  36. package/dist/knip.d.ts.map +1 -0
  37. package/dist/knip.js +84 -0
  38. package/dist/knip.js.map +1 -0
  39. package/dist/project.d.ts +4 -1
  40. package/dist/project.d.ts.map +1 -1
  41. package/dist/project.js +163 -147
  42. package/dist/project.js.map +1 -1
  43. package/dist/public-copy.d.ts +36 -0
  44. package/dist/public-copy.d.ts.map +1 -0
  45. package/dist/public-copy.js +517 -0
  46. package/dist/public-copy.js.map +1 -0
  47. package/dist/rule-registry.d.ts +292 -0
  48. package/dist/rule-registry.d.ts.map +1 -0
  49. package/dist/rule-registry.js +68 -0
  50. package/dist/rule-registry.js.map +1 -0
  51. package/dist/search-certification.d.ts +4 -0
  52. package/dist/search-certification.d.ts.map +1 -1
  53. package/dist/search-certification.js +17 -3
  54. package/dist/search-certification.js.map +1 -1
  55. package/dist/source-files.d.ts +54 -0
  56. package/dist/source-files.d.ts.map +1 -0
  57. package/dist/source-files.js +323 -0
  58. package/dist/source-files.js.map +1 -0
  59. package/dist/static-rules.d.ts +45 -0
  60. package/dist/static-rules.d.ts.map +1 -0
  61. package/dist/static-rules.js +762 -0
  62. package/dist/static-rules.js.map +1 -0
  63. package/dist/verification-policy.d.ts +13 -4
  64. package/dist/verification-policy.d.ts.map +1 -1
  65. package/dist/verification-policy.js +31 -12
  66. package/dist/verification-policy.js.map +1 -1
  67. package/package.json +37 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knip.js","sourceRoot":"","sources":["../src/knip.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared Knip preset for Brandlab Next.js projects:\n *\n * // knip.ts\n * import { brandlabKnip } from '@brandlab-digital/conformance/knip'\n * export default brandlabKnip({ mdx: true })\n *\n * It keeps Knip's own Next.js, Playwright, Vitest and ESLint plugin entries\n * (overriding them would replace the maintained defaults) and adds only what\n * those plugins cannot see in a Brandlab project: production entries Next loads\n * outside the app graph, CSS-only dependencies and, when enabled, MDX articles.\n * Knip calls the returned function with its command-line mode.\n */\n\nexport type BrandlabKnipCompiler = (text: string, filename: string) => string;\n\n/** The subset of Knip's configuration the preset produces; it satisfies `KnipConfig`. */\nexport interface BrandlabKnipConfig {\n entry: string[];\n ignoreExportsUsedInFile: boolean;\n compilers: Record<string, true | BrandlabKnipCompiler>;\n project?: string[];\n ignoreDependencies?: string[];\n}\n\n/** The parsed Knip arguments Knip passes to a function configuration. */\nexport interface BrandlabKnipMode {\n production?: boolean;\n strict?: boolean;\n}\n\n/** A Knip function configuration: Knip calls it with its parsed arguments. */\nexport type BrandlabKnipConfigFunction = (mode?: BrandlabKnipMode) => BrandlabKnipConfig;\n\nexport interface BrandlabKnipOptions {\n /** The project renders long-form MDX articles from src/content/. */\n mdx?: boolean;\n /**\n * Additional real entry files that no plugin can discover. Suffix a pattern\n * with `!` when it is part of the production graph. Never list whole source\n * directories to silence findings.\n */\n entry?: readonly string[];\n}\n\nconst PRODUCTION_ENTRIES = [\n // Knip's production mode excludes next.config by design, so the redirect table it\n // imports (the fixed 3.0 owner) would look unused although Next applies it.\n 'src/data/redirects.ts!',\n // Read statically by brandlab-verify; it is part of the project, not dead code.\n 'brandlab.config.ts!',\n];\n\nconst SOURCE_EXTENSIONS = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'mts', 'cts'];\n\nexport function brandlabKnip(options: BrandlabKnipOptions = {}): BrandlabKnipConfigFunction {\n const entry = [...PRODUCTION_ENTRIES];\n const compilers: BrandlabKnipConfig['compilers'] = { css: compileCssImports };\n if (options.mdx) {\n // Articles are loaded by slug through dynamic imports that Knip cannot follow.\n entry.push('src/content/**/*.{md,mdx}!');\n // Knip's MDX compiler exposes each article's imports to the module graph.\n compilers.mdx = true;\n }\n for (const pattern of options.entry ?? []) {\n if (typeof pattern !== 'string' || !pattern.trim()) {\n throw new TypeError('brandlabKnip entry patterns must be non-empty strings.');\n }\n if (/^(?:src\\/?)?\\*\\*(?:\\/\\*)?!?$/.test(pattern.trim())) {\n throw new Error(`brandlabKnip entry \"${pattern}\" marks all source as used; list the real entry files instead.`);\n }\n entry.push(pattern.trim());\n }\n const extensions = `{${[...SOURCE_EXTENSIONS, ...Object.keys(compilers), ...(options.mdx ? ['md'] : [])].join(',')}}`;\n const config: BrandlabKnipConfig = {\n entry: [...new Set(entry)],\n // Exports that are also consumed inside their own module are not dead code.\n ignoreExportsUsedInFile: true,\n compilers,\n };\n // Otherwise no ignoreDependencies: optional peers of the shared packages\n // (eslint-config-next for configs.next) already count as used, and anything else\n // must stay visible.\n return (mode = {}) => {\n const copy = { ...config, entry: [...config.entry], compilers: { ...config.compilers } };\n if (mode.production !== true && mode.strict !== true) return copy;\n return {\n ...copy,\n // Knip's default project marks every file as production (**/*!), so production mode\n // reported test tooling (vitest.setup.ts, tests/ helpers) as unused files. The\n // application lives in src/; root production files are entries (brandlab.config.ts)\n // or belong to a plugin (next.config.ts, mdx-components.tsx).\n project: [`src/**/*.${extensions}!`],\n // Knip's production mode never reads next.config (not even as an entry), where the\n // MDX wrapper imports @next/mdx; the default graph still checks that import.\n ...(options.mdx ? { ignoreDependencies: ['@next/mdx'] } : {}),\n };\n };\n}\n\nconst CSS_COMMENT = /\\/\\*[\\s\\S]*?\\*\\//g;\nconst CSS_DIRECTIVE =\n /@(?:import|config|plugin|reference)\\s+(?:url\\(\\s*)?(?:\"([^\"]+)\"|'([^']+)'|([^'\"\\s;)]+))\\s*\\)?[^;]*;/g;\n\n/**\n * Expose CSS-only dependencies (`@import \"tailwindcss\"`, `@plugin`, `@config`,\n * `@reference`, local `@import`) to Knip as module imports. Remote URLs and\n * data URIs are not dependencies and are skipped.\n */\nexport function compileCssImports(text: string): string {\n const imports: string[] = [];\n for (const match of text.replace(CSS_COMMENT, '').matchAll(CSS_DIRECTIVE)) {\n const specifier = match[1] ?? match[2] ?? match[3];\n if (!specifier || /^(?:[a-z][a-z0-9+.-]*:|\\/\\/|#)/i.test(specifier)) continue;\n imports.push(`import ${JSON.stringify(specifier)};`);\n }\n return imports.join('\\n');\n}\n"]}
package/dist/project.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { type PackedPreflight } from "./packed-preflight.js";
2
2
  import { type BrandlabContractVersions, type BrandlabReleaseManifest, type BrandlabRuntimeReleaseContract } from "./release-manifest.js";
3
3
  import { type BrandlabOperationalFinding, type BrandlabVerificationEvidence, type BrandlabVerificationResult } from "./verification-policy.js";
4
- export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, type BrandlabVerificationCheckId, type BrandlabVerificationCheckResult, type BrandlabVerificationEvidence, type BrandlabVerificationEvidenceKind, type BrandlabVerificationFamilyId, type BrandlabVerificationFamilyResult, type BrandlabVerificationResult, type BrandlabVerificationStatus, } from "./verification-policy.js";
4
+ export { BRANDLAB_STATIC_RULES, matchesBrandlabStaticRule, type BrandlabStaticRule, } from "./rule-registry.js";
5
+ export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, checkForRule, type BrandlabRuleEvaluation, type BrandlabVerificationCheckId, type BrandlabVerificationCheckResult, type BrandlabVerificationEvidence, type BrandlabVerificationEvidenceKind, type BrandlabVerificationFamilyId, type BrandlabVerificationFamilyResult, type BrandlabVerificationResult, type BrandlabVerificationStatus, } from "./verification-policy.js";
5
6
  export declare const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v2";
6
7
  export declare const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "current-only-v2";
7
8
  export type BrandlabConformanceMode = "local" | "ci" | "preflight";
@@ -61,5 +62,7 @@ interface ProjectPackageJsonLike {
61
62
  export declare function evaluateBrandlabProject(options: EvaluateBrandlabProjectOptions): BrandlabConformanceResult;
62
63
  export declare function shouldFailBrandlabConformance(result: BrandlabConformanceResult): boolean;
63
64
  export declare function detectInstalledBrandlabRelease(packageJson: ProjectPackageJsonLike | null): string | null;
65
+ /** Variable names documented in an env example file (`KEY=`, `export KEY=`); values are ignored. */
66
+ export declare function parseEnvironmentKeys(text: string): Set<string>;
64
67
  export type { BrandlabContractVersions };
65
68
  //# sourceMappingURL=project.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
package/dist/project.js CHANGED
@@ -1,10 +1,13 @@
1
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
3
  import { BRANDLAB_CURRENT_RUNTIME_RELEASE } from "./current-release.js";
4
4
  import { verifyPackedPreflight } from "./packed-preflight.js";
5
5
  import { BRANDLAB_SHARED_PACKAGE_NAMES, compareBrandlabSemver, digestBrandlabAuthority, isBrandlabSemver, projectBrandlabRuntimeReleaseContract, validateBrandlabReleaseManifest, validateBrandlabRuntimeReleaseContract, } from "./release-manifest.js";
6
+ import { BRANDLAB_SOURCE_ROOT_DIRECTORIES, collectBrandlabSource, lineAt, readOptionalText, stripCommentsPreservingLayout, } from "./source-files.js";
7
+ import { findContactContractRecipients, inspectStaticRules, recordEvaluation, staticFinding, } from "./static-rules.js";
6
8
  import { applyBrandlabVerificationPolicy, BRANDLAB_VERIFICATION_POLICY_VERSION, } from "./verification-policy.js";
7
- export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, } from "./verification-policy.js";
9
+ export { BRANDLAB_STATIC_RULES, matchesBrandlabStaticRule, } from "./rule-registry.js";
10
+ export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, checkForRule, } from "./verification-policy.js";
8
11
  export const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v2";
9
12
  export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "current-only-v2";
10
13
  const RUNTIME_PACKAGE_FIELDS = [
@@ -13,12 +16,11 @@ const RUNTIME_PACKAGE_FIELDS = [
13
16
  ["react-dom", "reactDom"],
14
17
  ["typescript", "typescript"],
15
18
  ];
16
- const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
17
19
  const SOURCE_RULES = [
18
20
  {
19
21
  ruleId: "brandlab-source-retired-analytics-event",
20
22
  surface: "analytics",
21
- pattern: /\b(?:contact_form_submission|phone_call|phone_calls|form_start|cta_click)\b/,
23
+ pattern: /\b(?:contact_form_submission|phone_call|phone_calls|form_start|form_submit|generate_lead|cta_click)\b/,
22
24
  expected: "Only the current Brandlab analytics event contract is used.",
23
25
  remediation: "Use contact_form for accepted forms and phone_link_click for telephone-link diagnostics.",
24
26
  },
@@ -106,6 +108,22 @@ const SOURCE_RULES = [
106
108
  expected: "The simplified contact runtime has no project-level state-service requirement.",
107
109
  remediation: "Remove the retired contact state variables and use the shared stateless contact flow.",
108
110
  },
111
+ {
112
+ ruleId: "brandlab-analytics-retired-phone-api",
113
+ surface: "analytics",
114
+ pattern: /\btrackPhoneCall\b/,
115
+ expected: "Telephone taps are reported only as phone_link_click by the shared instrumentation.",
116
+ actual: "The retired trackPhoneCall API is still referenced.",
117
+ remediation: "Remove trackPhoneCall; installBrandlabInstrumentation reports one phone_link_click per tap.",
118
+ },
119
+ {
120
+ ruleId: "brandlab-analytics-web-vitals",
121
+ surface: "analytics",
122
+ pattern: /\b(?:useReportWebVitals|reportWebVitals|reportBrandlabWebVital|createBrandlabWebVitalsReporter|CoreWebVitalsReporter)\b|['"](?:next\/web-vitals|web-vitals|web_vital)['"]|\bwebVitals\s*:/,
123
+ expected: "No web-vitals reporter; speed evidence comes from Search Console.",
124
+ actual: "A web-vitals reporter, web_vital event or webVitals flag is still wired.",
125
+ remediation: "Remove the web-vitals reporter and the webVitals flag; web_vital events are not sent in 3.0.",
126
+ },
109
127
  ];
110
128
  export function evaluateBrandlabProject(options) {
111
129
  const root = resolve(options.root);
@@ -133,8 +151,11 @@ export function evaluateBrandlabProject(options) {
133
151
  ? options.releaseManifest
134
152
  : null;
135
153
  const findings = [];
154
+ const evaluations = [];
155
+ const source = collectBrandlabSource(root);
136
156
  inspectRelease({
137
157
  findings,
158
+ evaluations,
138
159
  packageJson,
139
160
  packageLock,
140
161
  installedRelease,
@@ -143,12 +164,20 @@ export function evaluateBrandlabProject(options) {
143
164
  attestation,
144
165
  packedPreflight,
145
166
  });
146
- if (manifest?.runtime.nextVersionPolicy === "exact")
147
- inspectImageRuntime(root, findings);
148
- inspectProjectConfig(root, findings);
149
- inspectProjectSource(root, findings);
167
+ if (manifest?.runtime.nextVersionPolicy === "exact") {
168
+ inspectImageRuntime(root, findings, evaluations);
169
+ }
170
+ inspectProjectConfig(root, source, findings, evaluations);
171
+ inspectProjectSource(source, findings, evaluations);
172
+ inspectStaticRules({
173
+ root,
174
+ source,
175
+ config: readOptionalText(join(root, "brandlab.config.ts")),
176
+ findings,
177
+ evaluations,
178
+ });
150
179
  const sortedFindings = findings.sort(compareFindings);
151
- const policyResult = applyBrandlabVerificationPolicy(sortedFindings, options.verificationEvidence);
180
+ const policyResult = applyBrandlabVerificationPolicy(sortedFindings, options.verificationEvidence, evaluations);
152
181
  const operationalFindings = policyResult.operationalFindings.sort(compareFindings);
153
182
  const authorityDigest = digestBrandlabAuthority({
154
183
  release,
@@ -202,23 +231,28 @@ export function detectInstalledBrandlabRelease(packageJson) {
202
231
  return new Set(versions).size === 1 ? versions[0] : null;
203
232
  }
204
233
  function inspectRelease(options) {
234
+ recordEvaluation(options.evaluations, "release", "brandlab-release-package-json-missing");
205
235
  if (!options.packageJson) {
206
236
  options.findings.push(releaseFinding("brandlab-release-package-json-missing", "A readable package.json", "package.json is missing or invalid.", "Restore a valid package.json."));
207
237
  return;
208
238
  }
239
+ recordEvaluation(options.evaluations, "release", "brandlab-release-installed-version-unresolved");
209
240
  if (!options.installedRelease) {
210
241
  options.findings.push(releaseFinding("brandlab-release-installed-version-unresolved", "All four Brandlab packages pinned to one exact release", "The installed Brandlab release could not be resolved.", "Pin all four packages to one exact supported version."));
211
242
  return;
212
243
  }
244
+ recordEvaluation(options.evaluations, "release", "brandlab-release-unsupported");
213
245
  if (!options.manifest) {
214
246
  options.findings.push(releaseFinding("brandlab-release-unsupported", "The release selected by the active Brandlab channel", `${options.installedRelease} is not supported by the active platform.`, "Upgrade the project to the current candidate before further work."));
215
247
  return;
216
248
  }
249
+ recordEvaluation(options.evaluations, "release", "brandlab-release-declared-installed-mismatch");
217
250
  if (options.detectedRelease !== options.installedRelease) {
218
251
  options.findings.push(releaseFinding("brandlab-release-declared-installed-mismatch", options.installedRelease, options.detectedRelease ?? "unresolved", "Restore one exact package version across all four Brandlab packages.", "package.json"));
219
252
  }
220
253
  inspectBrandlabPackages({
221
254
  findings: options.findings,
255
+ evaluations: options.evaluations,
222
256
  packageJson: options.packageJson,
223
257
  packageLock: options.packageLock,
224
258
  installedRelease: options.installedRelease,
@@ -227,16 +261,19 @@ function inspectRelease(options) {
227
261
  });
228
262
  inspectRuntime({
229
263
  findings: options.findings,
264
+ evaluations: options.evaluations,
230
265
  packageJson: options.packageJson,
231
266
  packageLock: options.packageLock,
232
267
  manifest: options.manifest,
233
268
  });
234
269
  }
235
270
  function inspectBrandlabPackages(options) {
271
+ recordEvaluation(options.evaluations, "release", "brandlab-release-package-lock");
236
272
  if (options.packageLock.state !== "valid") {
237
273
  options.findings.push(releaseFinding(`brandlab-release-package-lock-${options.packageLock.state}`, "A valid npm lockfile v2 or v3", `package-lock.json state is ${options.packageLock.state}.`, "Regenerate the lockfile from the exact package declarations.", "package-lock.json"));
238
274
  return;
239
275
  }
276
+ recordEvaluation(options.evaluations, "release", "brandlab-release-package-pin-mismatch", "brandlab-release-package-resolution-mismatch", ...(options.attestation ? ["brandlab-release-package-integrity-mismatch"] : []));
240
277
  for (const packageName of BRANDLAB_SHARED_PACKAGE_NAMES) {
241
278
  const declared = declaredPackageSpec(options.packageJson, packageName);
242
279
  const locked = lockedRootSpec(options.packageLock, packageName);
@@ -258,12 +295,17 @@ function inspectBrandlabPackages(options) {
258
295
  : `version=${resolved.version ?? "missing"}; linked=${String(resolved.linked)}; integrity=${resolved.integrity ? "present" : "missing"}.`, "Run a clean npm install from the exact published release.", "package-lock.json"));
259
296
  }
260
297
  const expectedIntegrity = options.attestation?.packages[packageName].registry.integrity ?? null;
261
- if (expectedIntegrity && resolved?.integrity !== expectedIntegrity) {
298
+ if (options.attestation && !packed && !expectedIntegrity) {
299
+ // A bound manifest without registry integrity cannot prove the lockfile.
300
+ options.findings.push(releaseFinding("brandlab-release-package-integrity-mismatch", `${packageName} integrity matching the immutable manifest`, "The release manifest records no registry integrity for this package.", "Bind the verified release manifest recorded after registry publication.", "package-lock.json"));
301
+ }
302
+ else if (expectedIntegrity && resolved?.integrity !== expectedIntegrity) {
262
303
  options.findings.push(releaseFinding("brandlab-release-package-integrity-mismatch", `${packageName} integrity matching the immutable manifest`, "The lockfile integrity differs from the release attestation.", "Regenerate the lockfile from the verified registry package.", "package-lock.json"));
263
304
  }
264
305
  }
265
306
  }
266
307
  function inspectRuntime(options) {
308
+ recordEvaluation(options.evaluations, "runtime", ...RUNTIME_PACKAGE_FIELDS.map(([packageName]) => `brandlab-runtime-${slugify(packageName)}`), "brandlab-runtime-react-pair", "brandlab-runtime-node", "brandlab-runtime-package-manager", ...(options.manifest.runtime.sharp ? ["brandlab-runtime-sharp"] : []));
267
309
  for (const [packageName, runtimeField] of RUNTIME_PACKAGE_FIELDS) {
268
310
  const expected = options.manifest.runtime[runtimeField];
269
311
  const declared = declaredPackageSpec(options.packageJson, packageName);
@@ -276,6 +318,14 @@ function inspectRuntime(options) {
276
318
  options.findings.push(runtimeFinding(`brandlab-runtime-${slugify(packageName)}`, runtimeExpectation(packageName, expected, options.manifest.runtime.nextVersionPolicy), `package.json=${declared ?? "missing"}; lockfile=${locked ?? "missing"}; resolved=${resolved?.version ?? "missing"}.`, `Use the runtime versions permitted by the ${options.manifest.releaseVersion} contract; an exact mitigation pin requires a reviewed policy change before upgrading.`, "package.json"));
277
319
  }
278
320
  }
321
+ // React and React DOM are one runtime: a newer patch of one alone is a mismatch.
322
+ const declaredReact = declaredPackageSpec(options.packageJson, "react");
323
+ const declaredReactDom = declaredPackageSpec(options.packageJson, "react-dom");
324
+ const lockedReact = options.packageLock.resolvedPackages.get("react")?.version ?? null;
325
+ const lockedReactDom = options.packageLock.resolvedPackages.get("react-dom")?.version ?? null;
326
+ if (!declaredReact || declaredReact !== declaredReactDom || lockedReact !== lockedReactDom) {
327
+ options.findings.push(runtimeFinding("brandlab-runtime-react-pair", "react and react-dom declared and locked at the same exact version", `package.json react=${declaredReact ?? "missing"}, react-dom=${declaredReactDom ?? "missing"}; lockfile react=${lockedReact ?? "missing"}, react-dom=${lockedReactDom ?? "missing"}.`, "Pin react and react-dom to one exact version and regenerate the lockfile.", "package.json"));
328
+ }
279
329
  const node = options.packageJson.engines?.node ?? null;
280
330
  const lockNode = options.packageLock.rootEngines.node ?? null;
281
331
  if (node !== options.manifest.runtime.node || lockNode !== node) {
@@ -294,20 +344,22 @@ function inspectRuntime(options) {
294
344
  }
295
345
  }
296
346
  }
297
- function inspectImageRuntime(root, findings) {
347
+ function inspectImageRuntime(root, findings, evaluations) {
348
+ recordEvaluation(evaluations, "runtime", "brandlab-runtime-image-initialisation");
298
349
  const filename = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js"]
299
350
  .find((name) => existsSync(join(root, name)));
300
351
  const source = filename ? stripCommentsPreservingLayout(readOptionalText(join(root, filename)) ?? "") : "";
301
352
  // A direct side-effect import executes the shared guard before Next starts.
302
353
  // This source check is not runtime certification; packed/browser checks prove that separately.
303
- if (/^\s*import\s*['"]@brandlab-digital\/site-next\/image-runtime['"]/m.test(source) ||
304
- /^\s*require\(\s*['"]@brandlab-digital\/site-next\/image-runtime['"]\s*\)/m.test(source))
354
+ if (/^[ \t]*import[ \t]*['"]@brandlab-digital\/site-next\/image-runtime['"]/m.test(source) ||
355
+ /^[ \t]*require\([ \t]*['"]@brandlab-digital\/site-next\/image-runtime['"][ \t]*\)/m.test(source))
305
356
  return;
306
357
  findings.push(runtimeFinding("brandlab-runtime-image-initialisation", "The shared image-runtime side-effect import in next.config", "The startup decoder exclusion could not be established from configuration.", "Import @brandlab-digital/site-next/image-runtime at the top of next.config; verify the packed runtime and affected images.", filename ?? "next.config.ts"));
307
358
  }
308
- function inspectProjectConfig(root, findings) {
359
+ function inspectProjectConfig(root, projectSource, findings, evaluations) {
309
360
  const configPath = join(root, "brandlab.config.ts");
310
361
  const source = readOptionalText(configPath);
362
+ recordEvaluation(evaluations, "routes", "brandlab-config-missing", "brandlab-config-invalid");
311
363
  if (!source) {
312
364
  findings.push(staticFinding({
313
365
  ruleId: "brandlab-config-missing",
@@ -319,31 +371,33 @@ function inspectProjectConfig(root, findings) {
319
371
  }));
320
372
  return;
321
373
  }
374
+ // 3.0 removed the profile field; a leftover one is reported as a retired API.
322
375
  if (!/\bdefineBrandlabProject\s*\(\s*\{/.test(source) ||
323
- !/\bprojectId\s*:\s*['"][a-z0-9][a-z0-9-]*['"]/.test(source) ||
324
- !/\bprofile\s*:\s*['"]next-(?:local-service|root-level)['"]/.test(source)) {
376
+ !/\bprojectId\s*:\s*['"][a-z0-9][a-z0-9-]*['"]/.test(source)) {
325
377
  findings.push(staticFinding({
326
378
  ruleId: "brandlab-config-invalid",
327
379
  surface: "routes",
328
380
  path: "brandlab.config.ts",
329
- expected: "A literal defineBrandlabProject configuration with projectId and profile",
381
+ expected: "A literal defineBrandlabProject configuration with projectId",
330
382
  actual: "The minimum project identity contract could not be verified.",
331
383
  remediation: "Keep the configuration literal and limited to project variation.",
332
384
  }));
333
385
  }
334
- const envSource = readOptionalText(join(root, ".env.example")) ?? "";
335
- if (/\bcontact\s*:\s*\{/.test(source)) {
336
- requireEnvKey(findings, envSource, "RESEND_API_KEY", "contact");
337
- requireEnvKey(findings, envSource, "CONTACT_EMAIL", "contact");
386
+ const envKeys = parseEnvironmentKeys(readOptionalText(join(root, ".env.example")) ?? "");
387
+ const contracts = findContactContractRecipients(projectSource);
388
+ if (/\bcontact\s*:\s*\{/.test(source) || contracts.length > 0) {
389
+ requireEnvKey(findings, evaluations, envKeys, "RESEND_API_KEY", "contact");
390
+ inspectContactRecipients(contracts, envKeys, findings, evaluations);
338
391
  }
339
392
  const reviewsConfig = source.match(/\breviews\s*:\s*\{([^{}]*)\}/)?.[1] ?? "";
340
393
  const usesLiveGooglePlacesReviews = /\bprovider\s*:\s*['"]google-places['"]/.test(reviewsConfig) &&
341
394
  /\bmode\s*:\s*['"](?:summary|full)['"]/.test(reviewsConfig);
342
395
  if (usesLiveGooglePlacesReviews) {
343
- requireEnvKey(findings, envSource, "GOOGLE_MAPS_PLATFORM_API_KEY", "reviews");
396
+ requireEnvKey(findings, evaluations, envKeys, "GOOGLE_MAPS_PLATFORM_API_KEY", "reviews");
344
397
  }
398
+ recordEvaluation(evaluations, "environment", "brandlab-environment-retired-contact-state");
345
399
  for (const retired of ["CONTACT_CONTEXT_SECRET", "KV_REST_API_URL", "KV_REST_API_TOKEN"]) {
346
- if (new RegExp(`^${retired}=`, "m").test(envSource)) {
400
+ if (envKeys.has(retired)) {
347
401
  findings.push(staticFinding({
348
402
  ruleId: "brandlab-environment-retired-contact-state",
349
403
  surface: "environment",
@@ -355,11 +409,74 @@ function inspectProjectConfig(root, findings) {
355
409
  }
356
410
  }
357
411
  }
358
- function requireEnvKey(findings, envSource, key, capability) {
359
- if (new RegExp(`^${key}=`, "m").test(envSource))
412
+ /** The environment variable key rule site-core applies to delivery.recipientEnv. */
413
+ const ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
414
+ /** Variable names documented in an env example file (`KEY=`, `export KEY=`); values are ignored. */
415
+ export function parseEnvironmentKeys(text) {
416
+ const keys = new Set();
417
+ for (const rawLine of text.split(/\r?\n/)) {
418
+ const line = rawLine.trim();
419
+ if (!line || line.startsWith("#"))
420
+ continue;
421
+ const assignment = line.startsWith("export ") ? line.slice("export ".length).trimStart() : line;
422
+ const separator = assignment.indexOf("=");
423
+ if (separator <= 0)
424
+ continue;
425
+ const key = assignment.slice(0, separator).trimEnd();
426
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
427
+ keys.add(key);
428
+ }
429
+ return keys;
430
+ }
431
+ /** The recipient variable is whatever the contact contract declares, never an assumed name. */
432
+ function inspectContactRecipients(contracts, envKeys, findings, evaluations) {
433
+ recordEvaluation(evaluations, "contact", "brandlab-contact-recipient-env");
434
+ if (contracts.length === 0) {
435
+ findings.push(staticFinding({
436
+ ruleId: "brandlab-contact-recipient-env",
437
+ surface: "contact",
438
+ path: "src/config/contact.ts",
439
+ expected: "One defineContactContract contract declaring delivery.recipientEnv",
440
+ actual: "The contact capability is enabled but no contact contract was found.",
441
+ remediation: "Declare the contact contract with defineContactContract in src/config/contact.ts.",
442
+ }));
443
+ return;
444
+ }
445
+ for (const contract of contracts.filter(({ recipientEnv }) => !recipientEnv)) {
446
+ findings.push(staticFinding({
447
+ ruleId: "brandlab-contact-recipient-env",
448
+ surface: "contact",
449
+ path: contract.path,
450
+ line: contract.line,
451
+ expected: "delivery.recipientEnv declared as a literal environment variable key",
452
+ actual: "The recipient environment variable could not be read from the contract.",
453
+ remediation: "Write delivery.recipientEnv as a string literal such as 'CONTACT_EMAIL'.",
454
+ }));
455
+ }
456
+ for (const contract of contracts) {
457
+ if (!contract.recipientEnv || ENV_KEY_PATTERN.test(contract.recipientEnv))
458
+ continue;
459
+ findings.push(staticFinding({
460
+ ruleId: "brandlab-contact-recipient-env",
461
+ surface: "contact",
462
+ path: contract.path,
463
+ line: contract.line,
464
+ expected: "delivery.recipientEnv is an environment variable key (A-Z, 0-9 and _, starting with a letter)",
465
+ actual: `delivery.recipientEnv ${JSON.stringify(contract.recipientEnv.slice(0, 64))} is not an environment variable key.`,
466
+ remediation: "Name the recipient variable in upper case, for example 'CONTACT_EMAIL'.",
467
+ }));
468
+ }
469
+ const keys = [...new Set(contracts.flatMap(({ recipientEnv }) => recipientEnv && ENV_KEY_PATTERN.test(recipientEnv) ? [recipientEnv] : []))];
470
+ for (const key of keys.sort())
471
+ requireEnvKey(findings, evaluations, envKeys, key, "contact");
472
+ }
473
+ function requireEnvKey(findings, evaluations, envKeys, key, capability) {
474
+ const ruleId = `brandlab-environment-${capability}-${slugify(key)}`;
475
+ recordEvaluation(evaluations, "environment", ruleId);
476
+ if (envKeys.has(key))
360
477
  return;
361
478
  findings.push(staticFinding({
362
- ruleId: `brandlab-environment-${capability}-${slugify(key)}`,
479
+ ruleId,
363
480
  surface: "environment",
364
481
  path: ".env.example",
365
482
  expected: `${key} documented for the enabled ${capability} capability`,
@@ -367,112 +484,38 @@ function requireEnvKey(findings, envSource, key, capability) {
367
484
  remediation: `Document the non-secret ${key} variable name without adding a credential value.`,
368
485
  }));
369
486
  }
370
- function inspectProjectSource(root, findings) {
371
- const sourceRoot = join(root, "src");
372
- if (!existsSync(sourceRoot))
487
+ function inspectProjectSource(source, findings, evaluations) {
488
+ recordEvaluation(evaluations, "source", "brandlab-source-root-missing");
489
+ if (source.directoryRoots.length === 0) {
490
+ findings.push(staticFinding({
491
+ ruleId: "brandlab-source-root-missing",
492
+ surface: "source",
493
+ expected: `Project source under a recognised root (${BRANDLAB_SOURCE_ROOT_DIRECTORIES.map((name) => `${name}/`).join(", ")})`,
494
+ actual: "No recognised source root was found, so no source rule could run.",
495
+ remediation: "Keep application source under src/ (or a recognised legacy root) and rerun verification.",
496
+ }));
497
+ }
498
+ if (source.files.length === 0)
373
499
  return;
374
- for (const file of sourceFiles(sourceRoot)) {
375
- const source = readFileSync(file, "utf8");
376
- const executableSource = stripCommentsPreservingLayout(source);
500
+ for (const rule of SOURCE_RULES)
501
+ recordEvaluation(evaluations, rule.surface, rule.ruleId);
502
+ for (const file of source.files) {
377
503
  for (const rule of SOURCE_RULES) {
378
- const match = executableSource.match(rule.pattern);
504
+ const match = file.executable.match(rule.pattern);
379
505
  if (!match || match.index === undefined)
380
506
  continue;
381
507
  findings.push(staticFinding({
382
508
  ruleId: rule.ruleId,
383
509
  surface: rule.surface,
384
- path: normalizePath(relative(root, file)),
385
- line: lineAt(source, match.index),
510
+ path: file.path,
511
+ line: lineAt(file.text, match.index),
386
512
  expected: rule.expected,
387
- actual: "A project-owned implementation was detected.",
513
+ actual: rule.actual ?? "A project-owned implementation was detected.",
388
514
  remediation: rule.remediation,
389
515
  }));
390
516
  }
391
517
  }
392
518
  }
393
- function stripCommentsPreservingLayout(source) {
394
- const output = [...source];
395
- let state = "code";
396
- let escaped = false;
397
- for (let index = 0; index < output.length; index += 1) {
398
- const current = output[index];
399
- const next = output[index + 1];
400
- if (state === "line") {
401
- if (current === "\n" || current === "\r") {
402
- state = "code";
403
- }
404
- else {
405
- output[index] = " ";
406
- }
407
- continue;
408
- }
409
- if (state === "block") {
410
- if (current === "*" && next === "/") {
411
- output[index] = " ";
412
- output[index + 1] = " ";
413
- index += 1;
414
- state = "code";
415
- }
416
- else if (current !== "\n" && current !== "\r") {
417
- output[index] = " ";
418
- }
419
- continue;
420
- }
421
- if (state !== "code") {
422
- if (escaped) {
423
- escaped = false;
424
- }
425
- else if (current === "\\") {
426
- escaped = true;
427
- }
428
- else if ((state === "single" && current === "'") ||
429
- (state === "double" && current === '"') ||
430
- (state === "template" && current === "`")) {
431
- state = "code";
432
- }
433
- continue;
434
- }
435
- if (current === "'") {
436
- state = "single";
437
- }
438
- else if (current === '"') {
439
- state = "double";
440
- }
441
- else if (current === "`") {
442
- state = "template";
443
- }
444
- else if (current === "/" && next === "/") {
445
- output[index] = " ";
446
- output[index + 1] = " ";
447
- index += 1;
448
- state = "line";
449
- }
450
- else if (current === "/" && next === "*") {
451
- output[index] = " ";
452
- output[index + 1] = " ";
453
- index += 1;
454
- state = "block";
455
- }
456
- }
457
- return output.join("");
458
- }
459
- function sourceFiles(root) {
460
- const files = [];
461
- for (const entry of readdirSync(root, { withFileTypes: true })) {
462
- if (entry.name.startsWith(".") || entry.name === "node_modules")
463
- continue;
464
- const path = join(root, entry.name);
465
- if (entry.isDirectory()) {
466
- files.push(...sourceFiles(path));
467
- continue;
468
- }
469
- if (SOURCE_EXTENSIONS.has(extension(entry.name)) &&
470
- !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
471
- files.push(path);
472
- }
473
- }
474
- return files.sort();
475
- }
476
519
  function readPackageJson(root) {
477
520
  const source = readOptionalText(join(root, "package.json"));
478
521
  if (!source)
@@ -641,28 +684,11 @@ function runtimeFinding(ruleId, expected, actual, remediation, path) {
641
684
  remediation,
642
685
  };
643
686
  }
644
- function staticFinding(options) {
645
- return {
646
- ruleId: options.ruleId,
647
- surface: options.surface,
648
- severity: "blocker",
649
- contract: { name: "project", version: "current-only-v2" },
650
- evidenceKind: "static",
651
- ...(options.path ? { path: options.path } : {}),
652
- ...(options.line ? { line: options.line } : {}),
653
- expected: options.expected,
654
- actual: options.actual,
655
- remediation: options.remediation,
656
- };
657
- }
658
687
  function compareFindings(left, right) {
659
688
  return (left.ruleId.localeCompare(right.ruleId) ||
660
689
  (left.path ?? "").localeCompare(right.path ?? "") ||
661
690
  (left.line ?? 0) - (right.line ?? 0));
662
691
  }
663
- function readOptionalText(path) {
664
- return existsSync(path) ? readFileSync(path, "utf8") : null;
665
- }
666
692
  function stringRecord(value) {
667
693
  if (!isRecord(value))
668
694
  return {};
@@ -671,17 +697,7 @@ function stringRecord(value) {
671
697
  function isRecord(value) {
672
698
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
673
699
  }
674
- function extension(name) {
675
- const index = name.lastIndexOf(".");
676
- return index === -1 ? "" : name.slice(index);
677
- }
678
- function lineAt(source, index) {
679
- return source.slice(0, index).split("\n").length;
680
- }
681
700
  function slugify(value) {
682
701
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
683
702
  }
684
- function normalizePath(value) {
685
- return value.split(/[/\\]/).join("/");
686
- }
687
703
  //# sourceMappingURL=project.js.map