@bongos/core 1.19.274 → 1.19.276
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 +39 -24
- 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 +4 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +59 -3
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +6 -4
- package/docs/copy-registry.json +23 -5
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/goal-inbox.js +81 -3
- package/modules/ideas/inbox.js +332 -7
- package/modules/ideas/migrations/ideas_004_lineage_route_index.sql +37 -0
- package/modules/ideas/routes/inbox.js +78 -1
- package/modules/ideas/routing.js +232 -0
- package/modules/lifecycle/db.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/idea_routing.mjs +858 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/ideas/routing.js — THE LANDING MATRIX (task 1003065, BV1.R01,
|
|
4
|
+
// goal 1000071 "Idea routing — work that has a home lands in it").
|
|
5
|
+
// Spec: docs/specs/bongos-v1-idea-routing.md §"Landing matrix (C1/C2/C4)".
|
|
6
|
+
//
|
|
7
|
+
// THE PROBLEM THIS ANSWERS. Most filed "ideas" are really bug tasks or
|
|
8
|
+
// goal-bound feature tasks. Today they park in idea_inbox and wait for a human
|
|
9
|
+
// triage pass that, when it finally promotes them, drops the task into a version
|
|
10
|
+
// catch-all — because nothing in the idea path ever names a goal. Routing makes
|
|
11
|
+
// the home real: work that HAS a home lands in it at filing time, and the inbox
|
|
12
|
+
// keeps only work that doesn't.
|
|
13
|
+
//
|
|
14
|
+
// This file is the DECISION and nothing else: given what was filed and who filed
|
|
15
|
+
// it, does it route, and if so at what status. It is PURE — no DB, no clock, no
|
|
16
|
+
// rank lookup — so every cell of the matrix is pinned by a DB-free unit test
|
|
17
|
+
// (tests/idea_routing.mjs). The transaction that acts on the decision lives in
|
|
18
|
+
// inbox.captureIdea; keeping them apart is what makes the matrix auditable
|
|
19
|
+
// without a Postgres.
|
|
20
|
+
//
|
|
21
|
+
// FAIL-CLOSED IS THE POSTURE HERE, and it is the opposite of the advisory
|
|
22
|
+
// modules' fail-open. An advisory that misfires prints a wrong sentence; a
|
|
23
|
+
// landing decision that misfires puts UNVETTED WORK IN THE CLAIMABLE QUEUE.
|
|
24
|
+
// So every uncertainty resolves toward "don't route" or "route to backlog",
|
|
25
|
+
// never toward 'ready': an unrecognised kind soft-lands, an unknown/absent
|
|
26
|
+
// filer rank soft-lands, and anything the matrix has no opinion about stays in
|
|
27
|
+
// the inbox exactly as it does today.
|
|
28
|
+
|
|
29
|
+
// The one rule the owner interview (2026-08-17, Q1) settled: BUGS ARE
|
|
30
|
+
// URGENT-TRUE, THE REST GETS A NOD. A bug filed by a trusted builder is
|
|
31
|
+
// claimable the moment it is reported — that is the whole "no triage stop
|
|
32
|
+
// between reported and in the work queue" claim of criterion C1. Everything
|
|
33
|
+
// else — feature, cleanup, refactor, infra — soft-lands at 'backlog', one
|
|
34
|
+
// Metic+ promote from claimable, because those reshape what gets built and a
|
|
35
|
+
// filer should not be able to set the queue's agenda alone.
|
|
36
|
+
const HARD_LAND_KINDS = new Set(['bug']);
|
|
37
|
+
|
|
38
|
+
// Kinds that are JUDGMENT WORK and must never auto-route, no matter who files
|
|
39
|
+
// them with what goal. A criterion-proposal reshapes a version's scope-truth and
|
|
40
|
+
// is ratified by an Archon (POST /inbox/:id/ratify); a decision is a question
|
|
41
|
+
// awaiting an answer, not a unit of work. Converting either into a task would
|
|
42
|
+
// turn "we should think about this" into "someone should go do this".
|
|
43
|
+
const NEVER_ROUTE_KINDS = new Set(['criterion-proposal', 'decision']);
|
|
44
|
+
|
|
45
|
+
// The trusted tier, spelled here rather than imported so this file stays pure and
|
|
46
|
+
// DB-free. It is the same Metic+ boundary docs/canonical-permissions.md draws and
|
|
47
|
+
// the routes enforce; this module only READS a rank string it was handed. It
|
|
48
|
+
// grants nothing — the server-side rank floor and the goal's scope wall are
|
|
49
|
+
// separate, load-bearing gates (R05), and this decision is subordinate to both.
|
|
50
|
+
const METIC_PLUS = new Set(['metic', 'archon']);
|
|
51
|
+
|
|
52
|
+
// A Full Idea (grade='full') is the Board Room's subject — it is ratified by
|
|
53
|
+
// peers (goal 1000069), not converted into a task by its own filer. 'spark' and
|
|
54
|
+
// 'quick' carry no such meaning and route normally.
|
|
55
|
+
const NEVER_ROUTE_GRADE = 'full';
|
|
56
|
+
|
|
57
|
+
// decideLanding — the matrix, as one pure function.
|
|
58
|
+
//
|
|
59
|
+
// Returns { route: false, reason } when the thing belongs in the inbox, or
|
|
60
|
+
// { route: true, status, reason } when it becomes a task. `reason` is a stable
|
|
61
|
+
// machine token (not prose): the route echoes it, /idea-triage reads it, and the
|
|
62
|
+
// R15 reclassification sweep counts by it. Never throws.
|
|
63
|
+
function decideLanding(input) {
|
|
64
|
+
// Destructured defaults only fire on `undefined`, so an explicit `null` — what
|
|
65
|
+
// a caller spreading an absent object hands over — would throw. A landing
|
|
66
|
+
// decision that CRASHES is the worst of the three outcomes: it takes down the
|
|
67
|
+
// capture that was supposed to fail closed. Normalise first, decide after.
|
|
68
|
+
const { goalId = null, kind = null, grade = null, filerRank = null } = input || {};
|
|
69
|
+
// No goal named ⇒ the inbox is exactly right, and this is the DEFAULT PATH.
|
|
70
|
+
// Every capture vector that never learns about goals keeps working unchanged;
|
|
71
|
+
// routing is strictly additive (criterion C1 testable #5).
|
|
72
|
+
if (goalId == null || goalId === '') return { route: false, reason: 'no_goal' };
|
|
73
|
+
|
|
74
|
+
if (String(grade || '').toLowerCase() === NEVER_ROUTE_GRADE) {
|
|
75
|
+
return { route: false, reason: 'full_idea_never_routes' };
|
|
76
|
+
}
|
|
77
|
+
if (NEVER_ROUTE_KINDS.has(kind)) {
|
|
78
|
+
return { route: false, reason: 'judgment_kind' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (HARD_LAND_KINDS.has(kind)) {
|
|
82
|
+
// The rank split. An absent/unknown rank is NOT treated as trusted — see the
|
|
83
|
+
// fail-closed note above. This is the DECISION only; R05 adds the
|
|
84
|
+
// server-side enforcement (auto-derived requires_rank floor + scope wall).
|
|
85
|
+
return METIC_PLUS.has(String(filerRank || '').toLowerCase())
|
|
86
|
+
? { route: true, status: 'ready', reason: 'bug_trusted_filer' }
|
|
87
|
+
: { route: true, status: 'backlog', reason: 'bug_awaiting_nod' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Everything else with a home: it lands IN that home, at backlog. Note this
|
|
91
|
+
// deliberately swallows an unrecognised kind into the soft landing rather than
|
|
92
|
+
// refusing it — a task in a real goal at backlog is strictly better than a row
|
|
93
|
+
// rotting in the inbox, and 'backlog' means a human still says go.
|
|
94
|
+
return { route: true, status: 'backlog', reason: 'soft_landing' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The default priority for a routed task when the filer suggested none. 3 is the
|
|
98
|
+
// middle of the 1–5 band — a routed task is not automatically urgent, and not
|
|
99
|
+
// automatically bottom-of-queue either. (Decided without interview and recorded
|
|
100
|
+
// in the spec: "routed-task priority defaults to the idea's suggested_priority
|
|
101
|
+
// else 3".)
|
|
102
|
+
const DEFAULT_ROUTED_PRIORITY = 3;
|
|
103
|
+
|
|
104
|
+
// routedPriority — the idea's own suggestion, clamped to the task band. An idea's
|
|
105
|
+
// suggested_priority column is validated 1..5 at capture, but a routed idea can
|
|
106
|
+
// arrive from a vector that never validated it (Discord inbound, a seed), so this
|
|
107
|
+
// re-clamps rather than trusting the input. Out-of-band or absent ⇒ the default.
|
|
108
|
+
function routedPriority(suggestedPriority) {
|
|
109
|
+
const p = Number(suggestedPriority);
|
|
110
|
+
if (!Number.isInteger(p) || p < 1 || p > 5) return DEFAULT_ROUTED_PRIORITY;
|
|
111
|
+
return p;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
// ENFORCEMENT (task 1003066, BV1.R05) — the matrix decides, this gate BINDS.
|
|
116
|
+
//
|
|
117
|
+
// R01 landed decideLanding: what status a filing lands at. That is a decision
|
|
118
|
+
// about the QUEUE. It is not authority, and on its own it left a real hole:
|
|
119
|
+
// a routed task declares no touches[], so createTask's ADR 0084/0085 rank floor
|
|
120
|
+
// derives from an empty list and comes out 'xenos' — meaning a bug routed into a
|
|
121
|
+
// goal scoped to PROTECTED modules (permission/ship-grade/deploy/migration core)
|
|
122
|
+
// was born claimable by a Xenos the moment any Metic+ nodded it. The matrix's
|
|
123
|
+
// rank split does not close that: it governs who can make work READY, not who
|
|
124
|
+
// can then take it.
|
|
125
|
+
//
|
|
126
|
+
// WHY THIS ISN'T goalScopeCheck.check() CALLED VERBATIM, which is what
|
|
127
|
+
// POST /goals/:id/tasks does. That function DENIES on `empty_touches` — by
|
|
128
|
+
// design, because an AUTHORED task whose blast radius nobody declared cannot be
|
|
129
|
+
// bounded. Applied to a route it would refuse every single filing, since a person
|
|
130
|
+
// reporting a bug does not know which files it lives in; demanding touches[] as
|
|
131
|
+
// the price of reporting is exactly the triage tax this goal removes. So the wall
|
|
132
|
+
// is applied where it has something to bite on:
|
|
133
|
+
//
|
|
134
|
+
// * touches DECLARED (a programmatic filer that does know) → the real wall,
|
|
135
|
+
// verbatim, refusals and all.
|
|
136
|
+
// * touches ABSENT (every human filing) → nothing to subset-check, so the
|
|
137
|
+
// protection moves to the CLAIM side: the goal's protected scope raises the
|
|
138
|
+
// task's rank floor, and no sub-Metic can claim it however it got promoted.
|
|
139
|
+
//
|
|
140
|
+
// That is the same protected-roster source (permission-path-check via
|
|
141
|
+
// goalScopeCheck / moduleScopeMap), just enforced at the gate that still exists
|
|
142
|
+
// when touches[] doesn't.
|
|
143
|
+
//
|
|
144
|
+
// MEMBERSHIP IS DELIBERATELY *NOT* REQUIRED, and this is the one place the routed
|
|
145
|
+
// path diverges from the authored one on purpose. authorizeGoalTaskCreate refuses
|
|
146
|
+
// a non-member with not_a_member, because authoring work inside someone's
|
|
147
|
+
// workspace is a member's privilege. REPORTING is not authoring: criterion C5's
|
|
148
|
+
// whole promise is that "the filer never has to know the goal map", and C1's is
|
|
149
|
+
// that a bug filed anywhere lands in its home. Requiring membership would mean
|
|
150
|
+
// only insiders could report against a goal — the opposite of the goal. The
|
|
151
|
+
// safety that membership was carrying is carried here instead by the rank floor
|
|
152
|
+
// (claim side) and the matrix (ready side). Pinned by a test, so a later change
|
|
153
|
+
// has to argue with the test rather than quietly reinstate the wall.
|
|
154
|
+
|
|
155
|
+
// The rank a routed task must require when its goal's scope reaches protected
|
|
156
|
+
// territory. Metic is the trust boundary docs/canonical-permissions.md draws for
|
|
157
|
+
// the permission/ship/deploy core; createTask's highestRank() clamps UPWARD only,
|
|
158
|
+
// so handing this in can raise a floor and never lower one.
|
|
159
|
+
const PROTECTED_SCOPE_FLOOR = 'metic';
|
|
160
|
+
|
|
161
|
+
// authorizeRoutedLanding — the FINAL, binding landing decision. PURE: it takes
|
|
162
|
+
// facts the caller already resolved (the matrix verdict, whether the goal's scope
|
|
163
|
+
// includes a protected module, and the scope wall's own verdict if touches were
|
|
164
|
+
// declared) and returns what to stamp on the task, or a refusal.
|
|
165
|
+
//
|
|
166
|
+
// Ordered + fail-closed, the authorizeGoalTaskCreate precedent:
|
|
167
|
+
// not routing → scope-wall verdict → the floor.
|
|
168
|
+
// Returns { ok: true, status, requiresRank } or { ok: false, code, reason, details }.
|
|
169
|
+
function authorizeRoutedLanding(input) {
|
|
170
|
+
const { landing = null, scopeProtected = false, scopeCheck = null, filerRank = null } = input || {};
|
|
171
|
+
|
|
172
|
+
// Defensive: only a routing verdict may reach here. A caller that lost track of
|
|
173
|
+
// its own matrix result must not fall through into "authorized".
|
|
174
|
+
if (!landing || landing.route !== true) {
|
|
175
|
+
return { ok: false, code: 'not_routing', reason: 'authorizeRoutedLanding was asked to authorize a filing the matrix did not route.', details: null };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// The wall, when touches gave it something to check. Its verdict is passed
|
|
179
|
+
// through UNCHANGED — code, reason and details — so a refusal reads identically
|
|
180
|
+
// whether it came from a routed filing or an authored task, and there is only
|
|
181
|
+
// ever one definition of "out of scope" / "protected path".
|
|
182
|
+
if (scopeCheck && scopeCheck.ok === false) {
|
|
183
|
+
return { ok: false, code: scopeCheck.code, reason: scopeCheck.reason, details: scopeCheck.details ?? null };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// PROTECTED SCOPE + SUB-METIC FILER ⇒ REFUSE, not merely floor-stamp.
|
|
187
|
+
//
|
|
188
|
+
// This closes the one place the first cut of R05 was genuinely weaker than the
|
|
189
|
+
// authored gate rather than differently-shaped. With no touches[] there is no
|
|
190
|
+
// path for the protected-path arm to inspect, so the earlier version let a
|
|
191
|
+
// sub-Metic CREATE a task in protected territory and relied on the rank floor to
|
|
192
|
+
// stop them CLAIMING it. But POST /goals/:id/tasks refuses that author outright
|
|
193
|
+
// (protected_path), and admission refuses them the goal entirely
|
|
194
|
+
// (authorizeInvitationAccept: a sub-Metic may not even join a protected-scope
|
|
195
|
+
// goal). Letting the routed path mint a row there was the odd one out.
|
|
196
|
+
//
|
|
197
|
+
// Refusing costs the filer nothing they had before: omit goal_id and the report
|
|
198
|
+
// files to the inbox exactly as it always did, where a Metic+ triages it. What
|
|
199
|
+
// it removes is a sub-Metic's ability to put rows — however unclaimable — into
|
|
200
|
+
// the permission/ship/deploy core's queue, which is precisely what "the scope
|
|
201
|
+
// wall runs on routed tasks as on authored ones" is asking for.
|
|
202
|
+
//
|
|
203
|
+
// The rank floor below still applies to Metic+ filings in protected scope; the
|
|
204
|
+
// two are layers, not alternatives.
|
|
205
|
+
if (scopeProtected && !METIC_PLUS.has(String(filerRank || '').toLowerCase())) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
code: 'protected_scope',
|
|
209
|
+
reason: `This goal's scope reaches protected territory (the permission / ship-grade / deploy / migration core), so a filer below Metic cannot route work into it. File it without a goal — it lands in the inbox and a Metic+ routes it from there.`,
|
|
210
|
+
details: { required_rank: PROTECTED_SCOPE_FLOOR, filer_rank: filerRank ?? null },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
status: landing.status,
|
|
217
|
+
// null ⇒ say nothing and let createTask derive the floor exactly as it does
|
|
218
|
+
// for every other create vector. Only the protected case speaks up.
|
|
219
|
+
requiresRank: scopeProtected ? PROTECTED_SCOPE_FLOOR : null,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
decideLanding,
|
|
225
|
+
authorizeRoutedLanding,
|
|
226
|
+
PROTECTED_SCOPE_FLOOR,
|
|
227
|
+
routedPriority,
|
|
228
|
+
DEFAULT_ROUTED_PRIORITY,
|
|
229
|
+
HARD_LAND_KINDS,
|
|
230
|
+
NEVER_ROUTE_KINDS,
|
|
231
|
+
METIC_PLUS,
|
|
232
|
+
};
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -1421,7 +1421,7 @@ async function createTask({
|
|
|
1421
1421
|
// (requiresRank stays null for the goal vector → derived purely from touches +
|
|
1422
1422
|
// sensitivity), so goal_id never influences the requires_rank clamp.
|
|
1423
1423
|
goalId = null,
|
|
1424
|
-
}) {
|
|
1424
|
+
}, deps = {}) { // deps.client enlists this INSERT in the CALLER'S transaction (task 1003065, BV1.R01); the argument for it is at modules/ideas/inbox.js routeIdeaToGoal
|
|
1425
1425
|
// task 1498 / ADR 0085: every task carries a deliberate rank floor. Derive it
|
|
1426
1426
|
// from touches + sensitivity, then let an explicit requiresRank override only
|
|
1427
1427
|
// UPWARD (highestRank) — a requested rank below the derived floor is clamped
|
|
@@ -1456,7 +1456,7 @@ async function createTask({
|
|
|
1456
1456
|
if (goalIdEffective == null) {
|
|
1457
1457
|
goalIdEffective = await generalGoalIdForVersion(versionId);
|
|
1458
1458
|
}
|
|
1459
|
-
const { rows } = await pool.query(
|
|
1459
|
+
const { rows } = await (deps.client || pool).query(
|
|
1460
1460
|
`INSERT INTO tasks (version_id, title, description, status, touches,
|
|
1461
1461
|
est_minutes, est_cost_usd, manual_degree, priority,
|
|
1462
1462
|
automation_tag, credits_reward, source, source_ref, value_summary,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.276",
|
|
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.276",
|
|
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.276",
|
|
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
|
@@ -975,8 +975,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
975
975
|
// landed since 1.19.272 with no explicit bump. run 32441024959. (task 1002620)
|
|
976
976
|
// 1.19.274 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
977
977
|
// landed since 1.19.273 with no explicit bump. run 32490024209. (task 1002620)
|
|
978
|
+
// 1.19.275 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
979
|
+
// landed since 1.19.274 with no explicit bump. run 32492414871. (task 1002620)
|
|
980
|
+
// 1.19.276 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
981
|
+
// landed since 1.19.275 with no explicit bump. run 32499257959. (task 1002620)
|
|
978
982
|
// ---------------------------------------------------------------------------
|
|
979
|
-
const CORE_VERSION = '1.19.
|
|
983
|
+
const CORE_VERSION = '1.19.276'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
980
984
|
|
|
981
985
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
982
986
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|