No description
  • Rust 99.1%
  • Just 0.6%
  • Prolog 0.3%
Find a file
2026-06-02 22:58:37 +00:00
.claude init 2026-02-28 19:03:59 -08:00
.forgejo/workflows End-to-end works: prlg standalone runs; loglings finds it on PATH; an exercise solved with the canonical 2026-06-02 11:36:16 -07:00
crates/patch-prolog-core ● Issue #30 is fixed. 2026-06-02 15:57:14 -07:00
docs ● -v properly exits 2 (clap's argument error). All 363 tests still pass. 2026-06-02 11:44:38 -07:00
examples init 2026-02-28 19:03:59 -08:00
knowledge error handling 2026-05-21 10:28:57 -07:00
src ● -v properly exits 2 (clap's argument error). All 363 tests still pass. 2026-06-02 11:44:38 -07:00
tests ● Issue #30 is fixed. 2026-06-02 15:57:14 -07:00
.gitignore k 2026-03-06 05:43:07 -08:00
Cargo.lock chore: bump version to 0.4.1 2026-06-02 18:53:10 +00:00
Cargo.toml chore: bump version to 0.4.1 2026-06-02 18:53:10 +00:00
justfile End-to-end works: prlg standalone runs; loglings finds it on PATH; an exercise solved with the canonical 2026-06-02 11:36:16 -07:00
README.md End-to-end works: prlg standalone runs; loglings finds it on PATH; an exercise solved with the canonical 2026-06-02 11:36:16 -07:00
renovate.json renovate 2026-05-19 16:06:56 -07:00
ROADMAP.md error handling 2026-05-21 10:28:57 -07:00
rust-toolchain.toml ● CI setup complete. Summary: 2026-05-21 10:40:36 -07:00

patch-prolog

A Prolog compiler for linting generative AI output. You write rules in standard Prolog, compile them into a self-contained Rust binary, and query that binary at runtime — no interpreter, no file loading, no runtime dependencies.

How It Works

  1. Write Prolog rules (.pl files) in the knowledge/ directory
  2. cargo build compiles them into the binary via build.rs
  3. Run the binary with a query — it resolves against the embedded knowledge base

The rules are baked in. The binary is the program.

Example: Lint an AI-Generated Schema

The examples/linting.pl file defines rules for checking AI-generated API schemas:

% Flag sensitive fields that should not be exposed
violation(Field, sensitive_field) :-
    field(user, Field, _),
    sensitive(Field).

sensitive(ssn).
sensitive(password).

To compile and run it:

# Copy rules into the knowledge base
cp examples/linting.pl knowledge/

# Compile — rules are baked into the binary
cargo build --release

# Query for violations
./target/release/prlg --query "violation(Field, Reason)"
# → {"solutions":[{"Field":"ssn","Reason":"sensitive_field"},{"Field":"password","Reason":"sensitive_field"}],"count":2,"exhausted":true}

# Exit code 1 = violations found
echo $?
# → 1
# Text output
./target/release/prlg --query "violation(Field, Reason)" --format text
# Field = ssn
# Reason = sensitive_field
# Field = password
# Reason = sensitive_field

Example: Family Relationships

cp examples/family.pl knowledge/
cargo build --release

./target/release/prlg --query "grandparent(tom, X)" --format text
# X = bob
# X = carol

Exit Codes

Code Meaning
0 No solutions (compliant)
1 Solutions found (violations)
2 Parse error
3 Runtime error

Writing Rules

Place .pl files in knowledge/. They are compiled into the binary on cargo build — the binary has no runtime file dependencies.

The standard library (knowledge/stdlib.pl) is always included and provides: member/2, append/3, length/2, last/2, reverse/2, nth0/3, nth1/3.

Built-in Predicates

~60 built-in predicates covering core operations, type checking, control flow, arithmetic, I/O, term ordering, introspection, sorting, number conversion, and ISO exception handling (catch/3, throw/1). See docs/ARCHITECTURE.md for details.

Error Handling

The engine implements the ISO Prolog error-term taxonomy. Built-in errors are catchable with catch/3:

% Trap a runtime type error and degrade gracefully
consistent(X) :- catch(check(X), _, fail).

Calling an undefined predicate raises existence_error(procedure, F/A). For the linter use case ("missing data = compliant"), declare the predicate dynamic so missing clauses fail silently instead:

:- dynamic(field/1).
violation(F) :- field(F), \+ allowed(F).

The step-limit ceiling (resource_error(steps)) is intentionally uncatchable — catch/3 cannot trap it, so a malicious rule can't loop indefinitely by catching its own timeouts.

Documentation

Development

just ci             # Run the full CI suite (fmt-check, clippy, tests, build)
just test           # Run all 320 tests (147 unit + 173 integration)
just install        # Install the `prlg` binary to ~/.cargo/bin
cargo run -- examples/family.pl -o family    # Compile example
./family --query "grandparent(tom, X)" --format text

CI

CI runs on Forgejo Actions (.forgejo/workflows/ci-linux.yml) on every PR to main. The workflow calls just ci — the same command developers run locally — so there's no drift between local checks and CI. The justfile is the single source of truth for build/test/lint operations.

CI checks: code formatting (cargo fmt --check), clippy with -D warnings (warnings are errors), all tests, and a release build. Local Rust version is pinned by rust-toolchain.toml; the Forgejo runner image (navicore-rust) ships the same version.

Releasing

Releases are cut by pushing a vX.Y.Z tag. The .forgejo/workflows/release.yml workflow then:

  1. Bumps [workspace.package].version and the patch-prolog-core inter-crate pins in Cargo.toml to match the tag.
  2. Regenerates Cargo.lock.
  3. Commits the bump back to main as chore: bump version to X.Y.Z.
  4. Publishes patch-prolog-core to crates.io, waits 120 s for the index to settle, then publishes patch-prolog.
git tag v0.3.0
git push origin v0.3.0

Required Forgejo repo secrets (Settings → Actions → Secrets and Variables):

  • PAT — Forgejo personal access token with write:repository scope (lets the workflow push the version-bump commit back to main).
  • CRATES_IO_TOKEN — API token from https://crates.io/settings/tokens.

The release runner is navicore-rust — the same image CI uses — so publishes happen on the byte-identical Rust 1.95.0 toolchain that CI tested with.

Architecture

Rust workspace with two crates:

  • patch-prolog — CLI binary crate (src/main.rs, build.rs); installs as prlg
  • patch-prolog-core — Engine library (crates/patch-prolog-core/) — tokenizer, parser, unifier, solver, built-ins

The engine compiles Prolog at build time via build.rs, serializes with bincode, and embeds the compiled database in the binary. At runtime, queries are parsed and resolved against the embedded knowledge base. See docs/ARCHITECTURE.md for details.