@bongos/core 1.17.0 → 1.17.2

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 (230) hide show
  1. package/.bongos-core.json +363 -312
  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/CLAUDE.md +11 -238
  15. package/GOVERNANCE.md +1 -1
  16. package/README.md +1 -1
  17. package/bin/bongos.js +1 -0
  18. package/clients/bongos-client/README.md +4 -4
  19. package/clients/bongos-client/bongos-client.global.js +14 -1
  20. package/clients/bongos-client/index.cjs +14 -1
  21. package/clients/bongos-client/index.d.ts +19 -2
  22. package/clients/bongos-client/index.mjs +11 -1
  23. package/clients/bongos-client/package.json +1 -1
  24. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  25. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  26. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  27. package/docs/adr/0020-security-model-public-stub.md +1 -1
  28. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  29. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  30. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  31. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  32. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  33. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  34. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  35. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  36. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  37. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  38. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  39. package/docs/adr/0057-container-cost-ledger.md +3 -3
  40. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  41. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  42. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  43. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  44. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  45. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  46. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  47. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  48. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  49. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  50. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  51. package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
  52. package/docs/adr/0134-private-first-npm-distribution.md +20 -6
  53. package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
  54. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  55. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  56. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  57. package/docs/adr/README.md +14 -10
  58. package/docs/api/openapi.json +315 -6
  59. package/docs/api-reference.md +10 -5
  60. package/docs/architecture.md +3 -3
  61. package/docs/branding-contract.md +5 -4
  62. package/docs/canonical-permissions.md +1 -1
  63. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  64. package/docs/file-map.md +20 -19
  65. package/docs/handoff-template.md +2 -2
  66. package/docs/modules-contract.md +8 -1
  67. package/docs/onboarding/diagrams/README.md +7 -7
  68. package/docs/onboarding/drachmae-and-karma.md +18 -18
  69. package/docs/onboarding/primer.md +9 -9
  70. package/docs/recipes/audit-replay.md +2 -2
  71. package/docs/recipes/builder-box-lifecycle.md +2 -2
  72. package/docs/recipes/claude-design-loop.md +1 -1
  73. package/docs/recipes/instance-deploy.md +4 -4
  74. package/docs/recipes/instance-first-run.md +51 -0
  75. package/docs/recipes/ops-gotchas.md +1 -1
  76. package/docs/recipes/packaging-the-core.md +5 -5
  77. package/docs/recipes/private-npm-distribution.md +39 -12
  78. package/docs/recipes/self-host.md +1 -1
  79. package/docs/recipes/staging-environment.md +1 -1
  80. package/docs/recipes/upgrading-the-core.md +63 -8
  81. package/docs/research/cursor-competitive-analysis.md +1 -1
  82. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  83. package/migrations/core_191_module_submissions.sql +45 -0
  84. package/modules/autonomy/CLAUDE.md +11 -45
  85. package/modules/builder-settings/CLAUDE.md +11 -54
  86. package/modules/dev-box/CLAUDE.md +11 -34
  87. package/modules/discord/CLAUDE.md +11 -46
  88. package/modules/discord/module.json +2 -1
  89. package/modules/economy/CLAUDE.md +11 -44
  90. package/modules/economy/achievements.js +9 -1
  91. package/modules/economy/credits.js +10 -1
  92. package/modules/grading/CLAUDE.md +11 -38
  93. package/modules/hall-ui/CLAUDE.md +11 -57
  94. package/modules/hall-ui/public/builders.js +71 -4
  95. package/modules/hall-ui/public/goal-inbox.js +191 -0
  96. package/modules/hall-ui/public/goals-page.js +227 -22
  97. package/modules/hall-ui/public/goals.html +16 -0
  98. package/modules/hall-ui/public/hall-widgets.js +14 -1
  99. package/modules/hall-ui/public/index.html +3 -0
  100. package/modules/hall-ui/public/new-project-wizard.js +6 -4
  101. package/modules/hall-ui/public/style.css +43 -0
  102. package/modules/ideas/CLAUDE.md +11 -33
  103. package/modules/lifecycle/CLAUDE.md +11 -59
  104. package/modules/lifecycle/routes/goals.js +80 -0
  105. package/modules/memory/CLAUDE.md +11 -36
  106. package/modules/onboarding/CLAUDE.md +11 -51
  107. package/modules/provisioning/CLAUDE.md +15 -0
  108. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  109. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  110. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  111. package/modules/provisioning/module.json +17 -0
  112. package/modules/provisioning/onboard-plan.js +37 -0
  113. package/modules/provisioning/provisioning.js +539 -0
  114. package/modules/provisioning/rate-limit.js +35 -0
  115. package/modules/provisioning/routes/provisioning.js +607 -0
  116. package/modules/provisioning/tests/provisioning.mjs +232 -0
  117. package/modules/provisioning/ui/.gitkeep +0 -0
  118. package/modules/security/CLAUDE.md +11 -40
  119. package/modules/security/secret-scrub.js +10 -0
  120. package/modules/sessions/CLAUDE.md +11 -35
  121. package/modules/status-ui/CLAUDE.md +11 -43
  122. package/package-lock.json +2 -2
  123. package/package.json +7 -2
  124. package/public-landing/create.html +58 -12
  125. package/public-landing/index.html +40 -21
  126. package/scripts/discord/README.md +1 -1
  127. package/scripts/gds/CLAUDE.md +11 -140
  128. package/scripts/gds/claim.js +98 -48
  129. package/scripts/gds/claude-materialize.js +174 -20
  130. package/scripts/gds/claude-md-template.js +84 -0
  131. package/scripts/gds/cli-lib.js +112 -2
  132. package/scripts/gds/cutover-codemod.js +241 -0
  133. package/scripts/gds/fitness.js +54 -0
  134. package/scripts/gds/gen-api-client.js +10 -10
  135. package/scripts/gds/init.js +112 -16
  136. package/scripts/gds/lint-infra-exec.js +70 -0
  137. package/scripts/gds/lint-workflow-exec.js +63 -0
  138. package/scripts/gds/migration-namespace.js +1 -1
  139. package/scripts/gds/mirror-lineage-check.js +14 -5
  140. package/scripts/gds/mirror-redact.js +8 -0
  141. package/scripts/gds/module.js +118 -4
  142. package/scripts/gds/npmrc.js +93 -0
  143. package/scripts/gds/onboard.js +17 -24
  144. package/scripts/gds/package-core.js +74 -17
  145. package/scripts/gds/provision.js +102 -17
  146. package/scripts/gds/publish-manifest.js +60 -2
  147. package/scripts/gds/run-routine.js +51 -13
  148. package/scripts/gds/run-unit-tests.js +4 -0
  149. package/scripts/gds/seed-adopt-tasks.js +1 -1
  150. package/scripts/gds/status.js +6 -1
  151. package/scripts/gds/task.js +166 -0
  152. package/scripts/gds/update-channel.js +186 -0
  153. package/scripts/gds/upgrade.js +172 -25
  154. package/src/bongos/CLAUDE.md +11 -60
  155. package/src/bongos/auth.js +24 -0
  156. package/src/bongos/module-submissions.js +56 -0
  157. package/src/bongos/route-rank-check.js +6 -0
  158. package/src/bongos/routes/CLAUDE.md +11 -39
  159. package/src/bongos/routes/auth.js +6 -1
  160. package/src/bongos/routes/modules.js +123 -21
  161. package/src/bongos/serve-internal.js +10 -1
  162. package/src/branding.js +1 -0
  163. package/src/instance-config.js +1 -1
  164. package/src/module-api.js +24 -1
  165. package/src/module-loader/loader.js +36 -4
  166. package/src/module-loader/manifest-schema.js +16 -1
  167. package/tests/branding.mjs +11 -0
  168. package/tests/claim_error_surface.mjs +107 -0
  169. package/tests/claude_materialize.mjs +125 -0
  170. package/tests/consumer_layout_boot.mjs +302 -0
  171. package/tests/cutover_codemod.mjs +184 -0
  172. package/tests/device_flow_start.mjs +96 -0
  173. package/tests/first_run_engine_check.mjs +79 -0
  174. package/tests/goal_membership_requests.mjs +33 -3
  175. package/tests/hook_core_resolve.mjs +125 -0
  176. package/tests/hook_user_agents.mjs +62 -0
  177. package/tests/infra_exec_paths.mjs +104 -0
  178. package/tests/init.mjs +106 -6
  179. package/tests/instance_mismatch_guard.mjs +101 -0
  180. package/tests/mirror_lineage_proof.mjs +14 -0
  181. package/tests/module_cli.mjs +130 -2
  182. package/tests/module_loader.mjs +42 -0
  183. package/tests/module_manifest.mjs +19 -0
  184. package/tests/module_submissions.mjs +135 -0
  185. package/tests/npmrc.mjs +105 -0
  186. package/tests/onboard.mjs +62 -0
  187. package/tests/package_core.mjs +76 -22
  188. package/tests/platform_boot.mjs +5 -2
  189. package/tests/provision.mjs +107 -8
  190. package/tests/provisioning_github_app_routes.mjs +44 -2
  191. package/tests/publish_manifest.mjs +52 -1
  192. package/tests/run_routine.mjs +26 -1
  193. package/tests/run_routine_two_root.mjs +77 -0
  194. package/tests/scrubber_corpus.mjs +17 -0
  195. package/tests/served_paths_shipped.mjs +2 -2
  196. package/tests/task_cli.mjs +99 -0
  197. package/tests/update_channel.mjs +186 -0
  198. package/tests/upgrade.mjs +220 -25
  199. package/tests/workflow_exec_paths.mjs +98 -0
  200. package/docs/design/container-exterior-tiles.md +0 -49
  201. package/docs/design/container-exterior.md +0 -81
  202. package/docs/design/container-interior.md +0 -77
  203. package/docs/design/world-map-scroll.md +0 -41
  204. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  205. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  206. package/docs/recipes/cloudbongos-standup.md +0 -111
  207. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  208. package/docs/recipes/parallel-load-test.md +0 -32
  209. package/docs/recipes/pixel-art-pipeline.md +0 -411
  210. package/docs/recipes/staging-data.md +0 -50
  211. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  212. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  213. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  214. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  215. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  216. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  217. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  218. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  219. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  220. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  221. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  222. package/scripts/gds/art-stage.js +0 -152
  223. package/server.js +0 -212
  224. package/tests/art_key.mjs +0 -121
  225. package/tests/day_cycle.mjs +0 -197
  226. package/tests/snapshot_picker.mjs +0 -233
  227. package/tests/sprite_facing.mjs +0 -35
  228. package/tests/terrain_picker.mjs +0 -298
  229. package/tests/turn_in_place.mjs +0 -280
  230. package/tests/walkability.mjs +0 -188
package/tests/upgrade.mjs CHANGED
@@ -34,13 +34,13 @@ function scratchConsumer({ pinned = '1.14.1', installed = '1.14.1' } = {}) {
34
34
  const dir = mkdtempSync(join(tmpdir(), 'otb-upgrade-'));
35
35
  writeFileSync(join(dir, 'package.json'), JSON.stringify({
36
36
  name: 'test-consumer', version: '0.1.0',
37
- dependencies: { '@cloudbongos/core': `file:vendor/cloudbongos-core-${pinned}.tgz` },
37
+ dependencies: { '@bongos/core': `file:vendor/bongos-core-${pinned}.tgz` },
38
38
  scripts: { migrate: 'true', start: 'true' },
39
39
  }, null, 2) + '\n');
40
40
  if (installed) {
41
- const cd = join(dir, 'node_modules', '@cloudbongos', 'core');
41
+ const cd = join(dir, 'node_modules', '@bongos', 'core');
42
42
  mkdirSync(cd, { recursive: true });
43
- writeFileSync(join(cd, 'package.json'), JSON.stringify({ name: '@cloudbongos/core', version: installed }) + '\n');
43
+ writeFileSync(join(cd, 'package.json'), JSON.stringify({ name: '@bongos/core', version: installed }) + '\n');
44
44
  }
45
45
  return dir;
46
46
  }
@@ -52,8 +52,8 @@ function fakeRun(calls, { dirty = '', installTo = null, dir = null } = {}) {
52
52
  calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
53
53
  if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: dirty };
54
54
  if (cmd === 'npm' && args[0] === 'install' && installTo && dir) {
55
- writeFileSync(join(dir, 'node_modules', '@cloudbongos', 'core', 'package.json'),
56
- JSON.stringify({ name: '@cloudbongos/core', version: installTo }) + '\n');
55
+ writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'),
56
+ JSON.stringify({ name: '@bongos/core', version: installTo }) + '\n');
57
57
  }
58
58
  return { status: 0 };
59
59
  };
@@ -61,20 +61,20 @@ function fakeRun(calls, { dirty = '', installTo = null, dir = null } = {}) {
61
61
 
62
62
  // ---- 1. pure pin helpers ----------------------------------------------------
63
63
  t('versionFromTgzPath: extracts the version from a vendored tarball path', () => {
64
- assert.equal(u.versionFromTgzPath('file:vendor/cloudbongos-core-1.14.1.tgz'), '1.14.1');
65
- assert.equal(u.versionFromTgzPath('vendor/cloudbongos-core-1.15.0.tgz'), '1.15.0');
64
+ assert.equal(u.versionFromTgzPath('file:vendor/bongos-core-1.14.1.tgz'), '1.14.1');
65
+ assert.equal(u.versionFromTgzPath('vendor/bongos-core-1.15.0.tgz'), '1.15.0');
66
66
  assert.equal(u.versionFromTgzPath('^1.2.3'), null);
67
67
  });
68
68
 
69
69
  t('readPinnedCoreVersion / writePin: round-trip the file: dependency', () => {
70
70
  const dir = scratchConsumer({ pinned: '1.14.1' });
71
71
  assert.equal(u.readPinnedCoreVersion(dir), '1.14.1');
72
- const prev = u.writePin(dir, 'vendor/cloudbongos-core-1.15.0.tgz');
73
- assert.equal(prev, 'file:vendor/cloudbongos-core-1.14.1.tgz');
72
+ const prev = u.writePin(dir, 'vendor/bongos-core-1.15.0.tgz');
73
+ assert.equal(prev, 'file:vendor/bongos-core-1.14.1.tgz');
74
74
  assert.equal(u.readPinnedCoreVersion(dir), '1.15.0');
75
75
  });
76
76
 
77
- t('readInstalledCoreVersion: reads node_modules/@cloudbongos/core', () => {
77
+ t('readInstalledCoreVersion: reads node_modules/@bongos/core', () => {
78
78
  const dir = scratchConsumer({ installed: '1.14.1' });
79
79
  assert.equal(u.readInstalledCoreVersion(dir), '1.14.1');
80
80
  assert.equal(u.readInstalledCoreVersion(mkdtempSync(join(tmpdir(), 'empty-'))), null);
@@ -83,13 +83,13 @@ t('readInstalledCoreVersion: reads node_modules/@cloudbongos/core', () => {
83
83
  // ---- 2. vendorTarball -------------------------------------------------------
84
84
  t('vendorTarball: copies the .tgz + side-car manifest under the canonical name', () => {
85
85
  const src = mkdtempSync(join(tmpdir(), 'src-'));
86
- writeFileSync(join(src, 'cloudbongos-core-1.15.0.tgz'), 'TGZBYTES');
87
- writeFileSync(join(src, 'cloudbongos-core-1.15.0.manifest.json'), '{"v":1}');
86
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZBYTES');
87
+ writeFileSync(join(src, 'bongos-core-1.15.0.manifest.json'), '{"v":1}');
88
88
  const dir = scratchConsumer();
89
- const rel = u.vendorTarball({ instanceDir: dir, fromTgz: join(src, 'cloudbongos-core-1.15.0.tgz'), targetVersion: '1.15.0' });
90
- assert.equal(rel, join('vendor', 'cloudbongos-core-1.15.0.tgz'));
89
+ const rel = u.vendorTarball({ instanceDir: dir, fromTgz: join(src, 'bongos-core-1.15.0.tgz'), targetVersion: '1.15.0' });
90
+ assert.equal(rel, join('vendor', 'bongos-core-1.15.0.tgz'));
91
91
  assert.equal(readFileSync(join(dir, rel), 'utf8'), 'TGZBYTES');
92
- assert.ok(existsSync(join(dir, 'vendor', 'cloudbongos-core-1.15.0.manifest.json')), 'manifest copied too');
92
+ assert.ok(existsSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json')), 'manifest copied too');
93
93
  });
94
94
 
95
95
  // ---- 3. step shims issue the right argv ------------------------------------
@@ -137,11 +137,11 @@ t('guard: already on the target refuses (unless --force)', async () => {
137
137
  // ---- 5. dry-run mutates nothing --------------------------------------------
138
138
  t('dry-run: plans without rewriting package.json or spawning npm', async () => {
139
139
  const src = mkdtempSync(join(tmpdir(), 'src-'));
140
- writeFileSync(join(src, 'cloudbongos-core-1.15.0.tgz'), 'X');
140
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'X');
141
141
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
142
142
  const calls = [];
143
143
  const res = await u.runUpgrade(
144
- { to: '1.15.0', from: join(src, 'cloudbongos-core-1.15.0.tgz'), instance: dir, dryRun: true },
144
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, dryRun: true },
145
145
  { log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '' }) }
146
146
  );
147
147
  assert.equal(res.ok, true);
@@ -153,13 +153,13 @@ t('dry-run: plans without rewriting package.json or spawning npm', async () => {
153
153
  // ---- 6. full happy-path (every side effect injected; ledger skipped, DB-free) ----
154
154
  t('full: pre-flight → pin → install → migrate → materialize → restart → verify', async () => {
155
155
  const src = mkdtempSync(join(tmpdir(), 'src-'));
156
- writeFileSync(join(src, 'cloudbongos-core-1.15.0.tgz'), 'TGZ');
156
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
157
157
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
158
158
  const calls = [];
159
159
  let materialized = null;
160
160
  let regenArgs = null;
161
161
  const res = await u.runUpgrade(
162
- { to: '1.15.0', from: join(src, 'cloudbongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service' },
162
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service' },
163
163
  {
164
164
  log: () => {}, err: () => {},
165
165
  run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }),
@@ -174,26 +174,26 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
174
174
  assert.equal(res.versionOk, true, 'installed version must equal the target after install');
175
175
  // pin rewritten, tarball vendored
176
176
  assert.equal(u.readPinnedCoreVersion(dir), '1.15.0');
177
- assert.equal(readFileSync(join(dir, 'vendor', 'cloudbongos-core-1.15.0.tgz'), 'utf8'), 'TGZ');
177
+ assert.equal(readFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'utf8'), 'TGZ');
178
178
  // steps ran, in the expected order
179
179
  assert.deepEqual(
180
180
  calls.filter((c) => c.startsWith('npm') || c.startsWith('git') || c.startsWith('systemctl')),
181
181
  ['git status --porcelain', 'npm install --no-audit --no-fund', 'npm run migrate', 'systemctl restart emersonian.service']
182
182
  );
183
183
  // materialize pointed at the freshly-installed core
184
- assert.ok(materialized && materialized.coreRoot.endsWith(join('node_modules', '@cloudbongos', 'core')));
184
+ assert.ok(materialized && materialized.coreRoot.endsWith(join('node_modules', '@bongos', 'core')));
185
185
  assert.equal(materialized.instanceDir, dir);
186
186
  // task 2051: the API docs+client regen ran too, pointed at the same new core + instance
187
187
  assert.ok(regenArgs, 'regenerateApiArtifacts step invoked');
188
188
  assert.equal(regenArgs.instanceDir, dir);
189
- assert.ok(regenArgs.coreRoot.endsWith(join('node_modules', '@cloudbongos', 'core')));
189
+ assert.ok(regenArgs.coreRoot.endsWith(join('node_modules', '@bongos', 'core')));
190
190
  });
191
191
 
192
192
  // ---- 6b. regenerate the API artifacts on upgrade (task 2051) ----------------
193
193
  t('regenerateApiArtifacts: spawns docs then client from the new core, targeting the instance', () => {
194
194
  const calls = [];
195
195
  const run = (cmd, args, opts) => { calls.push({ cmd, args, opts }); return { status: 0 }; };
196
- const res = u.regenerateApiArtifacts({ instanceDir: '/inst', coreRoot: '/inst/node_modules/@cloudbongos/core' }, run, { existsSync: () => true });
196
+ const res = u.regenerateApiArtifacts({ instanceDir: '/inst', coreRoot: '/inst/node_modules/@bongos/core' }, run, { existsSync: () => true });
197
197
  assert.equal(res.ok, true);
198
198
  assert.deepEqual(res.ran, ['gen-api-docs.js', 'gen-api-client.js']);
199
199
  assert.equal(calls.length, 2);
@@ -225,11 +225,11 @@ t('regenerateApiArtifacts: a generator non-zero exit stops + reports (docs ran,
225
225
 
226
226
  t('runUpgrade: --skip-regen-docs skips the regen step', async () => {
227
227
  const src = mkdtempSync(join(tmpdir(), 'src-'));
228
- writeFileSync(join(src, 'cloudbongos-core-1.15.0.tgz'), 'TGZ');
228
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
229
229
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
230
230
  let regenCalled = false;
231
231
  const res = await u.runUpgrade(
232
- { to: '1.15.0', from: join(src, 'cloudbongos-core-1.15.0.tgz'), instance: dir, skipRegenDocs: true },
232
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, skipRegenDocs: true },
233
233
  {
234
234
  log: () => {}, err: () => {},
235
235
  run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }),
@@ -352,6 +352,201 @@ t('pollHealth: a persistent non-2xx status is reported (not treated as ready)',
352
352
  assert.equal(res.timedOut, true);
353
353
  });
354
354
 
355
+ // ---- 7. registry update channel (ADR 0134, task 2090) -----------------------
356
+ t('versionFromDep: parses bare/caret/tilde semver; file: spec → null (tgz path is versionFromTgzPath\'s job)', () => {
357
+ assert.equal(u.versionFromDep('1.17.0'), '1.17.0');
358
+ assert.equal(u.versionFromDep('^1.17.0'), '1.17.0');
359
+ assert.equal(u.versionFromDep('~1.17.0'), '1.17.0');
360
+ assert.equal(u.versionFromDep('file:vendor/bongos-core-1.17.0.tgz'), null);
361
+ assert.equal(u.versionFromDep(''), null);
362
+ assert.equal(u.versionFromDep(null), null);
363
+ });
364
+
365
+ t('writeRegistryPin / readPinnedCoreVersion: round-trip an exact registry version', () => {
366
+ const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
367
+ const prev = u.writeRegistryPin(dir, '1.17.0');
368
+ assert.equal(prev, 'file:vendor/bongos-core-1.16.0.tgz', 'returns the previous pin');
369
+ assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'pins the exact version (not file:)');
370
+ assert.equal(u.readPinnedCoreVersion(dir), '1.17.0', 'reads the registry pin back');
371
+ });
372
+
373
+ t('runUpgrade --registry (dry-run): plans a registry pin, no npm, pin unchanged', async () => {
374
+ const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
375
+ const calls = [];
376
+ const res = await u.runUpgrade(
377
+ { to: '1.17.0', registry: true, instance: dir, dryRun: true },
378
+ { log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '' }) }
379
+ );
380
+ assert.equal(res.ok, true);
381
+ assert.equal(res.dryRun, true);
382
+ assert.equal(u.readPinnedCoreVersion(dir), '1.16.0', 'pin unchanged in dry-run');
383
+ assert.ok(!existsSync(join(dir, '.npmrc')), 'dry-run writes no .npmrc');
384
+ assert.ok(!calls.some((c) => c.startsWith('npm')), 'no npm spawned in dry-run');
385
+ });
386
+
387
+ t('runUpgrade --registry + --from: refuses (mutually exclusive core sources)', async () => {
388
+ const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
389
+ const res = await u.runUpgrade(
390
+ { to: '1.17.0', registry: true, from: '/tmp/x.tgz', instance: dir },
391
+ { log: () => {}, err: () => {}, run: fakeRun([], { dirty: '' }) }
392
+ );
393
+ assert.equal(res.ok, false);
394
+ assert.match(res.error, /mutually exclusive/);
395
+ });
396
+
397
+ t('runUpgrade --registry (full): pins the exact version + writes the env-fed .npmrc, then install→migrate→verify', async () => {
398
+ const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
399
+ const calls = [];
400
+ const res = await u.runUpgrade(
401
+ { to: '1.17.0', registry: true, instance: dir, service: 'demo.service' },
402
+ {
403
+ log: () => {}, err: () => {},
404
+ run: fakeRun(calls, { dirty: '', installTo: '1.17.0', dir }),
405
+ materialize: () => ({ skills: 1, settings: true, hooks: 1 }),
406
+ regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
407
+ databaseUrl: '', pgDatabase: '',
408
+ }
409
+ );
410
+ assert.equal(res.ok, true, 'registry upgrade succeeds');
411
+ assert.equal(res.toVersion, '1.17.0');
412
+ assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'exact registry pin (no file:)');
413
+ const npmrc = readFileSync(join(dir, '.npmrc'), 'utf8');
414
+ assert.match(npmrc, /_authToken=\$\{NPM_TOKEN\}/, 'wrote the env-fed .npmrc');
415
+ assert.ok(calls.some((c) => /^npm install/.test(c)), 'ran npm install (resolves from the registry)');
416
+ assert.ok(!calls.some((c) => /vendor/.test(c)), 'did not vendor a tarball');
417
+ });
418
+
419
+ // ---- 9. auto-rollback on failure (task 2149) --------------------------------
420
+
421
+ // A pin-aware spawn shim: `npm install` installs whatever the CURRENT pin resolves to, so the
422
+ // forward install lands the target and a post-rollback install restores the previous core.
423
+ // Knobs simulate a failing target install or a failing migrate.
424
+ function pinAwareRun(calls, { dir, failMigrate = false, failInstallForVersion = null } = {}) {
425
+ return (cmd, args) => {
426
+ calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
427
+ if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: '' };
428
+ if (cmd === 'npm' && args[0] === 'install') {
429
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
430
+ const dep = pkg.dependencies['@bongos/core'];
431
+ const v = u.versionFromTgzPath(dep) || u.versionFromDep(dep);
432
+ if (failInstallForVersion && v === failInstallForVersion) return { status: 1 };
433
+ writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'),
434
+ JSON.stringify({ name: '@bongos/core', version: v }) + '\n');
435
+ return { status: 0 };
436
+ }
437
+ if (cmd === 'npm' && args[0] === 'run' && args[1] === 'migrate') return { status: failMigrate ? 1 : 0 };
438
+ return { status: 0 }; // systemctl / restart-cmd / anything else
439
+ };
440
+ }
441
+
442
+ // A fake pg that records every ledger INSERT's params (params[3] = note).
443
+ function recordingPg(queries) {
444
+ return { Pool: class { async query(sql, params) { queries.push({ sql, params }); } async end() {} } };
445
+ }
446
+
447
+ t('rollback on health failure: reverts pin, restarts again, re-checks health, writes ONE rolled_back ledger row', async () => {
448
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
449
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
450
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
451
+ const calls = [], queries = [];
452
+ let fetchN = 0;
453
+ const res = await u.runUpgrade(
454
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthUrl: 'http://x/healthz' },
455
+ {
456
+ log: () => {}, err: () => {},
457
+ run: pinAwareRun(calls, { dir }),
458
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
459
+ databaseUrl: 'postgres://x/y', pg: recordingPg(queries),
460
+ // forward health fails (1st fetch); the post-rollback re-check recovers (2nd fetch)
461
+ fetch: async () => { fetchN++; return fetchN === 1 ? { ok: false, status: 503 } : { ok: true, status: 200 }; },
462
+ sleep: async () => {}, now: () => 0, healthBudgetMs: 0,
463
+ }
464
+ );
465
+ assert.equal(res.ok, false, 'a failed health check is not a success');
466
+ assert.equal(res.rolledBack, true, 'the upgrade rolled back');
467
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'pin reverted to the previous core');
468
+ assert.equal(u.readInstalledCoreVersion(dir), '1.14.1', 'previous core reinstalled');
469
+ assert.equal(res.rollback.healthOk, true, 'health recovered after the rollback restart');
470
+ assert.equal(calls.filter((c) => c === 'systemctl restart demo.service').length, 2, 'restarted twice: forward + rollback');
471
+ assert.equal(queries.length, 1, 'only the rolled_back row is written (no phantom upgraded row)');
472
+ assert.equal(queries[0].params[1], '1.15.0', 'to_version = the attempted target');
473
+ assert.match(queries[0].params[3], /^rolled_back:/, 'note marks the reversal');
474
+ });
475
+
476
+ t('rollback on migrate failure: reverts pin + reinstalls previous core + rolled_back ledger', async () => {
477
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
478
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
479
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
480
+ const calls = [], queries = [];
481
+ const res = await u.runUpgrade(
482
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
483
+ {
484
+ log: () => {}, err: () => {},
485
+ run: pinAwareRun(calls, { dir, failMigrate: true }),
486
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
487
+ databaseUrl: 'postgres://x/y', pg: recordingPg(queries),
488
+ }
489
+ );
490
+ assert.equal(res.ok, false);
491
+ assert.equal(res.rolledBack, true);
492
+ assert.match(res.rollback.reason, /migrate failed/);
493
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'pin reverted');
494
+ assert.equal(u.readInstalledCoreVersion(dir), '1.14.1', 'previous core reinstalled');
495
+ assert.equal(queries.length, 1);
496
+ assert.match(queries[0].params[3], /^rolled_back: migrate failed/);
497
+ });
498
+
499
+ t('rollback on npm-install failure: reverts pin + reinstalls previous core', async () => {
500
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
501
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
502
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
503
+ const calls = [], queries = [];
504
+ const res = await u.runUpgrade(
505
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
506
+ {
507
+ log: () => {}, err: () => {},
508
+ run: pinAwareRun(calls, { dir, failInstallForVersion: '1.15.0' }),
509
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
510
+ databaseUrl: 'postgres://x/y', pg: recordingPg(queries),
511
+ }
512
+ );
513
+ assert.equal(res.ok, false);
514
+ assert.equal(res.rolledBack, true);
515
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'pin reverted after the failed install');
516
+ assert.equal(u.readInstalledCoreVersion(dir), '1.14.1', 'previous core reinstalled');
517
+ assert.match(queries[0].params[3], /^rolled_back: npm install failed/);
518
+ });
519
+
520
+ t('--no-rollback-on-failure: a failed health check does NOT revert (records the attempt, leaves the new pin)', async () => {
521
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
522
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
523
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
524
+ const queries = [];
525
+ const res = await u.runUpgrade(
526
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthUrl: 'http://x/healthz', rollbackOnFailure: false },
527
+ {
528
+ log: () => {}, err: () => {},
529
+ run: pinAwareRun([], { dir }),
530
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
531
+ databaseUrl: 'postgres://x/y', pg: recordingPg(queries),
532
+ fetch: async () => ({ ok: false, status: 503 }), sleep: async () => {}, now: () => 0, healthBudgetMs: 0,
533
+ }
534
+ );
535
+ assert.equal(res.ok, false);
536
+ assert.ok(!res.rolledBack, 'no rollback when opted out');
537
+ assert.equal(u.readPinnedCoreVersion(dir), '1.15.0', 'the new pin is left in place for inspection');
538
+ assert.equal(queries.length, 1, 'the attempt is still recorded');
539
+ assert.ok(!/^rolled_back/.test(queries[0].params[3] || ''), 'not marked as a rollback');
540
+ });
541
+
542
+ t('restorePin: writes a previous dep string back verbatim (file: or registry)', () => {
543
+ const dir = scratchConsumer({ pinned: '1.15.0' });
544
+ u.restorePin(dir, 'file:vendor/bongos-core-1.14.1.tgz');
545
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1');
546
+ u.restorePin(dir, '^1.16.0');
547
+ assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '^1.16.0');
548
+ });
549
+
355
550
  await Promise.all(pending);
356
551
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
357
552
  process.exit(failed ? 1 : 0);
@@ -0,0 +1,98 @@
1
+ // tests/workflow_exec_paths.mjs — task 2136 (ADR 0108 configurable-root).
2
+ //
3
+ // Proves every .github/workflows/*.yml invocation of a CORE script resolves in a consumer layout
4
+ // after the R89 split: each is routed through the both-layouts resolver (infra/core-node.sh for
5
+ // node targets, infra/core-path.sh for bash targets) AND names a PUBLISHABLE core file that ships
6
+ // in @bongos/core (→ present at node_modules/@bongos/core post-install) and exists at the core root
7
+ // today. No workflow still invokes a core script the bare repo-relative way. Also proves
8
+ // core-path.sh resolves both layouts (the bash-target sibling of core-node.sh).
9
+ //
10
+ // DB-free, no network. Run: node tests/workflow_exec_paths.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import { execFileSync } from 'node:child_process';
14
+ import { createRequire } from 'node:module';
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const { lintWorkflowExec } = require('../scripts/gds/lint-workflow-exec.js');
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const REPO = path.resolve(__dirname, '..');
24
+ const CORE_PATH_SH = path.join(REPO, 'infra', 'core-path.sh');
25
+
26
+ let passed = 0, failed = 0;
27
+ function test(name, fn) {
28
+ try { fn(); console.log(` ok ${name}`); passed++; }
29
+ catch (err) { console.error(` FAIL ${name}`); console.error(' ', err.message); failed++; }
30
+ }
31
+
32
+ // ---- A. the workflow lint is green on the real tree ---------------------------------------------
33
+ test('lint-workflow-exec: every core invocation is routed + publishable (no bare repo-relative form)', () => {
34
+ const r = lintWorkflowExec({ instanceRoot: REPO, coreRoot: REPO });
35
+ assert.deepEqual(r.violations, [], `expected no violations, got: ${JSON.stringify(r.violations, null, 2)}`);
36
+ assert.ok(r.ok, 'lint should pass');
37
+ // The security-critical merge gates + the functional gates must all be covered.
38
+ const rels = r.checked.map((c) => `${c.file}:${c.rel}`);
39
+ assert.ok(rels.some((s) => s.startsWith('gate-review.yml:') && s.endsWith('scripts/gds/gate-review.js')), 'gate-review routed');
40
+ assert.ok(rels.some((s) => s.startsWith('grade-gate.yml:') && s.endsWith('scripts/gds/ci-grade.js')), 'grade-gate routed');
41
+ assert.ok(rels.some((s) => s.endsWith('scripts/migrate.sh')), 'selfhost migrate routed via core-path.sh');
42
+ });
43
+
44
+ test('lint-workflow-exec: a bare core invocation in a workflow is flagged (counterfactual)', () => {
45
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'wf-lint-'));
46
+ try {
47
+ fs.mkdirSync(path.join(tmp, '.github', 'workflows'), { recursive: true });
48
+ fs.mkdirSync(path.join(tmp, 'scripts', 'gds'), { recursive: true });
49
+ fs.writeFileSync(path.join(tmp, 'scripts', 'gds', 'gate-review.js'), '// probe\n');
50
+ fs.writeFileSync(path.join(tmp, '.github', 'workflows', 'legacy.yml'),
51
+ 'jobs:\n x:\n steps:\n - run: node scripts/gds/gate-review.js\n');
52
+ const r = lintWorkflowExec({ instanceRoot: tmp, coreRoot: tmp });
53
+ assert.ok(!r.ok, 'a bare core invocation must fail the lint');
54
+ assert.equal(r.violations.length, 1);
55
+ assert.match(r.violations[0].why, /bare core invocation/);
56
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
57
+ });
58
+
59
+ // ---- B. core-path.sh resolves BOTH layouts ------------------------------------------------------
60
+ function runCorePath(rel, instanceRoot) {
61
+ return execFileSync('bash', [CORE_PATH_SH, rel], {
62
+ encoding: 'utf8', env: { ...process.env, BONGOS_INSTANCE_ROOT: instanceRoot },
63
+ }).trim();
64
+ }
65
+
66
+ test('core-path.sh prints the monolith path (script at the instance root)', () => {
67
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-mono-'));
68
+ try {
69
+ fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
70
+ fs.writeFileSync(path.join(tmp, 'scripts', 'migrate.sh'), '# probe\n');
71
+ assert.equal(runCorePath('scripts/migrate.sh', tmp), path.join(tmp, 'scripts', 'migrate.sh'));
72
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
73
+ });
74
+
75
+ test('core-path.sh falls back to the consumer path (script only under node_modules/@bongos/core)', () => {
76
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-cons-'));
77
+ try {
78
+ const pkg = path.join(tmp, 'node_modules', '@bongos', 'core', 'scripts');
79
+ fs.mkdirSync(pkg, { recursive: true });
80
+ fs.writeFileSync(path.join(pkg, 'migrate.sh'), '# probe\n');
81
+ assert.equal(runCorePath('scripts/migrate.sh', tmp), path.join(pkg, 'migrate.sh'));
82
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
83
+ });
84
+
85
+ test('core-path.sh exits non-zero (no stdout) when the script is in NO layout', () => {
86
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cp-none-'));
87
+ let threw = false;
88
+ try {
89
+ execFileSync('bash', [CORE_PATH_SH, 'scripts/nope.sh'], {
90
+ encoding: 'utf8', stdio: 'pipe', env: { ...process.env, BONGOS_INSTANCE_ROOT: tmp },
91
+ });
92
+ } catch (_) { threw = true; }
93
+ finally { fs.rmSync(tmp, { recursive: true, force: true }); }
94
+ assert.ok(threw, 'expected a non-zero exit when the script resolves in no layout');
95
+ });
96
+
97
+ console.log(`\nworkflow_exec_paths: ${passed} passed, ${failed} failed`);
98
+ process.exit(failed ? 1 : 0);
@@ -1,49 +0,0 @@
1
- # Container building — exterior tile spec (pixel-precise, V2)
2
-
3
- - **Task:** [#643](https://example.com/builders#/task/643) (V2 · product). Promoted from idea_inbox [#162](https://example.com/builders#/idea/162) (builder 25 art/design brief, 2026-06-04).
4
- - **Date:** 2026-06-18
5
- - **Builds on:** [`container-exterior.md`](container-exterior.md) (the structure-level design, task [#641](https://example.com/builders#/task/641)) — this is the **tile-level** layer it calls out. Colour is governed by the locked decisions below.
6
- - **Status:** Spec ready. **Generation waits for [#640](https://example.com/builders#/task/640) / [ADR 0044](../adr/0044-mediterranean-palette-replacement.md)** (every tile here re-quantizes against the replacement palette — per `container-exterior.md` §Pipeline). Spec work proceeds now.
7
-
8
- ## ⚠️ Colour: the original brief is superseded — weathered charcoal, not clean red
9
-
10
- idea [#162](https://example.com/builders#/idea/162)'s brief (this task's source) asked for **"saturated industrial red (or black) — clean, no weathering, no rust."** That was written **before** Lars resolved the question. The authoritative decision (later wins):
11
-
12
- - **Blocker [#7](https://example.com/builders#/blocker/7) (Lars, 2026-06-10, now a locked `style_guide.md` rule):** the container body is **weathered BLACK/CHARCOAL corrugated metal with rust streaks** — *not* red, *not* teal. Faded **Hermes-teal is an ACCENT only** (the door panel + a faint caduceus etch). It must read as an *ancient, heavy, discovered relic*, not factory-new cargo.
13
- - **`style_guide.md` (Lars, 2026-06-14):** **no white / light edge rim** on container (or other large-object) tiles — strip near-white edge pixels in post ("the white on the container actually sucks").
14
- - **`style_guide.md`:** non-ground objects carry a **1-px darker outline** against their interior (FireRed rock/tree convention); ground tiles do not.
15
-
16
- So every tile below is **weathered charcoal corrugated metal with rust streaks**; the brief's "clean red, no weathering, single-pixel white highlight" wording does **not** apply. The brief's *geometry* (corrugation, glass panels, deck, roof, staircase) is still the source — only the colour/finish is corrected.
17
-
18
- ## Per-tile pixel specs (32×32 base)
19
-
20
- ### Container wall tile
21
- - **Base:** weathered charcoal/gunmetal (a matte near-black with a faint blue-grey undertone), drawn from the replacement palette's charcoal/grey value steps (ADR 0044 guarantees enough steps).
22
- - **Corrugation:** 4–5 vertical ridges per tile. Each ridge: 1px lighter charcoal on the left (highlight), 1px darker on the right (shadow); flat panels between are the base.
23
- - **Weathering:** sparse vertical **rust streaks** (a desaturated orange-brown from the palette) bleeding downward from ridge tops + seams — irregular, 1–2px wide, not on every ridge. Reads as age, not damage.
24
- - **Edges:** 1px darker-charcoal **outline** on the object silhouette (per the style rule). Top edge 2px darker (top face in oblique light); bottom edge 1px near-black ground shadow. **No white/light rim** anywhere on the silhouette.
25
- - **Stacked-unit seam:** a 2px dark recessed line with a 1px shadow below, where the ground-floor container meets the second storey.
26
-
27
- ### Glass storefront / window panel (32×32, or 64×32 wide)
28
- - **Fill:** semi-transparent blue-grey (≈40–50% over what's behind). Through dark glass, a faint **warm amber interior glow** (the "dark exterior, warm interior" mood from `container-exterior.md`).
29
- - **Frame:** 3px matte-charcoal border (the frames dissolve into the dark facade rather than reading as separate elements).
30
- - **Interior hint:** extremely faint 1–2-shade-darker silhouettes (a plant, shelving) — not detailed.
31
- - **Reflection highlight — FLAG for Lars:** the brief wanted a white upper-left reflection strip; the no-white-rim rule targets the *object silhouette*, not interior glass. Proposed: a restrained **pale-blue** (not white) 1px upper-left reflection inside the frame. If Lars wants zero light pixels here, drop it. *(Open pixel-detail decision.)*
32
-
33
- ### Door tile (the one Hermes-teal accent)
34
- - Charcoal frame; the door panel is **faded Hermes-teal** (the single body-color exception), with a **faint caduceus etch** (1–2px lighter teal lines) where space allows, and a warm interior glow at the threshold. This is the only teal on the building.
35
-
36
- ### Deck tile (wood apron, walkable)
37
- - Horizontal planks: light warm tan base, 4–5 planks/tile, 1px dark gap between; very subtle 1–2px grain lines; 2px darker far-edge shadow. Drawn as an alpha-margined object overlay over sand (the `marble_path` technique, [ADR 0013](../adr/<redacted>.md)) so it works on any ground.
38
-
39
- ### Roof tile (top-down)
40
- - Flat charcoal surface, 1 shade darker than the wall face (less direct light); small 3×3 dark **container corner castings** at each corner; a faint 2px panel seam every 8px; 1px railing line on edge tiles (per `container-exterior.md` rooftop band).
41
-
42
- ### Exterior staircase (object overlay)
43
- - Dark charcoal steel: two parallel rails with treads between (top-down ladder look); 1px railing line each outer edge, 1px lighter than the tread. Arrives at the rooftop stair-landing tile.
44
-
45
- ## Mood target
46
- A **designed object, not a found one** — presence from its *shape and glass*, not from bright paint. The charcoal recedes into the coastal scene; at dusk the warm interior light through dark glass is the most dramatic visual in the game. The contradiction (an *ancient* shipping container) lives in the weathering + the teal-and-caduceus door.
47
-
48
- ## Pipeline mapping (generate after [#640](https://example.com/builders#/task/640))
49
- Per [ADR 0008](../adr/<redacted>.md) (families, not per-tile): `container_building_facade` (wall · glass · door · light-fixture · staircase), `container_building_roof` (field · railing · landing), `deck_wood` (16-autotile alpha-tile line family). Furniture singletons (table, chairs) per-tile. **Nothing generates until the palette swap ([#640](https://example.com/builders#/task/640)/ADR 0044) lands** — else every tile re-quantizes against the old palette and is redone.
@@ -1,81 +0,0 @@
1
- # Container building — exterior structure design (V2)
2
-
3
- - **Task:** [#641](https://example.com/builders#/task/641) (V2 · product · creative). Promoted from idea_inbox [#160](https://example.com/builders#/idea/160) (builder 25 art/design brief, 2026-06-04).
4
- - **Date:** 2026-06-11
5
- - **Status:** Design locked at the structure level. Pixel-precise tile specs are [#643](https://example.com/builders#/task/643) (facade); interior is [#642](https://example.com/builders#/task/642)/[#644](https://example.com/builders#/task/644)/[#645](https://example.com/builders#/task/645); palette implementation is [#640](https://example.com/builders#/task/640) / [ADR 0044](../adr/0044-mediterranean-palette-replacement.md).
6
-
7
- ## What changes
8
-
9
- The container is no longer a single beached 4×2 object (the current `CONTAINER_LAYOUT` in
10
- `src/world/terrain.js`). It becomes a **two-storey trading house built from stacked
11
- shipping containers** — a designed building with a glass storefront, a wooden deck,
12
- an exterior staircase, and a flat rooftop. It reads as a *destination*, not salvage.
13
-
14
- The narrative survives intact: Hermes placed the containers; the Examplens stacked
15
- them into a trading house. The mythic contradiction (an *ancient* shipping container)
16
- stays — see the colour note below.
17
-
18
- ## Colour: weathered black/charcoal (decided)
19
-
20
- The original brief asked for "bold, uniform red or black — not rusted or weathered."
21
- That option was resolved **after** the brief was written: blocker [#7](https://example.com/builders#/blocker/7) (Lars, 2026-06-10,
22
- recorded as a style-guide rule) fixed the container as **weathered black/charcoal
23
- corrugated metal with rust streaks**, with faded Hermes-teal as an *accent only* (the
24
- door + the caduceus etch). The later decision wins. So the building is charcoal and
25
- weathered — it is "designed architecture" by its *shape and glass*, not by looking
26
- factory-new. ADR 0044 already constrains the replacement palette to carry enough
27
- charcoal/grey value steps for this.
28
-
29
- ## Top-down structure (32×32 tiles, FireRed building anatomy)
30
-
31
- FireRed conveys buildings top-down as **facade rows (what you'd see standing in front)
32
- topped by roof rows**. A second storey = a taller facade. The reference is the Celadon
33
- department store: tall glass-fronted facade, flat roof.
34
-
35
- The building is an **8-wide × 6-tall** visual block (up from 4×2):
36
-
37
- | Rows (top→bottom) | Band | Content |
38
- |---|---|---|
39
- | 1–2 | **Rooftop** | Flat container roof; 1-px railing along the edge tiles; a stair landing tile at the right edge where the exterior staircase arrives. Future rooftop deck props (parasol, crates) are object overlays, not part of this family. |
40
- | 3–4 | **Upper-storey facade** | Charcoal corrugated container walls with a continuous band of **large glass panels** (semi-transparent blue-grey, single white highlight stripe). Container seams visible at the stacked-unit joints. |
41
- | 5–6 | **Ground-storey facade** | Glass storefront panels flanking a centred **door** (the one Hermes-teal accent, faint caduceus etch, warm interior glow). Wall-mounted light fixtures as small details on the wall tiles either side — lit at dusk/night. |
42
-
43
- Attached, outside the 8×6 block:
44
-
45
- - **Exterior staircase** — a 1-tile-wide steel stair strip climbing the building's right
46
- side from ground to the rooftop landing. Purely visual in V2 (blocking); the rooftop
47
- becomes walkable in a later version, the art shouldn't need to change for that.
48
- - **Wooden deck** — a 2-row walkable apron in front of the ground facade, drawn as an
49
- alpha-margined object overlay over sand (the marble-path technique, [ADR 0013](../adr/<redacted>.md)),
50
- so it works on any ground. Outdoor furniture (table, two chairs) are blocking
51
- object singletons placed on the deck, off the door's walking line.
52
-
53
- ### Placement and collision
54
-
55
- Anchor at **(11, 4)** in the current 30×18 world: the building spans cols 11–18,
56
- rows 4–9 (grass behind, sand in front), the deck spans rows 10–11, and the existing
57
- marble path (col 13, rows 10–13) shifts to meet the door at col 14–15. All building
58
- tiles **block**, including the door — interaction stays "stand adjacent and face it,"
59
- exactly like the current container door. Deck tiles are walkable; furniture blocks.
60
-
61
- ## Pipeline mapping (what gets generated, later)
62
-
63
- Per [ADR 0008](../adr/<redacted>.md), coherent groups are families, not per-tile calls:
64
-
65
- 1. **`container_building_facade`** family — wall, glass-panel, door, light-fixture, and
66
- staircase tiles (replaces the 2×4 `container` family in `art/template/families.json`).
67
- 2. **`container_building_roof`** family — roof field, railing edges/corners, stair landing.
68
- 3. **`deck_wood`** — 16-autotile alpha-tile line family (same recipe as `marble_path`).
69
- 4. **Furniture singletons** (table, chair) — per-tile pipeline.
70
-
71
- **Sequencing rule: generate nothing until [#640](https://example.com/builders#/task/640) / ADR 0044 lands.** Every one of these
72
- tiles would be quantized against the old palette and re-done after the Mediterranean
73
- swap. Spec work ([#643](https://example.com/builders#/task/643)) can proceed now; generation waits.
74
-
75
- ## Engineering follow-up (not this task)
76
-
77
- The world model needs a matching change in **both** `map.js`/`terrain.js` twins
78
- (server + client): new OBJECT ids, an 8×6 building layout replacing
79
- `CONTAINER_PANELS_ROWMAJOR`/`CONTAINER_LAYOUT`, deck overlay placement, and blocking
80
- rules as above. That is an engineering task to be filed when the facade family's tile
81
- ids are final (after [#643](https://example.com/builders#/task/643)).