P13-S1: every requirement is named, and now it is also numbered

168 of core_spec's 207 requirement blocks carried no `\label`, so no conformance
claim could cite them. All 207 are labelled now; the suite is 277/277.

Labelling alone would not have delivered a citable requirement. **No document in
the suite numbered its requirements.** `\newtcolorbox{requirement}` had no
counter, so a `\label` inside one bound to the enclosing sectioning unit and
`\ref` rendered a *section* number: core_spec said "see Requirement 2.5.4" where
2.5.4 is a subsubsection, and 61 of its 207 requirements shared a rendered number
with another -- one number, 5.6.3, was shared by six. Adding 168 labels to that
scheme would have produced 168 citable-but-ambiguous references. All six documents
now carry a real counter, numbered within chapter, and the box title shows it, so
a reader can see which requirement they are looking at. 277 labels, zero
collisions.

The counter is stepped with a `code=` key rather than tcolorbox's own
`auto counter`, and that is not a style choice. `auto counter` steps its counter
for `\label` purposes inside an internal `\sbox`, and `\refstepcounter`'s effect
on `\@currentlabel` is a local assignment discarded when that box closes --
before a `\label` written in the box body ever runs, which is how every
requirement in this suite is labelled. Measured on a three-box test document:
titles rendered `1.1 1.2 1.3` while the three refs resolved to `1.1 1.1 1.2`. The
idiomatic form would have shipped 207 silently wrong cross-references under
correct-looking numbers.

`requirement_labels.rs` locks all of it: every requirement block in every
`spec/*.tex` carries a label; labels match `req:<area>:<slug>`; the area matches
the chapter; labels are unique suite-wide; no `req:*` string cited anywhere in the
repository is undefined; and the counter is stepped where the label can see it --
a regression lock, because reverting to `auto counter` leaves every other check
green while the references break.

The citation check needed an escape. It cannot tell "cite this requirement" from
"name a label that does not exist", and documenting a dangling label is a
legitimate thing to do -- it had already rewritten a scoping plan's prose into a
euphemism to make itself pass. `DISCUSSED_NOT_CITED` carries the one such string
with its reason.

That string was the pass's other finding. `req:layoutir:vertical-bands` was cited
twice in the Pass-12 log and never existed. It should not be repointed at the two
*ownership* requirements: those govern which band a primitive belongs to, while
both entries describe the inter-staff solve realizing a band's declared *height*,
which no requirement governs at all. That is why the log invented a name. Both
citations now say so, and the gap is filed as P13-S4 -- shipped behaviour with no
governing requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-21 18:27:52 -04:00
parent a9a57120e9
commit 043c18cabf
17 changed files with 1076 additions and 9 deletions

View File

@ -0,0 +1,554 @@
//! Durable completeness and referential-integrity checks for requirement labels.
//!
//! Requirement labels are a public citation surface. These tests derive their
//! inputs from every specification document instead of maintaining a second list
//! of labels, and scan repository text so a dangling citation cannot hide in a
//! decision record or other consumer.
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
const CORE_REQUIREMENT_COUNT: usize = 207;
const SUITE_REQUIREMENT_COUNT: usize = 277;
const SUITE_LABEL_COUNT: usize = 277;
/// The normative chapter-to-area assignment. Keeping this as data makes adding a
/// requirement under the wrong chapter fail without encoding chapter names in
/// test control flow.
const CHAPTER_AREAS: &[(&str, &str, &str)] = &[
("core_spec.tex", "Pitch", "pitch"),
("core_spec.tex", "Time and Duration", "time"),
("core_spec.tex", "Tuning Systems and Pitch Spaces", "tuning"),
("core_spec.tex", "The Score Graph", "graph"),
(
"core_spec.tex",
"Semantic Operations and Concurrent Reduction",
"semops",
),
(
"core_spec.tex",
"Layout Intermediate Representation",
"layoutir",
),
("core_spec.tex", "File Format", "format"),
("core_spec.tex", "Constraint-Solver Interface", "solver"),
("core_spec.tex", "Performance Requirements", "perf"),
("core_spec.tex", "Extension Points", "ext"),
(
"core_spec.tex",
"Intentionally Deferred Types and Specifications",
"deferred",
),
("core_spec.tex", "Determinism Contract", "determinism"),
("binary_format.tex", "Encoding Conventions", "binfmt"),
("binary_format.tex", "Identifiers and Derivations", "binfmt"),
("binary_format.tex", "Graph Value Layouts", "binfmt"),
("binary_format.tex", "Operation Wire Forms", "binfmt"),
("binary_format.tex", "Bundle Physical Layout", "binfmt"),
(
"binary_format.tex",
"Extension Declaration Blobs and Edit Barriers",
"binfmt",
),
("binary_format.tex", "Golden Anchor Registry", "binfmt"),
("operation_catalog.tex", "The Catalog Framework", "catalog"),
(
"operation_catalog.tex",
"K0 --- Representative Primitives",
"opcat",
),
(
"operation_catalog.tex",
"v0 \\texorpdfstring{$\\rightarrow$}{->} v1 Payload Migration",
"migration",
),
("quality_metric_catalog.tex", "The Metric Model", "qmc"),
(
"quality_metric_catalog.tex",
"The Nine Normative Metrics",
"qmc",
),
(
"quality_metric_catalog.tex",
"Default Tie-Breaking Weights",
"qmc",
),
(
"quality_metric_catalog.tex",
"Per-Tier Metric Thresholds",
"qmc",
),
(
"quality_metric_catalog.tex",
"The Registered Profile Catalog",
"qmc",
),
("reference_suite.tex", "The Suite Entry Model", "refsuite"),
("reference_suite.tex", "The v0.1 Entry Set", "refsuite"),
("text_projection.tex", "The Canonical Text Form", "textproj"),
("text_projection.tex", "What Is Projected", "textproj"),
("text_projection.tex", "Requirements", "textproj"),
];
#[derive(Debug)]
struct RequirementBlock {
chapter: String,
line: usize,
labels: Vec<String>,
}
#[derive(Debug)]
struct SpecDocument {
name: String,
text: String,
requirements: Vec<RequirementBlock>,
}
fn repository_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn command_arguments(text: &str, command: &str) -> Vec<(usize, String)> {
let needle = format!(r"\{command}{{");
let mut arguments = Vec::new();
let mut cursor = 0;
while let Some(relative) = text[cursor..].find(&needle) {
let command_start = cursor + relative;
let argument_start = command_start + needle.len();
let bytes = text.as_bytes();
let mut depth = 1usize;
let mut end = argument_start;
while end < bytes.len() && depth != 0 {
match bytes[end] {
b'{' if end == 0 || bytes[end - 1] != b'\\' => depth += 1,
b'}' if end == 0 || bytes[end - 1] != b'\\' => depth -= 1,
_ => {}
}
end += 1;
}
assert_eq!(
depth, 0,
"unterminated \\{command} argument at byte {command_start}"
);
arguments.push((command_start, text[argument_start..end - 1].to_owned()));
cursor = end;
}
arguments
}
fn labels(text: &str) -> Vec<String> {
command_arguments(text, "label")
.into_iter()
.map(|(_, label)| label)
.filter(|label| label.starts_with("req:"))
.collect()
}
fn line_number(text: &str, byte: usize) -> usize {
text[..byte].bytes().filter(|byte| *byte == b'\n').count() + 1
}
fn load_spec(path: &Path) -> SpecDocument {
let text = fs::read_to_string(path).unwrap_or_else(|error| {
panic!("failed to read {}: {error}", path.display());
});
let chapters = command_arguments(&text, "chapter");
let begin = r"\begin{requirement}";
let end = r"\end{requirement}";
let mut requirements = Vec::new();
let mut cursor = 0;
while let Some(relative) = text[cursor..].find(begin) {
let block_start = cursor + relative;
let body_start = block_start + begin.len();
let body_end = text[body_start..]
.find(end)
.map(|relative_end| body_start + relative_end)
.unwrap_or_else(|| panic!("unterminated requirement in {}", path.display()));
let chapter = chapters
.iter()
.rev()
.find(|(position, _)| *position < block_start)
.map(|(_, title)| title.clone())
.unwrap_or_else(|| {
panic!(
"requirement before first chapter in {}:{}",
path.display(),
line_number(&text, block_start)
)
});
requirements.push(RequirementBlock {
chapter,
line: line_number(&text, block_start),
labels: labels(&text[body_start..body_end]),
});
cursor = body_end + end.len();
}
SpecDocument {
name: path
.file_name()
.expect("specification path has a file name")
.to_string_lossy()
.into_owned(),
text,
requirements,
}
}
fn specification_documents() -> Vec<SpecDocument> {
let spec = repository_root().join("spec");
let mut paths: Vec<_> = fs::read_dir(&spec)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", spec.display()))
.map(|entry| entry.expect("failed to read spec directory entry").path())
.filter(|path| path.extension().is_some_and(|extension| extension == "tex"))
.collect();
paths.sort();
paths.iter().map(|path| load_spec(path)).collect()
}
fn label_parts(label: &str) -> Option<(&str, &str)> {
let mut parts = label.split(':');
if parts.next()? != "req" {
return None;
}
let area = parts.next()?;
let slug = parts.next()?;
if parts.next().is_some()
|| area.is_empty()
|| !area
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
|| !slug
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_lowercase())
|| !slug
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
{
return None;
}
Some((area, slug))
}
fn all_defined_labels(documents: &[SpecDocument]) -> BTreeSet<String> {
documents
.iter()
.flat_map(|document| labels(&document.text))
.collect()
}
#[test]
fn every_requirement_block_has_one_label() {
let documents = specification_documents();
let core = documents
.iter()
.find(|document| document.name == "core_spec.tex")
.expect("core_spec.tex was not scanned");
assert_eq!(core.requirements.len(), CORE_REQUIREMENT_COUNT);
let suite_count: usize = documents
.iter()
.map(|document| document.requirements.len())
.sum();
assert_eq!(suite_count, SUITE_REQUIREMENT_COUNT);
let failures: Vec<_> = documents
.iter()
.flat_map(|document| {
document
.requirements
.iter()
.filter(|requirement| requirement.labels.len() != 1)
.map(|requirement| {
format!(
"{}:{} has {} requirement labels",
document.name,
requirement.line,
requirement.labels.len()
)
})
})
.collect();
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn requirement_labels_follow_the_grammar() {
let documents = specification_documents();
let all_labels: Vec<_> = documents
.iter()
.flat_map(|document| labels(&document.text))
.collect();
assert_eq!(all_labels.len(), SUITE_LABEL_COUNT);
let malformed: Vec<_> = all_labels
.iter()
.filter(|label| label_parts(label).is_none())
.collect();
assert!(
malformed.is_empty(),
"malformed requirement labels: {malformed:?}"
);
}
#[test]
fn requirement_label_areas_match_their_chapters() {
let documents = specification_documents();
let expected: BTreeMap<_, _> = CHAPTER_AREAS
.iter()
.map(|(file, chapter, area)| ((*file, *chapter), *area))
.collect();
assert_eq!(expected.len(), CHAPTER_AREAS.len(), "duplicate area data");
let mut failures = Vec::new();
for document in &documents {
for requirement in &document.requirements {
let Some(label) = requirement.labels.first() else {
continue;
};
let Some((area, _)) = label_parts(label) else {
continue;
};
let expected_area = expected
.get(&(document.name.as_str(), requirement.chapter.as_str()))
.unwrap_or_else(|| {
panic!(
"missing chapter-area data for {} chapter {:?}",
document.name, requirement.chapter
)
});
if area != *expected_area {
failures.push(format!(
"{}:{} chapter {:?} requires area {:?}, found {label}",
document.name, requirement.line, requirement.chapter, expected_area
));
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn requirement_labels_are_unique_across_the_suite() {
let documents = specification_documents();
let mut locations: BTreeMap<String, Vec<String>> = BTreeMap::new();
for document in &documents {
for requirement in &document.requirements {
for label in &requirement.labels {
locations
.entry(label.clone())
.or_default()
.push(format!("{}:{}", document.name, requirement.line));
}
}
}
let duplicates: Vec<_> = locations
.iter()
.filter(|(_, occurrences)| occurrences.len() > 1)
.map(|(label, occurrences)| format!("{label}: {}", occurrences.join(", ")))
.collect();
assert!(duplicates.is_empty(), "{}", duplicates.join("\n"));
assert_eq!(locations.len(), SUITE_LABEL_COUNT);
}
fn is_citation_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'-' | b'_')
}
/// Labels that are deliberately named in prose but are **not** requirements.
///
/// The citation scan cannot tell "cite this requirement" from "name a label that
/// does not exist" — and documenting a dangling label is a legitimate thing to do.
/// Without this escape the check forces prose to become vaguer than the finding it
/// records: it already rewrote a scoping plan's `req:layoutir:vertical-bands`
/// into a euphemism to make itself pass.
///
/// A row here is a claim that the string is discussed, never cited. Keep it short,
/// and give the reason.
const DISCUSSED_NOT_CITED: &[(&str, &str)] = &[(
"req:layoutir:vertical-bands",
"never existed; the Pass-12 log cited it for a behavioural fix no requirement \
governs. Named in spec/PLAN_P13S1_LABELS.md as the finding that motivated \
this checker.",
)];
fn requirement_strings(text: &str) -> BTreeSet<String> {
let bytes = text.as_bytes();
let mut found = BTreeSet::new();
let mut cursor = 0;
while cursor + 4 <= bytes.len() {
if &bytes[cursor..cursor + 4] != b"req:"
|| (cursor > 0 && is_citation_byte(bytes[cursor - 1]))
{
cursor += 1;
continue;
}
let mut end = cursor + 4;
while end < bytes.len() && is_citation_byte(bytes[end]) {
end += 1;
}
let candidate = &text[cursor..end];
if candidate.bytes().filter(|byte| *byte == b':').count() >= 2 && !candidate.ends_with(':')
{
found.insert(candidate.to_owned());
}
cursor = end;
}
found
}
fn is_generated_artifact(path: &Path) -> bool {
path.extension().is_some_and(|extension| {
matches!(
extension.to_str(),
Some("aux" | "fdb_latexmk" | "fls" | "log" | "out" | "pdf" | "toc" | "xdv")
)
})
}
fn repository_text_files(directory: &Path, files: &mut Vec<PathBuf>) {
for entry in fs::read_dir(directory)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display()))
{
let path = entry.expect("failed to read repository entry").path();
if path.is_dir() {
let name = path.file_name().and_then(|name| name.to_str());
if !matches!(name, Some(".git" | "target")) {
repository_text_files(&path, files);
}
} else if !is_generated_artifact(&path) {
files.push(path);
}
}
}
#[test]
fn every_requirement_citation_is_defined() {
let documents = specification_documents();
let defined = all_defined_labels(&documents);
assert_eq!(defined.len(), SUITE_LABEL_COUNT);
let root = repository_root();
let mut paths = Vec::new();
repository_text_files(&root, &mut paths);
paths.sort();
let mut cited = BTreeSet::new();
let mut undefined: BTreeMap<String, Vec<String>> = BTreeMap::new();
for path in paths {
let Ok(bytes) = fs::read(&path) else {
continue;
};
let Ok(text) = String::from_utf8(bytes) else {
continue;
};
for citation in requirement_strings(&text) {
cited.insert(citation.clone());
if !defined.contains(&citation) {
undefined.entry(citation).or_default().push(
path.strip_prefix(&root)
.unwrap_or(&path)
.display()
.to_string(),
);
}
}
}
assert!(
cited.len() >= SUITE_LABEL_COUNT,
"citation scan found only {} distinct requirement strings",
cited.len()
);
for (allowed, _) in DISCUSSED_NOT_CITED {
undefined.remove(*allowed);
}
let failures: Vec<_> = undefined
.iter()
.map(|(citation, paths)| format!("{citation}: {}", paths.join(", ")))
.collect();
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
/// Each document must step the requirement counter with a `code=` key, and must
/// **not** use tcolorbox's own `auto counter`.
///
/// This looks like a style preference and is not. `auto counter` steps its
/// counter for `\label` purposes inside an internal `\sbox`, and
/// `\refstepcounter`'s effect on `\@currentlabel` is a *local* assignment that is
/// discarded when that box closes — before a `\label` written in the box body
/// ever runs. Every requirement in this suite is labelled that way. The result is
/// the failure mode this counter exists to fix, wearing a disguise: the box
/// titles number 1, 2, 3 correctly while the cross-references bind to the last
/// sectioning unit and silently point at the wrong requirement.
///
/// Measured on a three-box test document: titles rendered `1.1 1.2 1.3` while the
/// three refs resolved to `1.1 1.1 1.2`.
///
/// So this is a regression lock, not a lint. `every_requirement_block_has_one_label`
/// would stay green through that change, and so would every uniqueness check —
/// the labels remain unique, they merely resolve to the wrong numbers.
/// Strips LaTeX `%` comments, honouring `\%`.
///
/// Needed because the box definitions carry a comment *naming* `auto counter` to
/// explain why it is not used. A check that cannot tell code from a comment about
/// the code fires on its own documentation.
fn without_latex_comments(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for line in text.lines() {
let bytes = line.as_bytes();
let mut end = line.len();
for (i, _) in line.char_indices() {
if bytes[i] == b'%' && (i == 0 || bytes[i - 1] != b'\\') {
end = i;
break;
}
}
out.push_str(&line[..end]);
out.push('\n');
}
out
}
#[test]
fn requirement_counters_are_stepped_where_the_label_can_see_it() {
let documents = specification_documents();
let mut checked = 0usize;
for document in &documents {
let text = without_latex_comments(&document.text);
if !text.contains("\\newtcolorbox{requirement}") {
continue;
}
checked += 1;
let name = &document.name;
assert!(
text.contains("code={\\refstepcounter{requirement}}"),
"{name}: the requirement box must step its counter via `code=`, which runs \
in the environment's own group so a `\\label` in the body sees it"
);
assert!(
!text.contains("auto counter"),
"{name}: tcolorbox's `auto counter` steps the counter inside an \\sbox, so \
every `\\label` in a box body silently binds to the enclosing section \
instead. Titles look right; cross-references do not. See the comment at \
the box definition."
);
}
assert_eq!(
checked,
documents.len(),
"every specification document defines a requirement box; if one stopped, this \
lock silently stopped covering it"
);
}

View File

@ -0,0 +1,121 @@
# Contract: naming core_spec's requirements (P13-S1)
Repo root `/home/jeans/Repos/active/epiphany`. Read this in full before writing
anything. The plan is `spec/PLAN_P13S1_LABELS.md`; both its rulings are granted.
## What this pass is
`core_spec.tex` has **207** requirement blocks. **39** carry a `\label`; **168**
do not, so no conformance claim can cite them. The five companions are 70/70
labelled — `core_spec` is the sole offender. This pass names the 168.
It is **additive**. Never rename, renumber, or move an existing label: 39 in
`core_spec` and 70 in the companions are cited by code, tests, DECISIONS records
and conformance vectors.
## Naming agents write no `.tex`
All 168 edits land in one file, so the naming work is split from the editing
work. **You produce a TSV proposal. You do not touch `core_spec.tex`.** One
later agent applies every proposal after reviewing them together.
### The TSV
One file per chapter at `spec/labels/<chapter-slug>.tsv`. One row per
**unlabelled** requirement, tab-separated, no header:
```
<ordinal> <proposed-label> <one-line summary of the rule>
```
`<ordinal>` is the **1-based index of the `\begin{requirement}` occurrence in
`core_spec.tex`, counting every requirement block in file order — labelled ones
included.**
**Do not use line numbers.** The concurrent numbering wave edits the tcolorbox
definition at line 227, upstream of every requirement (the first is at line 966),
so a one-line change there shifts every line number in the file and would apply
168 labels to the wrong blocks. Ordinals cannot move.
Compute them exactly this way, so every agent agrees:
```python
import re
s = open('spec/core_spec.tex').read()
blocks = list(re.finditer(r'\\begin\{requirement\}(.*?)\\end\{requirement\}', s, re.S))
# ordinal is i+1; blocks[i].group(1) is the body; unlabelled iff '\\label{' not in it
```
`<one-line summary>` is for the human reviewing 168 proposals side by side. Say
what the requirement *obliges*, in one clause. It is not a slug and not a quote.
## The label grammar
`req:<area>:<slug>`
`<area>` is fixed **per chapter** by this table. Do not invent one.
| chapter | area |
|---|---|
| Pitch | `pitch` |
| Time and Duration | `time` |
| Tuning Systems and Pitch Spaces | `tuning` |
| The Score Graph | `graph` |
| Semantic Operations and Concurrent Reduction | `semops` |
| Layout Intermediate Representation | `layoutir` |
| File Format | `format` |
| Constraint-Solver Interface | `solver` |
| Performance Requirements | `perf` |
| Extension Points | `ext` |
| Intentionally Deferred Types and Specifications | `deferred` |
| Determinism Contract | `determinism` |
`<slug>` matches `[a-z][a-z0-9-]*` and must:
* **name the rule, not the location or the type.** `spelling-algorithm`, not
`chapter-2-para-4`, not `pitchspelling`.
* **survive rewording.** A slug describing the *constraint* outlives an editorial
pass; one quoting the sentence does not. Ask: if someone rewrote this
requirement's prose next year without changing what it obliges, would the name
still be right?
* **be unique across the whole suite**, not merely within your chapter. Check
against every existing label first:
`grep -rhoE 'req:[a-z0-9]+:[a-z0-9-]+' spec/*.tex | sort -u`
Study the 39 existing `core_spec` labels before naming anything. They are the
house voice and your names must sit beside them without looking foreign.
## Judgment calls you must surface rather than resolve
Put these in your report, not silently in the TSV:
* **Two requirements stating one rule.** Do not invent two names for it. Flag it
— it is a spec defect of the same family as P13-I1's two-listings drift, and it
is worth more than a label.
* **A block that is not really a requirement** — a definition, an example, a
restatement of something ratified elsewhere. Name it anyway so the checker
passes, but say so.
* **A requirement whose rule you cannot state in one clause.** That usually means
it obliges more than one thing, which is itself worth reporting.
## Verify your own output
Before reporting:
1. Row count equals the number of unlabelled requirements in your chapters. State
both numbers.
2. Every ordinal you emit points at a block that is currently **unlabelled**
re-derive them with the snippet above, do not trust an earlier scroll.
3. Every label matches `req:<area>:<slug>` with your chapter's area.
4. No slug collides with an existing label or with another row of yours.
5. Tabs, not spaces, between fields. No header line. No trailing blank line.
Report the actual commands and their actual output. On this project an agent once
reported "verification passes" when errors did in fact point into its own file.
## Do not
* Edit `core_spec.tex`, any other `.tex`, or another agent's TSV.
* Run `cargo fmt --all`.
* Rename or move an existing label.
* Guess at a chapter's area prefix — the table above is complete.

View File

@ -304,7 +304,7 @@ that was an implementation defect, not a definition defect.
|---|---|---|---| |---|---|---|---|
| `vertical_density_penalty` measured over glyph members | **fix (conformance), no catalog change**`vertical_raw` now measures each staff band's full content (glyphs, strokes, curves), attributed by declared `vertical_band`, read back from the BAKED output rather than the solve's own extents (so a shift the bake drops surfaces as a real deviation instead of hiding behind solver intent). Axis on the pressure fixture: 1.0 → 2.7e-7; the floor warning is gone. Formula, contributing units, anchor, normalization all unchanged | quality_metric_catalog §`vertical_density_penalty` (`req:qmc:vertical`) — **clarification only** | `epiphany-engrave` (`quality.rs::vertical_raw`, `casting.rs::CastLayout::{stroke_system,curve_system}`) | | `vertical_density_penalty` measured over glyph members | **fix (conformance), no catalog change**`vertical_raw` now measures each staff band's full content (glyphs, strokes, curves), attributed by declared `vertical_band`, read back from the BAKED output rather than the solve's own extents (so a shift the bake drops surfaces as a real deviation instead of hiding behind solver intent). Axis on the pressure fixture: 1.0 → 2.7e-7; the floor warning is gone. Formula, contributing units, anchor, normalization all unchanged | quality_metric_catalog §`vertical_density_penalty` (`req:qmc:vertical`) — **clarification only** | `epiphany-engrave` (`quality.rs::vertical_raw`, `casting.rs::CastLayout::{stroke_system,curve_system}`) |
| "Content extent" was ambiguous | **clarify (editorial)**`req:qmc:vertical` now spells out that content extent means every primitive the band owns, each attributed by its declared `vertical_band`, and not the band's glyph `members`. Before primitive band ownership this reading was arguably unimplementable, which is why the defect survived. The stale rationale (claiming the vertical spring solve is deferred) is refreshed, and the inter-system half of the axis is recorded as a genuine trade-off against `page_fill_efficiency`, not a defect | quality_metric_catalog §`vertical_density_penalty` rationale | — | | "Content extent" was ambiguous | **clarify (editorial)**`req:qmc:vertical` now spells out that content extent means every primitive the band owns, each attributed by its declared `vertical_band`, and not the band's glyph `members`. Before primitive band ownership this reading was arguably unimplementable, which is why the defect survived. The stale rationale (claiming the vertical spring solve is deferred) is refreshed, and the inter-system half of the axis is recorded as a genuine trade-off against `page_fill_efficiency`, not a defect | quality_metric_catalog §`vertical_density_penalty` rationale | — |
| Solve read the gap from a constructor | **fix** — the inter-staff solve now targets the `preferred_height` of the `InterStaffGap` band `to_constrained` emitted for that staff pair, not `VerticalBand::inter_staff_gap`'s default. The band is now a height model: a region declaring a wider gap gets one, and the solve and the metric agree by construction rather than by both calling the same constructor | — (behavioural, within `req:layoutir:vertical-bands`) | `epiphany-engrave` (`casting.rs`) | | Solve read the gap from a constructor | **fix** — the inter-staff solve now targets the `preferred_height` of the `InterStaffGap` band `to_constrained` emitted for that staff pair, not `VerticalBand::inter_staff_gap`'s default. The band is now a height model: a region declaring a wider gap gets one, and the solve and the metric agree by construction rather than by both calling the same constructor | — (behavioural; no labelled requirement governs vertical-band *heights* — see P13-S4) | `epiphany-engrave` (`casting.rs`) |
**Version movements.** Quality Metric Catalog **0.2.0 → 0.3.0** — see the review **Version movements.** Quality Metric Catalog **0.2.0 → 0.3.0** — see the review
follow-up below. (The conformance fix above needed none on its own: formula, follow-up below. (The conformance fix above needed none on its own: formula,
@ -369,7 +369,7 @@ Quality Metric Catalog 0.3.0 all unchanged.
| Item | Disposition | Spec locus | Consumer | | Item | Disposition | Spec locus | Consumer |
|---|---|---|---| |---|---|---|---|
| Expand-only inter-staff solve | **fix (layout)** — the solve now closes a slack pair as well as opening a crowded one, realizing the `InterStaffGap` band's declared height exactly. `SYSTEM_STAFF_PITCH` is demoted from a floor to an initial arrangement. This is what `vertical_density_penalty` was reporting: an un-pressured multi-staff system sat at 0.739, honest sprawl against the declared gap | — (behavioural; `req:layoutir:vertical-bands`) | `epiphany-engrave` (`casting.rs`) | | Expand-only inter-staff solve | **fix (layout)** — the solve now closes a slack pair as well as opening a crowded one, realizing the `InterStaffGap` band's declared height exactly. `SYSTEM_STAFF_PITCH` is demoted from a floor to an initial arrangement. This is what `vertical_density_penalty` was reporting: an un-pressured multi-staff system sat at 0.739, honest sprawl against the declared gap | — (behavioural; no labelled requirement governs vertical-band *heights* — see P13-S4) | `epiphany-engrave` (`casting.rs`) |
| The gap band's height had no agreed meaning | **pin (code default, not spec)** — it is an **ink clearance**: the separation between the two staves' outermost content, which is exactly the unit `req:qmc:vertical` measures. `preferred` 2.0 → **5.0**, `min` 1.0 → **2.0**. The old 2.0 was a placeholder reconciled with nothing — neither the 8.0 staff-box gap the fixed pitch of 12 produces nor the ~6.4 ink clearance it leaves for plain content; realizing it would have crushed a relaxed system to a pitch of ~7.6. At 5.0 plain ledgered content settles near a pitch of 10.6 | `VerticalBand::inter_staff_gap` doc | `epiphany-layout-ir` | | The gap band's height had no agreed meaning | **pin (code default, not spec)** — it is an **ink clearance**: the separation between the two staves' outermost content, which is exactly the unit `req:qmc:vertical` measures. `preferred` 2.0 → **5.0**, `min` 1.0 → **2.0**. The old 2.0 was a placeholder reconciled with nothing — neither the 8.0 staff-box gap the fixed pitch of 12 produces nor the ~6.4 ink clearance it leaves for plain content; realizing it would have crushed a relaxed system to a pitch of ~7.6. At 5.0 plain ledgered content settles near a pitch of 10.6 | `VerticalBand::inter_staff_gap` doc | `epiphany-layout-ir` |
| **Cascade defect, latent since v11** | **fix (correctness)** — the recurrence subtracted the upper staff's shift from the measured gap and added it back through the accumulator, so every pair below the first was over-separated by exactly the shift above it. Both staves move: `shift_lower = shift_upper + target (upper_lo lower_hi)`, the *unshifted* gap. Invisible on two-staff fixtures (`shift_upper = 0`) and invisible to the cascade regression, which asserted only `s2 > s1` — true under both recurrences. `three_staff_close_content`'s lower pair realized **21.06** against a declared 4.0 | — | `epiphany-engrave` (`casting.rs`) | | **Cascade defect, latent since v11** | **fix (correctness)** — the recurrence subtracted the upper staff's shift from the measured gap and added it back through the accumulator, so every pair below the first was over-separated by exactly the shift above it. Both staves move: `shift_lower = shift_upper + target (upper_lo lower_hi)`, the *unshifted* gap. Invisible on two-staff fixtures (`shift_upper = 0`) and invisible to the cascade regression, which asserted only `s2 > s1` — true under both recurrences. `three_staff_close_content`'s lower pair realized **21.06** against a declared 4.0 | — | `epiphany-engrave` (`casting.rs`) |

View File

@ -51,6 +51,7 @@ S1 and S2 remain open.
| Id | One-line statement | Filed in | Status | | Id | One-line statement | Filed in | Status |
|---|---|---|---| |---|---|---|---|
| P13-S1 | **169 of core_spec's 207 `requirement` blocks carry no `\label`**, so no conformance claim can cite them. Chapter 4 (`Tuning Systems and Pitch Spaces`) is 9/9 unlabeled and Chapter 11 (`Determinism Contract`) is 15/15, but the gap is universal, not local: `Semantic Operations` 24/27, `The Score Graph` 22/28, `Pitch` 10/13. The requirements *are* normative and *are* implemented; they simply cannot be named. Every `req:*` label the repo cites was added ad hoc by the pass that needed it | this file | **open** (found while auditing Push 4a; the audit that surfaced it scoped it to Chapter 4, which is where it was noticed, not where it lives) | | P13-S1 | **169 of core_spec's 207 `requirement` blocks carry no `\label`**, so no conformance claim can cite them. Chapter 4 (`Tuning Systems and Pitch Spaces`) is 9/9 unlabeled and Chapter 11 (`Determinism Contract`) is 15/15, but the gap is universal, not local: `Semantic Operations` 24/27, `The Score Graph` 22/28, `Pitch` 10/13. The requirements *are* normative and *are* implemented; they simply cannot be named. Every `req:*` label the repo cites was added ad hoc by the pass that needed it | this file | **resolved** (all 207 core_spec requirements labelled, suite 277/277; and the pass found that labelling alone was insufficient — no document *numbered* its requirements, so a `\label` bound to the enclosing section and 61 of 207 shared a rendered number, one shared by six. All six documents now carry a real counter. Locked by `requirement_labels.rs`) |
| P13-S2 | `cmn-24` is declared in the built-in pitch-space table (`core_spec.tex` §"Built-in Catalog") as "CMN extended with 24-EDO quarter-tone accidentals", but **cannot be represented**: `PitchSpacePosition::Cmn.alteration` is an `i8` documented as *whole semitones*, and a quarter-tone is half of one. Either the space is not `Cmn`-representable (and needs `Integer`/`Registered`), or `alteration` needs a finer unit — a data-model major | `crates/epiphany-core/DECISIONS.md` (Push 4b blockers) | **open** (blocks Push 4b) | | P13-S2 | `cmn-24` is declared in the built-in pitch-space table (`core_spec.tex` §"Built-in Catalog") as "CMN extended with 24-EDO quarter-tone accidentals", but **cannot be represented**: `PitchSpacePosition::Cmn.alteration` is an `i8` documented as *whole semitones*, and a quarter-tone is half of one. Either the space is not `Cmn`-representable (and needs `Integer`/`Registered`), or `alteration` needs a finer unit — a data-model major | `crates/epiphany-core/DECISIONS.md` (Push 4b blockers) | **open** (blocks Push 4b) |
| P13-S3 | The `engraved_spelling_chain` introduced with `TransposeInterval`'s undo had a **single writer**. `RespellPitch` mutates the same graph attachments but recorded only on `respell_chain`, so (a) undoing a transposed transaction after a prior respell restored the pitch and **erased the respell**, and (b) a respell landing canonically *after* the transaction was invisible to the chain, so a `StrictInverse` undo reported `Applied` and **wiped the newer authoring** instead of refusing as superseded. A `BestEffort` undo could also restore the pre-transpose pitch while leaving a spelling authored against the transposed one | `crates/epiphany-ops/DECISIONS.md` (P13-S3) | **resolved** (both operations now record on the shared key; pitch value + spelling set undo as one unit; new `req:opcat:spelling-set-chain`. The chain stays *physically* separate from `respell_chain`, which is `RespellPitch`'s LWW conflict state — folding transposes in would make a concurrent respell conflict with a transpose and move the canonical bytes of every existing history) | | P13-S3 | The `engraved_spelling_chain` introduced with `TransposeInterval`'s undo had a **single writer**. `RespellPitch` mutates the same graph attachments but recorded only on `respell_chain`, so (a) undoing a transposed transaction after a prior respell restored the pitch and **erased the respell**, and (b) a respell landing canonically *after* the transaction was invisible to the chain, so a `StrictInverse` undo reported `Applied` and **wiped the newer authoring** instead of refusing as superseded. A `BestEffort` undo could also restore the pre-transpose pitch while leaving a spelling authored against the transposed one | `crates/epiphany-ops/DECISIONS.md` (P13-S3) | **resolved** (both operations now record on the shared key; pitch value + spelling set undo as one unit; new `req:opcat:spelling-set-chain`. The chain stays *physically* separate from `respell_chain`, which is `RespellPitch`'s LWW conflict state — folding transposes in would make a concurrent respell conflict with a transpose and move the canonical bytes of every existing history) |
| P13-S4 | **No labelled requirement governs vertical-band *heights*.** Pass 12 twice recorded a behavioural fix to the inter-staff solve — realizing an `InterStaffGap` band's declared `preferred_height` rather than a constructor default — and both times cited `req:layoutir:vertical-bands`, which never existed. The two real band requirements (`req:layoutir:primitive-band-ownership`, `req:layoutir:resolved-band-ownership`) govern *ownership*: which band a primitive belongs to, and that a resolved primitive retains it. Nothing states what a band's height means or that the solver must realize it, so the shipped behaviour is unspecified and the log invented a name for the gap | this file | **open** (found in P13-S1 review: an agent 'corrected' the dangling citations to the two ownership requirements, which replaced a visibly broken pointer with a silently wrong one) |

183
spec/PLAN_P13S1_LABELS.md Normal file
View File

@ -0,0 +1,183 @@
# P13-S1 — naming the requirements: scope and plan
Status: **approved for dispatch. Requirements will be numbered; the Determinism prefix is `determinism`.**
Prepared against `master` @ `a9a5712`. Every claim was checked against the source;
where I ran a count I give it.
---
## 1. The task is not what the tracker says
P13-S1 is filed as *"169 of core_spec's 207 requirement blocks carry no `\label`,
so no conformance claim can cite them."* The count is right (**168** today) and
the diagnosis is right. But labelling alone does not deliver a citable
requirement, because **no document in the suite numbers its requirements.**
`\newtcolorbox{requirement}` has no counter, in all six documents. So a
`\label` inside one binds to the last incremented counter — the enclosing
sectioning unit — and `\ref` renders a *section* number:
> `...see Requirement 2.5.4` — 2.5.4 is a subsubsection.
This is already ambiguous today, in documents I bumped this session:
**`text_projection.pdf` renders five different requirements as "Requirement
3.1."** In `core_spec`, **25 sectioning units contain more than one requirement**,
so 25 sites where added labels would collide on their rendered number.
Adding 168 labels to that scheme produces 168 citable-but-ambiguously-rendered
references. Numbering is a **prerequisite**, not a follow-up.
### Ruling: give `requirement` a counter
Accepted: use `auto counter, number within=chapter`, with the box title showing
`Requirement~\thetcbcounter`, in all six documents. Every `req:*` label then
binds to its requirement's counter, `\ref{req:pitch:spelling-algorithm}` yields
a unique requirement number within its document, and the box header identifies
the requirement it contains.
Cost: every rendered "Requirement X.Y.Z" in all six PDFs changes to a requirement
number. That is churn in the PDFs and in the rendered text of the 63 existing
cross-references in `core_spec` alone — but the new text is *correct* where the
old text was misleading. No `req:*` label string changes, so **no code, test, or
conformance vector is affected**: those cite labels, not numbers.
---
## 2. What I verified
| fact | value |
|---|---|
| `core_spec` requirement blocks | **207** |
| labelled | 39 |
| **unlabelled** | **168** |
| every existing label matches `req:<area>:<slug>` | 39/39 |
| the five companions | **70/70 labelled**`core_spec` is the sole offender |
| `req:*` strings cited anywhere but never defined | **1** |
| requirement blocks opening with a `\textbf{...}` lead | 18/207 |
**The one dangling citation is `req:layoutir:vertical-bands`**, cited twice in
`spec/PASS12_RATIFICATION_LOG.md`. It was never a requirement, and no labelled
requirement governs vertical-band *heights*, which is what both entries describe
— so the honest correction is to say so, not to point them at the two *ownership*
requirements (`req:layoutir:primitive-band-ownership`,
`req:layoutir:resolved-band-ownership`), which govern something else.
A historical log citing a name that does not exist is a wrong pointer, not a
record of what was decided; recommend correcting it to the real label.
**Naming is judgment work, not transcription.** Only 18 requirements open with a
bolded lead sentence a slug could be derived from mechanically. The other 189 must
be read and named for the *rule they state*. That is the whole cost of this pass,
and it is what parallelizes.
---
## 3. Per-chapter distribution and the area table
Area prefixes are **fixed here** so parallel agents cannot diverge. Seven are
already established by the 39 existing labels; five chapters have none and need
one. House style across the suite is short and lowercase (`binfmt`, `opcat`,
`qmc`, `refsuite`, `semops`, `layoutir`).
| chapter | blocks | todo | area prefix |
|---|---:|---:|---|
| Pitch | 13 | 10 | `pitch` (established) |
| Time and Duration | 20 | 16 | `time` (established) |
| Tuning Systems and Pitch Spaces | 9 | 9 | **`tuning`** (new) |
| The Score Graph | 28 | 22 | `graph` (established) |
| Semantic Operations and Concurrent Reduction | 27 | 24 | `semops` (established) |
| Layout Intermediate Representation | 22 | 11 | `layoutir` (established) |
| File Format | 34 | 24 | `format` (established) |
| Constraint-Solver Interface | 18 | 16 | `solver` (established) |
| Performance Requirements | 16 | 16 | **`perf`** (new) |
| Extension Points | 4 | 4 | **`ext`** (new) |
| Intentionally Deferred Types | 1 | 1 | **`deferred`** (new) |
| Determinism Contract | 15 | 15 | **`determinism`** (new; fixed by ruling) |
**Slug rules** (also fixed here):
* lowercase, hyphenated, `[a-z][a-z0-9-]*`;
* names the **rule**, not the section or the type — `spelling-algorithm`, not
`chapter-2-para-4` and not `pitchspelling`;
* stable under rewording: a slug that describes the *constraint* survives an
editorial pass, one that quotes the sentence does not;
* unique across the whole suite, not merely within the chapter.
---
## 4. Work breakdown — propose, then apply
**Every requirement to label lives in one file.** `core_spec.tex` is 15k+ lines and all 168
edits land in it, so the parallel fan-out that worked for the last three waves
would put eight agents in one file. It must not.
Split the judgment from the edit:
### Wave 1 — numbering (one agent; may run concurrently with Wave 2)
Add `auto counter, number within=chapter` to `requirement` and render
`Requirement~\thetcbcounter` in the box title in all six documents; rebuild all
six PDFs; confirm every existing `\ref{req:...}` still resolves and now renders
a requirement number. 63 cross-references in `core_spec` alone will change
their rendered text — that is the point.
### Wave 2 — naming (seven agents in parallel, **no `.tex` edits**)
Each agent takes a set of chapters and emits **one TSV file per chapter** under
`spec/labels/<chapter-slug>.tsv`, with one row per unlabelled requirement:
```
<line-number-of-\begin{requirement}> <proposed-label> <one-line summary of the rule>
```
No agent touches `core_spec.tex`. Conflicts become impossible, and the whole
proposal is reviewable in one place — 168 names side by side, which is the only
way to catch the near-duplicates and the inconsistent verb tenses that a
chapter-at-a-time review misses.
Suggested split, balanced by count: (File Format 24) · (Semantic Operations 24) ·
(The Score Graph 22) · (Time 16 + Pitch 10) · (Solver 16 + Layout IR 11) ·
(Performance 16 + Determinism 15) · (Tuning 9 + Extension 4 + Deferred 1).
### Wave 3 — review, apply, and lock (one agent)
Review all TSVs together for duplicate concepts, inconsistent phrasing, and
slug collisions; resolve those proposal defects before editing. Then apply all
168 accepted rows to `core_spec.tex`, correct both dangling `vertical-bands`
pseudo-label citations to the real ownership requirement each row describes,
and land the checker.
---
## 5. The checker — the durable half
A committed test, in the shape of `text_projection_grammar.rs`. It must enforce:
1. **Every** `\begin{requirement}` in **every** `spec/*.tex` carries a `\label`.
Not "most", not core_spec only — the companions are at 70/70 and must stay.
2. Every label matches `req:<area>:<slug>` with the slug grammar above.
3. The area matches the chapter, per the table in §3, held as data in the test.
4. Labels are **unique across the suite**.
5. **No `req:*` string cited anywhere in the repo is undefined** — this is what
kills the `vertical-bands` class of defect permanently, and it is the check
with the most future value.
Assert the counts, so the test cannot pass by scanning nothing: 207 blocks in
`core_spec`, ≥277 across the suite, ≥110 distinct citations resolved.
---
## 6. Traps
* **Do not renumber or rename an existing label.** 39 in `core_spec` and 70 in the
companions are cited by code, tests, DECISIONS records and conformance vectors.
This pass is additive.
* **A `\label` must sit inside its `requirement` block**, after any `\textbf`
lead. Placed before `\begin{requirement}` it binds to the wrong thing and the
checker will not catch it — only the rendered PDF will.
* **`\ref` renders a number, `\nameref` renders a title.** Existing prose says
"Requirement~\ref{...}"; keep that form.
* **Two requirements can state one rule in different chapters.** Where that
happens, do not invent two names for it — flag it, because it is a spec defect
(P13-I1's two-listings drift in another costume).
* Rebuild **all six** PDFs. `core_spec` is the one being edited, but Wave 1
touches every document's box definition.

Binary file not shown.

View File

@ -178,10 +178,15 @@
boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt}, boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt},
#1 #1
} }
% Numbered within chapter (this document has chapters); see core_spec.tex's
% requirement box for why a plain counter + `code=` step is used instead of
% tcolorbox's own "auto counter, number within=..." keys.
\newcounter{requirement}[chapter]
\renewcommand{\therequirement}{\thechapter.\arabic{requirement}}
\newtcolorbox{requirement}[1][]{ \newtcolorbox{requirement}[1][]{
enhanced, breakable, enhanced, breakable,
colback=white, colframe=epiphanygold, colback=white, colframe=epiphanygold,
fonttitle=\bfseries\color{white}, title={\scshape\hspace{2pt}Requirement}, fonttitle=\bfseries\color{white}, code={\refstepcounter{requirement}}, title={\scshape\hspace{2pt}Requirement~\therequirement},
coltitle=white, colbacktitle=epiphanygold, coltitle=white, colbacktitle=epiphanygold,
arc=1pt, boxrule=0pt, leftrule=2pt, arc=1pt, boxrule=0pt, leftrule=2pt,
left=10pt, right=10pt, top=8pt, bottom=8pt, left=10pt, right=10pt, top=8pt, bottom=8pt,

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -178,10 +178,15 @@
boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt}, boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt},
#1 #1
} }
% Numbered within chapter (this document has chapters); see core_spec.tex's
% requirement box for why a plain counter + `code=` step is used instead of
% tcolorbox's own "auto counter, number within=..." keys.
\newcounter{requirement}[chapter]
\renewcommand{\therequirement}{\thechapter.\arabic{requirement}}
\newtcolorbox{requirement}[1][]{ \newtcolorbox{requirement}[1][]{
enhanced, breakable, enhanced, breakable,
colback=white, colframe=epiphanygold, colback=white, colframe=epiphanygold,
fonttitle=\bfseries\color{white}, title={\scshape\hspace{2pt}Requirement}, fonttitle=\bfseries\color{white}, code={\refstepcounter{requirement}}, title={\scshape\hspace{2pt}Requirement~\therequirement},
coltitle=white, colbacktitle=epiphanygold, coltitle=white, colbacktitle=epiphanygold,
arc=1pt, boxrule=0pt, leftrule=2pt, arc=1pt, boxrule=0pt, leftrule=2pt,
left=10pt, right=10pt, top=8pt, bottom=8pt, left=10pt, right=10pt, top=8pt, bottom=8pt,

Binary file not shown.

View File

@ -178,10 +178,15 @@
boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt}, boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt},
#1 #1
} }
% Numbered within chapter (this document has chapters); see core_spec.tex's
% requirement box for why a plain counter + `code=` step is used instead of
% tcolorbox's own "auto counter, number within=..." keys.
\newcounter{requirement}[chapter]
\renewcommand{\therequirement}{\thechapter.\arabic{requirement}}
\newtcolorbox{requirement}[1][]{ \newtcolorbox{requirement}[1][]{
enhanced, breakable, enhanced, breakable,
colback=white, colframe=epiphanygold, colback=white, colframe=epiphanygold,
fonttitle=\bfseries\color{white}, title={\scshape\hspace{2pt}Requirement}, fonttitle=\bfseries\color{white}, code={\refstepcounter{requirement}}, title={\scshape\hspace{2pt}Requirement~\therequirement},
coltitle=white, colbacktitle=epiphanygold, coltitle=white, colbacktitle=epiphanygold,
arc=1pt, boxrule=0pt, leftrule=2pt, arc=1pt, boxrule=0pt, leftrule=2pt,
left=10pt, right=10pt, top=8pt, bottom=8pt, left=10pt, right=10pt, top=8pt, bottom=8pt,

Binary file not shown.

View File

@ -178,10 +178,15 @@
boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt}, boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt},
#1 #1
} }
% Numbered within chapter (this document has chapters); see core_spec.tex's
% requirement box for why a plain counter + `code=` step is used instead of
% tcolorbox's own "auto counter, number within=..." keys.
\newcounter{requirement}[chapter]
\renewcommand{\therequirement}{\thechapter.\arabic{requirement}}
\newtcolorbox{requirement}[1][]{ \newtcolorbox{requirement}[1][]{
enhanced, breakable, enhanced, breakable,
colback=white, colframe=epiphanygold, colback=white, colframe=epiphanygold,
fonttitle=\bfseries\color{white}, title={\scshape\hspace{2pt}Requirement}, fonttitle=\bfseries\color{white}, code={\refstepcounter{requirement}}, title={\scshape\hspace{2pt}Requirement~\therequirement},
coltitle=white, colbacktitle=epiphanygold, coltitle=white, colbacktitle=epiphanygold,
arc=1pt, boxrule=0pt, leftrule=2pt, arc=1pt, boxrule=0pt, leftrule=2pt,
left=10pt, right=10pt, top=8pt, bottom=8pt, left=10pt, right=10pt, top=8pt, bottom=8pt,

Binary file not shown.

View File

@ -181,10 +181,15 @@
boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt}, boxed title style={arc=0pt, sharp corners, boxrule=0pt, left=6pt, right=8pt, top=2pt, bottom=2pt},
#1 #1
} }
% Numbered within chapter (this document has chapters); see core_spec.tex's
% requirement box for why a plain counter + `code=` step is used instead of
% tcolorbox's own "auto counter, number within=..." keys.
\newcounter{requirement}[chapter]
\renewcommand{\therequirement}{\thechapter.\arabic{requirement}}
\newtcolorbox{requirement}[1][]{ \newtcolorbox{requirement}[1][]{
enhanced, breakable, enhanced, breakable,
colback=white, colframe=epiphanygold, colback=white, colframe=epiphanygold,
fonttitle=\bfseries\color{white}, title={\scshape\hspace{2pt}Requirement}, fonttitle=\bfseries\color{white}, code={\refstepcounter{requirement}}, title={\scshape\hspace{2pt}Requirement~\therequirement},
coltitle=white, colbacktitle=epiphanygold, coltitle=white, colbacktitle=epiphanygold,
arc=1pt, boxrule=0pt, leftrule=2pt, arc=1pt, boxrule=0pt, leftrule=2pt,
left=10pt, right=10pt, top=8pt, bottom=8pt, left=10pt, right=10pt, top=8pt, bottom=8pt,