Contracts

asset-kind.md

Contract — ASSET kind + TEMPLATE node

Internal doctrine/DRG contract (no HTTP surface). Binds the behavior of the two kinds.

Sidecar manifest (assets/<pack>/<name>.asset.yaml)

# round-trip: skip: illustrative sidecar-manifest shape sketch — the executable AssetManifest schema + round-trip coverage live in tests/specify_cli/doctrine/test_pack_validator.py (this block documents the on-disk convention, not a registered Pydantic contract)
id: company-logo            # required; bare URN -> asset:company-logo (no <pack>/ qualifier — D-05 revised)
mime: image/png             # required; type/subtype + must match path extension
path: logo.png              # required; relative, under assets/<pack>/, no ../ or absolute
title: Company logo         # optional

Behavioral contract

#GuaranteeVerified by
AT-1An assets/<pack>/*.asset.yaml is registered as a NodeKind.ASSET node addressable as bare asset:<id>.loader/extractor test
AT-2The blob's content is NOT schema-validated; only the manifest (id/mime/path, via AssetManifest) is. The blob is never scanned (glob = *.asset.yaml).pack_validator ASSET test
AT-3A duplicate asset: URN anywhere in the merged graph (built-in ∪ every pack ∪ project — all three layers) → hard-fail duplicate_asset_id, via the single post-merge uniqueness scan at merge_three_layers (not silent first-wins, not layered override).merge dup-id test (NFR-005)
AT-4A path that is absolute or escapes assets/<pack>/ (../) → hard-fail asset_path_escape, via the reused effective_root/OrgPackSubdirEscapeError containment (D-12), in a separate _validate_asset_manifests pass.containment test (NFR-005)
AT-5A mime not matching type/subtype, or inconsistent with the path extension → hard-fail asset_mime_invalid.mime test (NFR-005)
AT-6ASSET and TEMPLATE are node-declarable but NOT augmentation-eligible and NOT charter-activatable — via _NON_AUGMENTATION_ELIGIBLE_KINDS; charter list / YAML_KEY_MAP / augmentation globs / the context.py:500 bare-probe filter all exclude them.augmentation + charter-token + context-probe tests (S6)
AT-7An asset node reached via resolve_transitive_refs appears in the result (assets field), not dropped.query test (D-09)
AT-8_OrgDRGNode carries no asset-specific field (no mime/path); it stays identity-only for all 11 kinds. Asset mime/path are declared solely in the sidecar.model-shape test (D-08 revised)
TT-1An org pack declares a bare template:<id> node + an edge (e.g. styleguide requires it); the merged DRG contains node + edge; a query returns it.e2e fixture (S1)
TT-2An edge to a non-existent template URN → the generic dangling-ref check flags it (free).validator test (#2495 AC)
TT-3A duplicate template: URN across the merged graph (org-vs-org, or org-vs-built-in template_catalog) → hard-fail duplicate_template_id, via the same post-merge scan.merge dup-id test (D-04 revised)

Structural contract (totality guard — C-005)

tests/doctrine/test_kind_mapping_totality.py (new): for every module-level dict keyed by ArtifactKind or NodeKind (discovered by reflection/AST), assert it contains an entry (or a documented .get-default) for every member — so a future kind cannot silently miss a mapping site. Upgrades the existing subset guard (test_nodekind_artifactkind.py).

Lockstep contract (#2495)

_ORG_DRG_CANONICAL_KINDS == (charter.activations._ALLOWED_KINDS normalised) ∪ {mission_types} — the drift-guard test stays green because _ALLOWED_KINDS + _BUILTIN_ARTIFACT_KINDS move in lockstep with the loader universe.