@bongos/core 1.21.106 → 1.21.108
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 +72 -52
- package/.claude/skills/goal-create/SKILL.md +8 -3
- package/.claude/skills/idea-triage/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +6 -4
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +6 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +91 -2
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +25 -19
- package/docs/copy-registry.json +92 -37
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +189 -188
- package/migrations/core_275_claims_auto_pickup.sql +24 -0
- package/modules/copy-desk/docx.js +27 -7
- package/modules/copy-desk/page-status.js +3 -1
- package/modules/copy-desk/pages.js +44 -5
- package/modules/copy-desk/routes/copy-desk.js +4 -3
- package/modules/copy-desk/tests/copy_docx.mjs +38 -0
- package/modules/copy-desk/tests/copy_page_draft.mjs +79 -0
- package/modules/government/decision-rules.js +1 -0
- package/modules/hall-ui/public/paintbrush.js +152 -12
- package/modules/hall-ui/public/studio.js +3 -1
- package/modules/hall-ui/public/style.css +16 -0
- package/modules/hall-ui/public/tweak-editor-lib.js +24 -12
- package/modules/hall-ui/records/craft-mode.md +5 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/lifecycle/autopickup.js +94 -0
- package/modules/lifecycle/db-claims.js +18 -4
- package/modules/lifecycle/routes/claims.js +12 -1
- package/modules/lifecycle/routes/project-settings.js +59 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/government-walk.js +1 -0
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +12 -21
- package/scripts/gds/seed-verify-criteria.js +73 -18
- package/src/bongos/project-settings.js +79 -0
- package/src/module-api.js +8 -1
- package/tests/autopickup_cap.mjs +236 -0
- package/tests/autopickup_cap_db.mjs +115 -0
- package/tests/copy_desk_page_submit.mjs +36 -0
- package/tests/government_act_alone_buildings.mjs +1 -0
- package/tests/hall_tweak_editor.mjs +19 -3
- package/tests/paintbrush.mjs +151 -2
- package/tests/planning_session_skill.mjs +40 -0
- package/tests/seed_verify_criteria.mjs +130 -0
|
@@ -161,5 +161,45 @@ test('Phase 5 re-probes the session before the first write, and on a 401 re-auth
|
|
|
161
161
|
assert.match(p5, /idempotent/i, 'and says why a re-run is safe');
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
// ---- task 1004668: a new task does not wait at backlog, and ready is not claimable ----
|
|
165
|
+
// POST /tasks answers 'backlog', then the reconciler's sweep promotes any task with
|
|
166
|
+
// no unmet dependency within about a minute. Three playbooks said a new task sits at
|
|
167
|
+
// backlog until promoted; one triage told the owner 15 tasks awaited their go and then
|
|
168
|
+
// hit 409 cannot_promote on every one.
|
|
169
|
+
|
|
170
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\s+/g, ' ');
|
|
171
|
+
const PLAYBOOKS = ['planning-session', 'idea-triage', 'goal-create', 'owner-review', 'backlog-review']
|
|
172
|
+
.map((s) => [s, read(`.claude/skills/${s}/SKILL.md`)]);
|
|
173
|
+
|
|
174
|
+
test('no playbook says a newly created task lands at backlog and waits for a promote', () => {
|
|
175
|
+
for (const [name, text] of PLAYBOOKS) {
|
|
176
|
+
assert.ok(!/Every newly-seeded task lands `status='backlog'`/.test(text), `${name}: the seed-lands-at-backlog claim`);
|
|
177
|
+
assert.ok(!/Every new task lands status='backlog'/.test(text), `${name}: the new-task-lands-at-backlog claim`);
|
|
178
|
+
assert.ok(!/lands the new task in `backlog`, \*\*not\*\* `active`/.test(text), `${name}: the triage claim`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('the Phase 5 promote step reads the statuses first and names the claim gate as the enforcement', () => {
|
|
183
|
+
const step = promoteStep();
|
|
184
|
+
assert.match(step, /Do \*\*not\*\* assume the seed landed at `backlog`/, 'replaces the assumption with a check');
|
|
185
|
+
assert.match(step, /backlog sweep/, 'names what promotes them');
|
|
186
|
+
assert.match(step, /`ready` is not "claimable"/, 'says ready is not claimable');
|
|
187
|
+
assert.match(step, /DEPS_NOT_SHIPPED/, 'names the claim-time dependency gate');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('idea-triage and goal-create say to re-read the status and promote only what is still at backlog', () => {
|
|
191
|
+
const triage = PLAYBOOKS.find(([n]) => n === 'idea-triage')[1];
|
|
192
|
+
assert.match(triage, /auto-promoted to `ready` within about a minute/);
|
|
193
|
+
assert.match(triage, /Re-read its status after creating/);
|
|
194
|
+
assert.match(triage, /409 cannot_promote/);
|
|
195
|
+
const goal = PLAYBOOKS.find(([n]) => n === 'goal-create')[1];
|
|
196
|
+
assert.match(goal, /promote only a non-spike still at 'backlog'/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('the seeding contract names the pre-seed baseline for a populated goal', () => {
|
|
200
|
+
assert.ok(NORM.includes('baselineSeedCriteria'), 'names the baseline helper');
|
|
201
|
+
assert.match(NORM, /BEFORE the first write/);
|
|
202
|
+
});
|
|
203
|
+
|
|
164
204
|
console.log(`\nplanning_session_skill: ${passed} passed, ${failed} failed`);
|
|
165
205
|
if (failed > 0) process.exit(1);
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
import { strict as assert } from 'node:assert';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
18
19
|
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
20
21
|
const {
|
|
21
22
|
diffCriterionLinks,
|
|
22
23
|
expectedCriterionCounts,
|
|
24
|
+
baselineSeedCriteria,
|
|
23
25
|
verifySeedCriteriaLinks,
|
|
24
26
|
} = require('../scripts/gds/seed-verify-criteria.js');
|
|
25
27
|
|
|
@@ -145,5 +147,133 @@ await testAsync('a failed/malformed GET is treated as a verification failure, no
|
|
|
145
147
|
assert.ok(logs.some((l) => l.includes('VERIFY FAILED')));
|
|
146
148
|
});
|
|
147
149
|
|
|
150
|
+
// --- task 1004668: seeding into a goal that already carries linked work -----
|
|
151
|
+
//
|
|
152
|
+
// Goal 1000095 held six criteria with live links (1, 9, 4, 3, 6, 10) before a
|
|
153
|
+
// seed touched it. The old diff unioned those into the check with an expected
|
|
154
|
+
// count of zero, so a CORRECT seed exited 1 six times over.
|
|
155
|
+
|
|
156
|
+
const POPULATED = [
|
|
157
|
+
{ criterion_id: 'old-1', linked_tasks: 1 }, { criterion_id: 'old-2', linked_tasks: 9 },
|
|
158
|
+
{ criterion_id: 'old-3', linked_tasks: 4 }, { criterion_id: 'old-4', linked_tasks: 3 },
|
|
159
|
+
{ criterion_id: 'old-5', linked_tasks: 6 }, { criterion_id: 'old-6', linked_tasks: 10 },
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
test('THE INCIDENT: without a baseline, criteria the seed does not cite are not assumed to be zero', () => {
|
|
163
|
+
const rows = [...POPULATED, { criterion_id: 'ours', linked_tasks: 2 }];
|
|
164
|
+
assert.deepEqual(diffCriterionLinks({ ours: 2 }, rows), []);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('without a baseline the plan\'s own criteria are still checked', () => {
|
|
168
|
+
const rows = [...POPULATED, { criterion_id: 'ours', linked_tasks: 0 }];
|
|
169
|
+
assert.deepEqual(diffCriterionLinks({ ours: 2 }, rows), [{ criterion_id: 'ours', expected: 2, actual: 0 }]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('with a baseline, an untouched criterion at its pre-seed count passes', () => {
|
|
173
|
+
const baseline = { 'old-1': 1, 'old-2': 9, 'old-3': 4, 'old-4': 3, 'old-5': 6, 'old-6': 10, ours: 0 };
|
|
174
|
+
const rows = [...POPULATED, { criterion_id: 'ours', linked_tasks: 2 }];
|
|
175
|
+
assert.deepEqual(diffCriterionLinks({ ours: 2 }, rows, { baseline }), []);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('with a baseline, a stray link onto a criterion the seed never cites is caught', () => {
|
|
179
|
+
// The RETRO2 mislink lands tasks on the WRONG criterion. With a baseline that
|
|
180
|
+
// excess is visible even though the wrong criterion is not in the plan.
|
|
181
|
+
const baseline = { 'old-1': 1, ours: 0 };
|
|
182
|
+
const rows = [{ criterion_id: 'old-1', linked_tasks: 3 }, { criterion_id: 'ours', linked_tasks: 0 }];
|
|
183
|
+
assert.deepEqual(diffCriterionLinks({ ours: 2 }, rows, { baseline }), [
|
|
184
|
+
{ criterion_id: 'old-1', expected: 1, actual: 3 },
|
|
185
|
+
{ criterion_id: 'ours', expected: 2, actual: 0 },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('with a baseline, a planned criterion that already had links expects baseline + plan', () => {
|
|
190
|
+
const rows = [{ criterion_id: 'shared', linked_tasks: 7 }];
|
|
191
|
+
assert.deepEqual(diffCriterionLinks({ shared: 2 }, rows, { baseline: { shared: 5 } }), []);
|
|
192
|
+
assert.deepEqual(diffCriterionLinks({ shared: 2 }, rows, { baseline: { shared: 4 } }),
|
|
193
|
+
[{ criterion_id: 'shared', expected: 6, actual: 7 }]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a criterion created after the baseline starts at zero, so a link on it is caught', () => {
|
|
197
|
+
const rows = [{ criterion_id: 'brand-new', linked_tasks: 1 }];
|
|
198
|
+
assert.deepEqual(diffCriterionLinks({}, rows, { baseline: {} }),
|
|
199
|
+
[{ criterion_id: 'brand-new', expected: 0, actual: 1 }]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const goalWith = (criteria) => async () => ({ ok: true, status: 200, json: { criteria } });
|
|
203
|
+
|
|
204
|
+
// A RESUMED seed (grade finding on task 1004668). The first run created task A
|
|
205
|
+
// (linked to `ours`) and died; the re-run baselines AFTER that, replays A
|
|
206
|
+
// (`idempotent: true`) and creates B. The plan must count only B — tallying the
|
|
207
|
+
// replay too expects A's link twice and fails a correct resume.
|
|
208
|
+
function tallyCreated(responses) {
|
|
209
|
+
// The reference seed's tally rule, exactly: skip a replayed create.
|
|
210
|
+
return expectedCriterionCounts(responses.filter((r) => !r.idempotent).map((r) => r.crit));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
test('a resumed seed tallies only what THIS run created, and verifies OK', () => {
|
|
214
|
+
const baseline = { ours: 1 }; // A's link, from the first run
|
|
215
|
+
const responses = [{ crit: ['ours'], idempotent: true }, { crit: ['ours'] }];
|
|
216
|
+
const rows = [{ criterion_id: 'ours', linked_tasks: 2 }];
|
|
217
|
+
assert.deepEqual(diffCriterionLinks(tallyCreated(responses), rows, { baseline }), []);
|
|
218
|
+
// The bug the finding names: counting the replay expects 3.
|
|
219
|
+
const naive = expectedCriterionCounts(responses.map((r) => r.crit));
|
|
220
|
+
assert.deepEqual(diffCriterionLinks(naive, rows, { baseline }), [{ criterion_id: 'ours', expected: 3, actual: 2 }]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('a fully re-run seed (every create a replay) plans nothing and verifies OK', () => {
|
|
224
|
+
const responses = [{ crit: ['ours'], idempotent: true }, { crit: ['ours'], idempotent: true }];
|
|
225
|
+
assert.deepEqual(diffCriterionLinks(tallyCreated(responses), [{ criterion_id: 'ours', linked_tasks: 2 }], { baseline: { ours: 2 } }), []);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('the baselined reference seed skips replays when it tallies', () => {
|
|
229
|
+
const src = readFileSync(new URL('../scripts/gds/seed-bv2-ideator-surface-tasks.js', import.meta.url), 'utf8');
|
|
230
|
+
assert.match(src, /baselineSeedCriteria\(/, 'it takes a baseline');
|
|
231
|
+
assert.match(src, /if \(!r\.json\.idempotent\) criterionIdListsPerTask\.push\(t\.crit\);/, 'and tallies only created tasks');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await testAsync('baselineSeedCriteria snapshots the goal before any write', async () => {
|
|
235
|
+
const logs = [];
|
|
236
|
+
const baseline = await baselineSeedCriteria({
|
|
237
|
+
goalId: 1000095, fetchGoal: goalWith(POPULATED), log: (l) => logs.push(l), exit: () => { throw new Error('should not exit'); },
|
|
238
|
+
});
|
|
239
|
+
assert.deepEqual(baseline, { 'old-1': 1, 'old-2': 9, 'old-3': 4, 'old-4': 3, 'old-5': 6, 'old-6': 10 });
|
|
240
|
+
assert.ok(logs.some((l) => l.includes('6 already carrying linked tasks')));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await testAsync('baselineSeedCriteria refuses to seed blind when the goal cannot be read', async () => {
|
|
244
|
+
let exitCode = null; const logs = [];
|
|
245
|
+
await baselineSeedCriteria({
|
|
246
|
+
goalId: 1000095, fetchGoal: async () => ({ ok: false, status: 401, json: null }),
|
|
247
|
+
log: (l) => logs.push(l), exit: (c) => { exitCode = c; },
|
|
248
|
+
});
|
|
249
|
+
assert.equal(exitCode, 1);
|
|
250
|
+
assert.ok(logs.some((l) => l.includes('Refusing to seed blind')));
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await testAsync('a correct seed into a populated goal verifies OK end to end, baseline passed through', async () => {
|
|
254
|
+
const logs = []; let exitCalled = false;
|
|
255
|
+
const baseline = await baselineSeedCriteria({ goalId: 1000095, fetchGoal: goalWith(POPULATED), log: () => {} });
|
|
256
|
+
await verifySeedCriteriaLinks({
|
|
257
|
+
goalId: 1000095,
|
|
258
|
+
expectedCounts: expectedCriterionCounts([['ours'], ['ours']]),
|
|
259
|
+
baseline,
|
|
260
|
+
fetchGoal: goalWith([...POPULATED, { criterion_id: 'ours', linked_tasks: 2 }]),
|
|
261
|
+
log: (l) => logs.push(l),
|
|
262
|
+
exit: () => { exitCalled = true; },
|
|
263
|
+
});
|
|
264
|
+
assert.equal(exitCalled, false, logs.join('\n'));
|
|
265
|
+
assert.ok(logs.some((l) => l.includes('VERIFY OK') && l.includes('pre-seed count')));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await testAsync('without a baseline the OK line says what it did not check', async () => {
|
|
269
|
+
const logs = [];
|
|
270
|
+
await verifySeedCriteriaLinks({
|
|
271
|
+
goalId: 1000095, expectedCounts: { ours: 2 },
|
|
272
|
+
fetchGoal: goalWith([...POPULATED, { criterion_id: 'ours', linked_tasks: 2 }]),
|
|
273
|
+
log: (l) => logs.push(l), exit: () => { throw new Error('should not exit'); },
|
|
274
|
+
});
|
|
275
|
+
assert.ok(logs.some((l) => /were not checked/.test(l) && l.includes('baselineSeedCriteria')));
|
|
276
|
+
});
|
|
277
|
+
|
|
148
278
|
console.log(`\nseed_verify_criteria: ${passed} passed, ${failed} failed`);
|
|
149
279
|
if (failed > 0) process.exit(1);
|