You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#2172 (issue #2160) added the claimed-field set: packages/core/src/claimed-fields.ts derives, from a declaration's own props, every path it ever set. A live value on a path outside that set now reports correctly instead of being read as drift. (#2181 is renaming that #2172 bucket from "held elsewhere" to "unclaimed": DeepDiffResult.unclaimed, heading UNCLAIMED. Using "unclaimed" throughout this issue.)
Fountain is not one of the three cloud lexicons #2172's PR body names directly, but its noise rules follow the same shape on purpose. Its own module doc (lexicons/fountain/src/deep-observe-hooks.ts:9-17) says it "follows the GCP precedent" because a fountain REST payload never says who wrote a field either, so this lexicon carries the same category of hand-written provider-default table and the same followup applies. This issue is that per-lexicon check; nothing here has been shrunk yet. Pruned and unclaimed report differently: pruned means the field never reaches the tree at all, unclaimed means it is reported with its live value under the UNCLAIMED heading. That is why shrinking this table changes what a user sees on a clean estate, not just what chant computes internally.
What is in the file
fountainDeepNormalizationHooks.prune (lexicons/fountain/src/deep-observe-hooks.ts:214-244) checks, in order:
FOUNTAIN_SERVER_FIELDS (lines 69-74): SERVER_FIELDS (imported from ./import/parser) plus secret_count, agent_count, acp. Pruned on both sides unconditionally, regardless of declared state (checked line 216).
FOUNTAIN_KIND_SERVER_FIELDS has a second consumer: lexicons/fountain/src/export-resources.ts:35,48-50 reuses it to build the READ_ONLY map that strips these same fields out of generated declarations for chant import --from. FOUNTAIN_SERVER_FIELDS is itself built from SERVER_FIELDS, which is shared with the import/export path already (module doc, lines 66-68). Grepped every other exported name here across the lexicon and packages/core: FOUNTAIN_DEFAULTS has no consumer outside this file and its test.
Candidates versus must-stay
Candidate to shrink: FOUNTAIN_DEFAULTS, all 13 entries. Each only fires when the declaration never set the property (the counterpart === "absent" gate at line 226), which claimedFieldsOfProps now answers directly.
Two entries in FOUNTAIN_DEFAULTS are worth calling out individually, since the module's own comment already argues about them:
Environment.networking_type: "unrestricted" (line 96): the doc comment (lines 85-91) calls this "the one worth arguing about", because subtracting it means an environment that never states its networking posture reports nothing, while a declaredlimited environment flipped to unrestricted live is unaffected (it already has a declared counterpart and reports as drift today, unchanged). Removing this entry is the exact motivating scenario from lifecycle: the claimed-field set — a live value on a property you never declared is theirs, not drift #2160's epic: an undeclared value that quietly changes stops being invisible and starts being reported. It looks like the single best candidate in this file to remove first, and the one most worth a dedicated test.
Webhook.status: "active" (line 122): the comment (lines 117-121) explains it is deliberately in this table rather than in a server-field table so that disabled "survives as undeclared" (unclaimed, in current terms) while a healthy active webhook stays silent. Removing the entry does not change that asymmetry (a disabled webhook already reports today); it only makes the boring active case visible too, as UNCLAIMED: status = active on every clean read. Lower value than networking_type but still a legitimate candidate.
Must stay:
FOUNTAIN_SERVER_FIELDS and FOUNTAIN_KIND_SERVER_FIELDS: pruned on both sides unconditionally, not gated on absence at all, so the claimed-field set has nothing to say about them. FOUNTAIN_KIND_SERVER_FIELDS additionally feeds export-resources.ts's own read-only list; removing an entry here would also let generated code declare a field the API never accepts.
The empty-secrets-array rule (line 222): declared-side envelope pruning (an authored-but-empty list has no live counterpart to compare against), the same direction as GCP's CNRM envelope rule and AWS's blind-spot table. The claimed-field set only classifies live values, so it does not touch this.
The name rule (line 232): not a provider default; it exists because the live read is looked up by the declared name, so an undeclared name here is chant reading back its own lookup key, not a foreign value.
Cannot fully classify from the code alone: the null/{} rule (line 239). Like GCP's identical rule, it is not a per-kind table, it fires on any undeclared live field regardless of kind, and removing it would report every genuinely-unset field (a null description on any kind, an unset map column) as UNCLAIMED: null on every clean read. That reads closer to must-stay than to the five named FOUNTAIN_DEFAULTS kinds, but it needs the same real-estate check as the rest before anyone decides.
Verification before merging
Run chant lifecycle diff --live against a real fountain instance covering at least one Environment (for networking_type, setup_script, repositories), one Vault (description), one Agent (description, system, skills, sandbox_mode), one Schedule (enabled, one_off), and one Webhook (description, status, event_types), each declared with the relevant property left unset, before and after removing an entry. For networking_type specifically, also confirm a fresh environment created with no networking posture stated still passes FTN010's lint-time build finding (the doc notes that check is separate and stays a build finding, not drift, regardless of this table).
Each FOUNTAIN_DEFAULTS entry removed has a test in lexicons/fountain/src/deep-observe.test.ts asserting the field reports unclaimed with its live value, including a dedicated test for networking_type widening to unrestricted mirroring feat(core): the claimed-field set, and a live value chant never set stops being drift #2172's own egress-allowlist test for the same lexicon.
FTN010's build-time networking-posture check still passes unchanged if networking_type moves.
No entry moves without a matching real-estate run named in the PR (fountain instance, resource kinds, fields checked).
No KNOWN_FAILURES entry added for any lexicon.
lexicons/fountain/src/deep-observe.test.ts passes in full.
FOUNTAIN_SERVER_FIELDS, FOUNTAIN_KIND_SERVER_FIELDS and the empty-secrets/name rules are untouched, or the PR explains why one of them turned out to be coverable after all.
#2172 (issue #2160) added the claimed-field set:
packages/core/src/claimed-fields.tsderives, from a declaration's own props, every path it ever set. A live value on a path outside that set now reports correctly instead of being read as drift. (#2181 is renaming that #2172 bucket from "held elsewhere" to "unclaimed":DeepDiffResult.unclaimed, headingUNCLAIMED. Using "unclaimed" throughout this issue.)Fountain is not one of the three cloud lexicons #2172's PR body names directly, but its noise rules follow the same shape on purpose. Its own module doc (
lexicons/fountain/src/deep-observe-hooks.ts:9-17) says it "follows the GCP precedent" because a fountain REST payload never says who wrote a field either, so this lexicon carries the same category of hand-written provider-default table and the same followup applies. This issue is that per-lexicon check; nothing here has been shrunk yet. Pruned and unclaimed report differently: pruned means the field never reaches the tree at all, unclaimed means it is reported with its live value under theUNCLAIMEDheading. That is why shrinking this table changes what a user sees on a clean estate, not just what chant computes internally.What is in the file
fountainDeepNormalizationHooks.prune(lexicons/fountain/src/deep-observe-hooks.ts:214-244) checks, in order:FOUNTAIN_SERVER_FIELDS(lines 69-74):SERVER_FIELDS(imported from./import/parser) plussecret_count,agent_count,acp. Pruned on both sides unconditionally, regardless of declared state (checked line 216).FOUNTAIN_KIND_SERVER_FIELDS(lines 146-150):Fountain::V1::Teammate(agent_id,contact,conversation,last_turn,presence,preview,unread,usage_total, 8),Fountain::V1::Schedule(agent_id,last_conversation_id,last_error,last_run_at,next_run_at, 5),Fountain::V1::Webhook(consecutive_failures,disabled_at,disabled_reason, 3). Also unconditional, both sides (checked line 217).secrets-array rule (line 222): unconditional, both sides, fires on any path ending insecretswhose value is[].namerule (line 232): counterpart-gated live+absent (the gate is line 226), fires only on the top-levelnamepath.{}rule (line 239): same gate, fires on any undeclared live path whose value isnullor an empty object, regardless of kind.FOUNTAIN_DEFAULTS(lines 93-130):Fountain::V1::Environment(setup_script,networking_type,repositories, 3),Fountain::V1::Vault(description, 1),Fountain::V1::Agent(description,system,skills,sandbox_mode, 4),Fountain::V1::Teammate(none),Fountain::V1::Schedule(enabled,one_off, 2),Fountain::V1::Webhook(description,status,event_types, 3). 13 entries across 5 non-empty kinds, same gate as above (checked line 242).FOUNTAIN_KIND_SERVER_FIELDShas a second consumer:lexicons/fountain/src/export-resources.ts:35,48-50reuses it to build theREAD_ONLYmap that strips these same fields out of generated declarations forchant import --from.FOUNTAIN_SERVER_FIELDSis itself built fromSERVER_FIELDS, which is shared with the import/export path already (module doc, lines 66-68). Grepped every other exported name here across the lexicon andpackages/core:FOUNTAIN_DEFAULTShas no consumer outside this file and its test.Candidates versus must-stay
Candidate to shrink:
FOUNTAIN_DEFAULTS, all 13 entries. Each only fires when the declaration never set the property (thecounterpart === "absent"gate at line 226), whichclaimedFieldsOfPropsnow answers directly.Two entries in
FOUNTAIN_DEFAULTSare worth calling out individually, since the module's own comment already argues about them:Environment.networking_type: "unrestricted"(line 96): the doc comment (lines 85-91) calls this "the one worth arguing about", because subtracting it means an environment that never states its networking posture reports nothing, while a declaredlimitedenvironment flipped tounrestrictedlive is unaffected (it already has a declared counterpart and reports as drift today, unchanged). Removing this entry is the exact motivating scenario from lifecycle: the claimed-field set — a live value on a property you never declared is theirs, not drift #2160's epic: an undeclared value that quietly changes stops being invisible and starts being reported. It looks like the single best candidate in this file to remove first, and the one most worth a dedicated test.Webhook.status: "active"(line 122): the comment (lines 117-121) explains it is deliberately in this table rather than in a server-field table so thatdisabled"survives as undeclared" (unclaimed, in current terms) while a healthyactivewebhook stays silent. Removing the entry does not change that asymmetry (adisabledwebhook already reports today); it only makes the boringactivecase visible too, asUNCLAIMED: status = activeon every clean read. Lower value thannetworking_typebut still a legitimate candidate.Must stay:
FOUNTAIN_SERVER_FIELDSandFOUNTAIN_KIND_SERVER_FIELDS: pruned on both sides unconditionally, not gated on absence at all, so the claimed-field set has nothing to say about them.FOUNTAIN_KIND_SERVER_FIELDSadditionally feedsexport-resources.ts's own read-only list; removing an entry here would also let generated code declare a field the API never accepts.secrets-array rule (line 222): declared-side envelope pruning (an authored-but-empty list has no live counterpart to compare against), the same direction as GCP's CNRM envelope rule and AWS's blind-spot table. The claimed-field set only classifies live values, so it does not touch this.namerule (line 232): not a provider default; it exists because the live read is looked up by the declared name, so an undeclarednamehere is chant reading back its own lookup key, not a foreign value.Cannot fully classify from the code alone: the null/
{}rule (line 239). Like GCP's identical rule, it is not a per-kind table, it fires on any undeclared live field regardless of kind, and removing it would report every genuinely-unset field (a null description on any kind, an unset map column) asUNCLAIMED: nullon every clean read. That reads closer to must-stay than to the five namedFOUNTAIN_DEFAULTSkinds, but it needs the same real-estate check as the rest before anyone decides.Verification before merging
Run
chant lifecycle diff --liveagainst a real fountain instance covering at least oneEnvironment(fornetworking_type,setup_script,repositories), oneVault(description), oneAgent(description,system,skills,sandbox_mode), oneSchedule(enabled,one_off), and oneWebhook(description,status,event_types), each declared with the relevant property left unset, before and after removing an entry. Fornetworking_typespecifically, also confirm a fresh environment created with no networking posture stated still passes FTN010's lint-time build finding (the doc notes that check is separate and stays a build finding, not drift, regardless of this table).Ref #2160, #2172, #2181.
Acceptance
FOUNTAIN_DEFAULTSentry removed has a test inlexicons/fountain/src/deep-observe.test.tsasserting the field reports unclaimed with its live value, including a dedicated test fornetworking_typewidening tounrestrictedmirroring feat(core): the claimed-field set, and a live value chant never set stops being drift #2172's own egress-allowlist test for the same lexicon.networking_typemoves.KNOWN_FAILURESentry added for any lexicon.lexicons/fountain/src/deep-observe.test.tspasses in full.FOUNTAIN_SERVER_FIELDS,FOUNTAIN_KIND_SERVER_FIELDSand the empty-secrets/namerules are untouched, or the PR explains why one of them turned out to be coverable after all.