@decantr/verifier 2.3.2 → 2.4.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.
- package/README.md +6 -0
- package/dist/index.js +43 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/decantr-ci-report.v1.json +48 -0
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:
|
|
@@ -65,6 +67,10 @@ function isBlocking(report: ProjectHealthReport) {
|
|
|
65
67
|
- `@decantr/verifier/schema/file-critique-report.v1.json`
|
|
66
68
|
- `@decantr/verifier/schema/showcase-shortlist-report.v1.json`
|
|
67
69
|
|
|
70
|
+
## Security And Permissions
|
|
71
|
+
|
|
72
|
+
The verifier is a local library. It reads selected project source, Decantr context, and built `dist`/`.next` output when callers request project or runtime audits. Built-output runtime audit starts a temporary loopback static server and fetches from that local server only; it does not contact external hosts. The verifier does not write files, spawn processes, emit telemetry, or upload source by itself. See [security permissions](https://decantr.ai/reference/security-permissions.md).
|
|
73
|
+
|
|
68
74
|
## Compatibility
|
|
69
75
|
|
|
70
76
|
`@decantr/verifier` is stable in the `2.x` line for the documented verifier APIs and published report-schema exports.
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
10994
|
-
const
|
|
10995
|
-
const
|
|
10996
|
-
const
|
|
10997
|
-
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
|
+
);
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
|
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",
|