@bongos/core 1.19.571 → 1.19.572
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 +3 -3
- package/docs/api/openapi.json +13 -4
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-goals.js +47 -2
- package/modules/lifecycle/db.js +4 -0
- package/modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql +51 -0
- package/modules/lifecycle/routes/goal-route-authz.js +7 -1
- package/modules/lifecycle/routes/goals.js +37 -4
- package/modules/lifecycle/routes/versions.js +10 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/goal_admission_override.mjs +179 -0
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_version_gate.mjs +7 -4
- package/tests/goal_tier.mjs +4 -2
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.572",
|
|
6
|
+
"core_contract": "1.19.572",
|
|
7
|
+
"source_commit": "31894c826b1cb16600f02046b2e17a09f78f3712",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-07T00:51:07.099Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 450,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2042,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2516,
|
|
20
|
+
"tree_sha256": "dc4746621684478bdfcc20dba9ccc094ed1722956378f38939e11ff3f83e35d5",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -392,7 +392,7 @@
|
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "adaa20c8ce6bee76dc289f10929014382aa89d7373f94ddafc84f4c87eeb0ed4"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1812,12 +1812,12 @@
|
|
|
1812
1812
|
{
|
|
1813
1813
|
"path": "docs/api-reference.md",
|
|
1814
1814
|
"mode": "0000644",
|
|
1815
|
-
"sha256": "
|
|
1815
|
+
"sha256": "0a622847ab68980279beba2e1a54813b2393107144f88dd31228fe4dfd248b90"
|
|
1816
1816
|
},
|
|
1817
1817
|
{
|
|
1818
1818
|
"path": "docs/api/openapi.json",
|
|
1819
1819
|
"mode": "0000644",
|
|
1820
|
-
"sha256": "
|
|
1820
|
+
"sha256": "60ca0b131c661df16fb993182d2085d0b04ea15a4d91bec4318c3413fb8dc311"
|
|
1821
1821
|
},
|
|
1822
1822
|
{
|
|
1823
1823
|
"path": "docs/architecture.md",
|
|
@@ -2692,7 +2692,7 @@
|
|
|
2692
2692
|
{
|
|
2693
2693
|
"path": "docs/module-api-changelog.md",
|
|
2694
2694
|
"mode": "0000644",
|
|
2695
|
-
"sha256": "
|
|
2695
|
+
"sha256": "d7acc0617ab16007150c0f733248576e5c285286f22b32ec722fa061748994f9"
|
|
2696
2696
|
},
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "docs/modules-contract.md",
|
|
@@ -5467,7 +5467,7 @@
|
|
|
5467
5467
|
{
|
|
5468
5468
|
"path": "modules/lifecycle/db-goals.js",
|
|
5469
5469
|
"mode": "0000644",
|
|
5470
|
-
"sha256": "
|
|
5470
|
+
"sha256": "31f39ea088b03feb217af4ce2fbf0b4dba0d8dad3e8eec625cf11e0dd4cb4228"
|
|
5471
5471
|
},
|
|
5472
5472
|
{
|
|
5473
5473
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5507,7 +5507,7 @@
|
|
|
5507
5507
|
{
|
|
5508
5508
|
"path": "modules/lifecycle/db.js",
|
|
5509
5509
|
"mode": "0000644",
|
|
5510
|
-
"sha256": "
|
|
5510
|
+
"sha256": "61ed58e2d0dbe9aa0b889d27b6a9c38df024a40bc797f22b0ff766fedf85a9de"
|
|
5511
5511
|
},
|
|
5512
5512
|
{
|
|
5513
5513
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5624,6 +5624,11 @@
|
|
|
5624
5624
|
"mode": "0000644",
|
|
5625
5625
|
"sha256": "0d17971d88c2f75a61a5646325ebab121fd4406cdd39324a1128a5e610ce6d7d"
|
|
5626
5626
|
},
|
|
5627
|
+
{
|
|
5628
|
+
"path": "modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql",
|
|
5629
|
+
"mode": "0000644",
|
|
5630
|
+
"sha256": "4e4099764712b6ac8ad322cb846eb1e4b1118e936d03628e01a6a04adb4d4ebf"
|
|
5631
|
+
},
|
|
5627
5632
|
{
|
|
5628
5633
|
"path": "modules/lifecycle/mingle.js",
|
|
5629
5634
|
"mode": "0000644",
|
|
@@ -5682,12 +5687,12 @@
|
|
|
5682
5687
|
{
|
|
5683
5688
|
"path": "modules/lifecycle/routes/goal-route-authz.js",
|
|
5684
5689
|
"mode": "0000644",
|
|
5685
|
-
"sha256": "
|
|
5690
|
+
"sha256": "5022ec9cdd2ba5087bc0b4607691c21d361f4a4edbf029ff3dea37ade373e890"
|
|
5686
5691
|
},
|
|
5687
5692
|
{
|
|
5688
5693
|
"path": "modules/lifecycle/routes/goals.js",
|
|
5689
5694
|
"mode": "0000644",
|
|
5690
|
-
"sha256": "
|
|
5695
|
+
"sha256": "d408d89cf736a7d4cdf45ccff44c2038bbc64b13be42dd1b460198dfc8112535"
|
|
5691
5696
|
},
|
|
5692
5697
|
{
|
|
5693
5698
|
"path": "modules/lifecycle/routes/help-requests.js",
|
|
@@ -5747,7 +5752,7 @@
|
|
|
5747
5752
|
{
|
|
5748
5753
|
"path": "modules/lifecycle/routes/versions.js",
|
|
5749
5754
|
"mode": "0000644",
|
|
5750
|
-
"sha256": "
|
|
5755
|
+
"sha256": "5efe819220f9128844793a0c068babcd402860b434aab322fd9a99f8d2d26167"
|
|
5751
5756
|
},
|
|
5752
5757
|
{
|
|
5753
5758
|
"path": "modules/lifecycle/routes/visuals.js",
|
|
@@ -7542,12 +7547,12 @@
|
|
|
7542
7547
|
{
|
|
7543
7548
|
"path": "package-lock.json",
|
|
7544
7549
|
"mode": "0000644",
|
|
7545
|
-
"sha256": "
|
|
7550
|
+
"sha256": "439507cc695f5ef93f4fe1aa9a48556b4b9e11a023fa4e60912de1ed8befc5ee"
|
|
7546
7551
|
},
|
|
7547
7552
|
{
|
|
7548
7553
|
"path": "package.json",
|
|
7549
7554
|
"mode": "0000644",
|
|
7550
|
-
"sha256": "
|
|
7555
|
+
"sha256": "bf5fe42d980095754ea4b13501ede3a559376dae4127a99915767ba4be0c27d2"
|
|
7551
7556
|
},
|
|
7552
7557
|
{
|
|
7553
7558
|
"path": "public-docs/index.html",
|
|
@@ -9247,7 +9252,7 @@
|
|
|
9247
9252
|
{
|
|
9248
9253
|
"path": "src/module-api.js",
|
|
9249
9254
|
"mode": "0000644",
|
|
9250
|
-
"sha256": "
|
|
9255
|
+
"sha256": "be71a580784409ac5e4507201fbbf9915e16f0682098e3b9e98ec49a0af98216"
|
|
9251
9256
|
},
|
|
9252
9257
|
{
|
|
9253
9258
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10329,6 +10334,11 @@
|
|
|
10329
10334
|
"mode": "0000644",
|
|
10330
10335
|
"sha256": "3e157d5bf44370e4935b70203fe59e40cf9120d12c472f24ed7bd85cb33cbe91"
|
|
10331
10336
|
},
|
|
10337
|
+
{
|
|
10338
|
+
"path": "tests/goal_admission_override.mjs",
|
|
10339
|
+
"mode": "0000644",
|
|
10340
|
+
"sha256": "94b378fb96bbbb6285b61c4d00827f57e0b29d64d9036576670ffaf8ca233239"
|
|
10341
|
+
},
|
|
10332
10342
|
{
|
|
10333
10343
|
"path": "tests/goal_advisory.mjs",
|
|
10334
10344
|
"mode": "0000644",
|
|
@@ -10342,7 +10352,7 @@
|
|
|
10342
10352
|
{
|
|
10343
10353
|
"path": "tests/goal_category.mjs",
|
|
10344
10354
|
"mode": "0000644",
|
|
10345
|
-
"sha256": "
|
|
10355
|
+
"sha256": "dbdfd351d5edc3ac6adcb1a34e072fab0a8db9669030d7476bbf18aa1e81540d"
|
|
10346
10356
|
},
|
|
10347
10357
|
{
|
|
10348
10358
|
"path": "tests/goal_category_ui.mjs",
|
|
@@ -10367,7 +10377,7 @@
|
|
|
10367
10377
|
{
|
|
10368
10378
|
"path": "tests/goal_create_version_gate.mjs",
|
|
10369
10379
|
"mode": "0000644",
|
|
10370
|
-
"sha256": "
|
|
10380
|
+
"sha256": "28da055ccad13b327135dce6292df9c6bc53e2589648e0a4361ada0ddd21b863"
|
|
10371
10381
|
},
|
|
10372
10382
|
{
|
|
10373
10383
|
"path": "tests/goal_criterion_confirm_ui.mjs",
|
|
@@ -10422,7 +10432,7 @@
|
|
|
10422
10432
|
{
|
|
10423
10433
|
"path": "tests/goal_tier.mjs",
|
|
10424
10434
|
"mode": "0000644",
|
|
10425
|
-
"sha256": "
|
|
10435
|
+
"sha256": "3c420d28341a6e5aaeb5d4af978e72acefec3c9bd7d97f66fd11a72517118606"
|
|
10426
10436
|
},
|
|
10427
10437
|
{
|
|
10428
10438
|
"path": "tests/goal_wall_proof.mjs",
|
|
@@ -49,7 +49,7 @@ export interface GetGithubRepoResponse { ok: boolean; repo: unknown; private_dep
|
|
|
49
49
|
export interface GetGithubReposResponse { repos: unknown }
|
|
50
50
|
export interface GetGoalsIdConflictsResponse { ok: boolean; goal_id: unknown; conflicts: unknown; cross_goals_considered: unknown }
|
|
51
51
|
export interface GetGoalsIdRequestsResponse { requests: unknown }
|
|
52
|
-
export interface GetGoalsIdResponse { goal: unknown; members: unknown; criteria: unknown; dependencies: unknown; criterionless_task_count: unknown }
|
|
52
|
+
export interface GetGoalsIdResponse { goal: unknown; members: unknown; criteria: unknown; dependencies: unknown; criterionless_task_count: unknown; admitted: unknown }
|
|
53
53
|
export interface GetGoalsResponse { goals: unknown }
|
|
54
54
|
export interface GetGoalsRollupsResponse { rollups: unknown }
|
|
55
55
|
export interface GetGovernmentAssignmentsResponse { assignments: unknown }
|
|
@@ -278,8 +278,8 @@ export interface PostGoalsIdTransferRequest { builder_id?: StringifiableId }
|
|
|
278
278
|
export interface PostGoalsIdTransferResponse { ok: boolean; goal: unknown; changed: boolean }
|
|
279
279
|
export interface PostGoalsReorderRequest { version_id: string; ordered_ids: number[] }
|
|
280
280
|
export interface PostGoalsReorderResponse { ok: boolean; goals: unknown }
|
|
281
|
-
export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number; category_id?: number }
|
|
282
|
-
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown; category_advisory: unknown }
|
|
281
|
+
export interface PostGoalsRequest { version_id: string; title: string; subtitle?: string; description?: string; scope_modules?: string[]; succeeds_goal_id?: number; category_id?: number; admit_reason?: string }
|
|
282
|
+
export interface PostGoalsResponse { ok: boolean; goal: unknown; succeeds_goal_id: unknown; category_advisory: unknown; admitted: unknown }
|
|
283
283
|
export interface PostGovernmentAssignmentsRequest { builder_id: number; rank_key: string }
|
|
284
284
|
export interface PostGovernmentBoardItemsItemIdVotesRequest { direction: string; reason_md?: string; section_key?: string }
|
|
285
285
|
export interface PostGovernmentBoardItemsRequest { subject_type: string; proposal: Record<string, unknown>; rationale_md?: string; acknowledge_self_removal?: boolean }
|
package/docs/api/openapi.json
CHANGED
|
@@ -16955,14 +16955,16 @@
|
|
|
16955
16955
|
"members": {},
|
|
16956
16956
|
"criteria": {},
|
|
16957
16957
|
"dependencies": {},
|
|
16958
|
-
"criterionless_task_count": {}
|
|
16958
|
+
"criterionless_task_count": {},
|
|
16959
|
+
"admitted": {}
|
|
16959
16960
|
},
|
|
16960
16961
|
"required": [
|
|
16961
16962
|
"goal",
|
|
16962
16963
|
"members",
|
|
16963
16964
|
"criteria",
|
|
16964
16965
|
"dependencies",
|
|
16965
|
-
"criterionless_task_count"
|
|
16966
|
+
"criterionless_task_count",
|
|
16967
|
+
"admitted"
|
|
16966
16968
|
]
|
|
16967
16969
|
},
|
|
16968
16970
|
"GetGoalsResponse": {
|
|
@@ -20325,6 +20327,11 @@
|
|
|
20325
20327
|
"category_id": {
|
|
20326
20328
|
"type": "integer",
|
|
20327
20329
|
"minimum": 1
|
|
20330
|
+
},
|
|
20331
|
+
"admit_reason": {
|
|
20332
|
+
"type": "string",
|
|
20333
|
+
"minLength": 8,
|
|
20334
|
+
"maxLength": 500
|
|
20328
20335
|
}
|
|
20329
20336
|
},
|
|
20330
20337
|
"required": [
|
|
@@ -20341,13 +20348,15 @@
|
|
|
20341
20348
|
},
|
|
20342
20349
|
"goal": {},
|
|
20343
20350
|
"succeeds_goal_id": {},
|
|
20344
|
-
"category_advisory": {}
|
|
20351
|
+
"category_advisory": {},
|
|
20352
|
+
"admitted": {}
|
|
20345
20353
|
},
|
|
20346
20354
|
"required": [
|
|
20347
20355
|
"ok",
|
|
20348
20356
|
"goal",
|
|
20349
20357
|
"succeeds_goal_id",
|
|
20350
|
-
"category_advisory"
|
|
20358
|
+
"category_advisory",
|
|
20359
|
+
"admitted"
|
|
20351
20360
|
]
|
|
20352
20361
|
},
|
|
20353
20362
|
"PostGovernmentAssignmentsRequest": {
|
package/docs/api-reference.md
CHANGED
|
@@ -226,7 +226,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
226
226
|
| Method | Path | Rank | Body | Description |
|
|
227
227
|
|---|---|---|---|---|
|
|
228
228
|
| GET | `/api/bongos/goals` | `any-builder` | — | GET /goals?version=&status= — list goals (read). |
|
|
229
|
-
| POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id` | POST /goals — create a goal. |
|
|
229
|
+
| POST | `/api/bongos/goals` | `metic+archon` | `version_id`, `title`, `subtitle`, `description`, `scope_modules`, `succeeds_goal_id`, `category_id`, `admit_reason` | POST /goals — create a goal. |
|
|
230
230
|
| GET | `/api/bongos/goals/:id` | `any-builder` | — | GET /goals/:id — one goal with its members + linked criteria (read). |
|
|
231
231
|
| PATCH | `/api/bongos/goals/:id` | `any-builder` | — | PATCH /goals/:id — correct a goal's own title/subtitle/description (tasks 1003102, 1003224). |
|
|
232
232
|
| PATCH | `/api/bongos/goals/:id/accepting-requests` | `any-builder` | — | PATCH /goals/:id/accepting-requests { accepting_requests } — a private goal's lead closes or re-opens it to join-requests (task 1002971, … |
|
|
@@ -1591,5 +1591,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1591
1591
|
landed since 1.19.569 with no explicit bump. run 34060442118. (task 1002620)
|
|
1592
1592
|
1.19.571 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1593
1593
|
landed since 1.19.570 with no explicit bump. run 34065943703. (task 1002620)
|
|
1594
|
+
1.19.572 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1595
|
+
landed since 1.19.571 with no explicit bump. run 34071078624. (task 1002620)
|
|
1594
1596
|
---------------------------------------------------------------------------
|
|
1595
1597
|
```
|
|
@@ -49,8 +49,15 @@ const MEMBERSHIP_REQUEST_COLS =
|
|
|
49
49
|
// atomically. createdBy NULL (system-attributed, e.g. the migration-160 default
|
|
50
50
|
// goals) inserts no member — those goals have no owner; Archons act as one.
|
|
51
51
|
|
|
52
|
+
// `admission` (BV1.R07, task 1003594) is the R03 escape valve's record: when an
|
|
53
|
+
// Archon admits this goal into a BUILDING version, the who/why row rides in the
|
|
54
|
+
// SAME statement as the goal insert. Atomic on purpose — a goal that skipped the
|
|
55
|
+
// scope gate with no surviving record of who waved it through is precisely the
|
|
56
|
+
// state R03 exists to prevent, and a second round-trip can fail after the goal
|
|
57
|
+
// lands. Pass null (the normal path) and no admission row is written at all, so
|
|
58
|
+
// the presence of a row always means the gate was actually overridden.
|
|
52
59
|
async function createGoal(
|
|
53
|
-
{ versionId, title, subtitle = null, description = null, scopeModules = [], status = 'open', createdBy = null, categoryId = null },
|
|
60
|
+
{ versionId, title, subtitle = null, description = null, scopeModules = [], status = 'open', createdBy = null, categoryId = null, admission = null },
|
|
54
61
|
deps = {}
|
|
55
62
|
) {
|
|
56
63
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
@@ -64,13 +71,49 @@ async function createGoal(
|
|
|
64
71
|
), first_lead AS (
|
|
65
72
|
INSERT INTO goal_members (goal_id, builder_id, membership_kind)
|
|
66
73
|
SELECT g.id, $6::bigint, 'lead' FROM g WHERE $6::bigint IS NOT NULL
|
|
74
|
+
), admitted AS (
|
|
75
|
+
INSERT INTO lifecycle_goal_version_admissions (goal_id, version_id, admitted_by, reason)
|
|
76
|
+
SELECT g.id, g.version_id, $9::bigint, $10::text FROM g WHERE $9::bigint IS NOT NULL
|
|
67
77
|
)
|
|
68
78
|
SELECT * FROM g`,
|
|
69
|
-
[versionId, title, description, scopeModules, status, createdBy, subtitle, categoryId
|
|
79
|
+
[versionId, title, description, scopeModules, status, createdBy, subtitle, categoryId,
|
|
80
|
+
admission ? admission.admittedBy : null, admission ? admission.reason : null]
|
|
70
81
|
);
|
|
71
82
|
return rows[0];
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
// listVersionAdmissions (BV1.R07, task 1003594) — every goal admitted into ONE
|
|
86
|
+
// version, newest first. ADR 0250 §3: "the overrides are counted and shown on the
|
|
87
|
+
// version" — a visible counter is what keeps the exception honest, because an
|
|
88
|
+
// override nobody can see is just a slower version of having no rule. Returns []
|
|
89
|
+
// for the overwhelmingly common case of a version whose scope nobody widened.
|
|
90
|
+
async function listVersionAdmissions(versionId, deps = {}) {
|
|
91
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
92
|
+
const { rows } = await activePool.query(
|
|
93
|
+
`SELECT a.goal_id, a.version_id, a.admitted_by, a.reason, a.created_at, g.title AS goal_title
|
|
94
|
+
FROM lifecycle_goal_version_admissions a
|
|
95
|
+
JOIN goals g ON g.id = a.goal_id
|
|
96
|
+
WHERE a.version_id = $1
|
|
97
|
+
ORDER BY a.created_at DESC`,
|
|
98
|
+
[versionId]
|
|
99
|
+
);
|
|
100
|
+
return rows;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// getGoalAdmission (BV1.R07, task 1003594) — the override record for ONE goal, or
|
|
104
|
+
// null when the goal was created normally. Null is the overwhelmingly common
|
|
105
|
+
// answer and is not an absence of data: it is the positive fact "this goal did
|
|
106
|
+
// not skip the scope gate".
|
|
107
|
+
async function getGoalAdmission(goalId, deps = {}) {
|
|
108
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
109
|
+
const { rows } = await activePool.query(
|
|
110
|
+
`SELECT goal_id, version_id, admitted_by, reason, created_at
|
|
111
|
+
FROM lifecycle_goal_version_admissions WHERE goal_id = $1`,
|
|
112
|
+
[goalId]
|
|
113
|
+
);
|
|
114
|
+
return rows[0] ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
74
117
|
|
|
75
118
|
async function getGoal(id, deps = {}) {
|
|
76
119
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
@@ -807,6 +850,7 @@ module.exports = {
|
|
|
807
850
|
createGoal,
|
|
808
851
|
createMembershipRequest,
|
|
809
852
|
getGoal,
|
|
853
|
+
getGoalAdmission,
|
|
810
854
|
getGoalDependencies,
|
|
811
855
|
getGoalMember,
|
|
812
856
|
getMembershipRequest,
|
|
@@ -818,6 +862,7 @@ module.exports = {
|
|
|
818
862
|
listGoals,
|
|
819
863
|
listInboxFor,
|
|
820
864
|
listPendingRequestsForGoal,
|
|
865
|
+
listVersionAdmissions,
|
|
821
866
|
removeGoalMember,
|
|
822
867
|
reorderGoals,
|
|
823
868
|
requestClaimRelease,
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -72,6 +72,8 @@ const {
|
|
|
72
72
|
createGoal,
|
|
73
73
|
createMembershipRequest,
|
|
74
74
|
getGoal,
|
|
75
|
+
getGoalAdmission,
|
|
76
|
+
listVersionAdmissions,
|
|
75
77
|
getGoalDependencies,
|
|
76
78
|
getGoalMember,
|
|
77
79
|
getMembershipRequest,
|
|
@@ -217,6 +219,8 @@ module.exports = {
|
|
|
217
219
|
createVersion,
|
|
218
220
|
createGoal,
|
|
219
221
|
getGoal,
|
|
222
|
+
getGoalAdmission,
|
|
223
|
+
listVersionAdmissions,
|
|
220
224
|
getGoalDependencies,
|
|
221
225
|
listGoals,
|
|
222
226
|
reorderGoals,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- lifecycle_008_goal_version_admissions.sql — the R07 escape valve for R03
|
|
2
|
+
-- (task 1003594, goal 1000086, ADR 0250 D1).
|
|
3
|
+
--
|
|
4
|
+
-- WHY THIS TABLE EXISTS. BV1.R03 (task 1003591) closed a version's goal set the
|
|
5
|
+
-- moment the version starts building: new goals belong on the `planning` version.
|
|
6
|
+
-- That rule is right and it is also absolute, which is a problem — an instance
|
|
7
|
+
-- with no planning version cannot create a goal at all, and a genuinely urgent
|
|
8
|
+
-- goal has nowhere to go. R07 is the valve: an Archon may admit ONE named goal
|
|
9
|
+
-- into a building version and must say why.
|
|
10
|
+
--
|
|
11
|
+
-- PER-GOAL, NEVER A BLANKET UNLOCK. `goal_id` is the PRIMARY KEY, so an admission
|
|
12
|
+
-- is a fact about one goal and cannot be widened into "this version is open
|
|
13
|
+
-- again". There is deliberately no version-level switch here — a row per decision
|
|
14
|
+
-- is what makes the scope growth auditable after the fact, which is the whole
|
|
15
|
+
-- reason R03 exists (BONGOS-V1 reached 64 goals with nobody deciding to widen it).
|
|
16
|
+
--
|
|
17
|
+
-- THE ROW IS THE RECORD. reason is NOT NULL because an override with no stated
|
|
18
|
+
-- cause is indistinguishable from the rule not existing; six weeks later the only
|
|
19
|
+
-- question anyone asks is "why is this goal here", and the answer has to be in
|
|
20
|
+
-- the row. admitted_by is the actor (an Archon — enforced at the route, since
|
|
21
|
+
-- rank is a live DB read and pinning it here would freeze it at write time), and
|
|
22
|
+
-- created_at is when. Together: the goal, the actor, the reason, the timestamp.
|
|
23
|
+
--
|
|
24
|
+
-- WRITTEN ONLY WHEN THE OVERRIDE ACTUALLY DID WORK — i.e. the version was
|
|
25
|
+
-- `building`. A goal created normally on a planning version has no row, so the
|
|
26
|
+
-- presence of a row always means "this goal skipped the scope gate", never
|
|
27
|
+
-- "someone passed a flag that changed nothing".
|
|
28
|
+
--
|
|
29
|
+
-- ON DELETE CASCADE: the admission is a fact ABOUT the goal and is meaningless
|
|
30
|
+
-- without it. A deleted goal's override record is not history worth orphaning.
|
|
31
|
+
--
|
|
32
|
+
-- MODULE-OWNED and `lifecycle_`-prefixed so fitness check 11 permits it (ADR 0083
|
|
33
|
+
-- §Decision #5) — the lifecycle_004..007 convention. It only REFERENCES the core
|
|
34
|
+
-- `goals` / `builders` tables; a module migration may not ALTER them.
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS lifecycle_goal_version_admissions (
|
|
37
|
+
goal_id bigint PRIMARY KEY REFERENCES goals(id) ON DELETE CASCADE,
|
|
38
|
+
|
|
39
|
+
-- Denormalised from goals.version_id ON PURPOSE: the question this table
|
|
40
|
+
-- answers is "which versions had their scope widened, and why", and a goal can
|
|
41
|
+
-- be carried forward to another version later. The admission belongs to the
|
|
42
|
+
-- version that was building AT THE TIME.
|
|
43
|
+
version_id text NOT NULL REFERENCES versions(id),
|
|
44
|
+
|
|
45
|
+
admitted_by bigint NOT NULL REFERENCES builders(id),
|
|
46
|
+
reason text NOT NULL,
|
|
47
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_lifecycle_goal_admissions_version
|
|
51
|
+
ON lifecycle_goal_version_admissions (version_id);
|
|
@@ -120,7 +120,13 @@ function authorizeGoalCreate({ version, override = false }) {
|
|
|
120
120
|
message: `Version '${version.id}' is already building — its goal set is closed. Scope this goal onto the planning version instead, or ask an Archon to admit it with a reason.`,
|
|
121
121
|
} };
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
// `overrideApplied` says whether the override DID WORK, not whether one was
|
|
124
|
+
// offered (BV1.R07, task 1003594). An admit_reason on a planning version is a
|
|
125
|
+
// no-op — the create was always going to succeed — and recording it would file
|
|
126
|
+
// a "this goal skipped the scope gate" row against a goal that skipped nothing.
|
|
127
|
+
// It is returned from here rather than recomputed at the route because this is
|
|
128
|
+
// the ONE place allowed to reason about version.status; the route renders.
|
|
129
|
+
return { ok: true, overrideApplied: override === true && version.status !== 'planning' };
|
|
124
130
|
}
|
|
125
131
|
function authorizeGoalTaskCreate({ goal, version, isMember, builderRank, touches }) {
|
|
126
132
|
if (!goal) {
|
|
@@ -82,6 +82,11 @@ module.exports = function buildGoalsRouter() {
|
|
|
82
82
|
// task 1003275 — the human category. ADVISORY: absent is legal (it becomes a
|
|
83
83
|
// prompt, never a refusal), and no gate below reads it.
|
|
84
84
|
category_id: { type: 'integer', min: 1 },
|
|
85
|
+
// BV1.R07 (task 1003594) — the Archon admission. Its PRESENCE is the
|
|
86
|
+
// override; there is no boolean beside it, so an override cannot be
|
|
87
|
+
// requested without stating a cause. minLength 8 because 'ok' / '.' is not
|
|
88
|
+
// a reason, and the row is the only place the why survives.
|
|
89
|
+
admit_reason: { type: 'string', minLength: 8, maxLength: 500 },
|
|
85
90
|
})) return;
|
|
86
91
|
|
|
87
92
|
const scopeModules = req.body.scope_modules || [];
|
|
@@ -96,6 +101,16 @@ module.exports = function buildGoalsRouter() {
|
|
|
96
101
|
if (scopeIncludesProtected(scopeModules) && req.builder.rank !== 'archon') {
|
|
97
102
|
return res.fail('archon_required_for_protected_scope', { status: 403, message: 'A goal scoped to a protected module can only be created by an Archon.' });
|
|
98
103
|
}
|
|
104
|
+
// BV1.R07 (task 1003594, goal 1000086): admitting a goal into a building
|
|
105
|
+
// version is an Archon decision, so the field is refused by RANK before the
|
|
106
|
+
// version is even fetched — a Metic must not learn whether the override
|
|
107
|
+
// would have worked. Checked here rather than folded into the pure gate
|
|
108
|
+
// because rank is a live DB read on req.builder (ADR 0016), never a value
|
|
109
|
+
// the caller supplies.
|
|
110
|
+
const admitReason = (req.body.admit_reason || '').trim() || null;
|
|
111
|
+
if (admitReason !== null && req.builder.rank !== 'archon') {
|
|
112
|
+
return res.fail('archon_required_for_admission', { status: 403, message: 'Only an Archon may admit a goal into a version that is already building.' });
|
|
113
|
+
}
|
|
99
114
|
|
|
100
115
|
try {
|
|
101
116
|
const version = await db.getVersion(req.body.version_id);
|
|
@@ -103,7 +118,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
103
118
|
// fetched and then discarded after the null check — its status was never
|
|
104
119
|
// read, so a goal could be added to a version that had been building for
|
|
105
120
|
// months. The ordered, DB-free gate answers all of it, including the 404.
|
|
106
|
-
const versionDecision = authorizeGoalCreate({ version });
|
|
121
|
+
const versionDecision = authorizeGoalCreate({ version, override: admitReason !== null });
|
|
107
122
|
if (!versionDecision.ok) {
|
|
108
123
|
return res.status(versionDecision.status).json(versionDecision.body);
|
|
109
124
|
}
|
|
@@ -123,6 +138,14 @@ module.exports = function buildGoalsRouter() {
|
|
|
123
138
|
if (categoryId !== null && !(await goalCategory.getCategory(categoryId))) {
|
|
124
139
|
return res.fail('category_not_found', 404, { message: 'No such category. List them at GET /work-categories, or create one with POST /work-categories.' });
|
|
125
140
|
}
|
|
141
|
+
// The admission row is written ONLY when the gate says the override
|
|
142
|
+
// actually opened it (see authorizeGoalCreate) — an admit_reason on a
|
|
143
|
+
// planning version changed nothing and must not be recorded as a skipped
|
|
144
|
+
// gate. It rides inside createGoal's statement so the goal and its
|
|
145
|
+
// justification land together or not at all.
|
|
146
|
+
const admission = versionDecision.overrideApplied
|
|
147
|
+
? { admittedBy: req.builder.id, reason: admitReason }
|
|
148
|
+
: null;
|
|
126
149
|
const goal = await db.createGoal({
|
|
127
150
|
versionId: req.body.version_id,
|
|
128
151
|
title: req.body.title,
|
|
@@ -130,6 +153,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
130
153
|
scopeModules,
|
|
131
154
|
createdBy: req.builder.id,
|
|
132
155
|
categoryId,
|
|
156
|
+
admission,
|
|
133
157
|
});
|
|
134
158
|
// Stamp the predecessor's succeeded_by pointer at the new goal (the lineage).
|
|
135
159
|
if (succeedsId != null) {
|
|
@@ -144,7 +168,11 @@ module.exports = function buildGoalsRouter() {
|
|
|
144
168
|
category_advisory = goalCategory.adviseCategory({ categoryId, categories: await goalCategory.listCategories() });
|
|
145
169
|
} catch (_) { /* fail-open — advice only */ }
|
|
146
170
|
}
|
|
147
|
-
|
|
171
|
+
// `admitted` is null on every normal create. Non-null says the goal was
|
|
172
|
+
// waved into a building version, by whom and why — the caller should not
|
|
173
|
+
// have to re-read the goal to find out whether their override took effect.
|
|
174
|
+
const admitted = admission ? await db.getGoalAdmission(goal.id) : null;
|
|
175
|
+
return res.status(201).json({ ok: true, goal, succeeds_goal_id: succeedsId, category_advisory, admitted });
|
|
148
176
|
} catch (err) {
|
|
149
177
|
log.error('[gds] POST /goals', err);
|
|
150
178
|
return res.fail('goal_create_failed', { status: 500, message: 'internal error' });
|
|
@@ -257,7 +285,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
257
285
|
try {
|
|
258
286
|
const goal = await db.getGoal(id);
|
|
259
287
|
if (!goal) return res.fail('goal_not_found', 404);
|
|
260
|
-
const [members, criteria, dependencies, criterionless_task_count] = await Promise.all([
|
|
288
|
+
const [members, criteria, dependencies, criterionless_task_count, admitted] = await Promise.all([
|
|
261
289
|
db.listGoalMembers(id),
|
|
262
290
|
db.listCriteriaForGoal(id),
|
|
263
291
|
db.getGoalDependencies(id), // task 1761 — prerequisite goals + satisfied state
|
|
@@ -265,11 +293,16 @@ module.exports = function buildGoalsRouter() {
|
|
|
265
293
|
// page already renders, so a builder sees the gap without waiting for
|
|
266
294
|
// the create-time advisory to catch the next task.
|
|
267
295
|
db.countTasksWithoutCriterionLink(id),
|
|
296
|
+
// BV1.R07 (task 1003594): null on every normal goal. Non-null is the
|
|
297
|
+
// record that this goal was admitted into a building version — the
|
|
298
|
+
// override is worthless as an audit trail if reading the goal never
|
|
299
|
+
// shows it.
|
|
300
|
+
db.getGoalAdmission(id),
|
|
268
301
|
]);
|
|
269
302
|
const byId = await goalCategory.categoryIndex();
|
|
270
303
|
return res.json({
|
|
271
304
|
goal: goalCategory.decorateGoalCategory(decorateGoalScope(goal), byId),
|
|
272
|
-
members, criteria, dependencies, criterionless_task_count,
|
|
305
|
+
members, criteria, dependencies, criterionless_task_count, admitted,
|
|
273
306
|
});
|
|
274
307
|
} catch (err) {
|
|
275
308
|
log.error('[gds] GET /goals/:id', err);
|
|
@@ -76,9 +76,17 @@ module.exports = function buildVersionsRouter() {
|
|
|
76
76
|
router.get('/versions/:id/progress', asyncHandler('GET /versions/:id/progress', async (req, res) => {
|
|
77
77
|
const versionId = String(req.params.id || '').trim();
|
|
78
78
|
if (!versionId) return res.fail('version_id_required', 400);
|
|
79
|
-
|
|
79
|
+
// BV1.R07 (task 1003594) — ADR 0250 §3: "the overrides are counted and shown
|
|
80
|
+
// on the version". The scope gate's exceptions ride the aggregate the status
|
|
81
|
+
// dashboard already reads, rather than a route nobody would think to call.
|
|
82
|
+
// `admissions` is [] for a version whose scope nobody widened, which is the
|
|
83
|
+
// answer for every version that has never needed the valve.
|
|
84
|
+
const [progress, admissions] = await Promise.all([
|
|
85
|
+
doneWhen.criterionProgress(versionId),
|
|
86
|
+
db.listVersionAdmissions(versionId),
|
|
87
|
+
]);
|
|
80
88
|
res.set('Cache-Control', 'public, max-age=60');
|
|
81
|
-
res.json(progress);
|
|
89
|
+
res.json({ ...progress, admissions, admission_count: admissions.length });
|
|
82
90
|
}, { message: false, errorCode: 'progress_failed' }));
|
|
83
91
|
|
|
84
92
|
// POST /versions — create a version + its done-when criteria in one call. The
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.572",
|
|
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.572",
|
|
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.572",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
49
49
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
50
50
|
// the entry to that file. Look for a version's history there, not here.
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CORE_VERSION = '1.19.
|
|
52
|
+
const CORE_VERSION = '1.19.572'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
53
53
|
|
|
54
54
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
55
55
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// tests/goal_admission_override.mjs — the R03 escape valve: an Archon admits ONE
|
|
2
|
+
// named goal into a building version, and must say why (BV1.R07, task 1003594,
|
|
3
|
+
// goal 1000086, ADR 0250 D1).
|
|
4
|
+
//
|
|
5
|
+
// WHAT R03 LEFT BEHIND. BV1.R03 closed a version's goal set the moment the
|
|
6
|
+
// version starts building. Correct, and absolute: an instance whose only versions
|
|
7
|
+
// are `building` cannot create a goal at all — which is exactly the state
|
|
8
|
+
// cloudbongos.com was in when this task was claimed. R07 is the valve, and the
|
|
9
|
+
// thing that makes it a valve rather than a hole is that every use leaves a row
|
|
10
|
+
// naming the goal, the actor, the reason and the time.
|
|
11
|
+
//
|
|
12
|
+
// The gate stays pure and DB-free (the authorizeGoalTaskCreate precedent), so the
|
|
13
|
+
// decision half is exercised here without a database. The route half is pinned by
|
|
14
|
+
// reading the source — the same technique goal_create_version_gate.mjs uses — so
|
|
15
|
+
// the wiring cannot silently come undone.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/goal_admission_override.mjs
|
|
18
|
+
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
|
|
24
|
+
process.env.NODE_ENV = 'test';
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const { authorizeGoalCreate } = require('../modules/lifecycle/routes/goal-route-authz.js');
|
|
27
|
+
|
|
28
|
+
const V = (status) => ({ id: 'BONGOS-V1', status });
|
|
29
|
+
const decide = (version, override) => authorizeGoalCreate({ version, override });
|
|
30
|
+
|
|
31
|
+
const routeSrc = () => readFileSync(new URL('../modules/lifecycle/routes/goals.js', import.meta.url), 'utf8');
|
|
32
|
+
const postGoals = () => {
|
|
33
|
+
const src = routeSrc();
|
|
34
|
+
const start = src.indexOf("router.post('/goals'");
|
|
35
|
+
assert.notEqual(start, -1, 'POST /goals must exist');
|
|
36
|
+
return src.slice(start, src.indexOf('router.', start + 20));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ---- the valve opens, and says that it opened ------------------------------
|
|
40
|
+
|
|
41
|
+
test('an override admits a building version', () => {
|
|
42
|
+
assert.equal(decide(V('building'), true).ok, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('overrideApplied distinguishes "it worked" from "it was offered"', () => {
|
|
46
|
+
// The row must mean "this goal skipped the scope gate". On a planning version
|
|
47
|
+
// the create was always going to succeed, so the override did nothing and
|
|
48
|
+
// recording it would libel a perfectly normal goal.
|
|
49
|
+
assert.equal(decide(V('building'), true).overrideApplied, true, 'building: the override did the work');
|
|
50
|
+
assert.equal(decide(V('planning'), true).overrideApplied, false, 'planning: nothing to override');
|
|
51
|
+
assert.equal(decide(V('planning')).overrideApplied, false, 'no override offered at all');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('a refusal carries no overrideApplied claim', () => {
|
|
55
|
+
// ok:false has no admission story; asserting the field is absent stops a caller
|
|
56
|
+
// reading `.overrideApplied` off a rejection and getting a useful-looking false.
|
|
57
|
+
const d = decide(V('building'));
|
|
58
|
+
assert.equal(d.ok, false);
|
|
59
|
+
assert.equal('overrideApplied' in d, false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---- and it stays a valve, not a hole --------------------------------------
|
|
63
|
+
|
|
64
|
+
test('the override never reopens closed history', () => {
|
|
65
|
+
for (const status of ['shipped', 'paused', '', null, undefined]) {
|
|
66
|
+
assert.equal(decide(V(status), true).ok, false, `'${String(status)}' must stay closed`);
|
|
67
|
+
assert.equal(decide(V(status), true).body.error, 'version_closed');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('only a literal true overrides — a JSON body cannot coerce its way in', () => {
|
|
72
|
+
for (const v of ['yes', 1, {}, [], 'true']) {
|
|
73
|
+
assert.equal(decide(V('building'), v).ok, false);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---- the route: rank, reason, and the record -------------------------------
|
|
78
|
+
|
|
79
|
+
test('admit_reason is Archon-only, refused by rank before the version is read', () => {
|
|
80
|
+
const body = postGoals();
|
|
81
|
+
assert.match(body, /archon_required_for_admission/);
|
|
82
|
+
assert.match(body, /req\.builder\.rank !== 'archon'/);
|
|
83
|
+
// Ordering matters: the rank refusal sits above the getVersion call, so a Metic
|
|
84
|
+
// never learns whether the override would have worked on this version.
|
|
85
|
+
assert.ok(
|
|
86
|
+
body.indexOf('archon_required_for_admission') < body.indexOf('db.getVersion'),
|
|
87
|
+
'the rank check must precede the version fetch'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the override cannot be requested without a reason', () => {
|
|
92
|
+
const body = postGoals();
|
|
93
|
+
// There is no boolean flag beside admit_reason — its presence IS the request,
|
|
94
|
+
// so "override with no cause" is unrepresentable rather than merely refused.
|
|
95
|
+
assert.equal(/admit(_reason)?\s*:\s*\{\s*type:\s*'boolean'/.test(body), false);
|
|
96
|
+
assert.match(body, /admit_reason:\s*\{\s*type:\s*'string',\s*minLength:\s*[1-9]/,
|
|
97
|
+
'a non-empty minimum — "." is not a reason');
|
|
98
|
+
assert.match(body, /override:\s*admitReason !== null/,
|
|
99
|
+
'the reason is what opens the gate');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('whitespace is not a reason', () => {
|
|
103
|
+
assert.match(postGoals(), /req\.body\.admit_reason \|\| ''\)\.trim\(\) \|\| null/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the record is written from the gate\'s verdict, not a second status read', () => {
|
|
107
|
+
const body = postGoals();
|
|
108
|
+
assert.match(body, /versionDecision\.overrideApplied/);
|
|
109
|
+
assert.equal(/version\.status\s*[=!]==/.test(body), false,
|
|
110
|
+
'one place reasons about version status, and it is the gate');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('goal and justification land in ONE statement — no goal without its reason', () => {
|
|
114
|
+
const db = readFileSync(new URL('../modules/lifecycle/db-goals.js', import.meta.url), 'utf8');
|
|
115
|
+
const fn = db.slice(db.indexOf('async function createGoal'), db.indexOf('async function getGoalAdmission'));
|
|
116
|
+
assert.match(fn, /INSERT INTO lifecycle_goal_version_admissions/,
|
|
117
|
+
'the admission insert rides inside createGoal');
|
|
118
|
+
assert.ok(
|
|
119
|
+
fn.indexOf('INSERT INTO lifecycle_goal_version_admissions') < fn.indexOf('SELECT * FROM g`'),
|
|
120
|
+
'as a CTE in the same query, so it cannot fail after the goal has landed'
|
|
121
|
+
);
|
|
122
|
+
assert.match(fn, /WHERE \$9::bigint IS NOT NULL/,
|
|
123
|
+
'and writes nothing on the normal path');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('the row carries the goal, the actor, the reason and the time', () => {
|
|
127
|
+
const sql = readFileSync(
|
|
128
|
+
new URL('../modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql', import.meta.url), 'utf8');
|
|
129
|
+
for (const col of ['goal_id', 'admitted_by', 'reason', 'created_at']) {
|
|
130
|
+
assert.match(sql, new RegExp(`\\b${col}\\b`), `the done-when names ${col}`);
|
|
131
|
+
}
|
|
132
|
+
assert.match(sql, /reason\s+text NOT NULL/, 'an override with no stated cause is not an override');
|
|
133
|
+
assert.match(sql, /goal_id\s+bigint PRIMARY KEY/,
|
|
134
|
+
'per-goal, never a blanket unlock — one row per goal, and no version-level switch');
|
|
135
|
+
const tables = [...sql.matchAll(/CREATE TABLE IF NOT EXISTS (\w+)/g)].map((m) => m[1]);
|
|
136
|
+
assert.deepEqual(tables, ['lifecycle_goal_version_admissions'],
|
|
137
|
+
'exactly one table, keyed by goal — no second, version-level unlock switch beside it');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a module migration may reference core tables but never alter them', () => {
|
|
141
|
+
const sql = readFileSync(
|
|
142
|
+
new URL('../modules/lifecycle/migrations/lifecycle_008_goal_version_admissions.sql', import.meta.url), 'utf8');
|
|
143
|
+
assert.equal(/ALTER TABLE/i.test(sql), false, 'ADR 0083 §Decision #5');
|
|
144
|
+
assert.match(sql, /CREATE TABLE IF NOT EXISTS lifecycle_/, 'module-owned tables carry the module prefix');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---- the record is legible afterwards --------------------------------------
|
|
148
|
+
|
|
149
|
+
test('reading a goal shows whether it was admitted', () => {
|
|
150
|
+
const src = routeSrc();
|
|
151
|
+
const start = src.indexOf("router.get('/goals/:id'");
|
|
152
|
+
assert.notEqual(start, -1);
|
|
153
|
+
const body = src.slice(start, src.indexOf('router.', start + 20));
|
|
154
|
+
assert.match(body, /db\.getGoalAdmission\(id\)/);
|
|
155
|
+
assert.match(body, /admitted/, 'and returns it — an audit trail nobody can read is not one');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the create response says whether the override took effect', () => {
|
|
159
|
+
assert.match(postGoals(), /admitted \}\);/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---- ADR 0250 §3: the exception is counted, or it is not an exception --------
|
|
163
|
+
|
|
164
|
+
test('the version aggregate carries the override count', () => {
|
|
165
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/versions.js', import.meta.url), 'utf8');
|
|
166
|
+
const start = src.indexOf("router.get('/versions/:id/progress'");
|
|
167
|
+
assert.notEqual(start, -1);
|
|
168
|
+
const body = src.slice(start, src.indexOf('router.', start + 20));
|
|
169
|
+
assert.match(body, /db\.listVersionAdmissions\(versionId\)/);
|
|
170
|
+
assert.match(body, /admission_count: admissions\.length/,
|
|
171
|
+
'ADR 0250: an override that leaves no visible trace is a slower version of no rule');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('the admissions query is scoped to one version and names the goal', () => {
|
|
175
|
+
const db = readFileSync(new URL('../modules/lifecycle/db-goals.js', import.meta.url), 'utf8');
|
|
176
|
+
const fn = db.slice(db.indexOf('async function listVersionAdmissions'), db.indexOf('// getGoalAdmission'));
|
|
177
|
+
assert.match(fn, /WHERE a\.version_id = \$1/, 'one version, not the whole ledger');
|
|
178
|
+
assert.match(fn, /g\.title AS goal_title/, 'a count with no goal names is not reviewable');
|
|
179
|
+
});
|
package/tests/goal_category.mjs
CHANGED
|
@@ -169,7 +169,7 @@ test('the advisory is advice on a 201, never a refusal', () => {
|
|
|
169
169
|
// and never fails the create over it (best-effort, wrapped in try).
|
|
170
170
|
const routes = read('modules', 'lifecycle', 'routes', 'goals.js');
|
|
171
171
|
assert.match(routes, /category_advisory = goalCategory\.adviseCategory/);
|
|
172
|
-
assert.match(routes, /res\.status\(201\)\.json\(\{ ok: true, goal, succeeds_goal_id: succeedsId, category_advisory \}\)/,
|
|
172
|
+
assert.match(routes, /res\.status\(201\)\.json\(\{ ok: true, goal, succeeds_goal_id: succeedsId, category_advisory, admitted \}\)/,
|
|
173
173
|
'it rides the 201 — advice appended to a request that already succeeded');
|
|
174
174
|
});
|
|
175
175
|
|
|
@@ -30,7 +30,8 @@ const decide = (version, override) => authorizeGoalCreate({ version, override })
|
|
|
30
30
|
// ---- the truth table --------------------------------------------------------
|
|
31
31
|
|
|
32
32
|
test('planning accepts — that is what a planning version is for', () => {
|
|
33
|
-
|
|
33
|
+
// overrideApplied false: nothing was overridden, the version was simply open.
|
|
34
|
+
assert.deepEqual(decide(V('planning')), { ok: true, overrideApplied: false });
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
test('building refuses with a named 409 an agent can act on', () => {
|
|
@@ -77,8 +78,8 @@ test('an unrecognised status is NOT described as shipped', () => {
|
|
|
77
78
|
|
|
78
79
|
// ---- the override seam (R07 is not built yet) -------------------------------
|
|
79
80
|
|
|
80
|
-
test('override admits a building version — the R07 seam,
|
|
81
|
-
assert.deepEqual(decide(V('building'), true), { ok: true });
|
|
81
|
+
test('override admits a building version — the R07 seam, now wired (task 1003594)', () => {
|
|
82
|
+
assert.deepEqual(decide(V('building'), true), { ok: true, overrideApplied: true });
|
|
82
83
|
});
|
|
83
84
|
|
|
84
85
|
test('override does NOT reopen a shipped version', () => {
|
|
@@ -120,7 +121,9 @@ test('POST /goals consults the gate and returns its exact status + body', () =>
|
|
|
120
121
|
const start = src.indexOf("router.post('/goals'");
|
|
121
122
|
assert.notEqual(start, -1);
|
|
122
123
|
const body = src.slice(start, src.indexOf('router.', start + 20));
|
|
123
|
-
|
|
124
|
+
// BV1.R07 (task 1003594) wired the override argument in; the route still only
|
|
125
|
+
// renders whatever the gate decides.
|
|
126
|
+
assert.match(body, /const versionDecision = authorizeGoalCreate\(\{ version, override: [^)]*\}\);/);
|
|
124
127
|
assert.match(body, /return res\.status\(versionDecision\.status\)\.json\(versionDecision\.body\);/,
|
|
125
128
|
'the route renders the decision, it does not re-derive one');
|
|
126
129
|
assert.equal(/version\.status\s*[=!]==/.test(body), false,
|
package/tests/goal_tier.mjs
CHANGED
|
@@ -69,7 +69,9 @@ await test('createGoal: INSERT binds all six fields in order and returns the row
|
|
|
69
69
|
// title, and task 1003275 appended `category_id` as $8 the same way, so $1–$6
|
|
70
70
|
// keep the positions this test has always pinned. New goal fields go on the
|
|
71
71
|
// END of the bind list for exactly this reason.
|
|
72
|
-
|
|
72
|
+
// $9/$10 are the BV1.R07 admission (task 1003594) — null on the normal path,
|
|
73
|
+
// where nothing overrode the version scope gate.
|
|
74
|
+
assert.deepEqual(ins.params, ['BONGOS-V1', 'Goal hierarchy', 'desc', ['gds-core'], 'open', 42, null, null, null, null],
|
|
73
75
|
'version_id, title, description, scope_modules, status, created_by, then subtitle, then category_id — in that order');
|
|
74
76
|
assert.match(ins.sql, /RETURNING/, 'returns the row, not just inserts');
|
|
75
77
|
});
|
|
@@ -91,7 +93,7 @@ await test('createGoal: defaults — null description/createdBy, empty scope, op
|
|
|
91
93
|
const pool = fakePool((s) => (/INSERT INTO goals/.test(s) ? { rows: [GOAL_ROW] } : null));
|
|
92
94
|
await createGoal({ versionId: 'V1', title: 'general' }, { pool });
|
|
93
95
|
const ins = find(pool.queries, /INSERT INTO goals/);
|
|
94
|
-
assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null, null, null],
|
|
96
|
+
assert.deepEqual(ins.params, ['V1', 'general', null, [], 'open', null, null, null, null, null],
|
|
95
97
|
'unspecified fields fall back to null / [] / open — subtitle ($7) and category_id ($8) default to null too');
|
|
96
98
|
});
|
|
97
99
|
|