This document is the normative specification of the plasma-engine v0
policy format and evaluation semantics, as implemented in
plasma-engine/src/. It supersedes the OCaml-typed sketch in
policy-ast-v0.1.adoc (kept as design lineage).
Divergences from that sketch, all deliberate:
-
exhibit→ overlay — the mechanism (additive policy extension) is retained; the license-specific vocabulary is not. -
Rules carry an explicit severity (
error|warning|info, defaulterror). -
PMPL-specific conditions (
C_HasProvenanceManifest,C_HasExhibitReference) are dropped;spdx-license-isis added. -
S_Filegains a multi-subject formfile-pattern(a list of globs — theglobcrate has no brace expansion, so*/.{rs,toml}must be written as two patterns).
A policy is a TOML (authoring) or JSON (interchange) document deserialized
into the types in plasma-engine/src/ast.rs. Enums are internally tagged:
an object with a type field in kebab-case, e.g.
{ type = "file", path = "LICENSE*" }. This encoding — not the Rust types —
is the interchange contract.
schema_version = { major = 0, minor = 1 } # mandatory, checked first
id = "policy-id"
version = { major = 1, minor = 0 } # the policy's own version
[[rules]]
id = "unique-rule-id" # duplicate ids are a load error
modality = "obligation" # obligation | prohibition | permission
severity = "error" # optional; error | warning | info
subject = { type = "repo" }
resource = { type = "file", path = "LICENSE*" }
condition = { type = "true" } # optional; defaults to true
action = { type = "present" }
rationale = "why this rule exists" # optional
source = "provenance of the rule" # optional
[[overlays]] # optional
id = "overlay-id"
applies_to = []
[[overlays.effects]]
type = "add-rules"
# rules = [ ... same shape as above ... ]Subjects (type =):
repo · file {path} · file-pattern {patterns: [glob…]} ·
metadata {key} · release {tag} (reserved).
Resources: file {path} (path may be a glob) · header ·
manifest {path} · governance-field {key} · release (reserved).
Conditions: true · not {of} · all {of: […]} · any {of: […]} ·
repo-has-file {path} · has-spdx-header · spdx-license-is {expr} ·
governance-flag-set {key, value} · version-at-least {version} ·
file-matches-pattern {path, pattern} (reserved).
Actions: present · absent · valid · consistent-with {id} (reserved).
Modalities: obligation · prohibition · permission.
Every construct the v0 evaluator cannot give exact semantics to is rejected
at load, never mid-evaluation (plasma-engine/src/schema.rs):
-
schema_version≠ 0.1 →UnsupportedSchemaVersion -
duplicate rule ids — collision-free across the effective base ids (base minus overridden) plus every overlay-added id, so a same-id replacement of an overridden rule is legal
-
override-rulestargeting an id that names no base rule →OverrideUnknownRule -
a
file-matches-patternregex that does not compile →InvalidRegex(so evaluation never compiles-and-fails mid-run) -
reserved constructs: subject
release, resourcerelease, actionconsistent-with, overlay effectmodify-rules(diff semantics underspecified)
Consequence: any policy that loads, evaluates — evaluate is total.
Overlays extend a base policy per context. Two effects are evaluable:
-
add-rules— append rules to the effective set; their findings carrysource = "overlay:<id>"when the rule declares no source of its own. -
override-rules { ids }— remove base rules with those ids from the effective set. Each id must name a real base rule (checked at load). An overlay may pair an override with anadd-rulesreusing the same id to replace a base rule (e.g. relax a severity, or exempt a zone by overriding without a replacement).
modify-rules remains reserved. Overridden base rules are still fully
validated at load — evaluation skips them, but they must stay well-formed.
plasma-engine/src/facts.rs is the only impure module. All repository
knowledge flows through the FactSet; the evaluator never touches the
filesystem. Collection rules, pinned so an independent implementation can
reproduce identical facts:
-
Walk: every regular file under the root; an entry is skipped when its name starts with
.or is one oftarget,node_modules,vendor,_build,deps. Paths are recorded relative to the root,/-separated. -
SPDX headers: for files whose extension is in
plasma_parser::audit::header::AUDITABLE_EXTENSIONS, the rawSPDX-License-Identifier:value from the first 15 lines (MAX_HEADER_LINES), comment prefixes//,#,--,;;stripped. The raw string is stored; parsing is deferred to evaluation (still pure — a function of the stored string). -
Metadata: v0 collects
versionfrom the rootCargo.toml[package]table when present. -
Git:
is_repoandhead_refread directly from.git/HEAD— no subprocess, no clock. -
File contents (opt-in):
collect_opts(root, { contents: true })also reads each valid-UTF-8 file up toMAX_CONTENT_BYTES(1 MiB) intofile_contents. Binary and oversized files are simply absent. This layer is off by default —collectand a plainplasma factsomit it, andfile_contentsis skipped from serialized output when empty, so the default snapshot is byte-identical to before (an additive wire change).plasma check/fixturn it on automatically iff the policy usesfile-matches-pattern;plasma facts --with-contentsforces it. -
All collections are
BTreeMap/BTreeSet: iteration order, and therefore finding order, is deterministic. The fact set contains no timestamps.
evaluate(policy, facts) (plasma-engine/src/eval.rs):
-
Effective rules = (base rules minus any overridden by
override-rules) ++ overlayadd-rules(in document order). Findings from overlay rules carrysource = "overlay:<id>"when the rule declares no source of its own. The action planner shares this exact effective set, so a finding never resolves to an overridden rule. -
Each rule’s subject expands to concrete instances:
repo→ itself;file→ that path;file-pattern→ every collected file matching any pattern;metadata→ that key. -
The condition is evaluated as a total predicate of (facts, instance). If false, the rule is not applicable to that instance: no finding.
-
If applicable, the deontic matrix produces exactly one finding (violation or pass) per instance.
| Condition | Denotation |
|---|---|
|
∃ f ∈ files. glob(p, f) |
|
instance is a path f ∧ headers[f] = Some(_) |
|
header of instance parses ∧ parse(header) = parse(e); if either side fails to parse, exact-text comparison |
|
metadata[k] = v (absent key ⇒ false) |
|
numeric dotted comparison of metadata["version"] against v; absent or non-numeric ⇒ false |
|
regex |
|
strict boolean composition |
Glob semantics: glob::Pattern over /-separated relative paths, with one
extension: a */ prefix also matches zero directories (*/*.rs matches
main.rs). A malformed pattern matches nothing.
"exists" is resource existence for the instance; "valid" is structural
validity (for header: the SPDX expression parses; for everything else in
v0, validity coincides with existence).
| Modality | Action | Violation iff |
|---|---|---|
obligation |
present |
¬exists |
obligation |
absent |
exists |
obligation |
valid |
¬valid |
prohibition |
present |
exists |
prohibition |
absent |
¬exists |
prohibition |
valid |
valid |
permission |
any |
never (emits a pass finding) |
Satisfied rules emit pass findings (status = "pass"), retained in the
Evaluation and rendered with --verbose. This gives claim-verification
tooling positive evidence, not just the absence of complaints.
-
JSON (
plasma check --format json): theEvaluationstructure serialized directly — root, policy id/version, schema version, ordered findings, summary counts. Deterministic: identical inputs produce byte-identical output. -
SARIF 2.1.0 (
--format sarif): one SARIF rule per policy rule,ruleId = "plasma/<rule-id>"— a stable namespace; consumers key on it. Severity maps toerror|warning|note. Pass findings appear only with--verbose, askind: "pass". Repo- and metadata-scoped findings carry no artifact location (SARIF permits this). -
Facts (
plasma facts): theFactSetas JSON — the diffable before/after snapshot for agent-verification tooling.
The v0 engine commits to properties a future formal core (OCaml/Catala) can rely on, so that core can generate policies in this format or verify this evaluator against a formal semantics:
-
Determinism —
evaluateis a pure functionPolicy × FactSet → Evaluation: no IO, clocks, randomness, or environment reads; BTree ordering everywhere; byte-identical JSON for identical inputs. -
Totality — no panics on any loadable policy; every partial case has the defined false/absent semantics tabulated above; unsupported constructs are rejected at load, never mid-evaluation.
-
Versioned schema —
schema_versionis mandatory and checked first; the loader refuses other versions with a distinct error. -
No ambient state — all repository knowledge flows through the
FactSet, whose collection procedure is specified above. -
Closed condition algebra — conditions form a boolean algebra over the finite predicate vocabulary denoted above; nothing else can appear.
-
Stable rule namespace —
plasma/<rule-id>in SARIF is the interchange contract with sibling tooling (somethings-fishy,did-you-actually-do-that).
The action planner turns an Evaluation into a remediation Plan, keeping
the same purity split as evaluation:
-
plan(policy, evaluation, ctx) → Planis pure (action.rs). For each violation it looks up the rule by id and either synthesises a mechanicalActionor records aManualItemwith a reason — nothing is silently dropped.FixContextcarries the injectedlicense,author, andyear(never clock-derived, so planning stays deterministic). -
apply(plan, root, opts) → ApplyOutcomeis the IO boundary (apply.rs, the counterpart tofacts.rs). It executes actions, writing<file>.bakbackups first unless disabled, and is idempotent — an action whose effect already holds is skipped, not repeated.
| Action | Fires for | Effect |
|---|---|---|
|
violated |
prepend |
|
violated |
create the missing file with obvious placeholder content (never overwrites an existing file) |
Everything else is manual: a required file named by a glob (no single
filename to create), unparsable headers (need hand correction), prohibitions
(removal is not auto-applied). Future increments add UpdateField, etc.;
the enum and Plan shape are the stable contract.
Determinism/totality carry over: plan is pure and total; apply never
panics and reports per-action applied / skipped / errors.
Reserved constructs (release subjects/resources, consistent-with,
modify-rules) parse today and gain semantics in later minor versions
alongside their fact sources (release facts, decision registries). The
schema version
gates all of it: a policy using future semantics will not load on an engine
that cannot honour it.