@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.
Files changed (233) hide show
  1. package/.bongos-core.json +521 -281
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/GOVERNANCE.md +1 -1
  15. package/README.md +1 -1
  16. package/clients/bongos-client/README.md +1 -1
  17. package/clients/bongos-client/bongos-client.global.js +2 -0
  18. package/clients/bongos-client/index.cjs +2 -0
  19. package/clients/bongos-client/index.d.ts +4 -2
  20. package/clients/bongos-client/index.mjs +2 -0
  21. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  22. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  23. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  24. package/docs/adr/0020-security-model-public-stub.md +1 -1
  25. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  26. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  27. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  28. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  29. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  30. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  31. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  32. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  33. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  34. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  35. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  36. package/docs/adr/0057-container-cost-ledger.md +3 -3
  37. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  38. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  39. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  40. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  41. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  42. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  43. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  44. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  45. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  46. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  47. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  48. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  49. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  50. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  51. package/docs/adr/README.md +9 -6
  52. package/docs/api/openapi.json +65 -26
  53. package/docs/api-reference.md +3 -2
  54. package/docs/architecture.md +2 -2
  55. package/docs/branding-contract.md +5 -4
  56. package/docs/canonical-permissions.md +1 -1
  57. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  58. package/docs/file-map.md +18 -17
  59. package/docs/handoff-template.md +2 -2
  60. package/docs/modules-contract.md +6 -1
  61. package/docs/onboarding/diagrams/README.md +7 -7
  62. package/docs/onboarding/drachmae-and-karma.md +18 -18
  63. package/docs/onboarding/primer.md +9 -9
  64. package/docs/recipes/audit-replay.md +2 -2
  65. package/docs/recipes/builder-box-lifecycle.md +2 -2
  66. package/docs/recipes/claude-design-loop.md +1 -1
  67. package/docs/recipes/ops-gotchas.md +1 -1
  68. package/docs/recipes/private-npm-distribution.md +17 -0
  69. package/docs/recipes/staging-environment.md +1 -1
  70. package/docs/recipes/upgrading-the-core.md +55 -0
  71. package/docs/research/cursor-competitive-analysis.md +1 -1
  72. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  73. package/modules/discord/module.json +2 -1
  74. package/modules/economy/achievements.js +9 -1
  75. package/modules/economy/credits.js +10 -1
  76. package/modules/hall-ui/public/builders.js +137 -620
  77. package/modules/hall-ui/public/goal-inbox.js +232 -0
  78. package/modules/hall-ui/public/goals-page.js +4 -1
  79. package/modules/hall-ui/public/goals.html +8 -0
  80. package/modules/hall-ui/public/hall-kit.js +474 -0
  81. package/modules/hall-ui/public/hall-widgets.js +14 -5
  82. package/modules/hall-ui/public/index.html +3 -0
  83. package/modules/hall-ui/public/instances-panel.js +24 -3
  84. package/modules/hall-ui/public/new-project-wizard.js +7 -5
  85. package/modules/hall-ui/public/style.css +168 -0
  86. package/modules/hall-ui/public/work.css +194 -0
  87. package/modules/hall-ui/public/work.html +46 -374
  88. package/modules/hall-ui/public/work.js +137 -102
  89. package/modules/lifecycle/criterion-suggest.js +80 -0
  90. package/modules/lifecycle/db.js +42 -5
  91. package/modules/lifecycle/routes/goals.js +26 -0
  92. package/modules/lifecycle/routes/tasks.js +21 -0
  93. package/modules/provisioning/CLAUDE.md +15 -0
  94. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  95. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  96. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  97. package/modules/provisioning/module.json +17 -0
  98. package/modules/provisioning/onboard-plan.js +37 -0
  99. package/modules/provisioning/provisioning.js +539 -0
  100. package/modules/provisioning/rate-limit.js +35 -0
  101. package/modules/provisioning/routes/provisioning.js +607 -0
  102. package/modules/provisioning/tests/provisioning.mjs +232 -0
  103. package/modules/provisioning/ui/.gitkeep +0 -0
  104. package/modules/security/secret-scrub.js +10 -0
  105. package/package-lock.json +2 -2
  106. package/package.json +7 -2
  107. package/scripts/discord/README.md +1 -1
  108. package/scripts/gds/claim.js +98 -48
  109. package/scripts/gds/claude-materialize.js +30 -9
  110. package/scripts/gds/cutover-codemod.js +241 -0
  111. package/scripts/gds/fitness.js +54 -0
  112. package/scripts/gds/init.js +59 -3
  113. package/scripts/gds/lint-infra-exec.js +70 -0
  114. package/scripts/gds/lint-workflow-exec.js +63 -0
  115. package/scripts/gds/mirror-lineage-check.js +14 -5
  116. package/scripts/gds/mirror-redact.js +8 -0
  117. package/scripts/gds/onboard.js +4 -0
  118. package/scripts/gds/package-core.js +18 -3
  119. package/scripts/gds/publish-manifest.js +60 -2
  120. package/scripts/gds/run-routine.js +51 -13
  121. package/scripts/gds/run-unit-tests.js +4 -0
  122. package/scripts/gds/update-channel.js +186 -0
  123. package/scripts/gds/upgrade.js +116 -9
  124. package/scripts/hall-preview/README.md +103 -0
  125. package/scripts/hall-preview/fixtures/access-requests.json +1 -0
  126. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  127. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  128. package/scripts/hall-preview/fixtures/audit-log.json +1 -0
  129. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
  130. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  131. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  132. package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
  133. package/scripts/hall-preview/fixtures/goals.json +502 -0
  134. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  135. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  136. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  137. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  138. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  139. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  140. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  141. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  142. package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
  143. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  144. package/scripts/hall-preview/fixtures/instance.json +124 -0
  145. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  146. package/scripts/hall-preview/fixtures/me.json +476 -0
  147. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  148. package/scripts/hall-preview/fixtures/override-requests.json +1 -0
  149. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  150. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  151. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  152. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  153. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  154. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  155. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  156. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  157. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  158. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  159. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  160. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  161. package/scripts/hall-preview/fixtures/security__docs.json +1 -0
  162. package/scripts/hall-preview/fixtures/security__reports.json +1 -0
  163. package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
  164. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  165. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  166. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  167. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  168. package/scripts/hall-preview/fixtures/versions.json +89 -0
  169. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  170. package/scripts/hall-preview/server.js +178 -0
  171. package/scripts/migrate.sh +29 -4
  172. package/src/bongos/auth.js +14 -1
  173. package/src/bongos/optimizer.js +37 -2
  174. package/src/bongos/serve-internal.js +10 -1
  175. package/src/branding.js +1 -0
  176. package/src/module-api.js +12 -1
  177. package/src/module-loader/loader.js +36 -4
  178. package/src/module-loader/manifest-schema.js +16 -1
  179. package/tests/branding.mjs +11 -0
  180. package/tests/claim_error_surface.mjs +107 -0
  181. package/tests/claude_materialize.mjs +20 -0
  182. package/tests/consumer_layout_boot.mjs +302 -0
  183. package/tests/criterion_suggest.mjs +106 -0
  184. package/tests/cutover_codemod.mjs +184 -0
  185. package/tests/device_flow_start.mjs +25 -2
  186. package/tests/hook_core_resolve.mjs +125 -0
  187. package/tests/infra_exec_paths.mjs +104 -0
  188. package/tests/init.mjs +47 -0
  189. package/tests/migrate_root_resolution.mjs +97 -0
  190. package/tests/mirror_lineage_proof.mjs +14 -0
  191. package/tests/module_loader.mjs +42 -0
  192. package/tests/module_manifest.mjs +19 -0
  193. package/tests/onboard.mjs +13 -0
  194. package/tests/package_core.mjs +8 -0
  195. package/tests/provisioning_github_app_routes.mjs +15 -0
  196. package/tests/publish_manifest.mjs +52 -1
  197. package/tests/run_routine.mjs +26 -1
  198. package/tests/run_routine_two_root.mjs +77 -0
  199. package/tests/scrubber_corpus.mjs +17 -0
  200. package/tests/update_channel.mjs +186 -0
  201. package/tests/upgrade.mjs +131 -0
  202. package/tests/workflow_exec_paths.mjs +98 -0
  203. package/docs/design/container-exterior-tiles.md +0 -49
  204. package/docs/design/container-exterior.md +0 -81
  205. package/docs/design/container-interior.md +0 -77
  206. package/docs/design/world-map-scroll.md +0 -41
  207. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  208. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  209. package/docs/recipes/cloudbongos-standup.md +0 -111
  210. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  211. package/docs/recipes/parallel-load-test.md +0 -32
  212. package/docs/recipes/pixel-art-pipeline.md +0 -411
  213. package/docs/recipes/staging-data.md +0 -50
  214. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  215. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  216. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  217. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  218. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  219. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  220. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  221. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  222. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  223. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  224. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  225. package/scripts/gds/art-stage.js +0 -152
  226. package/server.js +0 -212
  227. package/tests/art_key.mjs +0 -121
  228. package/tests/day_cycle.mjs +0 -197
  229. package/tests/snapshot_picker.mjs +0 -233
  230. package/tests/sprite_facing.mjs +0 -35
  231. package/tests/terrain_picker.mjs +0 -298
  232. package/tests/turn_in_place.mjs +0 -280
  233. 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.)