@decantr/verifier 2.3.2 → 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,8 @@ 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
26
28
  - project source audits ignore test, spec, story, fixture, and mock files for production drift warnings such as localhost endpoints and unsafe rendering patterns
27
29
  - published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
28
30
  - project audits include runtime evidence when a built `dist/` output is present:
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);
@@ -1900,7 +1912,7 @@ function extractRouteHintsFromEssence(essence) {
1900
1912
  return [...routes].filter(Boolean).slice(0, 8);
1901
1913
  }
1902
1914
  function summarizeTopology(essence, reviewPack) {
1903
- const features = new Set(reviewPack?.data.features ?? []);
1915
+ const features = new Set(reviewPack?.data?.features ?? []);
1904
1916
  const sectionRoles = /* @__PURE__ */ new Set();
1905
1917
  const gatewayRoutes = /* @__PURE__ */ new Set();
1906
1918
  const primaryRoutes = /* @__PURE__ */ new Set();
@@ -2117,7 +2129,7 @@ function appendRuntimeAuditFindings(findings, runtimeAudit, projectRoot, topolog
2117
2129
  );
2118
2130
  return;
2119
2131
  }
2120
- if (runtimeAudit.rootDocumentOk === false) {
2132
+ if (runtimeAudit.rootDocumentOk === false && !isFrameworkBuildOutput) {
2121
2133
  findings.push(
2122
2134
  makeFinding({
2123
2135
  id: "runtime-root-document-invalid",
@@ -3773,6 +3785,8 @@ async function auditProject(projectRoot) {
3773
3785
  const reviewPack = loadReviewPack(projectRoot);
3774
3786
  const packManifest = loadPackManifest(projectRoot);
3775
3787
  const adoptionMode = readProjectAdoptionMode(projectRoot);
3788
+ const packHydrationOptional = adoptionMode === "contract-only";
3789
+ const packHydrationSeverity = packHydrationOptional ? "info" : "warn";
3776
3790
  const runtimeAudit = emptyRuntimeAudit();
3777
3791
  if (!existsSync2(essencePath)) {
3778
3792
  findings.push(
@@ -3850,10 +3864,10 @@ async function auditProject(projectRoot) {
3850
3864
  makeFinding({
3851
3865
  id: "pack-manifest-missing",
3852
3866
  category: "Execution Packs",
3853
- severity: "warn",
3854
- 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.",
3855
3869
  evidence: [join2(projectRoot, ".decantr", "context", "pack-manifest.json")],
3856
- 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."
3857
3871
  })
3858
3872
  );
3859
3873
  } else {
@@ -3914,10 +3928,10 @@ async function auditProject(projectRoot) {
3914
3928
  makeFinding({
3915
3929
  id: "review-pack-file-missing",
3916
3930
  category: "Review Contract",
3917
- severity: "warn",
3918
- 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.",
3919
3933
  evidence: [join2(projectRoot, ".decantr", "context", "review-pack.json")],
3920
- 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."
3921
3935
  })
3922
3936
  );
3923
3937
  }
@@ -3985,10 +3999,10 @@ function isCssWhitespace(char) {
3985
3999
  return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
3986
4000
  }
3987
4001
  function resolveFocusAreas(reviewPack) {
3988
- return reviewPack?.data.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
4002
+ return reviewPack?.data?.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
3989
4003
  }
3990
4004
  function resolveSeverityFromChecks(reviewPack, fallback, checkIds) {
3991
- const match = reviewPack?.successChecks.find((check) => checkIds.includes(check.id));
4005
+ const match = reviewPack?.successChecks?.find((check) => checkIds.includes(check.id));
3992
4006
  return match?.severity ?? fallback;
3993
4007
  }
3994
4008
  function findHardcodedColors(code) {
@@ -10990,11 +11004,16 @@ function critiqueSource({
10990
11004
  })
10991
11005
  );
10992
11006
  }
10993
- const hasProtectedRouteInReview = reviewPack?.data.routes.some((route) => isProtectedLikeRoute(route.path)) ?? false;
10994
- const hasRecoveryRouteInReview = reviewPack?.data.routes.some((route) => isRecoveryLikeRoute(route.path)) ?? false;
10995
- const hasSignInRouteInReview = reviewPack?.data.routes.some((route) => isSignInLikeRoute(route.path)) ?? false;
10996
- const hasRegistrationRouteInReview = reviewPack?.data.routes.some((route) => isRegistrationLikeRoute(route.path)) ?? false;
10997
- 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
+ );
10998
11017
  const signInFlowSignals = countSignInFlowSignals(code, filePath);
10999
11018
  const recoveryFlowSignals = countRecoveryFlowSignals(code, filePath);
11000
11019
  const signUpFlowSignals = countSignUpFlowSignals(code, filePath);
@@ -11030,7 +11049,7 @@ function critiqueSource({
11030
11049
  evidence: [
11031
11050
  filePath,
11032
11051
  `Sign-in flow signals: ${signInFlowSignals}`,
11033
- `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"}`,
11034
11053
  `Recovery route signals: ${authRecoveryRouteSignalCount}`
11035
11054
  ],
11036
11055
  file: filePath,
@@ -11048,7 +11067,7 @@ function critiqueSource({
11048
11067
  evidence: [
11049
11068
  filePath,
11050
11069
  `Sign-in flow signals: ${signInFlowSignals}`,
11051
- `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"}`,
11052
11071
  `Registration route signals: ${registrationRouteSignalCount}`
11053
11072
  ],
11054
11073
  file: filePath,
@@ -11066,7 +11085,7 @@ function critiqueSource({
11066
11085
  evidence: [
11067
11086
  filePath,
11068
11087
  `Recovery flow signals: ${recoveryFlowSignals}`,
11069
- `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"}`,
11070
11089
  `Anonymous entry route signals: ${anonymousEntryRouteSignalCount}`
11071
11090
  ],
11072
11091
  file: filePath,
@@ -11084,7 +11103,7 @@ function critiqueSource({
11084
11103
  evidence: [
11085
11104
  filePath,
11086
11105
  `Registration flow signals: ${signUpFlowSignals}`,
11087
- `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"}`,
11088
11107
  `Sign-in route signals: ${signInRouteSignalCount}`
11089
11108
  ],
11090
11109
  file: filePath,
@@ -11105,7 +11124,7 @@ function critiqueSource({
11105
11124
  `Auth callback error signals: ${authCallbackErrorSignalCount}`,
11106
11125
  `Auth callback exchange signals: ${authCallbackExchangeSignalCount}`,
11107
11126
  `Auth callback exchange error signals: ${authCallbackExchangeErrorSignalCount}`,
11108
- `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"}`,
11109
11128
  `Sign-in route signals: ${signInRouteSignalCount}`
11110
11129
  ],
11111
11130
  file: filePath,
@@ -11348,7 +11367,7 @@ function critiqueSource({
11348
11367
  })
11349
11368
  );
11350
11369
  }
11351
- const knownRoutes = reviewPack?.data.routes.length ?? packManifest?.pages.length ?? 0;
11370
+ const knownRoutes = reviewPack?.data?.routes?.length ?? packManifest?.pages.length ?? 0;
11352
11371
  const placeholderNavigationTargets = astSignals.placeholderNavigationTargetCount;
11353
11372
  scores.push({
11354
11373
  category: "Topology Context",