@bongos/core 1.19.275 → 1.19.277
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 -27
- package/.claude/skills/builder-sequence/SKILL.md +16 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +55 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +6 -4
- package/docs/copy-registry.json +23 -5
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/goal-inbox.js +81 -3
- package/modules/ideas/inbox.js +119 -12
- package/modules/ideas/migrations/ideas_004_lineage_route_index.sql +37 -0
- package/modules/ideas/routes/inbox.js +44 -0
- package/modules/ideas/routing.js +111 -0
- package/modules/lifecycle/ship-card.js +71 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/idea_routing.mjs +288 -1
- package/tests/session_boundary_handoff.mjs +184 -0
package/tests/idea_routing.mjs
CHANGED
|
@@ -32,7 +32,7 @@ process.env.NODE_ENV = 'test';
|
|
|
32
32
|
|
|
33
33
|
const require = createRequire(import.meta.url);
|
|
34
34
|
const routing = require('../modules/ideas/routing.js');
|
|
35
|
-
const { decideLanding, routedPriority, DEFAULT_ROUTED_PRIORITY } = routing;
|
|
35
|
+
const { decideLanding, authorizeRoutedLanding, routedPriority, DEFAULT_ROUTED_PRIORITY, PROTECTED_SCOPE_FLOOR } = routing;
|
|
36
36
|
|
|
37
37
|
// The lifecycle port is how the ideas module reaches createTask (never an
|
|
38
38
|
// import — the one-way module rule, ADR 0083). Register a stub provider, the
|
|
@@ -568,4 +568,291 @@ await test('route: NO goal_id is still the plain old capture — 201, no task',
|
|
|
568
568
|
assert.ok('kind_auto' in r.body);
|
|
569
569
|
});
|
|
570
570
|
|
|
571
|
+
// =========================================================================
|
|
572
|
+
// 7. R05 — ENFORCEMENT: the rank floor and the scope wall (task 1003066)
|
|
573
|
+
//
|
|
574
|
+
// R01 decided the queue status. This is the half that BINDS: a routed task
|
|
575
|
+
// declares no touches[], so createTask's ADR 0084/0085 floor derives from an
|
|
576
|
+
// empty list and comes out 'xenos' — a bug routed into a goal scoped to the
|
|
577
|
+
// PROTECTED core was born claimable by a Xenos the moment anyone nodded it.
|
|
578
|
+
// =========================================================================
|
|
579
|
+
|
|
580
|
+
await test('authorizeRoutedLanding: a protected-scope goal raises the rank floor', () => {
|
|
581
|
+
// filerRank must be a trusted tier here: a sub-Metic is refused outright by the
|
|
582
|
+
// protected_scope rule above, so the floor is only reachable for Metic+.
|
|
583
|
+
const landing = { route: true, status: 'ready', reason: 'bug_trusted_filer' };
|
|
584
|
+
assert.deepEqual(
|
|
585
|
+
authorizeRoutedLanding({ landing, scopeProtected: true, filerRank: 'archon' }),
|
|
586
|
+
{ ok: true, status: 'ready', requiresRank: PROTECTED_SCOPE_FLOOR }
|
|
587
|
+
);
|
|
588
|
+
assert.equal(PROTECTED_SCOPE_FLOOR, 'metic');
|
|
589
|
+
// An ordinary goal says NOTHING, so createTask derives the floor exactly as it
|
|
590
|
+
// does for every other create vector. Speaking only when there is something to
|
|
591
|
+
// say is what keeps this from becoming a second, competing floor.
|
|
592
|
+
assert.deepEqual(
|
|
593
|
+
authorizeRoutedLanding({ landing, scopeProtected: false, filerRank: 'xenos' }),
|
|
594
|
+
{ ok: true, status: 'ready', requiresRank: null }
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
await test('authorizeRoutedLanding: protected scope refuses a sub-Metic outright', () => {
|
|
599
|
+
const landing = { route: true, status: 'backlog', reason: 'bug_awaiting_nod' };
|
|
600
|
+
for (const rank of ['xenos', 'thetes', null, undefined, '', 'archon-ish']) {
|
|
601
|
+
const out = authorizeRoutedLanding({ landing, scopeProtected: true, filerRank: rank });
|
|
602
|
+
assert.equal(out.ok, false, `rank ${JSON.stringify(rank)} must be refused`);
|
|
603
|
+
assert.equal(out.code, 'protected_scope');
|
|
604
|
+
assert.equal(out.details.required_rank, PROTECTED_SCOPE_FLOOR);
|
|
605
|
+
}
|
|
606
|
+
for (const rank of ['metic', 'archon', 'ARCHON']) {
|
|
607
|
+
const out = authorizeRoutedLanding({ landing, scopeProtected: true, filerRank: rank });
|
|
608
|
+
assert.equal(out.ok, true, `rank ${rank} may route into protected scope`);
|
|
609
|
+
assert.equal(out.requiresRank, PROTECTED_SCOPE_FLOOR, 'and is still floored');
|
|
610
|
+
}
|
|
611
|
+
// An ordinary goal is untouched by this rule — a Xenos routes there freely.
|
|
612
|
+
assert.equal(authorizeRoutedLanding({ landing, scopeProtected: false, filerRank: 'xenos' }).ok, true);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
await test('authorizeRoutedLanding: the wall\'s verdict passes through UNCHANGED', () => {
|
|
616
|
+
// One definition of "out of scope" — the routed refusal is the authored
|
|
617
|
+
// refusal, code, reason and details included.
|
|
618
|
+
const wall = { ok: false, code: 'out_of_scope', reason: 'a path is outside', details: { out_of_scope: ['x'] } };
|
|
619
|
+
const out = authorizeRoutedLanding({
|
|
620
|
+
landing: { route: true, status: 'backlog', reason: 'soft_landing' },
|
|
621
|
+
scopeProtected: true, scopeCheck: wall,
|
|
622
|
+
});
|
|
623
|
+
assert.equal(out.ok, false);
|
|
624
|
+
assert.equal(out.code, 'out_of_scope');
|
|
625
|
+
assert.equal(out.reason, wall.reason);
|
|
626
|
+
assert.deepEqual(out.details, wall.details);
|
|
627
|
+
// And note the wall is checked BEFORE the floor is granted: a refused filing
|
|
628
|
+
// never returns a status at all.
|
|
629
|
+
assert.equal(out.status, undefined);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await test('authorizeRoutedLanding: fail-closed on a non-routing or absent verdict', () => {
|
|
633
|
+
for (const landing of [null, undefined, { route: false, reason: 'no_goal' }, {}]) {
|
|
634
|
+
const out = authorizeRoutedLanding({ landing });
|
|
635
|
+
assert.equal(out.ok, false, `landing ${JSON.stringify(landing)}`);
|
|
636
|
+
assert.equal(out.code, 'not_routing');
|
|
637
|
+
}
|
|
638
|
+
assert.equal(authorizeRoutedLanding().ok, false, 'no args ⇒ refuse');
|
|
639
|
+
assert.equal(authorizeRoutedLanding(null).ok, false, 'null ⇒ refuse, not a throw');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
await test('a protected-scope goal stamps requires_rank=metic on the routed task', async () => {
|
|
643
|
+
// goal 1000071\'s own shape: scope includes `lifecycle`, which is protected.
|
|
644
|
+
reset();
|
|
645
|
+
const pool = fakePool(routableHandler({
|
|
646
|
+
goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas', 'hall-ui', 'discord', 'lifecycle'] }],
|
|
647
|
+
}));
|
|
648
|
+
const out = await captureIdea(
|
|
649
|
+
{ title: 'broken', kind: 'bug', goalId: 7, capturedBy: 42, filerRank: 'archon' },
|
|
650
|
+
{ pool }
|
|
651
|
+
);
|
|
652
|
+
assert.equal(createTaskCalls[0].args.requiresRank, 'metic',
|
|
653
|
+
'a routed task in protected territory can never be claimed by a sub-Metic');
|
|
654
|
+
assert.equal(out.routing.requires_rank, 'metic', 'and the verdict says so out loud');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await test('an ordinary goal passes NO floor — createTask derives it, as for any vector', async () => {
|
|
658
|
+
reset();
|
|
659
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas'] }] }));
|
|
660
|
+
const out = await captureIdea(
|
|
661
|
+
{ title: 'broken', kind: 'bug', goalId: 7, capturedBy: 42, filerRank: 'archon' },
|
|
662
|
+
{ pool }
|
|
663
|
+
);
|
|
664
|
+
assert.equal('requiresRank' in createTaskCalls[0].args, false, 'the key is absent, not null');
|
|
665
|
+
assert.equal(out.routing.requires_rank, null);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
await test('a sub-Metic cannot route into PROTECTED scope at all — it is refused', async () => {
|
|
669
|
+
// The narc's major on the first R05 round: with no touches[] the protected-path
|
|
670
|
+
// arm had nothing to inspect, so a sub-Metic could MINT a row in the
|
|
671
|
+
// permission/ship/deploy core's queue and only the rank floor stopped them
|
|
672
|
+
// CLAIMING it. POST /goals/:id/tasks refuses that author outright, and
|
|
673
|
+
// admission refuses them the goal entirely — the routed path was the odd one
|
|
674
|
+
// out. Now it refuses too.
|
|
675
|
+
reset();
|
|
676
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['lifecycle'] }] }));
|
|
677
|
+
await assert.rejects(
|
|
678
|
+
() => captureIdea({ title: 'broken', kind: 'bug', goalId: 7, capturedBy: 9, filerRank: 'xenos' }, { pool }),
|
|
679
|
+
(e) => e.code === 'ROUTE_NOT_AUTHORIZED' && e.gate === 'protected_scope'
|
|
680
|
+
);
|
|
681
|
+
assert.equal(createTaskCalls.length, 0, 'no row lands in protected territory');
|
|
682
|
+
assert.ok(find(pool.queries, /^ROLLBACK$/));
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await test('...and the refusal costs the filer nothing they had before', async () => {
|
|
686
|
+
// The whole reason refusing is acceptable: the SAME report with no goal_id
|
|
687
|
+
// still files, exactly as it always did. Nobody loses the ability to report.
|
|
688
|
+
reset();
|
|
689
|
+
const pool = fakePool((s) => (/INSERT INTO idea_inbox/.test(s)
|
|
690
|
+
? { rows: [{ ...IDEA_ROW, status: 'open', suggested_goal_id: null }] } : null));
|
|
691
|
+
const idea = await captureIdea({ title: 'broken', kind: 'bug', capturedBy: 9, filerRank: 'xenos' }, { pool });
|
|
692
|
+
assert.equal(idea.routing.reason, 'no_goal');
|
|
693
|
+
assert.equal(createTaskCalls.length, 0);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
await test('a Metic+ filing into protected scope routes, and the floor still applies', async () => {
|
|
697
|
+
// Refusal and floor are LAYERS, not alternatives: the sub-Metic is refused
|
|
698
|
+
// above; a trusted filer gets through and the task is still born metic-floored,
|
|
699
|
+
// so a later nod cannot hand it to a newcomer.
|
|
700
|
+
reset();
|
|
701
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['lifecycle'] }] }));
|
|
702
|
+
await captureIdea({ title: 'broken', kind: 'bug', goalId: 7, capturedBy: 1, filerRank: 'metic' }, { pool });
|
|
703
|
+
assert.equal(createTaskCalls[0].args.status, 'ready');
|
|
704
|
+
assert.equal(createTaskCalls[0].args.requiresRank, 'metic');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await test('THE FLOOR CLAMPS UPWARD ONLY — createTask takes max(derived, explicit)', () => {
|
|
708
|
+
// The hacker's minor: R05's security claim rests on createTask's clamp, which
|
|
709
|
+
// lives in an unmodified file the panel could not open inside its read budget.
|
|
710
|
+
// Pin the real function here so the claim is checked in, not asserted in prose.
|
|
711
|
+
const lifecycleDb = require('../modules/lifecycle/db.js');
|
|
712
|
+
const { highestRank, deriveRequiredRank } = lifecycleDb;
|
|
713
|
+
// An empty touches[] — every routed task — derives the xenos floor. That is the
|
|
714
|
+
// hole R05's explicit 'metic' fills.
|
|
715
|
+
assert.equal(deriveRequiredRank([], false), 'xenos');
|
|
716
|
+
// The clamp: an explicit value can only RAISE.
|
|
717
|
+
assert.equal(highestRank('xenos', 'metic'), 'metic', 'explicit metic beats a xenos floor');
|
|
718
|
+
assert.equal(highestRank('metic', 'xenos'), 'metic', 'and an explicit xenos CANNOT lower a metic floor');
|
|
719
|
+
assert.equal(highestRank('archon', 'metic'), 'archon', 'a derived archon floor survives a lower explicit value');
|
|
720
|
+
// Which is exactly the composition createTask performs on a routed task:
|
|
721
|
+
assert.equal(highestRank(deriveRequiredRank([], false), 'metic'), 'metic');
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
await test('declared touches INSIDE the goal are allowed, and land on the task', async () => {
|
|
725
|
+
reset();
|
|
726
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas'] }] }));
|
|
727
|
+
await captureIdea({
|
|
728
|
+
title: 'broken', kind: 'bug', goalId: 7, filerRank: 'archon',
|
|
729
|
+
touches: ['modules/ideas/inbox.js'],
|
|
730
|
+
}, { pool });
|
|
731
|
+
assert.deepEqual(createTaskCalls[0].args.touches, ['modules/ideas/inbox.js']);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
await test('declared touches OUTSIDE the goal are refused by the real wall', async () => {
|
|
735
|
+
reset();
|
|
736
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas'] }] }));
|
|
737
|
+
await assert.rejects(
|
|
738
|
+
() => captureIdea({
|
|
739
|
+
title: 'broken', kind: 'bug', goalId: 7, filerRank: 'archon',
|
|
740
|
+
touches: ['src/bongos/auth.js'],
|
|
741
|
+
}, { pool }),
|
|
742
|
+
(e) => e.code === 'ROUTE_NOT_AUTHORIZED' && e.gate === 'out_of_scope'
|
|
743
|
+
);
|
|
744
|
+
assert.equal(createTaskCalls.length, 0, 'nothing is created');
|
|
745
|
+
assert.ok(find(pool.queries, /^ROLLBACK$/), 'and the transaction rolls back');
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
await test('a sub-Metic naming a PROTECTED path is refused by the wall too', async () => {
|
|
749
|
+
// `kernel` is itself a protected MODULE, so a sub-Metic filing there is stopped
|
|
750
|
+
// one step earlier by protected_scope. To see the protected-PATH wall bite on
|
|
751
|
+
// its own, the goal's scope must be unprotected while still covering a
|
|
752
|
+
// protected path — which is exactly what an ordinary goal owning `config/`
|
|
753
|
+
// looks like. Two independent walls, checked independently.
|
|
754
|
+
const kernelPathInOrdinaryScope = { ...LIVE_GOAL, scope_modules: ['ideas'] };
|
|
755
|
+
reset();
|
|
756
|
+
const pool = fakePool(routableHandler({ goalRows: [kernelPathInOrdinaryScope] }));
|
|
757
|
+
await assert.rejects(
|
|
758
|
+
() => captureIdea({
|
|
759
|
+
title: 'broken', kind: 'bug', goalId: 7, filerRank: 'xenos',
|
|
760
|
+
touches: ['src/bongos/auth.js'],
|
|
761
|
+
}, { pool }),
|
|
762
|
+
// Outside the ideas scope entirely, so the subset arm answers first — which is
|
|
763
|
+
// the wall's own documented order (subset before protected-path).
|
|
764
|
+
(e) => e.code === 'ROUTE_NOT_AUTHORIZED' && e.gate === 'out_of_scope'
|
|
765
|
+
);
|
|
766
|
+
assert.equal(createTaskCalls.length, 0);
|
|
767
|
+
|
|
768
|
+
// And a Metic+ filing into a PROTECTED-scope goal with a protected touch
|
|
769
|
+
// clears both walls and is still floored — proving rank, not the path, decides.
|
|
770
|
+
reset();
|
|
771
|
+
const pool2 = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['kernel'] }] }));
|
|
772
|
+
await captureIdea({
|
|
773
|
+
title: 'broken', kind: 'bug', goalId: 7, filerRank: 'archon',
|
|
774
|
+
touches: ['src/bongos/auth.js'],
|
|
775
|
+
}, { pool: pool2 });
|
|
776
|
+
assert.equal(createTaskCalls.length, 1);
|
|
777
|
+
assert.equal(createTaskCalls[0].args.requiresRank, 'metic', 'kernel is protected scope, so the floor still applies');
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
await test('NO touches means NO wall — reporting does not require naming files', async () => {
|
|
781
|
+
// The deliberate divergence from POST /goals/:id/tasks, which denies
|
|
782
|
+
// `empty_touches`. Demanding a blast radius as the price of reporting is the
|
|
783
|
+
// triage tax this goal removes; the protection moves to the rank floor.
|
|
784
|
+
reset();
|
|
785
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['kernel'] }] }));
|
|
786
|
+
const out = await captureIdea(
|
|
787
|
+
{ title: 'something is broken and I do not know where', kind: 'bug', goalId: 7, filerRank: 'archon' },
|
|
788
|
+
{ pool }
|
|
789
|
+
);
|
|
790
|
+
assert.equal(out.routing.routed, true, 'a report with no touches still routes');
|
|
791
|
+
assert.deepEqual(createTaskCalls[0].args.touches, []);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
await test('MEMBERSHIP IS NOT REQUIRED — reporting is not authoring (documented divergence)', async () => {
|
|
795
|
+
// authorizeGoalTaskCreate refuses a non-member with not_a_member. The routed
|
|
796
|
+
// path deliberately does not, because C5 promises the filer never has to know
|
|
797
|
+
// the goal map and C1 that a bug filed ANYWHERE lands in its home. This test
|
|
798
|
+
// exists so a later change has to argue with it rather than quietly reinstate
|
|
799
|
+
// the wall: nothing in the routed path reads goal_members at all.
|
|
800
|
+
reset();
|
|
801
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas'] }] }));
|
|
802
|
+
const out = await captureIdea(
|
|
803
|
+
{ title: 'broken', kind: 'bug', goalId: 7, capturedBy: 999, filerRank: 'archon' },
|
|
804
|
+
{ pool }
|
|
805
|
+
);
|
|
806
|
+
assert.equal(out.routing.routed, true);
|
|
807
|
+
assert.equal(find(pool.queries, /goal_members/), undefined,
|
|
808
|
+
'no membership lookup happens on the routed path — by design, not by omission');
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
await test('the goal SCOPE is read in the same locked statement as its status', async () => {
|
|
812
|
+
reset();
|
|
813
|
+
const pool = fakePool(routableHandler({ goalRows: [{ ...LIVE_GOAL, scope_modules: ['ideas'] }] }));
|
|
814
|
+
await captureIdea({ title: 'x', kind: 'bug', goalId: 7, filerRank: 'archon' }, { pool });
|
|
815
|
+
const sel = find(pool.queries, /FROM goals g JOIN versions v/);
|
|
816
|
+
assert.match(norm(sel.sql), /g\.scope_modules/,
|
|
817
|
+
'so a rescope cannot land between the wall check and the insert');
|
|
818
|
+
assert.match(norm(sel.sql), /FOR SHARE OF g/);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// =========================================================================
|
|
822
|
+
// 8. R05 — the NOD QUEUE: a soft landing nobody can see is a silent drop
|
|
823
|
+
// =========================================================================
|
|
824
|
+
|
|
825
|
+
await test('the nod queue asks for routed filings at backlog, oldest first', async () => {
|
|
826
|
+
const pool = fakePool((s) => (/FROM ideas_task_lineage/.test(s)
|
|
827
|
+
? { rows: [{ task_id: 1, title: 't', kind: 'bug', status: 'backlog', goal_id: 7 }] } : null));
|
|
828
|
+
const rows = await inbox.listRoutedAwaitingNod({ limit: 5 }, { pool });
|
|
829
|
+
assert.equal(rows.length, 1);
|
|
830
|
+
const q = norm(find(pool.queries, /FROM ideas_task_lineage/).sql);
|
|
831
|
+
assert.match(q, /l\.origin = 'route'/, "scoped by LINEAGE, not by status alone — a task sits at backlog for many reasons");
|
|
832
|
+
assert.match(q, /t\.status = 'backlog'/);
|
|
833
|
+
assert.match(q, /ORDER BY t\.created_at ASC/, 'oldest first: the longest-waiting report is the most overdue');
|
|
834
|
+
assert.match(q, /i\.captured_by/, "and it carries the filer, for Q3's kind + who-filed-it copy");
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
await test('the nod count uses the SAME predicate as the list', async () => {
|
|
838
|
+
const pool = fakePool((s) => (/COUNT/.test(s) ? { rows: [{ n: 4 }] } : null));
|
|
839
|
+
assert.equal(await inbox.countRoutedAwaitingNod({ pool }), 4);
|
|
840
|
+
const q = norm(pool.queries[0].sql);
|
|
841
|
+
assert.match(q, /l\.origin = 'route'/);
|
|
842
|
+
assert.match(q, /t\.status = 'backlog'/);
|
|
843
|
+
// A pill that disagrees with its own rows is worse than no pill.
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
await test('GET /inbox/awaiting-nod is registered BEFORE /inbox/:id, or it is unreachable', async () => {
|
|
847
|
+
// Express matches in registration order. Declared after the wildcard, this
|
|
848
|
+
// route arrives as /inbox/:id with id='awaiting-nod' and 400s on parseId —
|
|
849
|
+
// a bug with no symptom except an empty card.
|
|
850
|
+
const router = require('../modules/ideas/routes/inbox.js')({});
|
|
851
|
+
const paths = router.stack.filter((l) => l.route).map((l) => l.route.path);
|
|
852
|
+
const iNod = paths.indexOf('/inbox/awaiting-nod');
|
|
853
|
+
const iParam = paths.indexOf('/inbox/:id');
|
|
854
|
+
assert.ok(iNod >= 0, 'the route exists');
|
|
855
|
+
assert.ok(iNod < iParam, `literal segment must precede the wildcard (got ${iNod} vs ${iParam})`);
|
|
856
|
+
});
|
|
857
|
+
|
|
571
858
|
summary('idea_routing');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// tests/session_boundary_handoff.mjs
|
|
2
|
+
//
|
|
3
|
+
// THE SESSION BOUNDARY — task 1003150 (goal 1000053 "Session efficiency").
|
|
4
|
+
//
|
|
5
|
+
// The mirror of that goal's session-START series (1002435-1002438): those
|
|
6
|
+
// front-load what a session can know at open; this stops a session dragging what
|
|
7
|
+
// it no longer needs at close. A shipped task is the one moment the system KNOWS
|
|
8
|
+
// the work is finished, and the moment almost all of that context becomes dead
|
|
9
|
+
// weight.
|
|
10
|
+
//
|
|
11
|
+
// TWO HALVES, both pinned here:
|
|
12
|
+
// 1. ship-card.js handoffAdvice — PURE, so every branch is a real assertion with
|
|
13
|
+
// no card, no DB, no session.
|
|
14
|
+
// 2. .claude/skills/builder-sequence/SKILL.md — a DOC-CONTRACT check. The skill
|
|
15
|
+
// is the only place the between-links handoff can live (a hook cannot clear a
|
|
16
|
+
// conversation), and a prose step with nothing asserting it is exactly the
|
|
17
|
+
// kind of instruction that gets tidied away in a later edit. This is the same
|
|
18
|
+
// posture as the fitness doc checks: assert the RULE is stated, not its wording.
|
|
19
|
+
//
|
|
20
|
+
// WHY A THRESHOLD TEST AT ALL. The failure mode here is not a crash, it is
|
|
21
|
+
// wallpaper: advice that prints on every ship stops being read (the
|
|
22
|
+
// deploy-divergence footer is the in-repo cautionary tale — 21 core versions of
|
|
23
|
+
// identical warning that nobody acted on). So "stays silent on a normal ship" is
|
|
24
|
+
// the assertion that matters most, and it is first.
|
|
25
|
+
//
|
|
26
|
+
// Run: node tests/session_boundary_handoff.mjs
|
|
27
|
+
|
|
28
|
+
import { strict as assert } from 'node:assert';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { makeRunner } from './helpers.mjs';
|
|
34
|
+
|
|
35
|
+
const require = createRequire(import.meta.url);
|
|
36
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
|
+
const card = require('../modules/lifecycle/ship-card.js');
|
|
38
|
+
const { handoffAdvice, HEAVY_SESSION_CACHED_TOKENS, buildCardModel, renderMonospaceCard, renderCardHtml } = card;
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
// The three real sessions the threshold was calibrated against (2026-08-21).
|
|
43
|
+
const MEASURED = { light: 11.3e6, dragging: 36.1e6, heavy: 78.1e6 };
|
|
44
|
+
|
|
45
|
+
// =========================================================================
|
|
46
|
+
// 1. handoffAdvice — the decision
|
|
47
|
+
// =========================================================================
|
|
48
|
+
|
|
49
|
+
await test('SILENT on an ordinary ship — the whole design rests on this', () => {
|
|
50
|
+
// Advice that fires every time is wallpaper. A short session must never see it.
|
|
51
|
+
for (const cached of [0, 1, 1e6, MEASURED.light, HEAVY_SESSION_CACHED_TOKENS - 1]) {
|
|
52
|
+
assert.equal(handoffAdvice({ stage: 'shipped', cachedTokens: cached }), null,
|
|
53
|
+
`${cached} cached must stay silent`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await test('SPEAKS once the session is genuinely heavy', () => {
|
|
58
|
+
for (const cached of [HEAVY_SESSION_CACHED_TOKENS, MEASURED.dragging, MEASURED.heavy]) {
|
|
59
|
+
const advice = handoffAdvice({ stage: 'shipped', cachedTokens: cached });
|
|
60
|
+
assert.ok(advice, `${cached} cached should advise`);
|
|
61
|
+
assert.match(advice, /session-handoff/, 'and names the command that does something about it');
|
|
62
|
+
assert.match(advice, /fresh session/, 'and says what to do with it');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('the advice quotes the ACTUAL figure, so it is arguable', () => {
|
|
67
|
+
// A builder who disagrees can check the number rather than take it on faith.
|
|
68
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }), /78\.1M cached/);
|
|
69
|
+
assert.match(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.dragging }), /36\.1M cached/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await test('NEVER mid-task — resetting inside a claim is actively wrong', () => {
|
|
73
|
+
// Mid-task the context is what the work is standing on. Only a stage where the
|
|
74
|
+
// task has left the builder's hands may advise a reset.
|
|
75
|
+
for (const stage of ['claimed', 'review', 'built', 'completed', 'failed']) {
|
|
76
|
+
assert.equal(handoffAdvice({ stage, cachedTokens: MEASURED.heavy }), null,
|
|
77
|
+
`stage '${stage}' must not advise a reset`);
|
|
78
|
+
}
|
|
79
|
+
// ...and the two that may.
|
|
80
|
+
assert.ok(handoffAdvice({ stage: 'shipped', cachedTokens: MEASURED.heavy }));
|
|
81
|
+
assert.ok(handoffAdvice({ stage: 'confirmed', cachedTokens: MEASURED.heavy }),
|
|
82
|
+
'confirmed counts: the task is out of the builder\'s hands even though it has not landed');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await test('hostile or absent input is silence, never a throw', () => {
|
|
86
|
+
// This runs inside the ship card. A card that CRASHES at the end of a
|
|
87
|
+
// successful ship would turn a landed task into a scary traceback.
|
|
88
|
+
for (const args of [undefined, {}, null, { stage: 'shipped' }, { stage: 'shipped', cachedTokens: null },
|
|
89
|
+
{ stage: 'shipped', cachedTokens: 'lots' }, { stage: 'shipped', cachedTokens: NaN },
|
|
90
|
+
{ stage: 'shipped', cachedTokens: -1 }, { stage: null, cachedTokens: 1e9 }]) {
|
|
91
|
+
assert.doesNotThrow(() => handoffAdvice(args), `${JSON.stringify(args)} must not throw`);
|
|
92
|
+
assert.equal(handoffAdvice(args), null, `${JSON.stringify(args)} ⇒ silence`);
|
|
93
|
+
}
|
|
94
|
+
assert.equal(handoffAdvice(null), null, 'explicit null ⇒ silence, not a crash');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('the threshold sits between the session that felt fine and the one that dragged', () => {
|
|
98
|
+
// Documents the calibration as an assertion: if someone retunes the constant,
|
|
99
|
+
// this says what the number was chosen to separate.
|
|
100
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS > MEASURED.light,
|
|
101
|
+
'the 11.3M session was fine and must stay quiet');
|
|
102
|
+
assert.ok(HEAVY_SESSION_CACHED_TOKENS < MEASURED.dragging,
|
|
103
|
+
'the 36.1M session had begun to drag and should be caught');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// =========================================================================
|
|
107
|
+
// 2. The card — the advice has to actually reach the builder
|
|
108
|
+
// =========================================================================
|
|
109
|
+
|
|
110
|
+
await test('a heavy ship card carries the nudge, in BOTH renderers', () => {
|
|
111
|
+
const model = buildCardModel({
|
|
112
|
+
stage: 'shipped', taskId: 1003150, title: 'A task',
|
|
113
|
+
usage: { cachedTokens: MEASURED.heavy, newTokens: 891e3, costUsd: 49.81 },
|
|
114
|
+
});
|
|
115
|
+
assert.ok(model.handoff, 'it is on the model');
|
|
116
|
+
const mono = renderMonospaceCard(model);
|
|
117
|
+
assert.match(mono, /heavy session/, 'terminal card shows it');
|
|
118
|
+
assert.match(mono, /Next →/, 'and it sits with the Next action it qualifies');
|
|
119
|
+
// Position matters: the nudge must come AFTER Next, as a qualifier, not above it.
|
|
120
|
+
assert.ok(mono.indexOf('Next →') < mono.indexOf('heavy session'), 'below the action, not shouting over it');
|
|
121
|
+
assert.match(renderCardHtml(model), /heavy session/, 'in-feed card shows it too');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('a normal ship card is BYTE-IDENTICAL to before the nudge existed', () => {
|
|
125
|
+
// The regression that matters: every ordinary ship must look exactly as it did.
|
|
126
|
+
const usage = { cachedTokens: MEASURED.light, newTokens: 262e3, costUsd: 8.06 };
|
|
127
|
+
const model = buildCardModel({ stage: 'shipped', taskId: 1003148, title: 'A task', usage });
|
|
128
|
+
assert.equal(model.handoff, null);
|
|
129
|
+
const mono = renderMonospaceCard(model);
|
|
130
|
+
assert.doesNotMatch(mono, /heavy session/);
|
|
131
|
+
assert.doesNotMatch(mono, /session-handoff/);
|
|
132
|
+
assert.doesNotMatch(renderCardHtml(model), /heavy session/);
|
|
133
|
+
// A card with no usage data at all (old breadcrumbs) must also stay clean.
|
|
134
|
+
const bare = buildCardModel({ stage: 'shipped', taskId: 1, title: 'A task' });
|
|
135
|
+
assert.equal(bare.handoff, null);
|
|
136
|
+
assert.doesNotMatch(renderMonospaceCard(bare), /heavy session/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await test('the nudge is not a Usage/Deploy ROW — it is guidance, not a task fact', () => {
|
|
140
|
+
const model = buildCardModel({
|
|
141
|
+
stage: 'shipped', taskId: 1, title: 'A task',
|
|
142
|
+
usage: { cachedTokens: MEASURED.heavy },
|
|
143
|
+
});
|
|
144
|
+
assert.equal(model.rows.some((r) => /heavy session/.test(String(r.v))), false,
|
|
145
|
+
'rows report facts about the TASK; this is advice about the SESSION');
|
|
146
|
+
assert.equal(model.facts.some((f) => /heavy session/.test(String(f.value))), false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// =========================================================================
|
|
150
|
+
// 3. /builder-sequence — the doc contract
|
|
151
|
+
//
|
|
152
|
+
// A hook cannot clear a conversation and /session-handoff is read-only, so the
|
|
153
|
+
// runner skill is the ONLY place the between-links handoff can live. That makes
|
|
154
|
+
// its prose load-bearing, and load-bearing prose needs a test.
|
|
155
|
+
// =========================================================================
|
|
156
|
+
|
|
157
|
+
const SEQ = fs.readFileSync(path.join(ROOT, '.claude/skills/builder-sequence/SKILL.md'), 'utf8');
|
|
158
|
+
|
|
159
|
+
await test('the sequence runner states the between-links handoff step', () => {
|
|
160
|
+
assert.match(SEQ, /\/session-handoff/, 'it names the command');
|
|
161
|
+
assert.match(SEQ, /fresh session/i, 'and that the chain continues in a fresh one');
|
|
162
|
+
assert.match(SEQ, /heavy session/i, 'and the ship-card signal it keys off');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await test('...and tells the runner to STOP rather than claim the next link', () => {
|
|
166
|
+
// The failure mode: a runner that emits the prompt and then helpfully carries on
|
|
167
|
+
// in the same window has done the paperwork and none of the work.
|
|
168
|
+
assert.match(SEQ, /do not claim the next task in this window/i,
|
|
169
|
+
'the halt must be explicit — emitting a prompt and continuing anyway is the trap');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('...and says WHY, with the measurement, not just what', () => {
|
|
173
|
+
// An unexplained instruction is the first thing dropped in a later edit.
|
|
174
|
+
assert.match(SEQ, /re-?read/i, 'names the mechanism (context re-read per turn)');
|
|
175
|
+
assert.match(SEQ, /11\.3M|36\.1M|78\.1M/, 'carries the measured figures');
|
|
176
|
+
assert.match(SEQ, /durable|GDS/, 'and why a fresh window is safe: the chain state is durable');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('the runner is warned against running a chain to exhaustion', () => {
|
|
180
|
+
assert.match(SEQ, /What NOT to do/, 'the section exists');
|
|
181
|
+
assert.match(SEQ, /exhaustion|one window/i, 'and names the anti-pattern this step prevents');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
summary('session_boundary_handoff');
|