Skip to content

feat(adr): capabilities active at adoption need no add decision (ADR-305, #582) - #583

Merged
aaronsb merged 5 commits into
mainfrom
feat/adr-baseline-capabilities
Sep 27, 2026
Merged

aaronsb merged 5 commits into
mainfrom
feat/adr-baseline-capabilities

Conversation

@aaronsb

@aaronsb aaronsb commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Closes #582. Stacked on #581.

Summary

  • Decided: a project lists under baseline in adr.yaml the capabilities it had when it adopted adr/v1. Those need no add, and the first change on one needs no prior record. A capability declared after adoption still needs an add. ADR-305 amends ADR-304 §6 to say so, and widens §6's stale-citation wording to the targets adr cite already warns on (deprecated, rejected, abandoned).
  • Trades away: a record of why each baseline capability exists.
  • One-way? No. Removing a name from baseline restores the check for it.
  • Probes: Confident: no v0 golden moved; four new goldens cover baseline exemption, an unknown baseline name, a first change standing on the baseline, and a second change that must name the first (ordered by date, then number). Not confident: the "first change needs no prior" clause is my extension of the operator's pick, since ADR-186 couldn't migrate without it. Please check that it matches what you meant by "declared start active".
  • Inversion: at one end every capability needs a written add, which misstates a corpus that predates the contract. At the other end the vocabulary is its own authority and new capabilities enter unrecorded. The rule sits between them.

Changes

  • rules_v1.py: validates baseline, and the add check skips baseline names. rule_v1_change_replaces lets the first change on a baseline capability stand without a prior. decision_order is now shared with cmd_cite.
  • adr.yaml: twelve baseline capabilities. adr has ADR-304 as its add, so it isn't listed.
  • ADR-305 (proposed, change on adr, amends ADR-304#6). Its operator basis quotes the option picked in session.
  • ADR-186 migrated to v1 as a change on testing, with model: unrecorded. Every original section is kept, and the Summary is drawn from the existing text.

Not done

ADR-123 stays v0. It spans attend, matching and disclosure, and only constrain may list several capabilities. It needs either a split or the multi-capability rule, which this PR does not decide.

make test-adr: lint 12, archive 41, golden 101, macro 31, all passing. adr lint on the repo: 0 errors, 0 warnings.

…t change's prior (#582)

adr.yaml may list the capabilities a project had when it adopted adr/v1
under baseline. The add check skips them, and the first change on one
needs no prior record. A later change names the earlier decision, ordered
by date then number; cite now shares that ordering.
…86 migrates to v1 (#582)

agent-ways declares twelve baseline capabilities. ADR-305 also widens §6's
stale-citation wording to the targets adr cite already warns on. ADR-123
stays v0: it spans three capabilities and only constrain may list several.
@aaronsb

aaronsb commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Review: baseline capabilities (ADR-305, #582)

What this changes: baseline in adr.yaml exempts listed capabilities from the add check. The first change on a baseline capability needs no prior. ADR-305 amends ADR-304 §6, and ADR-186 migrates to v1.

Verified: assemble --check is clean, make test-adr passes, and repo adr lint gives 0/0. No v0 golden moved. The cmd_cite refactor keeps the old ordering: (date, base, part) sorts the same as (date, (base, part)), and titles are \d+(\.\d+)?, so int() can't raise. A malformed baseline (scalar, null, mapping, [1, 2]) raises a config error and doesn't crash. I ran all of these in a scratch copy of the v1 fixture.

1. High: which change counts as "first" shifts when older records migrate, and a frozen record can't be fixed

rules_v1.py:304-311, 337. _has_decision_on only counts v1 records. So "first change on a baseline capability" means the first one migrated, not the first one historically. ADR-186 (2026-09-17, testing) is exempt today. Suppose someone later migrates an older v0 testing record (ADR-142 or ADR-144, both in ADR-186's related) as a change on testing. That record becomes the first change and is exempt. ADR-186 then fails with "a change decision supersedes or amends a prior decision on 'testing'". Once ADR-186 is accepted v1 on main, amends is outside mutable_after_accept, so adding it trips rule_v1_frozen. Either way it errors, and ADR-123 plus the rest of the migration make this likely. I reproduced the underlying behaviour: a second change dated earlier than an existing exempt one moves the error onto the existing one.
Fix: tie the exemption to adoption, not to the relative order of records. Give baseline an adoption date, e.g. baseline: {adopted: 2026-09-26, capabilities: [...]}. A change on a baseline capability dated on or before that date stands on the baseline. After that date it names a prior, unless no decision on the capability exists. This also gives "declared after adoption" a meaning the tool can check.

2. Medium: rejected and abandoned decisions count as the prior

rules_v1.py:304-311. Any status counts. Probe: ADR-117 is a rejected change on a baseline capability, and ADR-118 is a later bare change. ADR-118 fails and must supersede the rejected record. Superseding an abandoned record passes, but it warns for a missing reciprocal superseded_by on the dead record. And ADR-305 §3 itself treats citations of rejected or abandoned records as stale. When no live prior exists, the baseline is the real prior.
Fix: skip other.status in {'rejected', 'abandoned'} in _has_decision_on, and add a golden for it.

3. Medium: the operator basis records the agent's words as the operator's

ADR-305:8-10. said: "Declared start active (Recommended)" is an option label the agent wrote, including its own "(Recommended)" steer. ADR-304 §11 defines said as the operator's words, verbatim where written. The selection was the operator's. The words were not. level: directed means "the operator made the call". That holds for choosing option 2 of #582. It doesn't cover the rest of the record:

Fix: record the act honestly. For example: said: "Declared start active", via: session 2026-09-27, multiple-choice on #582: selected option 2 of 3 (agent-written label, marked Recommended). Either drop to level: guided, or keep directed only after the operator confirms the first-change clause and §3. Another option is to move §3 into its own record or ground it in evidence (cmd_cite.py:7).

4. Low-medium: ADR-305 does not fully describe what the code does

  • §2 (:53) says "once a decision on the capability exists". The code leaves out constrain and hard-codes kind == 'decision'. It counts every status. And it exempts only a change with no supersedes/amends edges: a first change that amends an unrelated v1 record still fails. State these, or change the code.
  • §3 (:55-60) lists superseded, deprecated, rejected and abandoned. cmd_cite (cmd_cite.py:7, V1_NON_ACTIVE_STATUSES) also warns on archived. Add it, or the "already behaves this way" claim is inaccurate.
  • :29 Decided puts the §3 widening into the headline decision. It's a separate decision with a separate basis (see 3).

5. Low: order edge cases

  • decision_order sorts '' first when a date is missing. An undated second change is treated as the earliest, so the error lands on the dated record that really came first. The undated record gets only "Missing date" (probe H). Sort a missing date last, e.g. (adr.date is None, str(adr.date or ''), ...), so the error lands on the defective record.
  • Dates compare as strings. A non-ISO date: silently sorts wrong. If lint doesn't already check the date format, validate it or parse it.
  • kind == 'decision' (:307) conflicts with the module's "kinds are data" rule. The check is load-bearing, because without it a spec on the capability would count. Keying on the kind schema's verb: required keeps it generic.

6. Goldens: none pass vacuously, but some claims go untested

tests/adr-golden-test.sh:232-247. Each new golden fails if its code path is removed, which I checked against the outputs. v1-lint-baseline-change also quietly covers the constrain exclusion, since ADR-104 constrain: "*" precedes ADR-117. Say so in the comment. Gaps:

  • "ordered by date, then number": ADR-117 and ADR-118 share a date, so only the number tiebreak runs. Date precedence isn't exercised. Date ADR-118 earlier than ADR-117 and expect the error on ADR-117. I verified the code handles this.
  • No golden for a malformed baseline, which is the expected a list branch.
  • v1-lint-baseline baselines search, which also has an accepted cut (ADR-111) and a proposed add (ADR-113). It still discriminates, but a capability with no records would isolate the exemption.
  • No positive golden where the second change names the first and passes.
  • edit doesn't fail when str.replace changes nothing (pre-existing), so a fixture drift on the surfaces: anchor would show up only as a confusing golden diff.

7. ADR-186 migration: mostly faithful, one reframing

I compared against git show origin/main:...ADR-186.... Decided, Trades away, One-way, the Confident probe and the Inversion all trace to the original's Decision, Negative, Reversibility, Positive and Alternatives. The basis evidence quotes the Context. model: unrecorded and no operator basis are right under ADR-304 §7. One reframing: the Not confident probe (ADR-186:28, "whether a network fault in tier 1 gets read as a regression") turns a stated certainty ("A network fault fails the job", Negative) into an open question the original never raises. Better: Not confident: "network faults fail tier 1, and the record gives no mitigation." Also, the change on testing depends on finding 1 staying stable.

Assessment

The code is small and defensive, and it validates malformed config cleanly. Finding 1 is the one to settle before merge: under the current rule, migrating older records can break ADR-186 once it's frozen. Findings 2 and 3 are quick fixes. Recommend revise.


AI-assisted review via Claude

…so the prior never depends on migration order

Only decisions dated after adoption count as the prior for a bare change on
a baseline capability; constrain, rejected and abandoned decisions never do.
Goldens cover date-before-number ordering, a malformed baseline, a second
change that names the first, and a rejected prior. ADR-305 now describes the
rule as built, records the operator's picks as selections of agent-written
labels at level guided, and includes archived in the stale-citation list.
ADR-186's Summary names the container and drops a probe the original never
raised.
@aaronsb

aaronsb commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Review addressed in a6f71e8:

  1. Unstable "first change" (high). Fixed, but not with a plain adoption date. That version would reject the first post-adoption change on a capability such as cli, which has no record at all. The implemented rule: baseline: {adopted, capabilities}. A bare change stands on the baseline if it's dated on or before adopted, or if no earlier decision on that capability is dated after adopted. Only post-adoption decisions count as a prior, and every one of those was written as v1, so migrating an older record never changes the outcome. The trade-off is listed under Negative in ADR-305.
  2. Rejected and abandoned priors. They are excluded now, along with constrain. v1-lint-baseline-change-rejected-prior covers this.
  3. Operator basis. Both picks are recorded as selections of labels the agent wrote, at level: guided. The §4 widening is grounded in the operator's "accept as built" selection and in the cmd_cite evidence. The operator also stated the base decision behind ADR-186 in their own words, and that is quoted verbatim.
  4. Doc fidelity. ADR-305 §3 now states the constrain, status and no-edge conditions. §4 includes archived.
  5. Ordering. A missing or non-ISO date can't stand on the baseline (_iso_date). Decisions are identified by the presence of a verb, not by a hard-coded kind name.
  6. Goldens. They're rebuilt on a clean export capability. The new cases cover a pre-adoption change, date-before-number ordering (ADR-118 is numbered lower but dated later than ADR-119, and it alone errors), a malformed list form, a bad date plus an unknown name, a second change that names the first, and a rejected prior.
  7. ADR-186's Not-confident probe. Replaced with "none stated in the original record". The Decided bullet now names the container.

Still for the operator: whether ADR-186 gets an operator basis. That needs a considered entry in the operator's own words, per ADR-304 §11.

Base automatically changed from feat/adr-adopt-v1 to main September 27, 2026 15:25
@aaronsb
aaronsb merged commit 7812001 into main Sep 27, 2026
6 checks passed
@aaronsb
aaronsb deleted the feat/adr-baseline-capabilities branch September 27, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

adr/v1: capabilities that predate the record corpus have no honest add decision

1 participant