@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.
- package/.bongos-core.json +52 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/government/board.js +31 -0
- package/modules/government/db.js +33 -0
- package/modules/government/migrations/government_018_passed_items_index.sql +31 -0
- package/modules/government/routes/government.js +7 -0
- package/modules/ideas/inbox.js +11 -0
- package/modules/ideas/migrations/ideas_010_goal_ratification.sql +103 -0
- package/modules/ideas/module.json +1 -0
- package/modules/ideas/ratify.js +288 -1
- package/modules/ideas/routes/ratify-goal.js +163 -0
- package/modules/ideas/templates.js +16 -4
- package/modules/lifecycle/goal-from-idea.js +131 -0
- package/modules/lifecycle/lifecycle.js +14 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/idea_goal_ratification.mjs +553 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|