gzkit-internal config surface — design record¶
Status: PROPOSAL, now feeding
ADR-0.39.0. Nothing here is built. The operator excepted ADR order on 2026-09-20 so the ADR could be authored from this record rather than waiting behind four unlanded feature ADRs. It is not an ADR, initiates no OBPI, and moves no file. Operator ruling required before any of it lands.Figures are a dated observation of the 2026-09-20 tree and are ILLUSTRATIVE (
AGENTS.md§ Governance doctrine surfaces). Re-runconfig-derivation-2026-09-20-evidence/census.py.
Why¶
Operator, 2026-09-20, verbatim:
gzkit lacks a comprehensive and all-encompassing config system like airlineops had, we need to adopt this as a pre-requisite for 1.0. this is inexcusable. there are so many janky and undocumented rules, thresholds and pseudo-settings handing around gzkit. it is a travesty
And, scoping it:
I want a single configuration surface for the whole project. implementers would develop their own, so this is a 'gzkit-interal' config.
That second sentence is the load-bearing one. This surface is what gzkit
reads about itself. It is not what an adopter gets: gz init scaffolds an
adopter's own config, and an adopter's instance is theirs to own. The two must
not be conflated, because gzkit is simultaneously a product and its own first
consumer — the confusion this whole campaign exists to unwind.
Operator rulings this record implements¶
| # | Ruling |
|---|---|
| 1 | Scope is the data/ registries and module constants plus .gzkit.json — everything gzkit reads about itself |
| 2 | Shape is entry point + categorical subdirectories, the ../airlineops/config model |
| 3 | The waiver / grandfather / baseline rosters are a separate class — debt state, not settings |
| 4 | Migrate readers once, after the surface lands — not before |
The shape (ruling 2)¶
../airlineops/config/README.md states five principles; they are adopted
verbatim as this surface's contract:
- Single Entry Point — one file is THE config; code calls one loader.
- Categorical Subdirectories — every file belongs to a category; no loose files at root.
- No Parallel Systems — one loader, one source of truth.
- Config != Documentation — if no code imports it, it belongs in
docs/. - No Duplication — each value has ONE authoritative location.
config/
├── settings.json <- THE entry point: scalars inline, categories indexed
├── settings.local.json <- optional operator overrides, gitignored
├── catalogs/ <- reference data gzkit reads
├── governance/ <- campaign identity, check scopes, dispatch
├── surfaces/ <- what gzkit writes and delivers
└── thresholds/ <- tunable numbers
debt/ <- SEPARATE CLASS (ruling 3), not config
├── ratchet.json <- the index, today data/waiver_ratchet_registry.json
└── <29 roster files>
Merge order: packaged defaults → config/settings.json →
config/settings.local.json (optional). Same as airlineops. A local override
never enters git and never satisfies a gate.
The mapping — 48 files placed¶
Debt class — 29 files, OUT of the settings surface (ruling 3)¶
These record measured debt under shrink-only ratchets. Nobody tunes them; they
shrink as debt is repaid, and every one is already indexed by
waiver_ratchet_registry.json with a declared honesty mechanism. Treating them
as settings would invite editing them as settings, which is precisely the
laundering ADR-0.0.73 Boundary Invariant #8 forbids.
adversarial_validation_grandfather · advisory_scorecard_grandfather ·
behave_coverage_waivers · chores_layout_waivers ·
config_derivation_grandfather · direct_data_reach_grandfather ·
distribution_baseline_manifest · exemption_control_grandfather ·
fidelity_presence_grandfather · foundation_grandfather ·
ghi_cross_reference_baseline · handoff_section_grandfather ·
historical_self_close_waivers · interview_transcript_waivers ·
ledger_vocabulary_grandfather · mechanical_witness_grandfather ·
module_constant_grandfather · module_size_grandfather ·
persona_grandfather · population_control_grandfather ·
release_tag_reachability_grandfather · req_kind_grandfathering ·
sensitivity_floor_grandfather · support_proof_grandfather ·
surface_weight_waivers · tautological_test_baseline ·
tautological_test_waivers · uncalled_gate_grandfather ·
validator_reachability_grandfather
Settings surface — 19 files in four categories¶
| Category | Files |
|---|---|
governance/ |
active_campaign · check_scope_membership · check_step_scopes · check_step_concurrency · mandated_tier1_dispatch |
thresholds/ |
audit_thresholds · eval_feedback_thresholds · instructions_files_budget · surface_weight_floor |
surfaces/ |
agents_md_survival_declaration · vendor-manifest · transcribed_count_surfaces · security_surfaces |
catalogs/ |
frontier_model_cards · exemplar_corpus · advisor_archetype_rules · flags |
Two are indexes rather than settings and become part of the entry point
itself: config_registry.json (which registry is owned by whom) and
waiver_ratchet_registry.json (which roster carries which honesty mechanism —
moves to debt/ratchet.json with its class).
.gzkit.json (ruling 1)¶
GzkitConfig — VendorConfig, PathConfig, ArbConfig, AuthorshipConfig,
SmokeConfig — is the adopter-facing schema, and it stays that. What folds
in is gzkit's own instance of it: gzkit reads its own vendors, paths and
authorship from the one surface like everything else. An adopter's .gzkit.json
is untouched, because an adopter's config is theirs.
The 56 module constants¶
Not enumerated here, because deciding 56 rows individually is the work, not the design. The triage rule:
- A value an operator could reasonably want different → settings. Budgets, ceilings, review ages, cadences.
- A value that would break correctness if changed → code, with a comment. Parser depths, fence widths, buffer sizes.
- A timeout → settings, because every timeout is environment-dependent and
DEFAULT_TIMEOUT_SECONDSalready disagrees with itself across two modules (30.0 and 3.0) with nothing reconciling them.
data/module_constant_grandfather.json already rosters all 56 shrink-only, so
the triage can proceed one constant at a time against a list that cannot grow.
Addressing — the one open question this record does not settle¶
gzkit.registries.load_registry(project_root, name) exists and 1 of 50 readers
uses it. When files move into categories, name must mean something.
Option A — logical keys. load_registry(root, "thresholds.audit"); the entry
point maps key → path. Files can move forever without touching a caller. Costs a
rename of every call site at migration time, which ruling 4 already schedules.
Option B — filenames forever. load_registry(root, "audit_thresholds.json");
the entry point maps filename → path. Callers never change again, but the
surface's addresses stay the filenames of a layout it is meant to replace.
Recommendation: A. Ruling 4 already has every reader migrating once, so the rename is free at exactly the moment it is cheapest, and B permanently encodes the pre-config layout in the post-config API.
RULED 2026-09-20 — logical keys (Option A). Operator verbatim: "logical keys, and author 0.39.0 now — I'll except ADR order". The question is closed;
load_registry(root, "thresholds.audit")is the shape, and the entry point owns the key → path mapping.
Migration plan (ruling 4)¶
- Land this design as
ADR-0.39.0when ADR order permits, or by operator exception. This record is the input, not the decision. - Build the entry point and extend
load_registryto resolve through it. - Move the 19 settings files into categories; move the 29 debt files out.
- Migrate the 49 remaining readers once, to whichever addressing wins.
- Triage the 56 constants against the roster, which may only shrink.
- Retire
data/or leave it for genuine data — a decision this record defers.
What is already fenced, so the surface is not racing decay¶
Three shrink-only ratchets landed 2026-09-20 and hold the line while this waits: derivation (GHI #1066), module constants (#1066), and direct reads (#1067). New unsourced registries, new hardcoded thresholds and new parallel readers all fail closed today. The surface is owed; the bleeding is stopped.
What this record does not claim¶
- It does not claim the 19/29 split is correct at the row level. Two files are indexes rather than settings and are called out; others may be miscategorised, and category membership is cheap to change before anything moves.
- It does not decide whether
data/survives. - It does not estimate effort. The campaign's § 7 arithmetic covers OBPI work, and no OBPI has been authored for this.