@nifrajs/cli 3.3.0 → 4.0.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 +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +7 -15
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +162 -713
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +58 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +323 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +1 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +145 -54
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +306 -87
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +60 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +298 -33
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +56 -16
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diagnostics.d.ts +86 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +85 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -4
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +93 -74
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +71 -16
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +5 -6
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +3 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +263 -131
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +20 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +26 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +113 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-path.d.ts +10 -0
- package/dist/mcp-path.d.ts.map +1 -0
- package/dist/mcp-path.js +51 -0
- package/dist/mcp-path.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-reflect.d.ts.map +1 -1
- package/dist/mcp-reflect.js +2 -2
- package/dist/mcp-reflect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +64 -45
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +124 -25
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +159 -55
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +18 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +165 -82
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +28 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +12 -2
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts +21 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +823 -0
- package/dist/review.js.map +1 -0
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts +3 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +829 -34
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +154 -90
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +8 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts +3 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +33 -16
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +4123 -548
- package/docs/types.json +14391 -10060
- package/package.json +19 -11
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +199 -797
- package/src/check-scan.ts +394 -63
- package/src/check.ts +146 -58
- package/src/cli.ts +347 -92
- package/src/command-catalog.ts +407 -45
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +66 -15
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diagnostics.ts +179 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +93 -82
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +144 -32
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +5 -7
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +301 -161
- package/src/mcp-http.ts +22 -5
- package/src/mcp-io.ts +130 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-path.ts +50 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-reflect.ts +3 -2
- package/src/mcp-render.ts +72 -42
- package/src/mcp-root.ts +130 -24
- package/src/mcp-run.ts +190 -56
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +193 -91
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +42 -8
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +988 -0
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +947 -35
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +175 -98
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +27 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +40 -16
package/dist/review.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { toSarifLog } from "./diagnostics.js";
|
|
3
|
+
import { RULE_CODES } from "./rules/codes.js";
|
|
4
|
+
import { LEGACY_RULE_CODES } from "./rules/legacy.js";
|
|
5
|
+
export class ReviewInputError extends Error {
|
|
6
|
+
exitCode = 2;
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ReviewInputError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const REVIEW_INPUT_KEYS = new Set(["strict", "diff", "sarif", "fix", "dryRun", "write", "json"]);
|
|
13
|
+
const CODE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
14
|
+
const SAFE_GIT_REF = /^[A-Za-z0-9][A-Za-z0-9._/@+-]{0,255}$/;
|
|
15
|
+
function compareText(left, right) {
|
|
16
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
17
|
+
}
|
|
18
|
+
const CHECK_IDS = [
|
|
19
|
+
"typecheck",
|
|
20
|
+
"typed-client",
|
|
21
|
+
"server-boundary",
|
|
22
|
+
"route-boundary",
|
|
23
|
+
"pipeline",
|
|
24
|
+
"security",
|
|
25
|
+
"route-assurance",
|
|
26
|
+
"capability-provenance",
|
|
27
|
+
"manifest",
|
|
28
|
+
"dependency",
|
|
29
|
+
"contract-witness",
|
|
30
|
+
"coverage",
|
|
31
|
+
"hydration",
|
|
32
|
+
"configuration",
|
|
33
|
+
];
|
|
34
|
+
const REQUIRED_CHECKS = new Set([
|
|
35
|
+
"typecheck",
|
|
36
|
+
"typed-client",
|
|
37
|
+
"server-boundary",
|
|
38
|
+
"route-boundary",
|
|
39
|
+
"pipeline",
|
|
40
|
+
"security",
|
|
41
|
+
"dependency",
|
|
42
|
+
]);
|
|
43
|
+
const CHECK_CATEGORIES = {
|
|
44
|
+
typecheck: "correctness",
|
|
45
|
+
"typed-client": "correctness",
|
|
46
|
+
"server-boundary": "boundary",
|
|
47
|
+
"route-boundary": "boundary",
|
|
48
|
+
pipeline: "correctness",
|
|
49
|
+
security: "security",
|
|
50
|
+
"route-assurance": "assurance",
|
|
51
|
+
"capability-provenance": "capability",
|
|
52
|
+
manifest: "assurance",
|
|
53
|
+
dependency: "dependency",
|
|
54
|
+
"contract-witness": "contract",
|
|
55
|
+
coverage: "coverage",
|
|
56
|
+
hydration: "hydration",
|
|
57
|
+
configuration: "configuration",
|
|
58
|
+
};
|
|
59
|
+
const CHECK_SOURCES = {
|
|
60
|
+
typecheck: "check",
|
|
61
|
+
"typed-client": "check",
|
|
62
|
+
"server-boundary": "check",
|
|
63
|
+
"route-boundary": "check",
|
|
64
|
+
pipeline: "check",
|
|
65
|
+
security: "check",
|
|
66
|
+
"route-assurance": "assurance",
|
|
67
|
+
"capability-provenance": "capability",
|
|
68
|
+
manifest: "manifest",
|
|
69
|
+
dependency: "check",
|
|
70
|
+
"contract-witness": "contract",
|
|
71
|
+
coverage: "coverage",
|
|
72
|
+
hydration: "assurance",
|
|
73
|
+
configuration: "collector",
|
|
74
|
+
};
|
|
75
|
+
const ALLOWED_FIXES = new Set(["manifest.sync", "workspace-dist.rebuild"]);
|
|
76
|
+
const CHECK_CODES = {
|
|
77
|
+
"NF-C001": "typecheck",
|
|
78
|
+
"NF-C002": "typed-client",
|
|
79
|
+
"NF-C003": "typed-client",
|
|
80
|
+
"NF-C004": "server-boundary",
|
|
81
|
+
"NF-C005": "typed-client",
|
|
82
|
+
"NF-C006": "security",
|
|
83
|
+
"NF-C007": "typed-client",
|
|
84
|
+
"NF-C008": "dependency",
|
|
85
|
+
"NF-C009": "dependency",
|
|
86
|
+
"NF-C010": "dependency",
|
|
87
|
+
"NF-C011": "pipeline",
|
|
88
|
+
"NF-C012": "manifest",
|
|
89
|
+
"NF-C013": "manifest",
|
|
90
|
+
"NF-C014": "capability-provenance",
|
|
91
|
+
"NF-C015": "route-assurance",
|
|
92
|
+
"NF-C016": "configuration",
|
|
93
|
+
"NF-C017": "configuration",
|
|
94
|
+
"NF-C018": "route-boundary",
|
|
95
|
+
"NF-C019": "route-boundary",
|
|
96
|
+
"NF-C020": "route-boundary",
|
|
97
|
+
"NF-C021": "route-boundary",
|
|
98
|
+
"NF-C022": "route-boundary",
|
|
99
|
+
"NF-C023": "route-boundary",
|
|
100
|
+
"NF-D001": "dependency",
|
|
101
|
+
"NF-A001": "route-assurance",
|
|
102
|
+
"NF-H001": "hydration",
|
|
103
|
+
"NF-H002": "hydration",
|
|
104
|
+
"NF-H003": "hydration",
|
|
105
|
+
"NF-H004": "hydration",
|
|
106
|
+
"NF-K001": "contract-witness",
|
|
107
|
+
};
|
|
108
|
+
for (const code of ["NF-S001", "NF-S002", "NF-S003", "NF-S004", "NF-S005", "NF-S006", "NF-S007"])
|
|
109
|
+
CHECK_CODES[code] = "security";
|
|
110
|
+
const REVIEW_INPUT_SCHEMA = {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
strict: { type: "boolean" },
|
|
114
|
+
diff: { type: "string" },
|
|
115
|
+
sarif: { type: "string" },
|
|
116
|
+
fix: { type: "boolean" },
|
|
117
|
+
dryRun: { type: "boolean" },
|
|
118
|
+
write: { type: "boolean" },
|
|
119
|
+
json: { type: "boolean" },
|
|
120
|
+
},
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
};
|
|
123
|
+
function isRecord(value) {
|
|
124
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
125
|
+
}
|
|
126
|
+
function optionalBoolean(raw, key) {
|
|
127
|
+
const value = raw[key];
|
|
128
|
+
if (value === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (typeof value !== "boolean")
|
|
131
|
+
throw new ReviewInputError(`${key} must be a boolean`);
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function optionalString(raw, key) {
|
|
135
|
+
const value = raw[key];
|
|
136
|
+
if (value === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (typeof value !== "string" ||
|
|
139
|
+
value.length === 0 ||
|
|
140
|
+
value.includes("\u0000") ||
|
|
141
|
+
value.includes("\\"))
|
|
142
|
+
throw new ReviewInputError(`${key} must be a non-empty safe string`);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
/** Parse and validate the shared CLI/MCP review input before any collector is invoked. */
|
|
146
|
+
export function parseReviewInput(value) {
|
|
147
|
+
if (!isRecord(value))
|
|
148
|
+
throw new ReviewInputError("review input must be an object");
|
|
149
|
+
for (const key of Object.keys(value))
|
|
150
|
+
if (!REVIEW_INPUT_KEYS.has(key))
|
|
151
|
+
throw new ReviewInputError(`unknown review option: ${key}`);
|
|
152
|
+
const strict = optionalBoolean(value, "strict");
|
|
153
|
+
const diff = optionalString(value, "diff");
|
|
154
|
+
const sarif = optionalString(value, "sarif");
|
|
155
|
+
const fix = optionalBoolean(value, "fix");
|
|
156
|
+
const dryRun = optionalBoolean(value, "dryRun");
|
|
157
|
+
const write = optionalBoolean(value, "write");
|
|
158
|
+
const json = optionalBoolean(value, "json");
|
|
159
|
+
if (dryRun === true && fix !== true)
|
|
160
|
+
throw new ReviewInputError("dryRun requires fix");
|
|
161
|
+
if (write === true && fix !== true)
|
|
162
|
+
throw new ReviewInputError("write requires fix");
|
|
163
|
+
if (dryRun === true && write === true)
|
|
164
|
+
throw new ReviewInputError("dryRun and write are mutually exclusive");
|
|
165
|
+
return {
|
|
166
|
+
...(strict === undefined ? {} : { strict }),
|
|
167
|
+
...(diff === undefined ? {} : { diff }),
|
|
168
|
+
...(sarif === undefined ? {} : { sarif }),
|
|
169
|
+
...(fix === undefined ? {} : { fix }),
|
|
170
|
+
...(dryRun === undefined ? {} : { dryRun }),
|
|
171
|
+
...(write === undefined ? {} : { write }),
|
|
172
|
+
...(json === undefined ? {} : { json }),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function normalizedCode(value) {
|
|
176
|
+
if (typeof value !== "string")
|
|
177
|
+
return "application-rule";
|
|
178
|
+
const code = LEGACY_RULE_CODES[value] ?? value;
|
|
179
|
+
return CODE_PATTERN.test(code) ? code : "application-rule";
|
|
180
|
+
}
|
|
181
|
+
function normalizedSeverity(value) {
|
|
182
|
+
return value === "error" ? "error" : value === "warn" || value === "warning" ? "warning" : "info";
|
|
183
|
+
}
|
|
184
|
+
function normalizedLocation(cwd, file, line) {
|
|
185
|
+
if (typeof file !== "string" ||
|
|
186
|
+
file.length === 0 ||
|
|
187
|
+
file.includes("\u0000") ||
|
|
188
|
+
file.includes("\\"))
|
|
189
|
+
return undefined;
|
|
190
|
+
if (/^[A-Za-z]:[\\/]/.test(file))
|
|
191
|
+
return undefined;
|
|
192
|
+
const candidate = isAbsolute(file) ? resolve(file) : resolve(cwd, file);
|
|
193
|
+
const path = relative(resolve(cwd), candidate).split(sep).join("/");
|
|
194
|
+
if (path.length === 0 || path === ".." || path.startsWith("../") || path.startsWith("/"))
|
|
195
|
+
return undefined;
|
|
196
|
+
if (path.split("/").some((part) => part === "." || part === ".." || part.length === 0))
|
|
197
|
+
return undefined;
|
|
198
|
+
return {
|
|
199
|
+
path,
|
|
200
|
+
...(typeof line === "number" && Number.isSafeInteger(line) && line > 0 ? { line } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function codeCheck(code) {
|
|
204
|
+
return CHECK_CODES[code] ?? "configuration";
|
|
205
|
+
}
|
|
206
|
+
function sourceFor(check) {
|
|
207
|
+
return CHECK_SOURCES[check];
|
|
208
|
+
}
|
|
209
|
+
async function sha256Text(value) {
|
|
210
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
211
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
212
|
+
}
|
|
213
|
+
async function composeReport(input) {
|
|
214
|
+
// Keep the optional review leaf off ordinary CLI startup. The command catalog can describe the
|
|
215
|
+
// command without importing the contract implementation; only an actual review loads it.
|
|
216
|
+
const { composeReviewReport } = await import("@nifrajs/agent-review");
|
|
217
|
+
return composeReviewReport(input);
|
|
218
|
+
}
|
|
219
|
+
async function evidenceRef(source, material, path) {
|
|
220
|
+
const digest = await sha256Text(material);
|
|
221
|
+
const token = `r-${digest.slice(0, 32)}`;
|
|
222
|
+
return { source, token, digest, ...(path === undefined ? {} : { path }) };
|
|
223
|
+
}
|
|
224
|
+
function diagnosticValue(result) {
|
|
225
|
+
if (result.structuredDiagnostics !== undefined)
|
|
226
|
+
return result.structuredDiagnostics;
|
|
227
|
+
return result.diagnostics.map((value) => ({
|
|
228
|
+
code: value.code ?? value.rule,
|
|
229
|
+
severity: value.severity === "warning" ? "warn" : value.severity,
|
|
230
|
+
message: "Nifra diagnostic",
|
|
231
|
+
...(value.file === undefined ? {} : { file: value.file }),
|
|
232
|
+
...(value.line === undefined ? {} : { line: value.line }),
|
|
233
|
+
...(value.fix === undefined ? {} : { fix: { recipe: value.fix } }),
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
async function candidateFinding(cwd, diagnostic, check, index) {
|
|
237
|
+
const code = normalizedCode(diagnostic.code);
|
|
238
|
+
const severity = normalizedSeverity(diagnostic.severity);
|
|
239
|
+
const location = normalizedLocation(cwd, diagnostic.file, diagnostic.line);
|
|
240
|
+
const material = `${check}\u0000${code}\u0000${location?.path ?? ""}\u0000${location?.line ?? ""}\u0000${index}`;
|
|
241
|
+
const evidence = await evidenceRef(sourceFor(check), material, location?.path);
|
|
242
|
+
const recipe = diagnostic.fix?.recipe;
|
|
243
|
+
const fix = typeof recipe === "string" && ALLOWED_FIXES.has(recipe)
|
|
244
|
+
? { recipe: recipe }
|
|
245
|
+
: undefined;
|
|
246
|
+
const finding = {
|
|
247
|
+
id: `f-${check}-${index}`,
|
|
248
|
+
check,
|
|
249
|
+
code,
|
|
250
|
+
severity,
|
|
251
|
+
category: CHECK_CATEGORIES[check],
|
|
252
|
+
...(location === undefined ? {} : { location }),
|
|
253
|
+
evidence: [evidence],
|
|
254
|
+
...(fix === undefined ? {} : { fix }),
|
|
255
|
+
};
|
|
256
|
+
return {
|
|
257
|
+
diagnostic,
|
|
258
|
+
check,
|
|
259
|
+
code,
|
|
260
|
+
severity,
|
|
261
|
+
...(location === undefined ? {} : { location }),
|
|
262
|
+
finding,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function checkConfigured(id, diagnostics, verification) {
|
|
266
|
+
if (REQUIRED_CHECKS.has(id))
|
|
267
|
+
return true;
|
|
268
|
+
if (diagnostics.length > 0)
|
|
269
|
+
return true;
|
|
270
|
+
switch (id) {
|
|
271
|
+
case "route-assurance":
|
|
272
|
+
return verification.config !== undefined || verification.assuranceConfigPresent;
|
|
273
|
+
case "capability-provenance":
|
|
274
|
+
return verification.capability !== undefined;
|
|
275
|
+
case "manifest":
|
|
276
|
+
return verification.evidence !== undefined;
|
|
277
|
+
case "contract-witness":
|
|
278
|
+
return false;
|
|
279
|
+
case "coverage":
|
|
280
|
+
return false;
|
|
281
|
+
case "hydration":
|
|
282
|
+
return false;
|
|
283
|
+
case "configuration":
|
|
284
|
+
return false;
|
|
285
|
+
default:
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function checkStatus(findings) {
|
|
290
|
+
return findings.some((finding) => finding.severity === "error") ? "fail" : "pass";
|
|
291
|
+
}
|
|
292
|
+
function countsFor(findings, outOfScope) {
|
|
293
|
+
return {
|
|
294
|
+
findings: findings.length,
|
|
295
|
+
errors: findings.filter((finding) => finding.severity === "error").length,
|
|
296
|
+
warnings: findings.filter((finding) => finding.severity === "warning").length,
|
|
297
|
+
info: findings.filter((finding) => finding.severity === "info").length,
|
|
298
|
+
outOfScope,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function safeGitRef(value) {
|
|
302
|
+
return (SAFE_GIT_REF.test(value) &&
|
|
303
|
+
!value.includes("..") &&
|
|
304
|
+
!value.includes("@{") &&
|
|
305
|
+
!value.includes("//") &&
|
|
306
|
+
!value.endsWith(".") &&
|
|
307
|
+
!value.endsWith("/") &&
|
|
308
|
+
!value
|
|
309
|
+
.split("/")
|
|
310
|
+
.some((part) => part.length === 0 ||
|
|
311
|
+
part === "." ||
|
|
312
|
+
part === ".." ||
|
|
313
|
+
part.startsWith(".") ||
|
|
314
|
+
part.endsWith(".lock")));
|
|
315
|
+
}
|
|
316
|
+
async function runGit(cwd, args) {
|
|
317
|
+
const process = Bun.spawn(["git", "-C", cwd, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
318
|
+
const [stdout] = await Promise.all([
|
|
319
|
+
new Response(process.stdout).text(),
|
|
320
|
+
new Response(process.stderr).text(),
|
|
321
|
+
]);
|
|
322
|
+
const exitCode = await process.exited;
|
|
323
|
+
return { ok: exitCode === 0, stdout };
|
|
324
|
+
}
|
|
325
|
+
function nulPaths(value) {
|
|
326
|
+
return value
|
|
327
|
+
.split("\u0000")
|
|
328
|
+
.filter((path) => path.length > 0)
|
|
329
|
+
.map((path) => path.replaceAll("\\", "/"));
|
|
330
|
+
}
|
|
331
|
+
async function diffScope(cwd, ref) {
|
|
332
|
+
if (!safeGitRef(ref))
|
|
333
|
+
throw new ReviewInputError("invalid git ref");
|
|
334
|
+
const verified = await runGit(cwd, [
|
|
335
|
+
"rev-parse",
|
|
336
|
+
"--verify",
|
|
337
|
+
"--quiet",
|
|
338
|
+
"--end-of-options",
|
|
339
|
+
`${ref}^{commit}`,
|
|
340
|
+
]);
|
|
341
|
+
if (!verified.ok)
|
|
342
|
+
throw new ReviewInputError("invalid git ref");
|
|
343
|
+
const commit = verified.stdout.trim();
|
|
344
|
+
if (!/^[0-9a-f]{40,64}$/.test(commit))
|
|
345
|
+
throw new ReviewInputError("invalid git ref");
|
|
346
|
+
const changed = await runGit(cwd, [
|
|
347
|
+
"diff",
|
|
348
|
+
"--name-only",
|
|
349
|
+
"-z",
|
|
350
|
+
"--diff-filter=ACDMRTUXB",
|
|
351
|
+
commit,
|
|
352
|
+
"--",
|
|
353
|
+
]);
|
|
354
|
+
const untracked = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--"]);
|
|
355
|
+
if (!changed.ok || !untracked.ok)
|
|
356
|
+
throw new ReviewInputError("invalid git scope");
|
|
357
|
+
const paths = [...new Set([...nulPaths(changed.stdout), ...nulPaths(untracked.stdout)])].sort(compareText);
|
|
358
|
+
if (paths.some((path) => isAbsolute(path) ||
|
|
359
|
+
path.includes("\u0000") ||
|
|
360
|
+
path.includes("\\") ||
|
|
361
|
+
path.split("/").some((part) => part === "" || part === "." || part === "..")))
|
|
362
|
+
throw new ReviewInputError("invalid git scope");
|
|
363
|
+
return {
|
|
364
|
+
kind: "diff",
|
|
365
|
+
state: "valid",
|
|
366
|
+
gitRef: ref,
|
|
367
|
+
changedPaths: paths,
|
|
368
|
+
pathDigest: await sha256Text(JSON.stringify(paths)),
|
|
369
|
+
outOfScopeCount: 0,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function projectScope() {
|
|
373
|
+
const paths = [];
|
|
374
|
+
return {
|
|
375
|
+
kind: "project",
|
|
376
|
+
state: "valid",
|
|
377
|
+
changedPaths: paths,
|
|
378
|
+
pathDigest: await sha256Text(JSON.stringify(paths)),
|
|
379
|
+
outOfScopeCount: 0,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function invalidScope(kind, reasonCode) {
|
|
383
|
+
return {
|
|
384
|
+
kind,
|
|
385
|
+
state: "invalid",
|
|
386
|
+
changedPaths: [],
|
|
387
|
+
pathDigest: "0".repeat(64),
|
|
388
|
+
outOfScopeCount: 0,
|
|
389
|
+
reasonCode,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function inconclusiveReport(kind, reasonCode) {
|
|
393
|
+
const scope = invalidScope(kind, reasonCode);
|
|
394
|
+
const evidence = await evidenceRef("collector", `inconclusive\u0000${reasonCode}`);
|
|
395
|
+
const check = {
|
|
396
|
+
id: "configuration",
|
|
397
|
+
required: true,
|
|
398
|
+
status: "error",
|
|
399
|
+
duration: "none",
|
|
400
|
+
counts: { findings: 0, errors: 0, warnings: 0, info: 0, outOfScope: 0 },
|
|
401
|
+
findingIds: [],
|
|
402
|
+
evidence: [evidence],
|
|
403
|
+
reasonCode,
|
|
404
|
+
};
|
|
405
|
+
return composeReport({
|
|
406
|
+
strict: false,
|
|
407
|
+
scope,
|
|
408
|
+
checks: [check],
|
|
409
|
+
findings: [],
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* A collector or composer failure must never turn the review command into an unstructured crash.
|
|
414
|
+
* The normal path uses the public composer; this last-resort report is deliberately tiny and has
|
|
415
|
+
* no collector payload. The digest is still computed through the public canonicalizer whenever it
|
|
416
|
+
* is available, so an agent can consume the result with the same parser as every other report.
|
|
417
|
+
*/
|
|
418
|
+
async function safeInconclusiveReport(kind, reasonCode) {
|
|
419
|
+
try {
|
|
420
|
+
return await inconclusiveReport(kind, reasonCode);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
const evidence = {
|
|
424
|
+
source: "collector",
|
|
425
|
+
token: "r-inconclusive",
|
|
426
|
+
digest: "0".repeat(64),
|
|
427
|
+
};
|
|
428
|
+
const report = {
|
|
429
|
+
version: 1,
|
|
430
|
+
strict: false,
|
|
431
|
+
scope: invalidScope(kind, reasonCode),
|
|
432
|
+
checks: [
|
|
433
|
+
{
|
|
434
|
+
id: "configuration",
|
|
435
|
+
required: true,
|
|
436
|
+
status: "error",
|
|
437
|
+
duration: "none",
|
|
438
|
+
counts: { findings: 0, errors: 0, warnings: 0, info: 0, outOfScope: 0 },
|
|
439
|
+
findingIds: [],
|
|
440
|
+
evidence: [evidence],
|
|
441
|
+
reasonCode,
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
findings: [],
|
|
445
|
+
blocking: 0,
|
|
446
|
+
ok: false,
|
|
447
|
+
status: "inconclusive",
|
|
448
|
+
digest: "0".repeat(64),
|
|
449
|
+
};
|
|
450
|
+
try {
|
|
451
|
+
const { digestReviewReport } = await import("@nifrajs/agent-review");
|
|
452
|
+
return { ...report, digest: await digestReviewReport(report) };
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return report;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function composeCollectedReport(cwd, input, scope, verification, checkResult) {
|
|
460
|
+
const diagnostics = diagnosticValue(checkResult)
|
|
461
|
+
.map((diagnostic, index) => ({ diagnostic, index }))
|
|
462
|
+
.sort((left, right) => {
|
|
463
|
+
const leftLocation = normalizedLocation(cwd, left.diagnostic.file, left.diagnostic.line);
|
|
464
|
+
const rightLocation = normalizedLocation(cwd, right.diagnostic.file, right.diagnostic.line);
|
|
465
|
+
return (compareText(normalizedCode(left.diagnostic.code), normalizedCode(right.diagnostic.code)) ||
|
|
466
|
+
compareText(normalizedSeverity(left.diagnostic.severity), normalizedSeverity(right.diagnostic.severity)) ||
|
|
467
|
+
compareText(leftLocation?.path ?? "", rightLocation?.path ?? "") ||
|
|
468
|
+
(leftLocation?.line ?? 0) - (rightLocation?.line ?? 0) ||
|
|
469
|
+
compareText(left.diagnostic.fix?.recipe ?? "", right.diagnostic.fix?.recipe ?? "") ||
|
|
470
|
+
left.index - right.index);
|
|
471
|
+
});
|
|
472
|
+
const candidatesByCheck = new Map();
|
|
473
|
+
for (const id of CHECK_IDS)
|
|
474
|
+
candidatesByCheck.set(id, []);
|
|
475
|
+
for (let index = 0; index < diagnostics.length; index += 1) {
|
|
476
|
+
const diagnostic = diagnostics[index].diagnostic;
|
|
477
|
+
const candidate = await candidateFinding(cwd, diagnostic, codeCheck(normalizedCode(diagnostic.code)), index);
|
|
478
|
+
candidatesByCheck.get(candidate.check)?.push(candidate);
|
|
479
|
+
}
|
|
480
|
+
const changedPaths = new Set(scope.changedPaths);
|
|
481
|
+
const retainedByCheck = new Map();
|
|
482
|
+
let totalOutOfScope = 0;
|
|
483
|
+
let typeErrorOutOfScope = false;
|
|
484
|
+
for (const id of CHECK_IDS) {
|
|
485
|
+
const retained = [];
|
|
486
|
+
for (const candidate of candidatesByCheck.get(id) ?? []) {
|
|
487
|
+
if (scope.kind === "diff" &&
|
|
488
|
+
candidate.location !== undefined &&
|
|
489
|
+
!changedPaths.has(candidate.location.path)) {
|
|
490
|
+
totalOutOfScope += 1;
|
|
491
|
+
if (id === "typecheck" && candidate.severity === "error")
|
|
492
|
+
typeErrorOutOfScope = true;
|
|
493
|
+
}
|
|
494
|
+
else
|
|
495
|
+
retained.push(candidate);
|
|
496
|
+
}
|
|
497
|
+
retainedByCheck.set(id, retained);
|
|
498
|
+
}
|
|
499
|
+
const checks = [];
|
|
500
|
+
const findings = [];
|
|
501
|
+
for (const id of CHECK_IDS) {
|
|
502
|
+
const all = candidatesByCheck.get(id) ?? [];
|
|
503
|
+
const retained = retainedByCheck.get(id) ?? [];
|
|
504
|
+
const configured = checkConfigured(id, all, verification);
|
|
505
|
+
const isTypecheckUnavailable = id === "typecheck" &&
|
|
506
|
+
(checkResult.typecheck === "skipped" || checkResult.truncated !== undefined);
|
|
507
|
+
let status;
|
|
508
|
+
let reasonCode;
|
|
509
|
+
if (isTypecheckUnavailable) {
|
|
510
|
+
status = "unavailable";
|
|
511
|
+
reasonCode =
|
|
512
|
+
checkResult.truncated !== undefined
|
|
513
|
+
? "diagnostics-truncated"
|
|
514
|
+
: checkResult.typecheckNote?.toLowerCase().includes("typescript")
|
|
515
|
+
? "missing-typescript"
|
|
516
|
+
: "unsupported";
|
|
517
|
+
}
|
|
518
|
+
else if (id === "typecheck" && typeErrorOutOfScope) {
|
|
519
|
+
// A change can break a caller it never touched, so a type error outside the diff cannot be
|
|
520
|
+
// ruled out as this change's: the required check stays open instead of passing.
|
|
521
|
+
status = "unavailable";
|
|
522
|
+
reasonCode = "filtered-out-of-scope";
|
|
523
|
+
}
|
|
524
|
+
else if (!configured) {
|
|
525
|
+
status = "skipped";
|
|
526
|
+
reasonCode = "not-configured";
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
status = checkStatus(retained);
|
|
530
|
+
}
|
|
531
|
+
const checkEvidence = retained.length > 0
|
|
532
|
+
? retained
|
|
533
|
+
.map((candidate) => candidate.finding.evidence[0])
|
|
534
|
+
.filter((entry) => entry !== undefined)
|
|
535
|
+
: [await evidenceRef(sourceFor(id), `check\u0000${id}\u0000${status}`)];
|
|
536
|
+
const outOfScope = all.length - retained.length;
|
|
537
|
+
for (const candidate of retained)
|
|
538
|
+
findings.push(candidate.finding);
|
|
539
|
+
checks.push({
|
|
540
|
+
id,
|
|
541
|
+
required: REQUIRED_CHECKS.has(id),
|
|
542
|
+
status,
|
|
543
|
+
duration: status === "unavailable" && reasonCode !== "filtered-out-of-scope" ? "timeout" : "standard",
|
|
544
|
+
counts: countsFor(retained, outOfScope),
|
|
545
|
+
findingIds: retained.map((candidate) => candidate.finding.id),
|
|
546
|
+
evidence: checkEvidence,
|
|
547
|
+
...(reasonCode === undefined ? {} : { reasonCode }),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (verification.assuranceConfigPresent && verification.configError !== undefined) {
|
|
551
|
+
const id = "configuration";
|
|
552
|
+
const evidence = await evidenceRef("collector", "assurance-config-invalid");
|
|
553
|
+
const finding = {
|
|
554
|
+
id: "f-configuration-config",
|
|
555
|
+
check: id,
|
|
556
|
+
code: "NF-C015",
|
|
557
|
+
severity: "error",
|
|
558
|
+
category: "configuration",
|
|
559
|
+
evidence: [evidence],
|
|
560
|
+
};
|
|
561
|
+
findings.push(finding);
|
|
562
|
+
const index = checks.findIndex((check) => check.id === id);
|
|
563
|
+
const existing = checks[index];
|
|
564
|
+
if (existing !== undefined) {
|
|
565
|
+
checks[index] = {
|
|
566
|
+
...existing,
|
|
567
|
+
required: true,
|
|
568
|
+
status: "error",
|
|
569
|
+
counts: {
|
|
570
|
+
findings: existing.counts.findings + 1,
|
|
571
|
+
errors: existing.counts.errors + 1,
|
|
572
|
+
warnings: existing.counts.warnings,
|
|
573
|
+
info: existing.counts.info,
|
|
574
|
+
outOfScope: existing.counts.outOfScope,
|
|
575
|
+
},
|
|
576
|
+
findingIds: [...existing.findingIds, finding.id],
|
|
577
|
+
evidence: [evidence],
|
|
578
|
+
reasonCode: "config-invalid",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const finalScope = {
|
|
583
|
+
...scope,
|
|
584
|
+
outOfScopeCount: totalOutOfScope,
|
|
585
|
+
};
|
|
586
|
+
const fixes = input.fix === true
|
|
587
|
+
? [
|
|
588
|
+
...new Set(findings.flatMap((finding) => (finding.fix === undefined ? [] : [finding.fix.recipe]))),
|
|
589
|
+
]
|
|
590
|
+
.sort(compareText)
|
|
591
|
+
.map((recipe) => ({ recipe, status: "planned" }))
|
|
592
|
+
: undefined;
|
|
593
|
+
const draft = {
|
|
594
|
+
strict: input.strict === true,
|
|
595
|
+
scope: finalScope,
|
|
596
|
+
checks,
|
|
597
|
+
findings,
|
|
598
|
+
...(fixes === undefined ? {} : { fixes }),
|
|
599
|
+
};
|
|
600
|
+
const report = await composeReport(draft);
|
|
601
|
+
return {
|
|
602
|
+
report,
|
|
603
|
+
candidates: [...retainedByCheck.values()].flat(),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function staticDiagnostic(candidate) {
|
|
607
|
+
const label = RULE_CODES[candidate.code] ?? "Nifra application rule";
|
|
608
|
+
return {
|
|
609
|
+
code: candidate.code,
|
|
610
|
+
severity: candidate.severity === "warning" ? "warn" : candidate.severity,
|
|
611
|
+
message: label,
|
|
612
|
+
...(candidate.location === undefined
|
|
613
|
+
? {}
|
|
614
|
+
: {
|
|
615
|
+
file: candidate.location.path,
|
|
616
|
+
...(candidate.location.line === undefined ? {} : { line: candidate.location.line }),
|
|
617
|
+
}),
|
|
618
|
+
// Fix recipes receive only the internal bounded evidence they need. The public report still
|
|
619
|
+
// exposes opaque references; raw package names/paths never cross the report, SARIF, MCP, or
|
|
620
|
+
// browser boundaries.
|
|
621
|
+
evidence: candidate.diagnostic.evidence?.slice(0, 8) ?? [
|
|
622
|
+
candidate.finding.evidence[0]?.token ?? "review",
|
|
623
|
+
],
|
|
624
|
+
...(candidate.finding.fix === undefined
|
|
625
|
+
? {}
|
|
626
|
+
: { fix: { recipe: candidate.finding.fix.recipe } }),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
async function safeChangedPaths(cwd, paths) {
|
|
630
|
+
const { resolveInsideProject } = await import("./project-path.js");
|
|
631
|
+
const result = [];
|
|
632
|
+
for (const path of paths) {
|
|
633
|
+
if (typeof path !== "string" || path.length === 0 || isAbsolute(path))
|
|
634
|
+
continue;
|
|
635
|
+
const resolved = await resolveInsideProject(cwd, path);
|
|
636
|
+
if (resolved === undefined)
|
|
637
|
+
continue;
|
|
638
|
+
const relativePath = relative(resolve(cwd), resolved).split(sep).join("/");
|
|
639
|
+
if (relativePath.length > 0 && relativePath !== ".." && !relativePath.startsWith("../"))
|
|
640
|
+
result.push(relativePath);
|
|
641
|
+
}
|
|
642
|
+
return [...new Set(result)].sort(compareText);
|
|
643
|
+
}
|
|
644
|
+
async function applyRequestedFixes(cwd, candidates) {
|
|
645
|
+
const { applyDiagnosticRecipe } = await import("./fix-recipes.js");
|
|
646
|
+
const results = [];
|
|
647
|
+
const seen = new Set();
|
|
648
|
+
for (const candidate of candidates) {
|
|
649
|
+
const recipe = candidate.finding.fix?.recipe;
|
|
650
|
+
if (recipe === undefined || seen.has(recipe))
|
|
651
|
+
continue;
|
|
652
|
+
seen.add(recipe);
|
|
653
|
+
try {
|
|
654
|
+
const changed = await applyDiagnosticRecipe(cwd, staticDiagnostic(candidate));
|
|
655
|
+
const safe = await safeChangedPaths(cwd, changed);
|
|
656
|
+
results.push(safe.length === 0
|
|
657
|
+
? { recipe, status: "no-op", reasonCode: "fix-no-op" }
|
|
658
|
+
: { recipe, status: "changed", changedPaths: safe });
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
results.push({ recipe, status: "failed", reasonCode: "fix-failed" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return results;
|
|
665
|
+
}
|
|
666
|
+
async function resolveSarifPath(cwd, path) {
|
|
667
|
+
const { resolveInsideProject } = await import("./project-path.js");
|
|
668
|
+
const resolved = await resolveInsideProject(cwd, path);
|
|
669
|
+
if (resolved === undefined)
|
|
670
|
+
throw new ReviewInputError("sarif path must stay inside the project");
|
|
671
|
+
return resolved;
|
|
672
|
+
}
|
|
673
|
+
async function writeSarif(cwd, path, report, cliVersion) {
|
|
674
|
+
const resolved = await resolveSarifPath(cwd, path);
|
|
675
|
+
const diagnostics = report.findings.map((finding) => {
|
|
676
|
+
const label = RULE_CODES[finding.code] ?? "Nifra application rule";
|
|
677
|
+
return {
|
|
678
|
+
code: finding.code,
|
|
679
|
+
severity: finding.severity === "warning" ? "warn" : finding.severity,
|
|
680
|
+
message: label,
|
|
681
|
+
...(finding.location === undefined
|
|
682
|
+
? {}
|
|
683
|
+
: {
|
|
684
|
+
file: finding.location.path,
|
|
685
|
+
...(finding.location.line === undefined ? {} : { line: finding.location.line }),
|
|
686
|
+
}),
|
|
687
|
+
evidence: [finding.evidence[0]?.token ?? "review"],
|
|
688
|
+
...(finding.fix === undefined ? {} : { fix: { recipe: finding.fix.recipe } }),
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
try {
|
|
692
|
+
await Bun.write(resolved, `${JSON.stringify(toSarifLog(diagnostics, {
|
|
693
|
+
toolName: "nifra",
|
|
694
|
+
...(cliVersion === undefined ? {} : { toolVersion: cliVersion }),
|
|
695
|
+
}), null, 2)}\n`);
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
throw new ReviewInputError("could not write SARIF output");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
export async function runReview(input, ctx) {
|
|
702
|
+
const parsed = parseReviewInput(input);
|
|
703
|
+
// Validate destination confinement before Git or project collectors run. A malformed output
|
|
704
|
+
// target is an input error, not an inconclusive project result, and must not spend collector work.
|
|
705
|
+
if (parsed.sarif !== undefined)
|
|
706
|
+
await resolveSarifPath(ctx.cwd, parsed.sarif);
|
|
707
|
+
let scope;
|
|
708
|
+
try {
|
|
709
|
+
scope = parsed.diff === undefined ? await projectScope() : await diffScope(ctx.cwd, parsed.diff);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (error instanceof ReviewInputError)
|
|
713
|
+
return safeInconclusiveReport(parsed.diff === undefined ? "project" : "diff", error.message.includes("ref") ? "invalid-git-ref" : "invalid-git-scope");
|
|
714
|
+
return safeInconclusiveReport(parsed.diff === undefined ? "project" : "diff", "collector-error");
|
|
715
|
+
}
|
|
716
|
+
let verification;
|
|
717
|
+
try {
|
|
718
|
+
const { collectProjectVerification } = await import("./verification.js");
|
|
719
|
+
verification = await collectProjectVerification(ctx.cwd, {
|
|
720
|
+
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return safeInconclusiveReport(scope.kind, "collector-error");
|
|
725
|
+
}
|
|
726
|
+
let checkResult;
|
|
727
|
+
try {
|
|
728
|
+
checkResult = await verification.check();
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return safeInconclusiveReport(scope.kind, "collector-error");
|
|
732
|
+
}
|
|
733
|
+
let collected;
|
|
734
|
+
try {
|
|
735
|
+
collected = await composeCollectedReport(ctx.cwd, parsed, scope, verification, checkResult);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
return safeInconclusiveReport(scope.kind, "collector-error");
|
|
739
|
+
}
|
|
740
|
+
let report = collected.report;
|
|
741
|
+
if (parsed.fix === true && parsed.write === true && report.status !== "inconclusive") {
|
|
742
|
+
try {
|
|
743
|
+
const fixResults = await applyRequestedFixes(ctx.cwd, collected.candidates);
|
|
744
|
+
const { collectProjectVerification } = await import("./verification.js");
|
|
745
|
+
const rerunVerification = await collectProjectVerification(ctx.cwd, {
|
|
746
|
+
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
|
|
747
|
+
});
|
|
748
|
+
const rerunChecks = await rerunVerification.check();
|
|
749
|
+
const rerun = await composeCollectedReport(ctx.cwd, { ...parsed, fix: false }, scope, rerunVerification, rerunChecks);
|
|
750
|
+
report = await composeReport({
|
|
751
|
+
strict: rerun.report.strict,
|
|
752
|
+
scope: rerun.report.scope,
|
|
753
|
+
checks: rerun.report.checks,
|
|
754
|
+
findings: rerun.report.findings,
|
|
755
|
+
fixes: fixResults,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return safeInconclusiveReport(scope.kind, "collector-error");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (parsed.sarif !== undefined && report.status !== "inconclusive")
|
|
763
|
+
await writeSarif(ctx.cwd, parsed.sarif, report, ctx.cliVersion);
|
|
764
|
+
return report;
|
|
765
|
+
}
|
|
766
|
+
export function renderReviewReport(report) {
|
|
767
|
+
const lines = [
|
|
768
|
+
report.status === "pass"
|
|
769
|
+
? `✓ review passed (${report.findings.length} findings)`
|
|
770
|
+
: report.status === "fail"
|
|
771
|
+
? `✖ review failed (${report.blocking} blocking)`
|
|
772
|
+
: "? review inconclusive (required evidence unavailable)",
|
|
773
|
+
` digest: ${report.digest}`,
|
|
774
|
+
];
|
|
775
|
+
for (const finding of report.findings) {
|
|
776
|
+
const location = finding.location === undefined
|
|
777
|
+
? ""
|
|
778
|
+
: ` ${finding.location.path}${finding.location.line === undefined ? "" : `:${finding.location.line}`}`;
|
|
779
|
+
lines.push(` ${finding.severity} ${finding.code} [${finding.check}]${location}`);
|
|
780
|
+
}
|
|
781
|
+
for (const check of report.checks) {
|
|
782
|
+
if (check.status === "skipped")
|
|
783
|
+
lines.push(` skipped ${check.id}`);
|
|
784
|
+
if (check.status === "unavailable" || check.status === "error")
|
|
785
|
+
lines.push(` unavailable ${check.id}${check.reasonCode === undefined ? "" : ` (${check.reasonCode})`}`);
|
|
786
|
+
}
|
|
787
|
+
return lines;
|
|
788
|
+
}
|
|
789
|
+
export const reviewSpec = {
|
|
790
|
+
name: "review",
|
|
791
|
+
summary: "Run the deterministic, evidence-backed Nifra review.",
|
|
792
|
+
input: {
|
|
793
|
+
jsonSchema: REVIEW_INPUT_SCHEMA,
|
|
794
|
+
parse: parseReviewInput,
|
|
795
|
+
},
|
|
796
|
+
output: {
|
|
797
|
+
version: 1,
|
|
798
|
+
jsonSchema: {
|
|
799
|
+
type: "object",
|
|
800
|
+
required: ["version", "scope", "checks", "findings", "blocking", "ok", "status", "digest"],
|
|
801
|
+
},
|
|
802
|
+
parse: (value) => value,
|
|
803
|
+
},
|
|
804
|
+
transports: ["cli", "mcp"],
|
|
805
|
+
stability: "experimental",
|
|
806
|
+
argv: {
|
|
807
|
+
flags: [
|
|
808
|
+
{ name: "strict", field: "strict", type: "boolean" },
|
|
809
|
+
{ name: "diff", field: "diff", type: "string" },
|
|
810
|
+
{ name: "sarif", field: "sarif", type: "string" },
|
|
811
|
+
{ name: "fix", field: "fix", type: "boolean" },
|
|
812
|
+
{ name: "dry-run", field: "dryRun", type: "boolean" },
|
|
813
|
+
{ name: "write", field: "write", type: "boolean" },
|
|
814
|
+
{ name: "json", field: "json", type: "boolean" },
|
|
815
|
+
],
|
|
816
|
+
},
|
|
817
|
+
run: runReview,
|
|
818
|
+
render: renderReviewReport,
|
|
819
|
+
success: (report) => report.ok,
|
|
820
|
+
exitCode: (report) => (report.status === "inconclusive" ? 2 : report.ok ? 0 : 1),
|
|
821
|
+
json: (report) => report,
|
|
822
|
+
};
|
|
823
|
+
//# sourceMappingURL=review.js.map
|