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:
parent
658b1af00d
commit
57c7111cbc
28 changed files with 1751 additions and 7 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
.pytest_cache/
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
||||
.venv/
|
||||
23
README.md
23
README.md
|
|
@ -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
|
||||
|
|
|
|||
128
docs/adr/ADR-0001-stage0-library-stack.md
Normal file
128
docs/adr/ADR-0001-stage0-library-stack.md
Normal 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.
|
||||
10
examples/phase-001/attestation.json
Normal file
10
examples/phase-001/attestation.json
Normal 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"
|
||||
}
|
||||
24
examples/phase-001/extensions/cost-plus-operations.json
Normal file
24
examples/phase-001/extensions/cost-plus-operations.json
Normal 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"
|
||||
}
|
||||
24
examples/phase-001/extensions/development-license.json
Normal file
24
examples/phase-001/extensions/development-license.json
Normal 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"
|
||||
}
|
||||
23
examples/phase-001/extensions/phase-sponsorship.json
Normal file
23
examples/phase-001/extensions/phase-sponsorship.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
86
examples/phase-001/ledger.json
Normal file
86
examples/phase-001/ledger.json
Normal 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"
|
||||
}
|
||||
]
|
||||
32
examples/phase-001/manifest.json
Normal file
32
examples/phase-001/manifest.json
Normal 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
26
pyproject.toml
Normal 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"]
|
||||
52
schemas/conversion_attestation.schema.json
Normal file
52
schemas/conversion_attestation.schema.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
92
schemas/extension_contract.schema.json
Normal file
92
schemas/extension_contract.schema.json
Normal 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)."
|
||||
}
|
||||
}
|
||||
}
|
||||
108
schemas/ledger_entry.schema.json
Normal file
108
schemas/ledger_entry.schema.json
Normal 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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
108
schemas/phase_manifest.schema.json
Normal file
108
schemas/phase_manifest.schema.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
128
scripts/generate_golden_phase.py
Normal file
128
scripts/generate_golden_phase.py
Normal 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()
|
||||
14
src/target_revenue/__init__.py
Normal file
14
src/target_revenue/__init__.py
Normal 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",
|
||||
]
|
||||
80
src/target_revenue/conversion.py
Normal file
80
src/target_revenue/conversion.py
Normal 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
109
src/target_revenue/fold.py
Normal 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)
|
||||
75
src/target_revenue/hashing.py
Normal file
75
src/target_revenue/hashing.py
Normal 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
|
||||
176
src/target_revenue/validation.py
Normal file
176
src/target_revenue/validation.py
Normal 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
28
tests/conftest.py
Normal 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")
|
||||
24
tests/fixtures/non_conforming_extension.json
vendored
Normal file
24
tests/fixtures/non_conforming_extension.json
vendored
Normal 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
52
tests/test_conversion.py
Normal 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"
|
||||
)
|
||||
55
tests/test_extension_conformance.py
Normal file
55
tests/test_extension_conformance.py
Normal 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
133
tests/test_ledger_fold.py
Normal 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
|
||||
61
tests/test_manifest_validation.py
Normal file
61
tests/test_manifest_validation.py
Normal 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) == []
|
||||
|
|
@ -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.
|
||||
T02–T06 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue