Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .claude/rules/policy-modules.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
---
paths:
- "policy/**/*.rego"
- "batten.toml"
---

# Moved to `rules/policy-modules.md`

**The authority is [`rules/policy-modules.md`](../../rules/policy-modules.md). This file is a
Expand Down
127 changes: 125 additions & 2 deletions batten.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6846,6 +6846,38 @@ severity = "deny"
id = "release-tags"
tags = "v*"

# CLOUD-1787's demonstration, and the row that found the defect it demonstrates.
#
# Both gates below read frontmatter, which nothing could do before
# `Format::Markdown`. They ship WITH it rather than after it: non-negotiable
# rule 2 — a rule without a runnable gate is half a change — and a parser with
# no consumer is a mechanism nobody has shown decides anything.
#
# `sources` RATHER THAN `documents`, because the set is a glob and not a list.
# A stub added tomorrow is judged without anybody remembering to name it here,
# which is the whole difference between a gate over a directory and a gate over
# the five files somebody typed.
[[rule]]
id = "prose carry missing"
kind = "policy"
scope = "tree"
module = "policy/rules-paths-trigger.rego"
severity = "deny"
sources = [".claude/rules/*.md"]

# The second consumer, and the one that is GREEN on the tree it ships against —
# which is the half that makes the pair honest. A gate landed only where it
# already fires proves it can deny and never that it can pass; this one has four
# conforming files under it and a fixture proving it red, so both directions are
# demonstrated rather than one asserted.
[[rule]]
id = "prompt declare partial"
kind = "policy"
scope = "tree"
module = "policy/skill-frontmatter-complete.rego"
severity = "deny"
sources = ["skills/*/SKILL.md", ".claude/skills/*/SKILL.md"]

# The lockfile as it was STAGED (CLOUD-1203 unit A, the successor shape for
# `lock-complete`).
#
Expand Down Expand Up @@ -8395,14 +8427,31 @@ measured = "2026-09-07"
# COLD'S BASIS MOVES WITH WARM'S, for the 2026-09-06 entry's measured reason:
# refreshing one and not the other made the very next lap refuse on the other arm
# with warm never breached.
# THE 2026-09-11 REBASE MOVE, 244 -> 255, BOTH BASES, AND NEITHER FLOOR MOVES.
#
# WHAT MOVED THE COUNT is again the rebase rather than the new work alone.
# Replaying CLOUD-1787 onto the current trunk brought main's own additions —
# `release_token_precedence.rs` among them, which took the tree to 254 by itself —
# together with this branch's `frontmatter_gates.rs`. `target-prune` refused at
# `declared 244, live 255, tolerance 10`: the staleness arm, one file past the
# band, and not the disk.
#
# THE FLOORS DO NOT MOVE, for the 2026-09-08 entry's reason and not a new one. A
# basis refresh is a trend counter rather than a byte budget, so the count follows
# the tree and the floors stay where main re-measured them. Scaling them by
# 255/244 would be that entry's "same staleness wearing a newer number", and
# moving a floor still needs the independent measurement CLOUD-1158 owns.
#
# COLD'S BASIS MOVES WITH WARM'S, again because refreshing one and not the other
# makes the very next lap refuse on the other arm with warm never breached.
[prune.warm.basis]
glob = "crates/batten/tests/**/*.rs"
count = 244
count = 255
tolerance = 10

[prune.cold.basis]
glob = "crates/batten/tests/**/*.rs"
count = 244
count = 255
tolerance = 10

# THE REGROWABLE ROOTS THE ESCALATION MAY DROP (CLOUD-1157), in the order it drops
Expand Down Expand Up @@ -10292,6 +10341,80 @@ gloss = "declared and spent nowhere"
word = "wrong"
gloss = "does not match the declared shape"

[[verdict]]
id = "prose declare missing"
gloss = "a rule stub carries no `paths:` trigger, so the one mechanism it exists for does not fire"
class = """
The stub is kept for its loading trigger and nothing else — its own body says
deleting it would cost that trigger. Without one it is a file that loads for
nobody, asserting in prose that it does. Measured: one stub had never carried
the trigger, from the commit that wrote the claim onward, because no gate could
read frontmatter to notice (CLOUD-1787, CLOUD-1152).
"""

[[verdict.route]]
id = "prose write now"
kind = "document"
target = ".claude/rules"

[[verdict]]
id = "prose read unread"
gloss = "a rule stub's frontmatter would not parse, so its trigger could not be read"
class = """
Could-not-look, refused rather than reported clean: a module that iterates only
the stubs that parsed reports green over the one it never saw, and a dead gate
and a clean tree are byte-identical on the decision surface.
"""

[[verdict.route]]
id = "prose write now"
kind = "document"
target = ".claude/rules"

[[verdict]]
id = "prompt declare missing"
gloss = "a SKILL.md declares no usable `name` or `description`, so a harness has no basis to load it"
class = """
The failure is silent by construction: a skill a harness never selects looks
exactly like one it selected and that had nothing to say. Blank counts as absent
because `name:` with nothing after it and `name: \"\"` both parse, and a
predicate testing only for the key passes both.
"""

[[verdict.route]]
id = "prompt declare now"
kind = "document"
target = "skills"

[[verdict]]
id = "prompt name wrong"
gloss = "a SKILL.md's declared `name` is not the directory it ships in"
class = """
Two authorities for one identity. A harness routes by directory and displays by
`name`; while they disagree, neither is wrong on its own terms and the
disagreement is invisible to both.
"""

[[verdict.route]]
id = "prompt declare now"
kind = "document"
target = "skills"

[[verdict]]
id = "prompt read unread"
gloss = "a SKILL.md carries no frontmatter or would not parse, so its declared fields could not be read"
class = """
Could-not-look, refused rather than reported clean, for `rules trigger unread`'s
reason. A `SKILL.md` with no fence declares no fields at all, which is the
violation above reaching this module through the acquisition layer instead of
through the document.
"""

[[verdict.route]]
id = "prompt declare now"
kind = "document"
target = "skills"

[[verdict]]
id = "provision declare dropped"
gloss = "no [[provision.env]] row carries a no-proxy key, so the provisioned mise wrapper fences nothing"
Expand Down
2 changes: 2 additions & 0 deletions crates/batten/src/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ impl Drifted {
"baseline".to_owned(),
"--prune".to_owned(),
])),
reason: None,
})
}
}
Expand Down Expand Up @@ -709,6 +710,7 @@ mod tests {
identity: StoredIdentity::new(FindingKind::Code, fingerprint),
check: Check::Reevaluate,
remediation: Some(Remediation::NoFix("fix it".to_owned())),
reason: None,
}
}

Expand Down
35 changes: 26 additions & 9 deletions crates/batten/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ impl Report {
remediation: Some(crate::findings::Remediation::NoFix(
"cut instruction text until the set is under its budget".to_owned(),
)),
reason: None,
})
}
}
Expand Down Expand Up @@ -311,16 +312,22 @@ pub fn loaded(text: &str) -> String {
strip_comments(strip_frontmatter(text))
}

/// Drop a leading `---\n … \n---\n` frontmatter fence, if there is one.
/// Drop a leading frontmatter fence, if there is one.
///
/// **The fence rule is `facts::split_frontmatter`'s and not a second one here**
/// (CLOUD-1787). Both sides answer the same question — where does this file's
/// frontmatter end — and they answered it in two hand-rolled scans until
/// `Format::Markdown` needed the other half of the same split. Two spellings of
/// one rule is how a file gets taxed for bytes the loader drops, or read as a
/// document the loader counts.
///
/// Sharing it widened this side: a BOM, `\r\n` and a `...` terminator are now
/// recognised where the scan here saw only `---\n` … `\n---\n`. That is a
/// correction rather than a drift — the loader drops those fences too, so
/// counting them was always over-taxing.
fn strip_frontmatter(text: &str) -> &str {
let Some(rest) = text.strip_prefix("---\n") else {
return text;
};
// The closing fence is a `---` on its own line. Without one the document has
// no frontmatter, only a horizontal rule — leave it alone rather than
// guessing where it ends.
match rest.find("\n---\n") {
Some(end) => &rest[end + "\n---\n".len()..],
match crate::facts::split_frontmatter(text) {
Some(found) => found.body,
None => text,
}
}
Expand Down Expand Up @@ -634,6 +641,16 @@ mod tests {
);
// An unterminated fence is not frontmatter.
assert_eq!(loaded("---\nname: x\n"), "---\nname: x\n");
// The three spellings the shared rule recognises that the scan here did
// not (CLOUD-1787). Each was over-taxed before: the loader drops these
// fences too, so counting them charged for bytes that never reach a
// context window.
//
// Fails by: re-hand-rolling the fence scan in this module, which is how
// the two readers disagreed about the same bytes in the first place.
assert_eq!(loaded("\u{feff}---\nname: x\n---\nbody\n"), "body\n");
assert_eq!(loaded("---\r\nname: x\r\n---\r\nbody\n"), "body\n");
assert_eq!(loaded("---\nname: x\n...\nbody\n"), "body\n");
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/batten/src/defects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ impl Problem {
remediation: Some(crate::findings::Remediation::NoFix(
"resolve or rewrite the ledger row this points at".to_owned(),
)),
reason: None,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/batten/src/design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ impl Problem {
remediation: Some(crate::findings::Remediation::NoFix(
"rewrite or withdraw the design claim this points at".to_owned(),
)),
reason: None,
}
}
}
Expand Down
Loading
Loading