Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ jobs:
# ``tests/unit/test_observability_otel.py``.
run: uv sync --frozen --all-extras --group examples

- name: Validate conformance.toml against pinned spec
# Catches drift between conformance.toml and the pinned spec
# submodule's proposals/. External readers (notably the
# openarmature-spec docs build) fetch this manifest to surface
# python's impl status on the proposals index page, so a stale
# entry quietly serves wrong data to the docs site.
run: uv run python scripts/check_conformance_manifest.py

- name: Lint (ruff check)
run: uv run ruff check .

Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ jobs:
)
PY

- name: Validate conformance.toml against pinned spec
# Mirrors the equivalent step in ci.yml. Catches drift between
# conformance.toml and the pinned spec submodule's proposals/
# at release time as well as PR time, so a tag push that
# bypassed PR review still fails before publishing.
run: uv run python scripts/check_conformance_manifest.py

- name: Lint (ruff check)
run: uv run ruff check .

Expand Down
8 changes: 8 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ changelog entry.
release that changed user-visible behavior is reflected in the
upcoming version's section. The date matches the day the rc tag
is pushed (refresh it again at the real-release step).
- [ ] **`conformance.toml` is current.** Any proposal whose impl
landed in this cycle has its `[proposals."NNNN"]` entry — either
newly added (set `since` to the version about to ship) or
adjusted (e.g., `not-yet` → `implemented`). If the pinned spec
submodule was bumped, also bump `[manifest].spec_pin` to the new
tag. The CI guard at `scripts/check_conformance_manifest.py`
enforces structural consistency, but it can't check that
semantic status reflects reality — read the diff manually.
- [ ] **Docs sweep for stale references.** For each behavior change in
the upcoming release, grep the docs for the old wording, file
paths, and flag descriptions; reconcile in the same PR as the
Expand Down
151 changes: 151 additions & 0 deletions conformance.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Conformance manifest for openarmature-python.
#
# Records which openarmature-spec proposals are implemented in this
# package, and starting at which release version. Intended to be read
# by external consumers (notably the openarmature-spec docs build,
# which surfaces a per-implementation status column on the proposals
# index page).
#
# Stable URL (read-only, fetched at docs-build time):
# https://raw.githubusercontent.com/LunarCommand/openarmature-python/main/conformance.toml
#
# Maintenance: keep in sync with CHANGELOG.md. The CI guard at
# scripts/check_conformance_manifest.py validates this file against the
# pinned spec submodule's proposals/ directory on every PR and release
# build; any Accepted proposal lacking an entry (or any entry pointing
# at a non-existent / non-Accepted proposal) fails the build.
#
# Scope: this file lists ONLY proposals visible in the pinned spec
# submodule (openarmature-spec at the SHA pinned by this repo's
# submodule). Proposals accepted on the spec's main branch after this
# repo's last spec bump are intentionally absent — surfacing the gap
# between pinned-spec and spec-head is the consumer's job (e.g., the
# spec docs site computes the difference and renders accordingly).
#
# Convention: this file is only updated as part of release PRs. Between
# releases, the manifest reflects the most-recently-published version
# so external readers never see a `since` referring to an unreleased
# pre-tag commit.

[manifest]
implementation = "openarmature-python"
spec_pin = "v0.22.1"

# Status values:
# implemented — shipped behavior matches the proposal's contract
# partial — partial impl; consult `note` for what's missing
# textual-only — accepted proposal is purely textual (reframe,
# clarification, template) with no module-level
# change required; CHANGELOG note explains why
# not-yet — accepted in spec, not yet shipped in this package
#
# Drafts and Superseded proposals are deliberately absent from this
# file. The CI guard requires entries only for proposals whose spec
# header reads `Status: Accepted`.

[proposals."0001"]
status = "implemented"
since = "0.5.0"

[proposals."0002"]
status = "implemented"
since = "0.5.0"

[proposals."0003"]
status = "implemented"
since = "0.5.0"

[proposals."0004"]
status = "implemented"
since = "0.5.0"

[proposals."0005"]
status = "implemented"
since = "0.5.0"

[proposals."0006"]
status = "implemented"
since = "0.5.0"

[proposals."0007"]
status = "implemented"
since = "0.5.0"

[proposals."0008"]
status = "implemented"
since = "0.5.0"

[proposals."0009"]
status = "implemented"
since = "0.9.0"

[proposals."0010"]
status = "implemented"
since = "0.9.0"

[proposals."0011"]
status = "implemented"
since = "0.6.0"

[proposals."0012"]
status = "implemented"
since = "0.5.0"

[proposals."0013"]
status = "implemented"
since = "0.5.0"

[proposals."0014"]
status = "implemented"
since = "0.6.0"

[proposals."0015"]
status = "implemented"
since = "0.6.0"

[proposals."0016"]
status = "implemented"
since = "0.6.0"

[proposals."0017"]
status = "implemented"
since = "0.6.0"

[proposals."0018"]
status = "implemented"
since = "0.6.0"

[proposals."0019"]
status = "textual-only"
since = "0.9.0"
note = "Purely textual reframe of llm-provider §8 as a catalog of wire-format mappings (OpenAI-compatible body nested under §8.1). No module-level change required."

[proposals."0024"]
status = "implemented"
since = "0.8.0"

[proposals."0025"]
status = "implemented"
since = "0.9.0"

[proposals."0026"]
status = "textual-only"
since = "0.9.0"
note = "Purely textual §8 framing paragraph; the existing OpenAI §8.1 mapping is the template's reference shape, so no module-level work was needed."

[proposals."0027"]
status = "implemented"
since = "0.9.0"

[proposals."0028"]
status = "implemented"
since = "0.9.0"

[proposals."0029"]
status = "implemented"
since = "0.9.0"

[proposals."0030"]
status = "textual-only"
since = "0.9.0"
note = "Drain snapshot semantic and timeout-input validation already implemented as part of the proposal 0010 impl PR (v0.9.0); no additional module-level work needed."
154 changes: 154 additions & 0 deletions scripts/check_conformance_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Validate conformance.toml against the pinned spec submodule's proposals.

Failure modes caught:

- Accepted proposal in the spec has no entry in conformance.toml.
- Entry in conformance.toml refers to a proposal that doesn't exist in
the spec, or refers to a proposal whose Status is not "Accepted"
(e.g., Draft / Superseded — those are deliberately excluded from the
manifest so the docs site doesn't claim impl status for unsettled
proposals).
- Entry has an unknown `status` value or a malformed `since` version.

Read-only; intended for CI. Non-zero exit on any failure with a
human-readable diff. Runs under the repo's stdlib Python (>=3.12, so
`tomllib` is always available).
"""

from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path
from typing import Any, cast

REPO_ROOT = Path(__file__).resolve().parent.parent
MANIFEST_PATH = REPO_ROOT / "conformance.toml"
PROPOSALS_DIR = REPO_ROOT / "openarmature-spec" / "proposals"

ALLOWED_STATUSES = frozenset({"implemented", "partial", "textual-only", "not-yet"})
PROPOSAL_FILENAME_RE = re.compile(r"^(\d{4})-[a-z0-9-]+\.md$")
STATUS_LINE_RE = re.compile(r"^- \*\*Status:\*\*\s*(.+?)\s*$", re.MULTILINE)
SINCE_RE = re.compile(r"^\d+\.\d+\.\d+$")


def parse_spec_proposals() -> dict[str, str]:
# Returns {proposal_id: status} for every proposal markdown file in
# the pinned spec submodule. proposal_id is the 4-digit string used
# as the manifest key; status is the literal value from the file's
# `- **Status:** ...` header line.
if not PROPOSALS_DIR.is_dir():
sys.exit(
f"::error::proposals dir not found at {PROPOSALS_DIR} — "
"is the openarmature-spec submodule checked out?"
)

result: dict[str, str] = {}
for path in sorted(PROPOSALS_DIR.iterdir()):
m = PROPOSAL_FILENAME_RE.match(path.name)
if not m:
continue
proposal_id = m.group(1)
text = path.read_text(encoding="utf-8")
status_match = STATUS_LINE_RE.search(text)
if not status_match:
sys.exit(f"::error::proposal {proposal_id} ({path.name}) has no `- **Status:** ...` header line")
result[proposal_id] = status_match.group(1).strip()
return result


def load_manifest() -> dict[str, Any]:
# Returns {proposal_id: entry} for every [proposals."NNNN"] section
# in conformance.toml. Each `entry` is typed as Any rather than
# dict because TOML lets a user accidentally write a scalar value
# under [proposals]; the caller validates the type per-entry and
# emits a structured error on shape drift.
if not MANIFEST_PATH.is_file():
sys.exit(f"::error::manifest not found at {MANIFEST_PATH}")

with MANIFEST_PATH.open("rb") as f:
data = tomllib.load(f)

proposals = data.get("proposals", {})
if not isinstance(proposals, dict):
sys.exit("::error::conformance.toml [proposals] table malformed")
return cast(dict[str, Any], proposals)


def main() -> int:
spec = parse_spec_proposals()
manifest = load_manifest()

Comment thread
chris-colinsky marked this conversation as resolved.
accepted_ids = {pid for pid, status in spec.items() if status == "Accepted"}
manifest_ids = set(manifest.keys())

errors: list[str] = []

missing = sorted(accepted_ids - manifest_ids)
for pid in missing:
errors.append(f"Accepted spec proposal {pid} has no entry in conformance.toml")

extra = sorted(manifest_ids - accepted_ids)
for pid in extra:
if pid not in spec:
errors.append(
f"conformance.toml entry {pid} refers to a proposal that "
f"doesn't exist in openarmature-spec/proposals/"
)
else:
errors.append(
f"conformance.toml entry {pid} refers to a proposal whose "
f"spec Status is {spec[pid]!r}, not 'Accepted' — "
f"drafts and superseded proposals should be omitted"
)

for pid in sorted(manifest_ids):
raw_entry = manifest[pid]
if not isinstance(raw_entry, dict):
errors.append(
f"conformance.toml entry {pid} is not a table "
f'(got {type(raw_entry).__name__}); check the [proposals."{pid}"] '
f"section is a table, not a scalar"
)
continue
entry = cast(dict[str, Any], raw_entry)
status = entry.get("status")
if status not in ALLOWED_STATUSES:
errors.append(
f"conformance.toml entry {pid} has unknown status {status!r} "
f"(allowed: {sorted(ALLOWED_STATUSES)})"
)
# `since` is required for every status except `not-yet`.
since = entry.get("since")
if status == "not-yet":
if since is not None:
errors.append(
f"conformance.toml entry {pid} has status=not-yet but "
f"also a `since` field — drop `since` for not-yet entries"
)
else:
if since is None:
errors.append(f"conformance.toml entry {pid} has status={status!r} but no `since` field")
elif not isinstance(since, str):
errors.append(
f"conformance.toml entry {pid} `since` value {since!r} is not a string "
f"(got {type(since).__name__}); did you forget to quote it?"
)
elif not SINCE_RE.match(since):
errors.append(
f"conformance.toml entry {pid} `since` value {since!r} is not in MAJOR.MINOR.PATCH form"
)
Comment thread
chris-colinsky marked this conversation as resolved.
Comment thread
chris-colinsky marked this conversation as resolved.

if errors:
print("conformance.toml validation failed:", file=sys.stderr)
for err in errors:
print(f" - {err}", file=sys.stderr)
return 1

print(f"OK: {len(accepted_ids)} accepted proposals, {len(manifest_ids)} manifest entries, all consistent")
return 0


if __name__ == "__main__":
raise SystemExit(main())