@bongos/core 1.19.585 → 1.19.587
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 +54 -29
- package/.claude/skills/idea-triage/SKILL.md +24 -4
- package/docs/adr/0261-a-preselect-always-carries-a-reason-the-bundle-summary-is-the-floor.md +78 -0
- package/docs/adr/0262-a-bug-never-lands-in-the-inbox.md +96 -0
- package/docs/adr/README.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/discord/discord-bugs.js +23 -2
- package/modules/discord/discord-inbound.js +9 -0
- package/modules/ideas/homeless-home.js +51 -0
- package/modules/ideas/inbox.js +33 -18
- package/modules/ideas/routes/inbox.js +17 -0
- package/modules/ideas/routing.js +65 -2
- package/modules/lifecycle/db-goals.js +92 -0
- package/modules/lifecycle/db-versions.js +30 -0
- package/modules/lifecycle/db.js +4 -1
- package/modules/lifecycle/goal-advisory.js +17 -1
- package/modules/lifecycle/lifecycle.js +11 -0
- package/modules/public-landing/public/projects.html +21 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/filing_abuse_matrix.mjs +99 -0
- package/tests/idea_routing.mjs +226 -1
- package/tests/maintenance_goal.mjs +191 -0
- package/tests/projects_hub_module_picker.mjs +29 -11
- package/tests/task_goal_required.mjs +2 -1
- package/tests/wizard_preselect_why.mjs +137 -0
|
@@ -186,7 +186,8 @@ test('no FIFTH vector has appeared', () => {
|
|
|
186
186
|
// which case delete its opt-in and this line, do not extend the list.
|
|
187
187
|
const roster = new Set([...OPT_INS.map(([f]) => f), 'modules/lifecycle/db-tasks.js']);
|
|
188
188
|
const suspects = [
|
|
189
|
-
'modules/ideas/inbox.js', 'modules/
|
|
189
|
+
'modules/ideas/inbox.js', 'modules/ideas/homeless-home.js',
|
|
190
|
+
'modules/lifecycle/routes/goals.js',
|
|
190
191
|
'modules/lifecycle/routes/task-write-routes.js',
|
|
191
192
|
];
|
|
192
193
|
for (const f of suspects) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/wizard_preselect_why.mjs — the module picker always says WHY the preselect is on
|
|
2
|
+
// (task 1003684).
|
|
3
|
+
//
|
|
4
|
+
// The bug as filed said the starter-bundle "why" sentence was being dropped for
|
|
5
|
+
// Game + small-team. It was not: `adjustments` is a DELTA, and a rule that fires without
|
|
6
|
+
// changing anything claims nothing (ADR 0243). The game bundle is already
|
|
7
|
+
// ['dev-box','discord'] and the small-team rule ADDS dev-box, so the rule fires, moves
|
|
8
|
+
// nothing, and correctly reports []. Verified against live core 1.19.580:
|
|
9
|
+
// byTeamShape['small-team'] => {"bundle":["dev-box","discord"],"adjustments":[]}
|
|
10
|
+
//
|
|
11
|
+
// The real defect is what the owner then saw: two extras pre-switched-on with no reason
|
|
12
|
+
// beside them — precisely the state the paint site's own comment calls out, "a preselect an
|
|
13
|
+
// owner cannot see the reason for is one they have to audit, which is the thing ADR 0237
|
|
14
|
+
// says is worse than no preselect at all". Each bundle carries a curated `summary` authored
|
|
15
|
+
// for this, served on every row of GET /provisioning/starter-bundles, and never rendered
|
|
16
|
+
// anywhere in the wizard. It is now the floor beneath the adjustment sentences.
|
|
17
|
+
//
|
|
18
|
+
// Harness: the vm-slice idiom (tests/project_door_ui.mjs), since the wizard's script is
|
|
19
|
+
// inline in the page and there is no jsdom here.
|
|
20
|
+
//
|
|
21
|
+
// Run: node --test --test-reporter=tap tests/wizard_preselect_why.mjs
|
|
22
|
+
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { test } from 'node:test';
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import vm from 'node:vm';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
|
|
32
|
+
|
|
33
|
+
const SLICE_RE = /function bundleRow\(\)[\s\S]*?function preselectReasons\(\)[\s\S]*?\n {4}\}/;
|
|
34
|
+
const slice = (HUB.match(SLICE_RE) || [null])[0];
|
|
35
|
+
|
|
36
|
+
test('the preselect-reason slice is still findable', () => {
|
|
37
|
+
assert.ok(slice, 'bundleRow/tailoredRow/preselectReasons were not found in projects.html');
|
|
38
|
+
assert.match(slice, /function tailoredRow\(\)/, 'the slice spans the tailoring lookup too');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Faithful to the real payload shape (confirmed against live): every bundle row carries
|
|
42
|
+
// label/summary/bundle, and byTeamShape holds the resolved answer per team shape.
|
|
43
|
+
const BUNDLES = {
|
|
44
|
+
bundles: [
|
|
45
|
+
{
|
|
46
|
+
type: 'game',
|
|
47
|
+
label: 'Game starter',
|
|
48
|
+
summary: 'Somewhere for players to gather, and a place to walk a change through before anyone plays it.',
|
|
49
|
+
bundle: ['dev-box', 'discord'],
|
|
50
|
+
byTeamShape: {
|
|
51
|
+
// the live case: the rule fires, moves nothing, reports nothing
|
|
52
|
+
'small-team': { bundle: ['dev-box', 'discord'], adjustments: [] },
|
|
53
|
+
solo: {
|
|
54
|
+
bundle: ['discord'],
|
|
55
|
+
adjustments: [{ reason: 'You are building this alone, so the shared cloud environment is off — your own machine is the setup.' }],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'research',
|
|
61
|
+
label: 'Research starter',
|
|
62
|
+
summary: 'Extra agents to run the reading and the experiments the question needs.',
|
|
63
|
+
bundle: ['agents'],
|
|
64
|
+
byTeamShape: {
|
|
65
|
+
'small-team': {
|
|
66
|
+
bundle: ['agents', 'dev-box'],
|
|
67
|
+
adjustments: [{ reason: 'A small team gets one shared environment to build in, so nobody is debugging their own laptop.' }],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{ type: 'not-sure', label: 'Not sure', summary: '', bundle: [], byTeamShape: {} },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function why({ projType, teamShape, modules = null, bundles = BUNDLES }) {
|
|
76
|
+
const sandbox = { starterBundles: bundles, state: { projType, teamShape, modules } };
|
|
77
|
+
sandbox.globalThis = sandbox;
|
|
78
|
+
vm.createContext(sandbox);
|
|
79
|
+
vm.runInContext(slice + '\nvar __out = preselectReasons();', sandbox);
|
|
80
|
+
// Copy out of the vm realm: its Array has a different prototype, and assert/strict's
|
|
81
|
+
// deepEqual compares prototypes — so a correct result would otherwise fail as
|
|
82
|
+
// "same structure but not reference-equal".
|
|
83
|
+
return Array.from(sandbox.__out);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('game + small-team: no delta, so the type\'s own summary is the reason', () => {
|
|
87
|
+
// This is the exact combination from the live walk that surfaced the bug.
|
|
88
|
+
const out = why({ projType: 'game', teamShape: 'small-team' });
|
|
89
|
+
assert.deepEqual(out, ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a REAL adjustment still speaks for itself — the summary does not displace it', () => {
|
|
93
|
+
const out = why({ projType: 'research', teamShape: 'small-team' });
|
|
94
|
+
assert.equal(out.length, 1);
|
|
95
|
+
assert.match(out[0], /shared environment/);
|
|
96
|
+
assert.doesNotMatch(out[0], /reading and the experiments/, 'the rule\'s sentence wins, not the summary');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a removal adjustment is reported the same way', () => {
|
|
100
|
+
const out = why({ projType: 'game', teamShape: 'solo' });
|
|
101
|
+
assert.equal(out.length, 1);
|
|
102
|
+
assert.match(out[0], /building this alone/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('no team shape answered: falls through to the bundle, still explained', () => {
|
|
106
|
+
// tailoredRow() returns the unadjusted bundle with adjustments: [] when no shape is set,
|
|
107
|
+
// which before this change also produced an unexplained preselect.
|
|
108
|
+
const out = why({ projType: 'game', teamShape: null });
|
|
109
|
+
assert.deepEqual(out, ['Somewhere for players to gather, and a place to walk a change through before anyone plays it.']);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('once the owner has answered for themselves, the panel says nothing', () => {
|
|
113
|
+
// The pre-existing rule, and the one a careless fallback would have broken: handing back
|
|
114
|
+
// a reason for a toggle they just flipped reads as the panel arguing with them.
|
|
115
|
+
assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', modules: [] }), []);
|
|
116
|
+
assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', modules: ['agents'] }), []);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('an unknown type, or a bundle with no summary, says nothing rather than crashing', () => {
|
|
120
|
+
assert.deepEqual(why({ projType: 'no-such-type', teamShape: 'small-team' }), []);
|
|
121
|
+
assert.deepEqual(why({ projType: null, teamShape: 'small-team' }), []);
|
|
122
|
+
assert.deepEqual(why({ projType: 'not-sure', teamShape: 'small-team' }), [], 'empty summary is not a sentence');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('before the bundles read lands, nothing is claimed', () => {
|
|
126
|
+
assert.deepEqual(why({ projType: 'game', teamShape: 'small-team', bundles: null }), []);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('every sentence returned is a plain string, so the paint site can escape it directly', () => {
|
|
130
|
+
for (const t of ['game', 'research']) {
|
|
131
|
+
for (const s of ['small-team', 'solo', null]) {
|
|
132
|
+
for (const line of why({ projType: t, teamShape: s })) {
|
|
133
|
+
assert.equal(typeof line, 'string', `${t}/${s} yielded a non-string`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|