Skip to content
Draft
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
67 changes: 67 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copilot Instructions for constraint-workshop

## Repository purpose

This repository contains small, deterministic control primitives for software systems.
Each primitive is a standalone, testable, auditable brick with no framework dependencies,
no runtime state, and no hidden behavior.

## Coding standards

- **stdlib only** — no third-party runtime dependencies unless explicitly approved.
- **Deterministic** — same inputs must always produce the same outputs; no randomness, no time-dependent logic, no global state.
- **No side effects** — primitives must not log, mutate shared state, or perform I/O.
- **Pure Python** — target Python 3.10–3.12. Avoid walrus operators or syntax unavailable on 3.10.
- **Tests mandatory** — every primitive ships with a `pytest` test file. Tests must cover all invariants stated in the module docstring.
- **No ML / no network** — classifiers use phrase matching and regex only.

## Module layout conventions

```
<module>/
src/<module>/ # source package (importable)
tests/ # pytest tests
rules/ # declarative JSON rule files (where applicable)
baselines/ # hash-bound baseline artefacts (where applicable)
README.md # contract + usage
```

## Commit Gate specifics

- Verdicts are exactly: `ALLOW`, `REFUSE`, `ESCALATE` — no new verdict values.
- Scope matching is **exact string match only** — no glob, no regex, no prefix.
- `invariant_hash` is SHA-256 of the declared contract text (hex, lowercase, 64 chars).
- `decision_hash` is SHA-256 of `canonical_request + verdict + sorted(reasons)`.
- Authority drift detection: new allowlist edges with an unchanged `invariant_hash` must **FAIL**.
- The `/prometheus/` directory is observability-only and must never be imported by gate/engine code.

## Invariant litmus specifics

- Scoring: `+0.25` hard-invariant signal, `-0.25` cost-curve signal, `+0.15` quantification signal.
- Negation window: 2 words.
- No external dependencies beyond `re`.

## Hard decision invariants (enforced by all gate primitives)

These rules are non-negotiable and must be encoded in every gate, engine, and policy primitive:

1. **Fail-closed defaults** — if policy, identity, or logging is missing, the decision is `DENY`.
2. **No silent success** — every decision must produce a Receipt; if a Receipt cannot be created, the decision is `DENY`.
3. **No self-approval** — `actor_id` cannot appear as an approver for the same request; if it does, the decision is `DENY`.
4. **Approval required but absent** — if approval is required and no valid approval is present, the decision is `HOLD` (never `ALLOW`).
5. **Tamper-evident log** — Receipts carry `{seq, prev_hash, this_hash}`; any out-of-order sequence or invalid hash linkage must be rejected.

## Process rules

- **Prefer schemas + conformance tests before implementation.** Define the JSON schema and golden test vectors first; open a separate PR for the implementation.
- **PRs must be small and reviewable.** One primitive or one contract change per PR.
- **No scope expansion.** A PR that adds new verdicts, new primitives, or new cross-module imports without a preceding schema PR must be rejected.

## Pull request checklist

Before opening a PR, verify:
1. All new primitives have a matching `test_<module>.py`.
2. `pytest` passes with zero failures.
3. No new runtime dependencies introduced.
4. README updated if the public interface changed.
5. If a new `CommitRequest` field is added, update `canonicalise.py` and regenerate baselines.
104 changes: 104 additions & 0 deletions .github/prompts/commitboundary_spec_intake.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
mode: 'agent'
description: 'Intake prompt: generate schemas + golden test vectors for CommitBoundary v0 (no implementation).'
---

# CommitBoundary Spec Intake — v0

You are a specification author for the `constraint-workshop` repository.

## SCOPE CONSTRAINT — READ FIRST

You must generate **schemas and golden test vectors only**.
Do NOT generate any Python implementation classes, engine code, or runtime logic.
Do NOT create `BoundarySpec`, `BoundaryVerdict`, or any callable Python class.
Implementation is deferred to a separate, subsequent PR.

## Context

The repository contains deterministic control primitives (stdlib-only, no network, no randomness).
All decisions are fail-closed: missing policy → DENY; no Receipt possible → DENY;
self-approval → DENY; approval required but absent → HOLD (never ALLOW).

## What to generate

Produce exactly the following files as fenced code blocks with the path as the language label.

### 1. `spec/schemas/commit_boundary_spec.schema.json`

A JSON Schema (draft-07) describing a `BoundarySpec` document:
- `actor_id` (string, required)
- `action_classes` (array of strings, minItems 1, unique, required)
- `scope_paths` (array of strings, minItems 1, unique, required)
- `boundary_hash` (string, pattern `^[0-9a-f]{64}$`, required)

### 2. `spec/schemas/boundary_verdict.schema.json`

A JSON Schema (draft-07) describing a `BoundaryVerdict` document:
- `verdict` (string, enum `["WITHIN", "BREACH"]`, required)
- `actor_id` (string, required)
- `action_class` (string, required)
- `scope_paths_requested` (array of strings, required)
- `boundary_hash` (string, pattern `^[0-9a-f]{64}$`, required)

### 3. `spec/golden/build1/within.json`

A golden vector representing a **WITHIN** verdict:
- actor and action class exactly match the spec
- all requested scope_paths are in the spec's scope_paths
- Include a `"_comment"` field: `"Happy path: all fields match"`

### 4. `spec/golden/build1/breach_actor.json`

A golden vector representing a **BREACH** verdict caused by wrong `actor_id`.
- Include a `"_comment"` field: `"actor_id mismatch triggers BREACH"`

### 5. `spec/golden/build1/breach_action.json`

A golden vector representing a **BREACH** verdict caused by a disallowed `action_class`.
- Include a `"_comment"` field: `"action_class not in spec triggers BREACH"`

### 6. `spec/golden/build1/breach_scope.json`

A golden vector representing a **BREACH** verdict caused by an out-of-scope path.
- Include a `"_comment"` field: `"scope_path outside envelope triggers BREACH"`

### 7. `spec/golden/build2/hold_no_approval.json`

A golden vector representing a **HOLD** outcome:
- approval is required by the spec but no approval is present in the request
- Include a `"_comment"` field: `"approval required but absent -> HOLD, never ALLOW"`

### 8. `tests/test_schemas.py`

A `pytest` test file that:
- Loads `spec/schemas/commit_boundary_spec.schema.json` and `spec/schemas/boundary_verdict.schema.json` using only stdlib (`json`).
- Validates that each golden file in `spec/golden/build1/` and `spec/golden/build2/` is valid JSON (parseable).
- Checks that every golden verdict file contains a `"verdict"` key whose value is one of `["WITHIN", "BREACH", "HOLD"]`.
- Checks that every golden file contains a `"_comment"` key.
- Uses `jsonschema` if available, otherwise skips schema-validation tests with `pytest.importorskip`.

### 9. `tests/test_golden_invariants.py`

A `pytest` test file that encodes the hard decision invariants as parameterised checks against the golden vectors:
- For every golden file with `"verdict": "WITHIN"`: assert `actor_id`, `action_class`, and all `scope_paths_requested` are present and non-empty.
- For every golden file with `"verdict": "BREACH"`: assert at least one of actor, action, or scope is the cause (field `"breach_reason"` must be present).
- For `hold_no_approval.json`: assert `"verdict"` is `"HOLD"` and `"approval_present"` is `false`.
- No implementation logic — tests assert only the structure and values of the JSON golden files.

### 10. `README.md` section — append only

Append a new section at the end of the existing README with heading `## What is inspectable in v0`.

Content must include:
- One sentence stating that v0 ships schemas and golden test vectors only; implementation is in a future PR.
- A bullet list of the schema files under `spec/schemas/`.
- A bullet list of the golden vector files under `spec/golden/`.
- A bullet list of the test files and what invariant each covers.
- The sentence: "All verdicts are fail-closed: missing policy → DENY; no Receipt → DENY; self-approval → DENY; approval required but absent → HOLD."

## Output format

Return only fenced code blocks, one per file, with the relative file path as the fence label.
Do not add any prose, explanation, or commentary outside the code blocks.

123 changes: 123 additions & 0 deletions decision_space_ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# decision_space_ledger

Deterministic inspection utility for versioned decision-space snapshots.

**Inspection only.** This primitive does not modify any enforcement, gating, authority, or runtime logic. It reads snapshots, validates them, canonicalises them, hashes them, and produces structured diffs.

---

## Schema — `decision_space_snapshot_v1`

| Field | Type | Constraints |
|-------|------|-------------|
| `version` | string | Must be exactly `"v1"` |
| `variables` | array of strings | Unique items |
| `allowed_transitions` | array of `{from, to}` objects | Each item has exactly `from` (string) and `to` (string) |
| `exclusions` | array of strings | Unique items |
| `reason_code_families` | object → array of strings | Each value array has unique items |

The canonical JSON Schema (draft-07) is in `src/decision_space_ledger/decision_space_snapshot_v1.schema.json`.

---

## Invariants

| # | Invariant |
|---|-----------|
| 1 | `validate()` raises `ValueError` for any schema violation — no silent pass-through |
| 2 | `canonicalise()` is order-independent: logically equivalent snapshots produce byte-identical output |
| 3 | `canonical_hash()` is exactly 64 lowercase hex characters (SHA-256) |
| 4 | Same inputs always produce the same outputs (deterministic, no randomness, no global state) |
| 5 | `diff()` output lists are always sorted (deterministic across runs) |
| 6 | No side effects — no I/O, no logging, no state mutation |

---

## Interface

```python
from decision_space_ledger import validate, canonicalise, canonical_hash, diff

snapshot = {
"version": "v1",
"variables": ["APPROVE", "DENY", "HOLD"],
"allowed_transitions": [
{"from": "HOLD", "to": "APPROVE"},
{"from": "HOLD", "to": "DENY"},
],
"exclusions": [],
"reason_code_families": {
"approval": ["MANUAL", "AUTO"],
},
}

validate(snapshot) # raises ValueError on any violation
h = canonical_hash(snapshot) # 64-char lowercase hex SHA-256
delta = diff(snapshot, other) # structured diff dict
```

---

## CLI

Compare two snapshot files:

```bash
python -m decision_space_ledger snapshot_a.json snapshot_b.json
```

Output (JSON to stdout):

```json
{
"snapshot_a": {"path": "snapshot_a.json", "hash": "<sha256>"},
"snapshot_b": {"path": "snapshot_b.json", "hash": "<sha256>"},
"diff": {
"is_identical": false,
"variables_added": ["NEW_STATE"],
"variables_removed": [],
"transitions_added": [{"from": "HOLD", "to": "NEW_STATE"}],
"transitions_removed": [],
"exclusions_added": [],
"exclusions_removed": [],
"reason_code_families_added": [],
"reason_code_families_removed": [],
"reason_code_families_changed": {}
}
}
```

Exit codes: `0` success, `1` I/O or validation error, `2` incorrect usage.

---

## Run Tests

```bash
pytest decision_space_ledger/tests/ -v
```

Or from the repository root (tests are discovered automatically):

```bash
pytest -q
```

---

## Module layout

```
decision_space_ledger/
src/decision_space_ledger/
__init__.py # public exports
schema.py # SCHEMA constant + validate()
decision_space_snapshot_v1.schema.json # JSON Schema draft-07 reference
canonicalise.py # canonicalise() + canonical_hash()
diff.py # diff()
cli.py # CLI main()
__main__.py # python -m entry point
tests/
test_ledger.py # full unit tests (62 tests)
README.md
```
33 changes: 33 additions & 0 deletions decision_space_ledger/src/decision_space_ledger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Decision-Space Diff Ledger — inspection utility for versioned decision-space snapshots.

Inspection only. No modification of enforcement, gating, authority, or runtime logic.

Public API::

from decision_space_ledger import validate, canonicalise, canonical_hash, diff

snapshot = {
"version": "v1",
"variables": ["A", "B"],
"allowed_transitions": [{"from": "A", "to": "B"}],
"exclusions": [],
"reason_code_families": {"approval": ["MANUAL", "AUTO"]},
}
validate(snapshot)
h = canonical_hash(snapshot)
delta = diff(snapshot, other_snapshot)
"""

from .canonicalise import canonical_hash, canonicalise
from .diff import diff
from .schema import SCHEMA, validate

__version__ = "0.1.0"

__all__ = [
"SCHEMA",
"canonicalise",
"canonical_hash",
"diff",
"validate",
]
7 changes: 7 additions & 0 deletions decision_space_ledger/src/decision_space_ledger/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Allow running as: python -m decision_space_ledger <snapshot_a.json> <snapshot_b.json>"""

import sys

from .cli import main

sys.exit(main())
46 changes: 46 additions & 0 deletions decision_space_ledger/src/decision_space_ledger/canonicalise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Deterministic canonicalisation and SHA-256 hashing for decision_space_snapshot_v1.

Canonicalisation rules:
- ``variables`` — sorted lexicographically.
- ``allowed_transitions`` — sorted by (from, to).
- ``exclusions`` — sorted lexicographically.
- ``reason_code_families`` — each code list sorted lexicographically;
the families object itself uses sorted keys (via json.dumps sort_keys=True).
- Final JSON: sort_keys=True, no whitespace, UTF-8 encoded.

The hash is SHA-256 of the canonical bytes, returned as a lowercase hex string
(64 characters).
"""

import hashlib
import json


def canonicalise(snapshot):
"""Return canonical JSON bytes of a validated snapshot.

All order-independent arrays are sorted before serialisation so that
logically equivalent snapshots produce byte-identical output regardless
of insertion order.
"""
canonical = {
"version": snapshot["version"],
"variables": sorted(snapshot["variables"]),
"allowed_transitions": sorted(
snapshot["allowed_transitions"],
key=lambda t: (t["from"], t["to"]),
),
"exclusions": sorted(snapshot["exclusions"]),
"reason_code_families": {
family: sorted(codes)
for family, codes in snapshot["reason_code_families"].items()
},
}
return json.dumps(
canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=False
).encode("utf-8")


def canonical_hash(snapshot):
"""Return SHA-256 hex digest (lowercase, 64 chars) of the canonical snapshot."""
return hashlib.sha256(canonicalise(snapshot)).hexdigest()
Loading