@decantr/verifier 2.3.1 → 2.3.3

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/README.md CHANGED
@@ -23,6 +23,9 @@ npm install @decantr/verifier
23
23
  - interaction findings now include scanned file counts, file line ranges, and expected signal evidence where available, so CLI health/check output can point agents at source-grounded remediation
24
24
  - contract-only Brownfield critique avoids requiring Decantr treatments/decorators when the project keeps its own styling authority
25
25
  - project audits check that `pack-manifest.json` references real pack markdown/JSON files on disk
26
+ - project audits tolerate partial or malformed generated review packs without crashing Project Health, so half-attached Brownfield projects still receive actionable findings
27
+ - Next.js static/document outputs are treated as framework-rendered documents instead of requiring a Vite-style `id="root"` mount element
28
+ - project source audits ignore test, spec, story, fixture, and mock files for production drift warnings such as localhost endpoints and unsafe rendering patterns
26
29
  - published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
27
30
  - project audits include runtime evidence when a built `dist/` output is present:
28
31
  - root document
package/dist/index.js CHANGED
@@ -349,6 +349,17 @@ function normalizeRouteHint(route) {
349
349
  }
350
350
  return route;
351
351
  }
352
+ function isNextProject(projectRoot) {
353
+ if (existsSync(join(projectRoot, "next.config.js")) || existsSync(join(projectRoot, "next.config.ts")) || existsSync(join(projectRoot, "next.config.mjs")) || existsSync(join(projectRoot, "next.config.cjs"))) {
354
+ return true;
355
+ }
356
+ try {
357
+ const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
358
+ return Boolean(pkg.dependencies?.next || pkg.devDependencies?.next);
359
+ } catch {
360
+ return false;
361
+ }
362
+ }
352
363
  async function startStaticServer(rootDir) {
353
364
  const server = createServer((req, res) => {
354
365
  const requestedUrl = new URL(req.url ?? "/", "http://127.0.0.1");
@@ -410,14 +421,15 @@ async function auditBuiltDist(projectRoot, options = {}) {
410
421
  };
411
422
  }
412
423
  const routeHints = Array.isArray(options.routeHints) && options.routeHints.length > 0 ? options.routeHints.map((route) => normalizeRouteHint(route)).filter(Boolean).slice(0, 8) : ["/"];
424
+ const frameworkDocumentOutput = isNextProject(projectRoot);
413
425
  const indexHtml = readFileSync(indexPath, "utf-8");
414
426
  const assetPaths = extractAssetPaths(indexHtml);
415
427
  const server = await startStaticServer(distDir);
416
428
  try {
417
429
  const rootResponse = await fetch(`${server.baseUrl}/`);
418
430
  const rootHtml = await rootResponse.text();
419
- const failures = [];
420
- const rootDocumentOk = rootResponse.ok && /id="root"/.test(rootHtml);
431
+ const failures = frameworkDocumentOutput ? ["next-build-output"] : [];
432
+ const rootDocumentOk = rootResponse.ok && (frameworkDocumentOutput || /id="root"/.test(rootHtml));
421
433
  const titleOk = /<title>[^<]+<\/title>/i.test(rootHtml);
422
434
  const langOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(rootHtml);
423
435
  const viewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(rootHtml);
@@ -497,7 +509,7 @@ async function auditBuiltDist(projectRoot, options = {}) {
497
509
  for (const routeHint of routeHints) {
498
510
  const routeResponse = await fetch(`${server.baseUrl}${routeHint}`);
499
511
  const routeHtml = await routeResponse.text();
500
- const routeRootDocumentOk = routeResponse.ok && /id="root"/.test(routeHtml);
512
+ const routeRootDocumentOk = routeResponse.ok && (frameworkDocumentOutput || /id="root"/.test(routeHtml));
501
513
  const routeTitleOk = /<title>[^<]+<\/title>/i.test(routeHtml);
502
514
  const routeLangOk = /<html[^>]*\slang=(["'])[^"']+\1/i.test(routeHtml);
503
515
  const routeViewportOk = /<meta[^>]+name=(["'])viewport\1[^>]*>/i.test(routeHtml);
@@ -1185,6 +1197,12 @@ function isAuditableSourceFile(filePath) {
1185
1197
  if (/\.d\.ts$/i.test(filePath)) return false;
1186
1198
  return /\.(?:[cm]?[jt]sx?)$/i.test(filePath);
1187
1199
  }
1200
+ function isNonProductionSourceAuditFile(filePath) {
1201
+ const normalized = normalizeSourceAuditPath(filePath);
1202
+ return /(?:^|\/)(?:__tests__|__mocks__|tests?|specs?|fixtures?|mocks?|stories?)(?:\/|$)/i.test(
1203
+ normalized
1204
+ ) || /\.(?:test|spec|stories|story|fixture|mock)\.[cm]?[jt]sx?$/i.test(normalized);
1205
+ }
1188
1206
  function collectProjectSourceFiles(projectRoot) {
1189
1207
  const candidates = [
1190
1208
  "src",
@@ -1412,10 +1430,10 @@ function auditProjectSourceTree(projectRoot) {
1412
1430
  absolutePath: sourceFile,
1413
1431
  relativePath: relative(projectRoot, sourceFile) || sourceFile,
1414
1432
  code: readFileSync2(sourceFile, "utf-8")
1415
- }));
1433
+ })).filter((entry) => !isNonProductionSourceAuditFile(entry.relativePath));
1416
1434
  const clientReachableFiles = collectClientReachableSourceFiles(projectRoot, sourceEntries);
1417
1435
  const summary = {
1418
- filesChecked: sourceFiles.length,
1436
+ filesChecked: sourceEntries.length,
1419
1437
  inlineStyles: createSourceAuditBucket(),
1420
1438
  componentStyleTags: createSourceAuditBucket(),
1421
1439
  localCssRuntimeSignals: createSourceAuditBucket(),
@@ -1894,7 +1912,7 @@ function extractRouteHintsFromEssence(essence) {
1894
1912
  return [...routes].filter(Boolean).slice(0, 8);
1895
1913
  }
1896
1914
  function summarizeTopology(essence, reviewPack) {
1897
- const features = new Set(reviewPack?.data.features ?? []);
1915
+ const features = new Set(reviewPack?.data?.features ?? []);
1898
1916
  const sectionRoles = /* @__PURE__ */ new Set();
1899
1917
  const gatewayRoutes = /* @__PURE__ */ new Set();
1900
1918
  const primaryRoutes = /* @__PURE__ */ new Set();
@@ -2111,7 +2129,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
2111
2129
  );
2112
2130
  return;
2113
2131
  }
2114
- if (runtimeAudit.rootDocumentOk === false) {
2132
+ if (runtimeAudit.rootDocumentOk === false && !isFrameworkBuildOutput) {
2115
2133
  findings.push(
2116
2134
  makeFinding({
2117
2135
  id: "runtime-root-document-invalid",
@@ -3767,6 +3785,8 @@ async function auditProject(projectRoot) {
3767
3785
  const reviewPack = loadReviewPack(projectRoot);
3768
3786
  const packManifest = loadPackManifest(projectRoot);
3769
3787
  const adoptionMode = readProjectAdoptionMode(projectRoot);
3788
+ const packHydrationOptional = adoptionMode === "contract-only";
3789
+ const packHydrationSeverity = packHydrationOptional ? "info" : "warn";
3770
3790
  const runtimeAudit = emptyRuntimeAudit();
3771
3791
  if (!existsSync2(essencePath)) {
3772
3792
  findings.push(
@@ -3844,10 +3864,10 @@ async function auditProject(projectRoot) {
3844
3864
  makeFinding({
3845
3865
  id: "pack-manifest-missing",
3846
3866
  category: "Execution Packs",
3847
- severity: "warn",
3848
- message: "Compiled execution pack manifest is missing.",
3867
+ severity: packHydrationSeverity,
3868
+ message: packHydrationOptional ? "Compiled execution pack manifest is not hydrated yet; this is optional for contract-only Brownfield adoption." : "Compiled execution pack manifest is missing.",
3849
3869
  evidence: [join2(projectRoot, ".decantr", "context", "pack-manifest.json")],
3850
- suggestedFix: "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate scaffold, review, mutation, section, and page packs."
3870
+ suggestedFix: packHydrationOptional ? "Optional: run `decantr registry compile-packs decantr.essence.json --write-context` when you want hosted page packs and review packs for richer assistant context." : "Run `decantr registry compile-packs decantr.essence.json --write-context` to hydrate scaffold, review, mutation, section, and page packs."
3851
3871
  })
3852
3872
  );
3853
3873
  } else {
@@ -3908,10 +3928,10 @@ async function auditProject(projectRoot) {
3908
3928
  makeFinding({
3909
3929
  id: "review-pack-file-missing",
3910
3930
  category: "Review Contract",
3911
- severity: "warn",
3912
- message: "The compiled review pack file is missing.",
3931
+ severity: packHydrationSeverity,
3932
+ message: packHydrationOptional ? "The compiled review pack file is not hydrated yet; contract-only Brownfield projects can continue without it." : "The compiled review pack file is missing.",
3913
3933
  evidence: [join2(projectRoot, ".decantr", "context", "review-pack.json")],
3914
- suggestedFix: "Hydrate the full hosted context bundle with `decantr registry compile-packs decantr.essence.json --write-context` so critique consumers can anchor findings to the compiled review contract."
3934
+ suggestedFix: packHydrationOptional ? "Optional: hydrate hosted packs later if you want critique consumers to anchor findings to the compiled review contract." : "Hydrate the full hosted context bundle with `decantr registry compile-packs decantr.essence.json --write-context` so critique consumers can anchor findings to the compiled review contract."
3915
3935
  })
3916
3936
  );
3917
3937
  }
@@ -3979,10 +3999,10 @@ function isCssWhitespace(char) {
3979
3999
  return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
3980
4000
  }
3981
4001
  function resolveFocusAreas(reviewPack) {
3982
- return reviewPack?.data.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
4002
+ return reviewPack?.data?.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
3983
4003
  }
3984
4004
  function resolveSeverityFromChecks(reviewPack, fallback, checkIds) {
3985
- const match = reviewPack?.successChecks.find((check) => checkIds.includes(check.id));
4005
+ const match = reviewPack?.successChecks?.find((check) => checkIds.includes(check.id));
3986
4006
  return match?.severity ?? fallback;
3987
4007
  }
3988
4008
  function findHardcodedColors(code) {
@@ -10984,11 +11004,16 @@ function critiqueSource({
10984
11004
  })
10985
11005
  );
10986
11006
  }
10987
- const hasProtectedRouteInReview = reviewPack?.data.routes.some((route) => isProtectedLikeRoute(route.path)) ?? false;
10988
- const hasRecoveryRouteInReview = reviewPack?.data.routes.some((route) => isRecoveryLikeRoute(route.path)) ?? false;
10989
- const hasSignInRouteInReview = reviewPack?.data.routes.some((route) => isSignInLikeRoute(route.path)) ?? false;
10990
- const hasRegistrationRouteInReview = reviewPack?.data.routes.some((route) => isRegistrationLikeRoute(route.path)) ?? false;
10991
- const hasAnonymousEntryRouteInReview = reviewPack?.data.routes.some((route) => isAnonymousEntryLikeRoute(route.path)) ?? false;
11007
+ const reviewRoutes = reviewPack?.data?.routes ?? [];
11008
+ const hasProtectedRouteInReview = reviewRoutes.some((route) => isProtectedLikeRoute(route.path));
11009
+ const hasRecoveryRouteInReview = reviewRoutes.some((route) => isRecoveryLikeRoute(route.path));
11010
+ const hasSignInRouteInReview = reviewRoutes.some((route) => isSignInLikeRoute(route.path));
11011
+ const hasRegistrationRouteInReview = reviewRoutes.some(
11012
+ (route) => isRegistrationLikeRoute(route.path)
11013
+ );
11014
+ const hasAnonymousEntryRouteInReview = reviewRoutes.some(
11015
+ (route) => isAnonymousEntryLikeRoute(route.path)
11016
+ );
10992
11017
  const signInFlowSignals = countSignInFlowSignals(code, filePath);
10993
11018
  const recoveryFlowSignals = countRecoveryFlowSignals(code, filePath);
10994
11019
  const signUpFlowSignals = countSignUpFlowSignals(code, filePath);
@@ -11024,7 +11049,7 @@ function critiqueSource({
11024
11049
  evidence: [
11025
11050
  filePath,
11026
11051
  `Sign-in flow signals: ${signInFlowSignals}`,
11027
- `Reviewed recovery routes: ${reviewPack?.data.routes.filter((route) => isRecoveryLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11052
+ `Reviewed recovery routes: ${reviewRoutes.filter((route) => isRecoveryLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11028
11053
  `Recovery route signals: ${authRecoveryRouteSignalCount}`
11029
11054
  ],
11030
11055
  file: filePath,
@@ -11042,7 +11067,7 @@ function critiqueSource({
11042
11067
  evidence: [
11043
11068
  filePath,
11044
11069
  `Sign-in flow signals: ${signInFlowSignals}`,
11045
- `Reviewed registration routes: ${reviewPack?.data.routes.filter((route) => isRegistrationLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11070
+ `Reviewed registration routes: ${reviewRoutes.filter((route) => isRegistrationLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11046
11071
  `Registration route signals: ${registrationRouteSignalCount}`
11047
11072
  ],
11048
11073
  file: filePath,
@@ -11060,7 +11085,7 @@ function critiqueSource({
11060
11085
  evidence: [
11061
11086
  filePath,
11062
11087
  `Recovery flow signals: ${recoveryFlowSignals}`,
11063
- `Reviewed anonymous entry routes: ${reviewPack?.data.routes.filter((route) => isAnonymousEntryLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11088
+ `Reviewed anonymous entry routes: ${reviewRoutes.filter((route) => isAnonymousEntryLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11064
11089
  `Anonymous entry route signals: ${anonymousEntryRouteSignalCount}`
11065
11090
  ],
11066
11091
  file: filePath,
@@ -11078,7 +11103,7 @@ function critiqueSource({
11078
11103
  evidence: [
11079
11104
  filePath,
11080
11105
  `Registration flow signals: ${signUpFlowSignals}`,
11081
- `Reviewed sign-in routes: ${reviewPack?.data.routes.filter((route) => isSignInLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11106
+ `Reviewed sign-in routes: ${reviewRoutes.filter((route) => isSignInLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11082
11107
  `Sign-in route signals: ${signInRouteSignalCount}`
11083
11108
  ],
11084
11109
  file: filePath,
@@ -11099,7 +11124,7 @@ function critiqueSource({
11099
11124
  `Auth callback error signals: ${authCallbackErrorSignalCount}`,
11100
11125
  `Auth callback exchange signals: ${authCallbackExchangeSignalCount}`,
11101
11126
  `Auth callback exchange error signals: ${authCallbackExchangeErrorSignalCount}`,
11102
- `Reviewed sign-in routes: ${reviewPack?.data.routes.filter((route) => isSignInLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11127
+ `Reviewed sign-in routes: ${reviewRoutes.filter((route) => isSignInLikeRoute(route.path)).map((route) => route.path).join(", ") || "none"}`,
11103
11128
  `Sign-in route signals: ${signInRouteSignalCount}`
11104
11129
  ],
11105
11130
  file: filePath,
@@ -11342,7 +11367,7 @@ function critiqueSource({
11342
11367
  })
11343
11368
  );
11344
11369
  }
11345
- const knownRoutes = reviewPack?.data.routes.length ?? packManifest?.pages.length ?? 0;
11370
+ const knownRoutes = reviewPack?.data?.routes?.length ?? packManifest?.pages.length ?? 0;
11346
11371
  const placeholderNavigationTargets = astSignals.placeholderNavigationTargetCount;
11347
11372
  scores.push({
11348
11373
  category: "Topology Context",