Skip to content

fountain: the defaults table the claimed-field set now covers #2187

Description

@lex00

#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 (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).
  • The empty-secrets-array rule (line 222): unconditional, both sides, fires on any path ending in secrets whose value is [].
  • The name rule (line 232): counterpart-gated live+absent (the gate is line 226), fires only on the top-level name path.
  • The null/{} rule (line 239): same gate, fires on any undeclared live path whose value is null or 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_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 declared limited 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).

Ref #2160, #2172, #2181.

Acceptance

  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions