Validation report — S2-01 SkillsLoader three-tier merge with O_NOFOLLOW + body byte-offset¶
Story: ../S2-01-skills-loader.md
Validated: 2026-05-15
Validator: phase-story-validator (scheduled task: story-validation-corrector)
Verdict: HARDENED
Summary¶
The story ships src/codegenie/skills/ (three new files + a chokepoint extension to codegenie.hashing): the Skill Pydantic model (frozen=True, extra="forbid", ADR-0033-typed identifiers), the SkillsLoadError discriminated union, and SkillsLoader with __init__-pure-data + three-tier first-tier-wins merge + O_NOFOLLOW open + safe_yaml.load chokepoint reuse + progressive-disclosure body hashing. All references trace cleanly to 02-ADR-0007 (kernel-only scaffolding, no plugin loader), arch §"Component design" #9, arch §"Scenarios" Scenario 3, arch §"Edge cases" rows 8/9/16, production ADR-0033 §1 + §3–4 (newtype + sum types), CLAUDE.md §"Conventions to follow" (["*"] wildcard), Phase 0 ADR-0001 (hashing chokepoint), and Phase 1 ADR-0006 (safe_yaml chokepoint).
The draft was structurally sound — three-tier order is correct, security commitments are present, safe_yaml chokepoint discipline is preserved, ADR-0007's "no plugin loader" boundary respected — but had three block-tier executor-halt risks and a dozen harden-tier gaps that would have let a wrong implementation slip past the executor's Validator pass. The block-tier risks all collapse to "the prescribed implementation literally cannot be written as described against the verified repo state."
Stage 3 research skipped — no NEEDS RESEARCH findings. Every gap was answerable from arch + ADR-0007 + ADR-0033 + ADR-0001 + S1-04 / S1-03 validation precedents + verified repo state (src/codegenie/hashing.py, src/codegenie/parsers/safe_yaml.py, src/codegenie/tccm/loader.py, src/codegenie/types/identifiers.py).
Twelve ACs added, six ACs strengthened, one section of design-pattern Notes appended. Implementation outline rewritten to specify the hashing-chokepoint extension and the typed-tier discipline. Story is now ready for phase-story-executor.
Context Brief (Stage 1)¶
Story snapshot¶
- Goal as written: Ship
src/codegenie/skills/(__init__.py,model.py,loader.py) withSkillPydantic model (frozen, extra="forbid"),SkillsLoader(pure-data__init__, first I/O isload_all()), three-tier first-tier-wins merge,O_NOFOLLOWper-file open,safe_yaml.loadchokepoint reuse, body byte-offset-recorded progressive disclosure (tracemallocpeak < 20 KB on a 100 MB-body fixture), andfind_applicable(evidence_keys)monotonicity. - Non-goals: Layer D
SkillsIndexProbe(S6-01); per-tier signing (Phase 14);ConventionsCatalogLoader(S2-02); reference TCCM roundtrip (S2-03);safe_yaml.loads_bytesshim (rule of three until S2-02 / S2-03 materialize a second caller); skills body content rendering.
Phase 2 exit criteria touched¶
- G9 — kernel scaffolding ships, no plugin loader (02-ADR-0007). ✓
- Three-tier merge with first-tier-wins + loud
skill_shadowedwarning (arch §"Edge cases" row 16). ✓ - Progressive disclosure (commitment §2.7; tracemalloc-pinned). ✓
safe_yaml.loadchokepoint preserved (Phase 1 ADR-0006). ✓
Load-bearing commitments touched¶
- CLAUDE.md §"No LLM anywhere in the gather pipeline" — loader is deterministic, no LLM. ✓
- CLAUDE.md §"Facts, not judgments" —
Skillis data; the Planner (Phase 4+) decides applicability. ✓ - CLAUDE.md §"Honest confidence" — N/A (no confidence rendering in this story).
- CLAUDE.md §"Extension by addition" — three-tier order is ADR-amend-gated; new tier adds (doesn't edit). ✓
- CLAUDE.md §"Conventions to follow" —
["*"]wildcard forapplies_to_*(was missing from draft). - 02-ADR-0007 §Decision/§Consequences —
SkillsLoaderis kernel-side;O_NOFOLLOW+ three-tier merge +safe_yaml.loadchokepoint. ✓ - Phase 0 ADR-0001 §"single source of truth for hashing" — implicated by streaming-hash requirement; original story would have violated.
- Phase 1 ADR-0006 §"
safe_yamlchokepoint" — preserved via tempfile dance. ✓ - ADR-0033 §1 (newtypes) —
Skill.applies_to_tasks: list[str]was primitive-obsession; fixed tolist[TaskClassId]. - ADR-0033 §3 (sum types) —
_TIER_NAME: dict[int, str]was tag-and-dispatch overint; fixed toTier: Literal[...]. - ADR-0033 §4 (illegal states unrepresentable) —
SkillsLoadErrordiscriminated union is correctly typed; story's choice to diverge from S1-04's marker pattern is justified (partial-success multi-file semantics).
Sibling-family lineage¶
- Third loader-shaped consumer in Phase 2 after
safe_yaml.load(Phase 1 — single-file, raises) andTCCMLoader(S1-04 — single-file, marker exception + string-prefix reason). - First multi-file partial-success loader. This story sets the convention for S2-02 (
ConventionsCatalogLoader) and any future Phase 4+ loader of the same shape. Convention documented in Notes-for-implementer. - Rule-of-three threshold for shared loader-kernel: NOT YET REACHED.
safe_yaml,TCCMLoader,SkillsLoaderhave meaningfully different shapes (one parser, two loaders; two single-file, one multi-file; one raises, two returnResult). No single abstraction would compress all three without coupling unrelated concerns. Sharing is structural viasafe_yaml.load(the YAML chokepoint) andparsers/_io.py::open_capped(theO_NOFOLLOW+size-cap chokepoint), already established. - Rule-of-three threshold for hashing chokepoint: REACHED on extension.
content_hash(path)+content_hash_bytes(b)+content_hash_of_inputs(paths)already exists;content_hash_fd(fd, offset, size)is the natural fourth, additive, no edit to existing functions.
Goal-to-AC trace¶
- AC-1 → goal: YES (module surface,
__all__, discriminated union shape). - AC-2 → goal: STRENGTHENED (constructor purity; expanded I/O monkeypatch set to catch
Path.iterdir/Path.globsmuggling). - AC-3, AC-3a → goal: STRENGTHENED (full-field assertion; missing-tier silent skip).
- AC-4, AC-4a → goal: STRENGTHENED (added middle-tier-wins case to catch
_TIERSindexing mutation). - AC-5 → goal: STRENGTHENED (asserted no leakage of symlink-target contents).
- AC-6, AC-6b, AC-6c, AC-6d → goal: ADDED (umbrella-honesty for
unsafe_yaml;FrontmatterUnterminated;SchemaViolation). - AC-7, AC-7a → goal: STRENGTHENED (exact
body_offset/body_sizefor hand-constructed fixture). - AC-8 → goal: STRENGTHENED (format regex pin replaces ambiguous "[:32]" prescription).
- AC-9 → goal: kept (frozen + extra="forbid").
- AC-10 → goal: STRENGTHENED (added JSON-shape pin per S1-04 precedent).
- AC-11, AC-11a, AC-11b → goal: STRENGTHENED (correctness paired with monotonicity; pre-
load_allstate). - AC-12 → goal: kept (runtime spy).
- AC-13 → goal: kept (
forbidden-patternsextension). - AC-14, AC-15 → goal: kept (toolchain, TDD discipline).
- AC-16 → goal: ADDED (same-tier collision).
- AC-17 → alias of AC-7a.
- AC-18 → goal: ADDED (exact
os.openflag set). - AC-19 → goal: ADDED (within-tier deterministic ordering).
- AC-20 → goal: ADDED (TOCTOU file-disappearance → typed
IoFailure). - AC-21 → alias of AC-11a.
- AC-22 → goal: ADDED (
["*"]wildcard semantics). - AC-23 → goal: ADDED (defensive copy from
find_applicable). - AC-24 → goal: ADDED (AST source-scan for YAML chokepoint).
- AC-25 → goal: ADDED (AST source-scan for hashing chokepoint; new
content_hash_fdpublic symbol). - AC-26 → goal: ADDED (tempfile cleanup verified).
Open ambiguities resolved before Stage 2¶
codegenie.hashingstreaming API — does not exist (content_hash(path)only). Resolution: extend by addition (content_hash_fd); ADR-0001 chokepoint preserved.SkillId,TaskClassId,Languagenewtypes — exist incodegenie.types.identifiers(verified). Resolution: import, do not redefine.Result[T, E]Pydantic union — exists incodegenie.result(verified; lifted in S1-04). Resolution: useOk(value=...)/Err(error=...)constructors.safe_yaml.loadsignature —(path: Path, *, max_bytes: int, max_depth: int = 64) -> Mapping[str, JSONValue]; raisesMalformedYAMLErrorfor everyyaml.YAMLErrorsubclass plus non-mapping top-level (verified). Resolution:unsafe_yamlis umbrella; AC-6b documents this honestly.["*"]wildcard semantics — CLAUDE.md §"Conventions to follow when writing the POC" pins this. Resolution: AC-22.
Stage 2 — critic reports¶
Coverage (verdict: COVERAGE-HARDEN — 9 findings)¶
Block-tier:
- C1.1 (block) — codegenie.hashing.content_hash API mismatch. Story prescribed "streaming os.read(fd, 64 * 1024) chunks through codegenie.hashing.content_hash" but the public API is content_hash(path: Path) -> str — accepts neither fd nor chunks. The hashing module's docstring forbids direct blake3 / hashlib.sha256 imports outside that file (Phase 0 ADR-0001). Implementer has no Open/Closed-compliant path to satisfy AC-7 as written. Fix: Story now commits to extending codegenie.hashing with content_hash_fd(fd, *, offset, size) -> str; new AC-25 enforces the chokepoint; Implementation outline §0 lays out the addition.
- C1.2 (block) — find_applicable(evidence_keys: set[str]) semantics under-specified. Original spec said set(applies_to_tasks) <= evidence_keys AND set(applies_to_languages) <= evidence_keys — but evidence_keys is a flat set[str] that conflates task-class identifiers and language identifiers (e.g., is "typescript" a task or a language?). CLAUDE.md prescribes ["*"] as the wildcard sentinel for applies_to_* lists — the story didn't honor it. Fix: find_applicable(evidence: EvidenceQuery) with a typed EvidenceQuery(task: TaskClassId | None, languages: set[Language]); AC-22 pins ["*"] semantics; AC-11a closes correctness; AC-11 retains monotonicity.
Harden-tier:
- C1.3 — AC-3 happy-path doesn't enforce within-tier order across filesystems. Fix: AC-19 lexicographic sort.
- C1.4 — No AC exercises FrontmatterUnterminated trigger condition. Fix: AC-6c.
- C1.5 — No AC exercises SchemaViolation reason. Fix: AC-6d.
- C1.6 — AC-3a missing (missing-tier silent skip). Fix: AC-3a.
- C1.7 — AC for find_applicable([]) before load_all() missing. Fix: AC-11b.
- C1.8 — Three-tier order doc trace (user > repo > org) — confirmed against ADR-0007 §Decision. Not a finding.
- C1.9 — No AC for body field full-population in happy path. Fix: AC-3 strengthened to assert every field.
Test quality (verdict: TESTS-HARDEN — 12 findings; 29-row mutation table)¶
Mutation table excerpts (full table below):
| # | Wrong impl | Caught by draft? | Closure |
|---|---|---|---|
| 1 | body = f.read(); blake3(body) |
YES — AC-7 tracemalloc | — |
| 2 | Swap tier order silently (org > repo > user) | YES — AC-4 | — |
| 3 | First-tier-LOSES on collision | YES — AC-4 | — |
| 4 | Drop frozen=True from Skill |
YES — AC-9 | — |
| 5 | Drop extra="forbid" from Skill |
YES — AC-9 | — |
| 6 | os.open without O_NOFOLLOW |
YES (behavioral) — AC-5 | Strengthened by AC-18 (code-level) |
| 7 | YAML parsed via yaml.safe_load directly |
YES (ripgrep) — AC-12 | Strengthened by AC-24 (AST, alias-resistant) |
| 8 | yaml.load with default Loader → !!python/object executes |
YES — AC-6 | — |
| 9 | Symlink case raises instead of returning Result.Err | YES — AC-5 | — |
| 10 | Frontmatter missing terminator → infinite loop / OOM | NO | AC-6c |
| 11 | Schema violation → exception instead of typed error | NO | AC-6d |
| 12 | find_applicable returns all skills regardless of evidence |
NO (Hypothesis monotonicity vacuous on return []) |
AC-11a |
| 13 | find_applicable returns first match only |
NO | AC-11a multi-member fixture |
| 14 | body_blake3 length/format silent change |
PARTIAL — original spec ambiguous | AC-8 regex |
| 15 | body_blake3 uses SHA-256 instead of BLAKE3 |
NO (depends on C1.1 fix) | AC-25 chokepoint |
| 16 | body_size set to total file size |
NO | AC-7a |
| 17 | body_offset off-by-one |
NO | AC-7a |
| 18 | Constructor uses Path.glob (not os.listdir) — bypasses AC-2 monkeypatch |
PARTIAL | AC-2 strengthened |
| 19 | safe_yaml.load chokepoint circumvented via from yaml import safe_load as _y |
PARTIAL — ripgrep yaml\. misses alias |
AC-24 AST |
| 20 | O_NOFOLLOW opens but follows internal-path-component symlinks |
NA — documented behavior | Notes |
| 21 | _load_one_skill propagates non-ELOOP OSError (TOCTOU) |
NO | AC-20 |
| 22 | Multiple skill_shadowed events emitted |
YES — AC-4 len() == 1 |
— |
| 23 | Tempfile leaks | NO | AC-26 |
| 24 | _TIER_NAME off-by-one (org reported as "repo") |
NO — AC-4 only tests user-vs-org | AC-4a |
| 25 | find_applicable returns mutable internal list |
NO | AC-23 |
| 26 | TOCTOU symlink-replace between rglob and open — O_NOFOLLOW defends |
— | — |
| 27 | Body hashed across closing --- line |
NO | AC-7a |
| 28 | Billion-laughs YAML — safe_yaml.load depth cap defends |
— | Confirmed via S1-03 |
| 29 | Same-tier collision silently merged | NO | AC-16 |
Additional test-quality concerns:
- Property-based monotonicity is insufficient (TQ1). Fix: AC-11a paired correctness.
- Defensive-copy on find_applicable return missing (TQ — mutation safety). Fix: AC-23.
- Constructor I/O monkeypatch incomplete (TQ — os.scandir, Path.exists). Fix: AC-2 strengthened.
Consistency (verdict: CONSISTENCY-HARDEN — 10 findings)¶
- CN1 —
SkillsLoader.__init__signature differs fromTCCMLoader.__init__(which has zero args). Both are "pure data at init" — justified asymmetry; SkillsLoader needssearch_paths(multi-file walk); TCCMLoader takespathper call (single-file). Documented in Notes (H5 in Validation notes). - CN2 (resolved) —
SkillsLoadErrorPydantic union vsTCCMLoadErrormarker. Resolution: both correct, different shapes for different use-cases. Convention documented in Notes-for-implementer. - CN3 (block→harden) — Two
os.openevents per skill (SkillsLoader-sideO_NOFOLLOW+safe_yaml.load's ownO_NOFOLLOWon the tempfile). Defense-in-depth claim is correct. Fix: AC-18 asserts explicit flag set so a contributor who deletes the SkillsLoader-sideO_NOFOLLOWfails the test before any symlink fixture runs. - CN4 — Tempfile dance documented; AC-26 added to verify cleanup on every path (parse-success and parse-failure).
- CN5 (block) — Hashing chokepoint violation (same as C1.1). Fix: Story now extends
codegenie.hashing(AC-25). - CN6 (block) —
body_blake3format ambiguous ("[:32] or project's documented BLAKE3-fingerprint width"). Fix: Pinned tor"^blake3:[0-9a-f]{64}$"; AC-8. - CN7 (block) —
unsafe_yamlcovers allMalformedYAMLErrorcauses, not justConstructorError. Fix: AC-6b documents and tests the umbrella; Notes-for-implementer explains why splitting would be wrong (intent inference from__cause__chain). - CN8 — Primitive obsession on
applies_to_*(list[str]). Fix:list[TaskClassId]/list[Language]. ADR-0033 §1. - CN9 — Three-tier order doc trace confirmed. Not a finding.
- CN10 —
_TIER_NAME: dict[int, str]is tag-and-dispatch overint. Fix:Tier: TypeAlias = Literal["user", "repo", "org"]+_TIERS: Final[tuple[Tier, ...]] = (...). ADR-0033 §3.
Design patterns (verdict: DESIGN-HARDEN — 12 findings)¶
- DP1 (block, resolved by CN5/AC-25) — Hashing chokepoint extension is the load-bearing Open/Closed application.
codegenie.hashinggrows by exactly one symbol; existing call sites unchanged. - DP2 (harden, Notes-only) —
_load_one_skilldoes seven concerns. Functional-core / imperative-shell split recommended in Notes; not promoted to AC (Rule 3 / observable-behavior-already-constrained). - DP3 (harden, AC) —
_TIER_NAMEtyping. Closed by AC-1 (Tier in__all__) + Implementation outline §1 + Notes. - DP4 (harden, Notes-only) — Three-tier order is ADR-amend-gated; documented as intentional friction. Precedent: ADR-0030 "no
Unknownvariant." - DP5 (harden, Notes) —
find_applicablematching logic moved to pure top-level_matches(skills, evidence). Implementation outline §9. - DP6 (harden, AC) — Primitive obsession on
applies_to_*. Closed by typed lists in model + Notes. - DP7 (harden, Notes-only) —
LoadOutcomeas a dataclass-shaped value object (two lists). Acceptable; documented choice. - DP8 (harden, Notes-only) — Future plugin-validation extension seam. Not in Phase 2 scope.
- DP9 (harden, AC) —
O_NOCTTYflag bit never asserted. Closed by AC-18. - DP10 (Notes-only) — Smart-constructor pattern on
LoadOutcome. Overkill for Phase 2; recorded. - DP11 — confirmed: No premature Plugin Loader. ADR-0007 respected. ✓
- DP12 — confirmed: No env-var auto-discovery; explicit
search_paths. Arch §"Anti-patterns avoided" row 11 respected. ✓
Stage 3 — research¶
Skipped. Zero findings tagged NEEDS RESEARCH. Every closure was answerable from arch + ADRs + verified repo state + S1-04 / S1-03 precedents.
Stage 4 — edits applied¶
Story header¶
- Added
Phase 0 ADR-0001to "ADRs honored" — hashing chokepoint discipline. - Added
production ADR-0033 §1— newtypes for domain primitives (in addition to §3–4 already cited). - Inserted
## Validation notes (added 2026-05-15 by phase-story-validator)block right after the metadata, summarizing all changes.
Goal block¶
- Retyped
Skill.applies_to_tasks: list[str]→list[TaskClassId]. - Retyped
Skill.applies_to_languages: list[str]→list[Language]. - Added
Tier: TypeAlias = Literal["user", "repo", "org"]to the model surface. - Added
EvidenceQuery(task: TaskClassId | None, languages: set[Language]). - Retyped
body_offset/body_sizewithAnnotated[int, Field(ge=0)]. - Pinned
body_blake3format in the model docstring. - Updated
find_applicablesignature to takeEvidenceQuery, notset[str]. - Updated
load_allreturn type toResult[LoadOutcome, FatalLoadError].
Invariants (numbered list under Goal)¶
- §3 — Rewrote streaming-hash prescription to use new
content_hash_fdchokepoint extension. Pinnedr"^blake3:[0-9a-f]{64}$"format. - §4 — Rewrote three-tier merge to use typed
TierLiteral +_TIERSconstant; added within-tier lexicographic ordering; added same-tier collision behavior. - §5 — Added
IoFailurereason for TOCTOU and other non-ELOOPOSError; clarifiedunsafe_yamlumbrella semantics. - §6 — Extended
SkillsLoadErrordiscriminated union from four to five reasons (addedio_failure); added convention note distinguishing single-file marker vs multi-file Pydantic-union pattern. - §7 — Replaced flat
set[str]semantics with typedEvidenceQuery; added["*"]wildcard rule; added defensive-copy invariant.
Acceptance criteria¶
Strengthened in place:
- AC-1 — exact-set __all__ test; five reasons.
- AC-2 — expanded I/O monkeypatch set (os.scandir, Path.exists, Path.is_dir).
- AC-3 — every-field assertion (was id-only).
- AC-4 — full structured-fields assertion on skill_shadowed.
- AC-5 — leakage assertion (no symlink-target bytes in any Skill or log).
- AC-7 — pin deterministic fixture bytes.
- AC-8 — format regex pin replaces ambiguous "[:32]".
- AC-9 — constructable form with proper typed fields.
- AC-10 — JSON-shape pin per S1-04 precedent.
- AC-11 — monotonicity rewritten against EvidenceQuery semantics.
- AC-12 — runtime spy clarified; pairs with AC-24 AST.
Added:
- AC-3a — missing-tier silent skip.
- AC-4a — middle-tier-wins (catches _TIERS indexing mutation).
- AC-6b — pure-syntax YAML also lands as unsafe_yaml (umbrella honesty).
- AC-6c — FrontmatterUnterminated trigger.
- AC-6d — SchemaViolation trigger.
- AC-7a — exact body_offset / body_size for hand-constructed fixture (off-by-one catcher).
- AC-11a — find_applicable correctness paired with monotonicity.
- AC-11b — find_applicable([]) returns [] before load_all.
- AC-16 — same-tier collision is skill_shadowed with lexicographic-first wins.
- AC-17 — alias to AC-7a (retained for cross-reference).
- AC-18 — exact os.open flag set (catches O_NOFOLLOW drop before any fixture runs).
- AC-19 — within-tier deterministic ordering across filesystems.
- AC-20 — TOCTOU file disappearance → typed IoFailure.
- AC-21 — alias to AC-11a (retained for cross-reference).
- AC-22 — ["*"] wildcard semantics per CLAUDE.md.
- AC-23 — find_applicable defensive-copy.
- AC-24 — AST source-scan for direct YAML import (replaces ripgrep AC-12).
- AC-25 — AST source-scan for direct blake3/hashlib import; new content_hash_fd public symbol.
- AC-26 — tempfile cleanup verified on every path.
Implementation outline¶
- §0 added —
codegenie.hashing.content_hash_fdchokepoint extension. - §1 rewrote —
TierLiteral +_TIERSordering constant; typed identifier imports. - §2 rewrote — discriminated union grew to five reasons;
LoadOutcomeandFatalLoadErrorPydantic models added. - §3 rewrote — pure-data init contract strengthened.
- §4 rewrote —
_TIERSzipped withsearch_paths; within-tiersorted()for AC-19; same-tier collision handling. - §5 rewrote — TOCTOU
OSErrorhandling;tempfile.mkstempinstead ofNamedTemporaryFile; chokepoint extension call. - §6 kept (frontmatter scan).
- §7 redirected to
codegenie.hashing.content_hash_fd(now lives in §0). - §8 —
find_applicablethin-wrapper around pure_matches. - §9 — pure
_matches(skills, evidence)function. - §10 (was §9) — structlog event-name literals (unchanged).
Files to touch table¶
- Added
src/codegenie/hashing.py(modify, additive —content_hash_fd). - Added
tests/unit/test_hashing.py(modify, additive — parity tests for new symbol). - Split
tests/unit/skills/test_loader.pyfromtests/unit/skills/test_no_direct_yaml_import.py(AC-24) andtests/unit/skills/test_no_direct_blake3_import.py(AC-25).
Notes for the implementer¶
Appended ### Design-pattern notes (added by validator 2026-05-15) section with eleven paragraphs:
- Hashing chokepoint extension (Open/Closed).
- Functional-core / imperative-shell split (recommended; not AC-mandated).
- Tier typing (Literal, not int indices) — ADR-0033 §3 framing.
- SkillsLoadError discriminated union vs marker — convention for forward loaders.
- Primitive obsession on applies_to_* — ADR-0033 §1 framing.
- EvidenceQuery over set[str] — illegal-states-unrepresentable framing.
- ["*"] wildcard sentinel — CLAUDE.md convention.
- Three-tier order is ADR-amend-gated — intentional friction.
- unsafe_yaml umbrella — operational honesty.
- body_blake3 format pin — fail-loud.
- No O_NOFOLLOW defense-in-depth via safe_yaml.load — load-bearing flag at the SKILL.md open.
- TOCTOU rglob is not a snapshot.
- Same-tier collisions exist too.
Final verdict¶
HARDENED. Story is ready for phase-story-executor. Twelve ACs added, six strengthened. Implementation outline rewritten to specify the hashing-chokepoint extension and the typed-tier discipline. Design-pattern Notes paragraphs added covering functional-core split, primitive-obsession remedies, ADR-amend-gated extension friction, and the convention-forward distinction between single-file marker loaders (TCCMLoader) and multi-file Pydantic-union loaders (this story).
The patterns that make this story easy to maintain and extend by addition:
codegenie.hashing.content_hash_fdextension — adding the streaming-hash capability is a single new function, zero edits to existing call sites. Future fd-streaming callers (SBOM,runtime_traceblob hashing in Phase 5+) reuse it.TierLiteral +_TIERSordering constant — adding a fourth tier is an ADR-amend (intentional friction) + Literal extension +_TIERStuple element. No edits to merge logic, no edits to test fixtures (parameterized).@register_index_freshness_checkprecedent not applicable here (skills aren't index sources), but the same Open/Closed pattern applies: extending Skill validation rules in Phase 4+ adds new registry-decorated functions, never editsSkillmodel.EvidenceQuerytyped surface — Phase 4+ planners produce queries; the typed signature documents what evidence the loader sees and prevents future callers from passing flat string sets.SkillsLoadErrordiscriminated union — adding a sixthreasonis a new Pydantic class + a Literal-extension + a new variant in theAnnotated[Union[...]]. Theassert_neverin_load_one_skill's error-classification step (recommended in Notes via functional-core split) forces exhaustive matching at compile time.- Pure
_matchesfunction — adding fancier matching semantics (e.g., versioned skill applicability, deprecation) is a new pure function or an extension of_matches; the loader itself doesn't change.
These are the load-bearing extension points the validator's design-patterns critic surfaced and the synthesizer promoted to ACs or Notes.