Data Model: Layered Doctrine Resolution — Org Layer

Phase 1 output | Mission: layered-doctrine-org-layer-01KRNPEE | Date: 2026-05-15


1. Resolution Layer Model

DoctrineLayers (enum)

ValueDisplay nameDescriptionRoot location
builtinspec-kitty built-inBundled with the CLI; read-onlysite-packages/doctrine/ (resolved via resolve_doctrine_root())
orgorg / <pack-name>Installed per developer machine; operator-managed; one or more named packslocal_path per pack in doctrine.org.packs
projectprojectPer-project local overrides; developer-managed.kittify/doctrine/

Merge precedence: builtin < org (packs in declaration order) < project. Higher layer takes ownership of the resolved artifact on ID collision; its provenance becomes that layer. Field-level merge applies: fields present in the higher layer's YAML replace same-named fields in the lower layer; fields absent from the higher layer fall through. No two artifacts with the same ID coexist across layers — the higher layer's identity wins. The resolver emits a DoctrineLayerCollisionWarning each time a higher layer shadows a lower-layer artifact, with the artifact ID, source and target layers, and the count of replaced fields. See ADR architecture/2.x/adr/2026-05-16-1-doctrine-layer-merge-semantics.md.

Fallback: if no org packs are configured or no local paths exist on disk, resolution falls back silently to builtin + project. A spec-kitty doctor doctrine diagnostic is surfaced but no error is raised in normal operation. Projects with no doctrine.org.packs config are completely unaffected by this feature.


2. Config Models

OrgPackConfig

Pydantic model for a single named pack entry.

FieldTypeRequiredDescription
namestrYesUnique name for this pack (used by --pack flag and doctor doctrine display).
local_pathPathYesPath to the local clone (git) or snapshot directory (non-git). ~ is expanded.
source_type`Literal["git", "https", "api"] \None`No
url`str \None`No
ref`str \None`No

PackRegistry

Ordered list of OrgPackConfig. Declaration order = precedence (later = higher). Maps to .kittify/config.yaml under doctrine.org.packs.

Multi-pack config example:

doctrine:
  org:
    packs:
      - name: security
        local_path: "~/.kittify/org/security/"
        source_type: git
        url: "git@example.com:security/doctrine.git"
        ref: "v2.1.0"
      - name: architecture
        local_path: "~/.kittify/org/architecture/"
        source_type: git
        url: "git@example.com:architecture/doctrine.git"
      - name: compliance
        local_path: "~/.kittify/org/compliance/"
        source_type: api
        url: "https://governance.example.com/compliance/v1"

IT-provisioned clone (no fetch config):

doctrine:
  org:
    packs:
      - name: org-doctrine
        local_path: "/opt/company/org-doctrine/"

Backward-compat single-local_path form (treated as one anonymous pack):

doctrine:
  org:
    local_path: "~/.kittify/org/acme-corp/"

3. OrgDoctrineSource (fetch-time protocol)

Structural protocol (typing.Protocol). Any object satisfying this interface can act as a fetch source. Spec-kitty ships three implementations; third parties may add others.

OrgDoctrineSource
  fetch(target_dir: Path) -> FetchResult

FetchResult (dataclass):

FieldTypeDescription
okboolTrue if all artifacts were written successfully
artifacts_writtenintNumber of artifact files written to target_dir
pack_version`str \None`
errorslist[str]Error messages for any artifact that failed to fetch

Concrete implementations:

ClassMechanismAuth
GitSourceShallow clone / pull via git subprocessSSH keys or GIT_TOKEN env var
HttpsBundleSourceDownload tarball via requests; extract to target_dirSPEC_KITTY_ORG_TOKEN env var (bearer)
ApiSourceGET per artifact type to API endpoints; reconstruct pack layoutSPEC_KITTY_ORG_TOKEN env var (bearer) or SPEC_KITTY_ORG_AUTH_HEADER

All implementations:

an existing valid snapshot

timestamp, and source URL (redacted of credentials)

  • Validate the fetched content against the schema before writing to target_dir
  • Write atomically (write to a temp dir, then rename) so a failed fetch never corrupts
  • Write a pack-manifest.yaml at the root of target_dir containing pack version, fetch

4. OrgDoctrinePack (directory layout)

The canonical layout that all fetch sources must produce and that pack validate checks. See contracts/pack-layout.md for the normative specification.

<pack-root>/
├── pack-manifest.yaml          ← written by doctrine fetch (version, timestamp, source)
├── directives/
│   └── *.directive.yaml
├── tactics/
│   └── *.tactic.yaml
├── styleguides/
│   └── *.styleguide.yaml
├── toolguides/
│   └── *.toolguide.yaml
├── paradigms/
│   └── *.paradigm.yaml
├── procedures/
│   └── *.procedure.yaml
├── agent_profiles/
│   └── *.agent.yaml
├── mission_step_contracts/
│   └── *.contract.yaml
└── drg/                        ← optional; contains graph extension fragments
    └── *.graph.yaml            ← additive DRG nodes and edges only

Rules:

merged shipped + org artifact set (validated by pack validate).

  • All artifact subdirectories are optional. A pack that only contains directives/ is valid.
  • The drg/ directory is optional. If absent, the org layer contributes no DRG extensions.
  • pack-manifest.yaml is written by doctrine fetch; pack authors do not create it manually.
  • Artifact files must conform to their respective spec-kitty YAML schema.
  • DRG graph fragments in drg/ must not reference artifact URNs that don't exist in the

5. PackManifest (written by doctrine fetch)

pack_version: "v1.2.0"           # tag / SHA / "api-v2" / etc.
fetched_at: "2026-05-15T11:30:00Z"
source_type: git
source_url: "git@internal.example.com:platform/org-doctrine-distributable.git"
artifact_counts:
  directives: 12
  tactics: 4
  agent_profiles: 8
  toolguides: 3

spec-kitty doctor doctrine reads this file to display version and counts without re-loading all artifacts.


6. Three-Layer Repository Merge Invariants

The following invariants must hold after any _load() call on a three-layer repository.

InvariantDescription
Higher winsFor any artifact ID present in multiple layers, only the highest layer's artifact is in _items
Shipped completenessAll shipped artifacts are present in the final set unless overridden by a higher layer
No phantom org artifactsAn org artifact whose ID is new (not in shipped) is present in _items
No phantom project artifactsA project artifact whose ID is new (not in shipped or org) is present in _items
Skipped-bad-file resilienceA single malformed artifact file in any layer does not prevent valid artifacts in the same layer from loading
Language-scope preserved_include_item() is applied after merge; an org artifact that does not apply to the active language set is excluded

Property tests (hypothesis) will verify these invariants against randomly generated three-layer artifact sets.


7. Provenance Tracking Model

Each repository maintains a parallel _provenance: dict[str, str] keyed by artifact ID. Values use the machine-readable layer tag. This dict is populated during _load():

output; doctor doctrine shows which pack via the pack name separately)

  • Built-in artifacts → "builtin"
  • Org overrides or new org artifacts → "org" (all packs share the "org" tag in context
  • Project overrides or new project artifacts → "project"

DoctrineService exposes a get_provenance(artifact_type: str, artifact_id: str) -> str | None method for callers that need source attribution (context serialisation, doctor, lint).

The charter context --json "source" field uses these machine-readable values: "builtin", "org", "project". The human display in doctor doctrine maps "builtin" to "spec-kitty built-in" and org artifacts to "org / <pack-name>".


8. State transitions for doctrine fetch

fetch invoked
    ↓
source resolved from config
    ↓
credentials resolved from environment
    ↓
FetchResult ← source.fetch(temp_dir)
    ↓
[ok=False?] → report errors, exit non-zero; existing snapshot unchanged
    ↓
validate temp_dir against schema
    ↓
[validation fails?] → report errors, exit non-zero; existing snapshot unchanged
    ↓
atomic rename: temp_dir → local_path
    ↓
write pack-manifest.yaml
    ↓
report success

Atomicity guarantee: the existing snapshot is never overwritten unless validation passes. This is the only write operation during doctrine fetch.