diff --git a/.claude/skills/dld-audit-auto/SKILL.md b/.claude/skills/dld-audit-auto/SKILL.md index 2db195f..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 ``` @@ -57,6 +58,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 +78,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** — 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. ### 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..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 ``` @@ -55,6 +56,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 +85,19 @@ 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 + +**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 `:` — 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 `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 +119,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-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: : +# 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/.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..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 ``` @@ -57,6 +58,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 +78,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** — 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. ### 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..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 ``` @@ -55,6 +56,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 +85,19 @@ 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 + +**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 `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 +119,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-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/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_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 0ca06b2..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" @@ -73,6 +73,7 @@ title: "$title" timestamp: 2026-01-15T10:00:00Z status: $status supersedes: [] +amends: [] ${ns_line:+$ns_line }tags: [test, example] references: [] 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