@bongos/core 1.19.275 → 1.19.276
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 +30 -25
- 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/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/idea_routing.mjs +288 -1
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');
|