diff --git a/crates/batten/src/config.rs b/crates/batten/src/config.rs index 1f6b23725..51ba902fd 100644 --- a/crates/batten/src/config.rs +++ b/crates/batten/src/config.rs @@ -1193,7 +1193,7 @@ pub fn parse(text: &str, source: &str) -> Result { // it cannot read gets the chance to fail the parse, so the refusal says which // engine to install rather than which field was unknown. crate::engine::check(text, source, crate::engine::running_stamp)?; - let config = parse_ungated(text, source)?; + let config = parse_remembered(text, source)?; check_min_version(&config, source)?; // A WRITER of the forge declaration (CLOUD-1622). Every config LOAD funnels // through here — `load`, `load_authority`, `load_site` — so recording it once @@ -4232,6 +4232,34 @@ fn parse_ungated(text: &str, source: &str) -> Result { parse_ungated_with(text, source, Grammar::Enforced) } +/// [`parse_ungated`], answered from the last success when the bytes and the +/// source are the ones it parsed. +/// +/// ONE ADJUDICATION PARSED THE SAME AUTHORITY THREE TIMES (CLOUD-2103): +/// `resolve` loads it twice and `epoch::authority` once more, each a full +/// deserialize and every load-time validator over 583 KB. Under callgrind that +/// was 435M of a 1,481M-instruction `adjudicate`. The result is a pure function +/// of `(text, source)` — the parse reads no environment and no file, and the one +/// load-time report, [`check_min_version`]'s, stays outside so it is still made +/// on every load. Only a success is kept: a refusal is re-derived, so its +/// message is never a stale one. +//MUTANT config-parse-unremembered|s@^ \&\& remembered_text == text$@ \&\& false@|one_authority_is_parsed_once_per_process +fn parse_remembered(text: &str, source: &str) -> Result { + static LAST: std::sync::Mutex> = std::sync::Mutex::new(None); + if let Ok(last) = LAST.lock() + && let Some((remembered_text, remembered_source, config)) = last.as_ref() + && remembered_text == text + && remembered_source == source + { + return Ok(config.clone()); + } + let config = parse_ungated(text, source)?; + if let Ok(mut last) = LAST.lock() { + *last = Some((text.to_owned(), source.to_owned(), config.clone())); + } + Ok(config) +} + fn parse_ungated_with(text: &str, source: &str, grammar: Grammar) -> Result { CONFIG_PARSES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); // THE COMMON CASE COSTS ONE PARSE, and it used to cost three. @@ -6074,6 +6102,30 @@ mod tests { ); } + /// CLOUD-2103: the same bytes from the same source are parsed once per + /// process, and anything else — other bytes, another source — is parsed. + #[test] + fn one_authority_is_parsed_once_per_process() { + let text = "version = 1\n# one-authority-is-parsed-once\n"; + let first = parse(text, "first.toml").unwrap(); + let before = config_parses(); + let again = parse(text, "first.toml").unwrap(); + assert_eq!( + config_parses(), + before, + "the same authority is not re-parsed" + ); + assert_eq!(again, first, "and answers what the parse answered"); + parse(text, "second.toml").unwrap(); + assert_eq!(config_parses(), before + 1, "another source is parsed"); + parse("version = 1\n# other bytes\n", "second.toml").unwrap(); + assert_eq!(config_parses(), before + 2, "other bytes are parsed"); + assert!( + parse("version = 1\nnot toml at all [", "bad.toml").is_err(), + "a refusal is still a refusal" + ); + } + #[test] fn a_present_authority_is_parsed_and_reported_present() { let dir = std::env::temp_dir().join("batten-config-authority-present"); diff --git a/crates/batten/src/identity.rs b/crates/batten/src/identity.rs index e866df9e6..7ef293b0d 100644 --- a/crates/batten/src/identity.rs +++ b/crates/batten/src/identity.rs @@ -791,13 +791,34 @@ pub fn checkout_fingerprint(repo_root: &std::path::Path) -> anyhow::Result String { - let canonical: String = span.replace("\r\n", "\n").nfc().collect(); + let canonical = canonical_text(span); match mode { SpanNormalization::Collapsed => canonical.chars().filter(|c| !c.is_whitespace()).collect(), - SpanNormalization::Verbatim => canonical, + SpanNormalization::Verbatim => canonical.into_owned(), } } +/// `CRLF -> LF` then NFC, borrowing the input when both are the identity. +/// +/// THE SURFACE IS ALREADY CANONICAL, AND REWRITING IT WAS 37% OF AN ADJUDICATION +/// (CLOUD-2102). [`surface_fingerprint`] runs every policy file through here on +/// every hook call, and that text is LF and NFC by construction, so the two +/// rewrites produced the bytes they were given. Measured under callgrind on one +/// mcp-surface `adjudicate`: 719M of 2,087M instructions. Text with no `CRLF` +/// that the quick check calls NFC is returned as it is, which is exactly what +/// the rewrite returns for it; anything else — a `CRLF`, or a quick check +/// answering `No` or `Maybe` — takes the rewrite, and is the only text that does. +//MUTANT nfc-fast-path-never|s@^ if !span.contains("\\r\\n")$@ if false \&\& !span.contains("\\r\\n")@|normalized_text_is_fingerprinted_without_a_rewrite +fn canonical_text(span: &str) -> std::borrow::Cow<'_, str> { + if !span.contains("\r\n") + && unicode_normalization::is_nfc_quick(span.chars()) + == unicode_normalization::IsNormalized::Yes + { + return std::borrow::Cow::Borrowed(span); + } + std::borrow::Cow::Owned(span.replace("\r\n", "\n").nfc().collect()) +} + /// Hash the kind tag plus each field, every part length-prefixed (u64 LE), so /// field boundaries are injective: `("ab","c")` can never collide with /// `("a","bc")`. @@ -1314,19 +1335,22 @@ pub fn context_fingerprint(bytes: &[u8]) -> Fingerprint { /// identity: adding an empty file still moves the value. #[must_use] pub fn surface_fingerprint(entries: &[(String, Vec)]) -> Fingerprint { - let normalized: Vec<(Vec, Vec)> = entries + let normalized: Vec<(&[u8], std::borrow::Cow<'_, [u8]>)> = entries .iter() .map(|(path, contents)| { let content = match std::str::from_utf8(contents) { - Ok(text) => normalize_span(text, SpanNormalization::Verbatim).into_bytes(), - Err(_) => contents.clone(), + Ok(text) => match canonical_text(text) { + std::borrow::Cow::Borrowed(text) => std::borrow::Cow::Borrowed(text.as_bytes()), + std::borrow::Cow::Owned(text) => std::borrow::Cow::Owned(text.into_bytes()), + }, + Err(_) => std::borrow::Cow::Borrowed(contents.as_slice()), }; - (path.as_bytes().to_vec(), content) + (path.as_bytes(), content) }) .collect(); let fields: Vec<&[u8]> = normalized .iter() - .flat_map(|(path, content)| [path.as_slice(), content.as_slice()]) + .flat_map(|(path, content)| [*path, content.as_ref()]) .collect(); tagged_fingerprint(SURFACE_TAG, &fields) } @@ -1452,6 +1476,52 @@ pub const fn compare_to_anchor(anchor: u64, current: u64) -> CountChange { mod tests { use super::*; + /// CLOUD-2102: canonical text is borrowed, never rewritten, and every input + /// still canonicalizes to exactly what the full rewrite produces. + #[test] + fn normalized_text_is_fingerprinted_without_a_rewrite() { + let rewrite = |text: &str| -> String { text.replace("\r\n", "\n").nfc().collect() }; + for canonical in ["", "plain ascii\nwith lines\n", "caf\u{e9} composed\n"] { + assert!( + matches!(canonical_text(canonical), std::borrow::Cow::Borrowed(_)), + "{canonical:?} is already canonical and is borrowed" + ); + } + for text in [ + "", + "plain ascii\nwith lines\n", + "caf\u{e9} composed\n", + "cafe\u{301} decomposed\n", + "crlf\r\nline\r\n", + "lone\rcarriage\n", + "mixed e\u{301}\r\n", + ] { + assert_eq!(canonical_text(text), rewrite(text), "{text:?}"); + assert_eq!( + normalize_span(text, SpanNormalization::Verbatim), + rewrite(text), + "{text:?}" + ); + } + let entries = vec![ + (String::from("a.rego"), b"package a\n".to_vec()), + (String::from("b.toml"), b"k = 1\r\n".to_vec()), + (String::from("c.bin"), vec![0xff, 0xfe]), + ]; + let expected = tagged_fingerprint( + SURFACE_TAG, + &[ + b"a.rego", + b"package a\n", + b"b.toml", + b"k = 1\n", + b"c.bin", + &[0xff, 0xfe], + ], + ); + assert_eq!(surface_fingerprint(&entries), expected); + } + // -- CLOUD-594: the golden vectors. -- // // Every other identity test in this module re-derives its expected value diff --git a/mise.toml b/mise.toml index ad2f11a71..81b24238e 100644 --- a/mise.toml +++ b/mise.toml @@ -647,7 +647,7 @@ CI_VERDICT_STEPS = "Run mise run ,Run mise exec -- " # which is a property of the world and belongs on a clock (`lock-complete`). REGORUS_OPA_COMPLIANCE = "1.2.0" REGORUS_OPA_COMPLIANCE_FOR = "0.11" -MUTANT_GATES = ".config/nextest.toml,engine-testing,crates/batten/tests/it/common/mod.rs,agentic-experiment-record,answer-the-operator,claude-code-cloud,engine-disk-watch,engine-prune,awk-regex,cap-drift,cfg-gated-test,ci-cache-declared,ci-hygiene,ci-parity,ci-slow-inert,ci-suite-lane,ci-tools,claim-before-code,claim-order-is-stated,coderabbit-config,commit-hygiene,dead-capability,denials-outlive-the-turn,digest-major-agreement,egress-fencing,engine-checks-green,engine-config,engine-doctor,engine-exec,engine-handler,engine-hook,engine-land,engine-landed,engine-lease,engine-lib,engine-mcp,engine-perf,engine-semver,engine-pinned,engine-pipeline,engine-policy,engine-ready,engine-speculation,engine-surface,engine-verdict,engine-wiring,filed-here,could-not-look-laundered,fixture-forks,forge-verdict-required,glob-containment,harness-grant,harness-wiring,hk-fix-selection,hk-plan-required,hook-pin-check,hook-skip-local,landing-loop,landing-roster-guarded,leased-push,license-table,lock-complete,mcp-timeout-budget,mise,mise-action-floor,mise-pin-agreement,module-map,msrv-pin-agreement,mutation-declared-case,nextest-slow,no-doctests,obligations-bound,perf-assert,pinned-toolchain,pipefail-grep,plan-complete,pr-partition-restated,pr-unsubscribed,privileged-lane,prose-only,publish-credential,release-due,release-provision-parity,release-tag-shape,remedy-authorship,repetition-without-progress,report-only,review-answered,review-dispatched,rules-paths-trigger,run-shape,rust-paths-check,sbom-inventory,shell-hygiene,shell-retirement,shell-write-advisory,skill-frontmatter-complete,spawn-widening,stop-posture,suite-subject-retirable,task-substitution,test-targets,timeout-budget,trunk-based,validator-verdict-clean,verdict-routes-resolve,weakens-declared,worktree-registration,engine-mutate,engine-task,run-arg-shape,engine-cargo-graph,branch-age,engine-released,evaluator-closure,evaluator-io-probe,agent-spawn,macos-link,ntia,release-tracking,sbom-actions,task-callable,transcript-corpus,engine-rules,release-assets,durable-write,spawn-factory,turn-ask,engine-forge,engine-forge-query,engine-git,engine-census,ci-signal,engine-ci-signal,docs-tree-absent,ripcord-untracked,hk-pin-agreement,engine-record,engine-suites,engine-admission,engine-ci-step,engine-gitwrite,engine-refusal,engine-repair,crates/batten/tests/it/mutant_rows.rs,hk-fix-selection.pkl,engine-dist,engine-sbom,supply-chain,engine-reclaim,engine-durable,engine-step,engine-step-table,engine-mcp-grant,engine-mcp-posture,engine-preflight,engine-trust,engine-sweep,sweep-exit-table,tracker-hygiene,engine-tracker-reading,task-duplicate-close-check,engine-release,release-hygiene,engine-hk,hook-profile,engine-attestation,engine-turn,engine-unsubscribe,engine-probe,finding-sink,engine-commit,engine-receipt,engine-board-check,check-verdict,engine-budget,engine-codemod,engine-remedy,engine-config-edit,engine-propose,crates/batten/tests/it/stub_portability.rs,crates/batten/tests/it/truncate_handle.rs,git,engine-engine,engine-attribution,crates/batten/tests/it/pointer_only.rs" +MUTANT_GATES = ".config/nextest.toml,engine-testing,crates/batten/tests/it/common/mod.rs,agentic-experiment-record,answer-the-operator,claude-code-cloud,engine-disk-watch,engine-prune,awk-regex,cap-drift,cfg-gated-test,ci-cache-declared,ci-hygiene,ci-parity,ci-slow-inert,ci-suite-lane,ci-tools,claim-before-code,claim-order-is-stated,coderabbit-config,commit-hygiene,dead-capability,denials-outlive-the-turn,digest-major-agreement,egress-fencing,engine-checks-green,engine-config,engine-doctor,engine-exec,engine-handler,engine-hook,engine-land,engine-landed,engine-lease,engine-lib,engine-mcp,engine-perf,engine-semver,engine-identity,engine-pinned,engine-pipeline,engine-policy,engine-ready,engine-speculation,engine-surface,engine-verdict,engine-wiring,filed-here,could-not-look-laundered,fixture-forks,forge-verdict-required,glob-containment,harness-grant,harness-wiring,hk-fix-selection,hk-plan-required,hook-pin-check,hook-skip-local,landing-loop,landing-roster-guarded,leased-push,license-table,lock-complete,mcp-timeout-budget,mise,mise-action-floor,mise-pin-agreement,module-map,msrv-pin-agreement,mutation-declared-case,nextest-slow,no-doctests,obligations-bound,perf-assert,pinned-toolchain,pipefail-grep,plan-complete,pr-partition-restated,pr-unsubscribed,privileged-lane,prose-only,publish-credential,release-due,release-provision-parity,release-tag-shape,remedy-authorship,repetition-without-progress,report-only,review-answered,review-dispatched,rules-paths-trigger,run-shape,rust-paths-check,sbom-inventory,shell-hygiene,shell-retirement,shell-write-advisory,skill-frontmatter-complete,spawn-widening,stop-posture,suite-subject-retirable,task-substitution,test-targets,timeout-budget,trunk-based,validator-verdict-clean,verdict-routes-resolve,weakens-declared,worktree-registration,engine-mutate,engine-task,run-arg-shape,engine-cargo-graph,branch-age,engine-released,evaluator-closure,evaluator-io-probe,agent-spawn,macos-link,ntia,release-tracking,sbom-actions,task-callable,transcript-corpus,engine-rules,release-assets,durable-write,spawn-factory,turn-ask,engine-forge,engine-forge-query,engine-git,engine-census,ci-signal,engine-ci-signal,docs-tree-absent,ripcord-untracked,hk-pin-agreement,engine-record,engine-suites,engine-admission,engine-ci-step,engine-gitwrite,engine-refusal,engine-repair,crates/batten/tests/it/mutant_rows.rs,hk-fix-selection.pkl,engine-dist,engine-sbom,supply-chain,engine-reclaim,engine-durable,engine-step,engine-step-table,engine-mcp-grant,engine-mcp-posture,engine-preflight,engine-trust,engine-sweep,sweep-exit-table,tracker-hygiene,engine-tracker-reading,task-duplicate-close-check,engine-release,release-hygiene,engine-hk,hook-profile,engine-attestation,engine-turn,engine-unsubscribe,engine-probe,finding-sink,engine-commit,engine-receipt,engine-board-check,check-verdict,engine-budget,engine-codemod,engine-remedy,engine-config-edit,engine-propose,crates/batten/tests/it/stub_portability.rs,crates/batten/tests/it/truncate_handle.rs,git,engine-engine,engine-attribution,crates/batten/tests/it/pointer_only.rs" # The file inline tasks are declared in, for `mutate`'s `task-` route (CLOUD-1909). # The crate may not spell a consumer's filename (non-negotiable rule 1), so the # manifest is named here, beside the set it serves. Unset, a `task-` gate resolves