- Rust 99.1%
- Just 0.6%
- Prolog 0.3%
|
|
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| crates/patch-prolog-core | ||
| docs | ||
| examples | ||
| knowledge | ||
| src | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| justfile | ||
| README.md | ||
| renovate.json | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
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
- Write Prolog rules (
.plfiles) in theknowledge/directory cargo buildcompiles them into the binary viabuild.rs- 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
- docs/ARCHITECTURE.md — System structure, module responsibilities, data flows
- docs/TESTING.md — Test organization, running tests, writing tests
- docs/design/ — Design decisions and technical rationale
- ROADMAP.md — Completed phases and future work
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:
- Bumps
[workspace.package].versionand thepatch-prolog-coreinter-crate pins inCargo.tomlto match the tag. - Regenerates
Cargo.lock. - Commits the bump back to
mainaschore: bump version to X.Y.Z. - Publishes
patch-prolog-coreto crates.io, waits 120 s for the index to settle, then publishespatch-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 withwrite:repositoryscope (lets the workflow push the version-bump commit back tomain).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 asprlgpatch-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.