From d789613587e3650e5dce442ee6e508bb77bf4bba Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:20:38 +0200 Subject: [PATCH 1/2] fix(graph): preserve traced IDs before HTML agent-identity: mbp2025.direct.omp.mjwjfku9 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.2.11 agent-runtime: OMP 18.2.11 agent-model: openai-codex/gpt-5.6-sol tooling-profile: dotfiles@2df08b9-dirty --- crates/intent/src/graph_references.rs | 17 +++++++++++++++-- crates/intent/tests/intent_check.rs | 11 ++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/intent/src/graph_references.rs b/crates/intent/src/graph_references.rs index 7ad24fa..8df1a61 100644 --- a/crates/intent/src/graph_references.rs +++ b/crates/intent/src/graph_references.rs @@ -701,11 +701,14 @@ fn req_traces_in(document: &Document) -> Vec { { cursor += 1; } - let contains_placeholder = bytes.get(cursor) == Some(&b'<'); + let followed_by_angle_bracket = bytes.get(cursor) == Some(&b'<'); while cursor > target_start && bytes[cursor - 1] == b'.' { cursor -= 1; } - if cursor > target_start && !contains_placeholder { + let target = &line[target_start..cursor]; + let incomplete_placeholder = + followed_by_angle_bracket && !is_complete_req_target(target); + if cursor > target_start && !incomplete_placeholder { traces.push(ReqTrace { written: line[target_start..cursor].to_string(), range: line_start + found..line_start + cursor, @@ -727,6 +730,16 @@ fn is_req_target_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b'/' | b'#') } +fn is_complete_req_target(target: &str) -> bool { + let id = target + .rsplit_once('#') + .map_or(target, |(_, fragment)| fragment); + let bytes = id.as_bytes(); + qualified_id_end(bytes, 0) + .or_else(|| local_id_end(bytes, 0)) + .is_some_and(|end| end == bytes.len()) +} + fn resolve_req_trace( corpus_root: &Path, repository_root: &Path, diff --git a/crates/intent/tests/intent_check.rs b/crates/intent/tests/intent_check.rs index 6c444e4..c0462f3 100644 --- a/crates/intent/tests/intent_check.rs +++ b/crates/intent/tests/intent_check.rs @@ -468,7 +468,7 @@ fn graph_json_recognizes_all_supported_requirement_definition_forms() { .expect("requirements"); fs::write( h.repo.join("context/intent/spec.md"), - "# Spec\n\nThis builds on [requirements](./requirements.md).\n\nAPP-R02 and R03 constrain the graph.\nreq: APP-R02\nreq: \nreq: #\nreq: requirements.md#\nreq: APP-\n", + "# Spec\n\nThis builds on [requirements](./requirements.md).\n\nAPP-R02 and R03 constrain the graph.\nreq: APP-R02\nreq: APP-R02
\nreq: APP-R02\nreq: \nreq: #\nreq: requirements.md#\nreq: APP-\n", ) .expect("spec"); @@ -517,10 +517,15 @@ fn graph_json_recognizes_all_supported_requirement_definition_forms() { .collect::>(); assert_eq!( traces.len(), - 1, + 3, "template placeholders must not become requirement traces: {traces:#?}" ); - assert_eq!(traces[0]["written_target"], "APP-R02"); + assert!( + traces + .iter() + .all(|trace| trace["written_target"] == "APP-R02"), + "HTML immediately after a complete target must preserve the trace: {traces:#?}" + ); } #[test] From 756abdb613f097db28ab99d72db43155149ea3f9 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:31:42 +0200 Subject: [PATCH 2/2] fix(graph): normalize traced ID completeness agent-identity: mbp2025.direct.omp.mjwjfku9 agent-persona: generalist agent-supervisor: unavailable agent-tool: OMP agent-tool-version: 18.2.11 agent-runtime: OMP 18.2.11 agent-model: openai-codex/gpt-5.6-sol tooling-profile: dotfiles@2df08b9-dirty --- crates/intent/src/graph_references.rs | 7 +++++++ crates/intent/tests/intent_check.rs | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/intent/src/graph_references.rs b/crates/intent/src/graph_references.rs index 8df1a61..fff955c 100644 --- a/crates/intent/src/graph_references.rs +++ b/crates/intent/src/graph_references.rs @@ -734,6 +734,13 @@ fn is_complete_req_target(target: &str) -> bool { let id = target .rsplit_once('#') .map_or(target, |(_, fragment)| fragment); + let normalized; + let id = if id.bytes().any(|byte| byte.is_ascii_lowercase()) { + normalized = id.to_ascii_uppercase(); + normalized.as_str() + } else { + id + }; let bytes = id.as_bytes(); qualified_id_end(bytes, 0) .or_else(|| local_id_end(bytes, 0)) diff --git a/crates/intent/tests/intent_check.rs b/crates/intent/tests/intent_check.rs index c0462f3..49705ae 100644 --- a/crates/intent/tests/intent_check.rs +++ b/crates/intent/tests/intent_check.rs @@ -468,7 +468,7 @@ fn graph_json_recognizes_all_supported_requirement_definition_forms() { .expect("requirements"); fs::write( h.repo.join("context/intent/spec.md"), - "# Spec\n\nThis builds on [requirements](./requirements.md).\n\nAPP-R02 and R03 constrain the graph.\nreq: APP-R02\nreq: APP-R02
\nreq: APP-R02\nreq: \nreq: #\nreq: requirements.md#\nreq: APP-\n", + "# Spec\n\nThis builds on [requirements](./requirements.md).\n\nAPP-R02 and R03 constrain the graph.\nreq: APP-R02\nreq: APP-R02
\nreq: APP-R02\nreq: app-r02
\nreq: \nreq: #\nreq: requirements.md#\nreq: APP-\n", ) .expect("spec"); @@ -517,15 +517,23 @@ fn graph_json_recognizes_all_supported_requirement_definition_forms() { .collect::>(); assert_eq!( traces.len(), - 3, + 4, "template placeholders must not become requirement traces: {traces:#?}" ); - assert!( + assert_eq!( traces .iter() - .all(|trace| trace["written_target"] == "APP-R02"), + .filter(|trace| trace["written_target"] == "APP-R02") + .count(), + 3, "HTML immediately after a complete target must preserve the trace: {traces:#?}" ); + assert!( + traces.iter().any(|trace| { + trace["written_target"] == "app-r02" && trace["normalized_target"] == "APP-R02" + }), + "completeness must use the same case normalization as resolution: {traces:#?}" + ); } #[test]