@intentius/chant-lexicon-fountain 0.44.14 → 0.46.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.
@@ -2,10 +2,9 @@
2
2
  * fountain documentation generator.
3
3
  *
4
4
  * Calls the core docsPipeline with fountain-specific config. The reference
5
- * pages (resources, composites, ops, adoption, skills) are declared as
6
- * `extraPages` rather than left as hand-written files in docs/: the sidebar is
7
- * rebuilt from generated pages on every run, so a page the config does not
8
- * know about exists but cannot be navigated to.
5
+ * pages (resources, composites, ops, adoption, skills) live as authored MDX
6
+ * under `docs/pages/`, each tagged with its Diátaxis quadrant; the sidebar is
7
+ * grouped from that field (chant #1731 / #1733).
9
8
  */
10
9
  /**
11
10
  * Generate documentation site for the fountain lexicon.
@@ -1 +1 @@
1
- {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqNH;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDjF"}
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmFH;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBjF"}
@@ -65,6 +65,13 @@ export declare function computeFountainCoverage(specJSON: string | Buffer, surfa
65
65
  export declare function formatSummary(report: FountainCoverageReport): string;
66
66
  export declare function formatVerbose(report: FountainCoverageReport): string;
67
67
  /** Run coverage analysis for the fountain lexicon. */
68
+ /**
69
+ * The offline path behind `coverageReport()` (#1330): the same computation
70
+ * `coverage.test.ts` runs, over the committed spec snapshot and surface
71
+ * baseline. Never `fetchSchemas()` — check-lexicon runs on every PR and must
72
+ * not do network I/O.
73
+ */
74
+ export declare function coverageReportFromSnapshots(basePath?: string): FountainCoverageReport;
68
75
  export declare function analyzeFountainCoverage(opts?: {
69
76
  basePath?: string;
70
77
  verbose?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUxD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,OAAO,EAAE,eAAe,GACvB,sBAAsB,CAkDxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAkCpE;AAED,sDAAsD;AACtD,wBAAsB,uBAAuB,CAAC,IAAI,CAAC,EAAE;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAsBlC"}
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUxD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,OAAO,EAAE,eAAe,GACvB,sBAAsB,CAkDxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAkCpE;AAED,sDAAsD;AACtD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,sBAAsB,CASrF;AAED,wBAAsB,uBAAuB,CAAC,IAAI,CAAC,EAAE;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAsBlC"}
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAc,MAAM,gCAAgC,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAItE,0EAA0E;AAC1E,eAAO,MAAM,aAAa,UAA4F,CAAC;AAYvH,qBAAa,cAAe,SAAQ,eAAgB,YAAW,cAAc;IAC3E,SAAS,CAAC,iBAAiB,IAAI,OAAO,GAAG,IAAI;IAM7C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAsChC,OAAO,CAAC,QAAQ;CASjB"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../src/import/parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAc,MAAM,gCAAgC,CAAC;AAC7F,OAAO,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAItE,0EAA0E;AAC1E,eAAO,MAAM,aAAa,UAA4F,CAAC;AAYvH,qBAAa,cAAe,SAAQ,eAAgB,YAAW,cAAc;IAC3E,SAAS,CAAC,iBAAiB,IAAI,OAAO,GAAG,IAAI;IAM7C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAwChC,OAAO,CAAC,QAAQ;CASjB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "21807f66c47743a64aedc882acf7995ce951369ad393b4d45936f8ae656606b3",
4
+ "manifest.json": "8537e5940612ea9a51cc42d190619c524e38be641ae6a875bc9bf27d6917e79f",
5
5
  "meta.json": "6666b7a77db9210a329219c6a5e107e5ae8794a34542f7310ce16d9bf95c64f5",
6
6
  "types/index.d.ts": "1dfdcba184fffcf464dac7d1d71ef7abed2ae02a8d638c0de50e0b0317b7eb01",
7
7
  "rules/ftn001-no-secret-literals.ts": "897a4ce1ec790b3c1540d32892603bd33ff4bf30eb6ba4cd2565dee356d4962e",
@@ -17,5 +17,5 @@
17
17
  "skills/chant-fountain-secrets.md": "27e349a91589510a92e518c7d7824a4a322cab5ef242cf5799373a55cbfcd1cd",
18
18
  "skills/chant-fountain-locked-sandboxes.md": "de82f06cb3a08ba6bf3ae45fb9869e21d6da18b9ebe0fc769da8aebaceea7dd1"
19
19
  },
20
- "composite": "9e152606453be36deedffe0a89f98992982a91f3bc9c8650ef90a9d98a9d35c5"
20
+ "composite": "c6e025975a4ae6d4a79a848081a92da287e26aa85d01f42e63813bc858a22760"
21
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fountain",
3
- "version": "0.44.14",
3
+ "version": "0.46.0",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Fountain",
6
6
  "specVersion": "v0.3.0"
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAsB7F;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,aA8I5B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAsB7F;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,aAsJ5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAyC/D,eAAO,MAAM,kBAAkB,EAAE,UAmDhC,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AA4C/D,eAAO,MAAM,kBAAkB,EAAE,UAsDhC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-fountain",
3
- "version": "0.44.14",
3
+ "version": "0.46.0",
4
4
  "type": "module",
5
5
  "description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  "bundle": "tsx src/package-cli.ts"
51
51
  },
52
52
  "peerDependencies": {
53
- "@intentius/chant": "^0.44.14",
53
+ "@intentius/chant": "^0.46.0",
54
54
  "typescript": "^5.9.3"
55
55
  },
56
56
  "devDependencies": {
@@ -2,10 +2,9 @@
2
2
  * fountain documentation generator.
3
3
  *
4
4
  * Calls the core docsPipeline with fountain-specific config. The reference
5
- * pages (resources, composites, ops, adoption, skills) are declared as
6
- * `extraPages` rather than left as hand-written files in docs/: the sidebar is
7
- * rebuilt from generated pages on every run, so a page the config does not
8
- * know about exists but cannot be navigated to.
5
+ * pages (resources, composites, ops, adoption, skills) live as authored MDX
6
+ * under `docs/pages/`, each tagged with its Diátaxis quadrant; the sidebar is
7
+ * grouped from that field (chant #1731 / #1733).
9
8
  */
10
9
 
11
10
  import { dirname, join } from "path";
@@ -67,14 +66,13 @@ kind: Environment
67
66
  metadata:
68
67
  name: team-env
69
68
  spec:
70
- name: team-env
71
69
  networking_type: limited
72
70
  networking_config:
73
71
  allowed_hosts:
74
72
  - github.com
75
73
  \`\`\`
76
74
 
77
- \`metadata.name\` is the resource's declared \`name\`, not the name of the variable you exported it as. fountain reconciles by that name, so renaming the variable does not orphan the resource. An entity declared without a \`name\` falls back to the export name.
75
+ \`metadata.name\` is the resource's declared \`name\`, not the name of the variable you exported it as. fountain reconciles by that name, so renaming the variable does not orphan the resource. An entity declared without a \`name\` falls back to the export name. The name appears only in \`metadata\`; it is not repeated under \`spec\`, so the apply request carries one name per resource.
78
76
 
79
77
  The output is ejectable — \`fountain apply -f\` accepts it verbatim, so adopting chant here does not trap the manifests behind chant.
80
78
 
@@ -90,135 +88,6 @@ Resources carrying \`metadata."managed-by": chant\` are chant-owned. That marker
90
88
 
91
89
  \`spec.secrets\` is authored as an ordered \`{key, value}[]\`, same as any other typed prop. \`fountainApply\` converts it to the \`{KEY: value}\` map fountain's bulk apply expects on the wire; the server upserts it through the encrypted envelope path. Values are write-only upstream, so this is upsert-always — a changed value cannot be detected, only overwritten.`;
92
90
 
93
- const resourcesPage = `The lexicon types the three kinds \`fountain apply\` reconciles — the workload
94
- layer of [fountain](https://github.com/BinaryBourbon/fountain). Types are
95
- generated from fountain's served OpenAPI spec, so they track the real API.
96
- Conversations are deliberately **not** a resource: they are runs with a
97
- status lifecycle, modeled as ops.
98
-
99
- ## Environment
100
-
101
- A reusable sandbox baseline: packages, repos, setup script, env vars,
102
- encrypted secrets, and the networking policy. \`networking_type\` is
103
- \`"unrestricted" | "limited"\`; under \`limited\`, egress is restricted to
104
- \`networking_config.allowed_hosts\`, and with no hosts (or an empty list) the
105
- sandbox denies all egress — a deny-all, not an allow-all. FTN010 requires the
106
- networking intent to be explicit.
107
-
108
- ## Vault
109
-
110
- A bag of env-var overrides selected at conversation create. Vault values
111
- win on key collision with the environment — which is why agents can carry
112
- a vault allowlist upstream (fountain#136). \`allowed_vault_ids\` on an Agent is
113
- three-state: \`null\` allows any tenant vault, \`[]\` forbids all, a list is an
114
- allowlist.
115
-
116
- ## Agent
117
-
118
- A named, re-runnable agent configuration: model, runtime, skills (inline
119
- SKILL.md or GitHub-sourced with a \`ref\` pin), MCP servers, and an optional
120
- \`environment\` reference — typed, so a dangling reference is a build error,
121
- not a 422 at apply time.
122
-
123
- ## Example
124
-
125
- \`\`\`ts
126
- import { Environment, Agent } from "@intentius/chant-lexicon-fountain";
127
-
128
- export const conciergeEnv = new Environment({
129
- name: "concierge-env",
130
- networking_type: "limited",
131
- networking_config: { allowed_hosts: ["registry.npmjs.org", "github.com"] },
132
- metadata: { "managed-by": "chant" },
133
- });
134
-
135
- export const researcher = new Agent({
136
- name: "researcher",
137
- model: "anthropic/claude-sonnet-4-6",
138
- runtime: "claude",
139
- environment: conciergeEnv,
140
- skills: [{ source: "vercel-labs/agent-skills", ref: "main" }],
141
- });
142
- \`\`\``;
143
-
144
- const compositesPage = `## ConciergeStack
145
-
146
- An Environment + Agent pair with the secure-by-construction defaults for agents that touch anything sensitive:
147
-
148
- \`\`\`typescript
149
- import { ConciergeStack } from "@intentius/chant-lexicon-fountain";
150
-
151
- export const { environment, agent } = ConciergeStack({
152
- name: "concierge",
153
- model: "anthropic/claude-sonnet-4-6",
154
- allowedHosts: ["registry.npmjs.org", "github.com"],
155
- });
156
- \`\`\`
157
-
158
- | Default | Effect |
159
- |---|---|
160
- | \`networking_type: limited\` with an empty allowlist | deny-all egress — fountain's isolation mode |
161
- | \`allowed_vault_ids: []\` | no conversation may override the reviewed environment at spawn |
162
- | \`managed-by: chant\` on both | owned-only reconcile, prune, and drift filtering see them |
163
-
164
- Every default is the closed one, so loosening any of it is a visible, reviewable act: pass an allowlist, pass vault ids, or drop to the raw classes.
165
-
166
- Give such a sandbox **no cloud credentials of any kind** — anything readable inside it is exfiltratable by prompt injection. Services the agent needs live outside the sandbox behind their own auth; the sandbox gets at most a conversation-scoped token.`;
167
-
168
- const opsPage = `Two op activities ship with the lexicon, resolvable by name via \`loadActivities(["fountain"])\`.
169
-
170
- ## fountainApply
171
-
172
- The native applier: compiles the serializer's manifest YAML into fountain's bulk \`POST /api/apply\` request and sends it in one call.
173
-
174
- | Behavior | Detail |
175
- |---|---|
176
- | Create / update | By name, reconciled server-side. An agent's \`environment\` reference resolves by name — against the manifest or the tenant's existing environments — without a client-side id lookup. |
177
- | Order | Environment → Vault → Agent, fixed server-side regardless of manifest order. |
178
- | Prune | Off by default. With \`prune: true\`, chant-owned resources absent from the manifest are deleted, in reverse order — the one thing bulk apply doesn't cover, so this still lists live state per kind. |
179
- | Secrets | \`spec.secrets\` converts from chant's authored \`{key, value}[]\` to the wire's \`{KEY: value}\` map; the server upserts them through the encrypted envelope path in the same request. Upsert-always — values are write-only upstream. |
180
- | Failure | Best-effort per resource. Every result is collected before this throws, so one bad resource doesn't hide failures elsewhere in the manifest. |
181
-
182
- \`\`\`typescript
183
- await fountainApply({ manifestPath: "build/fountain.yaml", prune: true });
184
- \`\`\`
185
-
186
- Endpoint and token resolution: explicit args win, then \`FOUNTAIN_ENDPOINT\` / \`FOUNTAIN_TOKEN\`, then the hosted default.
187
-
188
- ## fountainRun
189
-
190
- Conversations are runs, not resources, so they are started rather than declared. \`fountainRun\` resolves the agent by name, starts a conversation (optionally with a prompt and an allowlisted vault), polls to \`completed | failed | timed_out\`, and terminates at its deadline so a hung sandbox never outlives the op.
191
-
192
- Multi-turn interaction — follow-up prompts, interrupt — is fountain's own conversations API. The lexicon stays at the lifecycle edges.`;
193
-
194
- const adoptionPage = `The lexicon reads live fountain state on three paths.
195
-
196
- ## Drift
197
-
198
- \`chant lifecycle diff --live\` reports each declared entity as observed present, observed absent, or **not observed** with a reason — a read failure is never reported as an absence, which would propose a spurious create. Ownership comes from the \`managed-by: chant\` marker, so \`--owned\` filters to what chant declared.
199
-
200
- An out-of-band change to a locked environment — a UI edit that adds a secret, opens networking, or drops the marker — is what this catches. Wire it into a scheduled watch and treat a hit as an incident, not housekeeping.
201
-
202
- ## Import
203
-
204
- \`chant import --from\` adopts UI-built resources into typed files. Server-written fields are stripped to the authored shape, and an agent's \`environment_id\` is resolved back to the exported environment's logical name so the generated code carries a reviewable reference.
205
-
206
- Secrets do not round-trip: values are write-only upstream and secret keys are not on the typed request surface, so environments export without them and the caller is warned per environment that carried any. Re-declare them through your secret provider.
207
-
208
- ## Graph
209
-
210
- \`chant graph --live\` reconstructs the topology from one edge: an Agent runs in an Environment. Vaults are deliberately edge-free — vault-to-agent binding is a conversation-time choice scoped by \`allowed_vault_ids\`, not standing topology.`;
211
-
212
- const skillsPage = `Three agent skills ship with the lexicon and load automatically in a project that uses it.
213
-
214
- | Skill | Covers |
215
- |---|---|
216
- | \`chant-fountain\` | The core authoring loop — declaring the three kinds, the build/apply/drift/import cycle, endpoint and auth |
217
- | \`chant-fountain-secrets\` | Secrets, \`env_vars\`, and \`\${VAR}\` substitution: the order of preference, vault precedence, and what does not round-trip |
218
- | \`chant-fountain-locked-sandboxes\` | The locked-down posture for untrusted or security-sensitive agents, and running conversations against them |
219
-
220
- Invoke one directly by name, or let it trigger on context (\`fountain\`, \`vault\`, \`sandbox\`, \`networking_type\`).`;
221
-
222
91
  /**
223
92
  * Generate documentation site for the fountain lexicon.
224
93
  */
@@ -237,38 +106,6 @@ export async function generateDocs(options?: { verbose?: boolean }): Promise<voi
237
106
  // No `resourceTypeUrl` key: DocsConfig has none, so passing it was a
238
107
  // silent no-op. The upstream reference link lives in the resources page
239
108
  // content, where it actually renders.
240
- extraPages: [
241
- {
242
- slug: "resources",
243
- title: "Resources",
244
- description: "The three fountain workload kinds the lexicon declares.",
245
- content: resourcesPage,
246
- },
247
- {
248
- slug: "composites",
249
- title: "Composites",
250
- description: "ConciergeStack — a locked-down Environment and Agent pair.",
251
- content: compositesPage,
252
- },
253
- {
254
- slug: "ops",
255
- title: "Ops",
256
- description: "fountainApply and fountainRun — the applier and the conversation runner.",
257
- content: opsPage,
258
- },
259
- {
260
- slug: "adoption",
261
- title: "Drift and Adoption",
262
- description: "Live observation, import, and the graph edge fountain reconstructs.",
263
- content: adoptionPage,
264
- },
265
- {
266
- slug: "skills",
267
- title: "Skills",
268
- description: "The agent skills the lexicon ships.",
269
- content: skillsPage,
270
- },
271
- ],
272
109
  };
273
110
 
274
111
  const result = docsPipeline(config);
@@ -1,8 +1,15 @@
1
- import { describe, expect, it } from "vitest";
1
+ import { describe, expect, it, vi } from "vitest";
2
2
  import { readFileSync } from "fs";
3
3
  import { join, dirname } from "path";
4
4
  import { fileURLToPath } from "url";
5
- import { computeFountainCoverage, formatVerbose, EXCLUDED_KINDS, UNSPECIFIED_ENDPOINTS } from "./coverage";
5
+ import {
6
+ computeFountainCoverage,
7
+ coverageReportFromSnapshots,
8
+ formatVerbose,
9
+ EXCLUDED_KINDS,
10
+ UNSPECIFIED_ENDPOINTS,
11
+ } from "./coverage";
12
+ import { fountainPlugin } from "./plugin";
6
13
  import { fetchSchemas } from "./spec/fetch";
7
14
 
8
15
  const srcDir = dirname(fileURLToPath(import.meta.url));
@@ -63,6 +70,30 @@ describe("fountain coverage", () => {
63
70
  });
64
71
  });
65
72
 
73
+ // chant #1330 — the same accounting, exposed to check-lexicon through the
74
+ // plugin contract. It must agree with the direct computation above and must
75
+ // stay offline: check-lexicon runs on every PR.
76
+ describe("coverageReport plugin contract", () => {
77
+ it("reports no unaccounted kinds over the committed snapshots", async () => {
78
+ const report = await fountainPlugin.coverageReport!();
79
+ expect(report.unaccountedKinds).toEqual([]);
80
+ expect(report.unaccountedKinds).toEqual(
81
+ computeFountainCoverage(spec, surface).unaccountedKinds,
82
+ );
83
+ });
84
+
85
+ it("does no network I/O", async () => {
86
+ const spy = vi.spyOn(globalThis, "fetch");
87
+ try {
88
+ await fountainPlugin.coverageReport!();
89
+ coverageReportFromSnapshots();
90
+ expect(spy).not.toHaveBeenCalled();
91
+ } finally {
92
+ spy.mockRestore();
93
+ }
94
+ });
95
+ });
96
+
66
97
  describe("unspecified upstream endpoints", () => {
67
98
  it("records a decision for every endpoint the spec cannot describe", () => {
68
99
  // The point of the list is the reasoning, not the route string. An entry
package/src/coverage.ts CHANGED
@@ -190,6 +190,23 @@ export function formatVerbose(report: FountainCoverageReport): string {
190
190
  }
191
191
 
192
192
  /** Run coverage analysis for the fountain lexicon. */
193
+ /**
194
+ * The offline path behind `coverageReport()` (#1330): the same computation
195
+ * `coverage.test.ts` runs, over the committed spec snapshot and surface
196
+ * baseline. Never `fetchSchemas()` — check-lexicon runs on every PR and must
197
+ * not do network I/O.
198
+ */
199
+ export function coverageReportFromSnapshots(basePath?: string): FountainCoverageReport {
200
+ const base = basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
201
+
202
+ const spec = readFileSync(join(base, "src", "spec", "fountain-openapi.snapshot.json"), "utf-8");
203
+ const surface = JSON.parse(
204
+ readFileSync(join(base, "surface.snapshot.json"), "utf-8"),
205
+ ) as SurfaceSnapshot;
206
+
207
+ return computeFountainCoverage(spec, surface);
208
+ }
209
+
193
210
  export async function analyzeFountainCoverage(opts?: {
194
211
  basePath?: string;
195
212
  verbose?: boolean;
@@ -66,7 +66,9 @@ export class FountainParser extends BaseValueParser implements TemplateParser {
66
66
  const meta = isRecord(doc.metadata) ? doc.metadata : {};
67
67
  const name = typeof meta.name === "string" ? meta.name : kind.toLowerCase();
68
68
  const spec = isRecord(doc.spec) ? doc.spec : {};
69
- resources.push(this.resource(type, name, spec));
69
+ // Manifests carry the name in `metadata` only; the typed resource
70
+ // needs it as its `name` prop.
71
+ resources.push(this.resource(type, name, { ...spec, name }));
70
72
  }
71
73
  return { resources, parameters: [] };
72
74
  }
@@ -10,7 +10,6 @@ kind: Environment
10
10
  metadata:
11
11
  name: concierge-env
12
12
  spec:
13
- name: concierge-env
14
13
  networking_type: limited
15
14
  networking_config:
16
15
  allowed_hosts:
@@ -24,7 +23,6 @@ kind: Agent
24
23
  metadata:
25
24
  name: researcher
26
25
  spec:
27
- name: researcher
28
26
  model: anthropic/claude-sonnet-4-6
29
27
  runtime: claude
30
28
  environment: concierge-env
@@ -50,6 +48,18 @@ describe("parser", () => {
50
48
  expect(ir.resources[0].properties.networking_type).toBe("limited");
51
49
  });
52
50
 
51
+ it("takes the name prop from metadata.name when spec carries none (#1606)", () => {
52
+ const ir = new FountainParser().parse(MANIFESTS);
53
+ expect(ir.resources.map((r) => r.properties.name)).toEqual(["concierge-env", "researcher"]);
54
+ });
55
+
56
+ it("lets metadata.name win over a stray spec.name", () => {
57
+ const ir = new FountainParser().parse(
58
+ "apiVersion: fountain.dev/v1\nkind: Vault\nmetadata:\n name: real\nspec:\n name: stale\n",
59
+ );
60
+ expect(ir.resources[0].properties.name).toBe("real");
61
+ });
62
+
53
63
  it("parses the fountain-plan.json sidecar", () => {
54
64
  const ir = new FountainParser().parse(
55
65
  JSON.stringify({ v: { kind: "Vault", spec: { name: "staging", id: "drop-me" } } }),
package/src/plugin.ts CHANGED
@@ -63,6 +63,14 @@ export const fountainPlugin: LexiconPlugin = {
63
63
 
64
64
  // ── Optional extensions ────────────────────────────────────
65
65
 
66
+ // #1330 — the fact check-lexicon gates on, computed offline from the
67
+ // committed spec snapshot + surface baseline. Never fetchSchemas():
68
+ // check-lexicon runs per-PR, no network.
69
+ async coverageReport(): Promise<{ unaccountedKinds?: string[] }> {
70
+ const { coverageReportFromSnapshots } = await import("./coverage");
71
+ return { unaccountedKinds: coverageReportFromSnapshots().unaccountedKinds };
72
+ },
73
+
66
74
  lintRules() {
67
75
  return rules;
68
76
  },
@@ -43,6 +43,25 @@ describe("fountain serializer", () => {
43
43
  expect(out).toContain("- github.com");
44
44
  });
45
45
 
46
+ it("carries the name in metadata only, never under spec (#1606)", () => {
47
+ const agent = entity("Fountain::V1::Agent", {
48
+ name: "tech-lead",
49
+ model: "anthropic/claude-sonnet-4-6",
50
+ runtime: "claude",
51
+ });
52
+
53
+ const out = fountainSerializer.serialize(new Map([["techLead", agent]])) as string;
54
+
55
+ expect(out).toContain("metadata:\n name: tech-lead\n");
56
+ expect(out).not.toContain("spec:\n name:");
57
+ expect(out.match(/^\s*name: /gm)).toHaveLength(1);
58
+
59
+ // The apply payload fountain receives has exactly one name per resource.
60
+ const [resource] = parseManifest(out);
61
+ expect(resource.name).toBe("tech-lead");
62
+ expect(resource.spec).not.toHaveProperty("name");
63
+ });
64
+
46
65
  it("separates multiple entities with document markers", () => {
47
66
  const env = entity("Fountain::V1::Environment", { name: "e" });
48
67
  const vault = entity("Fountain::V1::Vault", { name: "v" });
package/src/serializer.ts CHANGED
@@ -15,6 +15,9 @@ import { propsOf } from "./entity-props";
15
15
  *
16
16
  * Cross-resource references (e.g. `agent.environment`) serialize to the
17
17
  * referenced entity's name — fountain resolves names to ids at apply.
18
+ *
19
+ * The `name` prop becomes `metadata.name` and is not repeated under `spec`,
20
+ * matching fountain's own manifest examples.
18
21
  */
19
22
 
20
23
  const API_VERSION = "fountain.dev/v1";
@@ -73,9 +76,12 @@ export const fountainSerializer: Serializer = {
73
76
 
74
77
  const docs: string[] = [];
75
78
  for (const [name, entity] of entities) {
79
+ // `name` lives in `metadata` only. fountain's manifest format carries
80
+ // the upsert key there and nowhere else; a second copy under `spec`
81
+ // is at best redundant and at worst a conflicting value (#1606).
76
82
  const spec: Record<string, unknown> = {};
77
83
  for (const [key, val] of Object.entries(propsOf(entity))) {
78
- if (val === undefined) continue;
84
+ if (val === undefined || key === "name") continue;
79
85
  spec[key] = walkValue(val, entityNames, visitor);
80
86
  }
81
87