@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/src/check-diagnostics.ts
CHANGED
|
@@ -6,31 +6,30 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs"
|
|
9
|
-
import { join
|
|
9
|
+
import { join } from "node:path"
|
|
10
10
|
import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
11
|
+
import type { CapabilityGap } from "@nifrajs/core/capabilities"
|
|
12
|
+
import type { ProjectEvidenceSnapshot } from "@nifrajs/core/evidence"
|
|
13
13
|
import type { CapabilityProjectReport } from "./capabilities-tool.ts"
|
|
14
|
-
import type { SourceFinding, StaticRouteFinding } from "./check-scan.ts"
|
|
15
14
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./
|
|
21
|
-
import { type Diagnostic, diagnostic } from "./diagnostics.ts"
|
|
15
|
+
type Diagnostic,
|
|
16
|
+
type DiagnosticSuggestion,
|
|
17
|
+
diagnostic,
|
|
18
|
+
diagnosticCompatibilityOf,
|
|
19
|
+
} from "./diagnostics.ts"
|
|
22
20
|
import type { DuplicateInstallFinding } from "./doctor.ts"
|
|
23
|
-
import type { TypeScriptApi } from "./internal/typescript-import.ts"
|
|
24
21
|
import type { PipelineReport } from "./pipeline-report.ts"
|
|
25
22
|
import { freezeProjectFacts, type ProjectFactsSeed } from "./project-facts.ts"
|
|
26
|
-
import {
|
|
23
|
+
import { dataGuardRules } from "./rules/data-guard.ts"
|
|
24
|
+
import { parseRulePacks, runRuleRegistry } from "./rules/index.ts"
|
|
27
25
|
import { islandRules } from "./rules/islands.ts"
|
|
28
|
-
import { LEGACY_RULE_CODES, legacyRules } from "./rules/legacy.ts"
|
|
26
|
+
import { LEGACY_RULE_CODES, LEGACY_RULE_ORDER, legacyRules } from "./rules/legacy.ts"
|
|
29
27
|
import { nanoRules } from "./rules/nano.ts"
|
|
28
|
+
import { pageRules } from "./rules/pages.ts"
|
|
30
29
|
import { routeRules } from "./rules/routes.ts"
|
|
30
|
+
import { secretRules } from "./rules/secrets.ts"
|
|
31
31
|
import { securityRules } from "./rules/security.ts"
|
|
32
|
-
|
|
33
|
-
const TSC_LINE = /^(.+?)\((\d+),\d+\):\s*(?:error|warning)\s+TS\d+:\s*(.+)$/
|
|
32
|
+
import { zoneRules } from "./rules/zones.ts"
|
|
34
33
|
|
|
35
34
|
/** A single machine-readable check failure - the unit an agent (or CI) acts on. */
|
|
36
35
|
export interface CheckDiagnostic {
|
|
@@ -67,14 +66,7 @@ export interface CheckDiagnostic {
|
|
|
67
66
|
readonly verify?: string
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
export
|
|
71
|
-
readonly kind: "edit" | "command" | "manual"
|
|
72
|
-
readonly title: string
|
|
73
|
-
readonly diff?: string
|
|
74
|
-
/** argv array, not a shell string, so MCP clients can run it without quoting hazards. */
|
|
75
|
-
readonly command?: readonly string[]
|
|
76
|
-
readonly steps?: readonly string[]
|
|
77
|
-
}
|
|
69
|
+
export type CheckSuggestion = DiagnosticSuggestion
|
|
78
70
|
|
|
79
71
|
/** The structured result of a full check - what `--json` prints and the `nifra_check` MCP tool returns. */
|
|
80
72
|
export interface CheckResult {
|
|
@@ -97,6 +89,9 @@ export interface CheckResult {
|
|
|
97
89
|
* better-auth). Echoed here so `--json` / the MCP tool / the report can show what the typed-client scan
|
|
98
90
|
* deliberately skipped - a suppressed prefix stays auditable instead of silently hiding real drift. */
|
|
99
91
|
readonly externalMounts?: readonly string[]
|
|
92
|
+
/** Mounts declared `opaque` - outside capability assurance on purpose, each with its stated reason.
|
|
93
|
+
* Listed on every run so the report states its own boundary; never a failure. */
|
|
94
|
+
readonly knownGaps?: readonly CapabilityGap[]
|
|
100
95
|
/** Active per-rule overrides from `nifra.check.json` `rules`, echoed verbatim so a retagged or
|
|
101
96
|
* suppressed finding stays auditable in `--json`, the MCP tool, and the human report - config can
|
|
102
97
|
* lower (or raise) the gate, but never invisibly. */
|
|
@@ -355,29 +350,6 @@ function matchIgnoreGlobClass(spec: string, value: string): boolean {
|
|
|
355
350
|
return negated ? !matched : matched
|
|
356
351
|
}
|
|
357
352
|
|
|
358
|
-
const UNTYPED_CLIENT_HINT =
|
|
359
|
-
'client("…") without a type argument - write client<typeof app>("…") (or client(contract, url)) so the compiler can catch drift'
|
|
360
|
-
const FETCH_HINT =
|
|
361
|
-
"hand-rolled fetch() to your own API - call it through client<typeof app> (from @nifrajs/client) so the compiler catches drift"
|
|
362
|
-
const SERVER_IMPORT_HINT =
|
|
363
|
-
"server-only import in a route module (bundled for the browser) - reach it via c.db / ctx.api inside a loader, never a top-level import"
|
|
364
|
-
const RESPONSE_ROUTE_HINT =
|
|
365
|
-
"route handler returns a raw Response - the typed client infers `data: never`, so drift detection is lost for this route. Return a plain object (it's serialized for you); for a stream use a typed SSE route (`app.sse(...)`), which keeps typed events; or, if a raw Response is intended (file/redirect), add `{ response: t.… }` or a `// nifra-expect raw-response` comment to mark it and silence this"
|
|
366
|
-
const PIPELINE_DOC_HINT =
|
|
367
|
-
"nifra runs one bundler per phase - `vitePlugins` feed Vite, `clientPlugins`/`serverPlugins` feed Bun, and the file `nifra build` imports the adapter from is bundled into the server. See the Gotchas section of the Dev & HMR guide."
|
|
368
|
-
const UNDECLARED_DEP_HINT =
|
|
369
|
-
"imported package is not declared in package.json dependencies - run bun add to declare it"
|
|
370
|
-
const SQL_COMPILER_MISSING_HINT =
|
|
371
|
-
"the interpolated-SQL rule did NOT run - it parses source with the TypeScript compiler, which is an optional peer and is not installed here. This report says nothing about SQL injection either way. Install it with `bun add -d typescript`"
|
|
372
|
-
const SQL_INTERPOLATION_EXAMPLE = "$" + "{value}"
|
|
373
|
-
const INTERPOLATED_SQL_HINT = `SQL built by interpolating a value into the statement text - the value becomes statement, not a parameter, so anything the caller controls can end the literal and continue as SQL. Pass it as a bound parameter (\`?\` / \`$1\` and an argument), or use your driver's tagged template (sql\`… ${SQL_INTERPOLATION_EXAMPLE} …\`), which binds the substitutions for you`
|
|
374
|
-
const MANIFEST_DRIFT_HINT =
|
|
375
|
-
"server-manifest.ts is out of sync with routes/ - re-run the build to regenerate it (a disk-less worker bakes this route table, so the drift is a silent edge break), then commit it"
|
|
376
|
-
const TRUST_MANIFEST_DRIFT_HINT =
|
|
377
|
-
"nifra.manifest.json is missing, invalid, or out of sync - run `nifra manifest emit`, review it, and commit the regenerated trust artifact"
|
|
378
|
-
const CAPABILITY_HINT =
|
|
379
|
-
"effect/capability assurance failed - align the route declaration with approved adapter provenance; never bypass an owned effect seam"
|
|
380
|
-
|
|
381
353
|
export interface CheckTypecheckResult {
|
|
382
354
|
readonly ran: boolean
|
|
383
355
|
readonly ok: boolean
|
|
@@ -388,10 +360,6 @@ export interface CheckTypecheckResult {
|
|
|
388
360
|
|
|
389
361
|
export interface CheckAnalysisInput {
|
|
390
362
|
readonly facts: ProjectFactsSeed
|
|
391
|
-
readonly typecheck: CheckTypecheckResult
|
|
392
|
-
readonly sqlCompiler: TypeScriptApi | undefined
|
|
393
|
-
readonly checkConfigError?: string
|
|
394
|
-
readonly checkConfigWarnings: readonly string[]
|
|
395
363
|
}
|
|
396
364
|
|
|
397
365
|
export interface CheckDiagnosticsOptions {
|
|
@@ -399,761 +367,175 @@ export interface CheckDiagnosticsOptions {
|
|
|
399
367
|
readonly assurance?: CheckAssuranceContext
|
|
400
368
|
}
|
|
401
369
|
|
|
402
|
-
const bySite = (a: SourceFinding, b: SourceFinding): number =>
|
|
403
|
-
a.file.localeCompare(b.file) || a.line - b.line
|
|
404
|
-
|
|
405
|
-
function oneLineDiff(file: string, line: number, before: string, after: string): string {
|
|
406
|
-
return `--- ${file}:${line}\n+++ ${file}:${line}\n@@\n-${before}\n+${after}`
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function untypedClientSuggestion(f: SourceFinding): CheckSuggestion {
|
|
410
|
-
const replacement = f.snippet.replace(/(?<![.\w])client\s*\(/, "client<typeof app>(")
|
|
411
|
-
return replacement === f.snippet
|
|
412
|
-
? {
|
|
413
|
-
kind: "manual",
|
|
414
|
-
title: "Add the app type argument to the client factory",
|
|
415
|
-
steps: [
|
|
416
|
-
'Change `client("...")` to `client<typeof app>("...")`.',
|
|
417
|
-
"Make sure the backend app type is imported or otherwise in scope.",
|
|
418
|
-
],
|
|
419
|
-
}
|
|
420
|
-
: {
|
|
421
|
-
kind: "edit",
|
|
422
|
-
title: "Insert `<typeof app>` into the client factory call",
|
|
423
|
-
diff: oneLineDiff(f.file, f.line, f.snippet, replacement),
|
|
424
|
-
steps: ["Make sure the backend app type is imported or otherwise in scope."],
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function routeKey(method: string, path: string): string {
|
|
429
|
-
return `${method.toUpperCase()} ${path}`
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function typedClientCall(method: string, path: string): string {
|
|
433
|
-
const segs = path.split("/").filter((seg) => seg !== "")
|
|
434
|
-
let chain = "api"
|
|
435
|
-
if (segs.length === 0) chain += ".index"
|
|
436
|
-
else {
|
|
437
|
-
for (const seg of segs) {
|
|
438
|
-
chain += IDENT.test(seg) ? `.${seg}` : `[${JSON.stringify(seg)}]`
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return `${chain}.${method.toLowerCase()}()`
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function staticRouteMap(routes: readonly StaticRouteFinding[]): Map<string, StaticRouteFinding[]> {
|
|
445
|
-
const out = new Map<string, StaticRouteFinding[]>()
|
|
446
|
-
for (const route of routes) {
|
|
447
|
-
if (route.path.includes(":") || route.path.includes("*")) continue
|
|
448
|
-
const key = routeKey(route.method, route.path)
|
|
449
|
-
const bucket = out.get(key)
|
|
450
|
-
if (bucket === undefined) out.set(key, [route])
|
|
451
|
-
else bucket.push(route)
|
|
452
|
-
}
|
|
453
|
-
return out
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function ownFetchEditSuggestion(
|
|
457
|
-
f: SourceFinding,
|
|
458
|
-
routes: Map<string, StaticRouteFinding[]>,
|
|
459
|
-
): CheckSuggestion | undefined {
|
|
460
|
-
const call = parseSimpleFetchCall(f.snippet)
|
|
461
|
-
if (call === undefined || !SIMPLE_REWRITE_METHODS.has(call.method)) return undefined
|
|
462
|
-
const matches = routes.get(routeKey(call.method, call.path))
|
|
463
|
-
if (matches === undefined || matches.length !== 1) return undefined
|
|
464
|
-
const replacementCall = typedClientCall(call.method, call.path)
|
|
465
|
-
const replacement = `${f.snippet.slice(0, call.start)}${replacementCall}${f.snippet.slice(call.end)}`
|
|
466
|
-
if (replacement === f.snippet) return undefined
|
|
467
|
-
const route = matches[0]
|
|
468
|
-
if (route === undefined) return undefined
|
|
469
|
-
return {
|
|
470
|
-
kind: "edit",
|
|
471
|
-
title: "Rewrite simple own-API fetch to the typed nifra client",
|
|
472
|
-
diff: oneLineDiff(f.file, f.line, f.snippet, replacement),
|
|
473
|
-
steps: [
|
|
474
|
-
`Matched ${route.method} ${route.path} at ${route.file}:${route.line}.`,
|
|
475
|
-
"Use an in-scope typed client named `api` (`client<typeof app>(baseUrl)` or the route loader/action `api`).",
|
|
476
|
-
"Update downstream `Response` handling to branch on `{ ok, data, error }` if this variable is used later.",
|
|
477
|
-
],
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function ownFetchSuggestion(
|
|
482
|
-
f: SourceFinding,
|
|
483
|
-
routes: Map<string, StaticRouteFinding[]>,
|
|
484
|
-
): CheckSuggestion {
|
|
485
|
-
const exact = ownFetchEditSuggestion(f, routes)
|
|
486
|
-
if (exact !== undefined) return exact
|
|
487
|
-
return {
|
|
488
|
-
kind: "manual",
|
|
489
|
-
title: "Replace own-API fetch with the typed nifra client",
|
|
490
|
-
steps: [
|
|
491
|
-
"Call `nifra_routes` or read `nifra://routes` for the exact typed-client call form.",
|
|
492
|
-
"Create `const api = client<typeof app>(baseUrl)` from `@nifrajs/client`.",
|
|
493
|
-
"Replace the relative `fetch()` call with the generated `api...get/post/...` call and branch on `{ ok, data, error }`.",
|
|
494
|
-
],
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function serverImportSuggestion(
|
|
499
|
-
specifier: string,
|
|
500
|
-
chain: readonly string[],
|
|
501
|
-
fallback: boolean,
|
|
502
|
-
): CheckSuggestion {
|
|
503
|
-
const sink = chain[chain.length - 1] ?? specifier
|
|
504
|
-
// Surface the resolved chain in the fix steps so the agent sees the full path (`route → ../data →
|
|
505
|
-
// ../db → node:crypto`) and which module to cut - not just the route's own top-level import.
|
|
506
|
-
const chainStep =
|
|
507
|
-
chain.length > 2
|
|
508
|
-
? fallback
|
|
509
|
-
? `Server-only code reaches this route through \`${chain.join(" → ")}\` (the deeper chain couldn't be resolved precisely - trace it from \`${specifier}\`).`
|
|
510
|
-
: `Server-only code reaches this route transitively: \`${chain.join(" → ")}\`. The sink is \`${sink}\`; break the chain at the first hop (\`${specifier}\`) or move the sink behind the server boundary.`
|
|
511
|
-
: undefined
|
|
512
|
-
return {
|
|
513
|
-
kind: "manual",
|
|
514
|
-
title: "Move server-only code behind the route server boundary",
|
|
515
|
-
steps: [
|
|
516
|
-
...(chainStep !== undefined ? [chainStep] : []),
|
|
517
|
-
`Remove the top-level \`import … from "${specifier}"\` from this route module (it's bundled for the browser).`,
|
|
518
|
-
"Access backend/data work through the route `loader`/`action` context (`api`, `env`, or project server context).",
|
|
519
|
-
`If a direct module import is unavoidable, lazy-load it (\`await import("${specifier}")\`) inside the server-only loader/action path.`,
|
|
520
|
-
],
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function responseRouteSuggestion(): CheckSuggestion {
|
|
525
|
-
return {
|
|
526
|
-
kind: "manual",
|
|
527
|
-
title: "Preserve typed-client response inference",
|
|
528
|
-
steps: [
|
|
529
|
-
"Prefer returning a plain object from JSON routes; nifra serializes it for you.",
|
|
530
|
-
"For a stream, use a typed SSE route - `app.sse(...)` (or `sse(c, run)` from `@nifrajs/core/server`) - which keeps typed events instead of collapsing the client to `data: never`.",
|
|
531
|
-
"If this route must return a raw Response (redirect, file), declare an explicit response schema, or add a `// nifra-expect raw-response` comment above the return to mark it intentional and silence this advisory.",
|
|
532
|
-
],
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
370
|
export async function collectCheckDiagnostics(
|
|
537
371
|
cwd: string,
|
|
538
372
|
scan: CheckAnalysisInput,
|
|
539
373
|
opts: CheckDiagnosticsOptions = {},
|
|
540
374
|
): Promise<CheckResult> {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const { checkConfig } = factsSeed.policies
|
|
548
|
-
const tc = scan.typecheck
|
|
549
|
-
const sqlCompiler = scan.sqlCompiler
|
|
550
|
-
const checkConfigError = scan.checkConfigError
|
|
551
|
-
const checkConfigWarnings = scan.checkConfigWarnings
|
|
552
|
-
const diagnostics: CheckDiagnostic[] = []
|
|
553
|
-
const structuredExtras: Diagnostic[] = []
|
|
554
|
-
if (checkConfigError !== undefined) {
|
|
555
|
-
diagnostics.push({
|
|
556
|
-
rule: "check-config",
|
|
557
|
-
severity: "warning",
|
|
558
|
-
file: "nifra.check.json",
|
|
559
|
-
message: `nifra.check.json could not be parsed (${checkConfigError}) - its external-mount allowlist was ignored`,
|
|
560
|
-
fix: "Fix the JSON syntax in nifra.check.json",
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
for (const warning of checkConfigWarnings) {
|
|
564
|
-
diagnostics.push({
|
|
565
|
-
rule: "check-config",
|
|
566
|
-
severity: "warning",
|
|
567
|
-
file: "nifra.check.json",
|
|
568
|
-
message: `nifra.check.json: ${warning}`,
|
|
569
|
-
fix: "Fix the entry in nifra.check.json",
|
|
570
|
-
})
|
|
571
|
-
}
|
|
572
|
-
if (tc.missingTypeScript === true) {
|
|
573
|
-
// A tsconfig with no reachable `typescript` install is a broken gate, not a benign skip: the
|
|
574
|
-
// contract check the project asked for (by having a tsconfig) silently didn't run. Fail closed.
|
|
575
|
-
diagnostics.push({
|
|
576
|
-
rule: "typecheck",
|
|
577
|
-
severity: "error",
|
|
578
|
-
file: "tsconfig.json",
|
|
579
|
-
message:
|
|
580
|
-
"tsconfig.json is present but no `typescript` install was found from this directory upward - the typecheck gate did NOT run",
|
|
581
|
-
fix: "bun add -d typescript",
|
|
582
|
-
suggestion: {
|
|
583
|
-
kind: "command",
|
|
584
|
-
title: "Install TypeScript so the contract gate can run",
|
|
585
|
-
command: ["bun", "add", "-d", "typescript"],
|
|
586
|
-
},
|
|
587
|
-
})
|
|
588
|
-
}
|
|
589
|
-
if (tc.ran && !tc.ok) {
|
|
590
|
-
const lines = (tc.output ?? "").split("\n")
|
|
591
|
-
let matched = false
|
|
592
|
-
for (const l of lines) {
|
|
593
|
-
const m = TSC_LINE.exec(l.trim())
|
|
594
|
-
if (m) {
|
|
595
|
-
matched = true
|
|
596
|
-
diagnostics.push({
|
|
597
|
-
rule: "typecheck",
|
|
598
|
-
severity: "error",
|
|
599
|
-
file: m[1] as string,
|
|
600
|
-
line: Number(m[2]),
|
|
601
|
-
message: m[3] as string,
|
|
602
|
-
suggestion: {
|
|
603
|
-
kind: "manual",
|
|
604
|
-
title: "Fix the TypeScript contract error",
|
|
605
|
-
steps: [
|
|
606
|
-
"Open the reported file and line.",
|
|
607
|
-
"Align the handler, route schema, or typed-client call with the compiler error.",
|
|
608
|
-
"Run `nifra_check` again after the edit.",
|
|
609
|
-
],
|
|
610
|
-
},
|
|
611
|
-
})
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (!matched)
|
|
615
|
-
diagnostics.push({
|
|
616
|
-
rule: "typecheck",
|
|
617
|
-
severity: "error",
|
|
618
|
-
message: tc.output || "typecheck failed",
|
|
619
|
-
suggestion: {
|
|
620
|
-
kind: "manual",
|
|
621
|
-
title: "Fix the TypeScript contract error",
|
|
622
|
-
steps: ["Run `tsc --noEmit` locally for the full compiler output."],
|
|
623
|
-
},
|
|
624
|
-
})
|
|
625
|
-
}
|
|
626
|
-
const routes = staticRouteMap(staticRoutes)
|
|
627
|
-
for (const f of [...fetches].sort(bySite)) {
|
|
628
|
-
diagnostics.push({
|
|
629
|
-
rule: "typed-client",
|
|
630
|
-
severity: "error",
|
|
631
|
-
file: f.file,
|
|
632
|
-
line: f.line,
|
|
633
|
-
message: `${f.snippet} - ${FETCH_HINT}`,
|
|
634
|
-
fix: FETCH_HINT,
|
|
635
|
-
suggestion: ownFetchSuggestion(f, routes),
|
|
636
|
-
})
|
|
637
|
-
}
|
|
638
|
-
for (const f of [...removedImports].sort(bySite)) {
|
|
639
|
-
const entry = REMOVED_IMPORTS.find(
|
|
640
|
-
(candidate) =>
|
|
641
|
-
f.snippet.includes(`"${candidate.specifier}`) ||
|
|
642
|
-
f.snippet.includes(`'${candidate.specifier}`),
|
|
643
|
-
)
|
|
644
|
-
diagnostics.push({
|
|
645
|
-
rule: "removed-import",
|
|
646
|
-
severity: "error",
|
|
647
|
-
file: f.file,
|
|
648
|
-
line: f.line,
|
|
649
|
-
message: `${f.snippet} - removed in nifra ${entry?.since ?? "2.0"}: ${entry?.replacement ?? "see the changelog"}`,
|
|
650
|
-
fix: entry?.replacement ?? "see the changelog",
|
|
651
|
-
})
|
|
652
|
-
}
|
|
653
|
-
for (const f of [...untypedClients].sort(bySite)) {
|
|
654
|
-
diagnostics.push({
|
|
655
|
-
rule: "untyped-client",
|
|
656
|
-
severity: "error",
|
|
657
|
-
file: f.file,
|
|
658
|
-
line: f.line,
|
|
659
|
-
message: `${f.snippet} - ${UNTYPED_CLIENT_HINT}`,
|
|
660
|
-
fix: UNTYPED_CLIENT_HINT,
|
|
661
|
-
suggestion: untypedClientSuggestion(f),
|
|
662
|
-
})
|
|
663
|
-
}
|
|
664
|
-
for (const f of [...serverImports].sort(bySite)) {
|
|
665
|
-
// #4.4: the FULL transitive chain the import-resolution walk found - `route → ../data → ../db →
|
|
666
|
-
// node:crypto`, matching the build leak-guard's depth - instead of just the direct edge. The chain's
|
|
667
|
-
// tail is the actual server-only sink; the head is the route. When a precise resolve wasn't possible
|
|
668
|
-
// (a bare pkg / path alias), `fallback` is set and the chain degrades to the honest direct edge.
|
|
669
|
-
const chain = f.chain
|
|
670
|
-
const sink = chain[chain.length - 1] ?? f.specifier
|
|
671
|
-
diagnostics.push({
|
|
672
|
-
rule: "server-only-import",
|
|
673
|
-
severity: "error",
|
|
674
|
-
file: f.file,
|
|
675
|
-
line: f.line,
|
|
676
|
-
message: `${f.snippet} - server-only "${sink}" reaches the browser bundle via ${chain.join(" → ")}${f.fallback ? " (direct edge - couldn't resolve the transitive chain precisely)" : ""}; ${SERVER_IMPORT_HINT}`,
|
|
677
|
-
fix: SERVER_IMPORT_HINT,
|
|
678
|
-
chain,
|
|
679
|
-
suggestion: serverImportSuggestion(f.specifier, chain, f.fallback),
|
|
680
|
-
})
|
|
681
|
-
}
|
|
682
|
-
if (sqlCompiler === undefined) {
|
|
683
|
-
// A security rule that could not run must say so. Silence here is indistinguishable from a clean
|
|
684
|
-
// result, and this is the one rule whose clean result means "no SQL injection was found".
|
|
685
|
-
diagnostics.push({
|
|
686
|
-
rule: "interpolated-sql",
|
|
687
|
-
severity: "warning",
|
|
688
|
-
message: SQL_COMPILER_MISSING_HINT,
|
|
689
|
-
fix: SQL_COMPILER_MISSING_HINT,
|
|
690
|
-
suggestion: {
|
|
691
|
-
kind: "command",
|
|
692
|
-
title: "Install TypeScript so the SQL rule can run",
|
|
693
|
-
command: ["bun add -d typescript"],
|
|
694
|
-
},
|
|
695
|
-
})
|
|
696
|
-
}
|
|
697
|
-
for (const f of [...interpolatedSql].sort(bySite)) {
|
|
698
|
-
diagnostics.push({
|
|
699
|
-
rule: "interpolated-sql",
|
|
700
|
-
severity: "error",
|
|
701
|
-
file: f.file,
|
|
702
|
-
line: f.line,
|
|
703
|
-
message: `${f.snippet} - ${INTERPOLATED_SQL_HINT}`,
|
|
704
|
-
fix: "bind the value as a parameter instead of interpolating it into the statement",
|
|
705
|
-
suggestion: {
|
|
706
|
-
kind: "manual",
|
|
707
|
-
title: "Bind the value instead of interpolating it",
|
|
708
|
-
steps: [
|
|
709
|
-
"Replace the interpolation with a placeholder your driver understands (`?` for SQLite/MySQL, `$1` for Postgres).",
|
|
710
|
-
"Pass the value as an argument alongside the statement, so the driver binds it.",
|
|
711
|
-
`Or switch to the driver's tagged template (sql\`… ${SQL_INTERPOLATION_EXAMPLE} …\`): the tag receives substitutions separately and binds them.`,
|
|
712
|
-
"An identifier that genuinely cannot be bound (a table or column name) must be checked against an allowlist you control, never taken from the request.",
|
|
713
|
-
"If the statement is dynamic but every value is already bound (generating `($1),($2),…` for a batch insert), mark it with a `// nifra-expect sql-dynamic: <reason>` comment on the line above - the reason is required.",
|
|
714
|
-
],
|
|
715
|
-
},
|
|
716
|
-
})
|
|
717
|
-
}
|
|
718
|
-
// Advisory - surfaced but NOT folded into `ok`, so it never fails the gate (a raw Response is valid).
|
|
719
|
-
for (const f of [...responseRoutes].sort(bySite)) {
|
|
720
|
-
diagnostics.push({
|
|
721
|
-
rule: "response-route",
|
|
722
|
-
severity: "warning",
|
|
723
|
-
file: f.file,
|
|
724
|
-
line: f.line,
|
|
725
|
-
message: `${f.snippet} - ${RESPONSE_ROUTE_HINT}`,
|
|
726
|
-
fix: RESPONSE_ROUTE_HINT,
|
|
727
|
-
suggestion: responseRouteSuggestion(),
|
|
728
|
-
})
|
|
729
|
-
}
|
|
730
|
-
if (dr.ran) {
|
|
731
|
-
for (const f of dr.findings) {
|
|
732
|
-
diagnostics.push({
|
|
733
|
-
rule: "undeclared-dependency",
|
|
734
|
-
severity: "error",
|
|
735
|
-
file: f.file,
|
|
736
|
-
line: f.line,
|
|
737
|
-
message: `imports ${f.package} which is not declared in package.json - ${UNDECLARED_DEP_HINT}`,
|
|
738
|
-
fix: `add ${f.package} to package.json dependencies`,
|
|
739
|
-
suggestion: {
|
|
740
|
-
kind: "command",
|
|
741
|
-
title: `Declare ${f.package} in package.json`,
|
|
742
|
-
command: ["bun", "add", f.package],
|
|
743
|
-
},
|
|
744
|
-
})
|
|
745
|
-
}
|
|
746
|
-
for (const finding of dr.duplicateInstalls) {
|
|
747
|
-
// One line per physical copy: version, resolved absolute path, and who pulled it in. The
|
|
748
|
-
// absolute path is what a reader pastes into an editor or `rm -rf`; the importer list is what
|
|
749
|
-
// tells them WHICH package.json to fix - a relative path alone answers neither.
|
|
750
|
-
const copyLines = finding.copies.map(
|
|
751
|
-
(copy) =>
|
|
752
|
-
`${finding.package}@${copy.version} at ${copy.absolutePath ?? copy.path} - pulled in by ${copy.importers.join(", ")}`,
|
|
753
|
-
)
|
|
754
|
-
diagnostics.push({
|
|
755
|
-
rule: "duplicate-install",
|
|
756
|
-
severity: "error",
|
|
757
|
-
message: `${finding.package} identity preflight found ${finding.cause} - ${finding.explanation}${finding.topology === undefined ? "" : `. Topology: ${finding.topology}`}${finding.scope === undefined ? "" : `. Scope: ${finding.scope}`}`,
|
|
758
|
-
fix: finding.remediation,
|
|
759
|
-
evidence: copyLines,
|
|
760
|
-
suggestion: {
|
|
761
|
-
kind: "manual",
|
|
762
|
-
title: `Resolve ${finding.package} to one loaded copy`,
|
|
763
|
-
// The renderer prints this block verbatim before the steps: the exact config for fix 2, so
|
|
764
|
-
// the reader pastes instead of transcribing prose into JSON and TOML.
|
|
765
|
-
diff: [
|
|
766
|
-
"package.json:",
|
|
767
|
-
` "nifra": { "singleCopy": ["${finding.package}"] }`,
|
|
768
|
-
"bunfig.toml:",
|
|
769
|
-
` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
|
|
770
|
-
" [test]",
|
|
771
|
-
` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
|
|
772
|
-
].join("\n"),
|
|
773
|
-
steps: [
|
|
774
|
-
...copyLines.map((line) => `Copy: ${line}`),
|
|
775
|
-
"Fix 1 - deduplicate: align workspace dependency and peer ranges on one compatible version, remove stale nested installs, and reinstall from the workspace root so every importer resolves one physical copy.",
|
|
776
|
-
`Fix 2 - declare single-copy: apply the package.json and bunfig.toml config printed above; nifra then rewrites every duplicate to this app's copy.${finding.cause === "version-skew" ? " A declaration only covers same-version duplicates, so fix 1's range alignment must land first." : ""}`,
|
|
777
|
-
"Re-run `nifra check`; the gate stays failing until one fix lands.",
|
|
778
|
-
],
|
|
779
|
-
},
|
|
780
|
-
})
|
|
781
|
-
}
|
|
782
|
-
// Declared single-copy: the resolver collapses these before anything loads, so the build is sound
|
|
783
|
-
// and the gate stays green. It is still worth a line - the guarantee lives in a declaration now,
|
|
784
|
-
// and a reader who deletes it gets a null hook dispatcher with no obvious cause.
|
|
785
|
-
for (const finding of dr.deduplicatedInstalls ?? []) {
|
|
786
|
-
const copies = finding.copies.map((copy) => copy.path).join("; ")
|
|
787
|
-
diagnostics.push({
|
|
788
|
-
rule: "duplicate-install",
|
|
789
|
-
severity: "warning",
|
|
790
|
-
message: `${finding.package} is installed at ${finding.copies.length} paths (${copies}) and is declared single-copy - nifra resolves every duplicate to this app's copy`,
|
|
791
|
-
fix: finding.remediation,
|
|
792
|
-
})
|
|
793
|
-
}
|
|
794
|
-
// Advisory (never fails the gate): while actively editing a linked package its dist is always
|
|
795
|
-
// momentarily behind. The finding earns its keep when a dev server is about to start against it -
|
|
796
|
-
// Bun reads live `src` while Vite's SSR runner reads the artifact, so a stale one 500s inside
|
|
797
|
-
// framework/shared-package code and reads exactly like an upstream regression.
|
|
798
|
-
for (const f of dr.staleDists) {
|
|
799
|
-
// The rebuild is only actionable with the LOCATION: a workspace link routinely points into a
|
|
800
|
-
// sibling repo, so "rebuild @nifrajs/core" left the developer to go find the checkout and guess
|
|
801
|
-
// the script. Name the directory and the package's own script, or say plainly that it declares
|
|
802
|
-
// none - never suggest a command that would fail.
|
|
803
|
-
const rebuild =
|
|
804
|
-
f.buildScript === undefined
|
|
805
|
-
? `${f.packageDir} declares no build script - build it the way that package expects`
|
|
806
|
-
: `cd ${f.packageDir} && bun run ${f.buildScript} (or \`nifra fix --code NF-C010\`)`
|
|
807
|
-
diagnostics.push({
|
|
808
|
-
rule: "stale-workspace-dist",
|
|
809
|
-
severity: "warning",
|
|
810
|
-
message: f.missing
|
|
811
|
-
? `${f.package} was never built - ${f.distFile} is missing, but its export map serves it to Vite SSR/node consumers while Bun reads src - ${rebuild}`
|
|
812
|
-
: `${f.package} has a stale build artifact - ${f.distFile} is ${f.behindSeconds}s older than ${f.sourceFile}, and Vite SSR/node consumers read the artifact while Bun reads src - ${rebuild}`,
|
|
813
|
-
fix: rebuild,
|
|
814
|
-
evidence: [f.package, f.distFile, f.sourceFile, f.packageDir],
|
|
815
|
-
suggestion: {
|
|
816
|
-
kind: "manual",
|
|
817
|
-
title: `Rebuild ${f.package}`,
|
|
818
|
-
steps: [
|
|
819
|
-
rebuild,
|
|
820
|
-
"Only workspace-linked installs drift; npm tarballs are immutable and never flagged.",
|
|
821
|
-
],
|
|
822
|
-
},
|
|
823
|
-
})
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
// The two-pipeline rule, read from the config as TEXT (doctor collects it - see ./pipeline-report.ts).
|
|
827
|
-
// `loadApp` already refuses a misplaced plugin, but only when something loads the app; a check that
|
|
828
|
-
// never executes project code, and CI that never starts a dev server, would otherwise meet these for
|
|
829
|
-
// the first time as a production server that built cleanly and died at startup.
|
|
830
|
-
for (const f of dr.pipeline?.findings ?? []) {
|
|
831
|
-
diagnostics.push({
|
|
832
|
-
rule: "pipeline",
|
|
833
|
-
severity: f.severity,
|
|
834
|
-
file: f.file,
|
|
835
|
-
...(f.line !== undefined ? { line: f.line } : {}),
|
|
836
|
-
message: f.message,
|
|
837
|
-
fix: f.fix,
|
|
838
|
-
suggestion: { kind: "manual", title: f.fix, steps: [PIPELINE_DOC_HINT] },
|
|
839
|
-
})
|
|
840
|
-
}
|
|
841
|
-
// #7: a committed server-manifest.ts that drifted from routes/ - name the exact missing/extra routes.
|
|
842
|
-
for (const f of manifestDrift) {
|
|
843
|
-
const parts: string[] = []
|
|
844
|
-
if (f.missing.length > 0) parts.push(`missing from manifest: ${f.missing.join(", ")}`)
|
|
845
|
-
if (f.extra.length > 0) parts.push(`stale in manifest: ${f.extra.join(", ")}`)
|
|
846
|
-
diagnostics.push({
|
|
847
|
-
rule: "server-manifest-drift",
|
|
848
|
-
severity: "error",
|
|
849
|
-
file: f.file,
|
|
850
|
-
message: `${f.file} drifted from routes/ (${parts.join("; ")}) - ${MANIFEST_DRIFT_HINT}`,
|
|
851
|
-
fix: MANIFEST_DRIFT_HINT,
|
|
852
|
-
evidence: [...f.missing, ...f.extra],
|
|
853
|
-
suggestion: {
|
|
854
|
-
kind: "manual",
|
|
855
|
-
title: "Regenerate the committed server manifest",
|
|
856
|
-
steps: [
|
|
857
|
-
"Re-run your build (`nifra build --target <t>` or your build script) - it regenerates server-manifest.ts from the current routes/.",
|
|
858
|
-
"Commit the updated server-manifest.ts.",
|
|
859
|
-
],
|
|
860
|
-
},
|
|
861
|
-
})
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// G+B+D+F: when the project opts into capability assurance, `nifra check` becomes the static
|
|
865
|
-
// provenance firewall as well as the typed-contract gate. Loading is explicit/config-owned; projects
|
|
866
|
-
// without nifra.assurance.ts retain the historical scan and hot path unchanged.
|
|
867
|
-
const provided = opts.assurance
|
|
868
|
-
let applicationRulePacks: readonly import("./rules/index.ts").RulePack[] = []
|
|
869
|
-
let loadedPolicy = provided
|
|
870
|
-
let loadedCapability: CapabilityProjectReport | undefined = provided?.capability
|
|
375
|
+
// The rule registry is the only diagnostic producer. This preparation phase may load executable
|
|
376
|
+
// assurance configuration because rules need its reflected facts, but it never formats findings.
|
|
377
|
+
let factsSeed = scan.facts
|
|
378
|
+
let loadedPolicy = opts.assurance ?? factsSeed.policies.assurance
|
|
379
|
+
let loadedCapability = loadedPolicy?.capability ?? factsSeed.policies.capability
|
|
380
|
+
let applicationRulePacks = factsSeed.policies.rulePacks
|
|
871
381
|
const assuranceConfigPath = join(cwd, "nifra.assurance.ts")
|
|
872
|
-
if (provided !== undefined ? provided.present : existsSync(assuranceConfigPath)) {
|
|
873
|
-
try {
|
|
874
|
-
// Either the shared reflection from `collectProjectVerification` (no second pass) or, on the
|
|
875
|
-
// standalone path, loaded + reflected here. Both branches yield the same config + evidence.
|
|
876
|
-
let config: AssuranceConfig
|
|
877
|
-
let project: CapabilityProjectReport | undefined
|
|
878
|
-
let routeAssurance: AssuranceReport | undefined
|
|
879
|
-
let evidence: ProjectEvidenceSnapshot | undefined
|
|
880
|
-
if (provided !== undefined) {
|
|
881
|
-
// A load/evaluate failure travels as `provided.error`; re-throwing lands it in the same
|
|
882
|
-
// capability-config diagnostic the standalone catch produces.
|
|
883
|
-
if (provided.error !== undefined) throw provided.error
|
|
884
|
-
config = provided.config as AssuranceConfig
|
|
885
|
-
project = provided.capability
|
|
886
|
-
routeAssurance = provided.routeAssurance
|
|
887
|
-
evidence = provided.evidence
|
|
888
|
-
} else {
|
|
889
|
-
const { loadAssuranceConfig } = await import("./assure.ts")
|
|
890
|
-
config = await loadAssuranceConfig(cwd)
|
|
891
|
-
if (config.capabilities !== undefined) {
|
|
892
|
-
const { collectCapabilityProjectReport } = await import("./capabilities-tool.ts")
|
|
893
|
-
project = await collectCapabilityProjectReport(cwd, config.source, config.capabilities)
|
|
894
|
-
}
|
|
895
|
-
loadedPolicy = {
|
|
896
|
-
present: true,
|
|
897
|
-
config,
|
|
898
|
-
...(routeAssurance === undefined ? {} : { routeAssurance }),
|
|
899
|
-
...(project === undefined ? {} : { capability: project }),
|
|
900
|
-
...(evidence === undefined ? {} : { evidence }),
|
|
901
|
-
}
|
|
902
|
-
loadedCapability = project
|
|
903
|
-
}
|
|
904
|
-
applicationRulePacks = parseRulePacks(config.rulePacks)
|
|
905
|
-
const capabilityReport = project?.report
|
|
906
|
-
if (config.capabilities !== undefined && project !== undefined) {
|
|
907
|
-
for (const finding of project.report.findings) {
|
|
908
|
-
const violation =
|
|
909
|
-
finding.code === "forbidden-effect-import"
|
|
910
|
-
? project.violations.find(
|
|
911
|
-
(candidate) =>
|
|
912
|
-
candidate.method === finding.method && candidate.path === finding.path,
|
|
913
|
-
)
|
|
914
|
-
: undefined
|
|
915
|
-
const truncation =
|
|
916
|
-
finding.code === "provenance-truncated"
|
|
917
|
-
? project.truncations.find(
|
|
918
|
-
(candidate) =>
|
|
919
|
-
candidate.method === finding.method && candidate.path === finding.path,
|
|
920
|
-
)
|
|
921
|
-
: undefined
|
|
922
|
-
// An unmatched seam is a policy defect, not a route defect: the fix is in the policy file,
|
|
923
|
-
// so it gets its own steps instead of the route-side provenance guidance.
|
|
924
|
-
const seamFix =
|
|
925
|
-
finding.code === "unmatched-provenance-seam"
|
|
926
|
-
? "Write the seam exactly as the code imports it, or delete the rule."
|
|
927
|
-
: undefined
|
|
928
|
-
diagnostics.push({
|
|
929
|
-
rule: "capability-assurance",
|
|
930
|
-
severity: "error",
|
|
931
|
-
...(violation !== undefined
|
|
932
|
-
? { file: violation.module, chain: violation.chain }
|
|
933
|
-
: truncation !== undefined
|
|
934
|
-
? { chain: truncation.chain }
|
|
935
|
-
: {}),
|
|
936
|
-
message: `${finding.message}${seamFix === undefined ? ` - ${CAPABILITY_HINT}` : ""}`,
|
|
937
|
-
fix: seamFix ?? CAPABILITY_HINT,
|
|
938
|
-
suggestion:
|
|
939
|
-
seamFix === undefined
|
|
940
|
-
? {
|
|
941
|
-
kind: "manual",
|
|
942
|
-
title: "Restore declared effect provenance",
|
|
943
|
-
steps: [
|
|
944
|
-
"Route effectful work through an import listed in capabilities.provenance.imports.",
|
|
945
|
-
"Declare the exact capability token on the route; do not widen unrelated routes in the same file.",
|
|
946
|
-
"For domain writes, add the adapter the capability definition requires: `schema.idempotency` for the `request` tier, `.use(durableCommand({ journal }))` from @nifrajs/middleware for the `durable` tier.",
|
|
947
|
-
"Run `nifra capabilities snapshot` only after assurance passes, then review the lockfile diff.",
|
|
948
|
-
],
|
|
949
|
-
}
|
|
950
|
-
: {
|
|
951
|
-
kind: "manual",
|
|
952
|
-
title: "Point the provenance rule at a module that exists",
|
|
953
|
-
steps: [
|
|
954
|
-
"Copy the specifier from the import statement itself - it is matched as written, with no extension or index resolution.",
|
|
955
|
-
"Use a trailing `/*` when the seam is a directory of modules (`@myorg/db/*`).",
|
|
956
|
-
"For a routeModules entry, give the project-relative path of the file that implements the route.",
|
|
957
|
-
"Delete the rule if the seam it governed is gone; leaving it in place proves nothing.",
|
|
958
|
-
],
|
|
959
|
-
},
|
|
960
|
-
})
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (config.manifest !== undefined) {
|
|
964
|
-
const { buildNifraManifest, parseNifraManifest, serializeNifraManifest } = await import(
|
|
965
|
-
"@nifrajs/core/manifest"
|
|
966
|
-
)
|
|
967
|
-
const assurance =
|
|
968
|
-
routeAssurance ??
|
|
969
|
-
(await import("@nifrajs/core/assurance")).evaluateRouteAssurance(
|
|
970
|
-
config.source,
|
|
971
|
-
config.policy,
|
|
972
|
-
{
|
|
973
|
-
...(config.capabilities !== undefined
|
|
974
|
-
? { definitions: config.capabilities.definitions }
|
|
975
|
-
: {}),
|
|
976
|
-
},
|
|
977
|
-
)
|
|
978
|
-
evidence ??= snapshotProjectEvidence(config.source, {
|
|
979
|
-
assurance,
|
|
980
|
-
...(capabilityReport !== undefined ? { capabilities: capabilityReport } : {}),
|
|
981
|
-
})
|
|
982
|
-
const path = resolve(cwd, config.manifest.path ?? "nifra.manifest.json")
|
|
983
|
-
let message: string | undefined
|
|
984
|
-
if (!assurance.ok || (capabilityReport !== undefined && !capabilityReport.ok)) {
|
|
985
|
-
message =
|
|
986
|
-
"the configured assurance policy is failing, so a trusted manifest cannot be built"
|
|
987
|
-
} else if (!existsSync(path)) {
|
|
988
|
-
message = "the configured trust manifest is missing"
|
|
989
|
-
} else {
|
|
990
|
-
try {
|
|
991
|
-
const current = await buildNifraManifest({
|
|
992
|
-
evidence,
|
|
993
|
-
assurance,
|
|
994
|
-
...(capabilityReport !== undefined ? { capabilities: capabilityReport } : {}),
|
|
995
|
-
})
|
|
996
|
-
const storedText = await Bun.file(path).text()
|
|
997
|
-
const stored = await parseNifraManifest(storedText, path)
|
|
998
|
-
const expectedText = `${serializeNifraManifest(current)}\n`
|
|
999
|
-
if (storedText !== expectedText || stored.contentHash !== current.contentHash) {
|
|
1000
|
-
message = "the configured trust manifest does not match live route reflection"
|
|
1001
|
-
}
|
|
1002
|
-
} catch (error) {
|
|
1003
|
-
message = `the configured trust manifest is invalid: ${error instanceof Error ? error.message : String(error)}`
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
if (message !== undefined) {
|
|
1007
|
-
diagnostics.push({
|
|
1008
|
-
rule: "manifest-drift",
|
|
1009
|
-
severity: "error",
|
|
1010
|
-
file: path,
|
|
1011
|
-
message: `${message} - ${TRUST_MANIFEST_DRIFT_HINT}`,
|
|
1012
|
-
fix: TRUST_MANIFEST_DRIFT_HINT,
|
|
1013
|
-
suggestion: {
|
|
1014
|
-
kind: "command",
|
|
1015
|
-
title: "Regenerate the signed-manifest input artifact",
|
|
1016
|
-
command: ["nifra", "manifest", "emit"],
|
|
1017
|
-
steps: [
|
|
1018
|
-
"Review the route, assurance, capability, and classification delta before committing it.",
|
|
1019
|
-
],
|
|
1020
|
-
},
|
|
1021
|
-
})
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
loadedPolicy = { present: true, error: err }
|
|
1026
|
-
diagnostics.push({
|
|
1027
|
-
rule: "capability-config",
|
|
1028
|
-
severity: "error",
|
|
1029
|
-
file: "nifra.assurance.ts",
|
|
1030
|
-
message: `capability assurance config could not be evaluated: ${err instanceof Error ? err.message : String(err)}`,
|
|
1031
|
-
suggestion: {
|
|
1032
|
-
kind: "manual",
|
|
1033
|
-
title: "Repair the assurance config",
|
|
1034
|
-
steps: [
|
|
1035
|
-
"Ensure nifra.assurance.ts default-exports defineAssuranceConfig({ source, policy, capabilities }).",
|
|
1036
|
-
"Fix configuration/import errors; the provenance firewall fails closed when its policy cannot load.",
|
|
1037
|
-
],
|
|
1038
|
-
},
|
|
1039
|
-
})
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
382
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
diagnostic({
|
|
1052
|
-
code: "NF-K001",
|
|
1053
|
-
severity: "info",
|
|
1054
|
-
message: "no contract lock; run `nifra contracts snapshot` to enable drift detection",
|
|
1055
|
-
verify: "nifra contracts snapshot",
|
|
1056
|
-
}),
|
|
1057
|
-
)
|
|
1058
|
-
} else {
|
|
1059
|
-
if (contract.vacuous) {
|
|
1060
|
-
// Every route hashes to the empty-schema digest, so the lock exists but the drift rule guards
|
|
1061
|
-
// nothing: it can only ever compare "no schema" to "no schema". Say so, or a green contract check
|
|
1062
|
-
// reads as proof of a stable contract when there is no contract to be stable.
|
|
1063
|
-
structuredExtras.push(
|
|
1064
|
-
diagnostic({
|
|
1065
|
-
code: "NF-K001",
|
|
1066
|
-
severity: "info",
|
|
1067
|
-
message:
|
|
1068
|
-
"no route schemas declared - the trust manifest is vacuous: every contract hash is the empty-schema digest, so drift detection guards nothing. Declare route schemas (body/query/params/response) so the lock has a contract to protect.",
|
|
1069
|
-
verify: "nifra check --lints-only",
|
|
1070
|
-
}),
|
|
1071
|
-
)
|
|
383
|
+
if (loadedPolicy === undefined && existsSync(assuranceConfigPath)) {
|
|
384
|
+
try {
|
|
385
|
+
const { loadAssuranceConfig } = await import("./assure.ts")
|
|
386
|
+
const config = await loadAssuranceConfig(cwd)
|
|
387
|
+
let capability: import("./capabilities-tool.ts").CapabilityProjectReport | undefined
|
|
388
|
+
if (config.capabilities !== undefined) {
|
|
389
|
+
const { collectCapabilityProjectReport } = await import("./capabilities-tool.ts")
|
|
390
|
+
capability = await collectCapabilityProjectReport(cwd, config.source, config.capabilities)
|
|
1072
391
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
code: "NF-K001",
|
|
1078
|
-
...(finding.route !== undefined ? { evidence: [finding.route] } : {}),
|
|
1079
|
-
message: finding.message,
|
|
1080
|
-
fix: "run `nifra contracts snapshot` after reviewing the contract change",
|
|
1081
|
-
verify: "nifra check --lints-only",
|
|
1082
|
-
})
|
|
392
|
+
loadedPolicy = {
|
|
393
|
+
present: true,
|
|
394
|
+
config,
|
|
395
|
+
...(capability === undefined ? {} : { capability }),
|
|
1083
396
|
}
|
|
397
|
+
loadedCapability = capability
|
|
398
|
+
applicationRulePacks = parseRulePacks(config.rulePacks)
|
|
399
|
+
} catch (error) {
|
|
400
|
+
loadedPolicy = { present: true, error }
|
|
401
|
+
loadedCapability = undefined
|
|
402
|
+
applicationRulePacks = []
|
|
1084
403
|
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
verify: "nifra contracts snapshot",
|
|
1095
|
-
})
|
|
404
|
+
} else if (loadedPolicy?.present === true && loadedPolicy.error === undefined) {
|
|
405
|
+
try {
|
|
406
|
+
if (loadedPolicy.config === undefined) throw new Error("assurance configuration unavailable")
|
|
407
|
+
applicationRulePacks = parseRulePacks(loadedPolicy.config.rulePacks)
|
|
408
|
+
loadedCapability = loadedPolicy.capability ?? loadedCapability
|
|
409
|
+
} catch (error) {
|
|
410
|
+
loadedPolicy = { present: true, error }
|
|
411
|
+
loadedCapability = undefined
|
|
412
|
+
applicationRulePacks = []
|
|
1096
413
|
}
|
|
1097
414
|
}
|
|
1098
415
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
policies
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
rulePacks: applicationRulePacks,
|
|
1107
|
-
},
|
|
416
|
+
factsSeed = {
|
|
417
|
+
...factsSeed,
|
|
418
|
+
policies: {
|
|
419
|
+
...factsSeed.policies,
|
|
420
|
+
...(loadedPolicy === undefined ? {} : { assurance: loadedPolicy }),
|
|
421
|
+
...(loadedCapability === undefined ? {} : { capability: loadedCapability }),
|
|
422
|
+
rulePacks: applicationRulePacks,
|
|
1108
423
|
},
|
|
1109
|
-
|
|
1110
|
-
)
|
|
1111
|
-
const ruleContext
|
|
424
|
+
}
|
|
425
|
+
const projectFacts = freezeProjectFacts(factsSeed)
|
|
426
|
+
const ruleContext = {
|
|
1112
427
|
root: cwd,
|
|
1113
428
|
sources: projectFacts.source,
|
|
1114
429
|
project: projectFacts,
|
|
1115
|
-
|
|
1116
|
-
|
|
430
|
+
...(projectFacts.check.typescriptSession === undefined
|
|
431
|
+
? {}
|
|
432
|
+
: { typescriptSession: projectFacts.check.typescriptSession }),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const builtInRules = [
|
|
436
|
+
...legacyRules,
|
|
437
|
+
...securityRules,
|
|
438
|
+
...routeRules,
|
|
439
|
+
...pageRules,
|
|
440
|
+
...zoneRules,
|
|
441
|
+
...dataGuardRules,
|
|
442
|
+
...secretRules,
|
|
443
|
+
...islandRules,
|
|
444
|
+
...nanoRules,
|
|
445
|
+
]
|
|
1117
446
|
let registryDiagnostics: Diagnostic[] = []
|
|
447
|
+
let builtInsSucceeded = false
|
|
1118
448
|
try {
|
|
1119
|
-
registryDiagnostics = await runRuleRegistry(
|
|
1120
|
-
|
|
1121
|
-
[...legacyRules, ...securityRules, ...routeRules, ...islandRules, ...nanoRules],
|
|
1122
|
-
projectFacts.policies.rulePacks,
|
|
1123
|
-
)
|
|
1124
|
-
structuredDiagnostics.push(...registryDiagnostics)
|
|
449
|
+
registryDiagnostics = await runRuleRegistry(ruleContext, builtInRules)
|
|
450
|
+
builtInsSucceeded = true
|
|
1125
451
|
} catch (error) {
|
|
1126
452
|
registryDiagnostics = [
|
|
1127
453
|
diagnostic({
|
|
1128
454
|
code: "NF-C017",
|
|
1129
455
|
severity: "error",
|
|
1130
|
-
message:
|
|
456
|
+
message:
|
|
457
|
+
"rule registry failed closed: " +
|
|
458
|
+
(error instanceof Error ? error.message : String(error)),
|
|
1131
459
|
fix: { recipe: "rule-pack.repair", command: "nifra check --lints-only" },
|
|
1132
460
|
verify: "nifra check --lints-only",
|
|
1133
461
|
}),
|
|
1134
462
|
]
|
|
1135
|
-
structuredDiagnostics.push(...registryDiagnostics)
|
|
1136
463
|
}
|
|
464
|
+
if (builtInsSucceeded && projectFacts.policies.rulePacks.length > 0) {
|
|
465
|
+
try {
|
|
466
|
+
registryDiagnostics.push(
|
|
467
|
+
...(await runRuleRegistry(ruleContext, [], projectFacts.policies.rulePacks)),
|
|
468
|
+
)
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Keep all built-in findings when an application rule fails. The old compatibility path had
|
|
471
|
+
// already materialized those findings before packs ran; preserving that property matters when
|
|
472
|
+
// a broken pack must not hide a more actionable built-in failure.
|
|
473
|
+
registryDiagnostics.push(
|
|
474
|
+
diagnostic({
|
|
475
|
+
code: "NF-C017",
|
|
476
|
+
severity: "error",
|
|
477
|
+
message:
|
|
478
|
+
"rule registry failed closed: " +
|
|
479
|
+
(error instanceof Error ? error.message : String(error)),
|
|
480
|
+
fix: { recipe: "rule-pack.repair", command: "nifra check --lints-only" },
|
|
481
|
+
verify: "nifra check --lints-only",
|
|
482
|
+
}),
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const knownLegacyCodes = new Set(Object.values(LEGACY_RULE_CODES))
|
|
488
|
+
const compatibilityRows: CheckDiagnostic[] = []
|
|
1137
489
|
for (const item of registryDiagnostics) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
490
|
+
const compatibility = diagnosticCompatibilityOf(item)
|
|
491
|
+
if (compatibility !== undefined) {
|
|
492
|
+
compatibilityRows.push({
|
|
493
|
+
rule: compatibility.rule,
|
|
494
|
+
severity: item.severity === "warn" ? "warning" : item.severity,
|
|
495
|
+
...(compatibility.includeCode === true ? { code: item.code } : {}),
|
|
496
|
+
...(item.file === undefined ? {} : { file: item.file }),
|
|
497
|
+
...(item.line === undefined ? {} : { line: item.line }),
|
|
498
|
+
message: item.message,
|
|
499
|
+
...(compatibility.fix === undefined ? {} : { fix: compatibility.fix }),
|
|
500
|
+
// A legacy chain was a separate field; canonical diagnostics expose the same values as
|
|
501
|
+
// evidence for structured consumers, so do not leak that projection back into the old view.
|
|
502
|
+
...(item.evidence === undefined || compatibility.chain !== undefined
|
|
503
|
+
? {}
|
|
504
|
+
: { evidence: item.evidence }),
|
|
505
|
+
...(compatibility.chain === undefined ? {} : { chain: compatibility.chain }),
|
|
506
|
+
...(compatibility.suggestion === undefined ? {} : { suggestion: compatibility.suggestion }),
|
|
507
|
+
...(item.verify === undefined ? {} : { verify: item.verify }),
|
|
508
|
+
})
|
|
509
|
+
continue
|
|
510
|
+
}
|
|
511
|
+
// A structured informational contract note intentionally has no old human/legacy row. Every
|
|
512
|
+
// other non-legacy rule (security, route, island, nano, or an application pack) retains the
|
|
513
|
+
// existing fallback row keyed by its stable code.
|
|
514
|
+
if (knownLegacyCodes.has(item.code)) continue
|
|
515
|
+
compatibilityRows.push({
|
|
1143
516
|
rule: item.code,
|
|
1144
517
|
severity: item.severity === "error" ? "error" : "warning",
|
|
1145
518
|
code: item.code,
|
|
1146
|
-
...(item.file
|
|
1147
|
-
...(item.line
|
|
519
|
+
...(item.file === undefined ? {} : { file: item.file }),
|
|
520
|
+
...(item.line === undefined ? {} : { line: item.line }),
|
|
1148
521
|
message: item.message,
|
|
1149
|
-
...(item.evidence
|
|
1150
|
-
...(item.verify
|
|
522
|
+
...(item.evidence === undefined ? {} : { evidence: item.evidence }),
|
|
523
|
+
...(item.verify === undefined ? {} : { verify: item.verify }),
|
|
1151
524
|
})
|
|
1152
525
|
}
|
|
1153
526
|
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
527
|
+
// Keep the old compatibility order stable even though the canonical registry order is explicit.
|
|
528
|
+
const order = new Map(LEGACY_RULE_ORDER.map((rule, index) => [rule, index]))
|
|
529
|
+
const orderedLegacy = compatibilityRows
|
|
530
|
+
.map((value, index) => ({ value, index }))
|
|
531
|
+
.sort(
|
|
532
|
+
(a, b) =>
|
|
533
|
+
(order.get(a.value.rule) ?? LEGACY_RULE_ORDER.length) -
|
|
534
|
+
(order.get(b.value.rule) ?? LEGACY_RULE_ORDER.length) || a.index - b.index,
|
|
535
|
+
)
|
|
536
|
+
.map((entry) => entry.value)
|
|
537
|
+
|
|
538
|
+
const checkConfig = projectFacts.policies.checkConfig
|
|
1157
539
|
const overrideFor = (...keys: (string | undefined)[]): RuleOverride | undefined => {
|
|
1158
540
|
for (const key of keys) {
|
|
1159
541
|
if (key !== undefined && checkConfig.rules[key] !== undefined) return checkConfig.rules[key]
|
|
@@ -1168,51 +550,71 @@ export async function collectCheckDiagnostics(
|
|
|
1168
550
|
const codeToLegacy = new Map(
|
|
1169
551
|
Object.entries(LEGACY_RULE_CODES).map(([name, code]) => [code, name]),
|
|
1170
552
|
)
|
|
1171
|
-
const finalDiagnostics =
|
|
1172
|
-
const override = overrideFor(
|
|
1173
|
-
if (override === undefined) return [
|
|
1174
|
-
if (dropped(override,
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
return [{ ...d, severity: override.severity === "warn" ? "warning" : override.severity }]
|
|
553
|
+
const finalDiagnostics = orderedLegacy.flatMap<CheckDiagnostic>((value) => {
|
|
554
|
+
const override = overrideFor(value.rule, value.code, LEGACY_RULE_CODES[value.rule])
|
|
555
|
+
if (override === undefined) return [value]
|
|
556
|
+
if (dropped(override, value.file)) return []
|
|
557
|
+
if (override.severity === undefined || override.severity === "off") return [value]
|
|
558
|
+
return [{ ...value, severity: override.severity === "warn" ? "warning" : override.severity }]
|
|
1178
559
|
})
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
560
|
+
|
|
561
|
+
const structuredExtras = registryDiagnostics.filter(
|
|
562
|
+
(value) => diagnosticCompatibilityOf(value) === undefined && knownLegacyCodes.has(value.code),
|
|
563
|
+
)
|
|
564
|
+
const structuredDiagnostics = [
|
|
565
|
+
...structuredExtras,
|
|
566
|
+
...registryDiagnostics.filter((value) => !structuredExtras.includes(value)),
|
|
567
|
+
]
|
|
568
|
+
const finalStructured = structuredDiagnostics.flatMap<Diagnostic>((value) => {
|
|
569
|
+
const override = overrideFor(value.code, codeToLegacy.get(value.code))
|
|
570
|
+
if (override === undefined) return [value]
|
|
571
|
+
if (dropped(override, value.file)) return []
|
|
572
|
+
if (override.severity === undefined || override.severity === "off") return [value]
|
|
573
|
+
return [Object.freeze({ ...value, severity: override.severity })]
|
|
1185
574
|
})
|
|
1186
575
|
|
|
1187
|
-
// Cap the diagnostics when asked (the MCP path), so a project with thousands of findings can't return a
|
|
1188
|
-
// message that breaks the stdio transport. `ok` reflects the FULL set - truncation never flips it.
|
|
1189
576
|
const total = finalDiagnostics.length
|
|
1190
577
|
const max = opts.maxDiagnostics
|
|
1191
578
|
const shown = max !== undefined && total > max ? finalDiagnostics.slice(0, max) : finalDiagnostics
|
|
579
|
+
const nonLegacyStructuredCount = finalStructured.filter(
|
|
580
|
+
(value) => knownLegacyCodes.has(value.code) && diagnosticCompatibilityOf(value) === undefined,
|
|
581
|
+
).length
|
|
582
|
+
const structuredLimit =
|
|
583
|
+
shown.length < total ? shown.length + nonLegacyStructuredCount : finalStructured.length
|
|
584
|
+
|
|
585
|
+
const doctor = projectFacts.packages.doctor
|
|
586
|
+
const knownGaps = projectFacts.policies.capability?.report.gaps ?? []
|
|
1192
587
|
const result: CheckResult = {
|
|
1193
|
-
ok: !finalDiagnostics.some((
|
|
1194
|
-
typecheck:
|
|
1195
|
-
|
|
588
|
+
ok: !finalDiagnostics.some((value) => value.severity === "error"),
|
|
589
|
+
typecheck: projectFacts.check.typecheck.ran
|
|
590
|
+
? projectFacts.check.typecheck.ok
|
|
591
|
+
? "pass"
|
|
592
|
+
: "fail"
|
|
593
|
+
: "skipped",
|
|
594
|
+
...(!projectFacts.check.typecheck.ran && projectFacts.check.typecheck.note !== undefined
|
|
595
|
+
? { typecheckNote: projectFacts.check.typecheck.note }
|
|
596
|
+
: {}),
|
|
1196
597
|
diagnostics: shown,
|
|
1197
|
-
...(
|
|
1198
|
-
...(
|
|
598
|
+
...(projectFacts.pipeline !== undefined ? { pipeline: projectFacts.pipeline } : {}),
|
|
599
|
+
...(doctor.ran
|
|
1199
600
|
? {
|
|
1200
601
|
identityPreflight: {
|
|
1201
|
-
...(
|
|
1202
|
-
...(
|
|
1203
|
-
duplicates:
|
|
1204
|
-
deduplicated:
|
|
602
|
+
...(doctor.identityBasis === undefined ? {} : { basis: doctor.identityBasis }),
|
|
603
|
+
...(doctor.identityScanTruncated === true ? { truncated: true } : {}),
|
|
604
|
+
duplicates: doctor.duplicateInstalls,
|
|
605
|
+
deduplicated: doctor.deduplicatedInstalls ?? [],
|
|
1205
606
|
},
|
|
1206
607
|
}
|
|
1207
608
|
: {}),
|
|
1208
|
-
...(checkConfig.externalMounts.length
|
|
1209
|
-
? {
|
|
1210
|
-
: {}),
|
|
1211
|
-
...(
|
|
609
|
+
...(checkConfig.externalMounts.length === 0
|
|
610
|
+
? {}
|
|
611
|
+
: { externalMounts: checkConfig.externalMounts }),
|
|
612
|
+
...(knownGaps.length === 0 ? {} : { knownGaps }),
|
|
613
|
+
...(Object.keys(checkConfig.rules).length === 0 ? {} : { ruleOverrides: checkConfig.rules }),
|
|
1212
614
|
...(shown.length < total ? { truncated: { shown: shown.length, total } } : {}),
|
|
1213
615
|
}
|
|
1214
616
|
Object.defineProperty(result, "structuredDiagnostics", {
|
|
1215
|
-
value: finalStructured.slice(0,
|
|
617
|
+
value: finalStructured.slice(0, structuredLimit),
|
|
1216
618
|
enumerable: false,
|
|
1217
619
|
})
|
|
1218
620
|
return result
|