@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

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 (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
@@ -0,0 +1,262 @@
1
+ import { hasExtensionPolicy, effectiveExtensionPolicies } from './extensions.js';
2
+
3
+ import { ConfigError } from './errors.js';
4
+ import { effectivePolicies, registry } from './policies.js';
5
+
6
+
7
+ export const capabilityTargets = ['self-hosted', 'cloudflare', 'aws', 'vercel', 'static'] ;
8
+
9
+ /** `static` never reaches the policy-module handshake (src/types.ts `TargetName`): it refuses every
10
+ * runtime policy outright, so it needs no entry in that exhaustive per-target record. */
11
+
12
+
13
+ export const capabilityNames = ['extension','policies.extensions','proxy', 'signals', 'conditional', 'conditions', 'redirect', 'respond', 'page', 'static', 'download', 'function', 'middleware', 'parameters', 'methods', 'enabled', 'expires', 'request.body', 'response.headers', 'bindings', 'policies.agents', 'policies.security', 'policies.cache', 'policies.compression', 'policies.throttle'] ;
14
+
15
+ /** The resolved operator registration set, when known (loaded via --host-file, same as `inspectExtensions`). Keyed by extension name. */
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+ export function normalizeCapabilityTarget(target ) {
30
+ if (target === 'node') return 'self-hosted';
31
+ if ((capabilityTargets ).includes(target)) return target ;
32
+ throw new ConfigError('Unknown capability target; use self-hosted, cloudflare, aws, vercel or static');
33
+ }
34
+ const internalTarget = (target ) => target === 'self-hosted' ? 'node' : target;
35
+ const deployment = (target ) => target === 'self-hosted' ? 'local-runtime' : 'unverified';
36
+ const policyNames = Object.keys(registry) ;
37
+
38
+ // static hosting (S3 + CloudFront) has no server at all, so no capability
39
+ // needing request-time evaluation can be represented; only capabilities a
40
+ // build can lower ahead of time (redirect/respond/page/static/download,
41
+ // methods/enabled/expires) survive.
42
+ const staticRefusals = {
43
+ extension: 'no server, so no operator extension registry to delegate to',
44
+ 'policies.extensions': 'no server, so no operator extension registry to delegate to',
45
+ proxy: 'no server, so no bounded egress at request time',
46
+ signals: 'no server, so no fire-and-forget egress after a reply',
47
+ conditional: 'no server, so request-time condition matching is not possible',
48
+ conditions: 'no server, so request-time condition matching is not possible',
49
+ function: 'no server, so no dynamic execution',
50
+ middleware: 'no server, so no middleware execution',
51
+ parameters: 'no server, so no request-time parameter validation',
52
+ 'request.body': 'no server, so there is no request body to read or validate',
53
+ 'response.headers': 'no server, so response headers cannot be added per request; set them via S3 object metadata or a CloudFront response headers policy instead',
54
+ bindings: 'no server, so env/secret bindings cannot be resolved per request',
55
+ };
56
+
57
+ // Policy modules remain the authority for configuration-dependent support.
58
+ // `extension`/`policies.extensions` cannot get a target-independent answer:
59
+ // each registered extension declares its own `targets` (src/extensions.ts),
60
+ // so refusal on aws/vercel/self-hosted depends on that extension, not on the
61
+ // capability name. `extensionNames` names the specific extension(s) this
62
+ // requirement involves; `extensions` is the resolved registration set, when
63
+ // the caller has one (loaded via --host-file, same as `inspectExtensions`).
64
+ function decision(capability , target , policies , extensionNames , extensions ) {
65
+ if(capability==='extension'||capability==='policies.extensions'){
66
+ if(target==='cloudflare')return {support:'refused',reason:'Operator extensions have no Worker artifact lowering'};
67
+ if(target==='static')return {support:'refused',reason:staticRefusals[capability] };
68
+ if(!extensions)return {support:'conditional',reason:'Depends on the specific registered extension\'s declared targets; resolve with --host-file (CLI) or the runtime\'s extensions option'};
69
+ const names=extensionNames?.length?extensionNames:[...extensions.keys()];
70
+ if(!names.length)return {support:'unknown',reason:'No extension registration to check'};
71
+ const checked=names.map(name=>{const registration=extensions.get(name);return {name,support:(registration?(registration.targets.includes(internalTarget(target))?'native':'refused'):'unknown') };});
72
+ const unknown=checked.filter(item=>item.support==='unknown').map(item=>item.name);
73
+ if(unknown.length)return {support:'unknown',reason:`Not registered by the host file: ${unknown.join(', ')}`};
74
+ const refused=checked.filter(item=>item.support==='refused').map(item=>item.name);
75
+ if(refused.length)return {support:'refused',reason:`Refused by the extension's own declared targets: ${refused.join(', ')}`};
76
+ return {support:'native',reason:'Registered extension declares support for this target'};
77
+ }
78
+ const policy = policyNames.find(name => capability === `policies.${name}`);
79
+ if (policy) {
80
+ if (target === 'static') return { support: 'refused', reason: 'no server, so runtime policies are not enforced for static hosting' };
81
+ if (policy === 'throttle' && !policies && (target === 'aws' || target === 'vercel')) {
82
+ return { support: 'conditional', reason: 'Only partition: route is implemented; counters are per instance' };
83
+ }
84
+ const module = registry[policy];
85
+ const support = (module.targets(policies?.[policy]) )[internalTarget(target)] ?? 'unknown';
86
+ return { support, reason: support === 'refused' ? `${capability} cannot be compiled or enforced by this target`
87
+ : support === 'delegated' ? 'Delegated to the provider; exact compression settings and deployment behavior are unverified'
88
+ : support === 'unknown' ? 'No implementation evidence for this target'
89
+ : 'Implemented by the existing policy module; configuration validation still applies' };
90
+ }
91
+ if (target === 'static') {
92
+ const reason = staticRefusals[capability];
93
+ if (reason) return { support: 'refused', reason };
94
+ // redirect/respond/page/static/download/methods/enabled/expires fall through to the target's own success reason below.
95
+ } else if (target !== 'self-hosted') {
96
+ const reason = ['proxy','signals'].includes(capability) ? 'bounded egress currently requires the self-hosted Node lifecycle'
97
+ : target === 'cloudflare' && ['conditional','conditions'].includes(capability) ? 'conditional routing has no Worker artifact lowering yet'
98
+ : capability === 'function' ? 'functions need the self-hosted Node lifecycle, whether trusted (in-process) or sandboxed (worker threads and the WASM engine)'
99
+ : capability === 'middleware' ? 'middleware needs the self-hosted Node lifecycle, whether trusted (in-process) or sandboxed (worker threads and the WASM engine)'
100
+ : target === 'cloudflare' && ['page', 'static', 'download'].includes(capability) ? 'assets need a static-asset binding'
101
+ : target === 'cloudflare' && capability === 'bindings' ? 'env and secret bindings would have to be baked into the artifact'
102
+ : undefined;
103
+ if (reason) return { support: 'refused', reason };
104
+ }
105
+ return { support: target === 'cloudflare' || target === 'static' ? 'compiled' : 'native', reason: target === 'self-hosted'
106
+ ? 'Implemented and tested in the local runtime'
107
+ : target === 'static'
108
+ ? 'Compiled ahead of time into S3/CloudFront redirect metadata or static objects; provider deployment unverified'
109
+ : 'Implemented with local adapter tests; provider deployment unverified; transport normalization limits apply' };
110
+ }
111
+
112
+ /** Catalog, not a claim that every configuration or provider deployment works. */
113
+ export function getCapabilities(target ) {
114
+ const selected = target === undefined ? [...capabilityTargets] : [normalizeCapabilityTarget(target)];
115
+ return { format: 1, targets: selected.map(target => ({ target, deployment: deployment(target) })),
116
+ capabilities: capabilityNames.map(capability => ({ capability,
117
+ targets: Object.fromEntries(selected.map(target => [target, decision(capability, target)])) })) };
118
+ }
119
+
120
+ /** A safe projection shared by declaration preflight and the existing compiled IR. No values escape. */
121
+ export function routeCapabilities(route , document ) {
122
+ const result = [];
123
+ if(route.extension)result.push('extension');
124
+ if(hasExtensionPolicy(document,route))result.push('policies.extensions');
125
+ if(route.proxy)result.push('proxy');
126
+ if(route.signals?.length)result.push('signals');
127
+ if (route.match) result.push('conditions');
128
+ if (route.conditional) {
129
+ result.push('conditional');
130
+ for (const branch of [...route.conditional.cases, ...(route.conditional.fallback ? [route.conditional.fallback] : [])]) {
131
+ if (branch.redirect && !result.includes('redirect')) result.push('redirect');
132
+ if (branch.respond && !result.includes('respond')) result.push('respond');
133
+ }
134
+ }
135
+ for (const name of ['redirect', 'respond', 'page', 'static', 'download', 'function'] ) if (route[name]) result.push(name);
136
+ if (route.middleware?.length) result.push('middleware');
137
+ if (route.parameters?.length) result.push('parameters');
138
+ // Defaults are still semantics required by every route.
139
+ result.push('methods', 'enabled');
140
+ if (route.expires) result.push('expires');
141
+ if (route.request?.body) result.push('request.body');
142
+ if (Object.keys(route.response?.headers ?? {}).length) result.push('response.headers');
143
+ if (Object.keys(route.env ?? {}).length || Object.keys(route.secrets ?? {}).length) result.push('bindings');
144
+ const policies = effectivePolicies(document, route);
145
+ for (const name of policyNames) if (policies[name]) result.push(`policies.${name}`);
146
+ return result;
147
+ }
148
+
149
+ function analyze(document , routes , requestedTarget , registrations ) {
150
+ const target = normalizeCapabilityTarget(requestedTarget);
151
+ const extensions = registrations ? new Map(registrations.map(registration => [registration.name, registration])) : undefined;
152
+ const requirements = [];
153
+ if (Object.keys(document.extensions??{}).length) requirements.push({path:'(project)',capability:'extension',...decision('extension',target,undefined,Object.keys(document.extensions??{}),extensions)});
154
+ for (const [path, route] of routes) {
155
+ const policies = effectivePolicies(document, route);
156
+ for (const capability of routeCapabilities(route, document)) {
157
+ const extensionNames = capability==='extension' ? (route.extension?[route.extension]:[])
158
+ : capability==='policies.extensions' ? Object.keys(effectiveExtensionPolicies(document,route)) : undefined;
159
+ requirements.push({ path, capability, ...decision(capability, target, policies, extensionNames, extensions) });
160
+ }
161
+ }
162
+ const issues = requirements.filter(item => item.support === 'refused' || item.support === 'unknown' || item.support === 'conditional');
163
+ return { target, deployment: deployment(target), compatible: issues.length === 0, requirements, issues };
164
+ }
165
+
166
+ /**
167
+ * Preflight only: call after schema validation/site expansion, before resolving bindings.
168
+ * `registrations` is the resolved operator extension set, when known (the same
169
+ * shape --host-file loads for `inspectExtensions`); without it, `extension`/
170
+ * `policies.extensions` requirements report `conditional`/`unknown` rather
171
+ * than a blanket `native` that ignores the specific extension's own targets.
172
+ */
173
+ export function analyzeProjectCapabilities(loaded , target , registrations ) {
174
+ return analyze(loaded.document, Object.entries(loaded.routes), target, registrations);
175
+ }
176
+
177
+ /** Analyze normalized route semantics without exporting validators, resources or resolved secrets. */
178
+ export function analyzeCompiledCapabilities(document , compiled , target , registrations ) {
179
+ function* entries() {
180
+ for (const route of compiled.exact.values()) yield [route.pattern, route];
181
+ for (const bucket of compiled.byLength.values()) for (const route of bucket) yield [route.pattern, route];
182
+ for (const route of compiled.mounts) yield [route.pattern, route];
183
+ }
184
+ return analyze(document, entries(), target, registrations);
185
+ }
186
+
187
+ export function assertTargetCompatibility(report ) {
188
+ if (report.compatible) return;
189
+ throw new ConfigError(`Cannot activate/build project for ${report.target}:\n` + report.issues.map(issue =>
190
+ `${issue.path}\n capability: ${issue.capability}\n unsupported by target: ${report.target}\n ${issue.reason}`).join('\n'));
191
+ }
192
+
193
+ export function formatCapabilities(catalog ) {
194
+ return ['Capability'.padEnd(24) + catalog.targets.map(({ target }) => target.padEnd(16)).join(''),
195
+ ...catalog.capabilities.map(row => row.capability.padEnd(24) + catalog.targets.map(({ target }) => (row.targets[target]?.support ?? 'unknown').padEnd(16)).join('')),
196
+ '', 'Provider deployments: unverified. native/compiled describe local implementation tests.',
197
+ 'conditional requires configuration analysis; delegated relies on the provider (unverified).',
198
+ 'See docs/CAPABILITIES.md for transport limits and programmatic project analysis.', ''].join('\n');
199
+ }
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+
210
+
211
+ const policyDetail = (name , summary , constraints ) => ({ kind: 'policy', summary, schema: [`policies.${name}`], constraints:
212
+ ['Route policies merge over project/profile policies; `false` removes one', ...constraints], grants: [] });
213
+ /** Static facts about each catalog entry. Per-target support stays in `decision`; recipe and cookbook usage is scanned by `getCapability`. */
214
+ export const capabilityDetails = {
215
+ extension: { kind: 'project', summary: 'Route delegated to a versioned logical extension declared under top-level `extensions`.', schema: ['extension', 'extensions'],
216
+ constraints: ['Extension names match ^[a-z][a-z0-9-]{0,63}$; at most 16 project extensions', 'Each declaration needs `version: "1"` and a `config` object', 'Never loads project code; the registry is operator host code'],
217
+ grants: ['Operator extension registry loaded with --host-file, pinned to the exact project revision'] },
218
+ 'policies.extensions': { kind: 'policy', summary: 'Extension-provided policy attached through `policies.extensions`.', schema: ['policies.extensions'],
219
+ constraints: ['Names must reference declared top-level `extensions`', 'Refused on Cloudflare: no Worker artifact lowering'], grants: ['Operator extension registry loaded with --host-file'] },
220
+ proxy: { kind: 'egress', summary: 'Bounded HTTPS proxy to one declared upstream URL.', schema: ['proxy'],
221
+ constraints: ['`url` at most 8192 characters; headers at most 32, string values at most 4096 characters or `{secret}` references', 'Forwarded query keys and request headers are explicit allowlists (at most 32 each)', 'Self-hosted only: bounded egress requires the Node lifecycle'],
222
+ grants: ['External operator policy (--policy) with a revision-pinned exact-origin grant for the route', 'Secret bindings referenced by headers must be granted separately'] },
223
+ signals: { kind: 'egress', summary: 'Fire-and-forget HTTPS notifications after a route reply.', schema: ['signals'],
224
+ constraints: ['1 to 8 signals per route; each `url` at most 8192 characters with at most 32 headers', 'Self-hosted only: bounded egress requires the Node lifecycle'],
225
+ grants: ['External operator policy (--policy) with a revision-pinned exact-origin grant per signal destination'] },
226
+ conditional: { kind: 'routing', summary: 'Disjoint request-condition cases selecting a redirect or respond reply.', schema: ['conditional'],
227
+ constraints: ['1 to 16 cases plus an optional fallback; cases must be disjoint', 'Each case replies with `redirect` or `respond`, never a handler needing assets or code', 'Refused on Cloudflare until artifact lowering exists'], grants: [] },
228
+ conditions: { kind: 'routing', summary: 'Route-level `match` on query, headers or cookies.', schema: ['match'],
229
+ constraints: ['At least one of query, headers, cookies; at most 16 entries each with values up to 1024 characters', 'Refused on Cloudflare until artifact lowering exists'], grants: [] },
230
+ redirect: { kind: 'handler', summary: 'HTTP redirect to a URL template with optional query passing or mapping.', schema: ['redirect'],
231
+ constraints: ['`url` required, at most 8192 characters', 'status one of 301, 302, 303, 307, 308', '`query.pass` is `false` or an explicit list; `query.map` maps from path, query or header inputs'], grants: [] },
232
+ respond: { kind: 'handler', summary: 'Static text or JSON reply with a status code.', schema: ['respond'],
233
+ constraints: ['status 200 to 599', '`text` at most 1 MiB; `text` and `json` are mutually exclusive'], grants: [] },
234
+ page: { kind: 'handler', summary: 'Serve one HTML file from the project.', schema: ['page'],
235
+ constraints: ['`file` required, project-relative, 1 to 1024 characters', 'Fixed `cacheControl` choices; `contentType` must be a media type', 'Refused on Cloudflare: assets need a static-asset binding'], grants: [] },
236
+ static: { kind: 'handler', summary: 'Serve a project directory as a static mount.', schema: ['static'],
237
+ constraints: ['`directory` required, project-relative, 1 to 1024 characters; `index` must be a .html name', 'Refused on Cloudflare: assets need a static-asset binding'], grants: [] },
238
+ download: { kind: 'handler', summary: 'Serve a project file as an attachment.', schema: ['download'],
239
+ constraints: ['`file` required, project-relative, 1 to 1024 characters; `filename` at most 255 characters', 'Refused on Cloudflare: assets need a static-asset binding'], grants: [] },
240
+ function: { kind: 'handler', summary: 'Project function producing the reply; trusted and unsandboxed by default, sandboxed opt-in.', schema: ['function'],
241
+ constraints: ['`source` at most 1024 characters, project-relative; `export` defaults to the default export', '`args` are literals, `{from: path|query|header}` inputs or `{env}` references', 'Self-hosted only', 'Trusted by default (`sandbox` false or absent): runs in-process with full Node network/filesystem access, like any other project code', 'Route-level `sandbox: true` runs the whole `function`/`middleware` chain isolated instead: worker threads, the WASM engine, no network or filesystem in the guest (docs/FUNCTION-SECURITY.md)'], grants: [] },
242
+ middleware: { kind: 'middleware', summary: 'Modules run before the handler; trusted and unsandboxed by default, sandboxed opt-in.', schema: ['middleware'],
243
+ constraints: ['At most 16 entries, each with a project-relative `source` and optional `export`', 'Self-hosted only', 'Same trusted-by-default / `sandbox: true` opt-in as `function`, applied uniformly to the whole route'], grants: [] },
244
+ parameters: { kind: 'request', summary: 'Validated path, query and header inputs.', schema: ['parameters'],
245
+ constraints: ['Names match ^[A-Za-z_][A-Za-z0-9_-]*$ and `in` is path, query or header', 'Schema types: string, integer, number, boolean, array; length bounds up to 8192'], grants: [] },
246
+ methods: { kind: 'routing', summary: 'Allowed HTTP methods; defaults to GET and HEAD.', schema: ['methods'],
247
+ constraints: ['Unique subset of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS with at least one entry'], grants: [] },
248
+ enabled: { kind: 'routing', summary: 'Route on/off switch; disabled routes are still validated.', schema: ['enabled'], constraints: ['Boolean; defaults to true'], grants: [] },
249
+ expires: { kind: 'routing', summary: 'Timestamp after which the route stops matching.', schema: ['expires'], constraints: ['UTC timestamp YYYY-MM-DDTHH:MM:SS[.mmm]Z; expired routes are still validated'], grants: [] },
250
+ 'request.body': { kind: 'request', summary: 'Request body admission limits and format.', schema: ['request.body'],
251
+ constraints: ['`maxBytes` 0 to 1048576; up to 16 lowercase `contentTypes`', '`format` text or json'], grants: [] },
252
+ 'response.headers': { kind: 'request', summary: 'Static response headers added to the reply.', schema: ['response.headers'],
253
+ constraints: ['At most 64 headers; values up to 4096 characters or lists of at most 16', 'Cloudflare coalesces duplicate headers'], grants: [] },
254
+ bindings: { kind: 'binding', summary: 'Route `env` literals/references and `secrets` references.', schema: ['env', 'secrets'],
255
+ constraints: ['Names match ^[A-Za-z_][A-Za-z0-9_]*$', '`env` entries are `{value}` literals or `{env}` references; `secrets` entries are `{secret}` references', 'Refused on Cloudflare: bindings would be baked into the artifact'],
256
+ grants: ['External operator policy (--policy) granting each referenced env/secret name; values never enter the project'] },
257
+ 'policies.agents': policyDetail('agents', 'Agent allow/deny rules by bundled list name.', ['List names come from the bundled agent lists']),
258
+ 'policies.security': policyDetail('security', 'Security response headers.', ['Fixed header set with validated values']),
259
+ 'policies.cache': policyDetail('cache', 'Host cache strategy for route replies.', ['Refused on Cloudflare: no cache enforcement in the artifact']),
260
+ 'policies.compression': policyDetail('compression', 'Response compression.', ['Delegated on AWS, Vercel and Cloudflare; exact settings are unverified']),
261
+ 'policies.throttle': policyDetail('throttle', 'Per-instance request quota per window.', ['`quota` and `window` are required', 'Only `partition: route` is implemented on serverless targets; `client` is refused there; counters are per instance']),
262
+ };
@@ -0,0 +1,71 @@
1
+ import {readdirSync,readFileSync,statSync} from 'node:fs';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {parse} from 'yaml';
4
+ import {capabilityDetails,capabilityNames,capabilityTargets,formatCapabilities,getCapabilities,routeCapabilities} from './capabilities.js';
5
+
6
+ import {ConfigError} from './errors.js';
7
+ import {getSchemaFragment} from './schema-query.js';
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+ const root=(...parts )=>fileURLToPath(new URL('../'+parts.join('/'),import.meta.url));
21
+ const object=(value ) =>value!==null&&typeof value==='object'&&!Array.isArray(value);
22
+ function yamlFiles(directory ) {
23
+ const out =[];
24
+ for(const entry of readdirSync(directory,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))) {
25
+ if(entry.name.startsWith('.')||entry.name==='node_modules')continue;
26
+ const file=directory+'/'+entry.name;
27
+ if(entry.isDirectory())out.push(...yamlFiles(file));else if(/\.ya?ml$/.test(entry.name))out.push(file);
28
+ }
29
+ return out;
30
+ }
31
+ /** Bundled examples are fixed package content; they are read as data and never validated or activated here. */
32
+ function usage(base ,files ,name ) {
33
+ const result =[];
34
+ for(const file of files) {
35
+ let document ;try{document=parse(readFileSync(file,'utf8'));}catch{continue;}
36
+ if(!object(document))continue;
37
+ const routes =[];
38
+ if(name==='extension'&&object(document.extensions)&&Object.keys(document.extensions).length)routes.push('(project)');
39
+ for(const [path,route] of Object.entries(object(document.routes)?document.routes:{})) {
40
+ if(!object(route))continue;
41
+ try{if(routeCapabilities(route ,document ).includes(name))routes.push(path);}catch{/* unparsable example route: not a use */}
42
+ }
43
+ if(routes.length)result.push({file:file.slice(base.length+1),routes});
44
+ }
45
+ return result;
46
+ }
47
+ export function capabilityNameList() {return capabilityNames;}
48
+ /** One catalog entry with its schema fragments and bundled usage. No project, credentials or network are read. */
49
+ export function getCapability(name ) {
50
+ if(typeof name!=='string'||!(capabilityNames ).includes(name))throw new ConfigError('Unknown capability; valid names: '+capabilityNames.join(', '));
51
+ const capability=name ,detail=capabilityDetails[capability];
52
+ const catalog=getCapabilities();
53
+ const targets=Object.fromEntries(capabilityTargets.map(target=>[target,catalog.capabilities.find(row=>row.capability===capability) .targets[target] ])) ;
54
+ const refused=capabilityTargets.flatMap(target=>targets[target].support==='refused'?[{target,reason:targets[target].reason}]:[]);
55
+ const recipesRoot=root('recipes'),cookbookRoot=root('examples','cookbook');
56
+ const recipes=usage(recipesRoot,readdirSync(recipesRoot,{withFileTypes:true}).filter(entry=>entry.isDirectory()).map(entry=>recipesRoot+'/'+entry.name+'/urlcode.yaml').filter(file=>{try{return statSync(file).isFile();}catch{return false;}}),capability);
57
+ const cookbook=usage(cookbookRoot,yamlFiles(cookbookRoot),capability);
58
+ return {format:1,name:capability,...detail,schemaFragments:detail.schema.map(getSchemaFragment),targets,refused,recipes,cookbook};
59
+ }
60
+ export function formatCapability(entry ) {
61
+ const list=(items )=>items.length?items.map(item=>' - '+item):[' (none)'];
62
+ const usageLines=(items )=>items.length?items.map(item=>` - ${item.file}: ${item.routes.join(', ')}`):[' (none)'];
63
+ return [`${entry.name} (${entry.kind})`,entry.summary,'','Schema: '+entry.schema.join(', ')+' (urlcode schema <path>)',
64
+ ...entry.schemaFragments.map(fragment=>` ${fragment.pointer}\n`+JSON.stringify(fragment.schema,null,2).split('\n').map(line=>' '+line).join('\n')),
65
+ '','Constraints:',...list(entry.constraints),'','Required grants:',...list(entry.grants),'','Targets:',
66
+ ...capabilityTargets.map(target=>` ${target.padEnd(12)}${entry.targets[target].support.padEnd(12)}${entry.targets[target].reason}`),
67
+ '','Unsupported:',...(entry.refused.length?entry.refused.map(item=>` - ${item.target}: ${item.reason}`):[' (none)']),
68
+ '','Recipes:',...usageLines(entry.recipes),'','Cookbook routes:',...usageLines(entry.cookbook),
69
+ '','Provider deployments: unverified; see docs/CAPABILITIES.md.',''].join('\n');
70
+ }
71
+ export {formatCapabilities};
@@ -0,0 +1,105 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {fileURLToPath} from 'node:url';
3
+ import Ajv from 'ajv/dist/2020.js';
4
+ import {loadDocument,parseYaml} from './config.js';
5
+ import {applySite} from './site.js';
6
+ import {analyzeProjectCapabilities,capabilityTargets} from './capabilities.js';
7
+
8
+ import {readAuthoringFile,authoringPath} from './authoring-files.js';
9
+ import {assert} from './errors.js';
10
+
11
+ /**
12
+ * The metadata shape shared by recipes/NAME/recipe.yaml and examples/NAME/example.yaml
13
+ * (schemas/recipe.schema.json). The catalog entries are fixed package data: names
14
+ * come from a list in code, never from a directory scan, a registry or an argument.
15
+ */
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ /** The fields the capability preflight determines; `npm run check` refuses hand-written values that differ. */
26
+
27
+
28
+
29
+ const schemaFile=fileURLToPath(new URL('../schemas/recipe.schema.json',import.meta.url));
30
+
31
+ let validator ;
32
+ async function validate() {
33
+ if(!validator){
34
+ const schema =JSON.parse(await readFile(schemaFile,'utf8'));
35
+ validator=new Ajv.default({allErrors:false,strict:true,strictRequired:false}).compile(schema ) ;
36
+ }
37
+ return validator;
38
+ }
39
+ export const metadataFiles={recipe:'recipe.yaml',example:'example.yaml'} ;
40
+ /** Reads and schema-validates one metadata file; the id must equal the directory name, and file lists stay authoring-safe paths. */
41
+ export async function readMetadata(root ,id ,file ) {
42
+ const text=(await readAuthoringFile(root,file,65536)).toString('utf8');
43
+ const value =parseYaml(text),valid=await validate();
44
+ const first=valid(value)?undefined:valid.errors?.[0];
45
+ assert(!first,`${id}/${file} does not match schemas/recipe.schema.json: ${first?.instancePath||'/'} ${first?.message??''}`);
46
+ const metadata=value ;
47
+ assert(metadata.id===id,`${id}/${file} declares id ${metadata.id}`);
48
+ for(const path of metadata.files){try{authoringPath(path);}catch{assert(false,`${id}/${file} lists ${path}, which is not an authoring-safe path`);}}
49
+ return metadata;
50
+ }
51
+ const strength ={compatible:0,conditional:1,unknown:2,refused:3};
52
+ /** Preflight only: loads the project, expands site routes and asks the capability analysis for every target. No bindings, code or activation. */
53
+ export async function deriveMetadata(root ) {
54
+ const loaded=await loadDocument(root);await applySite(loaded,{});
55
+ const capabilities=new Set ();
56
+ const targets={} ;
57
+ for(const target of capabilityTargets){
58
+ const report=analyzeProjectCapabilities(loaded,target);
59
+ let verdict ='compatible';
60
+ for(const issue of report.issues){const support=issue.support ;if(strength[support]>strength[verdict])verdict=support;}
61
+ targets[target]=verdict;
62
+ if(target==='self-hosted')for(const requirement of report.requirements)capabilities.add(requirement.capability);
63
+ }
64
+ return {capabilities:[...capabilities].sort(),targets,routes:Object.keys(loaded.routes).length};
65
+ }
66
+ /** Reports where hand-written derived fields disagree with the preflight; empty means consistent. */
67
+ export function derivedDifferences(metadata ,derived ) {
68
+ const problems =[];
69
+ const same=(a ,b )=>JSON.stringify(a)===JSON.stringify(b);
70
+ if(!same(metadata.capabilities??[],derived.capabilities))problems.push(`capabilities should be [${derived.capabilities.join(', ')}]`);
71
+ for(const target of capabilityTargets)if(metadata.targets?.[target]!==derived.targets[target])problems.push(`targets.${target} should be ${derived.targets[target]}`);
72
+ if(metadata.routes!==derived.routes)problems.push(`routes should be ${derived.routes}`);
73
+ return problems;
74
+ }
75
+ const stopWords=new Set(['a','an','and','the','with','for','to','of','in','on','that','my','i','want','need']);
76
+ export function searchTerms(text ) {
77
+ assert(typeof text==='string' && text.length<=256,'Search text must be at most 256 characters');
78
+ const words=text.toLowerCase().split(/[^a-z0-9.-]+/).filter(Boolean);
79
+ const terms=[...new Set(words.filter(word=>!stopWords.has(word)||words.length===1))];
80
+ assert(terms.length>0 && terms.length<=16,'Provide one to sixteen search words');
81
+ return terms;
82
+ }
83
+ /**
84
+ * Local search over id, description, tags and capabilities: every term must match
85
+ * somewhere; the score prefers whole-tag and id hits over description substrings.
86
+ * Ties keep catalog order, so the smallest entry wins when a caller sorts by size first.
87
+ */
88
+ export function searchMetadata (entries ,text ,extraText =()=>[]) {
89
+ const terms=searchTerms(text),hits =[];
90
+ for(const entry of entries){
91
+ const tags=entry.tags.map(tag=>tag.toLowerCase()),capabilities=(entry.capabilities??[]).map(name=>name.toLowerCase());
92
+ const id=entry.id.toLowerCase(),description=entry.description.toLowerCase(),extra=extraText(entry).map(value=>value.toLowerCase());
93
+ let score=0;const matched =[];
94
+ for(const term of terms){
95
+ let best=0;
96
+ if(id===term || tags.includes(term) || capabilities.includes(term))best=4;
97
+ else if(id.includes(term) || tags.some(tag=>tag.includes(term)) || capabilities.some(name=>name.includes(term)))best=2;
98
+ else if(description.includes(term) || extra.some(value=>value.includes(term)))best=1;
99
+ if(best===0){score=0;break;}
100
+ score+=best;matched.push(term);
101
+ }
102
+ if(score>0)hits.push({entry,score,matched});
103
+ }
104
+ return hits.sort((a,b)=>b.score-a.score);
105
+ }