Metadata-Version: 2.4
Name: phocinae-rulepack
Version: 0.1.0
Summary: Deterministic rule-pack engine: read a ledger, apply a versioned rule pack, emit only the differences for human review. Zero model calls, zero third-party runtime dependencies.
Author: Phocinae
License: Apache-2.0
Project-URL: Homepage, https://github.com/Phocinae/phocinae-rulepack
Project-URL: Repository, https://github.com/Phocinae/phocinae-rulepack
Project-URL: Issues, https://github.com/Phocinae/phocinae-rulepack/issues
Keywords: rule-engine,deterministic,compliance,checklist,fail-closed,decision-model,phocinae,local-first
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: yamltest
Requires-Dist: PyYAML>=6.0; extra == "yamltest"
Dynamic: license-file

# phocinae-rulepack

**Read a ledger, apply one version of a rule pack, and emit only the differences
for a human to review.** Zero model calls. Zero third-party runtime dependencies.

<p>
  <img alt="python" src="https://img.shields.io/static/v1?label=python&message=%3E%3D3.9&color=3776AB&style=flat-square">
  <img alt="deps" src="https://img.shields.io/static/v1?label=runtime%20deps&message=0&color=success&style=flat-square">
  <img alt="model calls" src="https://img.shields.io/static/v1?label=model%20calls&message=0&color=blueviolet&style=flat-square">
  <img alt="License" src="https://img.shields.io/static/v1?label=License&message=Apache-2.0&color=blue&style=flat-square">
</p>

[中文](./README-zh.md)

---

## Why this exists, in one measurement

The same decision model, asked the same question two ways, scored like this on 53
labelled commands:

| how the question was asked | recall on destructive commands |
|---|---|
| "Should this be blocked?" (boolean) | **0.00** — it passed all 31 |
| allow / ask / deny (three-way) | 0.74 |
| harmless / risky / destructive (scale) | 0.97 |

And on a policy-check battery of 14 claims, the **rule kernel scored 14/14 with
zero model calls**, while the same model, asked to approve or reject the claims
directly, rejected **all 8** compliant ones.

The conclusion this package is built on: *"the rule kernel is worth shipping as a
deterministic plugin that uses no model at all."* This is that kernel.

If you want the measurement harness rather than the kernel, see
[`phocinae-formulate`](https://github.com/Phocinae/phocinae-formulate).

## Install

```sh
pip install phocinae-rulepack==0.1.0
```

Python 3.9 or newer. No dependencies. Nothing in this package opens a socket.

## Use

```sh
# validate a pack: schema, citation text, corpus hashes
phocinae-rulepack validate expense-policy

# run one document (JSON or YAML; '-' reads stdin)
phocinae-rulepack check expense-policy claim.json

# run a ledger and get one envelope
phocinae-rulepack check expense-policy ledger.json --format json

# what ships in the box
phocinae-rulepack list --packs-dir ./packs
```

Exit codes are the contract:

| code | meaning |
|---|---|
| `0` | every result is `pass` |
| `1` | at least one `fail` |
| `2` | no `fail`, but at least one `review` (grey zone → a human decides) |
| `3` | usage or configuration error, **or an empty scan** |

Priority is fixed and total: `3 > 1 > 2 > 0`.

## The three-state result

```json
{"rulepack":"expense-policy","verdict":"fail","model_calls":0,
 "reasons":[{"rule":"r2_meal_cap","verdict":"fail","reason_code":"OVER_CATEGORY_CAP",
             "cite":["c2","c4"],"evidence":"matched:r2_meal_cap"}],
 "unmatched_paths":["id"]}
```

* **`review` means a human decides.** It is not "look again later".
* **`pass` requires a rule that says pass.** A document no rule matched is
  `review` (`NO_RULE_MATCHED`) — nothing falls through to a silent allow.
* **A missing field or a wrongly-typed field never becomes `pass`.** Missing
  fields are `review`; they are never treated as compliance.
* **A real violation outranks a missing field.** If one rule finds a breach and
  another cannot be evaluated, the verdict is `fail` and both are reported.
* **`model_calls` is a real counter, and it is always `0`.**

## The evidence model is the point

A rule is only as good as the text behind it, so a citation must say where its
text came from and whether anyone has actually read the instrument:

| `evidence_level` | meaning | can it `fail` something? |
|---|---|---|
| `verbatim` | the quote appears character-for-character in the corpus | yes |
| `derived` | an authority's restatement, not the instrument's own words | yes, with a warning |
| `unavailable` | the instrument text was **not obtained** | **no — the verdict is capped to `review`** |

That cap is the difference between an honest tool and a fake gate. A rule whose
basis nobody has read must not be able to reject a customer's paperwork. The cap
is verified in both directions by the test suite: flip the citation to `derived`
and the same input must move from `review` to `fail`.

`validate` resolves every quote against the corpus and recomputes every
sha256. A one-character change to a quote, or a wrong hash, fails validation.
`--strict` additionally rejects sample corpus and `unavailable` citations.

## Writing a pack

```yaml
schema: phocinae-rulepack/v1
id: my-check
title: What this checks
version: 0.1.0
missing_field_policy: review        # only 'review' is accepted in v1
corpus:
  - path: sources/spec.txt
    role: authority                  # authority | production | sample
    sha256: <64 uppercase hex>
citations:
  - id: c1
    source: Where the text is from
    locator: clause 4.2
    quote: "the exact words, character for character"
    document: sources/spec.txt
    evidence_level: verbatim
rules:
  - id: r1_limit
    when:
      all_of:
        - {field: amount, op: ">", value: 500}
        - {field: receipt, op: "==", value: false}
    then: {verdict: fail, reason_code: OVER_LIMIT, cite: [c1]}
    require_fields: [amount, receipt]
```

The expression language is deliberately small: `field OP value` combined with
`all_of` / `any_of` / `not`, and a `value` may be `{"ref": "other.field"}` to
compare two fields. Operators: `== != < <= > >= in not_in matches exists missing`.

There is **no** arithmetic, no custom function, no loop, and no network access.
If you need a computed value, compute it in the adapter that builds the input
document — that keeps the judgement criterion in the pack, where it can be cited
and tested.

### The two YAML traps worth knowing

Both of these parse into something plausible and wrong, which is why the loader
raises instead of guessing:

```yaml
op: >            # WRONG: a folded block scalar. It swallows the lines below.
op: ">"          # right: quote operators

note: text with a ": " colon inside    # WRONG: YAML wants a new key there
note: "text with a \": \" colon inside"  # right: quote it, or use a block scalar
```

## What ships

| pack | scenario | rules |
|---|---|---|
| `expense-policy` | S15 (the expense pre-approval demo) | 8 |

> Naming note: "S15" here is the gallery's S15 expense demo. In the 182-scenario
> inventory S15 is something else (multilingual low-resource expansion).

## Testing

```sh
python tests/run_all.py
```

No network, no model, no third-party packages. The suite exits non-zero when
zero assertions ran — *"a check with nothing to check must not report success"*
is the same rule the engine applies to an empty scan.

Tests never use `tempfile`: scratch goes to `$PHOCINAE_RULEPACK_TMP`, or to a
directory beside the repository.

## What this is not

It is not a compliance, legal, medical or financial conclusion. It reports
differences for a human to review. It is not a model, and it does not call one.

## License

Apache-2.0.
