@bongos/core 1.19.647 → 1.19.648
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 +32 -22
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +5 -0
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-tasks.js +13 -1
- package/modules/lifecycle/db.js +40 -64
- package/modules/lifecycle/routes/task-write-routes.js +28 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-flow.js +69 -10
- package/src/module-api.js +1 -1
- package/tests/home_bounded_reads.mjs +14 -5
- package/tests/lifecycle_facade_surface.mjs +112 -0
- package/tests/migration_allocation.mjs +28 -0
- package/tests/patch_gate_ordering.mjs +4 -0
- package/tests/patch_value_summary_e2e.mjs +142 -0
- package/tests/ship_cannot_lie.mjs +42 -13
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.648",
|
|
6
|
+
"core_contract": "1.19.648",
|
|
7
|
+
"source_commit": "0606a231569b44bdc2521fcee567031ea8ea6f08",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-10T19:06:50.891Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 470,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2109,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2603,
|
|
20
|
+
"tree_sha256": "3260c3c3da2ae8a6b6630824444ada3145268a70b9194585231bb3391392db80",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/index.d.ts",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "5ef1befd1a1b30e8ef9e8b8ca08f768361d2422f6ddba2160a5395d46a84d443"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1892,12 +1892,12 @@
|
|
|
1892
1892
|
{
|
|
1893
1893
|
"path": "docs/api-reference.md",
|
|
1894
1894
|
"mode": "0000644",
|
|
1895
|
-
"sha256": "
|
|
1895
|
+
"sha256": "6c5f26c9c00387ecc84ed80726a4d62eca0f017f1d75c304d758961749fe616f"
|
|
1896
1896
|
},
|
|
1897
1897
|
{
|
|
1898
1898
|
"path": "docs/api/openapi.json",
|
|
1899
1899
|
"mode": "0000644",
|
|
1900
|
-
"sha256": "
|
|
1900
|
+
"sha256": "d63a4ab6e92a24d339a632360fb92246512e09eabbc5bff632881bda6d4abf50"
|
|
1901
1901
|
},
|
|
1902
1902
|
{
|
|
1903
1903
|
"path": "docs/architecture.md",
|
|
@@ -2772,7 +2772,7 @@
|
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/module-api-changelog.md",
|
|
2774
2774
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2775
|
+
"sha256": "f646af7c4521017e44e5dfef0b160c18907bfa0f07b5986603593485a34f2742"
|
|
2776
2776
|
},
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/modules-contract.md",
|
|
@@ -5662,7 +5662,7 @@
|
|
|
5662
5662
|
{
|
|
5663
5663
|
"path": "modules/lifecycle/db-tasks.js",
|
|
5664
5664
|
"mode": "0000644",
|
|
5665
|
-
"sha256": "
|
|
5665
|
+
"sha256": "fb6503bdaf013a13752968844693cbf957484f033b9df27a66f93eacb91c2cfa"
|
|
5666
5666
|
},
|
|
5667
5667
|
{
|
|
5668
5668
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -5672,7 +5672,7 @@
|
|
|
5672
5672
|
{
|
|
5673
5673
|
"path": "modules/lifecycle/db.js",
|
|
5674
5674
|
"mode": "0000644",
|
|
5675
|
-
"sha256": "
|
|
5675
|
+
"sha256": "a568b425560d8d8f4523995442454233b69016a31934810c5d4b7d83baab0b09"
|
|
5676
5676
|
},
|
|
5677
5677
|
{
|
|
5678
5678
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5902,7 +5902,7 @@
|
|
|
5902
5902
|
{
|
|
5903
5903
|
"path": "modules/lifecycle/routes/task-write-routes.js",
|
|
5904
5904
|
"mode": "0000644",
|
|
5905
|
-
"sha256": "
|
|
5905
|
+
"sha256": "25bfb492572d553dbd19595addc0c337706ac2c83df71fb04d7fa199b07b715b"
|
|
5906
5906
|
},
|
|
5907
5907
|
{
|
|
5908
5908
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
@@ -7727,12 +7727,12 @@
|
|
|
7727
7727
|
{
|
|
7728
7728
|
"path": "package-lock.json",
|
|
7729
7729
|
"mode": "0000644",
|
|
7730
|
-
"sha256": "
|
|
7730
|
+
"sha256": "83135ce62c34647b4bc57872526cb9f550a9fef2148c585839d57c600763961b"
|
|
7731
7731
|
},
|
|
7732
7732
|
{
|
|
7733
7733
|
"path": "package.json",
|
|
7734
7734
|
"mode": "0000644",
|
|
7735
|
-
"sha256": "
|
|
7735
|
+
"sha256": "a9a2455f79599041389b23a18607666ce74eba9be0199d21fbc62553824463f6"
|
|
7736
7736
|
},
|
|
7737
7737
|
{
|
|
7738
7738
|
"path": "public-docs/index.html",
|
|
@@ -8867,7 +8867,7 @@
|
|
|
8867
8867
|
{
|
|
8868
8868
|
"path": "scripts/gds/ship-flow.js",
|
|
8869
8869
|
"mode": "0000644",
|
|
8870
|
-
"sha256": "
|
|
8870
|
+
"sha256": "8bf28a3d4d15e83a1e94416ec2519d39bc2a3faa71303a4f2d22d429b6afaa95"
|
|
8871
8871
|
},
|
|
8872
8872
|
{
|
|
8873
8873
|
"path": "scripts/gds/ship-git.js",
|
|
@@ -9487,7 +9487,7 @@
|
|
|
9487
9487
|
{
|
|
9488
9488
|
"path": "src/module-api.js",
|
|
9489
9489
|
"mode": "0000644",
|
|
9490
|
-
"sha256": "
|
|
9490
|
+
"sha256": "dadc321fdd8e6a6f856bb9260c555284b9859361c556d47b14b25df45db900b0"
|
|
9491
9491
|
},
|
|
9492
9492
|
{
|
|
9493
9493
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11147,7 +11147,7 @@
|
|
|
11147
11147
|
{
|
|
11148
11148
|
"path": "tests/home_bounded_reads.mjs",
|
|
11149
11149
|
"mode": "0000644",
|
|
11150
|
-
"sha256": "
|
|
11150
|
+
"sha256": "6853241b64b5f71dfb782956a4fab468a05aa2a9ee41d5159a4cbea5f0424f38"
|
|
11151
11151
|
},
|
|
11152
11152
|
{
|
|
11153
11153
|
"path": "tests/hook_core_resolve.mjs",
|
|
@@ -11434,6 +11434,11 @@
|
|
|
11434
11434
|
"mode": "0000644",
|
|
11435
11435
|
"sha256": "7fa36152b6dbb17b2855d261ddbea96af63d26f97001273cd131104235ea431d"
|
|
11436
11436
|
},
|
|
11437
|
+
{
|
|
11438
|
+
"path": "tests/lifecycle_facade_surface.mjs",
|
|
11439
|
+
"mode": "0000644",
|
|
11440
|
+
"sha256": "cc14604bf130022ce2533247d29187da6260a791f850f73051bd522252d1b115"
|
|
11441
|
+
},
|
|
11437
11442
|
{
|
|
11438
11443
|
"path": "tests/linkify_check.mjs",
|
|
11439
11444
|
"mode": "0000644",
|
|
@@ -11552,7 +11557,7 @@
|
|
|
11552
11557
|
{
|
|
11553
11558
|
"path": "tests/migration_allocation.mjs",
|
|
11554
11559
|
"mode": "0000644",
|
|
11555
|
-
"sha256": "
|
|
11560
|
+
"sha256": "e9271b1a67d201cfd075ce272b1a5a4f3b66c80dcfa5798ca3ea0d3f5048be43"
|
|
11556
11561
|
},
|
|
11557
11562
|
{
|
|
11558
11563
|
"path": "tests/migration_namespace.mjs",
|
|
@@ -11792,7 +11797,12 @@
|
|
|
11792
11797
|
{
|
|
11793
11798
|
"path": "tests/patch_gate_ordering.mjs",
|
|
11794
11799
|
"mode": "0000644",
|
|
11795
|
-
"sha256": "
|
|
11800
|
+
"sha256": "db5e934ef61ddc8dc5804f25a9c84c04f30570db708fd6c77a157e101c69b4d4"
|
|
11801
|
+
},
|
|
11802
|
+
{
|
|
11803
|
+
"path": "tests/patch_value_summary_e2e.mjs",
|
|
11804
|
+
"mode": "0000644",
|
|
11805
|
+
"sha256": "86f60e682284c5f36fa23c6df0ec8b137f40cc548562d6c4496d11d200edb9bf"
|
|
11796
11806
|
},
|
|
11797
11807
|
{
|
|
11798
11808
|
"path": "tests/path_match.mjs",
|
|
@@ -12522,7 +12532,7 @@
|
|
|
12522
12532
|
{
|
|
12523
12533
|
"path": "tests/ship_cannot_lie.mjs",
|
|
12524
12534
|
"mode": "0000644",
|
|
12525
|
-
"sha256": "
|
|
12535
|
+
"sha256": "18a3bb613fff3dcc0dfbffb2283a866726af4347c161fc28853b36b5bdd75512"
|
|
12526
12536
|
},
|
|
12527
12537
|
{
|
|
12528
12538
|
"path": "tests/ship_card.mjs",
|
|
@@ -183,7 +183,7 @@ export interface PatchProvisioningInstancesIdDetailResponse { ok: boolean; descr
|
|
|
183
183
|
export interface PatchProvisioningInstancesIdSettingsResponse { ok: boolean; saved: unknown; planet: unknown; push: unknown }
|
|
184
184
|
export interface PatchTaskRecommendationsIdRequest { status: "declined" | "withdrawn" }
|
|
185
185
|
export interface PatchTaskRecommendationsIdResponse { ok: boolean; recommendation: unknown }
|
|
186
|
-
export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; title?: string; priority?: unknown; status?: unknown }
|
|
186
|
+
export interface PatchTasksIdRequest { kind?: string; discipline?: string; requires_rank?: string; module_key?: string; goal_id?: number; parent_task_id?: StringifiableId; parallel_safe?: unknown; newcomer_friendly?: boolean; xenos_claimable?: boolean; security_sensitive?: boolean; automation_tag?: string; needs_migration?: boolean; description?: string; title?: string; priority?: unknown; value_summary?: string; status?: unknown }
|
|
187
187
|
export interface PostAccessRequestsInviteRequest { github_login: string }
|
|
188
188
|
export interface PostAccessRequestsInviteResponse { ok: boolean; already_invited?: boolean; github_login?: unknown; request?: unknown }
|
|
189
189
|
export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
|
package/docs/api/openapi.json
CHANGED
|
@@ -18981,6 +18981,11 @@
|
|
|
18981
18981
|
"maxLength": 200
|
|
18982
18982
|
},
|
|
18983
18983
|
"priority": {},
|
|
18984
|
+
"value_summary": {
|
|
18985
|
+
"type": "string",
|
|
18986
|
+
"minLength": 1,
|
|
18987
|
+
"maxLength": 1000
|
|
18988
|
+
},
|
|
18984
18989
|
"status": {}
|
|
18985
18990
|
},
|
|
18986
18991
|
"additionalProperties": false
|
package/docs/api-reference.md
CHANGED
|
@@ -593,7 +593,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
593
593
|
| GET | `/api/bongos/tasks` | `any-builder` | — | |
|
|
594
594
|
| POST | `/api/bongos/tasks` | `metic+archon` | `version_id`, `title`, `description`, `status`, `touches`, `est_minutes`, `est_cost_usd`, `manual_degree`, `priority`, `automation_tag`, `credits_reward`, `source`, `source_ref`, `value_summary`, `kind`, `discipline`, `parallel_safe`, `newcomer_friendly`, `security_sensitive`, `requires_rank`, `auto_detect_touches`, `criterion_ids`, `module_key`, `goal_id` | Task create/promote/edit (plan/scope mutation) is Metic+ as of ADR 0090 (owner decision 2026-06-26): a Metic who can run idea-triage may … |
|
|
595
595
|
| GET | `/api/bongos/tasks/:id` | `any-builder` | — | single-task read; preserved per CRITICAL §4 (supervisor hot path). |
|
|
596
|
-
| PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `description`, `title`, `priority`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
|
|
596
|
+
| PATCH | `/api/bongos/tasks/:id` | `metic+archon` | `kind`, `discipline`, `requires_rank`, `module_key`, `goal_id`, `parent_task_id`, `parallel_safe`, `newcomer_friendly`, `xenos_claimable`, `security_sensitive`, `automation_tag`, `needs_migration`, `description`, `title`, `priority`, `value_summary`, `status` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
|
|
597
597
|
| POST | `/api/bongos/tasks/:id/abandon` | `metic+archon` | `reason` | POST /tasks/:id/abandon — terminal "retire this task" transition (task 987). |
|
|
598
598
|
| POST | `/api/bongos/tasks/:id/attest-gate` | `any-builder` | `head_sha` | POST /tasks/:id/attest-gate — body: { head_sha }. |
|
|
599
599
|
| POST | `/api/bongos/tasks/:id/confirm` | `any-builder` | `verified` | |
|
|
@@ -1743,5 +1743,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1743
1743
|
landed since 1.19.645 with no explicit bump. run 34512629504. (task 1002620)
|
|
1744
1744
|
1.19.647 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1745
1745
|
landed since 1.19.646 with no explicit bump. run 34515733973. (task 1002620)
|
|
1746
|
+
1.19.648 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1747
|
+
landed since 1.19.647 with no explicit bump. run 34518387614. (task 1002620)
|
|
1746
1748
|
---------------------------------------------------------------------------
|
|
1747
1749
|
```
|
|
@@ -308,7 +308,7 @@ async function findTaskBySourceRef({ source, sourceRef } = {}, deps = {}) {
|
|
|
308
308
|
|
|
309
309
|
const TASK_SETTER_COLUMNS = new Set([
|
|
310
310
|
'kind', 'discipline', 'newcomer_friendly', 'security_sensitive', 'parallel_safe', 'automation_tag',
|
|
311
|
-
'needs_migration', 'module_key', 'description', 'title', 'priority',
|
|
311
|
+
'needs_migration', 'module_key', 'description', 'title', 'priority', 'value_summary',
|
|
312
312
|
]);
|
|
313
313
|
|
|
314
314
|
async function updateTaskColumn(id, column, value) {
|
|
@@ -383,6 +383,17 @@ async function updateTaskTitle(id, title) { return updateTaskColumn(id, 'title',
|
|
|
383
383
|
// 1 = most urgent, 5 = least; null clears it back to unset (weighted as 5).
|
|
384
384
|
async function updateTaskPriority(id, priority) { return updateTaskColumn(id, 'priority', priority); }
|
|
385
385
|
|
|
386
|
+
// task 1003817: the shipped-value sentence — what the hall, /status and the
|
|
387
|
+
// Discord #ship-news line all read. It was written in exactly ONE place, the
|
|
388
|
+
// claim-resolve route, so a re-grade (which has no claim — regradeMain
|
|
389
|
+
// synthesizes a claimLike precisely because the claim is gone) could correct
|
|
390
|
+
// the summary in the PR and the merge commit and still leave the RECORD holding
|
|
391
|
+
// the first ship's sentence forever. This is the write vector that path uses.
|
|
392
|
+
// Rewritable, never blankable: the route refuses an empty or whitespace-only
|
|
393
|
+
// value, which is this column's form of the COALESCE/NULLIF posture db-ship.js
|
|
394
|
+
// keeps on every other value_summary write.
|
|
395
|
+
async function updateTaskValueSummary(id, valueSummary) { return updateTaskColumn(id, 'value_summary', valueSummary); }
|
|
396
|
+
|
|
386
397
|
|
|
387
398
|
async function updateTaskDiscipline(id, discipline) { return updateTaskColumn(id, 'discipline', discipline); }
|
|
388
399
|
|
|
@@ -1037,5 +1048,6 @@ module.exports = {
|
|
|
1037
1048
|
updateTaskRequiresRank,
|
|
1038
1049
|
updateTaskSecuritySensitive,
|
|
1039
1050
|
updateTaskStatus,
|
|
1051
|
+
updateTaskValueSummary,
|
|
1040
1052
|
versionAcceptsNewWork,
|
|
1041
1053
|
};
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -30,10 +30,16 @@
|
|
|
30
30
|
// FACADE (task 1003204, goal 1000079). This file was a 5,748-line monolith; the
|
|
31
31
|
// R03 de-monolith plan carved it into the twelve db-*.js units required below,
|
|
32
32
|
// each cohesive and well under the 1,500-line ratchet. The carve was STRUCTURAL:
|
|
33
|
-
// no function body changed, and
|
|
34
|
-
// original one verbatim — so every `require('./db')` call site, and every name
|
|
33
|
+
// no function body changed, and every `require('./db')` call site, and every name
|
|
35
34
|
// reachable through it, behaves exactly as before.
|
|
36
35
|
//
|
|
36
|
+
// The module.exports block was the carve's ORIGINAL list verbatim until task
|
|
37
|
+
// 1003817, which replaced the db-tasks.js third of it with a spread — see the
|
|
38
|
+
// long note above `const dbTasks` for why, and tests/lifecycle_facade_surface.mjs
|
|
39
|
+
// for the assertion that keeps that surface identical. The other eleven units
|
|
40
|
+
// are still re-listed by name; giving one of them a spread means giving it an
|
|
41
|
+
// omit list and a surface pin too, because a bare spread publishes internals.
|
|
42
|
+
//
|
|
37
43
|
// The units form a DAG (no require cycles), in dependency order:
|
|
38
44
|
// db-shared → db-rank-authz → {db-versions, db-goals, db-tasks} → db-analytics
|
|
39
45
|
// → db-ship → db-claims; db-deps-criteria and db-claim-reads stand alone.
|
|
@@ -100,38 +106,37 @@ const {
|
|
|
100
106
|
setGoalVisibility,
|
|
101
107
|
transferGoalOwnership,
|
|
102
108
|
} = require('./db-goals.js');
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
} = require('./db-tasks.js');
|
|
109
|
+
// task 1003817: db-tasks.js's exports ARE this facade's task surface, so they are
|
|
110
|
+
// spread into module.exports below instead of being re-listed name by name.
|
|
111
|
+
//
|
|
112
|
+
// WHY THE TWIN LIST HAD TO GO. What stood here was 31 sorted names destructured
|
|
113
|
+
// from db-tasks.js purely so module.exports could name them again — not one was
|
|
114
|
+
// called anywhere inside this file. Two hand-maintained copies of one list is
|
|
115
|
+
// real duplication, and the `duplicate_window_count` ratchet reads it as such:
|
|
116
|
+
// the lists were 29 identical normalized lines, one under the 30-line window, so
|
|
117
|
+
// task 1003750 adding `updateTaskPriority` to both pushed the run to 30 and cost
|
|
118
|
+
// an owner-decided baseline raise (59 → 60). That decision is recorded in
|
|
119
|
+
// config/fitness-baselines.json along with the instruction for whoever added the
|
|
120
|
+
// NEXT db-tasks function — this task, adding updateTaskValueSummary: fix the
|
|
121
|
+
// facade, do not raise the baseline again. Spreading removes the whole class, so
|
|
122
|
+
// the next setter costs nothing.
|
|
123
|
+
//
|
|
124
|
+
// TASK_FACADE_OMIT is what keeps this surface-PRESERVING rather than quietly
|
|
125
|
+
// wider. Two of db-tasks.js's 33 exports were deliberately never re-exported
|
|
126
|
+
// here, and a bare spread would publish them — which ADR 0091 §3 treats as a
|
|
127
|
+
// real surface change, not a tidy-up. Naming them means dropping one has to be
|
|
128
|
+
// somebody's decision. tests/lifecycle_facade_surface.mjs pins the resulting key
|
|
129
|
+
// set, so this can never silently widen or narrow.
|
|
130
|
+
const dbTasks = require('./db-tasks.js');
|
|
131
|
+
const TASK_FACADE_OMIT = new Set([
|
|
132
|
+
// Internal to db-tasks.js's own queries — a SQL fragment, not a callable.
|
|
133
|
+
'CREDITS_AWARDED_SUBQUERY',
|
|
134
|
+
// The version-gate predicate; routes reach it through db-versions, not here.
|
|
135
|
+
'versionAcceptsNewWork',
|
|
136
|
+
]);
|
|
137
|
+
const dbTasksFacade = Object.fromEntries(
|
|
138
|
+
Object.entries(dbTasks).filter(([name]) => !TASK_FACADE_OMIT.has(name))
|
|
139
|
+
);
|
|
135
140
|
const {
|
|
136
141
|
addDependency,
|
|
137
142
|
addTaskCriterion,
|
|
@@ -214,6 +219,7 @@ const {
|
|
|
214
219
|
} = require('./db-claims.js');
|
|
215
220
|
|
|
216
221
|
module.exports = {
|
|
222
|
+
...dbTasksFacade,
|
|
217
223
|
tallyPeerVotes,
|
|
218
224
|
PEER_VOTE_KARMA_LAG_DAYS,
|
|
219
225
|
RANKS,
|
|
@@ -263,19 +269,6 @@ module.exports = {
|
|
|
263
269
|
listCriteriaForGoal,
|
|
264
270
|
archiveGoalWithDispositions,
|
|
265
271
|
openTasksInGoal,
|
|
266
|
-
createTask,
|
|
267
|
-
generalGoalIdForVersion,
|
|
268
|
-
findTaskBySourceRef,
|
|
269
|
-
getTask,
|
|
270
|
-
listTasks,
|
|
271
|
-
updateTaskGoal,
|
|
272
|
-
createCriterion,
|
|
273
|
-
updateTaskKind,
|
|
274
|
-
updateTaskDescription,
|
|
275
|
-
updateTaskTitle,
|
|
276
|
-
updateTaskDiscipline,
|
|
277
|
-
updateTaskNewcomerFriendly,
|
|
278
|
-
updateTaskRequiresRank,
|
|
279
272
|
xenosClaimAllowed,
|
|
280
273
|
rankAllowsTask,
|
|
281
274
|
REQUIRES_RANK_TIER,
|
|
@@ -290,31 +283,14 @@ module.exports = {
|
|
|
290
283
|
LIVE_RANK_LADDER,
|
|
291
284
|
deriveRequiredRank,
|
|
292
285
|
highestRank,
|
|
293
|
-
rewardMissing,
|
|
294
|
-
suggestCreditsReward,
|
|
295
|
-
REWARD_SUGGESTION_CAP,
|
|
296
|
-
rewardGateAssignment,
|
|
297
|
-
rewardGateError,
|
|
298
|
-
setCreditsReward,
|
|
299
286
|
THETES_GRADUATION_THRESHOLD,
|
|
300
287
|
qualifiesForThetesGraduation,
|
|
301
288
|
countShippedTasksForBuilder,
|
|
302
289
|
singleRungPromotionError,
|
|
303
290
|
disambiguateWorktreeName,
|
|
304
291
|
foldWorktreeName,
|
|
305
|
-
updateTaskParallelSafe,
|
|
306
|
-
updateTaskPriority,
|
|
307
|
-
updateTaskSecuritySensitive,
|
|
308
|
-
updateTaskAutomationTag,
|
|
309
|
-
updateTaskNeedsMigration,
|
|
310
|
-
updateTaskModuleKey,
|
|
311
|
-
listClaimableTasks,
|
|
312
292
|
capNewcomerTasks,
|
|
313
|
-
taskTouchesProtectedPath, // SR-12 / G2 (task 994): autonomous-fit protected-path exclusion (pure, testable)
|
|
314
293
|
decorateGoalMembership, // task 1003137: feed-side joinable flag (pure, testable)
|
|
315
|
-
rankCrossDisciplineRecommendations,
|
|
316
|
-
listActiveClaims,
|
|
317
|
-
updateTaskStatus,
|
|
318
294
|
getTaskDependencies,
|
|
319
295
|
getTaskDependents,
|
|
320
296
|
attachTaskDepSummaries,
|
|
@@ -326,6 +326,14 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
326
326
|
// A whole-goal re-rank (needed-vs-nice) had no route and had to run as SQL.
|
|
327
327
|
// null clears it back to unset; the 1..5 bounds mirror tasks_priority_check.
|
|
328
328
|
priority: {}, // 1..5 or null — checked inline
|
|
329
|
+
// task 1003817: the shipped-value sentence. Written in exactly one place
|
|
330
|
+
// before this — the claim-resolve route — so `ship.js --regrade --summary`
|
|
331
|
+
// (no claim, by definition) could correct it in the PR and the merge commit
|
|
332
|
+
// and still leave the hall, /status and the Discord ship line reading the
|
|
333
|
+
// FIRST ship's text forever. Same task.edit permission and audit_log trail
|
|
334
|
+
// as the title/description/priority edits above; minLength 1 (plus the
|
|
335
|
+
// whitespace check below) so the record can be rewritten, never blanked.
|
|
336
|
+
value_summary: { type: 'string', minLength: 1, maxLength: LIMITS.VALUE_SUMMARY },
|
|
329
337
|
// task 1002614 (map D-18): DECLARED so strict-mode admits the request far
|
|
330
338
|
// enough to reach the deliberate status_not_patchable hint below (#514) —
|
|
331
339
|
// the hint had been unreachable dead code behind unknown_field since the
|
|
@@ -354,6 +362,7 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
354
362
|
const hasDescription = Object.prototype.hasOwnProperty.call(body, 'description');
|
|
355
363
|
const hasTitle = Object.prototype.hasOwnProperty.call(body, 'title');
|
|
356
364
|
const hasPriority = Object.prototype.hasOwnProperty.call(body, 'priority');
|
|
365
|
+
const hasValueSummary = Object.prototype.hasOwnProperty.call(body, 'value_summary');
|
|
357
366
|
// #514: status is a lifecycle field, not a PATCH-able column — it moves only
|
|
358
367
|
// through the dedicated transition routes. The #506 session tried
|
|
359
368
|
// `PATCH status=ready`, got the generic no_supported_fields error, and had
|
|
@@ -365,8 +374,8 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
365
374
|
if (Object.prototype.hasOwnProperty.call(body, 'status')) {
|
|
366
375
|
return res.fail('status_not_patchable', { status: 400, message: STATUS_HINT });
|
|
367
376
|
}
|
|
368
|
-
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription && !hasTitle && !hasPriority) {
|
|
369
|
-
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, title, description, priority, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
377
|
+
if (!hasParent && !hasKind && !hasDiscipline && !hasNewcomerOrXenos && !hasParallelSafe && !hasSecuritySensitive && !hasAutomationTag && !hasNeedsMigration && !hasRequiresRank && !hasModuleKey && !hasGoalId && !hasDescription && !hasTitle && !hasPriority && !hasValueSummary) {
|
|
378
|
+
return res.fail('no_supported_fields', { status: 400, message: 'PATCH /tasks/:id supports parent_task_id, kind, discipline, title, description, value_summary, priority, parallel_safe, security_sensitive, automation_tag, needs_migration, requires_rank, module_key, goal_id, and newcomer_friendly (alias: xenos_claimable). ' + STATUS_HINT });
|
|
370
379
|
}
|
|
371
380
|
// ---- task 1002609 (G-22): settle EVERY gate and validation before the ----
|
|
372
381
|
// ---- FIRST write. The previous shape validated-and-wrote per field in ----
|
|
@@ -393,6 +402,14 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
393
402
|
return res.fail('bad_priority', { status: 400, message: `priority must be an integer ${TASK_FIELD_BOUNDS.priority.min}-${TASK_FIELD_BOUNDS.priority.max} (1 = most urgent), or null to clear` });
|
|
394
403
|
}
|
|
395
404
|
}
|
|
405
|
+
// task 1003817: minLength 1 refuses "" but not " ", and the write trims —
|
|
406
|
+
// so without this a whitespace-only body would blank the shipped-value
|
|
407
|
+
// record through the one field whose whole point is that it survives. An
|
|
408
|
+
// omitted field writes nothing at all (hasValueSummary is false), which is
|
|
409
|
+
// this route's form of the COALESCE/NULLIF posture db-ship.js keeps.
|
|
410
|
+
if (hasValueSummary && body.value_summary.trim() === '') {
|
|
411
|
+
return res.fail('bad_value_summary', { status: 400, message: 'value_summary must be a non-empty sentence — the shipped-value record can be rewritten, never blanked. Omit the field to leave it unchanged.' });
|
|
412
|
+
}
|
|
396
413
|
if (hasSecuritySensitive) {
|
|
397
414
|
// task 990: explicit Archon-only-broadcast flag. Boolean only (no null) —
|
|
398
415
|
// it gates whether ship.js routes the broadcast to the Archon-only
|
|
@@ -556,6 +573,15 @@ module.exports = function registerTaskWriteRoutes(router) {
|
|
|
556
573
|
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
557
574
|
}
|
|
558
575
|
}
|
|
576
|
+
if (hasValueSummary) {
|
|
577
|
+
try {
|
|
578
|
+
updated = await db.updateTaskValueSummary(id, body.value_summary.trim());
|
|
579
|
+
if (!updated) return res.fail('task_not_found', 404);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
log.error('[gds] PATCH /tasks/:id value_summary', err);
|
|
582
|
+
return res.fail('patch_failed', { status: 500, message: 'internal error' });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
559
585
|
if (hasGoalId) {
|
|
560
586
|
try {
|
|
561
587
|
updated = await db.updateTaskGoal(id, body.goal_id);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.648",
|
|
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.648",
|
|
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.648",
|
|
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/ship-flow.js
CHANGED
|
@@ -659,6 +659,58 @@ async function shipMain() {
|
|
|
659
659
|
});
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
// persistRegradeSummary — store a re-grade's corrected --summary on the task row
|
|
663
|
+
// (task 1003817). A re-grade has no claim, so it cannot reach claim-resolve, the
|
|
664
|
+
// only writer of tasks.value_summary before this; PATCH /tasks/:id is the route
|
|
665
|
+
// it uses instead.
|
|
666
|
+
//
|
|
667
|
+
// WHY PATCH AND NOT publish-branch, which already RECEIVES value_summary and
|
|
668
|
+
// could have persisted it with a one-line change: publish-branch is only called
|
|
669
|
+
// when pushVia() === 'server'. A laptop land goes through ciLandLocal (gh), and
|
|
670
|
+
// --no-merge / --db-only / --allow-empty never land at all — so persisting there
|
|
671
|
+
// would have fixed one path of four and left the defect standing on the rest. It
|
|
672
|
+
// would also have made publish-branch a SECOND writer racing claim-resolve on
|
|
673
|
+
// the normal ship path, which sends the same field. Writing from here keeps one
|
|
674
|
+
// writer per ship: the claim on a first ship, this on a re-grade.
|
|
675
|
+
//
|
|
676
|
+
// Three deliberate postures:
|
|
677
|
+
// - OMITTED --summary writes NOTHING. The stored value is the fallback the
|
|
678
|
+
// grader was already handed, so a re-grade that omits the flag must leave
|
|
679
|
+
// the row exactly as it was — the COALESCE/NULLIF posture every other
|
|
680
|
+
// value_summary write in db-ship.js keeps. This is acceptance 2.
|
|
681
|
+
// - UNCHANGED text writes nothing either, so a no-op edit mints no audit_log row.
|
|
682
|
+
// - A FAILURE is LOUD BUT NON-BLOCKING. The grade is the expensive step and it
|
|
683
|
+
// has not run yet; discarding a re-grade over a summary that did not persist
|
|
684
|
+
// would trade the cheap problem for the costly one. The builder is told
|
|
685
|
+
// plainly that the row still holds the old sentence, so nothing is silent.
|
|
686
|
+
// An older pinned core that has not learned the field is named as exactly
|
|
687
|
+
// that rather than reported as a generic error — the same forward-compat
|
|
688
|
+
// posture the resolve call in shipMain documents at length (task 1003767),
|
|
689
|
+
// because @cloudbongos/cli ships separately from the core and an instance
|
|
690
|
+
// pins its own core version.
|
|
691
|
+
async function persistRegradeSummary({ taskId, summaryArg, task }) {
|
|
692
|
+
const next = (summaryArg || '').trim();
|
|
693
|
+
if (!next) return { written: false, reason: 'omitted' };
|
|
694
|
+
if (next === (task.value_summary || '').trim()) return { written: false, reason: 'unchanged' };
|
|
695
|
+
let r;
|
|
696
|
+
try {
|
|
697
|
+
r = await (await cliClient()).tasks.patchTasksId({ id: taskId, body: { value_summary: next } });
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.error(` ⚠ value_summary NOT stored (${err && err.message ? err.message : 'request failed'}) — task #${taskId} keeps its previous summary. The grade below is unaffected.`);
|
|
700
|
+
return { written: false, reason: 'error' };
|
|
701
|
+
}
|
|
702
|
+
if (!r.ok) {
|
|
703
|
+
if (isUnknownFieldRejection(r.status, r.data, 'value_summary')) {
|
|
704
|
+
console.error(` ⚠ value_summary NOT stored: this instance's core predates PATCH /tasks/:id accepting the field. Upgrade the core, or edit the summary in the hall. The grade below is unaffected.`);
|
|
705
|
+
} else {
|
|
706
|
+
console.error(` ⚠ value_summary NOT stored (${apiErrorLine(r.data, r.status)}) — task #${taskId} keeps its previous summary. The grade below is unaffected.`);
|
|
707
|
+
}
|
|
708
|
+
return { written: false, reason: 'refused' };
|
|
709
|
+
}
|
|
710
|
+
console.log(` value_summary updated on task #${taskId} — the hall, /status and the ship broadcast now read the corrected sentence.`);
|
|
711
|
+
return { written: true };
|
|
712
|
+
}
|
|
713
|
+
|
|
662
714
|
// ---------- Re-grade flow (#678) ----------
|
|
663
715
|
//
|
|
664
716
|
// `node scripts/gds/ship.js <id> --regrade` closes the task-595-regrade-gap
|
|
@@ -676,6 +728,7 @@ async function shipMain() {
|
|
|
676
728
|
// - grade: gradeAndRecord → POST /tasks/:id/grade (rank-open; applyGrade
|
|
677
729
|
// promotes completed→confirmed + awards credits on pass, idempotent)
|
|
678
730
|
// - on pass: the normal confirmed→shipped merge + sync tail runs.
|
|
731
|
+
|
|
679
732
|
async function regradeMain() {
|
|
680
733
|
await requireSession();
|
|
681
734
|
const args = process.argv.slice(2);
|
|
@@ -764,16 +817,14 @@ async function regradeMain() {
|
|
|
764
817
|
// task's stored value_summary so the merge commit + Discord #ship-news post
|
|
765
818
|
// carry meaningful text even when --summary is omitted.
|
|
766
819
|
//
|
|
767
|
-
// AND IT
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
// a server route change, so it is deliberately NOT folded into this CLI fix.
|
|
776
|
-
// tests/ship_cannot_lie.mjs pins both halves of that so the answer cannot rot.
|
|
820
|
+
// AND IT NOW REACHES THE TASK ROW TOO — task 1003817. It did not before: this
|
|
821
|
+
// `summary` fed the grader prompt, the merge commit message and the PR
|
|
822
|
+
// title/body and stopped there, so a builder who re-graded with a CORRECTED
|
|
823
|
+
// --summary saw it applied everywhere except the place that is the record —
|
|
824
|
+
// the hall, /status and the Discord #ship-news line all kept the first ship's
|
|
825
|
+
// sentence forever. persistRegradeSummary (below the preflight) is the write;
|
|
826
|
+
// PATCH /tasks/:id is the route it uses, and `value_summary` was added to that
|
|
827
|
+
// schema by this same task. tests/ship_cannot_lie.mjs pins the wiring.
|
|
777
828
|
const summary = summaryArg || task.value_summary || '';
|
|
778
829
|
|
|
779
830
|
// The grader helpers want a claim-like object for touches[] (preflight's
|
|
@@ -804,6 +855,14 @@ async function regradeMain() {
|
|
|
804
855
|
process.exit(1);
|
|
805
856
|
}
|
|
806
857
|
|
|
858
|
+
// ---- Persist a corrected --summary to the task row (task 1003817) ----
|
|
859
|
+
// Placed HERE deliberately: after the guards that can still abort the re-grade
|
|
860
|
+
// (so a refused run writes nothing), and BEFORE the grade — which is the order
|
|
861
|
+
// the normal ship already has, where claim-resolve stores value_summary and
|
|
862
|
+
// the panel runs afterwards. The row, the grader prompt, the merge commit and
|
|
863
|
+
// the PR then all carry the same sentence.
|
|
864
|
+
await persistRegradeSummary({ taskId, summaryArg, task });
|
|
865
|
+
|
|
807
866
|
// ---- Delta-aware prior-round context (task 1002662) ----
|
|
808
867
|
// The prior grade's findings + the builder's response + the interim fix diff
|
|
809
868
|
// ride to the panel so an already-answered finding can't re-veto on
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.648'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -99,16 +99,25 @@ await test('the limit is OPT-IN — the Work Board and /builder-start are untouc
|
|
|
99
99
|
await test('every db.* the claimable handler calls actually exists', () => {
|
|
100
100
|
// The bug this catches, caught the hard way: an earlier cut pushed the limit into the
|
|
101
101
|
// db layer, was reverted, and left the route calling `db.listClaimableTasksPaged` — a
|
|
102
|
-
// function that no longer existed. Every assertion in this file is source text,
|
|
103
|
-
// source text cannot see a missing symbol; a neighbouring suite found it instead.
|
|
102
|
+
// function that no longer existed. Every OTHER assertion in this file is source text,
|
|
103
|
+
// and source text cannot see a missing symbol; a neighbouring suite found it instead.
|
|
104
|
+
//
|
|
105
|
+
// So this one is not source text: it REQUIRES the facade and reads the real key. It
|
|
106
|
+
// used to grep db.js's module.exports block for each name, which was a proxy for
|
|
107
|
+
// exactly this and stopped being a valid one at task 1003817 — db.js now SPREADS
|
|
108
|
+
// db-tasks.js's exports instead of re-listing them, so `listClaimableTasks` is a live
|
|
109
|
+
// key that appears nowhere in the block's text. Requiring is safe here: the lifecycle
|
|
110
|
+
// db family is pool-lazy (tests/lifecycle_facade_surface.mjs relies on the same thing).
|
|
104
111
|
const from = ROUTE.indexOf("router.get('/tasks/claimable'");
|
|
105
112
|
const body = ROUTE.slice(from, ROUTE.indexOf('res.json({ claimable', from));
|
|
106
113
|
const called = [...new Set([...body.matchAll(/\bdb\.([A-Za-z0-9_]+)\s*\(/g)].map((m) => m[1]))];
|
|
107
114
|
assert.ok(called.length > 0, 'the handler calls the db layer');
|
|
108
|
-
const
|
|
109
|
-
const block = dbExports.slice(dbExports.lastIndexOf('module.exports'));
|
|
115
|
+
const db = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
|
|
110
116
|
for (const fn of called) {
|
|
111
|
-
assert.ok(
|
|
117
|
+
assert.ok(
|
|
118
|
+
typeof db[fn] === 'function',
|
|
119
|
+
`db.js must export ${fn} — the route calls it, and this is the runtime check, not a text match`
|
|
120
|
+
);
|
|
112
121
|
}
|
|
113
122
|
});
|
|
114
123
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// tests/lifecycle_facade_surface.mjs
|
|
2
|
+
//
|
|
3
|
+
// The lifecycle facade's TASK surface, pinned (task 1003817).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS FILE EXISTS. modules/lifecycle/db.js used to re-export db-tasks.js by
|
|
6
|
+
// hand: 31 names destructured at the top purely so module.exports could list
|
|
7
|
+
// them again, with no internal caller for a single one. Two hand-maintained
|
|
8
|
+
// copies of one list is real duplication, and the `duplicate_window_count`
|
|
9
|
+
// ratchet counted it — the two runs were 29 identical normalized lines, one
|
|
10
|
+
// under the 30-line window, so task 1003750 adding `updateTaskPriority` to both
|
|
11
|
+
// tipped it over and cost an owner-decided baseline raise (59 -> 60). The reason
|
|
12
|
+
// recorded in config/fitness-baselines.json told whoever added the NEXT
|
|
13
|
+
// db-tasks function to fix the facade instead of raising it again. Task 1003817
|
|
14
|
+
// added `updateTaskValueSummary`, so this is that fix: db.js now spreads
|
|
15
|
+
// db-tasks.js's exports minus an explicit omit list.
|
|
16
|
+
//
|
|
17
|
+
// A SPREAD IS CHEAPER TO MAINTAIN AND EASIER TO GET WRONG. Nothing stops it from
|
|
18
|
+
// silently WIDENING the module's public surface (publishing an internal that was
|
|
19
|
+
// deliberately withheld) or NARROWING it (a rename in db-tasks.js quietly
|
|
20
|
+
// dropping a name every route calls, which is a runtime `undefined is not a
|
|
21
|
+
// function` in an async Express handler — ADR 0091 §2 records exactly that
|
|
22
|
+
// outage for `LIVE_RANK_LADDER`, where it hung the request until Cloudflare
|
|
23
|
+
// 524'd). The old twin list at least failed loudly at require time. So the
|
|
24
|
+
// safety the list used to provide is re-established here, as an assertion rather
|
|
25
|
+
// than as boilerplate: db.js's task surface must be db-tasks.js's exports minus
|
|
26
|
+
// TASK_FACADE_OMIT, exactly — no more, no fewer.
|
|
27
|
+
//
|
|
28
|
+
// No live DB: requiring the lifecycle db family is pool-lazy.
|
|
29
|
+
//
|
|
30
|
+
// Run: node tests/lifecycle_facade_surface.mjs
|
|
31
|
+
|
|
32
|
+
import { strict as assert } from 'node:assert';
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { dirname, join } from 'node:path';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const repoRoot = join(__dirname, '..');
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
|
|
42
|
+
let passed = 0;
|
|
43
|
+
let failed = 0;
|
|
44
|
+
async function test(name, fn) {
|
|
45
|
+
try {
|
|
46
|
+
await fn();
|
|
47
|
+
console.log(` ok ${name}`);
|
|
48
|
+
passed++;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(` FAIL ${name}`);
|
|
51
|
+
console.error(' ', err.message);
|
|
52
|
+
failed++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const db = require(join(repoRoot, 'modules', 'lifecycle', 'db.js'));
|
|
57
|
+
const dbTasks = require(join(repoRoot, 'modules', 'lifecycle', 'db-tasks.js'));
|
|
58
|
+
const dbSrc = readFileSync(join(repoRoot, 'modules', 'lifecycle', 'db.js'), 'utf8');
|
|
59
|
+
|
|
60
|
+
// The omit list is read from the SOURCE, not re-typed here — a test that hardcodes
|
|
61
|
+
// the same two names cannot notice a third being added without a reason.
|
|
62
|
+
function omitListFromSource(src) {
|
|
63
|
+
const block = src.match(/const TASK_FACADE_OMIT = new Set\(\[([\s\S]*?)\]\)/);
|
|
64
|
+
assert.ok(block, 'db.js must declare TASK_FACADE_OMIT so the withheld names are a named decision');
|
|
65
|
+
return block[1]
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map((l) => l.replace(/\/\/.*/, '').trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map((l) => l.replace(/^'|',?$/g, ''));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await test('db.js re-exports every db-tasks export except the named omissions', () => {
|
|
73
|
+
const omit = new Set(omitListFromSource(dbSrc));
|
|
74
|
+
const expected = Object.keys(dbTasks).filter((k) => !omit.has(k)).sort();
|
|
75
|
+
const actual = expected.filter((k) => Object.prototype.hasOwnProperty.call(db, k));
|
|
76
|
+
const missing = expected.filter((k) => !Object.prototype.hasOwnProperty.call(db, k));
|
|
77
|
+
assert.deepEqual(missing, [], `db.js dropped task surface: ${missing.join(', ')} — a route calling one of these gets undefined at runtime, not an error at require`);
|
|
78
|
+
assert.equal(actual.length, expected.length);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await test('and publishes NONE of the omitted internals', () => {
|
|
82
|
+
const omit = omitListFromSource(dbSrc);
|
|
83
|
+
assert.ok(omit.length > 0, 'the omit list is not empty — a bare spread would widen the surface');
|
|
84
|
+
for (const name of omit) {
|
|
85
|
+
assert.ok(
|
|
86
|
+
Object.prototype.hasOwnProperty.call(dbTasks, name),
|
|
87
|
+
`${name} is omitted from a surface it is not on — stale entry, remove it`
|
|
88
|
+
);
|
|
89
|
+
assert.ok(
|
|
90
|
+
!Object.prototype.hasOwnProperty.call(db, name),
|
|
91
|
+
`${name} is on the omit list but reachable through db.js — the spread widened the facade`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await test('the spread is what wires it, and the twin name list is gone', () => {
|
|
97
|
+
assert.match(dbSrc, /\.\.\.dbTasksFacade,/, 'module.exports spreads the filtered db-tasks surface');
|
|
98
|
+
assert.match(
|
|
99
|
+
dbSrc,
|
|
100
|
+
/const dbTasksFacade = Object\.fromEntries\(\s*Object\.entries\(dbTasks\)\.filter\(\(\[name\]\) => !TASK_FACADE_OMIT\.has\(name\)\)/,
|
|
101
|
+
'the filter is the omit list, not an inline literal'
|
|
102
|
+
);
|
|
103
|
+
// The regression this replaces: a `const { … } = require('./db-tasks.js')`
|
|
104
|
+
// destructure whose only purpose was to feed module.exports.
|
|
105
|
+
assert.ok(
|
|
106
|
+
!/const \{[\s\S]*?\} = require\('\.\/db-tasks\.js'\)/.test(dbSrc),
|
|
107
|
+
'db-tasks.js must not be hand-destructured again — that is the duplication this task removed'
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(`\nlifecycle_facade_surface: ${passed} passed, ${failed} failed`);
|
|
112
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -205,6 +205,7 @@ function shouldReserveMigrationNumber(task) {
|
|
|
205
205
|
[() => db.updateTaskDescription(1, 'edited body'), 'description'], // task 1002647
|
|
206
206
|
[() => db.updateTaskTitle(1, 'corrected title'), 'title'], // task 1002792
|
|
207
207
|
[() => db.updateTaskPriority(1, 2), 'priority'], // task 1003750
|
|
208
|
+
[() => db.updateTaskValueSummary(1, 'corrected summary'), 'value_summary'], // task 1003817
|
|
208
209
|
];
|
|
209
210
|
for (const [call, column] of setters) {
|
|
210
211
|
const before = seen.length;
|
|
@@ -255,6 +256,33 @@ function shouldReserveMigrationNumber(task) {
|
|
|
255
256
|
assert.ok(guardAt > 0 && firstWriteAt > 0 && guardAt < firstWriteAt, 'the priority bounds check must run before the first write, not after');
|
|
256
257
|
});
|
|
257
258
|
|
|
259
|
+
// task 1003817: value_summary was the FOURTH write-once field, and the one
|
|
260
|
+
// that is the public record — the hall, /status and the Discord #ship-news
|
|
261
|
+
// line all read it. It had exactly one writer, the claim-resolve route, so
|
|
262
|
+
// `ship.js --regrade --summary` (no claim, by definition) could correct it in
|
|
263
|
+
// the PR and the merge commit and leave the record holding the first ship's
|
|
264
|
+
// sentence forever.
|
|
265
|
+
await test('PATCH /tasks/:id accepts value_summary so a re-graded summary reaches the record (task 1003817)', () => {
|
|
266
|
+
assert.ok(
|
|
267
|
+
/value_summary: \{ type: 'string', minLength: 1, maxLength: LIMITS\.VALUE_SUMMARY \}/.test(routesSrc),
|
|
268
|
+
'the PATCH schema must declare value_summary with the shared limit — rewritable, never blankable'
|
|
269
|
+
);
|
|
270
|
+
assert.ok(/const hasValueSummary = Object\.prototype\.hasOwnProperty\.call\(body, 'value_summary'\)/.test(routesSrc), 'value_summary is gated like every other field');
|
|
271
|
+
assert.ok(/if \(!hasParent &&[^)]*!hasValueSummary\b/.test(routesSrc), 'a value_summary-only PATCH is a supported request');
|
|
272
|
+
assert.ok(/db\.updateTaskValueSummary\(id, body\.value_summary\.trim\(\)\)/.test(routesSrc), 'the write goes through the allowlisted setter');
|
|
273
|
+
assert.ok(
|
|
274
|
+
/async function updateTaskValueSummary/.test(dbSrc) && /updateTaskValueSummary,/.test(dbSrc),
|
|
275
|
+
'the lifecycle db family must define it and re-export it (the ADR 0093 facade contract)'
|
|
276
|
+
);
|
|
277
|
+
// minLength 1 refuses "" but not " ", and the write trims — so the
|
|
278
|
+
// whitespace guard is what actually keeps the record un-blankable. It must
|
|
279
|
+
// sit above the first write, like the priority bounds check above.
|
|
280
|
+
const blankGuardAt = routesSrc.indexOf("res.fail('bad_value_summary'");
|
|
281
|
+
const firstWriteAt2 = routesSrc.indexOf('await db.updateTaskKind(');
|
|
282
|
+
assert.ok(blankGuardAt > 0, 'a whitespace-only value_summary is refused, not trimmed into a blank record');
|
|
283
|
+
assert.ok(blankGuardAt < firstWriteAt2, 'and that refusal runs before the first write');
|
|
284
|
+
});
|
|
285
|
+
|
|
258
286
|
await test('PATCH /tasks/:id accepts needs_migration so a wrong flag is correctable', () => {
|
|
259
287
|
assert.ok(
|
|
260
288
|
/needs_migration: \{ type: 'boolean' \}/.test(routesSrc),
|
|
@@ -49,6 +49,10 @@ const REFUSALS = [
|
|
|
49
49
|
// task 1003750: priority joined the PATCH surface; its bounds check is a
|
|
50
50
|
// validation refusal like the rest, so it must settle before the first write.
|
|
51
51
|
"'bad_priority'",
|
|
52
|
+
// task 1003817: value_summary joined the PATCH surface. Its refusal is the
|
|
53
|
+
// blank guard — minLength 1 stops "" but not " ", and the write trims — so
|
|
54
|
+
// it must settle before the first write like every other validation.
|
|
55
|
+
"'bad_value_summary'",
|
|
52
56
|
'badKind(res)',
|
|
53
57
|
"'bad_discipline'",
|
|
54
58
|
];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// tests/patch_value_summary_e2e.mjs — PATCH /tasks/:id value_summary over real
|
|
2
|
+
// HTTP (task 1003817).
|
|
3
|
+
//
|
|
4
|
+
// WHY A SECOND FILE. migration_allocation.mjs pins the two halves in isolation:
|
|
5
|
+
// the route's schema/gate/write as SOURCE TEXT, and db.updateTaskValueSummary
|
|
6
|
+
// driven against a stubbed pool so the real `SET value_summary =` SQL is proven.
|
|
7
|
+
// Neither exercises the WIRING, and the wiring is where this class of change
|
|
8
|
+
// fails — the whole defect being fixed is a value that reached three consumers
|
|
9
|
+
// and never reached the row. Specifically, source text cannot see that the
|
|
10
|
+
// route's STRICT validator (ADR 0118) actually admits the new field: before this
|
|
11
|
+
// task, `value_summary` on this route was a hard 400 `unknown_field`, and a
|
|
12
|
+
// schema entry that is subtly wrong (mis-typed, mis-named, declared on the
|
|
13
|
+
// create schema instead) fails exactly the same way while every text assertion
|
|
14
|
+
// still passes.
|
|
15
|
+
//
|
|
16
|
+
// So this drives the REAL router over a real socket with the db layer stubbed at
|
|
17
|
+
// the module boundary, the way task_visual_route_gate.mjs and
|
|
18
|
+
// goal_archive_route_e2e.mjs do — the router under test is the shipped one.
|
|
19
|
+
//
|
|
20
|
+
// THE CASES WORTH THE FILE: (2) proves acceptance 1 end to end — a corrected
|
|
21
|
+
// summary reaches the write. (4) and (5) prove acceptance 2 from the other side:
|
|
22
|
+
// an OMITTED field must not touch the row at all, and a whitespace-only one must
|
|
23
|
+
// be refused rather than trimmed into a blank record. `value_summary` is the
|
|
24
|
+
// public shipped-value sentence, so blanking it is the one failure mode that is
|
|
25
|
+
// worse than the bug.
|
|
26
|
+
//
|
|
27
|
+
// Run: node tests/patch_value_summary_e2e.mjs
|
|
28
|
+
|
|
29
|
+
import { strict as assert } from 'node:assert';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
process.env.NODE_ENV = 'test';
|
|
33
|
+
|
|
34
|
+
const express = require('express');
|
|
35
|
+
const api = require('../src/module-api.js');
|
|
36
|
+
const db = require('../modules/lifecycle/db.js');
|
|
37
|
+
|
|
38
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: '3', rank: 'archon' }; next(); };
|
|
39
|
+
api.requirePermission = () => (req, _res, next) => next();
|
|
40
|
+
|
|
41
|
+
const TASK = { id: 5150, title: 'a task', status: 'completed', version_id: 'BONGOS-V2', value_summary: 'the FIRST ship sentence' };
|
|
42
|
+
|
|
43
|
+
// Every write the handler can reach, recorded rather than performed — so a test
|
|
44
|
+
// can assert not only what WAS written but that nothing else was.
|
|
45
|
+
let writes = [];
|
|
46
|
+
db.getTask = async () => ({ ...TASK });
|
|
47
|
+
db.updateTaskValueSummary = async (id, v) => { writes.push({ fn: 'value_summary', id, v }); return { ...TASK, value_summary: v }; };
|
|
48
|
+
db.updateTaskTitle = async (id, v) => { writes.push({ fn: 'title', id, v }); return { ...TASK, title: v }; };
|
|
49
|
+
db.updateTaskKind = async (id, v) => { writes.push({ fn: 'kind', id, v }); return { ...TASK, kind: v }; };
|
|
50
|
+
|
|
51
|
+
const app = express();
|
|
52
|
+
app.use(express.json());
|
|
53
|
+
app.use((req, res, next) => {
|
|
54
|
+
res.fail = (code, statusOrOpts, details) => {
|
|
55
|
+
const o = typeof statusOrOpts === 'number' ? { status: statusOrOpts } : (statusOrOpts || {});
|
|
56
|
+
return res.status(o.status || 400).json({ error: code, message: o.message, details: o.details ?? details });
|
|
57
|
+
};
|
|
58
|
+
next();
|
|
59
|
+
});
|
|
60
|
+
app.use('/api/gds', require('../modules/lifecycle/routes/tasks.js')());
|
|
61
|
+
const server = app.listen(0);
|
|
62
|
+
await new Promise((r) => server.once('listening', r));
|
|
63
|
+
const base = `http://127.0.0.1:${server.address().port}/api/gds`;
|
|
64
|
+
|
|
65
|
+
const patch = async (body) => {
|
|
66
|
+
writes = [];
|
|
67
|
+
const r = await fetch(`${base}/tasks/${TASK.id}`, {
|
|
68
|
+
method: 'PATCH',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify(body),
|
|
71
|
+
});
|
|
72
|
+
return { status: r.status, body: await r.json().catch(() => null), writes };
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let passed = 0;
|
|
76
|
+
let failed = 0;
|
|
77
|
+
async function t(name, fn) {
|
|
78
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
79
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('PATCH /tasks/:id value_summary (task 1003817):');
|
|
83
|
+
|
|
84
|
+
// 1. The regression guard: this is what the route used to do with the field.
|
|
85
|
+
await t('the field is no longer rejected as an unknown field', async () => {
|
|
86
|
+
const r = await patch({ value_summary: 'anything' });
|
|
87
|
+
assert.notEqual(r.body && r.body.error, 'unknown_field',
|
|
88
|
+
'the strict validator must ADMIT value_summary — a missing schema entry looks exactly like the original bug');
|
|
89
|
+
assert.notEqual(r.body && r.body.error, 'no_supported_fields',
|
|
90
|
+
'and the supported-fields guard must count it, or a value_summary-only PATCH is refused');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// 2. ACCEPTANCE 1, end to end.
|
|
94
|
+
await t('a corrected summary reaches the write, trimmed', async () => {
|
|
95
|
+
const r = await patch({ value_summary: ' the CORRECTED sentence ' });
|
|
96
|
+
assert.equal(r.status, 200, `expected 200, got ${r.status} ${JSON.stringify(r.body)}`);
|
|
97
|
+
assert.deepEqual(r.writes, [{ fn: 'value_summary', id: TASK.id, v: 'the CORRECTED sentence' }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 3. It composes with the other planning fields rather than replacing them.
|
|
101
|
+
await t('it can travel with another field in one body', async () => {
|
|
102
|
+
const r = await patch({ value_summary: 'both', title: 'new title' });
|
|
103
|
+
assert.equal(r.status, 200);
|
|
104
|
+
const fns = r.writes.map((w) => w.fn).sort();
|
|
105
|
+
assert.deepEqual(fns, ['title', 'value_summary']);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 4. ACCEPTANCE 2, from the route's side.
|
|
109
|
+
await t('ACCEPTANCE 2: an omitted value_summary writes nothing at all', async () => {
|
|
110
|
+
const r = await patch({ title: 'only the title' });
|
|
111
|
+
assert.equal(r.status, 200);
|
|
112
|
+
assert.deepEqual(r.writes.filter((w) => w.fn === 'value_summary'), [],
|
|
113
|
+
'an omitted field must not reach the column — not even to write back what was there');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 5. The failure mode worse than the bug.
|
|
117
|
+
await t('a blank or whitespace-only summary is REFUSED, never written', async () => {
|
|
118
|
+
for (const bad of ['', ' ', '\t\n ']) {
|
|
119
|
+
const r = await patch({ value_summary: bad });
|
|
120
|
+
assert.equal(r.status, 400, `${JSON.stringify(bad)} must be refused, got ${r.status}`);
|
|
121
|
+
assert.deepEqual(r.writes, [], `${JSON.stringify(bad)} must not reach the column`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 6. And the refusal has to be diagnosable, since the caller is usually a script.
|
|
126
|
+
await t('the refusal names the field and says omission is how you leave it alone', async () => {
|
|
127
|
+
const r = await patch({ value_summary: ' ' });
|
|
128
|
+
assert.equal(r.body.error, 'bad_value_summary');
|
|
129
|
+
assert.match(r.body.message, /never blanked/i);
|
|
130
|
+
assert.match(r.body.message, /omit/i);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 7. The cap is the shared one, not a number re-typed on this route.
|
|
134
|
+
await t('an over-long summary is refused by the shared LIMITS cap', async () => {
|
|
135
|
+
const r = await patch({ value_summary: 'x'.repeat(api.LIMITS.VALUE_SUMMARY + 1) });
|
|
136
|
+
assert.equal(r.status, 400);
|
|
137
|
+
assert.deepEqual(r.writes, []);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
server.close();
|
|
141
|
+
console.log(`\npatch_value_summary_e2e: ${passed} passed, ${failed} failed`);
|
|
142
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -242,15 +242,16 @@ test('the refusal is wired into the regrade path, before any grade spend', () =>
|
|
|
242
242
|
.replace(/\/\/[^\n]*/g, '')), 'usage does not offer --notes');
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
test('ACCEPTANCE 3: --summary reaches the
|
|
245
|
+
test('ACCEPTANCE 3: --summary reaches the grader AND the task row on a re-grade', () => {
|
|
246
246
|
// Task 1003702's third acceptance asked whether --summary reaches the stored
|
|
247
|
-
// value_summary on a re-grade. It
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
247
|
+
// value_summary on a re-grade. It did NOT — a separate defect, filed and fixed
|
|
248
|
+
// as task 1003817, which added value_summary to the PATCH /tasks/:id schema and
|
|
249
|
+
// the persistRegradeSummary call below. This test used to pin the ABSENCE of a
|
|
250
|
+
// writer; it now pins its PRESENCE, for the reason this whole file exists: the
|
|
251
|
+
// panel reads the DIFF, so an answer that lives only in ship notes is an answer
|
|
252
|
+
// nobody can check — which is the very bug task 1003702 was.
|
|
252
253
|
const src = readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
|
|
253
|
-
const regrade = src.slice(src.indexOf('async function
|
|
254
|
+
const regrade = src.slice(src.indexOf('async function persistRegradeSummary'));
|
|
254
255
|
const code = regrade.replace(/\/\/[^\n]*/g, ''); // comments describe, they do not write
|
|
255
256
|
|
|
256
257
|
// Half one — the threading is intact, so nothing regressed: --summary still
|
|
@@ -261,10 +262,38 @@ test('ACCEPTANCE 3: --summary reaches the GRADER on a re-grade but never the tas
|
|
|
261
262
|
'the summary still reaches the grader prompt');
|
|
262
263
|
assert.match(code, /valueSummary: summary/, 'and the completion card');
|
|
263
264
|
|
|
264
|
-
// Half two — and there
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
assert.
|
|
269
|
-
'regradeMain
|
|
265
|
+
// Half two — and now there IS a writer, wired into the path (the (1) lesson:
|
|
266
|
+
// a helper nothing calls is not a fix).
|
|
267
|
+
assert.match(code, /patchTasksId\(\{[\s\S]{0,80}?body: \{ value_summary: next \}/,
|
|
268
|
+
'persistRegradeSummary PATCHes value_summary onto the task row');
|
|
269
|
+
assert.match(code, /await persistRegradeSummary\(\{ taskId, summaryArg, task \}\)/,
|
|
270
|
+
'and regradeMain actually calls it');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('a re-grade that omits --summary leaves the stored value_summary alone', () => {
|
|
274
|
+
// ACCEPTANCE 2. The row must survive an omitted flag: `summary` falls back to
|
|
275
|
+
// task.value_summary, so writing that fallback back would be a no-op at best
|
|
276
|
+
// and, on any future refactor of the fallback, a way to overwrite the record
|
|
277
|
+
// with something derived. persistRegradeSummary keys on summaryArg — the RAW
|
|
278
|
+
// flag — not on the resolved `summary`, and returns before the PATCH when it
|
|
279
|
+
// is absent. Pinned as source structure because the alternative (a live
|
|
280
|
+
// re-grade) costs a 4-worker panel run.
|
|
281
|
+
const src = readFileSync(new URL('../scripts/gds/ship-flow.js', import.meta.url), 'utf8');
|
|
282
|
+
const start = src.indexOf('async function persistRegradeSummary');
|
|
283
|
+
const fn = src.slice(start, src.indexOf('async function regradeMain', start));
|
|
284
|
+
const code = fn.replace(/\/\/[^\n]*/g, '');
|
|
285
|
+
|
|
286
|
+
// It reads the raw flag, never the resolved fallback.
|
|
287
|
+
assert.match(code, /const next = \(summaryArg \|\| ''\)\.trim\(\)/,
|
|
288
|
+
'the guard keys on the raw --summary, not the stored-value fallback');
|
|
289
|
+
|
|
290
|
+
// Both early returns precede the only PATCH in the function.
|
|
291
|
+
const omitted = code.indexOf("reason: 'omitted'");
|
|
292
|
+
const unchanged = code.indexOf("reason: 'unchanged'");
|
|
293
|
+
const patch = code.indexOf('patchTasksId');
|
|
294
|
+
assert.ok(omitted !== -1 && omitted < patch, 'an omitted --summary returns before the write');
|
|
295
|
+
assert.ok(unchanged !== -1 && unchanged < patch, 'an unchanged --summary returns before the write too');
|
|
296
|
+
|
|
297
|
+
// And a failed write never aborts the re-grade — no process.exit, no throw.
|
|
298
|
+
assert.ok(!/process\.exit/.test(code), 'a summary that will not persist must not kill the re-grade');
|
|
270
299
|
});
|