@bongos/core 1.19.561 → 1.19.562

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.561",
6
- "core_contract": "1.19.561",
7
- "source_commit": "c3f698331dac2a2225920780b219c28b7a354dc6",
5
+ "core_version": "1.19.562",
6
+ "core_contract": "1.19.562",
7
+ "source_commit": "8812128543b799aea8050fb1d51256bbdd0fc717",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-06T17:18:36.999Z",
9
+ "built_at": "2026-09-06T17:33:48.313Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 443,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2024,
14
+ "functional_verbatim": 2025,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2491,
20
- "tree_sha256": "e6f6d8606c0eeca8894673e7d1e73a986383fab212dc83aedbc0cb8a02f5a631",
19
+ "file_count": 2492,
20
+ "tree_sha256": "dc363435f4898d5c74509b25e810a61c1f3a4962b038c4f1c0c1527167e48217",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2657,7 +2657,7 @@
2657
2657
  {
2658
2658
  "path": "docs/module-api-changelog.md",
2659
2659
  "mode": "0000644",
2660
- "sha256": "b6fc3c1b36687285aa44544babb749b5dfff005f8fb95428e771302ff2f3fe5e"
2660
+ "sha256": "9c9e6c53ef623de12cdca7cd96dfc2221c19843f45d3a84a196862f4e195c908"
2661
2661
  },
2662
2662
  {
2663
2663
  "path": "docs/modules-contract.md",
@@ -5647,12 +5647,12 @@
5647
5647
  {
5648
5648
  "path": "modules/lifecycle/routes/goal-route-authz.js",
5649
5649
  "mode": "0000644",
5650
- "sha256": "909686de2d34b184e9676370782e4b07179b784c72eb6db4af22399051b15642"
5650
+ "sha256": "c380ad664c18beff53dd50e9bd5ad9ba78c05c18fae182cc0b0cc8ea41ee1a75"
5651
5651
  },
5652
5652
  {
5653
5653
  "path": "modules/lifecycle/routes/goals.js",
5654
5654
  "mode": "0000644",
5655
- "sha256": "9bef42b55351651b188e95e254814edd4e845e77cc67bc230960af77d0f6eaa2"
5655
+ "sha256": "9d3079d7f3f610aa5893f02fb4858f73ec98718d6096c423cd52c15d2b30d1ec"
5656
5656
  },
5657
5657
  {
5658
5658
  "path": "modules/lifecycle/routes/help-requests.js",
@@ -7477,12 +7477,12 @@
7477
7477
  {
7478
7478
  "path": "package-lock.json",
7479
7479
  "mode": "0000644",
7480
- "sha256": "7dc793d8391910582ab02e387931d90ef58114fc118ed14954d15ee8c4697658"
7480
+ "sha256": "c9fb4551642e3a6cdd1db0e7795805314e2a62b413eb96d2b6ee981146d666c3"
7481
7481
  },
7482
7482
  {
7483
7483
  "path": "package.json",
7484
7484
  "mode": "0000644",
7485
- "sha256": "f70384732bde00c05eeb33f54faa28fcc77df835866cb35df9dcd8f812e01ed7"
7485
+ "sha256": "fe6556a8d666ce01124d6666fd07d7ad01920fe596ca3718398fb4bbcc885465"
7486
7486
  },
7487
7487
  {
7488
7488
  "path": "public-docs/index.html",
@@ -9182,7 +9182,7 @@
9182
9182
  {
9183
9183
  "path": "src/module-api.js",
9184
9184
  "mode": "0000644",
9185
- "sha256": "84964fd2a837b5f09f4fc45abef8a9eea2e79cc9072292272e4219b6860af9d7"
9185
+ "sha256": "c08cf2ff080e74afdd1c88cb06f4960909b4984bc803f6eeddab57abe14fed79"
9186
9186
  },
9187
9187
  {
9188
9188
  "path": "src/module-loader/catalog.js",
@@ -10294,6 +10294,11 @@
10294
10294
  "mode": "0000644",
10295
10295
  "sha256": "1dd92e13f21ed2b029c64d07dcdd6c47aa4f59ce25e288786c4fa7245ba707c5"
10296
10296
  },
10297
+ {
10298
+ "path": "tests/goal_create_version_gate.mjs",
10299
+ "mode": "0000644",
10300
+ "sha256": "addd9fe7179236d24c316923fd47a60947c1a34f9e01eb1a4d47faa49b4fa18f"
10301
+ },
10297
10302
  {
10298
10303
  "path": "tests/goal_criterion_confirm_ui.mjs",
10299
10304
  "mode": "0000644",
@@ -1571,5 +1571,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1571
1571
  landed since 1.19.559 with no explicit bump. run 34047736326. (task 1002620)
1572
1572
  1.19.561 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1573
1573
  landed since 1.19.560 with no explicit bump. run 34048169727. (task 1002620)
1574
+ 1.19.562 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1575
+ landed since 1.19.561 with no explicit bump. run 34048961786. (task 1002620)
1574
1576
  ---------------------------------------------------------------------------
1575
1577
  ```
@@ -15,6 +15,7 @@ const db = require('../db');
15
15
  const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
16
16
  const goalScopeCheck = api.goalScopeCheck;
17
17
  const { isGoalOwner, authorizeGoalManagement } = require('../goal-authz');
18
+ const { versionAcceptsNewWork } = require('../db-tasks.js');
18
19
  const METIC_PLUS = ['metic', 'archon'];
19
20
 
20
21
  async function isMemberOrArchon(builder, goalId) {
@@ -65,6 +66,62 @@ function goalNotOpenRefusal(goal) {
65
66
  };
66
67
  }
67
68
 
69
+ // authorizeGoalCreate — PURE, FAIL-CLOSED. May a goal be created on this version?
70
+ // (BV1.R03, task 1003591, goal 1000086, ADR 0250 D1.)
71
+ //
72
+ // THE RULE: a version's goal set closes when the version starts BUILDING. New
73
+ // goals go on the `planning` version — that is what a planning version is for,
74
+ // scoping the next one in advance and at leisure. `POST /goals` used to fetch the
75
+ // version row and discard it after the null check, so a goal could be added to a
76
+ // version that had been building for months, which is how a version's scope grew
77
+ // without anyone deciding to widen it.
78
+ //
79
+ // Three answers, ordered fail-closed (an absent or unrecognised version denies):
80
+ // no version → 404 version_not_found
81
+ // not open to work → 409 version_closed (shipped, or a status we
82
+ // cannot vouch for)
83
+ // building → 409 version_not_planning (the R07 override admits
84
+ // ONE named goal, with a reason)
85
+ // planning → ok
86
+ //
87
+ // The open-to-work question is `versionAcceptsNewWork` — the same predicate the
88
+ // task-create path uses — rather than a second spelling of the status list, so
89
+ // "a version we can still file into" has one definition. This gate is STRICTER
90
+ // than that predicate on purpose: work may land on a building version, but its
91
+ // SCOPE may not grow. Reusing the predicate for the first question and adding the
92
+ // planning test on top is what keeps those two facts from being confused.
93
+ //
94
+ // `override` is the R07 Archon admission (task 1003594), which is not built yet.
95
+ // It is a parameter here rather than a later edit to the signature so that R07
96
+ // wires a decision in without reopening this ordering: pass true and a building
97
+ // version is accepted. It does NOT bypass version_closed — a shipped version is
98
+ // closed history, and no override makes it writable again.
99
+ function authorizeGoalCreate({ version, override = false }) {
100
+ if (!version) {
101
+ return { ok: false, status: 404, body: {
102
+ error: 'version_not_found', message: 'No such version.',
103
+ } };
104
+ }
105
+ if (!versionAcceptsNewWork(version.status)) {
106
+ return { ok: false, status: 409, body: {
107
+ error: 'version_closed', status: version.status,
108
+ // Says which status refused rather than asserting 'shipped': the predicate
109
+ // is fail-closed, so it also catches null and any status added later, and
110
+ // telling someone their planning-adjacent version "has shipped" would be a
111
+ // confident lie about state they can see for themselves.
112
+ message: version.status === 'shipped'
113
+ ? `Version '${version.id}' has shipped — it is closed history and cannot take new goals.`
114
+ : `Version '${version.id}' is '${version.status}', which is not a state that takes new goals.`,
115
+ } };
116
+ }
117
+ if (version.status !== 'planning' && override !== true) {
118
+ return { ok: false, status: 409, body: {
119
+ error: 'version_not_planning', status: version.status,
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
+ } };
122
+ }
123
+ return { ok: true };
124
+ }
68
125
  function authorizeGoalTaskCreate({ goal, version, isMember, builderRank, touches }) {
69
126
  if (!goal) {
70
127
  return { ok: false, status: 404, body: { error: 'goal_not_found' } };
@@ -458,6 +515,7 @@ function decorateGoalScope(goal) {
458
515
 
459
516
  module.exports = {
460
517
  authorizeCreateJoinRequest,
518
+ authorizeGoalCreate,
461
519
  authorizeGoalTaskCreate,
462
520
  authorizeInvitationAccept,
463
521
  authorizeInvitationResponse,
@@ -63,7 +63,7 @@ const JOIN_REQUEST_RESPONSE_ACTIONS = ['approve', 'deny']; // an owner/manager/A
63
63
  // proceed; a false result means "not a member" → the route should 403. This is the
64
64
  // membership half of the wall; the protected-module rank check is enforced per-route
65
65
  // (scope is never self-granted).
66
- const { authorizeCreateJoinRequest, authorizeGoalTaskCreate, authorizeInvitationAccept, authorizeInvitationResponse, authorizeJoinRequestResponse, authorizeMembershipKindChange, authorizeOwnershipTransfer, authorizeRescindInvitation, authorizeWithdrawJoinRequest, decorateGoalScope, isMemberOrArchon, protectedScopeAdmissionBlock } = require('./goal-route-authz.js');
66
+ const { authorizeCreateJoinRequest, authorizeGoalCreate, authorizeGoalTaskCreate, authorizeInvitationAccept, authorizeInvitationResponse, authorizeJoinRequestResponse, authorizeMembershipKindChange, authorizeOwnershipTransfer, authorizeRescindInvitation, authorizeWithdrawJoinRequest, decorateGoalScope, isMemberOrArchon, protectedScopeAdmissionBlock } = require('./goal-route-authz.js');
67
67
  // task 1003208: structured logging (pino via the doorway) — was console.*.
68
68
  const log = api.logger('lifecycle');
69
69
  module.exports = function buildGoalsRouter() {
@@ -99,8 +99,13 @@ module.exports = function buildGoalsRouter() {
99
99
 
100
100
  try {
101
101
  const version = await db.getVersion(req.body.version_id);
102
- if (!version) {
103
- return res.fail('version_not_found', { status: 404, message: 'No such version.' });
102
+ // BV1.R03 (task 1003591, goal 1000086, ADR 0250 D1): the version row was
103
+ // fetched and then discarded after the null check — its status was never
104
+ // read, so a goal could be added to a version that had been building for
105
+ // months. The ordered, DB-free gate answers all of it, including the 404.
106
+ const versionDecision = authorizeGoalCreate({ version });
107
+ if (!versionDecision.ok) {
108
+ return res.status(versionDecision.status).json(versionDecision.body);
104
109
  }
105
110
  // Rescope carry-forward (ADR 0086 §6 / BV1.R64): a new-version goal may declare
106
111
  // the (achieved) goal it continues. Validate the predecessor exists BEFORE
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.561",
3
+ "version": "1.19.562",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.561",
9
+ "version": "1.19.562",
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.561",
3
+ "version": "1.19.562",
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.561'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.562'; // 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,136 @@
1
+ // tests/goal_create_version_gate.mjs — a version's goal set closes when the
2
+ // version starts building (BV1.R03, task 1003591, goal 1000086, ADR 0250 D1).
3
+ //
4
+ // THE HOLE. `POST /goals` fetched the version row and discarded it after the null
5
+ // check. `version.status` was never read, so a goal could be added to a version
6
+ // that had been building for months — which is how a version's scope grows
7
+ // without anyone deciding to widen it, and why 64 goals accumulated on
8
+ // BONGOS-V1. New goals belong on the `planning` version: that is what a planning
9
+ // version is FOR, scoping the next one in advance.
10
+ //
11
+ // The gate is pure and DB-free (the `authorizeGoalTaskCreate` precedent), so its
12
+ // whole truth table is exercised here without a database, and the route's only
13
+ // job is to render the decision.
14
+ //
15
+ // Run: node --test tests/goal_create_version_gate.mjs
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { test } from 'node:test';
19
+ import { readFileSync } from 'node:fs';
20
+ import { createRequire } from 'node:module';
21
+
22
+ process.env.NODE_ENV = 'test';
23
+ const require = createRequire(import.meta.url);
24
+ const { authorizeGoalCreate } = require('../modules/lifecycle/routes/goal-route-authz.js');
25
+ const { versionAcceptsNewWork } = require('../modules/lifecycle/db-tasks.js');
26
+
27
+ const V = (status) => ({ id: 'BONGOS-V1', status });
28
+ const decide = (version, override) => authorizeGoalCreate({ version, override });
29
+
30
+ // ---- the truth table --------------------------------------------------------
31
+
32
+ test('planning accepts — that is what a planning version is for', () => {
33
+ assert.deepEqual(decide(V('planning')), { ok: true });
34
+ });
35
+
36
+ test('building refuses with a named 409 an agent can act on', () => {
37
+ const d = decide(V('building'));
38
+ assert.equal(d.ok, false);
39
+ assert.equal(d.status, 409);
40
+ assert.equal(d.body.error, 'version_not_planning');
41
+ assert.equal(d.body.status, 'building', 'the caller learns WHICH state refused');
42
+ assert.match(d.body.message, /planning version/,
43
+ 'and where the goal should go instead — a refusal with no next move gets routed around');
44
+ });
45
+
46
+ test('shipped refuses as closed history, not as "not planning"', () => {
47
+ // Two different facts deserve two different codes: a building version might
48
+ // still admit this goal via an Archon override; a shipped one never will.
49
+ const d = decide(V('shipped'));
50
+ assert.equal(d.body.error, 'version_closed');
51
+ assert.match(d.body.message, /has shipped/);
52
+ });
53
+
54
+ test('a missing version is a 404, before any status reasoning', () => {
55
+ const d = decide(null);
56
+ assert.equal(d.status, 404);
57
+ assert.equal(d.body.error, 'version_not_found');
58
+ });
59
+
60
+ test('FAIL-CLOSED: a status nobody taught this gate about refuses', () => {
61
+ // A version status added later must not default to "sure, file it here".
62
+ for (const status of ['paused', 'draft', '', undefined, null, 42]) {
63
+ const d = decide(V(status));
64
+ assert.equal(d.ok, false, `'${String(status)}' must not be accepted`);
65
+ assert.equal(d.body.error, 'version_closed');
66
+ }
67
+ });
68
+
69
+ test('an unrecognised status is NOT described as shipped', () => {
70
+ // The fail-closed arm catches more than `shipped`, and telling someone their
71
+ // version "has shipped" when it says 'paused' is a confident lie about state
72
+ // they can see for themselves.
73
+ const d = decide(V('paused'));
74
+ assert.match(d.body.message, /is 'paused'/);
75
+ assert.equal(/has shipped/.test(d.body.message), false);
76
+ });
77
+
78
+ // ---- the override seam (R07 is not built yet) -------------------------------
79
+
80
+ test('override admits a building version — the R07 seam, already shaped', () => {
81
+ assert.deepEqual(decide(V('building'), true), { ok: true });
82
+ });
83
+
84
+ test('override does NOT reopen a shipped version', () => {
85
+ // The escape hatch is for a scope decision, not for rewriting closed history.
86
+ assert.equal(decide(V('shipped'), true).body.error, 'version_closed');
87
+ assert.equal(decide(V('paused'), true).body.error, 'version_closed');
88
+ });
89
+
90
+ test('only a literal true overrides — a truthy value does not', () => {
91
+ for (const v of ['yes', 1, {}, [] ]) {
92
+ assert.equal(decide(V('building'), v).ok, false,
93
+ 'a JSON body coercing to truthy must not open the gate');
94
+ }
95
+ });
96
+
97
+ // ---- one definition of "open to work" ---------------------------------------
98
+
99
+ test('the gate reuses versionAcceptsNewWork rather than respelling the statuses', () => {
100
+ const src = readFileSync(new URL('../modules/lifecycle/routes/goal-route-authz.js', import.meta.url), 'utf8');
101
+ const body = src.slice(src.indexOf('function authorizeGoalCreate'), src.indexOf('function authorizeGoalTaskCreate'));
102
+ assert.match(body, /versionAcceptsNewWork\(version\.status\)/);
103
+ assert.equal(/'shipped'\s*,\s*'planning'|new Set\(\[/.test(body), false,
104
+ 'no second copy of the status list lives in this gate');
105
+ });
106
+
107
+ test('the gate is STRICTER than versionAcceptsNewWork, deliberately', () => {
108
+ // Work may still LAND on a building version; its SCOPE may not grow. Those are
109
+ // two different questions and this pins that they give different answers.
110
+ assert.equal(versionAcceptsNewWork('building'), true, 'tasks may still be filed');
111
+ assert.equal(decide(V('building')).ok, false, 'but a new goal may not');
112
+ assert.equal(versionAcceptsNewWork('planning'), true);
113
+ assert.equal(decide(V('planning')).ok, true);
114
+ });
115
+
116
+ // ---- the route renders the decision and nothing else ------------------------
117
+
118
+ test('POST /goals consults the gate and returns its exact status + body', () => {
119
+ const src = readFileSync(new URL('../modules/lifecycle/routes/goals.js', import.meta.url), 'utf8');
120
+ const start = src.indexOf("router.post('/goals'");
121
+ assert.notEqual(start, -1);
122
+ const body = src.slice(start, src.indexOf('router.', start + 20));
123
+ assert.match(body, /const versionDecision = authorizeGoalCreate\(\{ version \}\);/);
124
+ assert.match(body, /return res\.status\(versionDecision\.status\)\.json\(versionDecision\.body\);/,
125
+ 'the route renders the decision, it does not re-derive one');
126
+ assert.equal(/version\.status\s*[=!]==/.test(body), false,
127
+ 'and it must not compare the status itself — that is the gate\'s job');
128
+ });
129
+
130
+ test('the 404 moved INTO the gate, so the route has no second version check', () => {
131
+ const src = readFileSync(new URL('../modules/lifecycle/routes/goals.js', import.meta.url), 'utf8');
132
+ const start = src.indexOf("router.post('/goals'");
133
+ const body = src.slice(start, src.indexOf('router.', start + 20));
134
+ assert.equal(/if \(!version\)/.test(body), false,
135
+ 'one ordered decision, not a null check here and a status check there');
136
+ });