@intentius/chant-lexicon-fountain 0.33.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 (171) hide show
  1. package/README.md +63 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +16 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +3 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate-lexicon.d.ts +24 -0
  9. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  10. package/dist/codegen/generate-typescript.d.ts +11 -0
  11. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +15 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/naming.d.ts +14 -0
  15. package/dist/codegen/naming.d.ts.map +1 -0
  16. package/dist/codegen/package.d.ts +17 -0
  17. package/dist/codegen/package.d.ts.map +1 -0
  18. package/dist/composites/concierge-stack.d.ts +44 -0
  19. package/dist/composites/concierge-stack.d.ts.map +1 -0
  20. package/dist/coverage.d.ts +56 -0
  21. package/dist/coverage.d.ts.map +1 -0
  22. package/dist/describe-resources.d.ts +45 -0
  23. package/dist/describe-resources.d.ts.map +1 -0
  24. package/dist/detect.d.ts +10 -0
  25. package/dist/detect.d.ts.map +1 -0
  26. package/dist/entity-props.d.ts +11 -0
  27. package/dist/entity-props.d.ts.map +1 -0
  28. package/dist/export-resources.d.ts +29 -0
  29. package/dist/export-resources.d.ts.map +1 -0
  30. package/dist/generated/index.d.ts +5 -0
  31. package/dist/generated/index.d.ts.map +1 -0
  32. package/dist/generated/runtime.d.ts +5 -0
  33. package/dist/generated/runtime.d.ts.map +1 -0
  34. package/dist/import/generator.d.ts +15 -0
  35. package/dist/import/generator.d.ts.map +1 -0
  36. package/dist/import/parser.d.ts +19 -0
  37. package/dist/import/parser.d.ts.map +1 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/init-templates.d.ts +13 -0
  41. package/dist/init-templates.d.ts.map +1 -0
  42. package/dist/integrity.json +21 -0
  43. package/dist/lint/audit-catalog.d.ts +15 -0
  44. package/dist/lint/audit-catalog.d.ts.map +1 -0
  45. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts +11 -0
  46. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts.map +1 -0
  47. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts +11 -0
  48. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts.map +1 -0
  49. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts +3 -0
  50. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts.map +1 -0
  51. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts +3 -0
  52. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts.map +1 -0
  53. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts +3 -0
  54. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts.map +1 -0
  55. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts +3 -0
  56. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts.map +1 -0
  57. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts +3 -0
  58. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts.map +1 -0
  59. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts +11 -0
  60. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts.map +1 -0
  61. package/dist/lint/post-synth/index.d.ts +3 -0
  62. package/dist/lint/post-synth/index.d.ts.map +1 -0
  63. package/dist/lint/rules/ftn001-no-secret-literals.d.ts +3 -0
  64. package/dist/lint/rules/ftn001-no-secret-literals.d.ts.map +1 -0
  65. package/dist/lint/rules/index.d.ts +5 -0
  66. package/dist/lint/rules/index.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +7 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +6 -0
  72. package/dist/meta.json +69 -0
  73. package/dist/op/activities/fountain-apply.d.ts +76 -0
  74. package/dist/op/activities/fountain-apply.d.ts.map +1 -0
  75. package/dist/op/activities/fountain-run.d.ts +36 -0
  76. package/dist/op/activities/fountain-run.d.ts.map +1 -0
  77. package/dist/op/activities/index.d.ts +11 -0
  78. package/dist/op/activities/index.d.ts.map +1 -0
  79. package/dist/package-cli.d.ts +3 -0
  80. package/dist/package-cli.d.ts.map +1 -0
  81. package/dist/plugin.d.ts +8 -0
  82. package/dist/plugin.d.ts.map +1 -0
  83. package/dist/reference-catalog.d.ts +16 -0
  84. package/dist/reference-catalog.d.ts.map +1 -0
  85. package/dist/rules/ftn001-no-secret-literals.ts +71 -0
  86. package/dist/rules/ftn010-networking-explicit.ts +37 -0
  87. package/dist/rules/ftn011-no-unrestricted-networking.ts +36 -0
  88. package/dist/rules/ftn012-no-cloud-credential-env.ts +58 -0
  89. package/dist/rules/ftn013-substitution-resolvable.ts +93 -0
  90. package/dist/rules/ftn014-vault-shadowing.ts +61 -0
  91. package/dist/rules/ftn015-mcp-secret-env-substitution.ts +49 -0
  92. package/dist/rules/ftn016-runtime-model-valid.ts +50 -0
  93. package/dist/rules/ftn017-unique-resource-names.ts +43 -0
  94. package/dist/serializer.d.ts +3 -0
  95. package/dist/serializer.d.ts.map +1 -0
  96. package/dist/skills/chant-fountain-locked-sandboxes.md +34 -0
  97. package/dist/skills/chant-fountain-secrets.md +29 -0
  98. package/dist/skills/chant-fountain.md +45 -0
  99. package/dist/spec/fetch.d.ts +28 -0
  100. package/dist/spec/fetch.d.ts.map +1 -0
  101. package/dist/spec/parse.d.ts +58 -0
  102. package/dist/spec/parse.d.ts.map +1 -0
  103. package/dist/types/index.d.ts +60 -0
  104. package/dist/validate-cli.d.ts +3 -0
  105. package/dist/validate-cli.d.ts.map +1 -0
  106. package/dist/validate.d.ts +15 -0
  107. package/dist/validate.d.ts.map +1 -0
  108. package/package.json +61 -0
  109. package/src/codegen/docs-cli.ts +7 -0
  110. package/src/codegen/docs.ts +278 -0
  111. package/src/codegen/generate-cli.ts +8 -0
  112. package/src/codegen/generate-lexicon.ts +58 -0
  113. package/src/codegen/generate-typescript.ts +69 -0
  114. package/src/codegen/generate.ts +109 -0
  115. package/src/codegen/naming.ts +45 -0
  116. package/src/codegen/package.ts +32 -0
  117. package/src/composites/composites.test.ts +35 -0
  118. package/src/composites/concierge-stack.ts +71 -0
  119. package/src/coverage.test.ts +63 -0
  120. package/src/coverage.ts +178 -0
  121. package/src/describe-resources.test.ts +203 -0
  122. package/src/describe-resources.ts +192 -0
  123. package/src/detect.ts +37 -0
  124. package/src/entity-props.ts +22 -0
  125. package/src/export-resources.ts +103 -0
  126. package/src/generated/.gitkeep +0 -0
  127. package/src/generated/index.d.ts +60 -0
  128. package/src/generated/index.ts +10 -0
  129. package/src/generated/lexicon-fountain.json +69 -0
  130. package/src/generated/runtime.ts +4 -0
  131. package/src/import/generator.ts +112 -0
  132. package/src/import/parser.ts +83 -0
  133. package/src/import/roundtrip.test.ts +113 -0
  134. package/src/index.ts +17 -0
  135. package/src/init-templates.ts +60 -0
  136. package/src/lint/audit-catalog.ts +107 -0
  137. package/src/lint/post-synth/ftn010-networking-explicit.ts +37 -0
  138. package/src/lint/post-synth/ftn011-no-unrestricted-networking.ts +36 -0
  139. package/src/lint/post-synth/ftn012-no-cloud-credential-env.ts +58 -0
  140. package/src/lint/post-synth/ftn013-substitution-resolvable.ts +93 -0
  141. package/src/lint/post-synth/ftn014-vault-shadowing.ts +61 -0
  142. package/src/lint/post-synth/ftn015-mcp-secret-env-substitution.ts +49 -0
  143. package/src/lint/post-synth/ftn016-runtime-model-valid.ts +50 -0
  144. package/src/lint/post-synth/ftn017-unique-resource-names.ts +43 -0
  145. package/src/lint/post-synth/index.ts +21 -0
  146. package/src/lint/post-synth/post-synth.test.ts +133 -0
  147. package/src/lint/rules/ftn001-no-secret-literals.ts +71 -0
  148. package/src/lint/rules/index.ts +7 -0
  149. package/src/lint/rules/rules.test.ts +126 -0
  150. package/src/lsp/completions.test.ts +51 -0
  151. package/src/lsp/completions.ts +21 -0
  152. package/src/lsp/hover.test.ts +47 -0
  153. package/src/lsp/hover.ts +70 -0
  154. package/src/op/activities/fountain-apply.test.ts +208 -0
  155. package/src/op/activities/fountain-apply.ts +249 -0
  156. package/src/op/activities/fountain-run.ts +95 -0
  157. package/src/op/activities/index.ts +28 -0
  158. package/src/package-cli.ts +26 -0
  159. package/src/plugin.test.ts +73 -0
  160. package/src/plugin.ts +170 -0
  161. package/src/reference-catalog.ts +27 -0
  162. package/src/serializer.test.ts +109 -0
  163. package/src/serializer.ts +143 -0
  164. package/src/skills/chant-fountain-locked-sandboxes.md +34 -0
  165. package/src/skills/chant-fountain-secrets.md +29 -0
  166. package/src/skills/chant-fountain.md +45 -0
  167. package/src/spec/fetch.ts +53 -0
  168. package/src/spec/fountain-openapi.snapshot.json +1 -0
  169. package/src/spec/parse.ts +309 -0
  170. package/src/validate-cli.ts +13 -0
  171. package/src/validate.ts +34 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Fetch the fountain OpenAPI spec.
3
+ *
4
+ * Fountain serves its OpenAPI 3.1 spec from the running app at
5
+ * /api/openapi.json (generated from code via OpenApiSpex; public, no auth).
6
+ * This is an unversioned live endpoint: it always serves the spec of
7
+ * whatever is currently deployed, with no release tag to pin — fountain
8
+ * follows the rolling-spec model (like fly). A versioned spec artifact per
9
+ * release shipped upstream as a release asset (BinaryBourbon/fountain#140);
10
+ * when the hosted endpoint is reliably current this fetcher can prefer it.
11
+ *
12
+ * Fallback: a committed snapshot (fountain-openapi.snapshot.json) keeps
13
+ * generation hermetic when the live endpoint is unreachable — CI runs,
14
+ * offline dev, or the current state where the hosted instance predates the
15
+ * endpoint. The snapshot is refreshed alongside surface.snapshot.json when
16
+ * upstream moves (regenerate from a fountain checkout via
17
+ * `mix openapi.spec.json --spec FountainWeb.ApiSpec`).
18
+ */
19
+
20
+ import { join, dirname } from "path";
21
+ import { homedir } from "os";
22
+ import { readFileSync } from "fs";
23
+ import { fileURLToPath } from "url";
24
+ import { fetchWithCache } from "@intentius/chant/codegen/fetch";
25
+
26
+ const SCHEMA_URL = "https://founta.inevitable.fyi/api/openapi.json";
27
+
28
+ const CACHE_FILE = join(homedir(), ".chant", "fountain-openapi.json");
29
+
30
+ const SNAPSHOT_FILE = join(dirname(fileURLToPath(import.meta.url)), "fountain-openapi.snapshot.json");
31
+
32
+ /**
33
+ * Fetch the fountain OpenAPI spec with caching, falling back to the
34
+ * committed snapshot when the live endpoint is unreachable. Returns a
35
+ * single-entry map — the whole spec is one document; the parser fans it
36
+ * out into per-kind results.
37
+ */
38
+ export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
39
+ let raw: Buffer;
40
+ try {
41
+ raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
42
+ // The live host currently 404s (deploy lag) — a non-spec body must not
43
+ // silently replace the snapshot.
44
+ JSON.parse(raw.toString("utf-8"));
45
+ } catch (err) {
46
+ console.error(
47
+ `[fountain] live spec fetch failed (${err instanceof Error ? err.message.split("\n")[0] : err}) — ` +
48
+ `using committed snapshot`,
49
+ );
50
+ raw = readFileSync(SNAPSHOT_FILE);
51
+ }
52
+ return new Map([["fountain-openapi.json", raw]]);
53
+ }
@@ -0,0 +1 @@
1
+ {"components":{"responses":{},"schemas":{"SecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Secret"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"SecretListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretListResponse","x-validate":null},"ConversationCreateRequest":{"properties":{"agent_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"images":{"description":"Optional images to attach to the initial prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"prompt":{"description":"Optional first turn prompt.","type":"string","x-struct":null,"x-validate":null},"sprite_name":{"description":"Override the auto-generated sprite name.","type":"string","x-struct":null,"x-validate":null},"vault_id":{"description":"Optional vault whose secrets override the environment's baseline at sprite spawn. Must satisfy the agent's allowed_vault_ids when that allowlist is set.","format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null}},"required":["agent_id"],"title":"ConversationCreateRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationCreateRequest","x-validate":null},"Secret":{"description":"A named secret. Values are write-only — the API never returns them.","properties":{"environment_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"key":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","key","environment_id"],"title":"Secret","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Secret","x-validate":null},"VaultUpdate":{"properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null}},"title":"VaultUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultUpdate","x-validate":null},"ImageInput":{"description":"A base64-encoded image to attach to a prompt.","properties":{"data":{"description":"Base64-encoded image bytes.","type":"string","x-struct":null,"x-validate":null},"media_type":{"description":"MIME type of the image.","enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string","x-struct":null,"x-validate":null}},"required":["data","media_type"],"title":"ImageInput","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ImageInput","x-validate":null},"Repository":{"properties":{"mount_path":{"pattern":"^/","type":"string","x-struct":null,"x-validate":null},"url":{"format":"uri","pattern":"^https://","type":"string","x-struct":null,"x-validate":null}},"required":["url","mount_path"],"title":"Repository","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Repository","x-validate":null},"EnvironmentUpdate":{"properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null}},"title":"EnvironmentUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentUpdate","x-validate":null},"VaultSecret":{"description":"A named secret in a vault. Values are write-only — the API never returns them.","properties":{"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"key":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"vault_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null}},"required":["id","key","vault_id"],"title":"VaultSecret","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecret","x-validate":null},"TurnListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Turn"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"TurnListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.TurnListResponse","x-validate":null},"Sandbox":{"description":"One sprite lifespan owned by a conversation.","properties":{"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"sprite_name":{"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","starting","ready","terminated","failed"],"type":"string","x-struct":null,"x-validate":null}},"required":["id","sprite_name","status"],"title":"Sandbox","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Sandbox","x-validate":null},"Error":{"properties":{"error":{"type":"string","x-struct":null,"x-validate":null}},"required":["error"],"title":"Error","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Error","x-validate":null},"VaultSecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/VaultSecret"}},"required":["data"],"title":"VaultSecretResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretResponse","x-validate":null},"EnvironmentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Environment"}},"required":["data"],"title":"EnvironmentResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentResponse","x-validate":null},"ChangesetError":{"description":"Validation errors keyed by field, with each value an array of messages.","properties":{"errors":{"additionalProperties":{"items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null}},"required":["errors"],"title":"ChangesetError","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ChangesetError","x-validate":null},"Agent":{"description":"An AI agent definition: runtime, model, skills, MCP, env.","properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist. Vault values override the agent's environment on key collision, so this scopes who can override reviewed config.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"description":"Canonical provider/model_id (e.g. anthropic/claude-sonnet-4-6).","pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"description":"System prompt.","type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name","model","runtime"],"title":"Agent","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Agent","x-validate":null},"Conversation":{"description":"One chat with one agent inside one sandbox.","properties":{"agent_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"runtime_session_id":{"nullable":true,"type":"string","x-struct":null,"x-validate":null},"sandbox":{"nullable":true,"oneOf":[{"$ref":"#/components/schemas/Sandbox"}],"x-struct":null,"x-validate":null},"sandbox_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","running","idle","completed","failed","terminated"],"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"vault_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null}},"required":["id","runtime","status"],"title":"Conversation","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Conversation","x-validate":null},"EnvironmentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Environment"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"EnvironmentListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentListResponse","x-validate":null},"VaultSecretRequest":{"properties":{"key":{"type":"string","x-struct":null,"x-validate":null},"value":{"description":"Secret value (write-only).","type":"string","x-struct":null,"x-validate":null}},"required":["key","value"],"title":"VaultSecretRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretRequest","x-validate":null},"PromptRequest":{"properties":{"images":{"description":"Optional images to attach to this prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"prompt":{"type":"string","x-struct":null,"x-validate":null}},"required":["prompt"],"title":"PromptRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.PromptRequest","x-validate":null},"Turn":{"description":"One prompt → exit_code cycle within a conversation.","properties":{"ended_at":{"format":"date-time","nullable":true,"type":"string","x-struct":null,"x-validate":null},"exit_code":{"nullable":true,"type":"integer","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"image_count":{"description":"Number of images attached to this turn.","type":"integer","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"prompt":{"type":"string","x-struct":null,"x-validate":null},"started_at":{"format":"date-time","nullable":true,"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","running","completed","failed","interrupted"],"type":"string","x-struct":null,"x-validate":null},"turn_number":{"type":"integer","x-struct":null,"x-validate":null}},"required":["id","turn_number","prompt","status"],"title":"Turn","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Turn","x-validate":null},"VaultSecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/VaultSecret"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"VaultSecretListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretListResponse","x-validate":null},"HealthResponse":{"properties":{"status":{"example":"ok","type":"string","x-struct":null,"x-validate":null}},"required":["status"],"title":"HealthResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.HealthResponse","x-validate":null},"VaultResponse":{"properties":{"data":{"$ref":"#/components/schemas/Vault"}},"required":["data"],"title":"VaultResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultResponse","x-validate":null},"PromptResponse":{"properties":{"status":{"example":"queued","type":"string","x-struct":null,"x-validate":null}},"required":["status"],"title":"PromptResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.PromptResponse","x-validate":null},"AgentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Agent"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"AgentListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentListResponse","x-validate":null},"AgentUpdate":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"type":"string","x-struct":null,"x-validate":null}},"title":"AgentUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentUpdate","x-validate":null},"ConversationListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Conversation"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"ConversationListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationListResponse","x-validate":null},"VaultRequest":{"properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null}},"required":["name"],"title":"VaultRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultRequest","x-validate":null},"ConversationResponse":{"properties":{"data":{"$ref":"#/components/schemas/Conversation"}},"required":["data"],"title":"ConversationResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationResponse","x-validate":null},"Vault":{"description":"A free-floating bag of env-var overrides selected at conversation creation. Vault values override an environment's baseline secrets when the same key is set on both.","properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name"],"title":"Vault","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Vault","x-validate":null},"SecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/Secret"}},"required":["data"],"title":"SecretResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretResponse","x-validate":null},"SecretRequest":{"properties":{"key":{"type":"string","x-struct":null,"x-validate":null},"value":{"description":"Secret value (write-only).","type":"string","x-struct":null,"x-validate":null}},"required":["key","value"],"title":"SecretRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretRequest","x-validate":null},"AgentRequest":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"type":"string","x-struct":null,"x-validate":null}},"required":["name","model","runtime"],"title":"AgentRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentRequest","x-validate":null},"EnvironmentRequest":{"properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null}},"required":["name"],"title":"EnvironmentRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentRequest","x-validate":null},"Environment":{"description":"A reusable sandbox environment: packages, env vars, repos, networking.","properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name"],"title":"Environment","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Environment","x-validate":null},"VaultListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Vault"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"VaultListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultListResponse","x-validate":null},"AgentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Agent"}},"required":["data"],"title":"AgentResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentResponse","x-validate":null}},"securitySchemes":{"bearer":{"description":"ADMIN_TOKEN configured at boot.","scheme":"bearer","type":"http"}}},"info":{"description":"HTTP API for Agent on Demand. The same surface backs the LiveView UI\nand the `aod` CLI; if it's not here, it doesn't exist yet.\n\nAll `/api/*` endpoints require a bearer token (`ADMIN_TOKEN`).\n","title":"Agent on Demand","version":"0.1.0"},"openapi":"3.0.0","paths":{"/api/agents":{"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentListResponse"}}},"description":"Agents"}},"summary":"List agents","tags":["Agents"]},"post":{"callbacks":{},"operationId":"FountainWeb.AgentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRequest"}}},"description":"Agent attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an agent","tags":["Agents"]}},"/api/agents/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.AgentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an agent","tags":["Agents"]},"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an agent","tags":["Agents"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]}},"/api/conversations":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}},"description":"Conversations"}},"summary":"List conversations","tags":["Conversations"]},"post":{"callbacks":{},"description":"Creates a sandbox + conversation pair, starts the runtime in a fresh sprite, and (if `prompt` is supplied) sends it as turn 1. Pass `X-Fountain-Parent-Conversation-Id` header to record which conversation spawned this one. Legacy `X-AoD-Parent-Conversation-Id` is still accepted for sprites provisioned before the rename.","operationId":"FountainWeb.ConversationController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreateRequest"}}},"description":"Conversation attrs","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"402":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string","x-struct":null,"x-validate":null},"upgrade_url":{"type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null}}},"description":"Subscription required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Start a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/interrupt":{"post":{"callbacks":{},"operationId":"FountainWeb.ConversationController.interrupt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Interrupted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No turn running"}},"summary":"Interrupt the running turn","tags":["Conversations"]}},"/api/conversations/{conversation_id}/prompts":{"post":{"callbacks":{},"description":"Queues a new turn. If the ConversationServer has been GC'd (e.g. across a BEAM restart) a fresh sprite is provisioned and the runtime resumes via its session id.","operationId":"FountainWeb.ConversationController.prompt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptRequest"}}},"description":"Prompt","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Busy"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Send another prompt","tags":["Conversations"]}},"/api/conversations/{conversation_id}/stream":{"get":{"callbacks":{},"description":"Server-Sent Events stream of the conversation's log events. The `Last-Event-ID` request header resumes from a known event id; missed events are replayed before the live tail begins. Keep-alive heartbeats every 15s as `: heartbeat` comments.","operationId":"FountainWeb.ConversationController.stream","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string","x-struct":null,"x-validate":null}}},"description":"SSE stream"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Stream log events (SSE)","tags":["Conversations"]}},"/api/conversations/{conversation_id}/terminate":{"post":{"callbacks":{},"description":"Tears down the sprite and marks the conversation `terminated`. Idempotent for already-dead conversations.","operationId":"FountainWeb.ConversationController.terminate","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Terminated"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Terminate a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/turns":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.turns","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TurnListResponse"}}},"description":"Turns"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List turns in a conversation","tags":["Conversations"]}},"/api/conversations/{id}":{"delete":{"callbacks":{},"description":"Tears down the sprite if alive, then deletes the conversation row (cascades to turns and log events).","operationId":"FountainWeb.ConversationController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a conversation","tags":["Conversations"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a conversation","tags":["Conversations"]}},"/api/environments":{"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentListResponse"}}},"description":"Environments"}},"summary":"List environments","tags":["Environments"]},"post":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentRequest"}}},"description":"Environment attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an environment","tags":["Environments"]}},"/api/environments/{environment_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.SecretController.index","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretListResponse"}}},"description":"Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in an environment","tags":["Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.SecretController.create","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretRequest"}}},"description":"Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretResponse"}}},"description":"Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a secret","tags":["Secrets"]}},"/api/environments/{environment_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.SecretController.delete","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a secret by key","tags":["Secrets"]}},"/api/environments/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an environment","tags":["Environments"]},"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an environment","tags":["Environments"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]}},"/api/vaults":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultListResponse"}}},"description":"Vaults"}},"summary":"List vaults","tags":["Vaults"]},"post":{"callbacks":{},"operationId":"FountainWeb.VaultController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultRequest"}}},"description":"Vault attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create a vault","tags":["Vaults"]}},"/api/vaults/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault","tags":["Vaults"]},"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a vault","tags":["Vaults"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]}},"/api/vaults/{vault_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.index","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretListResponse"}}},"description":"Vault Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in a vault","tags":["Vault Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.VaultSecretController.create","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretRequest"}}},"description":"Vault Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretResponse"}}},"description":"Vault Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a vault secret","tags":["Vault Secrets"]}},"/api/vaults/{vault_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.delete","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault secret by key","tags":["Vault Secrets"]}},"/health":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns `{\"status\": \"ok\"}` if the app is up.","operationId":"FountainWeb.HealthController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Health response"}},"security":[],"summary":"Liveness probe","tags":["Health"]}}},"security":[{"bearer":[]}],"servers":[{"url":"http://localhost:4000","variables":{}}],"tags":[]}
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Fountain OpenAPI parser.
3
+ *
4
+ * Fountain's spec (OpenApiSpex, served at /api/openapi.json) is
5
+ * request/response DTOs, so we generate a curated set of resources — the
6
+ * three kinds `fountain apply` reconciles: Environment, Vault, Agent.
7
+ * Each pairs a request schema (writable authoring surface) with a response
8
+ * schema (read-only attributes). Object schemas reachable from the request
9
+ * schemas (e.g. Repository) are emitted as standalone property-type classes.
10
+ *
11
+ * Conversations are deliberately not a resource: they are runs with a
12
+ * status lifecycle, modeled as ops, not declarables.
13
+ */
14
+
15
+ import {
16
+ extractConstraints as coreExtractConstraints,
17
+ primaryType,
18
+ isEnumDefinition,
19
+ type JsonSchemaProperty,
20
+ type PropertyConstraints,
21
+ } from "@intentius/chant/codegen/json-schema";
22
+
23
+ // ── Types ──────────────────────────────────────────────────────────
24
+
25
+ export type { PropertyConstraints };
26
+
27
+ export interface ParsedProperty {
28
+ name: string;
29
+ tsType: string;
30
+ required: boolean;
31
+ description?: string;
32
+ constraints: PropertyConstraints;
33
+ }
34
+
35
+ export interface ParsedResource {
36
+ typeName: string;
37
+ description?: string;
38
+ properties: ParsedProperty[];
39
+ attributes: Array<{ name: string; tsType: string }>;
40
+ }
41
+
42
+ export interface FountainParseResult {
43
+ resource: ParsedResource;
44
+ /** Always empty — fountain emits property types as standalone results. */
45
+ propertyTypes: Array<{ name: string; defType: string }>;
46
+ /** Always empty — enums are inlined as string-literal unions. */
47
+ enums: Array<{ name: string; values: string[] }>;
48
+ /** Whether this entity is a property type (nested config shape). */
49
+ isProperty?: boolean;
50
+ }
51
+
52
+ // ── OpenAPI types ──────────────────────────────────────────────────
53
+
54
+ interface OpenAPISchema extends JsonSchemaProperty {
55
+ allOf?: OpenAPISchema[];
56
+ additionalProperties?: boolean | OpenAPISchema;
57
+ items?: OpenAPISchema;
58
+ properties?: Record<string, OpenAPISchema>;
59
+ nullable?: boolean;
60
+ }
61
+
62
+ interface OpenAPISpec {
63
+ components?: { schemas?: Record<string, OpenAPISchema> };
64
+ }
65
+
66
+ // ── Curated manifest ───────────────────────────────────────────────
67
+
68
+ /** The single service segment for all fountain type names (apiVersion fountain.dev/v1). */
69
+ const SERVICE = "V1";
70
+
71
+ interface ResourceSpec {
72
+ typeName: string;
73
+ request: string;
74
+ response: string;
75
+ }
76
+
77
+ const RESOURCES: ResourceSpec[] = [
78
+ { typeName: `Fountain::${SERVICE}::Environment`, request: "EnvironmentRequest", response: "Environment" },
79
+ { typeName: `Fountain::${SERVICE}::Vault`, request: "VaultRequest", response: "Vault" },
80
+ { typeName: `Fountain::${SERVICE}::Agent`, request: "AgentRequest", response: "Agent" },
81
+ ];
82
+
83
+ const REF_PREFIX = "#/components/schemas/";
84
+
85
+ // ── Parser ─────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Parse the fountain OpenAPI spec into the curated resources and the
89
+ * property types reachable from their request schemas.
90
+ */
91
+ export function parseFountainOpenAPI(data: string | Buffer): FountainParseResult[] {
92
+ const spec: OpenAPISpec = JSON.parse(typeof data === "string" ? data : data.toString("utf-8"));
93
+ const schemas = spec.components?.schemas ?? {};
94
+
95
+ const emitted = collectPropertyTypes(schemas);
96
+ const resolve = (prop: OpenAPISchema | undefined): string => resolveType(prop, schemas, emitted);
97
+
98
+ const results: FountainParseResult[] = [];
99
+
100
+ for (const rspec of RESOURCES) {
101
+ const req = schemas[rspec.request];
102
+ const res = schemas[rspec.response];
103
+ const reqProps = req?.properties ?? {};
104
+ const requiredSet = new Set(req?.required ?? []);
105
+
106
+ const properties: ParsedProperty[] = [];
107
+ for (const [name, prop] of Object.entries(reqProps)) {
108
+ properties.push({
109
+ name,
110
+ tsType: resolve(prop),
111
+ required: requiredSet.has(name),
112
+ description: prop.description,
113
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
114
+ });
115
+ }
116
+
117
+ // Attributes = response props not present in the request schema.
118
+ const attributes: Array<{ name: string; tsType: string }> = [];
119
+ for (const [name, prop] of Object.entries(res?.properties ?? {})) {
120
+ if (name in reqProps) continue;
121
+ attributes.push({ name, tsType: resolve(prop) });
122
+ }
123
+
124
+ results.push({
125
+ resource: {
126
+ typeName: rspec.typeName,
127
+ description: res?.description ?? req?.description,
128
+ properties,
129
+ attributes,
130
+ },
131
+ propertyTypes: [],
132
+ enums: [],
133
+ });
134
+ }
135
+
136
+ for (const schemaName of emitted) {
137
+ const def = schemas[schemaName];
138
+ const requiredSet = new Set(def.required ?? []);
139
+ const properties: ParsedProperty[] = [];
140
+ for (const [name, prop] of Object.entries(def.properties ?? {})) {
141
+ properties.push({
142
+ name,
143
+ tsType: resolve(prop),
144
+ required: requiredSet.has(name),
145
+ description: prop.description,
146
+ constraints: coreExtractConstraints(prop as JsonSchemaProperty),
147
+ });
148
+ }
149
+ results.push({
150
+ resource: {
151
+ typeName: `Fountain::${SERVICE}::${schemaToClassName(schemaName)}`,
152
+ description: def.description,
153
+ properties,
154
+ attributes: [],
155
+ },
156
+ propertyTypes: [],
157
+ enums: [],
158
+ isProperty: true,
159
+ });
160
+ }
161
+
162
+ return results;
163
+ }
164
+
165
+ /**
166
+ * Walk the request schemas and collect the set of object schemas reachable
167
+ * through their properties (following $ref, allOf, arrays, and maps).
168
+ */
169
+ function collectPropertyTypes(schemas: Record<string, OpenAPISchema>): string[] {
170
+ const emitted = new Set<string>();
171
+ const queue: string[] = [];
172
+
173
+ const seedFrom = (node: unknown) => {
174
+ for (const ref of collectRefs(node)) {
175
+ const target = schemas[ref];
176
+ if (!target) continue;
177
+ if (isObjectSchema(target) && !emitted.has(ref)) {
178
+ emitted.add(ref);
179
+ queue.push(ref);
180
+ }
181
+ }
182
+ };
183
+
184
+ for (const spec of RESOURCES) {
185
+ seedFrom(schemas[spec.request]?.properties);
186
+ }
187
+ while (queue.length > 0) {
188
+ const name = queue.shift()!;
189
+ seedFrom(schemas[name]?.properties);
190
+ }
191
+
192
+ return [...emitted];
193
+ }
194
+
195
+ /** Collect every `#/components/schemas/X` ref name nested anywhere in a node. */
196
+ function collectRefs(node: unknown, acc: Set<string> = new Set()): Set<string> {
197
+ if (!node || typeof node !== "object") return acc;
198
+ if (Array.isArray(node)) {
199
+ for (const item of node) collectRefs(item, acc);
200
+ return acc;
201
+ }
202
+ const obj = node as Record<string, unknown>;
203
+ const ref = obj.$ref;
204
+ if (typeof ref === "string" && ref.startsWith(REF_PREFIX)) acc.add(ref.slice(REF_PREFIX.length));
205
+ for (const [key, value] of Object.entries(obj)) {
206
+ if (key === "$ref") continue;
207
+ collectRefs(value, acc);
208
+ }
209
+ return acc;
210
+ }
211
+
212
+ /** An object schema with properties (not a pure enum). */
213
+ function isObjectSchema(def: OpenAPISchema): boolean {
214
+ return !!def.properties && Object.keys(def.properties).length > 0 && !isEnumDefinition(def);
215
+ }
216
+
217
+ // ── Type resolution ────────────────────────────────────────────────
218
+
219
+ function resolveType(
220
+ prop: OpenAPISchema | undefined,
221
+ schemas: Record<string, OpenAPISchema>,
222
+ emitted: string[],
223
+ ): string {
224
+ if (!prop) return "any";
225
+
226
+ if (prop.$ref) return resolveRefType(prop.$ref, schemas, emitted);
227
+
228
+ if (prop.allOf && prop.allOf.length > 0) {
229
+ const withRef = prop.allOf.find((s) => s.$ref);
230
+ if (withRef?.$ref) return resolveRefType(withRef.$ref, schemas, emitted);
231
+ }
232
+
233
+ if (prop.enum && prop.enum.length > 0) {
234
+ return [...prop.enum].sort().map((v) => JSON.stringify(v)).join(" | ");
235
+ }
236
+
237
+ const pt = primaryType(prop.type);
238
+ switch (pt) {
239
+ case "string":
240
+ return "string";
241
+ case "integer":
242
+ case "number":
243
+ return "number";
244
+ case "boolean":
245
+ return "boolean";
246
+ case "array": {
247
+ if (prop.items) {
248
+ const itemType = resolveType(prop.items, schemas, emitted);
249
+ return itemType.includes(" | ") ? `(${itemType})[]` : `${itemType}[]`;
250
+ }
251
+ return "any[]";
252
+ }
253
+ case "object":
254
+ if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
255
+ return `Record<string, ${resolveType(prop.additionalProperties, schemas, emitted)}>`;
256
+ }
257
+ return "Record<string, any>";
258
+ default:
259
+ return "any";
260
+ }
261
+ }
262
+
263
+ /** Resolve a `#/components/schemas/X` ref to a TypeScript type. */
264
+ function resolveRefType(ref: string, schemas: Record<string, OpenAPISchema>, emitted: string[]): string {
265
+ if (!ref.startsWith(REF_PREFIX)) return "any";
266
+ const name = ref.slice(REF_PREFIX.length);
267
+ const def = schemas[name];
268
+ if (!def) return "any";
269
+
270
+ if (emitted.includes(name)) return schemaToClassName(name);
271
+
272
+ if (isEnumDefinition(def)) {
273
+ return [...(def.enum ?? [])].sort().map((v) => JSON.stringify(v)).join(" | ");
274
+ }
275
+
276
+ if (def.properties) return "Record<string, any>";
277
+
278
+ const pt = primaryType(def.type);
279
+ switch (pt) {
280
+ case "string":
281
+ return "string";
282
+ case "integer":
283
+ case "number":
284
+ return "number";
285
+ case "boolean":
286
+ return "boolean";
287
+ default:
288
+ return "any";
289
+ }
290
+ }
291
+
292
+ // ── Naming helpers ─────────────────────────────────────────────────
293
+
294
+ /** Convert a schema name into a PascalCase class-name segment. */
295
+ export function schemaToClassName(schemaName: string): string {
296
+ return schemaName.charAt(0).toUpperCase() + schemaName.slice(1);
297
+ }
298
+
299
+ /** Extract short name: "Fountain::V1::Agent" → "Agent". */
300
+ export function fountainShortName(typeName: string): string {
301
+ const parts = typeName.split("::");
302
+ return parts[parts.length - 1];
303
+ }
304
+
305
+ /** Extract service name: "Fountain::V1::Agent" → "V1". */
306
+ export function fountainServiceName(typeName: string): string {
307
+ const parts = typeName.split("::");
308
+ return parts.length >= 2 ? parts[1] : SERVICE;
309
+ }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate";
3
+ import { printValidationResult } from "@intentius/chant/codegen/validate";
4
+
5
+ // `validate` takes an optional { basePath }; defaults to the lexicon root.
6
+ // printValidationResult throws on failure — the catch turns that into a
7
+ // nonzero exit so `npm run validate` (and prepack) actually gate on it.
8
+ try {
9
+ printValidationResult(await validate());
10
+ } catch (err) {
11
+ console.error(err instanceof Error ? err.message : String(err));
12
+ process.exit(1);
13
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Validate generated lexicon-fountain artifacts.
3
+ *
4
+ * Thin wrapper around the core validation framework
5
+ * with fountain-specific configuration.
6
+ */
7
+
8
+ import { dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
11
+
12
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
13
+
14
+ /**
15
+ * The curated surface, as named in spec/parse.ts. A regeneration that
16
+ * loses one — an upstream rename, a spec fetch that returned a partial
17
+ * document — must fail validation rather than ship a lexicon missing a
18
+ * kind. Repository is included: it is the one property type reachable
19
+ * from the request schemas, and its absence means ref-following broke.
20
+ */
21
+ const REQUIRED_NAMES: string[] = ["Environment", "Vault", "Agent", "Repository"];
22
+
23
+ /**
24
+ * Validate the generated lexicon-fountain artifacts.
25
+ */
26
+ export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
27
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
28
+
29
+ return validateLexiconArtifacts({
30
+ lexiconJsonFilename: "lexicon-fountain.json",
31
+ requiredNames: REQUIRED_NAMES,
32
+ basePath,
33
+ });
34
+ }