@bongos/core 1.19.294 → 1.19.296
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 +37 -22
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +9 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +188 -3
- package/docs/api-reference.md +5 -2
- package/docs/file-map.md +1 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/ideas/inbox.js +88 -0
- package/modules/ideas/routes/inbox.js +150 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/spark.js +155 -0
- package/src/bongos/route-rank-check.js +9 -0
- package/src/module-api.js +5 -1
- package/tests/idea_spark_mark.mjs +231 -0
- package/tests/idea_spark_queue.mjs +225 -0
package/modules/ideas/inbox.js
CHANGED
|
@@ -782,6 +782,91 @@ async function countRoutedAwaitingNod(deps = {}) {
|
|
|
782
782
|
return rows[0] ? rows[0].n : 0;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
// -------------------------------------------------------------------------
|
|
786
|
+
// The spark queue (task 1002921 / BV1.R145, goal 1000062)
|
|
787
|
+
//
|
|
788
|
+
// "What Big Idea Sparks are awaiting development?" — the ideator craft's pickup
|
|
789
|
+
// surface, and the first place in this codebase where one craft sources work
|
|
790
|
+
// from another craft's output.
|
|
791
|
+
//
|
|
792
|
+
// THE PREDICATE IS grade='spark' AND status='open', AND THAT IS THE WHOLE
|
|
793
|
+
// QUEUE. There is deliberately no `developed` flag and no separate queue table:
|
|
794
|
+
// R146 develops a spark into a Full Idea IN PLACE on the same idea_inbox row
|
|
795
|
+
// (same id, same captured_by), which flips its grade spark→full — so a developed
|
|
796
|
+
// spark leaves this queue as a CONSEQUENCE of being developed, not because
|
|
797
|
+
// anything remembered to remove it. A second source of truth for "still
|
|
798
|
+
// waiting" is a thing that can disagree with the grade; this cannot.
|
|
799
|
+
//
|
|
800
|
+
// AN INNER JOIN, WHICH INVERTS THIS MODULE'S USUAL RULE ON PURPOSE. Every other
|
|
801
|
+
// idea read LEFT JOINs ideas_grades and COALESCEs to 'quick', because an
|
|
802
|
+
// un-graded idea must still appear. Here the opposite is correct and safe:
|
|
803
|
+
// grade='spark' can never be implicit — absence of a row means quick — so a
|
|
804
|
+
// spark ALWAYS has a real row, and starting from ideas_grades lets
|
|
805
|
+
// ideas_grades_grade_score_idx (ideas_001, written for this very read) drive the
|
|
806
|
+
// scan instead of filtering a full idea_inbox pass afterwards.
|
|
807
|
+
//
|
|
808
|
+
// ORDERING — "newest-or-best first", and only the newest half is real. A spark
|
|
809
|
+
// carries no completeness score by construction (it has no template fields to
|
|
810
|
+
// score), so there is no "best" signal to sort by: upvotes live on builders as
|
|
811
|
+
// karma, not per-idea, and inventing a quality metric for an undeveloped thought
|
|
812
|
+
// is exactly the guess this queue should not be making. captured_at DESC, id
|
|
813
|
+
// DESC as a stable tiebreak for same-timestamp rows. When a real per-idea
|
|
814
|
+
// signal exists, this ORDER BY is the one line that changes.
|
|
815
|
+
//
|
|
816
|
+
// The skill-friction visibility gate (V3.R70) is applied here too, on the same
|
|
817
|
+
// title-prefix rule listOpenIdeas uses. A routine-filed friction row is unlikely
|
|
818
|
+
// to ever be sparked (marking requires an author), but the two idea listings
|
|
819
|
+
// agreeing by construction beats them agreeing by accident.
|
|
820
|
+
function sparkQueueWhere(viewerRank, params) {
|
|
821
|
+
const where = [`g.grade = 'spark'`, `i.status = 'open'`];
|
|
822
|
+
if (!rankCanSeeSkillFriction(viewerRank)) {
|
|
823
|
+
params.push(`${SKILL_FRICTION_TAG}%`);
|
|
824
|
+
where.push(`i.title NOT LIKE $${params.length}`);
|
|
825
|
+
}
|
|
826
|
+
return where;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function listSparkQueue({ limit = 50, viewerRank = null } = {}, deps = {}) {
|
|
830
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
831
|
+
const params = [];
|
|
832
|
+
const where = sparkQueueWhere(viewerRank, params);
|
|
833
|
+
params.push(limit);
|
|
834
|
+
const { rows } = await activePool.query(
|
|
835
|
+
// `b.github_login` because attribution is the POINT of this surface: an
|
|
836
|
+
// ideator picking work up should see whose thought they are developing, and
|
|
837
|
+
// R146 has to preserve that name. LEFT JOIN — a builder row can be missing.
|
|
838
|
+
`SELECT ${IDEA_COLS_Q}, ${GRADE_COLS_Q},
|
|
839
|
+
b.github_login AS filed_by_login
|
|
840
|
+
FROM ideas_grades g
|
|
841
|
+
JOIN idea_inbox i ON i.id = g.idea_id
|
|
842
|
+
LEFT JOIN builders b ON b.id = i.captured_by
|
|
843
|
+
WHERE ${where.join(' AND ')}
|
|
844
|
+
ORDER BY i.captured_at DESC, i.id DESC
|
|
845
|
+
LIMIT $${params.length}`,
|
|
846
|
+
params
|
|
847
|
+
);
|
|
848
|
+
return rows;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// The count behind the queue's pill — the SAME predicate as the list (the
|
|
852
|
+
// countRoutedAwaitingNod / countOpenIdeas precedent), so the number a viewer is
|
|
853
|
+
// shown can never disagree with the rows they can actually see. It deliberately
|
|
854
|
+
// takes viewerRank too: a count that ignored the visibility gate would tell a
|
|
855
|
+
// Xenos there are sparks they cannot find.
|
|
856
|
+
async function countSparkQueue({ viewerRank = null } = {}, deps = {}) {
|
|
857
|
+
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
858
|
+
const params = [];
|
|
859
|
+
const where = sparkQueueWhere(viewerRank, params);
|
|
860
|
+
const { rows } = await activePool.query(
|
|
861
|
+
`SELECT COUNT(*)::int AS n
|
|
862
|
+
FROM ideas_grades g
|
|
863
|
+
JOIN idea_inbox i ON i.id = g.idea_id
|
|
864
|
+
WHERE ${where.join(' AND ')}`,
|
|
865
|
+
params
|
|
866
|
+
);
|
|
867
|
+
return rows[0] ? rows[0].n : 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
785
870
|
// -------------------------------------------------------------------------
|
|
786
871
|
// Grade writes (task 1002916 / BV1.R140)
|
|
787
872
|
//
|
|
@@ -1109,6 +1194,9 @@ module.exports = {
|
|
|
1109
1194
|
// the nod queue (task 1003066, BV1.R05)
|
|
1110
1195
|
listRoutedAwaitingNod,
|
|
1111
1196
|
countRoutedAwaitingNod,
|
|
1197
|
+
// the spark queue (task 1002921, BV1.R145) — sparks awaiting development
|
|
1198
|
+
listSparkQueue,
|
|
1199
|
+
countSparkQueue,
|
|
1112
1200
|
// criterion-proposal ratification (V3.R31 / #252)
|
|
1113
1201
|
ratifyCriterionProposal,
|
|
1114
1202
|
parseProposalBody,
|
|
@@ -312,6 +312,43 @@ module.exports = function buildInboxRouter() {
|
|
|
312
312
|
}
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
|
|
316
|
+
// GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062).
|
|
317
|
+
//
|
|
318
|
+
// Big Idea Sparks awaiting development: the ideator craft's pickup surface,
|
|
319
|
+
// and the first place one craft sources work from another craft's output.
|
|
320
|
+
//
|
|
321
|
+
// READABLE BY ANY BUILDER, by design and by the done-when. A pickup surface
|
|
322
|
+
// that only trusted builders could read would defeat its own purpose — the
|
|
323
|
+
// whole point is that someone OTHER than the author comes along and develops
|
|
324
|
+
// the thought. The author's consent is already recorded (the R144 mark is what
|
|
325
|
+
// put the row here), so no further gate belongs on the read. Contrast
|
|
326
|
+
// /inbox/awaiting-nod directly above, which IS gated: that queue exists to be
|
|
327
|
+
// ACTED on by a triager, this one to be BROWSED by anyone.
|
|
328
|
+
//
|
|
329
|
+
// DECLARED BEFORE `GET /inbox/:id` DELIBERATELY — and this ordering is
|
|
330
|
+
// load-bearing, not stylistic. Express matches in declaration order, so with
|
|
331
|
+
// the routes the other way round `/inbox/sparks` binds `:id = 'sparks'` and
|
|
332
|
+
// parseId turns the whole queue into a 400 bad_id. Same reason
|
|
333
|
+
// /inbox/awaiting-nod sits above it. tests/idea_spark_queue.mjs pins the
|
|
334
|
+
// relative order so a future insertion cannot quietly re-shadow it.
|
|
335
|
+
//
|
|
336
|
+
// rank: any-builder — a pickup surface is public to builders (the R144 mark is
|
|
337
|
+
// the consent). Deliberately NOT pinned in route-rank-check: a read with no
|
|
338
|
+
// gate to protect has no downgrade to catch, and pinning it would only assert
|
|
339
|
+
// that a read stays open.
|
|
340
|
+
router.get('/inbox/sparks', api.requireBuilder, api.asyncHandler('GET /inbox/sparks', async (req, res) => {
|
|
341
|
+
const limitRaw = Number(req.query.limit);
|
|
342
|
+
const limit = Number.isInteger(limitRaw) && limitRaw > 0 && limitRaw <= 100 ? limitRaw : 50;
|
|
343
|
+
// Both halves take the viewer's rank so the pill can never claim sparks the
|
|
344
|
+
// list will not show (the countRoutedAwaitingNod precedent).
|
|
345
|
+
const viewerRank = req.builder && req.builder.rank;
|
|
346
|
+
const [sparks, count] = await Promise.all([
|
|
347
|
+
inbox.listSparkQueue({ limit, viewerRank }),
|
|
348
|
+
inbox.countSparkQueue({ viewerRank }),
|
|
349
|
+
]);
|
|
350
|
+
return res.json({ sparks, count });
|
|
351
|
+
}, { errorCode: 'spark_queue_failed' }));
|
|
315
352
|
// rank: any-builder — single idea read for the "#NNN" deep-link resolver
|
|
316
353
|
// (#1047). Mirrors GET /blockers/:id. The hall resolves /#/idea/N by id, but
|
|
317
354
|
// the /inbox LIST returns only the open-first-50, so a closed/older idea ref
|
|
@@ -443,6 +480,119 @@ module.exports = function buildInboxRouter() {
|
|
|
443
480
|
}
|
|
444
481
|
});
|
|
445
482
|
|
|
483
|
+
// -----------------------------------------------------------------------
|
|
484
|
+
// The Big Idea Spark mark (task 1002920 / BV1.R144, goal 1000062)
|
|
485
|
+
//
|
|
486
|
+
// "I have the spark but not the full picture — someone develop this."
|
|
487
|
+
//
|
|
488
|
+
// AUTHOR-ONLY, AND THAT IS THE WHOLE POINT. The owner's decision: this mark IS
|
|
489
|
+
// the consent that makes cross-builder development fair. Without it a Quick
|
|
490
|
+
// Idea is simply NOT available for someone else to develop — a quick idea is
|
|
491
|
+
// for turning a thought into a task fast, and it is explicitly *not* an
|
|
492
|
+
// unfinished Full Idea waiting to be finished by a stranger. So the gate is
|
|
493
|
+
// not "who is senior enough", it is "whose thought is this": enforced
|
|
494
|
+
// in-handler against `captured_by`, the same ownership posture as
|
|
495
|
+
// POST /inbox/:id/resubmit above and PATCH /inbox/:id/body below.
|
|
496
|
+
//
|
|
497
|
+
// A MARK, MODELLED AS A RESOURCE: POST puts it on, DELETE takes it off. The
|
|
498
|
+
// done-when requires the mark be REVERSIBLE, and un-sparking is the plain
|
|
499
|
+
// reading of that — an author who marked a thought as needing development may
|
|
500
|
+
// decide they had the full picture after all. Un-sparking writes an EXPLICIT
|
|
501
|
+
// grade='quick' row, which the R140 design states reads identically to having
|
|
502
|
+
// no row at all, so a reversal leaves no residue a reader has to interpret.
|
|
503
|
+
//
|
|
504
|
+
// ONLY A QUICK IDEA CAN BE SPARKED. A Full Idea already carries the developed
|
|
505
|
+
// picture (the five template fields + a score); asking for it to be developed
|
|
506
|
+
// is a contradiction, and sparking it would hand setIdeaGrade a `spark` write
|
|
507
|
+
// whose empty fieldValues would WIPE that work — exactly the stale/lost-field
|
|
508
|
+
// hazard setIdeaGrade's whole-row contract warns about. Refused 409, before
|
|
509
|
+
// any write.
|
|
510
|
+
//
|
|
511
|
+
// Both directions are IDEMPOTENT: sparking a spark, or un-sparking a quick,
|
|
512
|
+
// reports `unchanged` at 200 rather than erroring. A double-click on the R154
|
|
513
|
+
// hall control is not a failure, and the caller's intent is already satisfied.
|
|
514
|
+
//
|
|
515
|
+
// rank: any-builder (requireBuilder) + the in-handler author gate.
|
|
516
|
+
// Pinned in route-rank-check.MODULE_EXPECTED_RANKS['ideas'].
|
|
517
|
+
|
|
518
|
+
// Shared by both directions: load the idea, prove ownership, and refuse a
|
|
519
|
+
// grade this mark has no business touching. Returns the idea, or null when it
|
|
520
|
+
// has already answered the request (so the caller just returns).
|
|
521
|
+
async function loadSparkable(req, res, { requireOpen }) {
|
|
522
|
+
const id = parseId(req, res, { code: 'bad_id', positiveInt: true });
|
|
523
|
+
if (id === null) return null;
|
|
524
|
+
const idea = await inbox.getIdea(id);
|
|
525
|
+
if (!idea) { res.fail('idea_not_found', 404); return null; }
|
|
526
|
+
// Authority first, and it leaks nothing: any builder can already read this
|
|
527
|
+
// idea from the inbox, so a 403 here reveals only that they are not its author.
|
|
528
|
+
if (idea.captured_by == null || String(idea.captured_by) !== String(req.builder.id)) {
|
|
529
|
+
res.fail('not_your_idea', 403, {
|
|
530
|
+
reason: 'the spark mark is the AUTHOR\'s consent to have their idea developed; only they can give or withdraw it',
|
|
531
|
+
});
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
if (idea.grade === 'full') {
|
|
535
|
+
res.fail('not_a_quick_idea', 409, {
|
|
536
|
+
grade: idea.grade,
|
|
537
|
+
reason: 'a Full Idea already carries the developed picture; the spark mark asks for one',
|
|
538
|
+
});
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
// Only MARKING requires an open idea. A promoted or discarded idea is not
|
|
542
|
+
// awaiting development, and admitting it would seed the R145 spark queue
|
|
543
|
+
// with work nobody can pick up. WITHDRAWING the mark stays legal at any
|
|
544
|
+
// status — clearing a stale mark is housekeeping, and blocking it would be
|
|
545
|
+
// the one way to leave a permanent spark on a dead idea.
|
|
546
|
+
if (requireOpen && idea.status !== 'open') {
|
|
547
|
+
res.fail('idea_not_open', 409, {
|
|
548
|
+
status: idea.status,
|
|
549
|
+
reason: 'only an open idea can be marked as awaiting development',
|
|
550
|
+
});
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
return idea;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Mark a Quick Idea as a Big Idea Spark — "I have the spark but not the full
|
|
557
|
+
// picture, someone develop this". AUTHOR-ONLY: the mark is the author's consent
|
|
558
|
+
// to have their idea developed, so only they can set it. A Full Idea is refused
|
|
559
|
+
// (409) and a non-open idea cannot be newly marked; re-marking is a 200 no-op.
|
|
560
|
+
router.post('/inbox/:id/spark', api.requireBuilder, api.asyncHandler('POST /inbox/:id/spark', async (req, res) => {
|
|
561
|
+
const idea = await loadSparkable(req, res, { requireOpen: true });
|
|
562
|
+
if (!idea) return;
|
|
563
|
+
if (idea.grade === 'spark') {
|
|
564
|
+
return res.json({ idea, sparked: true, unchanged: true });
|
|
565
|
+
}
|
|
566
|
+
// grade only — a spark carries no template fields and no score (the R140
|
|
567
|
+
// migration's own shape: field_values is for a Full Idea). setIdeaGrade's
|
|
568
|
+
// defaults say exactly that, so they are left to speak for themselves.
|
|
569
|
+
const row = await inbox.setIdeaGrade(idea.id, { grade: 'spark' });
|
|
570
|
+
return res.json({
|
|
571
|
+
idea: { ...idea, grade: row.grade, template_key: row.template_key, field_values: row.field_values, completeness_score: row.completeness_score },
|
|
572
|
+
sparked: true,
|
|
573
|
+
unchanged: false,
|
|
574
|
+
});
|
|
575
|
+
}, { errorCode: 'idea_spark_failed' }));
|
|
576
|
+
|
|
577
|
+
// Withdraw the Big Idea Spark mark — the reversibility half of R144. AUTHOR-ONLY
|
|
578
|
+
// like the mark itself, and legal at ANY status so a stale mark on a promoted or
|
|
579
|
+
// discarded idea can always be cleared. Writes an explicit grade='quick' row,
|
|
580
|
+
// which reads identically to no grade row at all. Un-marking a quick idea is a
|
|
581
|
+
// 200 no-op; a Full Idea is refused (409) — it is not a withdrawn spark.
|
|
582
|
+
router.delete('/inbox/:id/spark', api.requireBuilder, api.asyncHandler('DELETE /inbox/:id/spark', async (req, res) => {
|
|
583
|
+
const idea = await loadSparkable(req, res, { requireOpen: false });
|
|
584
|
+
if (!idea) return;
|
|
585
|
+
if (idea.grade !== 'spark') {
|
|
586
|
+
return res.json({ idea, sparked: false, unchanged: true });
|
|
587
|
+
}
|
|
588
|
+
const row = await inbox.setIdeaGrade(idea.id, { grade: 'quick' });
|
|
589
|
+
return res.json({
|
|
590
|
+
idea: { ...idea, grade: row.grade, template_key: row.template_key, field_values: row.field_values, completeness_score: row.completeness_score },
|
|
591
|
+
sparked: false,
|
|
592
|
+
unchanged: false,
|
|
593
|
+
});
|
|
594
|
+
}, { errorCode: 'idea_unspark_failed' }));
|
|
595
|
+
|
|
446
596
|
// Metic+ (#360 / ADR 0018): idea triage (promote/discard/merge) shapes the
|
|
447
597
|
// backlog. Criterion C2 assigns triage to Metic, so this widened from
|
|
448
598
|
// archon-only to metic+archon when the three-rank model went live. Any
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.296",
|
|
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.296",
|
|
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.296",
|
|
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",
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Mark one of YOUR OWN quick ideas as a Big Idea Spark — "I have the spark but
|
|
3
|
+
// not the full picture, someone develop this" (task 1002920 / BV1.R144, goal
|
|
4
|
+
// 1000062).
|
|
5
|
+
//
|
|
6
|
+
// The mark is the CONSENT that makes cross-builder development fair: until an
|
|
7
|
+
// author sets it, their quick idea is not available for someone else to develop.
|
|
8
|
+
// So this command only ever works on ideas you filed — the server enforces that
|
|
9
|
+
// against idea_inbox.captured_by, and no rank can override it.
|
|
10
|
+
//
|
|
11
|
+
// It exists as its own command rather than a flag on capture.js because sparking
|
|
12
|
+
// is an act on an EXISTING idea, not a way to file one (capture refuses
|
|
13
|
+
// grade='spark' for the same reason). The builders-hall control is R154; this is
|
|
14
|
+
// the terminal path, and the two hit the same route.
|
|
15
|
+
//
|
|
16
|
+
// --list is the other side of the same idea: the SPARK QUEUE (task 1002921 /
|
|
17
|
+
// BV1.R145) — what other builders have marked as needing development. It lives
|
|
18
|
+
// here rather than in its own script because "mark it" and "see what is marked"
|
|
19
|
+
// are one mental model, and a queue nobody can read is not a pickup surface. The
|
|
20
|
+
// queue is readable by ANY builder: the author's mark is already the consent.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/gds/spark.js --list # the spark queue: what awaits development
|
|
24
|
+
// node scripts/gds/spark.js --list --limit 10
|
|
25
|
+
// node scripts/gds/spark.js 123 # mark idea 123 as a spark
|
|
26
|
+
// node scripts/gds/spark.js 123 --undo # withdraw the mark (back to quick)
|
|
27
|
+
//
|
|
28
|
+
// Exit codes:
|
|
29
|
+
// 0 the idea now carries the grade you asked for (including when it already did)
|
|
30
|
+
// 1 api / network error, or the server refused (not yours / not a quick idea)
|
|
31
|
+
// 2 bad invocation
|
|
32
|
+
|
|
33
|
+
const { cliClient, requireSession, hasFlag, arg, hallBase } = require('./cli-lib');
|
|
34
|
+
|
|
35
|
+
// The server's refusals, in the ideator's own words. Anything unmapped falls
|
|
36
|
+
// through to the raw envelope — a new server-side rule should be visible, not
|
|
37
|
+
// swallowed by a generic "failed".
|
|
38
|
+
const REFUSALS = {
|
|
39
|
+
not_your_idea: (d) =>
|
|
40
|
+
'That idea is not yours to mark. The spark mark is the author\'s consent to have their\n'
|
|
41
|
+
+ 'idea developed, so only the builder who filed it can give or withdraw it.',
|
|
42
|
+
not_a_quick_idea: (d) =>
|
|
43
|
+
`That idea is already a Full Idea (grade=${d.grade || 'full'}). It carries the developed\n`
|
|
44
|
+
+ 'picture the spark mark asks for — there is nothing to hand off.',
|
|
45
|
+
idea_not_open: (d) =>
|
|
46
|
+
`That idea is ${d.status || 'no longer open'}, so it is not awaiting development.\n`
|
|
47
|
+
+ '(You can still withdraw an existing mark with --undo.)',
|
|
48
|
+
idea_not_found: () => 'No idea with that id.',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// The envelope is ADR 0116's { error: { code, message, details } } — the extras a
|
|
52
|
+
// route passes as res.fail's third argument land under `details`, NOT spread onto
|
|
53
|
+
// `error`. Reading them off the wrong level is a silent "undefined" in the
|
|
54
|
+
// message rather than a crash, so tests/idea_spark_mark.mjs pins explain()
|
|
55
|
+
// against the real envelope() builder.
|
|
56
|
+
function explain(status, data) {
|
|
57
|
+
const err = data && data.error;
|
|
58
|
+
const code = err && typeof err === 'object' ? err.code : err;
|
|
59
|
+
const detail = (err && typeof err === 'object' && err.details) || {};
|
|
60
|
+
const say = typeof code === 'string' ? REFUSALS[code] : null;
|
|
61
|
+
if (say) return say(detail);
|
|
62
|
+
let rendered;
|
|
63
|
+
try { rendered = JSON.stringify(data, null, 2); } catch { rendered = String(data); }
|
|
64
|
+
return `spark failed (${status}): ${rendered}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Render the queue. Kept separate from the fetch so it is testable without a
|
|
68
|
+
// session, and so the empty case reads as an answer ("nothing is waiting")
|
|
69
|
+
// rather than as a blank screen that looks like a failure.
|
|
70
|
+
function renderQueue({ sparks, count }) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
if (!sparks || sparks.length === 0) {
|
|
73
|
+
lines.push('No sparks are awaiting development.');
|
|
74
|
+
lines.push('A spark is a quick idea whose AUTHOR marked it as needing development —');
|
|
75
|
+
lines.push('mark one of your own with: node scripts/gds/spark.js <idea-id>');
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
const shown = sparks.length;
|
|
79
|
+
const total = Number.isInteger(count) ? count : shown;
|
|
80
|
+
lines.push(`${total} spark${total === 1 ? '' : 's'} awaiting development${shown < total ? ` (showing ${shown})` : ''}:`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
for (const s of sparks) {
|
|
83
|
+
// The hall link, never a bare "#NNN" — that autolinks to a GitHub issue and
|
|
84
|
+
// 404s, since our idea ids are not GitHub's (the CLAUDE.md refs rule).
|
|
85
|
+
lines.push(` ${hallBase()}/#/idea/${s.id}`);
|
|
86
|
+
lines.push(` ${s.title}`);
|
|
87
|
+
const who = s.filed_by_login ? `@${s.filed_by_login}` : 'an unnamed builder';
|
|
88
|
+
lines.push(` had by ${who}${s.captured_at ? ` · ${String(s.captured_at).slice(0, 10)}` : ''}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
}
|
|
91
|
+
lines.push('Develop one into a Full Idea and the author keeps the attribution.');
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function listQueue() {
|
|
96
|
+
const api = await cliClient();
|
|
97
|
+
const limitRaw = arg('--limit');
|
|
98
|
+
const limit = limitRaw != null && /^[0-9]+$/.test(limitRaw) ? Number(limitRaw) : null;
|
|
99
|
+
const r = await api.inbox.getInboxSparks(limit ? { query: { limit } } : {});
|
|
100
|
+
if (!r.ok) {
|
|
101
|
+
console.error(explain(r.status, r.data));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
console.log(renderQueue(r.data || {}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
await requireSession();
|
|
109
|
+
|
|
110
|
+
if (hasFlag('--list')) return listQueue();
|
|
111
|
+
|
|
112
|
+
const positional = process.argv.slice(2).filter((a) => !a.startsWith('-'));
|
|
113
|
+
const id = Number(positional[0]);
|
|
114
|
+
const undo = hasFlag('--undo');
|
|
115
|
+
|
|
116
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
117
|
+
console.error('usage: node scripts/gds/spark.js <idea-id> [--undo]');
|
|
118
|
+
console.error(' node scripts/gds/spark.js --list [--limit N]');
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const api = await cliClient();
|
|
123
|
+
const r = undo
|
|
124
|
+
? await api.inbox.deleteInboxIdSpark({ id })
|
|
125
|
+
: await api.inbox.postInboxIdSpark({ id, body: {} });
|
|
126
|
+
|
|
127
|
+
if (!r.ok) {
|
|
128
|
+
console.error(explain(r.status, r.data));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { idea, sparked, unchanged } = r.data;
|
|
133
|
+
const title = idea && idea.title ? ` — ${idea.title}` : '';
|
|
134
|
+
if (unchanged) {
|
|
135
|
+
console.log(`Idea ${id} was already ${sparked ? 'a spark' : 'a quick idea'}${title}. Nothing changed.`);
|
|
136
|
+
} else if (sparked) {
|
|
137
|
+
console.log(`Idea ${id} is now a Big Idea Spark${title}.`);
|
|
138
|
+
console.log('Another builder can develop it into a Full Idea; you keep the attribution.');
|
|
139
|
+
} else {
|
|
140
|
+
console.log(`Idea ${id} is a quick idea again${title}. The mark is withdrawn.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// explain() and renderQueue() are exported because both are pinned by tests —
|
|
145
|
+
// explain against the real envelope shape, renderQueue for the empty case and the
|
|
146
|
+
// hall-link form. REFUSALS stays private: it is the message table, reachable
|
|
147
|
+
// through explain(), and a third export would be dead surface.
|
|
148
|
+
module.exports = { explain, renderQueue };
|
|
149
|
+
|
|
150
|
+
if (require.main === module) {
|
|
151
|
+
main().catch((err) => {
|
|
152
|
+
console.error('fatal:', err);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -416,6 +416,15 @@ const MODULE_EXPECTED_RANKS = {
|
|
|
416
416
|
// requireBuilder + an in-handler author|triage gate. Deliberately a separate
|
|
417
417
|
// route from the triage-gated PATCH /inbox/:id, which keeps its own rank.
|
|
418
418
|
'PATCH /inbox/:id/body': 'any-builder',
|
|
419
|
+
// task 1002920 (BV1.R144): the Big Idea Spark mark is AUTHOR-ONLY, which is a
|
|
420
|
+
// narrower rule than any rank expresses — the gate is "whose thought is this",
|
|
421
|
+
// enforced in-handler against captured_by. Pinned 'any-builder' for the same
|
|
422
|
+
// reason POST /goals/:id/members is: the pin's job here is to catch an
|
|
423
|
+
// accidental TIGHTENING to a rank floor (which would lock authors out of their
|
|
424
|
+
// own ideas and break the consent the mark exists to record) as loudly as it
|
|
425
|
+
// catches a gate being dropped.
|
|
426
|
+
'POST /inbox/:id/spark': 'any-builder',
|
|
427
|
+
'DELETE /inbox/:id/spark': 'any-builder',
|
|
419
428
|
'POST /inbox/:id/ratify': 'perm:idea.ratify',
|
|
420
429
|
},
|
|
421
430
|
// Cross-builder memory READS — the counterpart to the BFG cross-builder WRITE the
|
package/src/module-api.js
CHANGED
|
@@ -1015,8 +1015,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1015
1015
|
// landed since 1.19.292 with no explicit bump. run 32586174253. (task 1002620)
|
|
1016
1016
|
// 1.19.294 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1017
1017
|
// landed since 1.19.293 with no explicit bump. run 32588977219. (task 1002620)
|
|
1018
|
+
// 1.19.295 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1019
|
+
// landed since 1.19.294 with no explicit bump. run 32590508145. (task 1002620)
|
|
1020
|
+
// 1.19.296 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1021
|
+
// landed since 1.19.295 with no explicit bump. run 32591155887. (task 1002620)
|
|
1018
1022
|
// ---------------------------------------------------------------------------
|
|
1019
|
-
const CORE_VERSION = '1.19.
|
|
1023
|
+
const CORE_VERSION = '1.19.296'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1020
1024
|
|
|
1021
1025
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1022
1026
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|