Research — One org-pack chain authority (#6006)
Grounded by two profile-loaded opus lenses (paula-patterns inventory, architect-alphonso architecture) and two post-spec lenses (analyst-annie, reviewer-renata). Every file:line below verified on the stacked base kitty/org-pack-chain-authority-stack.
Decision 1 — Authority shape and home
- Decision:
resolve_pack_chain(repo_root, *, strict: bool)insrc/charter/activation/layer_roots.py.strict=Trueraises on a declared-but-missing pack (today'srequire_declared_org_rootslogic + remedy);strict=Falsereturns the existing-filtered chain (today'sresolve_existing_org_roots). Last-declared-wins ordering. - Rationale:
layer_roots.pyis already the charter-tier layer-root seam and already hostsresolve_org_root_chain; collapsing the five spellings behind one posture flag satisfies both "one authority" (FR-001) and "fail-closed missing pack" (FR-007) over one registry read. - Alternatives considered: two separate functions (strict/lenient) — rejected, duplicates the registry read and splits the authority; keeping
resolve_org_root_chainas a second entry — rejected (parallel authority; reviewer-renata BLOCKER 2).resolve_org_root_chainis absorbed intoresolve_pack_chain, zero callers remain outside the authority module.
Decision 2 — Do NOT widen resolve_layer_roots's dict
- Decision:
resolve_layer_rootskeeps returningdict[str, Path]with a singleroots["org"](pack 1). The chain travels as a separatelist[Path]. - Rationale:
pack_manager._scan_layer_dirsandkind_vocabulary._layer_scan_dirsunconditionally doroot / ...on each dict value; alist[Path]there TypeErrors or silently resolves a non-existent dir (NFR-002 silent-success). The separate-list[Path]convention already exists (_org_scan_dirs,preset_application,effective_set). This deliberately does not take the issue's literal "widenlayer_roots" wording — the architect ruling. - Alternatives considered:
dict[str, list[Path]]parallel map — rejected (only the org layer is ever multi-valued; over-general; still rewrites everyroot / ...site).
Decision 3 — Retire _activate_cascade_target by widening ActiveCharterManager
- Decision: Add
org_root_chain: list[Path] | None = Nonetoactivate/list_available/list_available_detailed/_scan_layer_dirs; emit one ORG pair per chain root last-declared-first; fall back to the single slot when None. Collapse_activate_cascade_targetto onemanager.activate(...). - Rationale: the retry loop exists only because the availability scan saw pack 1; pushing the chain into the scan removes the reason for the loop.
ActiveCharterManageris stateless, so the chain enters via method params, not a constructor. - Alternatives considered: keep the loop (independent-scope option) — rejected by operator (stack ruling: retire it on top of #6005).
Decision 4 — Fail-closed split (strict vs lenient)
- Decision: strict posture on decision surfaces —
charter activate/deactivateavailability scan,load_requirement_kinds,effective_set._declared_org_roots. Lenient posture (silent degrade) oncharter listdisplay,effective_set._readable_roots/_fallback_ids, and the runtime hot pathresolve_org_dirs. - Rationale: a missing pack on a governance decision must be loud (#4984); forcing the hot path/display closed would regress NFR-002/NFR-004 silent-degrade behavior. "Declared-but-missing" means declared-and-absent, never "none declared" (edge case: zero packs → no raise in either posture).
- Lenient surfaces are re-pointed onto
resolve_pack_chain(strict=False)(not left calling primitives), so the empty allowlist stays honest (reviewer-renata BLOCKER 1).
Decision 5 — Empty-allowlist gate discriminator (module-ownership)
- Decision: AST census mirroring
tests/architectural/test_remote_contact_owner.py. Outsidelayer_roots.py(authority) andorg_pack_config.py(primitives' own module), forbid callingresolve_org_roots/resolve_existing_org_roots/require_declared_org_roots/resolve_org_root_chainor iteratingload_pack_registry().packsto assemble roots. Empty allowlist; file-count floor + planted-violation + owner-bypass controls. - Rationale: a symbol-name blanket would false-positive on the legitimate single-root
resolve_layer_roots(which lives in the authority module, so module-ownership exempts it); a too-narrow name-only gate misseseffective_set/preset_applicationassembly (analyst-annie, reviewer-renata). Module-ownership is AST-checkable and matches the charter's empty-allowlist invariant. - Note: FR-002/FR-005 carry their OWN behavioural/structural controls — the gate proves "no foreign assembly", not "this caller reads the full chain" or "no
[0]index survives".
Decision 6 — Loader seam mirrors load_manifest, with two documented divergences
- Decision:
requirement_kinds.py(frozen models,extra="forbid") +load_requirement_kindsbesideload_manifest, mirroringresolve_org_expected_artifacts(last-matching-file-wins, whole-file,MalformedManifestError/schema →ManifestSchemaError-shaped). - Divergence A:
load_manifesthas no project tier; the loader adds.kittify/charter-packs/missions/<type>/requirement-kinds.yaml, project wins over org/built-in (#5956 design). (Corrected from the pre-cutover.kittify/doctrine/path in #5956's design: that directory is retired and refused by theLEGACY_CHARTER_STATEgate, FR-011.) - Divergence B:
load_manifestresolves the org chain through the lenientresolve_existing_org_roots; the loader resolves through the strictresolve_pack_chainso a declared-but-missing pack refuses (FR-007/FR-011). - Rationale: honors #5956's whole-file-override + fail-closed design and point (5)'s fail-closed intent; divergences are deliberate and recorded so an implementer does not "fix" them back to the
load_manifestprecedent. - Alternatives considered: field-merge override — rejected (D2 of #5956: whole-file). Lenient org chain — rejected (fails the #4984 seam for the loader).
Decision 7 — _org_scan_dirs flat-wins precedence is preserved
- Decision:
resolve_pack_chainowns root SELECTION and ORDER only.kind_vocabulary._org_scan_dirs's flat-layout-wins-regardless-of-root-order precedence (pinned bytests/charter/test_kind_vocabulary_scan_roots.py) is unchanged (NFR-005). - Rationale: the two rules are orthogonal (pack ordinal vs flat-vs-legacy layout). On a mixed-layout cross-pack collision the flat file still wins; last-declared-wins governs same-layout collisions and the simple-override surfaces (list,
--include/context body, loader). Flagged by analyst-annie as a latent contradiction; resolved by scoping.
Supply-chain security
N/A — this mission adds/upgrades/removes no dependency. No adversarial supply-chain evidence pass required (advisory posture, DIRECTIVE_051).
Contracts
No external HTTP/GraphQL/webhook interface. The new public surfaces are an internal Python function API (resolve_pack_chain, load_requirement_kinds) and a YAML file schema (requirement-kinds.yaml), both captured in data-model.md. meta.json sets contracts: "none" with a rationale so strict accept waives the contracts/ requirement.