@nifrajs/cli 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +10 -0
  2. package/dist/assure-hydration.d.ts.map +1 -1
  3. package/dist/assure-hydration.js +6 -1
  4. package/dist/assure-hydration.js.map +1 -1
  5. package/dist/assure.d.ts.map +1 -1
  6. package/dist/assure.js +3 -2
  7. package/dist/assure.js.map +1 -1
  8. package/dist/check-diagnostics.d.ts +165 -0
  9. package/dist/check-diagnostics.d.ts.map +1 -0
  10. package/dist/check-diagnostics.js +1009 -0
  11. package/dist/check-diagnostics.js.map +1 -0
  12. package/dist/check-scan.d.ts +245 -0
  13. package/dist/check-scan.d.ts.map +1 -0
  14. package/dist/check-scan.js +1599 -0
  15. package/dist/check-scan.js.map +1 -0
  16. package/dist/check.d.ts +4 -324
  17. package/dist/check.d.ts.map +1 -1
  18. package/dist/check.js +8 -2366
  19. package/dist/check.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +247 -10
  22. package/dist/cli.js.map +1 -1
  23. package/dist/command-catalog.d.ts +21 -1
  24. package/dist/command-catalog.d.ts.map +1 -1
  25. package/dist/command-catalog.js +122 -0
  26. package/dist/command-catalog.js.map +1 -1
  27. package/dist/env-file.js +5 -5
  28. package/dist/env-file.js.map +1 -1
  29. package/dist/frontend-guidance.d.ts +57 -0
  30. package/dist/frontend-guidance.d.ts.map +1 -0
  31. package/dist/frontend-guidance.js +195 -0
  32. package/dist/frontend-guidance.js.map +1 -0
  33. package/dist/levels-tool.d.ts.map +1 -1
  34. package/dist/levels-tool.js +6 -0
  35. package/dist/levels-tool.js.map +1 -1
  36. package/dist/load.d.ts.map +1 -1
  37. package/dist/load.js +4 -1
  38. package/dist/load.js.map +1 -1
  39. package/dist/mcp-context.d.ts +48 -0
  40. package/dist/mcp-context.d.ts.map +1 -0
  41. package/dist/mcp-context.js +215 -0
  42. package/dist/mcp-context.js.map +1 -0
  43. package/dist/mcp-docs-tools.d.ts.map +1 -1
  44. package/dist/mcp-docs-tools.js +31 -0
  45. package/dist/mcp-docs-tools.js.map +1 -1
  46. package/dist/mcp-exec.d.ts +74 -0
  47. package/dist/mcp-exec.d.ts.map +1 -0
  48. package/dist/mcp-exec.js +844 -0
  49. package/dist/mcp-exec.js.map +1 -0
  50. package/dist/mcp-http.js +1 -1
  51. package/dist/mcp-io.d.ts +26 -0
  52. package/dist/mcp-io.d.ts.map +1 -0
  53. package/dist/mcp-io.js +100 -0
  54. package/dist/mcp-io.js.map +1 -0
  55. package/dist/mcp-reflect.d.ts +16 -0
  56. package/dist/mcp-reflect.d.ts.map +1 -0
  57. package/dist/mcp-reflect.js +101 -0
  58. package/dist/mcp-reflect.js.map +1 -0
  59. package/dist/mcp-root.d.ts +7 -0
  60. package/dist/mcp-root.d.ts.map +1 -1
  61. package/dist/mcp-root.js +51 -3
  62. package/dist/mcp-root.js.map +1 -1
  63. package/dist/mcp.d.ts +10 -92
  64. package/dist/mcp.d.ts.map +1 -1
  65. package/dist/mcp.js +13 -1232
  66. package/dist/mcp.js.map +1 -1
  67. package/dist/openapi-tool.d.ts +5 -10
  68. package/dist/openapi-tool.d.ts.map +1 -1
  69. package/dist/openapi-tool.js +21 -2
  70. package/dist/openapi-tool.js.map +1 -1
  71. package/dist/openapi-types.d.ts +21 -0
  72. package/dist/openapi-types.d.ts.map +1 -0
  73. package/dist/openapi-types.js +214 -0
  74. package/dist/openapi-types.js.map +1 -0
  75. package/dist/port.d.ts.map +1 -1
  76. package/dist/port.js +2 -1
  77. package/dist/port.js.map +1 -1
  78. package/dist/project-facts.d.ts +2 -1
  79. package/dist/project-facts.d.ts.map +1 -1
  80. package/dist/project-facts.js.map +1 -1
  81. package/dist/release-verification.d.ts +4 -1
  82. package/dist/release-verification.d.ts.map +1 -1
  83. package/dist/release-verification.js +6 -138
  84. package/dist/release-verification.js.map +1 -1
  85. package/dist/rules/codes.d.ts +4 -0
  86. package/dist/rules/codes.d.ts.map +1 -1
  87. package/dist/rules/codes.js +4 -0
  88. package/dist/rules/codes.js.map +1 -1
  89. package/dist/rules/islands.d.ts +4 -0
  90. package/dist/rules/islands.d.ts.map +1 -0
  91. package/dist/rules/islands.js +155 -0
  92. package/dist/rules/islands.js.map +1 -0
  93. package/dist/rules/nano.d.ts +6 -0
  94. package/dist/rules/nano.d.ts.map +1 -0
  95. package/dist/rules/nano.js +264 -0
  96. package/dist/rules/nano.js.map +1 -0
  97. package/dist/rules/security.d.ts.map +1 -1
  98. package/dist/rules/security.js +38 -4
  99. package/dist/rules/security.js.map +1 -1
  100. package/dist/scaffold.d.ts +12 -6
  101. package/dist/scaffold.d.ts.map +1 -1
  102. package/dist/scaffold.js +149 -10
  103. package/dist/scaffold.js.map +1 -1
  104. package/dist/search-terms.d.ts.map +1 -1
  105. package/dist/search-terms.js +5 -3
  106. package/dist/search-terms.js.map +1 -1
  107. package/dist/stylex-migrate.d.ts +38 -0
  108. package/dist/stylex-migrate.d.ts.map +1 -0
  109. package/dist/stylex-migrate.js +1052 -0
  110. package/dist/stylex-migrate.js.map +1 -0
  111. package/dist/sync-manifest.d.ts.map +1 -1
  112. package/dist/sync-manifest.js +2 -1
  113. package/dist/sync-manifest.js.map +1 -1
  114. package/dist/verification-plan.d.ts +24 -0
  115. package/dist/verification-plan.d.ts.map +1 -0
  116. package/dist/verification-plan.js +116 -0
  117. package/dist/verification-plan.js.map +1 -0
  118. package/dist/work-graph.d.ts.map +1 -1
  119. package/dist/work-graph.js +6 -3
  120. package/dist/work-graph.js.map +1 -1
  121. package/docs/examples.json +36 -0
  122. package/docs/llms-full.txt +1226 -14
  123. package/docs/types.json +4185 -87
  124. package/package.json +9 -9
  125. package/src/assure-hydration.ts +6 -1
  126. package/src/assure.ts +4 -2
  127. package/src/check-diagnostics.ts +1219 -0
  128. package/src/check-scan.ts +1825 -0
  129. package/src/check.ts +47 -2752
  130. package/src/cli.ts +252 -10
  131. package/src/command-catalog.ts +155 -0
  132. package/src/env-file.ts +5 -5
  133. package/src/frontend-guidance.ts +259 -0
  134. package/src/levels-tool.ts +6 -0
  135. package/src/load.ts +4 -1
  136. package/src/mcp-context.ts +266 -0
  137. package/src/mcp-docs-tools.ts +37 -0
  138. package/src/mcp-exec.ts +1040 -0
  139. package/src/mcp-http.ts +1 -1
  140. package/src/mcp-io.ts +118 -0
  141. package/src/mcp-reflect.ts +140 -0
  142. package/src/mcp-root.ts +62 -4
  143. package/src/mcp.ts +61 -1511
  144. package/src/openapi-tool.ts +32 -2
  145. package/src/openapi-types.ts +261 -0
  146. package/src/port.ts +2 -1
  147. package/src/project-facts.ts +2 -4
  148. package/src/release-verification.ts +22 -164
  149. package/src/rules/codes.ts +4 -0
  150. package/src/rules/islands.ts +171 -0
  151. package/src/rules/nano.ts +295 -0
  152. package/src/rules/security.ts +40 -4
  153. package/src/scaffold.ts +169 -9
  154. package/src/search-terms.ts +7 -3
  155. package/src/stylex-migrate.ts +1139 -0
  156. package/src/sync-manifest.ts +2 -1
  157. package/src/verification-plan.ts +242 -0
  158. package/src/work-graph.ts +6 -3
@@ -1,6 +1,7 @@
1
1
  import { basename } from "node:path"
2
2
  import { toOpenAPI } from "@nifrajs/schema/openapi"
3
3
  import type { LoadedApp } from "./load.ts"
4
+ import { type InferredOpenAPIResponses, inferOpenAPIResponses } from "./openapi-types.ts"
4
5
 
5
6
  export type OpenApiFormat = "json" | "yaml"
6
7
 
@@ -52,10 +53,19 @@ function toYaml(value: unknown, depth = 0): string {
52
53
  * the trimmed document still references those names, and recomputing the exact reachable subset isn't
53
54
  * worth the risk of emitting a `$ref` with no target.
54
55
  */
55
- export function renderOpenApi(app: LoadedApp, format: OpenApiFormat, pathPrefix?: string): string {
56
+ function renderOpenApiWithResponses(
57
+ app: LoadedApp,
58
+ format: OpenApiFormat,
59
+ pathPrefix: string | undefined,
60
+ inferredResponses: InferredOpenAPIResponses | undefined,
61
+ ): string {
56
62
  const title = `${basename(app.cwd)} API`
57
63
  const input = hasRoutesMethod(app.backend) ? app.backend : {}
58
- const generated = toOpenAPI(input, { title, version: "1.0.0" })
64
+ const generated = toOpenAPI(input, {
65
+ title,
66
+ version: "1.0.0",
67
+ ...(inferredResponses === undefined ? {} : { inferredResponses }),
68
+ })
59
69
  // Narrow to operations under the prefix, rebuilding the doc (its `paths` is readonly). `components`
60
70
  // stays - see the doc comment for why the schema set isn't recomputed.
61
71
  const document =
@@ -69,3 +79,23 @@ export function renderOpenApi(app: LoadedApp, format: OpenApiFormat, pathPrefix?
69
79
  : generated
70
80
  return format === "yaml" ? toYaml(document) : JSON.stringify(document, null, 2)
71
81
  }
82
+
83
+ /** Render the current runtime route table without loading the TypeScript compiler. */
84
+ export function renderOpenApi(app: LoadedApp, format: OpenApiFormat, pathPrefix?: string): string {
85
+ return renderOpenApiWithResponses(app, format, pathPrefix, undefined)
86
+ }
87
+
88
+ /**
89
+ * Render OpenAPI with optional build-time response-type inference. The TypeScript compiler is loaded
90
+ * only in this explicit CLI path; the server and schema runtime packages remain compiler-free.
91
+ */
92
+ export async function renderOpenApiWithTypes(
93
+ app: LoadedApp,
94
+ format: OpenApiFormat,
95
+ pathPrefix?: string,
96
+ ): Promise<string> {
97
+ const inferred = await inferOpenAPIResponses(app.cwd)
98
+ for (const warning of inferred.warnings)
99
+ console.warn(`[nifra] OpenAPI type inference: ${warning}`)
100
+ return renderOpenApiWithResponses(app, format, pathPrefix, inferred.responses)
101
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Build-time response-type reflection for OpenAPI.
3
+ *
4
+ * This module is CLI-only. It loads the project's TypeScript compiler on demand, inspects the
5
+ * exported `backend` type, and returns inert JSON Schema metadata. It never imports or executes the
6
+ * generated schema at request time, and unsupported/opaque types are omitted rather than guessed.
7
+ */
8
+
9
+ import { existsSync } from "node:fs"
10
+ import { mkdtemp, rm, writeFile } from "node:fs/promises"
11
+ import { tmpdir } from "node:os"
12
+ import { dirname, join, resolve } from "node:path"
13
+ import type { JsonSchema } from "@nifrajs/core/reflection"
14
+ import type * as TSApi from "typescript"
15
+ import { importProjectTypeScript, type TypeScriptApi } from "./internal/typescript-import.ts"
16
+
17
+ export interface InferredOpenAPIResponse {
18
+ readonly description?: string
19
+ readonly schema?: JsonSchema
20
+ readonly contentType?: string
21
+ }
22
+
23
+ export type InferredOpenAPIResponses = Readonly<
24
+ Record<string, Readonly<Record<string, InferredOpenAPIResponse>>>
25
+ >
26
+
27
+ export interface OpenAPITypeInferenceResult {
28
+ readonly responses: InferredOpenAPIResponses
29
+ readonly warnings: readonly string[]
30
+ }
31
+
32
+ type Type = TSApi.Type
33
+ type TypeChecker = TSApi.TypeChecker
34
+
35
+ const hasFlag = (type: Type, flag: number): boolean => (type.flags & flag) !== 0
36
+
37
+ function schemaForType(
38
+ ts: TypeScriptApi,
39
+ checker: TypeChecker,
40
+ input: Type,
41
+ seen: Set<Type>,
42
+ depth: number,
43
+ ): JsonSchema | undefined {
44
+ if (depth > 20 || seen.has(input)) return undefined
45
+ // Keep `null` in unions: OpenAPI distinguishes `string | null` from `string`. Undefined members
46
+ // are omitted below because JSON has no undefined value; optional object properties are represented
47
+ // by the property's optional symbol flag instead.
48
+ const type = input
49
+ if (hasFlag(type, ts.TypeFlags.Any) || hasFlag(type, ts.TypeFlags.Unknown)) return undefined
50
+ if (hasFlag(type, ts.TypeFlags.Never)) return undefined
51
+ if (hasFlag(type, ts.TypeFlags.Undefined) || hasFlag(type, ts.TypeFlags.Void)) return undefined
52
+ if (hasFlag(type, ts.TypeFlags.StringLiteral)) {
53
+ return { const: (type as TSApi.StringLiteralType).value }
54
+ }
55
+ if (hasFlag(type, ts.TypeFlags.NumberLiteral)) {
56
+ return { const: (type as TSApi.NumberLiteralType).value }
57
+ }
58
+ if (hasFlag(type, ts.TypeFlags.BooleanLiteral)) {
59
+ return { const: checker.typeToString(type) === "true" }
60
+ }
61
+ if (hasFlag(type, ts.TypeFlags.StringLike)) return { type: "string" }
62
+ if (hasFlag(type, ts.TypeFlags.NumberLike)) return { type: "number" }
63
+ if (hasFlag(type, ts.TypeFlags.BooleanLike)) return { type: "boolean" }
64
+ if (hasFlag(type, ts.TypeFlags.BigIntLike)) return { type: "integer" }
65
+ if (hasFlag(type, ts.TypeFlags.Null)) return { type: "null" }
66
+
67
+ if (type.isUnion()) {
68
+ const members = type.types
69
+ .map((member) => schemaForType(ts, checker, member, seen, depth + 1))
70
+ .filter((schema): schema is JsonSchema => schema !== undefined)
71
+ if (members.length === 0) return undefined
72
+ if (members.length === 1) return members[0]
73
+ return { anyOf: members }
74
+ }
75
+
76
+ if (checker.isTupleType(type)) {
77
+ const elements = checker
78
+ .getTypeArguments(type as TSApi.TypeReference)
79
+ .map((member) => schemaForType(ts, checker, member, seen, depth + 1))
80
+ .filter((schema): schema is JsonSchema => schema !== undefined)
81
+ return elements.length === 0 ? undefined : { type: "array", prefixItems: elements }
82
+ }
83
+ if (checker.isArrayType(type)) {
84
+ const [element] = checker.getTypeArguments(type as TSApi.TypeReference)
85
+ const items =
86
+ element === undefined ? undefined : schemaForType(ts, checker, element, seen, depth + 1)
87
+ return items === undefined ? { type: "array" } : { type: "array", items }
88
+ }
89
+
90
+ if (type.symbol?.name === "Date") return { type: "string", format: "date-time" }
91
+ if ((type.flags & ts.TypeFlags.Object) === 0) return undefined
92
+
93
+ seen.add(type)
94
+ try {
95
+ const properties: Record<string, JsonSchema> = {}
96
+ const required: string[] = []
97
+ for (const property of checker.getPropertiesOfType(type)) {
98
+ const declaration = property.valueDeclaration ?? property.declarations?.[0]
99
+ if (declaration === undefined) continue
100
+ const propertyType = checker.getTypeOfSymbolAtLocation(property, declaration)
101
+ // Methods are implementation details, not JSON fields. Other unsupported property types make
102
+ // the whole response opaque instead of emitting a partial schema that could falsely promise a
103
+ // contract to generated clients.
104
+ if (checker.getSignaturesOfType(propertyType, ts.SignatureKind.Call).length > 0) continue
105
+ const propertySchema = schemaForType(ts, checker, propertyType, seen, depth + 1)
106
+ if (propertySchema === undefined) return undefined
107
+ properties[property.name] = propertySchema
108
+ if ((property.flags & ts.SymbolFlags.Optional) === 0) required.push(property.name)
109
+ }
110
+ if (Object.keys(properties).length === 0) return { type: "object" }
111
+ return {
112
+ type: "object",
113
+ properties,
114
+ ...(required.length > 0 ? { required } : {}),
115
+ }
116
+ } finally {
117
+ seen.delete(type)
118
+ }
119
+ }
120
+
121
+ function symbolNamed(
122
+ checker: TypeChecker,
123
+ source: TSApi.SourceFile,
124
+ name: string,
125
+ flags: number,
126
+ ): TSApi.Symbol | undefined {
127
+ return checker.getSymbolsInScope(source, flags).find((symbol) => symbol.name === name)
128
+ }
129
+
130
+ function propertyType(checker: TypeChecker, type: Type, name: string): Type | undefined {
131
+ const property = type.getProperty(name)
132
+ if (property === undefined) return undefined
133
+ const declaration =
134
+ property.valueDeclaration ?? property.declarations?.[0] ?? type.symbol?.valueDeclaration
135
+ return declaration === undefined
136
+ ? checker.getDeclaredTypeOfSymbol(property)
137
+ : checker.getTypeOfSymbolAtLocation(property, declaration)
138
+ }
139
+
140
+ function responseEntries(
141
+ ts: TypeScriptApi,
142
+ checker: TypeChecker,
143
+ location: TSApi.Node,
144
+ responseMap: Type,
145
+ warnings: string[],
146
+ ): Record<string, InferredOpenAPIResponse> {
147
+ const output: Record<string, InferredOpenAPIResponse> = {}
148
+ for (const property of responseMap.getProperties()) {
149
+ if (!/^[1-5][0-9]{2}$/.test(property.name)) continue
150
+ const bodyType = checker.getTypeOfSymbolAtLocation(property, location)
151
+ const schema = schemaForType(ts, checker, bodyType, new Set(), 0)
152
+ if (schema === undefined) {
153
+ const bodylessStatus =
154
+ property.name === "204" || property.name === "205" || property.name === "304"
155
+ const bodylessType =
156
+ hasFlag(bodyType, ts.TypeFlags.Undefined) || hasFlag(bodyType, ts.TypeFlags.Void)
157
+ if (bodylessStatus && bodylessType) {
158
+ output[property.name] = {}
159
+ continue
160
+ }
161
+ warnings.push(
162
+ `response ${property.name} has an unsupported or opaque TypeScript body (${checker.typeToString(bodyType)})`,
163
+ )
164
+ continue
165
+ }
166
+ output[property.name] = { schema }
167
+ }
168
+ return output
169
+ }
170
+
171
+ /** Infer response schemas from `<root>/backend.ts` without affecting runtime application loading. */
172
+ export async function inferOpenAPIResponses(root: string): Promise<OpenAPITypeInferenceResult> {
173
+ const warnings: string[] = []
174
+ const backendPath = resolve(root, "backend.ts")
175
+ if (!existsSync(backendPath)) return { responses: {}, warnings }
176
+
177
+ const ts = await importProjectTypeScript(root)
178
+ if (ts === undefined) {
179
+ warnings.push("TypeScript is not installed; response inference was skipped")
180
+ return { responses: {}, warnings }
181
+ }
182
+
183
+ const temp = await mkdtemp(join(tmpdir(), "nifra-openapi-"))
184
+ const probePath = join(temp, "probe.ts")
185
+ const probe = [
186
+ 'import type { Server as __NifraServer } from "@nifrajs/core/server"',
187
+ `import { backend as __nifra_backend } from ${JSON.stringify(backendPath)}`,
188
+ "type __NifraRegistry = typeof __nifra_backend extends __NifraServer<infer R, any, any> ? R : never",
189
+ "declare const __nifra_registry: __NifraRegistry",
190
+ ].join("\n")
191
+
192
+ try {
193
+ await writeFile(probePath, probe, "utf8")
194
+ const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json")
195
+ let options: TSApi.CompilerOptions = {
196
+ noEmit: true,
197
+ skipLibCheck: true,
198
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
199
+ }
200
+ let fileNames = [backendPath]
201
+ if (configPath !== undefined) {
202
+ const config = ts.readConfigFile(configPath, ts.sys.readFile)
203
+ if (config.error) {
204
+ warnings.push(
205
+ `could not read tsconfig.json: ${ts.flattenDiagnosticMessageText(config.error.messageText, " ")}`,
206
+ )
207
+ } else {
208
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, dirname(configPath))
209
+ options = { ...parsed.options, noEmit: true, skipLibCheck: true }
210
+ fileNames = parsed.fileNames.length > 0 ? parsed.fileNames : fileNames
211
+ }
212
+ }
213
+ const host = ts.createCompilerHost(options, true)
214
+ const program = ts.createProgram(
215
+ [...new Set([...fileNames, backendPath, probePath])],
216
+ options,
217
+ host,
218
+ )
219
+ const source = program.getSourceFile(probePath)
220
+ if (source === undefined) {
221
+ warnings.push("could not create the TypeScript response-inference probe")
222
+ return { responses: {}, warnings }
223
+ }
224
+ const checker = program.getTypeChecker()
225
+ const registrySymbol = symbolNamed(
226
+ checker,
227
+ source,
228
+ "__nifra_registry",
229
+ ts.SymbolFlags.BlockScopedVariable,
230
+ )
231
+ if (registrySymbol === undefined) {
232
+ warnings.push("backend does not expose a typed Nifra server registry")
233
+ return { responses: {}, warnings }
234
+ }
235
+ const registry = checker.getTypeOfSymbolAtLocation(registrySymbol, source)
236
+ const responses: Record<string, Record<string, InferredOpenAPIResponse>> = {}
237
+ for (const pathSymbol of registry.getProperties()) {
238
+ const routePath = pathSymbol.name
239
+ const methods = checker.getTypeOfSymbolAtLocation(pathSymbol, source)
240
+ for (const methodSymbol of methods.getProperties()) {
241
+ if (methodSymbol.name === "WS") continue
242
+ const info = checker.getTypeOfSymbolAtLocation(methodSymbol, source)
243
+ const responseType = propertyType(checker, info, "responses")
244
+ if (responseType === undefined) continue
245
+ const routeResponses = responseEntries(
246
+ ts,
247
+ checker,
248
+ source,
249
+ checker.getNonNullableType(responseType),
250
+ warnings,
251
+ )
252
+ if (Object.keys(routeResponses).length > 0) {
253
+ responses[`${methodSymbol.name.toUpperCase()} ${routePath}`] = routeResponses
254
+ }
255
+ }
256
+ }
257
+ return { responses, warnings }
258
+ } finally {
259
+ await rm(temp, { recursive: true, force: true })
260
+ }
261
+ }
package/src/port.ts CHANGED
@@ -312,10 +312,11 @@ async function walkPortSource(
312
312
  cwd: string,
313
313
  visit: (rel: string, content: string) => void,
314
314
  ): Promise<void> {
315
- for await (const rel of new Glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}").scan({
315
+ for await (const rawRel of new Glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}").scan({
316
316
  cwd,
317
317
  dot: false,
318
318
  })) {
319
+ const rel = rawRel.replaceAll("\\", "/")
319
320
  if (IGNORED.test(rel)) continue
320
321
  visit(rel, await Bun.file(join(cwd, rel)).text())
321
322
  }
@@ -1,13 +1,11 @@
1
1
  import type { CapabilityProjectReport } from "./capabilities-tool.ts"
2
+ import type { CheckAssuranceContext, CheckConfig, CheckDiagnostic } from "./check-diagnostics.ts"
2
3
  import type {
3
- CheckAssuranceContext,
4
- CheckConfig,
5
- CheckDiagnostic,
6
4
  ManifestDriftFinding,
7
5
  SourceFinding,
8
6
  StaticRouteFinding,
9
7
  TransitiveServerImportFinding,
10
- } from "./check.ts"
8
+ } from "./check-scan.ts"
11
9
  import type { DoctorResult } from "./doctor.ts"
12
10
  import type { PipelineReport } from "./pipeline-report.ts"
13
11
  import type { RulePack, SourceIndex } from "./rules/index.ts"
@@ -8,8 +8,14 @@
8
8
  import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs"
9
9
  import { tmpdir } from "node:os"
10
10
  import { dirname, join, relative, resolve, sep } from "node:path"
11
-
12
- export type ReleaseVerificationMode = "default" | "release"
11
+ import {
12
+ omittedVerificationGateIds,
13
+ type VerificationGateSpec,
14
+ type VerificationPlanMode,
15
+ verificationPlan,
16
+ } from "./verification-plan.ts"
17
+
18
+ export type ReleaseVerificationMode = VerificationPlanMode
13
19
  export type ReleaseGateStatus = "pass" | "fail" | "skipped"
14
20
 
15
21
  export interface ReleaseGateResult {
@@ -25,6 +31,8 @@ export interface ReleaseVerificationResult {
25
31
  readonly ok: boolean
26
32
  readonly mode: ReleaseVerificationMode
27
33
  readonly gates: readonly ReleaseGateResult[]
34
+ /** Release gates intentionally absent from the selected shorter plan. */
35
+ readonly omittedReleaseGateIds: readonly string[]
28
36
  }
29
37
 
30
38
  export interface ReleaseCommandResult {
@@ -44,164 +52,6 @@ export interface ReleaseVerificationOptions {
44
52
  readonly runCommand?: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>
45
53
  }
46
54
 
47
- interface ReleaseGateSpec {
48
- readonly id: string
49
- readonly commands: readonly (readonly string[])[]
50
- readonly remediation: string
51
- }
52
-
53
- const DEFAULT_GATES: readonly ReleaseGateSpec[] = [
54
- {
55
- id: "lint",
56
- commands: [["run", "lint"]],
57
- remediation: "Run `bun run lint` and fix the reported lint findings.",
58
- },
59
- {
60
- id: "typecheck",
61
- commands: [["run", "typecheck"]],
62
- remediation: "Run `bun run typecheck` and fix the reported TypeScript errors.",
63
- },
64
- {
65
- id: "tests",
66
- commands: [["run", "test"]],
67
- remediation: "Run `bun run test` and fix the first failing test.",
68
- },
69
- {
70
- id: "docs",
71
- commands: [["run", "check:docs"]],
72
- remediation: "Run `bun run check:docs` and update the failing documentation example.",
73
- },
74
- {
75
- id: "api-corpus",
76
- commands: [["run", "check:api"]],
77
- remediation: "Run `bun run gen:api` and review the generated API reference.",
78
- },
79
- {
80
- id: "cards-corpus",
81
- commands: [["run", "check:cards"]],
82
- remediation: "Run `bun run gen:cards` and review the generated package cards.",
83
- },
84
- {
85
- id: "node-outcome-corpus",
86
- commands: [["run", "check:node-outcome"]],
87
- remediation: "Run `bun run gen:node-outcome` and review the generated Node outcome contract.",
88
- },
89
- {
90
- id: "sitemap",
91
- commands: [["run", "check:sitemap"]],
92
- remediation: "Run `bun run gen:sitemap` and review the generated sitemap.",
93
- },
94
- {
95
- id: "public-boundary",
96
- commands: [["run", "check:public-boundary"]],
97
- remediation:
98
- "Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
99
- },
100
- {
101
- id: "size",
102
- commands: [["run", "check:size"]],
103
- remediation:
104
- "Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
105
- },
106
- // Appended, not inserted: RELEASE_GATES below composes this list BY INDEX
107
- // (`slice(0, 3)`, `[3]`), so a gate added in the middle would silently re-point those.
108
- {
109
- id: "changesets",
110
- commands: [["run", "check:changesets"]],
111
- remediation:
112
- "Run `bun run changeset` and name every package whose source changed, so the release documents it.",
113
- },
114
- ]
115
-
116
- const RELEASE_GATES: readonly ReleaseGateSpec[] = [
117
- {
118
- id: "build",
119
- commands: [["run", "build"]],
120
- remediation: "Run `bun run build` and fix the first package build failure.",
121
- },
122
- ...DEFAULT_GATES.slice(0, 3),
123
- {
124
- id: "coverage",
125
- commands: [
126
- ["run", "test:coverage"],
127
- ["run", "check:coverage"],
128
- ],
129
- remediation:
130
- "Run `bun run test:coverage` first, then `bun run check:coverage`, and fix the reported coverage regression.",
131
- },
132
- {
133
- id: "corpus",
134
- commands: [["run", "check:corpus"]],
135
- remediation:
136
- "Run `bun run gen:llms`, `bun run gen:api`, and `bun run gen:cards`, then rerun the corpus gate.",
137
- },
138
- DEFAULT_GATES[3] as ReleaseGateSpec,
139
- {
140
- id: "public-boundary",
141
- commands: [["run", "check:public-boundary"]],
142
- remediation:
143
- "Run `bun run check:public-boundary` and remove the reported public-boundary violation.",
144
- },
145
- {
146
- id: "size",
147
- commands: [["run", "check:size"]],
148
- remediation:
149
- "Run `bun run check:size` and either reduce the bundle or update the reviewed budget.",
150
- },
151
- {
152
- id: "core-performance",
153
- commands: [["run", "check:core-performance"]],
154
- remediation:
155
- "Run `bun run check:core-performance` and investigate the measured performance regression.",
156
- },
157
- {
158
- id: "publish",
159
- commands: [["run", "check:publish"]],
160
- remediation:
161
- "Run `bun run check:publish` and fix the publish-consumer metadata or type-surface failure.",
162
- },
163
- {
164
- id: "consumer",
165
- commands: [["run", "check:consumers"]],
166
- remediation: "Run `bun run check:consumers` and fix the isolated consumer failure.",
167
- },
168
- {
169
- id: "cold-start",
170
- commands: [["run", "check:cold-start"]],
171
- remediation:
172
- "Run `bun run check:cold-start` and fix the fresh scaffold install or build failure.",
173
- },
174
- {
175
- id: "cross-runtime-deno",
176
- commands: [
177
- ["run", "test:deno"],
178
- ["run", "check:deno-tarball"],
179
- ],
180
- remediation:
181
- "Run `bun run test:deno` and `bun run check:deno-tarball`, then fix the first Deno compatibility failure.",
182
- },
183
- {
184
- id: "cross-runtime-node",
185
- commands: [["run", "test:node"]],
186
- remediation: "Run `bun run test:node` and fix the Node runtime adapter failure.",
187
- },
188
- {
189
- id: "pipeline-parity",
190
- commands: [["run", "check:pipeline-parity"]],
191
- remediation:
192
- "Run `bun run check:pipeline-parity` and fix the development and production manifest drift.",
193
- },
194
- // Last in release mode on purpose: this is the gate on what the release SAYS, and it is the only
195
- // one whose failure is invisible afterwards - a shipped version cannot grow the changelog line it
196
- // never had.
197
- {
198
- id: "changesets",
199
- commands: [["run", "check:changesets"]],
200
- remediation:
201
- "Run `bun run changeset` and name every package whose source changed, so the release documents it.",
202
- },
203
- ]
204
-
205
55
  const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandResult> => {
206
56
  const child = Bun.spawn([process.execPath, ...spec.args], {
207
57
  cwd: spec.cwd,
@@ -278,14 +128,14 @@ export async function resolveVerificationRoot(start: string): Promise<string> {
278
128
  }
279
129
  }
280
130
 
281
- const gatePlan = (mode: ReleaseVerificationMode): readonly ReleaseGateSpec[] =>
282
- mode === "release" ? RELEASE_GATES : DEFAULT_GATES
131
+ const gatePlan = (mode: ReleaseVerificationMode): readonly VerificationGateSpec[] =>
132
+ verificationPlan(mode)
283
133
 
284
134
  const commandLabel = (args: readonly string[]): string => `bun ${args.join(" ")}`
285
135
 
286
136
  const runGate = async (
287
137
  root: string,
288
- gate: ReleaseGateSpec,
138
+ gate: VerificationGateSpec,
289
139
  runCommand: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>,
290
140
  fixtureRoot: string,
291
141
  ): Promise<ReleaseGateResult> => {
@@ -322,7 +172,7 @@ const runGate = async (
322
172
  }
323
173
  }
324
174
 
325
- const skippedGate = (gate: ReleaseGateSpec, failedId: string): ReleaseGateResult => ({
175
+ const skippedGate = (gate: VerificationGateSpec, failedId: string): ReleaseGateResult => ({
326
176
  id: gate.id,
327
177
  status: "skipped",
328
178
  commands: gate.commands.map(commandLabel),
@@ -358,6 +208,7 @@ export async function collectReleaseVerification(
358
208
  ok: gates.every((gate) => gate.status === "pass"),
359
209
  mode,
360
210
  gates,
211
+ omittedReleaseGateIds: omittedVerificationGateIds(mode),
361
212
  }
362
213
  }
363
214
 
@@ -369,6 +220,13 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
369
220
  if (gate.message !== undefined) lines.push(` ${gate.message}`)
370
221
  if (gate.status !== "pass") lines.push(` fix: ${gate.remediation}`)
371
222
  }
223
+ if (result.omittedReleaseGateIds.length > 0) {
224
+ lines.push(
225
+ "",
226
+ `note: default verification omits release gates: ${result.omittedReleaseGateIds.join(", ")}`,
227
+ " run `bun run check:release` for the full release gate.",
228
+ )
229
+ }
372
230
  lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
373
231
  return lines.join("\n")
374
232
  }
@@ -20,6 +20,10 @@ export const RULE_CODES = Object.freeze({
20
20
  "NF-C017": "Rule pack validation",
21
21
  "NF-C018": "Reserved client segment check",
22
22
  "NF-C019": "Duplicate route registration check",
23
+ "NF-C020": "Island enhancer cleanup check",
24
+ "NF-C021": "nano binding cleanup check",
25
+ "NF-C022": "nano bindList key check",
26
+ "NF-C023": "nano computed deps check",
23
27
  "NF-D001": "Doctor check",
24
28
  "NF-A001": "Assurance bundle check",
25
29
  "NF-H001": "Hydration mismatch",