@bongos/core 1.19.296 → 1.19.297

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.296",
6
- "core_contract": "1.19.296",
7
- "source_commit": "a0c264baf117184273d9a83f7c158844920f1d48",
5
+ "core_version": "1.19.297",
6
+ "core_contract": "1.19.297",
7
+ "source_commit": "1f1b380614ecad230b7399a63883dd4aa65a5c51",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-22T18:35:34.578Z",
9
+ "built_at": "2026-08-22T19:02:34.784Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 321,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1398,
14
+ "functional_verbatim": 1400,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1741,
20
- "tree_sha256": "a34c536a2753b233ccd552b62e7fcfbe3768304013f4130c641a9bb51d7e51d5",
19
+ "file_count": 1743,
20
+ "tree_sha256": "39da631b7c0c17d3b63eb793e9e7b624bf55dbe877a2bdd2a568608076258bf7",
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": "cc15a1c79d2dd761b8b50190f72d69b2ccec0a56bdd812a82913213a4b811da6"
350
+ "sha256": "6e44e5da05194cc5aae1784d591d5f560dc36527939eda359de794e3d29fdb37"
351
351
  },
352
352
  {
353
353
  "path": "clients/bongos-client/bongos-client.global.js",
354
354
  "mode": "0000644",
355
- "sha256": "b2add6cdda6bf67a34a33864f5c6c0a1bdaf7f3ac21314800d222b5f4be55eb5"
355
+ "sha256": "b05f34f719999b987f9ea6dcecec7bed016a0486ffb43a2ba95fcabbbe7457db"
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": "b6f803fa3594804e6781eab762eb91241c9146c1e3aa09a8883dd8015a55dc3b"
365
+ "sha256": "5fbf0ea8c5b9eba86cc1d471407d8f4bb1df23af72d4fe7a618576bf67233a14"
366
366
  },
367
367
  {
368
368
  "path": "clients/bongos-client/index.d.ts",
369
369
  "mode": "0000644",
370
- "sha256": "ee350739d64eb2ddce22fa0378ece2eebc363eae96c7755f087363b87e9581fe"
370
+ "sha256": "db6c391a39f2caea99486c5f03993b0cc52e3506835d750a28a4a0b2108bb441"
371
371
  },
372
372
  {
373
373
  "path": "clients/bongos-client/index.mjs",
374
374
  "mode": "0000644",
375
- "sha256": "9f4db5e5579a3e401a9d56ee473436b8c6cd00e82f209de784c321131f6e420d"
375
+ "sha256": "5ef5dcc11f3713394c6a6225c4206ef8955b27b17c95a47fd51ca43b7965729f"
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": "f7102579bfe64bb798e9ebc826849857218da439113c6f7b049cfc9a580f863f"
1415
+ "sha256": "0c2b5d0fd5e05cbe3b3c06ab10baf7e6af0414d85599519e3ef2eb289008b672"
1416
1416
  },
1417
1417
  {
1418
1418
  "path": "docs/api/openapi.json",
1419
1419
  "mode": "0000644",
1420
- "sha256": "a3d866c17cdb60660fb7603de26faccb0fd0fa345e47c26d9703e2fc1fdbfd40"
1420
+ "sha256": "ef7e4fbdb912b077c49b15e7ba0dd6333196fe0d82d4462a83c507669fd82767"
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": "9a2f67ab5989d068060bfe336d17ae88a33484fa8cbb5a92f23534b1c6de6289"
1555
+ "sha256": "b017e7a4425f92cc5cbec8d0a5fc192e3225794fb97955acb3689b12dc0de667"
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": "b6f803fa3594804e6781eab762eb91241c9146c1e3aa09a8883dd8015a55dc3b"
2810
+ "sha256": "5fbf0ea8c5b9eba86cc1d471407d8f4bb1df23af72d4fe7a618576bf67233a14"
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": "33a25e6bd24ee0952c03e2aeab3e1edb68f8cf84989ee04206c9efdb7d202fa7"
3710
+ "sha256": "ceaa172a8e0338dd9202dae6abea8a5278594937bd980ddaf9ac2b2017b555a4"
3711
3711
  },
3712
3712
  {
3713
3713
  "path": "modules/ideas/migrations/ideas_001_grades.sql",
@@ -3729,6 +3729,11 @@
3729
3729
  "mode": "0000644",
3730
3730
  "sha256": "eb1393f0ee8dccc57efcfe0acd0fdac7857389b74e0cb734042bb4293768b8e8"
3731
3731
  },
3732
+ {
3733
+ "path": "modules/ideas/migrations/ideas_005_grade_developed_by.sql",
3734
+ "mode": "0000644",
3735
+ "sha256": "3c7a48505c922bd0742888bc9c0598d776a4de0cc3d2745bd08d6452e07f34b9"
3736
+ },
3732
3737
  {
3733
3738
  "path": "modules/ideas/module.json",
3734
3739
  "mode": "0000644",
@@ -3742,7 +3747,7 @@
3742
3747
  {
3743
3748
  "path": "modules/ideas/routes/inbox.js",
3744
3749
  "mode": "0000644",
3745
- "sha256": "7de2735020ddd526ded8ca252fbfba157ee5eb174895660d37ac713b9d0d6fd2"
3750
+ "sha256": "eafc7ff6f4d9567813cefe2b47333caa4ab79b77ab979e77eb65ac88102a78f1"
3746
3751
  },
3747
3752
  {
3748
3753
  "path": "modules/ideas/routing.js",
@@ -4537,12 +4542,12 @@
4537
4542
  {
4538
4543
  "path": "package-lock.json",
4539
4544
  "mode": "0000644",
4540
- "sha256": "b937444a6338c17ae2692d47f126d612f87c2429783a3d9a95e4c5d6076f3f5b"
4545
+ "sha256": "9fea7a7292ec0de1ea5fea27ec984796c150ed75f0e94e30eca8837f55cbcee6"
4541
4546
  },
4542
4547
  {
4543
4548
  "path": "package.json",
4544
4549
  "mode": "0000644",
4545
- "sha256": "ce72a3e5a70ae07f431f223dccf12d2c896474bcb1f0d62724aea22206b23dde"
4550
+ "sha256": "8b60b6155a3fd2c2d0110df0404319a6a5da35eeed3ee8d60f61ae009672ed35"
4546
4551
  },
4547
4552
  {
4548
4553
  "path": "public-docs/index.html",
@@ -5782,7 +5787,7 @@
5782
5787
  {
5783
5788
  "path": "scripts/gds/spark.js",
5784
5789
  "mode": "0000755",
5785
- "sha256": "61ad614ec3fb8cbfc98d815629fd3ccafd179a8f2c058539103c9bf4e23e9504"
5790
+ "sha256": "dfd5aa72caa26eb2c8cf3063e1b4f51e1f4bcd7fddcdd93d243279c130850628"
5786
5791
  },
5787
5792
  {
5788
5793
  "path": "scripts/gds/start.js",
@@ -6197,7 +6202,7 @@
6197
6202
  {
6198
6203
  "path": "src/module-api.js",
6199
6204
  "mode": "0000644",
6200
- "sha256": "e5f489e0d2b9cc6d9c73cc853132a8a7a7e3fbcd8ea2325284d58143d4020a05"
6205
+ "sha256": "2bfcc90c50b5fd0945b97179e4445602f145896dd1821066cabcd411a8d7a9d9"
6201
6206
  },
6202
6207
  {
6203
6208
  "path": "src/module-loader/catalog.js",
@@ -7484,6 +7489,11 @@
7484
7489
  "mode": "0000644",
7485
7490
  "sha256": "7e2024b3f1fa79a6bdec55b134be6337d4867df0a77fedb205f32b783a3d6193"
7486
7491
  },
7492
+ {
7493
+ "path": "tests/idea_develop_spark.mjs",
7494
+ "mode": "0000644",
7495
+ "sha256": "03d2f692eaa02e85a5f53e89527d4ea1a20c032d4801e2a7acd421eda12ce76c"
7496
+ },
7487
7497
  {
7488
7498
  "path": "tests/idea_full_capture.mjs",
7489
7499
  "mode": "0000644",
@@ -7522,7 +7532,7 @@
7522
7532
  {
7523
7533
  "path": "tests/idea_spark_queue.mjs",
7524
7534
  "mode": "0000644",
7525
- "sha256": "6bea289f0473d07e21acd0b45b50511f061d8d1f0d09b7437a909081ea83344a"
7535
+ "sha256": "43f801610e37062c54e631904aecaa10aa2486139aba3aa9eeb820d84fd32368"
7526
7536
  },
7527
7537
  {
7528
7538
  "path": "tests/idea_task_lineage.mjs",
@@ -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
- - 297 operations across 47 resource groups
8
+ - 298 operations across 47 resource groups
9
9
 
10
10
  ## Install
11
11
 
@@ -405,6 +405,8 @@ function createClient(opts = {}) {
405
405
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
406
406
  // PATCH /inbox/{id}/body — rank: any-builder — PATCH /inbox/:id/body
407
407
  patchInboxIdBody: (args) => request("PATCH", "/inbox/{id}/body", { hasBody: true }, args),
408
+ // POST /inbox/{id}/develop — rank: any-builder — POST /inbox/:id/develop
409
+ postInboxIdDevelop: (args) => request("POST", "/inbox/{id}/develop", { hasBody: true }, args),
408
410
  // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
409
411
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
410
412
  // POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
@@ -404,6 +404,8 @@ function createClient(opts = {}) {
404
404
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
405
405
  // PATCH /inbox/{id}/body — rank: any-builder — PATCH /inbox/:id/body
406
406
  patchInboxIdBody: (args) => request("PATCH", "/inbox/{id}/body", { hasBody: true }, args),
407
+ // POST /inbox/{id}/develop — rank: any-builder — POST /inbox/:id/develop
408
+ postInboxIdDevelop: (args) => request("POST", "/inbox/{id}/develop", { hasBody: true }, args),
407
409
  // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
408
410
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
409
411
  // POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
@@ -246,6 +246,7 @@ export interface PostGovernmentBoardItemsResponse { ok: boolean; amendment: unkn
246
246
  export interface PostGovernmentRanksRankKeyResetResponse { ok: boolean; rank_key: unknown; seeded_rank: unknown; permission_count: unknown }
247
247
  export interface PostGovernmentRanksRequest { rank_key: string; label: string; description?: string }
248
248
  export interface PostGovernmentRanksResponse { rank: unknown }
249
+ export interface PostInboxIdDevelopRequest { field_values: Record<string, unknown>; template_key?: string }
249
250
  export interface PostInboxIdRatifyRequest { notes_md?: string }
250
251
  export interface PostInboxIdResubmitRequest { field_values: Record<string, unknown>; template_key?: string }
251
252
  export interface PostInboxIdSparkResponse { idea: unknown; sparked: boolean; unchanged: boolean }
@@ -668,6 +669,8 @@ export interface BongosClient {
668
669
  patchInboxId(args?: RequestArgs & { body?: PatchInboxIdRequest }): Promise<PatchInboxIdResponse>;
669
670
  /** PATCH /inbox/{id}/body — rank: any-builder */
670
671
  patchInboxIdBody(args: RequestArgs & { body: PatchInboxIdBodyRequest }): Promise<PatchInboxIdBodyResponse>;
672
+ /** POST /inbox/{id}/develop — rank: any-builder */
673
+ postInboxIdDevelop(args: RequestArgs & { body: PostInboxIdDevelopRequest }): Promise<ApiResponse>;
671
674
  /** POST /inbox/{id}/ratify — rank: metic+archon */
672
675
  postInboxIdRatify(args?: RequestArgs & { body?: PostInboxIdRatifyRequest }): Promise<ApiResponse>;
673
676
  /** POST /inbox/{id}/resubmit — rank: any-builder */
@@ -401,6 +401,8 @@ export function createClient(opts = {}) {
401
401
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
402
402
  // PATCH /inbox/{id}/body — rank: any-builder — PATCH /inbox/:id/body
403
403
  patchInboxIdBody: (args) => request("PATCH", "/inbox/{id}/body", { hasBody: true }, args),
404
+ // POST /inbox/{id}/develop — rank: any-builder — POST /inbox/:id/develop
405
+ postInboxIdDevelop: (args) => request("POST", "/inbox/{id}/develop", { hasBody: true }, args),
404
406
  // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
405
407
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
406
408
  // POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
@@ -6524,6 +6524,62 @@
6524
6524
  ]
6525
6525
  }
6526
6526
  },
6527
+ "/inbox/{id}/develop": {
6528
+ "post": {
6529
+ "operationId": "post_inbox_id_develop",
6530
+ "tags": [
6531
+ "inbox"
6532
+ ],
6533
+ "summary": "POST /inbox/:id/develop",
6534
+ "description": "POST /inbox/:id/develop — DEVELOP A SPARK INTO A FULL IDEA (task 1002922 / BV1.R146, goal 1000062). The pickup half of the handoff R144 and R145 set up: the author consented by marking the spark, the queue made it findable, and now another builder fills in the five template fields and the thought becomes a Full Idea. OPEN TO ANY BUILDER, INCLUDING THE AUTHOR. Cross-builder development is the entire point, so there is no author check here — the mirror image of R144, where the gate was the whole feature. The consent was already given and is already recorded (this row is a spark BECAUSE its author said so), and re-asking for it would make the queue unpickable. Self-development is likewise legal: an author may mark a thought and come back to it themselves. ONLY A SPARK CAN BE DEVELOPED, and \"only once\" is enforced by the DATABASE, not by this handler. developSpark's UPDATE carries `AND grade = 'spark'`, so two builders picking the same spark off the queue at the same instant cannot both succeed — the second matches zero rows. The pre-read below exists ONLY to tell a 404 apart from a 409 and to give a useful message; it is not the guard, and it deliberately does not try to be. WHAT THIS ROUTE DELIBERATELY DOES NOT DO: pay the stream-2 \"Full Idea passed\" credit. Capture and resubmit both fire it, but on those paths the recipient is unambiguously the author. Here there are two people, and choosing between them is R147's job (task 1002923 — \"karma for a ratified Full Idea, scaled by completeness and SPLIT WITH THE SPARK AUTHOR\"). Firing it here would not just pre-empt that decision, it would corrupt it: awardIdeaFullPassCredit's idempotency predicate is (builder_id, `idea.credit.full_pass:<id>`), so a payment to the DEVELOPER does not block a later payment to the AUTHOR — one idea would pay twice. The credit is idempotent per idea+builder, so R147 can still pay it at the right time, to the right people, in the right shares. The board window IS opened, because it is recipient-agnostic (it takes an idea and a score, not a builder) and without it a developed Full Idea could never reach ratification — the handoff would dead-end one step from done. Fail-open like every other caller of that port. rank: any-builder — development is the pickup, and the author's mark is the consent. Deliberately NOT pinned in route-rank-check: there is no gate here to protect, and pinning it would only assert that an open route stays open.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
6535
+ "x-rank": "any-builder",
6536
+ "x-source": "modules/ideas/routes/inbox.js",
6537
+ "parameters": [
6538
+ {
6539
+ "name": "id",
6540
+ "in": "path",
6541
+ "required": true,
6542
+ "schema": {
6543
+ "type": "string"
6544
+ },
6545
+ "description": "Path parameter `id`."
6546
+ }
6547
+ ],
6548
+ "requestBody": {
6549
+ "required": true,
6550
+ "content": {
6551
+ "application/json": {
6552
+ "schema": {
6553
+ "$ref": "#/components/schemas/PostInboxIdDevelopRequest"
6554
+ }
6555
+ }
6556
+ },
6557
+ "x-validated": true
6558
+ },
6559
+ "responses": {
6560
+ "200": {
6561
+ "description": "Success."
6562
+ },
6563
+ "400": {
6564
+ "$ref": "#/components/responses/ValidationFailed"
6565
+ },
6566
+ "401": {
6567
+ "$ref": "#/components/responses/Unauthorized"
6568
+ },
6569
+ "403": {
6570
+ "$ref": "#/components/responses/Forbidden"
6571
+ },
6572
+ "404": {
6573
+ "$ref": "#/components/responses/NotFound"
6574
+ }
6575
+ },
6576
+ "security": [
6577
+ {
6578
+ "builderSession": []
6579
+ }
6580
+ ]
6581
+ }
6582
+ },
6527
6583
  "/inbox/{id}/ratify": {
6528
6584
  "post": {
6529
6585
  "operationId": "post_inbox_id_ratify",
@@ -16997,6 +17053,22 @@
16997
17053
  "rank"
16998
17054
  ]
16999
17055
  },
17056
+ "PostInboxIdDevelopRequest": {
17057
+ "type": "object",
17058
+ "properties": {
17059
+ "field_values": {
17060
+ "type": "object"
17061
+ },
17062
+ "template_key": {
17063
+ "type": "string",
17064
+ "maxLength": 64
17065
+ }
17066
+ },
17067
+ "required": [
17068
+ "field_values"
17069
+ ],
17070
+ "additionalProperties": false
17071
+ },
17000
17072
  "PostInboxIdRatifyRequest": {
17001
17073
  "type": "object",
17002
17074
  "properties": {
@@ -18756,8 +18828,8 @@
18756
18828
  "description": "A required dependency/feature is not configured or is temporarily down."
18757
18829
  }
18758
18830
  },
18759
- "x-endpoint-count": 297,
18760
- "x-schema-count": 330,
18831
+ "x-endpoint-count": 298,
18832
+ "x-schema-count": 331,
18761
18833
  "x-undocumented-bodies": 8,
18762
18834
  "x-response-schemas": 221,
18763
18835
  "x-generated-by": "scripts/gds/gen-api-docs.js"
@@ -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. 297 endpoints across 47 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 298 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` (11)
261
+ ## `inbox` (12)
262
262
 
263
263
  | Method | Path | Rank | Body | Description |
264
264
  |---|---|---|---|---|
@@ -267,6 +267,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
267
267
  | GET | `/api/bongos/inbox/:id` | `any-builder` | — | single idea read for the "#NNN" deep-link resolver (task 1047). |
268
268
  | PATCH | `/api/bongos/inbox/:id` | `metic+archon` | `status`, `kind`, `promoted_to_task_id`, `reviewed_at`, `merged_into_idea_id`, `triage_note` | |
269
269
  | PATCH | `/api/bongos/inbox/:id/body` | `any-builder` | `body_md` | Metic+ (task 360 / ADR 0018): idea triage (promote/discard/merge) shapes the backlog. |
270
+ | POST | `/api/bongos/inbox/:id/develop` | `any-builder` | `field_values`, `template_key` | POST /inbox/:id/develop — DEVELOP A SPARK INTO A FULL IDEA (task 1002922 / BV1.R146, goal 1000062). |
270
271
  | POST | `/api/bongos/inbox/:id/ratify` | `metic+archon` | `notes_md` | V3.R31 (task 252) — Archon ratifies a criterion-proposal idea, atomically: - INSERT done_when_criteria row on the target version - UPDATE… |
271
272
  | POST | `/api/bongos/inbox/:id/resubmit` | `any-builder` | `field_values`, `template_key` | RESUBMIT a Full Idea after a rejection (R13 of goal 1000069, criterion C2: an idea never dies and never stalls). |
272
273
  | 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". |
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 ← (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'
178
+ │ ├── spark.js ← (tasks 1002920 / R144 + 1002921 / R145 + 1002922 / R146) the whole spark handoff from the terminal: mark one of your OWN quick ideas as a Big Idea Spark (`--undo` withdraws it), `--list` is the SPARK QUEUE, `--template` prints the five Full Idea fields, `--develop <id> --fields-file <json>` is the PICKUP (any builder; author + developer both recorded). 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`
@@ -404,6 +404,8 @@ function createClient(opts = {}) {
404
404
  patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
405
405
  // PATCH /inbox/{id}/body — rank: any-builder — PATCH /inbox/:id/body
406
406
  patchInboxIdBody: (args) => request("PATCH", "/inbox/{id}/body", { hasBody: true }, args),
407
+ // POST /inbox/{id}/develop — rank: any-builder — POST /inbox/:id/develop
408
+ postInboxIdDevelop: (args) => request("POST", "/inbox/{id}/develop", { hasBody: true }, args),
407
409
  // POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
408
410
  postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
409
411
  // POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
@@ -100,15 +100,22 @@ const FIELD_VALUES_MAX = 32768;
100
100
  // The grade half of an idea read: the COALESCE above, plus the template-shaped
101
101
  // store and the score. `g.*` columns are NULL for an ungraded idea, which is
102
102
  // exactly right — it has no template and no score, it just has a default grade.
103
+ // developed_by/developed_at (ideas_005, task 1002922 / BV1.R146) ride along on
104
+ // EVERY idea read rather than needing a second query: "who developed this" is
105
+ // part of an idea's identity once it has been developed, and the R145 queue, the
106
+ // hall detail view (R153) and the R147 credit split all want it. NULL for the
107
+ // overwhelming majority of rows — a quick idea and a self-authored Full Idea were
108
+ // never developed by anyone — which is the honest answer, not a missing one.
103
109
  const GRADE_COLS_Q = `COALESCE(g.grade, '${DEFAULT_GRADE}') AS grade,
104
- g.template_key, g.field_values, g.completeness_score`;
110
+ g.template_key, g.field_values, g.completeness_score,
111
+ g.developed_by, g.developed_at`;
105
112
  const IDEA_JOIN_GRADE = `LEFT JOIN ideas_grades g ON g.idea_id = i.id`;
106
113
 
107
114
  // The same projection for a row that provably has no grade row yet (a just-
108
115
  // INSERTed idea), so INSERT…RETURNING does not need a second round trip to
109
116
  // return the same shape every other read returns.
110
117
  function ungradedProjection() {
111
- return { grade: DEFAULT_GRADE, template_key: null, field_values: null, completeness_score: null };
118
+ return { grade: DEFAULT_GRADE, template_key: null, field_values: null, completeness_score: null, developed_by: null, developed_at: null };
112
119
  }
113
120
 
114
121
  // -------------------------------------------------------------------------
@@ -951,6 +958,81 @@ async function setIdeaCompleteness(ideaId, completenessScore) {
951
958
  return rows[0] ?? null;
952
959
  }
953
960
 
961
+ // -------------------------------------------------------------------------
962
+ // developSpark — turn a Big Idea Spark into a Full Idea IN PLACE
963
+ // (task 1002922 / BV1.R146, goal 1000062)
964
+ //
965
+ // The pickup half of the spark handoff: R144 recorded the author's consent, R145
966
+ // made the spark findable, and this is another builder filling in the five
967
+ // template fields.
968
+ //
969
+ // IN PLACE, ON THE SAME ROW, AND THAT IS A DONE-WHEN. idea_inbox.id,
970
+ // captured_by and captured_at are NOT touched — only the grade row moves. A new
971
+ // row for the developed version would break the hall's /#/idea/N deep link,
972
+ // ideas_task_lineage.idea_id, and the per-idea credit_log reason key. The
973
+ // author's claim on their own thought is precisely what must survive being
974
+ // developed by someone else; it is what the R147 split pays against.
975
+ //
976
+ // ONE GUARDED UPDATE, WHICH IS WHERE "ONLY ONCE" ACTUALLY LIVES. The predicate
977
+ // carries `AND grade = 'spark'`, so the database decides whether this row was
978
+ // still developable. A read-then-write ("is it a spark? ok, develop it") looks
979
+ // identical in a test and loses a race in production: two builders picking the
980
+ // same spark off the R145 queue at the same moment would both pass the check and
981
+ // the second would overwrite the first's work AND their attribution. Here the
982
+ // second UPDATE matches zero rows and the route turns that into a 409. Zero rows
983
+ // is therefore NOT an error condition to log — it is the answer.
984
+ //
985
+ // It is also why this is not expressed as setIdeaGrade + a second write:
986
+ // two statements cannot carry one guard, and a Full Idea with no recorded
987
+ // developer is the exact state the done-when forbids.
988
+ //
989
+ // THE STATUS HALF OF THE GUARD, AND WHY IT IS NOT REDUNDANT WITH THE ROUTE'S
990
+ // PRE-CHECK. Triage (`updateIdea`) writes idea_inbox.status and NEVER touches
991
+ // ideas_grades — so a spark that a Metic+ triager discards, merges or promotes
992
+ // keeps grade='spark' forever. Grade alone therefore does NOT mean "still
993
+ // developable", and a develop that trusted it would resurrect content the triage
994
+ // flow already decided was dead, opening a fresh board ratification window for
995
+ // it. The R145 queue already filters on status and the R144 mark already requires
996
+ // it; this closes the same hole on the write. In the PREDICATE rather than only
997
+ // in the handler because a triager discarding an idea mid-request is the same
998
+ // class of race as two developers colliding — and the fix for a race is never a
999
+ // check that ran earlier.
1000
+ //
1001
+ // NOTE ON setIdeaGrade's whole-row contract: developed_by/developed_at are
1002
+ // deliberately OUTSIDE it. That contract exists so a grade change cannot inherit
1003
+ // stale field_values; but a developer is a historical FACT about who did the
1004
+ // work, not current content, and setIdeaGrade's ON CONFLICT SET list omits both
1005
+ // columns — so a later re-score or an author's R13 resubmission preserves the
1006
+ // developer's attribution instead of silently erasing it.
1007
+ async function developSpark(ideaId, { developedBy, templateKey = DEFAULT_TEMPLATE_KEY, fieldValues = {}, completenessScore = null } = {}) {
1008
+ // Reuse the R140 payload validator so a bad template key or an oversize
1009
+ // field_values map is a structured throw here rather than a constraint
1010
+ // violation surfacing as a 500 (the assertGradePayload contract).
1011
+ assertGradePayload({ grade: 'full', templateKey, fieldValues, completenessScore });
1012
+ if (developedBy == null) throw { code: 'MISSING_DEVELOPER' };
1013
+ const { rows } = await pool.query(
1014
+ `UPDATE ideas_grades
1015
+ SET grade = 'full',
1016
+ template_key = $2,
1017
+ field_values = $3::jsonb,
1018
+ completeness_score = $4,
1019
+ developed_by = $5,
1020
+ developed_at = now()
1021
+ WHERE idea_id = $1
1022
+ AND grade = 'spark'
1023
+ AND EXISTS (
1024
+ SELECT 1 FROM idea_inbox i WHERE i.id = $1 AND i.status = 'open'
1025
+ )
1026
+ RETURNING idea_id, grade, template_key, field_values, completeness_score,
1027
+ developed_by, developed_at, created_at, updated_at`,
1028
+ [ideaId, templateKey, JSON.stringify(fieldValues), completenessScore, developedBy]
1029
+ );
1030
+ // null = "this row was not a developable spark when we looked". THREE ways to
1031
+ // get here, and the caller re-reads to say which: it was never a spark,
1032
+ // somebody else developed it first, or a triager closed the idea underneath us.
1033
+ return rows[0] ?? null;
1034
+ }
1035
+
954
1036
  // The grade row on its own. null = no row, which reads as grade='quick'
955
1037
  // everywhere the idea itself is read (see DEFAULT_GRADE).
956
1038
  async function getIdeaGrade(ideaId) {
@@ -1186,6 +1268,8 @@ module.exports = {
1186
1268
  updateIdeaBody,
1187
1269
  setIdeaGrade,
1188
1270
  setIdeaCompleteness,
1271
+ // develop a spark into a Full Idea, in place (task 1002922, BV1.R146)
1272
+ developSpark,
1189
1273
  // idea → task lineage (task 1002997 / BV1.R173) — the one-idea-many-tasks
1190
1274
  // link stream 1 (IC4) pays against
1191
1275
  linkIdeaTask,
@@ -0,0 +1,85 @@
1
+ -- ideas_005_grade_developed_by.sql — who developed this spark into a Full Idea
2
+ -- (task 1002922, BV1.R146, goal 1000062 "three grades of idea").
3
+ --
4
+ -- WHY. R144 let an author mark a quick idea as a Big Idea Spark ("I have the
5
+ -- spark but not the full picture, someone develop this") and R145 made those
6
+ -- sparks browsable. R146 is the pickup itself: another builder fills in the five
7
+ -- template fields and the spark becomes a Full Idea. The done-when requires the
8
+ -- row to keep its id and its ORIGINAL author while a DISTINCT developer is
9
+ -- recorded — and ideas_grades had nowhere to record the second person. This adds
10
+ -- that place.
11
+ --
12
+ -- WHY IN-PLACE, ON THE SAME ROW. The alternative — a new idea_inbox row for the
13
+ -- developed version — breaks every reference to the original: the hall's
14
+ -- /#/idea/N deep link, ideas_task_lineage.idea_id, the credit_log reason key
15
+ -- `idea.credit.full_pass:<id>`, and any prose that cited it. "References to the
16
+ -- idea never break" is a done-when, so development mutates the grade row and
17
+ -- leaves idea_inbox.id / captured_by / captured_at untouched. That is also why
18
+ -- the developer needs its own column instead of overwriting captured_by: the
19
+ -- author's claim on their own thought is exactly what must survive being
20
+ -- developed by someone else, and it is what the R147 credit split pays against.
21
+ --
22
+ -- WHY developed_at, AND NOT JUST updated_at. ideas_001 put a touch trigger on
23
+ -- this table, so updated_at moves on EVERY later write — a re-score, a
24
+ -- resubmission, a triage-adjacent grade rewrite. An attribution record whose
25
+ -- only timestamp drifts cannot answer "when was this developed", which is the
26
+ -- question the R147 split and any dispute about it will ask. One extra column is
27
+ -- cheaper than an unanswerable question.
28
+ --
29
+ -- SELF-DEVELOPMENT IS LEGAL, SO THERE IS NO developed_by <> captured_by CHECK.
30
+ -- An author may mark their own idea as needing development and then develop it
31
+ -- themselves (they may have had the full picture a week later). Forbidding that
32
+ -- would add a rule the owner never asked for; R147 simply sees a split where
33
+ -- both halves are the same person. The pair-coherence CHECK below is the only
34
+ -- constraint, and it guards a shape this module's own code controls.
35
+ --
36
+ -- Module migrations are ADDITIVE and NAMESPACED: every object here targets the
37
+ -- ideas_-owned ideas_grades table, no down-migration, idempotent (safe to re-run).
38
+
39
+ BEGIN;
40
+
41
+ -- ON DELETE SET NULL, matching the builders-FK posture of every other module
42
+ -- table that names a person (agents_001, government_001): losing a builder row
43
+ -- must never cascade away an idea's grade. The attribution degrades to "someone",
44
+ -- which the read path already renders (R145's queue prints "an unnamed builder").
45
+ ALTER TABLE ideas_grades
46
+ ADD COLUMN IF NOT EXISTS developed_by bigint REFERENCES builders(id) ON DELETE SET NULL;
47
+
48
+ ALTER TABLE ideas_grades
49
+ ADD COLUMN IF NOT EXISTS developed_at timestamptz;
50
+
51
+ -- The two columns are written together by developSpark() and mean nothing apart:
52
+ -- a developer with no timestamp cannot be dated, and a timestamp with no
53
+ -- developer names nobody. Defense in depth against a future partial write.
54
+ ALTER TABLE ideas_grades DROP CONSTRAINT IF EXISTS ideas_grades_developed_pair_chk;
55
+ ALTER TABLE ideas_grades ADD CONSTRAINT ideas_grades_developed_pair_chk
56
+ CHECK ((developed_by IS NULL) = (developed_at IS NULL));
57
+
58
+ -- "What has this builder developed?" — the read the R147 credit split walks, and
59
+ -- the one a builder's own profile will want. PARTIAL, because the overwhelming
60
+ -- majority of grade rows have no developer at all (every quick idea, every
61
+ -- self-authored full idea), so indexing them would be paying for empty space.
62
+ CREATE INDEX IF NOT EXISTS ideas_grades_developed_by_idx
63
+ ON ideas_grades (developed_by)
64
+ WHERE developed_by IS NOT NULL;
65
+
66
+ -- SAFETY NET — a migration that cannot prove its own effect must not report
67
+ -- success (the ideas_003 precedent). Read-only, no DDL: if either column is
68
+ -- missing, the ADD silently did nothing and R146's route would fail at runtime
69
+ -- on the one write whose whole job is preserving attribution.
70
+ DO $$
71
+ DECLARE missing text;
72
+ BEGIN
73
+ SELECT string_agg(c, ', ') INTO missing
74
+ FROM unnest(ARRAY['developed_by', 'developed_at']) AS c
75
+ WHERE NOT EXISTS (
76
+ SELECT 1 FROM information_schema.columns
77
+ WHERE table_name = 'ideas_grades' AND column_name = c
78
+ );
79
+ IF missing IS NOT NULL THEN
80
+ RAISE EXCEPTION
81
+ 'ideas_005: column(s) % missing from ideas_grades after ADD COLUMN — the migration did not take.', missing;
82
+ END IF;
83
+ END $$;
84
+
85
+ COMMIT;