@bongos/core 1.19.116 → 1.19.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.116",
6
- "core_contract": "1.19.116",
7
- "source_commit": "be42420efe0538b351e5eed4a355a47e28b430f5",
5
+ "core_version": "1.19.118",
6
+ "core_contract": "1.19.118",
7
+ "source_commit": "b2c7044681ffb9b34e63677b0487f02dba2111df",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-14T18:14:13.252Z",
9
+ "built_at": "2026-08-14T19:32:31.077Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 302,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1236,
14
+ "functional_verbatim": 1239,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1559,
20
- "tree_sha256": "8ac36ba3efd387f4c72fecb2fde8b6d68ae8c016c5e8336c2f78a24abb5791e1",
19
+ "file_count": 1562,
20
+ "tree_sha256": "d14e35ba8094c969d33de804077321c1dd6f7463d0d4a1d77585ebc85e501e7c",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -367,7 +367,7 @@
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "e7bc974c5ea756d1c197c1c8e52b902049af23e335149ff76e1ade248eb623d1"
370
+ "sha256": "167b7dcd701d2a23eec6dec515778e84cb262d95c070a5a749da642e5aada701"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
@@ -387,7 +387,7 @@
387
387
  {
388
388
  "path": "config/design-tokens.neutral.json",
389
389
  "mode": "0000644",
390
- "sha256": "16803a2b65d64052f9509b0ff50dbe36aadcc94639d5c6896736b1c9787a022b"
390
+ "sha256": "ffca38493475ebe55c6ac9513f8b7836162c26d2a532ef0a1cc6a3abc9ffac68"
391
391
  },
392
392
  {
393
393
  "path": "config/design-tokens.schema.json",
@@ -1337,12 +1337,12 @@
1337
1337
  {
1338
1338
  "path": "docs/api-reference.md",
1339
1339
  "mode": "0000644",
1340
- "sha256": "1a4562c2e0575f8505a22851c63f1cc3ecfd485ae5de6e725810786f692e6457"
1340
+ "sha256": "06954a3b30e235ef61a316d4f7414a2eaf2c0f2d3fe1063698e800f3655d1675"
1341
1341
  },
1342
1342
  {
1343
1343
  "path": "docs/api/openapi.json",
1344
1344
  "mode": "0000644",
1345
- "sha256": "3b5917da91883faf736528e7ba5600356f06056228a7c81c7d5e025762253699"
1345
+ "sha256": "3910ea26d65b5afc883d4b3488d605ee5786bda41ef86e280b5a180d6f6fa586"
1346
1346
  },
1347
1347
  {
1348
1348
  "path": "docs/architecture.md",
@@ -1367,7 +1367,7 @@
1367
1367
  {
1368
1368
  "path": "docs/design-contract.md",
1369
1369
  "mode": "0000644",
1370
- "sha256": "a86af6a923ed70bb8ea0846266f232f31a5fc9709d286362bcd37104d683342b"
1370
+ "sha256": "8a71b3573c3d9be7e543baa1817d42df81cd546e3cfd544c785533ccaa9da477"
1371
1371
  },
1372
1372
  {
1373
1373
  "path": "docs/design/front-door-and-hub-direction.md",
@@ -3744,6 +3744,11 @@
3744
3744
  "mode": "0000644",
3745
3745
  "sha256": "730de845214d3159cceb779ec3f20cddf2f6662bf2ada787d1c9cec483273afa"
3746
3746
  },
3747
+ {
3748
+ "path": "modules/platform-identity/migrations/platform_identity_006_project_type.sql",
3749
+ "mode": "0000644",
3750
+ "sha256": "4885d452673c2d6170835c8d7f03c101847ddb313a69dfb66c8e640f875ecdc1"
3751
+ },
3747
3752
  {
3748
3753
  "path": "modules/platform-identity/module.json",
3749
3754
  "mode": "0000644",
@@ -3752,7 +3757,7 @@
3752
3757
  {
3753
3758
  "path": "modules/platform-identity/platform-identity.js",
3754
3759
  "mode": "0000644",
3755
- "sha256": "2a0ee2667f79cbe8eaeb1dfb47b0c518bd969e80a60e42e0f709fa83365f908e"
3760
+ "sha256": "c145e51c02f147b295287b9759d5f44ffc77a533908b151b69d13781d84bc433"
3756
3761
  },
3757
3762
  {
3758
3763
  "path": "modules/platform-identity/pollers/code-sweep.js",
@@ -3772,7 +3777,7 @@
3772
3777
  {
3773
3778
  "path": "modules/platform-identity/routes/projects.js",
3774
3779
  "mode": "0000644",
3775
- "sha256": "3222e294691563874be9b19dda57fe879b29d0bd174edef06f6db0f483863b11"
3780
+ "sha256": "a3e25369ad9edc6aed1a8a6997d6b6cb794dce14abb3605954d535525af1a93a"
3776
3781
  },
3777
3782
  {
3778
3783
  "path": "modules/platform-identity/routes/scouting.js",
@@ -3787,7 +3792,7 @@
3787
3792
  {
3788
3793
  "path": "modules/platform-identity/tests/platform-identity.mjs",
3789
3794
  "mode": "0000644",
3790
- "sha256": "7766d918d22a2d73f9275ab07d6273b66fc0cdcdc6d2952986e01bab265344d2"
3795
+ "sha256": "13bd93a19ec0d53642a161a01e4ec2ced6930d96f455c5fcc72fbdc43f72e1ac"
3791
3796
  },
3792
3797
  {
3793
3798
  "path": "modules/platform-identity/ui/.gitkeep",
@@ -3829,6 +3834,11 @@
3829
3834
  "mode": "0000644",
3830
3835
  "sha256": "49dee0b12551089acc0d7f218f1cba00b1377ba581770146734b2c2f71d3bf7c"
3831
3836
  },
3837
+ {
3838
+ "path": "modules/provisioning/migrations/provisioning_007_project_type.sql",
3839
+ "mode": "0000644",
3840
+ "sha256": "ddbc1f7e659e716fcbb3ba3ec6c1237756f46dbd895bcb27a1dc925a7c782e76"
3841
+ },
3832
3842
  {
3833
3843
  "path": "modules/provisioning/module.json",
3834
3844
  "mode": "0000644",
@@ -3847,7 +3857,7 @@
3847
3857
  {
3848
3858
  "path": "modules/provisioning/provisioning.js",
3849
3859
  "mode": "0000644",
3850
- "sha256": "0363fd616966ec413d3c16bea222622ae0f9cf59a456627946c7cbdb89b7885e"
3860
+ "sha256": "9163b2b0de80a648371d03f89e0da8ff234a1cd4f038a1279f6324ae0921e0f4"
3851
3861
  },
3852
3862
  {
3853
3863
  "path": "modules/provisioning/rate-limit.js",
@@ -3857,7 +3867,7 @@
3857
3867
  {
3858
3868
  "path": "modules/provisioning/routes/provisioning.js",
3859
3869
  "mode": "0000644",
3860
- "sha256": "1880a5b0812a32feb9dc313024b3a27b1403884316f17fbaad68909872e21535"
3870
+ "sha256": "232d5ce81dc2821b9b953c7596ab385b89e2d5193891735e3220021437fd06db"
3861
3871
  },
3862
3872
  {
3863
3873
  "path": "modules/provisioning/tests/liveness.mjs",
@@ -3867,7 +3877,7 @@
3867
3877
  {
3868
3878
  "path": "modules/provisioning/tests/provisioning.mjs",
3869
3879
  "mode": "0000644",
3870
- "sha256": "e23faf9471d7d984dc86601b157aeac0518f01f9c72076045b94368aa7490b61"
3880
+ "sha256": "4a5c817d88771a62ccb5d26aea865976b7246a253b2ad7b00e11e3be856586ef"
3871
3881
  },
3872
3882
  {
3873
3883
  "path": "modules/provisioning/ui/.gitkeep",
@@ -4152,12 +4162,12 @@
4152
4162
  {
4153
4163
  "path": "package-lock.json",
4154
4164
  "mode": "0000644",
4155
- "sha256": "87bcf76333165f80466bc093e13d0ad6313d63db0e8518b083adf18cfc4c373d"
4165
+ "sha256": "a21e300cabe4c1a3af1b84fcb4b190c8ee109acd79d888e04351cb9064162457"
4156
4166
  },
4157
4167
  {
4158
4168
  "path": "package.json",
4159
4169
  "mode": "0000644",
4160
- "sha256": "f7a325a5a15a160f72580f8af598cf302a68ec9fc04a03085dcdfa341c52dc89"
4170
+ "sha256": "4faa734f756086158c7c1d7983352c08d52961e479c1487aadba9a770007d19e"
4161
4171
  },
4162
4172
  {
4163
4173
  "path": "public-docs/index.html",
@@ -4397,7 +4407,7 @@
4397
4407
  {
4398
4408
  "path": "public-landing/assets/cosmos.css",
4399
4409
  "mode": "0000644",
4400
- "sha256": "b615a392fea969c2b0549476843f26d5185cabc6556036e1f3337aacdeac359e"
4410
+ "sha256": "bd0b17f7a21120a9e754c88206bd718fdda1fce568df15441272af0b9004c7cb"
4401
4411
  },
4402
4412
  {
4403
4413
  "path": "public-landing/buddha.png",
@@ -4412,7 +4422,7 @@
4412
4422
  {
4413
4423
  "path": "public-landing/projects.html",
4414
4424
  "mode": "0000644",
4415
- "sha256": "961850a566b9fa797036b9ac8b12b3f0eacf5cc6f5fbabbb6230dccd02e836b8"
4425
+ "sha256": "576bc8fe1e3379dd2efa6323f889b65d9c4cbb5164b5e80716a6eee045bad002"
4416
4426
  },
4417
4427
  {
4418
4428
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5067,7 +5077,7 @@
5067
5077
  {
5068
5078
  "path": "scripts/gds/provision.js",
5069
5079
  "mode": "0000644",
5070
- "sha256": "7ad384495370312a57f7c1f0dab4770888c83afd08b89a8dc27aebae408abfd1"
5080
+ "sha256": "a10501b65207bbd72f9fc039d4cf1ef0be7f85dc832e8e95e1b26aa966e507b8"
5071
5081
  },
5072
5082
  {
5073
5083
  "path": "scripts/gds/publish-credential-check.js",
@@ -5732,7 +5742,7 @@
5732
5742
  {
5733
5743
  "path": "src/module-api.js",
5734
5744
  "mode": "0000644",
5735
- "sha256": "298ca0ddbe3bcae48f5c360ca66aea09a798f317816cbbb9180142b12a7fb6ae"
5745
+ "sha256": "2e899e310368c74a25215b99d3dabced0755c87f85569d23a069417dd20c661a"
5736
5746
  },
5737
5747
  {
5738
5748
  "path": "src/module-loader/catalog.js",
@@ -5777,7 +5787,7 @@
5777
5787
  {
5778
5788
  "path": "src/ui/adapters/claude-design/index.js",
5779
5789
  "mode": "0000644",
5780
- "sha256": "04218100d565edf00d822aefa262a001f503bf9f45ae79f4a46a3c2f1756ede5"
5790
+ "sha256": "7489ba1a6612068648359c65d1abeae8faa6de0d16e0cf58d67ce5a1f307e372"
5781
5791
  },
5782
5792
  {
5783
5793
  "path": "src/ui/adapters/figma/index.js",
@@ -6237,7 +6247,7 @@
6237
6247
  {
6238
6248
  "path": "tests/cosmos_token_contrast.mjs",
6239
6249
  "mode": "0000644",
6240
- "sha256": "a133a1ed438dfb589b0e31ce2dacced2e38e1047411d85e0086d4c2ff8b88f16"
6250
+ "sha256": "257acb76f7b7e8bb925b005e8a7bad96d712f0a216201aacee11762a2a2ff328"
6241
6251
  },
6242
6252
  {
6243
6253
  "path": "tests/cost.mjs",
@@ -6324,6 +6334,11 @@
6324
6334
  "mode": "0000644",
6325
6335
  "sha256": "1abfa6c8bb119d010d2a53f71373508a31a9cc3c28207ca1dbac2138a7eda595"
6326
6336
  },
6337
+ {
6338
+ "path": "tests/design_tokens_sync.mjs",
6339
+ "mode": "0000644",
6340
+ "sha256": "6d1a9dfba7e6ef7606c68abc9df3fb6e479f1a2939b83ad342f907b338f17f2c"
6341
+ },
6327
6342
  {
6328
6343
  "path": "tests/dev_lib.mjs",
6329
6344
  "mode": "0000644",
@@ -7257,7 +7272,7 @@
7257
7272
  {
7258
7273
  "path": "tests/projects_hub.mjs",
7259
7274
  "mode": "0000644",
7260
- "sha256": "8cd0d3f4ce68085f782fbd3e359039ad6d20198bc706481f165c39ee133cea3a"
7275
+ "sha256": "ee5c944f9e3bd3d618c977e76d4e2ba3e663ce59bf7833f9427a0191717ba57d"
7261
7276
  },
7262
7277
  {
7263
7278
  "path": "tests/provision.mjs",
@@ -244,7 +244,7 @@ export interface PostMyProjectsJoinRequest { origin: string }
244
244
  export interface PostMyProjectsJoinResponse { ok: boolean; status: unknown; project: unknown; message: unknown }
245
245
  export interface PostOverrideRequestsIdDecideRequest { decision: string; note?: string }
246
246
  export interface PostProjectsInviteRequest { client_id: string; github_login: string }
247
- export interface PostProjectsRequest { origin: string; name: string; tagline?: string; art_url?: string }
247
+ export interface PostProjectsRequest { origin: string; name: string; tagline?: string; art_url?: string; type?: string }
248
248
  export interface PostProjectsResponse { project: unknown }
249
249
  export interface PostProvisioningInstancesIdDomainRequest { domain: string }
250
250
  export interface PostProvisioningInstancesIdDomainResponse { ok: boolean; changed: boolean; queued: unknown; domain: unknown; message: unknown }
@@ -252,7 +252,7 @@ export interface PostProvisioningInstancesIdForceTeardownResponse { ok: boolean;
252
252
  export interface PostProvisioningInstancesIdGithubAppRequest { account?: string }
253
253
  export interface PostProvisioningInstancesIdGithubAppResponse { state: unknown; account: unknown; post_url: unknown; personal_url: unknown; org_url: unknown; org_login: unknown; manifest: unknown; redirect_url: unknown; expires_at: unknown }
254
254
  export interface PostProvisioningInstancesIdTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
255
- export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; tier?: string; onboard_mode?: string }
255
+ export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; tier?: string; onboard_mode?: string; type?: string }
256
256
  export interface PostSearchMarkRequest { chunk_id?: StringifiableId; useful?: unknown }
257
257
  export interface PostSearchRequest { expand?: unknown; q?: string; source_kind?: string; limit?: unknown }
258
258
  export interface PostSearchResponse { expand?: unknown; chunk?: unknown; query?: unknown; count?: unknown; total_full_tokens?: unknown; results?: unknown }
@@ -2,19 +2,19 @@
2
2
  "_note": "Cloud Bongos neutral design tokens — the baseline every instance resolves over. To override, copy values into config/design-tokens.json (the instance pack). These match config/branding.neutral.json theme.ui so a vanilla instance with no instance pack still renders correctly. See docs/design-contract.md.",
3
3
 
4
4
  "colors": {
5
- "bg": "#fff7ed",
6
- "bgCard": "#ffffff",
7
- "bgDeep": "#ffedd5",
8
- "bgFrame": "#fde4c8",
9
- "ink": "#2a1f17",
10
- "inkSoft": "#6b5d52",
11
- "inkFaint": "#857567",
12
- "rule": "#fdba74",
13
- "ruleSoft": "#fed7aa",
14
- "accent": "#f97316",
15
- "accentDeep": "#ea580c",
16
- "sea": "#38bdf8",
17
- "terracotta": "#fb7185"
5
+ "bg": "#f9d9a8",
6
+ "bgCard": "#fffaf2",
7
+ "bgDeep": "#f6d8ae",
8
+ "bgFrame": "#f0c489",
9
+ "ink": "#2b170e",
10
+ "inkSoft": "#6d4a33",
11
+ "inkFaint": "#785a41",
12
+ "rule": "#e8a463",
13
+ "ruleSoft": "#f6d8ae",
14
+ "accent": "#ce4806",
15
+ "accentDeep": "#9e3406",
16
+ "sea": "#0f7a96",
17
+ "terracotta": "#855b3d"
18
18
  },
19
19
 
20
20
  "fonts": {
@@ -15676,6 +15676,9 @@
15676
15676
  "art_url": {
15677
15677
  "type": "string",
15678
15678
  "maxLength": 2048
15679
+ },
15680
+ "type": {
15681
+ "type": "string"
15679
15682
  }
15680
15683
  },
15681
15684
  "required": [
@@ -15830,6 +15833,9 @@
15830
15833
  "onboard_mode": {
15831
15834
  "type": "string",
15832
15835
  "maxLength": 20
15836
+ },
15837
+ "type": {
15838
+ "type": "string"
15833
15839
  }
15834
15840
  },
15835
15841
  "required": [
@@ -353,7 +353,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
353
353
  | Method | Path | Rank | Body | Description |
354
354
  |---|---|---|---|---|
355
355
  | GET | `/api/bongos/projects` | `metic+archon` | — | the full curation list (includes unfeatured + owner attribution). |
356
- | POST | `/api/bongos/projects` | `metic+archon` | `origin`, `name`, `tagline`, `art_url` | create / update a catalog entry (manual seed for hand-wired instances; federated projects auto-catalogue at registration). |
356
+ | POST | `/api/bongos/projects` | `metic+archon` | `origin`, `name`, `tagline`, `art_url`, `type` | create / update a catalog entry (manual seed for hand-wired instances; federated projects auto-catalogue at registration). |
357
357
  | PATCH | `/api/bongos/projects/:id/featured` | `metic+archon` | `featured` | the feature / unfeature control (the ADR 0141 §5 curation gate). |
358
358
  | GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
359
359
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
@@ -368,7 +368,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
368
368
  | GET | `/api/bongos/provisioning/github-app/status` | `public` | — | GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding callback page polls (task 2096) to know when setup… |
369
369
  | GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
370
370
  | GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
371
- | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `tier`, `onboard_mode` | POST /provisioning/instances — request a new instance. |
371
+ | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
372
372
  | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one of the caller's own instances. |
373
373
  | POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
374
374
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
@@ -30,7 +30,7 @@ The validator merges both files (deep-merge, instance wins) and validates the re
30
30
 
31
31
  ### 1.3 Relationship to config/branding.json
32
32
 
33
- `config/branding.json` carries *identity* (product name, domains, currency, tracks). Design tokens carry *presentation* (colors, type, spacing). The two files are siblings; neither owns the other. Where both define overlapping values (e.g. `branding.json theme.ui.colors` and `design-tokens.json colors`), **design-tokens.json is authoritative at render time**. The `src/branding.js` resolver will be updated (task 1377) to expose resolved tokens alongside identity — adapters use `branding.resolveDesignTokens()`.
33
+ `config/branding.json` carries *identity* (product name, domains, currency, tracks). Design tokens carry *presentation* (colors, type, spacing). The two files are siblings; neither owns the other. Where both define overlapping values (e.g. `branding.json theme.ui.colors` and `design-tokens.json colors`), **design-tokens.json is authoritative at render time**. The **neutral pair** is the one exception: `config/branding.neutral.json theme.ui` is the authority and `config/design-tokens.neutral.json` mirrors it key-for-key — CI-asserted by `tests/design_tokens_sync.mjs` (task 1002752), so the design round-trip can never again start prototypes from colours the product stopped painting. Instance packs stay free per §1.2. The `src/branding.js` resolver will be updated (task 1377) to expose resolved tokens alongside identity — adapters use `branding.resolveDesignTokens()`.
34
34
 
35
35
  ### 1.4 Token schema overview
36
36
 
@@ -290,6 +290,8 @@ The `--dt-` prefix is reserved for this design system. Adapters must not emit CS
290
290
  6. Every `surfaceId` key in `design-sources.json` has a matching directory under `src/ui/` or `assets/ui/` (if any such directory exists — the file may precede the directories for a new instance).
291
291
  7. If `src/ui/adapters/` exists, each adapter directory has an `index.js` that exports `id`, `import`, and `export`.
292
292
 
293
+ The neutral-pair lockstep (§1.3) is deliberately not a validator check — it lives in `tests/design_tokens_sync.mjs`, so an instance running only `validate-design.js` is not forced to mirror its own pack.
294
+
293
295
  Usage:
294
296
 
295
297
  ```
@@ -0,0 +1,23 @@
1
+ -- platform_identity_006_project_type.sql — a project declares its Type (task 1002772).
2
+ --
3
+ -- Owner decision 2026-08-13: type is DECLARED at creation, never guessed from
4
+ -- module flags. Nullable on purpose: every existing row predates the field and
5
+ -- must not be back-filled with a guess — the hub renders an em dash for
6
+ -- "not declared", which is honest. The closed vocabulary is the owner's:
7
+ -- game / research / business / non-profit / not-sure ("not-sure" is the escape
8
+ -- hatch so nobody is forced into a wrong answer).
9
+ --
10
+ -- Additive + platform_identity_-namespaced (ADR 0083 §5): a new nullable column
11
+ -- plus a CHECK that only constrains non-null values. No existing row changes.
12
+ -- Idempotent: safe to re-run.
13
+
14
+ BEGIN;
15
+
16
+ ALTER TABLE platform_identity_projects
17
+ ADD COLUMN IF NOT EXISTS type text;
18
+
19
+ ALTER TABLE platform_identity_projects DROP CONSTRAINT IF EXISTS platform_identity_projects_type_check;
20
+ ALTER TABLE platform_identity_projects ADD CONSTRAINT platform_identity_projects_type_check
21
+ CHECK (type IS NULL OR type IN ('game', 'research', 'business', 'non-profit', 'not-sure'));
22
+
23
+ COMMIT;
@@ -146,7 +146,7 @@ async function getAccount(githubId, { pool = defaultPool } = {}) {
146
146
  // (task #1002109) calls this with a freshly generated secret; the plaintext is
147
147
  // hashed here and never stored. Upsert-on-client_id so re-provisioning rotates
148
148
  // the secret + redirect_uris without a duplicate row.
149
- async function registerClient({ clientId, secret, name, redirectUris, origin, ownerGithubId }, { pool = defaultPool } = {}) {
149
+ async function registerClient({ clientId, secret, name, redirectUris, origin, ownerGithubId, type }, { pool = defaultPool } = {}) {
150
150
  const secretHash = sha256hex(secret);
151
151
  const uris = Array.isArray(redirectUris) ? redirectUris : [];
152
152
  const { rows } = await pool.query(
@@ -167,7 +167,7 @@ async function registerClient({ clientId, secret, name, redirectUris, origin, ow
167
167
  // list the moment it federates (ADR 0141 §5). Best-effort — a catalog hiccup must
168
168
  // never fail client registration. Only when an origin is known.
169
169
  if (origin) {
170
- try { await upsertProject({ origin, name, ownerGithubId }, { pool }); }
170
+ try { await upsertProject({ origin, name, ownerGithubId, type }, { pool }); }
171
171
  catch (err) { console.error('[platform-identity] auto-catalogue failed (non-fatal):', err.message); }
172
172
  }
173
173
  return rows[0];
@@ -207,30 +207,39 @@ async function getClientByOrigin(origin, { pool = defaultPool } = {}) {
207
207
  // Projects catalog + Explore (ADR 0141 §5)
208
208
  // -------------------------------------------------------------------------
209
209
 
210
+ // The closed Type vocabulary (task 1002772, owner decision 2026-08-13): a project
211
+ // DECLARES its type at creation — 'not-sure' is the escape hatch so nobody is
212
+ // forced into a wrong answer. Mirrored by the CHECK in migration 006 (and by
213
+ // provisioning's copy on its own instances table).
214
+ const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
215
+
210
216
  // Upsert a catalog row (keyed by origin). Called when a client registers (so every
211
217
  // federated project is catalogued, unfeatured) and by the Archon create/curate route.
212
218
  // Never touches `featured` on update — that is Archon-only via setProjectFeatured.
213
- async function upsertProject({ origin, name, tagline, artUrl, ownerGithubId }, { pool = defaultPool } = {}) {
219
+ async function upsertProject({ origin, name, tagline, artUrl, ownerGithubId, type }, { pool = defaultPool } = {}) {
214
220
  const norm = String(origin || '').replace(/\/+$/, '');
215
221
  if (!norm) throw new Error('project origin required');
222
+ // off-vocabulary → null (the DB CHECK is the backstop; a declaration is never invented)
223
+ const declaredType = PROJECT_TYPES.includes(type) ? type : null;
216
224
  const { rows } = await pool.query(
217
- `INSERT INTO platform_identity_projects (origin, name, tagline, art_url, owner_github_id)
218
- VALUES ($1, $2, $3, $4, $5)
225
+ `INSERT INTO platform_identity_projects (origin, name, tagline, art_url, owner_github_id, type)
226
+ VALUES ($1, $2, $3, $4, $5, $6)
219
227
  ON CONFLICT (origin) DO UPDATE SET
220
228
  name = EXCLUDED.name,
221
229
  tagline = COALESCE(EXCLUDED.tagline, platform_identity_projects.tagline),
222
230
  art_url = COALESCE(EXCLUDED.art_url, platform_identity_projects.art_url),
223
231
  owner_github_id = COALESCE(EXCLUDED.owner_github_id, platform_identity_projects.owner_github_id),
232
+ type = COALESCE(EXCLUDED.type, platform_identity_projects.type),
224
233
  updated_at = now()
225
- RETURNING id, origin, name, tagline, art_url, owner_github_id, status, featured`,
226
- [norm, name || norm, tagline ?? null, artUrl ?? null, ownerGithubId ?? null],
234
+ RETURNING id, origin, name, tagline, art_url, owner_github_id, type, status, featured`,
235
+ [norm, name || norm, tagline ?? null, artUrl ?? null, ownerGithubId ?? null, declaredType],
227
236
  );
228
237
  return rows[0];
229
238
  }
230
239
 
231
240
  async function getProject(id, { pool = defaultPool } = {}) {
232
241
  const { rows } = await pool.query(
233
- `SELECT id, origin, name, tagline, art_url, owner_github_id, status, featured
242
+ `SELECT id, origin, name, tagline, art_url, owner_github_id, type, status, featured
234
243
  FROM platform_identity_projects WHERE id = $1`,
235
244
  [id],
236
245
  );
@@ -245,7 +254,7 @@ async function getProject(id, { pool = defaultPool } = {}) {
245
254
  // it, so as a public "last updated" it would lie.
246
255
  async function listFeaturedProjects({ pool = defaultPool } = {}) {
247
256
  const { rows } = await pool.query(
248
- `SELECT origin, name, tagline, art_url, created_at
257
+ `SELECT origin, name, tagline, art_url, type, created_at
249
258
  FROM platform_identity_projects
250
259
  WHERE featured = true AND status = 'active'
251
260
  ORDER BY updated_at DESC`,
@@ -256,7 +265,7 @@ async function listFeaturedProjects({ pool = defaultPool } = {}) {
256
265
  // The Archon curation list — every active project + its featured flag.
257
266
  async function listAllProjects({ pool = defaultPool } = {}) {
258
267
  const { rows } = await pool.query(
259
- `SELECT id, origin, name, tagline, art_url, owner_github_id, status, featured, updated_at
268
+ `SELECT id, origin, name, tagline, art_url, owner_github_id, type, status, featured, updated_at
260
269
  FROM platform_identity_projects
261
270
  WHERE status = 'active'
262
271
  ORDER BY featured DESC, updated_at DESC`,
@@ -512,6 +521,7 @@ module.exports = {
512
521
  setClientStatus,
513
522
  // projects catalog + Explore
514
523
  upsertProject,
524
+ PROJECT_TYPES,
515
525
  getProject,
516
526
  listFeaturedProjects,
517
527
  listAllProjects,
@@ -42,6 +42,7 @@ module.exports = function projectsRoutes() {
42
42
  name: { required: true, type: 'string', maxLength: 200 },
43
43
  tagline: { type: 'string', maxLength: 400 },
44
44
  art_url: { type: 'string', maxLength: 2048 },
45
+ type: { type: 'string', enum: [...pi.PROJECT_TYPES] },
45
46
  })) return;
46
47
  try {
47
48
  const project = await pi.upsertProject({
@@ -49,6 +50,7 @@ module.exports = function projectsRoutes() {
49
50
  name: req.body.name,
50
51
  tagline: req.body.tagline,
51
52
  artUrl: req.body.art_url,
53
+ type: req.body.type,
52
54
  }, { pool });
53
55
  res.status(201).json({ project });
54
56
  } catch (err) {
@@ -197,6 +197,19 @@ await ta('upsertProject upserts on normalized origin; update preserves tagline/a
197
197
  assert.doesNotMatch(c.text, /featured\s*=/);
198
198
  });
199
199
 
200
+ await ta('upsertProject carries the declared type, COALESCE-preserved like tagline (task 1002772)', async () => {
201
+ const pool = mockPool([{ rows: [{ id: '1', origin: 'https://p', type: 'game' }] }]);
202
+ await P.upsertProject({ origin: 'https://p', name: 'Proj', type: 'game' }, { pool });
203
+ const c = pool.calls[0];
204
+ assert.match(c.text, /\btype\b/, 'the declared type joins the upsert');
205
+ assert.match(c.text, /type\s*=\s*COALESCE/, 'a later typeless upsert must not erase a declaration');
206
+ assert.ok(c.params.includes('game'), 'the type value is parameterized');
207
+ // an off-vocabulary value is dropped, not thrown — the DB CHECK is the backstop
208
+ const pool2 = mockPool([{ rows: [{ id: '2' }] }]);
209
+ await P.upsertProject({ origin: 'https://q', name: 'Q', type: 'spacefaring' }, { pool: pool2 });
210
+ assert.ok(!pool2.calls[0].params.includes('spacefaring'), 'unknown types never reach the SQL');
211
+ });
212
+
200
213
  await ta('listFeaturedProjects returns a PUBLIC projection (featured+active only, no owner/status)', async () => {
201
214
  const pool = mockPool([{ rows: [{ origin: 'https://p', name: 'Proj', tagline: 't', art_url: null, created_at: '2026-08-01T00:00:00Z' }] }]);
202
215
  const out = await P.listFeaturedProjects({ pool });
@@ -210,6 +223,7 @@ await ta('listFeaturedProjects returns a PUBLIC projection (featured+active only
210
223
  assert.ok(sel.trim(), 'the projection is an explicit SELECT list');
211
224
  assert.doesNotMatch(sel, /\*/, 'no star projection on a public route');
212
225
  assert.match(sel, /created_at/, 'the LISTED date (when the row was catalogued) is public');
226
+ assert.match(sel, /\btype\b/, 'the declared type is public — the hub table reads it (task 1002772)');
213
227
  assert.doesNotMatch(c.text, /owner_github_id/, 'public projection must not leak owner id');
214
228
  // updated_at may sort (ORDER BY) but never project: it is bumped by feature
215
229
  // toggles and client re-registration, so as a public fact it would lie.
@@ -0,0 +1,24 @@
1
+ -- provisioning_007_project_type.sql — the wizard's declared project Type rides
2
+ -- the provisioning request (task 1002772).
3
+ --
4
+ -- A standalone project never federates into the hub catalog at create time, so
5
+ -- the declaration made in the create wizard has to live SOMEWHERE real the
6
+ -- moment it is filed — this column is that home. The control-plane runner
7
+ -- carries it into platform_identity_projects whenever a catalog row
8
+ -- materializes (federation), so the answer is never thrown away. Vocabulary
9
+ -- mirrors platform_identity_006_project_type.sql — both CHECKs are the
10
+ -- backstop if the two route validators ever drift.
11
+ --
12
+ -- Additive + provisioning_-namespaced (ADR 0083 §5): a new nullable column plus
13
+ -- a CHECK that only constrains non-null values. Idempotent: safe to re-run.
14
+
15
+ BEGIN;
16
+
17
+ ALTER TABLE provisioning_instances
18
+ ADD COLUMN IF NOT EXISTS type text;
19
+
20
+ ALTER TABLE provisioning_instances DROP CONSTRAINT IF EXISTS provisioning_instances_type_check;
21
+ ALTER TABLE provisioning_instances ADD CONSTRAINT provisioning_instances_type_check
22
+ CHECK (type IS NULL OR type IN ('game', 'research', 'business', 'non-profit', 'not-sure'));
23
+
24
+ COMMIT;
@@ -29,6 +29,12 @@ const HOSTING_SHAPES = ['co-tenant', 'dedicated', 'standalone'];
29
29
  // provision.js ran the greenfield path unconditionally and adopting an existing repo
30
30
  // always failed as an unrelated-history non-fast-forward (task 1002562).
31
31
  const ONBOARD_MODES = ['greenfield', 'adopt'];
32
+
33
+ // The declared project Type (task 1002772) — the wizard's answer rides the
34
+ // provisioning request so it survives until a catalog row materializes.
35
+ // Mirrors platform-identity's PROJECT_TYPES; both tables carry a matching
36
+ // CHECK, so a drift between the copies fails loudly at insert time.
37
+ const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
32
38
  // 'oauth-manifest' (task 2080) is queueable too — the GitHub-App-Manifest onboarding
33
39
  // conversion. Today the runner drains it by scanning provisioning_oauth_manifests
34
40
  // (drainOAuthManifests), not the intent queue, but it stays a valid action so the
@@ -146,6 +152,10 @@ function publicInstance(row) {
146
152
  // thing you need when diagnosing a standup (task 1002562). Coalesced for rows read
147
153
  // before provisioning_004 added the column.
148
154
  onboard_mode: row.onboard_mode || 'greenfield',
155
+ // The declared project Type (task 1002772) — the hub reads it off owned
156
+ // instances so a declaration shows before the catalog carries it. Nullable:
157
+ // rows predating provisioning_007, or a declined answer, project null.
158
+ type: row.type || null,
149
159
  // The verification overlay (task 1002696, redesign direction §3): what the
150
160
  // heartbeat last OBSERVED, which is NOT what `status` claims. null = never
151
161
  // probed — a different claim from last_verified_up=false, and the UI renders
@@ -359,22 +369,24 @@ async function listAllInstances(db, { status } = {}) {
359
369
  // re-fetches the existing row and returns { created:false } so the ROUTE can
360
370
  // decide (same owner → idempotent re-request; different owner → 409). Never
361
371
  // silently reassigns ownership.
362
- async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode }) {
372
+ async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode, type }) {
363
373
  const shape = HOSTING_SHAPES.includes(hostingShape) ? hostingShape : 'co-tenant';
364
374
  // Unknown/absent → 'greenfield', matching the column default. Falling back to the
365
375
  // SAFE mode matters: greenfield refuses to push over existing history (no --force
366
376
  // anywhere), whereas defaulting an unknown value to 'adopt' would put the layering
367
377
  // path on a repo nobody said was pre-existing.
368
378
  const mode = ONBOARD_MODES.includes(onboardMode) ? onboardMode : 'greenfield';
379
+ // Type is a declaration — off-vocabulary or absent stores null, never a guess.
380
+ const declaredType = PROJECT_TYPES.includes(type) ? type : null;
369
381
  const cost = costEstimateUsd({ hostingShape: shape, tier });
370
382
  try {
371
383
  const { rows } = await db.query(
372
384
  `INSERT INTO provisioning_instances
373
- (owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode)
374
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
385
+ (owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode, type)
386
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
375
387
  RETURNING *`,
376
388
  [ownerBuilderId, slug, targetRef ?? null, shape, domain ?? null,
377
- dbName ?? null, port ?? null, tier ?? null, cost, mode]
389
+ dbName ?? null, port ?? null, tier ?? null, cost, mode, declaredType]
378
390
  );
379
391
  return { instance: rows[0], created: true };
380
392
  } catch (e) {
@@ -642,7 +654,7 @@ async function costLedger(db, { limit = 500 } = {}) {
642
654
 
643
655
  module.exports = {
644
656
  // constants
645
- INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, INTENT_ACTIONS, TEARDOWN_FROM,
657
+ INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
646
658
  PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
647
659
  // pure
648
660
  isValidSlug, costEstimateUsd, publicInstance, normalizeHostname,
@@ -313,6 +313,8 @@ module.exports = function provisioningRoutes() {
313
313
  domain: { type: 'string', maxLength: 253 },
314
314
  tier: { type: 'string', maxLength: 40 },
315
315
  onboard_mode: { type: 'string', maxLength: 20 },
316
+ // the declared project Type (task 1002772) — optional, closed vocabulary
317
+ type: { type: 'string', enum: [...provisioning.PROJECT_TYPES] },
316
318
  })) return;
317
319
  const body = req.body || {};
318
320
  const slug = String(body.slug || '').trim().toLowerCase();
@@ -339,6 +341,7 @@ module.exports = function provisioningRoutes() {
339
341
  ownerBuilderId: req.builder.id, slug,
340
342
  targetRef: body.target_ref, hostingShape: shape,
341
343
  domain: body.domain, tier: body.tier, onboardMode,
344
+ type: body.type,
342
345
  });
343
346
  // Slug already taken by ANOTHER builder — refuse (never reassign ownership).
344
347
  if (!created && String(instance.owner_builder_id) !== String(req.builder.id)) {
@@ -373,12 +376,23 @@ module.exports = function provisioningRoutes() {
373
376
  };
374
377
  }
375
378
  }
379
+ // Same honesty for a changed Type (task 1002772): a re-request keeps the
380
+ // original declaration, and unlike domain there is no mutation route yet —
381
+ // say so rather than silently dropping the new answer.
382
+ let typeIgnored;
383
+ if (!created && body.type && body.type !== instance.type) {
384
+ typeIgnored = {
385
+ requested: body.type, current: instance.type || null,
386
+ hint: "a re-request never changes an existing instance's declared type",
387
+ };
388
+ }
376
389
  res.status(created ? 201 : 200).json({
377
390
  instance: provisioning.publicInstance(instance),
378
391
  open_intent: intent ? { action: intent.action, state: intent.state } : null,
379
392
  queued: intentCreated,
380
393
  created,
381
394
  ...(domainIgnored ? { domain_ignored: domainIgnored } : {}),
395
+ ...(typeIgnored ? { type_ignored: typeIgnored } : {}),
382
396
  });
383
397
  } catch (err) {
384
398
  console.error('[provisioning] POST /provisioning/instances', err);