Port Repixelizer surface to Eve

This commit is contained in:
Meta Vens 2026-06-04 01:57:19 +01:00
commit 0564d42f75
14 changed files with 1193 additions and 18 deletions

View file

@ -0,0 +1,83 @@
# Repixelizer Eve Surface Lowering
Repixelizer now emits a read-only `gamecult.eve.surface.v1` projection for its
front page, single-page app workflow, runtime config, auth projection, queue
summary, and command intents.
## Authority Map
- Owner: `repixelizer.gui` still owns hosted config, route policy, queue state,
job ownership, uploads, solver execution, and browser assets.
- Inputs: `HostedDemoConfig`, `AccessController.public_payload()`, and
`GuiJobManager.get_queue_summary()`.
- Output: `repixelizer.eve_surface.build_repixelizer_eve_surface()` emits the
retained Eve surface tree, style tokens, command descriptors, and renderer
comparison notes.
- Derived state: `/eve/surface`, `repixelize eve-surface`, and
`docs/fixtures/repixelizer.eve-surface.json` are projections. They do not
decide queue truth.
- Forbidden writers: Eve renderers, Odin, Periwinkle, and fixtures must route
commands back through `/api/jobs`, `/api/jobs/{job_id}`, or `/app/`; they do
not mutate Repixelizer jobs directly.
- Shared paths: browser GUI, CLI fixture export, hosted `/eve/surface`, and
provider advertisement all name the same surface and route authority.
- Cut line: no parallel status-card service, no renderer-owned queue summary,
and no fake native canvas authority until Eve can lower upload and image-edit
controls coherently.
## Published Surface
Generate the deterministic fixture:
```powershell
repixelize eve-surface --out docs/fixtures/repixelizer.eve-surface.json --updated-at 2026-06-04T00:00:00Z --public-base-url https://repixelizer.gamecult.org
```
The hosted GUI serves the live projection at:
```text
/eve/surface
```
The provider advertisement now marks `repixelizer.operator.surface` as
`available` with an HTTP JSON lowering at `/eve/surface`.
## Retro Style Contract
The Eve style profile is `repixelizer.retro.pixel`. The tokens intentionally
mirror the browser CSS instead of flattening the app into generic dashboard
chrome:
- title font: `Press Start 2P`
- body/mono font: `VT323`
- dark shell gradient and blue panel surfaces
- warm yellow/orange accent tokens
- chunky pixel borders, lower border weight, corner accents, scanline overlay,
dashed inner frame, and pixelated image sampling
The browser lowering remains the visual oracle for this pass because it already
implements the full upload, solver inspection, comparison canvas, editor, and
retro CSS.
## Periwinkle Comparison
Periwinkle's Android Kotlin lowerer currently has useful structural support:
it connects to the Eve dashboard broker, traverses `surface.root`, renders
pane/card/text hierarchies, supports basic metric progress bars, and attaches
some command handlers.
The current gap is style and control parity. `MainActivity.kt` still hard-codes
`toneColor`, `textColorFor`, and `textSizeFor`; it does not consume
`surface.styles.tokens`, does not load Repixelizer's pixel fonts, does not
render image assets from the surface tree, and does not lower the upload picker,
comparison canvas, pan/zoom inspection, eyedropper, or paint controls.
Minimum Periwinkle parity work:
- map provider style tokens before applying fallback tone/text/font rules
- package or cache `Press Start 2P` and `VT323`
- render `assetUri` / `assetRef` as pixelated `ImageView` content
- lower Repixelizer command controls to Android document picker and job route
calls
- add native canvas controls only after ownership remains routed through the
existing Repixelizer job and editor command paths

View file

@ -0,0 +1,558 @@
{
"commands": [
{
"authority": "browser-lowering",
"command": "repixelizer.web.open",
"method": "GET",
"route": "/app/",
"surfaceId": "repixelizer.operator.surface",
"transport": "http"
},
{
"authority": "repixelizer-route-policy",
"command": "repixelizer.job.submit",
"method": "POST",
"route": "/api/jobs",
"surfaceId": "repixelizer.operator.surface",
"transport": "http"
},
{
"authority": "repixelizer-job-owner",
"command": "repixelizer.job.cancel_own",
"method": "DELETE",
"route": "/api/jobs/{job_id}",
"surfaceId": "repixelizer.operator.surface",
"transport": "http"
}
],
"loweringComparison": {
"periwinkleAndroidKotlin": {
"keeps": [
"surface root traversal",
"pane/card/text hierarchy",
"TextView body content",
"basic ProgressBar metric rendering",
"node-bound command taps"
],
"losesUntilRendererParity": [
"Press Start 2P and VT323 font selection",
"Repixelizer shell frame, scanlines, pixel grid, corner accents, and chunky border widths",
"landing/app image assets; current Android lowerer prints asset refs when images are not explicitly implemented",
"upload picker, comparison canvas, pan/zoom inspection, eyedropper, and paint controls",
"provider style tokens; current Kotlin toneColor/textColorFor/textSizeFor are hard-coded"
],
"minimumParityCuts": [
"map surface.styles.tokens before toneColor/textColorFor/textSizeFor",
"add packaged pixel fonts or font download cache",
"render image assetUri/assetRef as ImageView with pixelated sampling",
"lower command controls to Android upload/document picker and job route calls"
],
"source": "E:\\Projects\\Eve\\android\\app\\src\\main\\java\\org\\gamecult\\eve\\MainActivity.kt",
"status": "surface-tree-renderer-present-style-token-gap"
}
},
"providerId": "repixelizer",
"providerKind": "repixelizer.product",
"runtime": {
"auth": {
"appSlug": "repixelizer",
"enabled": false,
"loginUrl": null,
"logoutUrl": null,
"mode": "off",
"protectQueue": false,
"required": false
},
"hostedDemo": false,
"limits": {
"defaultSteps": 48,
"heartbeatIntervalSeconds": 10,
"maxInputDimension": 4096,
"maxOutputDimension": 1024,
"maxSteps": 256,
"maxUploadBytes": 16777216,
"queueCapacity": 32,
"staleAfterSeconds": 30
},
"queue": {
"activeStatus": null,
"hasActiveJob": false,
"queueCapacity": 32,
"queueDepth": 0,
"waitingCount": 0
}
},
"schema": "gamecult.eve.surface.v1",
"surface": {
"id": "repixelizer.operator.surface",
"root": {
"children": [
{
"children": [
{
"children": [],
"id": "repixelizer.hero.copy",
"kind": "text",
"props": {
"role": "body",
"text": "Force fake pixel art back onto a real grid, then inspect each solver step without sanding off the crunchy old-machine charm."
},
"role": "body",
"text": "Force fake pixel art back onto a real grid, then inspect each solver step without sanding off the crunchy old-machine charm."
},
{
"children": [
{
"children": [],
"id": "repixelizer.hero.open.body",
"kind": "text",
"props": {
"role": "body",
"text": "Browser lowering remains the full interactive app until native Eve gains upload and canvas editing parity."
},
"role": "body",
"text": "Browser lowering remains the full interactive app until native Eve gains upload and canvas editing parity."
},
{
"children": [],
"id": "repixelizer.hero.open.route",
"kind": "text",
"props": {
"method": "GET",
"role": "mono",
"route": "/app/",
"text": "GET /app/"
},
"role": "mono",
"text": "GET /app/"
}
],
"commandId": "repixelizer.web.open",
"id": "repixelizer.hero.open",
"kind": "card",
"props": {
"commandId": "repixelizer.web.open",
"styleProfile": "repixelizer.retro.pixel",
"title": "Open demo",
"transport": {
"kind": "http",
"method": "GET",
"route": "/app/"
}
},
"role": "group",
"style": {
"tone": "cool",
"variant": "repixelizer-retro-card"
},
"text": "Open demo"
}
],
"id": "repixelizer.hero",
"kind": "pane",
"layout": {
"direction": "vertical",
"padding": 10
},
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Repixelizer hosted demo"
},
"role": "section",
"style": {
"tone": "warm",
"variant": "repixelizer-retro-pane"
},
"text": "Repixelizer hosted demo"
},
{
"children": [
{
"children": [
{
"children": [],
"id": "repixelizer.workflow.submit.body",
"kind": "text",
"props": {
"role": "body",
"text": "Submit a PNG/JPEG/WebP/GIF into the GUI job queue; queue ownership stays in repixelizer.gui."
},
"role": "body",
"text": "Submit a PNG/JPEG/WebP/GIF into the GUI job queue; queue ownership stays in repixelizer.gui."
},
{
"children": [],
"id": "repixelizer.workflow.submit.route",
"kind": "text",
"props": {
"method": "POST",
"role": "mono",
"route": "/api/jobs",
"text": "POST /api/jobs"
},
"role": "mono",
"text": "POST /api/jobs"
}
],
"commandId": "repixelizer.job.submit",
"id": "repixelizer.workflow.submit",
"kind": "card",
"props": {
"commandId": "repixelizer.job.submit",
"styleProfile": "repixelizer.retro.pixel",
"title": "Upload image",
"transport": {
"kind": "http",
"method": "POST",
"route": "/api/jobs"
}
},
"role": "group",
"style": {
"tone": "cool",
"variant": "repixelizer-retro-card"
},
"text": "Upload image"
},
{
"children": [
{
"children": [],
"id": "repixelizer.inspect.source",
"kind": "text",
"props": {
"role": "body",
"text": "source, edge map, lattice overlay, phase field, cleaned output"
},
"role": "body",
"text": "source, edge map, lattice overlay, phase field, cleaned output"
},
{
"children": [],
"id": "repixelizer.inspect.canvas",
"kind": "text",
"props": {
"role": "body",
"text": "comparison canvas and editor lower to native only after Periwinkle gets image/control parity"
},
"role": "body",
"text": "comparison canvas and editor lower to native only after Periwinkle gets image/control parity"
}
],
"id": "repixelizer.workflow.inspect",
"kind": "card",
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Inspect solver"
},
"role": "group",
"style": {
"tone": "panel",
"variant": "repixelizer-retro-card"
},
"text": "Inspect solver"
},
{
"children": [
{
"children": [],
"id": "repixelizer.workflow.cancel.body",
"kind": "text",
"props": {
"role": "body",
"text": "The command intent delegates to the existing owner-bound DELETE route."
},
"role": "body",
"text": "The command intent delegates to the existing owner-bound DELETE route."
},
{
"children": [],
"id": "repixelizer.workflow.cancel.route",
"kind": "text",
"props": {
"method": "DELETE",
"role": "mono",
"route": "/api/jobs/{job_id}",
"text": "DELETE /api/jobs/{job_id}"
},
"role": "mono",
"text": "DELETE /api/jobs/{job_id}"
}
],
"commandId": "repixelizer.job.cancel_own",
"id": "repixelizer.workflow.cancel",
"kind": "card",
"props": {
"commandId": "repixelizer.job.cancel_own",
"styleProfile": "repixelizer.retro.pixel",
"title": "Cancel own job",
"transport": {
"kind": "http",
"method": "DELETE",
"route": "/api/jobs/{job_id}"
}
},
"role": "group",
"style": {
"tone": "cool",
"variant": "repixelizer-retro-card"
},
"text": "Cancel own job"
}
],
"id": "repixelizer.workflow",
"kind": "pane",
"layout": {
"direction": "vertical",
"padding": 10
},
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Single page app"
},
"role": "section",
"style": {
"tone": "warm",
"variant": "repixelizer-retro-pane"
},
"text": "Single page app"
},
{
"children": [
{
"children": [
{
"children": [],
"id": "repixelizer.runtime.hosted",
"kind": "text",
"props": {
"role": "mono",
"text": "hostedDemo: False"
},
"role": "mono",
"text": "hostedDemo: False"
},
{
"children": [],
"id": "repixelizer.runtime.steps",
"kind": "text",
"props": {
"role": "mono",
"text": "defaultSteps: 48 maxSteps: 256"
},
"role": "mono",
"text": "defaultSteps: 48 maxSteps: 256"
},
{
"children": [],
"id": "repixelizer.runtime.dimensions",
"kind": "text",
"props": {
"role": "mono",
"text": "maxOutputDimension: 1024px"
},
"role": "mono",
"text": "maxOutputDimension: 1024px"
}
],
"id": "repixelizer.runtime.config",
"kind": "card",
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Runtime"
},
"role": "group",
"style": {
"tone": "panel",
"variant": "repixelizer-retro-card"
},
"text": "Runtime"
},
{
"children": [
{
"children": [],
"id": "repixelizer.queue.depth",
"kind": "text",
"props": {
"role": "strong",
"text": "0 active or waiting / 32 capacity"
},
"role": "strong",
"text": "0 active or waiting / 32 capacity"
},
{
"children": [],
"id": "repixelizer.queue.waiting",
"kind": "text",
"props": {
"role": "mono",
"text": "waitingCount: 0"
},
"role": "mono",
"text": "waitingCount: 0"
},
{
"children": [],
"id": "repixelizer.queue.active",
"kind": "text",
"props": {
"role": "mono",
"text": "hasActiveJob: False"
},
"role": "mono",
"text": "hasActiveJob: False"
}
],
"id": "repixelizer.runtime.queue",
"kind": "card",
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Queue"
},
"role": "group",
"style": {
"tone": "panel",
"variant": "repixelizer-retro-card"
},
"text": "Queue"
},
{
"children": [
{
"children": [],
"id": "repixelizer.auth.mode",
"kind": "text",
"props": {
"role": "mono",
"text": "mode: off"
},
"role": "mono",
"text": "mode: off"
},
{
"children": [],
"id": "repixelizer.auth.required",
"kind": "text",
"props": {
"role": "mono",
"text": "required: False"
},
"role": "mono",
"text": "required: False"
},
{
"children": [],
"id": "repixelizer.auth.providers",
"kind": "text",
"props": {
"role": "body",
"text": "providers: none"
},
"role": "body",
"text": "providers: none"
}
],
"id": "repixelizer.runtime.auth",
"kind": "card",
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Access"
},
"role": "group",
"style": {
"tone": "panel",
"variant": "repixelizer-retro-card"
},
"text": "Access"
}
],
"id": "repixelizer.runtime",
"kind": "pane",
"layout": {
"direction": "vertical",
"padding": 10
},
"props": {
"styleProfile": "repixelizer.retro.pixel",
"title": "Live projection"
},
"role": "section",
"style": {
"tone": "warm",
"variant": "repixelizer-retro-pane"
},
"text": "Live projection"
}
],
"id": "repixelizer.surface.root",
"kind": "surface",
"layout": {
"direction": "vertical",
"padding": 14
},
"props": {
"browserLowering": "https://repixelizer.gamecult.org/app/",
"frontPage": "https://repixelizer.gamecult.org/",
"styleProfile": "repixelizer.retro.pixel",
"subtitle": "Fake pixel art goes in. Real grid-aligned sprite art comes out.",
"title": "Repixelizer"
},
"role": "root",
"style": {
"tone": "warm",
"variant": "repixelizer-retro-shell"
},
"text": "Repixelizer"
},
"schema": "gamecult.eve.surface.v1",
"styles": {
"assets": {
"appUrl": "https://repixelizer.gamecult.org/app/",
"fontCss": "https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap",
"landingInput": "https://repixelizer.gamecult.org/app/landing-assets/character-input.png",
"landingOutput": "https://repixelizer.gamecult.org/app/landing-assets/character-repixelized.png"
},
"loweringHints": {
"android-native": "Load packaged or downloadable Press Start 2P and VT323 fonts, map style tokens before hard-coded tones, and render asset images instead of asset labels.",
"css": "Use the tokens as CSS custom properties and retain scanline/pixel-grid overlays.",
"tui": "Preserve title/body font distinction as role metadata; canvas editing lowers to artifact refs and command intents only."
},
"profile": "repixelizer.retro.pixel",
"tokens": {
"borderBottomWidthPx": 5,
"borderWidthPx": 4,
"buttonGlyph": ">",
"colorAccent": "#ffd84a",
"colorAccentStrong": "#ffae1a",
"colorBackgroundBottom": "#01050d",
"colorBackgroundMid": "#020813",
"colorBackgroundTop": "#01040b",
"colorCornerAccent": "#ffb72a",
"colorMuted": "#d3bb7f",
"colorPanel": "#0a2239",
"colorPanelAlt": "#061627",
"colorPanelBorder": "#0a2740",
"colorPanelBorderDeep": "#06192a",
"colorPixelGrid": "rgba(0, 0, 0, 0.3)",
"colorShadow": "rgba(0, 0, 0, 0.55)",
"colorShellSurfaceBottom": "#030b16",
"colorShellSurfaceTop": "#061424",
"colorText": "#efe5c9",
"colorTextBright": "#f7efd8",
"cornerAccentPx": 12,
"dashedInnerFrame": true,
"fontBody": "\"VT323\", \"Courier New\", monospace",
"fontMono": "\"VT323\", \"Courier New\", monospace",
"fontTitle": "\"Press Start 2P\", \"VT323\", monospace",
"imageRendering": "pixelated",
"scanlineOverlay": true
}
},
"title": "Repixelizer"
},
"title": "Repixelizer",
"type": "surface-state",
"updatedAt": "2026-06-04T00:00:00Z",
"version": 1
}

View file

@ -26,7 +26,7 @@
}
],
"freshness": {
"lastSeenAt": "2026-06-03T00:00:00Z",
"lastSeenAt": "2026-06-04T00:00:00Z",
"maxAgeMs": 15000,
"state": "fresh"
},
@ -139,6 +139,7 @@
],
"styleProfile": "repixelizer.product",
"tokenGroups": [
"repixelizer.retro.pixel",
"repixelizer.comparisonCanvas",
"repixelizer.cleanupTool",
"repixelizer.queuePanel",
@ -149,13 +150,19 @@
"surfaces": [
{
"audience": "operator",
"commands": [],
"commands": [
"repixelizer.web.open",
"repixelizer.job.submit",
"repixelizer.job.cancel_own"
],
"key": "cultmesh://repixelizer/surfaces/operator",
"mode": "read-only-first-cut",
"schema": "gamecult.eve.surface.v1",
"status": "planned",
"status": "available",
"styleProfile": "repixelizer.retro.pixel",
"surfaceId": "repixelizer.operator.surface",
"transport": "cultmesh-document"
"transport": "http-json",
"url": "https://repixelizer.gamecult.org/eve/surface"
},
{
"canonical": false,
@ -167,13 +174,13 @@
}
],
"title": "Repixelizer",
"updatedAt": "2026-06-03T00:00:00Z",
"updatedAt": "2026-06-04T00:00:00Z",
"verseId": "gamecult.local",
"witnesses": [
{
"freshness": {
"state": "planned",
"updatedAt": "2026-06-03T00:00:00Z"
"updatedAt": "2026-06-04T00:00:00Z"
},
"id": "repixelizer.hosted.witness",
"kind": "cc-export-path-reserved",

View file

@ -67,14 +67,16 @@ The reserved witness path is `state/repixelizer.witness.cc`.
- 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:
- an available canonical Eve operator surface projection at `/eve/surface`,
retaining the canonical CultMesh surface key:
`cultmesh://repixelizer/surfaces/operator`
- the current browser lowering at `/app/`
- route-owned command boundaries for job submit and own-job cancellation
- route-owned command boundaries for web open, 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.
The advertisement only tells discovery organs where truth lives, where the
read-only Eve projection is served, and what shape future durable witnesses must
take.

View file

@ -87,7 +87,7 @@ 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
## Read-Only Witness And Eve Surface Export
The first read-only provider advertisement is generated by:
@ -96,9 +96,20 @@ repixelize witness-advertisement --out docs/fixtures/repixelizer.provider-advert
```
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`.
the future CultCache export, and publishes the available Eve operator surface
HTTP projection without changing queue ownership. The detailed authority maps
live in `docs/repixelizer-witness-contract.md` and
`docs/eve-surface-lowering.md`.
The read-only Eve surface fixture is generated by:
```powershell
repixelize eve-surface --out docs/fixtures/repixelizer.eve-surface.json
```
The hosted GUI serves the live projection at `/eve/surface`. It reads
`HostedDemoConfig`, `AccessController.public_payload()`, and
`GuiJobManager.get_queue_summary()`; it does not own queue truth.
## Migration Order
@ -110,6 +121,7 @@ without changing queue ownership. The detailed authority map lives in
`.cc` witness.
4. Publish the witness through CultMesh.
5. Add an Eve DSL provider over the witness and existing health/config routes.
First read-only HTTP projection is live at `/eve/surface`.
6. Translate the existing browser GUI style and tool behavior into Eve style
tokens, canvas primitives, and command descriptors.
7. Lower the current website from the Eve surface instead of treating it as the

View file

@ -6,6 +6,7 @@ import sys
from .benchmark import run_roundtrip_benchmark
from .compare import run_compare
from .corpus import prepare_corpus
from .eve_surface import main as eve_surface_main
from .gui import main as gui_main
from .pipeline import run_pipeline
from .spritesheet import run_spritesheet
@ -142,6 +143,13 @@ def build_parser() -> argparse.ArgumentParser:
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.")
eve_surface_parser = subparsers.add_parser(
"eve-surface",
help="Emit the read-only Repixelizer Eve surface projection.",
)
eve_surface_parser.add_argument("--out", default=None, help="Write JSON to this path instead of stdout.")
eve_surface_parser.add_argument("--updated-at", default=None, help="Override the surface timestamp.")
eve_surface_parser.add_argument("--public-base-url", default=None, help="Hosted browser lowering base URL.")
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")
@ -159,6 +167,7 @@ def main(argv: list[str] | None = None) -> int:
"tune",
"prepare-corpus",
"witness-advertisement",
"eve-surface",
"gui",
"-h",
"--help",
@ -254,6 +263,16 @@ def main(argv: list[str] | None = None) -> int:
if value is not None:
witness_args.extend([option, str(value)])
return witness_advertisement_main(witness_args)
if command == "eve-surface":
eve_surface_args: list[str] = []
for option, value in (
("--out", args.out),
("--updated-at", args.updated_at),
("--public-base-url", args.public_base_url),
):
if value is not None:
eve_surface_args.extend([option, str(value)])
return eve_surface_main(eve_surface_args)
if command == "gui":
return gui_main(host=args.host, port=args.port, reload=args.reload)

View file

@ -0,0 +1,377 @@
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
EVE_SURFACE_SCHEMA = "gamecult.eve.surface.v1"
PROVIDER_ID = "repixelizer"
PROVIDER_KIND = "repixelizer.product"
DEFAULT_PUBLIC_BASE_URL = "https://repixelizer.gamecult.org"
def utc_now_iso() -> str:
return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def _text(element_id: str, text: str, *, role: str = "body", **props: Any) -> dict[str, Any]:
return {
"id": element_id,
"kind": "text",
"role": role,
"text": text,
"props": {"text": text, "role": role, **props},
"children": [],
}
def _card(element_id: str, title: str, children: list[dict[str, Any]], *, tone: str = "panel") -> dict[str, Any]:
return {
"id": element_id,
"kind": "card",
"text": title,
"role": "group",
"style": {"tone": tone, "variant": "repixelizer-retro-card"},
"props": {"title": title, "styleProfile": "repixelizer.retro.pixel"},
"children": children,
}
def _pane(element_id: str, title: str, children: list[dict[str, Any]], *, direction: str = "vertical") -> dict[str, Any]:
return {
"id": element_id,
"kind": "pane",
"text": title,
"role": "section",
"layout": {"direction": direction, "padding": 10},
"style": {"tone": "warm", "variant": "repixelizer-retro-pane"},
"props": {"title": title, "styleProfile": "repixelizer.retro.pixel"},
"children": children,
}
def _command_card(
element_id: str,
title: str,
body: str,
*,
command_id: str,
route: str,
method: str,
) -> dict[str, Any]:
element = _card(
element_id,
title,
[
_text(f"{element_id}.body", body),
_text(f"{element_id}.route", f"{method} {route}", role="mono", route=route, method=method),
],
tone="cool",
)
element["commandId"] = command_id
element["props"]["commandId"] = command_id
element["props"]["transport"] = {"kind": "http", "route": route, "method": method}
return element
def _styles(public_base_url: str) -> dict[str, Any]:
base = public_base_url.rstrip("/")
return {
"profile": "repixelizer.retro.pixel",
"tokens": {
"fontTitle": "\"Press Start 2P\", \"VT323\", monospace",
"fontBody": "\"VT323\", \"Courier New\", monospace",
"fontMono": "\"VT323\", \"Courier New\", monospace",
"colorBackgroundTop": "#01040b",
"colorBackgroundMid": "#020813",
"colorBackgroundBottom": "#01050d",
"colorShellSurfaceTop": "#061424",
"colorShellSurfaceBottom": "#030b16",
"colorPanel": "#0a2239",
"colorPanelAlt": "#061627",
"colorTextBright": "#f7efd8",
"colorText": "#efe5c9",
"colorMuted": "#d3bb7f",
"colorAccent": "#ffd84a",
"colorAccentStrong": "#ffae1a",
"colorPanelBorder": "#0a2740",
"colorPanelBorderDeep": "#06192a",
"colorCornerAccent": "#ffb72a",
"colorShadow": "rgba(0, 0, 0, 0.55)",
"colorPixelGrid": "rgba(0, 0, 0, 0.3)",
"imageRendering": "pixelated",
"borderWidthPx": 4,
"borderBottomWidthPx": 5,
"cornerAccentPx": 12,
"scanlineOverlay": True,
"dashedInnerFrame": True,
"buttonGlyph": ">",
},
"assets": {
"fontCss": "https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap",
"landingInput": f"{base}/app/landing-assets/character-input.png",
"landingOutput": f"{base}/app/landing-assets/character-repixelized.png",
"appUrl": f"{base}/app/",
},
"loweringHints": {
"css": "Use the tokens as CSS custom properties and retain scanline/pixel-grid overlays.",
"android-native": "Load packaged or downloadable Press Start 2P and VT323 fonts, map style tokens before hard-coded tones, and render asset images instead of asset labels.",
"tui": "Preserve title/body font distinction as role metadata; canvas editing lowers to artifact refs and command intents only.",
},
}
def _runtime_cards(
*,
config: HostedDemoConfig,
auth_payload: dict[str, Any],
queue_summary: dict[str, Any],
) -> list[dict[str, Any]]:
provider_labels = []
for provider in auth_payload.get("providers", []):
if isinstance(provider, dict):
label = provider.get("label") or provider.get("slug")
if isinstance(label, str) and label:
provider_labels.append(label)
elif isinstance(provider, str):
provider_labels.append(provider)
queue_copy = (
f"{queue_summary.get('queueDepth', 0)} active or waiting / "
f"{queue_summary.get('queueCapacity', config.queue_capacity)} capacity"
)
return [
_card(
"repixelizer.runtime.config",
"Runtime",
[
_text("repixelizer.runtime.hosted", f"hostedDemo: {config.hosted_demo}", role="mono"),
_text("repixelizer.runtime.steps", f"defaultSteps: {config.default_steps} maxSteps: {config.max_steps}", role="mono"),
_text("repixelizer.runtime.dimensions", f"maxOutputDimension: {config.max_output_dimension}px", role="mono"),
],
),
_card(
"repixelizer.runtime.queue",
"Queue",
[
_text("repixelizer.queue.depth", queue_copy, role="strong"),
_text("repixelizer.queue.waiting", f"waitingCount: {queue_summary.get('waitingCount', 0)}", role="mono"),
_text("repixelizer.queue.active", f"hasActiveJob: {queue_summary.get('hasActiveJob', False)}", role="mono"),
],
),
_card(
"repixelizer.runtime.auth",
"Access",
[
_text("repixelizer.auth.mode", f"mode: {auth_payload.get('mode', 'off')}", role="mono"),
_text("repixelizer.auth.required", f"required: {bool(auth_payload.get('required'))}", role="mono"),
_text("repixelizer.auth.providers", f"providers: {', '.join(provider_labels) or 'none'}"),
],
),
]
def _lowering_comparison() -> dict[str, Any]:
return {
"periwinkleAndroidKotlin": {
"source": "E:\\Projects\\Eve\\android\\app\\src\\main\\java\\org\\gamecult\\eve\\MainActivity.kt",
"status": "surface-tree-renderer-present-style-token-gap",
"keeps": [
"surface root traversal",
"pane/card/text hierarchy",
"TextView body content",
"basic ProgressBar metric rendering",
"node-bound command taps",
],
"losesUntilRendererParity": [
"Press Start 2P and VT323 font selection",
"Repixelizer shell frame, scanlines, pixel grid, corner accents, and chunky border widths",
"landing/app image assets; current Android lowerer prints asset refs when images are not explicitly implemented",
"upload picker, comparison canvas, pan/zoom inspection, eyedropper, and paint controls",
"provider style tokens; current Kotlin toneColor/textColorFor/textSizeFor are hard-coded",
],
"minimumParityCuts": [
"map surface.styles.tokens before toneColor/textColorFor/textSizeFor",
"add packaged pixel fonts or font download cache",
"render image assetUri/assetRef as ImageView with pixelated sampling",
"lower command controls to Android upload/document picker and job route calls",
],
}
}
def build_repixelizer_eve_surface(
*,
updated_at: str | None = None,
public_base_url: str = DEFAULT_PUBLIC_BASE_URL,
config: HostedDemoConfig | None = None,
access_controller: AccessController | None = None,
queue_summary: dict[str, Any] | None = None,
) -> dict[str, Any]:
config = config or HostedDemoConfig.from_env()
access_controller = access_controller or AccessController.from_env(hosted_demo=config.hosted_demo)
auth_payload = access_controller.public_payload()
queue_summary = queue_summary or {
"queueDepth": 0,
"waitingCount": 0,
"queueCapacity": config.queue_capacity,
"hasActiveJob": False,
"activeStatus": None,
}
updated_at = updated_at or utc_now_iso()
base = public_base_url.rstrip("/")
root = {
"id": "repixelizer.surface.root",
"kind": "surface",
"text": "Repixelizer",
"role": "root",
"layout": {"direction": "vertical", "padding": 14},
"style": {"tone": "warm", "variant": "repixelizer-retro-shell"},
"props": {
"title": "Repixelizer",
"subtitle": "Fake pixel art goes in. Real grid-aligned sprite art comes out.",
"styleProfile": "repixelizer.retro.pixel",
"browserLowering": f"{base}/app/",
"frontPage": f"{base}/",
},
"children": [
_pane(
"repixelizer.hero",
"Repixelizer hosted demo",
[
_text(
"repixelizer.hero.copy",
"Force fake pixel art back onto a real grid, then inspect each solver step without sanding off the crunchy old-machine charm.",
),
_command_card(
"repixelizer.hero.open",
"Open demo",
"Browser lowering remains the full interactive app until native Eve gains upload and canvas editing parity.",
command_id="repixelizer.web.open",
route="/app/",
method="GET",
),
],
),
_pane(
"repixelizer.workflow",
"Single page app",
[
_command_card(
"repixelizer.workflow.submit",
"Upload image",
"Submit a PNG/JPEG/WebP/GIF into the GUI job queue; queue ownership stays in repixelizer.gui.",
command_id="repixelizer.job.submit",
route="/api/jobs",
method="POST",
),
_card(
"repixelizer.workflow.inspect",
"Inspect solver",
[
_text("repixelizer.inspect.source", "source, edge map, lattice overlay, phase field, cleaned output"),
_text("repixelizer.inspect.canvas", "comparison canvas and editor lower to native only after Periwinkle gets image/control parity"),
],
),
_command_card(
"repixelizer.workflow.cancel",
"Cancel own job",
"The command intent delegates to the existing owner-bound DELETE route.",
command_id="repixelizer.job.cancel_own",
route="/api/jobs/{job_id}",
method="DELETE",
),
],
),
_pane("repixelizer.runtime", "Live projection", _runtime_cards(config=config, auth_payload=auth_payload, queue_summary=queue_summary)),
],
}
return {
"type": "surface-state",
"schema": EVE_SURFACE_SCHEMA,
"providerId": PROVIDER_ID,
"providerKind": PROVIDER_KIND,
"title": "Repixelizer",
"version": 1,
"updatedAt": updated_at,
"surface": {
"id": "repixelizer.operator.surface",
"schema": EVE_SURFACE_SCHEMA,
"title": "Repixelizer",
"root": root,
"styles": _styles(public_base_url),
},
"commands": [
{
"command": "repixelizer.web.open",
"surfaceId": "repixelizer.operator.surface",
"transport": "http",
"route": "/app/",
"method": "GET",
"authority": "browser-lowering",
},
{
"command": "repixelizer.job.submit",
"surfaceId": "repixelizer.operator.surface",
"transport": "http",
"route": "/api/jobs",
"method": "POST",
"authority": "repixelizer-route-policy",
},
{
"command": "repixelizer.job.cancel_own",
"surfaceId": "repixelizer.operator.surface",
"transport": "http",
"route": "/api/jobs/{job_id}",
"method": "DELETE",
"authority": "repixelizer-job-owner",
},
],
"runtime": {
"hostedDemo": config.hosted_demo,
"queue": queue_summary,
"auth": auth_payload,
"limits": config.public_payload()["limits"],
},
"loweringComparison": _lowering_comparison(),
}
def write_repixelizer_eve_surface(
output: Path | None,
*,
updated_at: str | None = None,
public_base_url: str = DEFAULT_PUBLIC_BASE_URL,
stream: TextIO = sys.stdout,
) -> None:
payload = build_repixelizer_eve_surface(updated_at=updated_at, public_base_url=public_base_url)
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_eve_surface_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="repixelize eve-surface",
description="Emit Repixelizer's read-only Eve surface projection.",
)
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 surface timestamp.")
parser.add_argument("--public-base-url", default=DEFAULT_PUBLIC_BASE_URL, help="Hosted browser lowering base URL.")
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_eve_surface_parser()
args = parser.parse_args(argv)
write_repixelizer_eve_surface(args.out, updated_at=args.updated_at, public_base_url=args.public_base_url)
return 0

View file

@ -1191,6 +1191,20 @@ def create_app():
def get_queue():
return JSONResponse(manager.get_queue_summary())
@app.get("/eve/surface")
def get_eve_surface(request: Request):
from .eve_surface import build_repixelizer_eve_surface
base_url = str(request.base_url).rstrip("/")
return JSONResponse(
build_repixelizer_eve_surface(
public_base_url=base_url,
config=config,
access_controller=access_controller,
queue_summary=manager.get_queue_summary(),
)
)
@app.get("/api/jobs/{job_id}")
def get_job(job_id: str):
job = manager.get_job(job_id)

View file

@ -113,12 +113,14 @@ def build_provider_advertisement(
{
"surfaceId": "repixelizer.operator.surface",
"schema": "gamecult.eve.surface.v1",
"transport": "cultmesh-document",
"transport": "http-json",
"key": eve_surface_key,
"url": f"{public_base_url.rstrip('/')}/eve/surface",
"audience": "operator",
"mode": "read-only-first-cut",
"commands": [],
"status": "planned",
"commands": ["repixelizer.web.open", "repixelizer.job.submit", "repixelizer.job.cancel_own"],
"status": "available",
"styleProfile": "repixelizer.retro.pixel",
},
{
"surfaceId": "repixelizer.web.app",
@ -179,6 +181,7 @@ def build_provider_advertisement(
{
"styleProfile": "repixelizer.product",
"tokenGroups": [
"repixelizer.retro.pixel",
"repixelizer.comparisonCanvas",
"repixelizer.cleanupTool",
"repixelizer.queuePanel",

View file

@ -35,3 +35,4 @@
{"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."}
{"ts": "2026-06-04T01:55:00+00:00", "type": "implementation", "status": "ok", "note": "Added read-only Repixelizer Eve surface projection: CLI fixture/export, hosted /eve/surface route, retro pixel style tokens, route-owned command descriptors, and Android/Periwinkle lowering comparison without moving gui.py queue ownership."}

View file

@ -54,6 +54,7 @@ accepted_design:
- `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.
- `src/repixelizer/eve_surface.py` owns the read-only Eve surface projection for Repixelizer's front page, single-page workflow, runtime/auth/queue status, retro style tokens, command descriptors, and Periwinkle lowering comparison; `/eve/surface` serves the live projection from `gui.py` without owning queue truth.
- 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.
@ -70,6 +71,7 @@ landed_baseline:
- `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.
- Read-only `gamecult.eve.surface.v1` fixture/export plus hosted `/eve/surface` projection, preserving Repixelizer's retro CSS identity as Eve style tokens and documenting Android/Periwinkle style-control parity gaps.
rejected_paths:
- name: tray_optimizer_sprawl
@ -101,7 +103,15 @@ current_status:
`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
and `src/repixelizer/eve_surface.py` emits a read-only
`gamecult.eve.surface.v1` projection served at `/eve/surface` and checked in
as `docs/fixtures/repixelizer.eve-surface.json`. The Eve surface keeps the
browser app as the lossless retro lowering, publishes Press Start 2P/VT323
and pixel-shell style tokens, routes commands back to existing GUI routes,
and records that Periwinkle's Kotlin renderer can traverse the surface tree
but does not yet honor provider style tokens, pixel fonts, image assets, or
native upload/canvas controls. These exports do not move 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

64
tests/test_eve_surface.py Normal file
View file

@ -0,0 +1,64 @@
from __future__ import annotations
import json
from pathlib import Path
from repixelizer.cli import main as cli_main
from repixelizer.eve_surface import build_repixelizer_eve_surface
def test_repixelizer_eve_surface_preserves_retro_style_tokens(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_repixelizer_eve_surface(
updated_at="2026-06-04T00:00:00Z",
public_base_url="https://repixelizer.example",
)
assert payload["schema"] == "gamecult.eve.surface.v1"
assert payload["providerId"] == "repixelizer"
assert payload["surface"]["root"]["props"]["styleProfile"] == "repixelizer.retro.pixel"
assert payload["runtime"]["hostedDemo"] is True
assert payload["runtime"]["queue"]["queueCapacity"] == 7
tokens = payload["surface"]["styles"]["tokens"]
assert "Press Start 2P" in tokens["fontTitle"]
assert "VT323" in tokens["fontBody"]
assert tokens["colorBackgroundTop"] == "#01040b"
assert tokens["colorAccent"] == "#ffd84a"
assert tokens["imageRendering"] == "pixelated"
assert tokens["scanlineOverlay"] is True
commands = {entry["command"]: entry for entry in payload["commands"]}
assert commands["repixelizer.job.submit"]["route"] == "/api/jobs"
assert commands["repixelizer.job.cancel_own"]["method"] == "DELETE"
android = payload["loweringComparison"]["periwinkleAndroidKotlin"]
assert android["status"] == "surface-tree-renderer-present-style-token-gap"
assert any("Press Start 2P" in gap for gap in android["losesUntilRendererParity"])
def test_eve_surface_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.eve-surface.json"
exit_code = cli_main(
[
"eve-surface",
"--out",
str(out),
"--updated-at",
"2026-06-04T00:00:00Z",
"--public-base-url",
"https://repixelizer.example",
]
)
assert exit_code == 0
payload = json.loads(out.read_text(encoding="utf-8"))
assert payload["updatedAt"] == "2026-06-04T00:00:00Z"
assert payload["surface"]["styles"]["assets"]["appUrl"] == "https://repixelizer.example/app/"

View file

@ -1195,6 +1195,26 @@ def test_gui_queue_panel_defaults_off_for_local_runs_and_can_be_forced(monkeypat
assert payload["ui"]["showQueuePanel"] is True
def test_gui_serves_eve_surface_from_live_runtime_projection(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app
monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1")
monkeypatch.setenv("REPIXELIZER_QUEUE_CAPACITY", "5")
monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool"))
monkeypatch.setenv("GC_ACCESS_MODE", "off")
app = create_app()
status, _headers, body = asyncio.run(_get_response(app, "/eve/surface"))
payload = _response_json(type("Response", (), {"body": body})())
assert status == 200
assert payload["schema"] == "gamecult.eve.surface.v1"
assert payload["surface"]["styles"]["tokens"]["colorAccent"] == "#ffd84a"
assert payload["runtime"]["queue"]["queueCapacity"] == 5
assert payload["runtime"]["auth"]["mode"] == "off"
assert payload["surface"]["styles"]["assets"]["appUrl"] == "http://testserver/app/"
def test_gui_queue_rejects_eleventh_waiting_job(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app

View file

@ -33,6 +33,11 @@ def test_provider_advertisement_names_witness_schemas(monkeypatch, tmp_path: Pat
assert witness["kind"] == "cc-export-path-reserved"
assert witness["path"] == "state/repixelizer.witness.cc"
assert witness["freshness"]["state"] == "planned"
surface = payload["surfaces"][0]
assert surface["status"] == "available"
assert surface["transport"] == "http-json"
assert surface["url"] == "https://repixelizer.gamecult.org/eve/surface"
assert surface["styleProfile"] == "repixelizer.retro.pixel"
def test_witness_advertisement_cli_writes_json(monkeypatch, tmp_path: Path) -> None: