mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
004fcbc249
## What
Adds `schemas/agentops-sdlc-provenance.v1.schema.json` — the JSON Schema
(draft-07) that formally defines one event of the committed, per-record
hash-chained provenance ledger at `docs/provenance/ledger.jsonl` (schema
id `agentops-sdlc-provenance.v1`, referenced by CLAUDE.md and the
ag-x31t council architecture).
Each event = one typed provenance edge `{from_id, from_type, to_id,
to_type, relation, evidence_ref, trust_tier(authored|inferred|mined),
ts}` plus export-time hash-chain fields `{prev_hash, payload_hash,
hash}` mirroring `cli/internal/rpi/ledger.go` chaining semantics
(`payload_hash` = sha256 over the edge payload; `hash` =
sha256(payload_hash + "\n" + prev_hash)).
## Acceptance (ag-x31t.2)
> First failing test: validates a `decision_produces_artifact` edge,
rejects one missing `trust_tier`.
- `scripts/validate-provenance-ledger.sh` — python `jsonschema` (Draft7)
validator with `--selftest` and `--jsonl` modes, mirroring
`validate-outcomes-rubric.sh`; SKIPs gracefully when jsonschema is
unavailable.
- `tests/scripts/validate-provenance-ledger.bats` — 12 checks. A
`decision_produces_artifact` edge VALIDATES; an otherwise-identical edge
missing `trust_tier` is REJECTED. Structural-JSON checks are the
always-on guard (run without jsonschema).
-
`tests/fixtures/provenance/{valid-decision-produces-artifact,invalid-missing-trust-tier}.json`
— fixtures with real sha256 chain hashes.
TDD verified: the selftest FAILS when the schema is edited to drop the
`trust_tier` requirement, PASSES when restored.
## Scope
Schema + validator + test only. Does **not** create/edit
`docs/provenance/ledger.jsonl`, the write-model (ag-x31t.4), export
(ag-x31t.5), or gate (ag-x31t.6). No symlinks. No holdout data.
```
gates: validator --selftest pass · bats 12/12 pass
```
Closes-scenario: ag-x31t.2#provenance-schema
Bounded-context: BC4-Factory
Evidence: schemas/agentops-sdlc-provenance.v1.schema.json
153 lines
5.0 KiB
Bash
Executable File
153 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# validate-provenance-ledger.sh — Validate provenance ledger events against
|
|
# schemas/agentops-sdlc-provenance.v1.schema.json (ag-x31t.2).
|
|
#
|
|
# The committed, per-record hash-chained provenance ledger
|
|
# (docs/provenance/ledger.jsonl) is the AUDIT authority for the SDLC
|
|
# provenance/intent graph (council: .agents/council/2026-05-30-debate-provenance-substrate.md).
|
|
# This validator is the schema's structural acceptance surface: each ledger
|
|
# event (one JSON object per line) must carry a typed provenance edge plus the
|
|
# export-time hash-chain fields, so a malformed edge — e.g. one missing a
|
|
# trust_tier — FAILS validation, not merely a code review.
|
|
#
|
|
# Usage:
|
|
# scripts/validate-provenance-ledger.sh <event.json> [<event.json>...]
|
|
# Validate each single-event JSON file. Exit 0 only if EVERY file is
|
|
# schema-valid.
|
|
# scripts/validate-provenance-ledger.sh --jsonl <ledger.jsonl> [...]
|
|
# Validate every non-blank line of each JSONL ledger file as one event.
|
|
# scripts/validate-provenance-ledger.sh --selftest
|
|
# Validate the tracked fixtures with expected pass/fail and exit 0 only if
|
|
# all match expectations.
|
|
#
|
|
# Gating validator: python `jsonschema` (Draft7), mirroring
|
|
# scripts/validate-outcomes-rubric.sh. If python3/jsonschema is unavailable the
|
|
# script SKIPs (exit 0 with a notice) so it never blocks an environment without
|
|
# the tool — the bats suite's structural-JSON checks are the always-on guard.
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
SCHEMA="$REPO_ROOT/schemas/agentops-sdlc-provenance.v1.schema.json"
|
|
FIXTURES="$REPO_ROOT/tests/fixtures/provenance"
|
|
|
|
if [[ ! -f "$SCHEMA" ]]; then
|
|
echo "FAIL: schema not found: $SCHEMA" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if ! command -v python3 >/dev/null 2>&1 || ! python3 -c 'import jsonschema' >/dev/null 2>&1; then
|
|
echo "SKIP: python3 + jsonschema not available; structural validation skipped" >&2
|
|
exit 0
|
|
fi
|
|
|
|
# validate_one <file> -> exit 0 if schema-valid, 1 if invalid, 2 on read error.
|
|
validate_one() {
|
|
python3 - "$SCHEMA" "$1" <<'PY'
|
|
import json, sys
|
|
from jsonschema import Draft7Validator
|
|
|
|
schema_path, data_path = sys.argv[1], sys.argv[2]
|
|
try:
|
|
with open(schema_path) as f:
|
|
schema = json.load(f)
|
|
with open(data_path) as f:
|
|
doc = json.load(f)
|
|
except Exception as e:
|
|
print(f"read error: {e}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
errors = sorted(Draft7Validator(schema).iter_errors(doc), key=lambda e: list(e.path))
|
|
if errors:
|
|
for e in errors:
|
|
loc = "/".join(str(p) for p in e.path) or "(root)"
|
|
print(f" invalid at {loc}: {e.message}", file=sys.stderr)
|
|
sys.exit(1)
|
|
sys.exit(0)
|
|
PY
|
|
}
|
|
|
|
# validate_jsonl <file> -> exit 0 if EVERY non-blank line validates.
|
|
validate_jsonl() {
|
|
python3 - "$SCHEMA" "$1" <<'PY'
|
|
import json, sys
|
|
from jsonschema import Draft7Validator
|
|
|
|
schema_path, data_path = sys.argv[1], sys.argv[2]
|
|
with open(schema_path) as f:
|
|
schema = json.load(f)
|
|
validator = Draft7Validator(schema)
|
|
bad = 0
|
|
with open(data_path) as f:
|
|
for n, line in enumerate(f, 1):
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
doc = json.loads(line)
|
|
except json.JSONDecodeError as e:
|
|
print(f" line {n}: not valid JSON: {e}", file=sys.stderr)
|
|
bad += 1
|
|
continue
|
|
errors = sorted(validator.iter_errors(doc), key=lambda e: list(e.path))
|
|
if errors:
|
|
for e in errors:
|
|
loc = "/".join(str(p) for p in e.path) or "(root)"
|
|
print(f" line {n} invalid at {loc}: {e.message}", file=sys.stderr)
|
|
bad += 1
|
|
sys.exit(1 if bad else 0)
|
|
PY
|
|
}
|
|
|
|
if [[ "${1:-}" == "--selftest" ]]; then
|
|
rc=0
|
|
declare -A EXPECT=(
|
|
[valid-decision-produces-artifact.json]=0
|
|
[invalid-missing-trust-tier.json]=1
|
|
)
|
|
for name in valid-decision-produces-artifact.json invalid-missing-trust-tier.json; do
|
|
want="${EXPECT[$name]}"
|
|
if validate_one "$FIXTURES/$name" >/dev/null 2>&1; then got=0; else got=1; fi
|
|
if [[ "$got" == "$want" ]]; then
|
|
echo "PASS: $name (expected $([[ $want == 0 ]] && echo valid || echo invalid))"
|
|
else
|
|
echo "FAIL: $name (expected $([[ $want == 0 ]] && echo valid || echo invalid), got $([[ $got == 0 ]] && echo valid || echo invalid))" >&2
|
|
rc=1
|
|
fi
|
|
done
|
|
exit "$rc"
|
|
fi
|
|
|
|
if [[ "${1:-}" == "--jsonl" ]]; then
|
|
shift
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "usage: $(basename "$0") --jsonl <ledger.jsonl> [...]" >&2
|
|
exit 2
|
|
fi
|
|
overall=0
|
|
for f in "$@"; do
|
|
if validate_jsonl "$f"; then
|
|
echo "PASS: $f"
|
|
else
|
|
echo "FAIL: $f (one or more events schema-invalid)" >&2
|
|
overall=1
|
|
fi
|
|
done
|
|
exit "$overall"
|
|
fi
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "usage: $(basename "$0") <event.json> [...] | --jsonl <ledger.jsonl> [...] | --selftest" >&2
|
|
exit 2
|
|
fi
|
|
|
|
overall=0
|
|
for f in "$@"; do
|
|
if validate_one "$f"; then
|
|
echo "PASS: $f"
|
|
else
|
|
echo "FAIL: $f (schema-invalid)" >&2
|
|
overall=1
|
|
fi
|
|
done
|
|
exit "$overall"
|