@bongos/core 1.19.229 → 1.19.230
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 +33 -23
- 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 +74 -2
- package/docs/api-reference.md +3 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/government/board.js +23 -0
- package/modules/government/db.js +24 -0
- package/modules/government/migrations/government_006_returned_items_index.sql +21 -0
- package/modules/government/routes/government.js +4 -0
- package/modules/ideas/routes/inbox.js +112 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/idea_resubmit.mjs +163 -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.230",
|
|
6
|
+
"core_contract": "1.19.230",
|
|
7
|
+
"source_commit": "d0c1085d2633cda96d73c28cdccd40f29771b832",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-18T11:
|
|
9
|
+
"built_at": "2026-08-18T11:23:52.179Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 311,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1313,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1645,
|
|
20
|
+
"tree_sha256": "6054b1bf932dda1ffc30ce0208aef3e91b62da91beb916a454d51ec6deaf95b0",
|
|
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": "ad3ebff5e165c917f14840844032150cacfb40335e29df7f23d51dbb0d475d21"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "e474bac9faa5da4da5098b736fd694a9a7ee327d7b952d08a4c366e70e253d84"
|
|
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": "b0bd3bbad51ea1b68f7fd36165d77fce12939cde82b37cd2266a00c103002f4b"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "7b1983354a89000e6d630d69c896515bac844a2e23a9b8c0da66e6c9b1f386d6"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "d61a7546173a2212e72c6cbee2ec67b193c3ef506d17ddbfe84e3786fb17cbab"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1372,12 +1372,12 @@
|
|
|
1372
1372
|
{
|
|
1373
1373
|
"path": "docs/api-reference.md",
|
|
1374
1374
|
"mode": "0000644",
|
|
1375
|
-
"sha256": "
|
|
1375
|
+
"sha256": "0f5532248c4c9554c20a6fc11bc60e696fadf996e1d6f7d049553b02614d5a2a"
|
|
1376
1376
|
},
|
|
1377
1377
|
{
|
|
1378
1378
|
"path": "docs/api/openapi.json",
|
|
1379
1379
|
"mode": "0000644",
|
|
1380
|
-
"sha256": "
|
|
1380
|
+
"sha256": "a983685efd08503f73301bf509dda382191aba27321116e064562c8d895ecc73"
|
|
1381
1381
|
},
|
|
1382
1382
|
{
|
|
1383
1383
|
"path": "docs/architecture.md",
|
|
@@ -2687,7 +2687,7 @@
|
|
|
2687
2687
|
{
|
|
2688
2688
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
2689
2689
|
"mode": "0000644",
|
|
2690
|
-
"sha256": "
|
|
2690
|
+
"sha256": "b0bd3bbad51ea1b68f7fd36165d77fce12939cde82b37cd2266a00c103002f4b"
|
|
2691
2691
|
},
|
|
2692
2692
|
{
|
|
2693
2693
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -2902,7 +2902,7 @@
|
|
|
2902
2902
|
{
|
|
2903
2903
|
"path": "modules/government/board.js",
|
|
2904
2904
|
"mode": "0000644",
|
|
2905
|
-
"sha256": "
|
|
2905
|
+
"sha256": "5db387128d2f7a1ebc15fefaf1e97aabeda9196df34c91bdeb4a12e9301087f0"
|
|
2906
2906
|
},
|
|
2907
2907
|
{
|
|
2908
2908
|
"path": "modules/government/catalog.js",
|
|
@@ -2917,7 +2917,7 @@
|
|
|
2917
2917
|
{
|
|
2918
2918
|
"path": "modules/government/db.js",
|
|
2919
2919
|
"mode": "0000644",
|
|
2920
|
-
"sha256": "
|
|
2920
|
+
"sha256": "e4ff648d5a9d83048214ff111768e5a2ae5e254f4b0a2f8f2fc966ce04138e21"
|
|
2921
2921
|
},
|
|
2922
2922
|
{
|
|
2923
2923
|
"path": "modules/government/migrations/governance_001_tables.sql",
|
|
@@ -2979,6 +2979,11 @@
|
|
|
2979
2979
|
"mode": "0000644",
|
|
2980
2980
|
"sha256": "dc570f8871b578a8af965ffbc91fc4e195f2e7fce7d7886166f0025ac2b47bce"
|
|
2981
2981
|
},
|
|
2982
|
+
{
|
|
2983
|
+
"path": "modules/government/migrations/government_006_returned_items_index.sql",
|
|
2984
|
+
"mode": "0000644",
|
|
2985
|
+
"sha256": "b2c06a954e93487f7df85eb7333c65284e364499fb96c31aad30a263c36b8f73"
|
|
2986
|
+
},
|
|
2982
2987
|
{
|
|
2983
2988
|
"path": "modules/government/module.json",
|
|
2984
2989
|
"mode": "0000644",
|
|
@@ -3012,7 +3017,7 @@
|
|
|
3012
3017
|
{
|
|
3013
3018
|
"path": "modules/government/routes/government.js",
|
|
3014
3019
|
"mode": "0000644",
|
|
3015
|
-
"sha256": "
|
|
3020
|
+
"sha256": "34ff36c8ab99797a51b44cceacea2be00d73a5d2a1750d71d815a7c9111b1a3a"
|
|
3016
3021
|
},
|
|
3017
3022
|
{
|
|
3018
3023
|
"path": "modules/government/routes/rank-sync.js",
|
|
@@ -3552,7 +3557,7 @@
|
|
|
3552
3557
|
{
|
|
3553
3558
|
"path": "modules/ideas/routes/inbox.js",
|
|
3554
3559
|
"mode": "0000644",
|
|
3555
|
-
"sha256": "
|
|
3560
|
+
"sha256": "f19fa968ad6fd08b65931039ecf3b8d4b31b1aabef56b42f62cd1a4fbd06328e"
|
|
3556
3561
|
},
|
|
3557
3562
|
{
|
|
3558
3563
|
"path": "modules/ideas/templates.js",
|
|
@@ -4307,12 +4312,12 @@
|
|
|
4307
4312
|
{
|
|
4308
4313
|
"path": "package-lock.json",
|
|
4309
4314
|
"mode": "0000644",
|
|
4310
|
-
"sha256": "
|
|
4315
|
+
"sha256": "ed784184ada68e6661376d541de1491914ae688e18213b306a6315366b571c7d"
|
|
4311
4316
|
},
|
|
4312
4317
|
{
|
|
4313
4318
|
"path": "package.json",
|
|
4314
4319
|
"mode": "0000644",
|
|
4315
|
-
"sha256": "
|
|
4320
|
+
"sha256": "eb8fe0fd9cd657bba4b693eba9c949bc1bec7b64b0690a63f95e510afdf5693c"
|
|
4316
4321
|
},
|
|
4317
4322
|
{
|
|
4318
4323
|
"path": "public-docs/index.html",
|
|
@@ -5927,7 +5932,7 @@
|
|
|
5927
5932
|
{
|
|
5928
5933
|
"path": "src/module-api.js",
|
|
5929
5934
|
"mode": "0000644",
|
|
5930
|
-
"sha256": "
|
|
5935
|
+
"sha256": "a15862c3769ab8efd7c3e87f535df17f692fe31d24476a5cef22be8d890e7a16"
|
|
5931
5936
|
},
|
|
5932
5937
|
{
|
|
5933
5938
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6952,7 +6957,7 @@
|
|
|
6952
6957
|
{
|
|
6953
6958
|
"path": "tests/government_seed.mjs",
|
|
6954
6959
|
"mode": "0000644",
|
|
6955
|
-
"sha256": "
|
|
6960
|
+
"sha256": "94e10e6d635ee8a4b16b33559bdfa30b7112bcc4a1fcfa59c30ee3648278f6a4"
|
|
6956
6961
|
},
|
|
6957
6962
|
{
|
|
6958
6963
|
"path": "tests/government_session_scope.mjs",
|
|
@@ -7114,6 +7119,11 @@
|
|
|
7114
7119
|
"mode": "0000644",
|
|
7115
7120
|
"sha256": "729c511f9bb3d4a9b7fb76c7b3843e1900c8d4c9cfa30fe08e482d1e41113839"
|
|
7116
7121
|
},
|
|
7122
|
+
{
|
|
7123
|
+
"path": "tests/idea_resubmit.mjs",
|
|
7124
|
+
"mode": "0000644",
|
|
7125
|
+
"sha256": "51e1286235cbde1b56188f29b0e2bd12ac73a9637dbf1c16b325f10147256f27"
|
|
7126
|
+
},
|
|
7117
7127
|
{
|
|
7118
7128
|
"path": "tests/idea_task_lineage.mjs",
|
|
7119
7129
|
"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
|
+
- 282 operations across 46 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -383,6 +383,8 @@ function createClient(opts = {}) {
|
|
|
383
383
|
patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
|
|
384
384
|
// POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
|
|
385
385
|
postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
|
|
386
|
+
// POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
|
|
387
|
+
postInboxIdResubmit: (args) => request("POST", "/inbox/{id}/resubmit", { hasBody: true }, args),
|
|
386
388
|
},
|
|
387
389
|
"instance": {
|
|
388
390
|
// GET /instance — rank: public — GET /instance
|
|
@@ -382,6 +382,8 @@ function createClient(opts = {}) {
|
|
|
382
382
|
patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
|
|
383
383
|
// POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
|
|
384
384
|
postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
|
|
385
|
+
// POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
|
|
386
|
+
postInboxIdResubmit: (args) => request("POST", "/inbox/{id}/resubmit", { hasBody: true }, args),
|
|
385
387
|
},
|
|
386
388
|
"instance": {
|
|
387
389
|
// GET /instance — rank: public — GET /instance
|
|
@@ -237,6 +237,7 @@ export interface PostGovernmentRanksRankKeyResetResponse { ok: boolean; rank_key
|
|
|
237
237
|
export interface PostGovernmentRanksRequest { rank_key: string; label: string; description?: string }
|
|
238
238
|
export interface PostGovernmentRanksResponse { rank: unknown }
|
|
239
239
|
export interface PostInboxIdRatifyRequest { notes_md?: string }
|
|
240
|
+
export interface PostInboxIdResubmitRequest { field_values: Record<string, unknown>; template_key?: string }
|
|
240
241
|
export interface PostInboxRequest { title: string; body_md?: string; kind?: string; suggested_version?: string; suggested_priority?: number; grade?: string; template_key?: string; field_values?: Record<string, unknown> }
|
|
241
242
|
export interface PostLearningsRequest { title: string; body_md: string; tags?: string[]; related_files?: string[]; source?: string; source_ref?: string; task_id?: number }
|
|
242
243
|
export interface PostLearningsResponse { skipped?: boolean; reason?: unknown; learning?: unknown }
|
|
@@ -634,6 +635,8 @@ export interface BongosClient {
|
|
|
634
635
|
patchInboxId(args?: RequestArgs & { body?: PatchInboxIdRequest }): Promise<PatchInboxIdResponse>;
|
|
635
636
|
/** POST /inbox/{id}/ratify — rank: metic+archon */
|
|
636
637
|
postInboxIdRatify(args?: RequestArgs & { body?: PostInboxIdRatifyRequest }): Promise<ApiResponse>;
|
|
638
|
+
/** POST /inbox/{id}/resubmit — rank: any-builder */
|
|
639
|
+
postInboxIdResubmit(args: RequestArgs & { body: PostInboxIdResubmitRequest }): Promise<ApiResponse>;
|
|
637
640
|
};
|
|
638
641
|
"instance": {
|
|
639
642
|
/** GET /instance — rank: public */
|
|
@@ -379,6 +379,8 @@ export function createClient(opts = {}) {
|
|
|
379
379
|
patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
|
|
380
380
|
// POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
|
|
381
381
|
postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
|
|
382
|
+
// POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
|
|
383
|
+
postInboxIdResubmit: (args) => request("POST", "/inbox/{id}/resubmit", { hasBody: true }, args),
|
|
382
384
|
},
|
|
383
385
|
"instance": {
|
|
384
386
|
// GET /instance — rank: public — GET /instance
|
package/docs/api/openapi.json
CHANGED
|
@@ -6030,6 +6030,62 @@
|
|
|
6030
6030
|
]
|
|
6031
6031
|
}
|
|
6032
6032
|
},
|
|
6033
|
+
"/inbox/{id}/resubmit": {
|
|
6034
|
+
"post": {
|
|
6035
|
+
"operationId": "post_inbox_id_resubmit",
|
|
6036
|
+
"tags": [
|
|
6037
|
+
"inbox"
|
|
6038
|
+
],
|
|
6039
|
+
"summary": "POST /inbox/:id/resubmit",
|
|
6040
|
+
"description": "RESUBMIT a Full Idea after a rejection (R13 of goal 1000069, criterion C2: an idea never dies and never stalls). AUTHOR-ONLY, enforced in-handler against captured_by: the rejection went back to the originator, and only they can answer it — the same ownership posture as marking a spark (R144). Unbounded by design (the grade gate bounds spam), and every past rejection stays visible: the closed board items and their objection votes are permanent rows, and this writes a NEW grade + opens a NEW window rather than editing history. THE FEEDBACK LOOP, both rejection sources, one shape: • a board return → the objection (reason + flagged section) off the most recent returned item, via the government port; • a sub-bar grade → the weakest-section flag, recomputed from the OLD field_values (derived, never stored — R09). The response says whether the FLAGGED SECTION actually changed, computed per-field exactly because completeness.js scores per-field: a resubmission that reworked everything EXCEPT what was objected to is told so plainly.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
6041
|
+
"x-rank": "any-builder",
|
|
6042
|
+
"x-source": "modules/ideas/routes/inbox.js",
|
|
6043
|
+
"parameters": [
|
|
6044
|
+
{
|
|
6045
|
+
"name": "id",
|
|
6046
|
+
"in": "path",
|
|
6047
|
+
"required": true,
|
|
6048
|
+
"schema": {
|
|
6049
|
+
"type": "string"
|
|
6050
|
+
},
|
|
6051
|
+
"description": "Path parameter `id`."
|
|
6052
|
+
}
|
|
6053
|
+
],
|
|
6054
|
+
"requestBody": {
|
|
6055
|
+
"required": true,
|
|
6056
|
+
"content": {
|
|
6057
|
+
"application/json": {
|
|
6058
|
+
"schema": {
|
|
6059
|
+
"$ref": "#/components/schemas/PostInboxIdResubmitRequest"
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
},
|
|
6063
|
+
"x-validated": true
|
|
6064
|
+
},
|
|
6065
|
+
"responses": {
|
|
6066
|
+
"200": {
|
|
6067
|
+
"description": "Success."
|
|
6068
|
+
},
|
|
6069
|
+
"400": {
|
|
6070
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
6071
|
+
},
|
|
6072
|
+
"401": {
|
|
6073
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
6074
|
+
},
|
|
6075
|
+
"403": {
|
|
6076
|
+
"$ref": "#/components/responses/Forbidden"
|
|
6077
|
+
},
|
|
6078
|
+
"404": {
|
|
6079
|
+
"$ref": "#/components/responses/NotFound"
|
|
6080
|
+
}
|
|
6081
|
+
},
|
|
6082
|
+
"security": [
|
|
6083
|
+
{
|
|
6084
|
+
"builderSession": []
|
|
6085
|
+
}
|
|
6086
|
+
]
|
|
6087
|
+
}
|
|
6088
|
+
},
|
|
6033
6089
|
"/instance": {
|
|
6034
6090
|
"get": {
|
|
6035
6091
|
"operationId": "get_instance",
|
|
@@ -16102,6 +16158,22 @@
|
|
|
16102
16158
|
},
|
|
16103
16159
|
"additionalProperties": false
|
|
16104
16160
|
},
|
|
16161
|
+
"PostInboxIdResubmitRequest": {
|
|
16162
|
+
"type": "object",
|
|
16163
|
+
"properties": {
|
|
16164
|
+
"field_values": {
|
|
16165
|
+
"type": "object"
|
|
16166
|
+
},
|
|
16167
|
+
"template_key": {
|
|
16168
|
+
"type": "string",
|
|
16169
|
+
"maxLength": 64
|
|
16170
|
+
}
|
|
16171
|
+
},
|
|
16172
|
+
"required": [
|
|
16173
|
+
"field_values"
|
|
16174
|
+
],
|
|
16175
|
+
"additionalProperties": false
|
|
16176
|
+
},
|
|
16105
16177
|
"PostInboxRequest": {
|
|
16106
16178
|
"type": "object",
|
|
16107
16179
|
"properties": {
|
|
@@ -17809,8 +17881,8 @@
|
|
|
17809
17881
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
17810
17882
|
}
|
|
17811
17883
|
},
|
|
17812
|
-
"x-endpoint-count":
|
|
17813
|
-
"x-schema-count":
|
|
17884
|
+
"x-endpoint-count": 282,
|
|
17885
|
+
"x-schema-count": 319,
|
|
17814
17886
|
"x-undocumented-bodies": 8,
|
|
17815
17887
|
"x-response-schemas": 214,
|
|
17816
17888
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
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. 282 endpoints across 46 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`.
|
|
@@ -243,7 +243,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
243
243
|
|---|---|---|---|---|
|
|
244
244
|
| GET | `/api/bongos/healthz` | `public` | — | liveness probe; status.example.com hits this anonymously. |
|
|
245
245
|
|
|
246
|
-
## `inbox` (
|
|
246
|
+
## `inbox` (6)
|
|
247
247
|
|
|
248
248
|
| Method | Path | Rank | Body | Description |
|
|
249
249
|
|---|---|---|---|---|
|
|
@@ -252,6 +252,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
252
252
|
| GET | `/api/bongos/inbox/:id` | `any-builder` | — | single idea read for the "#NNN" deep-link resolver (task 1047). |
|
|
253
253
|
| PATCH | `/api/bongos/inbox/:id` | `metic+archon` | `status`, `kind`, `promoted_to_task_id`, `reviewed_at`, `merged_into_idea_id`, `triage_note` | Metic+ (task 360 / ADR 0018): idea triage (promote/discard/merge) shapes the backlog. |
|
|
254
254
|
| 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… |
|
|
255
|
+
| 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). |
|
|
255
256
|
|
|
256
257
|
## `instance` (1)
|
|
257
258
|
|
|
@@ -382,6 +382,8 @@ function createClient(opts = {}) {
|
|
|
382
382
|
patchInboxId: (args) => request("PATCH", "/inbox/{id}", { hasBody: true }, args),
|
|
383
383
|
// POST /inbox/{id}/ratify — rank: metic+archon — POST /inbox/:id/ratify
|
|
384
384
|
postInboxIdRatify: (args) => request("POST", "/inbox/{id}/ratify", { hasBody: true }, args),
|
|
385
|
+
// POST /inbox/{id}/resubmit — rank: any-builder — POST /inbox/:id/resubmit
|
|
386
|
+
postInboxIdResubmit: (args) => request("POST", "/inbox/{id}/resubmit", { hasBody: true }, args),
|
|
385
387
|
},
|
|
386
388
|
"instance": {
|
|
387
389
|
// GET /instance — rank: public — GET /instance
|
|
@@ -351,6 +351,28 @@ async function maybeCloseAfterVote(itemId) {
|
|
|
351
351
|
return closeItem(item, { cause: 'vote' });
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
+
// ── R13's read: the feedback a returned idea carries back to its author ─────
|
|
355
|
+
//
|
|
356
|
+
// One reasoned objection is what returns an idea (§3), so the feedback IS that
|
|
357
|
+
// vote's reason + flagged section, straight off the most recent returned item.
|
|
358
|
+
// Null when this idea has never been returned by the board — the caller (the
|
|
359
|
+
// ideas resubmit path) falls back to the sub-bar weakest-section flag, which is
|
|
360
|
+
// derivable from the stored field_values at any time (R09). Shape mirrors the
|
|
361
|
+
// vote payload so the return flow renders ONE thing for both rejection sources.
|
|
362
|
+
async function latestRejectionForIdea(ideaId) {
|
|
363
|
+
const id = Number(ideaId);
|
|
364
|
+
if (!Number.isFinite(id) || id <= 0) return null;
|
|
365
|
+
const row = await db.getLatestReturnedItemWithObjection('full_idea', id);
|
|
366
|
+
if (!row) return null;
|
|
367
|
+
return Object.freeze({
|
|
368
|
+
source: 'board',
|
|
369
|
+
item_id: row.item_id,
|
|
370
|
+
section_key: row.section_key || null,
|
|
371
|
+
reason_md: row.reason_md || null,
|
|
372
|
+
closed_at: row.closed_at,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
354
376
|
module.exports = {
|
|
355
377
|
FULL_IDEA_PASS_BAR,
|
|
356
378
|
ideaPassesBar,
|
|
@@ -366,4 +388,5 @@ module.exports = {
|
|
|
366
388
|
passPayload,
|
|
367
389
|
closeItem,
|
|
368
390
|
maybeCloseAfterVote,
|
|
391
|
+
latestRejectionForIdea,
|
|
369
392
|
};
|
package/modules/government/db.js
CHANGED
|
@@ -437,6 +437,29 @@ async function getIdeaCompleteness(ideaId) {
|
|
|
437
437
|
return rows[0] ? rows[0].completeness_score : null;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
// R13's read: the most recent RETURNED item for a subject, carrying its
|
|
441
|
+
// objection (the single 'no' vote — one reasoned objection is what returns an
|
|
442
|
+
// idea, ADR 0175 §3). LEFT JOIN because a returned-by-expiry item could in
|
|
443
|
+
// principle carry no objection row; the caller falls back to the sub-bar flag.
|
|
444
|
+
async function getLatestReturnedItemWithObjection(subjectType, subjectId) {
|
|
445
|
+
// Ordering is doubly deterministic: the LATEST returned item, then that
|
|
446
|
+
// item's FIRST objection (v.cast_at, id as the tiebreak). Under consent
|
|
447
|
+
// several members can object before the sitting ends; the first objection is
|
|
448
|
+
// the one that decided the outcome, so it is the feedback the author answers.
|
|
449
|
+
// Indexed by government_006's returned-items index — this runs on every
|
|
450
|
+
// resubmit, and the items table only grows.
|
|
451
|
+
const { rows } = await pool.query(
|
|
452
|
+
`SELECT i.id AS item_id, i.closed_at, v.reason_md, v.section_key, v.voter_id
|
|
453
|
+
FROM government_board_items i
|
|
454
|
+
LEFT JOIN government_board_votes v ON v.item_id = i.id AND v.direction = 'no'
|
|
455
|
+
WHERE i.subject_type = $1 AND i.subject_id = $2 AND i.outcome = 'returned'
|
|
456
|
+
ORDER BY i.closed_at DESC, v.cast_at ASC, v.id ASC
|
|
457
|
+
LIMIT 1`,
|
|
458
|
+
[subjectType, subjectId],
|
|
459
|
+
);
|
|
460
|
+
return rows[0] || null;
|
|
461
|
+
}
|
|
462
|
+
|
|
440
463
|
// The expiry sweep's read (R11): every OPEN item whose deadline has passed, on
|
|
441
464
|
// the database clock, oldest deadline first. Served by the partial index
|
|
442
465
|
// government_board_items_open_by_deadline (government_003 built it for exactly
|
|
@@ -507,4 +530,5 @@ module.exports = {
|
|
|
507
530
|
closeBoardItem,
|
|
508
531
|
getIdeaCompleteness,
|
|
509
532
|
getExpiredOpenBoardItems,
|
|
533
|
+
getLatestReturnedItemWithObjection,
|
|
510
534
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- government_006_returned_items_index.sql — index the returned-item lookup
|
|
2
|
+
-- (task 1003026 / R13 of goal 1000069; a grader efficiency finding, and right).
|
|
3
|
+
--
|
|
4
|
+
-- getLatestReturnedItemWithObjection runs on EVERY resubmit: "the most recent
|
|
5
|
+
-- RETURNED item for this subject". government_board_items only grows — one row
|
|
6
|
+
-- per window, resubmission is unbounded by design — and the table's two
|
|
7
|
+
-- existing partial indexes both cover OPEN items (closed_at IS NULL), so this
|
|
8
|
+
-- closed-row lookup would otherwise scan historical volume that scales with
|
|
9
|
+
-- every sitting the project has ever held.
|
|
10
|
+
--
|
|
11
|
+
-- Partial on outcome='returned' (the only outcome the read asks for), keyed to
|
|
12
|
+
-- match the query's WHERE + ORDER BY exactly. Additive, idempotent, no rows
|
|
13
|
+
-- touched — safe on any database state.
|
|
14
|
+
|
|
15
|
+
BEGIN;
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS government_board_items_returned_by_subject
|
|
18
|
+
ON government_board_items (subject_type, subject_id, closed_at DESC)
|
|
19
|
+
WHERE outcome = 'returned';
|
|
20
|
+
|
|
21
|
+
COMMIT;
|
|
@@ -91,6 +91,10 @@ const governmentPort = {
|
|
|
91
91
|
// bar gate lives INSIDE board.js (a sub-bar idea never reaches the board, and
|
|
92
92
|
// no caller is trusted to pre-filter). Returns, never throws into capture.
|
|
93
93
|
openBoardWindowForIdea: board.openWindowForFullIdea,
|
|
94
|
+
// R13: the feedback a board-returned idea carries back to its author — the
|
|
95
|
+
// resubmit path in modules/ideas resolves this to show what was objected to
|
|
96
|
+
// and to check whether the flagged section actually changed.
|
|
97
|
+
latestRejectionForIdea: board.latestRejectionForIdea,
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
function registerSeams() {
|
|
@@ -254,6 +254,118 @@ module.exports = function buildInboxRouter() {
|
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
// rank: any-builder — RESUBMIT a Full Idea after a rejection (R13 of goal
|
|
258
|
+
// 1000069, criterion C2: an idea never dies and never stalls). AUTHOR-ONLY,
|
|
259
|
+
// enforced in-handler against captured_by: the rejection went back to the
|
|
260
|
+
// originator, and only they can answer it — the same ownership posture as
|
|
261
|
+
// marking a spark (R144). Unbounded by design (the grade gate bounds spam),
|
|
262
|
+
// and every past rejection stays visible: the closed board items and their
|
|
263
|
+
// objection votes are permanent rows, and this writes a NEW grade + opens a
|
|
264
|
+
// NEW window rather than editing history.
|
|
265
|
+
//
|
|
266
|
+
// THE FEEDBACK LOOP, both rejection sources, one shape:
|
|
267
|
+
// • a board return → the objection (reason + flagged section) off the most
|
|
268
|
+
// recent returned item, via the government port;
|
|
269
|
+
// • a sub-bar grade → the weakest-section flag, recomputed from the OLD
|
|
270
|
+
// field_values (derived, never stored — R09).
|
|
271
|
+
// The response says whether the FLAGGED SECTION actually changed, computed
|
|
272
|
+
// per-field exactly because completeness.js scores per-field: a resubmission
|
|
273
|
+
// that reworked everything EXCEPT what was objected to is told so plainly.
|
|
274
|
+
router.post('/inbox/:id/resubmit', api.requireBuilder, async (req, res) => {
|
|
275
|
+
const id = parseId(req, res, { code: 'bad_id', positiveInt: true });
|
|
276
|
+
if (id === null) return;
|
|
277
|
+
if (validateOrRespond(req, res, {
|
|
278
|
+
field_values: { required: true, type: 'object' },
|
|
279
|
+
template_key: { type: 'string', maxLength: 64 },
|
|
280
|
+
})) return;
|
|
281
|
+
try {
|
|
282
|
+
const idea = await inbox.getIdea(id);
|
|
283
|
+
if (!idea) return res.fail('idea_not_found', 404);
|
|
284
|
+
if (String(idea.captured_by) !== String(req.builder.id)) {
|
|
285
|
+
return res.fail('not_your_idea', 403, { reason: 'a rejection returns to its ORIGINATOR; only they can resubmit' });
|
|
286
|
+
}
|
|
287
|
+
if (idea.grade !== 'full') {
|
|
288
|
+
return res.fail('not_a_full_idea', 409, { grade: idea.grade, reason: 'resubmission is the Full Idea loop; a quick idea has no grade to answer' });
|
|
289
|
+
}
|
|
290
|
+
const templateKey = req.body.template_key ?? idea.template_key ?? undefined;
|
|
291
|
+
let v;
|
|
292
|
+
try {
|
|
293
|
+
v = validateFieldValues(req.body.field_values, templateKey ? { templateKey } : undefined);
|
|
294
|
+
} catch (validationErr) {
|
|
295
|
+
console.error('[gds] POST /inbox/:id/resubmit validation threw:', validationErr);
|
|
296
|
+
return res.fail('invalid_field_values', 400, { errors: [{ field: null, reason: 'unvalidatable' }] });
|
|
297
|
+
}
|
|
298
|
+
if (!v.ok) return res.fail('invalid_field_values', 400, { errors: v.errors, template_key: templateKey ?? null });
|
|
299
|
+
|
|
300
|
+
// What was flagged, before the old values are overwritten. Board return
|
|
301
|
+
// first (the human objection outranks the mechanical flag); sub-bar flag
|
|
302
|
+
// as the fallback; null when this idea was never rejected at all (a
|
|
303
|
+
// resubmit is then just a revision — still legal, still rescored).
|
|
304
|
+
const government = api.resolveOptional && api.resolveOptional('government');
|
|
305
|
+
let rejection = null;
|
|
306
|
+
if (government && government.latestRejectionForIdea) {
|
|
307
|
+
try { rejection = await government.latestRejectionForIdea(id); }
|
|
308
|
+
catch (e) { console.error('[gds] resubmit: rejection lookup failed (continuing without):', e); }
|
|
309
|
+
}
|
|
310
|
+
if (!rejection || !rejection.section_key) {
|
|
311
|
+
const flag = weakestSectionPayload(idea.field_values, templateKey ? { templateKey } : undefined);
|
|
312
|
+
if (flag) rejection = { source: 'grade', ...flag };
|
|
313
|
+
}
|
|
314
|
+
const flaggedKey = rejection && rejection.section_key ? rejection.section_key : null;
|
|
315
|
+
const oldFlagged = flaggedKey && idea.field_values ? String(idea.field_values[flaggedKey] ?? '').trim() : null;
|
|
316
|
+
const newFlagged = flaggedKey ? String(req.body.field_values[flaggedKey] ?? '').trim() : null;
|
|
317
|
+
const flaggedSectionChanged = flaggedKey ? oldFlagged !== newFlagged : null;
|
|
318
|
+
|
|
319
|
+
// The whole-row grade write (setIdeaGrade's documented semantics — a
|
|
320
|
+
// partial merge would inherit stale fields, the exact bug its contract
|
|
321
|
+
// names), then the SAME post-grade chain capture runs: the idempotent
|
|
322
|
+
// stream-2 credit (an idea that first passes on resubmission earns it
|
|
323
|
+
// now; an already-paid one books nothing), and the R07 window hook — the
|
|
324
|
+
// prior item is CLOSED, so the partial unique index admits a NEW window.
|
|
325
|
+
const row = await inbox.setIdeaGrade(id, {
|
|
326
|
+
grade: 'full',
|
|
327
|
+
...(templateKey ? { templateKey } : {}),
|
|
328
|
+
fieldValues: req.body.field_values,
|
|
329
|
+
completenessScore: completenessScore(req.body.field_values, templateKey ? { templateKey } : undefined),
|
|
330
|
+
});
|
|
331
|
+
const out = { ...idea, grade: row.grade, template_key: row.template_key, field_values: row.field_values, completeness_score: row.completeness_score };
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const reward = api.resolveOptional && api.resolveOptional('reward');
|
|
335
|
+
if (reward && reward.awardIdeaFullPassCredit) {
|
|
336
|
+
const r = await reward.awardIdeaFullPassCredit(api.pool, { ideaId: id, authorId: req.builder.id, completenessScore: row.completeness_score });
|
|
337
|
+
if (r && r.credited > 0) out.ideator_credit = r.credited;
|
|
338
|
+
}
|
|
339
|
+
} catch (creditErr) {
|
|
340
|
+
console.error('[gds] resubmit: pass-credit failed (regrade intact, credit skipped):', creditErr);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let board_window;
|
|
344
|
+
try {
|
|
345
|
+
if (government && government.openBoardWindowForIdea) {
|
|
346
|
+
const w = await government.openBoardWindowForIdea({ ideaId: id, completenessScore: row.completeness_score });
|
|
347
|
+
if (w && w.opened && w.item) board_window = { item_id: w.item.id, closes_at: w.item.closes_at };
|
|
348
|
+
else if (w && w.reason === 'below_bar') {
|
|
349
|
+
const flag = weakestSectionPayload(req.body.field_values, templateKey ? { templateKey } : undefined);
|
|
350
|
+
if (flag) out.rejection = flag; // still short — the NEW weakest section, fresh feedback
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (boardErr) {
|
|
354
|
+
console.error('[gds] resubmit: board-window open failed (regrade intact, window skipped):', boardErr);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
res.json({
|
|
358
|
+
idea: out,
|
|
359
|
+
resubmitted: true,
|
|
360
|
+
...(flaggedKey ? { flagged_section: flaggedKey, flagged_section_changed: flaggedSectionChanged, rejection_source: rejection.source || 'board' } : {}),
|
|
361
|
+
...(board_window ? { board_window } : {}),
|
|
362
|
+
});
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error('[gds] POST /inbox/:id/resubmit', err);
|
|
365
|
+
res.fail('resubmit_failed', { status: 500, message: 'internal error' });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
257
369
|
// Metic+ (#360 / ADR 0018): idea triage (promote/discard/merge) shapes the
|
|
258
370
|
// backlog. Criterion C2 assigns triage to Metic, so this widened from
|
|
259
371
|
// archon-only to metic+archon when the three-rank model went live. Any
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.230",
|
|
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.230",
|
|
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.230",
|
|
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/src/module-api.js
CHANGED
|
@@ -885,8 +885,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
885
885
|
// landed since 1.19.227 with no explicit bump. run 32101659669. (task 1002620)
|
|
886
886
|
// 1.19.229 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
887
887
|
// landed since 1.19.228 with no explicit bump. run 32130159805. (task 1002620)
|
|
888
|
+
// 1.19.230 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
889
|
+
// landed since 1.19.229 with no explicit bump. run 32131450528. (task 1002620)
|
|
888
890
|
// ---------------------------------------------------------------------------
|
|
889
|
-
const CORE_VERSION = '1.19.
|
|
891
|
+
const CORE_VERSION = '1.19.230'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
890
892
|
|
|
891
893
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
892
894
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -38,6 +38,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
38
38
|
'government_002_rename_permission_keys.sql', // ADR 0174 key rename (task 1003006)
|
|
39
39
|
'government_003_board_tables.sql', // ADR 0175 Board Room schema (task 1003016)
|
|
40
40
|
'government_005_seeded_rank_descriptions.sql', // ADR 0174 seeded-description text fix (task 1003083)
|
|
41
|
+
'government_006_returned_items_index.sql', // R13 returned-item lookup index (task 1003026)
|
|
41
42
|
]);
|
|
42
43
|
|
|
43
44
|
// Permission keys RENAMED after the grant migrations that first granted them.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// tests/idea_resubmit.mjs — return-to-originator, and resubmission (BV1.R13 of
|
|
2
|
+
// goal 1000069, criterion C2: a rejected idea never dies and never stalls).
|
|
3
|
+
//
|
|
4
|
+
// DB-free the idea_full_capture way: pool.query patched on the doorway, the
|
|
5
|
+
// REAL route handler driven through a stub req/res, a stub `government`
|
|
6
|
+
// provider registered on the seam registry (hasProvider-guarded, so this
|
|
7
|
+
// process's stub wins and the real module never loads a DB).
|
|
8
|
+
//
|
|
9
|
+
// What must hold:
|
|
10
|
+
// 1. AUTHOR-ONLY — the rejection went to the originator; anyone else is 403.
|
|
11
|
+
// 2. THE FLAGGED SECTION IS CHECKED per-field: a resubmission that reworked
|
|
12
|
+
// everything EXCEPT what was objected to is told flagged_section_changed
|
|
13
|
+
// false; one that answered it is told true.
|
|
14
|
+
// 3. The board objection OUTRANKS the mechanical sub-bar flag; the flag is
|
|
15
|
+
// the fallback when the board never returned it.
|
|
16
|
+
// 4. A passing resubmission re-opens a window (the prior item is closed, so
|
|
17
|
+
// R07's partial unique index admits a new one); a still-sub-bar one comes
|
|
18
|
+
// back with FRESH feedback on the new weakest section.
|
|
19
|
+
// 5. A quick idea cannot resubmit (409) — this is the Full Idea loop.
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const api = require('../src/module-api.js');
|
|
25
|
+
|
|
26
|
+
// ---- the government port stub (registered BEFORE the routes load) ----------
|
|
27
|
+
let rejectionAnswer = null; // what latestRejectionForIdea returns
|
|
28
|
+
let windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
29
|
+
const portCalls = [];
|
|
30
|
+
if (!api.hasProvider('government')) {
|
|
31
|
+
api.registerProvider('government', {
|
|
32
|
+
latestRejectionForIdea: async (id) => { portCalls.push(['rejection', id]); return rejectionAnswer; },
|
|
33
|
+
openBoardWindowForIdea: async (args) => { portCalls.push(['open', args]); return windowAnswer; },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- the pool stub ----------------------------------------------------------
|
|
38
|
+
let queries = [];
|
|
39
|
+
let storedIdea = null; // what GET-by-id returns
|
|
40
|
+
api.pool.query = async (sql, params = []) => {
|
|
41
|
+
queries.push({ sql, params });
|
|
42
|
+
if (/FROM idea_inbox i/i.test(sql) && /WHERE i\.id = \$1/i.test(sql)) {
|
|
43
|
+
return { rows: storedIdea ? [storedIdea] : [] };
|
|
44
|
+
}
|
|
45
|
+
if (/INSERT INTO ideas_grades/i.test(sql)) {
|
|
46
|
+
return { rows: [{ idea_id: params[0], grade: params[1], template_key: params[2], field_values: JSON.parse(params[3]), completeness_score: params[4] }] };
|
|
47
|
+
}
|
|
48
|
+
return { rows: [] };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const { BASE_TEMPLATE } = require('../modules/ideas/templates.js');
|
|
52
|
+
const routesFactory = require('../modules/ideas/routes/inbox.js');
|
|
53
|
+
const router = routesFactory({});
|
|
54
|
+
const layer = router.stack.find((l) => l.route && l.route.path === '/inbox/:id/resubmit' && l.route.methods.post);
|
|
55
|
+
assert.ok(layer, 'the resubmit route is declared');
|
|
56
|
+
const handlers = layer.route.stack.map((s) => s.handle);
|
|
57
|
+
const resubmit = handlers[handlers.length - 1];
|
|
58
|
+
|
|
59
|
+
function call(bodyIn, { builderId = '9', id = '77' } = {}) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const req = { body: bodyIn, params: { id }, builder: { id: builderId, rank: 'metic' }, query: {} };
|
|
62
|
+
const res = {
|
|
63
|
+
statusCode: 200,
|
|
64
|
+
status(c) { this.statusCode = c; return this; },
|
|
65
|
+
json(payload) { resolve({ status: this.statusCode, payload }); },
|
|
66
|
+
fail(code, status, extra) {
|
|
67
|
+
const s = typeof status === 'number' ? status : (status && status.status) || 500;
|
|
68
|
+
resolve({ status: s, payload: { error: code, ...(extra || {}) } });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
resubmit(req, res);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const FULL = Object.fromEntries(
|
|
76
|
+
BASE_TEMPLATE.fields.map((f) => [f.key, `A substantive answer for ${f.key}, long enough to clear the floor.`]),
|
|
77
|
+
);
|
|
78
|
+
const fullIdea = (over = {}) => ({
|
|
79
|
+
id: '77', title: 'T', status: 'open', captured_by: '9',
|
|
80
|
+
grade: 'full', template_key: 'base-v1', field_values: { ...FULL, fit: 'thin' }, completeness_score: 55,
|
|
81
|
+
...over,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let passed = 0, failed = 0;
|
|
85
|
+
async function test(name, fn) {
|
|
86
|
+
queries = []; portCalls.length = 0;
|
|
87
|
+
rejectionAnswer = null; windowAnswer = { opened: true, item: { id: '31', closes_at: null } };
|
|
88
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
89
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await test('author-only: anyone else is 403, and nothing is written', async () => {
|
|
93
|
+
storedIdea = fullIdea();
|
|
94
|
+
const r = await call({ field_values: FULL }, { builderId: '4' });
|
|
95
|
+
assert.equal(r.status, 403);
|
|
96
|
+
assert.equal(r.payload.error, 'not_your_idea');
|
|
97
|
+
assert.ok(!queries.some((q) => /INSERT INTO ideas_grades/i.test(q.sql)), 'no grade write');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await test('a quick idea cannot resubmit — the Full Idea loop only', async () => {
|
|
101
|
+
storedIdea = fullIdea({ grade: 'quick', template_key: null, field_values: null });
|
|
102
|
+
const r = await call({ field_values: FULL });
|
|
103
|
+
assert.equal(r.status, 409);
|
|
104
|
+
assert.equal(r.payload.error, 'not_a_full_idea');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('a board objection outranks the mechanical flag, and an ANSWERED section reads changed:true', async () => {
|
|
108
|
+
storedIdea = fullIdea();
|
|
109
|
+
rejectionAnswer = { source: 'board', item_id: '30', section_key: 'purpose', reason_md: 'purpose is vague', closed_at: 'T' };
|
|
110
|
+
const revised = { ...FULL, purpose: 'A completely reworked purpose that answers the objection head-on.' };
|
|
111
|
+
const r = await call({ field_values: revised });
|
|
112
|
+
assert.equal(r.status, 200);
|
|
113
|
+
assert.equal(r.payload.flagged_section, 'purpose', 'the human objection wins over the weakest-section flag');
|
|
114
|
+
assert.equal(r.payload.flagged_section_changed, true);
|
|
115
|
+
assert.equal(r.payload.rejection_source, 'board');
|
|
116
|
+
assert.deepEqual(r.payload.board_window, { item_id: '31', closes_at: null }, 'a new window opened');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('reworking everything EXCEPT the flagged section reads changed:false', async () => {
|
|
120
|
+
storedIdea = fullIdea();
|
|
121
|
+
rejectionAnswer = { source: 'board', item_id: '30', section_key: 'purpose', reason_md: 'r', closed_at: 'T' };
|
|
122
|
+
const revised = Object.fromEntries(Object.entries(FULL).map(([k, v]) => [k, k === 'purpose' ? v : `${v} — now even better.`]));
|
|
123
|
+
storedIdea = fullIdea({ field_values: FULL });
|
|
124
|
+
const r = await call({ field_values: revised });
|
|
125
|
+
assert.equal(r.payload.flagged_section_changed, false, 'told plainly: the objected-to section did not move');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await test('with no board return, the sub-bar weakest-section flag is the fallback', async () => {
|
|
129
|
+
storedIdea = fullIdea(); // old fit is 'thin' — the mechanical weakest
|
|
130
|
+
rejectionAnswer = null;
|
|
131
|
+
const revised = { ...FULL, fit: 'A fit section with real substance now, comfortably past the floor.' };
|
|
132
|
+
const r = await call({ field_values: revised });
|
|
133
|
+
assert.equal(r.payload.flagged_section, 'fit');
|
|
134
|
+
assert.equal(r.payload.rejection_source, 'grade');
|
|
135
|
+
assert.equal(r.payload.flagged_section_changed, true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await test('a still-sub-bar resubmission gets FRESH feedback and no window', async () => {
|
|
139
|
+
storedIdea = fullIdea();
|
|
140
|
+
windowAnswer = { opened: false, reason: 'below_bar' };
|
|
141
|
+
const stillThin = { ...FULL, big_picture: 'x' }; // new weakest is big_picture
|
|
142
|
+
const r = await call({ field_values: stillThin });
|
|
143
|
+
assert.equal(r.status, 200);
|
|
144
|
+
assert.equal(r.payload.board_window, undefined);
|
|
145
|
+
assert.equal(r.payload.idea.rejection.section_key, 'big_picture', 'the NEW weakest section, not the old flag');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await test('an invalid field set is a 400 naming the field, before any write', async () => {
|
|
149
|
+
storedIdea = fullIdea();
|
|
150
|
+
const r = await call({ field_values: { ...FULL, purpose: 42 } });
|
|
151
|
+
assert.equal(r.status, 400);
|
|
152
|
+
assert.equal(r.payload.error, 'invalid_field_values');
|
|
153
|
+
assert.ok(!queries.some((q) => /INSERT INTO ideas_grades/i.test(q.sql)));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await test('a missing idea is 404', async () => {
|
|
157
|
+
storedIdea = null;
|
|
158
|
+
const r = await call({ field_values: FULL });
|
|
159
|
+
assert.equal(r.status, 404);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(`\nidea_resubmit: ${passed} passed, ${failed} failed`);
|
|
163
|
+
if (failed > 0) process.exit(1);
|