ci: gate core against the feedpak spec

feedpak is published as an open format with its own repo, normative spec,
JSON Schemas, and reference validator. That makes the spec a contract with
everyone outside this repo: third-party packers, converters, and players
build against it, and it is meant to be the complete description of a pack.

Nothing enforced that. #583 added a manifest key (`original_audio`) that
core, lib/enrichment.py, and the stems plugin all now depend on, but which
was never added to the spec — so a spec-compliant pack stopped being a
fully-working pack, the reference validator could not warn authors about a
key it had never heard of, and third-party tooling began emitting an
`original/` directory reverse-engineered from an example in a code comment.
See #933.

We cannot mechanically prove core interprets a key the way the spec means.
We can prove three surface properties, and they cover the drift that
actually happens:

  1. key-coverage — every manifest key core reads is declared in the spec's
     manifest.schema.json (AST scan of lib/sloppak.py, lib/enrichment.py,
     lib/songmeta.py).
  2. forward — core's load_song() ingests every example pack the spec ships.
  3. reverse — every pack committed here passes the spec's own
     tools/validate.py (7/7 pass today).

The spec is pinned by SHA in .feedpak-spec-ref rather than tracked from its
default branch, so a change over there cannot redden an unrelated PR here;
bump it in its own PR, where a red result is precisely the signal that core
does not satisfy the new spec.

A gate with no legitimate way to say "yes, deliberately, not yet" gets
switched off the first time it blocks a release, so there are two escape
hatches: the reserved `x-` key prefix (always permitted, and it tells every
third-party packer the key is not stable surface), and
feedpak-spec-exceptions.yml, which requires a tracking issue per entry. An
exception that goes stale — the spec caught up, or core stopped reading the
key — fails the build, so the allowlist cannot become somewhere drift
quietly accumulates. `original_audio` is seeded there against #933 so the
gate lands green and starts blocking the next instance immediately, rather
than requiring #933 to be resolved first.

Dev/CI tooling only; never on the serve or Docker path (constitution
Principle I). jsonschema is installed in the CI job, not added to
requirements.txt.

Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
topkoa
2026-07-12 23:30:45 -04:00
parent 342def3851
commit 22332bef22
6 changed files with 453 additions and 0 deletions
+16
View File
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **CI gate: core must stay faithful to the feedpak spec (`feedpak-spec` job).** feedpak is published as
an open format with its own repo, normative spec, JSON Schemas, and reference validator — but nothing
stopped core from reading a manifest key the spec never defined, which is exactly what happened with
`original_audio` (#583#933). `tools/check_spec_conformance.py` now enforces three surface properties
in CI: (1) **key-coverage** — every manifest key core reads is declared in the spec's
`manifest.schema.json`, found by walking the AST of `lib/sloppak.py`, `lib/enrichment.py`, and
`lib/songmeta.py`; (2) **forward** — core's `load_song()` ingests every example pack the spec ships;
(3) **reverse** — every pack committed here passes the spec's own `tools/validate.py` (7/7 pass today).
The spec is pinned by SHA in `.feedpak-spec-ref` so a change over there can't redden an unrelated PR
here; bump it in its own PR, and a red result is the signal that core doesn't satisfy the new spec.
A key that must ship ahead of the spec uses the reserved `x-` prefix (always allowed) or is recorded in
`feedpak-spec-exceptions.yml` with a tracking issue — and the gate fails if such an exception goes stale,
so the allowlist can't become somewhere drift hides. `original_audio` is seeded there against #933 so the
gate lands green and starts blocking the *next* instance immediately. Docs: [docs/feedpak-spec-gate.md](docs/feedpak-spec-gate.md).
### Removed
- **The classic v2 UI shell is gone — v3 is the only UI (R3a).** `static/index.html`, the
`/v2` route, and the `FEEDBACK_UI` v2/legacy opt-out are deleted; `/` and `/v3` both serve