@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.
- package/README.md +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +64 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +400 -83
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +291 -53
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +8 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +74 -14
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +17 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +45 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +14 -10
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +5 -5
- package/dist/mcp-http.js +7 -7
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +15 -7
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +14 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +119 -47
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +83 -0
- package/dist/pipeline-guard.d.ts.map +1 -0
- package/dist/pipeline-guard.js +136 -0
- package/dist/pipeline-guard.js.map +1 -0
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +6 -6
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +53 -8
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +7 -7
- package/dist/upgrade.js +12 -12
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +178 -88
- package/docs/llms-full.txt +1766 -766
- package/docs/types.json +2171 -2807
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +493 -100
- package/src/cli.ts +306 -54
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +81 -16
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +65 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-docs-tools.ts +20 -10
- package/src/mcp-http.ts +7 -7
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +15 -7
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +127 -47
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +186 -0
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +57 -8
- package/src/upgrade.ts +12 -12
- package/src/verification.ts +123 -0
package/src/introspect.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Introspect a loaded nifra project into a Markdown brief
|
|
3
|
-
* page routes (`discoverRoutes`), and a conventions summary
|
|
2
|
+
* Introspect a loaded nifra project into a Markdown brief - the API routes (`backend.routes()`), the
|
|
3
|
+
* page routes (`discoverRoutes`), and a conventions summary - for piping into an AI coding agent's
|
|
4
4
|
* prompt. Used by `nifra context` (stdout) and the `nifra mcp` server (a tool result), so an agent sees
|
|
5
5
|
* *this* project's actual surface, not just generic docs.
|
|
6
6
|
*/
|
|
@@ -28,7 +28,7 @@ interface JsonSchemaNode {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Render a JSON Schema as a compact TypeScript-like type (`{ id: string, name?: string }`)
|
|
31
|
+
* Render a JSON Schema as a compact TypeScript-like type (`{ id: string, name?: string }`) - the
|
|
32
32
|
* shape agents already think in, at a fraction of the raw JSON Schema's tokens (~70-80% smaller).
|
|
33
33
|
* Anything the renderer doesn't model falls back to the raw JSON for that subtree, so the output
|
|
34
34
|
* is always faithful, just not always minimal.
|
|
@@ -74,7 +74,7 @@ export function tsTypeOf(schema: unknown, depth = 0): string {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/** One route's request/response field shapes, indented under its line. Reads the declared schemas off the
|
|
77
|
-
* descriptor so an agent gets the exact input + output contract
|
|
77
|
+
* descriptor so an agent gets the exact input + output contract - not just "this route is validated". */
|
|
78
78
|
function schemaLines(schema: ReflectedRoute["schema"]): string[] {
|
|
79
79
|
const s = schema
|
|
80
80
|
if (!s) return []
|
|
@@ -95,11 +95,11 @@ function schemaLines(schema: ReflectedRoute["schema"]): string[] {
|
|
|
95
95
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* The typed-client call form for a route
|
|
98
|
+
* The typed-client call form for a route - the exact `client<typeof app>` proxy chain an agent should
|
|
99
99
|
* write, derived from the same convention `@nifrajs/client` implements (so it never has to read the
|
|
100
100
|
* client tests to learn it): a static segment is a property (`.users`), a path param/wildcard is a call
|
|
101
101
|
* that appends the value (`({ id })`), the root path is `.index`, and the HTTP verb is the terminal call.
|
|
102
|
-
* Body verbs (POST/PUT/PATCH) take the body first then call-options; other verbs take call-options first
|
|
102
|
+
* Body verbs (POST/PUT/PATCH) take the body first then call-options; other verbs take call-options first -
|
|
103
103
|
* so the `{ query }` argument lands in the right slot for each.
|
|
104
104
|
*/
|
|
105
105
|
export function clientCall(method: string, path: string, schema: unknown): string {
|
|
@@ -140,10 +140,10 @@ export function apiRoutesSection(routes: readonly ReflectedRoute[]): string {
|
|
|
140
140
|
...schemaLines(r.schema),
|
|
141
141
|
` - call: \`${clientCall(r.method, r.path, r.schema)}\` → \`{ ok, status, data, error }\``,
|
|
142
142
|
])
|
|
143
|
-
return `## API routes (backend.ts)\n\nEach route's \`body\`/\`query\`/\`response\` shape is its contract
|
|
143
|
+
return `## API routes (backend.ts)\n\nEach route's \`body\`/\`query\`/\`response\` shape is its contract - the typed client derives request inputs and \`res.data\` from these, so a screen built on \`client<typeof app>\` stays in sync automatically. The \`call\` line is the exact \`client<typeof app>\` form: static path segments are properties, a path param is a call (\`({ id })\`), the verb is the terminal call (body first for POST/PUT/PATCH), and every call returns the never-throwing \`{ ok, status, data, error }\` Result.\n\n${lines.join("\n")}`
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/** Compact API-routes INDEX for the no-arg `nifra_context` call
|
|
146
|
+
/** Compact API-routes INDEX for the no-arg `nifra_context` call - `METHOD path` per route, WITHOUT the
|
|
147
147
|
* body/query/response shapes or the per-route `call` line. Bounds the first-call payload on a big backend:
|
|
148
148
|
* an agent gets the full route list (so it knows what's mounted) plus a pointer to fetch any route's full
|
|
149
149
|
* contract via the `path`/`kind` slice, instead of every schema up front. */
|
|
@@ -154,7 +154,7 @@ export function apiRoutesIndexSection(routes: readonly ReflectedRoute[]): string
|
|
|
154
154
|
const lines = [...routes]
|
|
155
155
|
.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
|
|
156
156
|
.map((r) => `- \`${r.method} ${r.path}\``)
|
|
157
|
-
return `## API routes (backend.ts)\n\n${routes.length} route${routes.length === 1 ? "" : "s"}. Call \`nifra_context\` again with \`path\` (a route prefix) and/or \`kind: "api"\` for the body/query/response contracts + the exact \`client<typeof app>\` call form
|
|
157
|
+
return `## API routes (backend.ts)\n\n${routes.length} route${routes.length === 1 ? "" : "s"}. Call \`nifra_context\` again with \`path\` (a route prefix) and/or \`kind: "api"\` for the body/query/response contracts + the exact \`client<typeof app>\` call form - or \`nifra_routes\` for the same as structured JSON.\n\n${lines.join("\n")}`
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
/** Markdown section listing the file-routed pages (URL pattern → source file). */
|
|
@@ -172,25 +172,25 @@ const CONVENTIONS = `## Conventions (summary)
|
|
|
172
172
|
|
|
173
173
|
- **Backend:** \`server()\` from \`@nifrajs/core\`; returning a value sends JSON. Validate untrusted input with
|
|
174
174
|
a route schema (\`{ body, query, params }\` using \`t\` from \`@nifrajs/schema\` or any Standard Schema) and read
|
|
175
|
-
the typed \`c.body\` / \`c.query\` / \`c.params\`
|
|
175
|
+
the typed \`c.body\` / \`c.query\` / \`c.params\` - never hand-parse.
|
|
176
176
|
- **Cross-cutting concerns** (rate limit / \`429\`, CORS, auth, CSRF, caching, security headers, body limits)
|
|
177
177
|
→ \`@nifrajs/middleware\`, applied with \`app.use(...)\`. Don't hand-roll them; call \`nifra_docs("middleware")\`
|
|
178
178
|
for the full list + usage.
|
|
179
179
|
- **Need the exact TypeScript of a \`@nifrajs/*\` symbol?** Call \`nifra_types({ name })\` (e.g. \`RateLimitStore\`,
|
|
180
|
-
\`RouteSchema\`)
|
|
180
|
+
\`RouteSchema\`) - the authoritative, complete declaration. Never read \`@nifrajs\` \`.d.ts\` files.
|
|
181
181
|
- **Response contract (no drift):** declare \`{ response: t.object({...}) }\` on a route to lock its output
|
|
182
|
-
shape
|
|
182
|
+
shape - the handler is type-checked against it and the client sees exactly that shape. The contract above
|
|
183
183
|
is the single source of truth for both sides.
|
|
184
|
-
- **Client
|
|
184
|
+
- **Client - ALWAYS use this for API calls:** \`client<typeof app>(url)\` derives request inputs AND
|
|
185
185
|
\`res.data\` from the backend's route types, so the compiler catches any frontend/backend drift. Never
|
|
186
|
-
hand-roll \`fetch\` + ad-hoc response types for an internal API
|
|
186
|
+
hand-roll \`fetch\` + ad-hoc response types for an internal API - that's exactly how screens drift. It
|
|
187
187
|
never throws: branch on \`res.ok ? res.data : res.error\`.
|
|
188
188
|
- **Pages:** file-routed under \`routes/\`; \`loader\`/\`action\` are server-only but the module is also bundled
|
|
189
|
-
for the browser
|
|
189
|
+
for the browser - **never top-level-import server-only code** (DB, secrets, \`process.env\`) into a route
|
|
190
190
|
file; reach it via \`ctx.api\` / \`ctx.env\`.
|
|
191
191
|
- \`app.fetch(Request)\` is the universal entry. Full reference: this repo's \`AGENTS.md\`, or \`llms-full.txt\`.`
|
|
192
192
|
|
|
193
|
-
/** Optional narrowing for {@link describeProject}
|
|
193
|
+
/** Optional narrowing for {@link describeProject} - a path prefix and/or one section. A filtered
|
|
194
194
|
* brief omits the conventions block (the agent already has it from its first full call), so
|
|
195
195
|
* follow-up calls cost a fraction of the tokens. */
|
|
196
196
|
export interface ContextFilter {
|
|
@@ -199,9 +199,9 @@ export interface ContextFilter {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
|
-
* Build the project brief as Markdown. A no-arg call returns a tight **INDEX**
|
|
202
|
+
* Build the project brief as Markdown. A no-arg call returns a tight **INDEX** - the route list (API
|
|
203
203
|
* routes as `METHOD path`, page routes as `pattern → file`) + conventions + a pointer to fetch full
|
|
204
|
-
* contracts via the slice
|
|
204
|
+
* contracts via the slice - so learning a big project is cheap, not a full-surface schema dump. Passing a
|
|
205
205
|
* {@link ContextFilter} (`path` prefix and/or `kind`) returns the narrowed, FULL detail (per-route
|
|
206
206
|
* body/query/response shapes + the typed-client `call` form), and omits the conventions block (the agent
|
|
207
207
|
* already has it from the index call).
|
|
@@ -211,7 +211,7 @@ export function describeProject(app: LoadedApp, filter?: ContextFilter): string
|
|
|
211
211
|
try {
|
|
212
212
|
manifest = discoverRoutes(app.routesDir)
|
|
213
213
|
} catch {
|
|
214
|
-
// No (or unreadable) routes/
|
|
214
|
+
// No (or unreadable) routes/ - the page-routes section reports that.
|
|
215
215
|
}
|
|
216
216
|
const prefix = filter?.path
|
|
217
217
|
let routes = backendRoutes(app.backend)
|
|
@@ -228,10 +228,10 @@ export function describeProject(app: LoadedApp, filter?: ContextFilter): string
|
|
|
228
228
|
const sections: string[] = [`# nifra project: ${app.cwd.slice(app.cwd.lastIndexOf("/") + 1)}`]
|
|
229
229
|
if (isIndex) {
|
|
230
230
|
sections.push(
|
|
231
|
-
"A full-stack nifra app
|
|
232
|
-
// Point agents at the verified surface, not recalled APIs
|
|
231
|
+
"A full-stack nifra app - Web-standard, one app on Bun, Node, Deno, and the edge. This is the route INDEX; fetch a route's full body/query/response contract on demand (see below).",
|
|
232
|
+
// Point agents at the verified surface, not recalled APIs - nifra is young, so training data is the
|
|
233
233
|
// wrong source. The MCP tools (nifra_docs / nifra_example) are checked against the installed version.
|
|
234
|
-
"> **Build against THIS surface + the verified tools
|
|
234
|
+
"> **Build against THIS surface + the verified tools - do not write nifra APIs from memory** (the\n> framework is young; recalled APIs drift). For framework code, call `nifra_example` (snippets\n> typechecked against the installed version) or `nifra_docs`; verify edits with `nifra_run` + `nifra_check`.",
|
|
235
235
|
)
|
|
236
236
|
}
|
|
237
237
|
if (filter?.kind !== "pages") {
|
|
@@ -255,7 +255,7 @@ export interface RouteJson {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/**
|
|
258
|
-
* The backend's API routes as structured JSON
|
|
258
|
+
* The backend's API routes as structured JSON - what the `nifra_routes` MCP tool returns, so an agent
|
|
259
259
|
* consumes the contract programmatically (`list_routes` / `get_route_schema`) instead of parsing the
|
|
260
260
|
* Markdown brief. Optionally filtered to routes whose path starts with `pathPrefix`. Reuses the exact
|
|
261
261
|
* same `call` form and TS-shaped contracts the Markdown brief shows.
|
|
@@ -292,11 +292,11 @@ export function routesToJson(app: LoadedApp, pathPrefix?: string): RouteJson[] {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// ===================================================================================================
|
|
295
|
-
// `nifra routes [--json]`
|
|
295
|
+
// `nifra routes [--json]` - the focused, uniform view of every route the app serves, with methods.
|
|
296
296
|
//
|
|
297
297
|
// `nifra context` lists page routes + API routes for an agent's prompt (Markdown, with schemas).
|
|
298
298
|
// `nifra routes` answers a different question: "what does this app actually serve, and with which
|
|
299
|
-
// methods?"
|
|
299
|
+
// methods?" - so an agent sees instantly that `POST /api/explain` is (or isn't) mounted, instead of
|
|
300
300
|
// discovering it via a 405. It marks which backend routes are AUTO-MOUNTED under the page router's
|
|
301
301
|
// `apiPrefix` (default `/api`), and `--json` makes it machine-consumable. The table-building +
|
|
302
302
|
// rendering are pure (no fs / no module loads), so they're unit-tested; the cwd gathering is the thin
|
|
@@ -313,7 +313,7 @@ export interface RouteTableEntry {
|
|
|
313
313
|
readonly path: string
|
|
314
314
|
/** Methods this route serves, sorted (page: `GET`, plus `POST` when it has an `action`). */
|
|
315
315
|
readonly methods: readonly string[]
|
|
316
|
-
/** Source file (page only
|
|
316
|
+
/** Source file (page only - the route module path relative to `routes/`). */
|
|
317
317
|
readonly file?: string
|
|
318
318
|
/** True when this API route is auto-mounted under the page router's `apiPrefix` (so it's reachable
|
|
319
319
|
* through the same web app, no hand-wired `if (pathname.startsWith("/api/"))`). Pages are never
|
|
@@ -345,7 +345,7 @@ function isUnderPrefix(path: string, prefix: string): boolean {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
* Build the unified, sorted route table from page routes + backend API routes. Pure
|
|
348
|
+
* Build the unified, sorted route table from page routes + backend API routes. Pure - the caller
|
|
349
349
|
* supplies page `hasAction` (resolved via a module load) and the API descriptors (`backend.routes()`).
|
|
350
350
|
* Page routes serve `GET` (+ `POST` when they export an `action`); API routes carry their declared
|
|
351
351
|
* method, and are flagged `autoMounted` when at/under `apiPrefix`. Sorted by path, then kind, then the
|
|
@@ -449,7 +449,7 @@ export async function describeRoutes(
|
|
|
449
449
|
try {
|
|
450
450
|
manifest = discoverRoutes(app.routesDir)
|
|
451
451
|
} catch {
|
|
452
|
-
// No (or unreadable) routes/
|
|
452
|
+
// No (or unreadable) routes/ - fall through with no pages; the table still shows API routes.
|
|
453
453
|
}
|
|
454
454
|
const pages: PageRouteInput[] = []
|
|
455
455
|
for (const route of manifest?.routes ?? []) {
|
package/src/levels-tool.ts
CHANGED
|
@@ -1,36 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra levels`
|
|
2
|
+
* `nifra levels` - the verification ladder, computed from gates that already exist:
|
|
3
3
|
*
|
|
4
|
-
* L0 typed contract
|
|
5
|
-
* L1 route assurance
|
|
6
|
-
* L2 capability lockfile
|
|
7
|
-
* L3 route manifest
|
|
8
|
-
* L4 invariant-tested
|
|
4
|
+
* L0 typed contract - `nifra check` passes (compiler-enforced frontend↔backend contract)
|
|
5
|
+
* L1 route assurance - `nifra.assurance.ts` present and every route satisfies its policy
|
|
6
|
+
* L2 capability lockfile - capability/effect assurance passes and the lockfile is in sync
|
|
7
|
+
* L3 route manifest - the emitted manifest hash-verifies and matches the current app
|
|
8
|
+
* L4 invariant-tested - every route passes through an explicitly isolated executor
|
|
9
9
|
*
|
|
10
|
-
* A level counts only when every level below it holds
|
|
10
|
+
* A level counts only when every level below it holds - the ladder is cumulative, so "L3" is one
|
|
11
11
|
* honest word for a whole posture. Levels are computed, never self-declared.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { existsSync } from "node:fs"
|
|
15
15
|
import { resolve } from "node:path"
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import type { AssuranceReport } from "@nifrajs/core/assurance"
|
|
17
|
+
import { snapshotCapabilities } from "@nifrajs/core/capabilities"
|
|
18
18
|
import { buildNifraManifest, parseNifraManifest } from "@nifrajs/core/manifest"
|
|
19
19
|
import { type ContractTestApp, runAdversarialContract } from "@nifrajs/testing"
|
|
20
|
-
import { loadAssuranceConfig } from "./assure.ts"
|
|
21
20
|
import {
|
|
22
|
-
|
|
21
|
+
type CapabilityProjectReport,
|
|
23
22
|
diffCapabilitySnapshots,
|
|
24
23
|
parseCapabilityLockfile,
|
|
25
24
|
} from "./capabilities-tool.ts"
|
|
26
|
-
import { collectCheckResult } from "./check.ts"
|
|
27
25
|
import { DEFAULT_MANIFEST_FILE } from "./manifest-tool.ts"
|
|
28
26
|
|
|
29
27
|
export interface VerificationLevelStatus {
|
|
30
28
|
readonly level: number
|
|
31
29
|
readonly name: string
|
|
32
30
|
readonly ok: boolean
|
|
33
|
-
/** Why the level does not hold
|
|
31
|
+
/** Why the level does not hold - a gate failure or a missing prerequisite. Empty when ok. */
|
|
34
32
|
readonly reasons: readonly string[]
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -51,10 +49,18 @@ export async function collectVerificationLevels(
|
|
|
51
49
|
cwd: string,
|
|
52
50
|
options: CollectLevelsOptions = {},
|
|
53
51
|
): Promise<VerificationLevelsResult> {
|
|
52
|
+
// The ladder is the fullest view over the one project verification: L0 is the check view, L1/L2 read
|
|
53
|
+
// the route-assurance + capability evidence the same collector already computed (no second walk), and
|
|
54
|
+
// only the ladder-specific L3 (manifest) and L4 (invariants) are evaluated here.
|
|
55
|
+
const { collectProjectVerification } = await import("./verification.ts")
|
|
56
|
+
const verification = await collectProjectVerification(cwd, {
|
|
57
|
+
...(options.config !== undefined ? { config: options.config } : {}),
|
|
58
|
+
maxDiagnostics: 5,
|
|
59
|
+
})
|
|
54
60
|
const statuses: { level: number; name: string; ok: boolean; reasons: string[] }[] = []
|
|
55
61
|
|
|
56
|
-
// L0
|
|
57
|
-
const check = await
|
|
62
|
+
// L0 - typed contract (`nifra check`).
|
|
63
|
+
const check = await verification.check()
|
|
58
64
|
statuses.push({
|
|
59
65
|
level: 0,
|
|
60
66
|
name: "typed contract",
|
|
@@ -63,11 +69,11 @@ export async function collectVerificationLevels(
|
|
|
63
69
|
})
|
|
64
70
|
|
|
65
71
|
// L1-L4 all hang off the assurance config; without it the ladder stops at L0 by definition.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
if (verification.config === undefined) {
|
|
73
|
+
const reason =
|
|
74
|
+
verification.configError instanceof Error
|
|
75
|
+
? verification.configError.message
|
|
76
|
+
: String(verification.configError)
|
|
71
77
|
for (const [level, name] of [
|
|
72
78
|
[1, "route assurance"],
|
|
73
79
|
[2, "capability lockfile"],
|
|
@@ -78,9 +84,10 @@ export async function collectVerificationLevels(
|
|
|
78
84
|
}
|
|
79
85
|
return finalize(statuses)
|
|
80
86
|
}
|
|
87
|
+
const config = verification.config
|
|
81
88
|
|
|
82
|
-
// L1
|
|
83
|
-
const assurance =
|
|
89
|
+
// L1 - route assurance.
|
|
90
|
+
const assurance = verification.routeAssurance as AssuranceReport
|
|
84
91
|
statuses.push({
|
|
85
92
|
level: 1,
|
|
86
93
|
name: "route assurance",
|
|
@@ -88,14 +95,13 @@ export async function collectVerificationLevels(
|
|
|
88
95
|
reasons: assurance.findings.map((f) => f.message).slice(0, 5),
|
|
89
96
|
})
|
|
90
97
|
|
|
91
|
-
// L2
|
|
98
|
+
// L2 - capability assurance + lockfile in sync.
|
|
92
99
|
const l2Reasons: string[] = []
|
|
93
|
-
|
|
100
|
+
const capabilityReport = verification.capability?.report
|
|
94
101
|
if (config.capabilities === undefined) {
|
|
95
102
|
l2Reasons.push("nifra.assurance.ts declares no capabilities policy")
|
|
96
103
|
} else {
|
|
97
|
-
const project =
|
|
98
|
-
capabilityReport = project.report
|
|
104
|
+
const project = verification.capability as CapabilityProjectReport
|
|
99
105
|
if (!project.report.ok) {
|
|
100
106
|
l2Reasons.push(...project.report.findings.map((f) => f.message).slice(0, 5))
|
|
101
107
|
}
|
|
@@ -115,11 +121,11 @@ export async function collectVerificationLevels(
|
|
|
115
121
|
reasons: l2Reasons,
|
|
116
122
|
})
|
|
117
123
|
|
|
118
|
-
// L3
|
|
124
|
+
// L3 - emitted manifest hash-verifies AND matches the current app.
|
|
119
125
|
const l3Reasons: string[] = []
|
|
120
126
|
const manifestPath = resolve(cwd, config.manifest?.path ?? DEFAULT_MANIFEST_FILE)
|
|
121
127
|
if (!existsSync(manifestPath)) {
|
|
122
|
-
l3Reasons.push(`manifest missing (${manifestPath})
|
|
128
|
+
l3Reasons.push(`manifest missing (${manifestPath}) - run \`nifra manifest emit\``)
|
|
123
129
|
} else {
|
|
124
130
|
try {
|
|
125
131
|
const recorded = await parseNifraManifest(await Bun.file(manifestPath).text(), manifestPath)
|
|
@@ -130,7 +136,7 @@ export async function collectVerificationLevels(
|
|
|
130
136
|
...(capabilityReport !== undefined ? { capabilities: capabilityReport } : {}),
|
|
131
137
|
})
|
|
132
138
|
if (current.contentHash !== recorded.contentHash) {
|
|
133
|
-
l3Reasons.push("manifest is stale
|
|
139
|
+
l3Reasons.push("manifest is stale - the app changed since it was emitted")
|
|
134
140
|
}
|
|
135
141
|
} else {
|
|
136
142
|
l3Reasons.push("manifest cannot be validated while assurance is failing")
|
|
@@ -146,11 +152,11 @@ export async function collectVerificationLevels(
|
|
|
146
152
|
reasons: l3Reasons,
|
|
147
153
|
})
|
|
148
154
|
|
|
149
|
-
// L4
|
|
155
|
+
// L4 - contract-generated invariants against an explicitly isolated executor.
|
|
150
156
|
const l4Reasons: string[] = []
|
|
151
157
|
if (config.invariants === undefined) {
|
|
152
158
|
l4Reasons.push(
|
|
153
|
-
"no isolated invariant executor configured
|
|
159
|
+
"no isolated invariant executor configured - add `invariants.executor` in nifra.assurance.ts",
|
|
154
160
|
)
|
|
155
161
|
} else {
|
|
156
162
|
const invariants = await runAdversarialContract(config.source as ContractTestApp, {
|
package/src/load.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zero-config app discovery. The CLI reads a few conventions from the project root:
|
|
3
|
-
* - `routes/`
|
|
4
|
-
* - `nifra.config.ts`
|
|
3
|
+
* - `routes/` - the file-based routes (per `@nifrajs/web/fs`).
|
|
4
|
+
* - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework}); `framework.ts` is
|
|
5
5
|
* used as a fallback. One of the two is required.
|
|
6
|
-
* - `backend.ts`
|
|
6
|
+
* - `backend.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
|
|
7
7
|
*
|
|
8
8
|
* Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `framework.ts` for the
|
|
9
|
-
* render adapter, so `framework.ts` must stay edge-bundlable
|
|
9
|
+
* render adapter, so `framework.ts` must stay edge-bundlable - it can't reference Vite plugins or the
|
|
10
10
|
* SFC compiler (Bun would pull them into the `target:"browser"` worker and fail on `child_process`).
|
|
11
11
|
* `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY by this CLI (which runs on
|
|
12
12
|
* Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`, `@nifrajs/web-vue/plugin`) never
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync } from "node:fs"
|
|
16
16
|
import { resolve } from "node:path"
|
|
17
|
+
import { checkPipelineSeparation } from "./pipeline-guard.ts"
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
* A plugin list
|
|
20
|
+
* A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms are
|
|
20
21
|
* accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for deferring a
|
|
21
|
-
* plugin's construction
|
|
22
|
+
* plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a dynamic `import()`
|
|
22
23
|
* even from an unused export, so a Vite/compiler import in an edge-bundled file fails regardless). Edge
|
|
23
24
|
* safety comes from WHERE the config lives: keep these fields in `nifra.config.ts` (CLI-only), not in
|
|
24
25
|
* the edge-imported `framework.ts`. See the module header.
|
|
@@ -28,7 +29,7 @@ export type PluginsField =
|
|
|
28
29
|
| (() => readonly unknown[] | Promise<readonly unknown[]>)
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* The CLI's framework wiring
|
|
32
|
+
* The CLI's framework wiring - exported from `nifra.config.ts` (or `framework.ts` as a fallback).
|
|
32
33
|
* `create-nifra` generates it. Only `adapter` + `clientModule` are required; the plugin/condition
|
|
33
34
|
* fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact don't).
|
|
34
35
|
*/
|
|
@@ -42,13 +43,40 @@ export interface NifraFramework {
|
|
|
42
43
|
/** Bun build plugins that compile routes for the CLIENT bundle (`nifra build`), e.g.
|
|
43
44
|
* `[vueBunPlugin("dom")]`. React/Preact JSX is Bun-native → none. */
|
|
44
45
|
readonly clientPlugins?: PluginsField
|
|
45
|
-
/** Bun runtime plugins that compile routes for SSR
|
|
46
|
+
/** Bun runtime plugins that compile routes for SSR - `nifra dev` (Bun-native route import) and
|
|
46
47
|
* `nifra start` register them via `Bun.plugin`, e.g. `[vueBunPlugin("ssr")]`. React/Preact → none. */
|
|
47
48
|
readonly serverPlugins?: PluginsField
|
|
48
49
|
/** Extra Bun.build / Vite resolve conditions, e.g. Solid's `["solid"]`. */
|
|
49
50
|
readonly conditions?: readonly string[]
|
|
50
51
|
/** Compile-time `define` replacements, e.g. Vue's `__VUE_*` flags. */
|
|
51
52
|
readonly define?: Readonly<Record<string, string>>
|
|
53
|
+
/** Static files directory. Defaults to `<app>/public`; `false` disables it. */
|
|
54
|
+
readonly publicDir?: string | false
|
|
55
|
+
/** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
|
|
56
|
+
readonly publicEnvPrefix?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refuse a config that puts one pipeline's plugins into the other's slot.
|
|
61
|
+
*
|
|
62
|
+
* Checked here rather than in `nifra check`, which holds a deliberate pre-`loadApp` invariant - reading
|
|
63
|
+
* plugins means importing the app's config, which is code execution. This runs at the moment the app
|
|
64
|
+
* IS loaded, so `dev`, `build` and `start` are all covered from one place, immediately before the
|
|
65
|
+
* plugins are handed to a bundler.
|
|
66
|
+
*
|
|
67
|
+
* Throws rather than warns. A misplaced plugin does not fail the build: Bun.build has no `transform`
|
|
68
|
+
* hook and Vite never calls `setup`, so the wrong-slot plugin is accepted and silently never invoked -
|
|
69
|
+
* the transform just does not happen, and the output looks plausible. A loud stop with the exact move
|
|
70
|
+
* is strictly better than a build that succeeds and omits the work. The classifier only fires on a
|
|
71
|
+
* decisive hook shape, so a plugin it cannot place is left alone.
|
|
72
|
+
*/
|
|
73
|
+
function assertPipelineSeparation(plugins: ResolvedPlugins, configFile: string): void {
|
|
74
|
+
const mismatches = checkPipelineSeparation(plugins)
|
|
75
|
+
if (mismatches.length === 0) return
|
|
76
|
+
throw new Error(
|
|
77
|
+
`[nifra] ${configFile}: plugins are in the wrong pipeline slot. nifra supports Vite and Bun, but a phase is owned by ONE of them - a plugin in the wrong slot is silently never called.\n\n` +
|
|
78
|
+
mismatches.map((m) => ` - ${m.fix}`).join("\n"),
|
|
79
|
+
)
|
|
52
80
|
}
|
|
53
81
|
|
|
54
82
|
/** Normalize a {@link PluginsField} (array | thunk | undefined) to a fresh array, awaiting a thunk. */
|
|
@@ -57,12 +85,20 @@ export async function resolvePlugins(field: PluginsField | undefined): Promise<u
|
|
|
57
85
|
return value ? [...value] : []
|
|
58
86
|
}
|
|
59
87
|
|
|
88
|
+
export interface ResolvedPlugins {
|
|
89
|
+
readonly vitePlugins: readonly unknown[]
|
|
90
|
+
readonly clientPlugins: readonly unknown[]
|
|
91
|
+
readonly serverPlugins: readonly unknown[]
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
export interface LoadedApp {
|
|
61
95
|
readonly cwd: string
|
|
62
96
|
readonly routesDir: string
|
|
63
97
|
/** Build output dir (also where `nifra start` reads `manifest.json` + serves `/assets/*`). */
|
|
64
98
|
readonly outDir: string
|
|
65
99
|
readonly framework: NifraFramework
|
|
100
|
+
/** Plugin thunks resolved exactly once during app loading and reused by every command phase. */
|
|
101
|
+
readonly resolvedPlugins: ResolvedPlugins
|
|
66
102
|
/** The `backend` export from `backend.ts`, or `undefined` if there's no `backend.ts`. */
|
|
67
103
|
readonly backend: unknown
|
|
68
104
|
}
|
|
@@ -73,7 +109,7 @@ export interface LoadAppOptions {
|
|
|
73
109
|
}
|
|
74
110
|
|
|
75
111
|
/**
|
|
76
|
-
* Root-level monorepo config
|
|
112
|
+
* Root-level monorepo config - exported from `nifra.config.ts` at the workspace root.
|
|
77
113
|
* Each key in `apps` is the short name used to namespace MCP tools (`nifra_<name>_context` etc.);
|
|
78
114
|
* each value is a path relative to the root that contains its own `nifra.config.ts` / `framework.ts`.
|
|
79
115
|
*
|
|
@@ -109,7 +145,7 @@ export async function detectMonorepo(
|
|
|
109
145
|
|
|
110
146
|
/**
|
|
111
147
|
* Load all apps declared in a monorepo config. Returns `{ name, cwd, app }[]` in declaration order.
|
|
112
|
-
* Individual app load failures throw
|
|
148
|
+
* Individual app load failures throw - each app must be valid.
|
|
113
149
|
*/
|
|
114
150
|
export async function loadMonorepoApps(
|
|
115
151
|
rootCwd: string,
|
|
@@ -151,7 +187,7 @@ export async function loadApp(
|
|
|
151
187
|
}
|
|
152
188
|
if (!existsSync(resolve(cwd, "routes"))) {
|
|
153
189
|
throw new Error(
|
|
154
|
-
`[nifra] no routes/ directory in ${cwd}
|
|
190
|
+
`[nifra] no routes/ directory in ${cwd} - nifra apps are file-routed under routes/.`,
|
|
155
191
|
)
|
|
156
192
|
}
|
|
157
193
|
const fw = (await importWithQuery(configPath, options.importQuery)) as Partial<NifraFramework>
|
|
@@ -160,6 +196,22 @@ export async function loadApp(
|
|
|
160
196
|
`[nifra] ${configFile} must export \`adapter\` (a render adapter object) and \`clientModule\` (a string).`,
|
|
161
197
|
)
|
|
162
198
|
}
|
|
199
|
+
// A `clientModule` given as a RELATIVE path (a local client entry, e.g. `./src/client.tsx`) is
|
|
200
|
+
// resolved to absolute here. It is embedded verbatim as an import specifier in the generated client
|
|
201
|
+
// entry, and `nifra dev` and `nifra build` write that entry into DIFFERENT directories - so a relative
|
|
202
|
+
// specifier would resolve against different bases and load in one phase but not the other. A bare or
|
|
203
|
+
// package specifier (`@nifrajs/web-react/client`) is location-independent and passes through unchanged.
|
|
204
|
+
const clientModule =
|
|
205
|
+
fw.clientModule.startsWith("./") || fw.clientModule.startsWith("../")
|
|
206
|
+
? resolve(cwd, fw.clientModule)
|
|
207
|
+
: fw.clientModule
|
|
208
|
+
const [vitePlugins, clientPlugins, serverPlugins] = await Promise.all([
|
|
209
|
+
resolvePlugins(fw.vitePlugins),
|
|
210
|
+
resolvePlugins(fw.clientPlugins),
|
|
211
|
+
resolvePlugins(fw.serverPlugins),
|
|
212
|
+
])
|
|
213
|
+
const resolvedPlugins = { vitePlugins, clientPlugins, serverPlugins }
|
|
214
|
+
assertPipelineSeparation(resolvedPlugins, configFile)
|
|
163
215
|
|
|
164
216
|
let backend: unknown
|
|
165
217
|
const backendPath = resolve(cwd, "backend.ts")
|
|
@@ -172,7 +224,8 @@ export async function loadApp(
|
|
|
172
224
|
cwd,
|
|
173
225
|
routesDir: resolve(cwd, "routes"),
|
|
174
226
|
outDir: resolve(cwd, outDirName),
|
|
175
|
-
framework: fw as NifraFramework,
|
|
227
|
+
framework: { ...fw, clientModule } as NifraFramework,
|
|
228
|
+
resolvedPlugins,
|
|
176
229
|
backend,
|
|
177
230
|
}
|
|
178
231
|
}
|
package/src/manifest-tool.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** `nifra manifest`
|
|
1
|
+
/** `nifra manifest` - emit/sign/diff the deployable route trust artifact. */
|
|
2
2
|
|
|
3
3
|
import { existsSync } from "node:fs"
|
|
4
4
|
import { resolve } from "node:path"
|
|
@@ -46,7 +46,9 @@ export async function runManifestEmit(
|
|
|
46
46
|
options: { readonly config?: string; readonly out?: string; readonly sign?: string } = {},
|
|
47
47
|
): Promise<boolean> {
|
|
48
48
|
const config = await loadAssuranceConfig(cwd, options.config)
|
|
49
|
-
const assurance = evaluateRouteAssurance(config.source, config.policy
|
|
49
|
+
const assurance = evaluateRouteAssurance(config.source, config.policy, {
|
|
50
|
+
...(config.capabilities !== undefined ? { definitions: config.capabilities.definitions } : {}),
|
|
51
|
+
})
|
|
50
52
|
const capabilityProject =
|
|
51
53
|
config.capabilities === undefined
|
|
52
54
|
? undefined
|
package/src/mcp-docs-tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The two project-INDEPENDENT MCP tools
|
|
2
|
+
* The two project-INDEPENDENT MCP tools - `nifra_docs` + `nifra_example` - as a single factory shared
|
|
3
3
|
* by every transport: the project stdio server ({@link projectTools}), the CLI HTTP server
|
|
4
4
|
* (`nifra docs-mcp`), and the site's Cloudflare-Pages worker (`/mcp`). The corpus source is injected as
|
|
5
5
|
* loaders, so the SAME tool definitions work whether the corpus comes from disk (`Bun.file`, on the
|
|
@@ -25,7 +25,7 @@ export function docsTools(
|
|
|
25
25
|
{
|
|
26
26
|
name: "nifra_docs",
|
|
27
27
|
description:
|
|
28
|
-
"Search nifra's framework documentation and get back ONLY the matching sections
|
|
28
|
+
"Search nifra's framework documentation and get back ONLY the matching sections - auth, uploads, ISR, WebSockets, loaders, deployment, anything. Call with no query for the cheap section index; pass query (e.g. \"isr revalidate\") for the top matching sections. Use this instead of reading llms-full.txt (~150 KB) whole. For the EXACT TypeScript of a type/interface/function, call nifra_types instead (don't read .d.ts files).",
|
|
29
29
|
inputSchema: {
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
@@ -40,7 +40,7 @@ export function docsTools(
|
|
|
40
40
|
handler: async (args) => {
|
|
41
41
|
const corpus = await loadDocs()
|
|
42
42
|
if (corpus === undefined) {
|
|
43
|
-
return "docs corpus missing
|
|
43
|
+
return "docs corpus missing - run `bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
44
44
|
}
|
|
45
45
|
const { query, limit } = args as { query?: string; limit?: number }
|
|
46
46
|
return renderDocsResult(corpus, query, clamp(limit, 1, 5, 3))
|
|
@@ -49,7 +49,7 @@ export function docsTools(
|
|
|
49
49
|
{
|
|
50
50
|
name: "nifra_example",
|
|
51
51
|
description:
|
|
52
|
-
'Get a VERIFIED, copy-pasteable nifra code example for a task
|
|
52
|
+
'Get a VERIFIED, copy-pasteable nifra code example for a task - auth route, file upload, ISR page, loader/action, typed client, SSE, deployment, etc. Every snippet is typechecked against the installed nifra version, so it compiles as-is. PREFER THIS over writing nifra code from memory (training data drifts). Call with no query for the grouped index; pass query (e.g. "protected route", "upload", "isr revalidate") for matching snippets.',
|
|
53
53
|
inputSchema: {
|
|
54
54
|
type: "object",
|
|
55
55
|
properties: {
|
|
@@ -64,7 +64,7 @@ export function docsTools(
|
|
|
64
64
|
handler: async (args) => {
|
|
65
65
|
const corpus = await loadExamples()
|
|
66
66
|
if (corpus === undefined) {
|
|
67
|
-
return "examples corpus missing
|
|
67
|
+
return "examples corpus missing - run `bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
68
68
|
}
|
|
69
69
|
const { query, limit } = args as { query?: string; limit?: number }
|
|
70
70
|
return renderExamplesResult(corpus, query, clamp(limit, 1, 5, 3))
|
|
@@ -73,14 +73,14 @@ export function docsTools(
|
|
|
73
73
|
{
|
|
74
74
|
name: "nifra_types",
|
|
75
75
|
description:
|
|
76
|
-
'Get the EXACT TypeScript declaration of any exported @nifrajs/* symbol
|
|
76
|
+
'Get the EXACT TypeScript declaration of any exported @nifrajs/* symbol - interface, type, class, function, const. Each signature is generated from the package\'s built .d.ts, so it is the LITERAL declaration: complete, authoritative, never prose and never truncated. THIS IS THE SOURCE OF TRUTH for nifra\'s types - do NOT read node_modules/@nifrajs/**/*.d.ts; call this instead. Pass `name` for an exact symbol (e.g. "RateLimitStore", "RouteSchema", "Context", "rateLimit"); pass `query` to search by keyword; omit both for the per-package index of names. A `name` lookup is always the complete declaration; a `query` returns a one-line summary plus the signature, collapsing an oversized body - pass `full: true` to override.',
|
|
77
77
|
inputSchema: {
|
|
78
78
|
type: "object",
|
|
79
79
|
properties: {
|
|
80
80
|
name: {
|
|
81
81
|
type: "string",
|
|
82
82
|
description:
|
|
83
|
-
"Exact symbol name
|
|
83
|
+
"Exact symbol name - returns its literal declaration (e.g. RateLimitStore).",
|
|
84
84
|
},
|
|
85
85
|
query: {
|
|
86
86
|
type: "string",
|
|
@@ -91,16 +91,26 @@ export function docsTools(
|
|
|
91
91
|
type: "number",
|
|
92
92
|
description: "Max results for a query/search (default 5, max 8).",
|
|
93
93
|
},
|
|
94
|
+
full: {
|
|
95
|
+
type: "boolean",
|
|
96
|
+
description:
|
|
97
|
+
"Query mode only: return whole declarations instead of collapsed ones. Off by default - a search is for picking a symbol, and one match can be tens of thousands of characters.",
|
|
98
|
+
},
|
|
94
99
|
},
|
|
95
100
|
additionalProperties: false,
|
|
96
101
|
},
|
|
97
102
|
handler: async (args) => {
|
|
98
103
|
const corpus = await loadTypes()
|
|
99
104
|
if (corpus === undefined) {
|
|
100
|
-
return "types corpus missing
|
|
105
|
+
return "types corpus missing - run `bun run build && bun run gen:llms` in the nifra repo (published builds ship it)."
|
|
106
|
+
}
|
|
107
|
+
const { name, query, limit, full } = args as {
|
|
108
|
+
name?: string
|
|
109
|
+
query?: string
|
|
110
|
+
limit?: number
|
|
111
|
+
full?: boolean
|
|
101
112
|
}
|
|
102
|
-
|
|
103
|
-
return renderTypesResult(corpus, name, query, clamp(limit, 1, 8, 5))
|
|
113
|
+
return renderTypesResult(corpus, name, query, clamp(limit, 1, 8, 5), full === true)
|
|
104
114
|
},
|
|
105
115
|
},
|
|
106
116
|
]
|