ADR 0012 — CI capability reference contract
Status: Proposed (issue #371; keystone of the multi-engine CI/CD parity EPIC #368)
Context
Multi-engine CI/CD parity (spec 0046, EPIC #368) needs one engine-neutral
description of what the project's CI does before any engine's pipeline can
be generated (sub-spec B) or drift-checked (sub-spec C). Today the CI lives
only as GitHub Actions YAML (.github/workflows/*.yml); there is no neutral
source of truth, so a second engine could only be hand-translated, with no way
to detect divergence. Spec 0047
requires a single reference (stable id, neutral trigger, portability mark)
plus a normative description of its own shape (R1–R9), and a traceability id
by which any engine's job attributes to exactly one capability (R6) — readable
by the yq parser sub-spec C uses.
Decision
Introduce ci/ci-capabilities.yml, a platform-neutral YAML reference: one
entry per CI job, each with a stable id, a trigger from a closed neutral
vocabulary (push, pull-request, tag, scheduled, manual) with
portable filters (branches, paths, tag-pattern), a
portability: portable|specific mark, and an evidence-backed exception per
specific entry. Job-to-capability traceability (C2): the capability id IS
the pipeline job's YAML key on every engine — read via yq '.jobs | keys'
(GHA) or top-level keys-minus-reserved (GitLab), with a trailing key-comment
# ci-capability: <id> fallback for engine-reserved job names (GitLab
pages). The shape is described normatively in
docs/ci-reference-format.md, which pins the
exact extraction expressions. YAML is chosen because yq is already a CI
dependency; no new validation toolchain enters. ci/ is a new top-level
core-layer path, registered in docs/layers.md and
.crewrig/core-paths.txt.
Alternatives considered
- C2 as an own-line YAML comment on the job node — rejected: empirically
unreadable by
yqv4.53.3 (line_comment/head_comment/foot_commentall return empty on the job node); only reachable via a brittle, position-dependent sibling-relative path. - C2 as a custom data field in the job map (
jobs.<id>.ci-capability) — rejected:actionlintand GitLab CI lint reject unknown job keywords; the only valid variant degrades to a sidecar id→jobs file, a second artifact to keep in sync — the exact drift C2 exists to remove. - C2 as a trailing key-comment (
<job>: # ci-capability: <id>) — proven readable (yq '.<job> | key | line_comment'), retained only as the fallback for engine-reserved job names; not primary because it duplicates the id and can silently disagree with the job key. - JSON Schema reference instead of YAML + a prose format doc — rejected:
introduces a validator toolchain absent from CI today, against the YAML/
yqdecision; validity is enforced by sub-spec C's checker against the four spec scenarios. - Reference under
.github/— rejected: frames the neutral contract as GitHub-owned, contradicting R9 and the platform-neutrality intent.
Consequences
- B and C build against a frozen C1/C2: B names each generated portable job by
its
idand hand-authors thespecificjobs; C reads.jobs | keysand fails closed on undocumented drift or untraceable jobs. id == job keycouples capability rename and job rename into one act — they cannot silently diverge; the cost is that a job key is constrained to the id charset and the GitLab-reserved-name fallback is needed forpages.- The engine × capability axis is self-documented by the YAML, distinct from
the CLI × feature matrix;
docs/cli-matrix.mdcarries one cross-reference row, not an engine sub-table. - Adding a third engine requires only a new mapping from the neutral vocabulary and naming its jobs by the existing ids — no capability definition changes (R9).
ci/joining the core manifest means adopters receive the reference on sync andcheck-core-paths.shguards it.