diff --git a/docs/architecture/INDEX.md b/docs/architecture/INDEX.md index 2a212636..de865395 100644 --- a/docs/architecture/INDEX.md +++ b/docs/architecture/INDEX.md @@ -123,6 +123,7 @@ _Documentation structure, tooling, coherence_ | [ADR-302](./documentation/ADR-302-unified-documentation-model.md) | A unified documentation model — typed graph, ways packaging, cross-repo convergence | Accepted | | [ADR-303](./documentation/ADR-303-active-set-semantics-adr-archive-and-supersession-reading-for-the-adr-corpus.md) | Active-set semantics, adr archive, and supersession reading for the ADR corpus | Accepted | | [ADR-304](./documentation/ADR-304-typed-decision-records-the-adr-v1-contract.md) | Typed decision records: the adr/v1 contract | Accepted | +| [ADR-305](./documentation/ADR-305-capabilities-active-at-adoption-need-no-add-decision.md) | Capabilities active at adoption need no add decision | accepted | ## Legacy (Pre-Domain Numbering) diff --git a/docs/architecture/adr.yaml b/docs/architecture/adr.yaml index e7990809..b8dfce93 100644 --- a/docs/architecture/adr.yaml +++ b/docs/architecture/adr.yaml @@ -60,6 +60,14 @@ capabilities: loop: The development loop skills (start, develop, merge, release, wrap) testing: The test suites and the live install fixture +# Capabilities that were active when agent-ways adopted adr/v1 on the +# adopted date. No record added them, so none needs an add decision. A change +# on one with no prior record to name stands on the baseline. Any capability +# declared after adoption needs an add (ADR-305). +baseline: + adopted: 2026-09-27 + capabilities: [docs, method, matching, disclosure, authoring, cli, attend, install, config, governance, loop, testing] + # Retire targets name a surface; these are agent-ways' namespaces. surfaces: cli: {} diff --git a/docs/architecture/documentation/ADR-305-capabilities-active-at-adoption-need-no-add-decision.md b/docs/architecture/documentation/ADR-305-capabilities-active-at-adoption-need-no-add-decision.md new file mode 100644 index 00000000..92b635f3 --- /dev/null +++ b/docs/architecture/documentation/ADR-305-capabilities-active-at-adoption-need-no-add-decision.md @@ -0,0 +1,110 @@ +--- +contract: adr/v1 +kind: decision +verb: change +capability: adr +amends: [ADR-304#6] +basis: + - operator: aaronsb + level: guided + said: "Declared start active (Recommended)" + via: "session 2026-09-27: the operator selected option 1 of 4 on #582; the label was written by the agent" + - operator: aaronsb + level: guided + said: "the prior decision (the base decision) was that we could no longer test agent-ways correctly, directly on the host, it had to go into a container" + via: session 2026-09-27, reviewing PR #583 + - operator: aaronsb + level: guided + said: "Accept as built (Recommended)" + via: "session 2026-09-27: the operator selected this for decisions 3 to 9 of the ADR-304 stack handoff, which included widening the §6 stale-citation wording; the label was written by the agent" + - evidence: "issue #582: ADR-186 changes a testing capability that existed before any record described it" + - evidence: "adr cite already warns on superseded, deprecated, rejected, abandoned and archived targets (cmd_cite.py, V1_NON_ACTIVE_STATUSES)" +agent: + name: Claude + model: claude-opus-5-5 +considered: + - operator: aaronsb + said: "ok. so basically, I think it's the correct direction and implements the change to adr as discussed." + via: session 2026-09-27, reviewing PR #583 + covers: [] +status: accepted +date: 2026-09-27 +deciders: + - aaronsb + - Claude +related: + - 304 +--- + +# ADR-305: Capabilities active at adoption need no add decision + +## Summary + +- **Decided:** a project records under `baseline` in `adr.yaml` the date it adopted adr/v1 and the capabilities it already had then. Those capabilities need no `add` decision, and a `change` on one with no prior record to name stands on the baseline. A capability declared after adoption still needs an `add`. +- **Trades away:** a record of why each baseline capability exists. The vocabulary line is its only description. +- **One-way?** No. Removing a name from `baseline` restores both checks for it. +- **Probes:** *Confident:* the outcome for a record does not depend on which other records have been migrated, since only decisions dated after adoption can be its prior, and every one of those was written as v1. *Not confident:* whether `baseline` will be used to skip an `add` for a capability that is actually new. +- **Inversion:** at one end every capability needs a written `add`, which misstates history for a corpus that predates the contract. At the other end the vocabulary is its own authority and nothing needs an `add`, which lets new capabilities in unrecorded. This decision exempts only what existed before adoption. + +## Context + +ADR-304 §6 requires every capability in the vocabulary to have an accepted `add` decision. Most of agent-ways' capabilities (matching, attend, install, testing and others) worked long before any record described them. Migrating an old record as an `add` misstates it. ADR-186 is the example the operator gave: it moved testing off the host and into a container, which changed a testing capability that no record had added. #582 set out three options: a baseline `add` record per capability, declared capabilities starting active, and a `change` that may list several capabilities. The operator chose the second, and the agent designed the mechanism within that direction. + +## Decision + +### 1. Baseline capabilities + +`adr.yaml` may carry: + +```yaml +baseline: + adopted: 2026-09-27 + capabilities: [docs, testing] +``` + +`adopted` is a `YYYY-MM-DD` date. Every name in `capabilities` must be in the vocabulary. Either defect fails lint. + +### 2. The add check, amended + +The ADR-304 §6 bullet on accepted `add` decisions now reads: + +- Every capability in the vocabulary, except those listed in `baseline`, has an accepted `add` decision. This warns while any v0 record remains and fails after, so a corpus that is still migrating does not fail on every capability. + +### 3. A change on a baseline capability + +ADR-304 §3 requires a `change` to supersede or amend a prior decision on the same capability. A `change` with no `supersedes` or `amends` edge on a baseline capability instead stands on the baseline when either: + +- it is dated on or before `adopted`, or +- no earlier decision on that capability is dated after `adopted`. Earlier means by date, then number. `constrain` decisions do not count, and neither do rejected or abandoned ones. + +Only decisions dated after adoption count as a prior, so migrating an older record never changes the outcome for another record. A record with no date, or a date that is not `YYYY-MM-DD`, cannot stand on the baseline. + +### 4. Stale citations, widened + +The ADR-304 §6 `doclint` bullet on superseded decisions now reads: + +- A citation of a superseded, deprecated, rejected, abandoned or archived record warns. For a superseded record, the warning names the successor. + +`adr cite` already behaves this way. The amendment brings the text in line with it. + +## Consequences + +### Positive + +- Migrating a record no longer requires inventing history. ADR-186 migrates as a `change` on `testing`. +- An adopting project writes one list and one date, not one record per capability. + +### Negative + +- ADR-123 spans attend, matching and disclosure. Only `constrain` may list several capabilities, so ADR-123 stays on v0 until it is split or a rule for multi-capability changes is decided. +- A post-adoption `change` need not name a pre-adoption record on the same capability, even after that record is migrated. The v0 prior warning still applies when it does name one. + +### Neutral + +- Projects that declare no `baseline` behave as before. + +## Alternatives Considered + +- **A baseline `add` per capability.** This keeps the history complete, but at the cost of twelve records written after the fact, each dated at adoption. +- **A multi-capability `change`.** This would let ADR-123 migrate, but it loosens the one-change, one-capability rule, and it does not fix the missing `add` decisions. +- **Count every v1 decision as a prior.** This was the first version of this PR. Which change counted as first then depended on the order in which records were migrated, and a frozen record could start failing when an older one migrated. diff --git a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md index e44b62b1..b7ff0599 100644 --- a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md +++ b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md @@ -1,4 +1,11 @@ --- +contract: adr/v1 +kind: decision +verb: change +capability: testing +agent: {name: Claude, model: unrecorded} +basis: + - evidence: the reviews of PRs #501, #502, #504 and #508, each of which found a defect on the install path by reading the code, none of it run end to end on a clean machine status: Accepted date: 2026-09-17 deciders: @@ -13,6 +20,14 @@ related: # ADR-186: Live integration fixture: install-path test levels and the tier 2 gate +## Summary + +- **Decided:** the install path is tested in a container, at two levels. Tier 1 installs and configures with no API key, on every pull request that touches the path. Tier 2 exercises a model with a key, on dispatch or a schedule only. +- **Trades away:** job time and network dependence on tier 1, and tokens on a fixed cadence for tier 2. +- **One-way?** No. The fixture is additive, and removing the job removes the gate and nothing else. +- **Probes:** *Confident:* the #501 shape (the refusal, the recovery and the kept hooks) is asserted on every pull request. *Not confident:* none stated in the original record. +- **Inversion:** at one end, a model runs on every pull request, so a fork's check passes with no secret and every pull request spends tokens. At the other end nothing runs a clean install and reviews keep finding install defects by reading code. The two tiers sit between them. + ## Context The install path is the installer script, `make setup` with its prebuilt downloads, `ways reconcile` into a config directory that already holds a user's own files, and the hook scripts that Claude Code runs from the merged `settings.json`. The reviews of PRs #501, #502, #504 and #508 each found a defect on that path by reading the code, and each said the same thing: none of it had been run end to end on a clean machine. diff --git a/hooks/ways/documentation/adr/adr-tool b/hooks/ways/documentation/adr/adr-tool index 66fb2684..4551934d 100755 --- a/hooks/ways/documentation/adr/adr-tool +++ b/hooks/ways/documentation/adr/adr-tool @@ -592,6 +592,17 @@ def _str_list(value) -> Optional[list]: def _mapping(value) -> dict: return value if isinstance(value, dict) else {} +def _iso_date(value) -> Optional[str]: + """value as a YYYY-MM-DD string, or None. YAML may load a date as one.""" + text = str(value) if value is not None else '' + return text if re.fullmatch(r'\d{4}-\d{2}-\d{2}', text) else None + +def v1_baseline(ctx) -> tuple: + """(adoption date or None, set of baseline capability names).""" + baseline = _mapping(ctx.config.get('baseline')) + return (_iso_date(baseline.get('adopted')), + set(_str_list(baseline.get('capabilities')) or [])) + def v1_kinds(ctx) -> dict: return _mapping(ctx.config.get('kinds')) @@ -700,12 +711,26 @@ def rule_v1_config_shape(ctx): bad("contract: adr/v1 declares no capabilities") if 'surfaces' in config and not isinstance(config['surfaces'], dict): bad("surfaces: expected a mapping of namespace to settings") + if 'baseline' in config: + baseline = config['baseline'] + names = _str_list(baseline.get('capabilities')) if isinstance(baseline, dict) else None + if names is None: + bad("baseline: expected 'adopted' (a YYYY-MM-DD date) and 'capabilities' (a list of names)") + else: + if not _iso_date(baseline.get('adopted')): + bad("baseline.adopted: expected a YYYY-MM-DD date") + if isinstance(config.get('capabilities'), dict): + for name in names: + if name not in config['capabilities']: + bad(f"baseline: '{name}' is not in the capabilities vocabulary") @config_rule(contract=V1) def rule_v1_capabilities_added(ctx): - """Every capability in the vocabulary has an accepted `add` decision. This - warns while v0 records remain, so a migrating corpus is not failed on every - capability, and fails once migration is done (ADR-304 §6).""" + """Every capability in the vocabulary has an accepted `add` decision, except + those in `baseline`: capabilities that were active when the project + adopted the contract, which no record added. This warns while v0 records + remain, so a migrating corpus is not failed on every capability, and fails + once migration is done (ADR-304 §6, amended by ADR-305).""" added = set() for adr in ctx.corpus: if (is_v1_record(adr, ctx) @@ -716,8 +741,9 @@ def rule_v1_capabilities_added(ctx): # hold the check at warning. migrating = any(not is_v1_record(adr, ctx) and not is_archived(adr.path) for adr in ctx.corpus) + _, baseline = v1_baseline(ctx) for name in v1_capabilities(ctx): - if name not in added: + if name not in added and name not in baseline: ctx.config_issues.append(Issue( f"capability '{name}' has no accepted add decision", 'warning' if migrating else 'error')) @@ -840,11 +866,40 @@ def rule_v1_edges(adr, ctx): if section and not section_exists(target, section): v1_issue(adr, f"{field_name}: ADR-{number} has no section '{section}'") +def decision_order(adr) -> tuple: + """Records in decision order: by date, then number (ADR-304 §3).""" + base, _, part = str(adr.number or '0').partition('.') + return (str(adr.date or ''), int(base or 0), int(part or 0)) + +def _stands_on_baseline(capability: str, adr, ctx) -> bool: + """A change with no prior edge stands on the baseline when the capability + is in it and either the change predates adoption, or no live decision on + the capability has been made since adoption before it. Only decisions + dated after adoption count: each was written as v1, so migrating an older + record never moves the answer (ADR-305).""" + adopted, baseline = v1_baseline(ctx) + when = _iso_date(adr.date) + if capability not in baseline or not adopted or not when: + return False + if when <= adopted: + return True + for other in ctx.corpus: + other_when = _iso_date(other.date) + if (other is not adr and is_v1_record(other, ctx) + and other.frontmatter.get('verb') not in (None, 'constrain') + and str(other.status or '').lower() not in ('rejected', 'abandoned') + and covers(other, capability) + and other_when and other_when > adopted + and decision_order(other) < decision_order(adr)): + return False + return True + @corpus_rule(contract=V1) def rule_v1_change_replaces(adr, ctx): """A change decision supersedes or amends a prior decision on the same capability. When the prior covers more than this capability ('*' or a - list), the change amends it (ADR-304 §3).""" + list), the change amends it (ADR-304 §3). A change on a baseline + capability with no prior record to name stands on the baseline (ADR-305).""" if not is_v1_record(adr, ctx) or adr.frontmatter.get('verb') != 'change': return edges = [] @@ -861,6 +916,8 @@ def rule_v1_change_replaces(adr, ctx): continue if fits: v1_issue(adr, f"a change on '{capability}' against a broader decision amends it rather than superseding it") + elif not edges and _stands_on_baseline(capability, adr, ctx): + continue elif v0_priors: numbers = ', '.join(f"ADR-{t.number}" for t in v0_priors) v1_issue(adr, f"cannot confirm the prior decision on '{capability}': {numbers} is still v0", 'warning') @@ -2220,7 +2277,7 @@ def cmd_cite(args): verb = adr.frontmatter.get('verb') if (is_v1_record(adr, ctx) and verb in ('add', 'cut') and str(adr.status or '').lower() == 'accepted'): - order = (str(adr.date or ''), _number_order(adr.number)) + order = decision_order(adr) for capability in capability_scope(adr): if capability not in latest or order >= latest[capability][0]: latest[capability] = (order, adr) @@ -2334,10 +2391,6 @@ def _cite_key(number: str) -> str: base = base.lstrip('0') or '0' return f"{base}.{part.lstrip('0') or '0'}" if part else base -def _number_order(number) -> tuple: - base, _, part = _cite_key(number or '0').partition('.') - return (int(base), int(part or 0)) - def _cite_files(root: Path) -> list: """Repository files, sorted: git's view when available (tracked and untracked but not ignored), else a walk that prunes dependency and build diff --git a/hooks/ways/documentation/adr/src/cmd_cite.py b/hooks/ways/documentation/adr/src/cmd_cite.py index fe02038f..9a934c1a 100644 --- a/hooks/ways/documentation/adr/src/cmd_cite.py +++ b/hooks/ways/documentation/adr/src/cmd_cite.py @@ -74,7 +74,7 @@ def is_proposed(adr) -> bool: verb = adr.frontmatter.get('verb') if (is_v1_record(adr, ctx) and verb in ('add', 'cut') and str(adr.status or '').lower() == 'accepted'): - order = (str(adr.date or ''), _number_order(adr.number)) + order = decision_order(adr) for capability in capability_scope(adr): if capability not in latest or order >= latest[capability][0]: latest[capability] = (order, adr) @@ -188,10 +188,6 @@ def _cite_key(number: str) -> str: base = base.lstrip('0') or '0' return f"{base}.{part.lstrip('0') or '0'}" if part else base -def _number_order(number) -> tuple: - base, _, part = _cite_key(number or '0').partition('.') - return (int(base), int(part or 0)) - def _cite_files(root: Path) -> list: """Repository files, sorted: git's view when available (tracked and untracked but not ignored), else a walk that prunes dependency and build diff --git a/hooks/ways/documentation/adr/src/rules_v1.py b/hooks/ways/documentation/adr/src/rules_v1.py index 5c7b93af..83ccb1f7 100644 --- a/hooks/ways/documentation/adr/src/rules_v1.py +++ b/hooks/ways/documentation/adr/src/rules_v1.py @@ -37,6 +37,17 @@ def _str_list(value) -> Optional[list]: def _mapping(value) -> dict: return value if isinstance(value, dict) else {} +def _iso_date(value) -> Optional[str]: + """value as a YYYY-MM-DD string, or None. YAML may load a date as one.""" + text = str(value) if value is not None else '' + return text if re.fullmatch(r'\d{4}-\d{2}-\d{2}', text) else None + +def v1_baseline(ctx) -> tuple: + """(adoption date or None, set of baseline capability names).""" + baseline = _mapping(ctx.config.get('baseline')) + return (_iso_date(baseline.get('adopted')), + set(_str_list(baseline.get('capabilities')) or [])) + def v1_kinds(ctx) -> dict: return _mapping(ctx.config.get('kinds')) @@ -145,12 +156,26 @@ def bad(message): bad("contract: adr/v1 declares no capabilities") if 'surfaces' in config and not isinstance(config['surfaces'], dict): bad("surfaces: expected a mapping of namespace to settings") + if 'baseline' in config: + baseline = config['baseline'] + names = _str_list(baseline.get('capabilities')) if isinstance(baseline, dict) else None + if names is None: + bad("baseline: expected 'adopted' (a YYYY-MM-DD date) and 'capabilities' (a list of names)") + else: + if not _iso_date(baseline.get('adopted')): + bad("baseline.adopted: expected a YYYY-MM-DD date") + if isinstance(config.get('capabilities'), dict): + for name in names: + if name not in config['capabilities']: + bad(f"baseline: '{name}' is not in the capabilities vocabulary") @config_rule(contract=V1) def rule_v1_capabilities_added(ctx): - """Every capability in the vocabulary has an accepted `add` decision. This - warns while v0 records remain, so a migrating corpus is not failed on every - capability, and fails once migration is done (ADR-304 §6).""" + """Every capability in the vocabulary has an accepted `add` decision, except + those in `baseline`: capabilities that were active when the project + adopted the contract, which no record added. This warns while v0 records + remain, so a migrating corpus is not failed on every capability, and fails + once migration is done (ADR-304 §6, amended by ADR-305).""" added = set() for adr in ctx.corpus: if (is_v1_record(adr, ctx) @@ -161,8 +186,9 @@ def rule_v1_capabilities_added(ctx): # hold the check at warning. migrating = any(not is_v1_record(adr, ctx) and not is_archived(adr.path) for adr in ctx.corpus) + _, baseline = v1_baseline(ctx) for name in v1_capabilities(ctx): - if name not in added: + if name not in added and name not in baseline: ctx.config_issues.append(Issue( f"capability '{name}' has no accepted add decision", 'warning' if migrating else 'error')) @@ -285,11 +311,40 @@ def rule_v1_edges(adr, ctx): if section and not section_exists(target, section): v1_issue(adr, f"{field_name}: ADR-{number} has no section '{section}'") +def decision_order(adr) -> tuple: + """Records in decision order: by date, then number (ADR-304 §3).""" + base, _, part = str(adr.number or '0').partition('.') + return (str(adr.date or ''), int(base or 0), int(part or 0)) + +def _stands_on_baseline(capability: str, adr, ctx) -> bool: + """A change with no prior edge stands on the baseline when the capability + is in it and either the change predates adoption, or no live decision on + the capability has been made since adoption before it. Only decisions + dated after adoption count: each was written as v1, so migrating an older + record never moves the answer (ADR-305).""" + adopted, baseline = v1_baseline(ctx) + when = _iso_date(adr.date) + if capability not in baseline or not adopted or not when: + return False + if when <= adopted: + return True + for other in ctx.corpus: + other_when = _iso_date(other.date) + if (other is not adr and is_v1_record(other, ctx) + and other.frontmatter.get('verb') not in (None, 'constrain') + and str(other.status or '').lower() not in ('rejected', 'abandoned') + and covers(other, capability) + and other_when and other_when > adopted + and decision_order(other) < decision_order(adr)): + return False + return True + @corpus_rule(contract=V1) def rule_v1_change_replaces(adr, ctx): """A change decision supersedes or amends a prior decision on the same capability. When the prior covers more than this capability ('*' or a - list), the change amends it (ADR-304 §3).""" + list), the change amends it (ADR-304 §3). A change on a baseline + capability with no prior record to name stands on the baseline (ADR-305).""" if not is_v1_record(adr, ctx) or adr.frontmatter.get('verb') != 'change': return edges = [] @@ -306,6 +361,8 @@ def rule_v1_change_replaces(adr, ctx): continue if fits: v1_issue(adr, f"a change on '{capability}' against a broader decision amends it rather than superseding it") + elif not edges and _stands_on_baseline(capability, adr, ctx): + continue elif v0_priors: numbers = ', '.join(f"ADR-{t.number}" for t in v0_priors) v1_issue(adr, f"cannot confirm the prior decision on '{capability}': {numbers} is still v0", 'warning') diff --git a/tests/adr-golden-test.sh b/tests/adr-golden-test.sh index f862f0fe..1b525f65 100755 --- a/tests/adr-golden-test.sh +++ b/tests/adr-golden-test.sh @@ -229,6 +229,39 @@ capture v1-lint-check lint --check capture v1-list list capture v1-view-spec view 102 capture v1-lint-precedent-relative lint docs/architecture/system/ADR-109-precedent-chain.md +# Baseline capabilities (ADR-305). export joins the vocabulary as a baseline +# capability adopted on 2025-05-10: it needs no add decision. +baseline_fresh() { + fresh v1 + edit docs/architecture/adr.yaml "s.replace('surfaces:', 'baseline:\\n adopted: 2025-05-10\\n capabilities: [export]\\n\\nsurfaces:', 1).replace(' search: Query over the store\\n', ' search: Query over the store\\n export: Export from the store\\n')" +} +# export_change NUMBER SLUG DATE [EXTRA-FRONTMATTER-LINE] +export_change() { + (cd "$WORK/repo" && printf -- '---\ncontract: adr/v1\nkind: decision\nverb: change\ncapability: export\n%sstatus: proposed\ndate: %s\ndeciders: [developer]\nagent: {name: Claude, model: m}\nbasis:\n - evidence: export measured\n---\n\n# ADR-%s: %s\n\n## Summary\n\n- **Probes:** *Confident:* a. *Not confident:* b.\n- **Inversion:** c.\n' "${4:+$4\n}" "$3" "$1" "$2" > "docs/architecture/system/ADR-$1-$2.md") +} +baseline_fresh +capture v1-lint-baseline lint +# Malformed baseline config: a bare list, then an unknown name and a bad date. +edit docs/architecture/adr.yaml "s.replace('baseline:\\n adopted: 2025-05-10\\n capabilities: [export]', 'baseline: [export]')" +capture v1-lint-baseline-list lint +edit docs/architecture/adr.yaml "s.replace('baseline: [export]', 'baseline:\\n adopted: May 2025\\n capabilities: [export, exprt]')" +capture v1-lint-baseline-unknown lint +# A change before adoption stands on the baseline, and so does the first +# change after it. ADR-118 is numbered lower but dated later than ADR-119, +# so it is the second post-adoption change and must name ADR-119. +baseline_fresh +export_change 117 pre-adoption-export 2025-05-09 +export_change 119 stream-exports 2025-05-20 +export_change 118 compress-exports 2025-05-22 +capture v1-lint-baseline-change lint +edit docs/architecture/system/ADR-118-compress-exports.md "s.replace('capability: export\n', 'capability: export\nsupersedes: [ADR-119]\n')" +capture v1-lint-baseline-change-names-prior lint +# A rejected post-adoption change is no prior. +edit docs/architecture/system/ADR-118-compress-exports.md "s.replace('supersedes: [ADR-119]\n', '')" +edit docs/architecture/system/ADR-119-stream-exports.md "s.replace('status: proposed', 'status: rejected')" +capture v1-lint-baseline-change-rejected-prior lint +fresh v1 +fresh v1 capture v1-cite cite # The cut on search, enacted: citations of search records now fail. (cd "$WORK/repo" && sed -i.bak 's/^verb: cut$/verb: cut\nenacted: abcdef1/' docs/architecture/system/ADR-111-cut-search.md \ diff --git a/tests/fixtures/adr/golden/v1-lint-baseline-change-names-prior.out b/tests/fixtures/adr/golden/v1-lint-baseline-change-names-prior.out new file mode 100644 index 00000000..d852dfdb --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline-change-names-prior.out @@ -0,0 +1,30 @@ + +Scanned: 18 ADRs + +Status distribution: + accepted: 9 + proposed: 9 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 4 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ⚠️ capability 'search' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +docs/architecture/system/ADR-118-compress-exports.md + ⚠️ supersedes: ADR-119 does not declare the reciprocal superseded_by: ADR-118 — one-directional links rot + +════════════════════════════════════════════════════════════ +Summary: 0 errors, 4 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-lint-baseline-change-rejected-prior.out b/tests/fixtures/adr/golden/v1-lint-baseline-change-rejected-prior.out new file mode 100644 index 00000000..a9dc0d54 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline-change-rejected-prior.out @@ -0,0 +1,28 @@ + +Scanned: 18 ADRs + +Status distribution: + accepted: 9 + proposed: 8 + rejected: 1 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 3 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ⚠️ capability 'search' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +════════════════════════════════════════════════════════════ +Summary: 0 errors, 3 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-lint-baseline-change.out b/tests/fixtures/adr/golden/v1-lint-baseline-change.out new file mode 100644 index 00000000..a6555279 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline-change.out @@ -0,0 +1,30 @@ + +Scanned: 18 ADRs + +Status distribution: + accepted: 9 + proposed: 9 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 4 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ⚠️ capability 'search' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +docs/architecture/system/ADR-118-compress-exports.md + ❌ a change decision supersedes or amends a prior decision on 'export' + +════════════════════════════════════════════════════════════ +Summary: 1 errors, 3 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-lint-baseline-list.out b/tests/fixtures/adr/golden/v1-lint-baseline-list.out new file mode 100644 index 00000000..0b521ce3 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline-list.out @@ -0,0 +1,29 @@ + +Scanned: 15 ADRs + +Status distribution: + accepted: 9 + proposed: 6 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 3 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ❌ baseline: expected 'adopted' (a YYYY-MM-DD date) and 'capabilities' (a list of names) + ⚠️ capability 'search' has no accepted add decision + ⚠️ capability 'export' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +════════════════════════════════════════════════════════════ +Summary: 1 errors, 4 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-lint-baseline-unknown.out b/tests/fixtures/adr/golden/v1-lint-baseline-unknown.out new file mode 100644 index 00000000..0b300228 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline-unknown.out @@ -0,0 +1,29 @@ + +Scanned: 15 ADRs + +Status distribution: + accepted: 9 + proposed: 6 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 3 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ❌ baseline.adopted: expected a YYYY-MM-DD date + ❌ baseline: 'exprt' is not in the capabilities vocabulary + ⚠️ capability 'search' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +════════════════════════════════════════════════════════════ +Summary: 2 errors, 3 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-lint-baseline.out b/tests/fixtures/adr/golden/v1-lint-baseline.out new file mode 100644 index 00000000..e9a6b42c --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-baseline.out @@ -0,0 +1,27 @@ + +Scanned: 15 ADRs + +Status distribution: + accepted: 9 + proposed: 6 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 3 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ⚠️ capability 'search' has no accepted add decision + +docs/architecture/system/ADR-108-ingest-over-v0.md + ⚠️ cannot confirm the prior decision on 'ingest': ADR-110 is still v0 + +docs/architecture/system/ADR-114-open-concern.md + ⚠️ open concern: Batch size may starve small tenants + +════════════════════════════════════════════════════════════ +Summary: 0 errors, 3 warnings +════════════════════════════════════════════════════════════ + +[exit 0]