@nifrajs/cli 2.1.0 → 2.3.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.
Files changed (126) hide show
  1. package/README.md +25 -25
  2. package/dist/assure.d.ts +2 -2
  3. package/dist/assure.d.ts.map +1 -1
  4. package/dist/assure.js +17 -10
  5. package/dist/assure.js.map +1 -1
  6. package/dist/capabilities-tool.d.ts +7 -0
  7. package/dist/capabilities-tool.d.ts.map +1 -1
  8. package/dist/capabilities-tool.js +68 -16
  9. package/dist/capabilities-tool.js.map +1 -1
  10. package/dist/check.d.ts +64 -22
  11. package/dist/check.d.ts.map +1 -1
  12. package/dist/check.js +400 -83
  13. package/dist/check.js.map +1 -1
  14. package/dist/cli.d.ts +37 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +291 -53
  17. package/dist/cli.js.map +1 -1
  18. package/dist/diff-tool.d.ts +3 -3
  19. package/dist/diff-tool.js +7 -7
  20. package/dist/docs-search.d.ts +3 -3
  21. package/dist/docs-search.js +6 -6
  22. package/dist/doctor.d.ts +8 -4
  23. package/dist/doctor.d.ts.map +1 -1
  24. package/dist/doctor.js +74 -14
  25. package/dist/doctor.js.map +1 -1
  26. package/dist/examples.d.ts +5 -5
  27. package/dist/examples.js +5 -5
  28. package/dist/init-agents.d.ts +14 -14
  29. package/dist/init-agents.js +14 -14
  30. package/dist/internal/typescript-import.d.ts +24 -0
  31. package/dist/internal/typescript-import.d.ts.map +1 -0
  32. package/dist/internal/typescript-import.js +32 -0
  33. package/dist/internal/typescript-import.js.map +1 -0
  34. package/dist/introspect.d.ts +12 -12
  35. package/dist/introspect.js +24 -24
  36. package/dist/levels-tool.d.ts +8 -8
  37. package/dist/levels-tool.d.ts.map +1 -1
  38. package/dist/levels-tool.js +33 -30
  39. package/dist/levels-tool.js.map +1 -1
  40. package/dist/load.d.ts +17 -6
  41. package/dist/load.d.ts.map +1 -1
  42. package/dist/load.js +45 -7
  43. package/dist/load.js.map +1 -1
  44. package/dist/manifest-tool.d.ts +1 -1
  45. package/dist/manifest-tool.d.ts.map +1 -1
  46. package/dist/manifest-tool.js +4 -2
  47. package/dist/manifest-tool.js.map +1 -1
  48. package/dist/mcp-docs-tools.d.ts +1 -1
  49. package/dist/mcp-docs-tools.d.ts.map +1 -1
  50. package/dist/mcp-docs-tools.js +14 -10
  51. package/dist/mcp-docs-tools.js.map +1 -1
  52. package/dist/mcp-http.d.ts +5 -5
  53. package/dist/mcp-http.js +7 -7
  54. package/dist/mcp-protocol.d.ts +2 -2
  55. package/dist/mcp-protocol.js +2 -2
  56. package/dist/mcp-render.d.ts +3 -3
  57. package/dist/mcp-render.js +15 -7
  58. package/dist/mcp-render.js.map +1 -1
  59. package/dist/mcp-run.d.ts +2 -2
  60. package/dist/mcp-run.js +2 -2
  61. package/dist/mcp.d.ts +14 -14
  62. package/dist/mcp.d.ts.map +1 -1
  63. package/dist/mcp.js +119 -47
  64. package/dist/mcp.js.map +1 -1
  65. package/dist/openapi-tool.d.ts +1 -1
  66. package/dist/openapi-tool.js +2 -2
  67. package/dist/pipeline-guard.d.ts +83 -0
  68. package/dist/pipeline-guard.d.ts.map +1 -0
  69. package/dist/pipeline-guard.js +136 -0
  70. package/dist/pipeline-guard.js.map +1 -0
  71. package/dist/port.d.ts +9 -9
  72. package/dist/port.js +27 -27
  73. package/dist/recipes/1.8.0.d.ts +2 -2
  74. package/dist/recipes/1.8.0.js +5 -5
  75. package/dist/recipes/index.d.ts +2 -2
  76. package/dist/recipes/index.js +1 -1
  77. package/dist/scaffold.d.ts +3 -3
  78. package/dist/scaffold.js +8 -8
  79. package/dist/sync-routes.d.ts +28 -0
  80. package/dist/sync-routes.d.ts.map +1 -0
  81. package/dist/sync-routes.js +55 -0
  82. package/dist/sync-routes.js.map +1 -0
  83. package/dist/types-search.d.ts +6 -6
  84. package/dist/types-search.d.ts.map +1 -1
  85. package/dist/types-search.js +53 -8
  86. package/dist/types-search.js.map +1 -1
  87. package/dist/upgrade.d.ts +7 -7
  88. package/dist/upgrade.js +12 -12
  89. package/dist/verification.d.ts +51 -0
  90. package/dist/verification.d.ts.map +1 -0
  91. package/dist/verification.js +80 -0
  92. package/dist/verification.js.map +1 -0
  93. package/docs/examples.json +178 -88
  94. package/docs/llms-full.txt +1766 -766
  95. package/docs/types.json +2171 -2807
  96. package/package.json +22 -11
  97. package/src/assure.ts +17 -18
  98. package/src/capabilities-tool.ts +82 -16
  99. package/src/check.ts +493 -100
  100. package/src/cli.ts +306 -54
  101. package/src/diff-tool.ts +8 -8
  102. package/src/docs-search.ts +6 -6
  103. package/src/doctor.ts +81 -16
  104. package/src/examples.ts +7 -7
  105. package/src/init-agents.ts +17 -17
  106. package/src/internal/typescript-import.ts +33 -0
  107. package/src/introspect.ts +28 -28
  108. package/src/levels-tool.ts +37 -31
  109. package/src/load.ts +65 -12
  110. package/src/manifest-tool.ts +4 -2
  111. package/src/mcp-docs-tools.ts +20 -10
  112. package/src/mcp-http.ts +7 -7
  113. package/src/mcp-protocol.ts +2 -2
  114. package/src/mcp-render.ts +15 -7
  115. package/src/mcp-run.ts +2 -2
  116. package/src/mcp.ts +127 -47
  117. package/src/openapi-tool.ts +2 -2
  118. package/src/pipeline-guard.ts +186 -0
  119. package/src/port.ts +31 -31
  120. package/src/recipes/1.8.0.ts +5 -5
  121. package/src/recipes/index.ts +2 -2
  122. package/src/scaffold.ts +8 -8
  123. package/src/sync-routes.ts +69 -0
  124. package/src/types-search.ts +57 -8
  125. package/src/upgrade.ts +12 -12
  126. package/src/verification.ts +123 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "2.1.0",
4
- "description": "The nifra CLI — zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
3
+ "version": "2.3.0",
4
+ "description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -41,14 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@nifrajs/client": "2.1.0",
45
- "@nifrajs/core": "2.1.0",
46
- "@nifrajs/mcp": "2.1.0",
47
- "@nifrajs/runner": "2.1.0",
48
- "@nifrajs/schema": "2.1.0",
49
- "@nifrajs/testing": "2.1.0",
50
- "@nifrajs/web": "2.1.0",
51
- "create-nifra": "2.1.0"
44
+ "@nifrajs/client": "2.3.0",
45
+ "@nifrajs/core": "2.3.0",
46
+ "@nifrajs/mcp": "2.3.0",
47
+ "@nifrajs/runner": "2.3.0",
48
+ "@nifrajs/schema": "2.3.0",
49
+ "@nifrajs/testing": "2.3.0",
50
+ "@nifrajs/web": "2.3.0",
51
+ "create-nifra": "2.3.0"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",
@@ -58,5 +58,16 @@
58
58
  "bugs": {
59
59
  "url": "https://github.com/nifrajs/nifra/issues"
60
60
  },
61
- "homepage": "https://github.com/nifrajs/nifra#readme"
61
+ "homepage": "https://github.com/nifrajs/nifra#readme",
62
+ "peerDependencies": {
63
+ "typescript": "^5.0.0 || ^6.0.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "typescript": {
67
+ "optional": true
68
+ }
69
+ },
70
+ "devDependencies": {
71
+ "typescript": "^6.0.3"
72
+ }
62
73
  }
package/src/assure.ts CHANGED
@@ -1,13 +1,9 @@
1
- /** `nifra assure` — evaluate a project's reflected routes against `nifra.assurance.ts`. */
1
+ /** `nifra assure` - evaluate a project's reflected routes against `nifra.assurance.ts`. */
2
2
 
3
3
  import { existsSync } from "node:fs"
4
4
  import { resolve } from "node:path"
5
5
  import { pathToFileURL } from "node:url"
6
- import {
7
- type AssuranceConfig,
8
- type AssuranceReport,
9
- evaluateRouteAssurance,
10
- } from "@nifrajs/core/assurance"
6
+ import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
11
7
 
12
8
  export const DEFAULT_ASSURANCE_CONFIG = "nifra.assurance.ts"
13
9
 
@@ -28,7 +24,7 @@ export async function loadAssuranceConfig(
28
24
  const path = resolve(cwd, configPath)
29
25
  if (!existsSync(path)) {
30
26
  throw new Error(
31
- `[nifra] route assurance config not found: ${path} — create ${DEFAULT_ASSURANCE_CONFIG} with a default defineAssuranceConfig({ source, policy }) export.`,
27
+ `[nifra] route assurance config not found: ${path} - create ${DEFAULT_ASSURANCE_CONFIG} with a default defineAssuranceConfig({ source, policy }) export.`,
32
28
  )
33
29
  }
34
30
  const specifier = pathToFileURL(path)
@@ -60,19 +56,22 @@ export async function collectAssuranceReport(
60
56
  cwd: string,
61
57
  configPath?: string,
62
58
  ): Promise<AssuranceReport> {
63
- const config = await loadAssuranceConfig(cwd, configPath)
64
- const routeReport = evaluateRouteAssurance(config.source, config.policy)
65
- if (config.capabilities === undefined) return routeReport
66
- const { collectCapabilityProjectReport } = await import("./capabilities-tool.ts")
67
- const capabilityProject = await collectCapabilityProjectReport(
68
- cwd,
69
- config.source,
70
- config.capabilities,
71
- )
59
+ // The route-assurance view over the one project verification. The config load, route reflection, and
60
+ // capability walk it needs are exactly what `collectProjectVerification` already ran.
61
+ const { collectProjectVerification } = await import("./verification.ts")
62
+ const verification = await collectProjectVerification(cwd, {
63
+ ...(configPath !== undefined ? { config: configPath } : {}),
64
+ })
65
+ // A missing/broken config threw here before; re-throw the same error to keep that contract.
66
+ if (verification.configError !== undefined) throw verification.configError
67
+ // A present config means routeAssurance is computed; capability is set exactly when the config
68
+ // declares a capabilities policy.
69
+ const routeReport = verification.routeAssurance as AssuranceReport
70
+ if (verification.capability === undefined) return routeReport
72
71
  return Object.freeze({
73
72
  ...routeReport,
74
- ok: routeReport.ok && capabilityProject.report.ok,
75
- capabilities: capabilityProject.report,
73
+ ok: routeReport.ok && verification.capability.report.ok,
74
+ capabilities: verification.capability.report,
76
75
  })
77
76
  }
78
77
 
@@ -14,7 +14,6 @@ import {
14
14
  validCapabilityId,
15
15
  } from "@nifrajs/core/capabilities"
16
16
  import { reflectRoutes } from "@nifrajs/core/reflection"
17
- import { loadAssuranceConfig } from "./assure.ts"
18
17
  import { scanStaticRouteText, stripComments, walkSource } from "./check.ts"
19
18
 
20
19
  const EFFECT_IMPORT =
@@ -61,6 +60,14 @@ export interface CapabilityImportViolation {
61
60
  export interface CapabilityProjectReport {
62
61
  readonly report: CapabilityAssuranceReport
63
62
  readonly violations: readonly CapabilityImportViolation[]
63
+ readonly truncations: readonly CapabilityProvenanceTruncation[]
64
+ }
65
+
66
+ export interface CapabilityProvenanceTruncation {
67
+ readonly method: string
68
+ readonly path: string
69
+ readonly reason: "depth-limit" | "module-limit"
70
+ readonly chain: readonly string[]
64
71
  }
65
72
 
66
73
  function resolveLocalModule(
@@ -113,8 +120,12 @@ interface WalkResult {
113
120
  readonly covered: boolean
114
121
  readonly evidence: Array<{ id: string; kind: "static"; source: string }>
115
122
  readonly violations: CapabilityImportViolation[]
123
+ readonly truncations: CapabilityProvenanceTruncation[]
116
124
  }
117
125
 
126
+ const MAX_PROVENANCE_MODULES = 500
127
+ const MAX_PROVENANCE_CHAIN = 16
128
+
118
129
  async function walkRouteModules(
119
130
  cwd: string,
120
131
  modules: readonly string[],
@@ -127,11 +138,34 @@ async function walkRouteModules(
127
138
  const visited = new Set<string>()
128
139
  const evidence: Array<{ id: string; kind: "static"; source: string }> = []
129
140
  const violations: CapabilityImportViolation[] = []
141
+ const truncations: CapabilityProvenanceTruncation[] = []
130
142
  let covered = false
131
143
 
132
- while (queue.length > 0 && visited.size < 500) {
144
+ while (queue.length > 0) {
145
+ if (visited.size >= MAX_PROVENANCE_MODULES) {
146
+ const pending = queue[0]
147
+ truncations.push({
148
+ method,
149
+ path,
150
+ reason: "module-limit",
151
+ chain: Object.freeze([
152
+ ...(pending?.chain ?? []),
153
+ `<${MAX_PROVENANCE_MODULES}-module limit>`,
154
+ ]),
155
+ })
156
+ break
157
+ }
133
158
  const current = queue.shift()
134
- if (current === undefined || visited.has(current.module) || current.chain.length > 16) continue
159
+ if (current === undefined || visited.has(current.module)) continue
160
+ if (current.chain.length > MAX_PROVENANCE_CHAIN) {
161
+ truncations.push({
162
+ method,
163
+ path,
164
+ reason: "depth-limit",
165
+ chain: Object.freeze([...current.chain, `<${MAX_PROVENANCE_CHAIN}-hop limit>`]),
166
+ })
167
+ continue
168
+ }
135
169
  visited.add(current.module)
136
170
  let content = sources.get(current.module)
137
171
  if (content === undefined) {
@@ -174,7 +208,7 @@ async function walkRouteModules(
174
208
  }
175
209
  }
176
210
  }
177
- return { covered, evidence, violations }
211
+ return { covered, evidence, violations, truncations }
178
212
  }
179
213
 
180
214
  /** Build coverage-qualified static evidence for every reflected route. */
@@ -196,6 +230,7 @@ export async function collectCapabilityProjectReport(
196
230
 
197
231
  const evidenceRoutes = []
198
232
  const violations: CapabilityImportViolation[] = []
233
+ const truncations: CapabilityProvenanceTruncation[] = []
199
234
  for (const route of reflectRoutes(source)) {
200
235
  const modules = new Set(automatic.get(routeKey(route.method, route.path)) ?? [])
201
236
  for (const association of policy.provenance.routeModules ?? []) {
@@ -225,10 +260,11 @@ export async function collectCapabilityProjectReport(
225
260
  evidence: walked.evidence,
226
261
  })
227
262
  violations.push(...walked.violations)
263
+ truncations.push(...walked.truncations)
228
264
  }
229
265
  const evaluated = evaluateCapabilityAssurance(source, policy, { routes: evidenceRoutes })
230
266
  const report: CapabilityAssuranceReport =
231
- violations.length === 0
267
+ violations.length === 0 && truncations.length === 0
232
268
  ? evaluated
233
269
  : Object.freeze({
234
270
  ...evaluated,
@@ -241,9 +277,19 @@ export async function collectCapabilityProjectReport(
241
277
  path: violation.path,
242
278
  message: `${violation.method} ${violation.path} reaches forbidden ${violation.specifier} via ${violation.chain.join(" → ")}: ${violation.reason}`,
243
279
  })),
280
+ ...truncations.map((truncation) => ({
281
+ code: "provenance-truncated" as const,
282
+ method: truncation.method,
283
+ path: truncation.path,
284
+ message: `${truncation.method} ${truncation.path} capability provenance hit the ${truncation.reason === "depth-limit" ? "import-depth" : "module-count"} safety limit via ${truncation.chain.join(" → ")}; assurance cannot prove the remaining graph`,
285
+ })),
244
286
  ]),
245
287
  })
246
- return Object.freeze({ report, violations: Object.freeze(violations) })
288
+ return Object.freeze({
289
+ report,
290
+ violations: Object.freeze(violations),
291
+ truncations: Object.freeze(truncations),
292
+ })
247
293
  }
248
294
 
249
295
  export function parseCapabilityLockfile(content: string, sourcePath: string): CapabilitySnapshot {
@@ -261,7 +307,7 @@ export function parseCapabilityLockfile(content: string, sourcePath: string): Ca
261
307
  !Array.isArray(candidate.routes)
262
308
  ) {
263
309
  throw new Error(
264
- `[nifra] ${sourcePath} is not a nifra capability lockfile — expected { "nifraCapabilities": 1, "routes": [...] }`,
310
+ `[nifra] ${sourcePath} is not a nifra capability lockfile - expected { "nifraCapabilities": 1, "routes": [...] }`,
265
311
  )
266
312
  }
267
313
  for (const route of candidate.routes) {
@@ -341,18 +387,28 @@ async function currentProject(
341
387
  readonly policy: CapabilityPolicy
342
388
  readonly project: CapabilityProjectReport
343
389
  }> {
344
- const config = await loadAssuranceConfig(cwd, configPath)
345
- if (config.capabilities === undefined) {
390
+ // Source the capability report from the one project verification that `check`/`assure`/`levels` also
391
+ // read, rather than loading the config and walking the module graph a second time here. `snapshot` and
392
+ // `check` are then thin faces over the shared core; the lockfile write/diff is the only work that stays
393
+ // capability-specific.
394
+ const { collectProjectVerification } = await import("./verification.ts")
395
+ const verification = await collectProjectVerification(
396
+ cwd,
397
+ configPath !== undefined ? { config: configPath } : {},
398
+ )
399
+ // A missing/broken config threw here before; re-throw the same error to keep that contract.
400
+ if (verification.configError !== undefined) throw verification.configError
401
+ const policy = verification.config?.capabilities
402
+ if (policy === undefined) {
346
403
  throw new Error("[nifra] assurance config does not define capabilities")
347
404
  }
348
- return {
349
- policy: config.capabilities,
350
- project: await collectCapabilityProjectReport(cwd, config.source, config.capabilities),
351
- }
405
+ // The core computes `capability` exactly when the config declares a capabilities policy, so it is
406
+ // present whenever `policy` is.
407
+ return { policy, project: verification.capability as CapabilityProjectReport }
352
408
  }
353
409
 
354
410
  function unsafeProject(project: CapabilityProjectReport): boolean {
355
- return !project.report.ok || project.violations.length > 0
411
+ return !project.report.ok || project.violations.length > 0 || project.truncations.length > 0
356
412
  }
357
413
 
358
414
  /** Write a lockfile only from a clean, fully-covered project report. */
@@ -387,7 +443,13 @@ export async function runCapabilityCheck(
387
443
  if (options.json === true) {
388
444
  console.log(
389
445
  JSON.stringify(
390
- { ok, report: project.report, violations: project.violations, changes },
446
+ {
447
+ ok,
448
+ report: project.report,
449
+ violations: project.violations,
450
+ truncations: project.truncations,
451
+ changes,
452
+ },
391
453
  null,
392
454
  2,
393
455
  ),
@@ -398,7 +460,11 @@ export async function runCapabilityCheck(
398
460
  for (const finding of project.report.findings) console.log(`✖ ${finding.message}`)
399
461
  for (const violation of project.violations)
400
462
  console.log(
401
- `✖ ${violation.method} ${violation.path}: ${violation.chain.join(" → ")} — ${violation.reason}`,
463
+ `✖ ${violation.method} ${violation.path}: ${violation.chain.join(" → ")} - ${violation.reason}`,
464
+ )
465
+ for (const truncation of project.truncations)
466
+ console.log(
467
+ `✖ ${truncation.method} ${truncation.path}: ${truncation.chain.join(" → ")} - ${truncation.reason}`,
402
468
  )
403
469
  for (const change of changes) console.log(`✖ capability lock drift: ${change}`)
404
470
  }