@bongos/core 1.17.1 → 1.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +521 -281
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# Parallel-load test results — 2026-05-30 ([#272](https://example.com/builders#/task/272), criterion C6)
|
|
2
|
-
|
|
3
|
-
> **Harness:** [`tests/parallel-load.mjs`](../../tests/parallel-load.mjs) · **Procedure:** [parallel-load-test.md](parallel-load-test.md) · **Target:** seeded staging ([#262](https://example.com/builders#/task/262)) at `http://127.0.0.1:3001` on the droplet (direct, bypassing Cloudflare).
|
|
4
|
-
|
|
5
|
-
This is the live run that closes **[#272](https://example.com/builders#/task/272) (V3.R53)** and satisfies criterion **C6 — parallel-safe-under-load**. It exercises the deterministic claim concurrency audited in [#269](https://example.com/builders#/task/269) (the `claimTask()` transaction + `uniq_active_claim_per_task` index) against a real running server under concurrent load.
|
|
6
|
-
|
|
7
|
-
## Bottom line
|
|
8
|
-
|
|
9
|
-
**Across both runs: ZERO `server_error` (5xx), ZERO races, ZERO anomalies.** Every claim resolved to exactly one of: `success`, `collision` (409 — exactly-one-wins), or `throttled` (429 — the rate limiter cleanly shedding excess writes). Staging's journal logged **0** 5xx responses over the full window. C6's bar — "all claims succeed or 409 cleanly; no 500s, no race conditions" — holds, with the added finding that excess write load is shed cleanly via 429 rather than ever degrading into a race or crash.
|
|
10
|
-
|
|
11
|
-
## Run A — sustained 30-minute run (the [#272](https://example.com/builders#/task/272) acceptance run)
|
|
12
|
-
|
|
13
|
-
`--workers 5 --duration 1800 --work-ms 150` (natural claim→work→release→repeat loop across distinct tasks):
|
|
14
|
-
|
|
15
|
-
| metric | value |
|
|
16
|
-
|---|---|
|
|
17
|
-
| wall | 1800.1s (30 min) |
|
|
18
|
-
| claim attempts | 144 |
|
|
19
|
-
| success | 13 |
|
|
20
|
-
| collision (409, exactly-one-wins) | 8 (5.6%) |
|
|
21
|
-
| throttled (429) | 123 |
|
|
22
|
-
| **server_error (5xx)** | **0** |
|
|
23
|
-
| client_error / other | 0 / 0 |
|
|
24
|
-
| anomalies | 0 |
|
|
25
|
-
| staging 5xx in journal | 0 |
|
|
26
|
-
|
|
27
|
-
The system stayed healthy (`/healthz` 200) for the entire 30 minutes with no degradation. Low attempt volume is expected: the per-IP rate limiter (below) caps writes, and releases that get throttled leave tasks `active`, draining the claimable pool — so workers spend most cycles idling on an empty queue rather than hammering. That's the system behaving correctly under its own backpressure.
|
|
28
|
-
|
|
29
|
-
## Run B — high-intensity contention burst (forces collisions)
|
|
30
|
-
|
|
31
|
-
`--workers 5 --duration 25 --work-ms 100 --contend` (all 5 workers race the SAME task — the canonical "two builders, one task" case):
|
|
32
|
-
|
|
33
|
-
| metric | value |
|
|
34
|
-
|---|---|
|
|
35
|
-
| claim attempts | 2733 |
|
|
36
|
-
| success | 8 |
|
|
37
|
-
| collision (409, exactly-one-wins) | 17 |
|
|
38
|
-
| throttled (429) | 2708 |
|
|
39
|
-
| **server_error (5xx)** | **0** |
|
|
40
|
-
| anomalies | 0 |
|
|
41
|
-
|
|
42
|
-
This is the direct demonstration of the C6 invariant: when multiple workers race the same task row, **exactly one wins and the rest get a clean 409 `ALREADY_CLAIMED`** — never two winners, never a 5xx.
|
|
43
|
-
|
|
44
|
-
## The rate limiter (a relevant, healthy finding)
|
|
45
|
-
|
|
46
|
-
`src/bongos/middleware/rate-limit.js` enforces **30 writes / 60s per-IP** and **60 writes / 60s per-session** on write methods (GET is unlimited). Because all simulated workers run from the droplet's single IP, the per-IP limit binds first, so most write attempts under load return **429** (`throttled` above). For a parallel-safety test this is a *positive* result: under sustained overload the server sheds excess writes cleanly (429 + `Retry-After`) instead of racing or 500ing. To exercise the claim logic at higher volume in a future run, raise the limit on staging or distribute workers across IPs.
|
|
47
|
-
|
|
48
|
-
## Harness fixes made while running this (committed under [#272](https://example.com/builders#/task/272))
|
|
49
|
-
|
|
50
|
-
Running against the real server surfaced live-path bugs the self-test (mock) couldn't:
|
|
51
|
-
|
|
52
|
-
- **claimable parsing:** the harness read `.data.tasks`, but `GET /tasks/claimable` returns `{ claimable: [...] }` → it saw 0 tasks and never claimed. Fixed to read `.claimable` (with `.tasks` fallback).
|
|
53
|
-
- **429 classification:** 429 was bucketed as `other` / flagged as a release anomaly. Added a dedicated `throttled` outcome (clean) so the report and the pass/fail conclusion are accurate.
|
|
54
|
-
- **`--contend` mode:** added so workers can be pointed at one task to demonstrate exactly-one-wins collisions on demand (Run B).
|
|
55
|
-
|
|
56
|
-
## Reproduce
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
# On the droplet (staging seeded per #262, service running, alphanumeric token minted):
|
|
60
|
-
cd /home/lars/example-staging
|
|
61
|
-
BASE_URL=http://127.0.0.1:3001 TOKEN=<redacted> node tests/parallel-load.mjs --workers 5 --duration 1800 --work-ms 150
|
|
62
|
-
# add --contend to force same-task collisions
|
|
63
|
-
```
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# Parallel-load test — exercising claim concurrency under N builders
|
|
2
|
-
|
|
3
|
-
> **Harness:** [`tests/parallel-load.mjs`](../../tests/parallel-load.mjs) (built under prep task [#472](https://example.com/builders#/task/472)).
|
|
4
|
-
> **Live run:** criterion **C6** task **[#272](https://example.com/builders#/task/272)** (V3.R53) — a 30-min, 5-builder run against **staging**, gated on **[#262](https://example.com/builders#/task/262)** (staging data seeding).
|
|
5
|
-
|
|
6
|
-
The harness spawns N concurrent simulated "builder" loops that hammer the GDS claim lifecycle (claim → simulated work → release → repeat) and classifies every claim outcome into `success` / `collision` (clean 409/refusal) / `server_error` (the anomaly we hunt) / `client_error` / `other`. It validates the deterministic claim concurrency C6 requires — the same `claimTask()` transaction + `uniq_active_claim_per_task` index audited in [multi-builder-merge.md](multi-builder-merge.md).
|
|
7
|
-
|
|
8
|
-
## Self-test (CI-safe, no network)
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
node tests/parallel-load.mjs # defaults: 5 workers, 60s
|
|
12
|
-
node tests/parallel-load.mjs --workers 5 --duration 3 --work-ms 50
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
With no `BASE_URL`, the harness drives an **in-process mock server** whose claim semantics mirror the DB (one active claim per task; touches-overlap → conflict). The board deliberately has two tasks sharing a path so collisions are *forced*, then asserts: ≥1 success, ≥1 collision, **zero** server_error, no invariant violation, and that attempts == sum of classes. Exit 0 = harness accounting proven correct. This runs in CI without a droplet.
|
|
16
|
-
|
|
17
|
-
## Live run against staging ([#272](https://example.com/builders#/task/272) — once [#262](https://example.com/builders#/task/262) has seeded staging)
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
# Get builder tokens (one or more simulated builders share the load).
|
|
21
|
-
# NEVER point this at production — it mutates real claim/task state. The
|
|
22
|
-
# harness refuses a prod example.com host unless you pass --i-know.
|
|
23
|
-
BASE_URL=https://staging.example.com \
|
|
24
|
-
TOKENS=$tokA,$tokB,$tokC,$tokD,$tokE \
|
|
25
|
-
node tests/parallel-load.mjs --workers 5 --duration 1800
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
`#272` acceptance: run 30 min (`--duration 1800`); **every claim resolves to `success` or a clean `collision` — zero `server_error`, no races**; the report's throughput + collision-rate are the deliverable. A single anomaly (any 5xx, thrown error, or invariant violation) exits 1 and fails the run.
|
|
29
|
-
|
|
30
|
-
## Why the split (prep [#472](https://example.com/builders#/task/472) vs live [#272](https://example.com/builders#/task/272))
|
|
31
|
-
|
|
32
|
-
`#272`'s done-when explicitly requires a seeded staging environment, which `#262` (backlog) provides. Rather than block the runnable harness behind infra, [#472](https://example.com/builders#/task/472) built + self-validated the harness now; [#272](https://example.com/builders#/task/272) stays the live-staging run. When [#262](https://example.com/builders#/task/262) ships, point the harness at staging per above to close [#272](https://example.com/builders#/task/272) and satisfy C6.
|
|
@@ -1,411 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
scope: art *pipeline* (code, design system, review loops) — the tooling builders use to make tiles, plus the tile PNGs it produces that ship into `public/assets/tiles/`.
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Pixel-art generation pipeline — recipes and gotchas
|
|
6
|
-
|
|
7
|
-
**TL;DR**: AI image models will NOT produce true 32×32 pixel art on demand. The pipeline survives this by post-processing every output (median-downsample → palette quantize → snap) and by falling back to procedural Python when the AI repeatedly misses the subject. Self-review via Gemini 2.5 Flash vision against a structured rubric is what makes the loop autonomous.
|
|
8
|
-
|
|
9
|
-
## Critical recipes
|
|
10
|
-
|
|
11
|
-
### Generating a 32×32 tile from Nano Banana / Imagen
|
|
12
|
-
|
|
13
|
-
Both models output ~1024×1024. We don't ask for 32×32 directly — they ignore that constraint. Instead we:
|
|
14
|
-
|
|
15
|
-
1. **Ask for "32×32 logical pixels rendered at 1024×1024"** in the prompt — i.e., each "logical pixel" should appear as a clear ~32-pixel block in the output.
|
|
16
|
-
2. **Center-crop to a square** (Imagen sometimes pads).
|
|
17
|
-
3. **Median-downsample by block** (32×32 blocks → one output pixel per block via `np.median`). NOT nearest-neighbor — too noisy. NOT bilinear — introduces AA.
|
|
18
|
-
4. **Palette-quantize** every pixel to the nearest of the 64 colors in `modules/art-pipeline/template/palette.json`.
|
|
19
|
-
|
|
20
|
-
Code: `modules/art-pipeline/pipeline/post_process.py:process()`.
|
|
21
|
-
|
|
22
|
-
### When the AI refuses to produce the right subject
|
|
23
|
-
|
|
24
|
-
Empirically, Nano Banana frequently generates **sand-colored textures even when prompted explicitly for grass**. Multiple retries with stronger prompts didn't help. Imagen 4 had similar failure modes but on different subjects.
|
|
25
|
-
|
|
26
|
-
**The fix that worked**: a **Tier 5 procedural fallback** in `procedural_fallback.py`. For each common ground-tile id, we have a hand-coded Python generator that scatters palette colors at controlled density. Always palette-correct, always 32×32, always tileable. Score reliably ≥9 on objective criteria.
|
|
27
|
-
|
|
28
|
-
The orchestrator escalates to T5 only after T1–T4 fail twice each. So we burn AI cost first (looks more authentic when it works), and fall back to procedural only when stuck.
|
|
29
|
-
|
|
30
|
-
### Python TLS on macOS Python.org installer
|
|
31
|
-
|
|
32
|
-
Default Python from python.org doesn't trust system CAs. `urllib.request` against Google APIs throws `CERTIFICATE_VERIFY_FAILED`. Fix: install certifi (`pip install --user certifi`) and pass an SSL context built from `certifi.where()`. See top of `gen_api.py` and `review.py`. **Windows:** native Windows Python usually trusts the system CA store, so this often isn't needed; if it is, run `py -m pip install certifi` (`python3`/`--user` semantics differ on Windows).
|
|
33
|
-
|
|
34
|
-
### Output buffering during long runs
|
|
35
|
-
|
|
36
|
-
Python's `print()` is line-buffered when stdout is a TTY but fully buffered otherwise. Long-running orchestrator runs show no live output unless launched with `python3 -u …` or `PYTHONUNBUFFERED=1`. Per-attempt state IS persisted to `modules/art-pipeline/iterations/<tile>/log.jsonl` regardless, so progress is observable even without live stdout. (**Windows:** `python3` is usually `py`/`python`; the `PYTHONUNBUFFERED=1 python3 …` inline-env form is bash-only — set it separately first: `set PYTHONUNBUFFERED=1` in cmd / `$env:PYTHONUNBUFFERED=1` in PowerShell, or just use `py -u …`.)
|
|
37
|
-
|
|
38
|
-
## Structural insights
|
|
39
|
-
|
|
40
|
-
### The reference clusters carry the style
|
|
41
|
-
|
|
42
|
-
Slicing the 24 Pokemon FireRed reference maps on a 16×16 grid yields ~8,800 unique tiles. K-means on RGB histograms collapses them into 64 visual clusters. The **cluster overview sheet** (`modules/art-pipeline/template/cluster_overview.png`, 8×8 grid of representative tiles) is what we pass to the AI reviewer as the "what FireRed looks like" reference. It carries more style information than any prose description.
|
|
43
|
-
|
|
44
|
-
Each vocab item in `vocabulary.json` lists `ref_clusters` — the cluster ids most relevant to that tile (e.g. grass tiles reference c19, c04, c21). Tier 2 generation passes representative tiles from those clusters as image-conditioning, which improves style match.
|
|
45
|
-
|
|
46
|
-
### The locked palette is non-negotiable
|
|
47
|
-
|
|
48
|
-
64 colors extracted from the Pokemon reference corpus via weighted k-means. **Every output pixel must snap to one of these colors.** This guarantees:
|
|
49
|
-
- Coherent visual world (no two tiles fight each other).
|
|
50
|
-
- AA-free outputs (palette is small enough that quantization snaps blurry edges hard).
|
|
51
|
-
- Diff-able tile evolution: if a tile changes, the change is in pixel choices, not subtle color drift.
|
|
52
|
-
|
|
53
|
-
If we ever want to expand the palette (e.g. add cooler marble tones), it's an ADR-worthy decision and a one-line update to `palette.json` — but every existing tile needs re-quantizing.
|
|
54
|
-
|
|
55
|
-
### The rubric is the memory
|
|
56
|
-
|
|
57
|
-
Lars feedback ("trees too modern, more gnarled") becomes a rule appended to `style_guide.md`. The rule influences every subsequent generation because the reviewer reads the style guide on every call. **The system improves over time without manual prompt-engineering** — feedback is durable, not conversational.
|
|
58
|
-
|
|
59
|
-
## What does NOT work (do not waste time on these)
|
|
60
|
-
|
|
61
|
-
- **Asking the model directly for 32×32 output.** Models output 1024×1024 regardless of prompt. Always post-process.
|
|
62
|
-
- **Generating sprites with transparent backgrounds via prompt.** Gemini fights this. Use solid magenta (#FF00FF) background, then color-key it out in post-process.
|
|
63
|
-
- **Using `nearest-neighbor downsample` for the AI→32×32 step.** Picks up too much per-pixel noise. Median is consistently better.
|
|
64
|
-
- **Trusting the AI's own "this is pixel art" claim.** It will produce pixel-art-styled outputs that have soft edges, wrong-grid alignment, and 1000+ colors. Always run deterministic checks before AI review.
|
|
65
|
-
- **Generating siblings independently.** Per-tile generation cannot produce sibling coherence (same character in different facings, paths that connect, transitions that line up). The model has no cross-call memory. See "Family pipeline" below for the fix.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Family pipeline (Phase 1+ architecture)
|
|
70
|
-
|
|
71
|
-
The original per-tile pipeline produced character sprites where each direction looked like a different person. Root cause: independent generations have no shared style anchor. The family pipeline (`modules/art-pipeline/pipeline/family_*.py`) fixes this by making the **family** — not the tile — the unit of generation.
|
|
72
|
-
|
|
73
|
-
For decision rationale and alternatives considered, see [ADR 0008](../adr/<redacted>.md).
|
|
74
|
-
|
|
75
|
-
### What good looks like (numerically)
|
|
76
|
-
|
|
77
|
-
After migrating the player character to a family:
|
|
78
|
-
|
|
79
|
-
| Metric | Per-tile pipeline | Family pipeline |
|
|
80
|
-
|---|---|---|
|
|
81
|
-
| Silhouette area CV across frames | 0.67 (177→900 px) | 0.11 (213→282 px) |
|
|
82
|
-
| Silhouette height CV | 0.08 | 0.00 (all 27 px) |
|
|
83
|
-
| Palette Jaccard, worst pair | 0.29 | 0.74 |
|
|
84
|
-
| Dominant-color overlap, worst pair | 0.00 | 0.60–0.80 |
|
|
85
|
-
| Luminance EMD, worst pair | 71.97 | 12–16 |
|
|
86
|
-
| Generations to a passing result | 4 separate calls | 1 sheet call |
|
|
87
|
-
| Cost per character set | ~$0.16 | ~$0.04 |
|
|
88
|
-
|
|
89
|
-
### Recipe: generating a family
|
|
90
|
-
|
|
91
|
-
1. **Define the family** in `modules/art-pipeline/template/families.json`. A family has `kind` (taxonomy), `layout` (e.g. `2x2`, `1x4`), `members` (each with a cell coordinate), `shared_style` (one paragraph that every member must respect), and optional per-family threshold overrides.
|
|
92
|
-
2. **One image-gen call** per family. The sheet prompt instructs the model on the cell layout *and* the cross-cell coherence rules. Code: `family_generator.build_sheet_prompt()`.
|
|
93
|
-
3. **Slice** the returned sheet by the family's layout. Each cell becomes a 32×32 PIL image after median-downsample + palette-quantize. Code: `family_generator.slice_sheet()`.
|
|
94
|
-
4. **Run deterministic family checks** on the sliced cells. These compute numeric coherence metrics (silhouette area CV, height CV, palette Jaccard, dominant-color overlap, luminance EMD). Hard-fail if any is below its threshold. Code: `family_checks.run_all()`.
|
|
95
|
-
5. **Only if step 4 passes**, run the Gemini family-coherence review. The review is told which deterministic checks already passed, so it focuses on subjective criteria (`family_consistency`, `character_identity`, `grid_alignment`, `firered_match`, `mythic_greek`). Code: `family_review.review_family()`.
|
|
96
|
-
6. **Atomic-swap to publish.** The orchestrator does NOT auto-publish. Run with `--publish` to copy sliced cells into `modules/art-pipeline/generated/tiles/`. Default behavior is "save under `iterations/_families/<id>/` and let the human approve."
|
|
97
|
-
|
|
98
|
-
### Recipe: composing the sheet prompt
|
|
99
|
-
|
|
100
|
-
The prompt is built in three blocks:
|
|
101
|
-
- **Header**: "Generate ONE pixel-art sheet for a 16-bit RPG tile-set in the style of Pokemon FireRed, restyled for ancient Mediterranean Greece. Output a single image laid out as a {rows}-row by {cols}-column grid of {n} cells."
|
|
102
|
-
- **Discipline**: "REQUIREMENTS for every cell: top-down view, hard pixel edges, NO anti-aliasing, NO blur, NO frame, NO border between cells, NO text, NO labels, NO arrows, NO numbering."
|
|
103
|
-
- **Coherence**: "CRITICAL coherence rules across cells: every cell uses the SAME palette of colors, the SAME lighting direction (top-left), the SAME saturation level, and the SAME dithering density. The sheet must read as ONE coordinated set."
|
|
104
|
-
|
|
105
|
-
Per-cell descriptions then go in. For character families, an additional rule: *"All cells depict the SAME individual: identical body shape, identical clothing colors, identical hair, identical proportions. Only the facing direction changes."*
|
|
106
|
-
|
|
107
|
-
### Why deterministic-first ordering matters
|
|
108
|
-
|
|
109
|
-
Putting deterministic checks **before** the Gemini review changes the cost shape entirely:
|
|
110
|
-
|
|
111
|
-
- Old loop (per-tile): every retry burned a Gemini review call to discover failure.
|
|
112
|
-
- New loop (family): a failing family fails the deterministic gate for free, and the failure produces a *numeric* fix-hint ("CV 0.67 > 0.35, make characters same height") that's far more actionable than prose critique.
|
|
113
|
-
|
|
114
|
-
Gemini becomes a polish check, not a failure detector. On the player_character run, Gemini was called once. Old per-tile equivalent would have been 4–24 review calls.
|
|
115
|
-
|
|
116
|
-
### Anchor refinement (failure recovery)
|
|
117
|
-
|
|
118
|
-
The orchestrator's tier 5 (`t5_anchor_refine`) is the recovery move when the first attempt has *one* good cell but family coherence fails:
|
|
119
|
-
|
|
120
|
-
1. After every attempt, `family_checks.find_anchor_member()` picks the member whose silhouette + palette is most representative of the family.
|
|
121
|
-
2. That member's PNG is saved as `iterations/_families/<id>/_anchor.png`.
|
|
122
|
-
3. On the next attempt, `t5_anchor_refine` passes the anchor as a reference image to nano-banana, with the prompt: *"The first attached image is the locked anchor. Every cell must match the anchor's clothing colors, hair, body proportions, and palette. Only the facing direction changes."*
|
|
123
|
-
|
|
124
|
-
In the player run this never fired (first attempt passed). It exists as the recovery path for harder families.
|
|
125
|
-
|
|
126
|
-
### Thresholds: per-family, not global
|
|
127
|
-
|
|
128
|
-
`families.json` has a top-level `defaults` block (currently 9.0/7.0 with default deterministic thresholds) and per-family overrides. Why per-family:
|
|
129
|
-
|
|
130
|
-
- A character family tolerates more palette variation than a ground-tile family (different shading per facing is expected).
|
|
131
|
-
- A ground-tile family must self-seam tightly (column 0 ≈ column 31), but characters have no self-seam concept at all.
|
|
132
|
-
- A path autotile must satisfy edge contracts that don't apply to characters.
|
|
133
|
-
|
|
134
|
-
Override only what differs. Inherited keys flow from `defaults.deterministic_thresholds` to the family.
|
|
135
|
-
|
|
136
|
-
### Phase 1 calibration: lower threshold to bootstrap, ramp later
|
|
137
|
-
|
|
138
|
-
For player_character we set `pass_threshold: 7.0`, `min_individual: 5.0` to get to a first pass cheaply and validate the architecture. The actual run scored 8.6 then 9.0 — well above the 7.0 bar, suggesting the architecture had more headroom than the lowered bar required. **Operating rule: leave the threshold at 7.0 until enough families have run that we have data on real ceiling.** Ramping prematurely just creates noise.
|
|
139
|
-
|
|
140
|
-
### Family-pipeline pitfalls observed
|
|
141
|
-
|
|
142
|
-
- **`.env.local` does not propagate to git worktrees.** It's gitignored, lives only in the worktree where it was created. When working in a new worktree, copy it from a sibling worktree before running anything that touches the Google API.
|
|
143
|
-
- **The Gemini reviewer can be flattering when the deterministic block tells it everything passed.** Stay vigilant: read at least one Gemini critique JSON per session to confirm the prose tracks the numbers.
|
|
144
|
-
- **Cell magenta-keying still has occasional residue** at the seam between sprite and the cell's magenta border. Same fix as the per-tile pipeline: ensure the model's magenta is pure (no shading, no dithering on the magenta) and trust the post-process keying.
|
|
145
|
-
- **Sheet aspect ratio matters.** A 2×2 layout produces a square output (good — both Imagen and Nano Banana prefer 1:1). A 1×4 layout produces 4:1, which Imagen's `aspect` parameter supports but Nano Banana's wrapper does not control. Prefer 2×2 over 1×4 for 4-member families until that's resolved.
|
|
146
|
-
|
|
147
|
-
### When to migrate an existing tile into a family
|
|
148
|
-
|
|
149
|
-
The old per-tile pipeline still works for the 22 non-character tiles. We migrate when:
|
|
150
|
-
1. The existing per-tile output is unsatisfactory (e.g., the four player frames).
|
|
151
|
-
2. The tile has natural siblings (transitions, autotiles, multi-tile objects, character spritesheets).
|
|
152
|
-
3. We're rebuilding the area for some other reason and might as well do it right.
|
|
153
|
-
|
|
154
|
-
Otherwise leave existing tiles alone — they're published and working.
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
## Stress test (Phase 1.5) — 8 families, three generation patterns
|
|
159
|
-
|
|
160
|
-
After the player_character family validated the architecture for one pattern, we ran a stress test against 8 families spanning every shape of family we could think of: characters, transitions, multi-tile objects, paths, cliffs, houses, boats, coastlines. Round 1 surfaced an architecture gap; Round 2 fixed the prompt strategy; Round 3 fixed the check dispatch. Final result: 7 of 8 families pass, with the 8th failing on a real subjective style judgment (not architecture).
|
|
161
|
-
|
|
162
|
-
Total stress-test cost: **$1.44 of $80**. Three rounds, ~30 image-gen calls, ~20 Gemini reviews.
|
|
163
|
-
|
|
164
|
-
### The three patterns (this is what we learned)
|
|
165
|
-
|
|
166
|
-
What initially looked like one architecture is actually **three**. Each needs different prompt language and different check dispatch.
|
|
167
|
-
|
|
168
|
-
#### Pattern A — coordinated variants
|
|
169
|
-
Each cell is a different *view of one concept*. Cells never sit adjacent in-game — different cells get placed at different world locations. Coherence = identity (same character, same boundary style, same foam color).
|
|
170
|
-
|
|
171
|
-
Examples: `player_character` (4 facings), `vendor_hyperion` (1 pose), `transition_grass_sand` (4 directional boundaries), `coastline_sand_sea` (4 directional shorelines), `small_boat` (1 sprite).
|
|
172
|
-
|
|
173
|
-
- Prompt: describe each cell separately with strong cross-cell coherence rules ("identical clothing, identical hair, identical foam color across all cells").
|
|
174
|
-
- Deterministic checks that apply: cross-cell color coherence (palette Jaccard, dominant overlap, luminance EMD) + sprite silhouette CV for character kinds.
|
|
175
|
-
- Edge contracts: do NOT apply (cells aren't placed adjacent in-game).
|
|
176
|
-
- Gemini family-review: judges identity coherence + style match.
|
|
177
|
-
|
|
178
|
-
#### Pattern B — continuous illustration
|
|
179
|
-
The family is **one object** drawn as a single illustration that we slice on a virtual grid afterward. The model is NOT told about cells at all. Cells are different *parts of one drawing*; their palettes are *naturally* different (a roof and a foundation are different colors, by design).
|
|
180
|
-
|
|
181
|
-
Examples: `container` (one shipping container in 4 cells), `small_house` (one Mediterranean dwelling in 9 cells), `cliff_face` (one cliff in 3 cells, vertically).
|
|
182
|
-
|
|
183
|
-
- Prompt: describe the whole subject as a single illustration filling the image edge-to-edge. Explicitly forbid "cell borders, panel separators, frames, grid lines." Tell the model: *"the image will be sliced afterward, but you do not need to think about that."*
|
|
184
|
-
- Deterministic checks that apply: only per-cell post-processing (palette quantize, dimensions, AA). NO cross-cell coherence (false negatives — different parts have different palettes by design). NO edge contracts (tautological — the slicer guarantees adjacency by construction).
|
|
185
|
-
- Gemini family-review: judges whether the assembled illustration reads as one coherent thing AND matches the project style.
|
|
186
|
-
|
|
187
|
-
#### Pattern C — true autotile family (not yet validated)
|
|
188
|
-
A set of tiles where each cell is a different *topology* (NW corner, straight, T-junction, cross, etc.) that must connect cleanly at specific edges. Pattern A doesn't work (the model can't keep the path width identical across separate variant generations). Pattern B doesn't work either — the path autotile we tried produced a "border tile set" (square frame with no interior cross) when the model drew it as one continuous loop, which is not the same as a 9-topology autotile.
|
|
189
|
-
|
|
190
|
-
Best evidence: `path_autotile` under continuous_illustration produced usable tiles for paths-along-borders but lost the four-way cross that a full autotile needs.
|
|
191
|
-
|
|
192
|
-
**Pattern C likely requires a third strategy not yet built**: cell-by-cell generation with the previously-generated cell's edge as an image-conditioning reference. This is the academic "Wang tile" / image-conditioning approach. We have not yet implemented it. **Open architectural question.**
|
|
193
|
-
|
|
194
|
-
### How to set the strategy
|
|
195
|
-
|
|
196
|
-
`families.json` field: `slice_strategy`. Values:
|
|
197
|
-
|
|
198
|
-
```json
|
|
199
|
-
"slice_strategy": "coordinated_variants" // Pattern A (default if absent)
|
|
200
|
-
"slice_strategy": "continuous_illustration" // Pattern B
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Per-strategy required fields:
|
|
204
|
-
- Pattern A: per-member `desc` describing each cell.
|
|
205
|
-
- Pattern B: top-level `continuous_description` describing the whole subject as one illustration. Per-member `desc` becomes a label-only field used for the slice filename.
|
|
206
|
-
|
|
207
|
-
### What was wrong with our first attempt
|
|
208
|
-
|
|
209
|
-
Round 1 used Pattern A's prompt for everything. For continuous-object families this was wrong because:
|
|
210
|
-
- The model interpreted "grid of cells" as a sprite-sheet brief and drew **black panel borders between every cell**, even when explicitly told "no border between cells."
|
|
211
|
-
- Each cell was treated as an independent painting, so palette and lighting drifted dramatically (e.g. a 3×3 house had 3 different palettes, one per row, with luminance EMD = 84).
|
|
212
|
-
- Edge contracts measured 0–50% match.
|
|
213
|
-
|
|
214
|
-
Round 2 fixed the prompt strategy. Edge contracts immediately jumped (some hit 100%). Round 3 fixed the check dispatch (skip cross-cell + edge contracts for Pattern B). All four continuous-illustration families passed Gemini review.
|
|
215
|
-
|
|
216
|
-
### Round-by-round measurements (durable record)
|
|
217
|
-
|
|
218
|
-
| Family | Pattern | R1 result | R2 result | R3 result | Final Gemini |
|
|
219
|
-
|---|---|---|---|---|---|
|
|
220
|
-
| vendor_hyperion | A (singleton) | ✅ pass | — | — | 8.0 |
|
|
221
|
-
| transition_grass_sand | A (variants) | ✅ pass | — | — | 9.0 |
|
|
222
|
-
| small_boat | A (singleton) | ✅ pass | — | — | 9.25 |
|
|
223
|
-
| coastline_sand_sea | A (variants) | ❌ palette/dominant fail | ✅ pass (tightened anchor) | — | 8.5 |
|
|
224
|
-
| container | B (continuous) | ❌ edge 9–50% | partial (50–100%) | ✅ pass | 8.25 |
|
|
225
|
-
| small_house | B (continuous) | ❌ all 15 checks fail | worse (luminance EMD 116) | ✅ pass | 8.5 |
|
|
226
|
-
| cliff_face | B (continuous) | ❌ edge 0–25% | ❌ edge 0–60% | ⚠️ style fail (Gemini 6.25 — "too painterly, not FireRed enough") | 6.25 |
|
|
227
|
-
| path_autotile | B (continuous) | ❌ edge 0–50% | partial (50–90%) | ✅ pass (as border tile set, not full autotile) | 8.0 |
|
|
228
|
-
|
|
229
|
-
### Insights worth keeping
|
|
230
|
-
|
|
231
|
-
1. **Different patterns need different prompt language AND different check dispatch.** Conflating them makes the architecture appear broken when only the prompt strategy is wrong.
|
|
232
|
-
|
|
233
|
-
2. **The model can draw beautiful single illustrations** when not constrained to a "grid of cells" brief. Round 2's container, house, cliff, and path images were genuinely good art — the original problem was framing.
|
|
234
|
-
|
|
235
|
-
3. **Edge contracts are a Pattern-A concept misapplied to Pattern B.** For continuous illustrations, the slicer guarantees adjacency by construction. Checking pixel-match at slice lines is meaningless and misleading.
|
|
236
|
-
|
|
237
|
-
4. **Cross-cell color coherence is a Pattern-A concept.** For continuous illustrations, different parts naturally have different palettes (roof ≠ foundation, sky ≠ ground). The check correctly flagged this as "incoherent" but in fact it was a feature, not a bug.
|
|
238
|
-
|
|
239
|
-
5. **Gemini's right role is subjective polish, not failure detection.** When the deterministic gate is appropriately scoped, Gemini reliably catches subjective style problems (cliff_face's "too painterly, blended transitions instead of hard pixel edges" was a real, useful critique). When the deterministic gate is wrongly scoped, Gemini calls get wasted on false positives.
|
|
240
|
-
|
|
241
|
-
6. **The fix from 4/8 to 7/8 was small.** Two contained changes: (a) add a `slice_strategy` field with a new prompt builder branch; (b) skip cross-cell + edge-contract checks for `continuous_illustration` in `family_checks.run_all()`. About 60 lines of Python plus the schema field. The architecture core (family schema, generator pipeline, retry logic, Gemini review prompt) was unchanged.
|
|
242
|
-
|
|
243
|
-
7. **Stress-testing surfaces gaps cheaply.** Each round was ~$0.50. The full 3-round stress test cost ~$1.44 to identify and validate fixes for an architectural assumption that would have shipped silently broken if we'd kept moving on player_character → vendor → next-family.
|
|
244
|
-
|
|
245
|
-
### Open questions surfaced (not yet resolved)
|
|
246
|
-
|
|
247
|
-
- **Pattern C strategy (true autotile families).** Path autotile under Pattern B produced a usable border-tile set but lost the topology variants a full autotile needs. Solution likely involves cell-by-cell generation with image-conditioning anchors. Defer until V1 actually needs an autotile path.
|
|
248
|
-
- **Cliff style failure.** Gemini consistently scored cliff at 6.25 across attempts, citing "too painterly, soft transitions, blended dithering." The model produces beautiful illustrations but they don't match FireRed's hard-pixel-edge discipline. Possible fixes: (a) tighter style-specific prompt for vertical structures, (b) post-processing to flatten dithering, (c) accept that cliff is a V2 problem. Not yet decided.
|
|
249
|
-
- **"Object-as-asset" vs "object-as-tile-set" question.** Pattern B continuous illustrations might be better used as a single sprite asset placed across multiple grid cells in-game, rather than as 4 / 9 independent tiles. Either model works visually; the choice is about the game-engine integration. **Defer to whenever we're ready to place a multi-tile object in the world map.**
|
|
250
|
-
|
|
251
|
-
### Pattern B operational rule
|
|
252
|
-
|
|
253
|
-
When adding a new continuous-illustration family:
|
|
254
|
-
|
|
255
|
-
1. Set `slice_strategy: "continuous_illustration"`.
|
|
256
|
-
2. Provide a top-level `continuous_description` describing the whole subject as ONE illustration. Use language like *"filling the entire image edge-to-edge,"* *"NO cell borders,"* *"the image will be sliced afterward."*
|
|
257
|
-
3. Pick a layout that matches the natural orientation (e.g. 3×1 for a vertical cliff, 3×3 for a building front, 2×2 for a square object).
|
|
258
|
-
4. Per-member `desc` becomes label-only — describe what each slice will contain post-hoc, but don't expect the model to read these.
|
|
259
|
-
5. Don't define `edge_contracts` — they're skipped anyway.
|
|
260
|
-
6. Trust the slicer + Gemini family-review.
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
## Pattern C — autotile families (Phase A–D)
|
|
265
|
-
|
|
266
|
-
For a region of one terrain surrounded by another (grass islands in sand, sand
|
|
267
|
-
beaches against sea) and for line/path topology (marble flagstones threading
|
|
268
|
-
through grass), neither Pattern A nor Pattern B works. We hit this in stress
|
|
269
|
-
testing: the path autotile under Pattern B produced a usable border tile set
|
|
270
|
-
but lost the topology variants a real autotile needs. The model is good at
|
|
271
|
-
texture appearance; it is not good at producing all the topology variants of
|
|
272
|
-
a tile set as one image.
|
|
273
|
-
|
|
274
|
-
The fix: **AI generates 3–5 small primitive textures; a pure-Python compositor
|
|
275
|
-
deterministically composes the 47 (or 16) tiles by per-quadrant masking.**
|
|
276
|
-
Standards-compliant: 47-blob (RPG Maker A2 / Tiled / Cr31 / Godot blob) for
|
|
277
|
-
region terrain, 16-path (4-cardinal) for line terrain. For decision rationale
|
|
278
|
-
see [ADR 0009](../adr/0009-autotile-architecture.md).
|
|
279
|
-
|
|
280
|
-
### Why this works
|
|
281
|
-
|
|
282
|
-
**Edges match by construction.** Every tile that has the same edge configuration
|
|
283
|
-
samples its boundary pixels from the same primitive at the same logical
|
|
284
|
-
position. So when two such tiles sit next to each other in the world, the seam
|
|
285
|
-
pixels are guaranteed identical to within bounded jitter. We didn't need a
|
|
286
|
-
retry loop for seam alignment — the compositor produces correct seams on the
|
|
287
|
-
first pass. Validated by 100% edge-match on the all-interior composition test.
|
|
288
|
-
|
|
289
|
-
**The model only does what it's good at.** The AI generates 3–5 small texture
|
|
290
|
-
chunks (interior, exterior, edge, concave-corner, convex-corner). These are
|
|
291
|
-
within the model's competence — they're each a small piece of a coherent
|
|
292
|
-
texture. The model is not asked to plan tile-set topology, which is what
|
|
293
|
-
breaks down.
|
|
294
|
-
|
|
295
|
-
**Cost is dramatically lower.** A 47-tile family from 5 primitive generations
|
|
296
|
-
is ~$0.20–0.45 vs ~$1.88 if every tile were generated independently. Per
|
|
297
|
-
final tile this is ~$0.005–0.010 vs ~$0.040 — about 4–8× cheaper.
|
|
298
|
-
|
|
299
|
-
### Recipe: building a region autotile family (47-blob)
|
|
300
|
-
|
|
301
|
-
1. **Define the family** in `modules/art-pipeline/template/families.json` with
|
|
302
|
-
`kind: "autotile_blob_47"`. Required fields: `interior_terrain`,
|
|
303
|
-
`exterior_terrain`, `interior_short`, `exterior_short`, `shared_style`,
|
|
304
|
-
and a `primitives` array of 5 entries with roles
|
|
305
|
-
`interior` / `exterior` / `patch_horiz_NW` / `patch_concave_NW` /
|
|
306
|
-
`patch_convex_NW`. Each primitive has its own `id`, `desc`, and `size_px`
|
|
307
|
-
(32 for interior/exterior textures, 16 for boundary patches).
|
|
308
|
-
2. **Run the orchestrator**:
|
|
309
|
-
`python3 modules/art-pipeline/pipeline/autotile_orchestrator.py <family_id>`.
|
|
310
|
-
3. The orchestrator generates each primitive with retries (up to 3 attempts
|
|
311
|
-
for texture-role primitives, 1 attempt for non-texture primitives), applies
|
|
312
|
-
seamless-enforcement to texture roles, calls
|
|
313
|
-
`autotile_compositor.compose_blob47()`, and saves all 47 tiles.
|
|
314
|
-
4. The validation harness renders three test compositions (all-interior,
|
|
315
|
-
island, organic) and reports edge-match scores.
|
|
316
|
-
|
|
317
|
-
### Recipe: building a path autotile family (16-path)
|
|
318
|
-
|
|
319
|
-
Same as region but with `kind: "autotile_path_16"` and a 3-primitive bundle:
|
|
320
|
-
`terrain` / `path_node` / `path_arm_N`. The path_arm_N primitive is anchored
|
|
321
|
-
at the top edge with the path running vertically; the compositor rotates it
|
|
322
|
-
for the other 3 cardinal directions.
|
|
323
|
-
|
|
324
|
-
### Recipe: seamless texture enforcement (the trick that made it work)
|
|
325
|
-
|
|
326
|
-
AI-generated textures don't naturally tile seamlessly — col 0 ≠ col 31 in the
|
|
327
|
-
raw output. The seamless-enforcement step in
|
|
328
|
-
`autotile_orchestrator._make_seamless()` averages the leftmost and rightmost
|
|
329
|
-
columns of pixels, applies the average to both columns, repeats for top/bottom
|
|
330
|
-
rows, then re-quantizes to the locked palette. Result: col 0 == col 31 and row
|
|
331
|
-
0 == row 31 by construction. The visible cost: the 4 outermost pixels of every
|
|
332
|
-
edge are slightly blended versions of opposite-edge content. For texture-style
|
|
333
|
-
primitives (grass, sand, sea) this is invisible.
|
|
334
|
-
|
|
335
|
-
Without this step, even a "seamless" prompt produces ~50–70% edge match in
|
|
336
|
-
all-interior compositions. With the step, 100%.
|
|
337
|
-
|
|
338
|
-
### Why we still split silhouette / color / luminance / edge-contracts
|
|
339
|
-
|
|
340
|
-
The deterministic-checks framework (Pattern A/B) doesn't apply to autotiles:
|
|
341
|
-
- Cross-cell color coherence: primitives are independent; we don't compare them.
|
|
342
|
-
- Edge contracts: tautological for autotiles (the compositor produces matching
|
|
343
|
-
edges by construction).
|
|
344
|
-
|
|
345
|
-
The autotile validation harness uses a different scoring metric: **edge-match
|
|
346
|
-
score on rendered test compositions**. For each adjacent tile pair in a
|
|
347
|
-
rendered composition, count how many seam pixels match within a perceptual
|
|
348
|
-
tolerance. 100% means the composition is seamless when displayed.
|
|
349
|
-
|
|
350
|
-
### Pattern C results — Phase B/C run
|
|
351
|
-
|
|
352
|
-
| Family | Primitives | all_interior | island | organic / winding | cost |
|
|
353
|
-
|---|---|---|---|---|---|
|
|
354
|
-
| grass_on_sand | 5 | 100.0% | 85.7% | 77.2% | $0.47 |
|
|
355
|
-
| sand_on_sea | 5 | 100.0% | 85.7% | 74.7% | $0.23 |
|
|
356
|
-
| marble_path | 3 | n/a | n/a | winding 99.2% / cross 99.1% | $0.16 |
|
|
357
|
-
| cliff_top | 5 | 100.0% | 86.2% | 74.7% | $0.27 |
|
|
358
|
-
|
|
359
|
-
**100% match on all-interior compositions** validates the seamless-tile
|
|
360
|
-
approach. The 75–86% scores on boundary compositions (island, organic) reflect
|
|
361
|
-
the boundary primitives being independently generated — they're stylistically
|
|
362
|
-
coherent but not pixel-identical at every transition. Acceptable for V1;
|
|
363
|
-
generating boundary primitives in pairs with image conditioning would close
|
|
364
|
-
this gap and is a V2 candidate.
|
|
365
|
-
|
|
366
|
-
The marble_path 99% score is dramatically higher because path tiles are stamps
|
|
367
|
-
on a uniform background — most seams are terrain-to-terrain (perfectly
|
|
368
|
-
tileable), only a few are path-to-terrain.
|
|
369
|
-
|
|
370
|
-
### Pattern C pitfalls observed
|
|
371
|
-
|
|
372
|
-
- **Direction-asymmetric terrain breaks 47-blob.** Cliffs need different tiles
|
|
373
|
-
for north-facing vs south-facing edges (the south edge shows a stone face;
|
|
374
|
-
the north edge is just grass). 47-blob produces the same edge tile on all
|
|
375
|
-
four sides of a cliff region. Result: cliff_top is topologically correct
|
|
376
|
-
but stylistically weak. **For real cliffs, V1+ needs a direction-aware
|
|
377
|
-
cliff scheme** (per-edge tile selection or a 4-orientation autotile
|
|
378
|
-
variant). Document and defer.
|
|
379
|
-
- **Per-tile interior repetition.** Without per-tile sample-offset jitter,
|
|
380
|
-
every interior tile looks identical. Visible in large interior fields.
|
|
381
|
-
Phase A–D ships without jitter; V2 candidate.
|
|
382
|
-
- **Triple junctions are unrepresentable.** Where 3 terrains meet at one
|
|
383
|
-
tile, the 47-blob convention can't pick a tile. Forbid triple junctions
|
|
384
|
-
in level design (this is the standard solution in the industry).
|
|
385
|
-
- **Two-way terrain pairs need two families.** `grass_on_sand` does not
|
|
386
|
-
serve `sand_on_grass` — the boundary direction matters. For V1, declare
|
|
387
|
-
the families you need.
|
|
388
|
-
|
|
389
|
-
### Runtime architecture
|
|
390
|
-
|
|
391
|
-
The autotile picker is split-implementation:
|
|
392
|
-
- `modules/art-pipeline/pipeline/autotile_mapping.py` — Python source of truth, used by the
|
|
393
|
-
compositor at build time.
|
|
394
|
-
- `public/game/world/autotile.js` — JavaScript mirror, used by the in-game
|
|
395
|
-
renderer. Cross-verified against the Python table at build time (a
|
|
396
|
-
smoke-test compares all 256 inputs).
|
|
397
|
-
|
|
398
|
-
When the runtime data-model refactor lands (Phase D, deferred for review):
|
|
399
|
-
the world map stores terrain ids per cell; the renderer iterates cells and
|
|
400
|
-
calls `selectBlob47(myTerrain, neighbors)` or `selectPath16(neighbors)` to
|
|
401
|
-
pick the tile index, then loads the corresponding PNG from the family's
|
|
402
|
-
tile set.
|
|
403
|
-
|
|
404
|
-
### Open question
|
|
405
|
-
|
|
406
|
-
**Cliffs deserve a direction-aware scheme.** The 47-blob approach gives a
|
|
407
|
-
stylized "shelf" look on all four edges of a cliff region. Real cliff
|
|
408
|
-
autotiles in RPG Maker (A4 layer) and similar systems use a strip-based
|
|
409
|
-
scheme where the south-facing edge is drawn as a vertical wall while other
|
|
410
|
-
edges are flat transitions. If V1 ships cliffs, this needs to be built.
|
|
411
|
-
Otherwise defer to V2. Decision pending Lars.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Staging data — what's real, what's faked
|
|
2
|
-
|
|
3
|
-
> **Seeded by** [`scripts/gds/seed-staging.sh`](../../scripts/gds/seed-staging.sh) (V3.R41 / [#262](https://example.com/builders#/task/262)).
|
|
4
|
-
> Staging DB: `example_staging` on the droplet. Staging is the controlled mirror used for red-team ([#272](https://example.com/builders#/task/272) parallel-load, future C9 work) — **never** a place to trust as a record of truth.
|
|
5
|
-
|
|
6
|
-
## How seeding works
|
|
7
|
-
|
|
8
|
-
`seed-staging.sh` runs **on the droplet** (both databases are local, peer auth as `lars`). It takes a **full `pg_dump` of production (schema + data)**, drops and recreates the staging `public` schema, restores the dump, then anonymizes.
|
|
9
|
-
|
|
10
|
-
Why a full dump (not data-only): prod has a circular / self-referential FK (`tasks.parent_task_id`). A full dump adds all FK constraints **after** loading the rows, so that restores cleanly — and staging inherits prod's **exact schema**, so **no separate `migrate.sh` step is needed**. (A fresh `migrate.sh` from an empty DB would also fail: migration 021 is a *data* migration that assumes prod state.)
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
# On the droplet:
|
|
14
|
-
bash scripts/gds/seed-staging.sh
|
|
15
|
-
|
|
16
|
-
# Or from a laptop in one shot:
|
|
17
|
-
ssh lars@<droplet> 'bash -s' < scripts/gds/seed-staging.sh
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Re-runnable any time; each run replaces staging's data wholesale.
|
|
21
|
-
|
|
22
|
-
## What's REAL (copied verbatim from prod)
|
|
23
|
-
|
|
24
|
-
The structure and content of the work-tracking graph, so staging behaves like the real system:
|
|
25
|
-
|
|
26
|
-
- **tasks, claims, versions, dependencies, done_when_criteria, task_criteria** — real titles, descriptions, statuses, ranks, links.
|
|
27
|
-
- **cost_log, credit_log, learnings, idea_inbox, grades, achievements** — real rows.
|
|
28
|
-
- **builder_memory** — real per-builder memory content (builder-authored project notes; internal, not personal data).
|
|
29
|
-
|
|
30
|
-
These are *project* data (what the team built), not end-user data. Staging lives on the same access-controlled droplet as prod.
|
|
31
|
-
|
|
32
|
-
## What's FAKED / removed (the anonymization step)
|
|
33
|
-
|
|
34
|
-
- **Builder identity is anonymized.** Every `builders` row gets `github_login = builder-<id>`, `display_name = Builder <id>`, `github_id = 900000<id>`, `avatar_url = NULL`. No real GitHub handle survives on staging.
|
|
35
|
-
- **All sessions are purged.** After the restore, the script nulls every column that references `builder_sessions` and then `DELETE`s every session row, so the seeded DB ends with **zero** sessions and **no real bearer token**. Mint a staging-only token directly (insert a row into `example_staging.builder_sessions`) when a test needs to authenticate — note the auth regex only accepts `[A-Za-z0-9]+`, so the token must be alphanumeric (no hyphens).
|
|
36
|
-
|
|
37
|
-
### Honest note on the restore window (not a zero-trust guarantee)
|
|
38
|
-
|
|
39
|
-
Because it's a *full* dump, real session rows **are** loaded during the restore and only deleted a moment later in the anonymize step — so for the brief restore→purge window (~1s) real bearer tokens transiently exist in the staging DB, and the temporary dump file under `/tmp` (mode `0600`, shredded on normal exit) holds real `github_login` values until cleanup. This is acceptable because staging is the **same trust domain** as prod (one access-controlled droplet, peer auth) — but it is **not** a "real tokens never touch staging" guarantee. If you need that guarantee (e.g. seeding onto a less-trusted host), revoke prod sessions first or seed during a maintenance window. The end state after a successful run is always zero sessions.
|
|
40
|
-
|
|
41
|
-
## Reseeding cadence
|
|
42
|
-
|
|
43
|
-
Re-run on demand (before a red-team / load-test session) or on a weekly cron. To install the weekly cron on the droplet:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
# 03:30 every Monday — refresh staging from prod.
|
|
47
|
-
( crontab -l 2>/dev/null; echo "30 3 * * 1 bash /home/lars/example-staging/scripts/gds/seed-staging.sh >> /home/lars/seed-staging.log 2>&1" ) | crontab -
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
(On-demand is the common case; the cron is optional convenience. The staging checkout must be on current `main` so it has the latest schema — `git pull` it before a manual seed if prod's schema has moved. Until the cron is installed, treat staging data as a point-in-time snapshot from the last manual seed.)
|