@nifrajs/cli 2.4.0 → 2.6.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/docs/types.json CHANGED
@@ -128,7 +128,7 @@
128
128
  "name": "AdversarialContractOptions",
129
129
  "kind": "interface",
130
130
  "package": "@nifrajs/testing",
131
- "signature": "export interface AdversarialContractOptions {\n /** Base origin for generated requests. Default `http://nifra.contract`. */\n readonly origin?: string;\n /** Replayable seed used for deterministic witness generation. */\n readonly seed?: number;\n /** Known-good request values keyed by `\"METHOD /path\"`. Required for opaque Standard Schemas. */\n readonly witnesses?: Readonly<Record<string, ContractWitness>>;\n /** Select a subset of reflected routes. */\n readonly include?: (route: ReflectedRoute) => boolean;\n /** Execute every case against these targets. Default: the supplied app, named `in-process`. */\n readonly runtimes?: readonly ContractRuntime[];\n /** Maximum validator-proven mutations per body/query schema. Default 32. */\n readonly maxMutationsPerInput?: number;\n /** Maximum serialized witness size. Default 256 KiB. */\n readonly maxWitnessBytes?: number;\n /** Statuses that prove an invalid input was rejected. Default `[422]`. */\n readonly expectedValidationStatuses?: readonly number[];\n /**\n * Attach auth, tenant, or runtime-specific state. Return a new Request; the input Request is already\n * fresh for this case and runtime.\n */\n readonly prepareRequest?: (request: Request, context: ContractCaseContext) => Request | Promise<Request>;\n /** Override rejection semantics (for apps with a custom validation response). */\n readonly isRejected?: (response: Response, context: ContractCaseContext) => boolean | Promise<boolean>;\n /** Validate a real successful response for every declared response schema. Default true. */\n readonly validateResponses?: boolean;\n /** Fail the report when a contracted target cannot be exercised. Default true. */\n readonly requireCoverage?: boolean;\n /** Greedily minimize an invalid input that unexpectedly reaches the app. Default true. */\n readonly shrinkFailures?: boolean;\n /** Cap shrink probes for one failure. Default 12. */\n readonly maxShrinkAttempts?: number;\n /** Replay only these stable case IDs. Coverage gaps are advisory when this is set. */\n readonly only?: string | readonly string[];\n /**\n * Derive an inspectable JSON Schema from a validator that carries none (zod/valibot/arktype expose a\n * Standard Schema validator but no `.jsonSchema`). Called for any body/query schema whose `jsonSchema`\n * is undefined; a non-undefined return turns on witness synthesis AND constraint-driven mutation\n * generation for that route. Fail-safe: throwing or returning undefined leaves the schema opaque\n * (NO_WITNESS, as today). Ready-made for zod: `import { zodJsonSchema } from \"@nifrajs/testing/zod\"`.\n */\n readonly reflectJsonSchema?: (schema: unknown) => JsonSchema | undefined;\n}"
131
+ "signature": "export interface AdversarialContractOptions {\n /** Base origin for generated requests. Default `http://nifra.contract`. */\n readonly origin?: string;\n /** Replayable seed used for deterministic witness generation. */\n readonly seed?: number;\n /** Known-good request values keyed by `\"METHOD /path\"`. Required for opaque Standard Schemas. */\n readonly witnesses?: Readonly<Record<string, ContractWitness>>;\n /** Select a subset of reflected routes. */\n readonly include?: (route: ReflectedRoute) => boolean;\n /** Execute every case against these targets. Default: the supplied app, named `in-process`. */\n readonly runtimes?: readonly ContractRuntime[];\n /** Maximum validator-proven mutations per body/query schema. Default 32. */\n readonly maxMutationsPerInput?: number;\n /** Maximum serialized witness size. Default 256 KiB. */\n readonly maxWitnessBytes?: number;\n /** Statuses that prove an invalid input was rejected. Default `[422]`. */\n readonly expectedValidationStatuses?: readonly number[];\n /**\n * Attach auth, tenant, or runtime-specific state. Return a new Request; the input Request is already\n * fresh for this case and runtime.\n */\n readonly prepareRequest?: (request: Request, context: ContractCaseContext) => Request | Promise<Request>;\n /** Override rejection semantics (for apps with a custom validation response). */\n readonly isRejected?: (response: Response, context: ContractCaseContext) => boolean | Promise<boolean>;\n /** Validate a real successful response for every declared response schema. Default true. */\n readonly validateResponses?: boolean;\n /** Fail the report when a contracted target cannot be exercised. Default true. */\n readonly requireCoverage?: boolean;\n /** Greedily minimize an invalid input that unexpectedly reaches the app. Default true. */\n readonly shrinkFailures?: boolean;\n /** Cap shrink probes for one failure. Default 12. */\n readonly maxShrinkAttempts?: number;\n /** Replay only these stable case IDs. Coverage gaps are advisory when this is set. */\n readonly only?: string | readonly string[];\n /**\n * Derive an inspectable JSON Schema from a validator that carries none (zod/valibot/arktype expose a\n * Standard Schema validator but no `.jsonSchema`). Called for any body/query schema whose `jsonSchema`\n * is undefined; a non-undefined return turns on witness synthesis AND constraint-driven mutation\n * generation for that route. Fail-safe: throwing or returning undefined leaves the schema opaque\n * (NO_WITNESS, as today). Ready-made for zod: `import { zodJsonSchema } from \"@nifrajs/testing/zod\"`.\n *\n * Defaults to `@nifrajs/mock`'s `autoReflectJsonSchema`, which recognizes zod by its Standard Schema\n * vendor tag and converts via `z.toJSONSchema` when zod is installed - so zod routes get witnesses and\n * constraint-driven mutations with no wiring. Pass a hook to override, or `() => undefined` to force\n * every opaque schema to stay opaque.\n */\n readonly reflectJsonSchema?: (schema: unknown) => JsonSchema | undefined;\n}"
132
132
  },
133
133
  {
134
134
  "name": "AdversarialContractReport",
@@ -523,6 +523,13 @@
523
523
  "signature": "export type AuthPlugin<P> = NifraPlugin & {\n /** The verified principal for this request, or `null` (no/invalid token in `optional` mode). */\n principal(request: Request): P | null;\n /** The verified principal, or **throws a `401` `Response`** when absent. */\n requirePrincipal(request: Request): P;\n};",
524
524
  "doc": "/**\n * A token-auth plugin (`bearer` / `apiKey`). Apply it with `app.use(auth)` - it rejects unauthorized\n * requests to **routes defined after it** with `401` (unless `optional`). Read the verified principal\n * inside a handler/loader via {@link AuthPlugin.principal} (nullable) or\n * {@link AuthPlugin.requirePrincipal} (throws `401` when absent). The principal is verified once per\n * request and cached, so reading it is free.\n *\n * `beforeHandle` is **order-scoped**: place `app.use(auth)` before the routes it should guard. Hold the\n * returned instance to read the principal (mirrors `@nifrajs/auth`'s `sessions.get(c)` and\n * `@nifrajs/better-auth`'s `getSession(auth, req)`):\n *\n * ```ts\n * const auth = bearer({ verify: (t) => lookupUser(t) }) // P = User (inferred)\n * const app = server().use(auth).get(\"/me\", (c) => auth.requirePrincipal(c.req))\n * ```\n */"
525
525
  },
526
+ {
527
+ "name": "autoReflectJsonSchema",
528
+ "kind": "function",
529
+ "package": "@nifrajs/mock",
530
+ "signature": "export declare function autoReflectJsonSchema(schema: unknown): JsonSchema | undefined;",
531
+ "doc": "/**\n * Derive a JSON Schema from a zod Standard Schema, or `undefined` for anything else (non-zod vendors,\n * zod not installed, unconvertible schemas). Safe as an always-on default: it can only ever turn an\n * opaque schema inspectable, never change one that already carries JSON Schema metadata.\n */"
532
+ },
526
533
  {
527
534
  "name": "Await",
528
535
  "kind": "function",
@@ -2350,7 +2357,7 @@
2350
2357
  "name": "CreateMcpServerOptions",
2351
2358
  "kind": "interface",
2352
2359
  "package": "@nifrajs/mcp",
2353
- "signature": "export interface CreateMcpServerOptions {\n readonly name: string;\n readonly version: string;\n readonly tools?: readonly McpTool[];\n /** MCP Apps widgets - their resources are registered and the UI capability is advertised. */\n readonly widgets?: readonly McpWidget[];\n /** Extra (non-widget) resources to expose. */\n readonly resources?: readonly McpResource[];\n readonly prompts?: readonly McpPrompt[];\n /** GET health-page text. */\n readonly health?: string;\n /** Max JSON-RPC body size in bytes (default 1 MB). */\n readonly maxBodyBytes?: number;\n}"
2360
+ "signature": "export interface CreateMcpServerOptions {\n readonly name: string;\n readonly version: string;\n readonly tools?: readonly McpTool[];\n /** MCP Apps widgets - their resources are registered and the UI capability is advertised. */\n readonly widgets?: readonly McpWidget[];\n /** Extra (non-widget) resources to expose. */\n readonly resources?: readonly McpResource[];\n readonly prompts?: readonly McpPrompt[];\n /** GET health-page text. */\n readonly health?: string;\n /** Max JSON-RPC body size in bytes (default 1 MB). */\n readonly maxBodyBytes?: number;\n /** Natural-language guidance for LLMs, surfaced in the modern `server/discover` result (2026-07-28). */\n readonly instructions?: string;\n /** Origin allowlist for the DNS-rebinding guard. Omit to allow any origin; set it to reject other\n * browser origins with 403 (e.g. a hardened, non-public mount). */\n readonly allowedOrigins?: readonly string[];\n}"
2354
2361
  },
2355
2362
  {
2356
2363
  "name": "createMemoryCausalityStore",
@@ -2846,7 +2853,7 @@
2846
2853
  "name": "DefineMcpToolOptions",
2847
2854
  "kind": "interface",
2848
2855
  "package": "@nifrajs/mcp",
2849
- "signature": "export interface DefineMcpToolOptions<S extends StandardSchemaV1 = UntypedArgs> {\n readonly name: string;\n readonly description: string;\n /**\n * A Standard Schema for the arguments (nifra's `t`, zod, valibot, arktype, …). It validates every\n * call before the handler runs - invalid arguments return an in-band `isError` result naming each\n * issue, so the calling agent can correct and retry - and it types the handler's `args`. Schemas\n * that carry a JSON Schema (nifra's `t` does) also become the advertised `inputSchema` for free.\n */\n readonly input?: S;\n /**\n * JSON Schema for the wire descriptor (`tools/list`). Defaults to `input`'s own JSON Schema when\n * it exposes one, else an empty object schema. Pass explicitly when `input` comes from a vendor\n * whose schemas carry no JSON Schema, so the agent still sees real parameter docs.\n */\n readonly inputSchema?: Record<string, unknown>;\n /** Link a widget so the tool's result renders as interactive UI in MCP Apps hosts (iframe). */\n readonly widget?: McpWidget;\n /** Render-intent for generative hosts that build their own themed UI from `structuredContent`. Lands\n * in `_meta.ui.intent`. Independent of `widget` - a tool can offer both (widget for MCP Apps hosts,\n * intent for generative builders). */\n readonly intent?: McpUiIntent;\n readonly handler: (args: InferOutput<S>, context: McpToolContext) => McpToolHandlerResult | Promise<McpToolHandlerResult>;\n}"
2856
+ "signature": "export interface DefineMcpToolOptions<S extends StandardSchemaV1 = UntypedArgs> {\n readonly name: string;\n readonly description: string;\n /**\n * A Standard Schema for the arguments (nifra's `t`, zod, valibot, arktype, …). It validates every\n * call before the handler runs - invalid arguments return an in-band `isError` result naming each\n * issue, so the calling agent can correct and retry - and it types the handler's `args`. Schemas\n * that carry a JSON Schema (nifra's `t` does) also become the advertised `inputSchema` for free.\n */\n readonly input?: S;\n /**\n * JSON Schema for the wire descriptor (`tools/list`). Defaults to `input`'s own JSON Schema when\n * it exposes one, else an empty object schema. Pass explicitly when `input` comes from a vendor\n * whose schemas carry no JSON Schema, so the agent still sees real parameter docs.\n */\n readonly inputSchema?: Record<string, unknown>;\n /** MCP tool safety hints (`readOnlyHint`/`destructiveHint`/…) surfaced in `tools/list`. Hosts use\n * them to decide confirmation UX, and connector directory reviews expect every tool to carry them. */\n readonly annotations?: McpToolAnnotations;\n /** Link a widget so the tool's result renders as interactive UI in MCP Apps hosts (iframe). */\n readonly widget?: McpWidget;\n /** Render-intent for generative hosts that build their own themed UI from `structuredContent`. Lands\n * in `_meta.ui.intent`. Independent of `widget` - a tool can offer both (widget for MCP Apps hosts,\n * intent for generative builders). */\n readonly intent?: McpUiIntent;\n readonly handler: (args: InferOutput<S>, context: McpToolContext) => McpToolHandlerResult | Promise<McpToolHandlerResult>;\n}"
2850
2857
  },
2851
2858
  {
2852
2859
  "name": "defineMcpWidget",
@@ -5033,7 +5040,7 @@
5033
5040
  "name": "JsonRpcResponse",
5034
5041
  "kind": "type",
5035
5042
  "package": "@nifrajs/mcp",
5036
- "signature": "export type JsonRpcResponse = {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n result: unknown;\n} | {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n error: {\n code: number;\n message: string;\n };\n};"
5043
+ "signature": "export type JsonRpcResponse = {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n result: unknown;\n} | {\n jsonrpc: \"2.0\";\n id: JsonRpcId;\n error: {\n code: number;\n message: string;\n data?: unknown;\n };\n};"
5037
5044
  },
5038
5045
  {
5039
5046
  "name": "JsonSchema",
@@ -5420,6 +5427,13 @@
5420
5427
  "signature": "export declare const MCP_CONFIG: McpConfig;",
5421
5428
  "doc": "/** The one canonical MCP config object both registries serialize - the anti-drift seam. */"
5422
5429
  },
5430
+ {
5431
+ "name": "MCP_ERROR",
5432
+ "kind": "const",
5433
+ "package": "@nifrajs/mcp",
5434
+ "signature": "export declare const MCP_ERROR: {\n readonly HEADER_MISMATCH: -32020;\n readonly UNSUPPORTED_VERSION: -32022;\n};",
5435
+ "doc": "/** MCP-reserved JSON-RPC error codes (2026-07-28 error-code policy carves out -32020..-32099 for the spec). */"
5436
+ },
5423
5437
  {
5424
5438
  "name": "MCP_JSON_PATH",
5425
5439
  "kind": "const",
@@ -5478,13 +5492,13 @@
5478
5492
  "name": "McpHttpOptions",
5479
5493
  "kind": "interface",
5480
5494
  "package": "@nifrajs/cli",
5481
- "signature": "export interface McpHttpOptions {\n /** Maximum JSON-RPC request body size in bytes. Default 1 MB. */\n readonly maxBodyBytes?: number;\n /** Resources / prompts / the MCP Apps UI extension served alongside the tools. */\n readonly features?: McpServerFeatures;\n /** Shown on the GET health page so each host can describe itself. */\n readonly health?: string;\n}"
5495
+ "signature": "export interface McpHttpOptions {\n /** Maximum JSON-RPC request body size in bytes. Default 1 MB. */\n readonly maxBodyBytes?: number;\n /** Resources / prompts / the MCP Apps UI extension served alongside the tools. */\n readonly features?: McpServerFeatures;\n /** Shown on the GET health page so each host can describe itself. */\n readonly health?: string;\n /**\n * Origin allowlist for the DNS-rebinding guard. Omit (the default) to allow any origin - the right\n * choice for a public, unauthenticated docs server that can't enumerate its browser clients' origins.\n * Set it (e.g. a localhost origin for a hardened local host) to reject any other browser origin with 403.\n */\n readonly allowedOrigins?: readonly string[];\n}"
5482
5496
  },
5483
5497
  {
5484
5498
  "name": "McpHttpOptions",
5485
5499
  "kind": "interface",
5486
5500
  "package": "@nifrajs/mcp",
5487
- "signature": "export interface McpHttpOptions {\n /** Maximum JSON-RPC request body size in bytes. Default 1 MB. */\n readonly maxBodyBytes?: number;\n /** Resources / prompts / the MCP Apps UI extension served alongside the tools. */\n readonly features?: McpServerFeatures;\n /** Shown on the GET health page so each host can describe itself. */\n readonly health?: string;\n}"
5501
+ "signature": "export interface McpHttpOptions {\n /** Maximum JSON-RPC request body size in bytes. Default 1 MB. */\n readonly maxBodyBytes?: number;\n /** Resources / prompts / the MCP Apps UI extension served alongside the tools. */\n readonly features?: McpServerFeatures;\n /** Shown on the GET health page so each host can describe itself. */\n readonly health?: string;\n /**\n * Origin allowlist for the DNS-rebinding guard. Omit (the default) to allow any origin - the right\n * choice for a public, unauthenticated docs server that can't enumerate its browser clients' origins.\n * Set it (e.g. a localhost origin for a hardened local host) to reject any other browser origin with 403.\n */\n readonly allowedOrigins?: readonly string[];\n}"
5488
5502
  },
5489
5503
  {
5490
5504
  "name": "mcpJson",
@@ -5568,7 +5582,7 @@
5568
5582
  "name": "McpServerFeatures",
5569
5583
  "kind": "interface",
5570
5584
  "package": "@nifrajs/mcp",
5571
- "signature": "export interface McpServerFeatures {\n readonly resources?: readonly McpResource[];\n readonly prompts?: readonly McpPrompt[];\n /** Advertise the MCP Apps UI extension in `initialize`. Present when the server serves `ui://` widgets;\n * `mimeTypes` defaults to `[UI_MIME]`. */\n readonly ui?: {\n readonly mimeTypes?: readonly string[];\n };\n}"
5585
+ "signature": "export interface McpServerFeatures {\n readonly resources?: readonly McpResource[];\n readonly prompts?: readonly McpPrompt[];\n /** Advertise the MCP Apps UI extension in `initialize`. Present when the server serves `ui://` widgets;\n * `mimeTypes` defaults to `[UI_MIME]`. */\n readonly ui?: {\n readonly mimeTypes?: readonly string[];\n };\n /** Natural-language guidance for LLMs on how to use this server. Surfaced in the modern\n * `server/discover` result (2026-07-28); legacy `initialize` does not carry it. */\n readonly instructions?: string;\n}"
5572
5586
  },
5573
5587
  {
5574
5588
  "name": "McpTool",
@@ -5934,7 +5948,21 @@
5934
5948
  "name": "MockServerOptions",
5935
5949
  "kind": "interface",
5936
5950
  "package": "@nifrajs/mock",
5937
- "signature": "export interface MockServerOptions {\n /** Seed for deterministic mock generation. */\n readonly seed?: number | undefined;\n /** Log mock requests to console. */\n readonly verbose?: boolean | undefined;\n /**\n * Derive a JSON Schema from a response validator that carries none (zod/valibot expose a Standard Schema\n * validator but no `.jsonSchema`), so their routes mock real data instead of `{}`. Fail-safe: throwing or\n * returning undefined falls back to `{}`, as today. Ready-made for zod: `@nifrajs/testing/zod`.\n */\n readonly reflectJsonSchema?: (schema: unknown) => JsonSchema | undefined;\n}"
5951
+ "signature": "export interface MockServerOptions {\n /** Seed for deterministic mock generation. */\n readonly seed?: number | undefined;\n /** Log mock requests to console. */\n readonly verbose?: boolean | undefined;\n /**\n * Derive a JSON Schema from a response validator that carries none (zod/valibot expose a Standard Schema\n * validator but no `.jsonSchema`), so their routes mock real data instead of `{}`. Fail-safe: throwing or\n * returning undefined falls back to `{}`, as today. Ready-made for zod: `@nifrajs/testing/zod`.\n *\n * Defaults to {@link autoReflectJsonSchema}, which recognizes zod by its Standard Schema vendor tag and\n * converts via `z.toJSONSchema` when zod is installed - so zod routes mock real data with no wiring.\n * Pass a hook to override, or `() => undefined` to force every opaque schema back to `{}`.\n */\n readonly reflectJsonSchema?: (schema: unknown) => JsonSchema | undefined;\n}"
5952
+ },
5953
+ {
5954
+ "name": "MODERN_PROTOCOL_VERSION",
5955
+ "kind": "const",
5956
+ "package": "@nifrajs/mcp",
5957
+ "signature": "export declare const MODERN_PROTOCOL_VERSION = \"2026-07-28\";",
5958
+ "doc": "/** The stateless per-request revision (2026-07-28+). A request carrying its version in `_meta`\n * (`io.modelcontextprotocol/protocolVersion`) is served in modern mode; an `initialize` request stays\n * legacy. This server is dual-era: `handleRpc` answers both on the same dispatch, per request. */"
5959
+ },
5960
+ {
5961
+ "name": "modernVersionOf",
5962
+ "kind": "function",
5963
+ "package": "@nifrajs/mcp",
5964
+ "signature": "export declare function modernVersionOf(params: Record<string, unknown> | undefined): string | undefined;",
5965
+ "doc": "/** The protocol version a modern (2026-07-28+) request declares in `_meta`, or `undefined` for a legacy\n * request. Its presence is what puts the dispatch in modern mode for that one request. */"
5938
5966
  },
5939
5967
  {
5940
5968
  "name": "mountIslands",
@@ -7028,8 +7056,8 @@
7028
7056
  "name": "PROTOCOL_VERSION",
7029
7057
  "kind": "const",
7030
7058
  "package": "@nifrajs/mcp",
7031
- "signature": "export declare const PROTOCOL_VERSION = \"2024-11-05\";",
7032
- "doc": "/**\n * The pure MCP (Model Context Protocol) JSON-RPC dispatch - no I/O, no `Bun.*`, no side effects, so it\n * unit-tests cleanly. A transport (stdio in `@nifrajs/cli`'s `mcp.ts`, Streamable-HTTP in {@link ./http.ts})\n * wires this to a byte stream; the tools/resources are injected, so the protocol logic is exercised\n * without spawning or reading streams.\n *\n * Beyond base MCP this also implements the server half of **MCP Apps (SEP-1865)** - interactive `ui://`\n * widgets: a tool links a UI resource via `_meta.ui.resourceUri`, `tools/call` returns `structuredContent`\n * for the widget to render, and `initialize` advertises the `io.modelcontextprotocol/ui` extension. All of\n * it is additive - a tool whose handler returns a plain `string` behaves exactly as before.\n */"
7059
+ "signature": "export declare const PROTOCOL_VERSION = \"2025-06-18\";",
7060
+ "doc": "/** The protocol revision this server advertises by default. A handshake-era version - the HTTP transport\n * still uses `initialize`; current clients reach it through their backward-compatibility fallback.\n * `initialize` echoes the client's own requested version instead when it's one this server also speaks\n * ({@link SUPPORTED_PROTOCOL_VERSIONS}). */"
7033
7061
  },
7034
7062
  {
7035
7063
  "name": "PublicDirCache",
@@ -7490,6 +7518,13 @@
7490
7518
  "signature": "export interface RenderAdapterConformanceFixture {\n /** A page that renders the `page`, `data`, and `pending` markers from the supplied props. */\n readonly page: unknown;\n /** The outer layout, which renders `markers.outer` before its child. */\n readonly outerLayout: unknown;\n /** The inner layout, which renders `markers.inner` before its child. */\n readonly innerLayout: unknown;\n /** Props passed to the page. Use values represented by the `data` and `pending` markers. */\n readonly props: RenderProps;\n /**\n * Set when the fixture renders **deferred/streaming-only** content (e.g. Suspense that resolves during\n * the stream). Streaming and buffered (`renderToString`) output then legitimately differ byte-for-byte,\n * so the hydration-equivalence check is relaxed to \"buffered output still contains every marker\" instead\n * of exact equality. Leave unset for plain synchronous content (the stricter, preferred check).\n */\n readonly deferred?: boolean;\n /** Unique text fragments observable in the rendered HTML. */\n readonly markers: {\n readonly page: string;\n readonly data: string;\n readonly pending: string;\n readonly outer: string;\n readonly inner: string;\n };\n}",
7491
7519
  "doc": "/** Framework-specific values that let the shared conformance module exercise a render adapter. */"
7492
7520
  },
7521
+ {
7522
+ "name": "RenderAssemblyCache",
7523
+ "kind": "interface",
7524
+ "package": "@nifrajs/web",
7525
+ "signature": "export interface RenderAssemblyCache {\n /** Shell up to (not including) the deferred-runtime insertion point. */\n shellPre?: string;\n /** Shell from after the deferred-runtime insertion point through the open `#root` container. */\n shellPost?: string;\n /** Tail opener: the inline script open tag + the route-id global (before the action global). */\n tailPre?: string;\n /** Tail mid: the prerendered-paths global (between the action and layout-data globals). */\n tailMid?: string;\n /** Tail data prefix: the `window.<data global>=` assignment head. */\n tailData?: string;\n /** Tail from after the serialized data through `</html>`. */\n tailPost?: string;\n}",
7526
+ "doc": "/** The mutable per-route slot {@link RenderPageOptions.assemblyCache} fills. Opaque - create as `{}`. */"
7527
+ },
7493
7528
  {
7494
7529
  "name": "RenderedPage",
7495
7530
  "kind": "interface",
@@ -7521,7 +7556,7 @@
7521
7556
  "name": "RenderPageOptions",
7522
7557
  "kind": "interface",
7523
7558
  "package": "@nifrajs/web",
7524
- "signature": "export interface RenderPageOptions {\n readonly adapter: RenderAdapter;\n /** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */\n readonly chain: readonly unknown[];\n /** The loader output for this request. */\n readonly data: unknown;\n /** An action's data return (POST only) - surfaced to the page as `actionData` + serialized\n * for the client so post-POST hydration matches. Omit on GETs. */\n readonly actionData?: unknown;\n /**\n * URL of the built client entry (loaded as a module script).\n *\n * Optional ONLY on a `hydrate: false` page, where there is no client takeover to feed and this value\n * reaches no output at all. The union below is what enforces that: omitting it on a hydrating page\n * stays a compile error rather than rendering `src=\"\"`.\n */\n readonly clientEntry?: string;\n /** Chunk URLs for the **matched** route (its layout chain + own chunk) to `modulepreload` in the\n * shell - so the route code downloads in parallel with the entry instead of after it\n * (`buildClient`'s per-route map). Empty/omitted ⇒ only the entry is preloaded (unchanged). */\n readonly preload?: readonly string[];\n /** Stylesheet URLs for the **matched** route (its layout chain + own CSS, from `buildClient`'s\n * `BuildManifest.css`) - injected as `<link rel=\"stylesheet\">` in `<head>` so styles arrive with the\n * first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */\n readonly styles?: readonly string[];\n /** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches\n * a static `_data.json` on soft-nav into a prerendered route. Empty/omitted ⇒ not injected. */\n readonly prerenderedPaths?: readonly string[];\n /** ISR: route freshness in seconds, emitted as the `x-nifra-isr-revalidate` header for a `withISR`\n * wrapper to read. Omit ⇒ no header (the wrapper's default TTL applies). */\n readonly revalidate?: number;\n /** Matched route id; written to `window.__NIFRA_ROUTE__` so the client hydrates this chain. */\n readonly routeId?: string;\n /** The matched route's decoded path params - surfaced to the page as `params` (via {@link RenderProps})\n * so an adapter's `useParams` is SSR-correct. Omit ⇒ `{}` (a route with no dynamic segments, or an\n * error/404 render). */\n readonly params?: Readonly<Record<string, string>>;\n /** The request's `pathname + search` - surfaced as `path` (via {@link RenderProps}) so an adapter's\n * `useLocation`/`useSearchParams` render the right URL server-side and hydrate without drift. Omit ⇒\n * `\"\"`. */\n readonly path?: string;\n /** The route's validated search (`searchOf(mod.searchSchema, url.search)`, the same value handed to\n * the loader as `ctx.search`), surfaced as `search` (via {@link RenderProps}) so an adapter's\n * `useSearch` is SSR-correct. Omit ⇒ `{}` (a render with no search context). */\n readonly search?: Record<string, unknown>;\n /** Per-layout loader data, forwarded to the adapter as `RenderProps.layoutData`. Aligned with the\n * chain's layout prefix; omitted when no layout in the chain has a loader. */\n readonly layoutData?: readonly unknown[];\n /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */\n readonly status?: number;\n /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`\n * is ignored (the document is always HTML) and the ISR freshness header is applied after these,\n * so neither can be overridden from here. */\n readonly headers?: HeadersLike;\n /** Document title (fallback when `head.title` is unset). */\n readonly title?: string;\n /** Resolved route head - `title` overrides `title` above; `meta`/`link` render as managed\n * (`data-nifra`) tags the client updates on navigation.\n *\n * **Head contract (the layout chain contributes).** `createWebApp` resolves this via\n * {@link mergeHeads}: a route's head is its **layout chain's** `meta`/`head` exports merged with the\n * page's. A `_layout.tsx` may `export const meta` (or `export function meta(args)`) - its tags land\n * on every page below it (the home for `hreflang`, `preconnect`, a section-default `<title>`). The\n * merge is **nearest-wins for scalars** (the page's `title` overrides an inner layout's, which\n * overrides an outer one; an undefined page title keeps the layout's) and **concatenated for the\n * `meta`/`link` arrays** (outermost layout first, page last). `<meta>`/`<link>` attributes pass a\n * shared tag-specific allowlist (including inert `data-*` metadata) and values are escaped. */\n readonly head?: Meta;\n /** Id of the container wrapping the app markup (default `\"root\"`). A non-default id also gets\n * {@link ROOT_ATTRIBUTE}, which is how the generated client entry finds the container to hydrate -\n * the entry is built once and cannot know what a given render chose. */\n readonly rootId?: string;\n /** When `false`, emit a complete but **non-hydrated** document - no client entry script, data\n * globals, or modulepreloads. Used for server-rendered `_error` pages: a terminal state that needs no\n * client takeover, and it sidesteps an SSR/hydrate mismatch (the server rendered the boundary, not\n * the page the client manifest maps this route id to). Default `true`. */\n readonly hydrate?: boolean;\n /** Island client bundles (`@nifrajs/web/islands`) to load as `<script type=\"module\">` in the document\n * tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount\n * no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */\n readonly islandScripts?: readonly string[];\n /** CSP nonce applied to every framework-owned executable script in this document. */\n readonly nonce?: string;\n}"
7559
+ "signature": "export interface RenderPageOptions {\n readonly adapter: RenderAdapter;\n /** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */\n readonly chain: readonly unknown[];\n /** The loader output for this request. */\n readonly data: unknown;\n /** An action's data return (POST only) - surfaced to the page as `actionData` + serialized\n * for the client so post-POST hydration matches. Omit on GETs. */\n readonly actionData?: unknown;\n /**\n * URL of the built client entry (loaded as a module script).\n *\n * Optional ONLY on a `hydrate: false` page, where there is no client takeover to feed and this value\n * reaches no output at all. The union below is what enforces that: omitting it on a hydrating page\n * stays a compile error rather than rendering `src=\"\"`.\n */\n readonly clientEntry?: string;\n /** Chunk URLs for the **matched** route (its layout chain + own chunk) to `modulepreload` in the\n * shell - so the route code downloads in parallel with the entry instead of after it\n * (`buildClient`'s per-route map). Empty/omitted ⇒ only the entry is preloaded (unchanged). */\n readonly preload?: readonly string[];\n /** Stylesheet URLs for the **matched** route (its layout chain + own CSS, from `buildClient`'s\n * `BuildManifest.css`) - injected as `<link rel=\"stylesheet\">` in `<head>` so styles arrive with the\n * first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */\n readonly styles?: readonly string[];\n /** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches\n * a static `_data.json` on soft-nav into a prerendered route. Empty/omitted ⇒ not injected. */\n readonly prerenderedPaths?: readonly string[];\n /** ISR: route freshness in seconds, emitted as the `x-nifra-isr-revalidate` header for a `withISR`\n * wrapper to read. Omit ⇒ no header (the wrapper's default TTL applies). */\n readonly revalidate?: number;\n /** Matched route id; written to `window.__NIFRA_ROUTE__` so the client hydrates this chain. */\n readonly routeId?: string;\n /** The matched route's decoded path params - surfaced to the page as `params` (via {@link RenderProps})\n * so an adapter's `useParams` is SSR-correct. Omit ⇒ `{}` (a route with no dynamic segments, or an\n * error/404 render). */\n readonly params?: Readonly<Record<string, string>>;\n /** The request's `pathname + search` - surfaced as `path` (via {@link RenderProps}) so an adapter's\n * `useLocation`/`useSearchParams` render the right URL server-side and hydrate without drift. Omit ⇒\n * `\"\"`. */\n readonly path?: string;\n /** The route's validated search (`searchOf(mod.searchSchema, url.search)`, the same value handed to\n * the loader as `ctx.search`), surfaced as `search` (via {@link RenderProps}) so an adapter's\n * `useSearch` is SSR-correct. Omit ⇒ `{}` (a render with no search context). */\n readonly search?: Record<string, unknown>;\n /** Per-layout loader data, forwarded to the adapter as `RenderProps.layoutData`. Aligned with the\n * chain's layout prefix; omitted when no layout in the chain has a loader. */\n readonly layoutData?: readonly unknown[];\n /** HTTP status for the response (default 200; e.g. 404 for a not-found page). */\n readonly status?: number;\n /** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`\n * is ignored (the document is always HTML) and the ISR freshness header is applied after these,\n * so neither can be overridden from here. */\n readonly headers?: HeadersLike;\n /** Document title (fallback when `head.title` is unset). */\n readonly title?: string;\n /** Resolved route head - `title` overrides `title` above; `meta`/`link` render as managed\n * (`data-nifra`) tags the client updates on navigation.\n *\n * **Head contract (the layout chain contributes).** `createWebApp` resolves this via\n * {@link mergeHeads}: a route's head is its **layout chain's** `meta`/`head` exports merged with the\n * page's. A `_layout.tsx` may `export const meta` (or `export function meta(args)`) - its tags land\n * on every page below it (the home for `hreflang`, `preconnect`, a section-default `<title>`). The\n * merge is **nearest-wins for scalars** (the page's `title` overrides an inner layout's, which\n * overrides an outer one; an undefined page title keeps the layout's) and **concatenated for the\n * `meta`/`link` arrays** (outermost layout first, page last). `<meta>`/`<link>` attributes pass a\n * shared tag-specific allowlist (including inert `data-*` metadata) and values are escaped. */\n readonly head?: Meta;\n /** Id of the container wrapping the app markup (default `\"root\"`). A non-default id also gets\n * {@link ROOT_ATTRIBUTE}, which is how the generated client entry finds the container to hydrate -\n * the entry is built once and cannot know what a given render chose. */\n readonly rootId?: string;\n /** When `false`, emit a complete but **non-hydrated** document - no client entry script, data\n * globals, or modulepreloads. Used for server-rendered `_error` pages: a terminal state that needs no\n * client takeover, and it sidesteps an SSR/hydrate mismatch (the server rendered the boundary, not\n * the page the client manifest maps this route id to). Default `true`. */\n readonly hydrate?: boolean;\n /** Island client bundles (`@nifrajs/web/islands`) to load as `<script type=\"module\">` in the document\n * tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount\n * no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */\n readonly islandScripts?: readonly string[];\n /** CSP nonce applied to every framework-owned executable script in this document. */\n readonly nonce?: string;\n /**\n * Advanced: a per-route slot the renderer fills with the request-invariant document pieces (shell\n * prefix/suffix, tail statics) on the first render and reuses afterwards, skipping their re-assembly.\n *\n * Only pass a cache when every shell-shaping input is IDENTICAL across the requests sharing it:\n * same `head` content (a static meta chain - never a `meta(data)` function), `title`, `styles`,\n * `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`. Per-request\n * values (`data`, `params`, `search`, `actionData`, `layoutData`, deferred state) are always assembled\n * fresh and safe. A per-request `nonce` disables the cache automatically. `createWebApp` wires this\n * per route, gated on the route + layout metas being static.\n */\n readonly assemblyCache?: RenderAssemblyCache;\n}"
7525
7560
  },
7526
7561
  {
7527
7562
  "name": "renderPageResult",
@@ -7730,7 +7765,7 @@
7730
7765
  "kind": "function",
7731
7766
  "package": "@nifrajs/mcp",
7732
7767
  "signature": "export declare function respondMcpHttp(request: Request, tools: McpTool[], serverInfo: {\n name: string;\n version: string;\n}, options?: McpHttpOptions): Promise<Response>;",
7733
- "doc": "/**\n * Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC\n * response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a\n * JSON-RPC parse error. The dispatch is the shared, transport-agnostic {@link handleRpc}.\n */"
7768
+ "doc": "/**\n * Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC\n * response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a\n * JSON-RPC parse error. Dual-era: a modern (2026-07-28) POST that mirrors its method/name/version into\n * headers is validated against the body before dispatch and gets spec HTTP statuses; legacy requests are\n * served unchanged. The dispatch is the shared, transport-agnostic {@link handleRpc}.\n */"
7734
7769
  },
7735
7770
  {
7736
7771
  "name": "responseCache",
@@ -8123,8 +8158,7 @@
8123
8158
  "name": "RouterContext",
8124
8159
  "kind": "const",
8125
8160
  "package": "@nifrajs/web-react",
8126
- "signature": "export declare const RouterContext: import(\"react\").Context<RouterContextValue>;",
8127
- "doc": "/** Router context. The default ({} params, \"\" path, {} search) is what a component sees when rendered\n * outside a nifra route tree (the hooks stay defined, no throw, so a stray `useParams` degrades\n * gracefully). */"
8161
+ "signature": "export declare const RouterContext: import(\"react\").Context<RouterContextValue>;"
8128
8162
  },
8129
8163
  {
8130
8164
  "name": "RouterContextValue",
@@ -8198,7 +8232,7 @@
8198
8232
  "name": "rpcError",
8199
8233
  "kind": "const",
8200
8234
  "package": "@nifrajs/mcp",
8201
- "signature": "export declare const rpcError: (id: JsonRpcId, code: number, message: string) => JsonRpcResponse;"
8235
+ "signature": "export declare const rpcError: (id: JsonRpcId, code: number, message: string, data?: unknown) => JsonRpcResponse;"
8202
8236
  },
8203
8237
  {
8204
8238
  "name": "rpcResult",
@@ -8498,15 +8532,13 @@
8498
8532
  "name": "SearchContext",
8499
8533
  "kind": "const",
8500
8534
  "package": "@nifrajs/web-preact",
8501
- "signature": "export declare const SearchContext: import(\"preact\").Context<Record<string, unknown>>;",
8502
- "doc": "/** The current route's validated search, provided by `compose` on SSR + client mount alike (derived from\n * the URL via the shared `searchOfChain`). `{}` outside a nifra route tree. Read via {@link useSearch}. */"
8535
+ "signature": "export declare const SearchContext: import(\"preact\").Context<Record<string, unknown>>;"
8503
8536
  },
8504
8537
  {
8505
8538
  "name": "SearchContext",
8506
8539
  "kind": "const",
8507
8540
  "package": "@nifrajs/web-solid",
8508
- "signature": "export declare const SearchContext: import(\"solid-js\").Context<Accessor<Record<string, unknown>> | undefined>;",
8509
- "doc": "/** The current route's validated search accessor, provided by `compose` on SSR + client mount alike\n * (derived from the URL via the shared `searchOfChain`). Read via {@link useSearch}. */"
8541
+ "signature": "export declare const SearchContext: Context<Accessor<Record<string, unknown>> | undefined>;"
8510
8542
  },
8511
8543
  {
8512
8544
  "name": "searchOf",
@@ -9731,6 +9763,13 @@
9731
9763
  "package": "@nifrajs/uploads",
9732
9764
  "signature": "export interface StripImageMetadataOptions {\n /** Output format. Default: keep png/jpeg, else re-encode to webp. */\n readonly format?: StripFormat;\n /** Encode quality (1-100). Default 82. */\n readonly quality?: number;\n}"
9733
9765
  },
9766
+ {
9767
+ "name": "stripSvgPreamble",
9768
+ "kind": "function",
9769
+ "package": "@nifrajs/web",
9770
+ "signature": "export declare function stripSvgPreamble(xml: string): string;",
9771
+ "doc": "/**\n * Strip the XML declaration, comments, and DOCTYPE from an SVG source by index scan. Shared by every\n * adapter's SVG-component transform instead of chained regex `replace`s, for two reasons: the lazy\n * `[\\s\\S]*?` scans backtrack quadratically on an unterminated marker, and sequential single-pass\n * replaces can splice removed delimiters back together (`<<!---->!DOCTYPE …>` survives them). The\n * scan removes each block in one forward pass, so neither failure mode exists. An unterminated\n * marker swallows the rest of the input - the correct reading, matching an XML parser.\n */"
9772
+ },
9734
9773
  {
9735
9774
  "name": "StylesheetEmitter",
9736
9775
  "kind": "interface",
@@ -10775,14 +10814,14 @@
10775
10814
  "name": "VERSION",
10776
10815
  "kind": "const",
10777
10816
  "package": "@nifrajs/core",
10778
- "signature": "export declare const VERSION: \"2.4.0\";",
10817
+ "signature": "export declare const VERSION: \"2.6.0\";",
10779
10818
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
10780
10819
  },
10781
10820
  {
10782
10821
  "name": "VERSION",
10783
10822
  "kind": "const",
10784
10823
  "package": "nifra",
10785
- "signature": "export declare const VERSION: \"2.4.0\";",
10824
+ "signature": "export declare const VERSION: \"2.6.0\";",
10786
10825
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
10787
10826
  },
10788
10827
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
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",
@@ -41,14 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@nifrajs/client": "2.4.0",
45
- "@nifrajs/core": "2.4.0",
46
- "@nifrajs/mcp": "2.4.0",
47
- "@nifrajs/runner": "2.4.0",
48
- "@nifrajs/schema": "2.4.0",
49
- "@nifrajs/testing": "2.4.0",
50
- "@nifrajs/web": "2.4.0",
51
- "create-nifra": "2.4.0"
44
+ "@nifrajs/client": "2.6.0",
45
+ "@nifrajs/core": "2.6.0",
46
+ "@nifrajs/mcp": "2.6.0",
47
+ "@nifrajs/runner": "2.6.0",
48
+ "@nifrajs/schema": "2.6.0",
49
+ "@nifrajs/testing": "2.6.0",
50
+ "@nifrajs/web": "2.6.0",
51
+ "create-nifra": "2.6.0"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",
@@ -18,7 +18,9 @@ import { scanStaticRouteText, stripComments, walkSource } from "./check.ts"
18
18
 
19
19
  const EFFECT_IMPORT =
20
20
  /\bimport\s+(?!type\b)(?:[^'"();]*?\bfrom\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)|\brequire\s*\(\s*["']([^"']+)["']\s*\)|\bexport\s+(?!type\b)[^'";]*?\bfrom\s*["']([^"']+)["']/g
21
- const TEMPLATE_EFFECT_IMPORT = /\b(?:import|require)\s*\(\s*`([^`$]*(?:\\.[^`$]*)*)`\s*\)/g
21
+ // `\\` is excluded from both inner classes: letting the class also match a lone backslash makes the
22
+ // `(A*(\\.A*)*)` shape ambiguous, which is exponential on a run of backslashes.
23
+ const TEMPLATE_EFFECT_IMPORT = /\b(?:import|require)\s*\(\s*`([^`$\\]*(?:\\.[^`$\\]*)*)`\s*\)/g
22
24
 
23
25
  /** Value-bearing import edges relevant to effect provenance, in source order. */
24
26
  export function scanEffectImports(content: string): string[] {
package/src/check.ts CHANGED
@@ -89,7 +89,25 @@ function parseQuotedLiteral(raw: string): string | undefined {
89
89
  } catch {
90
90
  if (!raw.startsWith("'") || !raw.endsWith("'")) return undefined
91
91
  try {
92
- return JSON.parse(`"${raw.slice(1, -1).replace(/"/g, '\\"')}"`) as string
92
+ // Convert the single-quoted body to a JSON string body by scan, handling backslash pairs
93
+ // atomically: `\'` becomes `'`, other escapes pass through untouched, and a bare `"` is
94
+ // escaped. A quote-only `replace` mis-handles content with backslashes (double-escaping
95
+ // an already-escaped quote, or letting a trailing backslash swallow the added escape).
96
+ const inner = raw.slice(1, -1)
97
+ let body = ""
98
+ for (let i = 0; i < inner.length; i++) {
99
+ const ch = inner[i] as string
100
+ if (ch === "\\" && i + 1 < inner.length) {
101
+ const next = inner[i + 1] as string
102
+ body += next === "'" ? "'" : ch + next
103
+ i++
104
+ } else if (ch === '"') {
105
+ body += '\\"'
106
+ } else {
107
+ body += ch
108
+ }
109
+ }
110
+ return JSON.parse(`"${body}"`) as string
93
111
  } catch {
94
112
  return undefined
95
113
  }
@@ -1204,6 +1222,7 @@ export interface CheckDiagnostic {
1204
1222
  | "response-route"
1205
1223
  | "undeclared-dependency"
1206
1224
  | "duplicate-install"
1225
+ | "stale-workspace-dist"
1207
1226
  | "server-manifest-drift"
1208
1227
  | "manifest-drift"
1209
1228
  | "capability-assurance"
@@ -1729,6 +1748,28 @@ export async function collectCheckResult(
1729
1748
  },
1730
1749
  })
1731
1750
  }
1751
+ // Advisory (never fails the gate): while actively editing a linked package its dist is always
1752
+ // momentarily behind. The finding earns its keep when a dev server is about to start against it -
1753
+ // Bun reads live `src` while Vite's SSR runner reads the artifact, so a stale one 500s inside
1754
+ // framework/shared-package code and reads exactly like an upstream regression.
1755
+ for (const f of dr.staleDists) {
1756
+ diagnostics.push({
1757
+ rule: "stale-workspace-dist",
1758
+ severity: "warning",
1759
+ message: f.missing
1760
+ ? `${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 ${f.package}`
1761
+ : `${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 ${f.package}`,
1762
+ fix: `rebuild ${f.package}`,
1763
+ suggestion: {
1764
+ kind: "manual",
1765
+ title: `Rebuild ${f.package}`,
1766
+ steps: [
1767
+ `Run the package's build (usually \`bun run build\` in its directory) so ${f.distFile} matches its source again.`,
1768
+ "Only workspace-linked installs drift; npm tarballs are immutable and never flagged.",
1769
+ ],
1770
+ },
1771
+ })
1772
+ }
1732
1773
  }
1733
1774
  // #7: a committed server-manifest.ts that drifted from routes/ - name the exact missing/extra routes.
1734
1775
  for (const f of manifestDrift) {
@@ -1948,6 +1989,7 @@ export async function runCheck(
1948
1989
  ["response-route", "route returns a raw Response (typed client → data: never)"],
1949
1990
  ["undeclared-dependency", "undeclared dependency in package.json"],
1950
1991
  ["duplicate-install", "duplicate identity-sensitive dependency install"],
1992
+ ["stale-workspace-dist", "workspace-linked dist older than its source"],
1951
1993
  ["server-manifest-drift", "server-manifest.ts drifted from routes/"],
1952
1994
  ["manifest-drift", "versioned trust manifest drift"],
1953
1995
  ["capability-assurance", "effect/capability assurance"],
@@ -1955,7 +1997,7 @@ export async function runCheck(
1955
1997
  ["check-config", "nifra.check.json"],
1956
1998
  ] as const) {
1957
1999
  const ds = counts(rule)
1958
- if (rule === "response-route") {
2000
+ if (rule === "response-route" || rule === "stale-workspace-dist") {
1959
2001
  // Advisory: surfaced with ⚠, never folded into pass/fail.
1960
2002
  console.log(ds.length === 0 ? `✓ ${label}: none` : `⚠ ${label}: ${ds.length} (advisory)`)
1961
2003
  } else if (rule !== "typecheck") {
package/src/cli.ts CHANGED
@@ -154,7 +154,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
154
154
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
155
155
 
156
156
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
157
- const CLI_VERSION = "2.4.0"
157
+ const CLI_VERSION = "2.6.0"
158
158
 
159
159
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
160
160
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter