From 6824eb872c9f9e0e3f1922a81d24dd5c5da37608 Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Fri, 10 Jul 2026 15:07:18 +0000 Subject: [PATCH] Enable the content-policy dimension by default The built-in baseline is active without any repository configuration. .postil/content-policy.md extends it and contentPolicy.enabled: false fully opts out. The baseline's fabricated-claims and transcript-leakage rules emit error-severity findings, so a default failOn: error gate blocks on them. --- ARCHITECTURE.md | 5 ++-- README.md | 32 +++++++++++++----------- ROADMAP.md | 10 ++++---- src/config.rs | 65 +++++++++++++++++++++++++++++++++++-------------- src/envelope.rs | 4 +-- src/prompt.rs | 3 ++- tests/e2e.rs | 15 +++--------- 7 files changed, 79 insertions(+), 55 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c83fa16..101d6ee 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -50,9 +50,8 @@ second model call: guardrails are repo-specific merge rules from `.postil/guardr (violations are `kind: guardrail`); content policy reviews human-readable prose only (Markdown, comments, docstrings, user-facing strings, PR title/body — never code logic or identifiers) against a built-in baseline plus optional `.postil/content-policy.md` -additions (violations are `kind: contentPolicy`). Content policy is off by default; -either an explicit `contentPolicy.enabled: true` or the mere presence of -`.postil/content-policy.md` turns it on, mirroring how guardrails activates. +additions (violations are `kind: contentPolicy`). Content policy is on by default; +`contentPolicy.enabled: false` fully disables the baseline and repo additions. ## Invariants diff --git a/README.md b/README.md index ca23954..7f69471 100644 --- a/README.md +++ b/README.md @@ -99,17 +99,20 @@ the rule it breaks. ## Content policy -Off by default. Reviews human-readable prose in the diff — Markdown, code comments, -docstrings, user-facing/log strings, and the PR title/description, never code logic or -identifiers — for fabricated or contradicted documentation claims, self-contradictions -the same PR creates, authoring-process narration and AI-authorship residue, leaked -conversation/transcript text, and (lower severity) stale temporal/TODO residue and house -style. Violations are reported as `contentPolicy` findings that name the rule broken. - -Turn it on with `contentPolicy.enabled: true` in `.postil.yaml` for the built-in -baseline, or drop repo-specific additions in `.postil/content-policy.md` (which also -turns it on by itself, the same way `.postil/guardrails.md` does); the file's rules are -appended to the baseline, not a replacement for it. +On by default. It reviews human-readable prose in the diff, including Markdown, code +comments, docstrings, user-facing/log strings, and the PR title/description, never code +logic or identifiers. It checks for fabricated or contradicted documentation claims, +self-contradictions the same PR creates, authoring-process narration and AI-authorship +residue, leaked conversation/transcript text, and (lower severity) stale temporal/TODO +residue and house style. Violations are reported as `contentPolicy` findings that name +the rule broken. + +The built-in baseline reports fabricated or contradicted claims and conversation leaks +at `error`, self-contradictions and authorship residue at `warn`, and stale/style residue +at `info`. With the default `gate.failOn: error`, genuine violations of either error rule +fail the gate. Repo-specific rules in `.postil/content-policy.md` are appended to the +baseline, not a replacement for it. Set `contentPolicy.enabled: false` in `.postil.yaml` +to fully disable both the baseline and repo-specific additions. ## Configuration @@ -132,9 +135,10 @@ gate: failOn: error # info | warn | error | never onError: block # block (fail closed, default) | advisory (fail open on # provider outage only; unusable model output still blocks) -contentPolicy: - enabled: false # true reviews prose in the diff for fabricated claims, AI- - # authorship residue, and stale/style residue (see above) +# Content policy uses the built-in baseline by default and extends it with +# .postil/content-policy.md. Uncomment to fully opt out: +# contentPolicy: +# enabled: false model: name: deepseek/deepseek-v4-pro cascade: [anthropic/claude-sonnet-4.6] diff --git a/ROADMAP.md b/ROADMAP.md index 572680b..067569c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,11 +13,11 @@ - SARIF 2.1.0 output (`--sarif`) for code-scanning ingestion. - Repo guardrails file (`.postil/guardrails.md`) injected into the prompt; violations surface as `kind: guardrail` findings that quote the rule. -- Content policy (`contentPolicy.enabled` or `.postil/content-policy.md`, off by - default): reviews prose in the diff for fabricated/contradicted doc claims, - same-PR self-contradictions, AI-authorship/process-narration residue, leaked - conversation text, and stale temporal/TODO/style residue; surfaces as - `kind: contentPolicy`. +- Content policy (built-in baseline by default, optional + `.postil/content-policy.md` additions, `contentPolicy.enabled: false` opt-out): + reviews prose in the diff for fabricated/contradicted doc claims, same-PR + self-contradictions, AI-authorship/process-narration residue, leaked conversation + text, and stale temporal/TODO/style residue; surfaces as `kind: contentPolicy`. - `postil plan` deterministic config dry-run; `postil doctor`; pre-push hook. - `.coderabbit.yaml` translation for zero-cost migration. - Model cascade + concurrent multi-model consensus over any OpenAI-compatible endpoint; diff --git a/src/config.rs b/src/config.rs index 4e3bdd1..107a60e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -93,7 +93,7 @@ pub struct Config { /// rules. Findings that violate one are emitted as `kind: guardrail`. pub guardrails: Option, /// Active content-policy rules (built-in baseline, optionally extended by - /// `.postil/content-policy.md`), or `None` when the dimension is off. + /// `.postil/content-policy.md`), or `None` after an explicit opt-out. /// Findings are emitted as `kind: contentPolicy`. See [`load`](Self::load). pub content_policy: Option, /// Set by an explicit `contentPolicy: { enabled: false }`, so a repo can @@ -122,7 +122,7 @@ impl Default for Config { api_base: DEFAULT_API_BASE.to_string(), consensus: 1, guardrails: None, - content_policy: None, + content_policy: Some(BUILTIN_CONTENT_POLICY.to_string()), content_policy_disabled: false, source: "defaults".to_string(), } @@ -180,8 +180,8 @@ pub struct ModelSection { /// documentation claims, self-contradictions the same PR creates, authoring- /// process narration and AI-authorship residue, leaked conversation/transcript /// text, and (low severity) stale temporal/TODO residue and house style. -/// Off by default; enabling it or dropping a `.postil/content-policy.md` file -/// turns it on. Findings are reported as `kind: contentPolicy`. +/// On by default; `contentPolicy: { enabled: false }` fully disables it. +/// Findings are reported as `kind: contentPolicy`. #[derive(Debug, Default, Deserialize, Serialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct ContentPolicySection { @@ -220,9 +220,8 @@ impl Config { cfg.guardrails = Some(trimmed.to_string()); } } - // A repo-specific content-policy file turns the dimension on (like - // guardrails.md) even without an explicit `contentPolicy.enabled: - // true`, unless that was explicitly set to false above. + // Repo-specific content-policy rules extend the built-in baseline, + // unless the repository explicitly opted out above. if !cfg.content_policy_disabled { let content_policy_path = root.join(".postil").join("content-policy.md"); if let Ok(text) = std::fs::read_to_string(&content_policy_path) { @@ -334,7 +333,10 @@ impl Config { } if let Some(cp) = f.content_policy { match cp.enabled { - Some(true) => self.content_policy = Some(BUILTIN_CONTENT_POLICY.to_string()), + Some(true) => { + self.content_policy = Some(BUILTIN_CONTENT_POLICY.to_string()); + self.content_policy_disabled = false; + } Some(false) => { self.content_policy = None; self.content_policy_disabled = true; @@ -471,12 +473,10 @@ model: # # trusted local repo. # consensus: 1 +# Content policy is on by default. Repo rules in .postil/content-policy.md extend +# the built-in baseline. Uncomment this explicit opt-out to fully disable it. # contentPolicy: -# enabled: true # review prose in the diff (docs, comments, PR body) for -# # fabricated claims, AI-authorship residue, leaked -# # conversation text, and stale/style nits. Off by default; -# # also turns on automatically if .postil/content-policy.md -# # exists. See https://postil.dev/docs/content-policy +# enabled: false # See https://postil.dev/docs/content-policy "#; /// Built-in content-policy baseline, active whenever the dimension is on @@ -631,10 +631,11 @@ mod tests { } #[test] - fn content_policy_off_by_default() { + fn content_policy_on_by_default() { let dir = tempfile::tempdir().unwrap(); let c = Config::load(dir.path(), None).unwrap(); - assert!(c.content_policy.is_none()); + assert_eq!(c.content_policy.as_deref(), Some(BUILTIN_CONTENT_POLICY)); + assert!(!c.content_policy_disabled); } #[test] @@ -650,7 +651,19 @@ mod tests { } #[test] - fn content_policy_file_presence_turns_it_on_and_is_appended() { + fn builtin_content_policy_severity_profile_matches_default_gate() { + assert_eq!(BUILTIN_CONTENT_POLICY.matches("report at error").count(), 2); + assert_eq!(BUILTIN_CONTENT_POLICY.matches("report at warn").count(), 2); + assert_eq!(BUILTIN_CONTENT_POLICY.matches("report at info").count(), 2); + + let c = Config::default(); + assert!(c.gate_fail_on.fails(Severity::Error)); + assert!(!c.gate_fail_on.fails(Severity::Warn)); + assert!(!c.gate_fail_on.fails(Severity::Info)); + } + + #[test] + fn content_policy_file_is_appended_after_the_builtin_baseline() { let dir = tempfile::tempdir().unwrap(); std::fs::create_dir(dir.path().join(".postil")).unwrap(); write( @@ -660,8 +673,10 @@ mod tests { ); let c = Config::load(dir.path(), None).unwrap(); let policy = c.content_policy.as_deref().unwrap(); - assert!(policy.contains("Fabricated")); - assert!(policy.contains("Never mention unreleased pricing")); + assert!(policy.starts_with(BUILTIN_CONTENT_POLICY)); + assert!( + policy.contains("--- REPO-SPECIFIC ADDITIONS ---\nNever mention unreleased pricing.") + ); } #[test] @@ -697,5 +712,19 @@ mod tests { ); let c = Config::load(dir.path(), None).unwrap(); assert!(c.content_policy.is_none()); + assert!(c.content_policy_disabled); + } + + #[test] + fn content_policy_explicit_false_disables_the_default_baseline() { + let dir = tempfile::tempdir().unwrap(); + write( + dir.path(), + ".postil.yaml", + "contentPolicy:\n enabled: false\n", + ); + let c = Config::load(dir.path(), None).unwrap(); + assert!(c.content_policy.is_none()); + assert!(c.content_policy_disabled); } } diff --git a/src/envelope.rs b/src/envelope.rs index 645afd7..b1696e7 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -42,8 +42,8 @@ pub enum Kind { /// Violates the content policy: a fabricated/contradicted doc claim, a /// self-contradiction the same PR creates, authoring-process or AI /// narration residue, leaked conversation/transcript text, or (low - /// severity, opt-in noise) stale temporal/TODO residue and house style. - /// Only emitted when `contentPolicy` is active for the repo. + /// severity) stale temporal/TODO residue and house style. Only emitted when + /// `contentPolicy` is active for the repo. ContentPolicy, } diff --git a/src/prompt.rs b/src/prompt.rs index 70a4607..1f4fd21 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -261,7 +261,8 @@ mod tests { #[test] fn system_prompt_omits_content_policy_when_inactive() { - let cfg = Config::default(); + let mut cfg = Config::default(); + cfg.content_policy = None; let p = system_prompt(&cfg); assert!(!p.contains("CONTENT POLICY")); } diff --git a/tests/e2e.rs b/tests/e2e.rs index 3d5503c..b84cff3 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -917,7 +917,7 @@ fn llm_with_summary(summary: &str, findings: Value) -> Value { } // Shared GitHub remote setup for the content-policy PR-description tests. The PR -// body carries prose the model flags; content policy is enabled via config. +// body carries prose the model flags; content policy is active by default. async fn content_policy_pr_server(llm: Value) -> MockServer { let server = MockServer::start().await; Mock::given(method("POST")) @@ -976,11 +976,6 @@ async fn content_policy_pr_body_finding_survives_grounding() { .await; let dir = tempfile::tempdir().unwrap(); - std::fs::write( - dir.path().join(".postil.yaml"), - "contentPolicy:\n enabled: true\n", - ) - .unwrap(); let out = postil() .current_dir(dir.path()) .env("POSTIL_API_BASE", server.uri()) @@ -1030,16 +1025,11 @@ async fn content_policy_pr_body_finding_survives_grounding() { #[tokio::test] async fn content_policy_clean_run_does_not_fail_close() { - // With content policy on and no violations, the run stays clean: the numbered + // With default content policy and no violations, the run stays clean: the numbered // PR-description block must not induce a spurious ungrounded/fail-closed run. let server = content_policy_pr_server(llm_with_summary("", json!([]))).await; let dir = tempfile::tempdir().unwrap(); - std::fs::write( - dir.path().join(".postil.yaml"), - "contentPolicy:\n enabled: true\n", - ) - .unwrap(); let out = postil() .current_dir(dir.path()) .env("POSTIL_API_BASE", server.uri()) @@ -1873,6 +1863,7 @@ fn init_writes_starter_and_config_shows_provenance() { let stdout = String::from_utf8(out.get_output().stdout.clone()).unwrap(); assert!(stdout.contains("source: .postil.yaml")); assert!(stdout.contains("gate.failOn: error")); + assert!(stdout.contains("contentPolicy: active")); } #[test]