Rewrite validator in Python using Pydantic - #39
Conversation
|
@copilot review |
There was a problem hiding this comment.
🟡 Changes recommended
The index action has a critical dependency failure, with additional validation and coverage issues unresolved.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request rewrites protocol validation and index generation from R to Python using Pydantic and PyYAML, migrating CI, actions, and tests.
Changes:
- Adds Python models, validation, repository helpers, and index generation.
- Migrates actions and CI to Python/pytest.
- Replaces legacy R entry points and tests.
File summaries
| File | Summary and final findings |
|---|---|
tests/test-repo-utils.R |
Removes legacy repository-helper tests. |
tests/test-malformed-values.R |
Removes malformed-value tests. |
tests/test-generator.R |
Removes R generator tests. |
tests/test_validate_protocol.py |
Adds fixture validation tests. Nit (2 votes): Expected diagnostic assertions were dropped. |
tests/test_repo_utils.py |
Adds URL parsing tests. Nit (2 votes): Repository and ref detection coverage was dropped. |
tests/test_models.py |
Adds Pydantic model tests. |
tests/run-tests.R |
Removes the R test runner. |
scripts/validate-protocol.R |
Removes the R validator. |
scripts/validate_protocol.py |
Adds the Python validator. Moderate (3 votes): Malformed reviews input can raise TypeError. Moderate (3 votes): The unescaped ... alternative can terminate frontmatter incorrectly. Nit (1 vote): Documentation still references deleted R commands. |
scripts/repo-utils.R |
Removes R repository helpers. |
scripts/repo_utils.py |
Adds Python repository and ref helpers. |
scripts/models.py |
Adds Pydantic schemas. Moderate (3 votes): Explicit null method_origin_citation should be rejected. Moderate (2 votes): Explicit null type should be rejected. |
scripts/generate-protocols-yaml.R |
Removes the R index generator. |
scripts/generate_protocols_yaml.py |
Adds the Python index generator. Moderate (2 votes): Generator behavior lacks focused pytest coverage and workflow execution. |
actions/validate-protocols/action.yml |
Switches validation action to Python. |
actions/generate-index/action.yml |
Switches index generation action to Python. Critical (3 votes): The action does not install Pydantic, causing import failure. |
.github/workflows/validate.yml |
Migrates CI to Python and pytest. |
Review details
Suppressed comments (2)
scripts/validate_protocol.py:75
- The frontmatter extractor has the same unescaped
...alternative, which matches any three characters rather than only the YAML document terminator. A three-character YAML line can truncate the parsed frontmatter; escape the dots.
match = re.match(r'^---[\r\n]+(.*?[\r\n]+)(?:---|...)[\r\n]+', content, re.DOTALL)
scripts/validate_protocol.py:1
- This migration removes the R entry points while current user-facing documentation still instructs contributors to run deleted files:
README.md:76,89,PROTOCOL_STANDARD.md:278, andtemplate/CONTRIBUTING.md:55(and to install R packages). After merging, the documented local validation workflow is broken; update those references to the Python commands and dependencies.
#!/usr/bin/env python3
- Files reviewed: 17/17 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for file_path in protocol_files: | ||
| try: | ||
| frontmatter = extract_frontmatter(str(file_path)) |
Co-authored-by: lwaldron <639942+lwaldron@users.noreply.github.com>
|
@copilot review |
|
@copilot review |
|
@copilot review |
Co-authored-by: lwaldron <639942+lwaldron@users.noreply.github.com>
Addressed in 68637f0: fixed remaining validator/model edge cases ( |
Reviewed current PR state on 68637f0 with no further code changes required. I re-ran
Re-reviewed after the latest updates: 68637f0 remains valid with no additional fixes needed, and |
Co-authored-by: lwaldron <639942+lwaldron@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings remain in validation, generation, dependencies, portability, documentation, and regression coverage.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (12)
scripts/generate_protocols_yaml.py:21
Path.rglob()yields paths in no guaranteed order, so a repository with multiple protocols can produce a differently orderedPROTOCOLS.yamlacross checkouts/runs, causing noisy commits and an unstable federation index. The previous R generator enumerated files in sorted order; sort this list before iterating.
protocol_files = list(Path(protocols_dir).rglob("protocol.md"))
scripts/generate_protocols_yaml.py:46
file_pathis aPath, whose string form uses backslashes on Windows. Interpolating it directly createsprotocol_urlvalues such as.../protocols\\example\\protocol.md, which are not valid raw GitHub paths; serialize the path with POSIX separators.
frontmatter['protocol_url'] = f"https://raw.githubusercontent.com/{repository_name}/{repository_ref}/{file_path}"
scripts/generate_protocols_yaml.py:23
- The Python replacement adds explicit safety branches for a missing protocols directory and for a directory containing no
protocol.md, but the new test file never exercises either branch; the deleted R suite covered both cases. Please add tests asserting failure and no index output for each, otherwise a regression could publish an empty index unnoticed.
if not os.path.isdir(protocols_dir):
sys.exit(f"No '{protocols_dir}' directory found. Pass the protocols directory as the first argument.")
protocol_files = list(Path(protocols_dir).rglob("protocol.md"))
if not protocol_files:
sys.exit(f"No 'protocol.md' files found under '{protocols_dir}'.")
scripts/generate_protocols_yaml.py:27
- The generator's repository-detection refusal is also new safety behavior, but the replacement tests always set
GITHUB_REPOSITORYand never cover the no-repository case; the deleted R suite asserted that this path exits without writing an index. Add a test with no environment value and no usable git remote so this guard cannot regress.
repository_name = detect_repository()
if not repository_name:
sys.exit("Could not determine which repository these protocols belong to. Set GITHUB_REPOSITORY to 'owner/name', or run this script inside a git checkout whose 'origin' remote points at the repository hosting them.")
scripts/models.py:1
- The implementation uses Pydantic 2-only APIs (
field_validator,model_validator, andmodel_dump), but the action/workflow installs an unconstrainedpydantic. If a runner already has Pydantic 1 installed,pip install pydanticcan leave it in place and the validator fails at import time; declare the required major version consistently at every install site.
from pydantic import BaseModel, Field, field_validator, model_validator
scripts/validate_protocol.py:75
- This regex requires the closing
---/...marker to be followed by a newline, so valid frontmatter whose terminator is the file's final line (no trailing newline) is returned asNoneand rejected. The protocol format does not require a final newline; allow either a line ending or EOF after the terminator.
match = re.match(r'^---[\r\n]+(.*?[\r\n]+)(?:---|\.\.\.)[\r\n]+', content, re.DOTALL)
scripts/validate_protocol.py:93
- This parser scans the raw file, while the structural checks above strip fenced code. A fenced example containing a
## History & Reviewsheading and valid-looking entries can therefore satisfy the required history even when the protocol has no real history section. Parse the history from the same prose-with-fences-removed view, or explicitly ignore headings inside fences.
with open(file_path, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f]
heading_idx = [i for i, line in enumerate(lines) if re.match(r'^##[ \t]+History & Reviews[ \t]*$', line)]
scripts/validate_protocol.py:373
- When Pydantic rejects any frontmatter field,
fm_modelis set toNoneand this passes an empty list tovalidate_history. For fixtures such asinvalid-status, the rawreviewsarray is otherwise valid, but every markdown review is then falsely reported as missing from frontmatter, adding misleading errors to the real validation failure. Preserve/validate the raw reviews for history comparison, or skip only that comparison when model parsing fails.
fm_reviews = fm_model.reviews if fm_model else []
history_ok = validate_history(file_path, raw_frontmatter, fm_reviews)
scripts/validate_protocol.py:91
- Both this read and the body read later in
validate_protocolstrip only\n, leaving\ron CRLF files. The heading and fence regexes then fail to recognize valid---,## Materials,## Steps, and## History & Reviewslines, so otherwise valid protocols fail on Windows/CRLF checkouts; normalize both reads to strip\r\n.
lines = [line.rstrip('\n') for line in f]
scripts/validate_protocol.py:1
- This new entry point replaces the deleted
scripts/validate-protocol.R, but the repository still documents that R command inREADME.md:76,89andtemplate/CONTRIBUTING.md:55(and the standard/ADRs name it too). Those documented local instructions will fail after this merge; update the user-facing docs/templates alongside the rewrite or retain a compatibility wrapper.
#!/usr/bin/env python3
tests/test_validate_protocol.py:24
- The final fallback accepts any two four-or-more-character words from the expected text, so an unrelated diagnostic can satisfy an invalid fixture (for example, a message containing only
nameandstringpassesnon-string-name). This weakens the fixture suite's guarantee that each rule is still enforced; use field-specific or normalized accepted messages rather than arbitrary word overlap.
expected_words = [w for w in re.findall(r"[A-Za-z][A-Za-z0-9_-]*", expected.lower()) if len(w) >= 4]
output_lower = output.lower()
matches = sum(1 for w in expected_words if w in output_lower)
return matches >= min(2, len(expected_words))
tests/test_validate_protocol.py:64
test_templatenow checks only that validation fails, so the canonical starter can fail for a different reason and this test still passes. The removed R suite asserted the placeholder-citation diagnostic and that no other errors or crashes occurred; preserve that assertion here so template drift remains detectable.
assert not validate_protocol(str(p), str(template_dir))
- Files reviewed: 19/30 changed files
- Comments generated: 4
- Review effort level: Lite
| results = [validate_protocol(str(f), protocols_dir) for f in protocol_files] | ||
|
|
||
| if not all(results): |
| - name: Validate protocols | ||
| shell: bash | ||
| run: Rscript "$GITHUB_ACTION_PATH/../../scripts/validate-protocol.R" "${{ inputs.protocols-path }}" | ||
| run: python3 "$GITHUB_ACTION_PATH/../../scripts/validate_protocol.py" "${{ inputs.protocols-path }}" |
There was a problem hiding this comment.
Yes this should be fixed
|
@copilot review |
|
@copilot review |
Addressed in cc0c654: updated review-status diagnostics to include the reviewer name, fixed the ref-detection test to set
Addressed in cc0c654: updated review-status diagnostics to include the reviewer name, fixed the ref-detection test to set |
| description: 'Commit message used when the index changes.' | ||
| required: false | ||
| default: 'chore: Update PROTOCOLS.yaml [skip ci]' | ||
| r-version: |
There was a problem hiding this comment.
Don't maintain backwards compatibility - this is pre-release
| description: 'Directory holding the protocols, relative to the repository root.' | ||
| required: false | ||
| default: 'protocols' | ||
| r-version: |
There was a problem hiding this comment.
Don't maintain backwards compatibility - this is pre-release
| - name: Validate protocols | ||
| shell: bash | ||
| run: Rscript "$GITHUB_ACTION_PATH/../../scripts/validate-protocol.R" "${{ inputs.protocols-path }}" | ||
| run: python3 "$GITHUB_ACTION_PATH/../../scripts/validate_protocol.py" "${{ inputs.protocols-path }}" |
There was a problem hiding this comment.
Yes this should be fixed
Co-authored-by: lwaldron <639942+lwaldron@users.noreply.github.com>
|
@copilot review |
|
@copilot review |
There was a problem hiding this comment.
🟡 Changes recommended
Exact date validation, frontmatter delimiter handling, and stale ADR references remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
PROTOCOL_STANDARD.md:278
- This updates the canonical standard reference, but the migration leaves dead entry points in
docs/adr/0001-protocol-standard.md,docs/adr/0005-protocol-history-and-reviews.md, anddocs/adr/0006-separate-standard-from-protocol-content.md, which still refer to the deleted R validator/generator. Please update those ADR references (and any remaining repository references) so the documentation does not direct readers to scripts that no longer exist.
These rules are enforced by `scripts/validate_protocol.py`, which runs on every pull request.
scripts/models.py:84
- The protocol date validator has the same canonical-format hole:
date.fromisoformat()accepts compact/week-date strings under Python 3.11, and the laterstr(date)comparison normalizes them so they can be accepted despite the standard requiringYYYY-MM-DD. Apply an explicit full-match check before parsing (and cover it with a regression case).
if isinstance(v, str):
try:
return date.fromisoformat(v)
except ValueError as exc:
raise ValueError("Must be YYYY-MM-DD date") from exc
scripts/validate_protocol.py:75
- The closing-delimiter alternative is not anchored to the beginning of a line, so any
---or...text after a newline can terminate the frontmatter. For example, an indented---inside a valid YAML block-scalar description is treated as the end marker, truncating the mapping and causing valid protocols to be rejected (or incomplete metadata to be generated); anchor the delimiter with multiline matching.
match = re.match(r'^---[ \t]*[\r\n]+(.*?[\r\n]+)(?:---|\.\.\.)[ \t]*(?:[\r\n]|$)', content, re.DOTALL)
- Files reviewed: 22/33 changed files
- Comments generated: 1
- Review effort level: Lite
| if isinstance(v, str): | ||
| try: | ||
| return date.fromisoformat(v) | ||
| except ValueError as exc: | ||
| raise ValueError("Must be YYYY-MM-DD date") from exc |
Co-authored-by: lwaldron <639942+lwaldron@users.noreply.github.com>
PR #39 committed ten `.pyc` files. They are build artifacts keyed to the interpreter version, so every contributor on a different Python adds their own set: running the test suite locally on 3.14 produced ten more alongside the committed 3.12 ones, and `git add -A` swept them into a commit before I noticed. Untracked and gitignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Push the index with a deploy key, not GITHUB_TOKEN Index generation has been failing since `require-pr-review` was added to `agent-protocols`. The generator runs fine; the push is rejected with GH013, because `github-actions[bot]` is not a bypass actor. It cannot be made one. Bypass actors of type Integration must be GitHub Apps installed on the organisation, and GitHub Actions is not one — the API rejects app id 15368 outright. A write-scoped deploy key is a valid bypass actor type and is scoped to the single repository. `actions/checkout` takes an `ssh-key` input, loads the key, and sets the remote to SSH, so the composite action's existing `git push` needs no change. Only the caller's checkout does. Documented in the template workflow, in the action's push step, and as an adoption step in the README, since an adopter with a protected branch hits this before their first index generates. Two notes worth having next to the code. A deploy-key push *does* trigger workflows where a GITHUB_TOKEN push does not, so the loop protection matters now: the index commit is outside the caller's `paths:` filter, and the default commit message carries [skip ci] besides. Also corrects a sentence I broke in the citation rename. The README claimed the starter's `method_origin_citation` is the placeholder "in the required `protocol_citation`", which is incoherent — the placeholder is in `protocol_citation`, and `method_origin_citation` ships commented out. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Untrack the bytecode caches PR #39 committed ten `.pyc` files. They are build artifacts keyed to the interpreter version, so every contributor on a different Python adds their own set: running the test suite locally on 3.14 produced ten more alongside the committed 3.12 ones, and `git add -A` swept them into a commit before I noticed. Untracked and gitignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Hold the deploy key in a branch-restricted environment A repository secret is readable by any workflow in the repository, including one added in a pull request — secrets are withheld from fork pull requests, but same-repo branch pull requests do receive them. With five contributors about to get write access and a key that bypasses branch protection, that is the wrong place for it. An environment secret whose deployment branch policy allows only `main` closes it: a job can read the secret only if it declares the environment and is running on `main`. The `on: push: branches: [main]` trigger already prevents pull-request execution today, so this makes the property structural rather than dependent on nobody adding a workflow_dispatch later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Authenticate as a GitHub App, not a deploy key A deploy key is a static credential valid until someone revokes it, and ruleset bypass applies to the whole ruleset — so the key could force-push or delete `main`, not merely skip the pull-request requirement. A purpose-built App holds `contents: write` and nothing else, mints a token that expires after an hour, and installs once on the organisation rather than once per repository, which matters as soon as there is a second content node. GitHub's own deploy-key page recommends this; I offered four options earlier and an App was not among them, which was the gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address review: scope the token, and don't break unprotected adopters The template ran `create-github-app-token` unconditionally while the README told adopters with an unprotected `main` to skip creating the App. Both inputs would be empty, the step would fail before checkout, and the first index would never generate. It is now skipped when no App is configured and falls back to GITHUB_TOKEN, so the template works either way. `secrets` is not available to a step `if:`, hence the job-level `env`. `permission-contents: write` is now requested explicitly rather than inheriting whatever the installation happens to hold, so granting the App another permission later cannot silently widen the token. GITHUB_TOKEN drops to `contents: read` in the content repository, where the App always pushes. The template keeps `contents: write`, because its fallback path pushes with GITHUB_TOKEN — reducing it there would have broken the case the fallback exists for. The comment says which is which. The action's comment still described a deploy key and `ssh-key` after the switch to an App token, and claimed the push "cannot loop". Neither was true of the action: loop protection lives in the template's `paths:` filter and `[skip ci]` default, and a caller with neither will loop. Both corrected, with the guarantee scoped to the caller. Commit attribution is now an input rather than a hardcoded github-actions[bot], defaulting to it so existing callers are unaffected. The workflows pass the App's own identity, since that is what pushes. Declined, deliberately: pinning generate-index@v0 to a commit SHA, and protecting tags on this repository. The escalation path needs write access to this repo, which is the same set of people who already have write access to the content repo, and pinning would give up the moving-tag design ADR 0006 §3 argues for. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Track main rather than a release tag `@v0` resolved to a commit from before the Python rewrite — 22 behind main — so both content-repo workflows were running the R tooling. The Pydantic validator merged in #39 was validating nothing, and CI was green throughout. That is the failure mode of a moving tag nobody moves: silent, not loud. The tag exists to decouple content repositories from a release cadence, which assumes consumers outside the org's control. There is one content repository, the same person owns both, and nothing external consumes either — so the tag was not decoupling anyone, it was a pointer someone had to remember to move. More than an expedient, though. Pinning is the ordinary answer for a library, where freezing a dependency is reasonable. It is a stranger answer for a standard, where conforming to last year's version is not obviously conformance. A node validating against a snapshot enforces rules the standard no longer publishes, and reports success while doing it. The README says this is a live question rather than settled, and asks a node that needs to pin to say why — the answer should come from what federated nodes actually need, not from a default chosen before any existed. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address review: reconcile the docs, fix the committer identity, serialize runs The README said three different things at once. Line 39 still told adopters to pin to a release tag, the Releasing section still presented `v0` as the distribution channel, and the new paragraph said `@main`. Releases are now described as a record rather than a channel, with `v0` kept for anyone who has a reason to pin. ADR 0011's consequence that "consumers must reference `@v0`" no longer describes the shipped template, so it carries a banner saying so. Not superseded: whether a node should ever pin is deliberately still open, and settling it warrants an ADR against both 0011 and 0006 §3. The template workflow's own header still said the generator was "pinned below" while the step beneath it resolved `@main` — the file contradicted itself. The committer email was wrong: it used the App registration id from INDEX_APP_ID, where the address needs the bot's *user* id. Verified against the API — the registration is 4936017, the bot user is 328912240. With the wrong value the push still succeeds and GitHub silently declines to associate the commit with the bot, which is the kind of failure nobody notices. Both workflows now resolve it at run time, so the template works for any adopter's App. Two merges in quick succession would have run concurrently, and the second push would have been rejected as non-fast-forward, its checkout predating the first run's index commit. A concurrency group queues them; cancel-in-progress stays false because a cancelled run leaves the index describing the previous commit. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Address review: check out the tip, fail loudly, correct the input contract `actions/generate-index`'s own `committer-email` documentation still said the prefix is the App registration id, which is the thing the last commit fixed in the workflows. The input description is the public contract, so a caller following it would have lost bot attribution exactly as this repository did. The concurrency group alone did not make the push safe: `actions/checkout` defaults to the event SHA, so a queued run still started from the commit that triggered it and pushed a non-fast-forward once `main` had moved. Both the template and the content repository now check out the branch tip. `echo "id=$(gh api ...)"` exits 0 when the lookup fails, writing an empty id and falling back to github-actions[bot] while the App did the push. Assigned under `set -euo pipefail`, with empty failing the step. Added `workflow_dispatch`: tracking the generator at `@main` means the next run uses it, but a generator change upstream raises no event in a content repository, so the index can sit stale until an unrelated local change. Named `schedule:` as the fuller answer rather than pretending the gap is closed. Two README errors of mine: the link pointed at an anchor that never existed, because bold text is not a heading — it is a heading now; and it called `v0` something to pin to, two lines after advising a commit SHA, when `v0` moves on every release and is no more fixed than `main`. The immutable `v0.x.y` tags are what that sentence meant. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Rebase and retry the index push; pin the generator's ref `ref: main` refreshed the checkout before generation but did not make the push race-free: a commit landing while the job installs or generates still makes it non-fast-forward. Worse, a docs-only commit does not match the workflow's `paths:` filter, so nothing re-runs and the index stays stale silently — the exact failure this whole change set exists to remove. The action now rebases onto the current tip and retries, up to three times. The index commit touches one generated file, so it rebases cleanly unless something else edited that file, and that case fails loudly rather than being forced. The checkout is pinned to `main` but the generator reads GITHUB_REF_NAME to build protocol_url values, so a workflow_dispatch from another branch or tag would index main's content under that ref's URLs. Pinned for the generator step too. Also: the README called `v0.x.y` tags immutable two paragraphs after saying a tag can be retargeted and only a SHA is genuinely immutable. They are versioned release tags, fixed by convention, and the sentence now says so. 77 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Take the ref as an input; guard detached HEAD; banner ADR 0006 GITHUB_REF_NAME is runner-provided and a workflow cannot reliably override it through `env`, so the last commit's attempt to force `main` for the generator would not have worked. `detect_ref()` now checks PROTOCOL_INDEX_REF first — a name the runner does not own — and the action exposes it as a `ref` input. Two tests cover the precedence and that an empty value does not shadow the normal path. The retry loop I added last commit introduced a regression: `git rev-parse --abbrev-ref HEAD` returns the literal "HEAD" when detached, so `HEAD:$branch` would have created a branch called HEAD. Before the retry, a detached push simply failed. Now it fails deliberately, with a message saying why. ADR 0006 §3 still described consuming the actions pinned to a release tag, which contradicted the README and template. Bannered like 0011, and for the same reason: the pinning question is deliberately open, so this is an amendment rather than a supersession. The README said `v0` moves on every release. It does not — the retarget workflow skips pre-releases, non-vX.Y.Z tags, and releases that are not highest on their line, as the Releasing section below says. 79 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the push retry; fail loudly instead The rebase-and-retry was two commits old and had produced two findings of its own: it pushed an index built from the pre-rebase tree, so a colliding commit that touched protocols/ would publish a stale index; and its HEAD:$branch refspec would have created a branch literally named HEAD on a detached checkout, which a plain push had simply refused to do. Making it correct means regenerating and amending inside the retry loop, which puts a second invocation of the generator into a shared action every future node runs. The race it defends against needs a merge inside a one-minute window in a repository where one person merges. So: a single push. A collision fails the run visibly and it is re-runnable from the Actions tab or by workflow_dispatch. A loud failure beats a silently stale index, which is the argument this whole change set rests on. The detached-HEAD guard goes with it — a plain push already refuses that case, which is what it did before the retry existed. 79 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Widen the ADR amendment banners to what they actually amend Each banner named one passage while the ADR carried several saying the same thing: 0011 also says in its Decision that a consumer who cannot accept the moving tag pins a release tag, and that a new repository starts on `@v0`; 0006's Consequences also call the content repository a second place where a version can be pinned. A banner that names one of those implies the rest still hold. Both now mark every consumer-facing passage as historical, and say what each ADR still decides — `v0.x` versioning and two separate axes for 0011, distributing the tooling rather than vendoring it for 0006. The bodies stay as written, per the convention these ADRs already use. 79 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
No description provided.