@bongos/core 1.19.115 → 1.19.117
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 +39 -24
- package/clients/bongos-client/index.d.ts +2 -2
- package/docs/api/openapi.json +6 -0
- package/docs/api-reference.md +2 -2
- package/modules/platform-identity/migrations/platform_identity_006_project_type.sql +23 -0
- package/modules/platform-identity/platform-identity.js +20 -10
- package/modules/platform-identity/routes/projects.js +2 -0
- package/modules/platform-identity/tests/platform-identity.mjs +14 -0
- package/modules/provisioning/migrations/provisioning_007_project_type.sql +24 -0
- package/modules/provisioning/provisioning.js +17 -5
- package/modules/provisioning/routes/provisioning.js +14 -0
- package/modules/provisioning/tests/provisioning.mjs +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +5 -1
- package/public-landing/projects.html +150 -34
- package/scripts/gds/provision.js +3 -1
- package/scripts/gds/seed-bv1-profile-page-tasks.js +161 -0
- package/src/module-api.js +5 -1
- package/tests/cosmos_token_contrast.mjs +7 -0
- package/tests/projects_hub.mjs +42 -11
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.117",
|
|
6
|
+
"core_contract": "1.19.117",
|
|
7
|
+
"source_commit": "195266c548d657ee6924d36607d548a0e712cbf9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-14T18:51:25.372Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 302,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1238,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1561,
|
|
20
|
+
"tree_sha256": "e8cb405106a8092556a788698759b8a579439a080653ec3d5a5db66d16ec8e6b",
|
|
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": "
|
|
370
|
+
"sha256": "167b7dcd701d2a23eec6dec515778e84cb262d95c070a5a749da642e5aada701"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1337,12 +1337,12 @@
|
|
|
1337
1337
|
{
|
|
1338
1338
|
"path": "docs/api-reference.md",
|
|
1339
1339
|
"mode": "0000644",
|
|
1340
|
-
"sha256": "
|
|
1340
|
+
"sha256": "06954a3b30e235ef61a316d4f7414a2eaf2c0f2d3fe1063698e800f3655d1675"
|
|
1341
1341
|
},
|
|
1342
1342
|
{
|
|
1343
1343
|
"path": "docs/api/openapi.json",
|
|
1344
1344
|
"mode": "0000644",
|
|
1345
|
-
"sha256": "
|
|
1345
|
+
"sha256": "3910ea26d65b5afc883d4b3488d605ee5786bda41ef86e280b5a180d6f6fa586"
|
|
1346
1346
|
},
|
|
1347
1347
|
{
|
|
1348
1348
|
"path": "docs/architecture.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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
4165
|
+
"sha256": "2d570f38602e9cb4ab3417d45f21d61ffb2b9cef5ba9ff9354350a95af8f6190"
|
|
4156
4166
|
},
|
|
4157
4167
|
{
|
|
4158
4168
|
"path": "package.json",
|
|
4159
4169
|
"mode": "0000644",
|
|
4160
|
-
"sha256": "
|
|
4170
|
+
"sha256": "b9a96ae403012d889fbae5d2bd481f76fa1bc2221e5e8c8a6bd49e7494759ee9"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
5080
|
+
"sha256": "a10501b65207bbd72f9fc039d4cf1ef0be7f85dc832e8e95e1b26aa966e507b8"
|
|
5071
5081
|
},
|
|
5072
5082
|
{
|
|
5073
5083
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -5204,6 +5214,11 @@
|
|
|
5204
5214
|
"mode": "0000644",
|
|
5205
5215
|
"sha256": "5de4203d6a764c8aec374c7802f69f659816d2c0d75bbe70144f7dfb56df2e39"
|
|
5206
5216
|
},
|
|
5217
|
+
{
|
|
5218
|
+
"path": "scripts/gds/seed-bv1-profile-page-tasks.js",
|
|
5219
|
+
"mode": "0000644",
|
|
5220
|
+
"sha256": "68f6edfcea2eaaf1262dc125d538f2b3806c58864839d6e34c3135235f29ed4b"
|
|
5221
|
+
},
|
|
5207
5222
|
{
|
|
5208
5223
|
"path": "scripts/gds/seed-bv1-profiles-tasks.js",
|
|
5209
5224
|
"mode": "0000644",
|
|
@@ -5727,7 +5742,7 @@
|
|
|
5727
5742
|
{
|
|
5728
5743
|
"path": "src/module-api.js",
|
|
5729
5744
|
"mode": "0000644",
|
|
5730
|
-
"sha256": "
|
|
5745
|
+
"sha256": "1c1a8e95f442958ff1476c19e6b68fd2af5475e365e3d5203656bfb2c63f3fa4"
|
|
5731
5746
|
},
|
|
5732
5747
|
{
|
|
5733
5748
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6232,7 +6247,7 @@
|
|
|
6232
6247
|
{
|
|
6233
6248
|
"path": "tests/cosmos_token_contrast.mjs",
|
|
6234
6249
|
"mode": "0000644",
|
|
6235
|
-
"sha256": "
|
|
6250
|
+
"sha256": "257acb76f7b7e8bb925b005e8a7bad96d712f0a216201aacee11762a2a2ff328"
|
|
6236
6251
|
},
|
|
6237
6252
|
{
|
|
6238
6253
|
"path": "tests/cost.mjs",
|
|
@@ -7252,7 +7267,7 @@
|
|
|
7252
7267
|
{
|
|
7253
7268
|
"path": "tests/projects_hub.mjs",
|
|
7254
7269
|
"mode": "0000644",
|
|
7255
|
-
"sha256": "
|
|
7270
|
+
"sha256": "ee5c944f9e3bd3d618c977e76d4e2ba3e663ce59bf7833f9427a0191717ba57d"
|
|
7256
7271
|
},
|
|
7257
7272
|
{
|
|
7258
7273
|
"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 }
|
package/docs/api/openapi.json
CHANGED
|
@@ -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": [
|
package/docs/api-reference.md
CHANGED
|
@@ -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. |
|
|
@@ -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);
|
|
@@ -228,5 +228,12 @@ await ta('claimNextOAuthManifest flips code_received→converting, gated on an A
|
|
|
228
228
|
assert.match(sql, /m\.status = 'code_received' AND i\.status = 'active'/);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
t('publicInstance projects the declared type — nullable, never silently dropped (task 1002772)', () => {
|
|
232
|
+
const p = P.publicInstance({ id: 1, slug: 'x', status: 'active', type: 'game' });
|
|
233
|
+
assert.equal(p.type, 'game', 'the allow-list projection must carry the declaration');
|
|
234
|
+
const q = P.publicInstance({ id: 2, slug: 'y', status: 'active' });
|
|
235
|
+
assert.equal(q.type, null, 'a row predating provisioning_007 projects null, not undefined');
|
|
236
|
+
});
|
|
237
|
+
|
|
231
238
|
console.log(`\nprovisioning module: ${passed} passed, ${failed} failed`);
|
|
232
239
|
process.exit(failed ? 1 : 0);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.117",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.117",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.117",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -581,7 +581,11 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
581
581
|
.tbl .colStatus{width:1%;white-space:nowrap;}
|
|
582
582
|
.tbl .colGo{width:1%;white-space:nowrap;padding-left:8px;}
|
|
583
583
|
.tbl .colName{width:auto;}
|
|
584
|
-
|
|
584
|
+
.tbl .colType{width:1%;white-space:nowrap;}
|
|
585
|
+
/* the declared-type tag (task 1002772) — its own vocabulary: not the relationship
|
|
586
|
+
.pill, not the liveness .chip. Two different facts never wear the same chrome. */
|
|
587
|
+
.typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--line);border-radius:999px;padding:2px 9px;white-space:nowrap;}
|
|
588
|
+
@media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}}
|
|
585
589
|
|
|
586
590
|
/* ── the living sky (task 1002762): camera, spotlight, hover card ──
|
|
587
591
|
The camera is ONE transform on .skyCam; orbs carry only their own physics
|