Skip to content

Commit d58ad52

Browse files
author
Viktor Sovietov
committed
Language contract and CI tightened per external review: E1009/E1011/E1012, mandatory oracle CI, proof budgets, CLI split
1 parent ff017ca commit d58ad52

26 files changed

Lines changed: 1314 additions & 527 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,34 @@ jobs:
2222
- run: cargo build --workspace --all-targets
2323
- run: cargo test --workspace
2424

25-
# INFRA-11: Soufflé is required only by the differential (Bool) jobs.
26-
# Non-Soufflé exit harnesses (INFRA-6/7/8) run in `build` above.
27-
# This job is wired here as a stub so the dependency is documented from day one.
28-
souffle-diff:
25+
# INFRA-11: the external differential oracles are MANDATORY here — a missing
26+
# binary fails the job (STRATA_REQUIRE_ORACLES), it does not silently skip.
27+
# `build` above stays oracle-free so the core loop is fast and hermetic;
28+
# skip-if-absent remains a local-development courtesy only.
29+
oracle-diff:
2930
runs-on: ubuntu-latest
30-
if: false # TODO(INFRA-11): flip on once souffle is pinned/installed
31+
env:
32+
STRATA_REQUIRE_ORACLES: 1
3133
steps:
3234
- uses: actions/checkout@v4
33-
- run: echo "souffle differential stub — see crates/strata-cli/tests/souffle_diff.rs"
35+
- uses: dtolnay/rust-toolchain@stable
36+
- uses: Swatinem/rust-cache@v2
37+
- name: Install clingo/gringo/clasp (Ubuntu; the clingo binary ships in the gringo package)
38+
run: |
39+
set -euo pipefail
40+
sudo apt-get update
41+
sudo apt-get install -y gringo clasp
42+
clingo --version
43+
gringo --version
44+
clasp --version || true # clasp --version exits nonzero on some builds
45+
- name: Install Soufflé 2.5 (pinned release .deb)
46+
run: |
47+
set -euo pipefail
48+
curl -fsSL -o /tmp/souffle.deb \
49+
https://github.com/souffle-lang/souffle/releases/download/2.5/x86_64-ubuntu-2404-souffle-2.5-Linux.deb
50+
sudo apt-get install -y /tmp/souffle.deb
51+
souffle --version
52+
- name: Soufflé differential (Bool engine vs souffle, incl. fuzz)
53+
run: cargo test -p strata-cli --test souffle_diff
54+
- name: clingo/clasp differentials (ASP stack)
55+
run: cargo test -p strata-asp

.github/workflows/gpu.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: gpu
2+
3+
# The GPU differentials need CUDA hardware, which hosted runners don't have.
4+
# This workflow is manual (workflow_dispatch) and targets a self-hosted runner
5+
# labeled `gpu`; it exists so the GPU validation has a first-class, repeatable
6+
# entry point instead of living only in a shell history. Until such a runner is
7+
# registered, dispatching it simply queues — the honest state of affairs.
8+
on:
9+
workflow_dispatch:
10+
11+
env:
12+
CARGO_TERM_COLOR: always
13+
14+
jobs:
15+
cuda-diff:
16+
runs-on: [self-hosted, gpu]
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: dtolnay/rust-toolchain@stable
20+
- name: GPU kernels vs CPU oracles (bit-exact)
21+
run: cargo test -p strata-gpu --features cuda --release
22+
- name: Record the environment the numbers came from
23+
run: |
24+
nvidia-smi
25+
rustc --version

ARCHITECTURE.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ The reference interpreter is the oracle. Three overlapping checks defend it:
111111
2. **our Bool engine == Soufflé**, a differential harness that translates Core-IR
112112
to a Soufflé `.dl` program and compares results
113113
(`cargo test -p strata-cli --test souffle_diff`, needs `souffle`; skips
114-
cleanly if absent). `Trop` is checked against an independent shortest-path
115-
oracle instead.
114+
cleanly if absent locally — the `oracle-diff` CI job makes it mandatory via
115+
`STRATA_REQUIRE_ORACLES=1`). `Trop` is checked against an independent
116+
shortest-path oracle instead.
116117
3. The probabilistic and ASP engines are themselves the slow, obviously-correct
117118
references (exact enumeration; reduct) that future fast/compiled/GPU methods
118119
must reproduce bit-for-bit.
@@ -131,7 +132,12 @@ validated bit-for-bit against a reference oracle:
131132
planner (cost-based ordering, hypertree decomposition, tensor-contraction
132133
width); radix/hypercube partition groundwork for multi-GPU; and the ASP
133134
grounding-simplification pass. Every kernel is diffed against an independent
134-
CPU oracle.
135+
CPU oracle. **Reproducibility caveat, stated plainly:** hosted CI has no
136+
CUDA hardware, so the GPU differentials do not run under the badge — they
137+
run on a CUDA machine via `cargo test -p strata-gpu --features cuda` (the
138+
manual `gpu` workflow dispatches exactly that on a self-hosted runner).
139+
On CPU-only checkouts the crate builds as a stub that returns
140+
`GpuError::NotBuilt`.
135141
- **`strata-prob`** — knowledge compilation for режим B: provenance circuits
136142
(decomposable-AND / deterministic-OR), exact weighted model counting,
137143
reverse-mode gradients, top-k proofs, and a compilation cache — demonstrated

CONTRIBUTING.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ The differential fuzzer's program count is tunable:
3939
STRATA_SOUFFLE_FUZZ_N=10000 cargo test -p strata-cli --test souffle_diff fuzz_bool_vs_souffle
4040
```
4141

42-
If `souffle` is not on `PATH`, the Soufflé jobs skip cleanly rather than fail.
42+
If `souffle` is not on `PATH`, the Soufflé jobs skip cleanly rather than fail —
43+
a local-development courtesy only. In CI the `oracle-diff` job installs Soufflé
44+
pinned to a release .deb and clingo/gringo/clasp from the distro (versions
45+
logged in the job output) and sets `STRATA_REQUIRE_ORACLES=1`, which turns a
46+
missing oracle into a hard failure: a green badge means the external
47+
differentials actually ran.
4348

4449
## The bar for a change (what CI enforces)
4550

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ phases (see [ARCHITECTURE.md](ARCHITECTURE.md)).
2323
> (stable model) solver for `@asp` modules. Cross-checked against
2424
> [Soufflé](https://souffle-lang.github.io/) and fuzzed (10k random programs,
2525
> naive vs semi-naive and vs Soufflé). **The whole shipped grammar executes**;
26-
> the GPU backend runs beside this stack, validated bit-for-bit against it.
26+
> the GPU backend runs beside this stack, validated bit-for-bit against it on
27+
> CUDA hardware (`--features cuda`; hosted CI runs the CPU stub — see
28+
> [Beyond the CPU pipeline](ARCHITECTURE.md#beyond-the-cpu-pipeline)).
2729
2830
## Quick start
2931

@@ -156,7 +158,10 @@ strata-cli the `strata` binary
156158
`strata-front` and `strata-check` are siblings that both depend only on
157159
`strata-ir`; the engine crates (`strata-gpu`, `strata-terms`, `strata-prob`)
158160
sit beside `strata-eval` and are validated bit-for-bit against the reference
159-
stack. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full picture and
161+
stack — for `strata-gpu` that validation needs CUDA hardware
162+
(`cargo test -p strata-gpu --features cuda`; hosted CI runs the CPU stub, so
163+
the badge does not cover the GPU differentials). See
164+
[ARCHITECTURE.md](ARCHITECTURE.md) for the full picture and
160165
[CONTRIBUTING.md](CONTRIBUTING.md) to build and test.
161166

162167
## Correctness

crates/strata-asp/src/clasp.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ mod tests {
8282
fn assert_clasp_agrees(rules: &[Rule], facts: &[(String, Vec<Val>)], cons: &[Vec<Literal>]) {
8383
let g = ground(rules, facts, cons).unwrap();
8484
let Some(clasp) = solve_with("clasp", &g) else {
85+
if std::env::var_os("STRATA_REQUIRE_ORACLES").is_some() {
86+
panic!("clasp not installed — but STRATA_REQUIRE_ORACLES is set");
87+
}
8588
eprintln!("skipping: clasp not installed");
8689
return;
8790
};

crates/strata-asp/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,12 @@ fn for_each_assignment(
250250
mut emit: impl FnMut(&HashMap<String, Val>),
251251
) -> Result<(), AspError> {
252252
let k = vars.len();
253+
// An empty universe grounds a rule with variables to nothing at all —
254+
// zero assignments, not an index panic (the `strata check`-ok /
255+
// `strata run`-panic hole an external review's follow-up found).
256+
if k > 0 && universe.is_empty() {
257+
return Ok(());
258+
}
253259
let u = universe.len().max(1);
254260
// universe^k, bounded.
255261
let total = (0..k)
@@ -454,6 +460,20 @@ fn least_model(n_atoms: usize, rules: &[(usize, &[usize])]) -> HashSet<usize> {
454460
#[cfg(test)]
455461
mod tests {
456462
use super::*;
463+
464+
#[test]
465+
fn empty_universe_grounds_a_var_rule_to_nothing() {
466+
// No constants anywhere: a rule with variables has zero instantiations
467+
// (never an index panic), and the empty model is the one stable model.
468+
use strata_ir::high::program::{atom, var, Literal, Rule};
469+
let rules = vec![Rule {
470+
head: atom("p", vec![var("X")]),
471+
body: vec![Literal::Neg(atom("q", vec![var("X")]))],
472+
}];
473+
let models = solve(&rules, &[], &[]).expect("solve");
474+
assert_eq!(models.len(), 1);
475+
assert!(models[0].is_empty(), "the empty model is stable");
476+
}
457477
use strata_ir::high::program::{atom, var, Rule};
458478

459479
fn a(pred: &str) -> Atom {

crates/strata-asp/tests/ablation.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ fn ablate(name: &str, graphs: &[Graph], effort: impl Fn(u64, u64) -> u64) {
156156
eprintln!("ablation [{name}] effort — baseline {base}, GNN {gnn}, anti {anti}");
157157
}
158158

159+
/// Under `STRATA_REQUIRE_ORACLES` (the oracle CI job), a missing external
160+
/// oracle is a hard failure — the differential must actually run. Locally,
161+
/// absence skips cleanly (INFRA-11).
162+
fn skip_or_die(what: &str) {
163+
if std::env::var_os("STRATA_REQUIRE_ORACLES").is_some() {
164+
panic!("{what} — but STRATA_REQUIRE_ORACLES is set, the oracle differential must run");
165+
}
166+
eprintln!("skipping: {what}");
167+
}
168+
159169
#[test]
160170
fn gnn_ablation_two_regimes() {
161171
let mut rng = Rng(0x5EED_5EED);
@@ -167,7 +177,7 @@ fn gnn_ablation_two_regimes() {
167177
colors: 3,
168178
};
169179
if run(&program(&probe, Guide::None), false).is_none() {
170-
eprintln!("skipping ablation: clingo not installed");
180+
skip_or_die("ablation: clingo not installed");
171181
return;
172182
}
173183

crates/strata-asp/tests/clingo_diff.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ fn fact2(pred: &str, a: i64, b: i64) -> GroundAtom {
113113
(pred.to_string(), vec![Val::Int(a), Val::Int(b)])
114114
}
115115

116+
/// Under `STRATA_REQUIRE_ORACLES` (the oracle CI job), a missing external
117+
/// oracle is a hard failure — the differential must actually run. Locally,
118+
/// absence skips cleanly (INFRA-11).
119+
fn skip_or_die(what: &str) {
120+
if std::env::var_os("STRATA_REQUIRE_ORACLES").is_some() {
121+
panic!("{what} — but STRATA_REQUIRE_ORACLES is set, the oracle differential must run");
122+
}
123+
eprintln!("skipping: {what}");
124+
}
125+
116126
/// Assert our reference answer sets equal clingo's, projected to `show_pred`.
117127
/// Skips if clingo is unavailable.
118128
fn assert_agrees(
@@ -124,11 +134,11 @@ fn assert_agrees(
124134
show_pred: &str,
125135
) {
126136
let Some(c_models) = clingo_models(lp) else {
127-
eprintln!("skipping {name}: clingo not installed");
137+
skip_or_die(&format!("{name}: clingo not installed"));
128138
return;
129139
};
130140
let Some(ours) = our_models(rules, facts, cons, show_pred) else {
131-
eprintln!("skipping {name}: clasp not installed");
141+
skip_or_die(&format!("{name}: clasp not installed"));
132142
return;
133143
};
134144
assert_eq!(

crates/strata-check/src/diagnostics.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,24 @@ pub mod codes {
2929
/// exact probabilistic provenance through recursion. Carries the nearest
3030
/// allowed alternative in its message (D9/I4).
3131
pub const TABLE_2_4_FORBIDDEN: DiagCode = DiagCode(1008);
32+
/// A fact's `::` annotation does not fit the predicate's declared
33+
/// annotation: an integer weight belongs on `Trop` only, a probability on
34+
/// `Bool`/`Prov`/`Prov_k` only and inside [0, 1], and a `Trop` fact must
35+
/// carry a weight.
36+
pub const FACT_ANNOTATION_MISMATCH: DiagCode = DiagCode(1009);
3237
/// A fact on a `neural` predicate is not probabilistic: a neural predicate's
3338
/// ground atoms are a model's soft outputs, so they must carry a probability
3439
/// (`p :: n(...)`), never be asserted as certain.
3540
pub const NEURAL_FACT_NOT_SOFT: DiagCode = DiagCode(1010);
41+
/// A construct that has no meaning under `@asp` (stable-model semantics):
42+
/// `::` fact annotations, `input` declarations, queries, `neural`
43+
/// predicates, compound fact arguments. Refused by name, never silently
44+
/// dropped.
45+
pub const ASP_UNSUPPORTED: DiagCode = DiagCode(1011);
46+
/// A predicate is declared more than once with a conflicting signature —
47+
/// silently letting the last declaration win would make every other check
48+
/// order-dependent.
49+
pub const CONFLICTING_DECLARATION: DiagCode = DiagCode(1012);
3650

3751
pub const ALL: &[(DiagCode, &str)] = &[
3852
(UNDECLARED_PRED, "check.undeclared-predicate"),
@@ -43,7 +57,10 @@ pub mod codes {
4357
(NOT_EXECUTABLE, "check.not-executable-annotation"),
4458
(SEMIRING_CONFLICT, "check.semiring-conflict"),
4559
(TABLE_2_4_FORBIDDEN, "check.table-2.4-forbidden"),
60+
(FACT_ANNOTATION_MISMATCH, "check.fact-annotation-mismatch"),
4661
(NEURAL_FACT_NOT_SOFT, "check.neural-fact-not-soft"),
62+
(ASP_UNSUPPORTED, "check.asp-unsupported"),
63+
(CONFLICTING_DECLARATION, "check.conflicting-declaration"),
4764
];
4865
}
4966

0 commit comments

Comments
 (0)