@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 +3 -0
- package/dist/index.js +51 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
10988
|
-
const
|
|
10989
|
-
const
|
|
10990
|
-
const
|
|
10991
|
-
const
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
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",
|