@bongos/core 1.19.936 → 1.19.937

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.
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/goal-from-idea.js — creating the GOAL a ratified Full Idea
4
+ // becomes, inside somebody else's transaction (task 1004096 / BV2.R17, spec R23,
5
+ // goal 1000095, criterion `ideator-full-idea-shapes-the-space`).
6
+ //
7
+ // WHY THIS LIVES IN LIFECYCLE AND NOT IN modules/ideas. `goals`, `versions` and
8
+ // `done_when_criteria` are this module's tables, and the ideas module may not
9
+ // import lifecycle (the one-way module rule, ADR 0083). The alternative was to
10
+ // widen the `lifecycle` port with four primitives — getVersion, createGoal,
11
+ // createCriterion, authorizeGoalCreate — and let the ideas module compose them.
12
+ // That is the shape to avoid: composing them IS the policy, and the policy is
13
+ // "may a goal be created here, by this rank, with this scope", which is a
14
+ // lifecycle judgement. Handing a caller the four pieces hands them four chances
15
+ // to leave one out, and the one most easily left out is the gate.
16
+ //
17
+ // SO THIS IS ONE PORT CALL THAT EITHER CREATES BOTH ROWS OR NAMES ITS REFUSAL.
18
+ // The ideas module supplies the DATA a ratification carries (title, the five
19
+ // answers, the version and scope the board chose) and the actor's live rank; it
20
+ // never gets to decide whether the create is allowed.
21
+ //
22
+ // IT RUNS IN THE CALLER'S TRANSACTION (`deps.client`, required — see below), for
23
+ // the ensureMaintenanceGoal reason: a goal committed on its own connection would
24
+ // survive a rolled-back ratification as an empty goal nobody asked for, pointing
25
+ // at an idea still sitting `open` in the inbox.
26
+ //
27
+ // EVERY GATE POST /goals APPLIES IS APPLIED HERE, and that is the point of the
28
+ // file rather than a courtesy. `idea.ratify`'s floor is METIC (ADR 0157 lowered
29
+ // it from archon), while `POST /goals` refuses a protected-`scope_modules` goal
30
+ // to anyone below Archon and refuses to grow a BUILDING version's goal set at
31
+ // all without a named Archon admission. A ratification path that skipped either
32
+ // would be a Metic-reachable way around both — the scope wall and the R03 gate
33
+ // would still be in the code and no longer in the way of anything.
34
+
35
+ const api = require('../../src/module-api');
36
+ const db = require('./db.js');
37
+ const { authorizeGoalCreate } = require('./routes/goal-route-authz.js');
38
+
39
+ const { isModuleKey, scopeIncludesProtected, moduleKeys } = api.moduleScopeMap;
40
+
41
+ // A typed refusal, never a throw: the caller is inside a transaction it has to
42
+ // roll back itself, and an exception thrown across a port is the thing that
43
+ // makes a rollback easy to forget. `status` travels with the refusal so the
44
+ // route renders it without a second mapping table.
45
+ function refuse(code, status, extra = {}) {
46
+ return { ok: false, code, status, ...extra };
47
+ }
48
+
49
+ // createRatifiedGoal — the goal + its first done-when criterion, in the caller's
50
+ // transaction, or a named refusal.
51
+ //
52
+ // `criterionMd` is REQUIRED and is not a convenience. The criterion is what makes
53
+ // the created row a goal in this project's sense rather than an empty container:
54
+ // ADR 0263's close gate reads done-when criteria, so a goal born with none can
55
+ // never be achieved and holds its version open forever. BV2.R17's whole claim is
56
+ // that a Full Idea's "what does a successful implementation look like" answer IS
57
+ // that criterion — so the two rows are one act, and this function will not make
58
+ // the first without the second.
59
+ //
60
+ // `criterionRef` is the stable `criterion_id` text. The caller derives it from
61
+ // the source idea (`idea-<n>`), which makes the UNIQUE (version_id,
62
+ // criterion_id) constraint a second, database-level guard against ratifying the
63
+ // same idea twice onto the same version — the `proposal-<idea_id>` trick
64
+ // ratifyCriterionProposal already uses, for the same reason.
65
+ async function createRatifiedGoal(
66
+ { versionId, title, description = null, subtitle = null, scopeModules = [], createdBy = null,
67
+ criterionMd, criterionRef, admitReason = null, actorRank = null },
68
+ deps = {}
69
+ ) {
70
+ if (!deps.client) {
71
+ // Not a policy refusal — a programming error, and one whose consequence is a
72
+ // half-committed ratification. It throws rather than returning, because no
73
+ // caller can sensibly handle it and none should try.
74
+ throw new Error('createRatifiedGoal requires deps.client — it must run in the ratification transaction');
75
+ }
76
+ if (typeof title !== 'string' || !title.trim()) return refuse('TITLE_REQUIRED', 400);
77
+ if (typeof criterionMd !== 'string' || !criterionMd.trim()) return refuse('CRITERION_REQUIRED', 400);
78
+ if (typeof criterionRef !== 'string' || !criterionRef.trim()) return refuse('CRITERION_REF_REQUIRED', 400);
79
+
80
+ const scope = Array.isArray(scopeModules) ? scopeModules : [];
81
+ // Fail closed on an unknown module key, POST /goals' first gate: a scope wall
82
+ // that can name a module which does not exist is not a wall.
83
+ const unknown = scope.filter((k) => !isModuleKey(k));
84
+ if (unknown.length) return refuse('UNKNOWN_MODULE', 400, { unknown, valid: moduleKeys() });
85
+ // The two RANK gates. `actorRank` is the caller's LIVE rank read (ADR 0016) —
86
+ // never a value a request body carried — and an absent rank denies, because a
87
+ // rank we were not told is not a rank we may assume.
88
+ if (scopeIncludesProtected(scope) && actorRank !== 'archon') {
89
+ return refuse('ARCHON_REQUIRED_FOR_PROTECTED_SCOPE', 403, { scope_modules: scope });
90
+ }
91
+ const reason = typeof admitReason === 'string' ? admitReason.trim() || null : null;
92
+ // Refused by RANK before the version is fetched, the POST /goals ordering: a
93
+ // Metic must not learn whether the override would have worked.
94
+ if (reason !== null && actorRank !== 'archon') {
95
+ return refuse('ARCHON_REQUIRED_FOR_ADMISSION', 403);
96
+ }
97
+
98
+ // FOR UPDATE, so a version freeze committing between this read and this
99
+ // transaction's COMMIT cannot leave the goal and its criterion on a version
100
+ // that closed underneath them (getVersion's own note explains why a plain read
101
+ // is not enough under READ COMMITTED).
102
+ const version = await db.getVersion(versionId, { client: deps.client, forUpdate: true });
103
+ const decision = authorizeGoalCreate({ version, override: reason !== null });
104
+ if (!decision.ok) {
105
+ return refuse(decision.body.error, decision.status, { message: decision.body.message, version_status: decision.body.status });
106
+ }
107
+
108
+ const goal = await db.createGoal({
109
+ versionId,
110
+ title: title.trim(),
111
+ subtitle,
112
+ description,
113
+ scopeModules: scope,
114
+ createdBy,
115
+ // The admission row is written ONLY when the override actually opened the
116
+ // gate — `overrideApplied`, not "a reason was supplied". A reason on a
117
+ // planning version changed nothing and must not be filed as a skipped gate.
118
+ admission: decision.overrideApplied ? { admittedBy: createdBy, reason } : null,
119
+ }, { client: deps.client });
120
+
121
+ const criterion = await db.createCriterion({
122
+ versionId,
123
+ criterionId: criterionRef.trim(),
124
+ criterionMd,
125
+ goalId: goal.id,
126
+ }, { client: deps.client });
127
+
128
+ return { ok: true, goal, criterion, admitted: decision.overrideApplied };
129
+ }
130
+
131
+ module.exports = { createRatifiedGoal };
@@ -19,6 +19,7 @@
19
19
  const db = require('./db');
20
20
  const classifier = require('./task-classifier');
21
21
  const goalSuggest = require('./goal-suggest');
22
+ const goalFromIdea = require('./goal-from-idea');
22
23
  const helpRequests = require('./help-requests');
23
24
  const taskRecommendations = require('./task-recommendations');
24
25
  const activity = require('./activity');
@@ -66,6 +67,19 @@ module.exports = {
66
67
  // lifecycle tables and the ideas module may not import lifecycle (ADR 0083).
67
68
  ensureMaintenanceGoal: (...a) => db.ensureMaintenanceGoal(...a),
68
69
  currentBuildingVersionId: (...a) => db.currentBuildingVersionId(...a),
70
+ // createRatifiedGoal (task 1004096 / BV2.R17): the GOAL a ratified Full Idea
71
+ // becomes, plus the done-when criterion its "what does a successful
72
+ // implementation look like" answer supplies — both rows, in the ideas module's
73
+ // ratification transaction, or one named refusal.
74
+ //
75
+ // ONE CALL RATHER THAN FOUR PRIMITIVES, deliberately. The ideas module could
76
+ // have been handed getVersion + createGoal + createCriterion +
77
+ // authorizeGoalCreate and left to compose them, but composing them IS the
78
+ // goal-creation policy — the scope wall, the rank gates, the R03 building-
79
+ // version gate — and that policy is a lifecycle judgement. `idea.ratify`'s
80
+ // floor is Metic, so a caller free to leave the gate out would be a
81
+ // Metic-reachable way around it. See modules/lifecycle/goal-from-idea.js.
82
+ createRatifiedGoal: (...a) => goalFromIdea.createRatifiedGoal(...a),
69
83
 
70
84
  // --- work-tracking READS the staying-core read routes resolve --------------
71
85
  // public.js (the status surface): version/ship feed + grade trend.
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.936",
3
+ "version": "1.19.937",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.936",
9
+ "version": "1.19.937",
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.936",
3
+ "version": "1.19.937",
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
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.936'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.937'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -42,6 +42,7 @@ const NON_GRANT_MIGRATIONS = new Set([
42
42
  'government_007_amendments.sql', // R14 amendment proposals table (task 1003027)
43
43
  'government_008_closed_items_index.sql', // R16 closed-history index (task 1003029)
44
44
  'government_017_board_section_keys_v2.sql', // BV2.R06 board section_key CHECK widening (task 1004085)
45
+ 'government_018_passed_items_index.sql', // BV2.R17 passed-item lookup index (task 1004096)
45
46
  ]);
46
47
 
47
48
  // Permission keys RENAMED after the grant migrations that first granted them.