Status: Accepted
Date: 2026-10-11
Deciders: Stijn Dejongh (owner; ruled on the model and the vocabulary on 2026-10-11).
Technical Story: the layout question on
spec-kitty-open-packs#32
(seeding the built-in pack into open-packs for #3504).
Related: #5961 and
#5992 (pack-tier spelling), #6048 (implementation),
#6046 (requires_core, another pack.yaml field).
Context and Problem Statement
A Charter Pack's level is never declared. It is inferred from how the pack was wired in:
- the built-in pack is the directory
resolve_pack_root("built-in")finds; - an org pack is a directory listed under
charter_packs.org.packsin.kittify/config.yaml; - a project pack is the directory under
.kittify/charter-packs/.
Only the built-in pack has a pack.yaml (PackDescriptor: pack_id, pack_version,
parent_pack, name). Org packs such as qa are recognised by the files they hold
(org-charter.yaml, drg/fragment.yaml). The validator infers what to expect from those files.
That inference broke as soon as packs started living outside the repository that uses them.
Seeding the built-in pack into spec-kitty-open-packs (#32) showed it:
- placed under
packs/, the built-in pack was registered as an org pack by that repository's CI; doctor charter-packsthen failed, because it had nodrg/fragment.yaml;- it also shadowed the CLI's own built-in layer.
Nothing in the pack said what it was, so nothing could refuse the mistake.
The level names themselves have no single owner either. Code spells the built-in level both
"built-in" and "builtin" (#5961, #5992), and about 93 source files carry the layer literals.
Decision Drivers
- A pack must say what it is, so a wrong placement is refused by name rather than discovered by a downstream failure.
- Reuse the vocabulary the product already speaks (config keys, glossary, paths). Do not add a third one.
- One authority for the level names, which #5961 needs anyway.
- Avoid terms that already mean something else: "root pack" already means a pack with no
parent_pack.
Considered Options
- Field name:
pack_level,pack_tier,layer. - Values:
built-in/org/project(A);core/org/project(B);base/organization/project(C);root/organisational/repository(the first proposal).
Decision Outcome
Chosen: pack_level with values built-in, org and project.
- Every Charter Pack has a
pack.yaml, and it declarespack_level. It is a required field ofPackDescriptorand must be one ofbuilt-in,orgorproject.pack_id,pack_version,parent_packandnameare unchanged. - The level names have one authority.
kernel.pack_tiersis the single authority for the three level names; the descriptor reuses itsPackTierLiteral — no second enum is introduced. Every layer literal, including the former no-hyphen"builtin"provenance spelling (#5961, #5992), derives from it (this mission performs that fold). - Loading refuses a mismatch. A pack whose declared
pack_leveldisagrees with where it was configured is refused withPACK_LEVEL_MISMATCH, naming the file, the declared level and the configured one. Apack.yamlwithout the field is refused withPACK_LEVEL_MISSING, after the migration window described below. - The validator branches on the declared level, not on the files it finds. For example,
drg/fragment.yamlandorg-charter.yamlare expected oforgpacks, and the sharded*.graph.yamlplus a generatedpack-manifest.yamlof thebuilt-inpack. - Migration. An upgrade migration writes
pack.yaml(minting apack_id) withpack_level: projectfor an existing project pack. For one release, an org pack without apack.yamlloads with a deprecation warning that names the file to add. After that it is refused.
Rejected:
root/organisational/repository. "root pack" already means a pack with noparent_pack.organisationalfails the US-spelling docs gate and disagrees with the config keyorg.repositoryclashes withprojectand with "repository root checkout".pack_tierandlayer. "Tier" is overloaded (model tiers, tiered rigour).layeris overloaded by the architectural layer rules.- Values B and C. Both rename what config, paths and the glossary already say.
Consequences
Positive
- A mis-placed pack is refused at load with a named code. The open-packs layout question is
settled:
built-in/can live atpacks/built-in/, and open-packs CI registers only packs that declareorg. - #5961 gets its single owner for the tier spelling.
- The validator's expectations become explicit per level.
Negative
- Every org and project pack must gain a
pack.yaml. That needs a migration for consumer project packs and a deprecation window for org packs. - This is a
PackDescriptorschema change. Bundle it withrequires_core(#6046) so pack authors meet one schema bump, not two. - The open-packs layout fix waits for a core release that ships the field.
Provenance-spelling fold (retires NFR-003)
- This mission retires the NFR-003 provenance-spelling freeze. The earlier carve-out
language (in
kernel.pack_tiers, the pack-tier gate docstring, and the first draft of this ADR) asserted that the fold had not happened and that two spellings —"built-in"(tier token) and"builtin"(provenance/layer/rank) — coexisted. That freeze is lifted: the two vocabularies become one. Every layer literal now derives fromkernel.pack_tiers, which strengthens single-authority rather than weakening it. - Cross-version persisted-value note. The fold changes the serialized provenance spelling
from
"builtin"to"built-in". This is verified safe for the current CLI: no read-back path compares a loaded value to the string"builtin", regenerated JSON and the dogfoodpack-manifest.yamlself-heal on the next write, and nothing persists the old spelling as a lookup key. The only theoretical edge is an older CLI reading a new-spelling artifact after an upgrade; no such mixed-version read path is exercised today, so no data migration ships.
Confirmation
charter pack validaterefuses a pack withoutpack_levelonce the window closes, and refuses one whose level disagrees with its configured position.- An architectural test pins that every layer literal in
src/derives from the singlekernel.pack_tiersauthority (thePackTierLiteral), not a second enum. - In open-packs, the built-in pack moves to
packs/built-in/withpack_level: built-in, andvalidate-packs.shregisters onlypack_level: orgpacks.
Amendment 2026-10-11
Decision 3 says loading refuses a level mismatch "where it was configured". That is
enforced at load time only for org packs (load_org_pack reads pack.yaml through
load_pack_descriptor and refuses a declared level other than org, and refuses an
unreadable or schema-invalid pack.yaml). For the built-in and project positions nothing refuses at load. The mismatch check
lives in the validator (validate_pack(..., expected_level=...) and
check_pack_level), which a position-aware caller must opt into; at the time of this
amendment no production caller (including charter pack validate and
spec-kitty doctor charter-packs) passes expected_level, so on a bare path the
mismatch check is skipped fail-closed and only PACK_LEVEL_MISSING and the schema
checks run. This amendment records where the check runs today; it adds no load-time
enforcement. Wiring a position into those callers is follow-up work.