feat(bottle): composition via extends: (PRD 0025, issue #88) #89

Merged
didericis merged 5 commits from bottle-extends-prd-0025 into main 2026-05-28 01:26:46 -04:00

5 Commits

Author SHA1 Message Date
didericis-codex f029a3d7f5 refactor(manifest): drop stale migration errors
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 54s
test / unit (push) Successful in 29s
test / integration (push) Successful in 40s
2026-05-28 01:08:05 -04:00
didericis-codex 59ee32cc8d refactor(manifest): key git config by host
test / unit (pull_request) Successful in 33s
test / integration (pull_request) Successful in 42s
2026-05-28 00:49:34 -04:00
didericis-claude 85104742ca docs(readme): document bottle extends: composition (PRD 0025)
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 42s
2026-05-27 23:31:02 -04:00
didericis-claude a5c8b4e7b2 feat(manifest): bottle composition via \extends:\ resolver (PRD 0025, #88)
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 39s
Add an optional `extends: <bottle-name>` field to bottle
frontmatter. Two-pass load:

  1. Collect raw frontmatter for every bottle file.
  2. Recursively resolve each name into a merged Bottle via
     `_resolve_one_bottle` + `_merge_bottles`.

Merge rules (per PRD 0025):

- env: dict merge, child wins on key collision
- git: full replace if child declares `git:`
- git_user: per-field overlay (child's non-empty fields win)
- egress: full replace if child declares `egress:`
- supervise: full replace if child declares `supervise:`

List-valued fields full-replace because partial merge is
ambiguous (ordering matters, name collisions ambiguous); env is
dict-merge because dict-keyed override is the natural shape.
git_user overlays per-field so a parent can declare just the
name and a child can add just the email.

Cycles / self-extends / missing-parent / non-string `extends:`
all die at parse with a pointer that includes the chain (cycles)
or the available names (missing parent). Resolution is cached
per-name so a diamond reference graph doesn't reparse the same
parent N times.

Both load paths threaded:
- `_load_bottles_from_dir` (md files) — collect raws, then
  resolve.
- `Manifest.from_json_obj` (JSON / test fixtures) — same.

Tests (24, in `test_manifest_extends.py`):
- Leaf without extends parses unchanged
- Child inherits parent unchanged when child only declares
  `extends:`
- env: disjoint union, collision (child wins), child-omits
- git: replace, omit, explicit-empty-clears-parent
- egress: same shape (replace, inherit)
- git_user: parent-only, child-overrides-both, partial fields
- 3-step chain (grandparent → parent → child)
- Errors: missing parent, self-extends, 2-node cycle, 3-node
  cycle, non-string extends

685 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:30:40 -04:00
didericis-claude 4f7a506a9e docs(prd): 0025 — bottle composition via extends: (issue #88)
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 40s
2026-05-27 23:27:04 -04:00