@intentius/chant-lexicon-fountain 0.44.13 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codegen/docs.d.ts +3 -4
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/serializer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/docs.ts +4 -167
- package/src/import/parser.ts +3 -1
- package/src/import/roundtrip.test.ts +12 -2
- package/src/serializer.test.ts +19 -0
- package/src/serializer.ts +7 -1
package/dist/codegen/docs.d.ts
CHANGED
|
@@ -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)
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
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
|
|
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"}
|
|
@@ -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;
|
|
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"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "28dd49dfa56314218e8f0679b05ad176cd7996e751444eb51e6887633c04a4ba",
|
|
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": "
|
|
20
|
+
"composite": "b7579f9b30c25525a5424d24868d39e12858de8ea2da23773c3760edca697810"
|
|
21
21
|
}
|
package/dist/manifest.json
CHANGED
package/dist/serializer.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
3
|
+
"version": "0.45.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.
|
|
53
|
+
"@intentius/chant": "^0.45.0",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
package/src/codegen/docs.ts
CHANGED
|
@@ -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)
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
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);
|
package/src/import/parser.ts
CHANGED
|
@@ -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
|
-
|
|
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/serializer.test.ts
CHANGED
|
@@ -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
|
|