From 70881dd565d6b32dfeb5963b3182220708340dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Utterstr=C3=B6m?= Date: Sun, 15 Mar 2026 22:30:24 +0100 Subject: [PATCH 1/2] Add `amends` relationship for partial decision modification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `amends` frontmatter field allows a decision to partially modify a previous one without fully superseding it. Amended decisions stay `accepted` — the amendment changes part of the original's scope while the rest remains in effect. - create-decision.sh: --amends flag, format_amends(), frontmatter line - dld-decide/dld-plan: ask about supersede vs amend, --amends in examples - dld-audit: informational check for amended decisions, missing amends detection - dld-audit-auto: amend-aware auto-fix rules, inferred amends backfill - dld-snapshot: *Amends:*/*Amended by:* annotations and diagram edges - dld-implement: context-gathering includes amended decisions - Steering rule, format spec, concept docs, README updated - Bats tests: new amends test + fixture update - CLAUDE.md: document bats test runner path Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/dld-audit-auto/SKILL.md | 5 +++++ .claude/skills/dld-audit/SKILL.md | 13 ++++++++++++- .claude/skills/dld-decide/SKILL.md | 9 ++++++--- .../skills/dld-decide/scripts/create-decision.sh | 14 +++++++++++++- .claude/skills/dld-implement/SKILL.md | 2 +- .claude/skills/dld-plan/SKILL.md | 5 ++++- .claude/skills/dld-snapshot/SKILL.md | 10 +++++++--- CLAUDE.md | 10 ++++++++++ README.md | 3 ++- docs/concept/dld-concept.md | 3 ++- docs/concept/dld-faq.md | 2 +- docs/concept/dld-tldr.md | 2 +- docs/framework/decision-record-format.md | 5 +++++ rules/dld-workflow.md | 2 +- skills/dld-audit-auto/SKILL.md | 5 +++++ skills/dld-audit/SKILL.md | 13 ++++++++++++- skills/dld-decide/SKILL.md | 9 ++++++--- skills/dld-decide/scripts/create-decision.sh | 14 +++++++++++++- skills/dld-implement/SKILL.md | 2 +- skills/dld-plan/SKILL.md | 5 ++++- skills/dld-snapshot/SKILL.md | 10 +++++++--- tests/test_create_decision.bats | 9 +++++++++ tests/test_helper/common.bash | 1 + 23 files changed, 128 insertions(+), 25 deletions(-) diff --git a/.claude/skills/dld-audit-auto/SKILL.md b/.claude/skills/dld-audit-auto/SKILL.md index 2db195f..a919006 100644 --- a/.claude/skills/dld-audit-auto/SKILL.md +++ b/.claude/skills/dld-audit-auto/SKILL.md @@ -57,6 +57,7 @@ Check for all drift categories: 3. **Stale references in decisions** — frontmatter references to files that no longer exist 4. **Unreferenced code changes** — annotated files modified since last audit (if previous audit state exists) 5. **Decisions without annotations** — accepted decisions with code references but no corresponding annotations in code +6. **Missing amendment relationships** — decisions whose body references modifying part of a previous decision but have an empty `amends` field For check (4), if `decisions/.dld-state.yaml` exists with an `audit.commit_hash`, first verify reachability: ```bash @@ -76,6 +77,10 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing superseded decisions** — Update the annotation to reference the superseding decision (read the `supersedes` field of the newer decision to find the chain). For deprecated decisions, remove the annotation. +**Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. + +**Missing amendment relationships** — If a decision's body mentions modifying part of a previous decision (references another DL-ID and describes a partial change) but has an empty `amends` field, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. + **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. ### Best-effort fixes (apply, but flag prominently for review): diff --git a/.claude/skills/dld-audit/SKILL.md b/.claude/skills/dld-audit/SKILL.md index e3089b4..0e07d40 100644 --- a/.claude/skills/dld-audit/SKILL.md +++ b/.claude/skills/dld-audit/SKILL.md @@ -55,6 +55,10 @@ Annotations in code that reference non-existent decision IDs. These indicate dec Annotations referencing decisions with status `deprecated` or `superseded`. Code is still tied to a decision that's no longer active. +#### b2) Annotations referencing amended decisions + +Annotations referencing decisions that have been amended by a newer decision (check all decisions for `amends` fields that reference this ID). This is **informational, not an error** — the original decision is still active, but the developer should be aware of the amendment. Surface these as notes, not issues. + #### c) Stale references in decisions Decision records whose `references` list code paths that no longer exist in the repository. Use file existence checks. @@ -80,7 +84,11 @@ git diff --name-only ..HEAD Cross-reference this list with annotated files. Files that changed but whose associated decisions weren't updated may indicate undocumented drift. -#### e) Decisions without annotations +#### e) Missing amendment relationships + +Decisions whose Context or Decision sections reference modifying a specific aspect of a previous decision (e.g., mentioning another decision ID in the body and describing a partial change) but have an empty `amends` field. This suggests the `amends` relationship wasn't captured in the frontmatter. + +#### f) Decisions without annotations `accepted` decisions that have code references in their frontmatter but no corresponding `@decision` annotations found in the code. The references claim code is linked, but the annotations are missing. @@ -102,6 +110,9 @@ Present findings grouped by severity: #### Deprecated/Superseded References - `src/auth/login.ts:15` references `DL-003` (status: superseded by DL-012) +#### Amended Decisions (informational) +- `src/billing/vat.ts:42` references `DL-003` — amended by DL-012. Verify code aligns with the amendment. + #### Modified Annotated Files (since last audit) - `src/billing/vat.ts` — modified, contains `@decision(DL-012)`. Review if decision needs updating. diff --git a/.claude/skills/dld-decide/SKILL.md b/.claude/skills/dld-decide/SKILL.md index 6b23227..39aca94 100644 --- a/.claude/skills/dld-decide/SKILL.md +++ b/.claude/skills/dld-decide/SKILL.md @@ -64,9 +64,9 @@ Good reasons to ask: Scan existing decision files for potential relationships: - Decisions that reference the same code paths - Decisions with overlapping tags -- Decisions that this one might supersede +- Decisions that this one might supersede or amend -If you find related decisions, mention them and ask whether this decision supersedes any of them. +If you find related decisions, mention them and ask whether this decision **supersedes** (fully replaces) or **amends** (partially modifies) any of them. A superseded decision gets marked as `superseded` and is no longer active. An amended decision stays `accepted` — the amendment changes part of its scope while the rest remains in effect. ### 4. Determine namespace (namespaced projects only) @@ -93,10 +93,11 @@ printf "## Context\n\nWhat prompted this decision.\n\n## Decision\n\nWhat was de --namespace "billing" \ --tags "tag1, tag2" \ --supersedes "DL-003, DL-007" \ + --amends "DL-005" \ --body-stdin ``` -Flags `--namespace`, `--tags`, `--supersedes` are optional. The script creates the file with YAML frontmatter and the body content, and outputs the file path. +Flags `--namespace`, `--tags`, `--supersedes`, `--amends` are optional. The script creates the file with YAML frontmatter and the body content, and outputs the file path. > **Note:** If the body contains literal `%` characters, escape them as `%%` (printf format string requirement). @@ -105,6 +106,8 @@ If this decision supersedes others, also update their status: bash .claude/skills/dld-common/scripts/update-status.sh DL-003 superseded ``` +**Do not** update the status of amended decisions — they stay `accepted`. + ### 7. Regenerate INDEX.md ```bash diff --git a/.claude/skills/dld-decide/scripts/create-decision.sh b/.claude/skills/dld-decide/scripts/create-decision.sh index 88ecf07..a620ced 100755 --- a/.claude/skills/dld-decide/scripts/create-decision.sh +++ b/.claude/skills/dld-decide/scripts/create-decision.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Create a decision record file. -# Usage: create-decision.sh --id --title [--namespace <ns>] [--tags <t1,t2>] [--supersedes <DL-X,DL-Y>] [--body-stdin] +# Usage: create-decision.sh --id <DL-NNN> --title <title> [--namespace <ns>] [--tags <t1,t2>] [--supersedes <DL-X,DL-Y>] [--amends <DL-X,DL-Y>] [--body-stdin] # All flags except --id and --title are optional. # --body-stdin reads the markdown body (Context, Decision, Rationale, Consequences sections) from stdin. @@ -14,6 +14,7 @@ TITLE="" NAMESPACE="" TAGS="" SUPERSEDES="" +AMENDS="" BODY="" READ_STDIN=false @@ -24,6 +25,7 @@ while [[ $# -gt 0 ]]; do --namespace) NAMESPACE="$2"; shift 2 ;; --tags) TAGS="$2"; shift 2 ;; --supersedes) SUPERSEDES="$2"; shift 2 ;; + --amends) AMENDS="$2"; shift 2 ;; --body-stdin) READ_STDIN=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac @@ -76,6 +78,15 @@ format_supersedes() { fi } +# Format amends as YAML inline array +format_amends() { + if [[ -z "$1" ]]; then + echo "[]" + else + echo "[$1]" + fi +} + { echo "---" echo "id: $ID" @@ -83,6 +94,7 @@ format_supersedes() { echo "timestamp: $TIMESTAMP" echo "status: proposed" echo "supersedes: $(format_supersedes "$SUPERSEDES")" + echo "amends: $(format_amends "$AMENDS")" if [[ "$MODE" == "namespaced" && -n "$NAMESPACE" ]]; then echo "namespace: $NAMESPACE" fi diff --git a/.claude/skills/dld-implement/SKILL.md b/.claude/skills/dld-implement/SKILL.md index 00d8eeb..c5f701f 100644 --- a/.claude/skills/dld-implement/SKILL.md +++ b/.claude/skills/dld-implement/SKILL.md @@ -53,7 +53,7 @@ Read each decision record carefully. Understand: - What was decided - The rationale and constraints - The code areas referenced -- Any superseded decisions (read those too for context on what changed) +- Any superseded or amended decisions (read those too for context on what changed) ### 2. Make code changes diff --git a/.claude/skills/dld-plan/SKILL.md b/.claude/skills/dld-plan/SKILL.md index 693884b..9b45a10 100644 --- a/.claude/skills/dld-plan/SKILL.md +++ b/.claude/skills/dld-plan/SKILL.md @@ -54,7 +54,7 @@ Once determined, also read `decisions/records/<namespace>/PRACTICES.md` if it ex Before proposing the breakdown, scan existing decision files for: - Decisions that reference the same code areas - Decisions with overlapping tags or topics -- Decisions that the new feature might supersede +- Decisions that the new feature might supersede or amend Mention any related decisions to the developer so the breakdown accounts for them. @@ -102,6 +102,7 @@ printf "## Context\n\n...\n\n## Decision\n\n...\n\n## Rationale\n\n...\n\n## Con --namespace "billing" \ --tags "payment-gateway" \ --supersedes "DL-003" \ + --amends "DL-005" \ --body-stdin ``` @@ -112,6 +113,8 @@ If any decision supersedes an existing one, also update the old decision's statu bash .claude/skills/dld-common/scripts/update-status.sh DL-003 superseded ``` +**Do not** update the status of amended decisions — they stay `accepted`. + For each decision, compose a focused body. Keep it concise — the full feature context is captured across the group. Each individual decision should capture its own specific rationale. ### 7. Regenerate INDEX.md diff --git a/.claude/skills/dld-snapshot/SKILL.md b/.claude/skills/dld-snapshot/SKILL.md index 4db2c59..c890abc 100644 --- a/.claude/skills/dld-snapshot/SKILL.md +++ b/.claude/skills/dld-snapshot/SKILL.md @@ -60,6 +60,8 @@ Write `decisions/SNAPSHOT.md` — the detailed per-decision reference. ### DL-NNN: <Title> *Supersedes: DL-XXX* ← only if applicable +*Amends: DL-XXX* ← only if applicable +*Amended by: DL-YYY* ← only if another decision amends this one <The Decision section from the record — what was decided. Copy verbatim or lightly condense.> @@ -84,6 +86,7 @@ Write `decisions/SNAPSHOT.md` — the detailed per-decision reference. - Only include `accepted` decisions - Skip `superseded`, `deprecated`, and `proposed` decisions entirely - When a decision supersedes another (check the `supersedes` field in the YAML frontmatter), include a `*Supersedes: DL-XXX*` note +- When a decision amends another (check the `amends` field), include an `*Amends: DL-XXX*` note on the amending decision and an `*Amended by: DL-YYY*` note on the original decision - The **Decision** section content should be copied directly or lightly condensed — this is the authoritative statement - The **Rationale** should be condensed to 1-3 sentences — enough to understand *why*, not every detail - **Code** references should list paths and symbols from the frontmatter `references` field @@ -135,12 +138,13 @@ sequenceDiagram ## Decision Relationships -<Only include this section if there are supersession chains or closely related decision groups. -If no decisions supersede others and there are no meaningful relationship clusters, omit this section entirely.> +<Only include this section if there are supersession chains, amendment relationships, or closely related decision groups. +If no decisions supersede or amend others and there are no meaningful relationship clusters, omit this section entirely.> ```mermaid graph LR DL-003 -->|superseded by| DL-012 + DL-012 -.->|amends| DL-005 DL-012 -.->|related| DL-014 ``` @@ -160,7 +164,7 @@ belong to a single area. E.g., "PostgreSQL for primary data store (DL-001)", - **Include Mermaid diagrams where they add clarity.** Good candidates: - Architecture/component diagrams when decisions span multiple components - Sequence diagrams when decisions describe flows or protocols - - Relationship diagrams when there are supersession chains + - Relationship diagrams when there are supersession chains or amendment relationships - Don't force diagrams if the decisions are simple or unrelated - **Keep it proportional.** A project with 5 decisions needs a short overview. A project with 50 needs more structure. Scale the document to the content. - **Group by namespace (namespaced projects) or by domain theme (flat projects).** For flat projects, identify natural domain groupings from tags and decision content. diff --git a/CLAUDE.md b/CLAUDE.md index 7f7fc5f..0d0a1ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,6 +52,16 @@ Scripts live in `skills/<skill>/scripts/` (tessl) and `.claude/skills/<skill>/sc Scripts use `set -euo pipefail` and source `common.sh` via `BASH_SOURCE` path resolution. +## Testing + +Tests use [bats-core](https://github.com/bats-core/bats-core) installed as a git submodule at `tests/bats/`. Run tests with: + +```bash +tests/bats/bin/bats tests/ +``` + +If tests fail with "Could not find bats-support", init submodules first: `git submodule update --init --recursive` + ## Conventions - Commit messages: concise, no buzzwords diff --git a/README.md b/README.md index 60762ba..155002c 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ title: "Use exponential backoff for payment gateway retries" timestamp: 2026-02-15T09:20:00Z status: accepted supersedes: [DL-002] +amends: [] tags: [payments, resilience] references: - path: src/payments/gateway.ts @@ -270,7 +271,7 @@ This project uses Decision-Linked Development. Decision records (DL-*.md) live i - When you encounter `@decision(DL-XXX)` annotations in code, use `/dld-lookup DL-XXX` to read the referenced decision BEFORE modifying the annotated code. - ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step. -- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (via `/dld-decide`) that explicitly supersedes the old one. +- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (via `/dld-decide`) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead. - Use `/dld-decide` to record new decisions - Use `/dld-implement` to implement proposed decisions - Use `/dld-lookup` to query decisions by ID, tag, or code path diff --git a/docs/concept/dld-concept.md b/docs/concept/dld-concept.md index 960fc37..6db8135 100644 --- a/docs/concept/dld-concept.md +++ b/docs/concept/dld-concept.md @@ -60,7 +60,8 @@ Building on the established terminology and concepts from Architecture Decision - **Decision** — what was decided - **Rationale** — why this choice over alternatives - **Consequences** — what becomes easier or harder -- **Supersedes** — references to previous decisions that this one replaces. A single new decision may supersede one or several older decisions. +- **Supersedes** — references to previous decisions that this one fully replaces. A single new decision may supersede one or several older decisions. +- **Amends** — references to previous decisions that this one partially modifies. Unlike supersession, amended decisions stay active — the amendment changes part of the original's scope while the rest remains in effect. - **Code references** — explicit links to the areas of the codebase this decision affects By aligning with ADR conventions and terminology, DLD lowers the adoption barrier for teams already familiar with architectural decision records, while extending the concept to cover not just architectural but also functional, product, and implementation-level decisions. diff --git a/docs/concept/dld-faq.md b/docs/concept/dld-faq.md index f96b168..f689cb4 100644 --- a/docs/concept/dld-faq.md +++ b/docs/concept/dld-faq.md @@ -64,7 +64,7 @@ Over time, you can expand coverage. You can also use AI to help bootstrap: have **What if a decision turns out to be wrong?** -You don't edit or delete the original decision. You create a new decision that supersedes it, explaining why the previous approach is no longer valid. This preserves the full history — which is valuable both for understanding how the system evolved and for preventing the same mistake from being repeated. +You don't edit or delete the original decision. If the original decision is entirely wrong, you create a new decision that supersedes it, explaining why the previous approach is no longer valid. If only part of it needs to change, you create a new decision that *amends* it — the original stays active, and the amendment clarifies what changed. This preserves the full history — which is valuable both for understanding how the system evolved and for preventing the same mistake from being repeated. --- diff --git a/docs/concept/dld-tldr.md b/docs/concept/dld-tldr.md index 4dd255f..00a2eac 100644 --- a/docs/concept/dld-tldr.md +++ b/docs/concept/dld-tldr.md @@ -18,7 +18,7 @@ In practice, `@decision(DL-XXX)` annotations on methods and classes act as mecha Three additional design choices support this: -1. **The decision log is append-only.** Decisions can supersede previous ones — including multiple at once — but the content (reasoning and intent) is never rewritten. Metadata like `status` and `references` can be updated mechanically (e.g., after code refactors). This creates a complete timeline of how the system evolved, borrowing directly from event sourcing. +1. **The decision log is append-only.** Decisions can supersede (fully replace) or amend (partially modify) previous ones — but the content (reasoning and intent) is never rewritten. Metadata like `status` and `references` can be updated mechanically (e.g., after code refactors). This creates a complete timeline of how the system evolved, borrowing directly from event sourcing. 2. **The spec is a generated projection**, not a manually maintained document. Just like event sourcing builds read models from event streams, an LLM periodically generates a consolidated "current state" snapshot from the decision log. Humans never maintain the spec — only the individual decisions. diff --git a/docs/framework/decision-record-format.md b/docs/framework/decision-record-format.md index 1e7e399..fb749cd 100644 --- a/docs/framework/decision-record-format.md +++ b/docs/framework/decision-record-format.md @@ -61,6 +61,7 @@ title: "Short descriptive title" timestamp: 2026-03-07T14:30:00Z status: accepted # proposed | accepted | deprecated | superseded supersedes: [] # e.g. [DL-003, DL-007] — decisions this one replaces +amends: [] # e.g. [DL-003] — decisions this one partially modifies namespace: billing # optional — only in namespaced projects tags: [] # optional — used for grouping, filtering, and search references: # code areas this decision affects @@ -79,6 +80,7 @@ references: # code areas this decision affects | `timestamp` | yes | ISO 8601 timestamp of when the decision was made | | `status` | yes | One of: `proposed`, `accepted`, `deprecated`, `superseded` | | `supersedes` | no | List of decision IDs this one replaces | +| `amends` | no | List of decision IDs this one partially modifies (amended decisions stay `accepted`) | | `namespace` | no | Namespace this decision belongs to (namespaced projects only) | | `tags` | no | Tags for grouping related decisions, categorization, and filtering. When a larger feature is planned as multiple decisions, a shared tag (e.g., `payment-gateway`) groups them together. | | `references` | no | Code locations this decision affects | @@ -95,6 +97,8 @@ proposed → accepted → deprecated - **deprecated** — No longer relevant (e.g., the feature was removed). No replacement decision. - **superseded** — Replaced by one or more newer decisions. The superseding decision(s) will list this ID in their `supersedes` field. +Note: A decision can also be *amended* by a newer decision without being superseded. When a decision is amended, it stays `accepted` — the amendment modifies part of the original decision's scope while the rest remains in effect. The amending decision lists the original in its `amends` field. This is distinct from supersession, which fully replaces the original. + The immutability principle applies to the decision's *content* — the reasoning and intent captured in the markdown body. Metadata fields like `status` and `references` are maintainable: status changes when a decision is superseded or deprecated, and references are updated when code is refactored (files renamed, functions moved). This keeps the reverse index (decision → code) accurate without requiring a new decision for every rename. The `@decision` annotations in code are the authoritative link and naturally survive refactors since they move with the code. #### Code References @@ -151,6 +155,7 @@ title: "Customer-specific VAT rounding for EU trade" timestamp: 2026-02-15T09:20:00Z status: accepted supersedes: [DL-003] +amends: [] namespace: billing tags: [vat, eu-compliance, rounding] references: diff --git a/rules/dld-workflow.md b/rules/dld-workflow.md index 8bedc3b..e9e4674 100644 --- a/rules/dld-workflow.md +++ b/rules/dld-workflow.md @@ -6,7 +6,7 @@ This project uses Decision-Linked Development. Decision records (DL-*.md) live i - When you encounter `@decision(DL-XXX)` annotations in code, use `/dld-lookup DL-XXX` to read the referenced decision BEFORE modifying the annotated code. - ALWAYS look up and verify related decisions before modifying annotated code. Do not skip this step. -- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (via `/dld-decide`) that explicitly supersedes the old one. +- NEVER modify code in a way that contradicts an existing decision without first confirming with the user. If the change requires breaking a previous decision, a new decision must be recorded (via `/dld-decide`) that explicitly supersedes the old one. If it only partially modifies a previous decision, record it as an amendment instead. - Use `/dld-decide` to record new decisions - Use `/dld-plan` to break down a feature into multiple grouped decisions - Use `/dld-implement` to implement proposed decisions diff --git a/skills/dld-audit-auto/SKILL.md b/skills/dld-audit-auto/SKILL.md index 8fa0cb2..02b75a0 100644 --- a/skills/dld-audit-auto/SKILL.md +++ b/skills/dld-audit-auto/SKILL.md @@ -57,6 +57,7 @@ Check for all drift categories: 3. **Stale references in decisions** — frontmatter references to files that no longer exist 4. **Unreferenced code changes** — annotated files modified since last audit (if previous audit state exists) 5. **Decisions without annotations** — accepted decisions with code references but no corresponding annotations in code +6. **Missing amendment relationships** — decisions whose body references modifying part of a previous decision but have an empty `amends` field For check (4), if `decisions/.dld-state.yaml` exists with an `audit.commit_hash`, first verify reachability: ```bash @@ -76,6 +77,10 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing superseded decisions** — Update the annotation to reference the superseding decision (read the `supersedes` field of the newer decision to find the chain). For deprecated decisions, remove the annotation. +**Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. + +**Missing amendment relationships** — If a decision's body mentions modifying part of a previous decision (references another DL-ID and describes a partial change) but has an empty `amends` field, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. + **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. ### Best-effort fixes (apply, but flag prominently for review): diff --git a/skills/dld-audit/SKILL.md b/skills/dld-audit/SKILL.md index dbed01b..73c4242 100644 --- a/skills/dld-audit/SKILL.md +++ b/skills/dld-audit/SKILL.md @@ -55,6 +55,10 @@ Annotations in code that reference non-existent decision IDs. These indicate dec Annotations referencing decisions with status `deprecated` or `superseded`. Code is still tied to a decision that's no longer active. +#### b2) Annotations referencing amended decisions + +Annotations referencing decisions that have been amended by a newer decision (check all decisions for `amends` fields that reference this ID). This is **informational, not an error** — the original decision is still active, but the developer should be aware of the amendment. Surface these as notes, not issues. + #### c) Stale references in decisions Decision records whose `references` list code paths that no longer exist in the repository. Use file existence checks. @@ -80,7 +84,11 @@ git diff --name-only <commit_hash>..HEAD Cross-reference this list with annotated files. Files that changed but whose associated decisions weren't updated may indicate undocumented drift. -#### e) Decisions without annotations +#### e) Missing amendment relationships + +Decisions whose Context or Decision sections reference modifying a specific aspect of a previous decision (e.g., mentioning another decision ID in the body and describing a partial change) but have an empty `amends` field. This suggests the `amends` relationship wasn't captured in the frontmatter. + +#### f) Decisions without annotations `accepted` decisions that have code references in their frontmatter but no corresponding `@decision` annotations found in the code. The references claim code is linked, but the annotations are missing. @@ -102,6 +110,9 @@ Present findings grouped by severity: #### Deprecated/Superseded References - `src/auth/login.ts:15` references `DL-003` (status: superseded by DL-012) +#### Amended Decisions (informational) +- `src/billing/vat.ts:42` references `DL-003` — amended by DL-012. Verify code aligns with the amendment. + #### Modified Annotated Files (since last audit) - `src/billing/vat.ts` — modified, contains `@decision(DL-012)`. Review if decision needs updating. diff --git a/skills/dld-decide/SKILL.md b/skills/dld-decide/SKILL.md index 70c252e..b660f6d 100644 --- a/skills/dld-decide/SKILL.md +++ b/skills/dld-decide/SKILL.md @@ -64,9 +64,9 @@ Good reasons to ask: Scan existing decision files for potential relationships: - Decisions that reference the same code paths - Decisions with overlapping tags -- Decisions that this one might supersede +- Decisions that this one might supersede or amend -If you find related decisions, mention them and ask whether this decision supersedes any of them. +If you find related decisions, mention them and ask whether this decision **supersedes** (fully replaces) or **amends** (partially modifies) any of them. A superseded decision gets marked as `superseded` and is no longer active. An amended decision stays `accepted` — the amendment changes part of its scope while the rest remains in effect. ### 4. Determine namespace (namespaced projects only) @@ -93,10 +93,11 @@ printf "## Context\n\nWhat prompted this decision.\n\n## Decision\n\nWhat was de --namespace "billing" \ --tags "tag1, tag2" \ --supersedes "DL-003, DL-007" \ + --amends "DL-005" \ --body-stdin ``` -Flags `--namespace`, `--tags`, `--supersedes` are optional. The script creates the file with YAML frontmatter and the body content, and outputs the file path. +Flags `--namespace`, `--tags`, `--supersedes`, `--amends` are optional. The script creates the file with YAML frontmatter and the body content, and outputs the file path. > **Note:** If the body contains literal `%` characters, escape them as `%%` (printf format string requirement). @@ -105,6 +106,8 @@ If this decision supersedes others, also update their status: bash ../dld-common/scripts/update-status.sh DL-003 superseded ``` +**Do not** update the status of amended decisions — they stay `accepted`. + ### 7. Regenerate INDEX.md ```bash diff --git a/skills/dld-decide/scripts/create-decision.sh b/skills/dld-decide/scripts/create-decision.sh index 88ecf07..a620ced 100755 --- a/skills/dld-decide/scripts/create-decision.sh +++ b/skills/dld-decide/scripts/create-decision.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Create a decision record file. -# Usage: create-decision.sh --id <DL-NNN> --title <title> [--namespace <ns>] [--tags <t1,t2>] [--supersedes <DL-X,DL-Y>] [--body-stdin] +# Usage: create-decision.sh --id <DL-NNN> --title <title> [--namespace <ns>] [--tags <t1,t2>] [--supersedes <DL-X,DL-Y>] [--amends <DL-X,DL-Y>] [--body-stdin] # All flags except --id and --title are optional. # --body-stdin reads the markdown body (Context, Decision, Rationale, Consequences sections) from stdin. @@ -14,6 +14,7 @@ TITLE="" NAMESPACE="" TAGS="" SUPERSEDES="" +AMENDS="" BODY="" READ_STDIN=false @@ -24,6 +25,7 @@ while [[ $# -gt 0 ]]; do --namespace) NAMESPACE="$2"; shift 2 ;; --tags) TAGS="$2"; shift 2 ;; --supersedes) SUPERSEDES="$2"; shift 2 ;; + --amends) AMENDS="$2"; shift 2 ;; --body-stdin) READ_STDIN=true; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac @@ -76,6 +78,15 @@ format_supersedes() { fi } +# Format amends as YAML inline array +format_amends() { + if [[ -z "$1" ]]; then + echo "[]" + else + echo "[$1]" + fi +} + { echo "---" echo "id: $ID" @@ -83,6 +94,7 @@ format_supersedes() { echo "timestamp: $TIMESTAMP" echo "status: proposed" echo "supersedes: $(format_supersedes "$SUPERSEDES")" + echo "amends: $(format_amends "$AMENDS")" if [[ "$MODE" == "namespaced" && -n "$NAMESPACE" ]]; then echo "namespace: $NAMESPACE" fi diff --git a/skills/dld-implement/SKILL.md b/skills/dld-implement/SKILL.md index 5a1574d..aa5e839 100644 --- a/skills/dld-implement/SKILL.md +++ b/skills/dld-implement/SKILL.md @@ -53,7 +53,7 @@ Read each decision record carefully. Understand: - What was decided - The rationale and constraints - The code areas referenced -- Any superseded decisions (read those too for context on what changed) +- Any superseded or amended decisions (read those too for context on what changed) ### 2. Make code changes diff --git a/skills/dld-plan/SKILL.md b/skills/dld-plan/SKILL.md index 21b0703..48f6ebb 100644 --- a/skills/dld-plan/SKILL.md +++ b/skills/dld-plan/SKILL.md @@ -54,7 +54,7 @@ Once determined, also read `decisions/records/<namespace>/PRACTICES.md` if it ex Before proposing the breakdown, scan existing decision files for: - Decisions that reference the same code areas - Decisions with overlapping tags or topics -- Decisions that the new feature might supersede +- Decisions that the new feature might supersede or amend Mention any related decisions to the developer so the breakdown accounts for them. @@ -102,6 +102,7 @@ printf "## Context\n\n...\n\n## Decision\n\n...\n\n## Rationale\n\n...\n\n## Con --namespace "billing" \ --tags "payment-gateway" \ --supersedes "DL-003" \ + --amends "DL-005" \ --body-stdin ``` @@ -112,6 +113,8 @@ If any decision supersedes an existing one, also update the old decision's statu bash ../dld-common/scripts/update-status.sh DL-003 superseded ``` +**Do not** update the status of amended decisions — they stay `accepted`. + For each decision, compose a focused body. Keep it concise — the full feature context is captured across the group. Each individual decision should capture its own specific rationale. ### 7. Regenerate INDEX.md diff --git a/skills/dld-snapshot/SKILL.md b/skills/dld-snapshot/SKILL.md index 398d81e..7a6f37e 100644 --- a/skills/dld-snapshot/SKILL.md +++ b/skills/dld-snapshot/SKILL.md @@ -60,6 +60,8 @@ Write `decisions/SNAPSHOT.md` — the detailed per-decision reference. ### DL-NNN: <Title> *Supersedes: DL-XXX* ← only if applicable +*Amends: DL-XXX* ← only if applicable +*Amended by: DL-YYY* ← only if another decision amends this one <The Decision section from the record — what was decided. Copy verbatim or lightly condense.> @@ -84,6 +86,7 @@ Write `decisions/SNAPSHOT.md` — the detailed per-decision reference. - Only include `accepted` decisions - Skip `superseded`, `deprecated`, and `proposed` decisions entirely - When a decision supersedes another (check the `supersedes` field in the YAML frontmatter), include a `*Supersedes: DL-XXX*` note +- When a decision amends another (check the `amends` field), include an `*Amends: DL-XXX*` note on the amending decision and an `*Amended by: DL-YYY*` note on the original decision - The **Decision** section content should be copied directly or lightly condensed — this is the authoritative statement - The **Rationale** should be condensed to 1-3 sentences — enough to understand *why*, not every detail - **Code** references should list paths and symbols from the frontmatter `references` field @@ -135,12 +138,13 @@ sequenceDiagram ## Decision Relationships -<Only include this section if there are supersession chains or closely related decision groups. -If no decisions supersede others and there are no meaningful relationship clusters, omit this section entirely.> +<Only include this section if there are supersession chains, amendment relationships, or closely related decision groups. +If no decisions supersede or amend others and there are no meaningful relationship clusters, omit this section entirely.> ```mermaid graph LR DL-003 -->|superseded by| DL-012 + DL-012 -.->|amends| DL-005 DL-012 -.->|related| DL-014 ``` @@ -160,7 +164,7 @@ belong to a single area. E.g., "PostgreSQL for primary data store (DL-001)", - **Include Mermaid diagrams where they add clarity.** Good candidates: - Architecture/component diagrams when decisions span multiple components - Sequence diagrams when decisions describe flows or protocols - - Relationship diagrams when there are supersession chains + - Relationship diagrams when there are supersession chains or amendment relationships - Don't force diagrams if the decisions are simple or unrelated - **Keep it proportional.** A project with 5 decisions needs a short overview. A project with 50 needs more structure. Scale the document to the content. - **Group by namespace (namespaced projects) or by domain theme (flat projects).** For flat projects, identify natural domain groupings from tags and decision content. diff --git a/tests/test_create_decision.bats b/tests/test_create_decision.bats index 9966233..b965df4 100644 --- a/tests/test_create_decision.bats +++ b/tests/test_create_decision.bats @@ -26,6 +26,7 @@ teardown() { assert_output --partial 'title: "Test decision"' assert_output --partial 'status: proposed' assert_output --partial 'supersedes: []' + assert_output --partial 'amends: []' assert_output --partial 'tags: []' assert_output --partial 'references: []' } @@ -46,6 +47,14 @@ teardown() { assert_output --partial 'supersedes: [DL-001]' } +@test "create-decision includes amends" { + run bash "$SCRIPT" --id DL-002 --title "Amender" --amends "DL-001" + assert_success + + run cat decisions/records/DL-002.md + assert_output --partial 'amends: [DL-001]' +} + @test "create-decision reads body from stdin" { echo "## Context Some context here. diff --git a/tests/test_helper/common.bash b/tests/test_helper/common.bash index 0ca06b2..849eefb 100644 --- a/tests/test_helper/common.bash +++ b/tests/test_helper/common.bash @@ -73,6 +73,7 @@ title: "$title" timestamp: 2026-01-15T10:00:00Z status: $status supersedes: [] +amends: [] ${ns_line:+$ns_line }tags: [test, example] references: [] From 8fe9dc7fef268134309eeab62fdc1d43bccd8950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Utterstr=C3=B6m?= <jimutt@outlook.com> Date: Sun, 15 Mar 2026 22:39:42 +0100 Subject: [PATCH 2/2] Add find-missing-amends script and fix macOS test failures - find-missing-amends.sh: deterministic script that finds decision IDs referenced in body text but not declared in supersedes/amends fields. Outputs candidates for the agent to evaluate. - Audit skills updated to run the script and evaluate candidates - 8 new bats tests for find-missing-amends - Fix macOS /var vs /private/var symlink issue in test helper (pwd -P) - Fix macOS sed compatibility in test_structure.bats (awk instead) - All 111 tests now pass on macOS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --- .claude/skills/dld-audit-auto/SKILL.md | 3 +- .claude/skills/dld-audit/SKILL.md | 11 +- .../dld-audit/scripts/find-missing-amends.sh | 55 +++++++ skills/dld-audit-auto/SKILL.md | 3 +- skills/dld-audit/SKILL.md | 11 +- .../dld-audit/scripts/find-missing-amends.sh | 55 +++++++ tests/test_find_missing_amends.bats | 147 ++++++++++++++++++ tests/test_helper/common.bash | 4 +- tests/test_structure.bats | 2 +- 9 files changed, 284 insertions(+), 7 deletions(-) create mode 100755 .claude/skills/dld-audit/scripts/find-missing-amends.sh create mode 100755 skills/dld-audit/scripts/find-missing-amends.sh create mode 100644 tests/test_find_missing_amends.bats diff --git a/.claude/skills/dld-audit-auto/SKILL.md b/.claude/skills/dld-audit-auto/SKILL.md index a919006..f880d10 100644 --- a/.claude/skills/dld-audit-auto/SKILL.md +++ b/.claude/skills/dld-audit-auto/SKILL.md @@ -22,6 +22,7 @@ Shared scripts: Skill-specific scripts: ``` .claude/skills/dld-audit/scripts/find-annotations.sh +.claude/skills/dld-audit/scripts/find-missing-amends.sh .claude/skills/dld-audit/scripts/update-audit-state.sh ``` @@ -79,7 +80,7 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. -**Missing amendment relationships** — If a decision's body mentions modifying part of a previous decision (references another DL-ID and describes a partial change) but has an empty `amends` field, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. +**Missing amendment relationships** — Run `bash .claude/skills/dld-audit/scripts/find-missing-amends.sh` to get candidates. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. diff --git a/.claude/skills/dld-audit/SKILL.md b/.claude/skills/dld-audit/SKILL.md index 0e07d40..8ee2a54 100644 --- a/.claude/skills/dld-audit/SKILL.md +++ b/.claude/skills/dld-audit/SKILL.md @@ -18,6 +18,7 @@ Shared scripts: Skill-specific scripts: ``` .claude/skills/dld-audit/scripts/find-annotations.sh +.claude/skills/dld-audit/scripts/find-missing-amends.sh .claude/skills/dld-audit/scripts/update-audit-state.sh ``` @@ -86,7 +87,15 @@ Cross-reference this list with annotated files. Files that changed but whose ass #### e) Missing amendment relationships -Decisions whose Context or Decision sections reference modifying a specific aspect of a previous decision (e.g., mentioning another decision ID in the body and describing a partial change) but have an empty `amends` field. This suggests the `amends` relationship wasn't captured in the frontmatter. +**This check is mandatory — do not skip it.** Run the find-missing-amends script to get initial candidates: + +```bash +bash .claude/skills/dld-audit/scripts/find-missing-amends.sh +``` + +This outputs lines in the format `<source-id>:<referenced-id>` — decisions whose body references another decision ID that isn't listed in their `supersedes` or `amends` fields. Not every candidate is a missing amendment — some are just informational references (e.g., "this is similar to DL-005"). + +For each candidate, read the source decision's body and evaluate whether the reference describes a partial modification of the referenced decision. Look for language like: "supersedes the X portions of", "changes the Y behavior from DL-Z", "replaces the approach in DL-Z for...", "modifies how DL-Z handles...". If so, flag it as a missing amendment. #### f) Decisions without annotations diff --git a/.claude/skills/dld-audit/scripts/find-missing-amends.sh b/.claude/skills/dld-audit/scripts/find-missing-amends.sh new file mode 100755 index 0000000..aee8673 --- /dev/null +++ b/.claude/skills/dld-audit/scripts/find-missing-amends.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Find decisions that reference other decision IDs in their body +# but don't list them in supersedes or amends. +# Output: one line per candidate in the format: <source-id>:<referenced-id> +# Outputs nothing (exit 0) if no candidates found. +# These are candidates — the agent must evaluate whether the reference +# is actually a partial modification or just informational. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../../dld-common/scripts/common.sh" + +RECORDS_DIR="$(get_records_dir)" + +# Find all decision files +shopt -s nullglob +files=("$RECORDS_DIR"/DL-*.md "$RECORDS_DIR"/*/DL-*.md) +shopt -u nullglob + +if [[ ${#files[@]} -eq 0 ]]; then + exit 0 +fi + +for file in "${files[@]}"; do + id=$(basename "$file" .md) + + # Extract supersedes and amends from frontmatter (between --- markers) + frontmatter=$(sed -n '1,/^---$/{ /^---$/d; p; }; /^---$/,/^---$/{ /^---$/d; p; }' "$file" | head -50) + declared=$(echo "$frontmatter" | grep -E '^(supersedes|amends):' | grep -oE 'DL-[0-9]+' || true) + + # Extract body (everything after the second ---) + body=$(awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$file") + + # Find DL-IDs referenced in the body + body_refs=$(echo "$body" | grep -oE 'DL-[0-9]+' | sort -u || true) + + if [[ -z "$body_refs" ]]; then + continue + fi + + for ref in $body_refs; do + # Skip self-references + if [[ "$ref" == "$id" ]]; then + continue + fi + + # Skip if already declared in supersedes or amends + if echo "$declared" | grep -qF "$ref" 2>/dev/null; then + continue + fi + + echo "$id:$ref" + done +done diff --git a/skills/dld-audit-auto/SKILL.md b/skills/dld-audit-auto/SKILL.md index 02b75a0..9313ac9 100644 --- a/skills/dld-audit-auto/SKILL.md +++ b/skills/dld-audit-auto/SKILL.md @@ -22,6 +22,7 @@ Shared scripts: Skill-specific scripts: ``` ../dld-audit/scripts/find-annotations.sh +../dld-audit/scripts/find-missing-amends.sh ../dld-audit/scripts/update-audit-state.sh ``` @@ -79,7 +80,7 @@ Apply fixes for each issue category. Use judgment on what can be safely fixed au **Annotations referencing amended decisions** — Do **not** rewrite or remove these annotations. The original decision is still active. Instead, note the amendment relationship in the PR description so reviewers can verify the code aligns with the amendment. -**Missing amendment relationships** — If a decision's body mentions modifying part of a previous decision (references another DL-ID and describes a partial change) but has an empty `amends` field, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. +**Missing amendment relationships** — Run `bash ../dld-audit/scripts/find-missing-amends.sh` to get candidates. For each candidate, read the source decision's body and determine if it describes a partial modification. If so, add the referenced ID to the `amends` field. Flag prominently in the PR for review, since this is an inferred relationship. **Decisions without annotations** — If an accepted decision has code references but no annotations, and the referenced files exist, add the missing `@decision(DL-NNN)` annotations to the referenced code locations. diff --git a/skills/dld-audit/SKILL.md b/skills/dld-audit/SKILL.md index 73c4242..16504dc 100644 --- a/skills/dld-audit/SKILL.md +++ b/skills/dld-audit/SKILL.md @@ -18,6 +18,7 @@ Shared scripts: Skill-specific scripts: ``` scripts/find-annotations.sh +scripts/find-missing-amends.sh scripts/update-audit-state.sh ``` @@ -86,7 +87,15 @@ Cross-reference this list with annotated files. Files that changed but whose ass #### e) Missing amendment relationships -Decisions whose Context or Decision sections reference modifying a specific aspect of a previous decision (e.g., mentioning another decision ID in the body and describing a partial change) but have an empty `amends` field. This suggests the `amends` relationship wasn't captured in the frontmatter. +**This check is mandatory — do not skip it.** Run the find-missing-amends script to get initial candidates: + +```bash +bash scripts/find-missing-amends.sh +``` + +This outputs lines in the format `<source-id>:<referenced-id>` — decisions whose body references another decision ID that isn't listed in their `supersedes` or `amends` fields. Not every candidate is a missing amendment — some are just informational references (e.g., "this is similar to DL-005"). + +For each candidate, read the source decision's body and evaluate whether the reference describes a partial modification of the referenced decision. Look for language like: "supersedes the X portions of", "changes the Y behavior from DL-Z", "replaces the approach in DL-Z for...", "modifies how DL-Z handles...". If so, flag it as a missing amendment. #### f) Decisions without annotations diff --git a/skills/dld-audit/scripts/find-missing-amends.sh b/skills/dld-audit/scripts/find-missing-amends.sh new file mode 100755 index 0000000..aee8673 --- /dev/null +++ b/skills/dld-audit/scripts/find-missing-amends.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Find decisions that reference other decision IDs in their body +# but don't list them in supersedes or amends. +# Output: one line per candidate in the format: <source-id>:<referenced-id> +# Outputs nothing (exit 0) if no candidates found. +# These are candidates — the agent must evaluate whether the reference +# is actually a partial modification or just informational. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/../../dld-common/scripts/common.sh" + +RECORDS_DIR="$(get_records_dir)" + +# Find all decision files +shopt -s nullglob +files=("$RECORDS_DIR"/DL-*.md "$RECORDS_DIR"/*/DL-*.md) +shopt -u nullglob + +if [[ ${#files[@]} -eq 0 ]]; then + exit 0 +fi + +for file in "${files[@]}"; do + id=$(basename "$file" .md) + + # Extract supersedes and amends from frontmatter (between --- markers) + frontmatter=$(sed -n '1,/^---$/{ /^---$/d; p; }; /^---$/,/^---$/{ /^---$/d; p; }' "$file" | head -50) + declared=$(echo "$frontmatter" | grep -E '^(supersedes|amends):' | grep -oE 'DL-[0-9]+' || true) + + # Extract body (everything after the second ---) + body=$(awk 'BEGIN{n=0} /^---$/{n++; next} n>=2{print}' "$file") + + # Find DL-IDs referenced in the body + body_refs=$(echo "$body" | grep -oE 'DL-[0-9]+' | sort -u || true) + + if [[ -z "$body_refs" ]]; then + continue + fi + + for ref in $body_refs; do + # Skip self-references + if [[ "$ref" == "$id" ]]; then + continue + fi + + # Skip if already declared in supersedes or amends + if echo "$declared" | grep -qF "$ref" 2>/dev/null; then + continue + fi + + echo "$id:$ref" + done +done diff --git a/tests/test_find_missing_amends.bats b/tests/test_find_missing_amends.bats new file mode 100644 index 0000000..2240398 --- /dev/null +++ b/tests/test_find_missing_amends.bats @@ -0,0 +1,147 @@ +#!/usr/bin/env bats +# Tests for dld-audit/scripts/find-missing-amends.sh + +load 'test_helper/common' + +SCRIPT="" + +setup() { + setup_flat_project + SCRIPT="$SKILLS_DIR/dld-audit/scripts/find-missing-amends.sh" +} + +teardown() { + teardown_project +} + +# Helper: create a decision with a custom body +# Usage: create_decision_with_body <id> <supersedes> <amends> <body> +create_decision_with_body() { + local id="$1" + local supersedes="$2" + local amends="$3" + local body="$4" + + cat > "decisions/records/$id.md" <<EOF +--- +id: $id +title: "Test decision $id" +timestamp: 2026-01-15T10:00:00Z +status: accepted +supersedes: [$supersedes] +amends: [$amends] +tags: [test] +references: [] +--- + +$body +EOF +} + +@test "find-missing-amends returns nothing with no decisions" { + run bash "$SCRIPT" + assert_success + assert_output "" +} + +@test "find-missing-amends returns nothing when no body references" { + create_decision_with_body "DL-001" "" "" "## Decision +Just a simple decision with no references." + run bash "$SCRIPT" + assert_success + assert_output "" +} + +@test "find-missing-amends finds undeclared body reference" { + create_decision_with_body "DL-001" "" "" "## Decision +Original decision." + create_decision_with_body "DL-002" "" "" "## Decision +This changes the caching strategy from DL-001." + run bash "$SCRIPT" + assert_success + assert_output "DL-002:DL-001" +} + +@test "find-missing-amends ignores self-references" { + create_decision_with_body "DL-001" "" "" "## Decision +This is DL-001, referencing itself." + run bash "$SCRIPT" + assert_success + assert_output "" +} + +@test "find-missing-amends ignores references already in supersedes" { + create_decision_with_body "DL-001" "" "" "## Decision +Original." + create_decision_with_body "DL-002" "DL-001" "" "## Decision +Replaces DL-001 entirely." + run bash "$SCRIPT" + assert_success + assert_output "" +} + +@test "find-missing-amends ignores references already in amends" { + create_decision_with_body "DL-001" "" "" "## Decision +Original." + create_decision_with_body "DL-002" "" "DL-001" "## Decision +Partially modifies DL-001." + run bash "$SCRIPT" + assert_success + assert_output "" +} + +@test "find-missing-amends reports multiple missing references" { + create_decision_with_body "DL-001" "" "" "## Decision +First." + create_decision_with_body "DL-002" "" "" "## Decision +Second." + create_decision_with_body "DL-003" "" "" "## Decision +This modifies parts of DL-001 and DL-002." + run bash "$SCRIPT" + assert_success + assert_line "DL-003:DL-001" + assert_line "DL-003:DL-002" +} + +@test "find-missing-amends works with namespaced decisions" { + setup_namespaced_project + mkdir -p decisions/records/billing + + cat > "decisions/records/billing/DL-001.md" <<'EOF' +--- +id: DL-001 +title: "Original" +timestamp: 2026-01-15T10:00:00Z +status: accepted +supersedes: [] +amends: [] +namespace: billing +tags: [test] +references: [] +--- + +## Decision +Original billing decision. +EOF + + cat > "decisions/records/billing/DL-002.md" <<'EOF' +--- +id: DL-002 +title: "Amendment" +timestamp: 2026-01-16T10:00:00Z +status: accepted +supersedes: [] +amends: [] +namespace: billing +tags: [test] +references: [] +--- + +## Decision +Changes the rounding portion of DL-001. +EOF + + run bash "$SCRIPT" + assert_success + assert_output "DL-002:DL-001" +} diff --git a/tests/test_helper/common.bash b/tests/test_helper/common.bash index 849eefb..9c2e58e 100644 --- a/tests/test_helper/common.bash +++ b/tests/test_helper/common.bash @@ -9,7 +9,7 @@ SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/skills" # Create a temporary git repo with a flat dld config setup_flat_project() { - TEST_PROJECT="$(mktemp -d)" + TEST_PROJECT="$(cd "$(mktemp -d)" && pwd -P)" cd "$TEST_PROJECT" git init --quiet git config user.email "test@test.com" @@ -27,7 +27,7 @@ YAML # Create a temporary git repo with a namespaced dld config setup_namespaced_project() { - TEST_PROJECT="$(mktemp -d)" + TEST_PROJECT="$(cd "$(mktemp -d)" && pwd -P)" cd "$TEST_PROJECT" git init --quiet git config user.email "test@test.com" diff --git a/tests/test_structure.bats b/tests/test_structure.bats index 501dabc..7b36f31 100644 --- a/tests/test_structure.bats +++ b/tests/test_structure.bats @@ -121,7 +121,7 @@ setup() { assert_equal "$first_line" "---" "Missing opening --- in $skill_name" # Check name field matches directory - name_field="$(sed -n '/^---$/,/^---$/{ /^name:/p }' "$skill_md" | head -1 | sed 's/^name:[[:space:]]*//')" + name_field="$(awk '/^---$/{n++; next} n==1 && /^name:/{print; exit}' "$skill_md" | sed 's/^name:[[:space:]]*//')" assert_equal "$name_field" "$skill_name" "name mismatch in $skill_name" # Check description field exists