@bongos/core 1.19.22 → 1.19.23
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 +20 -20
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -2
- package/docs/api-reference.md +1 -1
- package/modules/grading/grader.js +6 -0
- package/modules/lifecycle/routes/done-when.js +22 -20
- package/modules/lifecycle/routes/tasks.js +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship.js +30 -0
- package/src/module-api.js +3 -1
- package/tests/criterion_authoring_gate.mjs +31 -24
- package/tests/grade_server_authoritative.mjs +7 -0
- package/tests/grade_unavailable_display.mjs +28 -0
- package/tests/override_request_ownership.mjs +8 -3
- package/tests/task_ownership_gate.mjs +26 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.23",
|
|
6
|
+
"core_contract": "1.19.23",
|
|
7
|
+
"source_commit": "fcdc2c0100a2b96183b34980f7c33572b67c7d1f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-11T01:
|
|
9
|
+
"built_at": "2026-08-11T01:44:34.493Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 285,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1510,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "80499ca716f5acc869706f3832aba22ffa0facdf31abd6eca71baec9b273d628",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -352,7 +352,7 @@
|
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/index.d.ts",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "7e6732aedaf7aaa3c657fc05e2dfd1dcc63e0fb06586d4077248d79d6198297e"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1277,12 +1277,12 @@
|
|
|
1277
1277
|
{
|
|
1278
1278
|
"path": "docs/api-reference.md",
|
|
1279
1279
|
"mode": "0000644",
|
|
1280
|
-
"sha256": "
|
|
1280
|
+
"sha256": "e28575936d1cb0cce684b20723743fd0d801ccd0a7ec58fff5360e160b23b5e7"
|
|
1281
1281
|
},
|
|
1282
1282
|
{
|
|
1283
1283
|
"path": "docs/api/openapi.json",
|
|
1284
1284
|
"mode": "0000644",
|
|
1285
|
-
"sha256": "
|
|
1285
|
+
"sha256": "b232ca69ae97842be312b76f6ac49ca66090c5c8fef392f609bb857e5f5c2979"
|
|
1286
1286
|
},
|
|
1287
1287
|
{
|
|
1288
1288
|
"path": "docs/architecture.md",
|
|
@@ -2882,7 +2882,7 @@
|
|
|
2882
2882
|
{
|
|
2883
2883
|
"path": "modules/grading/grader.js",
|
|
2884
2884
|
"mode": "0000644",
|
|
2885
|
-
"sha256": "
|
|
2885
|
+
"sha256": "a08e9c017a07bffa311a9de6df007c30d9f3766c86d0ce2ec88660fed60d37aa"
|
|
2886
2886
|
},
|
|
2887
2887
|
{
|
|
2888
2888
|
"path": "modules/grading/module.json",
|
|
@@ -3437,7 +3437,7 @@
|
|
|
3437
3437
|
{
|
|
3438
3438
|
"path": "modules/lifecycle/routes/done-when.js",
|
|
3439
3439
|
"mode": "0000644",
|
|
3440
|
-
"sha256": "
|
|
3440
|
+
"sha256": "cec4dce0ab8d11b16de73dbee8adb782f47c12a32d203dd1ff726afd23caadf3"
|
|
3441
3441
|
},
|
|
3442
3442
|
{
|
|
3443
3443
|
"path": "modules/lifecycle/routes/gate-approvals.js",
|
|
@@ -3457,7 +3457,7 @@
|
|
|
3457
3457
|
{
|
|
3458
3458
|
"path": "modules/lifecycle/routes/tasks.js",
|
|
3459
3459
|
"mode": "0000644",
|
|
3460
|
-
"sha256": "
|
|
3460
|
+
"sha256": "add6acd00adc842e90348ca789da0a473aa2dc68d949cf2b5586c68ef5eaa1be"
|
|
3461
3461
|
},
|
|
3462
3462
|
{
|
|
3463
3463
|
"path": "modules/lifecycle/routes/versions.js",
|
|
@@ -4002,12 +4002,12 @@
|
|
|
4002
4002
|
{
|
|
4003
4003
|
"path": "package-lock.json",
|
|
4004
4004
|
"mode": "0000644",
|
|
4005
|
-
"sha256": "
|
|
4005
|
+
"sha256": "969211290cee8ccee357109c325ff4741c8cce3319d520b22506126099beea7e"
|
|
4006
4006
|
},
|
|
4007
4007
|
{
|
|
4008
4008
|
"path": "package.json",
|
|
4009
4009
|
"mode": "0000644",
|
|
4010
|
-
"sha256": "
|
|
4010
|
+
"sha256": "b00d5423d5019e305bb488af793202ccb4d3089da6c129851e32f85ea4219904"
|
|
4011
4011
|
},
|
|
4012
4012
|
{
|
|
4013
4013
|
"path": "public-docs/index.html",
|
|
@@ -5197,7 +5197,7 @@
|
|
|
5197
5197
|
{
|
|
5198
5198
|
"path": "scripts/gds/ship.js",
|
|
5199
5199
|
"mode": "0000644",
|
|
5200
|
-
"sha256": "
|
|
5200
|
+
"sha256": "085a7feeff06fa7d2b833c483795161e2c4a6ee79630e2f09d7ae2f06eeb253c"
|
|
5201
5201
|
},
|
|
5202
5202
|
{
|
|
5203
5203
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5627,7 +5627,7 @@
|
|
|
5627
5627
|
{
|
|
5628
5628
|
"path": "src/module-api.js",
|
|
5629
5629
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5630
|
+
"sha256": "0e4203ec0098beb9a54a4ea56b3c5701cfca5bb02b6676114a9257f5b353aff5"
|
|
5631
5631
|
},
|
|
5632
5632
|
{
|
|
5633
5633
|
"path": "src/module-loader/loader.js",
|
|
@@ -6137,7 +6137,7 @@
|
|
|
6137
6137
|
{
|
|
6138
6138
|
"path": "tests/criterion_authoring_gate.mjs",
|
|
6139
6139
|
"mode": "0000644",
|
|
6140
|
-
"sha256": "
|
|
6140
|
+
"sha256": "c3cafa042f7ac9715569b9c3312e27b920e88497047415cb653bb576cf5380a9"
|
|
6141
6141
|
},
|
|
6142
6142
|
{
|
|
6143
6143
|
"path": "tests/criterion_progress.mjs",
|
|
@@ -6582,12 +6582,12 @@
|
|
|
6582
6582
|
{
|
|
6583
6583
|
"path": "tests/grade_server_authoritative.mjs",
|
|
6584
6584
|
"mode": "0000644",
|
|
6585
|
-
"sha256": "
|
|
6585
|
+
"sha256": "c650bd3c79d469f3ffb1cdada6f23521dd09c97844affd5b9e7f340d1796daa6"
|
|
6586
6586
|
},
|
|
6587
6587
|
{
|
|
6588
6588
|
"path": "tests/grade_unavailable_display.mjs",
|
|
6589
6589
|
"mode": "0000644",
|
|
6590
|
-
"sha256": "
|
|
6590
|
+
"sha256": "82746689da273a8bd9d53d80204b15147d87574d2331c3a70d736d95a6126a09"
|
|
6591
6591
|
},
|
|
6592
6592
|
{
|
|
6593
6593
|
"path": "tests/grader.mjs",
|
|
@@ -6992,7 +6992,7 @@
|
|
|
6992
6992
|
{
|
|
6993
6993
|
"path": "tests/override_request_ownership.mjs",
|
|
6994
6994
|
"mode": "0000644",
|
|
6995
|
-
"sha256": "
|
|
6995
|
+
"sha256": "dac9d9891eac706432aff97ce35e27692064a2791dc1e546215c74e68b81e8a8"
|
|
6996
6996
|
},
|
|
6997
6997
|
{
|
|
6998
6998
|
"path": "tests/package_core.mjs",
|
|
@@ -7502,7 +7502,7 @@
|
|
|
7502
7502
|
{
|
|
7503
7503
|
"path": "tests/task_ownership_gate.mjs",
|
|
7504
7504
|
"mode": "0000644",
|
|
7505
|
-
"sha256": "
|
|
7505
|
+
"sha256": "cfb57e35ce65f991309e4de64c03f9252e19117a1ad406cf9bb04c1716f57746"
|
|
7506
7506
|
},
|
|
7507
7507
|
{
|
|
7508
7508
|
"path": "tests/touches_overlap.mjs",
|
|
@@ -131,7 +131,7 @@ export interface PatchMeSkillPrefsResponse { prefs: unknown; effective: unknown;
|
|
|
131
131
|
export interface PatchMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
|
|
132
132
|
export interface PatchProjectsIdFeaturedRequest { featured: boolean }
|
|
133
133
|
export interface PatchProjectsIdFeaturedResponse { project: unknown }
|
|
134
|
-
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 }
|
|
134
|
+
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; status?: unknown }
|
|
135
135
|
export interface PostAccessRequestsRequest { github_login: string; display_name?: string; note?: string }
|
|
136
136
|
export interface PostAccessRequestsResponse { ok: boolean; request?: unknown; already_pending?: boolean }
|
|
137
137
|
export interface PostAuthAppPairApproveRequest { pair_code: string }
|
package/docs/api/openapi.json
CHANGED
|
@@ -12008,7 +12008,7 @@
|
|
|
12008
12008
|
"versions"
|
|
12009
12009
|
],
|
|
12010
12010
|
"summary": "POST /versions/:id/done-when",
|
|
12011
|
-
"description": "Auth (Metic+, then the goal's own authority): create a done-when criterion at RUNTIME. ADR 0105 moved scope-truth seeds (versions/goals/criteria) out of the normal migration path, so a brand-new criterion on a RUNNING instance can no longer be added by a plain migration (migrations/instance/ applies only under the owner opt-in GDS_APPLY_INSTANCE_SEEDS=1, a DR step). This is the vector a planning session uses to add a criterion to a live version WITHOUT a droplet migrate. ADR 0154 — authority over a GOAL's criteria follows GOAL OWNERSHIP, not global rank. A Metic who may create the goal (POST /goals) and author the tasks under it (POST /tasks, ADR 0090) may now write the criteria those tasks gate — but only inside a goal they own or manage. Deliberately NOT a flat re-rank: - An unsatisfied criterion permanently blocks version close (version-close.js counts satisfied === total per VERSION, regardless of goal) and goal auto-achieve (db.achieveGoalIfComplete). There is NO delete route, so it cannot be undone via the API — the same backward ratchet as unsatisfy, which stays Archon. - So a non-Archon must name a goal_id (version-level criteria stay Archon), must be that goal's OWNER or MANAGER (isGoalOwnerOrManager — the same predicate behind visibility/invite/role management), and the goal must be open on a planning/building version. That bounds the blast radius to a workspace they already hold. An unauthorized caller
|
|
12011
|
+
"description": "Auth (Metic+, then the goal's own authority): create a done-when criterion at RUNTIME. ADR 0105 moved scope-truth seeds (versions/goals/criteria) out of the normal migration path, so a brand-new criterion on a RUNNING instance can no longer be added by a plain migration (migrations/instance/ applies only under the owner opt-in GDS_APPLY_INSTANCE_SEEDS=1, a DR step). This is the vector a planning session uses to add a criterion to a live version WITHOUT a droplet migrate. ADR 0154 — authority over a GOAL's criteria follows GOAL OWNERSHIP, not global rank. A Metic who may create the goal (POST /goals) and author the tasks under it (POST /tasks, ADR 0090) may now write the criteria those tasks gate — but only inside a goal they own or manage. Deliberately NOT a flat re-rank: - An unsatisfied criterion permanently blocks version close (version-close.js counts satisfied === total per VERSION, regardless of goal) and goal auto-achieve (db.achieveGoalIfComplete). There is NO delete route, so it cannot be undone via the API — the same backward ratchet as unsatisfy, which stays Archon. - So a non-Archon must name a goal_id (version-level criteria stay Archon), must be that goal's OWNER or MANAGER (isGoalOwnerOrManager — the same predicate behind visibility/invite/role management), and the goal must be open on a planning/building version. That bounds the blast radius to a workspace they already hold. An unauthorized caller gets an honest 403 goal_not_yours (task 1002614 — goal reads are open to any builder, so there is no existence oracle for a mask to protect). Pinned in route-rank-check.MODULE_EXPECTED_RANKS. rank: metic+archon — criterion create, walled by goal ownership (ADR 0154).\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `criterion.create` (all required).",
|
|
12012
12012
|
"x-rank": "metic+archon",
|
|
12013
12013
|
"x-source": "modules/lifecycle/routes/done-when.js",
|
|
12014
12014
|
"x-permissions": [
|
|
@@ -13788,7 +13788,8 @@
|
|
|
13788
13788
|
"automation_tag": {
|
|
13789
13789
|
"type": "string",
|
|
13790
13790
|
"maxLength": 64
|
|
13791
|
-
}
|
|
13791
|
+
},
|
|
13792
|
+
"status": {}
|
|
13792
13793
|
},
|
|
13793
13794
|
"additionalProperties": false
|
|
13794
13795
|
},
|
package/docs/api-reference.md
CHANGED
|
@@ -463,7 +463,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
463
463
|
| GET | `/api/bongos/tasks` | `any-builder` | — | reads (filtered task list); preserved per CRITICAL §4. |
|
|
464
464
|
| 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 … |
|
|
465
465
|
| GET | `/api/bongos/tasks/:id` | `any-builder` | — | single-task read; preserved per CRITICAL §4 (supervisor hot path). |
|
|
466
|
-
| 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` | PATCH /tasks/:id — supports body.parent_task_id (V2 C2), body.kind (V2 B1), body.discipline (mig 014), body.parallel_safe (mig 028), and … |
|
|
466
|
+
| 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`, `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 … |
|
|
467
467
|
| POST | `/api/bongos/tasks/:id/abandon` | `metic+archon` | `reason` | POST /tasks/:id/abandon — terminal "retire this task" transition (task 987). |
|
|
468
468
|
| POST | `/api/bongos/tasks/:id/attest-gate` | `any-builder` | `head_sha` | POST /tasks/:id/attest-gate — body: { head_sha }. |
|
|
469
469
|
| POST | `/api/bongos/tasks/:id/confirm` | `any-builder` | `verified` | |
|
|
@@ -734,6 +734,12 @@ function serverSideGuards(gradeResult, { committedFiles, builderRank, diffStats,
|
|
|
734
734
|
downgraded_to: downgradeTo,
|
|
735
735
|
client_passed: clientPassed,
|
|
736
736
|
tripped: guardsTripped,
|
|
737
|
+
// task 1002614 (map P-2): a permission_path trip is an AUTHORIZATION
|
|
738
|
+
// refusal of the builder's rank over protected paths — not a verdict on
|
|
739
|
+
// the code. Named here so clients render it as a wall, not a quality
|
|
740
|
+
// FAIL the builder should fix-and-retry (the envelope used to teach
|
|
741
|
+
// exactly that wrong lesson). route_rank stays a code finding.
|
|
742
|
+
authorization_refusal: guardsTripped.some((g) => g.guard === 'permission_path'),
|
|
737
743
|
},
|
|
738
744
|
},
|
|
739
745
|
notes_md: `${base.notes_md || ''}${guardNote}`,
|
|
@@ -26,11 +26,11 @@ const { validateOrRespond, parseId, asyncHandler, LIMITS } = api;
|
|
|
26
26
|
// pinned by a DB-free unit test (the authorizeGoalTaskCreate precedent in
|
|
27
27
|
// routes/goals.js). The route resolves goal/version first — their absence is a
|
|
28
28
|
// 404/400 there — so this gate only decides AUTHORITY, in this order:
|
|
29
|
-
// archon → allow · goal named? · owner|manager? (
|
|
30
|
-
// Authority sits ahead of both state checks on purpose —
|
|
31
|
-
// An Archon short-circuits: version-level
|
|
32
|
-
//
|
|
33
|
-
// { ok:
|
|
29
|
+
// archon → allow · goal named? · owner|manager? (403) · goal open? · version open?
|
|
30
|
+
// Authority sits ahead of both state checks on purpose — an outsider never learns
|
|
31
|
+
// goal state from the 409s below. An Archon short-circuits: version-level
|
|
32
|
+
// (goal-less) criteria and any goal in any state stay theirs. Returns
|
|
33
|
+
// { ok:true } or { ok:false, status, body }.
|
|
34
34
|
//
|
|
35
35
|
// "Version open" = `planning` or `building`. NOT just `building`: a planning session
|
|
36
36
|
// scopes a version at status='planning', which is the main moment criteria get
|
|
@@ -47,16 +47,18 @@ function authorizeCriterionCreate({ goal, version, actorId, actorRank, actorMemb
|
|
|
47
47
|
message: 'Name the goal this criterion belongs to — a version-level criterion is Archon-only (ADR 0154).',
|
|
48
48
|
} };
|
|
49
49
|
}
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// already holds the goal learns anything further about it.
|
|
50
|
+
// Authority BEFORE state (an outsider still never sees goal state via the 409
|
|
51
|
+
// below). Until task 1002614 the refusal here was MASKED behind the same
|
|
52
|
+
// `bad_goal_id` 400 as an absent goal, as an anti-oracle for goal ids (ADR
|
|
53
|
+
// 0112). The mask asserted a falsehood about a goal that exists and bought no
|
|
54
|
+
// secrecy: ADR 0112 privacy is join-gating, not read-hiding — GET /goals/:id
|
|
55
|
+
// serves any authenticated builder the goal, its members, criteria and state
|
|
56
|
+
// (choke-point map D-3). So the refusal now names its wall.
|
|
58
57
|
if (!isGoalOwnerOrManager({ goal, actorId, actorRank, actorMembership })) {
|
|
59
|
-
return { ok: false,
|
|
58
|
+
return { ok: false, status: 403, body: {
|
|
59
|
+
error: 'goal_not_yours',
|
|
60
|
+
message: `Criteria on goal ${goal.id} can be authored only by the goal's owner or a manager (ADR 0154). Ask them to add it — or to grant you the manager role.`,
|
|
61
|
+
} };
|
|
60
62
|
}
|
|
61
63
|
if (goal.status !== 'open') {
|
|
62
64
|
return { ok: false, status: 409, body: {
|
|
@@ -112,8 +114,9 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
112
114
|
// must be that goal's OWNER or MANAGER (isGoalOwnerOrManager — the same
|
|
113
115
|
// predicate behind visibility/invite/role management), and the goal must be
|
|
114
116
|
// open on a planning/building version. That bounds the blast radius to a
|
|
115
|
-
// workspace they already hold. An unauthorized caller
|
|
116
|
-
//
|
|
117
|
+
// workspace they already hold. An unauthorized caller gets an honest 403
|
|
118
|
+
// goal_not_yours (task 1002614 — goal reads are open to any builder, so
|
|
119
|
+
// there is no existence oracle for a mask to protect).
|
|
117
120
|
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS.
|
|
118
121
|
// rank: metic+archon — criterion create, walled by goal ownership (ADR 0154).
|
|
119
122
|
router.post('/versions/:id/done-when', auth.requireBuilder, auth.requirePermission('criterion.create'), asyncHandler('POST /versions/:id/done-when', async (req, res) => {
|
|
@@ -127,9 +130,9 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
127
130
|
})) return;
|
|
128
131
|
const version = await db.getVersion(versionId);
|
|
129
132
|
if (!version) return res.fail('version_not_found', { status: 404, message: `No version '${versionId}'.` });
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
+
// bad_goal_id now means exactly what it says: the goal is absent or belongs
|
|
134
|
+
// to another version. (The unauthorized case rendered through this same
|
|
135
|
+
// helper until task 1002614 — see the mask note in authorizeCriterionCreate.)
|
|
133
136
|
const badGoalId = () => res.fail('bad_goal_id', { status: 400, message: `goal_id ${req.body.goal_id} does not exist on version '${versionId}'.` });
|
|
134
137
|
// A goal_id, if given, must exist AND belong to this version.
|
|
135
138
|
let goal = null;
|
|
@@ -148,7 +151,6 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
148
151
|
goal, version, actorId: req.builder.id, actorRank: req.builder.rank, actorMembership,
|
|
149
152
|
});
|
|
150
153
|
if (!decision.ok) {
|
|
151
|
-
if (decision.maskAsBadGoalId) return badGoalId();
|
|
152
154
|
return res.status(decision.status).json(decision.body);
|
|
153
155
|
}
|
|
154
156
|
try {
|
|
@@ -129,6 +129,15 @@ async function gateTaskOwnership(req, res, taskId, { allowSupervisor = false } =
|
|
|
129
129
|
if (allowSupervisor && rank === 'metic') return true;
|
|
130
130
|
const holder = await db.getLastClaimHolder(taskId);
|
|
131
131
|
if (holder && String(holder.builder_id) === String(req.builder.id)) return true;
|
|
132
|
+
// task 1002614 (map N-2): a mistyped id used to earn the same not_your_task
|
|
133
|
+
// 403 as someone else's work — a misattribution that sent builders debugging
|
|
134
|
+
// ownership. An ABSENT task is the clean 404 an Archon already saw (they pass
|
|
135
|
+
// above and hit the handler's own lookup). A never-claimed task that EXISTS
|
|
136
|
+
// still fails closed with the 403 below (the L2 credit-theft block).
|
|
137
|
+
if (!holder && !(await db.getTask(taskId))) {
|
|
138
|
+
res.fail('task_not_found', 404);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
132
141
|
res.fail('not_your_task', { status: 403, message: 'Only the task\'s claim holder (or an Archon) may grade/confirm/ship it.' });
|
|
133
142
|
return false;
|
|
134
143
|
}
|
|
@@ -764,6 +773,11 @@ module.exports = function buildTasksRouter() {
|
|
|
764
773
|
xenos_claimable: { type: 'boolean' }, // pre-rename alias for newcomer_friendly
|
|
765
774
|
security_sensitive: { type: 'boolean' }, // task 990 — Archon-only ship broadcast
|
|
766
775
|
automation_tag: { type: 'string', maxLength: 64 },
|
|
776
|
+
// task 1002614 (map D-18): DECLARED so strict-mode admits the request far
|
|
777
|
+
// enough to reach the deliberate status_not_patchable hint below (#514) —
|
|
778
|
+
// the hint had been unreachable dead code behind unknown_field since the
|
|
779
|
+
// R11 strict-validation sweep landed after it.
|
|
780
|
+
status: {},
|
|
767
781
|
})) return;
|
|
768
782
|
const id = parseId(req, res, { code: 'bad_task_id', positiveInt: true });
|
|
769
783
|
if (id === null) return;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.23",
|
|
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.23",
|
|
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.23",
|
|
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.js
CHANGED
|
@@ -2776,6 +2776,36 @@ function formatGradeOutcome(g, taskId) {
|
|
|
2776
2776
|
return out.join('\n');
|
|
2777
2777
|
}
|
|
2778
2778
|
|
|
2779
|
+
// task 1002614 (map P-2): a protected-path refusal used to render as an
|
|
2780
|
+
// ordinary quality FAIL — "fix the issues and --regrade" — although no rank of
|
|
2781
|
+
// the builder's can land that diff. Present it as the authorization wall it is
|
|
2782
|
+
// (the #943 grader-unavailable branch is the precedent for a distinct shape).
|
|
2783
|
+
// Keyed off the guard name so it also reads grades from servers predating the
|
|
2784
|
+
// authorization_refusal marker.
|
|
2785
|
+
const guardTripped = g.signals?.server_guard?.tripped || [];
|
|
2786
|
+
const permRefusal = g.signals?.server_guard?.authorization_refusal === true
|
|
2787
|
+
|| guardTripped.some((t) => t.guard === 'permission_path');
|
|
2788
|
+
if (!g.passed && permRefusal) {
|
|
2789
|
+
out.push('');
|
|
2790
|
+
out.push('Subagent grade: REFUSED (authorization) · not a quality verdict — your work was not judged and rejected');
|
|
2791
|
+
out.push('');
|
|
2792
|
+
out.push('The committed diff touches PROTECTED surfaces above your rank, so the server');
|
|
2793
|
+
out.push('refused to accept it — no re-attempt at your rank can land this diff as-is.');
|
|
2794
|
+
if ((g.issues || []).length > 0) {
|
|
2795
|
+
out.push('');
|
|
2796
|
+
out.push('What was refused (file · rule):');
|
|
2797
|
+
for (const it of g.issues.filter((i) => String(i.worker || '').includes('permission-path'))) {
|
|
2798
|
+
out.push(` ${it.file || '?'} — ${it.summary || ''}`);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
out.push('');
|
|
2802
|
+
out.push('Ways forward:');
|
|
2803
|
+
out.push(' - Split the protected files out of your diff and re-ship the rest, or');
|
|
2804
|
+
out.push(' - hand this task to a builder at/above the floor the refusal names.');
|
|
2805
|
+
out.push(`(--regrade re-runs the same server check and will refuse again unless the diff changes.)`);
|
|
2806
|
+
return out.join('\n');
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2779
2809
|
const verdict = g.passed ? 'PASS' : 'FAIL';
|
|
2780
2810
|
out.push('');
|
|
2781
2811
|
out.push(`Subagent grade: ${verdict} · blended ${g.score}/10 · threshold ${g.threshold} (both axes)`);
|
package/src/module-api.js
CHANGED
|
@@ -471,8 +471,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
471
471
|
// landed since 1.19.20 with no explicit bump. run 31446766597. (task 1002620)
|
|
472
472
|
// 1.19.22 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
473
473
|
// landed since 1.19.21 with no explicit bump. run 31449168428. (task 1002620)
|
|
474
|
+
// 1.19.23 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
475
|
+
// landed since 1.19.22 with no explicit bump. run 31450197816. (task 1002620)
|
|
474
476
|
// ---------------------------------------------------------------------------
|
|
475
|
-
const CORE_VERSION = '1.19.
|
|
477
|
+
const CORE_VERSION = '1.19.23'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
476
478
|
|
|
477
479
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
478
480
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -84,13 +84,15 @@ await test('a non-Archon with NO goal_id is refused — version-level criteria s
|
|
|
84
84
|
await test('a plain MEMBER may not author — membership is not management', () => {
|
|
85
85
|
const d = ask({ goal: G_OPEN, actorId: MEMBER, actorMembership: PLAIN });
|
|
86
86
|
assert.equal(d.ok, false);
|
|
87
|
-
assert.equal(d.
|
|
87
|
+
assert.equal(d.status, 403);
|
|
88
|
+
assert.equal(d.body.error, 'goal_not_yours');
|
|
88
89
|
});
|
|
89
90
|
|
|
90
91
|
await test('a STRANGER (no membership at all) may not author', () => {
|
|
91
92
|
const d = ask({ goal: G_OPEN, actorId: STRANGER, actorMembership: null });
|
|
92
93
|
assert.equal(d.ok, false);
|
|
93
|
-
assert.equal(d.
|
|
94
|
+
assert.equal(d.status, 403);
|
|
95
|
+
assert.equal(d.body.error, 'goal_not_yours');
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
await test('an ACHIEVED goal is closed to new criteria (409) — no reopening settled scope', () => {
|
|
@@ -107,38 +109,43 @@ await test('an ARCHIVED goal is closed to new criteria (409)', () => {
|
|
|
107
109
|
assert.equal(d.body.error, 'goal_not_open');
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
// ----
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
112
|
+
// ---- the refusal is HONEST (task 1002614, map D-3) ----
|
|
113
|
+
// Until task 1002614 an unauthorized caller was MASKED behind the same
|
|
114
|
+
// bad_goal_id 400 as an absent goal, as an anti-oracle for goal ids (ADR 0112).
|
|
115
|
+
// The mask asserted a falsehood about a goal that exists and protected nothing:
|
|
116
|
+
// ADR 0112 privacy is join-gating, not read-hiding — GET /goals/:id serves any
|
|
117
|
+
// authenticated builder the goal, members, criteria and state. So the refusal
|
|
118
|
+
// now names its wall, and these pins hold it honest.
|
|
114
119
|
|
|
115
|
-
await test('an unauthorized caller
|
|
120
|
+
await test('an unauthorized caller gets an honest 403 naming the wall — not a false bad_goal_id', () => {
|
|
116
121
|
const d = ask({ goal: G_OPEN, actorId: STRANGER, actorMembership: null });
|
|
117
|
-
assert.equal(d.
|
|
118
|
-
assert.equal(d.
|
|
119
|
-
assert.
|
|
122
|
+
assert.equal(d.status, 403);
|
|
123
|
+
assert.equal(d.body.error, 'goal_not_yours');
|
|
124
|
+
assert.match(d.body.message, /owner or a manager/, 'the refusal names who CAN author');
|
|
125
|
+
assert.match(d.body.message, /ADR 0154/, 'and where the rule comes from');
|
|
120
126
|
});
|
|
121
127
|
|
|
122
|
-
await test('the
|
|
128
|
+
await test('the refusal is uniform across goal states — authority still precedes state', () => {
|
|
129
|
+
// A stranger probing open/achieved/archived/system goals sees the SAME 403 —
|
|
130
|
+
// goal state stays behind the authority wall on this route (it is readable via
|
|
131
|
+
// GET /goals/:id, but this write route does not volunteer it).
|
|
123
132
|
const probes = [G_OPEN, G_ACHIEVED, G_ARCHIVED, G_SYSTEM].map((goal) =>
|
|
124
133
|
ask({ goal, actorId: STRANGER, actorMembership: null }));
|
|
125
|
-
for (const d of probes)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
for (const d of probes) {
|
|
135
|
+
assert.equal(d.status, 403);
|
|
136
|
+
assert.equal(d.body.error, 'goal_not_yours');
|
|
137
|
+
}
|
|
129
138
|
});
|
|
130
139
|
|
|
131
|
-
await test('authority is checked BEFORE goal state —
|
|
132
|
-
// An owner on an archived goal DOES get goal_not_open (useful, and they already
|
|
133
|
-
// know the goal exists). A stranger on the same goal gets the mask instead.
|
|
140
|
+
await test('authority is checked BEFORE goal state — an owner sees goal_not_open, a stranger never does', () => {
|
|
134
141
|
assert.equal(ask({ goal: G_ARCHIVED, actorId: OWNER, actorMembership: LEAD }).body.error, 'goal_not_open');
|
|
135
|
-
assert.equal(ask({ goal: G_ARCHIVED, actorId: STRANGER, actorMembership: null }).
|
|
142
|
+
assert.equal(ask({ goal: G_ARCHIVED, actorId: STRANGER, actorMembership: null }).body.error, 'goal_not_yours');
|
|
136
143
|
});
|
|
137
144
|
|
|
138
|
-
await test('
|
|
139
|
-
assert.match(doneWhenSrc, /const badGoalId = \(\) => res\.fail\('bad_goal_id'/, 'one helper builds the refusal');
|
|
145
|
+
await test('bad_goal_id now means exactly what it says — the mask branch is gone from the route', () => {
|
|
146
|
+
assert.match(doneWhenSrc, /const badGoalId = \(\) => res\.fail\('bad_goal_id'/, 'one helper builds the absent/foreign refusal');
|
|
140
147
|
assert.match(doneWhenSrc, /if \(!goal \|\| String\(goal\.version_id\) !== String\(versionId\)\) return badGoalId\(\);/, 'absent/foreign goal path');
|
|
141
|
-
assert.
|
|
148
|
+
assert.doesNotMatch(doneWhenSrc, /maskAsBadGoalId/, 'no code path renders an authorization refusal as bad_goal_id');
|
|
142
149
|
});
|
|
143
150
|
|
|
144
151
|
await test('a SHIPPED version is refused (409) — closed scope', () => {
|
|
@@ -171,7 +178,7 @@ await test('a PLANNING version is ALLOWED — that is when criteria get written'
|
|
|
171
178
|
await test('a system goal (created_by NULL) has no owner — a Metic cannot claim it', () => {
|
|
172
179
|
const d = ask({ goal: G_SYSTEM, actorId: OWNER, actorMembership: null });
|
|
173
180
|
assert.equal(d.ok, false);
|
|
174
|
-
assert.equal(d.
|
|
181
|
+
assert.equal(d.body.error, 'goal_not_yours');
|
|
175
182
|
// ...but a manager row still admits, and an Archon always does (below).
|
|
176
183
|
assert.equal(ask({ goal: G_SYSTEM, actorId: OWNER, actorMembership: LEAD }).ok, true);
|
|
177
184
|
});
|
|
@@ -215,7 +222,7 @@ await test('ordering: goal_id_required precedes the version check', () => {
|
|
|
215
222
|
|
|
216
223
|
await test('ordering: an unauthorized caller never reaches the goal-state branch', () => {
|
|
217
224
|
const d = ask({ goal: G_ARCHIVED, actorId: STRANGER, actorMembership: null });
|
|
218
|
-
assert.equal(d.
|
|
225
|
+
assert.equal(d.body.error, 'goal_not_yours', 'the refusal is the authority wall, never goal_not_open');
|
|
219
226
|
});
|
|
220
227
|
|
|
221
228
|
// ---- the wall is the SAME one the sibling goal routes use ----
|
|
@@ -106,6 +106,11 @@ await test('sub-Metic forged PASS touching protected path → hard FAIL', () =>
|
|
|
106
106
|
assert.equal(g.signals.confidence, 0, 'fail forwards confidence 0 so the server gate also fails');
|
|
107
107
|
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'permission_path'));
|
|
108
108
|
assert.ok(g.issues.some((i) => i.worker === 'server-permission-path'));
|
|
109
|
+
// task 1002614 (map P-2): this FAIL is an AUTHORIZATION refusal, and the
|
|
110
|
+
// envelope must say so — a quality-verdict shape taught builders to
|
|
111
|
+
// fix-and-retry a diff no rank of theirs can land.
|
|
112
|
+
assert.equal(g.signals.server_guard.authorization_refusal, true,
|
|
113
|
+
'a permission_path trip is named an authorization refusal, not a quality verdict');
|
|
109
114
|
});
|
|
110
115
|
|
|
111
116
|
await test('xenos forged PASS touching migrations/ → hard FAIL', () => {
|
|
@@ -129,6 +134,8 @@ await test('route-rank hard-fail (forged routes dir) flips a client PASS to FAIL
|
|
|
129
134
|
assert.equal(g.passed, false, 'an ungated write route in the audited routes hard-fails the grade');
|
|
130
135
|
assert.equal(g.signals.server_guard.downgraded_to, 'fail');
|
|
131
136
|
assert.ok(g.signals.server_guard.tripped.some((t) => t.guard === 'route_rank'));
|
|
137
|
+
assert.equal(g.signals.server_guard.authorization_refusal, false,
|
|
138
|
+
'route_rank is a CODE finding — only permission_path is an authorization refusal (task 1002614)');
|
|
132
139
|
} finally {
|
|
133
140
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
134
141
|
}
|
|
@@ -50,6 +50,34 @@ test('a real FAIL still prints the FAIL block + fix-the-issues guidance (#943 re
|
|
|
50
50
|
assert.doesNotMatch(out, /GRADER UNAVAILABLE/);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
test('an authorization refusal (permission_path guard) → REFUSED, not a quality FAIL (task 1002614 / P-2)', () => {
|
|
54
|
+
// The server guard refused a sub-Metic diff over protected paths. The old
|
|
55
|
+
// rendering was a plain FAIL + "fix the issues" — teaching a re-attempt no
|
|
56
|
+
// rank of the builder's could ever land.
|
|
57
|
+
const g = {
|
|
58
|
+
passed: false, score: 0, threshold: 6, notes_md: '',
|
|
59
|
+
issues: [{ severity: 'blocker', file: 'migrations/099_x.sql',
|
|
60
|
+
summary: '[server permission-path] protected surface — an Archon must own it', worker: 'server-permission-path' }],
|
|
61
|
+
signals: { server_guard: { downgraded_to: 'fail', authorization_refusal: true,
|
|
62
|
+
tripped: [{ guard: 'permission_path', severity: 'blocker', summary: 'protected machinery' }] } },
|
|
63
|
+
};
|
|
64
|
+
const out = formatGradeOutcome(g, 1002614);
|
|
65
|
+
assert.match(out, /REFUSED \(authorization\)/, 'must label the outcome as an authorization refusal');
|
|
66
|
+
assert.match(out, /not a quality verdict/, 'must say the work was not judged and rejected');
|
|
67
|
+
assert.match(out, /migrations\/099_x\.sql/, 'names the refused file');
|
|
68
|
+
assert.doesNotMatch(out, /Fix the issues above/, 'must NOT teach fix-and-retry against a wall');
|
|
69
|
+
assert.doesNotMatch(out, /blended .*\/10/, 'a refusal is not a score');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the guard-name fallback also triggers the refusal shape (grades from pre-marker servers)', () => {
|
|
73
|
+
const g = {
|
|
74
|
+
passed: false, score: 0, threshold: 6, issues: [],
|
|
75
|
+
signals: { server_guard: { tripped: [{ guard: 'permission_path' }] } },
|
|
76
|
+
};
|
|
77
|
+
const out = formatGradeOutcome(g, 1);
|
|
78
|
+
assert.match(out, /REFUSED \(authorization\)/);
|
|
79
|
+
});
|
|
80
|
+
|
|
53
81
|
test('a PASS prints the PASS block, no failure guidance (#943 regression guard)', () => {
|
|
54
82
|
const g = {
|
|
55
83
|
passed: true, score: 9, threshold: 6,
|
|
@@ -71,8 +71,12 @@ const TASK = { id: 996, title: 'fixture task', credits_reward: 15, kind: 'featur
|
|
|
71
71
|
// ===========================================================================
|
|
72
72
|
|
|
73
73
|
const savedGetLastClaimHolder = db.getLastClaimHolder;
|
|
74
|
-
|
|
74
|
+
const savedGetTask = db.getTask;
|
|
75
|
+
// taskExists backs the N-2 absent-task 404 (task 1002614): the gate consults
|
|
76
|
+
// db.getTask only when no claim holder exists.
|
|
77
|
+
function stubHolder(holderId, { taskExists = true } = {}) {
|
|
75
78
|
db.getLastClaimHolder = async () => (holderId == null ? null : { builder_id: holderId });
|
|
79
|
+
db.getTask = async () => (taskExists ? { id: TASK.id } : undefined);
|
|
76
80
|
}
|
|
77
81
|
function mkReq(id, rank) {
|
|
78
82
|
return { builder: { id, rank, github_login: `b${id}` } };
|
|
@@ -133,9 +137,10 @@ await test('gate (L2): an Archon may still file an override-request on a never-c
|
|
|
133
137
|
assert.equal(res.statusCode, null);
|
|
134
138
|
});
|
|
135
139
|
|
|
136
|
-
// Restore the real
|
|
137
|
-
// seam and never touches getLastClaimHolder).
|
|
140
|
+
// Restore the real lookups before the attribution layer (which uses the deps.pool
|
|
141
|
+
// seam and never touches getLastClaimHolder/getTask).
|
|
138
142
|
db.getLastClaimHolder = savedGetLastClaimHolder;
|
|
143
|
+
db.getTask = savedGetTask;
|
|
139
144
|
|
|
140
145
|
// ===========================================================================
|
|
141
146
|
// Layer 2 — on approval, credit + tasks.shipped_by go to the CLAIM HOLDER.
|
|
@@ -41,8 +41,12 @@ const WORKER = 25; // the claim holder who did the work
|
|
|
41
41
|
const OTHER = 99; // an unrelated builder
|
|
42
42
|
|
|
43
43
|
// Stub the claim-holder lookup. holderId === null → no claim on record.
|
|
44
|
-
|
|
44
|
+
// taskExists backs the N-2 absent-task 404 (task 1002614): the gate consults
|
|
45
|
+
// db.getTask only when no claim holder exists, to tell a mistyped id from a
|
|
46
|
+
// never-claimed task.
|
|
47
|
+
function stubHolder(holderId, { taskExists = true } = {}) {
|
|
45
48
|
db.getLastClaimHolder = async () => (holderId == null ? null : { builder_id: holderId });
|
|
49
|
+
db.getTask = async () => (taskExists ? { id: 123 } : undefined);
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
function mkReq(id, rank) {
|
|
@@ -108,6 +112,27 @@ await test('L2: a never-claimed task FAILS CLOSED for a non-Archon (the bypass-c
|
|
|
108
112
|
assert.equal(res.body.error.code, 'not_your_task');
|
|
109
113
|
});
|
|
110
114
|
|
|
115
|
+
await test('N-2 (task 1002614): a NONEXISTENT task id is a clean 404, not not_your_task', async () => {
|
|
116
|
+
// A sub-Metic who mistypes an id used to be told the task was not theirs —
|
|
117
|
+
// the same 403 as someone else's task — while an Archon saw a clean 404.
|
|
118
|
+
stubHolder(null, { taskExists: false });
|
|
119
|
+
const res = mkRes();
|
|
120
|
+
assert.equal(await gateTaskOwnership(mkReq(OTHER, 'thetes'), res, 999999), false);
|
|
121
|
+
assert.equal(res.statusCode, 404);
|
|
122
|
+
assert.equal(res.body.error.code, 'task_not_found');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('N-2: the 404 never overrides ownership — someone ELSE\'s task stays 403 even if getTask errs', async () => {
|
|
126
|
+
// The existence lookup runs ONLY when no claim holder exists: with a holder
|
|
127
|
+
// on record the gate must answer not_your_task without consulting getTask.
|
|
128
|
+
db.getLastClaimHolder = async () => ({ builder_id: WORKER });
|
|
129
|
+
db.getTask = async () => { throw new Error('must not be called when a holder exists'); };
|
|
130
|
+
const res = mkRes();
|
|
131
|
+
assert.equal(await gateTaskOwnership(mkReq(OTHER, 'thetes'), res, 123), false);
|
|
132
|
+
assert.equal(res.statusCode, 403);
|
|
133
|
+
assert.equal(res.body.error.code, 'not_your_task');
|
|
134
|
+
});
|
|
135
|
+
|
|
111
136
|
await test('L2: a never-claimed task is still landable by an Archon (legit /confirm /grade path)', async () => {
|
|
112
137
|
stubHolder(null);
|
|
113
138
|
const res = mkRes();
|