@intentius/chant 0.46.0 → 0.49.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 (183) hide show
  1. package/dist/audit/core.d.ts +21 -3
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts +3 -2
  4. package/dist/audit/discover.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/build.d.ts +3 -3
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/lint.d.ts.map +1 -1
  10. package/dist/cli/handlers/lifecycle.d.ts +1 -1
  11. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  12. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  13. package/dist/cli/mcp/tools/explain.d.ts +6 -0
  14. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  15. package/dist/cli/plugins.d.ts +1 -1
  16. package/dist/cli/plugins.d.ts.map +1 -1
  17. package/dist/cli/reporters/stylish.d.ts +15 -1
  18. package/dist/cli/reporters/stylish.d.ts.map +1 -1
  19. package/dist/components/auto-release.d.ts +4 -0
  20. package/dist/components/auto-release.d.ts.map +1 -1
  21. package/dist/components/starter-plugin.d.ts +2 -0
  22. package/dist/components/starter-plugin.d.ts.map +1 -1
  23. package/dist/components/verbs/ensure-secret.d.ts +50 -0
  24. package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
  25. package/dist/components/verbs/index.d.ts +8 -0
  26. package/dist/components/verbs/index.d.ts.map +1 -1
  27. package/dist/components/verbs/r2-sync.d.ts +76 -0
  28. package/dist/components/verbs/r2-sync.d.ts.map +1 -0
  29. package/dist/components/verbs/wrangler.d.ts +108 -0
  30. package/dist/components/verbs/wrangler.d.ts.map +1 -0
  31. package/dist/config.d.ts +26 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/deep-observation.d.ts +14 -0
  34. package/dist/deep-observation.d.ts.map +1 -1
  35. package/dist/effect-receipt.d.ts +177 -0
  36. package/dist/effect-receipt.d.ts.map +1 -0
  37. package/dist/fold/subset.d.ts +15 -2
  38. package/dist/fold/subset.d.ts.map +1 -1
  39. package/dist/index.d.ts +4 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/lexicon.d.ts +44 -3
  42. package/dist/lexicon.d.ts.map +1 -1
  43. package/dist/lifecycle/change-set.d.ts +33 -5
  44. package/dist/lifecycle/change-set.d.ts.map +1 -1
  45. package/dist/lifecycle/index.d.ts +2 -0
  46. package/dist/lifecycle/index.d.ts.map +1 -1
  47. package/dist/lifecycle/observation-baseline.d.ts +21 -3
  48. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  49. package/dist/lifecycle/receipt-plan.d.ts +62 -0
  50. package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
  51. package/dist/lifecycle/release-ledger.d.ts +20 -0
  52. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  53. package/dist/lifecycle/teardown.d.ts +6 -4
  54. package/dist/lifecycle/teardown.d.ts.map +1 -1
  55. package/dist/lifecycle/unobserved-gate.d.ts +67 -0
  56. package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
  57. package/dist/lint/knowledge-checks.d.ts +48 -0
  58. package/dist/lint/knowledge-checks.d.ts.map +1 -0
  59. package/dist/lint/output-checks.d.ts +5 -0
  60. package/dist/lint/output-checks.d.ts.map +1 -0
  61. package/dist/lint/pipeline-change-gate.d.ts +101 -0
  62. package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
  63. package/dist/lint/post-synth.d.ts +12 -0
  64. package/dist/lint/post-synth.d.ts.map +1 -1
  65. package/dist/lint/receipt-checks.d.ts +9 -0
  66. package/dist/lint/receipt-checks.d.ts.map +1 -0
  67. package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
  68. package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
  69. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
  70. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
  71. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  72. package/dist/lint/rules/index.d.ts +3 -1
  73. package/dist/lint/rules/index.d.ts.map +1 -1
  74. package/dist/okf-read.d.ts +78 -0
  75. package/dist/okf-read.d.ts.map +1 -0
  76. package/dist/op/builders.d.ts +98 -1
  77. package/dist/op/builders.d.ts.map +1 -1
  78. package/dist/op/index.d.ts +4 -2
  79. package/dist/op/index.d.ts.map +1 -1
  80. package/dist/op/local-executor.d.ts +2 -1
  81. package/dist/op/local-executor.d.ts.map +1 -1
  82. package/dist/op/receipt-store.d.ts +138 -0
  83. package/dist/op/receipt-store.d.ts.map +1 -0
  84. package/dist/op/types.d.ts +31 -1
  85. package/dist/op/types.d.ts.map +1 -1
  86. package/dist/secret-materialization.d.ts +138 -0
  87. package/dist/secret-materialization.d.ts.map +1 -0
  88. package/dist/secret-provenance.d.ts +218 -0
  89. package/dist/secret-provenance.d.ts.map +1 -0
  90. package/dist/serializer.d.ts +11 -0
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/yaml.d.ts.map +1 -1
  93. package/package.json +4 -1
  94. package/src/audit/core.test.ts +57 -0
  95. package/src/audit/core.ts +0 -0
  96. package/src/audit/detect-bundle.test.ts +1 -1
  97. package/src/audit/discover.test.ts +24 -0
  98. package/src/audit/discover.ts +11 -2
  99. package/src/audit/rules-doc.ts +11 -1
  100. package/src/build.test.ts +41 -0
  101. package/src/build.ts +34 -6
  102. package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
  103. package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
  104. package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
  105. package/src/cli/commands/audit.test.ts +53 -0
  106. package/src/cli/commands/audit.ts +1 -1
  107. package/src/cli/commands/build.test.ts +80 -0
  108. package/src/cli/commands/build.ts +106 -8
  109. package/src/cli/commands/lint.ts +15 -3
  110. package/src/cli/handlers/explain.test.ts +70 -1
  111. package/src/cli/handlers/graph.ts +2 -2
  112. package/src/cli/handlers/lifecycle.test.ts +115 -1
  113. package/src/cli/handlers/lifecycle.ts +84 -11
  114. package/src/cli/mcp/resource-handlers.ts +38 -1
  115. package/src/cli/mcp/server.test.ts +58 -1
  116. package/src/cli/mcp/tools/explain.ts +51 -2
  117. package/src/cli/plugins.ts +4 -2
  118. package/src/cli/reporters/stylish.test.ts +154 -0
  119. package/src/cli/reporters/stylish.ts +154 -33
  120. package/src/components/auto-release.ts +6 -0
  121. package/src/components/registry.test.ts +7 -2
  122. package/src/components/starter-plugin.ts +17 -0
  123. package/src/components/verbs/ensure-secret.test.ts +130 -0
  124. package/src/components/verbs/ensure-secret.ts +79 -0
  125. package/src/components/verbs/index.ts +8 -0
  126. package/src/components/verbs/r2-sync.test.ts +107 -0
  127. package/src/components/verbs/r2-sync.ts +124 -0
  128. package/src/components/verbs/wrangler.test.ts +170 -0
  129. package/src/components/verbs/wrangler.ts +241 -0
  130. package/src/config.test.ts +15 -0
  131. package/src/config.ts +30 -0
  132. package/src/deep-observation.test.ts +19 -0
  133. package/src/deep-observation.ts +17 -0
  134. package/src/effect-receipt-exclusion.test.ts +190 -0
  135. package/src/effect-receipt.test.ts +419 -0
  136. package/src/effect-receipt.ts +412 -0
  137. package/src/fold/subset.test.ts +26 -0
  138. package/src/fold/subset.ts +45 -19
  139. package/src/index.ts +4 -0
  140. package/src/lexicon.ts +48 -3
  141. package/src/lifecycle/change-set.ts +46 -7
  142. package/src/lifecycle/index.ts +2 -0
  143. package/src/lifecycle/observation-baseline.test.ts +46 -0
  144. package/src/lifecycle/observation-baseline.ts +33 -1
  145. package/src/lifecycle/receipt-plan.test.ts +250 -0
  146. package/src/lifecycle/receipt-plan.ts +249 -0
  147. package/src/lifecycle/release-ledger.ts +20 -0
  148. package/src/lifecycle/teardown.test.ts +31 -0
  149. package/src/lifecycle/teardown.ts +6 -4
  150. package/src/lifecycle/unobserved-gate.test.ts +109 -0
  151. package/src/lifecycle/unobserved-gate.ts +102 -0
  152. package/src/lint/knowledge-checks.test.ts +80 -0
  153. package/src/lint/knowledge-checks.ts +74 -0
  154. package/src/lint/output-checks.test.ts +85 -0
  155. package/src/lint/output-checks.ts +99 -0
  156. package/src/lint/pipeline-change-gate.test.ts +144 -0
  157. package/src/lint/pipeline-change-gate.ts +153 -0
  158. package/src/lint/post-synth.ts +15 -0
  159. package/src/lint/receipt-checks.test.ts +101 -0
  160. package/src/lint/receipt-checks.ts +93 -0
  161. package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
  162. package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
  163. package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
  164. package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
  165. package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
  166. package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
  167. package/src/lint/rules/index.ts +7 -1
  168. package/src/okf-read.test.ts +149 -0
  169. package/src/okf-read.ts +197 -0
  170. package/src/op/builders.ts +139 -1
  171. package/src/op/effect-step.test.ts +311 -0
  172. package/src/op/index.ts +10 -3
  173. package/src/op/local-executor.ts +172 -25
  174. package/src/op/op.test.ts +25 -2
  175. package/src/op/receipt-store.ts +211 -0
  176. package/src/op/types.ts +33 -1
  177. package/src/secret-materialization.test.ts +199 -0
  178. package/src/secret-materialization.ts +235 -0
  179. package/src/secret-provenance.test.ts +388 -0
  180. package/src/secret-provenance.ts +475 -0
  181. package/src/serializer.ts +12 -0
  182. package/src/yaml.test.ts +88 -0
  183. package/src/yaml.ts +76 -6
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Secret provenance vocabulary (chant #1828, epic #1365).
3
+ *
4
+ * A secret's PROVENANCE is where its value comes from — never what the value
5
+ * is. chant's constitutional line on secrets: no code path may hold, log,
6
+ * hash, or compare a secret value. This module makes that structural: the
7
+ * declaration types have no field that could carry material, the factory
8
+ * rejects material-shaped fields at both the type level (`never` fields) and
9
+ * at runtime (own-property check, naming only the offending KEY), and
10
+ * "mismatch" anywhere downstream means presence, declared key-set, and
11
+ * metadata — never a value or a value-derived hash (#1365 decision 6).
12
+ *
13
+ * The closed kind set:
14
+ *
15
+ * - `referenced` — the value exists out of band (a human or an external
16
+ * process put it where consumers read it). chant records only that the
17
+ * estate depends on it, which is what lets the consumed-but-unproduced
18
+ * check (#1382) ship as an error instead of a guess.
19
+ * - `from-provider` — a declared provider binding materializes it (the
20
+ * operator/CRD seam; `K8s::Infisical::InfisicalSecret` from #1321 is the
21
+ * exemplar). The declaration POINTS at that binding, it does not re-model
22
+ * it.
23
+ * - `generated-once` — minted on first materialization, then never
24
+ * regenerated (present means done). The declaration carries contract flags
25
+ * only — e.g. the declared key-set — never mint parameters holding
26
+ * material. `generated-once` secrets never enter the prunable set (#1365
27
+ * decision 5).
28
+ * - `committed-encrypted` — sops-style ciphertext committed in the repo,
29
+ * decrypted by the delivery system (Flux) straight into the target. The
30
+ * declaration records a repo-relative PATH to the ciphertext, never the
31
+ * bytes: the factory is pure and touches no filesystem, so it still folds
32
+ * under `--sandbox`. The bytes are read at `buildRoots()` — the one
33
+ * sanctioned impure seam — and emitted as a sidecar file, never as a
34
+ * document in the primary output. See
35
+ * `docs/design/committed-encrypted-sops-provenance.md`.
36
+ *
37
+ * Declarations are serializer-neutral: discovery collects them like any
38
+ * entity (they are Declarables), but `partitionByLexicon` (../build.ts)
39
+ * excludes them from every serializer partition, so no lexicon ever emits
40
+ * them. They are data that lint rules and lexicons READ (via
41
+ * {@link collectSecretDeclarations}), not output.
42
+ */
43
+ import { type Declarable } from "./declarable.js";
44
+ /** The closed union of secret origins. */
45
+ export type SecretProvenance = "referenced" | "from-provider" | "generated-once" | "committed-encrypted";
46
+ /** Every provenance kind, for exhaustiveness checks. */
47
+ export declare const SECRET_PROVENANCE_KINDS: readonly SecretProvenance[];
48
+ /**
49
+ * Encryption tools a {@link CommittedEncryptedSecretDeclaration} understands.
50
+ * A closed union with one member, not a free string: a second tool later is a
51
+ * deliberate widening with a detection rule attached, not an unvalidated
52
+ * string that silently means nothing.
53
+ */
54
+ export type SecretEncryption = "sops";
55
+ /** Every encryption tool, for exhaustiveness checks. */
56
+ export declare const SECRET_ENCRYPTION_TOOLS: readonly SecretEncryption[];
57
+ /** Marker symbol identifying a secret provenance declaration. `Symbol.for` so
58
+ * it survives the entity-wire codec (../discovery/entity-wire-codec.ts). */
59
+ export declare const SECRET_DECLARATION_MARKER: unique symbol;
60
+ /** The `entityType` every secret declaration carries. */
61
+ export declare const SECRET_DECLARATION_ENTITY_TYPE = "Chant::SecretProvenance";
62
+ /**
63
+ * Points a `from-provider` declaration at the provider binding that
64
+ * materializes the secret — the CRD seam, not a re-model of it. The binding
65
+ * itself is an ordinary declarable in its own lexicon (e.g. an
66
+ * `InfisicalSecret` instance, #1321); this ref names it so lint can resolve
67
+ * the pair without core depending on any lexicon.
68
+ */
69
+ export interface SecretProviderRef {
70
+ /** Entity name (export name) of the provider binding declarable in this project. */
71
+ readonly binding: string;
72
+ /**
73
+ * Expected `entityType` of the binding (e.g.
74
+ * `"K8s::Infisical::InfisicalSecret"`). Optional; when present, lint can
75
+ * verify the named binding is actually the kind of seam the declaration
76
+ * claims.
77
+ */
78
+ readonly entityType?: string;
79
+ }
80
+ /**
81
+ * Fields that would carry secret material, forbidden by construction. Typed
82
+ * as `never` on every factory input so they fail to compile even when the
83
+ * input object was widened or spread (plain excess-property checks only catch
84
+ * object literals). The runtime check in {@link declareSecret} is the same
85
+ * list, for untyped callers.
86
+ */
87
+ interface NoSecretMaterial {
88
+ readonly value?: never;
89
+ readonly data?: never;
90
+ readonly stringData?: never;
91
+ readonly material?: never;
92
+ readonly plaintext?: never;
93
+ readonly ciphertext?: never;
94
+ }
95
+ /** Fields shared by every secret declaration. */
96
+ interface SecretDeclarationBase extends Declarable {
97
+ readonly [SECRET_DECLARATION_MARKER]: true;
98
+ readonly lexicon: "chant";
99
+ readonly entityType: typeof SECRET_DECLARATION_ENTITY_TYPE;
100
+ /** The secret's name as consumers know it (e.g. the k8s Secret name). */
101
+ readonly name: string;
102
+ /** Which of the closed kind set this declaration is. */
103
+ readonly provenance: SecretProvenance;
104
+ }
105
+ /** A secret whose value exists out of band. */
106
+ export interface ReferencedSecretDeclaration extends SecretDeclarationBase {
107
+ readonly provenance: "referenced";
108
+ /** Where consumers find it — free-form (a namespace, a vault path, a doc link). */
109
+ readonly scope?: string;
110
+ }
111
+ /** A secret a declared provider binding materializes. */
112
+ export interface FromProviderSecretDeclaration extends SecretDeclarationBase {
113
+ readonly provenance: "from-provider";
114
+ readonly provider: SecretProviderRef;
115
+ }
116
+ /** A secret minted on first materialization and never regenerated. */
117
+ export interface GeneratedOnceSecretDeclaration extends SecretDeclarationBase {
118
+ readonly provenance: "generated-once";
119
+ /**
120
+ * The declared key-set of the materialized secret. This is the contract a
121
+ * materializer (#1830) checks for mismatch — presence and keys, never
122
+ * values.
123
+ */
124
+ readonly keys?: readonly string[];
125
+ }
126
+ /**
127
+ * A secret whose ciphertext is committed to the repo and decrypted by the
128
+ * delivery system on the way into the target.
129
+ *
130
+ * The declaration is a POINTER: `file` is a repo-relative path, and nothing
131
+ * here carries bytes. That is what keeps the factory pure — the ciphertext is
132
+ * read at `buildRoots()` and emitted as a sidecar, never inlined into the
133
+ * primary output an applier reads.
134
+ */
135
+ export interface CommittedEncryptedSecretDeclaration extends SecretDeclarationBase {
136
+ readonly provenance: "committed-encrypted";
137
+ /** Repo-relative path to the committed ciphertext file. */
138
+ readonly file: string;
139
+ /** Encryption tool. Defaults to `"sops"` when omitted. */
140
+ readonly encryption: SecretEncryption;
141
+ /**
142
+ * Public recipient identifiers — age recipients or PGP fingerprints. Public
143
+ * by definition, which is the opposite of material; a private key here is
144
+ * refused by the factory.
145
+ */
146
+ readonly recipients?: readonly string[];
147
+ /**
148
+ * The declared key-set of the decrypted Secret — the same contract meaning
149
+ * as {@link GeneratedOnceSecretDeclaration.keys}: presence and key names,
150
+ * never values. SOPS leaves key NAMES cleartext, so this is checkable
151
+ * against the file itself.
152
+ */
153
+ readonly keys?: readonly string[];
154
+ }
155
+ /** A secret provenance declaration — the discriminant is `provenance`. */
156
+ export type SecretDeclaration = ReferencedSecretDeclaration | FromProviderSecretDeclaration | GeneratedOnceSecretDeclaration | CommittedEncryptedSecretDeclaration;
157
+ /** Narrow a declaration to the committed-encrypted kind. */
158
+ export declare function isCommittedEncryptedSecret(decl: SecretDeclaration): decl is CommittedEncryptedSecretDeclaration;
159
+ /** Factory input for a `referenced` secret. */
160
+ export interface ReferencedSecretInput extends NoSecretMaterial {
161
+ readonly name: string;
162
+ readonly provenance: "referenced";
163
+ readonly scope?: string;
164
+ }
165
+ /** Factory input for a `from-provider` secret. */
166
+ export interface FromProviderSecretInput extends NoSecretMaterial {
167
+ readonly name: string;
168
+ readonly provenance: "from-provider";
169
+ readonly provider: SecretProviderRef;
170
+ }
171
+ /** Factory input for a `generated-once` secret. */
172
+ export interface GeneratedOnceSecretInput extends NoSecretMaterial {
173
+ readonly name: string;
174
+ readonly provenance: "generated-once";
175
+ readonly keys?: readonly string[];
176
+ }
177
+ /** Factory input for a `committed-encrypted` secret. */
178
+ export interface CommittedEncryptedSecretInput extends NoSecretMaterial {
179
+ readonly name: string;
180
+ readonly provenance: "committed-encrypted";
181
+ /**
182
+ * Repo-relative path to the committed ciphertext file — `file`, never
183
+ * `ciphertext`: the declaration points at bytes, it does not carry them,
184
+ * and `ciphertext` is a forbidden material field.
185
+ */
186
+ readonly file: string;
187
+ /** Encryption tool. Closed union; `"sops"` is its only member today. */
188
+ readonly encryption?: SecretEncryption;
189
+ /** Public recipient identifiers — age recipients or PGP fingerprints. */
190
+ readonly recipients?: readonly string[];
191
+ /** The declared key-set of the decrypted Secret. Names only, never values. */
192
+ readonly keys?: readonly string[];
193
+ }
194
+ export type SecretDeclarationInput = ReferencedSecretInput | FromProviderSecretInput | GeneratedOnceSecretInput | CommittedEncryptedSecretInput;
195
+ /**
196
+ * Declare a secret's provenance. The returned object is a locked Declarable:
197
+ * discovery collects it like any entity, `chant list` shows it, lint and
198
+ * lexicons read it — and no serializer ever emits it.
199
+ *
200
+ * Only the fields the kind defines are copied onto the declaration; anything
201
+ * else on the input — in particular anything that could carry material — is
202
+ * either a compile error ({@link NoSecretMaterial}) or a thrown error here.
203
+ * The thrown message names the offending KEY only, never its value.
204
+ */
205
+ export declare function declareSecret(input: ReferencedSecretInput): ReferencedSecretDeclaration;
206
+ export declare function declareSecret(input: FromProviderSecretInput): FromProviderSecretDeclaration;
207
+ export declare function declareSecret(input: GeneratedOnceSecretInput): GeneratedOnceSecretDeclaration;
208
+ export declare function declareSecret(input: CommittedEncryptedSecretInput): CommittedEncryptedSecretDeclaration;
209
+ /** Type guard for a secret provenance declaration. */
210
+ export declare function isSecretDeclaration(value: unknown): value is SecretDeclaration;
211
+ /**
212
+ * Extract the secret declarations from a discovered entity map — the read
213
+ * surface for lint rules (#1382) and lexicon materializers (#1830). Keyed by
214
+ * entity name (export name), the same key `DiscoveryResult.entities` uses.
215
+ */
216
+ export declare function collectSecretDeclarations(entities: ReadonlyMap<string, Declarable>): Map<string, SecretDeclaration>;
217
+ export {};
218
+ //# sourceMappingURL=secret-provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-provenance.d.ts","sourceRoot":"","sources":["../src/secret-provenance.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,17 @@ export interface SerializeContext {
18
18
  * ad-hoc builds (e.g. context tools) that pass no config.
19
19
  */
20
20
  config?: Record<string, unknown>;
21
+ /**
22
+ * Effect receipts (#1832, epic #1703) declared in this lexicon's partition.
23
+ * Withheld from the `entities` map by the build: receipts are observe-only
24
+ * to the generic apply path, and the serialized output is what feeds
25
+ * appliers, so a receipt must never enter the apply-bound document. A
26
+ * serializer that wants to render receipts for visibility (#1835) reads
27
+ * them here and puts them anywhere BUT the section an applier writes from;
28
+ * a serializer that ignores this field emits nothing for them, which is the
29
+ * safe default.
30
+ */
31
+ receipts?: ReadonlyMap<string, Declarable>;
21
32
  }
22
33
  /**
23
34
  * Result of serialization that may include additional files (e.g. nested stack templates).
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;OAaG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEtC;;;;;OAKG;IACH,SAAS,CACP,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,OAAO,CAAC,EAAE,aAAa,EAAE,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,gBAAgB,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC;CACpD"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,eAAe,CAAC;IAE5B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEjC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC5C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;OAaG;IACH,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEtC;;;;;OAKG;IACH,SAAS,CACP,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,OAAO,CAAC,EAAE,aAAa,EAAE,EACzB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,gBAAgB,CAAC;IAE7B;;;;;OAKG;IACH,iBAAiB,CAAC,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC;CACpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAelD"}
1
+ {"version":3,"file":"yaml.d.ts","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA0BH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgH/D;AAMD,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASlE;AAiFD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CA8Eb;AA0GD;;GAEG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,WAAW,CAqGb;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAwBlD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -91,5 +91,8 @@
91
91
  "devDependencies": {
92
92
  "@cdktf/hcl2json": "^0.21.0",
93
93
  "ajv": "^8.20.0"
94
+ },
95
+ "overrides": {
96
+ "esbuild": "^0.28.1"
94
97
  }
95
98
  }
@@ -187,4 +187,61 @@ describe("auditFiles", () => {
187
187
  });
188
188
  expect(findings.map((f) => f.checkId)).toEqual(["OK001"]);
189
189
  });
190
+
191
+ test("parse-to-graph: an injected entities parser lets entity-reading checks fire per file (#1567)", async () => {
192
+ const entityCheck: PostSynthCheck = {
193
+ id: "ENT001",
194
+ description: "reads ctx.entities, not ctx.outputs",
195
+ check: (ctx) =>
196
+ [...ctx.entities.keys()].map((name) => ({ checkId: "ENT001", severity: "warning" as const, message: `saw ${name}`, entity: name })),
197
+ };
198
+ const findings = await auditFiles(
199
+ [
200
+ { path: "a.yaml", content: "one", lexicon: "custom" },
201
+ { path: "b.yaml", content: "two", lexicon: "custom" },
202
+ ],
203
+ {
204
+ checksProvider: async () => [entityCheck],
205
+ entitiesProvider: async () => (content) => new Map([[content, { entityType: "T::X" } as never]]),
206
+ },
207
+ );
208
+ expect(findings.map((f) => `${f.file}:${f.entity}`).sort()).toEqual(["a.yaml:one", "b.yaml:two"]);
209
+ });
210
+
211
+ test("parse-to-graph merge: the same entity name in two files keeps both — a duplicate check sees the collision cross-file", async () => {
212
+ const dupCheck: PostSynthCheck = {
213
+ id: "DUP001",
214
+ description: "fires when more than one entity is present",
215
+ check: (ctx) => (ctx.entities.size > 1 ? [{ checkId: "DUP001", severity: "error" as const, message: "name declared twice" }] : []),
216
+ };
217
+ const findings = await auditFiles(
218
+ [
219
+ { path: "a.yaml", content: "x", lexicon: "custom" },
220
+ { path: "b.yaml", content: "y", lexicon: "custom" },
221
+ ],
222
+ {
223
+ checksProvider: async () => [dupCheck],
224
+ // both files declare the entity name "dev" — the merged pass must keep both
225
+ entitiesProvider: async () => () => new Map([["dev", { entityType: "T::X" } as never]]),
226
+ },
227
+ );
228
+ const dup = findings.find((f) => f.checkId === "DUP001");
229
+ expect(dup).toBeDefined();
230
+ expect(dup!.file).toBe(CROSS_FILE);
231
+ });
232
+
233
+ test("an entities parser that throws contributes no entities and does not abort the audit", async () => {
234
+ const entityCheck: PostSynthCheck = {
235
+ id: "ENT002",
236
+ description: "counts entities",
237
+ check: (ctx) => (ctx.entities.size > 0 ? [{ checkId: "ENT002", severity: "warning" as const, message: "saw entities" }] : []),
238
+ };
239
+ const findings = await auditFiles([{ path: "a.yaml", content: "x", lexicon: "custom" }], {
240
+ checksProvider: async () => [entityCheck],
241
+ entitiesProvider: async () => () => {
242
+ throw new Error("unparseable");
243
+ },
244
+ });
245
+ expect(findings).toEqual([]);
246
+ });
190
247
  });
package/src/audit/core.ts CHANGED
Binary file
@@ -9,7 +9,7 @@ import { fileURLToPath } from "url";
9
9
  * service content-detect all lexicons on Workers.
10
10
  */
11
11
  const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
12
- const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm"];
12
+ const LEXICONS = ["k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"];
13
13
 
14
14
  describe("detectTemplate modules are edge-bundle clean", () => {
15
15
  test("importing every lexicon's /detect pulls in no typescript or plugin.ts", async () => {
@@ -42,6 +42,29 @@ describe("discoverByDetection (unified, detectTemplate-driven)", () => {
42
42
  expect(found.some((i) => i.lexicon === "k8s")).toBe(false);
43
43
  });
44
44
 
45
+ test("fountain wins over k8s in a mixed repo — each manifest classifies to its own lexicon (#1566)", async () => {
46
+ const plugins = await loadAuditPlugins();
47
+ const found = discoverByDetection(fixture("audit-fountain"), plugins);
48
+ expect(targets(found)).toEqual(["fountain:agents/fleet.yaml", "k8s:k8s/deploy.yaml"]);
49
+ });
50
+
51
+ test("a multi-document fountain file classifies as one fountain input (no document dropped)", async () => {
52
+ const plugins = await loadAuditPlugins();
53
+ const found = discoverByDetection(fixture("audit-fountain"), plugins);
54
+ const fountain = found.find((i) => i.lexicon === "fountain")!;
55
+ // All three documents ride along in the input's content.
56
+ expect(fountain.content.match(/apiVersion: fountain\.dev\/v1/g)).toHaveLength(3);
57
+ });
58
+
59
+ test("without the fountain plugin, fountain YAML falls to k8s (not-installed convention)", () => {
60
+ const files = [{ path: "fleet.yaml", content: "apiVersion: fountain.dev/v1\nkind: Environment\nmetadata:\n name: dev\n" }];
61
+ const k8s = { name: "k8s", detectTemplate: (d: unknown) => !!d && typeof d === "object" && "apiVersion" in (d as object) && "kind" in (d as object) };
62
+ expect(classifyFiles(files, [k8s]).map((i) => i.lexicon)).toEqual(["k8s"]);
63
+ // with a fountain plugin present, fountain claims it first
64
+ const fountain = { name: "fountain", detectTemplate: (d: unknown) => (d as { apiVersion?: unknown })?.apiVersion === "fountain.dev/v1" };
65
+ expect(classifyFiles(files, [k8s, fountain]).map((i) => i.lexicon)).toEqual(["fountain"]);
66
+ });
67
+
45
68
  test("detects a Helm chart as a bundle, not loose manifests", async () => {
46
69
  const plugins = await loadAuditPlugins();
47
70
  const found = discoverByDetection(fixture("audit-helm"), plugins);
@@ -80,6 +103,7 @@ describe("lexicon hints for unclaimed files (#1623)", () => {
80
103
  expect(hintLexiconForFile("charts/x/Chart.yaml", "name: x\n")).toBe("helm");
81
104
  expect(hintLexiconForFile("infra/main.tf", "resource {}\n")).toBe("terraform");
82
105
  expect(hintLexiconForFile("k8s/deploy.yaml", "apiVersion: apps/v1\nkind: Deployment\n")).toBe("k8s");
106
+ expect(hintLexiconForFile("agents/fleet.yaml", "apiVersion: fountain.dev/v1\nkind: Environment\n")).toBe("fountain");
83
107
  expect(hintLexiconForFile("gcp/bucket.yaml", "apiVersion: storage.cnrm.cloud.google.com/v1beta1\nkind: StorageBucket\n")).toBe("gcp");
84
108
  expect(hintLexiconForFile("cfn/stack.yaml", "AWSTemplateFormatVersion: '2010-09-09'\nResources: {}\n")).toBe("aws");
85
109
  expect(hintLexiconForFile("cfn/stack.json", '{"Resources":{"B":{"Type":"AWS::S3::Bucket"}}}')).toBe("aws");
@@ -22,7 +22,8 @@
22
22
  * - Helm charts are a directory BUNDLE keyed by `Chart.yaml`; the helm checks
23
23
  * read `output.files`, so the whole chart is one AuditInput.
24
24
  * - k8s `detectTemplate` matches any `apiVersion`+`kind`, including GCP Config
25
- * Connector (`cnrm.cloud.google.com`) resources, so gcp must be tried first.
25
+ * Connector (`cnrm.cloud.google.com`) resources and fountain
26
+ * (`fountain.dev/v1`) manifests, so gcp and fountain must be tried first.
26
27
  * - aws/azure checks `JSON.parse` their input, so YAML/templated content is
27
28
  * normalized to a JSON string here.
28
29
  */
@@ -34,7 +35,7 @@ import type { LexiconPlugin } from "../lexicon";
34
35
  import type { AuditInput, AuditLexicon } from "./core";
35
36
 
36
37
  /** Lexicons the auditor knows how to detect and run checks for. */
37
- export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm"] as const;
38
+ export const AUDIT_LEXICONS = ["github", "gitlab", "forgejo", "k8s", "docker", "aws", "azure", "gcp", "helm", "fountain"] as const;
38
39
 
39
40
  /**
40
41
  * Load the plugins used for detection. Each is loaded in isolation (no
@@ -170,6 +171,13 @@ const CONTENT_DETECTORS: ContentDetector[] = [
170
171
  accepts: isYaml,
171
172
  detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
172
173
  },
174
+ {
175
+ // fountain (`apiVersion: fountain.dev/v1`) — must win over k8s, whose
176
+ // detectTemplate matches any apiVersion+kind document (#1566).
177
+ lexicon: "fountain",
178
+ accepts: isYaml,
179
+ detect: (plugin, _name, content) => (anyDoc(plugin, content) ? content : null),
180
+ },
173
181
  {
174
182
  lexicon: "k8s",
175
183
  accepts: isYaml,
@@ -263,6 +271,7 @@ export function hintLexiconForFile(path: string, content: string): LexiconHint |
263
271
  if (/\.tf$/i.test(name)) return "terraform";
264
272
  const head = content.slice(0, 64 * 1024);
265
273
  if (/cnrm\.cloud\.google\.com/.test(head)) return "gcp";
274
+ if (/fountain\.dev\/v1/.test(head)) return "fountain";
266
275
  if (/AWSTemplateFormatVersion|["']?Type["']?\s*:\s*["']AWS::/.test(head)) return "aws";
267
276
  if (/deploymentTemplate\.json/.test(head)) return "azure";
268
277
  if (isYaml(name)) {
@@ -20,14 +20,24 @@ const GROUPS: Array<{ heading: string; prefixes: string[]; blurb: string }> = [
20
20
  { heading: "Azure ARM (AZR)", prefixes: ["AZR"], blurb: "Run against ARM deployment templates (JSON)." },
21
21
  { heading: "GCP Config Connector (WGC)", prefixes: ["WGC"], blurb: "Run against Config Connector (cnrm.cloud.google.com) manifests." },
22
22
  { heading: "Helm (WHM)", prefixes: ["WHM"], blurb: "Run against Helm charts (Chart.yaml + templates)." },
23
+ { heading: "fountain (FTN)", prefixes: ["FTN"], blurb: "Run against fountain manifests (`apiVersion: fountain.dev/v1`) — standalone `fountain apply` YAML is parsed back into the entity graph, so the same rules fire on `chant build` and `chant audit`." },
23
24
  ];
24
25
 
26
+ /**
27
+ * MDX treats `{expr}` as a JavaScript expression and `<x` as JSX, so rule
28
+ * text containing literal braces (`${VAR}` substitution references) or angle
29
+ * brackets must be escaped or the docs build fails at render time.
30
+ */
31
+ function mdxEscape(text: string): string {
32
+ return text.replace(/\{/g, "\\{").replace(/</g, "\\<");
33
+ }
34
+
25
35
  function ruleBlock(m: RuleMeta): string {
26
36
  const tags = `${m.tier} · ${m.fixKind}`;
27
37
  const authority = m.authority?.length
28
38
  ? `\n\nAuthority: ${m.authority.map((a) => `[${a.name}](${a.url})`).join(" · ")}`
29
39
  : "";
30
- return `### ${m.id}\n\n**${m.title}** — ${tags}\n\n${m.remediation}${authority}`;
40
+ return `### ${m.id}\n\n**${mdxEscape(m.title)}** — ${tags}\n\n${mdxEscape(m.remediation)}${authority}`;
31
41
  }
32
42
 
33
43
  /**
package/src/build.test.ts CHANGED
@@ -338,6 +338,47 @@ export const discovered = {
338
338
  expect(result.entities.get("discovered")?.entityType).toBe("Discovered");
339
339
  });
340
340
 
341
+ test("a contributor sees the discovered entities, read-only", async () => {
342
+ await writeFile(
343
+ join(testDir, "app.infra.ts"),
344
+ `
345
+ export const discovered = {
346
+ lexicon: "test",
347
+ entityType: "Discovered",
348
+ props: {},
349
+ [Symbol.for("chant.declarable")]: true,
350
+ };
351
+ `,
352
+ );
353
+
354
+ let seenNames: string[] = [];
355
+ const result = await build(testDir, [recordingSerializer([])], undefined, {
356
+ buildRoots: [
357
+ async (ctx) => {
358
+ seenNames = [...(ctx.entities?.keys() ?? [])];
359
+ return { entities: new Map() };
360
+ },
361
+ ],
362
+ });
363
+
364
+ expect(result.errors).toEqual([]);
365
+ expect(seenNames).toContain("discovered");
366
+ });
367
+
368
+ test("a later contributor sees what an earlier one contributed", async () => {
369
+ const seen: string[][] = [];
370
+ await build(testDir, [recordingSerializer([])], undefined, {
371
+ buildRoots: [
372
+ async () => ({ entities: new Map([["first", entity("First")]]) }),
373
+ async (ctx) => {
374
+ seen.push([...(ctx.entities?.keys() ?? [])]);
375
+ return { entities: new Map() };
376
+ },
377
+ ],
378
+ });
379
+ expect(seen[0]).toContain("first");
380
+ });
381
+
341
382
  test("contributors run exactly once per build", async () => {
342
383
  let calls = 0;
343
384
  await build(testDir, [], undefined, {
package/src/build.ts CHANGED
@@ -2,10 +2,12 @@ import type { Declarable } from "./declarable";
2
2
  import type { Serializer, SerializerResult } from "./serializer";
3
3
  import type { OwnershipMarker } from "./ownership";
4
4
  import type { BuildError, DiscoveryErrorType } from "./errors";
5
- import type { IntrinsicDef, BuildRootContribution } from "./lexicon";
5
+ import type { IntrinsicDef, BuildRootContribution, BuildRootContributor } from "./lexicon";
6
6
  import type { BuildParamProvenance } from "./provenance";
7
7
  import { DiscoveryError, BuildError as BuildErrorClass } from "./errors";
8
8
  import { LexiconOutput, isLexiconOutput } from "./lexicon-output";
9
+ import { isSecretDeclaration } from "./secret-provenance";
10
+ import { splitReceiptEntities } from "./effect-receipt";
9
11
  import { AttrRef } from "./attrref";
10
12
  import { isAttrRefLike } from "./utils";
11
13
  import { isChildProject, type ChildProjectInstance } from "./child-project";
@@ -112,7 +114,9 @@ export function computeStackGraph(
112
114
  // Dependency map: node → producers it depends on.
113
115
  const nodes = lexiconNames.length
114
116
  ? [...lexiconNames]
115
- : [...new Set([...entities.values()].map((e) => e.lexicon))];
117
+ : // Secret provenance declarations (#1828) never form a stack — their
118
+ // pseudo-lexicon has no serializer and must not appear in the manifest.
119
+ [...new Set([...entities.values()].filter((e) => !isSecretDeclaration(e)).map((e) => e.lexicon))];
116
120
  const deps = new Map<string, Set<string>>();
117
121
  for (const n of nodes) deps.set(n, new Set());
118
122
  for (const { from, to } of edges) {
@@ -229,7 +233,7 @@ export interface BuildOptions {
229
233
  * binaries); a contributed name colliding with a discovered entity is a
230
234
  * build error, never a silent overwrite.
231
235
  */
232
- buildRoots?: Array<() => Promise<BuildRootContribution>>;
236
+ buildRoots?: BuildRootContributor[];
233
237
  }
234
238
 
235
239
  export interface BuildResult {
@@ -294,6 +298,11 @@ export function partitionByLexicon(
294
298
  for (const [name, entity] of entities) {
295
299
  // LexiconOutput instances are collected separately; skip them here
296
300
  if (isLexiconOutput(entity)) continue;
301
+ // Secret provenance declarations (#1828) are serializer-neutral: data
302
+ // that lint and lexicons read from the entity map, never output. Keeping
303
+ // them out of every partition means no serializer sees them and no
304
+ // "No serializer found" warning fires for their pseudo-lexicon.
305
+ if (isSecretDeclaration(entity)) continue;
297
306
  const lexicon = entity.lexicon;
298
307
  if (!partitions.has(lexicon)) {
299
308
  partitions.set(lexicon, new Map());
@@ -590,13 +599,18 @@ export interface BuildRootMergeResult {
590
599
  */
591
600
  export async function mergeBuildRootEntities(
592
601
  entities: Map<string, Declarable>,
593
- contributors: ReadonlyArray<() => Promise<BuildRootContribution>>,
602
+ contributors: ReadonlyArray<BuildRootContributor>,
594
603
  ): Promise<BuildRootMergeResult> {
595
604
  const warnings: string[] = [];
596
605
  const errors: string[] = [];
597
606
  for (const contribute of contributors) {
598
607
  try {
599
- const contribution = await contribute();
608
+ // The discovered set, read-only, so a contributor can react to what the
609
+ // project DECLARED (a committed-encrypted `declareSecret()` naming a
610
+ // ciphertext file to resolve). Contributors run in order, so this also
611
+ // carries what earlier contributors added; the merge below is still the
612
+ // only writer, which is what keeps the collision refusal meaningful.
613
+ const contribution = await contribute({ entities });
600
614
  warnings.push(...(contribution.warnings ?? []));
601
615
  for (const [name, entity] of contribution.entities) {
602
616
  if (entities.has(name)) {
@@ -785,20 +799,34 @@ async function buildFromDiscoveryResult(
785
799
  const outputs = new Map<string, string | SerializerResult>();
786
800
  for (const [lexiconName, lexiconEntities] of partitions) {
787
801
  const serializer = serializersByName.get(lexiconName);
802
+ // #1832: effect receipts are withheld from the apply-bound entity set at
803
+ // this seam — the narrowest choke point every applier's input flows
804
+ // through, since appliers consume the serialized outputs assembled here.
805
+ // Receipts ride `SerializeContext.receipts` instead, so a lexicon can
806
+ // render them for visibility (#1835) without them ever entering the
807
+ // document an applier writes (or prunes) from. The `effect()` step is the
808
+ // sole receipt writer (epic #1703, decision 3).
809
+ const { applyBound, receipts } = splitReceiptEntities(lexiconEntities);
788
810
  if (serializer) {
789
811
  const lexiconLexiconOutputs = [
790
812
  ...(outputsByLexicon.get(lexiconName) ?? []),
791
813
  ...unassignedOutputs,
792
814
  ];
793
- const serialized = serializer.serialize(lexiconEntities, lexiconLexiconOutputs, {
815
+ const serialized = serializer.serialize(applyBound, lexiconLexiconOutputs, {
794
816
  ownership: options?.ownership,
795
817
  config: options?.config,
818
+ ...(receipts.size > 0 ? { receipts } : {}),
796
819
  });
797
820
  // Collect any non-fatal serializer diagnostics into the build warnings.
798
821
  if (typeof serialized !== "string" && serialized.warnings) {
799
822
  for (const w of serialized.warnings) warnings.push(w);
800
823
  }
801
824
  outputs.set(lexiconName, serialized);
825
+ } else if (applyBound.size === 0 && receipts.size > 0) {
826
+ // A partition holding only receipts (the core factory's "chant"
827
+ // pseudo-lexicon) has nothing apply-bound to serialize; that is the
828
+ // designed shape until a lexicon materializes the receipt (#1835), not
829
+ // a missing-serializer problem.
802
830
  } else {
803
831
  warnings.push(`No serializer found for lexicon "${lexiconName}"`);
804
832
  }
@@ -0,0 +1,27 @@
1
+ apiVersion: fountain.dev/v1
2
+ kind: Environment
3
+ metadata:
4
+ name: dev
5
+ spec:
6
+ networking_type: unrestricted
7
+ env_vars:
8
+ API_URL: https://api.example.com
9
+ AWS_SECRET_ACCESS_KEY: not-a-real-secret
10
+ ---
11
+ apiVersion: fountain.dev/v1
12
+ kind: Vault
13
+ metadata:
14
+ name: staging
15
+ spec:
16
+ secrets:
17
+ - key: API_URL
18
+ value: https://staging.example.com
19
+ ---
20
+ apiVersion: fountain.dev/v1
21
+ kind: Agent
22
+ metadata:
23
+ name: researcher
24
+ spec:
25
+ model: anthropic/claude-sonnet-4-6
26
+ runtime: claude
27
+ environment: dev
@@ -0,0 +1,16 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: web
5
+ spec:
6
+ selector:
7
+ matchLabels:
8
+ app: web
9
+ template:
10
+ metadata:
11
+ labels:
12
+ app: web
13
+ spec:
14
+ containers:
15
+ - name: web
16
+ image: nginx:1.27