From 1e1a3441fe0f8dbc1502b72242c2329dac97faf5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 3 Jun 2026 03:22:39 +0100 Subject: [PATCH] Add Repixelizer witness advertisement export --- README.md | 4 +- .../repixelizer.provider-advertisement.json | 191 +++++++++++++ docs/repixelizer-witness-contract.md | 80 ++++++ docs/verse-service-contract.md | 29 +- src/repixelizer/cli.py | 38 ++- src/repixelizer/witness.py | 266 ++++++++++++++++++ state/evidence.jsonl | 1 + state/map.yaml | 17 +- tests/test_witness.py | 61 ++++ 9 files changed, 673 insertions(+), 14 deletions(-) create mode 100644 docs/fixtures/repixelizer.provider-advertisement.json create mode 100644 docs/repixelizer-witness-contract.md create mode 100644 src/repixelizer/witness.py create mode 100644 tests/test_witness.py diff --git a/README.md b/README.md index 9201013..2d7d866 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ That is the machine described in `docs/lean-optimizer-algorithm-map.md`. Comparison mode just runs that same `phase-field` result next to the baselines. For hosted service state, queue visibility, and the future CultCache/CultMesh/Eve -operator surface, see `docs/verse-service-contract.md`. +operator surface, see `docs/verse-service-contract.md`. The read-only Odin +advertisement fixture/export is documented in +`docs/repixelizer-witness-contract.md`. ## Current Status diff --git a/docs/fixtures/repixelizer.provider-advertisement.json b/docs/fixtures/repixelizer.provider-advertisement.json new file mode 100644 index 0000000..de72044 --- /dev/null +++ b/docs/fixtures/repixelizer.provider-advertisement.json @@ -0,0 +1,191 @@ +{ + "commands": [ + { + "authority": "repixelizer-route-policy", + "command": "repixelizer.job.submit", + "method": "POST", + "result": "accepted-denied-or-queued", + "route": "/api/jobs", + "surfaceId": "repixelizer.web.app", + "transport": "http" + }, + { + "authority": "repixelizer-job-owner", + "command": "repixelizer.job.cancel_own", + "method": "DELETE", + "result": "accepted-denied-or-not-found", + "route": "/api/jobs/{job_id}", + "surfaceId": "repixelizer.web.app", + "transport": "http" + } + ], + "contacts": [ + { + "kind": "repo", + "path": "E:\\Projects\\repixelizer" + } + ], + "freshness": { + "lastSeenAt": "2026-06-03T00:00:00Z", + "maxAgeMs": 15000, + "state": "fresh" + }, + "kind": "service.product", + "nestedVerses": [ + { + "authorityBoundary": "repixelizer-local-session-or-heimdall-account", + "carryRules": { + "commands": "repixelizer-route-policy-only", + "identity": "heimdall-claims-projected-when-enabled" + }, + "kind": "session-space", + "parentVerseId": "gamecult.local", + "stateSchemas": [ + "repixelizer.job.v0", + "repixelizer.job_event.v0", + "repixelizer.auth_projection.v0" + ], + "surfaceIds": [ + "repixelizer.web.app" + ], + "verseId": "repixelizer.session.{sessionId}" + }, + { + "authorityBoundary": "read-only-first-cut", + "kind": "hosted-operator-space", + "parentVerseId": "gamecult.local", + "stateSchemas": [ + "repixelizer.job.v0", + "repixelizer.queue_snapshot.v0", + "repixelizer.job_event.v0", + "repixelizer.runtime_config.v0", + "repixelizer.auth_projection.v0" + ], + "surfaceIds": [ + "repixelizer.operator.surface" + ], + "verseId": "repixelizer.operator" + } + ], + "providerId": "repixelizer", + "runtime": { + "auth": { + "enabled": false, + "mode": "off", + "protectQueue": false, + "providers": [], + "required": false + }, + "hostedDemo": false, + "queueCapacity": 32, + "showQueuePanel": false, + "spoolPath": "C:\\Users\\Meta\\AppData\\Local\\Temp\\repixelizer-gui-spool" + }, + "schema": "gamecult.eve.provider_advertisement.v1", + "schemas": [ + { + "authority": "accepted", + "description": "Job id, owner binding, status, timestamps, input/output artifact refs, error summary, and retention state.", + "owner": "repixelizer.gui", + "portable": true, + "schema": "repixelizer.job.v0", + "storage": "memory-dev-only-with-cc-witness-reserved" + }, + { + "authority": "accepted", + "description": "Queue capacity, waiting/running counts, active job ids, oldest waiting age, and hosted mode.", + "owner": "repixelizer.gui", + "portable": true, + "schema": "repixelizer.queue_snapshot.v0", + "storage": "memory-dev-only-with-cc-witness-reserved" + }, + { + "authority": "redacted-projection", + "description": "Redacted progress event, stage, index, timestamp, and job id.", + "owner": "repixelizer.gui", + "portable": true, + "schema": "repixelizer.job_event.v0", + "storage": "memory-dev-only-with-cc-witness-reserved" + }, + { + "authority": "accepted", + "description": "Hosted flags, queue limits, visible UI flags, solver config hash placeholder, deployment id, and spool path.", + "owner": "repixelizer.gui", + "portable": true, + "schema": "repixelizer.runtime_config.v0", + "storage": "memory-dev-only-with-cc-witness-reserved" + }, + { + "authority": "external-authority-projection", + "description": "Auth mode, provider availability, subject capability summary, and Heimdall claim freshness without raw tokens.", + "owner": "repixelizer.access", + "portable": true, + "schema": "repixelizer.auth_projection.v0", + "storage": "heimdall-projection-with-cc-witness-reserved" + } + ], + "serviceId": "repixelizer.service", + "styleCapabilities": [ + { + "lossiness": { + "read-only-first-cut": "operator surface names state and routes but does not own queue truth", + "tui": "no-pixel-canvas-editing; status, queue, artifact refs, and command intents only" + }, + "preferredLowerings": [ + "css", + "android-native", + "native-eve", + "tui" + ], + "styleProfile": "repixelizer.product", + "tokenGroups": [ + "repixelizer.comparisonCanvas", + "repixelizer.cleanupTool", + "repixelizer.queuePanel", + "repixelizer.operatorStatus" + ] + } + ], + "surfaces": [ + { + "audience": "operator", + "commands": [], + "key": "cultmesh://repixelizer/surfaces/operator", + "mode": "read-only-first-cut", + "schema": "gamecult.eve.surface.v1", + "status": "planned", + "surfaceId": "repixelizer.operator.surface", + "transport": "cultmesh-document" + }, + { + "canonical": false, + "canonicalSurfaceId": "repixelizer.operator.surface", + "schema": "gamecult.eve.surface.v1", + "surfaceId": "repixelizer.web.app", + "transport": "browser-lowering", + "url": "https://repixelizer.gamecult.org/app/" + } + ], + "title": "Repixelizer", + "updatedAt": "2026-06-03T00:00:00Z", + "verseId": "gamecult.local", + "witnesses": [ + { + "freshness": { + "state": "planned", + "updatedAt": "2026-06-03T00:00:00Z" + }, + "id": "repixelizer.hosted.witness", + "kind": "cc-export-path-reserved", + "path": "state/repixelizer.witness.cc", + "redaction": "bulk-images-raw-tokens-and-private-claims-removed", + "schemas": [ + "repixelizer.job.v0", + "repixelizer.queue_snapshot.v0", + "repixelizer.job_event.v0", + "repixelizer.runtime_config.v0", + "repixelizer.auth_projection.v0" + ] + } + ] +} diff --git a/docs/repixelizer-witness-contract.md b/docs/repixelizer-witness-contract.md new file mode 100644 index 0000000..0ff90fe --- /dev/null +++ b/docs/repixelizer-witness-contract.md @@ -0,0 +1,80 @@ +# Repixelizer Witness Contract + +Repixelizer's hosted queue is still owned by `src/repixelizer/gui.py`. +This document defines the read-only first cut for making that state visible to +Odin, Huginn, CultMesh, and Eve without moving queue authority. + +## Objective + +Publish a `gamecult.eve.provider_advertisement.v1` fixture/export that names +Repixelizer's typed witness shapes and reserved CultCache `.cc` witness path. +The export is an authority map. It is not the queue store, not an Eve renderer, +and not a replacement for the current hosted routes. + +Generate it with: + +```powershell +repixelize witness-advertisement --out docs/fixtures/repixelizer.provider-advertisement.json +``` + +The checked-in fixture is: + +`docs/fixtures/repixelizer.provider-advertisement.json` + +## Authority Map + +- Owner: `src/repixelizer/gui.py` owns queue admission, job lifecycle, progress + events, cancellation, heartbeat expiry, and spool cleanup. +- Inputs: uploaded image bytes, normalized hosted options, queue capacity, + worker heartbeat, cancellation requests, app-local subject binding, and + Heimdall-derived access projection. +- Outputs: in-process job records, queue summaries, SSE progress events, output + image artifacts, diagnostics refs, and route-level JSON projections. +- Derived state: provider advertisement JSON, future `.cc` witness export, + CultMesh publication, Odin discovery state, and Eve operator surfaces. +- Forbidden writers: Odin, Eve renderers, provider-advertisement fixtures, + witness exporters, and `.cc` inspection tools do not decide queue truth. +- Shared paths: browser GUI, hosted API routes, future `.cc` witness export, + future CultMesh publication, and Eve surfaces must describe the same + Repixelizer-owned job state. +- Cut line: this pass reserves witness names and paths only. A later pass may + add a real `.cc` writer or move queue ownership, but that must be an explicit + rebuild of ownership, not an exporter pretending to be persistence. + +## Witness Schemas + +The first advertisement names these schemas: + +- `repixelizer.job.v0`: job id, account/session binding, status, timestamps, + input artifact ref, output artifact ref, error summary, and retention state. +- `repixelizer.queue_snapshot.v0`: queue capacity, waiting/running counts, + active job ids, oldest waiting age, and hosted-demo mode. +- `repixelizer.job_event.v0`: redacted progress event, stage, index, timestamp, + and job id. +- `repixelizer.runtime_config.v0`: hosted flags, queue protection, visible UI + flags, solver config hash placeholder, deployment id, and spool path. +- `repixelizer.auth_projection.v0`: auth mode, provider availability, subject + capability summary, and Heimdall claim freshness without raw tokens. + +The reserved witness path is `state/repixelizer.witness.cc`. + +## Provider Advertisement + +`repixelize witness-advertisement` emits: + +- `schema: gamecult.eve.provider_advertisement.v1` +- `providerId: repixelizer` +- schema catalog entries for the witness shapes above +- a reserved `.cc` witness entry with bulk image bytes, raw tokens, and private + claims marked as redacted +- a planned canonical Eve operator surface key: + `cultmesh://repixelizer/surfaces/operator` +- the current browser lowering at `/app/` +- route-owned command boundaries for job submit and own-job cancellation +- nested Verse declarations for session and hosted-operator spaces +- runtime projection from `HostedDemoConfig.from_env()` and + `AccessController.from_env()` + +This is deliberately read-only. The current queue is still in-process memory. +The advertisement only tells discovery organs where truth lives and what shape +future durable witnesses must take. diff --git a/docs/verse-service-contract.md b/docs/verse-service-contract.md index d14f405..661db31 100644 --- a/docs/verse-service-contract.md +++ b/docs/verse-service-contract.md @@ -87,20 +87,35 @@ tool palettes, and component variants. A Kotlin Android Eve runtime should be able to run the cleanup flow with native controls while preserving the same command boundary and product identity. +## Read-Only Witness Export + +The first read-only provider advertisement is generated by: + +```powershell +repixelize witness-advertisement --out docs/fixtures/repixelizer.provider-advertisement.json +``` + +It names the witness schemas above, reserves `state/repixelizer.witness.cc` for +the future CultCache export, and publishes the planned Eve operator surface key +without changing queue ownership. The detailed authority map lives in +`docs/repixelizer-witness-contract.md`. + ## Migration Order 1. Define Repixelizer CultCache document shapes for job, queue, runtime, event, and auth projection witness state. -2. Add a read-only export from the current `gui.py` queue/job manager into a +2. Publish a read-only `gamecult.eve.provider_advertisement.v1` fixture/export + that names those shapes and the reserved `.cc` witness path. +3. Add a read-only export from the current `gui.py` queue/job manager into a `.cc` witness. -3. Publish the witness through CultMesh. -4. Add an Eve DSL provider over the witness and existing health/config routes. -5. Translate the existing browser GUI style and tool behavior into Eve style +4. Publish the witness through CultMesh. +5. Add an Eve DSL provider over the witness and existing health/config routes. +6. Translate the existing browser GUI style and tool behavior into Eve style tokens, canvas primitives, and command descriptors. -6. Lower the current website from the Eve surface instead of treating it as the +7. Lower the current website from the Eve surface instead of treating it as the product UI owner. -7. Register the surface with Odin. -8. Only after the witness is stable, decide whether queue ownership should move +8. Register the surface with Odin. +9. Only after the witness is stable, decide whether queue ownership should move from in-process memory into the typed store. The invariant: Heimdall owns shared auth; Repixelizer owns jobs and artifacts. diff --git a/src/repixelizer/cli.py b/src/repixelizer/cli.py index d1fb581..69c7cd7 100644 --- a/src/repixelizer/cli.py +++ b/src/repixelizer/cli.py @@ -10,6 +10,7 @@ from .gui import main as gui_main from .pipeline import run_pipeline from .spritesheet import run_spritesheet from .tuning import tune_solver_hyperparams +from .witness import main as witness_advertisement_main def build_parser() -> argparse.ArgumentParser: @@ -131,6 +132,16 @@ def build_parser() -> argparse.ArgumentParser: tune_parser.add_argument("--workers", type=int, default=1, help="Parallel benchmark worker processes per trial") prepare_parser = subparsers.add_parser("prepare-corpus", help="Normalize imported corpus sheets into single sprites.") prepare_parser.add_argument("--corpus-dir", default="examples/corpus", help="Corpus root containing originals/") + witness_parser = subparsers.add_parser( + "witness-advertisement", + help="Emit the read-only Repixelizer Eve provider advertisement.", + ) + witness_parser.add_argument("--out", default=None, help="Write JSON to this path instead of stdout.") + witness_parser.add_argument("--updated-at", default=None, help="Override the advertisement timestamp.") + witness_parser.add_argument("--verse-id", default=None, help="Authoritative Verse id.") + witness_parser.add_argument("--public-base-url", default=None, help="Hosted browser lowering base URL.") + witness_parser.add_argument("--cc-witness-path", default=None, help="Reserved CultCache .cc witness path.") + witness_parser.add_argument("--eve-surface-key", default=None, help="Planned CultMesh Eve surface key.") gui_parser = subparsers.add_parser("gui", help="Launch the web GUI.") gui_parser.add_argument("--host", default="127.0.0.1", help="Host interface to bind") gui_parser.add_argument("--port", type=int, default=8000, help="Port to bind") @@ -140,7 +151,19 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: raw_argv = list(sys.argv[1:] if argv is None else argv) - if not raw_argv or raw_argv[0] not in {"run", "compare", "spritesheet", "benchmark", "tune", "prepare-corpus", "gui", "-h", "--help"}: + commands = { + "run", + "compare", + "spritesheet", + "benchmark", + "tune", + "prepare-corpus", + "witness-advertisement", + "gui", + "-h", + "--help", + } + if not raw_argv or raw_argv[0] not in commands: raw_argv = ["run", *raw_argv] parser = build_parser() args = parser.parse_args(raw_argv) @@ -218,6 +241,19 @@ def main(argv: list[str] | None = None) -> int: if command == "prepare-corpus": prepare_corpus(args.corpus_dir) return 0 + if command == "witness-advertisement": + witness_args: list[str] = [] + for option, value in ( + ("--out", args.out), + ("--updated-at", args.updated_at), + ("--verse-id", args.verse_id), + ("--public-base-url", args.public_base_url), + ("--cc-witness-path", args.cc_witness_path), + ("--eve-surface-key", args.eve_surface_key), + ): + if value is not None: + witness_args.extend([option, str(value)]) + return witness_advertisement_main(witness_args) if command == "gui": return gui_main(host=args.host, port=args.port, reload=args.reload) diff --git a/src/repixelizer/witness.py b/src/repixelizer/witness.py new file mode 100644 index 0000000..334b469 --- /dev/null +++ b/src/repixelizer/witness.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import argparse +import json +import sys +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, TextIO + +from .access import AccessController +from .gui import HostedDemoConfig + + +PROVIDER_ADVERTISEMENT_SCHEMA = "gamecult.eve.provider_advertisement.v1" +PROVIDER_ID = "repixelizer" +SERVICE_ID = "repixelizer.service" +DEFAULT_VERSE_ID = "gamecult.local" +DEFAULT_PUBLIC_BASE_URL = "https://repixelizer.gamecult.org" +DEFAULT_CC_WITNESS_PATH = "state/repixelizer.witness.cc" +DEFAULT_EVE_SURFACE_KEY = "cultmesh://repixelizer/surfaces/operator" + +WITNESS_SCHEMAS: tuple[dict[str, Any], ...] = ( + { + "schema": "repixelizer.job.v0", + "owner": "repixelizer.gui", + "authority": "accepted", + "storage": "memory-dev-only-with-cc-witness-reserved", + "portable": True, + "description": "Job id, owner binding, status, timestamps, input/output artifact refs, error summary, and retention state.", + }, + { + "schema": "repixelizer.queue_snapshot.v0", + "owner": "repixelizer.gui", + "authority": "accepted", + "storage": "memory-dev-only-with-cc-witness-reserved", + "portable": True, + "description": "Queue capacity, waiting/running counts, active job ids, oldest waiting age, and hosted mode.", + }, + { + "schema": "repixelizer.job_event.v0", + "owner": "repixelizer.gui", + "authority": "redacted-projection", + "storage": "memory-dev-only-with-cc-witness-reserved", + "portable": True, + "description": "Redacted progress event, stage, index, timestamp, and job id.", + }, + { + "schema": "repixelizer.runtime_config.v0", + "owner": "repixelizer.gui", + "authority": "accepted", + "storage": "memory-dev-only-with-cc-witness-reserved", + "portable": True, + "description": "Hosted flags, queue limits, visible UI flags, solver config hash placeholder, deployment id, and spool path.", + }, + { + "schema": "repixelizer.auth_projection.v0", + "owner": "repixelizer.access", + "authority": "external-authority-projection", + "storage": "heimdall-projection-with-cc-witness-reserved", + "portable": True, + "description": "Auth mode, provider availability, subject capability summary, and Heimdall claim freshness without raw tokens.", + }, +) + + +def utc_now_iso() -> str: + return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z") + + +def build_provider_advertisement( + *, + updated_at: str | None = None, + verse_id: str = DEFAULT_VERSE_ID, + public_base_url: str = DEFAULT_PUBLIC_BASE_URL, + cc_witness_path: str = DEFAULT_CC_WITNESS_PATH, + eve_surface_key: str = DEFAULT_EVE_SURFACE_KEY, + config: HostedDemoConfig | None = None, + access_controller: AccessController | None = None, +) -> dict[str, Any]: + config = config or HostedDemoConfig.from_env() + access_controller = access_controller or AccessController.from_env(hosted_demo=config.hosted_demo) + updated_at = updated_at or utc_now_iso() + auth_payload = access_controller.public_payload() + + return { + "schema": PROVIDER_ADVERTISEMENT_SCHEMA, + "providerId": PROVIDER_ID, + "serviceId": SERVICE_ID, + "verseId": verse_id, + "title": "Repixelizer", + "kind": "service.product", + "updatedAt": updated_at, + "freshness": { + "state": "fresh", + "lastSeenAt": updated_at, + "maxAgeMs": 15000, + }, + "schemas": list(WITNESS_SCHEMAS), + "witnesses": [ + { + "id": "repixelizer.hosted.witness", + "kind": "cc-export-path-reserved", + "path": cc_witness_path, + "schemas": [entry["schema"] for entry in WITNESS_SCHEMAS], + "redaction": "bulk-images-raw-tokens-and-private-claims-removed", + "freshness": { + "state": "planned", + "updatedAt": updated_at, + }, + } + ], + "surfaces": [ + { + "surfaceId": "repixelizer.operator.surface", + "schema": "gamecult.eve.surface.v1", + "transport": "cultmesh-document", + "key": eve_surface_key, + "audience": "operator", + "mode": "read-only-first-cut", + "commands": [], + "status": "planned", + }, + { + "surfaceId": "repixelizer.web.app", + "schema": "gamecult.eve.surface.v1", + "transport": "browser-lowering", + "url": f"{public_base_url.rstrip('/')}/app/", + "canonical": False, + "canonicalSurfaceId": "repixelizer.operator.surface", + }, + ], + "commands": [ + { + "command": "repixelizer.job.submit", + "surfaceId": "repixelizer.web.app", + "transport": "http", + "route": "/api/jobs", + "method": "POST", + "authority": "repixelizer-route-policy", + "result": "accepted-denied-or-queued", + }, + { + "command": "repixelizer.job.cancel_own", + "surfaceId": "repixelizer.web.app", + "transport": "http", + "route": "/api/jobs/{job_id}", + "method": "DELETE", + "authority": "repixelizer-job-owner", + "result": "accepted-denied-or-not-found", + }, + ], + "nestedVerses": [ + { + "verseId": "repixelizer.session.{sessionId}", + "parentVerseId": verse_id, + "kind": "session-space", + "authorityBoundary": "repixelizer-local-session-or-heimdall-account", + "surfaceIds": ["repixelizer.web.app"], + "stateSchemas": [ + "repixelizer.job.v0", + "repixelizer.job_event.v0", + "repixelizer.auth_projection.v0", + ], + "carryRules": { + "identity": "heimdall-claims-projected-when-enabled", + "commands": "repixelizer-route-policy-only", + }, + }, + { + "verseId": "repixelizer.operator", + "parentVerseId": verse_id, + "kind": "hosted-operator-space", + "authorityBoundary": "read-only-first-cut", + "surfaceIds": ["repixelizer.operator.surface"], + "stateSchemas": [entry["schema"] for entry in WITNESS_SCHEMAS], + }, + ], + "styleCapabilities": [ + { + "styleProfile": "repixelizer.product", + "tokenGroups": [ + "repixelizer.comparisonCanvas", + "repixelizer.cleanupTool", + "repixelizer.queuePanel", + "repixelizer.operatorStatus", + ], + "preferredLowerings": ["css", "android-native", "native-eve", "tui"], + "lossiness": { + "tui": "no-pixel-canvas-editing; status, queue, artifact refs, and command intents only", + "read-only-first-cut": "operator surface names state and routes but does not own queue truth", + }, + } + ], + "contacts": [ + { + "kind": "repo", + "path": "E:\\Projects\\repixelizer", + } + ], + "runtime": { + "hostedDemo": config.hosted_demo, + "queueCapacity": config.queue_capacity, + "showQueuePanel": config.show_queue_panel, + "spoolPath": str(config.spool_dir), + "auth": { + "enabled": bool(auth_payload.get("enabled")), + "mode": auth_payload.get("mode"), + "required": bool(auth_payload.get("required")), + "protectQueue": bool(auth_payload.get("protectQueue")), + "providers": auth_payload.get("providers", []), + }, + }, + } + + +def write_provider_advertisement( + output: Path | None, + *, + updated_at: str | None = None, + verse_id: str = DEFAULT_VERSE_ID, + public_base_url: str = DEFAULT_PUBLIC_BASE_URL, + cc_witness_path: str = DEFAULT_CC_WITNESS_PATH, + eve_surface_key: str = DEFAULT_EVE_SURFACE_KEY, + stream: TextIO = sys.stdout, +) -> None: + payload = build_provider_advertisement( + updated_at=updated_at, + verse_id=verse_id, + public_base_url=public_base_url, + cc_witness_path=cc_witness_path, + eve_surface_key=eve_surface_key, + ) + encoded = json.dumps(payload, indent=2, sort_keys=True) + if output is None: + stream.write(f"{encoded}\n") + return + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(f"{encoded}\n", encoding="utf-8") + + +def build_witness_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="repixelize witness-advertisement", + description="Emit Repixelizer's read-only Eve provider advertisement fixture.", + ) + parser.add_argument("--out", type=Path, default=None, help="Write JSON to this path instead of stdout.") + parser.add_argument("--updated-at", default=None, help="Override the advertisement timestamp.") + parser.add_argument("--verse-id", default=DEFAULT_VERSE_ID, help="Authoritative Verse id.") + parser.add_argument("--public-base-url", default=DEFAULT_PUBLIC_BASE_URL, help="Hosted browser lowering base URL.") + parser.add_argument("--cc-witness-path", default=DEFAULT_CC_WITNESS_PATH, help="Reserved CultCache .cc witness path.") + parser.add_argument("--eve-surface-key", default=DEFAULT_EVE_SURFACE_KEY, help="Planned CultMesh Eve surface key.") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_witness_parser() + args = parser.parse_args(argv) + write_provider_advertisement( + args.out, + updated_at=args.updated_at, + verse_id=args.verse_id, + public_base_url=args.public_base_url, + cc_witness_path=args.cc_witness_path, + eve_surface_key=args.eve_surface_key, + ) + return 0 diff --git a/state/evidence.jsonl b/state/evidence.jsonl index db4419b..4aafc38 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -34,3 +34,4 @@ {"ts": "2026-05-30T22:47:57+00:00", "type": "implementation", "status": "ok", "note": "Added spritesheet CLI mode: detect separated foreground regions or select requested largest components, call run_pipeline_rgba per crop, pack outputs in source-row order, and write spritesheet diagnostics; fixture auto-detects six sprites."} {"ts": "2026-05-30T23:39:55+00:00", "type": "implementation", "status": "ok", "note": "Spritesheet automatic sizing now uses a sheet-level median texel density before reconstruction; on the 12 generated spritesheets in 019e7aa3-4bbb-7302-bafd-273f2aa31e53, worst per-sheet density drift improved from 42.21% to 1.786% across 153 sprites."} {"ts": "2026-05-31T01:13:17+00:00", "type": "implementation", "status": "ok", "note": "Spritesheet mode now supports explicit grid ownership via sheet_columns/sheet_rows plus individual sprite export. The 12 generated character sheets were rerun as 3x2 grids, producing 72 exports with no component chopping and worst within-sheet density deviation 0.124%."} +{"ts": "2026-06-03T02:21:02+00:00", "type": "implementation", "status": "ok", "note": "Added read-only Repixelizer provider advertisement fixture/export naming job/queue/event/runtime/auth witness schemas and reserved state/repixelizer.witness.cc path without moving gui.py queue ownership."} diff --git a/state/map.yaml b/state/map.yaml index 80cbfd0..f241e4a 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -53,6 +53,7 @@ accepted_design: - Hosted mode owns the public front door in `src/repixelizer/gui.py`: `/` is a landing page there, while local/self-host runs keep root as a plain redirect to `/app/`. - `src/repixelizer/access.py` owns the hosted access seam: trusted-header ingress still works, `GC_ACCESS_MODE=heimdall` verifies Heimdall Ed25519 access tokens locally, and auth attempts are adopted into local httpOnly cookie sessions. - In Heimdall mode, Repixelizer keeps a separate httpOnly refresh cookie and asks Heimdall to reissue a short-lived access claim before forcing the user back through provider OAuth. + - `src/repixelizer/witness.py` owns the read-only first-cut Odin provider advertisement export. It names Repixelizer job, queue, event, runtime, and auth witness schemas plus the reserved `state/repixelizer.witness.cc` path, but it does not own queue truth or write CultCache. - The solver starts from fixed lattice centers, adds tiny seeded noise, follows a direct hierarchical flatness signal, enforces local lattice coherence through spring relaxation, then rounds to real source pixels for final output. - Cleanup is local and secondary; it does not define the core reconstruction model. - `source_fidelity` and `source_structure` separate lattice agreement from visible structure. @@ -68,6 +69,7 @@ landed_baseline: - Spritesheet CLI mode detects separated foreground regions or selects the largest requested sprite count, then repixelizes each crop independently through `phase-field`. - `source_structure` reporting alongside `source_fidelity`. - Hosted landing page and Heimdall auth seam in the web layer. + - Read-only `gamecult.eve.provider_advertisement.v1` fixture/export for Odin discovery and future CultCache/Eve witness work. rejected_paths: - name: tray_optimizer_sprawl @@ -95,11 +97,16 @@ current_status: endpoint before falling back to provider OAuth, while `src/repixelizer/gui.py` owns provider-start routes, direct backend callback receipt, hosted landing-page login buttons, route guards, and queue/job - ownership checks. Self-host remains permissive by default unless - `GC_ACCESS_*` turns the seam on. The solver is shipping with a usable but - not final tuned config: local search radius `0.215`, blend `0.24`, move - weight `0.09`, grid alignment `4.75`, signal energy power `1.4`, and signal - peak power `3.25`, aligned in `config/solver_params.json` and the docs. + ownership checks. `src/repixelizer/witness.py` now emits a read-only + `gamecult.eve.provider_advertisement.v1` export and checked-in fixture that + names the Repixelizer job/queue/event/runtime/auth witness schemas and + reserved `state/repixelizer.witness.cc` path for Odin/Huginn discovery, + without moving queue ownership out of `gui.py`. Self-host remains + permissive by default unless `GC_ACCESS_*` turns the seam on. The solver is + shipping with a usable but not final tuned config: local search radius + `0.215`, blend `0.24`, move weight `0.09`, grid alignment `4.75`, signal + energy power `1.4`, and signal peak power `3.25`, aligned in + `config/solver_params.json` and the docs. Diagnostic edge analysis remains an observer/GUI gauge. The tuning pass proved that wide local-search windows caused neighbor feature poaching and that tighter radius plus stronger blend is the current motion-geometry basin. It did not fully diff --git a/tests/test_witness.py b/tests/test_witness.py new file mode 100644 index 0000000..0eb6c14 --- /dev/null +++ b/tests/test_witness.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from repixelizer.cli import main as cli_main +from repixelizer.witness import build_provider_advertisement + + +def test_provider_advertisement_names_witness_schemas(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1") + monkeypatch.setenv("REPIXELIZER_QUEUE_CAPACITY", "7") + monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool")) + monkeypatch.setenv("GC_ACCESS_MODE", "off") + + payload = build_provider_advertisement(updated_at="2026-06-03T00:00:00Z") + + assert payload["schema"] == "gamecult.eve.provider_advertisement.v1" + assert payload["providerId"] == "repixelizer" + assert payload["runtime"]["hostedDemo"] is True + assert payload["runtime"]["queueCapacity"] == 7 + assert payload["runtime"]["auth"]["mode"] == "off" + + schema_names = {entry["schema"] for entry in payload["schemas"]} + assert schema_names == { + "repixelizer.auth_projection.v0", + "repixelizer.job.v0", + "repixelizer.job_event.v0", + "repixelizer.queue_snapshot.v0", + "repixelizer.runtime_config.v0", + } + witness = payload["witnesses"][0] + assert witness["kind"] == "cc-export-path-reserved" + assert witness["path"] == "state/repixelizer.witness.cc" + assert witness["freshness"]["state"] == "planned" + + +def test_witness_advertisement_cli_writes_json(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool")) + monkeypatch.setenv("GC_ACCESS_MODE", "off") + out = tmp_path / "repixelizer.provider-advertisement.json" + + exit_code = cli_main( + [ + "witness-advertisement", + "--out", + str(out), + "--updated-at", + "2026-06-03T00:00:00Z", + "--verse-id", + "gamecult.test", + "--cc-witness-path", + "state/test-repixelizer.witness.cc", + ] + ) + + assert exit_code == 0 + payload = json.loads(out.read_text(encoding="utf-8")) + assert payload["verseId"] == "gamecult.test" + assert payload["witnesses"][0]["path"] == "state/test-repixelizer.witness.cc" + assert payload["commands"][0]["route"] == "/api/jobs"