Files
wehub-resource-sync 26382a7ac6
CI / Clippy (push) Failing after 15m13s
CI / Test (ubuntu-latest) (push) Failing after 16m1s
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Build (no embeddings / no ORT) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Cookbook (Node) (push) Has been cancelled
CI / Pi Extension (Node) (push) Has been cancelled
CI / Rust SDK (lean-ctx-client) (push) Has been cancelled
CI / Embed SDK (lean-ctx-sdk) (push) Has been cancelled
CI / Python SDK (leanctx) (push) Has been cancelled
CI / Hermes Plugin (Python) (push) Has been cancelled
CI / SDK Conformance Matrix (push) Has been cancelled
CI / Coverage (push) Has been cancelled
CI / cargo-deny (push) Has been cancelled
CI / Adversarial Safety (push) Has been cancelled
CI / Benchmarks (push) Has been cancelled
CI / Output-Quality Gate (eval A/B) (push) Has been cancelled
CI / Documentation (push) Has been cancelled
CI / CI Green (push) Has been cancelled
JetBrains Plugin / Actionlint (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (rust) (push) Has been cancelled
JetBrains Plugin / Validation (push) Has been cancelled
JetBrains Plugin / Build (push) Has been cancelled
JetBrains Plugin / Test (push) Has been cancelled
Security Check / Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:30 +08:00

144 lines
5.4 KiB
Rust

//! Contract-freeze CI gate (GL #394, CONTRACTS.md § Stability matrix).
//!
//! Two invariants, enforced on every CI run:
//!
//! 1. **Completeness** — every `docs/contracts/*.md` file is classified in
//! `core::contracts::contract_docs()` (frozen / stable / experimental) and
//! every classified entry actually exists on disk. No contract can stay
//! unclassified (pattern: `feature_keys_partition` in plans.rs).
//! 2. **Immutability** — the content hash of every `frozen` contract doc
//! matches the committed snapshot `docs/contracts/frozen-hashes.json`. A
//! drifting hash means someone edited a frozen artifact: semantic changes
//! must land as a new `-v2.md` file instead. Deliberate typo fixes update
//! the snapshot via `LEANCTX_UPDATE_FROZEN_HASHES=1 cargo test --test
//! contracts_frozen` and must be justified in the PR.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use lean_ctx::core::contracts::{ContractStatus, contract_docs};
use sha2::{Digest, Sha256};
fn contracts_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../docs/contracts")
}
fn snapshot_path() -> PathBuf {
contracts_dir().join("frozen-hashes.json")
}
/// Hash with CRLF normalized away so Windows checkouts (autocrlf) agree with
/// the committed snapshot.
fn content_hash(path: &Path) -> String {
let raw = std::fs::read(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let normalized: Vec<u8> = {
let mut out = Vec::with_capacity(raw.len());
let mut iter = raw.iter().peekable();
while let Some(&b) = iter.next() {
if b == b'\r' && iter.peek() == Some(&&b'\n') {
continue;
}
out.push(b);
}
out
};
let mut hasher = Sha256::new();
hasher.update(&normalized);
use std::fmt::Write;
hasher.finalize().iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
#[test]
fn every_contract_doc_is_classified() {
let dir = contracts_dir();
let mut on_disk: Vec<String> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("cannot list {}: {e}", dir.display()))
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| {
std::path::Path::new(n)
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("md"))
})
.collect();
on_disk.sort();
let mut classified: Vec<String> = contract_docs()
.iter()
.map(|d| d.doc_file.to_string())
.collect();
classified.sort();
let unclassified: Vec<_> = on_disk.iter().filter(|f| !classified.contains(f)).collect();
assert!(
unclassified.is_empty(),
"unclassified contract docs (add them to core::contracts::contract_docs() \
with a frozen/stable/experimental status): {unclassified:?}"
);
let missing: Vec<_> = classified.iter().filter(|f| !on_disk.contains(f)).collect();
assert!(
missing.is_empty(),
"contract_docs() lists files that do not exist in docs/contracts/: {missing:?}"
);
}
#[test]
fn frozen_contract_docs_are_immutable() {
let dir = contracts_dir();
let current: BTreeMap<String, String> = contract_docs()
.iter()
.filter(|d| d.status == ContractStatus::Frozen)
.map(|d| (d.doc_file.to_string(), content_hash(&dir.join(d.doc_file))))
.collect();
assert!(!current.is_empty(), "freeze gate without frozen contracts");
let snap_path = snapshot_path();
if std::env::var_os("LEANCTX_UPDATE_FROZEN_HASHES").is_some() {
let json = serde_json::to_string_pretty(&current).expect("serialize snapshot");
std::fs::write(&snap_path, json + "\n").expect("write snapshot");
eprintln!("frozen-hashes.json regenerated — justify this in the PR");
return;
}
let snapshot: BTreeMap<String, String> =
serde_json::from_str(&std::fs::read_to_string(&snap_path).unwrap_or_else(|e| {
panic!(
"missing {} — generate it once via \
LEANCTX_UPDATE_FROZEN_HASHES=1 cargo test --test contracts_frozen ({e})",
snap_path.display()
)
}))
.expect("frozen-hashes.json is valid JSON");
for (file, hash) in &current {
match snapshot.get(file) {
None => panic!(
"{file} is frozen but missing from frozen-hashes.json — \
regenerate via LEANCTX_UPDATE_FROZEN_HASHES=1 cargo test --test contracts_frozen"
),
Some(expected) if expected != hash => panic!(
"FROZEN CONTRACT MODIFIED: docs/contracts/{file} changed.\n\
Frozen contracts are immutable (CONTRACTS.md § Contract file rule).\n\
→ semantic change: create the next version file (e.g. -v2.md) and classify it; \
leave the v1 file untouched.\n\
→ deliberate typo fix: LEANCTX_UPDATE_FROZEN_HASHES=1 cargo test --test \
contracts_frozen, and justify the edit in the PR."
),
Some(_) => {}
}
}
let stale: Vec<_> = snapshot
.keys()
.filter(|k| !current.contains_key(*k))
.collect();
assert!(
stale.is_empty(),
"frozen-hashes.json lists files that are no longer frozen/present: {stale:?} — regenerate the snapshot"
);
}