Spec: Conformance
Status: stable (frozen at v1.0.0) · Owner: Layer 0 (engine) + Layer 1 (detector bindings)
Conformance answers "is this consumer correctly wired?" against a rule spec that ships inside each release — so advancing a pin brings the current rule set into force, and improving the framework surfaces the new gap in every not-yet-adopted consumer.
Two rule sources compose:
- Core rules (
conformance/core-rules.yml, shipped by the framework): platform-neutral wiring requirements every slice needs — gate wired and required, sync scheduled, watch present, manifest committed, pins are tags. - Publisher rules (optional
conformance.ymlbeside the export declaration): slice-specific requirements a publisher adds (e.g. "profile declared", domain-specific checks viatooldetectors).
1. Rule schema (v1)
schema_version: 1
rules:
- id: gate-wired # stable kebab-case, never renamed
title: "Gate lane wired and required on PRs"
category: enforcement # prerequisites | protection | vendoring |
# enforcement | sync | promotion | custom
severity: mandatory # mandatory (unwaivable) | waivable | advisory
detector:
kind: pipeline-wired # see registry §2
component: gate # gate | sync | watch | migration-verify
pinned: true # the reference must be an immutable tag
events: [pull_request] # pull_request | schedule
required_check: true # must be enforced, not merely present
remediation: >-
Wire the gate pipeline (scaffold: `vendkit init`), pin it to a release
tag, and make it a required check / build-validation policy.
Statuses per rule: pass, fail, waived, attested, skipped, error.
fail/error are gaps. vendkit conformance [--strict]: strict exits 1 on
any non-waived gap; default is advisory (exit 0, report only). Waivers live in
the consumer slice config (waivers: [{rule, reason}]) and are honoured only
for severity: waivable rules — a waiver on a mandatory rule is itself a
finding.
2. Detector registry
Platform-neutral kinds, implemented in Layer 0 unless noted:
| Kind | Decides | Notes |
|---|---|---|
file-exists |
path present | |
manifest-tracked |
path is a consumer_path in the slice manifest |
|
profile-bound |
slice config declares a profile the declaration defines | |
codeowners-covers |
ownership review covers given patterns | SCM-axis: GitHub parses CODEOWNERS; Azure Repos does not honour it → degrades to the required_reviewers_policy attestation (DR-0015) |
paths-lockstep |
if the gate pipeline path-filters, the filter covers every consumer_path |
parses the dialect named by the slice config's ci:; skipped under ci: none |
pipeline-wired |
a pipeline references the given framework component, pinned, on the given events, enforced | dialect by ci:, §3; skipped under ci: none (manual mode is visible, not hidden) |
attest |
consumer config asserts a non-tree-decidable fact | §4 |
tool |
a manifest-tracked executable exits 0 | shared with migration checks |
Pipeline parsing is format knowledge and stays in core; anything needing a vendor service goes through the fact-verify handler (§4) — the format-vs-service rule (DR-0015).
Publishers may not invent detector kinds; they extend via tool (vendored,
gate-verified executables). This keeps the checker's trusted computing base
fixed.
3. pipeline-wired: per-CI decidability
The same rule is decided differently per CI dialect (selected by the slice
config's ci: field, never env-sniffed — a fleet audit decides identically
to CI):
| Aspect | github-actions | azure-pipelines |
|---|---|---|
| Locate pipelines | .github/workflows/*.yml |
azure-pipelines/*.yml, azure-pipelines.yml |
| "references component" | the pipeline invokes the component's CLI subcommand (direct call, composite action, or template all bottom out in the invocation) | same |
pinned |
a refs/tags/vX.Y.Z checkout ref, @vX.Y.Z, or 40-hex SHA |
resources.repositories ref: starts refs/tags/ |
events: [pull_request] |
tree-decidable: on: pull_request |
not tree-decidable: Azure Repos ignores pr:; requires Build Validation policy → attest (or fact-verify handler, §4) |
events: [schedule] |
tree-decidable: on: schedule |
tree-decidable: schedules: block |
required_check |
branch protection / ruleset → attest or fact-verify |
Build Validation policy → attest or fact-verify |
ci: none |
every pipeline-wired/paths-lockstep rule reports skipped — manual orchestration forfeits automated enforcement, stated in every report |
same |
The rule spec stays identical across platforms; only the dialect binding
changes (INV-8). Where a platform fact is not tree-decidable the binding
degrades to an attest sub-check automatically, with the fact-verify
handler as the upgrade path (§4).
4. Attestations
Non-tree-decidable prerequisites (branch protection enabled, sync credential provisioned and policy-exempt, publisher repo readable by CI identity) are asserted in the consumer slice config:
attestations:
branch_protection_enabled: true
sync_credential_provisioned: true
Attested rules report attested, not pass — dashboards can distinguish
verified facts from asserted ones. conformance --verify-attestations
sends each attested fact to the configured fact-verify handler
(handler-protocol spec §3): verdict true promotes attested → pass,
false demotes to fail (a wrong attestation is a finding), unknown
leaves it attested. Core calls no vendor API itself — it composes the intent
from a stable machine fact key (never the human detail prose) plus the
repo/branch coordinate (--repo, --base-branch, or the handler's CI env)
and hands it to the handler, which owns all HTTP.
The reference handlers perform real API verification (replacing the earlier
unknown stub):
| attested fact | GitHub check | Azure DevOps check |
|---|---|---|
required_check_enforced |
branch protection requires a status check | an enabled blocking Build-validation policy |
pull_request_enforcement |
— (tree-decidable) | an enabled Build-validation policy |
required_reviewers_policy |
— (tree-decidable) | an enabled Required-reviewers policy |
true means the platform confirms the control; false means it is
definitively not enforced; unknown is emitted only when the token lacks
scope (a 401/403 is unknown, never false), the endpoint is unavailable, or
the fact key is unrecognised (forward-compatible). Verification uses a
read-scoped VENDKIT_TOKEN_FACT_VERIFY (GitHub: GITHUB_TOKEN/GH_TOKEN
fallback; ADO: SYSTEM_ACCESSTOKEN/ADO_PAT).
5. Fleet view
vendkit conformance --json emits a machine document (per-rule status,
gap count, pin, pin lag, slice, profile). A fleet audit — a scheduled,
read-only job that clones each consumer, runs conformance + gate verify +
watch compare locally, and publishes one aggregated dashboard — is the intended
consumer of this format. It requires only read access (no inversion of the
trust model) and is specified as a Layer 3 optional component in the roadmap
(M4), not core.
5.1 The conformance --json document
--json emits a single object (not a bare rule array) — the fleet-view
interchange format:
{
"slice": "docs",
"profile": "code-repo",
"pin": { "version": "v1.4.2", "sha256": "…" },
"pin_lag": null,
"gap_count": 2,
"rules": [
{ "rule_id": "gate-wired", "title": "…", "severity": "mandatory",
"status": "fail", "detail": "…" }
]
}
slice,profile— from the consumer slice config.pin— the engine pin from the slice config (DR-0016):version, and the recordedsha256for the audit host's own platform (<goos>/<goarch>). Omitted when the slice records no engine pin (e.g.ci: none);sha256is omitted when unrecorded (an advisory pin).pin_lag— how far behind the latest release the pin is. Not determinable offline: computing it needs the publisher's release list, and core issues no network/SCM call. It is therefore emitted as JSONnull. A fleet audit running in a network-capable scheduled job may fill it before aggregating.gap_count— number of rules whose status is a gap (fail/error).rules— the per-rule results (rule_id,title,severity,status,detail), unchanged from earlier releases.
This replaces the pre-existing --json output (a bare array of rule results):
the array is now the rules field. The human (non---json) output is
unchanged.
5.2 The fleet command
vendkit fleet [--json] [<path>…] is the read-only aggregation half of the
fleet audit. The clone-and-run over many repos is the scheduled external job
(Layer 3, optional); fleet folds the resulting conformance --json
documents into one aggregated report. It clones nothing, fetches nothing, and
calls no network or SCM API.
Input. Each positional <path> is either a file — one document, or a JSON
array of documents — or a directory, in which case every *.json file inside
it (sorted) is read. With no paths, documents are read from stdin: a JSON
array, or one-or-more JSON objects (newline-delimited or concatenated). Flags
and paths may be given in any order. A source that is not valid conformance
JSON, or an object lacking slice, fails loudly, named, with exit 2.
Output. A human summary always (fleet size, a census of consumers by worst
status, total gaps, and a per-consumer table sorted worst offenders first),
plus the machine facts consumers= and total-gaps= on the CI surface. With
--json, the fleet-level interchange document is additionally emitted:
{
"total_consumers": 2,
"by_worst_status": { "fail": 1, "pass": 1 },
"total_gaps": 2,
"consumers": [
{ "slice": "docs", "profile": "code-repo",
"pin": { "version": "v0.9.0" }, "pin_lag": null,
"gap_count": 2, "worst_status": "fail" }
]
}
Each consumer row carries slice, profile, pin, pin_lag, gap_count,
and worst_status. Worst status is the most severe rule status in a
document, ranked (worst → best) error > fail > attested > skipped >
waived > pass: gaps first, then unverified assertions, then forfeited
enforcement (ci: none), then deliberately accepted, then clean. Rows are
sorted by that rank (desc), then gap count (desc), then slice name — so the
worst offenders lead the dashboard. fleet is advisory: it aggregates and
reports, exiting 0 (usage/parse failures aside).