fix!: repoint published schema namespace to a controlled URI root - #939
Merged
Conversation
The canonical published-schema URI root was `https://raes.dev/schemas/`, carried over from the identity cutover in #921. That domain is registered to a third party, as was the root it replaced. Using an uncontrolled domain in `$id` gives up the only property the URI form buys — that the identifier is unambiguously ours to assign — and left issue #908 blocked on provisioning a domain the project cannot obtain. Repoint every published `$id`, the semantic-invariant profile URI, and the concept authority reference at `https://raesystem.github.io/rae/`, which is bound to the repository's own GitHub organisation. `$id` is an identifier and is not required to resolve, so this carries no DNS or hosting obligation; the path matches the org's GitHub Pages layout if the schemas are ever served. Refresh the schema-publication content hashes for the 84 affected schemas and record the correction as an amendment to ADR-096. Also re-pin the CHANGELOG.md identity digest, which went stale on dev when release-please appended the 2.0.0 notes. The appended content introduces no retired identity occurrences; the count is unchanged at 297. BREAKING CHANGE: published schema `$id` values move from `https://raes.dev/schemas/` to `https://raesystem.github.io/rae/schemas/`. Consumers pinning or caching schema identifiers must update them. Refs #908
`check_identity_cutover` pinned CHANGELOG.md by whole-file digest, but release-please rewrites that file on every release, inserting each new section above the existing ones. The pin therefore goes stale at every release by construction. It did at v2.0.0: PR #937 back-merged the release into `dev` with the required `verify` check failing, and the failure was overridden rather than fixed, leaving `dev` red on this gate. Add a `generated-release-history` record class that matches how the file is actually maintained. It pins the classified tail exactly and holds everything written above it to the live-tree rule of zero retired identity occurrences, so newly generated content carries no historical exemption. Editing or truncating the tail still fails closed. This is strictly stricter than the whole-file pin it replaces: the old rule allowed any content anywhere in the file so long as the digest was re-recorded, which is exactly the re-record-and-move-on habit that let the override stand. Manifest schema moves to historical-identity-records/v3. Refs #908
The identity gate holds the generated head of CHANGELOG.md to zero retired identity occurrences, but nothing checked the text that lands there. A feature PR into `dev` is squash-merged, so its title becomes the commit release-please writes into the changelog, and any BREAKING CHANGE footer becomes a changelog entry. A retired name in either surfaced days later as a red release PR, far from the change that caused it. Check both at the point they are authored, in the existing PR title guard that already runs on PRs targeting `dev` from the trusted base ref. Body prose is deliberately not checked. Explaining the retired name is legitimate — this PR's own description does it — and only the title and breaking-change footers reach the changelog. The pattern is duplicated in text form rather than imported, because check_pr_title is stdlib-only by design and check_identity_cutover pulls in PyYAML. A test asserts the two patterns agree on the same samples so they cannot drift. Refs #908
The previous commit checked the PR body for BREAKING CHANGE footers. That is the wrong text. This repository squashes with squash_merge_commit_title=PR_TITLE and squash_merge_commit_message=COMMIT_MESSAGES, so the squash commit body release-please parses is the concatenated commit messages; the PR body never reaches it. Supply the PR's commit messages to the guard from the workflow and check those. The messages are fetched with a read-only token and written to a file rather than passed as an argument, so untrusted PR data is never shell-interpolated. The PR body is still checked, so the rule holds if the squash setting changes. Refs #908
Two CI failures on this branch. `ruff format` rejected tests/test_runtime_contracts.py. The namespace rewrite lengthened a URI past the 120-column limit and I only linted the files I edited by hand, not the ones the rewrite touched. `title-guard` failed with "unrecognized arguments: --commit-messages-file". The workflow file comes from the PR head but the checker is deliberately taken from the base ref, so on the PR that introduces the flag the base copy cannot accept it yet. Probe for support before passing it, matching the bootstrap shape the workflow already uses for a missing checker. Commit-message footer enforcement becomes active from the next PR onward; title enforcement is unaffected now. Refs #908
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related fixes on the identity-cutover surface, found by pulling one thread.
1. The published schema namespace pointed at a domain we do not own.
The canonical root was
https://raes.dev/schemas/, adopted during the identity cutover in #921.raes.devis registered to a third party — as wasaces.devbefore it, which belongs to the Auto Care Association (ACES = Aftermarket Catalog Exchange Standard) and redirects to their site.Using a domain the project does not control forfeits the one property the URI form of
$idexists to provide: that the identifier is unambiguously ours to assign. It also left #908 blocked onOBL-908-REMOTE-IDENTITIES-1, an obligation to "provision raes.dev" that cannot be discharged.Per the JSON Schema specification,
$idis an identifier and imposes no behaviour — "JSON Schema does not guarantee that a schema with an HTTP URL identifier is actually resolvable at such URL." Official guidance is to use a domain you control when serving schemas over HTTP, and a non-locatable URI otherwise. Nothing in this repo dereferences$id.The root is now
https://raesystem.github.io/rae/, bound to this repository's own GitHub organisation: uniquely controlled, no DNS obligation, and matching the Pages path if the schemas are ever served. The alternative considered wasurn:uuid:per schema — unimpeachable but opaque across 84 contracts.2. The identity gate was guaranteed to go stale on every release.
check_identity_cutoverpinnedCHANGELOG.mdby whole-file digest, but release-please rewrites that file on every release, inserting each new section above the existing ones. The pin goes stale by construction.It did at v2.0.0. PR #937 back-merged the release into
devwith the requiredverifycheck failing, and it was merged anyway —devhas been red on this gate since 2026-07-27.generated-release-historyreplaces the whole-file pin with one that matches how the file is actually maintained: the classified tail is pinned exactly, and everything written above it is held to the live-tree rule of zero retired identity occurrences. This is stricter than what it replaces — the old rule permitted any content anywhere so long as the digest was re-recorded, which is precisely the re-record-and-move-on habit that let the override stand. Editing or truncating the tail still fails closed.3. Nothing checked the text that lands in the changelog.
Fixing (2) leaves the failure in the right place but still late: a retired name in a commit subject would pass every check on its own PR and surface days later as a red release PR. The guard that already runs on PRs into
devnow rejects retired naming in the title and inBREAKING CHANGE:footers.The text it checks is the text that actually reaches the changelog. This repo squashes with
squash_merge_commit_title=PR_TITLEandsquash_merge_commit_message=COMMIT_MESSAGES, so that is the PR title plus the commit messages — not the PR body. The workflow supplies the commit messages via a read-only API call written to a file, never shell-interpolated. Body prose is deliberately not checked: explaining the retired name is legitimate — this description does it — and prose outside a footer never reaches the changelog.Changes
$idvalues, the semantic-invariant profile URI, and the concept authority reference repointed (133 occurrences across 91 files)._RAES_SEMANTIC_INVARIANT_PROFILE_URIand theLiteralannotations inraes_contractsupdated to match.generated-release-historyrecord class incheck_identity_cutover; manifest schema moves tohistorical-identity-records/v3.pr-title-retired-identityrule incheck_pr_title, with the pattern held equivalent to the gate's by test rather than imported (the guard is stdlib-only by design).adr-index.yamlentries; the index identity digest is re-pinned.Historical records under
docs/decisions/are otherwise untouched, per the ADR-096 boundary.Verification
tools/check_*.pygates pass, includingcheck_identity_cutover— which fails ondevtoday.surfaces,interfaces, andnamespacesdo not trip the word-boundary-anchored rule.Follow-up, not fixed here
OBL-908-REMOTE-IDENTITIES-1on ACES naming audit: 2020 live occurrences remain after the rename (full inventory) #908 is premised on provisioningraes.dev. There is no domain to provision; it should be closed as invalid. The SonarCloud and Ground Control project-identifier halves are unaffected by this PR.devhasenforce_admins: false, which is what allowed a failing required check to be merged. With a single maintainer, enabling it would block all merges, so this PR does not touch it.Refs #908