Implement TREV-WP-0002 Stage 0 foundation: schemas, pure fold, golden Phase

Delivers the offline runnable specification foundation for the Trust Layer
(TSD §3-§6), not a hosted Trust Service:

- JSON Schemas for Phase Manifest, Ledger Entry, Extension Contract, and
  Conversion Attestation, encoding the Stage 0 working defaults (Q3 future
  license enum, Q6 single-currency Phases, Q8 required longstop_at).
- src/target_revenue: pure Outstanding Target fold, SHA-256 hash-chain
  verification, Ed25519 signing helpers, extension conformance checks
  (including a core-term-redefinition heuristic), and conversion detection
  that never requires an attestation document to determine conversion status.
- examples/phase-001: golden Phase package matching the concept doc's
  worked example, generated via scripts/generate_golden_phase.py so the
  hash chain is computed by the library itself, not hand-typed.
- 32 passing pytest tests covering manifest/ledger/extension conformance,
  tamper/reorder detection, and the full lifecycle fold to conversion.
- docs/adr/ADR-0001: proposed (not accepted) Stage 0 stack choice, per the
  WP-0002-T01 human-accept gate — implementation proceeded against the
  proposal as the workplan note permits, but the task stays open.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-28 18:47:29 +02:00
parent 658b1af00d
commit 57c7111cbc
28 changed files with 1751 additions and 7 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
__pycache__/
*.pyc
.pytest_cache/
*.egg-info/
build/
dist/
.venv/

View file

@ -38,8 +38,10 @@ Program assessment: [`history/260728-SWOT-Assessment.md`](history/260728-SWOT-As
| `specs/` | Product/tech specs, working defaults, future normative extracts |
| `workplans/` | Active delivery plans |
| `history/` | Dated non-normative exploration and assessments |
| `examples/` | Golden Phase packages (planned; WP-0002) |
| `schemas/` | Machine-readable schemas (planned; WP-0002) |
| `examples/` | Golden Phase packages (`phase-001`, from WP-0002) |
| `schemas/` | Machine-readable JSON Schemas (WP-0002) |
| `src/target_revenue/` | Pure Python validators, hash chain, Outstanding Target fold, conversion detection (WP-0002) |
| `docs/adr/` | Architecture decisions; ADR-0001 (Stage 0 library stack) is **proposed**, pending human accept |
**`spec/` vs `specs/`:** The concept document predates the plural `specs/` directory and remains under `spec/` so history and cross-references stay stable. New product/tech artifacts go under `specs/`. Do not move the concept file without a deliberate migration note.
@ -53,6 +55,23 @@ Program assessment: [`history/260728-SWOT-Assessment.md`](history/260728-SWOT-As
Hub index: [`WORK-RECORDS.md`](WORK-RECORDS.md) · brief: [`.custodian-brief.md`](.custodian-brief.md)
## Stage 0 library (schemas, pure fold, validators)
WP-0002 delivers an **offline, dependency-light library** — not a hosted
Trust Service (`SCOPE.md` §3). It validates the schemas in `schemas/`,
computes the pure Outstanding Target fold, verifies the SHA-256 hash chain,
and checks Monetization Extension conformance.
```bash
pip install -e ".[dev]"
python3 -m pytest tests/ # 32 tests, no network required
python3 scripts/generate_golden_phase.py # regenerate examples/phase-001/ledger.json
```
The implementation stack (Python, `jsonschema`, `pytest`, `hatchling`) is
proposed in [`docs/adr/ADR-0001-stage0-library-stack.md`](docs/adr/ADR-0001-stage0-library-stack.md)
and is **not yet accepted** — see `CONTRIBUTING.md` human decision gates.
## What this repo does not claim yet
- Final legal license text or enforceable TRSL

View file

@ -0,0 +1,128 @@
---
id: ADR-0001
title: "Stage 0 library stack: schemas, pure fold, and validators"
status: proposed
date: 2026-07-28
decided_by: null
workstream: TREV-WP-0002 (trust-service-foundation)
alternatives_considered: [TypeScript/Node, Go]
---
# ADR-0001 — Stage 0 Library Stack (Schemas, Pure Fold, Validators)
## Status
**Proposed.** Per `CONTRIBUTING.md` "Human decision gates" and
`workplans/TREV-WP-0002-trust-service-foundation.md` T01, this ADR locks
implementation technology for the repository and **must not be treated as
accepted without explicit human sign-off**. Agents may implement against
this proposal, but WP-0002-T01 stays `todo` (not `done`) until a maintainer
accepts or revises it.
## Scope
This decision covers **only** the Stage 0 deliverables of WP-0002:
- JSON Schema (or equivalent) definitions for Phase Manifest, Target Ledger
Entry, Extension Contract, Transaction Allocation, Conversion Attestation;
- a pure, deterministic Outstanding Target fold;
- offline conformance validators;
- a golden Phase package and its test suite.
It explicitly does **not** cover: hosted Phase/Extension Registry services,
multi-tenant ledger APIs, a metrics product, or a federation protocol
(`SCOPE.md` §3§4). Those require a separate, later ADR once a Trust Service
implementation workplan exists.
## Context
`specs/TechnicalSpecificationDocument.md` §10 is deliberately non-binding on
language, runtime, and storage. Something has to be chosen to write the
Stage 0 library, but the choice should be cheap to revisit later since the
hosted Trust Service is a different, larger decision.
Requirements pulled from TSD §3, §6 and `specs/OpenQuestions-WorkingDefaults.md`
Q14:
- deterministic, pure computation (no hidden state, no floating-point order
sensitivity) for the Outstanding Target fold;
- JSON-Schema-shaped validation that a non-Python tool could later
reimplement against the same `.schema.json` files;
- SHA-256 canonical-serialization hash chaining and Ed25519 signatures over
the same canonical bytes;
- a test suite runnable offline, with no network services.
## Decision
**Python 3.11+, `jsonschema` for schema validation, dataclasses for typed
domain objects, `pytest` for the conformance suite, `hatchling` src-layout
packaging.**
Canonical serialization for hashing: JSON with sorted keys, no insignificant
whitespace, UTF-8 encoding (`json.dumps(..., sort_keys=True,
separators=(",", ":"))`), hashed with SHA-256. Signatures: Ed25519 via
`cryptography` or `PyNaCl`, over the same canonical bytes.
## Rationale
| What Stage 0 needs | Python fit |
|---|---|
| JSON Schema authoring and validation | `jsonschema` is a direct, widely-used implementation |
| Pure fold over ledger entries | Trivial with dataclasses + `functools.reduce`; no framework needed |
| Canonical serialization + hashing | Stdlib `json` + `hashlib` sufficient |
| Ed25519 signing for examples | `cryptography` covers this without custom crypto code |
| Offline conformance test suite | `pytest` is the de facto standard; matches this workspace's other Python repos (e.g. `shard-wiki`) |
| Low ceremony for a schema/fold library, not a service | Python's scripting ergonomics outweigh static-typing benefits at this scope |
### Why not TypeScript/Node
Would be a reasonable alternative if the primary consumer were browser-based
tooling (cf. `binect-js`), but Stage 0 has no UI deliverable — it is a
schema + pure-function library consumed by CLI/tests. Node adds packaging
overhead (npm registry, `package.json` versioning discipline) without a
corresponding benefit here.
### Why not Go
Go would fit well if this were becoming a long-lived, standalone,
performance-sensitive service (cf. `key-cape` ADR-0001's reasoning) — but
Stage 0 is explicitly **not** the hosted Trust Service. A statically-typed,
compiled toolchain is more ceremony than a schema/fold/test library needs at
this stage. Revisit for the later hosted-service ADR.
## Consequences
### Positive
- Fast path to a working, testable schema + fold library.
- `jsonschema` files are directly reusable by a future non-Python
implementation (hosted service ADR is unconstrained by this choice).
- Matches existing Python conventions in this workspace (pytest, hatchling
src-layout) for reviewer familiarity.
### Negative / risks
- Python's dynamic typing means domain-model discipline must be enforced by
convention (dataclasses + docstrings + tests), not the compiler.
- Should not be read as a decision about the hosted Trust Service's stack —
that is explicitly deferred and must not be assumed to inherit this choice
without its own ADR.
### Compensating guardrails
1. Typed domain objects (`dataclasses`, `frozen=True` where the schema marks
a field immutable) — no raw dicts crossing function boundaries in
`fold.py` or `conversion.py`.
2. All schema validation goes through the `.schema.json` files under
`schemas/` — no ad hoc field checks duplicated in Python without a
corresponding schema entry.
3. The fold function must be pure: same `(manifest, entries)` input always
produces the same Outstanding Target output, with no I/O.
## Revisit trigger
Reconsider this decision when a hosted Trust Service implementation
workplan is opened (`SCOPE.md` §3 "Production Trust Service"). That
decision should weigh multi-tenant hosting, storage, and operational
concerns not in scope here, and may reasonably choose a different stack
without invalidating this one.

View file

@ -0,0 +1,10 @@
{
"phase": "trsl:phase:example-001",
"milestone_release": "release-1.0",
"conversion_timestamp": "2027-06-01T00:00:00Z",
"future_license": "MIT",
"final_development_credit": 67000.0,
"final_remission_credit": 33000.0,
"final_outstanding_target": 0.0,
"ledger_checkpoint": "trsl:entry:example0010006"
}

View file

@ -0,0 +1,24 @@
{
"id": "trsl:extension:cost-plus-operations",
"version": "1.0",
"value": {
"description": "Hosted and monitored operation of the software: compute, storage, networking, monitoring, backup, and incident response."
},
"pricing": {
"method": "cost-plus, default 50% surcharge over eligible operations cost"
},
"allocation": {
"rule": "Development allocation defaults to 0% of the operations price. A Phase may explicitly declare a nonzero development_allocation on the surcharge portion; absent that declaration, none of this revenue counts toward the Phase target.",
"default_rate": 0.0
},
"recognition": {
"event": "payment-settled"
},
"reversal": {
"rule": "Refunds reverse proportionally; chargebacks reverse fully via a compensating credit-reversal entry."
},
"evidence": {
"requirement": "Settled payment reference; eligible operations cost breakdown recommended for disputed amounts."
},
"status": "registered"
}

View file

@ -0,0 +1,24 @@
{
"id": "trsl:extension:development-license",
"version": "1.0",
"value": {
"description": "Commercial-use rights and early commercial access to the protected Milestone Release."
},
"pricing": {
"method": "fixed-fee-per-entitlement"
},
"allocation": {
"rule": "100% of the collected development fee, net of tax, refunds, and chargebacks, becomes Development Credit for the identified Phase.",
"default_rate": 1.0
},
"recognition": {
"event": "payment-settled"
},
"reversal": {
"rule": "Refunds and chargebacks generate a compensating credit-reversal ledger entry for the reversed amount."
},
"evidence": {
"requirement": "Settled payment reference (processor transaction id) at minimum (working default Q10 tier E1); contract and invoice recommended for high-value credits (E2)."
},
"status": "registered"
}

View file

@ -0,0 +1,23 @@
{
"id": "trsl:extension:phase-sponsorship",
"version": "1.0",
"value": {
"description": "Financial support explicitly directed at a named Phase, rather than the project generally."
},
"pricing": {
"method": "declared-amount, negotiated per sponsorship agreement"
},
"allocation": {
"rule": "The sponsoring party and the licensor explicitly declare the development_allocation fraction of the sponsorship amount at the time the sponsorship is recorded. Unrestricted general project sponsorship is not assigned to any Phase by default."
},
"recognition": {
"event": "payment-settled"
},
"reversal": {
"rule": "A withdrawn or refunded sponsorship generates a compensating credit-reversal entry for the previously declared development_allocation."
},
"evidence": {
"requirement": "Sponsorship agreement or written declaration stating the target Phase and declared allocation, plus settled payment reference."
},
"status": "registered"
}

View file

@ -0,0 +1,24 @@
{
"id": "trsl:extension:service-with-development-allocation",
"version": "1.0",
"value": {
"description": "Installation, migration, configuration, integration, customization, support, or training service engagement."
},
"pricing": {
"method": "time-and-materials or fixed-fee, per service agreement"
},
"allocation": {
"rule": "Development allocation defaults to 0%. When a reusable deliverable from the engagement is incorporated into the governed Milestone Release, the reusable portion is explicitly split out and declared as development_allocation; customer-specific configuration is never allocated.",
"default_rate": 0.0
},
"recognition": {
"event": "payment-settled"
},
"reversal": {
"rule": "Refunds and chargebacks generate a compensating credit-reversal entry for the reversed amount."
},
"evidence": {
"requirement": "Settled payment reference; a statement identifying the reusable deliverable is required whenever a nonzero allocation is claimed."
},
"status": "registered"
}

View file

@ -0,0 +1,86 @@
[
{
"id": "trsl:entry:example0010001",
"phase": "trsl:phase:example-001",
"type": "development-credit",
"amount": 25000,
"currency": "USD",
"recognized_at": "2026-09-01T09:00:00Z",
"extension": {
"id": "trsl:extension:development-license",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-dc-0001",
"previous_entry_hash": "GENESIS"
},
{
"id": "trsl:entry:example0010002",
"phase": "trsl:phase:example-001",
"type": "development-credit",
"amount": 10000,
"currency": "USD",
"recognized_at": "2026-09-15T09:00:00Z",
"extension": {
"id": "trsl:extension:phase-sponsorship",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-dc-0002",
"previous_entry_hash": "5a67c0dc3407de30db7db5183fc3f609ae687dd529336b74fcba4a6ba75580c9"
},
{
"id": "trsl:entry:example0010003",
"phase": "trsl:phase:example-001",
"type": "development-credit",
"amount": 2000,
"currency": "USD",
"recognized_at": "2026-10-01T09:00:00Z",
"extension": {
"id": "trsl:extension:service-with-development-allocation",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-dc-0003",
"previous_entry_hash": "d9bc08b582a1ffb59adc818b93bf0946619f4a18bde8d4fa96ef07249a2ce58e"
},
{
"id": "trsl:entry:example0010004",
"phase": "trsl:phase:example-001",
"type": "remission-credit",
"amount": 18000,
"currency": "USD",
"recognized_at": "2026-12-01T00:00:00Z",
"extension": {
"id": "trsl:policy:linear-longstop-v0",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-rc-0001",
"previous_entry_hash": "1285da9f580663a8a3fce847ed64fc81c14b29d566243a7f65eb6b4237f5bf4a"
},
{
"id": "trsl:entry:example0010005",
"phase": "trsl:phase:example-001",
"type": "development-credit",
"amount": 30000,
"currency": "USD",
"recognized_at": "2027-03-01T09:00:00Z",
"extension": {
"id": "trsl:extension:development-license",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-dc-0004",
"previous_entry_hash": "244af54599234f8320e3f3f294d1e0890e62bbf1f1c5f4dd27b0d96abc9f330a"
},
{
"id": "trsl:entry:example0010006",
"phase": "trsl:phase:example-001",
"type": "remission-credit",
"amount": 15000,
"currency": "USD",
"recognized_at": "2027-06-01T00:00:00Z",
"extension": {
"id": "trsl:policy:linear-longstop-v0",
"version": "1.0"
},
"evidence_reference": "confidential:evidence:example-001-rc-0002",
"previous_entry_hash": "5c360e5510dd860d68198abf80d25d5bf7509b0527cd5acc5f5bea93d31b5e3b"
}
]

View file

@ -0,0 +1,32 @@
{
"framework": "TRF-0.1",
"license": "TRSL-0.1",
"phase": {
"id": "trsl:phase:example-001",
"milestone_release": {
"name": "release-1.0",
"source_revision": "abc123",
"artifact_sha256": "44cd1493bd179c1207c7025c1372cdad89aac114e151db27aba2bdf3d55c688a"
},
"initial_target": {
"amount": 100000,
"currency": "USD"
},
"target_basis": {
"estimated_effort_days": 1,
"daily_rate": 1000,
"approved_direct_costs": 0,
"target_multiple": 100
},
"future_license": "MIT",
"degeneration_policy": "trsl:policy:linear-longstop-v0@1.0",
"longstop_at": "2031-08-01T00:00:00Z",
"ledger": "examples/phase-001/ledger.json"
},
"extensions": [
"trsl:extension:development-license@1.0",
"trsl:extension:cost-plus-operations@1.0",
"trsl:extension:phase-sponsorship@1.0",
"trsl:extension:service-with-development-allocation@1.0"
]
}

26
pyproject.toml Normal file
View file

@ -0,0 +1,26 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "target-revenue"
version = "0.1.0"
description = "Stage 0 schemas, pure Outstanding Target fold, and offline validators for the Target Revenue Framework (TRF)."
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT-0" }
dependencies = [
"jsonschema>=4.21",
"cryptography>=42.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
]
[tool.hatch.build.targets.wheel]
packages = ["src/target_revenue"]
[tool.pytest.ini_options]
testpaths = ["tests"]

View file

@ -0,0 +1,52 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://target-revenue.example/schemas/conversion_attestation.schema.json",
"title": "Conversion Attestation",
"description": "Stage 0 schema per specs/TechnicalSpecificationDocument.md §3.5. Evidence of conversion only — never a precondition for conversion status (PRD FR-7/G6, working default Q13). Tooling must be able to recompute conversion from the Phase Manifest and Target Ledger alone, without this document.",
"type": "object",
"required": [
"phase",
"milestone_release",
"conversion_timestamp",
"future_license",
"final_development_credit",
"final_remission_credit",
"final_outstanding_target",
"ledger_checkpoint"
],
"additionalProperties": false,
"properties": {
"phase": {
"type": "string",
"pattern": "^trsl:phase:[a-zA-Z0-9._-]+$"
},
"milestone_release": {
"type": "string",
"description": "Must match phase.milestone_release.name in the Phase Manifest."
},
"conversion_timestamp": {
"type": "string",
"format": "date-time",
"description": "The moment the ledger fold first reaches Outstanding Target = 0."
},
"future_license": {
"type": "string",
"enum": ["MIT", "Apache-2.0"]
},
"final_development_credit": { "type": "number", "minimum": 0 },
"final_remission_credit": { "type": "number", "minimum": 0 },
"final_outstanding_target": {
"type": "number",
"const": 0,
"description": "MUST equal 0."
},
"ledger_checkpoint": {
"type": "string",
"description": "Points to the last ledger entry id or hash included in the fold."
},
"signature": {
"type": "string",
"description": "Ed25519 signature over the canonical serialization. Optional for Stage 0 fixtures."
}
}
}

View file

@ -0,0 +1,92 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://target-revenue.example/schemas/extension_contract.schema.json",
"title": "Monetization Extension Contract",
"description": "Stage 0 schema per specs/TechnicalSpecificationDocument.md §3.3. Structural conformance only — the semantic rule that allocation.rule may not redefine core terms is enforced in code (src/target_revenue/validation.py), not expressible in JSON Schema alone.",
"type": "object",
"required": [
"id",
"version",
"value",
"pricing",
"allocation",
"recognition",
"reversal",
"evidence",
"status"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^trsl:extension:[a-zA-Z0-9._-]+$"
},
"version": {
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)*$"
},
"value": {
"type": "object",
"required": ["description"],
"additionalProperties": false,
"properties": {
"description": { "type": "string", "minLength": 1 }
}
},
"pricing": {
"type": "object",
"required": ["method"],
"additionalProperties": false,
"properties": {
"method": { "type": "string", "minLength": 1 }
}
},
"allocation": {
"type": "object",
"required": ["rule"],
"additionalProperties": false,
"properties": {
"rule": { "type": "string", "minLength": 1 },
"default_rate": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Default development_allocation fraction, per canonical profile defaults (working default Q11)."
}
}
},
"recognition": {
"type": "object",
"required": ["event"],
"additionalProperties": false,
"properties": {
"event": {
"type": "string",
"enum": ["order", "invoice", "payment-settled", "delivery"],
"description": "Working default Q10: payment-settled only for Stage 0 fold inputs."
}
}
},
"reversal": {
"type": "object",
"required": ["rule"],
"additionalProperties": false,
"properties": {
"rule": { "type": "string", "minLength": 1 }
}
},
"evidence": {
"type": "object",
"required": ["requirement"],
"additionalProperties": false,
"properties": {
"requirement": { "type": "string", "minLength": 1 }
}
},
"status": {
"type": "string",
"enum": ["registered", "canonical", "deprecated"],
"description": "Assigned by Trust Service / maintainer review, not the extension author. canonical promotion is a documented human action (SCOPE §4)."
}
}
}

View file

@ -0,0 +1,108 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://target-revenue.example/schemas/ledger_entry.schema.json",
"title": "Target Ledger Entry",
"description": "Stage 0 schema per specs/TechnicalSpecificationDocument.md §3.2 and specs/OpenQuestions-WorkingDefaults.md Q6/Q10/Q14. Append-only; corrections are new compensating entries, never mutation.",
"type": "object",
"required": [
"id",
"phase",
"type",
"amount",
"currency",
"recognized_at",
"evidence_reference",
"previous_entry_hash"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^trsl:entry:[a-zA-Z0-9]+$",
"description": "Globally unique, monotonically orderable (ULID or equivalent)."
},
"phase": {
"type": "string",
"pattern": "^trsl:phase:[a-zA-Z0-9._-]+$"
},
"type": {
"type": "string",
"enum": [
"development-credit",
"remission-credit",
"credit-reversal",
"remission-correction",
"administrative-correction",
"conversion-checkpoint"
],
"description": "Closed set per TSD §3.2; not extensible per-project."
},
"amount": {
"type": "number",
"description": "Positive for credits; the fold interprets sign by entry type (see fold.py). Reversal/correction entries reduce their target's cumulative total."
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "Must match the Phase's initial_target.currency (working default Q6). No FX conversion in the pure fold."
},
"recognized_at": {
"type": "string",
"format": "date-time",
"description": "Settlement time, not invoice time (Rule 4 / working default Q10 payment-settled)."
},
"extension": {
"type": "object",
"description": "Required for development-credit/remission-credit entries.",
"required": ["id", "version"],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^trsl:(extension|policy):[a-zA-Z0-9._-]+$"
},
"version": { "type": "string" }
}
},
"evidence_reference": {
"type": "string",
"description": "URI, may use the confidential: scheme. Tiering per working default Q10 (E0/E1/E2)."
},
"previous_entry_hash": {
"type": "string",
"pattern": "^[a-f0-9]{64}$|^GENESIS$",
"description": "SHA-256 hex digest of the previous entry's canonical serialization for this Phase, or the literal GENESIS for the first entry."
},
"signature": {
"type": "string",
"description": "Ed25519 signature over the canonical serialization (working default Q14). Optional in Stage 0 fixtures, required for any public claim."
},
"reverses": {
"type": "string",
"pattern": "^trsl:entry:[a-zA-Z0-9]+$",
"description": "Required on credit-reversal and remission-correction entries: the entry id being reversed or corrected."
}
},
"allOf": [
{
"if": {
"properties": { "type": { "const": "credit-reversal" } }
},
"then": { "required": ["reverses"] }
},
{
"if": {
"properties": { "type": { "const": "remission-correction" } }
},
"then": { "required": ["reverses"] }
},
{
"if": {
"properties": {
"type": { "enum": ["development-credit", "remission-credit"] }
}
},
"then": { "required": ["extension"] }
}
]
}

View file

@ -0,0 +1,108 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://target-revenue.example/schemas/phase_manifest.schema.json",
"title": "Phase Manifest",
"description": "Stage 0 schema per specs/TechnicalSpecificationDocument.md §3.1 and specs/OpenQuestions-WorkingDefaults.md Q3/Q6/Q8. Field names and semantics are authoritative from spec/TargetRevenueLicenseConcept.md §16.",
"type": "object",
"required": ["framework", "license", "phase"],
"additionalProperties": false,
"properties": {
"framework": {
"type": "string",
"description": "Framework version tag, e.g. TRF-0.1."
},
"license": {
"type": "string",
"description": "TRSL version tag, e.g. TRSL-0.1."
},
"phase": {
"type": "object",
"required": [
"id",
"milestone_release",
"initial_target",
"future_license",
"degeneration_policy",
"longstop_at",
"ledger"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^trsl:phase:[a-zA-Z0-9._-]+$",
"description": "Globally unique, immutable once published."
},
"milestone_release": {
"type": "object",
"required": ["name", "source_revision"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"source_revision": { "type": "string" },
"artifact_sha256": {
"type": "string",
"pattern": "^[a-f0-9]{64}$",
"description": "Recommended; required if a built artifact (not just source) is governed."
}
}
},
"initial_target": {
"type": "object",
"required": ["amount", "currency"],
"additionalProperties": false,
"properties": {
"amount": { "type": "number", "exclusiveMinimum": 0 },
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$",
"description": "ISO 4217 code. Working default Q6: one native currency per Phase."
}
}
},
"target_basis": {
"type": "object",
"description": "Recommended transparency metadata (working default Q5), not legally required.",
"additionalProperties": false,
"properties": {
"estimated_effort_days": { "type": "number", "minimum": 0 },
"daily_rate": { "type": "number", "minimum": 0 },
"approved_direct_costs": { "type": "number", "minimum": 0 },
"target_multiple": {
"type": "number",
"minimum": 0,
"description": "Open decimal per working default Q4. Guidance classes {0,1,10,100,1000} are not a closed enum."
}
}
},
"future_license": {
"type": "string",
"enum": ["MIT", "Apache-2.0"],
"description": "Working default Q3: closed enum for Stage 0."
},
"degeneration_policy": {
"type": "string",
"pattern": "^trsl:policy:[a-zA-Z0-9._-]+@[0-9]+(\\.[0-9]+)*$",
"description": "e.g. trsl:policy:linear-longstop-v0@1.0 (working default Q7)."
},
"longstop_at": {
"type": "string",
"format": "date-time",
"description": "Required for Stage 0 per working default Q8. Full-remission / maximum-protection instant."
},
"ledger": {
"type": "string",
"description": "URL or relative path URI to the authoritative Target Ledger for this Phase."
}
}
},
"extensions": {
"type": "array",
"items": {
"type": "string",
"pattern": "^trsl:extension:[a-zA-Z0-9._-]+@[0-9]+(\\.[0-9]+)*$"
},
"description": "Optional. Applicable monetization profiles/extensions for this Phase."
}
}
}

View file

@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""Regenerate examples/phase-001/ledger.json with a correct hash chain.
The golden Phase package must exercise a real SHA-256 hash chain, not
hand-typed digests. This script builds the entry sequence from
spec/TargetRevenueLicenseConcept.md §23's illustrative example and writes it
out via the same hashing.entry_hash() the library and tests use, so the
fixture and the code that verifies it can never silently drift apart.
Run: python3 scripts/generate_golden_phase.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO_ROOT / "src"))
from target_revenue import hashing # noqa: E402
PHASE_ID = "trsl:phase:example-001"
def entry(
entry_id: str,
entry_type: str,
amount: float,
recognized_at: str,
extension_id: str,
extension_version: str,
evidence_reference: str,
previous_entry_hash: str,
) -> dict:
return {
"id": entry_id,
"phase": PHASE_ID,
"type": entry_type,
"amount": amount,
"currency": "USD",
"recognized_at": recognized_at,
"extension": {"id": extension_id, "version": extension_version},
"evidence_reference": evidence_reference,
"previous_entry_hash": previous_entry_hash,
}
def main() -> None:
entries: list[dict] = []
previous_hash = hashing.GENESIS
specs = [
# (id, type, amount, recognized_at, extension_id, evidence_reference)
(
"trsl:entry:example0010001",
"development-credit",
25000,
"2026-09-01T09:00:00Z",
"trsl:extension:development-license",
"confidential:evidence:example-001-dc-0001",
),
(
"trsl:entry:example0010002",
"development-credit",
10000,
"2026-09-15T09:00:00Z",
"trsl:extension:phase-sponsorship",
"confidential:evidence:example-001-dc-0002",
),
(
"trsl:entry:example0010003",
"development-credit",
2000,
"2026-10-01T09:00:00Z",
"trsl:extension:service-with-development-allocation",
"confidential:evidence:example-001-dc-0003",
),
(
"trsl:entry:example0010004",
"remission-credit",
18000,
"2026-12-01T00:00:00Z",
"trsl:policy:linear-longstop-v0",
"confidential:evidence:example-001-rc-0001",
),
(
"trsl:entry:example0010005",
"development-credit",
30000,
"2027-03-01T09:00:00Z",
"trsl:extension:development-license",
"confidential:evidence:example-001-dc-0004",
),
(
"trsl:entry:example0010006",
"remission-credit",
15000,
"2027-06-01T00:00:00Z",
"trsl:policy:linear-longstop-v0",
"confidential:evidence:example-001-rc-0002",
),
]
for entry_id, entry_type, amount, recognized_at, ext_id, evidence in specs:
record = entry(
entry_id,
entry_type,
amount,
recognized_at,
ext_id,
"1.0",
evidence,
previous_hash,
)
entries.append(record)
previous_hash = hashing.entry_hash(record)
out_path = REPO_ROOT / "examples" / "phase-001" / "ledger.json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(entries, f, indent=2)
f.write("\n")
print(f"wrote {len(entries)} entries to {out_path}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,14 @@
"""Stage 0 schemas, pure fold, and offline validators for the Target Revenue Framework.
See specs/TechnicalSpecificationDocument.md for the authoritative data model
and specs/OpenQuestions-WorkingDefaults.md for provisional Stage 0 defaults.
This package implements schema-shaped, pure-function tooling only; it is not
a hosted Trust Service (SCOPE.md §3).
"""
__all__ = [
"hashing",
"validation",
"fold",
"conversion",
]

View file

@ -0,0 +1,80 @@
"""Conversion Event detection and Conversion Attestation generation.
TSD §3.5 / PRD FR-7 / G6 / working default Q13: the Conversion Event is a
pure fact about the Manifest + Ledger, never a discretionary declaration.
`is_converted` and `conversion_status` below never require an attestation
document to answer "has this Phase converted?" the attestation, when
generated, is evidence of an already-true fact, not its cause.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from . import fold as fold_module
from . import validation
@dataclass(frozen=True)
class ConversionStatus:
"""Pure, offline-computable conversion status for a Phase."""
is_converted: bool
development_credit: float
remission_credit: float
outstanding_target: float
future_license: str
def conversion_status(
manifest: dict[str, Any], entries: list[dict[str, Any]]
) -> ConversionStatus:
"""Compute conversion status directly from Manifest + Ledger.
No attestation document is consulted or required. This function is the
canonical "has this Phase converted" answer for any conformant tool.
"""
result = fold_module.fold_phase(manifest, entries)
return ConversionStatus(
is_converted=result.is_converted,
development_credit=result.development_credit,
remission_credit=result.remission_credit,
outstanding_target=result.outstanding_target,
future_license=manifest["phase"]["future_license"],
)
def generate_attestation(
manifest: dict[str, Any],
entries: list[dict[str, Any]],
conversion_timestamp: str,
) -> dict[str, Any]:
"""Build a Conversion Attestation document for an already-converted Phase.
Raises ValueError if the Phase has not actually converted (Outstanding
Target > 0) an attestation must never assert a conversion that hasn't
happened, and must never be required to make one happen.
"""
status = conversion_status(manifest, entries)
if not status.is_converted:
raise ValueError(
"cannot generate a Conversion Attestation: Outstanding Target "
f"is {status.outstanding_target}, not 0"
)
last_entry = entries[-1] if entries else None
checkpoint = last_entry["id"] if last_entry else "GENESIS"
attestation = {
"phase": manifest["phase"]["id"],
"milestone_release": manifest["phase"]["milestone_release"]["name"],
"conversion_timestamp": conversion_timestamp,
"future_license": status.future_license,
"final_development_credit": status.development_credit,
"final_remission_credit": status.remission_credit,
"final_outstanding_target": status.outstanding_target,
"ledger_checkpoint": checkpoint,
}
validation.validate_conversion_attestation(attestation)
return attestation

109
src/target_revenue/fold.py Normal file
View file

@ -0,0 +1,109 @@
"""Pure Outstanding Target fold over a Phase's Target Ledger.
Outstanding Target = max(0, Initial Target - Development Credit - Remission
Credit), per spec/TargetRevenueLicenseConcept.md §7.7/§13 and
specs/TechnicalSpecificationDocument.md §3.2. This module contains no I/O:
given the same (initial_target_amount, entries) it always returns the same
result (TSD §6.1 determinism), so any independent implementation can
recompute the same Outstanding Target from the same Manifest + Ledger.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from . import hashing
#: Entry types that do not carry a numeric effect on the fold.
_INFORMATIONAL_TYPES = frozenset({"conversion-checkpoint"})
_KNOWN_TYPES = frozenset(
{
"development-credit",
"remission-credit",
"credit-reversal",
"remission-correction",
"administrative-correction",
"conversion-checkpoint",
}
)
@dataclass(frozen=True)
class FoldResult:
"""Result of folding a Phase's ledger to a point in the entry sequence."""
development_credit: float
remission_credit: float
outstanding_target: float
entries_applied: int
@property
def is_converted(self) -> bool:
return self.outstanding_target <= 0
def fold_outstanding_target(
initial_target_amount: float, entries: list[dict[str, Any]]
) -> FoldResult:
"""Fold an ordered list of ledger entries into cumulative totals.
Entry-type effects (Stage 0):
development-credit += amount to Development Credit
remission-credit += amount to Remission Credit
credit-reversal -= amount from Development Credit (Rule 8:
compensating entry, never edits the
original)
remission-correction += amount to Remission Credit (amount may be
negative to correct an over-remission)
administrative-correction += amount to Development Credit (Stage 0
simplification: a generic correction
bucket; not yet specialized by target side)
conversion-checkpoint no numeric effect (marker only)
"""
development_credit = 0.0
remission_credit = 0.0
for entry in entries:
entry_type = entry["type"]
if entry_type not in _KNOWN_TYPES:
raise ValueError(f"unknown ledger entry type: {entry_type!r}")
if entry_type in _INFORMATIONAL_TYPES:
continue
amount = entry["amount"]
if entry_type == "development-credit":
development_credit += amount
elif entry_type == "remission-credit":
remission_credit += amount
elif entry_type == "credit-reversal":
development_credit -= amount
elif entry_type == "remission-correction":
remission_credit += amount
elif entry_type == "administrative-correction":
development_credit += amount
outstanding_target = max(
0.0, initial_target_amount - development_credit - remission_credit
)
return FoldResult(
development_credit=development_credit,
remission_credit=remission_credit,
outstanding_target=outstanding_target,
entries_applied=len(entries),
)
def fold_phase(
manifest: dict[str, Any], entries: list[dict[str, Any]]
) -> FoldResult:
"""Verify a Phase's ledger chain and compute its Outstanding Target.
Raises ValueError if the hash chain is broken. Does not perform schema
or currency validation call src.target_revenue.validation for that
before folding untrusted input.
"""
hashing.verify_chain(entries)
initial_target_amount = manifest["phase"]["initial_target"]["amount"]
return fold_outstanding_target(initial_target_amount, entries)

View file

@ -0,0 +1,75 @@
"""Canonical serialization, SHA-256 hash chaining, and Ed25519 signing.
Working default Q14 (specs/OpenQuestions-WorkingDefaults.md): hash chain is
SHA-256 over a canonical JSON serialization; signatures are Ed25519 over the
same canonical bytes. This module is the single source of truth for what
"canonical serialization" means so hashing and signing never drift.
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
GENESIS = "GENESIS"
def canonical_bytes(record: dict[str, Any]) -> bytes:
"""Deterministic byte representation of a record for hashing/signing.
Sorted keys, no insignificant whitespace, UTF-8. The `signature` and
`previous_entry_hash` fields are excluded because they are computed from
(or alongside) this representation, not part of what they attest to.
"""
payload = {
k: v
for k, v in record.items()
if k not in ("signature", "previous_entry_hash")
}
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
def entry_hash(record: dict[str, Any]) -> str:
"""SHA-256 hex digest of a ledger entry's canonical bytes."""
return hashlib.sha256(canonical_bytes(record)).hexdigest()
def verify_chain(entries: list[dict[str, Any]]) -> None:
"""Raise ValueError on the first broken link in an entry chain.
Entries must already be in ledger (append) order. The first entry's
`previous_entry_hash` must equal GENESIS; every subsequent entry's
`previous_entry_hash` must equal the SHA-256 of the prior entry's
canonical bytes.
"""
expected = GENESIS
for i, entry in enumerate(entries):
actual = entry.get("previous_entry_hash")
if actual != expected:
raise ValueError(
f"hash chain broken at entry index {i} (id={entry.get('id')!r}): "
f"expected previous_entry_hash={expected!r}, got {actual!r}"
)
expected = entry_hash(entry)
def sign_record(record: dict[str, Any], private_key: Ed25519PrivateKey) -> str:
"""Ed25519 signature (hex) over the record's canonical bytes."""
return private_key.sign(canonical_bytes(record)).hex()
def verify_record_signature(
record: dict[str, Any], signature_hex: str, public_key: Ed25519PublicKey
) -> bool:
"""True if signature_hex is a valid Ed25519 signature over record's canonical bytes."""
try:
public_key.verify(bytes.fromhex(signature_hex), canonical_bytes(record))
return True
except Exception:
return False

View file

@ -0,0 +1,176 @@
"""Offline, pure conformance validators for TRF Stage 0 schemas.
Validates against the .schema.json files under schemas/ (structural
conformance) plus a small set of business rules that JSON Schema cannot
express: cross-record currency consistency (working default Q6), manifest
immutability across versions (Rule 1), and the extension-contract rule that
`allocation.rule` may not redefine core terms (TSD §3.3, PRD FR-4).
Every function here is pure: given the same input dict(s), it always
produces the same result, with no I/O beyond reading the static schema files
at import/call time (TSD §6.1 determinism).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import jsonschema
_SCHEMAS_DIR = Path(__file__).resolve().parents[2] / "schemas"
# TSD §6.2/§3.3: an extension's allocation.rule must not redefine these.
CORE_TERMS = [
"Phase",
"Milestone Release",
"Initial Target",
"Development Credit",
"Remission Credit",
"Outstanding Target",
"Conversion Event",
"Future License",
]
class ConformanceError(ValueError):
"""Raised with all collected validation errors for a single record."""
def __init__(self, errors: list[str]):
self.errors = errors
super().__init__("; ".join(errors))
def _load_schema(filename: str) -> dict[str, Any]:
with open(_SCHEMAS_DIR / filename, encoding="utf-8") as f:
return json.load(f)
def _schema_errors(schema_filename: str, record: dict[str, Any]) -> list[str]:
schema = _load_schema(schema_filename)
validator = jsonschema.Draft202012Validator(schema)
return [
f"{'/'.join(str(p) for p in e.path) or '<root>'}: {e.message}"
for e in sorted(validator.iter_errors(record), key=lambda e: list(e.path))
]
# --- Phase Manifest --------------------------------------------------------
def validate_phase_manifest(manifest: dict[str, Any]) -> None:
"""Raise ConformanceError if the manifest is not schema-conformant."""
errors = _schema_errors("phase_manifest.schema.json", manifest)
if errors:
raise ConformanceError(errors)
def check_manifest_immutability(
published: dict[str, Any], candidate: dict[str, Any]
) -> list[str]:
"""Compare a published manifest against a proposed replacement.
Returns a list of violations (empty if none). Per Rule 1: phase.id must
not change, and phase.initial_target.amount must not change except
through an explicit, versioned correction record distinct from this
check (Stage 0 has no such record type yet, so any change here is
flagged).
"""
errors: list[str] = []
old_phase = published.get("phase", {})
new_phase = candidate.get("phase", {})
if old_phase.get("id") != new_phase.get("id"):
errors.append(
f"phase.id changed from {old_phase.get('id')!r} to {new_phase.get('id')!r}"
)
old_target = old_phase.get("initial_target", {})
new_target = new_phase.get("initial_target", {})
if old_target.get("amount") != new_target.get("amount"):
errors.append(
"phase.initial_target.amount changed from "
f"{old_target.get('amount')!r} to {new_target.get('amount')!r} "
"without an explicit, versioned correction record"
)
if old_target.get("currency") != new_target.get("currency"):
errors.append(
"phase.initial_target.currency changed from "
f"{old_target.get('currency')!r} to {new_target.get('currency')!r}"
)
return errors
# --- Target Ledger Entry ----------------------------------------------------
def validate_ledger_entry(entry: dict[str, Any]) -> None:
"""Raise ConformanceError if the entry is not schema-conformant."""
errors = _schema_errors("ledger_entry.schema.json", entry)
if errors:
raise ConformanceError(errors)
def check_currency_consistency(
manifest: dict[str, Any], entries: list[dict[str, Any]]
) -> list[str]:
"""Working default Q6: every entry's currency must match the Phase's."""
native = manifest.get("phase", {}).get("initial_target", {}).get("currency")
errors = []
for entry in entries:
if entry.get("currency") != native:
errors.append(
f"entry {entry.get('id')!r} currency {entry.get('currency')!r} "
f"does not match Phase native currency {native!r}"
)
return errors
# --- Monetization Extension Contract ---------------------------------------
def check_extension_core_term_redefinition(extension: dict[str, Any]) -> list[str]:
"""Heuristic check that allocation.rule does not redefine a core term.
Stage 0 limitation: this is a pattern-based heuristic (assignment-like
phrasing immediately following a core term name), not a full semantic
check. It is deterministic and sufficient to reject the deliberately
non-conforming fixture required by WP-0002-T04; it is not a substitute
for human review of new extensions before promotion to `canonical`.
"""
rule_text = extension.get("allocation", {}).get("rule", "")
normalized = rule_text.lower()
errors: list[str] = []
forbidden_patterns = ("=", "redefine", "means", "is defined as", "shall be")
for term in CORE_TERMS:
term_l = term.lower()
if term_l not in normalized:
continue
for pattern in forbidden_patterns:
if f"{term_l} {pattern}" in normalized or f"{term_l}{pattern}" in normalized:
errors.append(
f"allocation.rule appears to redefine core term {term!r} "
f"(matched {term_l + ' ' + pattern!r})"
)
return errors
def validate_extension_contract(extension: dict[str, Any]) -> None:
"""Raise ConformanceError on structural or core-term-redefinition failure.
A non-conforming extension is rejected here, not silently accepted with
reduced trust (TSD §3.3 conformance rule).
"""
errors = _schema_errors("extension_contract.schema.json", extension)
errors += check_extension_core_term_redefinition(extension)
if errors:
raise ConformanceError(errors)
# --- Conversion Attestation --------------------------------------------------
def validate_conversion_attestation(attestation: dict[str, Any]) -> None:
"""Raise ConformanceError if the attestation is not schema-conformant."""
errors = _schema_errors("conversion_attestation.schema.json", attestation)
if errors:
raise ConformanceError(errors)

28
tests/conftest.py Normal file
View file

@ -0,0 +1,28 @@
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SRC = REPO_ROOT / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))
EXAMPLES_DIR = REPO_ROOT / "examples" / "phase-001"
FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures"
def load_json(path: Path) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def golden_manifest() -> dict:
return load_json(EXAMPLES_DIR / "manifest.json")
def golden_entries() -> list[dict]:
return load_json(EXAMPLES_DIR / "ledger.json")
def golden_extension(name: str) -> dict:
return load_json(EXAMPLES_DIR / "extensions" / f"{name}.json")

View file

@ -0,0 +1,24 @@
{
"id": "trsl:extension:bad-redefinition",
"version": "1.0",
"value": {
"description": "Deliberately non-conforming fixture for WP-0002-T04: attempts to redefine a core term instead of only classifying a payment."
},
"pricing": {
"method": "flat-fee"
},
"allocation": {
"rule": "Outstanding Target = 0 for any payment received under this extension, regardless of the Phase ledger.",
"default_rate": 1.0
},
"recognition": {
"event": "payment-settled"
},
"reversal": {
"rule": "No reversals."
},
"evidence": {
"requirement": "None."
},
"status": "registered"
}

52
tests/test_conversion.py Normal file
View file

@ -0,0 +1,52 @@
import pytest
from conftest import golden_entries, golden_manifest
from target_revenue import conversion, validation
def test_conversion_status_not_converted_partway_through():
manifest = golden_manifest()
status = conversion.conversion_status(manifest, golden_entries()[:4])
assert status.is_converted is False
assert status.outstanding_target == 45000
def test_conversion_status_converted_after_full_sequence():
manifest = golden_manifest()
status = conversion.conversion_status(manifest, golden_entries())
assert status.is_converted is True
assert status.outstanding_target == 0
assert status.future_license == "MIT"
def test_conversion_status_requires_no_attestation_document():
"""PRD FR-7/G6: conversion status must be computable from Manifest +
Ledger alone, with no attestation file consulted or required."""
manifest = golden_manifest()
entries = golden_entries()
status_without_attestation = conversion.conversion_status(manifest, entries)
assert status_without_attestation.is_converted is True
def test_generate_attestation_for_converted_phase():
manifest = golden_manifest()
entries = golden_entries()
attestation = conversion.generate_attestation(
manifest, entries, conversion_timestamp="2027-06-01T00:00:00Z"
)
validation.validate_conversion_attestation(attestation)
assert attestation["final_outstanding_target"] == 0
assert attestation["final_development_credit"] == 67000
assert attestation["final_remission_credit"] == 33000
assert attestation["future_license"] == "MIT"
assert attestation["phase"] == "trsl:phase:example-001"
assert attestation["ledger_checkpoint"] == entries[-1]["id"]
def test_generate_attestation_rejects_unconverted_phase():
manifest = golden_manifest()
partial_entries = golden_entries()[:4] # Outstanding Target still 45000
with pytest.raises(ValueError, match="Outstanding Target"):
conversion.generate_attestation(
manifest, partial_entries, conversion_timestamp="2026-12-01T00:00:00Z"
)

View file

@ -0,0 +1,55 @@
import copy
import pytest
from conftest import golden_extension, load_json, FIXTURES_DIR
from target_revenue import validation
@pytest.mark.parametrize(
"name",
[
"development-license",
"cost-plus-operations",
"phase-sponsorship",
"service-with-development-allocation",
],
)
def test_canonical_stage0_extensions_are_conformant(name):
validation.validate_extension_contract(golden_extension(name))
def test_non_conforming_extension_is_rejected():
extension = load_json(FIXTURES_DIR / "non_conforming_extension.json")
with pytest.raises(validation.ConformanceError) as excinfo:
validation.validate_extension_contract(extension)
assert "Outstanding Target" in str(excinfo.value)
def test_extension_missing_required_field_is_rejected():
extension = copy.deepcopy(golden_extension("development-license"))
del extension["evidence"]
with pytest.raises(validation.ConformanceError):
validation.validate_extension_contract(extension)
def test_extension_bad_status_is_rejected():
extension = copy.deepcopy(golden_extension("development-license"))
extension["status"] = "endorsed" # not in {registered, canonical, deprecated}
with pytest.raises(validation.ConformanceError):
validation.validate_extension_contract(extension)
def test_core_term_redefinition_heuristic_is_case_insensitive():
extension = copy.deepcopy(golden_extension("development-license"))
extension["allocation"]["rule"] = "Development Credit means whatever the licensor prefers."
errors = validation.check_extension_core_term_redefinition(extension)
assert errors
assert "Development Credit" in errors[0]
def test_legitimate_rule_referencing_core_term_without_redefining_is_allowed():
extension = golden_extension("development-license")
# References "Development Credit" as a target, not a redefinition.
errors = validation.check_extension_core_term_redefinition(extension)
assert errors == []

133
tests/test_ledger_fold.py Normal file
View file

@ -0,0 +1,133 @@
import copy
import pytest
from conftest import golden_entries, golden_manifest
from target_revenue import fold, hashing, validation
def test_golden_entries_are_individually_conformant():
for entry in golden_entries():
validation.validate_ledger_entry(entry)
def test_golden_entries_currency_matches_phase():
errors = validation.check_currency_consistency(golden_manifest(), golden_entries())
assert errors == []
def test_currency_mismatch_is_detected():
manifest = golden_manifest()
entries = copy.deepcopy(golden_entries())
entries[0]["currency"] = "EUR"
errors = validation.check_currency_consistency(manifest, entries)
assert len(errors) == 1
assert "EUR" in errors[0]
def test_golden_hash_chain_is_valid():
hashing.verify_chain(golden_entries())
def test_tampered_entry_breaks_hash_chain():
entries = copy.deepcopy(golden_entries())
entries[2]["amount"] = 999999 # tamper after the chain was computed
with pytest.raises(ValueError, match="hash chain broken"):
hashing.verify_chain(entries)
def test_reordered_entries_break_hash_chain():
entries = copy.deepcopy(golden_entries())
entries[1], entries[2] = entries[2], entries[1]
with pytest.raises(ValueError, match="hash chain broken"):
hashing.verify_chain(entries)
def test_fold_matches_concept_section_23_first_checkpoint():
"""spec/TargetRevenueLicenseConcept.md §23: after the first four entries,
Development Credit $37,000, Remission Credit $18,000, Outstanding $45,000."""
manifest = golden_manifest()
first_four = golden_entries()[:4]
result = fold.fold_outstanding_target(
manifest["phase"]["initial_target"]["amount"], first_four
)
assert result.development_credit == 37000
assert result.remission_credit == 18000
assert result.outstanding_target == 45000
assert not result.is_converted
def test_fold_reaches_zero_and_converts():
"""spec/TargetRevenueLicenseConcept.md §23: full six-entry sequence
reaches Outstanding Target = 0 (Development Credit $67,000 + Remission
Credit $33,000 = Initial Target $100,000)."""
manifest = golden_manifest()
result = fold.fold_phase(manifest, golden_entries())
assert result.development_credit == 67000
assert result.remission_credit == 33000
assert result.outstanding_target == 0
assert result.is_converted
def test_credit_reversal_reduces_development_credit():
entries = [
{
"id": "trsl:entry:t0001",
"phase": "trsl:phase:t",
"type": "development-credit",
"amount": 5000,
"currency": "USD",
"recognized_at": "2026-01-01T00:00:00Z",
"extension": {"id": "trsl:extension:development-license", "version": "1.0"},
"evidence_reference": "confidential:evidence:t0001",
"previous_entry_hash": "GENESIS",
},
]
entries.append(
{
"id": "trsl:entry:t0002",
"phase": "trsl:phase:t",
"type": "credit-reversal",
"amount": 5000,
"currency": "USD",
"recognized_at": "2026-01-02T00:00:00Z",
"evidence_reference": "confidential:evidence:t0002-refund",
"previous_entry_hash": hashing.entry_hash(entries[0]),
"reverses": "trsl:entry:t0001",
}
)
result = fold.fold_outstanding_target(100000, entries)
assert result.development_credit == 0
assert result.outstanding_target == 100000
def test_remission_correction_can_reduce_over_remission():
entries = [
{
"id": "trsl:entry:t0001",
"phase": "trsl:phase:t",
"type": "remission-credit",
"amount": 20000,
"currency": "USD",
"recognized_at": "2026-01-01T00:00:00Z",
"extension": {"id": "trsl:policy:linear-longstop-v0", "version": "1.0"},
"evidence_reference": "confidential:evidence:t0001",
"previous_entry_hash": "GENESIS",
},
]
entries.append(
{
"id": "trsl:entry:t0002",
"phase": "trsl:phase:t",
"type": "remission-correction",
"amount": -5000,
"currency": "USD",
"recognized_at": "2026-01-02T00:00:00Z",
"evidence_reference": "confidential:evidence:t0002-correction",
"previous_entry_hash": hashing.entry_hash(entries[0]),
"reverses": "trsl:entry:t0001",
}
)
result = fold.fold_outstanding_target(100000, entries)
assert result.remission_credit == 15000
assert result.outstanding_target == 85000

View file

@ -0,0 +1,61 @@
import copy
import pytest
from conftest import golden_manifest
from target_revenue import validation
def test_golden_manifest_is_conformant():
validation.validate_phase_manifest(golden_manifest())
def test_manifest_missing_longstop_is_rejected():
manifest = copy.deepcopy(golden_manifest())
del manifest["phase"]["longstop_at"]
with pytest.raises(validation.ConformanceError):
validation.validate_phase_manifest(manifest)
def test_manifest_bad_future_license_is_rejected():
manifest = copy.deepcopy(golden_manifest())
manifest["phase"]["future_license"] = "GPL-3.0"
with pytest.raises(validation.ConformanceError):
validation.validate_phase_manifest(manifest)
def test_manifest_missing_required_field_is_rejected():
manifest = copy.deepcopy(golden_manifest())
del manifest["phase"]["initial_target"]
with pytest.raises(validation.ConformanceError):
validation.validate_phase_manifest(manifest)
def test_manifest_rejects_unknown_top_level_field():
manifest = copy.deepcopy(golden_manifest())
manifest["unexpected_field"] = True
with pytest.raises(validation.ConformanceError):
validation.validate_phase_manifest(manifest)
def test_initial_target_amount_immutable_across_versions():
published = golden_manifest()
candidate = copy.deepcopy(published)
candidate["phase"]["initial_target"]["amount"] = 70000
errors = validation.check_manifest_immutability(published, candidate)
assert errors, "reducing initial_target.amount must be flagged"
assert "initial_target.amount" in errors[0]
def test_phase_id_immutable_across_versions():
published = golden_manifest()
candidate = copy.deepcopy(published)
candidate["phase"]["id"] = "trsl:phase:example-002"
errors = validation.check_manifest_immutability(published, candidate)
assert errors
assert "phase.id" in errors[0]
def test_unchanged_manifest_has_no_immutability_violations():
manifest = golden_manifest()
assert validation.check_manifest_immutability(manifest, manifest) == []

View file

@ -51,6 +51,14 @@ state_hub_task_id: "bdaa3e6a-e13d-4d4f-a6b0-9c56c18850a9"
human_accept_required: true
```
Result 2026-07-28: Drafted `docs/adr/ADR-0001-stage0-library-stack.md`
(status: **proposed**) — Python 3.11+, `jsonschema`, `pytest`, `hatchling`
src-layout; SHA-256 canonical-serialization hash chain; Ed25519 signing.
T02T06 were implemented against this proposal so work could proceed in
parallel, per the workplan note that "agents may draft the ADR for review."
**This task stays `todo`** until a maintainer accepts or revises the ADR —
implementation proceeding does not constitute acceptance.
`specs/TechnicalSpecificationDocument.md` is non-binding on language and
storage (§10). Record an ADR for the **Stage 0 library only**: language for
JSON Schema (or equivalent) validators and the pure fold, canonical
@ -68,11 +76,18 @@ technology choices. Agents may draft the ADR for review.
```task
id: TREV-WP-0002-T02
status: todo
status: done
priority: high
state_hub_task_id: "92568684-2dff-497c-9593-7a0e91cb95a3"
```
Result 2026-07-28: `schemas/phase_manifest.schema.json` +
`src/target_revenue/validation.py::validate_phase_manifest` /
`check_manifest_immutability`. `longstop_at` required, `future_license`
closed to `{MIT, Apache-2.0}`, `phase.id`/`initial_target` immutability
checked across manifest versions. Covered by
`tests/test_manifest_validation.py` (8 tests, all passing).
Implement machine-readable schema + pure validator for Phase Manifest
(TSD §3.1), applying Stage 0 working defaults:
@ -89,11 +104,19 @@ this workplan.
```task
id: TREV-WP-0002-T03
status: todo
status: done
priority: high
state_hub_task_id: "df4133ee-9776-438d-901a-78a906356cfb"
```
Result 2026-07-28: `schemas/ledger_entry.schema.json` (six-type closed enum,
`reverses` required on `credit-reversal`/`remission-correction`);
`src/target_revenue/hashing.py` (SHA-256 canonical-serialization chain +
Ed25519 sign/verify helpers); `src/target_revenue/fold.py`
(`fold_outstanding_target` / `fold_phase`, pure `max(0, T0-C-R)`). Covered by
`tests/test_ledger_fold.py` (10 tests: currency mismatch, tamper/reorder
detection, §23 checkpoint numbers, reversal/correction paths).
Implement ledger entry schema (TSD §3.2): six entry types, currency match to
Phase native currency (Q6), `previous_entry_hash` chain, signature field
shape. Implement Outstanding Target as a pure fold:
@ -108,11 +131,19 @@ hosted append service is out of scope.
```task
id: TREV-WP-0002-T04
status: todo
status: done
priority: medium
state_hub_task_id: "8454269e-fb3d-4751-b564-ef0cc33f9c97"
```
Result 2026-07-28: `schemas/extension_contract.schema.json` +
`validation.validate_extension_contract` /
`check_extension_core_term_redefinition` (documented as a Stage 0 pattern
heuristic, not full semantic review). Four working-default-Q11 fixtures
under `examples/phase-001/extensions/` (all `registered`); one deliberately
non-conforming fixture at `tests/fixtures/non_conforming_extension.json`.
Covered by `tests/test_extension_conformance.py` (9 tests).
Implement Monetization Extension Contract schema (TSD §3.3) and conformance
check: required fields; `allocation.rule` must not redefine core terms.
Support status values `registered` / `canonical` / `deprecated` as data;
@ -125,11 +156,18 @@ practical).
```task
id: TREV-WP-0002-T05
status: todo
status: done
priority: high
state_hub_task_id: "e60c8ccf-cd98-4b15-8ad2-e7718b08acc4"
```
Result 2026-07-28: `schemas/conversion_attestation.schema.json` +
`src/target_revenue/conversion.py` (`conversion_status`,
`generate_attestation`). `conversion_status` never reads an attestation
file; `generate_attestation` raises `ValueError` if called before Outstanding
Target actually reaches zero. Covered by `tests/test_conversion.py` (5
tests) plus the generated `examples/phase-001/attestation.json`.
Implement Conversion Event detection as pure read over Manifest + fold
(Outstanding Target reaches zero) and Conversion Attestation **document
schema** / optional generator (TSD §3.5). Enforce: attestation is never a
@ -140,11 +178,19 @@ an attestation file (PRD FR-7 / G6; working default Q13).
```task
id: TREV-WP-0002-T06
status: todo
status: done
priority: medium
state_hub_task_id: "90711ebf-3d89-4b45-8300-489f39cfa3cc"
```
Result 2026-07-28: `examples/phase-001/` (manifest, six-entry ledger, four
extension fixtures, generated attestation) built via
`scripts/generate_golden_phase.py` so the hash chain is computed by the
library itself, never hand-typed. Full pytest suite: 32 tests across
`tests/test_manifest_validation.py`, `tests/test_ledger_fold.py`,
`tests/test_extension_conformance.py`, `tests/test_conversion.py` — all
passing, no network dependency (`python3 -m pytest tests/`).
Build `examples/phase-001/` (or equivalent) exercising concept §23:
- Initial Target $100,000;