CB-WP-0037 T01: F18 gets a reproduction, and F24 falls out of it
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
F18 was the only open finding clay-borg owns, the only register row lacking a reproduction, and the only off-target metric. It is also understated: it reads as display data, but among the 14 unvendored files are DARVO.csv (mandatory_effect, advance), Relations.csv (formation, breaking) and Scenarios.csv — rules the engine already implements from a secondary source and has never checked against the primary one. The reproduction records column reads AT THE ACCESSOR rather than counting them from the source: a list beside the code would be a second copy of a fact the get calls already carry, and grepping would over-count because six column names are shared between vendored files. The first version was wrong in this repo's signature way — it watched Table::at only, so it called visibility, required_solution and point_value unread when the engine reads all three through problems_of's own index lookups. Correct about the accessor, wrong about the engine: the ADR-0018 family, committed inside the artifact built to measure it. Problems.csv went 7/13 to 10/13 once the manual reader was recorded too. F24 raised: solution_deck() is a Rust literal that never opens Solutions.csv. It agrees today, which is the point — the engine is right by maintenance coincidence rather than by reading. Role `default`, with a test that goes red the moment either side moves. open, lacking a reproduction: 1 -> 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bd9e168af5
commit
e5b805c185
3 changed files with 328 additions and 2 deletions
|
|
@ -81,6 +81,53 @@ const TOKENS_CSV: &str = include_str!("../../../editions/ground-darvo-r0/Tokens.
|
||||||
pub struct Table {
|
pub struct Table {
|
||||||
cols: Vec<String>,
|
cols: Vec<String>,
|
||||||
rows: Vec<Vec<String>>,
|
rows: Vec<Vec<String>>,
|
||||||
|
/// Which file this is, for the coverage probe (CB-WP-0037 T01).
|
||||||
|
/// Test-only, like the probe it feeds: the shipped path has no use
|
||||||
|
/// for it and `-D warnings` is right to say so.
|
||||||
|
#[cfg(test)]
|
||||||
|
what: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which columns of which file the engine actually asks for.
|
||||||
|
///
|
||||||
|
/// **Recorded at the accessor, not counted from the source** (F18's
|
||||||
|
/// reproduction). A list of column names written beside the code would be
|
||||||
|
/// a second copy of a fact the `get` calls already carry, and this repo
|
||||||
|
/// has 62 untagged literals as evidence of where that goes. Grepping the
|
||||||
|
/// source instead would over-count, because six column names are shared
|
||||||
|
/// between vendored files -- and over-counting coverage understates the
|
||||||
|
/// gap, which is the wrong direction for a measurement whose whole job is
|
||||||
|
/// to size it.
|
||||||
|
///
|
||||||
|
/// Test-only: it exists to measure, and a global mutable has no business
|
||||||
|
/// in the shipped path.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod probe {
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
|
fn reads() -> &'static Mutex<BTreeMap<String, BTreeSet<String>>> {
|
||||||
|
static R: OnceLock<Mutex<BTreeMap<String, BTreeSet<String>>>> = OnceLock::new();
|
||||||
|
R.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record(file: &str, column: &str) {
|
||||||
|
reads()
|
||||||
|
.lock()
|
||||||
|
.expect("probe")
|
||||||
|
.entry(file.to_string())
|
||||||
|
.or_default()
|
||||||
|
.insert(column.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn columns_read(file: &str) -> BTreeSet<String> {
|
||||||
|
reads()
|
||||||
|
.lock()
|
||||||
|
.expect("probe")
|
||||||
|
.get(file)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Table {
|
impl Table {
|
||||||
|
|
@ -103,10 +150,17 @@ impl Table {
|
||||||
}
|
}
|
||||||
rows.push(f);
|
rows.push(f);
|
||||||
}
|
}
|
||||||
Ok(Self { cols, rows })
|
Ok(Self {
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
#[cfg(test)]
|
||||||
|
what: what.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn at(&self, name: &str) -> Result<usize, String> {
|
fn at(&self, name: &str) -> Result<usize, String> {
|
||||||
|
#[cfg(test)]
|
||||||
|
crate::edition::probe::record(&self.what, name);
|
||||||
self.cols
|
self.cols
|
||||||
.iter()
|
.iter()
|
||||||
.position(|c| c == name)
|
.position(|c| c == name)
|
||||||
|
|
@ -275,7 +329,15 @@ pub fn problems_of(scenario_id: &str) -> Result<Vec<EditionProblem>, String> {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| c.trim_start_matches('\u{feff}').trim().to_string())
|
.map(|c| c.trim_start_matches('\u{feff}').trim().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
|
// CB-WP-0037 T01: recorded here too. `problems_of` predates `Table`
|
||||||
|
// and resolves its own indices, so a probe that only watched
|
||||||
|
// `Table::at` reported `visibility`, `required_solution` and
|
||||||
|
// `point_value` as unread when the engine reads all three. Correct
|
||||||
|
// about the accessor, wrong about the engine -- the family ADR-0018
|
||||||
|
// is named for, committed inside the artifact built to measure it.
|
||||||
let at = |name: &str| -> Result<usize, String> {
|
let at = |name: &str| -> Result<usize, String> {
|
||||||
|
#[cfg(test)]
|
||||||
|
probe::record("Problems.csv", name);
|
||||||
cols.iter()
|
cols.iter()
|
||||||
.position(|c| c == name)
|
.position(|c| c == name)
|
||||||
.ok_or_else(|| format!("edition data has no column {name:?}"))
|
.ok_or_else(|| format!("edition data has no column {name:?}"))
|
||||||
|
|
@ -458,6 +520,109 @@ mod card_text_tests {
|
||||||
|
|
||||||
/// ADR-0015 D5 made this visible rather than leaving it a surprise:
|
/// ADR-0015 D5 made this visible rather than leaving it a surprise:
|
||||||
/// the edition ships four scenarios and the engine deals one.
|
/// the edition ships four scenarios and the engine deals one.
|
||||||
|
/// **F18's reproduction** (CB-WP-0037 T01).
|
||||||
|
///
|
||||||
|
/// The finding says the engine cannot show what the cards do because
|
||||||
|
/// it does not read the data. That was asserted from a hand count and
|
||||||
|
/// never measured, so the row sat open with no artifact — the only
|
||||||
|
/// one in the register lacking one.
|
||||||
|
///
|
||||||
|
/// This measures it: **per vendored file, columns present against
|
||||||
|
/// columns the engine asks for**, recorded at the accessor so the
|
||||||
|
/// number cannot drift from the code.
|
||||||
|
///
|
||||||
|
/// **Per file, never a ratio.** *"20 of 47"* is the sum shape
|
||||||
|
/// GameDesign §1.2 exists to refuse; which columns, in which file, is
|
||||||
|
/// what a reader can act on.
|
||||||
|
///
|
||||||
|
/// **It can go red in both directions.** Read a new column and the
|
||||||
|
/// unread list shrinks; vendor a file and a new row appears. A
|
||||||
|
/// coverage check that only ever prints the same number is not
|
||||||
|
/// measuring coverage (ADR-0006 D3).
|
||||||
|
#[test]
|
||||||
|
fn the_engine_reads_only_part_of_what_it_vendored() {
|
||||||
|
// Touch every public reader, so the probe sees a real pass.
|
||||||
|
let _ = problem_texts("SCN_01");
|
||||||
|
let _ = problems_of("SCN_01");
|
||||||
|
let _ = actions();
|
||||||
|
let _ = solutions();
|
||||||
|
let _ = modes();
|
||||||
|
let _ = tokens();
|
||||||
|
|
||||||
|
let files = [
|
||||||
|
("Problems.csv", CSV),
|
||||||
|
("Actions.csv", ACTIONS_CSV),
|
||||||
|
("Solutions.csv", SOLUTIONS_CSV),
|
||||||
|
("Modes.csv", MODES_CSV),
|
||||||
|
("Tokens.csv", TOKENS_CSV),
|
||||||
|
];
|
||||||
|
let mut report = String::new();
|
||||||
|
let mut total_unread = 0usize;
|
||||||
|
for (name, csv) in files {
|
||||||
|
let header: Vec<String> = fields(csv.lines().next().expect("header"))
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| c.trim_start_matches('\u{feff}').trim().to_string())
|
||||||
|
.collect();
|
||||||
|
let read = probe::columns_read(name);
|
||||||
|
let unread: Vec<&String> = header.iter().filter(|c| !read.contains(*c)).collect();
|
||||||
|
total_unread += unread.len();
|
||||||
|
report.push_str(&format!(
|
||||||
|
"{name}: {} of {} columns read; unread: {unread:?}\n",
|
||||||
|
header.len() - unread.len(),
|
||||||
|
header.len(),
|
||||||
|
));
|
||||||
|
assert!(
|
||||||
|
!read.is_empty(),
|
||||||
|
"{name} is vendored and no column of it is read at all:\n{report}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The probe must actually have seen something, or every count
|
||||||
|
// above is zero for a reason that has nothing to do with the gap.
|
||||||
|
assert!(
|
||||||
|
total_unread > 0,
|
||||||
|
"every vendored column is read — F18's premise no longer holds, \
|
||||||
|
so the finding needs closing rather than this test passing:\n{report}"
|
||||||
|
);
|
||||||
|
println!("{report}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **F24** (CB-WP-0037 T01). The draw pile is a Rust literal.
|
||||||
|
///
|
||||||
|
/// `solution_deck()` builds 6 of each suit from an array and never
|
||||||
|
/// opens `Solutions.csv`, whose `suit` and `quantity` columns say the
|
||||||
|
/// same thing. **It currently agrees** — 24 rows, 6 per suit — so
|
||||||
|
/// nothing is wrong today, and that is exactly the problem: the
|
||||||
|
/// engine is right by maintenance coincidence, not by reading, and
|
||||||
|
/// the day `ground-game` changes a quantity nothing here notices.
|
||||||
|
///
|
||||||
|
/// This test is the guard until the literal is deleted (T02): it goes
|
||||||
|
/// red if either side moves.
|
||||||
|
#[test]
|
||||||
|
fn the_hardcoded_deck_still_matches_the_edition() {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
let t = Table::parse(SOLUTIONS_CSV, "Solutions.csv").expect("solutions");
|
||||||
|
let mut from_file: BTreeMap<String, u32> = BTreeMap::new();
|
||||||
|
for row in &t.rows {
|
||||||
|
let suit = t.get(row, "suit").expect("suit").to_string();
|
||||||
|
let n: u32 = t
|
||||||
|
.get(row, "quantity")
|
||||||
|
.expect("quantity")
|
||||||
|
.parse()
|
||||||
|
.expect("quantity is a number");
|
||||||
|
*from_file.entry(suit).or_default() += n;
|
||||||
|
}
|
||||||
|
let mut from_code: BTreeMap<String, u32> = BTreeMap::new();
|
||||||
|
for c in solution_deck() {
|
||||||
|
*from_code.entry(format!("{:?}", c.suit)).or_default() += 1;
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
from_file, from_code,
|
||||||
|
"the hardcoded solution deck and the edition disagree — the \
|
||||||
|
literal is the copy, so the edition is right and F24 has \
|
||||||
|
become a live defect rather than a latent one"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_edition_ships_more_scenarios_than_the_engine_deals() {
|
fn the_edition_ships_more_scenarios_than_the_engine_deals() {
|
||||||
let mut found = 0;
|
let mut found = 0;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
||||||
| F15 | underdetermined | note | — | — | 2026-08-05 | clay-borg |
|
| F15 | underdetermined | note | — | — | 2026-08-05 | clay-borg |
|
||||||
| F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg |
|
| F16 | inconsistent | withdrawn | games/ground/examples/difficulty.rs | counterexample | 2026-08-05 | clay-borg |
|
||||||
| F17 | degenerate | raised | games/ground/examples/attack-value.rs | counterexample | 2026-08-06 | ground-game |
|
| F17 | degenerate | raised | games/ground/examples/attack-value.rs | counterexample | 2026-08-06 | ground-game |
|
||||||
| F18 | inert | raised | — | — | 2026-08-06 | clay-borg |
|
| F18 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_engine_reads_only_part_of_what_it_vendored` | counterexample | 2026-08-06 | clay-borg |
|
||||||
|
| F24 | inert | raised | `games/ground/src/edition.rs::card_text_tests::the_hardcoded_deck_still_matches_the_edition` | default | 2026-08-07 | clay-borg |
|
||||||
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
|
| F19 | degenerate | applied | crates/cb-render-html/src/lib.rs::overhead_table | counterexample | 2026-08-06 | clay-borg |
|
||||||
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
|
| F20 | inert | applied | crates/cb-render-html/src/lib.rs::ending_page | counterexample | 2026-08-06 | clay-borg |
|
||||||
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
|
| F21 | degenerate | note | — | — | 2026-08-06 | clay-borg |
|
||||||
|
|
@ -201,6 +202,17 @@ kinds, states and metrics: [`GameDesign.md`](GameDesign.md). Reported by
|
||||||
sees `Clarify` where the card reads *"Ask What Happened — Invite a
|
sees `Clarify` where the card reads *"Ask What Happened — Invite a
|
||||||
concrete account before judging."* **`inert`**: the data exists and
|
concrete account before judging."* **`inert`**: the data exists and
|
||||||
cannot fire, because nothing reads it. **Ours, and CB-WP-0028 fixes it.**
|
cannot fire, because nothing reads it. **Ours, and CB-WP-0028 fixes it.**
|
||||||
|
- **F24 — the draw pile is a Rust literal.** `solution_deck()` builds six
|
||||||
|
of each suit from an array and never opens `Solutions.csv`, whose `suit`
|
||||||
|
and `quantity` columns say the same thing. **They agree today** — 24
|
||||||
|
rows, six per suit — so nothing is wrong now, and that is the point:
|
||||||
|
the engine is right by maintenance coincidence rather than by reading.
|
||||||
|
**`inert`**: the data exists and cannot fire. Role `default`, not
|
||||||
|
`counterexample` — the reproduction is green *because* the two agree,
|
||||||
|
which is the state GameDesign §1.3 says to expect from a documented
|
||||||
|
provisional choice, and it turns red the moment either side moves.
|
||||||
|
**Ours.** CB-WP-0037 T02 deletes the literal.
|
||||||
|
|
||||||
- **F15 — the rules define one game, not a series.** `OutcomeView` gives
|
- **F15 — the rules define one game, not a series.** `OutcomeView` gives
|
||||||
`personal` (per seat), `group_success` (per table) and `winners`. Summing
|
`personal` (per seat), `group_success` (per table) and `winners`. Summing
|
||||||
the first and counting the third answer different questions, and GROUND
|
the first and counting the third answer different questions, and GROUND
|
||||||
|
|
|
||||||
149
workplans/CB-WP-0037-the-fourteen-unread-files.md
Normal file
149
workplans/CB-WP-0037-the-fourteen-unread-files.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
---
|
||||||
|
id: CB-WP-0037
|
||||||
|
kind: product
|
||||||
|
title: "The fourteen unread files"
|
||||||
|
status: active
|
||||||
|
---
|
||||||
|
|
||||||
|
# Purpose
|
||||||
|
|
||||||
|
```
|
||||||
|
structural tier M (imports more of an external dataset under AM-4's
|
||||||
|
budgets, and gives an open register row its first
|
||||||
|
reproduction)
|
||||||
|
chaos d8 = 1 → no override
|
||||||
|
declared tier M
|
||||||
|
```
|
||||||
|
|
||||||
|
**Declaration 9 of chaos window 3.**
|
||||||
|
|
||||||
|
## Why this, and why now
|
||||||
|
|
||||||
|
**F18 is the only open finding clay-borg owns**, the only row in the
|
||||||
|
register lacking a reproduction, and the only metric off target
|
||||||
|
(`open, lacking a reproduction: 1, target 0`). F17 is with `ground-game`.
|
||||||
|
|
||||||
|
**And F18 is understated.** Its prose describes display data — *"the cards
|
||||||
|
cannot say what they do"* — and calls itself fixed by CB-WP-0028. Measured
|
||||||
|
now: **5 of 19 files vendored**, and among the 14 unread are
|
||||||
|
|
||||||
|
| file | what is in it |
|
||||||
|
|---|---|
|
||||||
|
| `DARVO.csv` | `mandatory_effect`, `target_memory`, `advance` — **the DARVO sequence, as rules** |
|
||||||
|
| `Relations.csv` | `formation`, `breaking`, `rules_text` — **how Bonds and Rivalries form and break** |
|
||||||
|
| `Scenarios.csv` | four scenarios with their setups — **the engine hardcodes one** |
|
||||||
|
| `Rules_Text.csv` | 21 sections of the rules themselves |
|
||||||
|
| `Player_Mats.csv` | includes `choice_rule` and `stress_track` |
|
||||||
|
|
||||||
|
**The engine implements DARVO and relation behaviour without ever having
|
||||||
|
read the edition's statement of them.** That is not inert display data; it
|
||||||
|
is rules taken from a secondary source and never checked against the
|
||||||
|
primary one.
|
||||||
|
|
||||||
|
A spot check is reassuring — REVERSE's engine path flips Focus to Blame,
|
||||||
|
gives the target +1 Stress, takes a Protection and reduces the owner's
|
||||||
|
Stress by 2, exactly as `DARVO.csv` states. **Reassuring is not checked**,
|
||||||
|
and the whole point of this repo is the difference.
|
||||||
|
|
||||||
|
## Task: give F18 a reproduction
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CB-WP-0037-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
An artifact that measures the gap instead of asserting it: **per edition
|
||||||
|
file, columns present against columns the engine reads.**
|
||||||
|
|
||||||
|
**Controls:**
|
||||||
|
- **it can go red and green** — improving coverage must move it, or it is
|
||||||
|
not measuring coverage (ADR-0006 D3);
|
||||||
|
- it reports **per file**, never a single ratio: *"5 of 19"* is the sum
|
||||||
|
shape GameDesign §1.2 exists to refuse;
|
||||||
|
- **an absent upstream is reported absent, never as a pass** — the shape
|
||||||
|
`edition-check` already uses.
|
||||||
|
|
||||||
|
**Done 2026-08-07.** `the_engine_reads_only_part_of_what_it_vendored`.
|
||||||
|
**F18 has a reproduction**, and the register's one off-target metric —
|
||||||
|
`open, lacking a reproduction: 1` — is now **0**.
|
||||||
|
|
||||||
|
**Recorded at the accessor, not counted from the source.** A list of
|
||||||
|
column names beside the code would be a second copy of a fact the `get`
|
||||||
|
calls already carry; grepping the source would over-count, because six
|
||||||
|
column names are shared between vendored files.
|
||||||
|
|
||||||
|
**And the first version was wrong in this repo's signature way.** It
|
||||||
|
watched `Table::at` only, so it reported `visibility`, `required_solution`
|
||||||
|
and `point_value` as unread when the engine reads all three —
|
||||||
|
`problems_of` predates `Table` and resolves its own indices. Correct about
|
||||||
|
the accessor, wrong about the engine: **the ADR-0018 family, committed
|
||||||
|
inside the artifact built to measure it.** Problems.csv went 7/13 → 10/13
|
||||||
|
once the manual reader was recorded too.
|
||||||
|
|
||||||
|
Measured, per file, never as a ratio:
|
||||||
|
|
||||||
|
| file | read | unread |
|
||||||
|
|---|---|---|
|
||||||
|
| Problems.csv | 10/13 | `problem_id`, `symbol_id`, `back_design_id` |
|
||||||
|
| Actions.csv | 4/9 | `symbol_id`, **`resolution_order`**, **`target`**, **`stress_restriction`**, `designer_note` |
|
||||||
|
| Solutions.csv | 4/8 | **`suit`**, `symbol_id`, **`quantity`**, `back_design_id` |
|
||||||
|
| Modes.csv | 5/7 | `mode_type`, `back_design_id` |
|
||||||
|
| Tokens.csv | 6/9 | `shape`, `size`, `symbol_id` |
|
||||||
|
|
||||||
|
**`resolution_order`, `target` and `stress_restriction` are rules**, and
|
||||||
|
the engine implements all three from `GroundRules.md` without reading the
|
||||||
|
edition's statement of them. That is T02's work.
|
||||||
|
|
||||||
|
**F24 raised**: `solution_deck()` is a Rust literal — six of each suit,
|
||||||
|
never opening `Solutions.csv`. **It agrees today** (24 rows, six per
|
||||||
|
suit), which is why it is `inert` with role `default` rather than a
|
||||||
|
counterexample: right by maintenance coincidence, not by reading.
|
||||||
|
`the_hardcoded_deck_still_matches_the_edition` is the guard until T02
|
||||||
|
deletes the literal, and it turns red the moment either side moves.
|
||||||
|
|
||||||
|
## Task: vendor what carries rules, and check it against the engine
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CB-WP-0037-T02
|
||||||
|
status: todo
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
`DARVO.csv`, `Relations.csv`, `Scenarios.csv` first — they carry
|
||||||
|
mechanism.
|
||||||
|
|
||||||
|
**Controls:**
|
||||||
|
- **every divergence between the edition's text and the engine is a
|
||||||
|
finding**, raised, not quietly fixed: the edition is `ground-game`'s to
|
||||||
|
rule on;
|
||||||
|
- **agreement is recorded too.** A survey that finds nothing and leaves no
|
||||||
|
trace cannot be told apart from one never run (CB-EV-0027 §3);
|
||||||
|
- AM-4's dependency and size budgets hold, or the pass says what it cost.
|
||||||
|
|
||||||
|
## Task: classify the rest
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CB-WP-0037-T03
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
`Symbols`, `Design_Tokens`, `Back_Designs`, `BOM`, `Print_Manifest` are
|
||||||
|
production and visual identity.
|
||||||
|
|
||||||
|
**Control:** each becomes either a vendored file or an **ornament
|
||||||
|
declaration with a falsifier** ([`OrnamentRegister.md`](../specs/OrnamentRegister.md)).
|
||||||
|
**O4 already says nothing may be declared about `Player_Mats` or
|
||||||
|
`Glossary` until they are read** — so read them or leave O4 alone.
|
||||||
|
|
||||||
|
## Task: evidence
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: CB-WP-0037-T04
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
`evidence/CB-EV-*.md`. **Did the engine's rules match the edition's?**
|
||||||
|
Say so either way, per rule, not as a count.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue