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:
parent
a9a57120e9
commit
043c18cabf
|
|
@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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`) |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue