Port Repixelizer surface to Eve
This commit is contained in:
parent
1e1a3441fe
commit
0564d42f75
14 changed files with 1193 additions and 18 deletions
83
docs/eve-surface-lowering.md
Normal file
83
docs/eve-surface-lowering.md
Normal 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
|
||||
558
docs/fixtures/repixelizer.eve-surface.json
Normal file
558
docs/fixtures/repixelizer.eve-surface.json
Normal 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
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
377
src/repixelizer/eve_surface.py
Normal file
377
src/repixelizer/eve_surface.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
|
|
@ -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
64
tests/test_eve_surface.py
Normal 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/"
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue