@bongos/core 1.19.497 → 1.19.499
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 +17 -17
- package/.claude/skills/planning-session/SKILL.md +11 -0
- package/docs/copy-inventory.md +34 -23
- package/docs/copy-registry.json +136 -36
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/ideas.css +5 -0
- package/modules/hall-ui/public/ideas.html +57 -0
- package/modules/hall-ui/public/ideas.js +173 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/idea_spark_hall.mjs +136 -2
- package/tests/planning_session_skill.mjs +38 -0
|
@@ -75,6 +75,20 @@
|
|
|
75
75
|
let draftTimer = null;
|
|
76
76
|
let filing = false;
|
|
77
77
|
|
|
78
|
+
// QUICK FILING (task 1003069 / R08, goal 1000071 "Idea routing"). A second,
|
|
79
|
+
// independent capture path on this page: title + kind + optional body + a
|
|
80
|
+
// goal picker fed by the R07 suggester (GET /goals/suggestions). It lives
|
|
81
|
+
// beside the Full Idea form above rather than inside it — a routed capture
|
|
82
|
+
// must never carry grade='full' (routing.js NEVER_ROUTE_GRADE), so the two
|
|
83
|
+
// cannot share one submit. Quick-mode only: hidden while developing a spark.
|
|
84
|
+
const QUICK_SUGGEST_DEBOUNCE_MS = 300;
|
|
85
|
+
let buildingVersionIds = []; // fetched once — the suggester is scoped per version
|
|
86
|
+
let allOpenGoals = []; // the picker's "browse everything" half
|
|
87
|
+
let allGoalsById = new Map(); // for naming the goal a routed capture landed in
|
|
88
|
+
let goalTouchedByUser = false; // once the builder picks, stop clobbering it with fresh suggestions
|
|
89
|
+
let quickSuggestTimer = null;
|
|
90
|
+
let quickFiling = false;
|
|
91
|
+
|
|
78
92
|
// ---------------------------------------------------------------------------
|
|
79
93
|
// draft persistence
|
|
80
94
|
// ---------------------------------------------------------------------------
|
|
@@ -347,6 +361,155 @@
|
|
|
347
361
|
toast('Draft discarded.');
|
|
348
362
|
}
|
|
349
363
|
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// quick filing — kind + goal picker (task 1003069 / R08)
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
const fqTitleEl = () => $('#fq-title');
|
|
369
|
+
const fqKindEl = () => $('#fq-kind');
|
|
370
|
+
const fqBodyEl = () => $('#fq-body');
|
|
371
|
+
const fqGoalEl = () => $('#fq-goal');
|
|
372
|
+
|
|
373
|
+
// The picker's options: "no goal" first (the default — a filer never has to
|
|
374
|
+
// know the goal map, C5), then whatever the suggester found, then every open
|
|
375
|
+
// goal it didn't. Suggested ids are excluded from the second list rather than
|
|
376
|
+
// shown twice.
|
|
377
|
+
function renderGoalOptions(suggested) {
|
|
378
|
+
const sel = fqGoalEl();
|
|
379
|
+
const suggestedIds = new Set((suggested || []).map((g) => String(g.id)));
|
|
380
|
+
const rest = allOpenGoals.filter((g) => !suggestedIds.has(String(g.id)));
|
|
381
|
+
const optionHtml = (g, why) => `<option value="${escapeHtml(String(g.id))}">${escapeHtml(g.title || `goal ${g.id}`)}${why ? ` — matched: ${escapeHtml(why)}` : ''}</option>`;
|
|
382
|
+
let html = `<option value="" selected>No goal (homeless)</option>`;
|
|
383
|
+
if (suggested && suggested.length) {
|
|
384
|
+
html += `<optgroup label="Suggested">${suggested.map((g) => optionHtml(g, (g.matched_terms || []).join(', '))).join('')}</optgroup>`;
|
|
385
|
+
}
|
|
386
|
+
html += `<optgroup label="All open goals">${rest.map((g) => optionHtml(g)).join('')}</optgroup>`;
|
|
387
|
+
sel.innerHTML = html;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// The suggester is ranked per version (GET /goals/suggestions requires one),
|
|
391
|
+
// so a filer whose instance has more than one version building gets both
|
|
392
|
+
// consulted and merged by score. An outage — or no building version at all —
|
|
393
|
+
// degrades to "no suggestions"; the homeless option and the full goal list
|
|
394
|
+
// are still there either way (goal-suggest.js's own fail-open contract).
|
|
395
|
+
async function refreshGoalSuggestions() {
|
|
396
|
+
if (goalTouchedByUser) return; // never yank a manual pick out from under someone
|
|
397
|
+
const title = fqTitleEl().value.trim();
|
|
398
|
+
if (!title || !buildingVersionIds.length) { renderGoalOptions([]); return; }
|
|
399
|
+
const kind = fqKindEl().value;
|
|
400
|
+
try {
|
|
401
|
+
const results = await Promise.all(buildingVersionIds.map((versionId) => api.goals.getGoalsSuggestions({
|
|
402
|
+
query: { version: versionId, title, kind, limit: 5 },
|
|
403
|
+
})));
|
|
404
|
+
const byId = new Map();
|
|
405
|
+
for (const r of results) {
|
|
406
|
+
if (!r.ok || !r.data || !Array.isArray(r.data.suggestions)) continue;
|
|
407
|
+
for (const g of r.data.suggestions) {
|
|
408
|
+
const prev = byId.get(String(g.id));
|
|
409
|
+
if (!prev || g.score > prev.score) byId.set(String(g.id), g);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const merged = Array.from(byId.values()).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
413
|
+
renderGoalOptions(merged);
|
|
414
|
+
} catch (_) {
|
|
415
|
+
renderGoalOptions([]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function onQuickTitleEdit() {
|
|
420
|
+
clearTimeout(quickSuggestTimer);
|
|
421
|
+
quickSuggestTimer = setTimeout(refreshGoalSuggestions, QUICK_SUGGEST_DEBOUNCE_MS);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function showQuickErr(msg) {
|
|
425
|
+
$('#fq-err').textContent = msg || '';
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// The server names why a routed capture was refused (task 1003066's landing
|
|
429
|
+
// gate) or names the refusal code when it doesn't — read it, never re-derive
|
|
430
|
+
// the landing matrix here (routing.js stays the one place that decides).
|
|
431
|
+
function explainQuickFiling(status, data) {
|
|
432
|
+
const err = data && data.error;
|
|
433
|
+
const code = err && (typeof err === 'object' ? err.code : err);
|
|
434
|
+
const details = (err && typeof err === 'object' && err.details) || {};
|
|
435
|
+
if (details.reason) return `Not filed — ${details.reason}`;
|
|
436
|
+
if (code) return `Not filed — ${code}.`;
|
|
437
|
+
if (status === 401 || status === 403) return 'Not filed — your session may have expired. Reload and try again.';
|
|
438
|
+
return `Not filed (${status}). Nothing was saved — try again.`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function fileQuick(ev) {
|
|
442
|
+
ev.preventDefault();
|
|
443
|
+
if (quickFiling) return;
|
|
444
|
+
const title = fqTitleEl().value.trim();
|
|
445
|
+
showQuickErr('');
|
|
446
|
+
$('#fq-result').innerHTML = '';
|
|
447
|
+
if (!title) { showQuickErr('Not filed — a title is required.'); return; }
|
|
448
|
+
quickFiling = true;
|
|
449
|
+
$('#fq-submit').disabled = true;
|
|
450
|
+
|
|
451
|
+
const goalId = fqGoalEl().value;
|
|
452
|
+
const body = { title, body_md: fqBodyEl().value, kind: fqKindEl().value };
|
|
453
|
+
if (goalId) body.goal_id = Number(goalId);
|
|
454
|
+
|
|
455
|
+
const r = await api.inbox.postInbox({ body });
|
|
456
|
+
quickFiling = false;
|
|
457
|
+
$('#fq-submit').disabled = false;
|
|
458
|
+
|
|
459
|
+
if (!r.ok) {
|
|
460
|
+
showQuickErr(explainQuickFiling(r.status, r.data));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// The routing verdict travels on idea.routing; a routed capture also
|
|
465
|
+
// carries the task it became at the top level (routed_task) — see
|
|
466
|
+
// routes/inbox.js. Not routed (no goal picked, or the matrix declined
|
|
467
|
+
// one) is exactly today's inbox behaviour.
|
|
468
|
+
const routedTask = r.data && r.data.routed_task;
|
|
469
|
+
const routing = r.data && r.data.idea && r.data.idea.routing;
|
|
470
|
+
if (routedTask && routedTask.id && routing && routing.routed) {
|
|
471
|
+
const href = window.OTBTaskDetail && typeof window.OTBTaskDetail.href === 'function'
|
|
472
|
+
? window.OTBTaskDetail.href(routedTask.id)
|
|
473
|
+
: `/task/${encodeURIComponent(routedTask.id)}`;
|
|
474
|
+
const goal = allGoalsById.get(String(routing.goal_id));
|
|
475
|
+
const goalName = goal ? goal.title : `goal ${routing.goal_id}`;
|
|
476
|
+
$('#fq-result').innerHTML = `Filed as <a href="${href}">task ${escapeHtml(String(routedTask.id))}</a> under ${escapeHtml(goalName)}.`;
|
|
477
|
+
} else {
|
|
478
|
+
$('#fq-result').textContent = 'Filed to the inbox.';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fqTitleEl().value = '';
|
|
482
|
+
fqBodyEl().value = '';
|
|
483
|
+
fqKindEl().value = 'bug';
|
|
484
|
+
goalTouchedByUser = false;
|
|
485
|
+
renderGoalOptions([]);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function bootQuickComposer() {
|
|
489
|
+
fqKindEl().value = 'bug';
|
|
490
|
+
renderGoalOptions([]);
|
|
491
|
+
fqTitleEl().addEventListener('input', onQuickTitleEdit);
|
|
492
|
+
fqKindEl().addEventListener('change', onQuickTitleEdit);
|
|
493
|
+
fqGoalEl().addEventListener('change', () => { goalTouchedByUser = true; });
|
|
494
|
+
$('#fq-form').addEventListener('submit', fileQuick);
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const [versionsRes, goalsRes] = await Promise.all([
|
|
498
|
+
api.versions.getVersions({}),
|
|
499
|
+
api.goals.getGoals({ query: { status: 'open' } }),
|
|
500
|
+
]);
|
|
501
|
+
buildingVersionIds = (versionsRes.ok && versionsRes.data && Array.isArray(versionsRes.data.versions))
|
|
502
|
+
? versionsRes.data.versions.filter((v) => v.status === 'building').map((v) => v.id)
|
|
503
|
+
: [];
|
|
504
|
+
allOpenGoals = (goalsRes.ok && goalsRes.data && Array.isArray(goalsRes.data.goals)) ? goalsRes.data.goals : [];
|
|
505
|
+
allGoalsById = new Map(allOpenGoals.map((g) => [String(g.id), g]));
|
|
506
|
+
renderGoalOptions([]);
|
|
507
|
+
} catch (_) {
|
|
508
|
+
// The picker degrades to "homeless only" — filing itself never depends
|
|
509
|
+
// on this read succeeding.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
350
513
|
// ---------------------------------------------------------------------------
|
|
351
514
|
// boot
|
|
352
515
|
// ---------------------------------------------------------------------------
|
|
@@ -433,6 +596,16 @@
|
|
|
433
596
|
// A surface that does not depend on the failure must not be hostage to it.
|
|
434
597
|
loadQueue();
|
|
435
598
|
|
|
599
|
+
// Quick filing (R08) is a THIRD independent surface on this page, offered
|
|
600
|
+
// only outside develop mode — developing a spark always produces a Full
|
|
601
|
+
// Idea, never a fresh quick report (see the section's own header comment).
|
|
602
|
+
if (DEVELOP_ID) {
|
|
603
|
+
const scroll = $('#fq-compose-scroll');
|
|
604
|
+
if (scroll) scroll.hidden = true;
|
|
605
|
+
} else {
|
|
606
|
+
bootQuickComposer();
|
|
607
|
+
}
|
|
608
|
+
|
|
436
609
|
const r = await api.inbox.getInboxTemplate({});
|
|
437
610
|
if (!r.ok || !r.data || !r.data.template) {
|
|
438
611
|
$('#fi-sub').textContent = 'Could not load the idea template. Reload, or file from the terminal with capture.js.';
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.499",
|
|
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.499",
|
|
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.499",
|
|
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
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.499'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -48,7 +48,19 @@ const QUEUE = [
|
|
|
48
48
|
{ id: '78', title: 'A shared changelog', status: 'open', grade: 'spark', captured_at: '2026-07-30T00:00:00Z', filed_by_login: null },
|
|
49
49
|
];
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// task 1003069 (R08, goal 1000071) — the quick filing form's fixtures: one
|
|
52
|
+
// building version and two open goals, so the suggester + full-list halves of
|
|
53
|
+
// the picker both have something real to render against.
|
|
54
|
+
const VERSIONS = [{ id: 'BV1', status: 'building' }];
|
|
55
|
+
const GOALS = [
|
|
56
|
+
{ id: '77', title: 'Goal Seventy Seven', version_id: 'BV1' },
|
|
57
|
+
{ id: '88', title: 'Another goal', version_id: 'BV1' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function boot({
|
|
61
|
+
search = '', sparks = QUEUE, count = null, idea = SPARK, ideaOk = true, store = {},
|
|
62
|
+
versions = VERSIONS, goals = GOALS, suggestions = [],
|
|
63
|
+
} = {}) {
|
|
52
64
|
const els = new Map();
|
|
53
65
|
const makeEl = () => ({
|
|
54
66
|
dataset: {}, style: {}, textContent: '', value: '', hidden: false, disabled: false,
|
|
@@ -77,9 +89,36 @@ function boot({ search = '', sparks = QUEUE, count = null, idea = SPARK, ideaOk
|
|
|
77
89
|
getInboxSparks: async () => { calls.push(['sparks', null]); return { status: 200, ok: true, data: { sparks, count: count == null ? sparks.length : count } }; },
|
|
78
90
|
getInboxId: async (a) => { calls.push(['idea', a]); return { status: ideaOk ? 200 : 404, ok: ideaOk, data: ideaOk ? { idea } : null }; },
|
|
79
91
|
postInboxScore: async (a) => { calls.push(['score', a]); return { status: 200, ok: true, data: { score: 60, fields: TEMPLATE.fields.map((f) => ({ key: f.key, substance: 24, floor: 40, ratio: 0.6 })) } }; },
|
|
80
|
-
|
|
92
|
+
// task 1003069 (R08) — a quick capture that named a goal comes back
|
|
93
|
+
// ROUTED (the shape captureIdea/routeIdeaToGoal actually returns:
|
|
94
|
+
// idea.routing + a top-level routed_task); one with no goal_id is a
|
|
95
|
+
// quick capture that stayed homeless; anything else is the existing
|
|
96
|
+
// Full Idea fixture the earlier tests in this file already assert on.
|
|
97
|
+
postInbox: async (a) => {
|
|
98
|
+
calls.push(['capture', a]);
|
|
99
|
+
if (a.body && a.body.goal_id != null) {
|
|
100
|
+
return { status: 201, ok: true, data: {
|
|
101
|
+
idea: { id: '910', routing: { routed: true, goal_id: a.body.goal_id, status: 'ready' } },
|
|
102
|
+
routed_task: { id: '5555', goal_id: a.body.goal_id },
|
|
103
|
+
} };
|
|
104
|
+
}
|
|
105
|
+
if (a.body && a.body.kind && !('grade' in a.body)) {
|
|
106
|
+
return { status: 201, ok: true, data: { idea: { id: '911', routing: { routed: false, reason: 'no_goal' } } } };
|
|
107
|
+
}
|
|
108
|
+
return { status: 201, ok: true, data: { idea: { id: '900', grade: 'full', completeness_score: 80 } } };
|
|
109
|
+
},
|
|
81
110
|
postInboxIdDevelop: async (a) => { calls.push(['develop', a]); return { status: 200, ok: true, data: { idea: { ...idea, grade: 'full', completeness_score: 80, ideator_credit: 5 } } }; },
|
|
82
111
|
},
|
|
112
|
+
// task 1003069 (R08) — the quick-filing goal picker's reads: the building
|
|
113
|
+
// versions (to scope the suggester) and the open goal list (the picker's
|
|
114
|
+
// browse-all half). Overridable per test via boot({ versions, goals, suggestions }).
|
|
115
|
+
versions: {
|
|
116
|
+
getVersions: async () => { calls.push(['versions', null]); return { status: 200, ok: true, data: { versions } }; },
|
|
117
|
+
},
|
|
118
|
+
goals: {
|
|
119
|
+
getGoals: async () => { calls.push(['goals', null]); return { status: 200, ok: true, data: { goals } }; },
|
|
120
|
+
getGoalsSuggestions: async (a) => { calls.push(['suggest', a]); return { status: 200, ok: true, data: { suggestions } }; },
|
|
121
|
+
},
|
|
83
122
|
};
|
|
84
123
|
|
|
85
124
|
const toasts = [];
|
|
@@ -111,6 +150,7 @@ function boot({ search = '', sparks = QUEUE, count = null, idea = SPARK, ideaOk
|
|
|
111
150
|
queueHtml: () => el('fi-queue').innerHTML,
|
|
112
151
|
fill: () => { for (const f of TEMPLATE.fields) el(`fi-f-${f.key}`).value = `an answer for ${f.key} long enough to count`; },
|
|
113
152
|
submit: () => el('fi-form')._handlers.submit({ preventDefault() {} }),
|
|
153
|
+
submitQuick: () => el('fq-form')._handlers.submit({ preventDefault() {} }),
|
|
114
154
|
};
|
|
115
155
|
}
|
|
116
156
|
|
|
@@ -196,6 +236,10 @@ await test('filing in develop mode calls the DEVELOP route with no title and no
|
|
|
196
236
|
assert.ok(call[1].body.field_values, 'it carries the five answers');
|
|
197
237
|
assert.ok(!('title' in call[1].body), 'and no title');
|
|
198
238
|
assert.ok(!('grade' in call[1].body), 'and no grade');
|
|
239
|
+
// task 1003069 (R08) — routing is a QUICK-mode-only affordance; developing a
|
|
240
|
+
// spark always produces a Full Idea (routing.js NEVER_ROUTE_GRADE), so this
|
|
241
|
+
// path must never carry a goal_id even if one happened to be in view.
|
|
242
|
+
assert.ok(!('goal_id' in call[1].body), 'and no goal_id — develop mode never routes');
|
|
199
243
|
assert.ok(!r.calls.some((c) => c[0] === 'capture'), 'and never files a second idea');
|
|
200
244
|
});
|
|
201
245
|
|
|
@@ -259,5 +303,95 @@ await test('the plain composer is unaffected — develop mode is never entered',
|
|
|
259
303
|
assert.equal(call[1].body.title, 'a new idea');
|
|
260
304
|
});
|
|
261
305
|
|
|
306
|
+
// --- quick filing: kind + goal picker (task 1003069 / R08, goal 1000071) ---
|
|
307
|
+
//
|
|
308
|
+
// A second, independent capture path on this same page: title + kind +
|
|
309
|
+
// optional body + a goal picker fed by the R07 suggester. It exists beside the
|
|
310
|
+
// Full Idea form rather than inside it — a routed capture must never carry
|
|
311
|
+
// grade='full' (routing.js NEVER_ROUTE_GRADE) — so quick and full cannot share
|
|
312
|
+
// one submit, and it is offered only outside develop mode (developing a spark
|
|
313
|
+
// always produces a Full Idea from someone else's thought, never a fresh
|
|
314
|
+
// lightweight report).
|
|
315
|
+
|
|
316
|
+
await test('the quick-filing section is offered in plain mode and hidden while developing a spark', async () => {
|
|
317
|
+
const plain = boot();
|
|
318
|
+
await settle(); await settle();
|
|
319
|
+
assert.equal(plain.el('fq-compose-scroll').hidden, false);
|
|
320
|
+
|
|
321
|
+
const developing = boot({ search: '?develop=77' });
|
|
322
|
+
await settle(); await settle(); await settle();
|
|
323
|
+
assert.equal(developing.el('fq-compose-scroll').hidden, true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await test('typing a title debounces a call to the goal suggester, scoped to the building version and the kind control', async () => {
|
|
327
|
+
const r = boot({ suggestions: [{ id: '77', title: 'Goal Seventy Seven', score: 4, matched_terms: ['goal'] }] });
|
|
328
|
+
await settle(); await settle();
|
|
329
|
+
r.el('fq-title').value = 'a goal seventy seven problem';
|
|
330
|
+
r.el('fq-title')._handlers.input();
|
|
331
|
+
await new Promise((res) => setTimeout(res, 400));
|
|
332
|
+
const call = r.calls.find((c) => c[0] === 'suggest');
|
|
333
|
+
assert.ok(call, 'the suggester was called');
|
|
334
|
+
assert.equal(call[1].query.version, 'BV1');
|
|
335
|
+
assert.equal(call[1].query.title, 'a goal seventy seven problem');
|
|
336
|
+
assert.equal(call[1].query.kind, 'bug', 'the kind control feeds the suggester too');
|
|
337
|
+
const html = r.el('fq-goal').innerHTML;
|
|
338
|
+
assert.match(html, /No goal \(homeless\)/);
|
|
339
|
+
assert.match(html, /Suggested/);
|
|
340
|
+
assert.match(html, /Goal Seventy Seven/);
|
|
341
|
+
assert.match(html, /Another goal/, 'the full open-goal list is still offered below the suggestion');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await test('an empty title refuses locally, without hitting the network', async () => {
|
|
345
|
+
const r = boot();
|
|
346
|
+
await settle(); await settle();
|
|
347
|
+
await r.submitQuick();
|
|
348
|
+
await settle();
|
|
349
|
+
assert.ok(!r.calls.some((c) => c[0] === 'capture'), 'no request was made');
|
|
350
|
+
assert.match(r.el('fq-err').textContent, /title is required/);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await test('submitting with no goal picked sends no goal_id — an unrouted response says it went to the inbox', async () => {
|
|
354
|
+
const r = boot();
|
|
355
|
+
await settle(); await settle();
|
|
356
|
+
r.el('fq-title').value = 'a quiet feature idea';
|
|
357
|
+
await r.submitQuick();
|
|
358
|
+
await settle();
|
|
359
|
+
const call = r.calls.find((c) => c[0] === 'capture');
|
|
360
|
+
assert.ok(call, 'POST /inbox was called');
|
|
361
|
+
assert.equal(call[1].body.kind, 'bug', 'bug is the default kind');
|
|
362
|
+
assert.ok(!('goal_id' in call[1].body), 'homeless sends no goal_id at all');
|
|
363
|
+
assert.ok(!('grade' in call[1].body), 'a quick capture carries no grade either');
|
|
364
|
+
assert.equal(r.el('fq-result').textContent, 'Filed to the inbox.');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await test('submitting with a goal picked sends goal_id as a NUMBER, and a routed response renders the task link', async () => {
|
|
368
|
+
const r = boot();
|
|
369
|
+
await settle(); await settle();
|
|
370
|
+
r.el('fq-title').value = 'the widget is broken';
|
|
371
|
+
r.el('fq-kind').value = 'bug';
|
|
372
|
+
r.el('fq-goal').value = '77';
|
|
373
|
+
r.el('fq-goal')._handlers.change();
|
|
374
|
+
await r.submitQuick();
|
|
375
|
+
await settle();
|
|
376
|
+
const call = r.calls.find((c) => c[0] === 'capture');
|
|
377
|
+
assert.ok(call);
|
|
378
|
+
assert.equal(call[1].body.goal_id, 77, 'sent as a number — the option carries a string value');
|
|
379
|
+
const html = r.el('fq-result').innerHTML;
|
|
380
|
+
assert.match(html, /task 5555/);
|
|
381
|
+
assert.match(html, /href="\/task\/5555"/);
|
|
382
|
+
assert.match(html, /Goal Seventy Seven/, 'names the goal the task landed in');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await test('the kind control travels with the filing', async () => {
|
|
386
|
+
const r = boot();
|
|
387
|
+
await settle(); await settle();
|
|
388
|
+
r.el('fq-title').value = 'clean up the queue module';
|
|
389
|
+
r.el('fq-kind').value = 'refactor';
|
|
390
|
+
await r.submitQuick();
|
|
391
|
+
await settle();
|
|
392
|
+
const call = r.calls.find((c) => c[0] === 'capture');
|
|
393
|
+
assert.equal(call[1].body.kind, 'refactor');
|
|
394
|
+
});
|
|
395
|
+
|
|
262
396
|
console.log(`\nidea_spark_hall: ${passed} passed, ${failed} failed`);
|
|
263
397
|
if (failed > 0) process.exit(1);
|
|
@@ -53,5 +53,43 @@ test('step 5 (run the seed script) points at the self-check rather than treating
|
|
|
53
53
|
assert.ok(step5.slice(0, 400).includes('verifySeedCriteriaLinks'), 'step 5 references the self-check by name');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
// --- task 1003075: the mechanics-map subagent becomes a named step ---------
|
|
57
|
+
|
|
58
|
+
test('a named "Map the mechanism" step exists between Phase 1 and Phase 2, in order', () => {
|
|
59
|
+
const phase1 = SKILL.indexOf('### Phase 1 —');
|
|
60
|
+
const mapStep = SKILL.indexOf('### Phase 1.5 — Map the mechanism');
|
|
61
|
+
const phase2 = SKILL.indexOf('### Phase 2 —');
|
|
62
|
+
assert.ok(phase1 !== -1 && mapStep !== -1 && phase2 !== -1, 'all three headings must exist');
|
|
63
|
+
assert.ok(phase1 < mapStep && mapStep < phase2, 'Phase 1.5 must sit between Phase 1 and Phase 2');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the mechanics-map step names its input, its output artifact, and where the output is consumed', () => {
|
|
67
|
+
const start = NORM.indexOf('Phase 1.5 — Map the mechanism');
|
|
68
|
+
const end = NORM.indexOf('Phase 2 — Criteria', start);
|
|
69
|
+
const section = NORM.slice(start, end === -1 ? undefined : end);
|
|
70
|
+
assert.ok(section.length > 0, 'the Phase 1.5 section must be non-empty');
|
|
71
|
+
assert.ok(/\bInput\b/.test(section), 'names its input');
|
|
72
|
+
assert.ok(section.includes('mechanics map'), 'names its output artifact (the mechanics map)');
|
|
73
|
+
assert.ok(/\bConsumed by\b/.test(section), 'names where the output is consumed');
|
|
74
|
+
assert.ok(section.includes('Phase 2') && section.includes('Phase 3'), 'names the specific later steps that consume it');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the mechanics-map step names the subagent mechanism and the retro precedent that motivated it', () => {
|
|
78
|
+
const start = NORM.indexOf('Phase 1.5 — Map the mechanism');
|
|
79
|
+
const end = NORM.indexOf('Phase 2 — Criteria', start);
|
|
80
|
+
const section = NORM.slice(start, end === -1 ? undefined : end);
|
|
81
|
+
assert.ok(section.includes('subagent'), 'names a subagent as the mechanism');
|
|
82
|
+
assert.ok(section.includes('goal 1000071'), 'cites the idea-routing session as the precedent');
|
|
83
|
+
assert.ok(section.includes('task 1003075'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('the step is scoped to process-change sessions, not every planning session', () => {
|
|
87
|
+
const start = NORM.indexOf('Phase 1.5 — Map the mechanism');
|
|
88
|
+
const end = NORM.indexOf('Phase 2 — Criteria', start);
|
|
89
|
+
const section = NORM.slice(start, end === -1 ? undefined : end);
|
|
90
|
+
assert.ok(/process-change/i.test(section));
|
|
91
|
+
assert.ok(/skip/i.test(section), 'says explicitly when to skip it');
|
|
92
|
+
});
|
|
93
|
+
|
|
56
94
|
console.log(`\nplanning_session_skill: ${passed} passed, ${failed} failed`);
|
|
57
95
|
if (failed > 0) process.exit(1);
|