Skip to content

feat(config)!: report unknown runtime config fields - #244

Draft
lan17 wants to merge 5 commits into
mainfrom
claude/config-unknown-field-metric
Draft

lan17 wants to merge 5 commits into
mainfrom
claude/config-unknown-field-metric

Conversation

@lan17

@lan17 lan17 commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Problem

A key config field that DialCache does not know is ignored on main in every port, with no metric and no log. A misspelled emergency switch such as ramp: { remtoe: 0 } keeps Redis serving, and nothing tells the operator why. Review of #138 found the same case for its logging group on 2026-08-15. Every other bad input already records config_resolution when a call reads it. No issue exists; the gap came up while #242 was being designed.

Change

This is a Portable behavior change: it adds a CONTRACTS.md obligation that every port mirrors. The record is design/2026-10-24-unknown-config-field-metric.md, and this pull request holds only the record until every decision is accepted.

Two decisions are confirmed in session:

  • D1: instrument instead of validating strictly. In runtime config, unknown fields stay ignored and invalid values keep main's per-leaf consequences. Each enabled call whose provider reply carries an unknown field records one error{layer: noop, error: config_unknown_field} event, and a bounded warning names the field paths.
  • D6: static defaults stay exactly as on main, neither checked nor reported. They are typed code the service owns, so checking them buys little. An earlier same-day choice to make them throw is recorded under D6's Rejected.

The record also covers:

  • the Quint policy-profile codes 26 to 30;
  • the replay projection change, with no driver changes;
  • the planned invariants, witnesses and mutants;
  • the AGENTS.md Critical behavior bullet the implementation adds.

The record replaces the strict-validation plan that #242's D14 pointed to. D14's choice still holds: #242 adds no unknown-field reporting.

Open questions

Each decision below is Open in the record. The recommendation is the record's Choice.

  1. D2, which names count: unknown keys at the top level, in the ttlSec and ramp maps, in shadow, and in shadow.mismatchLogging once feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242 lands. A key counts whatever its value. shadowRamp keeps its whole-call failure. A Python field supplied under both spellings counts once, as unknown. Recommended.
  2. D3, the signal: a new config_unknown_field error kind on noop, plus one warning per use case. The warning names up to eight paths and repeats only when that use case's set of unknown paths changes. Recommended over reusing config_resolution or adding a field-name label.
  3. D4, cadence: whenever a call reads runtime config, as you asked: one event per enabled call whose provider reply carries an unknown field, recorded before traversal and before joining a flight. Still to confirm: a reply that fails as a whole records only config_resolution, because a TypeScript provider calling new DialCacheKeyConfig(badJson) throws before DialCache sees the fields.
  4. D5, constructors that drop fields: the TypeScript DialCacheKeyConfig constructor and Python Policy keep a private marker, so provider replies built from parsed JSON still report. Recommended.
  5. D7, evidence: a portable obligation in the existing policy profile, with a behaviorVersion bump, at the Full tier. Recommended over native-only tests.
  6. D8, rollout: a release note, plus an upgrade note telling operators to route the new error kind before upgrading. The only breaking effect is Rust's exhaustive ErrorKind gaining a variant. The same change adds #[non_exhaustive] to ErrorKind and DisabledReason. Before 1.0, feat and feat! both bump the minor version. Recommended.
  7. D9, runtime overhead: no per-call key walk. Typed Go and Rust configs never check. A TypeScript DialCacheKeyConfig or Python Policy answers from construction. Raw Go, Rust and Python maps compare their size with the known keys the merge already looks up. TypeScript plain objects are remembered in a WeakMap. Only a config that carries an unknown field pays to name its paths. Recommended.
  8. Ordering with feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242: wait for feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242 to merge before implementing, because it edits config.ts, runtime-config.ts and dialcache.ts and adds the mismatchLogging group (recommended), or stack on its branch.
  9. feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242's D14 wording: its reason names a strict record that will not exist. Whether to reword D14 in feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242 is left to you; this pull request does not touch feat(shadow): log the paths of mismatching fields in confirmed-mismatch warnings #242.

Validation

Head: 9cfb3b5

  • PR CI on 9cfb3b5: pending at the time of writing. Every commit touches one Markdown file under design/, which no ledger pins.

Not run: the Full tier (D7) applies to the implementing commits, which wait for the design gate.

Follow-ups

  • Possible port drift: while building this record's inventory, a code reading suggested that Go (go/engine.go) and Rust (rust/src/execution.rs) record the stale-age config_resolution only in the coalescing leader, while TypeScript and Python record it for every caller. This is unconfirmed by a test, and no issue is filed yet.

BREAKING CHANGE: Rust's public ErrorKind enum gains the ConfigUnknownField variant, so a Rust caller that matches every variant must add an arm. ErrorKind and DisabledReason also become #[non_exhaustive], so later kinds will not break callers again. TypeScript, Go and Python gain only a new config_unknown_field error label value, and static defaults behave as before.

lan17 added 2 commits October 24, 2026 23:57
Unknown fields in a key config are ignored on main with no signal, so a
misspelled kill switch such as ramp.remtoe keeps serving silently. The
record keeps main's per-leaf fail-open rules, as the maintainer chose over
strict validation, and proposes one config_unknown_field diagnostic per
observed config, with every other decision open at the design gate.
@codecov-commenter

codecov-commenter commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.73%. Comparing base (b4e4fba) to head (9cfb3b5).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #244   +/-   ##
=======================================
  Coverage   93.73%   93.73%           
=======================================
  Files          78       78           
  Lines       11357    11357           
  Branches      656      656           
=======================================
+ Hits        10645    10646    +1     
+ Misses        642      641    -1     
  Partials       70       70           
Flag Coverage Δ
go 91.17% <ø> (+0.05%) ⬆️
python 92.08% <ø> (ø)
rust 94.29% <ø> (ø)
typescript 96.56% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ck cheap

The maintainer chose in session that a static default with an unknown field
fails closed, throwing where an invalid static value throws today (D6), and
asked for no runtime overhead. Only provider replies now report, once per
enabled call that reads one (D4), and the new D9 keeps the check off the hot
path: typed configs never check, constructed configs answer from
construction, raw maps compare their size with the known keys the merge
already looks up. The change is now breaking in every port (D8).
@lan17 lan17 changed the title feat(config): report unknown runtime config fields feat(config)!: report unknown runtime config fields and reject them in static defaults Oct 25, 2026
lan17 added 2 commits October 25, 2026 01:01
The maintainer chose in session to drop static defaults from the change:
they are typed code the service owns, so checking them buys little, and the
throw would have stopped services whose untyped defaults run today. D6 now
leaves them exactly as on main, the earlier fail-closed choice moves to
Rejected, and the only breaking effect left is Rust's new ErrorKind variant.
@lan17 lan17 changed the title feat(config)!: report unknown runtime config fields and reject them in static defaults feat(config)!: report unknown runtime config fields Oct 25, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants