Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
What changed in PACT, newest first. Each entry says what an author can now write, or what now
holds that did not; the commits are listed under it for whoever wants the detail.

## 2026-11 — the kernel's operations are a file, and each field says which it is (13 November)

A runtime that runs PACT one operation at a time needs to know which fields reach each of the 76
operations of the seven primitives. That list lived in one place, the `OPS` table inside
`scripts/pattern-coverage.py`, and said it in keyword prose (`limits, at-most, fuel,
checks-at-most`), which a runtime can only match against field names by guessing. Nothing here
changes what an author writes or what `pact check` accepts.

- **`spec/kernel.yaml`** lists the 76 operations: for each, its primitive, what it does, and the
keyword that is it today. `scripts/pattern-coverage.py` reads them from there rather than
keeping its own copy. An operation written as syntax inside a value rather than as a field of its
own (`one of a, b` in a shape line, `input.x` in a binding) says where with `written-in:`.
- **`kernel-op:` on a field of `spec/schema.yaml`** names the operations the field is:
`store.claim` on `action.same-request-key`, `allowance.reach` on `agent.uses`, several where a
field is several (`stage.does`). 295 fields carry one; descriptions, names and `because:` carry
none. The checker reads it into `Field::kernel_ops`, and `pattern-coverage.py --ops` lists each
operation's fields.
- **`crates/pact-schema/tests/every_kernel_operation_is_written_somewhere.rs`** holds the two
files to each other: a `kernel-op:` naming an operation the kernel does not list fails, and so
does an operation that no field names and no `written-in:` places.
- **`scripts/primitive-coverage.py` adds a keyword's operations' primitives to its own.** Its main
ingredient is unchanged; the "takes any part in" counts of docs/97 §6 grow where the table had
given a keyword fewer primitives than its operations have (`agent.accepts` is a signature, so an
Actor as well as a Shape).

## 2026-11 — one text and one fingerprint for a value, written down (13 November)

What a condition reads out of a value, the share `in-share:` puts it in, the digest a person's yes
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1000,11 +1000,11 @@ framework is an extra.
|---|---|
| **Design** | Thesis, 29 binding decisions, FRD (120 requirements), 60-row refusal ledger — complete |
| **Research** | 14 source-grounded studies, ~15,750 lines, over 140 repos (~15 GB) + 57 papers |
| **Code** | Loader, diagnostics, schema engine, CLI, harness, resolver, evals, SLO — **5145 tests (1687 Rust + 3458 adapter), clippy clean, TypeScript type-checked** |
| **Code** | Loader, diagnostics, schema engine, CLI, harness, resolver, evals, SLO — **5149 tests (1691 Rust + 3458 adapter), clippy clean, TypeScript type-checked** |
| **Adapters** | **All 7 named targets**, proven against one shared conformance suite |

```bash
./scripts/test-all.sh # 5145 tests, Rust + 7 adapters, fully offline
./scripts/test-all.sh # 5149 tests, Rust + 7 adapters, fully offline
```

**What is still owed** is not hidden — it lives in
Expand Down
2 changes: 2 additions & 0 deletions crates/pact-schema/src/from_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ impl Schema {
.trim()
.to_string();
field.template = f.get("template").and_then(as_bool).unwrap_or(false);
// docs/97 §5.4 — see `Field::kernel_ops`.
field.kernel_ops = names(f, "kernel-op");
group.fields.push(field);
}
None => diags.push(Diagnostic::error(
Expand Down
13 changes: 13 additions & 0 deletions crates/pact-schema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,18 @@ pub struct Field {
/// template, may appear in (96 §C). Read by `pact_loader::holes`, which
/// kept the same list as a Rust const of two field names.
pub template: bool,
/// The kernel operations this field is (docs/97 §2 and §5.4), as
/// `spec/kernel.yaml` names them — `store.claim` on
/// `action.same-request-key`, `allowance.reach` on `agent.uses`. Empty on a
/// field that is no operation: a description, a name, a `because:`.
///
/// Data for the reason `template` is: a runtime that keeps one
/// implementation per operation reads which fields reach it from here,
/// rather than by matching an operation's keyword prose against field
/// names, where `limits` is a group, two fields and every field inside
/// them. `tests/every_kernel_operation_is_written_somewhere.rs` holds the
/// names to `spec/kernel.yaml`.
pub kernel_ops: Vec<String>,
}

impl Field {
Expand Down Expand Up @@ -444,6 +456,7 @@ impl Field {
model_kind: Vec::new(),
plays_role: String::new(),
template: false,
kernel_ops: Vec::new(),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//! Every kernel operation is written somewhere, and every field that says it is
//! one names an operation that exists.
//!
//! `spec/kernel.yaml` lists the 76 operations of the seven primitives (docs/97
//! §2 and §5.4). `spec/schema.yaml` says on each field which of them the field
//! IS, with `kernel-op:`. The two are kept in two files because they answer two
//! questions — what the kernel can do, and where an author writes it — and a
//! pair of hand-kept lists drifts unless something holds them to each other.
//! This is that thing, in both directions:
//!
//! - a `kernel-op:` naming an operation `spec/kernel.yaml` does not list is a
//! field claiming to be something the kernel cannot do;
//! - an operation no field names and no `written-in:` places is one an author
//! has no way to write, which is the gap docs/97 §9 closed for sixteen
//! operations and which nothing stopped from reopening.
//!
//! The last test runs the same check on the real files with one line changed
//! each way, so neither half can pass by checking nothing.

use camino::Utf8Path;
use pact_diag::Diagnostics;
use pact_doc::{Node, parse_yaml};
use pact_schema::Schema;
use pact_schema::from_doc::schema_from_yaml;
use std::collections::{BTreeMap, BTreeSet};

/// The seven primitives, and the prefix every operation of each carries.
const PRIMITIVES: [(&str, &str); 7] = [
("Shape", "shape"),
("Origin", "label"),
("Store", "store"),
("Actor", "actor"),
("Flow", "flow"),
("Check", "check"),
("Allowance", "allowance"),
];

fn read(name: &str) -> String {
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join(format!("../../spec/{name}"));
std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("spec/{name} is part of the repository"))
}

fn schema(text: &str) -> Schema {
let mut diags = Diagnostics::new();
let schema = schema_from_yaml(text, &mut diags);
assert!(!diags.has_errors(), "spec/schema.yaml loads clean");
schema
}

/// One operation of `spec/kernel.yaml`.
struct Op {
primitive: String,
meaning: String,
keyword: String,
written_in: Option<String>,
}

fn kernel(text: &str) -> BTreeMap<String, Op> {
let doc = parse_yaml(text, Utf8Path::new("spec/kernel.yaml")).expect("spec/kernel.yaml parses");
let ops = doc
.get("ops")
.and_then(Node::as_map)
.expect("spec/kernel.yaml states `ops:`");
let text_at = |node: &Node, key: &str| node.get(key).and_then(Node::as_str).map(str::to_string);
ops.iter()
.map(|(name, entry)| {
let node = &entry.node;
(
name.clone(),
Op {
primitive: text_at(node, "primitive").unwrap_or_default(),
meaning: text_at(node, "meaning").unwrap_or_default(),
keyword: text_at(node, "keyword").unwrap_or_default(),
written_in: text_at(node, "written-in"),
},
)
})
.collect()
}

/// Which fields name each operation, as `<group>.<field>`.
fn named_by(schema: &Schema) -> BTreeMap<String, BTreeSet<String>> {
let mut by: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for group in schema.groups() {
for field in &group.fields {
for op in &field.kernel_ops {
by.entry(op.clone())
.or_default()
.insert(format!("{}.{}", group.name, field.name));
}
}
}
by
}

/// Everything wrong between the two files, one sentence each.
fn problems(schema: &Schema, ops: &BTreeMap<String, Op>) -> Vec<String> {
let mut found = Vec::new();
let by = named_by(schema);
for (op, fields) in &by {
if !ops.contains_key(op) {
found.push(format!(
"{} say they are `{op}`, and spec/kernel.yaml lists no such operation",
fields.iter().cloned().collect::<Vec<_>>().join(", ")
));
}
}
for (name, op) in ops {
if !by.contains_key(name) && op.written_in.is_none() {
found.push(format!(
"`{name}` is named by no field's `kernel-op:` and says nowhere else it is written (`written-in:`)"
));
}
}
found
}

#[test]
fn every_operation_says_its_primitive_what_it_does_and_its_keyword() {
let ops = kernel(&read("kernel.yaml"));
assert_eq!(
ops.len(),
76,
"docs/97 §5.4 counts 76 operations; a change to that number is a change to the kernel and to the document"
);
for (name, op) in &ops {
let prefix = PRIMITIVES
.iter()
.find(|(primitive, _)| *primitive == op.primitive)
.map(|(_, prefix)| *prefix)
.unwrap_or_else(|| panic!("`{name}` belongs to '{}', which is not one of the seven", op.primitive));
assert!(
name.split_once('.').is_some_and(|(head, tail)| head == prefix && !tail.is_empty()),
"`{name}` is a {} operation and is named `{prefix}.<what it does>`",
op.primitive
);
assert!(!op.meaning.trim().is_empty(), "`{name}` says what it does");
assert!(!op.keyword.trim().is_empty(), "`{name}` names the keyword that is it today");
}
}

#[test]
fn the_schema_and_the_kernel_agree_in_both_directions() {
let found = problems(&schema(&read("schema.yaml")), &kernel(&read("kernel.yaml")));
assert!(found.is_empty(), "{}", found.join("\n"));
}

#[test]
fn a_field_shared_through_an_anchor_carries_its_operation_everywhere_it_is_used() {
// `interface:` and `chosen-by:` are written once, on `knowledge`, and reused
// on `skill` and `tool` by anchor. The attribute travels with them.
let by = named_by(&schema(&read("schema.yaml")));
let requirement = &by["actor.by-requirement"];
for field in ["knowledge.interface", "skill.interface", "tool.interface", "tool.chosen-by"] {
assert!(requirement.contains(field), "{field} is `actor.by-requirement`");
}
let pairs = by.values().map(BTreeSet::len).sum::<usize>();
assert!(
pairs >= 340,
"the schema pairs a field with an operation {pairs} times; it did more than 340 times, \
and a list that shrinks is one that stopped being kept"
);
}

#[test]
fn the_check_fails_on_a_name_nothing_lists_and_on_an_operation_nothing_writes() {
let schema_text = read("schema.yaml");
let kernel_text = read("kernel.yaml");

// A field claiming an operation the kernel does not have.
let claimed = schema_text.replacen("kernel-op: store.fork", "kernel-op: store.spoon", 1);
assert_ne!(claimed, schema_text, "`stage.forks` is `store.fork` in the shipped schema");
let found = problems(&schema(&claimed), &kernel(&kernel_text));
assert!(
found.iter().any(|p| p.contains("stage.forks") && p.contains("`store.spoon`")),
"{found:?}"
);
// ...which also leaves `store.fork` with nowhere it is written.
assert!(found.iter().any(|p| p.starts_with("`store.fork`")), "{found:?}");

// An operation whose only place was `written-in:`, with that line gone.
let unplaced = kernel_text.replacen(
" written-in: \"a shape line: `one of a, b`, wherever a field takes an answer shape\"\n",
"",
1,
);
assert_ne!(unplaced, kernel_text, "`shape.one-of` is written in a shape line");
let found = problems(&schema(&schema_text), &kernel(&unplaced));
assert_eq!(
found,
vec!["`shape.one-of` is named by no field's `kernel-op:` and says nowhere else it is written (`written-in:`)"]
);
}
2 changes: 1 addition & 1 deletion docs/90-REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ them and changes nothing.
| | |
|---|---|
| Schema | **43 groups, 262 field entries, 207 distinct field names** (core 123, expert 69, both 15), 40 closed enums, 27 cross-reference constraints, **0 fields missing `tier:` or `surface:`** |
| Rust | 5 crates, **1687 tests**, `unsafe_code = forbid`, clippy-deny, edition 2024 |
| Rust | 5 crates, **1691 tests**, `unsafe_code = forbid`, clippy-deny, edition 2024 |
| Python | **47 modules** (`find adapters/python/src -name "*.py" \| wc -l`), **3458 tests**, **nine transports** — seven model-bound (`anthropic`, `autogen`, `langchain`, `langgraph`, `ollama`, `openai_agents`, `pydantic_ai`), one bound to a remote *agent* (`a2a`), one deterministic mock; `_metering.py`, `_summarise.py` and `_tool_choice.py` are shared code the transports import, which is what the `_` says — offline by default |
| TypeScript | 5 modules, 2,305 lines, a second port of **the stepping, the ceilings and the stage path** (`harness.ts:15-21`: *"AC-5.3's bar is the stage path"*) — **not** of the loop entire: no interceptor chain, no durable suspension record, no ledger (`notDoneHere`; a park returns `halted: "suspended"` and writes nothing down). This row is load-bearing for the Phase 3 pricing below (E4) |
| CLI | **6 verbs**: `check`, `show`, `waits`, `discover`, `card`, `help` |
Expand Down
Loading
Loading