From 3705c508bb49c50540c19d293c69d065335fc854 Mon Sep 17 00:00:00 2001 From: Daniel Polo <106583643+danielPoloWork@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:51:43 +0200 Subject: [PATCH] docs(bugs): add in-repo bug ledger and agent triage protocol (ADR-0039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defects had only half a home: a fixed bug is recorded in the CHANGELOG `Fixed` category and the hotfix/PATCH flow, but a known-open defect and the triage of an incoming report (reproduction, root cause, verdict — including a rejected one) had no durable, reviewable record. For a reference repo whose premise is that every important artifact is versioned and offline-readable (ADRs, patterns, the journal), the investigation of a defect is exactly such an artifact. Add an in-repo bug ledger under docs/bugs/: one Markdown record per defect, BUG-NNNN-.md under a discovery-date tree docs/bugs/// — a stable monotonic id (cross-referenced from commits/PRs/CHANGELOG) combined with the journal's date-tree foldering to keep any directory small. The ledger is the source of truth (a GitHub issue is referenced, not authoritative). Structured frontmatter carries status/severity/reporter/discovered/affected-versions/fixed-in; the lifecycle is open -> confirmed -> fixed plus the terminal wontfix / duplicate / cannot-reproduce. Codify the agent triage protocol (AGENTS.md §7.7): a record is created only for a verified, reproducible defect; a third-party report is reproduced and root-caused before acceptance, and a report that does not hold up is still recorded as cannot-reproduce/rejected so the triage trail survives. Deciding a defect is real is a judgment task, so it lives in the agent contract, not a hook; structural integrity is enforced by a new `bugs` consistency-lint check (frontmatter, vocabularies, filename↔id and path↔discovered agreement, monotonic ids, index bijection, fixed↔fixed-in). The check is a no-op on an empty ledger, so the gate is green from the moment the scaffold lands. Governance lives in docs/workflow/maintenance.md (defect lifecycle + remediation row); rationale and alternatives in ADR-0039. Relax MD025.front_matter_title so a record may carry both a frontmatter title and a visible H1. Documentation/process/tooling-only; no API change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .markdownlint.json | 1 + AGENTS.md | 15 +++- CHANGELOG.md | 15 ++++ docs/README.md | 1 + .../0039-bug-ledger-and-triage-protocol.md | 66 ++++++++++++++ docs/adr/README.md | 1 + docs/bugs/README.md | 83 +++++++++++++++++ docs/bugs/template.md | 62 +++++++++++++ docs/workflow/maintenance.md | 12 +++ tools/consistency_lint.py | 90 ++++++++++++++++++- 10 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0039-bug-ledger-and-triage-protocol.md create mode 100644 docs/bugs/README.md create mode 100644 docs/bugs/template.md diff --git a/.markdownlint.json b/.markdownlint.json index 52134d8..dcdf331 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -3,6 +3,7 @@ "MD010": { "code_blocks": false }, "MD013": false, "MD024": { "siblings_only": true }, + "MD025": { "front_matter_title": "" }, "MD033": false, "MD036": false, "MD040": false, diff --git a/AGENTS.md b/AGENTS.md index 2710ab4..34ad7a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,8 @@ The full specification is in [`docs/specs/01_spec_cpp_memory_pool.md`](docs/spec │ ├── adr/ # Architecture Decision Records │ ├── patterns/ # design-patterns catalogue │ ├── specs/ # functional/technical specifications -│ └── workflow/ # git & documentation conventions +│ ├── workflow/ # git & documentation conventions +│ └── bugs/ # in-repo bug ledger (ADR-0039) └── (build/, CMakeLists.txt, etc.) # created in Milestone 1 ``` @@ -250,6 +251,18 @@ At the **close of a work session that changed the project's state**, the agent: The journal is documentation that ships with the work, like any other doc in this section — not a separate bookkeeping PR. +### 7.7 Bug ledger & triage protocol + +Known defects and the triage of incoming reports live in [`docs/bugs/`](docs/bugs/) — one Markdown file per defect, named `BUG-NNNN-.md` under a discovery-date tree `docs/bugs///`, indexed by [`docs/bugs/README.md`](docs/bugs/README.md) ([ADR-0039](docs/adr/0039-bug-ledger-and-triage-protocol.md)). The ledger is the **source of truth** for defects (a GitHub issue, if any, is referenced, not authoritative); it holds the **open/in-flight/triaged** side, while the **closing** side — what shipped in which release — is the `CHANGELOG` `Fixed` line (§11). `NNNN` is a globally-monotonic id, never reused or renumbered (like an ADR number). + +The agent's obligations: + +1. **When asked to hunt for / find bugs**, create a ledger file *only* for a **verified, reproducible** defect — never a speculative one. Start from [`docs/bugs/template.md`](docs/bugs/template.md), fill the frontmatter (`status: confirmed`, `reporter: internal`, `severity`, `discovered`, `affected-versions`), capture the reproduction and root cause, and add the index row — in the same PR as the investigation. +2. **When a third party reports a bug**, **reproduce and root-cause it first.** Only on confirmation create a record (`reporter: third-party`, the reproduction as evidence). A report you **cannot** substantiate is **still recorded** — as `cannot-reproduce` (or `rejected` / `duplicate`) — documenting the investigation that reached that verdict, so the triage trail is preserved. Never transcribe an unconfirmed third-party claim into the ledger as if it were a real defect. +3. **When a fix lands**, flip the record to `status: fixed`, set `fixed-in`, link the fixing PR, and add the `CHANGELOG` `Fixed` line — in the same PR, per the hotfix/PATCH flow in [`docs/workflow/maintenance.md`](docs/workflow/maintenance.md). + +This is a judgment task, not an automated trigger: deciding a defect is *real* belongs to the agent. Structural integrity (frontmatter keys, the `status`/`severity`/`reporter` vocabularies, filename↔`id` and path↔`discovered` agreement, monotonic ids, the index bijection, and that a `fixed` record names its `fixed-in`) is enforced by the consistency lint's `bugs` check (§6.4) — run `python tools/consistency_lint.py` before drafting the PR. + ## 8. Design Patterns Policy This is a **reference implementation**, and demonstrating fluency with classical design patterns is part of its value. Therefore: diff --git a/CHANGELOG.md b/CHANGELOG.md index 75af380..d045d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,21 @@ dated version block (`## [X.Y.Z] — YYYY-MM-DD`) when a release PR closes a mil ### Added +- **In-repo bug ledger (`docs/bugs/`) + agent triage protocol.** Known defects and + the triage of incoming reports now have a durable, reviewable home: one Markdown + record per defect, `BUG-NNNN-.md` under a discovery-date tree + `docs/bugs///`, with a stable monotonic id, structured frontmatter + (`status`/`severity`/`reporter`/…), an index + template, and a lifecycle + (`open → confirmed → fixed`, plus `wontfix`/`duplicate`/`cannot-reproduce`). The + ledger is the source of truth (a GitHub issue is referenced, not authoritative) and + cross-references the `CHANGELOG` `Fixed` line at close. The agent rule + ([`AGENTS.md`](AGENTS.md) §7.7) requires a record only for **verified** defects, and + **verification before acceptance** of third-party reports (unsubstantiated reports + are still recorded as `cannot-reproduce`/`rejected`). A new `bugs` consistency-lint + check guards frontmatter, ids, the index bijection, and the `fixed`↔`fixed-in` link. + Governance in [`docs/workflow/maintenance.md`](docs/workflow/maintenance.md); + rationale in [ADR-0039](docs/adr/0039-bug-ledger-and-triage-protocol.md). + Documentation/process/tooling-only; no API change. - README gains a **Technology stack** section (language standards, build / test / docs / tooling, and packaging, with versions — `zero runtime dependencies`) and a top-of-page **"Read this in: 简体中文 · 日本語"** pointer to the `docs/i18n/` diff --git a/docs/README.md b/docs/README.md index 8281eeb..8b97ed3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ This directory holds the durable, versioned documentation for `pbr-cpp-memory-po | `docs/patterns/` | Living catalogue of design patterns adopted, rejected, or under consideration. | | `docs/workflow/` | Repository workflow conventions (git, documentation maintenance, release). | | `docs/development/` | Procedural how-to guides for working on the code locally (toolchain, build, debug). | +| `docs/bugs/` | In-repo bug ledger — one record per known defect, with the triage trail (ADR-0039). | ## Reading order for newcomers diff --git a/docs/adr/0039-bug-ledger-and-triage-protocol.md b/docs/adr/0039-bug-ledger-and-triage-protocol.md new file mode 100644 index 0000000..673bca2 --- /dev/null +++ b/docs/adr/0039-bug-ledger-and-triage-protocol.md @@ -0,0 +1,66 @@ +# ADR-0039: In-repo bug ledger and agent triage protocol + +- **Status:** Accepted +- **Date:** 2026-06-15 +- **Deciders:** Project architect (maintainer), agent +- **Related:** [ADR-0036](0036-session-journal-extraction.md) (dated per-file documentation precedent), [ADR-0034](0034-post-release-maintenance-protocol.md) (maintenance governance), [ADR-0035](0035-agent-runnable-consistency-lint.md) (the lint this extends), [`docs/workflow/maintenance.md`](../workflow/maintenance.md), [`AGENTS.md`](../../AGENTS.md) §7.7, [`docs/bugs/`](../bugs/) + +## Context + +Entering the maintained-product phase (post-`v1.0.0`), the project tracks two halves of a defect's life but not the whole: + +- A **fixed** defect already has a home — the `Fixed` category of the `CHANGELOG` ([ADR-0038](0038-changelog-version-split.md)) plus the hotfix/PATCH flow in [`docs/workflow/maintenance.md`](../workflow/maintenance.md). That records *what was fixed and in which release*. +- A **known-but-open** defect, and the **triage** of an incoming report (reproduction attempt, root-cause analysis, the verdict — including *rejected*), had **no durable record**. It lived only in chat or in a transient issue. + +This is a gap for a reference repository whose whole premise is that every important artifact is versioned, reviewable, and readable offline (ADRs, the patterns catalogue, the session journal). A defect's investigation is exactly such an artifact: it has evidence, a root cause, an impact assessment, and a verdict that a future reader should be able to reconstruct without access to the original conversation. + +A second, agent-specific force: the maintainer wants two repeatable behaviours from the agent — (1) when asked to *hunt* for bugs, the agent should land each **verified** finding as a durable record rather than a chat message; (2) when a **third party** reports a bug, the agent must **verify it (reproduce + root-cause) before** accepting it, never transcribe an unconfirmed claim into the record. These are judgment-bearing behaviours, so they belong in the agent contract, not in a deterministic hook. + +## Decision + +We add an **in-repo bug ledger** under [`docs/bugs/`](../bugs/) and codify the agent's bug-handling protocol. + +**Storage & identity (the "ID + date-tree mix").** One Markdown file per defect, named `BUG-NNNN-.md`, stored under a discovery-date folder tree: `docs/bugs///BUG-NNNN-.md`. + +- `NNNN` is a **zero-padded, globally monotonic** id (like ADR numbers — never reused, never renumbered), giving every defect a short stable handle (`BUG-0007`) for cross-references from commits, PRs, and the `CHANGELOG` `Fixed` line. +- The `/` folders (by **discovery** date) keep any one directory small — the same idiom already used by the session journal ([ADR-0036](0036-session-journal-extraction.md)), so there is no new convention to learn. +- [`docs/bugs/README.md`](../bugs/README.md) is the index (newest first) resolving every id to its path; [`docs/bugs/template.md`](../bugs/template.md) is the per-bug template. + +**The ledger is the source of truth.** Bug records live in the repo and are reviewed like any other artifact. A GitHub issue, if one exists, is referenced from the record but is not authoritative — consistent with ADRs and the journal living in-repo, and with the project's offline-readable, self-contained premise. + +**Structured frontmatter** carries the queryable state: `id`, `title`, `status`, `severity`, `reporter`, `discovered`, `affected-versions`, and (once closed) `fixed-in`. The lifecycle is `open → confirmed → fixed`, with the terminal states `wontfix`, `duplicate`, and `cannot-reproduce`. + +**Agent triage protocol** (codified in [`AGENTS.md`](../../AGENTS.md) §7.7): + +- *"find / hunt for bugs"* → the agent creates a ledger file only for a **verified, reproducible** defect — never a speculative one. +- *third-party report* → the agent **reproduces and root-causes first**; only on confirmation does it create a `confirmed` record (with `reporter: third-party` and the reproduction as evidence). A report it **cannot** substantiate is **still recorded** — as `cannot-reproduce` (or `rejected`/`duplicate`) with the investigation that reached that verdict — so the triage trail is preserved rather than lost. +- A fix then flows through the existing maintenance machinery: the fixing PR flips the record to `fixed`, fills `fixed-in`, and adds the `CHANGELOG` `Fixed` line — the ledger and the changelog cross-reference each other. + +**Integrity is enforced by the consistency lint** ([ADR-0035](0035-agent-runnable-consistency-lint.md)): a new `bugs` check validates each record's frontmatter (required keys, allowed `status`/`severity`/`reporter` vocabularies), the filename↔`id` agreement, the path↔`discovered`-date agreement, globally-unique non-gapped ids, the index↔files bijection, and that a `fixed` record names its `fixed-in`. With zero records the check is a no-op, so the gate is green from the moment the scaffold lands. + +## Alternatives Considered + +- **GitHub Issues as the source of truth.** The platform-native tracker, free workflow and search. Rejected as the *authority* because it breaks the repo's self-contained, offline-readable premise (the investigation would not travel with a clone or a release tarball) and splits the record from the code and the ADRs it references. Issues remain welcome as a *front door* — their number is cross-referenced from the record. +- **Flat `docs/bugs/BUG-NNNN-slug.md` (no date folders), pure ADR shape.** Simplest, and the id already gives identity. Rejected on the maintainer's explicit concern: a single directory accreting every defect over a multi-year maintained life grows unwieldy (and is awkward to browse on Windows). The date-tree mix keeps each directory small at no cost to the stable id. +- **Pure date-tree `…///-slug.md` (journal shape, no id).** Matches the journal exactly, but the filename-as-identity is long and not resolvable from a short handle — cross-referencing a defect from a commit or `CHANGELOG` line would need the full dated slug. Rejected in favour of keeping the short monotonic `BUG-NNNN` id *and* the date folders. +- **Record only confirmed defects; drop unsubstantiated reports.** Fewer files. Rejected because the *triage* — the evidence that a reported bug could **not** be reproduced — is itself valuable institutional memory for a reference project, and prevents the same rejected report from being re-litigated. +- **A deterministic hook that auto-creates the file on a trigger phrase.** Rejected because deciding a defect is *real* (reproduce + root-cause, or refute a third-party claim) is a judgment task a hook cannot perform. A hook can enforce *structure* — that role is filled by the consistency-lint `bugs` check; the *judgment* lives in the agent contract. + +## Consequences + +- **No API / ABI / build impact** — documentation, process, and tooling only. +- A new agent obligation (`AGENTS.md` §7.7): verified defects and triaged third-party reports become ledger files in the same PR as the investigation, like any other doc-with-the-work rule. +- The maintained-product governance gains a defect-lifecycle section in [`docs/workflow/maintenance.md`](../workflow/maintenance.md) tying the ledger to the existing `Fixed`/hotfix/security flows; a `fixed` record and its `CHANGELOG` line are kept in lockstep by convention and, partly, by the lint. +- The consistency lint grows a sixth-plus `bugs` check and its remediation row; CI re-runs it via the existing `consistency` job. The path↔`discovered` agreement means the lint reads frontmatter, so a malformed date or a misfiled record fails fast and locally. +- Relative links inside a bug record must account for the `docs/bugs///` depth (`../../../` reaches `docs/`, `../../../../` the repo root) — the same rule the journal already follows; the `docs.yml` link check guards it. +- This is a **maintenance/process change, not a feature** ([`AGENTS.md`](../../AGENTS.md) §7.3, [ADR-0037](0037-new-feature-roadmap-placement.md)): no roadmap milestone, recorded in `CHANGELOG`, justified by this ADR. + +## References + +- [ADR-0036](0036-session-journal-extraction.md) — the dated per-session-file precedent the date-tree reuses. +- [ADR-0034](0034-post-release-maintenance-protocol.md) — the maintenance governance the lifecycle plugs into. +- [ADR-0035](0035-agent-runnable-consistency-lint.md) — the consistency lint extended with the `bugs` check. +- [ADR-0038](0038-changelog-version-split.md) — the `CHANGELOG` whose `Fixed` category records the closing side of a defect. +- [`docs/bugs/README.md`](../bugs/README.md) — the ledger index and how-to. +- [`docs/workflow/maintenance.md`](../workflow/maintenance.md) — the defect-lifecycle governance section. +- [`AGENTS.md`](../../AGENTS.md) §7.7 — the agent triage protocol. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1d9c02a..7bc4c5e 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -71,5 +71,6 @@ Do **not** write one for purely local implementation details, formatting, or tri | 0036 | [Extract the session journal from ROADMAP.md into dated per-session files](0036-session-journal-extraction.md) | Accepted | | 0037 | [A new feature is planned on the roadmap — new milestone or appended item](0037-new-feature-roadmap-placement.md) | Accepted | | 0038 | [Split the changelog into one immutable Markdown file per release](0038-changelog-version-split.md) | Accepted | +| 0039 | [In-repo bug ledger and agent triage protocol](0039-bug-ledger-and-triage-protocol.md) | Accepted | When adding a new ADR, append a row to this table in the same PR. diff --git a/docs/bugs/README.md b/docs/bugs/README.md new file mode 100644 index 0000000..44e05e1 --- /dev/null +++ b/docs/bugs/README.md @@ -0,0 +1,83 @@ +# Bug ledger + +The durable, in-repo record of **known defects** and the **triage** of incoming +reports for `pbr-cpp-memory-pool`. One Markdown file per defect, reviewed like any +other artifact ([ADR-0039](../adr/0039-bug-ledger-and-triage-protocol.md)). + +This ledger is the **source of truth** for defects. A GitHub issue, if one exists, is +*referenced* from a record but is not authoritative — the investigation travels with +the repo, offline, like the ADRs and the [session journal](../journal/). + +The ledger holds the **open / in-flight / triaged** side of a defect's life; the +**closing** side — *what shipped in which release* — is recorded in the `CHANGELOG` +`Fixed` category ([ADR-0038](../adr/0038-changelog-version-split.md)) and governed by +[`docs/workflow/maintenance.md`](../workflow/maintenance.md). A `fixed` record and its +`CHANGELOG` line cross-reference each other. + +## Format + +File naming: `BUG-NNNN-.md`, stored under a **discovery-date** tree: + +``` +docs/bugs///BUG-NNNN-.md +``` + +- `NNNN` is a **zero-padded, globally monotonic** id — never reused, never renumbered + (like an ADR number). It is the short stable handle (`BUG-0007`) used to reference a + defect from a commit, a PR, or a `CHANGELOG` line. +- The `/` folders are the **discovery** year and month, keeping any one + directory small — the same idiom as the [session journal](../journal/). + +Each record carries structured frontmatter: + +| Key | Meaning | Vocabulary | +|-----|---------|------------| +| `id` | the `BUG-NNNN` handle — matches the filename | — | +| `title` | one-line description | — | +| `status` | lifecycle state | `open` · `confirmed` · `fixed` · `wontfix` · `duplicate` · `cannot-reproduce` | +| `severity` | impact level | `low` · `medium` · `high` · `critical` | +| `reporter` | who raised it | `internal` · `third-party` | +| `discovered` | discovery date — matches the `/` path | `YYYY-MM-DD` | +| `affected-versions` | version range affected | e.g. `">=1.0.0,<1.1.1"` | +| `fixed-in` | release that fixes it (required once `status: fixed`) | e.g. `v1.1.1` | + +Start from [`template.md`](template.md). + +## Lifecycle + +``` +open ─► confirmed ─► fixed + │ │ + └─────────┴─► wontfix | duplicate | cannot-reproduce (terminal) +``` + +- **open** — recorded, not yet root-caused/confirmed. +- **confirmed** — reproduced and root-caused; awaiting a fix. +- **fixed** — a release fixes it; `fixed-in` is set and the `CHANGELOG` `Fixed` line links back here. +- **wontfix / duplicate / cannot-reproduce** — terminal; the record documents *why* (a `duplicate` links the canonical `BUG-NNNN`). + +## How a record is created + +The agent's triage protocol is codified in [`AGENTS.md`](../../AGENTS.md) §7.7. In short: + +- **Hunting for bugs** → a record is created only for a **verified, reproducible** defect. +- **A third-party report** → the agent **reproduces and root-causes first**; only then a + `confirmed` record (`reporter: third-party`, repro as evidence). A report that does + **not** hold up is still recorded — as `cannot-reproduce` / `rejected` / `duplicate` — + with the investigation that reached that verdict. + +The fix lands through the normal hotfix/PATCH flow ([`docs/workflow/maintenance.md`](../workflow/maintenance.md)), +which flips the record to `fixed`, sets `fixed-in`, and adds the `CHANGELOG` `Fixed` line — +all in the same PR. Integrity (frontmatter, ids, index bijection, date agreement) is +checked by `python tools/consistency_lint.py` (the `bugs` check). + +## Index + +Newest first, grouped by year and month. *(No defects recorded yet — the ledger is empty +as of `v1.1.0`.)* + + diff --git a/docs/bugs/template.md b/docs/bugs/template.md new file mode 100644 index 0000000..dab2490 --- /dev/null +++ b/docs/bugs/template.md @@ -0,0 +1,62 @@ +--- +id: BUG-NNNN +title: +status: open +severity: low +reporter: internal +discovered: YYYY-MM-DD +affected-versions: =1.0.0,<1.1.1", or "unknown"> +fixed-in: +--- + +# BUG-NNNN: + +> Copy this file to `docs/bugs///BUG-NNNN-.md`, fill the +> frontmatter, and add an index row to [`README.md`](README.md). `NNNN` is the next +> free globally-monotonic number (see the index). Relative links to repo files use +> paths relative to this depth: `../../../` reaches `docs/`, `../../../../` the root. +> Delete this quote block in the real record. + +## Summary + +One or two sentences: what goes wrong, observed from the outside. + +## Environment + +- **Affected versions:** +- **Toolchain / platform:** +- **Configuration:** + +## Reproduction + +The smallest steps or code that trigger the defect. A failing test is ideal — link it. + +```cpp +// minimal repro +``` + +## Expected vs. actual + +- **Expected:** +- **Actual:** + +## Root cause + +The underlying reason, once understood. For an `open` record this may be a hypothesis; +mark it as such. For `cannot-reproduce`/`rejected`, document *why* the report did not +hold up — this is the value of recording it. + +## Impact + +Who/what is affected and how badly. Justifies the `severity`. + +## Fix / workaround + +The fix (link the PR and the `CHANGELOG` `Fixed` line once closed), or the interim +workaround for consumers while the record is `open`. + +## References + +- Fixing PR: <#NNN, once closed> +- `CHANGELOG` entry: +- Related: diff --git a/docs/workflow/maintenance.md b/docs/workflow/maintenance.md index 2428cc9..32dbc8a 100644 --- a/docs/workflow/maintenance.md +++ b/docs/workflow/maintenance.md @@ -40,6 +40,17 @@ The mechanics are identical to a milestone close ([`release.md`](release.md) §1 In all cases the agent prepares the release PR; the maintainer merges; the agent tags; the maintainer publishes ([AGENTS.md](../../AGENTS.md) §11, [ADR-0008](../adr/0008-delegate-tag-creation-and-push-to-the-agent.md)). +## Bug ledger & defect lifecycle + +Defects are tracked in the **in-repo bug ledger** under [`docs/bugs/`](../bugs/) — one Markdown file per defect, `BUG-NNNN-.md` under a discovery-date tree, the ledger being the source of truth ([ADR-0039](../adr/0039-bug-ledger-and-triage-protocol.md)). The ledger and this protocol meet at the point a defect is **fixed**: + +1. **Record** — a verified defect (found internally) or a confirmed third-party report becomes a `confirmed` ledger file. An unsubstantiated report is recorded as `cannot-reproduce` / `rejected` / `duplicate`. The agent rule is [`AGENTS.md`](../../AGENTS.md) §7.7. +2. **Assess the SemVer level** of the fix by the decision tree above (a logic fix is usually a **PATCH**; a fix that must change documented behaviour is **MINOR**/**MAJOR**). +3. **Fix** through the hotfix/backport flow below. In the **same PR**, flip the record to `status: fixed`, set `fixed-in: vX.Y.Z`, link the fixing PR, and add the `CHANGELOG` line under **`Fixed`** (or **`Security`** for a vulnerability). The ledger record and the `CHANGELOG` line cross-reference each other — keep them in lockstep. +4. **Security defects** follow the embargoed flow below, not the public ledger, until the advisory is published; the record is then added (or de-embargoed) with the advisory / CVE reference. + +The `bugs` consistency check (below) guards the record's structure and the `fixed`↔`fixed-in` link. + ## Hotfix & backport workflow Two cases, decided by **whether `master` is currently releasable**: @@ -78,6 +89,7 @@ Run `python tools/consistency_lint.py` before drafting any post-1.0 PR ([AGENTS. | `spec-map` | A Spec Coverage Map row has an empty *Roadmap items* cell or no status glyph. | Give the spec row at least one fulfilling roadmap item and a legend glyph (⏳/🚧/✅/❎). | | `i18n-freshness` | A `translated` page's English source changed after the source commit recorded in the manifest. | Re-sync the affected `docs/i18n//…` page to the new source, then update that manifest row's source commit (or, if the source change does not affect the prose, re-pin the commit after reviewing). | | `milestones` | The README marks a milestone ✅ while a ROADMAP item in it is unchecked, or a checkbox is malformed. | Check the remaining ROADMAP item(s), or correct the README table; fix any `- [ ]`/`- [x]` typo. | +| `bugs` | A `docs/bugs/` record has bad frontmatter (missing key, unknown `status`/`severity`/`reporter`), its filename/path disagrees with its `id`/`discovered`, ids are non-unique or gapped, the index ↔ files bijection is broken, or a `fixed` record has no `fixed-in`. | Fix the record per [`docs/bugs/README.md`](../bugs/README.md): match the filename to `BUG-NNNN`, file it under `//`, use the next free id, add/repair the index row, and set `fixed-in` once `status: fixed`. | ## What this protocol does not change diff --git a/tools/consistency_lint.py b/tools/consistency_lint.py index ad78d89..1cfd7da 100644 --- a/tools/consistency_lint.py +++ b/tools/consistency_lint.py @@ -21,7 +21,10 @@ roadmap-items cell); 5. the i18n manifest has no `translated` entry staler than its English source (the recorded source commit is the source file's latest commit); - 6. ROADMAP / README milestone-completion state is internally consistent. + 6. ROADMAP / README milestone-completion state is internally consistent; + 7. every docs/bugs/ record has valid frontmatter, its filename/path agrees with + its id/discovered date, ids are unique and non-gapped, the index <-> files + bijection holds, and a `fixed` record names its `fixed-in` release. Each check is independent; all run, then the report lists every failure. """ @@ -284,6 +287,90 @@ def check_milestones(): fail(name, f"malformed ROADMAP checkbox: {ln.strip()[:60]}") +# -------------------------------------------------------------------------- +# 7. Bug ledger integrity (docs/bugs/, ADR-0039) +# -------------------------------------------------------------------------- +BUG_STATUSES = { + "open", "confirmed", "fixed", "wontfix", "duplicate", "cannot-reproduce", +} +BUG_SEVERITIES = {"low", "medium", "high", "critical"} +BUG_REPORTERS = {"internal", "third-party"} +BUG_REQUIRED = ("id", "title", "status", "severity", "reporter", "discovered") + + +def _parse_frontmatter(text): + """Return the leading `--- ... ---` block as a dict, or None if absent.""" + if not text.startswith("---"): + return None + end = text.find("\n---", 3) + if end == -1: + return None + fields = {} + for line in text[3:end].splitlines(): + if not line.strip() or ":" not in line: + continue + key, _, value = line.partition(":") + fields[key.strip()] = value.strip() + return fields + + +def check_bugs(): + name = "bugs" + bugs_dir = os.path.join(ROOT, "docs", "bugs") + if not os.path.isdir(bugs_dir): + return # no ledger yet -> nothing to check + index_path = os.path.join(bugs_dir, "README.md") + index = read("docs", "bugs", "README.md") if os.path.exists(index_path) else "" + + numbers = [] + for cur, _dirs, files in os.walk(bugs_dir): + for fn in files: + m = re.fullmatch(r"BUG-(\d{4})-[a-z0-9-]+\.md", fn) + if not m: + continue # README.md, template.md, etc. + num = int(m.group(1)) + rel = os.path.relpath(os.path.join(cur, fn), bugs_dir).replace(os.sep, "/") + fm = _parse_frontmatter(read("docs", "bugs", *rel.split("/"))) + if fm is None: + fail(name, f"{rel}: missing or malformed YAML frontmatter") + continue + for key in BUG_REQUIRED: + if not fm.get(key): + fail(name, f"{rel}: missing required frontmatter key '{key}'") + if fm.get("id") != f"BUG-{m.group(1)}": + fail(name, f"{rel}: frontmatter id '{fm.get('id')}' != filename BUG-{m.group(1)}") + if fm.get("status") and fm["status"] not in BUG_STATUSES: + fail(name, f"{rel}: unknown status '{fm['status']}'") + if fm.get("severity") and fm["severity"] not in BUG_SEVERITIES: + fail(name, f"{rel}: unknown severity '{fm['severity']}'") + if fm.get("reporter") and fm["reporter"] not in BUG_REPORTERS: + fail(name, f"{rel}: unknown reporter '{fm['reporter']}'") + disc = fm.get("discovered", "") + dm = re.fullmatch(r"(\d{4})-(\d{2})-\d{2}", disc) + if not dm: + fail(name, f"{rel}: discovered '{disc}' is not YYYY-MM-DD") + elif f"{dm.group(1)}/{dm.group(2)}/" not in rel: + fail(name, f"{rel}: path does not match discovered date {disc} " + f"(expected under {dm.group(1)}/{dm.group(2)}/)") + if fm.get("status") == "fixed" and not fm.get("fixed-in"): + fail(name, f"{rel}: status 'fixed' requires a 'fixed-in' release") + if f"({rel})" not in index: + fail(name, f"{rel} is not linked from docs/bugs/README.md index") + numbers.append(num) + + if numbers: + if len(set(numbers)) != len(numbers): + fail(name, "duplicate BUG ids found") + for i, n in enumerate(sorted(numbers), start=1): + if n != i: + fail(name, f"BUG numbering gap/dup: expected {i:04d}, found {n:04d}") + break + # Every BUG-NNNN-*.md the index links to must exist on disk. + for link in re.findall(r"\]\((\d{4}/\d{2}/BUG-\d{4}-[a-z0-9-]+\.md)\)", index): + if not os.path.exists(os.path.join(bugs_dir, link)): + fail(name, f"index links missing bug file '{link}'") + + CHECKS = [ check_version_lockstep, check_adr_index, @@ -291,6 +378,7 @@ def check_milestones(): check_spec_map, check_i18n_freshness, check_milestones, + check_bugs, ]