@bongos/core 1.19.295 → 1.19.296
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 +27 -22
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +52 -3
- package/docs/api-reference.md +3 -2
- package/docs/file-map.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/ideas/inbox.js +88 -0
- package/modules/ideas/routes/inbox.js +37 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/spark.js +57 -5
- package/src/module-api.js +3 -1
- package/tests/idea_spark_queue.mjs +225 -0
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.296",
|
|
6
|
+
"core_contract": "1.19.296",
|
|
7
|
+
"source_commit": "a0c264baf117184273d9a83f7c158844920f1d48",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-22T18:
|
|
9
|
+
"built_at": "2026-08-22T18:35:34.578Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 321,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1398,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1741,
|
|
20
|
+
"tree_sha256": "a34c536a2753b233ccd552b62e7fcfbe3768304013f4130c641a9bb51d7e51d5",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -347,12 +347,12 @@
|
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/README.md",
|
|
349
349
|
"mode": "0000644",
|
|
350
|
-
"sha256": "
|
|
350
|
+
"sha256": "cc15a1c79d2dd761b8b50190f72d69b2ccec0a56bdd812a82913213a4b811da6"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "b2add6cdda6bf67a34a33864f5c6c0a1bdaf7f3ac21314800d222b5f4be55eb5"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -362,17 +362,17 @@
|
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/index.cjs",
|
|
364
364
|
"mode": "0000644",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "b6f803fa3594804e6781eab762eb91241c9146c1e3aa09a8883dd8015a55dc3b"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "ee350739d64eb2ddce22fa0378ece2eebc363eae96c7755f087363b87e9581fe"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "9f4db5e5579a3e401a9d56ee473436b8c6cd00e82f209de784c321131f6e420d"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1412,12 +1412,12 @@
|
|
|
1412
1412
|
{
|
|
1413
1413
|
"path": "docs/api-reference.md",
|
|
1414
1414
|
"mode": "0000644",
|
|
1415
|
-
"sha256": "
|
|
1415
|
+
"sha256": "f7102579bfe64bb798e9ebc826849857218da439113c6f7b049cfc9a580f863f"
|
|
1416
1416
|
},
|
|
1417
1417
|
{
|
|
1418
1418
|
"path": "docs/api/openapi.json",
|
|
1419
1419
|
"mode": "0000644",
|
|
1420
|
-
"sha256": "
|
|
1420
|
+
"sha256": "a3d866c17cdb60660fb7603de26faccb0fd0fa345e47c26d9703e2fc1fdbfd40"
|
|
1421
1421
|
},
|
|
1422
1422
|
{
|
|
1423
1423
|
"path": "docs/architecture.md",
|
|
@@ -1552,7 +1552,7 @@
|
|
|
1552
1552
|
{
|
|
1553
1553
|
"path": "docs/file-map.md",
|
|
1554
1554
|
"mode": "0000644",
|
|
1555
|
-
"sha256": "
|
|
1555
|
+
"sha256": "9a2f67ab5989d068060bfe336d17ae88a33484fa8cbb5a92f23534b1c6de6289"
|
|
1556
1556
|
},
|
|
1557
1557
|
{
|
|
1558
1558
|
"path": "docs/handoff-template.md",
|
|
@@ -2807,7 +2807,7 @@
|
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
2809
2809
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2810
|
+
"sha256": "b6f803fa3594804e6781eab762eb91241c9146c1e3aa09a8883dd8015a55dc3b"
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
2813
2813
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -3707,7 +3707,7 @@
|
|
|
3707
3707
|
{
|
|
3708
3708
|
"path": "modules/ideas/inbox.js",
|
|
3709
3709
|
"mode": "0000644",
|
|
3710
|
-
"sha256": "
|
|
3710
|
+
"sha256": "33a25e6bd24ee0952c03e2aeab3e1edb68f8cf84989ee04206c9efdb7d202fa7"
|
|
3711
3711
|
},
|
|
3712
3712
|
{
|
|
3713
3713
|
"path": "modules/ideas/migrations/ideas_001_grades.sql",
|
|
@@ -3742,7 +3742,7 @@
|
|
|
3742
3742
|
{
|
|
3743
3743
|
"path": "modules/ideas/routes/inbox.js",
|
|
3744
3744
|
"mode": "0000644",
|
|
3745
|
-
"sha256": "
|
|
3745
|
+
"sha256": "7de2735020ddd526ded8ca252fbfba157ee5eb174895660d37ac713b9d0d6fd2"
|
|
3746
3746
|
},
|
|
3747
3747
|
{
|
|
3748
3748
|
"path": "modules/ideas/routing.js",
|
|
@@ -4537,12 +4537,12 @@
|
|
|
4537
4537
|
{
|
|
4538
4538
|
"path": "package-lock.json",
|
|
4539
4539
|
"mode": "0000644",
|
|
4540
|
-
"sha256": "
|
|
4540
|
+
"sha256": "b937444a6338c17ae2692d47f126d612f87c2429783a3d9a95e4c5d6076f3f5b"
|
|
4541
4541
|
},
|
|
4542
4542
|
{
|
|
4543
4543
|
"path": "package.json",
|
|
4544
4544
|
"mode": "0000644",
|
|
4545
|
-
"sha256": "
|
|
4545
|
+
"sha256": "ce72a3e5a70ae07f431f223dccf12d2c896474bcb1f0d62724aea22206b23dde"
|
|
4546
4546
|
},
|
|
4547
4547
|
{
|
|
4548
4548
|
"path": "public-docs/index.html",
|
|
@@ -5782,7 +5782,7 @@
|
|
|
5782
5782
|
{
|
|
5783
5783
|
"path": "scripts/gds/spark.js",
|
|
5784
5784
|
"mode": "0000755",
|
|
5785
|
-
"sha256": "
|
|
5785
|
+
"sha256": "61ad614ec3fb8cbfc98d815629fd3ccafd179a8f2c058539103c9bf4e23e9504"
|
|
5786
5786
|
},
|
|
5787
5787
|
{
|
|
5788
5788
|
"path": "scripts/gds/start.js",
|
|
@@ -6197,7 +6197,7 @@
|
|
|
6197
6197
|
{
|
|
6198
6198
|
"path": "src/module-api.js",
|
|
6199
6199
|
"mode": "0000644",
|
|
6200
|
-
"sha256": "
|
|
6200
|
+
"sha256": "e5f489e0d2b9cc6d9c73cc853132a8a7a7e3fbcd8ea2325284d58143d4020a05"
|
|
6201
6201
|
},
|
|
6202
6202
|
{
|
|
6203
6203
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7519,6 +7519,11 @@
|
|
|
7519
7519
|
"mode": "0000644",
|
|
7520
7520
|
"sha256": "fb5af0aa1b34116114a1c734c548cafaa52eefda57bd145c0008995bc69addee"
|
|
7521
7521
|
},
|
|
7522
|
+
{
|
|
7523
|
+
"path": "tests/idea_spark_queue.mjs",
|
|
7524
|
+
"mode": "0000644",
|
|
7525
|
+
"sha256": "6bea289f0473d07e21acd0b45b50511f061d8d1f0d09b7437a909081ea83344a"
|
|
7526
|
+
},
|
|
7522
7527
|
{
|
|
7523
7528
|
"path": "tests/idea_task_lineage.mjs",
|
|
7524
7529
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 297 operations across 47 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -415,6 +415,8 @@ function createClient(opts = {}) {
|
|
|
415
415
|
postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
|
|
416
416
|
// GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
|
|
417
417
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
418
|
+
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
419
|
+
getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
|
|
418
420
|
},
|
|
419
421
|
"instance": {
|
|
420
422
|
// GET /instance — rank: public — GET /instance
|
|
@@ -414,6 +414,8 @@ function createClient(opts = {}) {
|
|
|
414
414
|
postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
|
|
415
415
|
// GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
|
|
416
416
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
417
|
+
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
418
|
+
getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
|
|
417
419
|
},
|
|
418
420
|
"instance": {
|
|
419
421
|
// GET /instance — rank: public — GET /instance
|
|
@@ -50,6 +50,7 @@ export interface GetHealthzResponse { ok: boolean; auth_configured: unknown }
|
|
|
50
50
|
export interface GetInboxAwaitingNodResponse { awaiting: unknown; count: unknown }
|
|
51
51
|
export interface GetInboxIdResponse { idea: unknown }
|
|
52
52
|
export interface GetInboxResponse { ideas: unknown; open_count: unknown }
|
|
53
|
+
export interface GetInboxSparksResponse { sparks: unknown; count: unknown }
|
|
53
54
|
export interface GetLeaderboardResponse { leaderboard: unknown; page: unknown }
|
|
54
55
|
export interface GetLearningsResponse { learnings: unknown }
|
|
55
56
|
export interface GetMeAchievementsResponse { unlocked: unknown }
|
|
@@ -677,6 +678,8 @@ export interface BongosClient {
|
|
|
677
678
|
postInboxIdSpark(args?: RequestArgs): Promise<PostInboxIdSparkResponse>;
|
|
678
679
|
/** GET /inbox/awaiting-nod — rank: metic+archon */
|
|
679
680
|
getInboxAwaitingNod(args?: RequestArgs): Promise<GetInboxAwaitingNodResponse>;
|
|
681
|
+
/** GET /inbox/sparks — rank: any-builder */
|
|
682
|
+
getInboxSparks(args?: RequestArgs): Promise<GetInboxSparksResponse>;
|
|
680
683
|
};
|
|
681
684
|
"instance": {
|
|
682
685
|
/** GET /instance — rank: public */
|
|
@@ -411,6 +411,8 @@ export function createClient(opts = {}) {
|
|
|
411
411
|
postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
|
|
412
412
|
// GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
|
|
413
413
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
414
|
+
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
415
|
+
getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
|
|
414
416
|
},
|
|
415
417
|
"instance": {
|
|
416
418
|
// GET /instance — rank: public — GET /instance
|
package/docs/api/openapi.json
CHANGED
|
@@ -6307,6 +6307,44 @@
|
|
|
6307
6307
|
]
|
|
6308
6308
|
}
|
|
6309
6309
|
},
|
|
6310
|
+
"/inbox/sparks": {
|
|
6311
|
+
"get": {
|
|
6312
|
+
"operationId": "get_inbox_sparks",
|
|
6313
|
+
"tags": [
|
|
6314
|
+
"inbox"
|
|
6315
|
+
],
|
|
6316
|
+
"summary": "GET /inbox/sparks",
|
|
6317
|
+
"description": "GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062). Big Idea Sparks awaiting development: the ideator craft's pickup surface, and the first place one craft sources work from another craft's output. READABLE BY ANY BUILDER, by design and by the done-when. A pickup surface that only trusted builders could read would defeat its own purpose — the whole point is that someone OTHER than the author comes along and develops the thought. The author's consent is already recorded (the R144 mark is what put the row here), so no further gate belongs on the read. Contrast /inbox/awaiting-nod directly above, which IS gated: that queue exists to be ACTED on by a triager, this one to be BROWSED by anyone. DECLARED BEFORE `GET /inbox/:id` DELIBERATELY — and this ordering is load-bearing, not stylistic. Express matches in declaration order, so with the routes the other way round `/inbox/sparks` binds `:id = 'sparks'` and parseId turns the whole queue into a 400 bad_id. Same reason /inbox/awaiting-nod sits above it. tests/idea_spark_queue.mjs pins the relative order so a future insertion cannot quietly re-shadow it. rank: any-builder — a pickup surface is public to builders (the R144 mark is the consent). Deliberately NOT pinned in route-rank-check: a read with no gate to protect has no downgrade to catch, and pinning it would only assert that a read stays open.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
6318
|
+
"x-rank": "any-builder",
|
|
6319
|
+
"x-source": "modules/ideas/routes/inbox.js",
|
|
6320
|
+
"responses": {
|
|
6321
|
+
"200": {
|
|
6322
|
+
"description": "Success.",
|
|
6323
|
+
"content": {
|
|
6324
|
+
"application/json": {
|
|
6325
|
+
"schema": {
|
|
6326
|
+
"$ref": "#/components/schemas/GetInboxSparksResponse"
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
},
|
|
6331
|
+
"400": {
|
|
6332
|
+
"$ref": "#/components/responses/BadRequest"
|
|
6333
|
+
},
|
|
6334
|
+
"401": {
|
|
6335
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
6336
|
+
},
|
|
6337
|
+
"403": {
|
|
6338
|
+
"$ref": "#/components/responses/Forbidden"
|
|
6339
|
+
}
|
|
6340
|
+
},
|
|
6341
|
+
"security": [
|
|
6342
|
+
{
|
|
6343
|
+
"builderSession": []
|
|
6344
|
+
}
|
|
6345
|
+
]
|
|
6346
|
+
}
|
|
6347
|
+
},
|
|
6310
6348
|
"/inbox/{id}": {
|
|
6311
6349
|
"get": {
|
|
6312
6350
|
"operationId": "get_inbox_id",
|
|
@@ -14028,6 +14066,17 @@
|
|
|
14028
14066
|
"open_count"
|
|
14029
14067
|
]
|
|
14030
14068
|
},
|
|
14069
|
+
"GetInboxSparksResponse": {
|
|
14070
|
+
"type": "object",
|
|
14071
|
+
"properties": {
|
|
14072
|
+
"sparks": {},
|
|
14073
|
+
"count": {}
|
|
14074
|
+
},
|
|
14075
|
+
"required": [
|
|
14076
|
+
"sparks",
|
|
14077
|
+
"count"
|
|
14078
|
+
]
|
|
14079
|
+
},
|
|
14031
14080
|
"GetLeaderboardResponse": {
|
|
14032
14081
|
"type": "object",
|
|
14033
14082
|
"properties": {
|
|
@@ -18707,9 +18756,9 @@
|
|
|
18707
18756
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
18708
18757
|
}
|
|
18709
18758
|
},
|
|
18710
|
-
"x-endpoint-count":
|
|
18711
|
-
"x-schema-count":
|
|
18759
|
+
"x-endpoint-count": 297,
|
|
18760
|
+
"x-schema-count": 330,
|
|
18712
18761
|
"x-undocumented-bodies": 8,
|
|
18713
|
-
"x-response-schemas":
|
|
18762
|
+
"x-response-schemas": 221,
|
|
18714
18763
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
18715
18764
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 297 endpoints across 47 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -258,7 +258,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
258
258
|
|---|---|---|---|---|
|
|
259
259
|
| GET | `/api/bongos/healthz` | `public` | — | liveness probe; status.example.com hits this anonymously. |
|
|
260
260
|
|
|
261
|
-
## `inbox` (
|
|
261
|
+
## `inbox` (11)
|
|
262
262
|
|
|
263
263
|
| Method | Path | Rank | Body | Description |
|
|
264
264
|
|---|---|---|---|---|
|
|
@@ -272,6 +272,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
272
272
|
| POST | `/api/bongos/inbox/:id/spark` | `any-builder` | — | Mark a Quick Idea as a Big Idea Spark — "I have the spark but not the full picture, someone develop this". |
|
|
273
273
|
| DELETE | `/api/bongos/inbox/:id/spark` | `any-builder` | — | Withdraw the Big Idea Spark mark — the reversibility half of R144. |
|
|
274
274
|
| GET | `/api/bongos/inbox/awaiting-nod` | `metic+archon` | — | GET /inbox/awaiting-nod — the NOD QUEUE (task 1003066, BV1.R05). |
|
|
275
|
+
| GET | `/api/bongos/inbox/sparks` | `any-builder` | — | GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062). |
|
|
275
276
|
|
|
276
277
|
## `instance` (1)
|
|
277
278
|
|
package/docs/file-map.md
CHANGED
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
│ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
|
|
176
176
|
│ ├── recall.js ← `/recall` CLI: two-call progressive-disclosure search (summary → `--expand <id>`) over Bongos retrieval layer (ADR 0060); `node scripts/gds/recall.js <query>`
|
|
177
177
|
│ ├── capture.js ← file an idea_inbox row from the CLI (the ideas.capture kernel port); backs the "capture a tangent" flow across skills
|
|
178
|
-
│ ├── spark.js ← (
|
|
178
|
+
│ ├── spark.js ← (tasks 1002920 / R144 + 1002921 / R145) mark one of your OWN quick ideas as a Big Idea Spark — "someone develop this"; `--undo` withdraws it; `--list` is the SPARK QUEUE (what awaits development, readable by any builder). Author-only server-side for the mark (it is the consent); its own command because sparking acts on an EXISTING idea, which is why capture.js refuses grade='spark'
|
|
179
179
|
│ ├── worktree.js ← (task 1002809) junction-safe worktree add/remove/list — outside-repo worktrees get a node_modules junction via node (no shell quoting), removed BEFORE `git worktree remove`
|
|
180
180
|
│ ├── learning-capture.js ← file a `learnings` row (DB-backed short-form rule capture, replacing hand-edited recipe files for a single gotcha)
|
|
181
181
|
│ ├── docs-entropy.js ← ([#…] docs-entropy) markdown-corpus linter: broken internal links + cross-file duplicate-prose detector across every tracked `.md`; `--verbose` / `--file-ideas`
|
|
@@ -414,6 +414,8 @@ function createClient(opts = {}) {
|
|
|
414
414
|
postInboxIdSpark: (args) => request("POST", "/inbox/{id}/spark", { hasBody: true }, args),
|
|
415
415
|
// GET /inbox/awaiting-nod — rank: metic+archon — GET /inbox/awaiting-nod
|
|
416
416
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
417
|
+
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
418
|
+
getInboxSparks: (args) => request("GET", "/inbox/sparks", { hasBody: false }, args),
|
|
417
419
|
},
|
|
418
420
|
"instance": {
|
|
419
421
|
// GET /instance — rank: public — GET /instance
|
package/modules/ideas/inbox.js
CHANGED
|
@@ -782,6 +782,91 @@ async function countRoutedAwaitingNod(deps = {}) {
|
|
|
782
782
|
return rows[0] ? rows[0].n : 0;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
// -------------------------------------------------------------------------
|
|
786
|
+
// The spark queue (task 1002921 / BV1.R145, goal 1000062)
|
|
787
|
+
//
|
|
788
|
+
// "What Big Idea Sparks are awaiting development?" — the ideator craft's pickup
|
|
789
|
+
// surface, and the first place in this codebase where one craft sources work
|
|
790
|
+
// from another craft's output.
|
|
791
|
+
//
|
|
792
|
+
// THE PREDICATE IS grade='spark' AND status='open', AND THAT IS THE WHOLE
|
|
793
|
+
// QUEUE. There is deliberately no `developed` flag and no separate queue table:
|
|
794
|
+
// R146 develops a spark into a Full Idea IN PLACE on the same idea_inbox row
|
|
795
|
+
// (same id, same captured_by), which flips its grade spark→full — so a developed
|
|
796
|
+
// spark leaves this queue as a CONSEQUENCE of being developed, not because
|
|
797
|
+
// anything remembered to remove it. A second source of truth for "still
|
|
798
|
+
// waiting" is a thing that can disagree with the grade; this cannot.
|
|
799
|
+
//
|
|
800
|
+
// AN INNER JOIN, WHICH INVERTS THIS MODULE'S USUAL RULE ON PURPOSE. Every other
|
|
801
|
+
// idea read LEFT JOINs ideas_grades and COALESCEs to 'quick', because an
|
|
802
|
+
// un-graded idea must still appear. Here the opposite is correct and safe:
|
|
803
|
+
// grade='spark' can never be implicit — absence of a row means quick — so a
|
|
804
|
+
// spark ALWAYS has a real row, and starting from ideas_grades lets
|
|
805
|
+
// ideas_grades_grade_score_idx (ideas_001, written for this very read) drive the
|
|
806
|
+
// scan instead of filtering a full idea_inbox pass afterwards.
|
|
807
|
+
//
|
|
808
|
+
// ORDERING — "newest-or-best first", and only the newest half is real. A spark
|
|
809
|
+
// carries no completeness score by construction (it has no template fields to
|
|
810
|
+
// score), so there is no "best" signal to sort by: upvotes live on builders as
|
|
811
|
+
// karma, not per-idea, and inventing a quality metric for an undeveloped thought
|
|
812
|
+
// is exactly the guess this queue should not be making. captured_at DESC, id
|
|
813
|
+
// DESC as a stable tiebreak for same-timestamp rows. When a real per-idea
|
|
814
|
+
// signal exists, this ORDER BY is the one line that changes.
|
|
815
|
+
//
|
|
816
|
+
// The skill-friction visibility gate (V3.R70) is applied here too, on the same
|
|
817
|
+
// title-prefix rule listOpenIdeas uses. A routine-filed friction row is unlikely
|
|
818
|
+
// to ever be sparked (marking requires an author), but the two idea listings
|
|
819
|
+
// agreeing by construction beats them agreeing by accident.
|
|
820
|
+
function sparkQueueWhere(viewerRank, params) {
|
|
821
|
+
const where = [`g.grade = 'spark'`, `i.status = 'open'`];
|
|
822
|
+
if (!rankCanSeeSkillFriction(viewerRank)) {
|
|
823
|
+
params.push(`${SKILL_FRICTION_TAG}%`);
|
|
824
|
+
where.push(`i.title NOT LIKE $${params.length}`);
|
|
825
|
+
}
|
|
826
|
+
return where;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function listSparkQueue({ limit = 50, viewerRank = null } = {}, deps = {}) {
|
|
830
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
831
|
+
const params = [];
|
|
832
|
+
const where = sparkQueueWhere(viewerRank, params);
|
|
833
|
+
params.push(limit);
|
|
834
|
+
const { rows } = await activePool.query(
|
|
835
|
+
// `b.github_login` because attribution is the POINT of this surface: an
|
|
836
|
+
// ideator picking work up should see whose thought they are developing, and
|
|
837
|
+
// R146 has to preserve that name. LEFT JOIN — a builder row can be missing.
|
|
838
|
+
`SELECT ${IDEA_COLS_Q}, ${GRADE_COLS_Q},
|
|
839
|
+
b.github_login AS filed_by_login
|
|
840
|
+
FROM ideas_grades g
|
|
841
|
+
JOIN idea_inbox i ON i.id = g.idea_id
|
|
842
|
+
LEFT JOIN builders b ON b.id = i.captured_by
|
|
843
|
+
WHERE ${where.join(' AND ')}
|
|
844
|
+
ORDER BY i.captured_at DESC, i.id DESC
|
|
845
|
+
LIMIT $${params.length}`,
|
|
846
|
+
params
|
|
847
|
+
);
|
|
848
|
+
return rows;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// The count behind the queue's pill — the SAME predicate as the list (the
|
|
852
|
+
// countRoutedAwaitingNod / countOpenIdeas precedent), so the number a viewer is
|
|
853
|
+
// shown can never disagree with the rows they can actually see. It deliberately
|
|
854
|
+
// takes viewerRank too: a count that ignored the visibility gate would tell a
|
|
855
|
+
// Xenos there are sparks they cannot find.
|
|
856
|
+
async function countSparkQueue({ viewerRank = null } = {}, deps = {}) {
|
|
857
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
858
|
+
const params = [];
|
|
859
|
+
const where = sparkQueueWhere(viewerRank, params);
|
|
860
|
+
const { rows } = await activePool.query(
|
|
861
|
+
`SELECT COUNT(*)::int AS n
|
|
862
|
+
FROM ideas_grades g
|
|
863
|
+
JOIN idea_inbox i ON i.id = g.idea_id
|
|
864
|
+
WHERE ${where.join(' AND ')}`,
|
|
865
|
+
params
|
|
866
|
+
);
|
|
867
|
+
return rows[0] ? rows[0].n : 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
785
870
|
// -------------------------------------------------------------------------
|
|
786
871
|
// Grade writes (task 1002916 / BV1.R140)
|
|
787
872
|
//
|
|
@@ -1109,6 +1194,9 @@ module.exports = {
|
|
|
1109
1194
|
// the nod queue (task 1003066, BV1.R05)
|
|
1110
1195
|
listRoutedAwaitingNod,
|
|
1111
1196
|
countRoutedAwaitingNod,
|
|
1197
|
+
// the spark queue (task 1002921, BV1.R145) — sparks awaiting development
|
|
1198
|
+
listSparkQueue,
|
|
1199
|
+
countSparkQueue,
|
|
1112
1200
|
// criterion-proposal ratification (V3.R31 / #252)
|
|
1113
1201
|
ratifyCriterionProposal,
|
|
1114
1202
|
parseProposalBody,
|
|
@@ -312,6 +312,43 @@ module.exports = function buildInboxRouter() {
|
|
|
312
312
|
}
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
|
|
316
|
+
// GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062).
|
|
317
|
+
//
|
|
318
|
+
// Big Idea Sparks awaiting development: the ideator craft's pickup surface,
|
|
319
|
+
// and the first place one craft sources work from another craft's output.
|
|
320
|
+
//
|
|
321
|
+
// READABLE BY ANY BUILDER, by design and by the done-when. A pickup surface
|
|
322
|
+
// that only trusted builders could read would defeat its own purpose — the
|
|
323
|
+
// whole point is that someone OTHER than the author comes along and develops
|
|
324
|
+
// the thought. The author's consent is already recorded (the R144 mark is what
|
|
325
|
+
// put the row here), so no further gate belongs on the read. Contrast
|
|
326
|
+
// /inbox/awaiting-nod directly above, which IS gated: that queue exists to be
|
|
327
|
+
// ACTED on by a triager, this one to be BROWSED by anyone.
|
|
328
|
+
//
|
|
329
|
+
// DECLARED BEFORE `GET /inbox/:id` DELIBERATELY — and this ordering is
|
|
330
|
+
// load-bearing, not stylistic. Express matches in declaration order, so with
|
|
331
|
+
// the routes the other way round `/inbox/sparks` binds `:id = 'sparks'` and
|
|
332
|
+
// parseId turns the whole queue into a 400 bad_id. Same reason
|
|
333
|
+
// /inbox/awaiting-nod sits above it. tests/idea_spark_queue.mjs pins the
|
|
334
|
+
// relative order so a future insertion cannot quietly re-shadow it.
|
|
335
|
+
//
|
|
336
|
+
// rank: any-builder — a pickup surface is public to builders (the R144 mark is
|
|
337
|
+
// the consent). Deliberately NOT pinned in route-rank-check: a read with no
|
|
338
|
+
// gate to protect has no downgrade to catch, and pinning it would only assert
|
|
339
|
+
// that a read stays open.
|
|
340
|
+
router.get('/inbox/sparks', api.requireBuilder, api.asyncHandler('GET /inbox/sparks', async (req, res) => {
|
|
341
|
+
const limitRaw = Number(req.query.limit);
|
|
342
|
+
const limit = Number.isInteger(limitRaw) && limitRaw > 0 && limitRaw <= 100 ? limitRaw : 50;
|
|
343
|
+
// Both halves take the viewer's rank so the pill can never claim sparks the
|
|
344
|
+
// list will not show (the countRoutedAwaitingNod precedent).
|
|
345
|
+
const viewerRank = req.builder && req.builder.rank;
|
|
346
|
+
const [sparks, count] = await Promise.all([
|
|
347
|
+
inbox.listSparkQueue({ limit, viewerRank }),
|
|
348
|
+
inbox.countSparkQueue({ viewerRank }),
|
|
349
|
+
]);
|
|
350
|
+
return res.json({ sparks, count });
|
|
351
|
+
}, { errorCode: 'spark_queue_failed' }));
|
|
315
352
|
// rank: any-builder — single idea read for the "#NNN" deep-link resolver
|
|
316
353
|
// (#1047). Mirrors GET /blockers/:id. The hall resolves /#/idea/N by id, but
|
|
317
354
|
// the /inbox LIST returns only the open-first-50, so a closed/older idea ref
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.296",
|
|
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.296",
|
|
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.296",
|
|
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",
|
package/scripts/gds/spark.js
CHANGED
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
// grade='spark' for the same reason). The builders-hall control is R154; this is
|
|
14
14
|
// the terminal path, and the two hit the same route.
|
|
15
15
|
//
|
|
16
|
+
// --list is the other side of the same idea: the SPARK QUEUE (task 1002921 /
|
|
17
|
+
// BV1.R145) — what other builders have marked as needing development. It lives
|
|
18
|
+
// here rather than in its own script because "mark it" and "see what is marked"
|
|
19
|
+
// are one mental model, and a queue nobody can read is not a pickup surface. The
|
|
20
|
+
// queue is readable by ANY builder: the author's mark is already the consent.
|
|
21
|
+
//
|
|
16
22
|
// Usage:
|
|
23
|
+
// node scripts/gds/spark.js --list # the spark queue: what awaits development
|
|
24
|
+
// node scripts/gds/spark.js --list --limit 10
|
|
17
25
|
// node scripts/gds/spark.js 123 # mark idea 123 as a spark
|
|
18
26
|
// node scripts/gds/spark.js 123 --undo # withdraw the mark (back to quick)
|
|
19
27
|
//
|
|
@@ -22,7 +30,7 @@
|
|
|
22
30
|
// 1 api / network error, or the server refused (not yours / not a quick idea)
|
|
23
31
|
// 2 bad invocation
|
|
24
32
|
|
|
25
|
-
const { cliClient, requireSession, hasFlag } = require('./cli-lib');
|
|
33
|
+
const { cliClient, requireSession, hasFlag, arg, hallBase } = require('./cli-lib');
|
|
26
34
|
|
|
27
35
|
// The server's refusals, in the ideator's own words. Anything unmapped falls
|
|
28
36
|
// through to the raw envelope — a new server-side rule should be visible, not
|
|
@@ -56,15 +64,58 @@ function explain(status, data) {
|
|
|
56
64
|
return `spark failed (${status}): ${rendered}`;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
// Render the queue. Kept separate from the fetch so it is testable without a
|
|
68
|
+
// session, and so the empty case reads as an answer ("nothing is waiting")
|
|
69
|
+
// rather than as a blank screen that looks like a failure.
|
|
70
|
+
function renderQueue({ sparks, count }) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
if (!sparks || sparks.length === 0) {
|
|
73
|
+
lines.push('No sparks are awaiting development.');
|
|
74
|
+
lines.push('A spark is a quick idea whose AUTHOR marked it as needing development —');
|
|
75
|
+
lines.push('mark one of your own with: node scripts/gds/spark.js <idea-id>');
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
const shown = sparks.length;
|
|
79
|
+
const total = Number.isInteger(count) ? count : shown;
|
|
80
|
+
lines.push(`${total} spark${total === 1 ? '' : 's'} awaiting development${shown < total ? ` (showing ${shown})` : ''}:`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
for (const s of sparks) {
|
|
83
|
+
// The hall link, never a bare "#NNN" — that autolinks to a GitHub issue and
|
|
84
|
+
// 404s, since our idea ids are not GitHub's (the CLAUDE.md refs rule).
|
|
85
|
+
lines.push(` ${hallBase()}/#/idea/${s.id}`);
|
|
86
|
+
lines.push(` ${s.title}`);
|
|
87
|
+
const who = s.filed_by_login ? `@${s.filed_by_login}` : 'an unnamed builder';
|
|
88
|
+
lines.push(` had by ${who}${s.captured_at ? ` · ${String(s.captured_at).slice(0, 10)}` : ''}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
lines.push('Develop one into a Full Idea and the author keeps the attribution.');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function listQueue() {
|
|
96
|
+
const api = await cliClient();
|
|
97
|
+
const limitRaw = arg('--limit');
|
|
98
|
+
const limit = limitRaw != null && /^[0-9]+$/.test(limitRaw) ? Number(limitRaw) : null;
|
|
99
|
+
const r = await api.inbox.getInboxSparks(limit ? { query: { limit } } : {});
|
|
100
|
+
if (!r.ok) {
|
|
101
|
+
console.error(explain(r.status, r.data));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
console.log(renderQueue(r.data || {}));
|
|
105
|
+
}
|
|
106
|
+
|
|
59
107
|
async function main() {
|
|
60
108
|
await requireSession();
|
|
61
109
|
|
|
110
|
+
if (hasFlag('--list')) return listQueue();
|
|
111
|
+
|
|
62
112
|
const positional = process.argv.slice(2).filter((a) => !a.startsWith('-'));
|
|
63
113
|
const id = Number(positional[0]);
|
|
64
114
|
const undo = hasFlag('--undo');
|
|
65
115
|
|
|
66
116
|
if (!Number.isInteger(id) || id <= 0) {
|
|
67
117
|
console.error('usage: node scripts/gds/spark.js <idea-id> [--undo]');
|
|
118
|
+
console.error(' node scripts/gds/spark.js --list [--limit N]');
|
|
68
119
|
process.exit(2);
|
|
69
120
|
}
|
|
70
121
|
|
|
@@ -90,10 +141,11 @@ async function main() {
|
|
|
90
141
|
}
|
|
91
142
|
}
|
|
92
143
|
|
|
93
|
-
//
|
|
94
|
-
// against the real envelope
|
|
95
|
-
//
|
|
96
|
-
|
|
144
|
+
// explain() and renderQueue() are exported because both are pinned by tests —
|
|
145
|
+
// explain against the real envelope shape, renderQueue for the empty case and the
|
|
146
|
+
// hall-link form. REFUSALS stays private: it is the message table, reachable
|
|
147
|
+
// through explain(), and a third export would be dead surface.
|
|
148
|
+
module.exports = { explain, renderQueue };
|
|
97
149
|
|
|
98
150
|
if (require.main === module) {
|
|
99
151
|
main().catch((err) => {
|
package/src/module-api.js
CHANGED
|
@@ -1017,8 +1017,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1017
1017
|
// landed since 1.19.293 with no explicit bump. run 32588977219. (task 1002620)
|
|
1018
1018
|
// 1.19.295 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1019
1019
|
// landed since 1.19.294 with no explicit bump. run 32590508145. (task 1002620)
|
|
1020
|
+
// 1.19.296 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1021
|
+
// landed since 1.19.295 with no explicit bump. run 32591155887. (task 1002620)
|
|
1020
1022
|
// ---------------------------------------------------------------------------
|
|
1021
|
-
const CORE_VERSION = '1.19.
|
|
1023
|
+
const CORE_VERSION = '1.19.296'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1022
1024
|
|
|
1023
1025
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1024
1026
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// tests/idea_spark_queue.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002921 (BV1.R145, goal 1000062) — the spark queue.
|
|
4
|
+
//
|
|
5
|
+
// The done-when, as assertions: the GET returns OPEN sparks NOT YET DEVELOPED,
|
|
6
|
+
// newest first; a DEVELOPED spark leaves the queue; and the queue is readable by
|
|
7
|
+
// ANY builder (a Xenos included — it is a pickup surface, and the author's R144
|
|
8
|
+
// mark is the consent).
|
|
9
|
+
//
|
|
10
|
+
// It also pins two things that are load-bearing but invisible in a passing app:
|
|
11
|
+
// the DECLARATION ORDER against `GET /inbox/:id` (which would otherwise capture
|
|
12
|
+
// the literal "sparks" and 400 the whole queue), and the fact that the count and
|
|
13
|
+
// the list share one predicate — a pill that disagrees with its rows is the
|
|
14
|
+
// classic version of this bug.
|
|
15
|
+
//
|
|
16
|
+
// DB-free the idea_full_capture.mjs way: pool.query patched on the doorway before
|
|
17
|
+
// the modules load, so the REAL SQL is captured and the REAL handler runs.
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/idea_spark_queue.mjs
|
|
20
|
+
|
|
21
|
+
import { strict as assert } from 'node:assert';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const api = require('../src/module-api.js');
|
|
26
|
+
|
|
27
|
+
let queries = [];
|
|
28
|
+
let listRows = [];
|
|
29
|
+
let countN = 0;
|
|
30
|
+
|
|
31
|
+
api.pool.query = async (sql, params = []) => {
|
|
32
|
+
queries.push({ sql, params });
|
|
33
|
+
if (/COUNT\(\*\)/i.test(sql)) return { rows: [{ n: countN }] };
|
|
34
|
+
return { rows: listRows };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const inbox = require('../modules/ideas/inbox.js');
|
|
38
|
+
const routesFactory = require('../modules/ideas/routes/inbox.js');
|
|
39
|
+
const router = routesFactory({});
|
|
40
|
+
|
|
41
|
+
const mounted = router.stack.filter((l) => l.route)
|
|
42
|
+
.map((l) => `${Object.keys(l.route.methods)[0].toUpperCase()} ${l.route.path}`);
|
|
43
|
+
|
|
44
|
+
function handlerFor(pathStr, method) {
|
|
45
|
+
const layer = router.stack.find((l) => l.route && l.route.path === pathStr && l.route.methods[method]);
|
|
46
|
+
assert.ok(layer, `route ${method.toUpperCase()} ${pathStr} is not mounted`);
|
|
47
|
+
const stack = layer.route.stack.map((s) => s.handle);
|
|
48
|
+
return stack[stack.length - 1];
|
|
49
|
+
}
|
|
50
|
+
const getSparks = handlerFor('/inbox/sparks', 'get');
|
|
51
|
+
|
|
52
|
+
function call({ query = {}, rank = 'metic' } = {}) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const req = { params: {}, body: {}, query, builder: { id: '9', rank } };
|
|
55
|
+
const res = {
|
|
56
|
+
statusCode: 200,
|
|
57
|
+
status(c) { this.statusCode = c; return this; },
|
|
58
|
+
json(payload) { resolve({ status: this.statusCode, payload }); },
|
|
59
|
+
fail(code, status, extra) {
|
|
60
|
+
const s = typeof status === 'number' ? status : (status && status.status) || 500;
|
|
61
|
+
resolve({ status: s, payload: { error: code, ...(extra || {}) } });
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
Promise.resolve(getSparks(req, res, (err) => reject(err || new Error('next() called')))).catch(reject);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const listSql = () => queries.find((q) => !/COUNT\(\*\)/i.test(q.sql));
|
|
69
|
+
const countSql = () => queries.find((q) => /COUNT\(\*\)/i.test(q.sql));
|
|
70
|
+
|
|
71
|
+
let passed = 0;
|
|
72
|
+
let failed = 0;
|
|
73
|
+
async function test(name, fn) {
|
|
74
|
+
queries = [];
|
|
75
|
+
listRows = [];
|
|
76
|
+
countN = 0;
|
|
77
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
78
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- declaration order: the bug that would 400 the whole queue -----------
|
|
82
|
+
|
|
83
|
+
await test('GET /inbox/sparks is declared BEFORE GET /inbox/:id', () => {
|
|
84
|
+
const iSparks = mounted.indexOf('GET /inbox/sparks');
|
|
85
|
+
const iId = mounted.indexOf('GET /inbox/:id');
|
|
86
|
+
assert.ok(iSparks >= 0, 'the spark queue route is mounted');
|
|
87
|
+
assert.ok(iId >= 0, 'the by-id read is mounted');
|
|
88
|
+
assert.ok(
|
|
89
|
+
iSparks < iId,
|
|
90
|
+
`declaration order is load-bearing: with :id first, "/inbox/sparks" binds id="sparks" and parseId 400s the queue (sparks@${iSparks}, :id@${iId})`,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// --- the predicate: open, undeveloped sparks, newest first ---------------
|
|
95
|
+
|
|
96
|
+
await test('the queue asks for grade=spark AND status=open — nothing else', async () => {
|
|
97
|
+
const r = await call();
|
|
98
|
+
assert.equal(r.status, 200);
|
|
99
|
+
const sql = listSql().sql;
|
|
100
|
+
assert.match(sql, /g\.grade = 'spark'/, 'only sparks');
|
|
101
|
+
assert.match(sql, /i\.status = 'open'/, 'only open ideas');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await test('a DEVELOPED spark leaves the queue by GRADE, with no `developed` flag', async () => {
|
|
105
|
+
await call();
|
|
106
|
+
const sql = listSql().sql;
|
|
107
|
+
// R146 develops in place and flips the grade spark→full, so grade='spark' IS
|
|
108
|
+
// "not yet developed". A second source of truth could disagree with the grade.
|
|
109
|
+
assert.doesNotMatch(sql, /developed/i, 'no separate developed column/flag');
|
|
110
|
+
assert.doesNotMatch(sql, /promoted_to_task_id\s*IS\s*NULL/i, 'exit is the grade flip, not a promote link');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await test('newest first, with a stable tiebreak', async () => {
|
|
114
|
+
await call();
|
|
115
|
+
assert.match(listSql().sql, /ORDER BY i\.captured_at DESC, i\.id DESC/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await test('an INNER join from ideas_grades — a spark is never implicit', async () => {
|
|
119
|
+
await call();
|
|
120
|
+
const sql = listSql().sql;
|
|
121
|
+
// Inverted from every other idea read on purpose: absence of a grade row means
|
|
122
|
+
// 'quick', so a spark ALWAYS has a real row, and starting from ideas_grades
|
|
123
|
+
// lets ideas_grades_grade_score_idx drive the scan.
|
|
124
|
+
assert.match(sql, /FROM ideas_grades g/, 'driven by the graded table');
|
|
125
|
+
assert.match(sql, /JOIN idea_inbox i ON i\.id = g\.idea_id/);
|
|
126
|
+
assert.doesNotMatch(sql, /LEFT JOIN ideas_grades/, 'not the COALESCE-to-quick read shape');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await test('the author is named — attribution is the point of a pickup surface', async () => {
|
|
130
|
+
listRows = [{ id: '5', title: 'a spark', captured_by: '3', grade: 'spark', filed_by_login: 'someone' }];
|
|
131
|
+
const r = await call();
|
|
132
|
+
assert.match(listSql().sql, /github_login/);
|
|
133
|
+
assert.equal(r.payload.sparks[0].filed_by_login, 'someone');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// --- the count cannot disagree with the list ----------------------------
|
|
137
|
+
|
|
138
|
+
await test('count and list share ONE predicate', async () => {
|
|
139
|
+
countN = 7;
|
|
140
|
+
const r = await call();
|
|
141
|
+
assert.equal(r.payload.count, 7);
|
|
142
|
+
const l = listSql().sql;
|
|
143
|
+
const c = countSql().sql;
|
|
144
|
+
for (const clause of [/g\.grade = 'spark'/, /i\.status = 'open'/]) {
|
|
145
|
+
assert.match(l, clause);
|
|
146
|
+
assert.match(c, clause, 'the pill must not claim sparks the list will not show');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// --- readable by ANY builder -------------------------------------------
|
|
151
|
+
|
|
152
|
+
await test('a XENOS can read the queue — the R144 mark is the consent', async () => {
|
|
153
|
+
listRows = [{ id: '5', title: 'a spark', grade: 'spark' }];
|
|
154
|
+
const r = await call({ rank: 'xenos' });
|
|
155
|
+
assert.equal(r.status, 200, 'no rank gate on a pickup surface');
|
|
156
|
+
assert.equal(r.payload.sparks.length, 1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await test('but the skill-friction visibility gate still applies, to BOTH halves', async () => {
|
|
160
|
+
await call({ rank: 'xenos' });
|
|
161
|
+
const l = listSql();
|
|
162
|
+
const c = countSql();
|
|
163
|
+
assert.match(l.sql, /i\.title NOT LIKE/, 'a Xenos does not see routine-filed friction rows');
|
|
164
|
+
assert.match(c.sql, /i\.title NOT LIKE/, 'nor are they counted for them');
|
|
165
|
+
assert.ok(l.params.some((p) => typeof p === 'string' && p.endsWith('%')));
|
|
166
|
+
|
|
167
|
+
queries = [];
|
|
168
|
+
await call({ rank: 'archon' });
|
|
169
|
+
assert.doesNotMatch(listSql().sql, /i\.title NOT LIKE/, 'a trusted rank sees everything');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// --- limit -------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
await test('limit defaults to 50 and is clamped to 1..100', async () => {
|
|
175
|
+
await call();
|
|
176
|
+
assert.equal(listSql().params.at(-1), 50, 'default');
|
|
177
|
+
|
|
178
|
+
for (const [given, want] of [['10', 10], ['0', 50], ['-5', 50], ['1000', 50], ['abc', 50], ['100', 100]]) {
|
|
179
|
+
queries = [];
|
|
180
|
+
await call({ query: { limit: given } });
|
|
181
|
+
assert.equal(listSql().params.at(-1), want, `limit=${given}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await test('the count query takes no limit — it counts the whole queue', async () => {
|
|
186
|
+
await call({ query: { limit: '5' } });
|
|
187
|
+
assert.equal(countSql().params.length, 0, 'a trusted viewer adds no filter param, and the count never takes the limit');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// --- the CLI's rendering of the queue ------------------------------------
|
|
191
|
+
|
|
192
|
+
const { renderQueue } = require('../scripts/gds/spark.js');
|
|
193
|
+
|
|
194
|
+
await test('the empty queue reads as an ANSWER, not a blank screen', () => {
|
|
195
|
+
const out = renderQueue({ sparks: [], count: 0 });
|
|
196
|
+
assert.match(out, /No sparks are awaiting development/);
|
|
197
|
+
assert.match(out, /mark one of your own/, 'it says what to do next');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await test('each row links to the HALL, never a bare #NNN', () => {
|
|
201
|
+
const out = renderQueue({
|
|
202
|
+
sparks: [{ id: '812', title: 'a thought', filed_by_login: 'someone', captured_at: '2026-08-20T10:11:12Z' }],
|
|
203
|
+
count: 1,
|
|
204
|
+
});
|
|
205
|
+
// A bare "#NNN" autolinks to a GitHub issue and 404s — our ids are not
|
|
206
|
+
// GitHub's. The hall deep-link resolver form is /builders/#/idea/N.
|
|
207
|
+
assert.match(out, /\/builders\/#\/idea\/812/);
|
|
208
|
+
assert.doesNotMatch(out, /(^|[^/])#812\b/, 'no bare ref');
|
|
209
|
+
assert.match(out, /@someone/, 'the author is named');
|
|
210
|
+
assert.match(out, /2026-08-20/);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await test('a missing author degrades to prose, not "@undefined"', () => {
|
|
214
|
+
const out = renderQueue({ sparks: [{ id: '1', title: 't', filed_by_login: null }], count: 1 });
|
|
215
|
+
assert.match(out, /an unnamed builder/);
|
|
216
|
+
assert.doesNotMatch(out, /undefined|null/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await test('the header distinguishes "showing N" from the true total', () => {
|
|
220
|
+
assert.match(renderQueue({ sparks: [{ id: '1', title: 't' }], count: 5 }), /5 sparks awaiting development \(showing 1\)/);
|
|
221
|
+
assert.match(renderQueue({ sparks: [{ id: '1', title: 't' }], count: 1 }), /^1 spark awaiting development:/m, 'singular, no "showing"');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
console.log(`\nidea_spark_queue: ${passed} passed, ${failed} failed`);
|
|
225
|
+
if (failed > 0) process.exit(1);
|