Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba536eec1 | ||
|
|
e119e76301 | ||
|
|
afe05b0c7e | ||
|
|
2467e1b025 |
3 changed files with 348 additions and 23 deletions
56
README.md
56
README.md
|
|
@ -1,9 +1,41 @@
|
|||
# Thing
|
||||
|
||||
This repository is Thing. Its present tree is Thing 1, kept running for live
|
||||
consumers until each ports; Thing 2.0 is built here (`docs/thing2-target.md`).
|
||||
The Thing 1 renderer repos are archived under their Eve names: EveUnity,
|
||||
EveFlutter, EveElectron, EveTui, EvePlugins and EveConformance.
|
||||
This repository is Thing (the Eve display, control and sensor edge for GameCult
|
||||
apps). Its present tree is Thing 1, kept running for live consumers until each
|
||||
ports; Thing 2.0 is built here (`docs/thing2-target.md`). The Thing 1 renderer
|
||||
repos are archived under their Eve names: EveUnity, EveFlutter, EveElectron,
|
||||
EveTui, EvePlugins and EveConformance.
|
||||
|
||||
## Start Here
|
||||
|
||||
Value: services publish a surface once, as a typed retained document
|
||||
(`gamecult.eve.surface.v1`), and every runtime lowers it locally. Providers keep
|
||||
authority over state and commands; Eve owns projection, input capture and
|
||||
timestamped sensor publication.
|
||||
|
||||
Problem: without a shared surface contract, each service hand-builds its own
|
||||
dashboard and each renderer re-implements layout, binding and commands.
|
||||
|
||||
Live (Thing 1):
|
||||
|
||||
- `packages/org.gamecult.eve.surface`: renderer-neutral C# surface contract and
|
||||
composition builders; tests in `tests/GameCult.Eve.Surface.Serialization.Tests`.
|
||||
- `packages/eve-contracts`: TypeScript types and validators generated from
|
||||
`schemas/` (`npm run generate`, `npm test`).
|
||||
- `packages/eve-browser-lowering`: browser DOM/CSS lowering of surface documents
|
||||
(`npm test`).
|
||||
- `web/`: browser reference oracle (`web/index.html`, `web/eve-dsl.js`; tests
|
||||
with `node --test web/eve-dsl.test.mjs`).
|
||||
- `docs/surface-contract-v1.md`: the contract. `docs/repo-strategy.md`: repo
|
||||
ownership. `docs/parity-testing-harness.md` with
|
||||
`scripts/run-parity-harness.ps1`: parity runs.
|
||||
- `android/` and `Sources/` (iOS Theos app, `make package`): native proof
|
||||
clients; build steps under Build Shape below.
|
||||
|
||||
Planned (Thing 2.0, in progress): `crates/thing-contract` is the first Rust
|
||||
piece. The layout solver, text, accessibility tree, closed render-command list
|
||||
and thin backends described in `docs/thing2-target.md` and `docs/thing2-map.md`
|
||||
are not built yet; `docs/thing-campaign.md` holds the rename plan.
|
||||
|
||||
CultMesh-based surface web, portable control layer, and timestamped sensor
|
||||
edge.
|
||||
|
|
@ -214,11 +246,8 @@ render:
|
|||
|
||||
## VoidBot CEF Stream
|
||||
|
||||
Start the Starfire relay from `E:\Projects\VoidBot`:
|
||||
|
||||
```powershell
|
||||
npm run swarm:eve-cef-relay -- --width 1620 --height 2160 --scale 2 --port 8792
|
||||
```
|
||||
The CEF relay that produced these frames is not in VoidBot's default branch, so
|
||||
no relay command is documented here until one exists.
|
||||
|
||||
EveCanvas displays binary JPEG frames and returns touch events only when a
|
||||
stream lowering is configured through `EVE_STREAM_URLS`. The native app no
|
||||
|
|
@ -226,12 +255,11 @@ longer guesses a Starfire relay address.
|
|||
|
||||
## Mimir Sensor Uplink
|
||||
|
||||
Start Mimir with `config/mimir-runtime.raven-eve.example.json` or run the
|
||||
receiver processes directly:
|
||||
Run the receiver processes from a Mimir checkout (a sibling `..\Mimir` here):
|
||||
|
||||
```powershell
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8793 --path /eve/camera --source-id eve-camera --type video-frame
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveSensorReceiver\Mimir.EveSensorReceiver.csproj -- --port 8794 --path /eve/mic --source-id eve-mic --type audio-block
|
||||
```
|
||||
|
||||
EveCanvas sends binary `mimir.eve_media_observation.v1` camera and microphone
|
||||
|
|
@ -245,7 +273,7 @@ guess receiver URLs.
|
|||
Start the dashboard authority on Starfire:
|
||||
|
||||
```powershell
|
||||
dotnet run --project E:\Projects\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
|
||||
dotnet run --project ..\Mimir\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --port 8795
|
||||
```
|
||||
|
||||
EveCanvas connects to the Mimir dashboard WebSocket lowering for native retained
|
||||
|
|
|
|||
201
docs/research/swarm-dashboard.md
Normal file
201
docs/research/swarm-dashboard.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Swarm dashboard: contract state, sources, first cut
|
||||
|
||||
Serves Eureka's ruling `swarm-dashboard-first-thing2-surface` (not readable from
|
||||
the libby mind; cited by the id in the claiming follow-up
|
||||
`libby-body:follow_up:chat-s354-359`). Read-only pass by the `libby` Self run
|
||||
`libby:run:mind-20261031T061423908Z`, 2026-10-31. Nothing was built or deployed.
|
||||
Claims below were probed on 2026-10-31 unless marked **unverified**.
|
||||
|
||||
The question the dashboard answers for a stranger: **is anyone working, and on what.**
|
||||
|
||||
## 1. Where Thing 2's surface contract stands
|
||||
|
||||
Probed against the forge `GameCult/Thing` (main `e119e76`, 2026-10-22) and its
|
||||
`hands/thing2-*` branches. The source of truth for cut status is the `thing2`
|
||||
campaign in the Eureka mind, which this pass cannot read; the prose map
|
||||
(`docs/thing2-map.md`, last edited 2026-10-12) is the evidence used.
|
||||
|
||||
| Piece | State |
|
||||
| --- | --- |
|
||||
| Contract, Rust (`crates/thing-contract`) | On main. 13-arm closed `Node` union, `SurfaceDocument`, `CompositionDocument`, `CommandDecl/Invocation/Result`, `AssetCatalog`; byte goldens under `tests/goldens/`. |
|
||||
| Contract, C# and TypeScript | Not on main. Branches `hands/thing2-contract-cs` (`3dadfc6`, 2026-10-14) and `hands/thing2-contract-ts` (`aeede04`, 2026-10-12). The map lists the cross-runtime store check and publication as waiting. |
|
||||
| Core (resolution, `thing.std` compositions, binding via the CultMesh port, Taffy layout, command dispatch) | Not built. No crate exists. Decoding Verse surfaces waits on `cultlib-gaps:follow_up:decode-stack-safety`. |
|
||||
| TUI backend | Not built. Planned first backend. |
|
||||
| Web backend and shell | Not built. |
|
||||
| Discovery/advertisement | Not a Thing kind (`thing2:ruling:advertisement-owner`); read through a core port; today's `gamecult.eve.provider_advertisement.v1` is unchanged. |
|
||||
|
||||
`README.md` says the same: the layout solver, text, accessibility tree, render
|
||||
commands and backends "are not built yet". **Nothing can render a Thing 2
|
||||
surface today.** The only live renderers are Thing 1's (browser oracle, Ghostlight's vendored lowering,
|
||||
the Nightwing browser reference), and Eve dashboards are quarantined (`gamecult.md`: no new Eve surfaces).
|
||||
|
||||
### What a provider must publish to render
|
||||
|
||||
From `docs/thing2-map.md` "The consumer contract" and `crates/thing-contract/src`:
|
||||
|
||||
1. A `SurfaceDocument` (`gamecult.thing.surface.v1`, key `thing:surface:<providerId>.<label>`,
|
||||
`version` bumped only on structure change). Closed nodes: `Box, Text, Image, Button, Toggle,
|
||||
Slider, TextInput, Select, Repeat, Overlay, Plot, Embed, Use`. A swarm board is `Box` + `Text` +
|
||||
`Repeat` (one row per instance) + `Use` of `thing.std` compositions (`card`, `metric`, `key-value`).
|
||||
2. The **bound state records** the document points at, under the provider's own schema and key. A
|
||||
`Bind` is `{provider?, schema, key, member path, format?}` plus `Item` inside a `Repeat`. A
|
||||
collection binds to `{provider, schema, key_prefix}` or a list member of one record. No
|
||||
expressions, no derived values: **the state's owner publishes derived values** (map rationale,
|
||||
P5). So the dashboard's "working / idle" must be a published field, not computed in the surface.
|
||||
3. Discovery: the provider is found through the existing advertisement, not a Thing record.
|
||||
4. Commands only if the surface acts. A read-only board declares none.
|
||||
|
||||
A provider needs no layout or renderer code. It needs the contract package in its language
|
||||
(Rust is the only one on main) and typed CultMesh state.
|
||||
|
||||
## 2. Sources (all typed state)
|
||||
|
||||
### 2a. Run documents, eight minds
|
||||
|
||||
Each mind is its own Huginn daemon (`HEARTBEAT_PERSONAS` in `/etc/gamecult/heartbeat/heartbeat.env`):
|
||||
eureka 17872, libby 17873, heimdall 17874, kiko 17875, aqua 17876, weksa 17877, nibu 17878,
|
||||
void 17879, all `rudp://10.77.0.1:<port>`. Probed: from this Self run,
|
||||
`EUREKA_INSTANCE=<name> HUGINN_ENDPOINT=<endpoint> huginn query '{"schemas":["epiphany.pipeline.run.v2"],"limit":200}'`
|
||||
answers on all eight, read-only, with caller `client unknown` (no per-client read identity yet).
|
||||
Counts that day: eureka 68, libby 78, heimdall 94, kiko 70, aqua 81, weksa 74, nibu 71, void 87.
|
||||
|
||||
`epiphany.pipeline.run.v2` (`huginn schema run`): `instance, label, operated_by (Operator|Mind),
|
||||
turn (PersonaTurn|SelfRun), host, started_on (a date), budget_usd, claims [PipelineRef], campaigns`.
|
||||
|
||||
What the dashboard can and cannot take from it:
|
||||
|
||||
- **Open/closed is not on the Run.** It is derived: a Run is closed when a `Resolution` with
|
||||
`subject` = the run is in force (`huginn query` prints `InForce` or `Resolved:<outcome>`).
|
||||
Resolution key `<instance>:resolution:run.<label>.n<seq>`. `huginn history <runId>` reads it.
|
||||
- **Start time is a date.** The label `mind-YYYYMMDDTHHMMSSmmmZ` encodes the start to the
|
||||
millisecond, but that is a convention in a string. The admission header (`huginn view`) carries
|
||||
`admitted <timestamp>`, and the Resolution's header gives the end. Both are header facts, not
|
||||
document fields. A dashboard needing start and end times wants them as typed fields.
|
||||
- **"Outcome" is nearly empty.** Of 78 libby runs, 77 resolved `Recorded` and one is in force.
|
||||
The meaning lives in free text: `reason "answered 1"` (55), `"answered 2"` (5), `"the connector
|
||||
refused the credential lease: UnknownCaller"` (3), `"unit not active"`, `"failed after 0
|
||||
answered..."`, plus rationales `The Persona unit's start ended.` (61) and `The Self run's carrier
|
||||
closed the run its claude left open.` (11). Two Self-run resolutions carry the run's whole result
|
||||
message inside `reason` (long prose, operator-facing detail). A public board
|
||||
must **never project `reason` or `rationale`**.
|
||||
- **"On what" is only on Self runs.** `claims` holds the follow-up or other document the run took;
|
||||
a `PersonaTurn` has no claims (its work is the chat room). The claimed `follow_up.item` is the
|
||||
operator's chat-relayed brief, free text, sometimes private. The typed handle (claim id, campaign
|
||||
slug, the claimed document's `repo`) is safe; the `item` is not.
|
||||
- **An open Run can be a dead Run.** Eleven libby Self runs were closed with "carrier closed the
|
||||
run its claude left open", so for some interval an in-force Run belonged to nobody. "Open" is
|
||||
"admitted and not yet resolved", not "a process is working".
|
||||
- **Dollar fields:** `budget_usd` is on every Run. Do not bind it.
|
||||
|
||||
Snapshot at 06:16 UTC: two Self runs in force, `libby:run:mind-20261031T061423908Z` (this one) and
|
||||
`void:run:mind-20261031T061543899Z` (claims `void-body:follow_up:chat-s335-340`); no Persona run
|
||||
in force in any of the eight.
|
||||
|
||||
### 2b. Heartbeat state and poll trace
|
||||
|
||||
Source: `Huginn/crates/mind-body/src/heartbeat.rs`. `mind-heartbeat` is one Idunn-managed unit, sole
|
||||
writer of `heartbeat.cc` (a single-file CultCache store under the unit's state directory). Two kinds:
|
||||
|
||||
- `eureka.heartbeat.initiative.v1`, key = instance slug: `initiative_usd, as_of, urgency, urgency_at,
|
||||
last_burned_run, filled_events, classified_seq, launched_run, launch_spend_usd,
|
||||
launch_spend_known, completions_through, launch_window`.
|
||||
- `eureka.heartbeat.poll_trace.v1`, key = zero-padded unix ms, written only for polls that changed
|
||||
something: `at, instances [InstanceTrace], launches [run label], burns [BurnTrace], classifier`.
|
||||
`InstanceTrace`: `instance, initiative_usd, rate_usd_per_h, urgency, urgency_age_s, busy,
|
||||
released, crossing_at, waited_s, idle_s, filled_by, fill_reason, note`.
|
||||
|
||||
Usable without dollars: `busy` (an in-force Persona run, from the mind), `released`, `crossing_at`,
|
||||
`waited_s`, `idle_s`, `filled_by`/`fill_reason` (`tag` or `name`), `note`, `launches`, `urgency`,
|
||||
`classifier`. These say **why nobody is working** (waiting for initiative, held by the envelope).
|
||||
Dollar fields to leave alone: `initiative_usd`, `rate_usd_per_h`, `launch_spend_usd`,
|
||||
`spend_usd`, and in the Initiative record the same names. The metering teardown removes them, and
|
||||
`initiative_usd` is the unit initiative itself is denominated in, so the heartbeat's own model will
|
||||
change under whatever binds this.
|
||||
|
||||
Gaps:
|
||||
|
||||
- **Not verified live.** `heartbeat.cc` is not readable from this Self run's account, and no
|
||||
`/var/lib/gamecult` path for it was visible. The shape above is from source and
|
||||
`eureka-body-map.md:534-540`, not from a read of the file.
|
||||
- **No wire.** The heartbeat writes a file; it does not serve one. A reader opening a single-file
|
||||
store while the heartbeat rewrites it has no stated safety (`FileStore::open` pulls the whole file
|
||||
per call). A dashboard source must be published state, not a second process reading the file.
|
||||
`mind-heartbeat timeline` prints the latest poll for a human.
|
||||
- The trace is written only when a poll "changed something", so its newest line can be old while
|
||||
the system is healthy. Staleness must come from the heartbeat's own published liveness, which
|
||||
does not exist as a field.
|
||||
|
||||
### 2c. Connector usage readout
|
||||
|
||||
Source: `ModelConnector/schemas/usage_readout.json`, `crates/model-connector/src/readout.rs`.
|
||||
`gamecult.model.usage_readout.v1`: `{schema_id, at_unix_ms, plan_quota: [{window, status,
|
||||
utilization_permille (nullable), resets_at_unix_ms, observed_at_unix_ms}]}`. Whole-plan windows, no
|
||||
per-caller field, **no dollars**: the safe source for "how much capacity is left".
|
||||
|
||||
Gaps: it is a connector operation (`usage_readout` on `gamecult.model.connector`), reached at the
|
||||
heartbeat's `MIND_CONNECTOR_ENDPOINT` (`tcp://127.0.0.1:4110`), a loopback port with a per-caller
|
||||
credential. Three libby Self runs were already refused with `UnknownCaller` on a lease, so a new
|
||||
caller needs registration before it can ask. It is a request/answer, not a document in a store. A
|
||||
dashboard needs a publisher that polls it and republishes it as a CultMesh record. `plan_quota`
|
||||
also rides on every lease grant. **Unverified:** the live values, since asking requires a caller
|
||||
credential this pass was not given.
|
||||
|
||||
### 2d. Not a source: the presence provider
|
||||
|
||||
CultLib's 10-29 presence provider attests Idunn-managed **service** liveness, not attention or work
|
||||
(`libby-body:follow_up:presence-attention-not-carried`). It can answer "is the heartbeat unit up".
|
||||
It cannot answer "is anyone working".
|
||||
|
||||
## 3. Smallest first cut
|
||||
|
||||
**Cut 1: one published read model, `swarm.work.v1`, from the Runs of the eight minds. No heartbeat,
|
||||
no usage, no renderer.**
|
||||
|
||||
One record per instance (eight, key `<instance>`), derived and published by a single projector
|
||||
(owner: the swarm-dashboard provider), holding only closed-set or id fields:
|
||||
|
||||
- `state`: `Working` (a Self run in force), `Turn` (a Persona run in force), or `Idle`.
|
||||
- `run`: label of the newest in-force run, its `turn`, its start (from the admission header), else the
|
||||
newest closed run's end.
|
||||
- `on`: the claim id(s) and campaign slug(s), no free text.
|
||||
- `observed_at` and the per-mind `as_of`, so a dead projector reads as stale.
|
||||
|
||||
This answers the stranger's question with typed state only. It deliberately omits `reason`,
|
||||
`rationale`, `item`, `budget_usd`, and every `*_usd` field.
|
||||
|
||||
The Thing 2 surface for it is a `SurfaceDocument` that is a `Repeat` over those eight records, one
|
||||
`thing.std.card` each, bound to `state` / `on` / the time fields. It can be authored and golden-tested
|
||||
with the Rust contract on main. It cannot be seen until core (2a) and a backend exist. That is the
|
||||
actual gating item, and it is not this cut's to work around with a Thing 1 or Eve surface.
|
||||
|
||||
Cut 2: add heartbeat liveness and the why-idle fields (`busy`, `released`, `waited_s`, `idle_s`,
|
||||
`note`, `launches`), once the heartbeat publishes them as typed CultMesh state (it currently writes a
|
||||
file only, and the dollar-denominated initiative model is under teardown).
|
||||
Cut 3: add the `plan_quota` windows once a publisher polls the connector under a registered caller.
|
||||
|
||||
### Forks for the operator (recommendations given)
|
||||
|
||||
1. **Who is the stranger?** If public, the board shows ids and campaign slugs only (cut 1 as
|
||||
written); "on what" in words needs a typed `public_label` that the claiming agent sets, not
|
||||
`follow_up.item`. Recommend ids only until that field exists.
|
||||
2. **Where does the projector live?** Recommend a small reader that queries the eight Huginns (all
|
||||
answer today) and publishes `swarm.work.v1`, rather than adding publication to `mind-heartbeat`,
|
||||
which is Idunn-managed with a single-file store and a dollar model in flux. Confidence moderate:
|
||||
the cost is a new daemon, which needs the independent-lifecycle account `gamecult.md` requires.
|
||||
3. **Is "open" enough?** Recommend marking `Working` only when a Run is in force **and** its
|
||||
carrier is attested (the Run was admitted by a launch that is still live); the data for that
|
||||
attestation is not in the Run. Until it is, say in the surface that `Working` means "admitted,
|
||||
unresolved".
|
||||
4. **Typed times.** Recommend asking Huginn for `started_at` and `ended_at` as Run/Resolution
|
||||
document fields, so no consumer parses labels or header facts.
|
||||
5. **Sequencing.** A stranger sees nothing until core and a backend land. If the swarm board should
|
||||
come earlier than the TUI backend, say so; the TUI is the planned first backend and gives agents
|
||||
a reader, but a stranger needs the web backend (map cut 4).
|
||||
|
||||
### Substrate gaps this pass felt
|
||||
|
||||
- The `swarm-dashboard-first-thing2-surface` ruling and the `thing2` campaign are in Eureka's mind,
|
||||
not readable from libby's: cut status here comes from a 2026-10-12 prose map.
|
||||
- `heartbeat.cc` unreadable from a Self run, and a file, not a served document.
|
||||
- No typed `ended_at` / `started_at` on Run; outcome is free text in `reason`.
|
||||
- No per-client read identity on the eight Huginns (`client unknown`).
|
||||
|
|
@ -22,7 +22,7 @@ restate their options. Research for the forks:
|
|||
|
||||
## Body facts
|
||||
|
||||
Probes P1-P16 ran 2026-10-10, P17-P19 2026-10-12, against `origin/<default>` after `git fetch` unless a
|
||||
Probes P1-P16 ran 2026-10-10, P17-P22 2026-10-12, against `origin/<default>` after `git fetch` unless a
|
||||
local path is named. Scripts are in the session scratchpad
|
||||
`imagination-thing2-contract/`.
|
||||
|
||||
|
|
@ -206,6 +206,75 @@ records are rebuilt, not read (`thing:ruling:operator-compat-rebuild`), so the
|
|||
Thing 2.0 key grammar strands nothing. Probe evidence of the collision itself:
|
||||
Soul's `soul-thing2-contract-rs/rust1.log` KEY lines.
|
||||
|
||||
**P20. In C#, the constructor MessagePack calls is the one door, and the decode
|
||||
recursion fits a default thread inside the bound.** Probe
|
||||
`imagination-contract-cs-ts/` (throwaway commit `e5030d3b` on CultLib
|
||||
`b3bd8dd5`, Yggdrasil, image dotnet sdk 10.0, MessagePack 3.1.7, Debug and
|
||||
Release, log `cs2.log`). A `[MessagePackObject]` class with get-only `[Key]`
|
||||
properties and one constructor taking the keys in slot order, throwing on
|
||||
`seq < 1`:
|
||||
- public constructor: `CultDocumentRegistry` accepts it; `result-pending.bin`
|
||||
decodes through `CultDocumentMessagePackSerialization` and re-encodes
|
||||
identical; `result-seq-zero.bin` is refused by the constructor through
|
||||
`DeserializeUntyped`, through `MessagePackSerializerOptions.Standard`, and
|
||||
through `System.Text.Json` (which calls the same constructor); no message
|
||||
holds the golden's canary.
|
||||
- private constructor with `[SerializationConstructor]` under
|
||||
`[MessagePackObject(AllowPrivate = true)]`: the same results, the registry
|
||||
accepts it, reflection finds no public constructor and no public setter, and
|
||||
`System.Text.Json` refuses the type (`NotSupportedException`).
|
||||
- internal constructor without `AllowPrivate`: the registry refuses the type
|
||||
("no constructor MessagePack can call"), and `CultDocumentRegistry.Shared`
|
||||
then refuses every type in the assembly, so one bad type breaks the package.
|
||||
- `outcome-unknown-arm.bin` is refused through `CultDocumentMessagePackSerialization`
|
||||
(`CultUnionGuard`: "Union ... has no arm for key 4"), but decodes with a null
|
||||
outcome through `MessagePackSerializerOptions.Standard`: the guard lives only
|
||||
in CultLib's options, so a package decodes only through them.
|
||||
- Stack: a Box-shaped recursive union (3 containers per level) reaches the
|
||||
256 bound at 84 levels. Decoding recurses per level: depth 10 decodes on a
|
||||
64 KiB thread, depth 84 needs 256 KiB in Debug and 128 KiB in Release, and an
|
||||
overflow aborts the process (rc 134). .NET's 1 MiB default thread holds the
|
||||
deepest admitted payload with at least 4x margin, so C# carries no part of
|
||||
`cultlib-gaps:follow_up:decode-stack-safety`.
|
||||
|
||||
**P21. In TypeScript, cultcache-ts runs a document's codec and schema on both
|
||||
doors, and its recursion fits the default stack.** Same probe commit, image
|
||||
kotlin (Node 24.21.0), log `ts2.log`. cultcache-ts at `b3bd8dd5` (0.14.0) has
|
||||
`slots`, `union`, `list`, `lazy`, `unit` and the 256 payload bound;
|
||||
`decodeDocumentPayload` runs the codec then `schema.parse`, and
|
||||
`encodeDocumentPayload` runs `schema.parse` then the codec
|
||||
(`src/document.ts`). A `seq` codec delegating to `int32` with a `>= 1` check,
|
||||
and a `CommandResult` class with a `#brand` private field, a private
|
||||
constructor, a codec wrapping its slot codec that mints an instance on decode,
|
||||
and a `schema.parse` that refuses anything without the brand:
|
||||
- `result-pending.bin` decodes to an instance and re-encodes identical;
|
||||
`result-seq-zero.bin` is refused ("seq (slot 1): Seq must be at least 1");
|
||||
`outcome-unknown-arm.bin` is refused ("Union Outcome: unknown arm key 4");
|
||||
`result-extra-slot.bin` reads as seq 1; no message holds the canary.
|
||||
- `structuredClone`, spread and `JSON.parse` of an instance lose the brand,
|
||||
and encoding any of them, or an object literal with seq 0, is refused; the
|
||||
factory with seq 0 throws.
|
||||
- `tsc` (strict, NodeNext, ESM importing the CommonJS cultcache-ts) passes
|
||||
with `@ts-expect-error` on an object literal and a spread typed
|
||||
`CommandResult`, on `new CommandResult(...)`, and on assigning `seq`;
|
||||
`structuredClone(result)` and `JSON.parse` still type as `CommandResult`, so
|
||||
the runtime brand check is the guard there.
|
||||
- Stack: the same chain reaches the bound at 84 levels. Depth 84 decodes with
|
||||
`--stack-size` 200 and the default 984; at 60 to 120 the codec throws (a
|
||||
`RangeError`, wrapped by the codec into its own refusal), never aborting.
|
||||
|
||||
**P22. Packaging around the two packages.** Eve `6ae2da0`: `.gitignore`
|
||||
ignores `packages/*` with one `!` pair per package (lines 2-16), so a new
|
||||
package directory is invisible to git until it gets its pair, and global
|
||||
`obj/` (line 17) is where the Thing 1 test project keeps its output
|
||||
(`BaseOutputPath obj\bin\`). `GameCult.Eve.Surface.csproj` reads CultLib from
|
||||
`CultLibRoot`, defaulting to the sibling checkout `..\..\..\CultLib`.
|
||||
`npm view @gamecult/cultcache-ts` answers 404: cultcache-ts is on no registry,
|
||||
and the consumers that use it (Heimdall, StreamPixels) take it as
|
||||
`file:vendor/CultLib/packages/cultcache-ts` from a submodule. No file outside
|
||||
`crates/` and `docs/` names `thing-contract` or `GameCult.Thing.Contract` at
|
||||
`6ae2da0` except `Cargo.lock`.
|
||||
|
||||
## The model page
|
||||
|
||||
Every persistent kind Thing 2.0 has. Ids follow rename table T: schema
|
||||
|
|
@ -413,19 +482,29 @@ code fails it.
|
|||
consumers' languages: Rust, JS/TS, C#). The first proof (P9) was run: a
|
||||
Rust-written recursive union does not decode in the C# reference, nor the
|
||||
reverse (P12), and TS decodes only what a hand-written codec says (P13). The
|
||||
contract waits on CultLib's union parity
|
||||
(`thing2:follow_up:cultcache-union-parity`). Three cuts: `contract-rs`, then
|
||||
contract waited on CultLib's union parity
|
||||
(`thing2:follow_up:cultcache-union-parity`), which is on CultLib main by
|
||||
`b3bd8dd5` (P20, P21) and unreleased. Three cuts: `contract-rs`, then
|
||||
`contract-ts` and `contract-cs`. The published schema of each kind is its
|
||||
CultCache catalog entry, identical in the three packages; no JSON Schema
|
||||
files are written, because Thing 2.0 has no JSON reader of these kinds (the
|
||||
browser reads them through core, not `ajv`). Python and Kotlin packages
|
||||
follow as their own deliverables.
|
||||
`contract-rs` attempt 1 landed on `hands/thing2-contract-rs` (`649844aa`);
|
||||
Soul's `thing2:verdict:cut-contract-rs.s1` returned seven findings. Its fix
|
||||
batch is `thing2:cut_spec:cut-contract-rs.r2`. Waiting, one line each:
|
||||
- `contract-ts`, `contract-cs`: their r1 specs predate r2's domain types,
|
||||
key grammar and refusal goldens, and are refreshed when the frontier
|
||||
reaches them.
|
||||
`contract-rs` landed on Eve main (`6ae2da0`) after two Soul passes
|
||||
(`thing2:verdict:cut-contract-rs.s1`, `.s2`). The siblings are mapped
|
||||
against `6ae2da0` and run side by side, touching only their own package and
|
||||
one `.gitignore` insertion each, at separate anchors:
|
||||
`thing2:cut_spec:cut-contract-cs.r2` and `thing2:cut_spec:cut-contract-ts.r2`.
|
||||
Waiting, one line each:
|
||||
- Cross-runtime store check, after both siblings land: the C# package opens
|
||||
a store the TS package wrote and the reverse, which proves the catalog
|
||||
type names (`GameCult.Thing.Contract.<RustName>`) agree; neither sibling
|
||||
can prove it alone.
|
||||
- Publication: when `cultlib-gaps:cut_spec:cut-union-parity-release.r3`
|
||||
publishes cultcache-ts, the TS package's dependency moves from the sibling
|
||||
`file:` path to the published version, and both packages get a release
|
||||
line. Until then both read CultLib from the sibling checkout at the Cargo
|
||||
rev (P22).
|
||||
- CultLib pin bump: when `cultlib-gaps:follow_up:decode-stack-safety` lands,
|
||||
move the pin and run `deep_tree` on the default thread (its 16 MiB thread
|
||||
goes); the crate docs' hazard note goes with it.
|
||||
|
|
@ -505,6 +584,23 @@ have no live consumer and are parked until one appears.
|
|||
instead impose a grammar on ids the contract does not own. Listing a
|
||||
provider's records filters on the record's provider field, not a key prefix,
|
||||
because a dotted provider is a prefix of a longer one.
|
||||
- **Why each sibling holds its domains where its decoder constructs.** The
|
||||
rule from contract-rs is that a domain is checked on every door by one owner.
|
||||
C# has no newtype that keeps the leaf's bytes without a custom formatter,
|
||||
which CultLib owns, but MessagePack, System.Text.Json and code all build a
|
||||
C# value through the one constructor that takes the slots (P20), so that
|
||||
constructor, calling `ThingDomain`, is the door. cultcache-ts runs each
|
||||
slot's codec on encode and decode (P21), so a domain codec over the plain
|
||||
leaf is the door there, the same shape as Rust's newtype `Deserialize`.
|
||||
`CommandResult` is closed differently in each language: a private
|
||||
constructor under `AllowPrivate` in C#, a `#brand` field checked by the
|
||||
document's `schema.parse` in TS, because TypeScript's structural types let
|
||||
any object with the right fields pass as a class with only public members.
|
||||
- **Why the siblings read CultLib from a sibling checkout, not a submodule.**
|
||||
The C# package already reads CultLib that way (`CultLibRoot`), cultcache-ts
|
||||
is on no registry yet (P22), and the Rust crate pins the same commit by
|
||||
Cargo rev. A submodule would be a third pin to keep equal to the Cargo rev;
|
||||
publication replaces the `file:` path instead.
|
||||
- **Why contract-rs merges before CultLib's stack fix.** The overflow is the
|
||||
decode door's (`cultlib-gaps:follow_up:decode-stack-safety`) and every Rust
|
||||
reader of these kinds inherits it whether or not the contract merges. The
|
||||
|
|
|
|||
Loading…
Reference in a new issue