diff --git a/docs/governance/workstreams.md b/docs/governance/workstreams.md index 2dc8d19b..19a7c5f7 100644 --- a/docs/governance/workstreams.md +++ b/docs/governance/workstreams.md @@ -224,6 +224,7 @@ state to its index: [`docs/adr/`](../adr/README.md), [`docs/pdr/`](../pdr/README | D208 | **A person named verifier was the author's word** (#363). On the third project, 2026-11-04, pull request #643, opened by an agent App, said `Verifier: @napkinstack-admin` over results, evidence and commits it had written itself; that account never touched the pull request. T2 compared the name with the change's authors only: the sheet passed it, and on a project that owes no approval the merge waited on T4 alone, which the author turns green by running its own tests at the head. The project's session caught it by reading | **Done**: `nstack pr-check` reads, in one GraphQL query, whose hand is on the pull request — its description's editors, its reviewers, its commenters — and T2 refuses a person named verifier who has none — each person named, so that a second name does not carry the first; a bot named with its `[bot]` suffix is the same account. A verifier session is out of scope: it acts under the same App as the author's. Unread — no token, no answer, a field refused, more than one page —, it is said as a warning, never failed; on a workstation and under `nstack attest`, with no pull request number or recording the head, it is not read. The skeleton's `pull-request.yml` gives it `PR_NUMBER` and the job's token, with `pull-requests: read`, and runs again on a submitted review, as it does on an edit of the description; a comment is read at the next run. `RULES.md`, `05-workflow.md` §7 and `verification.md` say it. A fresh verifier's findings fixed: a field refused by the forge crashed the check, a review did not start it again, a second name carried the first, and four mutants survived. Test seen red first: the case of #643 refused, each kind of hand accepted, the query's shape and its unread answers, and the workflow's wiring | | D209 | **An unattested head read "compliant" on the forge** (#367). On a fourth project, 2026-11-07, `checks: attested`, a pull request was opened with no record on its head: the CI job "Test sheet and cycle" said `attested — judged where nstack attest runs, not in CI` and `Pull request rules: compliant.` — `check_attested` returned before reading anything under GitHub Actions, the checkout keeping no credential to fetch the notes; the status `nstack / local checks` is posted only by a session holding a token, and that one had none. Only an observer's local `pr-check` said A1 | **Done**: the record is read from the remote's notes in CI as on a workstation — the fetch carries the job's token in git's environment (`http.extraheader`), masked for the runner, never in an argument a log shows, only towards GitHub and only under GitHub Actions — on a workstation the remote's own credential answers, an expired token in the environment would refuse it; an unattested head is refused (A1), a failed or incomplete record too (A2). A remote that cannot be read — no token, a refusal — is a warning that names why, never the silent line. The A1 action says the check reads the record again at its next run, a push or an edit of the description. `07-governance.md` §7, `RULES.md` and the workflow's `contents: read` say it. Tests seen red first: a record pushed with the notes, read by a fresh checkout under CI; no record on the remote refused; a failed record refused; no remote, and a remote that refuses, warned with git's reason; notes listed but not fetched unread; the token reaching git in CI and never on a workstation. A fresh verifier's findings fixed: the token was sent on a workstation too (an expired one refused the fetch, seen on a private repository), the refusing remote was untested, git's reason could be a fragment, the secret was not masked; three mutants killed. Probed on a private repository: an installation token in the environment reads its notes | | D210 | **`nstack claim` took an earlier cycle's start branch, then made none.** On a project, a cycle's D5 claim answered *work continues on d5-…, which exists: no other branch is made* — the start branch of an earlier cycle's D5, another title, its one empty *Start* commit never deleted on the remote: any `d-…` branch counted as the deliverable's work (D149). Once that branch was deleted, claiming again only renewed the hold, and still made no work branch | **Done**: an empty branch whose first commit off the default branch is a claim's start for another title of the same id is an earlier cycle's, never this deliverable's work — the claim makes its own; a hand-named branch, and this deliverable's own start, are still taken. A renewed hold with no work branch left gets its first one again, unless the cycle's default branch has merged the deliverable; with one, the renewal says nothing of it. `project.md` says both. Six tests: two seen red first, the merge guard's seen red without it | +| D211 | **C10 read an amendment's date as a decision's due date.** On a project, the plan warned that a record's success criterion was *due at cycle 07's closure*: its criterion cited the record it amends, with the amendment's date — and another, due before a date months later, was named due at the closure its amendment date reached. PDR-0020 §6 read any `YYYY-MM-DD` of the criterion as due, inferring nothing from prose | **Done**: PDR-0023 (proposed, option B chosen by the decider) amends PDR-0020 §1 and §6 — only a date written in bold, `**YYYY-MM-DD**`, alone between the markers, is due; a plain date, an amendment's or a reference's, is not read; still nothing inferred from prose. The forms' comment, `06-decisions.md` §5, `framing.md`'s closure, `RULES.md` and the glossary say it. This repository's own records are outside C10 and keep their plain dates. Tests seen red first: an amendment date alone, an amendment date reached beside a bold date not reached, a plain date, a bold run holding words; the existing cases' due dates written in bold | | v0.6.0 | **What is announced refuses, and the documents say what is true**: D51 to D54, and the documentary lot of the internal audit | **Done (2026-09-20)**: five workstreams merged (#83, #84, #85, #86, #87), **v0.6.0 published**, tag on `8ae544c`, verified on published artefacts — including an update of `tool-library` from v0.5.0, 9 files and no conflict, still compliant. Found doing it: D55. Next: the pilot, and M11 beside it | | Pilot | **`NapkinStack/flet`, the first real project**, run by a session that knew nothing of this repository, with the maintainer as decider and this repository's agent as observer — public on the Free plan, where the whole checklist is available, rather than private on Team where G5 is paid and G6 does not exist | **Set aside by the maintainer, 2026-10-15**, at its cycle boundary; the real product followed since is `summit`. **Ran from 2026-09-20**: discovery decided *go* after three rounds and two independent challenges, charter accepted; cycle 01 closed `stopped/reframed`, **cycle 02 closed** with two deliverables accepted and one deferred (its #27), **cycle 03 framed and approved** (#28, #30), which starts its first `critical` module; the framework taken to v0.6.1 by itself at the boundary (#29), v0.6.2 proposed (#31). It has produced D56 to D63, D68, D70 to D77, replicated D26 and D55, and measured D49 — the first figures on what an independent verifier finds (PDR-0003) | | M11 | **Ceremony follows consequence** (PDR-0011): the framework charges what a failure costs, per module and per project, and states each price before it is paid; with PDR-0004's routing, and PDR-0007, PDR-0008 and PDR-0010 — through to v0.7.0 | **Published as v0.7.0 (2026-10-12).** Planned 2026-09-23 (#98). Merged: **M11f** (#101), **M11a** (#102), **M11b** (#103), **M11d** (#104), **M11e** (#105), **M11c** (#106), **M11h** (#107), **M11g** (#108) closed by its probe — not clean, returned to M13; **M11m** (D68, D74, D75); **M11l** (PDR-0010, D59, D60); **M11n** (D79); **M11o** (PDR-0010 extended: every document has its moment); **M11p** (PDR-0002's three levels given a place: `docs/project/rules/`); **M11j** (PDR-0007: the unframed stage); **M11q** (no folder created empty; PDR-0012 proposed: a project's tree is the project's). **M11k** (PDR-0008's print mode: `nstack settings`; D77, D78). **M11s** (PDR-0012 accepted; the framework's files under `.nstack/`; an update carries what a project adapted); **M11u** (an update rewrites the old addresses a project wrote in its own files, D85). **M11t** (`nstack new-contract`: the contracts module is created by the first contract; PDR-0012 built in full); **M11v** (what the third project found: D81, D82, D83, D86, D87). **M11i**: v0.7.0 published 2026-10-12 (#125, D88). To do: **M11r** PDR-0008's apply mode and D57, after a probe on the real forge; the two earlier projects take v0.7.0 themselves, observed | diff --git a/docs/pdr/0023-a-criterion-s-due-date-is-the-one-written-in-bold.md b/docs/pdr/0023-a-criterion-s-due-date-is-the-one-written-in-bold.md new file mode 100644 index 00000000..5a5d9b32 --- /dev/null +++ b/docs/pdr/0023-a-criterion-s-due-date-is-the-one-written-in-bold.md @@ -0,0 +1,136 @@ +# PDR-0023 — A criterion's due date is the one written in bold + +- **Status**: Proposed (2026-11-10), option B chosen by the decider who assigned the defect; + accepted when a maintainer approves the pull request that carries it. It amends PDR-0020 §1 + and §6 on one point, what a date of the criterion is; the rest of PDR-0020 stands +- **Date**: 2026-11-10 +- **Decision makers**: NapkinStack maintainers (`@NapkinStack/maintainers`) +- **Modules affected**: the engine (the plan's rule C10), the decision forms, the framing + playbook's closure, the decisions handbook, `RULES.md`, the glossary + +> The PDR describes **what the product must do and why**, never its implementation. +> The how belongs to an ADR. + +--- + +## User problem + +PDR-0020 makes a closure read the decisions whose criterion is due, and reads as due *any* date +written `YYYY-MM-DD` in the success criterion section (§1, §6). A criterion carries other dates +than the one it is due by. On a project, C10 warned that a record was *due at cycle 07's closure* +because its criterion cited the record it amends, with the amendment's date; another, due +*before* a date months later, was named due at the closure its amendment date reached. + +The decider is then told to read a decision that is not due, writes a reading line that answers +nothing, and learns to dismiss the warning — the one thing the rule exists to prevent. The only +way out today is to move the amendment out of the criterion, which is where a reader looks for +it. + +## Goal + +A closure is told a decision is due only by the date the criterion says it is due by. + +## Out of scope + +- **Reading the words around a date.** *Before*, *by*, *amended*, *see*: PDR-0020 §6 infers + nothing from prose, and this keeps it so. +- **Rewriting existing records.** A record whose due date is plain stops being read as due; + the closure that reaches it says so (Decision, 4), and its author bolds it. +- **Cycles named in the criterion.** A cycle named by its file or as *cycle 03* stays due as + PDR-0020 says. +- **The reading line.** Its date stays plain: it records a reading, not a deadline. + +--- + +## Prior art + +| Product / reference | Solution chosen | What we keep from it | +|---|---|---| +| PDR-0020's own forms | The sentence *observed before **\***, its date already in bold | **The form already marks the due date**: the rule reads the mark the form shows | +| Front matter of static sites (Jekyll, Hugo: `date`, `expiryDate`) | A date that drives behaviour is in a field of its own, never found in prose | **A date that acts is marked as such**; another date is text | +| GOV.UK Tech Docs Template (`review_in`) | The review date is a named field | The same: the deadline is where the tool is told to look | +| iCalendar, `DUE` (RFC 5545) | A due date is its own property, apart from `DTSTAMP`, `LAST-MODIFIED` | **A due date and an edit date are never the same field** | + +**The convention the user already knows:** a deadline set apart from the text around it. A +field would be the strongest; the forms are prose sentences, and their due date is already bold. + +--- + +## Options considered + +| Option | What the user experiences | Cost | Chosen? | +|---|---|---|---| +| Do nothing | Amendment and reference dates warned as due; the warning dismissed | 0 | No | +| A. Only a date after *before* or *by* | Most criteria read as written — but prose inferred, which PDR-0020 §6 refuses; *on*, *until*, *at the latest* missed | Low | No | +| **B. Only a date written in bold, `**YYYY-MM-DD**`** | The form's date is due, as it is shown; any other date is text. A plain due date in an older record stops being named until bolded | Low | **Chosen** | +| C. Keep the rule; amendments moved out of the criterion | Nothing changes in the engine; every author must know to keep the section clean | 0, paid by every author | No | + +## Decision + +**Option B.** It amends PDR-0020 §1 and §6 on what a date of the criterion is: + +1. **A date of the criterion** is a date written in bold, alone between the markers: + `**2027-01-31**`. A plain date — an amendment's, a reference's, a remark's — is not read. + A bold run holding other words, `**before 2027-01-31**`, is not one either: the rule stays + exact. +2. **Everything else in PDR-0020 stands**: the criterion section only, a cycle named by its file + or as *cycle 03*, comments not read, a reading line per closure, a warning and never a + refusal. +3. **The forms, the handbook, the playbook, `RULES.md` and the glossary say it**, where they + state the rule. +4. **A plain due date is reported, not read.** A criterion with no bold date and no cycle named, + holding a plain date a closure reached with no reading after it, gets a C10 warning of its + own: no due date C10 can read, put it in bold. A date written *amended YYYY-MM-DD* is left + out — the one phrase read, only to keep an amendment quiet; it never makes a date due. + A criterion left with no due date C10 can read is reported; a plain date beside a bold due + date is no longer read, by design. + +--- + +## Expected behaviour + +**Nominal journey.** Cycle 07 closes on 2026-11-07. A record's criterion reads *(PDR-0004, +amended 2026-11-02) … before **2027-01-31***. `nstack plan` names nothing for it; the closure +whose end reaches 2027-01-31 names it. + +**Edge cases.** A criterion with only an amendment date, or only plain dates, is never due by a +date. A criterion with a bold date and a named cycle is due at both, as before. A plain due date +in a record written before this decision stops being read as due: the closure that reaches it +warns that the criterion has no due date C10 can read, and says to bold it. + +**Acceptance criteria** *(testable)*: + +- [ ] Given a closed cycle and a criterion with *amended YYYY-MM-DD* the closure reached and a + bold date it did not reach, then no warning. +- [ ] Given a criterion with only an amendment date the closure reached, then no warning. +- [ ] Given a criterion with a bold date the closure reached, then C10 warns, as PDR-0020 says. +- [ ] Given a plain date, or a bold run holding words, then no warning that it is due. +- [ ] Given a plain date the closure reached, with no bold date and no cycle named, then C10 + warns that the criterion has no due date it can read; with an amendment date only, nothing. + +--- + +## Success criterion + +We will consider this was the right call if, on the project that met it and one other, no +closure warns of a decision for a date that was not its due date, and no due decision goes +unread for a date left plain, before **2027-03-31**. + +How it is observed: the C10 warnings of their closures, against the records' criteria. + +If the criterion is not met: supersede — a due date as a field of its own. + +## Removal condition + +This decision is removed with PDR-0020's rule C10, or superseded if a due date gets a field of +its own. + +## Impacts + +- **Projects**: a criterion whose due date is plain stops being read as due; the closure that + reaches it says to bold it; amendment and reference dates stop being named. No check turns red: C10 is a warning. +- **The engine**: the plan's rule C10 reads only bold dates. +- **The skeleton**: the PDR and ADR forms' comment, `06-decisions.md` §5, `framing.md`'s + closure, `RULES.md` and the glossary say it. +- **This repository**: its own records are outside C10 (PDR-0020, *Out of scope*); their plain + dates are left as they are. diff --git a/docs/pdr/README.md b/docs/pdr/README.md index 61051291..143ab057 100644 --- a/docs/pdr/README.md +++ b/docs/pdr/README.md @@ -43,6 +43,7 @@ The PDR describes **what the product must do and why**, never its implementation | [0017](./0017-a-change-s-runs-show-what-their-logs-said.md) | A change's runs show what their logs said | **Accepted (2026-10-21)**, from #224; the how in ADR-0009; clarified 2026-10-24 (#245, #246) | 2027-03-31, the third project's end-to-end runs | | [0018](./0018-git-is-required-the-forge-is-the-project-s-choice.md) | Git is required; the forge is the project's choice | **Accepted (2026-10-21)**, from #223; the how in ADR-0010 | 2027-06-30, a first project off GitHub | | [0019](./0019-the-project-s-success-is-recorded-where-a-cycle-closes.md) | The project's success is recorded where a cycle closes | **Accepted (2026-10-25)**, the second step of D155's view of the project; built as D159 | 2027-03-31, two closures of the third project | -| [0020](./0020-a-closure-reads-the-decisions-whose-criterion-is-due.md) | A closure reads the decisions whose criterion is due | **Accepted (2026-10-26)**, from #279; built as D168 | 2027-03-31, two closures of the third project | +| [0020](./0020-a-closure-reads-the-decisions-whose-criterion-is-due.md) | A closure reads the decisions whose criterion is due | **Accepted (2026-10-26)**, from #279; built as D168; §1 and §6 amended by PDR-0023 (a due date is written in bold) | 2027-03-31, two closures of the third project | | [0021](./0021-an-agent-may-approve-another-s-change.md) | An agent may approve another's change, where the project says so | **Accepted (2026-10-28)**, as proposed; built as D175; clarified 2026-10-29 (D181): the identity's independence, not the person's | 2027-04-30, 40 pull requests of the third project approved by another contributor's agent | | [0022](./0022-agents-deliver-alone-a-person-is-asked-only-where-it-matters.md) | Agents deliver alone; a person is asked only where it matters | **Accepted (2026-11-01)**; built as D193, D194 and D196 | 2027-03-31, the third project and one new project | +| [0023](./0023-a-criterion-s-due-date-is-the-one-written-in-bold.md) | A criterion's due date is the one written in bold | Proposed (2026-11-10), option B chosen by the decider; amends PDR-0020 §1 and §6; built as D211 | 2027-03-31, the project that met it and one other | diff --git a/platform/tests/test_plan.py b/platform/tests/test_plan.py index 2849ce9a..1e8a666a 100644 --- a/platform/tests/test_plan.py +++ b/platform/tests/test_plan.py @@ -589,10 +589,10 @@ def judged(root) -> str: return said.getvalue() -BEFORE = "Observed before 2026-10-01." +BEFORE = "Observed before **2026-10-01**." DUE_CASES = { "a date the closure reached": ("adr/0003-x.md", "Observed before **2026-10-01**.", {}, True), - "the closure's own day": ("pdr/0001-x.md", "Observed before 2026-10-17.", {}, True), + "the closure's own day": ("pdr/0001-x.md", "Observed before **2026-10-17**.", {}, True), "the cycle named by its file": ("adr/0003-x.md", "Observed by the end of 03-the-window.", {}, True), "the cycle named by its number": ("adr/0003-x.md", "Observed in cycle 3.", {}, True), "read at the closure": ("adr/0003-x.md", f"{BEFORE}\n\nRead at 03-the-window's closure, 2026-10-18: met", {}, False), @@ -601,13 +601,21 @@ def judged(root) -> str: "read, the cycle in backticks and words after": ( "adr/0003-x.md", f"{BEFORE}\n\nRead at `03-the-window`'s closure, 2026-10-18: met; see 04-next", {}, False), "a reading dated before the closure": ( - "adr/0003-x.md", "Observed before 2026-10-17.\n\nRead at 03-the-window's closure, 2026-10-16: met", {}, True), + "adr/0003-x.md", "Observed before **2026-10-17**.\n\nRead at 03-the-window's closure, 2026-10-16: met", {}, True), "a reading of another closure": ("adr/0003-x.md", "By the end of 03-the-window.\n\n" "Read at 02-other's closure, 2026-10-18: met", {}, True), - "a date no closure reached yet": ("adr/0003-x.md", "Observed before 2026-10-18.", {}, False), + "a date no closure reached yet": ("adr/0003-x.md", "Observed before **2026-10-18**.", {}, False), "no date and no cycle": ("adr/0003-x.md", "Observed when users say so.", {}, False), "a date written otherwise": ("adr/0003-x.md", "Observed before 1 October 2026.", {}, False), - "a date only in a comment": ("adr/0003-x.md", "", {}, False), + "a date only in a comment": ("adr/0003-x.md", "", {}, False), + # PDR-0023: only a date written in bold is due; another is a reference, an amendment, a remark. + "a plain date": ("adr/0003-x.md", "Observed before 2026-10-01.", {}, False), + "an amendment date alone": ("pdr/0001-x.md", "Observed by users (PDR-0004, amended 2026-10-01).", {}, False), + "an amendment date beside a bold one not reached": ( + "pdr/0001-x.md", "Observed (PDR-0004, amended 2026-10-01) before **2026-10-18**.", {}, False), + "an amendment date beside a bold one reached": ( + "pdr/0001-x.md", "Observed (PDR-0004, amended 2026-10-20) before **2026-10-01**.", {}, True), + "a bold date inside words": ("adr/0003-x.md", "Observed **before 2026-10-01**.", {}, False), "a superseded record": ("adr/0003-x.md", BEFORE, {"status": "Superseded by ADR-0004"}, False), "a proposed record": ("pdr/0001-x.md", BEFORE, {"status": "Proposed"}, False), "a record dated after the closure": ("adr/0003-x.md", BEFORE, {"date": "2026-10-20"}, False), @@ -619,15 +627,42 @@ def test_a_closure_is_told_which_decisions_are_due_and_nothing_fails(tmp_path, n framed(tmp_path, cycles={"03-the-window.md": closed(), "04-next.md": cycle()}) decision(tmp_path, name, criterion, **header) said = judged(tmp_path) - assert said.count("WARNING [C10]") == (2 if warned else 0), said # once by plan, once by fitness + assert said.count("its success criterion is due at") == (2 if warned else 0), said # by plan, by fitness if warned: assert f"[C10] docs/{name}\n its success criterion is due at 03-the-window's closure (2026-10-17)" in said assert "\"Read at 03-the-window's closure, : met | missed | superseded\"" in said +PLAIN_CASES = { + "a plain date the closure reached": ("Observed before 2026-10-01.", True), + "a bold run holding words": ("Observed **before 2026-10-01**.", True), + "an amendment date alone": ("Observed by users (PDR-0004, amended 2026-10-01).", False), + "an amendment date beside a bold one": ("Observed (PDR-0004, amended 2026-10-01) before **2026-10-18**.", False), + "a bold date and a plain one": ("Observed from 2026-09-20 before **2026-10-18**.", False), + "a plain date no closure reached yet": ("Observed before 2026-10-18.", False), + "a plain date beside a named cycle": ("Observed by the end of cycle 04 (2026-10-01).", False), + "a plain date already read": ("Observed before 2026-10-01.\n\nRead at 03-the-window's closure, 2026-10-18: met", + False), +} + + +@pytest.mark.parametrize(("criterion", "noted"), PLAIN_CASES.values(), ids=PLAIN_CASES.keys()) +def test_a_plain_due_date_a_closure_reached_is_said_never_read(tmp_path, criterion, noted): + """PDR-0023: a plain date is not read as due; a record that held only one is told so, not dropped.""" + framed(tmp_path, cycles={"03-the-window.md": closed(), "04-next.md": cycle()}) + decision(tmp_path, "adr/0003-x.md", criterion) + said = judged(tmp_path) + assert "is due at" not in said, said + assert said.count("WARNING [C10]") == (2 if noted else 0), said + if noted: + assert ("[C10] docs/adr/0003-x.md\n its success criterion holds 2026-10-01, a date 03-the-window's " + "closure (2026-10-17) reached, but no due date C10 can read") in said, said + assert "put the due date in bold, **YYYY-MM-DD**" in said, said + + def test_a_later_date_is_due_again_at_the_closure_that_reaches_it(tmp_path): framed(tmp_path, cycles={"03-the-window.md": closed(), "04-next.md": closed() | {"ended_on": "2026-11-07"}}) - decision(tmp_path, "adr/0003-x.md", "First by 2026-10-01, then by 2026-11-01.\n\n" + decision(tmp_path, "adr/0003-x.md", "First by **2026-10-01**, then by **2026-11-01**.\n\n" "Read at 03-the-window's closure, 2026-10-18: met") said = judged(tmp_path) assert "due at 04-next's closure (2026-11-07)" in said and "due at 03-the-window's" not in said, said diff --git a/skeleton/.nstack/forms/adr.md b/skeleton/.nstack/forms/adr.md index 5d1e88ca..e8bdadb9 100644 --- a/skeleton/.nstack/forms/adr.md +++ b/skeleton/.nstack/forms/adr.md @@ -90,8 +90,9 @@ description: the argument that settled it.> What we do if it is not: --- diff --git a/skeleton/.nstack/forms/pdr.md b/skeleton/.nstack/forms/pdr.md index 9604c389..0c277c92 100644 --- a/skeleton/.nstack/forms/pdr.md +++ b/skeleton/.nstack/forms/pdr.md @@ -114,8 +114,9 @@ How it is observed: If the criterion is not met: --- diff --git a/skeleton/.nstack/handbook/06-decisions.md b/skeleton/.nstack/handbook/06-decisions.md index d4d32d98..0ab98742 100644 --- a/skeleton/.nstack/handbook/06-decisions.md +++ b/skeleton/.nstack/handbook/06-decisions.md @@ -182,7 +182,11 @@ Every `BUILD` decision, every PDR, and every ADR that deviates from the conventi carries a sentence of this form: > *We will consider this was the right call if* **\** *is -> observed before* **\**. +> observed before* **\**. + +The due date is written in bold, `**2027-01-31**`, and only it: a plain date in the criterion — +an amendment's, a reference's — is never read as one (C10). A criterion with no bold date and no +cycle named, whose plain date a closure reached, is reported so its due date is bolded, not lost. Without a date nobody ever comes back to check, and `docs/adr/` becomes a graveyard — which is worse than no documentation at all, because it inspires false confidence. diff --git a/skeleton/.nstack/handbook/RULES.md b/skeleton/.nstack/handbook/RULES.md index 60caa571..1dbebd6e 100644 --- a/skeleton/.nstack/handbook/RULES.md +++ b/skeleton/.nstack/handbook/RULES.md @@ -74,7 +74,7 @@ a family, a number a rule; a retired rule's number is never reused. | `C7` | A discovery decided with no decider, date or challenger — or a charter with no go before it | `.nstack/handbook/project.md` | | `C8` | A document left as its form once its moment has come: the front page's sentence at a go, the whole front page at an accepted charter, the module map once a module holds code | `06-decisions.md` §10 | | `C9` | *(warning)* An accepted charter changed by a pull request that carries no decision record | `06-decisions.md` §4 | -| `C10` | *(warning)* An accepted decision record due at a closure — its criterion names the cycle, or a date its end reached — with no reading line | `06-decisions.md` §6 | +| `C10` | *(warning)* An accepted decision record due at a closure — its criterion names the cycle, or a date in bold, `**YYYY-MM-DD**`, its end reached — with no reading line; a criterion with neither, holding a plain date its end reached, is told to bold its due date | `06-decisions.md` §6 | ## H — hygiene diff --git a/skeleton/.nstack/handbook/glossary.md b/skeleton/.nstack/handbook/glossary.md index 6e56b211..0bdaf859 100644 --- a/skeleton/.nstack/handbook/glossary.md +++ b/skeleton/.nstack/handbook/glossary.md @@ -36,7 +36,7 @@ An agent speaking to a person uses these words, and explains one the first time | **Deliverable** | Something a user can observe, with acceptance criteria, in one module | `D` in a cycle | "D18: a user reads one meeting's minutes beside its transcript." | | **proposed · ready · in-progress · delivered · accepted · deferred · dropped** | A deliverable's states: drafted, testable and waiting, being built (an older written state, still read), delivered — computed from an `nstack deliver` record, never written — accepted, written at the closure from the records (`nstack deliver --write-states`), moved to later, abandoned | `state:` of a deliverable | "D26 is ready: its criteria can be tested." | | **met · observing** | A success criterion's states, recorded at a cycle's closure: reached; being observed since a day, for a criterion that holds over time. One never recorded is not yet met | `success:` of a closed or stopped cycle | "Criterion 1 is observed since 8 November: two weeks to go." | -| **Reading · met · missed · superseded** | A decision record's success criterion read at a cycle's closure, when the criterion names the cycle or a date its end reached; written in one line under the criterion, never by rewriting the record | `Read at 's closure, : …` in a PDR or an ADR; rule C10 | "The engine decision was read at 03-the-window's closure: missed — a new record supersedes it." | +| **Reading · met · missed · superseded** | A decision record's success criterion read at a cycle's closure, when the criterion names the cycle or a date in bold (`**YYYY-MM-DD**`) its end reached; written in one line under the criterion, never by rewriting the record | `Read at 's closure, : …` in a PDR or an ADR; rule C10 | "The engine decision was read at 03-the-window's closure: missed — a new record supersedes it." | | **Hold · stale** | A deliverable taken by one contributor and their agent before its first change, so that nobody else delivers it; stale when nobody has worked on it for the project's `hold_days`, and then anyone may take it over. A hold lasts its cycle: its branch stays, the hold's record | `nstack claim`, `nstack plan`; rule K5 | "D30 is held by @alice since Monday; D31 is free." | | **proposed · accepted · closed · stopped** | A cycle's states, and a charter's first two. A next cycle the decider accepted while one runs stays proposed with the day of their word, and the running cycle's closure accepts it | `status:` of a cycle or the charter; `accepted_on:` | "Cycle 02 closed with two deliverables accepted." | | **Oracle** | The executable success criterion of a task, written and seen failing before the change | The *Oracle* section of a pull request | "The oracle: a test that a refused key says why." | diff --git a/skeleton/.nstack/playbooks/framing.md b/skeleton/.nstack/playbooks/framing.md index d5e1a990..8c632889 100644 --- a/skeleton/.nstack/playbooks/framing.md +++ b/skeleton/.nstack/playbooks/framing.md @@ -207,4 +207,4 @@ concerns one module. | An accepted charter has to change — a constraint, a user, what the project is not | A **PDR** first, which the change cites; the charter is never rewritten in place (CI warns, C9) | | The framing chooses a module, a stack, a contract | An **ADR**: the module's creation decision is one | | A spike's result binds the project | A PDR or an ADR; otherwise a line in the project's log issue | -| A cycle's **closure** | Read every decision the plan names as due (C10): its criterion names this cycle, or a date its end reached. The record gains one line under its criterion, *Read at 's closure, : met \| missed \| superseded*, and nothing else; a *missed* one is the decider's to answer, by a new record or the removal condition | +| A cycle's **closure** | Read every decision the plan names as due (C10): its criterion names this cycle, or a date in bold, `**YYYY-MM-DD**`, its end reached. The record gains one line under its criterion, *Read at 's closure, : met \| missed \| superseded*, and nothing else; a *missed* one is the decider's to answer, by a new record or the removal condition | diff --git a/src/napkinstack/fitness/plan.py b/src/napkinstack/fitness/plan.py index c4e596af..7c79564d 100644 --- a/src/napkinstack/fitness/plan.py +++ b/src/napkinstack/fitness/plan.py @@ -22,7 +22,8 @@ modules/README.md none of its own once a module holds more than its description C10 (a warning) a closure reads the decisions whose criterion is due: an accepted record of docs/pdr/ or docs/adr/ whose success criterion names a closed or stopped cycle, or a date - its end has reached, says so in a reading line (PDR-0020) + in bold its end has reached, says so in a reading line (PDR-0020, PDR-0023); one with neither, + holding a plain date its end reached, is told to bold its due date Templates, whose file name starts with "_", are not checked. `nstack plan` then says the project across its cycles (`project`), a display that never fails. @@ -673,7 +674,10 @@ def _check_discovery(path: Path, fail) -> str | None: RECORDS = (Path("docs") / "pdr", Path("docs") / "adr") CRITERION = re.compile(r"^## Success criterion[^\n]*\n(.*?)(?=^## |\Z)", re.S | re.M) READING = re.compile(r"Read at `?(.+?)`?'s closure, (\d{4}-\d{2}-\d{2}): (?:met|missed|superseded)\b", re.I) -DAY = re.compile(r"(? bool: def _due(root: Path, read: list[tuple[Path, dict | None]]) -> list[str]: """C10: each accepted decision record whose success criterion is due at a closure and says no reading of it (PDR-0020). Only the criterion section is read: a cycle named by its file or as - *cycle NN*, a date written YYYY-MM-DD. A named cycle is due at its closure; a date at the first + *cycle NN*, a date written in bold, **YYYY-MM-DD**. A named cycle is due at its closure; a date at the first closure that reaches it — read once, a later date waits for a later closure, and a reading on or after a date answers it, even at a cycle that stopped before it. A closure before the record's own - date is not one it could be read at. Said, never refused.""" + date is not one it could be read at. A criterion with no bold date and no cycle named, holding a + plain date a closure reached and no reading after it — an amendment's date aside — is said too: + its due date, if that is one, is not read until it is bolded. Said, never refused.""" closures = sorted(((ended, path.stem) for path, data in read if data and one_of(data.get("status"), OUTCOMES) and (ended := as_date(data.get("ended_on"))))) @@ -717,6 +723,17 @@ def _due(root: Path, read: list[tuple[Path, dict | None]]) -> list[str]: reached = next((closure for closure in open_ if closure[0] >= day), None) if reached and not any(on >= day for _, on in readings): due.add(reached) + if not DAY.search(criterion) and not NUMBERED.search(criterion) \ + and not any(_names(criterion, p.stem) for p, _ in read): + plain = [(day, reached) for day in filter(None, map(as_date, PLAIN.findall(AMENDED.sub("", criterion)))) + if not any(on >= day for _, on in readings) + and (reached := next((closure for closure in open_ if closure[0] >= day), None))] + if plain: + day, (ended, stem) = min(plain) + said.append(f"[C10] {path.relative_to(root)}\n its success criterion holds {day}, a date " + f"{stem}'s closure ({ended}) reached, but no due date C10 can read: a plain date " + "is never read as due (.nstack/handbook/06-decisions.md §5).\n Action: put the due date in bold, " + f"**YYYY-MM-DD** — **{day}** if it is this one — in its success criterion.") for ended, stem in sorted(due): if not any(_names(name, stem) and on >= ended for name, on in readings): said.append(f"[C10] {path.relative_to(root)}\n its success criterion is due at {stem}'s "