Skip to content

M0+M1+M2: workspace, engine skeleton, T0 catalogue + falsifier gate - #19

Merged
hyperpolymath merged 7 commits into
mainfrom
feat/pons-m0-m2
Sep 15, 2026
Merged

hyperpolymath merged 7 commits into
mainfrom
feat/pons-m0-m2

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Implements the ratified M0→M1→M2 milestones of docs/PLAN.adoc:

  • M0: virtual Cargo workspace (pons-core, pons-rules, pons-cli, pons-protocols), tree-sitter substrate smoke test across Python/JS/TS/TSX/Rust grammars, CI workflow, Justfile.
  • M1: Finding/RawFinding model (private fields, EvidenceClass derived from Tier, never trusted from wire), Rule/RuleCtx in pons-core::engine, human reporter with a colour-independent (heuristic — not a verdict) suffix for SPECULATIVE findings, pons scan CLI wiring. CLI exit-code contract recorded as docs/adr/0005-cli-exit-code-contract.adoc (exit 0 by default even with findings; opt-in --fail-on <severity>; exit 2 reserved for operational errors; SPECULATIVE findings never trigger exit 1).
  • M2: full T0 rule catalogue (8 rules — div-by-literal-zero, swallowed-error, self-assignment, constant-condition, while-true-no-break, empty-effect-loop, unreachable-after-jump, string-concat-in-loop), each with per-language tree-sitter queries and a positive/negative fixture corpus, plus the pons-rules/tests/falsifier.rs gate that proves every rule against its own corpus (and fails loudly on unresolvable fixture extensions or a language declared by a rule with no fixture in that language).

Note on branch history

This PR is opened from feat/pons-m0-m2 rather than the originally-planned feat/pons-m0-m1, because that local branch name had picked up an unrelated commit from a concurrent session (a workflow-campaign preservation commit whose own message says "NOT pushed"). feat/pons-m0-m2 is a clean cherry-pick of just the two pons commits (M1, M2) on top of the existing M0 commit — no .github/** changes, no overlap with that other work.

Test plan

  • cargo build --workspace
  • cargo test -p pons-core --test smoke
  • just falsify (cargo test -p pons-rules --test falsifier)
  • cargo test --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo fmt --check

M3 (JSON/SARIF reporters, T1 dataflow, T2 typestate, T3 speculative rules, suppression) is out of scope for this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P2r6GWM6CYLgsSRDU386b3

mistral-vibe and others added 6 commits September 11, 2026 20:35
Pin all uses: references to full 40-char commit SHAs to prevent
supply-chain attacks via mutable tags or branches. This resolves
Scorecard Pinned-Dependencies alerts and Hypatia WH004 findings.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Pin all uses: references to full 40-char commit SHAs to prevent
supply-chain attacks via mutable tags or branches. This resolves
Scorecard Pinned-Dependencies alerts and Hypatia WH004 findings.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Replace local copy with call to hyperpolymath/cicd-suite workflow
for single-source maintenance.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
Virtual Cargo workspace (pons-core/pons-rules/pons-cli/pons-protocols)
with the tree-sitter substrate smoke test (ADR-0001), plus M1's finding
model (finding.rs), extension-to-grammar table (lang.rs), and recursive
source discovery honouring .gitignore/.ponsignore (source.rs).

Cargo.lock is committed deliberately, deviating from estate .gitignore
norms: pons-cli builds a binary, so the lockfile is the correct Rust
convention here, not dead weight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2r6GWM6CYLgsSRDU386b3
Adds pons-core::engine (Rule/RuleCtx trait + orchestrating Engine),
parse.rs (tree-sitter parse per Lang), the suppress.rs M7 stub,
report::human (deterministic line output, with the fixed
"(heuristic — not a verdict)" suffix demoting SPECULATIVE-tier
findings, colour-independent since colour is lost in piped CI
output), pons-rules::RuleRegistry (empty until M2's T0 catalogue),
and real pons-cli wiring for `pons scan <path> [--fail-on <severity>]`.

CLI exit-code contract per plan decision: 0 by default even with
findings, 1 only via explicit --fail-on threshold, 2 for operational
errors (bad path, scan failure); SPECULATIVE findings never trigger
exit 1 regardless of threshold.

M1 exit gate verified: `pons scan <dir>` runs end-to-end with zero
rules registered, no panic, exit 0. cargo test --workspace: 17/17
passing (includes the SPECULATIVE-demotion unit test). clippy -D
warnings and fmt --check both clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2r6GWM6CYLgsSRDU386b3
Adds the full T0 rule catalogue per docs/PLAN.adoc's M2 build order:
div-by-literal-zero, swallowed-error, self-assignment, constant-condition,
while-true-no-break, empty-effect-loop, unreachable-after-jump, and
string-concat-in-loop (no Rust variant — String += &str is amortised O(1)
there). Each rule ships tree-sitter .scm queries per language, a Rust
implementation with unit tests, and a fixtures/<rule-id>/{positive,negative}
corpus proven by the new pons-rules/tests/falsifier.rs gate (`just falsify`),
which also fails loudly on unresolvable fixture extensions and on any
declared language missing its own positive/negative fixture.

string-concat-in-loop's string-typed-RHS narrowing (needed so `count += 1`
never matches) fires when either the appended value itself contains a
string literal, or the accumulator was declared with a string initialiser
earlier in its enclosing scope — the latter is what makes the common
"append each loop item onto an accumulator string" shape detectable even
when the appended item is a plain identifier.

Also lands docs/adr/0005-cli-exit-code-contract.adoc (implemented in
pons-cli since the M1→M2 transition) and the Finding/RawFinding hardening
that preceded M2's rule files: private Finding fields with a wire-shadow
Deserialize that re-derives evidence from tier, and Rule::check returning
RawFinding (no rule_id) so only Engine::scan can stamp rule_id from
Rule::id().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P2r6GWM6CYLgsSRDU386b3
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 161 files, which is 61 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 2cfe1188-8e37-409f-9ba7-7a08454b35db

📥 Commits

Reviewing files that changed from the base of the PR and between 9d9c67f and a466929.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (161)
  • .github/workflows/main-estate-audit.yml
  • .github/workflows/rust-ci.yml
  • .github/workflows/scorecard.yml
  • .github/workflows/secret-scanner.yml
  • .gitignore
  • Cargo.toml
  • Justfile
  • crates/pons-cli/Cargo.toml
  • crates/pons-cli/src/main.rs
  • crates/pons-core/Cargo.toml
  • crates/pons-core/src/engine.rs
  • crates/pons-core/src/finding.rs
  • crates/pons-core/src/lang.rs
  • crates/pons-core/src/lib.rs
  • crates/pons-core/src/parse.rs
  • crates/pons-core/src/report/human.rs
  • crates/pons-core/src/report/mod.rs
  • crates/pons-core/src/source.rs
  • crates/pons-core/src/suppress.rs
  • crates/pons-core/tests/smoke.rs
  • crates/pons-protocols/Cargo.toml
  • crates/pons-protocols/src/lib.rs
  • crates/pons-rules/Cargo.toml
  • crates/pons-rules/rules/constant-condition/javascript.scm
  • crates/pons-rules/rules/constant-condition/python.scm
  • crates/pons-rules/rules/constant-condition/rust.scm
  • crates/pons-rules/rules/constant-condition/tsx.scm
  • crates/pons-rules/rules/constant-condition/typescript.scm
  • crates/pons-rules/rules/div-by-literal-zero/javascript.scm
  • crates/pons-rules/rules/div-by-literal-zero/python.scm
  • crates/pons-rules/rules/div-by-literal-zero/rust.scm
  • crates/pons-rules/rules/div-by-literal-zero/tsx.scm
  • crates/pons-rules/rules/div-by-literal-zero/typescript.scm
  • crates/pons-rules/rules/empty-effect-loop/javascript.scm
  • crates/pons-rules/rules/empty-effect-loop/python.scm
  • crates/pons-rules/rules/empty-effect-loop/rust.scm
  • crates/pons-rules/rules/empty-effect-loop/tsx.scm
  • crates/pons-rules/rules/empty-effect-loop/typescript.scm
  • crates/pons-rules/rules/self-assignment/javascript.scm
  • crates/pons-rules/rules/self-assignment/python.scm
  • crates/pons-rules/rules/self-assignment/rust.scm
  • crates/pons-rules/rules/self-assignment/tsx.scm
  • crates/pons-rules/rules/self-assignment/typescript.scm
  • crates/pons-rules/rules/string-concat-in-loop/javascript.scm
  • crates/pons-rules/rules/string-concat-in-loop/python.scm
  • crates/pons-rules/rules/string-concat-in-loop/tsx.scm
  • crates/pons-rules/rules/string-concat-in-loop/typescript.scm
  • crates/pons-rules/rules/swallowed-error/javascript.scm
  • crates/pons-rules/rules/swallowed-error/python.scm
  • crates/pons-rules/rules/swallowed-error/tsx.scm
  • crates/pons-rules/rules/swallowed-error/typescript.scm
  • crates/pons-rules/rules/unreachable-after-jump/javascript.scm
  • crates/pons-rules/rules/unreachable-after-jump/python.scm
  • crates/pons-rules/rules/unreachable-after-jump/rust.scm
  • crates/pons-rules/rules/unreachable-after-jump/tsx.scm
  • crates/pons-rules/rules/unreachable-after-jump/typescript.scm
  • crates/pons-rules/rules/while-true-no-break/javascript.scm
  • crates/pons-rules/rules/while-true-no-break/python.scm
  • crates/pons-rules/rules/while-true-no-break/rust.scm
  • crates/pons-rules/rules/while-true-no-break/tsx.scm
  • crates/pons-rules/rules/while-true-no-break/typescript.scm
  • crates/pons-rules/src/lib.rs
  • crates/pons-rules/src/registry.rs
  • crates/pons-rules/src/t0/constant_condition.rs
  • crates/pons-rules/src/t0/div_by_literal_zero.rs
  • crates/pons-rules/src/t0/empty_effect_loop.rs
  • crates/pons-rules/src/t0/mod.rs
  • crates/pons-rules/src/t0/self_assignment.rs
  • crates/pons-rules/src/t0/string_concat_in_loop.rs
  • crates/pons-rules/src/t0/swallowed_error.rs
  • crates/pons-rules/src/t0/unreachable_after_jump.rs
  • crates/pons-rules/src/t0/while_true_no_break.rs
  • crates/pons-rules/tests/falsifier.rs
  • docs/PLAN.adoc
  • docs/adr/0005-cli-exit-code-contract.adoc
  • fixtures/constant-condition/README.adoc
  • fixtures/constant-condition/negative/basic.js
  • fixtures/constant-condition/negative/basic.py
  • fixtures/constant-condition/negative/basic.rs
  • fixtures/constant-condition/negative/basic.ts
  • fixtures/constant-condition/negative/basic.tsx
  • fixtures/constant-condition/positive/basic.js
  • fixtures/constant-condition/positive/basic.py
  • fixtures/constant-condition/positive/basic.rs
  • fixtures/constant-condition/positive/basic.ts
  • fixtures/constant-condition/positive/basic.tsx
  • fixtures/div-by-literal-zero/README.adoc
  • fixtures/div-by-literal-zero/negative/basic.js
  • fixtures/div-by-literal-zero/negative/basic.py
  • fixtures/div-by-literal-zero/negative/basic.rs
  • fixtures/div-by-literal-zero/negative/basic.ts
  • fixtures/div-by-literal-zero/negative/basic.tsx
  • fixtures/div-by-literal-zero/negative/string_format.py
  • fixtures/div-by-literal-zero/positive/basic.js
  • fixtures/div-by-literal-zero/positive/basic.py
  • fixtures/div-by-literal-zero/positive/basic.rs
  • fixtures/div-by-literal-zero/positive/basic.ts
  • fixtures/div-by-literal-zero/positive/basic.tsx
  • fixtures/div-by-literal-zero/positive/modulo.py
  • fixtures/empty-effect-loop/README.adoc
  • fixtures/empty-effect-loop/negative/basic.js
  • fixtures/empty-effect-loop/negative/basic.py
  • fixtures/empty-effect-loop/negative/basic.rs
  • fixtures/empty-effect-loop/negative/basic.ts
  • fixtures/empty-effect-loop/negative/basic.tsx
  • fixtures/empty-effect-loop/positive/basic.js
  • fixtures/empty-effect-loop/positive/basic.py
  • fixtures/empty-effect-loop/positive/basic.rs
  • fixtures/empty-effect-loop/positive/basic.ts
  • fixtures/empty-effect-loop/positive/basic.tsx
  • fixtures/self-assignment/README.adoc
  • fixtures/self-assignment/negative/basic.js
  • fixtures/self-assignment/negative/basic.py
  • fixtures/self-assignment/negative/basic.rs
  • fixtures/self-assignment/negative/basic.ts
  • fixtures/self-assignment/negative/basic.tsx
  • fixtures/self-assignment/positive/basic.js
  • fixtures/self-assignment/positive/basic.py
  • fixtures/self-assignment/positive/basic.rs
  • fixtures/self-assignment/positive/basic.ts
  • fixtures/self-assignment/positive/basic.tsx
  • fixtures/string-concat-in-loop/README.adoc
  • fixtures/string-concat-in-loop/negative/basic.js
  • fixtures/string-concat-in-loop/negative/basic.py
  • fixtures/string-concat-in-loop/negative/basic.ts
  • fixtures/string-concat-in-loop/negative/basic.tsx
  • fixtures/string-concat-in-loop/positive/basic.js
  • fixtures/string-concat-in-loop/positive/basic.py
  • fixtures/string-concat-in-loop/positive/basic.ts
  • fixtures/string-concat-in-loop/positive/basic.tsx
  • fixtures/swallowed-error/README.adoc
  • fixtures/swallowed-error/negative/basic.js
  • fixtures/swallowed-error/negative/basic.py
  • fixtures/swallowed-error/negative/basic.ts
  • fixtures/swallowed-error/negative/basic.tsx
  • fixtures/swallowed-error/positive/basic.js
  • fixtures/swallowed-error/positive/basic.py
  • fixtures/swallowed-error/positive/basic.ts
  • fixtures/swallowed-error/positive/basic.tsx
  • fixtures/unreachable-after-jump/README.adoc
  • fixtures/unreachable-after-jump/negative/basic.js
  • fixtures/unreachable-after-jump/negative/basic.py
  • fixtures/unreachable-after-jump/negative/basic.rs
  • fixtures/unreachable-after-jump/negative/basic.ts
  • fixtures/unreachable-after-jump/negative/basic.tsx
  • fixtures/unreachable-after-jump/positive/basic.js
  • fixtures/unreachable-after-jump/positive/basic.py
  • fixtures/unreachable-after-jump/positive/basic.rs
  • fixtures/unreachable-after-jump/positive/basic.ts
  • fixtures/unreachable-after-jump/positive/basic.tsx
  • fixtures/while-true-no-break/README.adoc
  • fixtures/while-true-no-break/negative/basic.js
  • fixtures/while-true-no-break/negative/basic.py
  • fixtures/while-true-no-break/negative/basic.rs
  • fixtures/while-true-no-break/negative/basic.ts
  • fixtures/while-true-no-break/negative/basic.tsx
  • fixtures/while-true-no-break/positive/basic.js
  • fixtures/while-true-no-break/positive/basic.py
  • fixtures/while-true-no-break/positive/basic.rs
  • fixtures/while-true-no-break/positive/basic.ts
  • fixtures/while-true-no-break/positive/basic.tsx

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)
E Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@hyperpolymath
hyperpolymath merged commit ed2a3c4 into main Sep 15, 2026
6 of 9 checks passed
@hyperpolymath
hyperpolymath deleted the feat/pons-m0-m2 branch September 15, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants