@bongos/core 1.19.815 → 1.19.816
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 +15 -15
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-goals.js +36 -0
- package/modules/lifecycle/db.js +2 -0
- package/modules/lifecycle/goal-suggest.js +75 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/capture.js +43 -8
- package/src/module-api.js +1 -1
- package/tests/capture_goal_routing.mjs +65 -1
- package/tests/goal_suggest.mjs +148 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.816",
|
|
6
|
+
"core_contract": "1.19.816",
|
|
7
|
+
"source_commit": "aa1a7485bebfc27a8b19c0acbff29287f1212828",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-17T08:14:24.758Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 489,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2730,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "78e804a2a5bda8a926f2f33af27c70ca3a6e0b4b5b3aa9a5b3bd8c66dd5cec24",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2542,7 +2542,7 @@
|
|
|
2542
2542
|
{
|
|
2543
2543
|
"path": "docs/module-api-changelog.md",
|
|
2544
2544
|
"mode": "0000644",
|
|
2545
|
-
"sha256": "
|
|
2545
|
+
"sha256": "c86194915e38b871c476151bdf00719d190411407942cfc8dd9f0a7734630ab0"
|
|
2546
2546
|
},
|
|
2547
2547
|
{
|
|
2548
2548
|
"path": "docs/modules-contract.md",
|
|
@@ -5557,7 +5557,7 @@
|
|
|
5557
5557
|
{
|
|
5558
5558
|
"path": "modules/lifecycle/db-goals.js",
|
|
5559
5559
|
"mode": "0000644",
|
|
5560
|
-
"sha256": "
|
|
5560
|
+
"sha256": "1b229bd4bf18563dd772c0be46f320ae533e71a5c7ba32aaa4b523ac9f97a4dc"
|
|
5561
5561
|
},
|
|
5562
5562
|
{
|
|
5563
5563
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5597,7 +5597,7 @@
|
|
|
5597
5597
|
{
|
|
5598
5598
|
"path": "modules/lifecycle/db.js",
|
|
5599
5599
|
"mode": "0000644",
|
|
5600
|
-
"sha256": "
|
|
5600
|
+
"sha256": "30c8884948a8dee8213c55279c12f06e104c7d95ab3d7fad9dc67506ed1ac1ab"
|
|
5601
5601
|
},
|
|
5602
5602
|
{
|
|
5603
5603
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -5657,7 +5657,7 @@
|
|
|
5657
5657
|
{
|
|
5658
5658
|
"path": "modules/lifecycle/goal-suggest.js",
|
|
5659
5659
|
"mode": "0000644",
|
|
5660
|
-
"sha256": "
|
|
5660
|
+
"sha256": "cdbfa36b58dc26b7ce9d7a3d1f2812edd0ea050c2b27fe3837be6b6edf7d9ac5"
|
|
5661
5661
|
},
|
|
5662
5662
|
{
|
|
5663
5663
|
"path": "modules/lifecycle/help-requests.js",
|
|
@@ -7792,12 +7792,12 @@
|
|
|
7792
7792
|
{
|
|
7793
7793
|
"path": "package-lock.json",
|
|
7794
7794
|
"mode": "0000644",
|
|
7795
|
-
"sha256": "
|
|
7795
|
+
"sha256": "aa6749d880220869386e00488652da7ae32a0116855dba519e6b943c47f7180c"
|
|
7796
7796
|
},
|
|
7797
7797
|
{
|
|
7798
7798
|
"path": "package.json",
|
|
7799
7799
|
"mode": "0000644",
|
|
7800
|
-
"sha256": "
|
|
7800
|
+
"sha256": "966ab00e5b62d0280043e13e156ce400cd3afa3f7c5bc04f02eea79392fd26f3"
|
|
7801
7801
|
},
|
|
7802
7802
|
{
|
|
7803
7803
|
"path": "public-docs/index.html",
|
|
@@ -8057,7 +8057,7 @@
|
|
|
8057
8057
|
{
|
|
8058
8058
|
"path": "scripts/gds/capture.js",
|
|
8059
8059
|
"mode": "0000644",
|
|
8060
|
-
"sha256": "
|
|
8060
|
+
"sha256": "7320809dacf61454d114d5c55c244e7eac3314f36302eab879950355540573f6"
|
|
8061
8061
|
},
|
|
8062
8062
|
{
|
|
8063
8063
|
"path": "scripts/gds/category-advisory-guard.js",
|
|
@@ -9687,7 +9687,7 @@
|
|
|
9687
9687
|
{
|
|
9688
9688
|
"path": "src/module-api.js",
|
|
9689
9689
|
"mode": "0000644",
|
|
9690
|
-
"sha256": "
|
|
9690
|
+
"sha256": "575b32eed8f62dad0302816f97e77604488891d13188bf5b690a867c37a962ac"
|
|
9691
9691
|
},
|
|
9692
9692
|
{
|
|
9693
9693
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10312,7 +10312,7 @@
|
|
|
10312
10312
|
{
|
|
10313
10313
|
"path": "tests/capture_goal_routing.mjs",
|
|
10314
10314
|
"mode": "0000644",
|
|
10315
|
-
"sha256": "
|
|
10315
|
+
"sha256": "2ddceb37d46eda5c3ab2a56b54c86f3cf55596479b5fe7f30646f45b1ac11ac2"
|
|
10316
10316
|
},
|
|
10317
10317
|
{
|
|
10318
10318
|
"path": "tests/cascade-dispatch.mjs",
|
|
@@ -11102,7 +11102,7 @@
|
|
|
11102
11102
|
{
|
|
11103
11103
|
"path": "tests/goal_suggest.mjs",
|
|
11104
11104
|
"mode": "0000644",
|
|
11105
|
-
"sha256": "
|
|
11105
|
+
"sha256": "41cf7774b86acabe7621563430fc4784152dea05f6cd3136b00081eca876e74d"
|
|
11106
11106
|
},
|
|
11107
11107
|
{
|
|
11108
11108
|
"path": "tests/goal_tier.mjs",
|
|
@@ -2104,5 +2104,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2104
2104
|
landed since 1.19.813 with no explicit bump. run 35194334679. (task 1002620)
|
|
2105
2105
|
1.19.815 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2106
2106
|
landed since 1.19.814 with no explicit bump. run 35197139985. (task 1002620)
|
|
2107
|
+
1.19.816 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2108
|
+
landed since 1.19.815 with no explicit bump. run 35198566737. (task 1002620)
|
|
2107
2109
|
---------------------------------------------------------------------------
|
|
2108
2110
|
```
|
|
@@ -832,6 +832,41 @@ async function listCriteriaForGoal(goalId, deps = {}) {
|
|
|
832
832
|
}));
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
+
// listCriterionTextForGoals — every goal's done-when criterion prose, keyed by
|
|
836
|
+
// goal id, in ONE query (task 1003859). The goal suggester ranks on this.
|
|
837
|
+
//
|
|
838
|
+
// WHY BULK. The suggester scores all open goals for a version at once — eleven
|
|
839
|
+
// of them on BONGOS-V2 — so per-goal reads would be eleven round trips on a
|
|
840
|
+
// path that runs at capture time, for prose that is a few hundred bytes a goal.
|
|
841
|
+
//
|
|
842
|
+
// NO INDEX ON goal_id, DELIBERATELY. done_when_criteria carries a handful of rows
|
|
843
|
+
// per goal and low hundreds in total — a scan of it is cheaper than the index it
|
|
844
|
+
// would need maintaining, and adding one would mint a migration (a reserved number
|
|
845
|
+
// and human review) to speed up a read that is already the smallest thing on this
|
|
846
|
+
// path. idx_done_when_version exists for the version-scoped read; revisit if the
|
|
847
|
+
// table ever grows a digit.
|
|
848
|
+
//
|
|
849
|
+
// WHY NOT listCriteriaForGoal. That read carries linked-task counts and the
|
|
850
|
+
// pending_review derivation, none of which a text ranker wants; this returns the
|
|
851
|
+
// criterion prose and nothing else.
|
|
852
|
+
async function listCriterionTextForGoals(goalIds, deps = {}) {
|
|
853
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
854
|
+
const ids = (goalIds || []).map((g) => String(g)).filter(Boolean);
|
|
855
|
+
if (ids.length === 0) return new Map();
|
|
856
|
+
const { rows } = await activePool.query(
|
|
857
|
+
`SELECT goal_id, criterion_md
|
|
858
|
+
FROM done_when_criteria
|
|
859
|
+
WHERE goal_id = ANY($1::bigint[])`,
|
|
860
|
+
[ids]
|
|
861
|
+
);
|
|
862
|
+
const byGoal = new Map();
|
|
863
|
+
for (const r of rows) {
|
|
864
|
+
const k = String(r.goal_id);
|
|
865
|
+
byGoal.set(k, (byGoal.get(k) ? byGoal.get(k) + ' ' : '') + String(r.criterion_md || ''));
|
|
866
|
+
}
|
|
867
|
+
return byGoal;
|
|
868
|
+
}
|
|
869
|
+
|
|
835
870
|
// isGoalMember — is this builder a member of this goal? The membership predicate
|
|
836
871
|
// behind the bounded-authority gates (BV1.R64 ask-to-widen; the R60 authoring
|
|
837
872
|
// gate reuses the same idea). Returns a plain boolean.
|
|
@@ -1251,6 +1286,7 @@ module.exports = {
|
|
|
1251
1286
|
isGoalMember,
|
|
1252
1287
|
listActiveClaimsForTasks,
|
|
1253
1288
|
listCriteriaForGoal,
|
|
1289
|
+
listCriterionTextForGoals,
|
|
1254
1290
|
openTasksInGoal,
|
|
1255
1291
|
listGoalMembers,
|
|
1256
1292
|
listGoals,
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -88,6 +88,7 @@ const {
|
|
|
88
88
|
isGoalMember,
|
|
89
89
|
listActiveClaimsForTasks,
|
|
90
90
|
listCriteriaForGoal,
|
|
91
|
+
listCriterionTextForGoals,
|
|
91
92
|
archiveGoalWithDispositions,
|
|
92
93
|
openTasksInGoal,
|
|
93
94
|
listGoalMembers,
|
|
@@ -267,6 +268,7 @@ module.exports = {
|
|
|
267
268
|
listInboxFor,
|
|
268
269
|
listPendingRequestsForGoal,
|
|
269
270
|
listCriteriaForGoal,
|
|
271
|
+
listCriterionTextForGoals,
|
|
270
272
|
archiveGoalWithDispositions,
|
|
271
273
|
openTasksInGoal,
|
|
272
274
|
xenosClaimAllowed,
|
|
@@ -18,12 +18,25 @@
|
|
|
18
18
|
// so there is one notion of "these two texts are about the same thing" in the
|
|
19
19
|
// module, not two.
|
|
20
20
|
//
|
|
21
|
-
// WHAT A GOAL IS MATCHED ON: its title
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
21
|
+
// WHAT A GOAL IS MATCHED ON: its title, its scope_modules, and its DONE-WHEN
|
|
22
|
+
// CRITERIA (task 1003859). Criteria were added because title-only ranking went
|
|
23
|
+
// dead the moment the goal board adopted ADR 0264's naming: all eleven open
|
|
24
|
+
// BONGOS-V2 goals are titled 'Working area N — <subject>', so a real subject word
|
|
25
|
+
// in a draft — 'release', 'grader', 'dependency' — had almost nothing to overlap,
|
|
26
|
+
// every goal scored under threshold, and the suggester returned [] for every idea
|
|
27
|
+
// walked at the 2026-09-12 triage. It also left suggested_goal_id NULL at capture
|
|
28
|
+
// time on every BONGOS-V2 filing, so triage inherited nothing either. The criteria
|
|
29
|
+
// carry the subject vocabulary the titles stopped carrying.
|
|
30
|
+
//
|
|
31
|
+
// Criterion terms are weighted BELOW title terms (see CRITERION_WEIGHT): a goal
|
|
32
|
+
// named for the subject is still the stronger claim, and criteria are longer text
|
|
33
|
+
// that would otherwise swamp it.
|
|
34
|
+
//
|
|
35
|
+
// Still deliberately NOT the goal DESCRIPTION — a goal description is where a whole
|
|
36
|
+
// planning spec lives (goal 1000062 carries an entire decision record in its), so
|
|
37
|
+
// it shares tokens with almost any draft and would flatten every score toward the
|
|
38
|
+
// same number. Modules are included because they are a strong, cheap signal: a
|
|
39
|
+
// draft about the hall really does belong with a goal scoped to `hall-ui`.
|
|
27
40
|
|
|
28
41
|
const { tokenize } = require('./criterion-suggest.js');
|
|
29
42
|
const { versionAcceptsNewWork } = require('./db-tasks.js');
|
|
@@ -38,10 +51,47 @@ const captureErr = (err) => api.logger('lifecycle').error({ err: err && err.mess
|
|
|
38
51
|
// Matches suggestCriteria's default so the two suggesters feel the same.
|
|
39
52
|
const SUGGEST_LIMIT = 5;
|
|
40
53
|
|
|
54
|
+
// A criterion term counts for less than the same term in the title. Criteria are
|
|
55
|
+
// several sentences per goal against a title's handful of words, so equal weight
|
|
56
|
+
// would let a goal that merely MENTIONS a subject outrank one NAMED for it.
|
|
57
|
+
const CRITERION_WEIGHT = 0.5;
|
|
58
|
+
|
|
59
|
+
// rankableCriteria — the criterion prose the ranker may read, for the goals it is
|
|
60
|
+
// about to score. Both callers go through this, so the two rules below have ONE
|
|
61
|
+
// home and cannot drift apart.
|
|
62
|
+
//
|
|
63
|
+
// RULE 1 — PRIVATE GOALS ARE EXCLUDED. This function's own header explains that
|
|
64
|
+
// GET /goals (and therefore this suggester) already lists private goals' TITLES to
|
|
65
|
+
// any signed-in builder, and that reconciling that is a decision about the goal
|
|
66
|
+
// board as a whole rather than something a suggester settles. Criteria are a
|
|
67
|
+
// different order of exposure: score + matched_terms make a ranker into a
|
|
68
|
+
// word-guessing oracle, so ranking on a private goal's done-when prose would let
|
|
69
|
+
// any builder probe the CONTENT of work they cannot see, one candidate word per
|
|
70
|
+
// request. So this addition stops at the visibility line the status quo already
|
|
71
|
+
// draws — a private goal keeps ranking exactly as it did before (title + modules),
|
|
72
|
+
// and nothing new is disclosed. Widening or narrowing the title-level status quo
|
|
73
|
+
// stays the separate decision it was.
|
|
74
|
+
//
|
|
75
|
+
// RULE 2 — BEST EFFORT. A criteria read that fails degrades to title-only ranking,
|
|
76
|
+
// which is what this path did before the criteria existed. A suggester is advisory
|
|
77
|
+
// everywhere it is called; taking down a bug report or 500ing a picker to protect
|
|
78
|
+
// a ranking refinement would be the wrong trade.
|
|
79
|
+
async function rankableCriteria(db, goals, onError) {
|
|
80
|
+
if (typeof db.listCriterionTextForGoals !== 'function') return null;
|
|
81
|
+
const rankable = (goals || []).filter((g) => g && g.visibility !== 'private');
|
|
82
|
+
if (rankable.length === 0) return null;
|
|
83
|
+
try {
|
|
84
|
+
return await db.listCriterionTextForGoals(rankable.map((g) => g.id));
|
|
85
|
+
} catch (err) { onError(err); return null; }
|
|
86
|
+
}
|
|
87
|
+
|
|
41
88
|
// Rank open goals against a DRAFT capture. Pure — no DB, no writes; the caller
|
|
42
89
|
// supplies the goal rows.
|
|
43
90
|
//
|
|
44
91
|
// goals — open goal rows { id, title, scope_modules }
|
|
92
|
+
// opts.criteriaByGoal — Map<goalId, criterion prose> (db.listCriterionTextForGoals).
|
|
93
|
+
// Optional: omitted, this ranks exactly as it did before — the pure
|
|
94
|
+
// function stays callable with no DB anywhere near it.
|
|
45
95
|
// draft — { title, body } (body optional: a capture's prose, weighted lower)
|
|
46
96
|
// opts — { versionId, limit, includeZero }
|
|
47
97
|
//
|
|
@@ -54,7 +104,7 @@ const SUGGEST_LIMIT = 5;
|
|
|
54
104
|
// offer "browse all" without this function pretending to a confidence it lacks.
|
|
55
105
|
function suggestGoals(goals, draft = {}, opts = {}) {
|
|
56
106
|
const { title = '', body = '' } = draft;
|
|
57
|
-
const { versionId = null, limit = SUGGEST_LIMIT, includeZero = false } = opts;
|
|
107
|
+
const { versionId = null, limit = SUGGEST_LIMIT, includeZero = false, criteriaByGoal = null } = opts;
|
|
58
108
|
|
|
59
109
|
// Weighted term map: body terms at 1, title terms override to 2 (a term in
|
|
60
110
|
// both IS a title term — the stronger signal wins). suggestCriteria's rule.
|
|
@@ -73,10 +123,20 @@ function suggestGoals(goals, draft = {}, opts = {}) {
|
|
|
73
123
|
// by the >=3-char filter, which is fine — 'hall' is the signal.
|
|
74
124
|
...tokenize((g.scope_modules || []).join(' ')),
|
|
75
125
|
]);
|
|
126
|
+
// Criterion terms that the title/modules already cover add nothing — they are
|
|
127
|
+
// scored at the full title weight above, and double-counting would reward a
|
|
128
|
+
// goal for repeating itself.
|
|
129
|
+
const criterionTerms = new Set();
|
|
130
|
+
if (criteriaByGoal) {
|
|
131
|
+
for (const t of tokenize(criteriaByGoal.get(String(g.id)) || '')) {
|
|
132
|
+
if (!goalTerms.has(t)) criterionTerms.add(t);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
76
135
|
let score = 0;
|
|
77
136
|
const matched = [];
|
|
78
137
|
for (const [term, w] of weights) {
|
|
79
138
|
if (goalTerms.has(term)) { score += w; matched.push(term); }
|
|
139
|
+
else if (criterionTerms.has(term)) { score += w * CRITERION_WEIGHT; matched.push(term); }
|
|
80
140
|
}
|
|
81
141
|
return { ...g, score, matched_terms: matched.sort() };
|
|
82
142
|
}).sort((a, b) => (b.score - a.score) || (Number(a.id) - Number(b.id)));
|
|
@@ -133,11 +193,16 @@ async function suggestGoalForCapture({ title = '', body = '' } = {}, deps = {})
|
|
|
133
193
|
&& g.title !== generalGoalTitle(g.version_id));
|
|
134
194
|
if (homes.length === 0) return null;
|
|
135
195
|
|
|
196
|
+
// Criterion prose for the same goals — this is the capture-time half of task
|
|
197
|
+
// 1003859, and it is the half that matters most: suggested_goal_id is written
|
|
198
|
+
// HERE, and it was NULL on every BONGOS-V2 filing.
|
|
199
|
+
const criteriaByGoal = await rankableCriteria(db, homes, captureErr);
|
|
200
|
+
|
|
136
201
|
// versionId is passed null: the catch-alls are already gone, and passing one
|
|
137
202
|
// version's id here would only re-filter for that version. includeZero stays
|
|
138
203
|
// false, so a filing that matches nothing gets NO goal rather than the
|
|
139
204
|
// first one dressed up as a suggestion.
|
|
140
|
-
const [best] = suggestGoals(homes, { title, body }, { versionId: null, limit: 1 });
|
|
205
|
+
const [best] = suggestGoals(homes, { title, body }, { versionId: null, limit: 1, criteriaByGoal });
|
|
141
206
|
if (!best) return null;
|
|
142
207
|
|
|
143
208
|
// suggestGoals normalises to { id, title, scope_modules, … } and drops
|
|
@@ -184,7 +249,8 @@ function goalSuggestHandler({ db, LIMITS }) {
|
|
|
184
249
|
// was an oversight, not a decision.
|
|
185
250
|
try {
|
|
186
251
|
const open = await db.listGoals({ versionId, status: 'open' });
|
|
187
|
-
const
|
|
252
|
+
const criteriaByGoal = await rankableCriteria(db, open || [], proseErr);
|
|
253
|
+
const suggestions = suggestGoals(open, { title, body }, { versionId, limit, criteriaByGoal });
|
|
188
254
|
return res.json({
|
|
189
255
|
version_id: versionId,
|
|
190
256
|
// `kind` is echoed, NOT ranked on. Kind decides how a capture LANDS (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.816",
|
|
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.816",
|
|
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.816",
|
|
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/scripts/gds/capture.js
CHANGED
|
@@ -50,8 +50,12 @@
|
|
|
50
50
|
// the active-claim default below is skipped —
|
|
51
51
|
// an opt-out that still guessed a goal would not
|
|
52
52
|
// be an opt-out.
|
|
53
|
-
// (neither) → default to the ACTIVE CLAIM's goal
|
|
54
|
-
//
|
|
53
|
+
// (neither) → default to the ACTIVE CLAIM's goal — but ONLY
|
|
54
|
+
// when the captured text and the claimed task
|
|
55
|
+
// share a subject word (task 1003859); an
|
|
56
|
+
// unrelated filing says so and stays homeless.
|
|
57
|
+
// Applies if this builder holds a claim and its
|
|
58
|
+
// task has a goal
|
|
55
59
|
// ("capturing mid-work almost always concerns
|
|
56
60
|
// the work" — the interview's own words). With
|
|
57
61
|
// no default to offer, the R07 suggester
|
|
@@ -160,6 +164,29 @@ function sanitizeForTerminal(s) {
|
|
|
160
164
|
return String(s == null ? '' : s).replace(/[\x00-\x1f\x7f]/g, '');
|
|
161
165
|
}
|
|
162
166
|
|
|
167
|
+
// claimShareSubject — does the captured text have ANYTHING in common with the
|
|
168
|
+
// task I happen to be holding? (task 1003859)
|
|
169
|
+
//
|
|
170
|
+
// WHY THE DEFAULT NEEDED A GUARD. The active-claim default is a convenience for
|
|
171
|
+
// the ordinary case: you are working on X and you file something about X. But it
|
|
172
|
+
// fired unconditionally, so filing an UNRELATED bug mid-session minted it into
|
|
173
|
+
// the claim's goal — this very bug was filed from a session holding a security
|
|
174
|
+
// task and landed in Working area 10 (Security), which has nothing to do with it,
|
|
175
|
+
// and had to be moved by hand. ADR 0234's owner-interview decision is that a
|
|
176
|
+
// guess must not mint work into a goal, and an inherited goal with no subject in
|
|
177
|
+
// common is exactly a guess.
|
|
178
|
+
//
|
|
179
|
+
// Reuses criterion-suggest's tokenize — the same notion of 'these two texts are
|
|
180
|
+
// about the same thing' the goal suggester ranks with, so the guard and the
|
|
181
|
+
// suggester cannot disagree about what a subject word is.
|
|
182
|
+
const { tokenize } = require('../../modules/lifecycle/criterion-suggest.js');
|
|
183
|
+
|
|
184
|
+
function claimShareSubject(capturedText, claimTitle) {
|
|
185
|
+
const theirs = new Set(tokenize(claimTitle));
|
|
186
|
+
if (theirs.size === 0) return true; // nothing to disagree with — keep the old behaviour
|
|
187
|
+
return tokenize(capturedText).some((t) => theirs.has(t));
|
|
188
|
+
}
|
|
189
|
+
|
|
163
190
|
// resolveGoalForCapture — the WHOLE of --goal resolution for one capture, in the
|
|
164
191
|
// interview's own precedence: an explicit id wins outright; 'none' is a hard
|
|
165
192
|
// opt-out (no default, no hint); otherwise the active-claim default runs, and
|
|
@@ -168,26 +195,34 @@ function sanitizeForTerminal(s) {
|
|
|
168
195
|
// is testable without capturing stdout. Never throws (both collaborators above
|
|
169
196
|
// already degrade to null on their own).
|
|
170
197
|
async function resolveGoalForCapture({ goalArg, version, title, kind, client = cliClient } = {}) {
|
|
198
|
+
const claimLines = [];
|
|
171
199
|
const parsed = parseGoalArg(goalArg);
|
|
172
200
|
if (parsed.kind === 'id') return { goalId: parsed.id, lines: [] };
|
|
173
201
|
if (parsed.kind === 'none') return { goalId: null, lines: [] };
|
|
174
202
|
|
|
175
203
|
const defaulted = await resolveActiveClaimGoal({ client });
|
|
176
204
|
if (defaulted) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
205
|
+
// Only inherit when the two are actually about the same thing. When they are
|
|
206
|
+
// not, say so and fall through to the suggester — which ranks on what was
|
|
207
|
+
// WRITTEN rather than on what happens to be open — instead of filing silently
|
|
208
|
+
// into the wrong goal.
|
|
209
|
+
if (claimShareSubject(title, defaulted.taskTitle)) {
|
|
210
|
+
return {
|
|
211
|
+
goalId: defaulted.goalId,
|
|
212
|
+
lines: [`↪ defaulting --goal to ${defaulted.goalId} from your active claim on task #${defaulted.taskId} ("${sanitizeForTerminal(defaulted.taskTitle)}") — override with --goal <id>, or --goal none to file homeless.`],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
claimLines.push(`↪ NOT defaulting to your active claim's goal: task #${defaulted.taskId} ("${sanitizeForTerminal(defaulted.taskTitle)}") shares no subject with what you filed — pass --goal ${defaulted.goalId} if it really belongs there.`);
|
|
181
216
|
}
|
|
182
217
|
|
|
183
218
|
const hint = await fetchGoalSuggestion({ client, version, title, kind });
|
|
184
219
|
if (hint) {
|
|
185
220
|
return {
|
|
186
221
|
goalId: null,
|
|
187
|
-
lines: [`💡 possible home: goal ${hint.id} "${sanitizeForTerminal(hint.title)}" (score ${hint.score}) — re-run with --goal ${hint.id} (or --goal none to stay homeless on purpose).`],
|
|
222
|
+
lines: [...claimLines, `💡 possible home: goal ${hint.id} "${sanitizeForTerminal(hint.title)}" (score ${hint.score}) — re-run with --goal ${hint.id} (or --goal none to stay homeless on purpose).`],
|
|
188
223
|
};
|
|
189
224
|
}
|
|
190
|
-
return { goalId: null, lines:
|
|
225
|
+
return { goalId: null, lines: claimLines };
|
|
191
226
|
}
|
|
192
227
|
|
|
193
228
|
// DEFAULT IS NO, and only an explicit yes counts. Extracted so the done-when
|
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.816'; // 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');
|
|
@@ -181,7 +181,11 @@ await test('no --goal: the active-claim default fires and names how to opt out',
|
|
|
181
181
|
me: { ok: true, data: { active_claims: [{ task_id: 55, title: 'Fix the thing' }] } },
|
|
182
182
|
task: { ok: true, data: { task: { id: 55, goal_id: 1000071, title: 'Fix the thing' } } },
|
|
183
183
|
});
|
|
184
|
-
|
|
184
|
+
// The captured text shares 'thing' with the claim — the ordinary case the
|
|
185
|
+
// default exists for. (It used to be the bare title 't'; since task 1003859 the
|
|
186
|
+
// default only fires on a shared subject word, so a placeholder no longer
|
|
187
|
+
// exercises this path.)
|
|
188
|
+
const r = await capture.resolveGoalForCapture({ goalArg: null, version: 'BV1', title: 'the thing is broken', client });
|
|
185
189
|
assert.equal(r.goalId, 1000071);
|
|
186
190
|
assert.equal(r.lines.length, 1);
|
|
187
191
|
assert.match(r.lines[0], /1000071/);
|
|
@@ -239,4 +243,64 @@ await test('WIRING: a routing refusal is surfaced through the existing verbatim
|
|
|
239
243
|
assert.match(CAPTURE_SRC, /console\.error\(formatCaptureFailure\(r\.status, r\.data\)\);/);
|
|
240
244
|
});
|
|
241
245
|
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// the active-claim default must not mint UNRELATED work into the claim's goal
|
|
248
|
+
// (task 1003859)
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
//
|
|
251
|
+
// The default fired unconditionally, so filing an unrelated bug mid-session
|
|
252
|
+
// routed it into whatever goal you happened to be holding — task 1003859 itself
|
|
253
|
+
// was filed from a security claim and landed in Working area 10, and had to be
|
|
254
|
+
// moved by hand. ADR 0234: a guess must not mint work into a goal.
|
|
255
|
+
|
|
256
|
+
await test('no --goal, claim shares NO subject with the filing: does not inherit, and says why', async () => {
|
|
257
|
+
const { client, calls } = stubClient({
|
|
258
|
+
me: { ok: true, data: { active_claims: [{ task_id: 55, title: 'Harden the OAuth callback against replay' }] } },
|
|
259
|
+
task: { ok: true, data: { task: { id: 55, goal_id: 1000094, title: 'Harden the OAuth callback against replay' } } },
|
|
260
|
+
suggestions: { ok: true, data: { suggestions: [] } },
|
|
261
|
+
});
|
|
262
|
+
const r = await capture.resolveGoalForCapture({
|
|
263
|
+
goalArg: null, version: 'BV1', title: 'The goal suggester returns zero suggestions for every idea', client,
|
|
264
|
+
});
|
|
265
|
+
assert.equal(r.goalId, null, 'an unrelated claim must not decide where this work lives');
|
|
266
|
+
assert.equal(r.lines.length, 1, 'staying homeless silently would be the same bug with no evidence');
|
|
267
|
+
assert.match(r.lines[0], /NOT defaulting/);
|
|
268
|
+
assert.match(r.lines[0], /1000094/, 'it must name the goal it declined, so --goal can override it');
|
|
269
|
+
assert.equal(calls.suggestions.length, 1, 'with no default, the suggester still gets its turn');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await test('a declined default still yields to a real suggestion, keeping both lines', async () => {
|
|
273
|
+
const { client } = stubClient({
|
|
274
|
+
me: { ok: true, data: { active_claims: [{ task_id: 55, title: 'Harden the OAuth callback against replay' }] } },
|
|
275
|
+
task: { ok: true, data: { task: { id: 55, goal_id: 1000094, title: 'Harden the OAuth callback against replay' } } },
|
|
276
|
+
suggestions: { ok: true, data: { suggestions: [{ id: 1000091, title: 'Working area 7', score: 3 }] } },
|
|
277
|
+
});
|
|
278
|
+
const r = await capture.resolveGoalForCapture({
|
|
279
|
+
goalArg: null, version: 'BV1', title: 'The goal suggester returns zero suggestions', client,
|
|
280
|
+
});
|
|
281
|
+
assert.equal(r.goalId, null);
|
|
282
|
+
assert.equal(r.lines.length, 2, 'the refusal and the hint are different facts — both are printed');
|
|
283
|
+
assert.match(r.lines[0], /NOT defaulting/);
|
|
284
|
+
assert.match(r.lines[1], /1000091/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await test('--goal N still wins outright over the guard (it is not a veto)', async () => {
|
|
288
|
+
const { client, calls } = stubClient({
|
|
289
|
+
me: { ok: true, data: { active_claims: [{ task_id: 55, title: 'Something else entirely' }] } },
|
|
290
|
+
task: { ok: true, data: { task: { id: 55, goal_id: 1000094 } } },
|
|
291
|
+
});
|
|
292
|
+
const r = await capture.resolveGoalForCapture({ goalArg: '1000094', version: 'BV1', title: 'unrelated words here', client });
|
|
293
|
+
assert.deepEqual(r, { goalId: 1000094, lines: [] });
|
|
294
|
+
assert.equal(calls.me, 0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await test('a claim title with no subject words at all still inherits — the guard only fires on real disagreement', async () => {
|
|
298
|
+
const { client } = stubClient({
|
|
299
|
+
me: { ok: true, data: { active_claims: [{ task_id: 55, title: 'a' }] } },
|
|
300
|
+
task: { ok: true, data: { task: { id: 55, goal_id: 1000071, title: 'a' } } },
|
|
301
|
+
});
|
|
302
|
+
const r = await capture.resolveGoalForCapture({ goalArg: null, version: 'BV1', title: 'anything at all', client });
|
|
303
|
+
assert.equal(r.goalId, 1000071, 'nothing to compare against is not evidence of a mismatch');
|
|
304
|
+
});
|
|
305
|
+
|
|
242
306
|
summary();
|
package/tests/goal_suggest.mjs
CHANGED
|
@@ -199,13 +199,23 @@ await test('the handler clamps limit into a sane range', async () => {
|
|
|
199
199
|
// confident answer and declines whenever "confident" is not available.
|
|
200
200
|
// =========================================================================
|
|
201
201
|
|
|
202
|
-
const fakeDb = ({ versions = [], goals = [], throws = null, calls = null } = {}) => ({
|
|
202
|
+
const fakeDb = ({ versions = [], goals = [], throws = null, calls = null, criteria = null } = {}) => ({
|
|
203
203
|
listVersions: async () => { if (throws === 'versions') throw new Error('boom'); return versions; },
|
|
204
204
|
listGoals: async (args) => {
|
|
205
205
|
if (calls) calls.push(args);
|
|
206
206
|
if (throws === 'goals') throw new Error('boom');
|
|
207
207
|
return goals;
|
|
208
208
|
},
|
|
209
|
+
// task 1003859: the criterion prose the ranker reads alongside the title.
|
|
210
|
+
// Omitted (criteria: null) it is absent from the fake ENTIRELY, which is also
|
|
211
|
+
// the pinned shape for a db that predates this read.
|
|
212
|
+
...(criteria ? {
|
|
213
|
+
listCriterionTextForGoals: async (ids) => {
|
|
214
|
+
if (throws === 'criteria') throw new Error('boom');
|
|
215
|
+
if (calls) calls.push({ criteriaFor: ids.map(String) });
|
|
216
|
+
return new Map(Object.entries(criteria));
|
|
217
|
+
},
|
|
218
|
+
} : {}),
|
|
209
219
|
});
|
|
210
220
|
|
|
211
221
|
const BUILDING = { id: 'BONGOS-V1', status: 'building' };
|
|
@@ -298,4 +308,141 @@ await test('suggestGoalForCapture: the catch-all is never offered as a home', as
|
|
|
298
308
|
assert.equal(got, null, 'a bucket is not a workspace — dropped for EVERY open version, not just one');
|
|
299
309
|
});
|
|
300
310
|
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// ranking on DONE-WHEN CRITERIA (task 1003859)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
//
|
|
315
|
+
// The real failure: since ADR 0264 every open BONGOS-V2 goal is titled
|
|
316
|
+
// 'Working area N — <subject>'. A subject word in a draft had nothing to overlap
|
|
317
|
+
// in the title, every goal scored 0, and the suggester returned [] for every idea
|
|
318
|
+
// walked at the 2026-09-12 triage — which also left suggested_goal_id NULL on
|
|
319
|
+
// every filing. These reproduce that board rather than a convenient one.
|
|
320
|
+
|
|
321
|
+
const WORKING_AREAS = [
|
|
322
|
+
{ id: 1000091, title: 'Working area 7', scope_modules: [], version_id: 'BONGOS-V2' },
|
|
323
|
+
{ id: 1000094, title: 'Working area 10', scope_modules: [], version_id: 'BONGOS-V2' },
|
|
324
|
+
];
|
|
325
|
+
const WA_CRITERIA = {
|
|
326
|
+
1000091: 'A release is published and an instance can move its pin to it. The grader runs and its findings are readable.',
|
|
327
|
+
1000094: 'Every authenticated route re-checks the caller rank. A credential never reaches a log.',
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
await test('the real board: title-only ranking scores every Working area goal ZERO', () => {
|
|
331
|
+
const got = suggestGoals(WORKING_AREAS, { title: 'the grader returns no findings' }, { versionId: 'BONGOS-V2', includeZero: true });
|
|
332
|
+
assert.deepEqual(got.map((g) => g.score), [0, 0],
|
|
333
|
+
'this is the bug, pinned: the naming convention left the ranker nothing to match');
|
|
334
|
+
assert.deepEqual(suggestGoals(WORKING_AREAS, { title: 'the grader returns no findings' }, { versionId: 'BONGOS-V2' }), [],
|
|
335
|
+
'and so the suggester returned nothing at all');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await test('with criteria, the same draft finds its goal, with the matched terms shown', () => {
|
|
339
|
+
const got = suggestGoals(
|
|
340
|
+
WORKING_AREAS,
|
|
341
|
+
{ title: 'the grader returns no findings' },
|
|
342
|
+
{ versionId: 'BONGOS-V2', criteriaByGoal: new Map(Object.entries(WA_CRITERIA)) }
|
|
343
|
+
);
|
|
344
|
+
assert.equal(got.length, 1, 'only the goal whose criteria talk about grading');
|
|
345
|
+
assert.equal(got[0].id, 1000091);
|
|
346
|
+
assert.ok(got[0].score > 0);
|
|
347
|
+
assert.ok(got[0].matched_terms.includes('grader'), `expected 'grader' among ${JSON.stringify(got[0].matched_terms)}`);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await test('a criterion match weighs LESS than the same word in the title', () => {
|
|
351
|
+
const byTitle = { id: 10, title: 'Grader health', scope_modules: [], version_id: 'BONGOS-V2' };
|
|
352
|
+
const byCriteria = { id: 11, title: 'Working area 7', scope_modules: [], version_id: 'BONGOS-V2' };
|
|
353
|
+
const got = suggestGoals(
|
|
354
|
+
[byCriteria, byTitle],
|
|
355
|
+
{ title: 'grader' },
|
|
356
|
+
{ versionId: 'BONGOS-V2', criteriaByGoal: new Map([['11', 'the grader runs nightly']]) }
|
|
357
|
+
);
|
|
358
|
+
assert.equal(got[0].id, 10, 'a goal NAMED for the subject still outranks one that merely mentions it');
|
|
359
|
+
assert.ok(got[0].score > got[1].score);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await test('a term in both title and criteria is counted ONCE, at the title weight', () => {
|
|
363
|
+
const g = { id: 12, title: 'Grader health', scope_modules: [], version_id: 'BONGOS-V2' };
|
|
364
|
+
const withCriteria = suggestGoals([g], { title: 'grader' }, { versionId: 'BONGOS-V2', criteriaByGoal: new Map([['12', 'grader grader grader']]) });
|
|
365
|
+
const without = suggestGoals([g], { title: 'grader' }, { versionId: 'BONGOS-V2' });
|
|
366
|
+
assert.equal(withCriteria[0].score, without[0].score, 'repeating itself must not inflate a goal');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await test('no criteriaByGoal: ranking is EXACTLY what it was before (the pure function stays DB-free)', () => {
|
|
370
|
+
const draft = { title: 'the hall sidebar collapses on mobile' };
|
|
371
|
+
const a = suggestGoals([HALL_GOAL, ECON_GOAL], draft, { versionId: 'BONGOS-V1' });
|
|
372
|
+
const b = suggestGoals([HALL_GOAL, ECON_GOAL], draft, { versionId: 'BONGOS-V1', criteriaByGoal: null });
|
|
373
|
+
assert.deepEqual(a, b);
|
|
374
|
+
assert.equal(a[0].id, 81);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await test('suggestGoalForCapture reads the criteria, so suggested_goal_id is populated on the real board', async () => {
|
|
378
|
+
const calls = [];
|
|
379
|
+
const got = await suggestGoalForCapture(
|
|
380
|
+
{ title: 'the grader returns no findings', body: '' },
|
|
381
|
+
{ db: fakeDb({ versions: [{ id: 'BONGOS-V2', status: 'building' }], goals: WORKING_AREAS, criteria: WA_CRITERIA, calls }) }
|
|
382
|
+
);
|
|
383
|
+
assert.ok(got, 'this returned NULL before task 1003859 — which is why suggested_goal_id was null on every filing');
|
|
384
|
+
assert.equal(got.id, 1000091);
|
|
385
|
+
assert.equal(got.version_id, 'BONGOS-V2');
|
|
386
|
+
assert.ok(calls.some((c) => c && c.criteriaFor), 'the criteria read must actually be issued');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await test('a criteria read that THROWS degrades to title-only ranking, never to a lost filing', async () => {
|
|
390
|
+
const got = await suggestGoalForCapture(
|
|
391
|
+
{ title: 'the hall sidebar collapses on mobile', body: '' },
|
|
392
|
+
{ db: fakeDb({ versions: [BUILDING], goals: [HALL_GOAL, ECON_GOAL], criteria: {}, throws: 'criteria' }) }
|
|
393
|
+
);
|
|
394
|
+
assert.ok(got, 'a bug report must survive a criteria read that fails');
|
|
395
|
+
assert.equal(got.id, 81, 'and still rank on what it can see');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await test('a db with NO listCriterionTextForGoals at all still works (older core, or a lean fake)', async () => {
|
|
399
|
+
const got = await suggestGoalForCapture(
|
|
400
|
+
{ title: 'the hall sidebar collapses on mobile', body: '' },
|
|
401
|
+
{ db: fakeDb({ versions: [BUILDING], goals: [HALL_GOAL, ECON_GOAL] }) }
|
|
402
|
+
);
|
|
403
|
+
assert.equal(got.id, 81);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// a PRIVATE goal's criteria are never ranked on (task 1003859, grade round 1)
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
//
|
|
410
|
+
// score + matched_terms make a ranker into a word-guessing oracle: one candidate
|
|
411
|
+
// word per request tells you whether that word appears in the ranked text. The
|
|
412
|
+
// suggester already exposes private goals' TITLES (GET /goals does too — a wider
|
|
413
|
+
// status quo this task does not settle), but their done-when prose is a different
|
|
414
|
+
// order of exposure, so the criteria read stops at the visibility line.
|
|
415
|
+
|
|
416
|
+
const PRIVATE_GOAL = { id: 900, title: 'Working area 12', scope_modules: [], version_id: 'BONGOS-V2', visibility: 'private' };
|
|
417
|
+
const PUBLIC_GOAL = { id: 901, title: 'Working area 13', scope_modules: [], version_id: 'BONGOS-V2', visibility: 'public' };
|
|
418
|
+
|
|
419
|
+
await test('the criteria of a PRIVATE goal are never fetched — not even to rank', async () => {
|
|
420
|
+
const asked = [];
|
|
421
|
+
const db = {
|
|
422
|
+
listGoals: async () => [PRIVATE_GOAL, PUBLIC_GOAL],
|
|
423
|
+
listCriterionTextForGoals: async (ids) => { asked.push(...ids.map(String)); return new Map(); },
|
|
424
|
+
};
|
|
425
|
+
const res = fakeRes();
|
|
426
|
+
await goalSuggestHandler({ db, LIMITS: { TITLE: 200 } })(
|
|
427
|
+
{ query: { version: 'BONGOS-V2', title: 'embargo' } }, res
|
|
428
|
+
);
|
|
429
|
+
assert.deepEqual(asked, ['901'], 'only the public goal may contribute criterion text');
|
|
430
|
+
assert.ok(!asked.includes('900'), 'a private goal must not be probeable one word at a time');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await test('a private goal still ranks on its TITLE — this narrows nothing that already worked', () => {
|
|
434
|
+
const got = suggestGoals([PRIVATE_GOAL], { title: 'working area 12' }, { versionId: 'BONGOS-V2' });
|
|
435
|
+
assert.equal(got.length, 1, 'the title-level status quo is a separate decision, left exactly as it was');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await test('every goal private ⇒ no criteria read is issued at all', async () => {
|
|
439
|
+
let called = 0;
|
|
440
|
+
const db = {
|
|
441
|
+
listGoals: async () => [PRIVATE_GOAL],
|
|
442
|
+
listCriterionTextForGoals: async () => { called++; return new Map(); },
|
|
443
|
+
};
|
|
444
|
+
await goalSuggestHandler({ db, LIMITS: { TITLE: 200 } })({ query: { version: 'BONGOS-V2', title: 'x y z' } }, fakeRes());
|
|
445
|
+
assert.equal(called, 0, 'a query with nothing rankable must not be issued');
|
|
446
|
+
});
|
|
447
|
+
|
|
301
448
|
summary();
|