@mmnto/cli 1.124.0 → 2.1.0
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/dist/commands/describe.d.ts +13 -1
- package/dist/commands/describe.d.ts.map +1 -1
- package/dist/commands/describe.js +29 -1
- package/dist/commands/describe.js.map +1 -1
- package/dist/commands/describe.test.js +86 -3
- package/dist/commands/describe.test.js.map +1 -1
- package/dist/commands/doctor.d.ts +16 -10
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +162 -54
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +444 -28
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/hook-totemdir-render.test.js +55 -1
- package/dist/commands/hook-totemdir-render.test.js.map +1 -1
- package/dist/commands/init-templates.d.ts +2 -2
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +39 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/init.test.js +32 -0
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-hooks-exit-contract.test.js +32 -1
- package/dist/commands/install-hooks-exit-contract.test.js.map +1 -1
- package/dist/commands/install-hooks.d.ts +143 -10
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/install-hooks.js +596 -92
- package/dist/commands/install-hooks.js.map +1 -1
- package/dist/commands/install-hooks.test.js +1535 -19
- package/dist/commands/install-hooks.test.js.map +1 -1
- package/dist/commands/legs.d.ts +35 -7
- package/dist/commands/legs.d.ts.map +1 -1
- package/dist/commands/legs.js +26 -7
- package/dist/commands/legs.js.map +1 -1
- package/dist/commands/legs.test.js +87 -0
- package/dist/commands/legs.test.js.map +1 -1
- package/dist/commands/release-train-shape.test.d.ts +2 -0
- package/dist/commands/release-train-shape.test.d.ts.map +1 -0
- package/dist/commands/release-train-shape.test.js +226 -0
- package/dist/commands/release-train-shape.test.js.map +1 -0
- package/dist/commands/spec-templates.d.ts +17 -4
- package/dist/commands/spec-templates.d.ts.map +1 -1
- package/dist/commands/spec-templates.js +23 -3
- package/dist/commands/spec-templates.js.map +1 -1
- package/dist/commands/spec.d.ts +57 -8
- package/dist/commands/spec.d.ts.map +1 -1
- package/dist/commands/spec.js +124 -28
- package/dist/commands/spec.js.map +1 -1
- package/dist/commands/spec.test.js +277 -44
- package/dist/commands/spec.test.js.map +1 -1
- package/dist/commands/tools-hook-parity.test.js +200 -0
- package/dist/commands/tools-hook-parity.test.js.map +1 -1
- package/dist/utils.d.ts +5 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/utils.test.js +8 -3
- package/dist/utils.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { GROUNDING_ANCHOR_FREE_TEXT, GROUNDING_ANCHOR_ISSUE, GROUNDING_ANCHOR_MIXED, GROUNDING_ANCHOR_RECORD, GroundingAnchorSchema,
|
|
6
|
+
import { GROUNDING_ANCHOR_FREE_TEXT, GROUNDING_ANCHOR_ISSUE, GROUNDING_ANCHOR_MIXED, GROUNDING_ANCHOR_RECORD, GroundingAnchorSchema, hasUnrenderableHeadingChar, isRelevanceInRange, PROMPT_SOURCE_BUILTIN, PROMPT_SOURCE_OVERRIDE, TotemConfigError, } from '@mmnto/totem';
|
|
7
7
|
import { cleanTmpDir } from '../test-utils.js';
|
|
8
8
|
import { log } from '../ui.js';
|
|
9
9
|
import { assemblePrompt, assertOutDoesNotOverwriteRecord, buildRecordSearchQuery, evaluateGroundingFloor, expandSpecQuery, formatGroundingRefusal, isRecordPathOutsideRoot, loadSpecRecord, MAX_LESSON_CHARS, MAX_LESSONS, MAX_SPECS, resolveDefaultSpecPath, resolveGroundingAnchor, retrieveContext, sanitizeSpecFilename, SPEC_SEARCH_POOL, SPEC_SYSTEM_PROMPT, specCommand, validateOutputOptions, validateSpecInvocation, } from './spec.js';
|
|
@@ -531,19 +531,26 @@ describe('SPEC_REQUIRED_SECTIONS', () => {
|
|
|
531
531
|
expect(promptLines, `${section} is not a line of SPEC_SYSTEM_PROMPT`).toContain(section);
|
|
532
532
|
}
|
|
533
533
|
});
|
|
534
|
-
it('every entry is renderable into the single-quoted node -e reader (
|
|
534
|
+
it('every entry is renderable into the single-quoted node -e reader as a HEADING (mmnto-ai/totem#2737)', () => {
|
|
535
535
|
for (const section of SPEC_REQUIRED_SECTIONS) {
|
|
536
|
-
// A quote, backslash, dollar, backtick
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
536
|
+
// A quote, backslash, dollar, backtick or control character would break
|
|
537
|
+
// the `sh` single-quoted word, the JS string literal inside it, or both —
|
|
538
|
+
// and could forge hook lines. Printable non-ASCII could not: the heading
|
|
539
|
+
// predicate permits it where the PATH predicate cannot, because git
|
|
540
|
+
// C-quotes path bytes above 0x7e in the `diff --name-only` output the
|
|
541
|
+
// hooks' `grep -q` filters read and a heading meets no such filter. The
|
|
542
|
+
// executed proof that the em dash survives the render is the frozen
|
|
543
|
+
// falsifier in install-hooks.test.ts, over the four schema-constrained R3
|
|
544
|
+
// drafts that carry the Verification heading byte-identical.
|
|
545
|
+
expect(hasUnrenderableHeadingChar(section), `${section} cannot be rendered`).toBe(false);
|
|
540
546
|
}
|
|
541
547
|
});
|
|
542
|
-
it('names
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
]);
|
|
548
|
+
it('names all nine promised sections, in prompt order', () => {
|
|
549
|
+
// Copied from SPEC_SYSTEM_PROMPT rather than retyped, so the em dash in the
|
|
550
|
+
// Verification heading can never enter this file as a look-alike byte
|
|
551
|
+
// (the mmnto-ai/totem#2692 authoring trap).
|
|
552
|
+
expect([...SPEC_REQUIRED_SECTIONS]).toEqual(SPEC_SYSTEM_PROMPT.split('\n').filter((line) => line.startsWith('### ')));
|
|
553
|
+
expect(SPEC_REQUIRED_SECTIONS).toHaveLength(9);
|
|
547
554
|
});
|
|
548
555
|
});
|
|
549
556
|
// ─── resolveGroundingAnchor (mmnto-ai/totem#2700) ────────
|
|
@@ -674,32 +681,62 @@ function relevantHit(relevance, overrides = {}) {
|
|
|
674
681
|
}
|
|
675
682
|
const FLOOR = 0.25;
|
|
676
683
|
/** The floor's PLACE exactly as a refusal renders it (`FLOOR_PLACE` in spec.ts). */
|
|
677
|
-
const FLOOR_PLACE_TEXT = 'searchRelevanceFloor in totem.config.ts
|
|
684
|
+
const FLOOR_PLACE_TEXT = 'searchRelevanceFloor in totem.config.ts';
|
|
685
|
+
/** The whole floor line when NO floor is configured (`FLOOR_LINE_UNSET` in spec.ts, mmnto-ai/totem#2727). */
|
|
686
|
+
const FLOOR_LINE_UNSET_TEXT = 'floor none — searchRelevanceFloor unset in totem.config.ts (no default; calibrate per repo — see config-reference)';
|
|
687
|
+
/**
|
|
688
|
+
* The refusal's floor LINE, whole. A `toContain` of the value-and-place text
|
|
689
|
+
* alone is satisfied by a line that appends MORE after it — which is exactly
|
|
690
|
+
* the retired "(schema default 0.25 when unset)" suffix — so the pins that
|
|
691
|
+
* guard that suffix's removal compare the entire line, not a prefix of it
|
|
692
|
+
* (mmnto-ai/totem#2727 fold, F3).
|
|
693
|
+
*/
|
|
694
|
+
function floorLineOf(message) {
|
|
695
|
+
const line = message.split('\n').find((candidate) => candidate.startsWith('floor '));
|
|
696
|
+
if (line === undefined)
|
|
697
|
+
throw new Error(`no floor line in refusal message:\n${message}`);
|
|
698
|
+
return line;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Every floor call below injects the SAME predicate production injects
|
|
702
|
+
* (mmnto-ai/totem#2738 fold 2, F2): core's `isRelevanceInRange`, the one the
|
|
703
|
+
* grounding bundle builder omits on. The predicate is a PARAMETER in
|
|
704
|
+
* production because a static value import from `@mmnto/totem` inside
|
|
705
|
+
* `commands/**` pulls LanceDB into every CLI startup (mmnto-ai/totem#2339);
|
|
706
|
+
* tests may import it statically — that rule excludes `**\/*.test.ts`.
|
|
707
|
+
*
|
|
708
|
+
* The predicate precedes `floor` in the real signature so mmnto-ai/totem#2758
|
|
709
|
+
* can make `floor` optional without a required-after-optional parameter.
|
|
710
|
+
*/
|
|
711
|
+
function evaluateFloor(context, floor) {
|
|
712
|
+
return evaluateGroundingFloor(context, isRelevanceInRange, floor);
|
|
713
|
+
}
|
|
678
714
|
describe('evaluateGroundingFloor', () => {
|
|
679
715
|
it('0 retrieved items REFUSES — nothing grounds the run (the charter rule, not an MCP mirror)', () => {
|
|
680
|
-
const verdict =
|
|
716
|
+
const verdict = evaluateFloor(emptyContext(), FLOOR);
|
|
681
717
|
expect(verdict).toEqual({
|
|
682
718
|
refuse: true,
|
|
683
719
|
hits: 0,
|
|
684
720
|
bestRelevance: null,
|
|
685
721
|
withheld: [],
|
|
686
722
|
floorExempt: 0,
|
|
723
|
+
faulted: 0,
|
|
687
724
|
});
|
|
688
725
|
});
|
|
689
726
|
it('every signal-bearing hit below the floor, none exempt, REFUSES', () => {
|
|
690
|
-
const verdict =
|
|
727
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(0.2), relevantHit(0.11)] }, FLOOR);
|
|
691
728
|
expect(verdict.refuse).toBe(true);
|
|
692
729
|
expect(verdict.hits).toBe(2);
|
|
693
730
|
expect(verdict.bestRelevance).toBeCloseTo(0.2, 10);
|
|
694
731
|
expect(verdict.floorExempt).toBe(0);
|
|
695
732
|
});
|
|
696
733
|
it('one hit AT the floor PROCEEDS (the floor is inclusive)', () => {
|
|
697
|
-
const verdict =
|
|
734
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(0.1), relevantHit(FLOOR)] }, FLOOR);
|
|
698
735
|
expect(verdict.refuse).toBe(false);
|
|
699
736
|
expect(verdict.withheld).toEqual([]);
|
|
700
737
|
});
|
|
701
738
|
it('one floor-EXEMPT hit beside below-floor signal PROCEEDS (a keyword-only hit is never withheld for a weak sibling)', () => {
|
|
702
|
-
const verdict =
|
|
739
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(0.05)], code: [relevantHit(undefined)] }, FLOOR);
|
|
703
740
|
expect(verdict.refuse).toBe(false);
|
|
704
741
|
expect(verdict.floorExempt).toBe(1);
|
|
705
742
|
expect(verdict.withheld).toEqual([]);
|
|
@@ -710,14 +747,14 @@ describe('evaluateGroundingFloor', () => {
|
|
|
710
747
|
// silently loosen the refusal arm, since an FTS-only lesson is floor-EXEMPT
|
|
711
748
|
// and `refuse` requires `floorExempt === 0`.
|
|
712
749
|
it('a run whose ONLY retrieved item is an FTS-only lesson REFUSES as 0 hits', () => {
|
|
713
|
-
const verdict =
|
|
750
|
+
const verdict = evaluateFloor({ ...emptyContext(), lessons: [relevantHit(undefined, { type: 'lesson' })] }, FLOOR);
|
|
714
751
|
expect(verdict.refuse).toBe(true);
|
|
715
752
|
expect(verdict.hits).toBe(0);
|
|
716
753
|
expect(verdict.floorExempt).toBe(0);
|
|
717
754
|
expect(verdict.bestRelevance).toBeNull();
|
|
718
755
|
});
|
|
719
756
|
it('a below-floor spec beside an FTS-only lesson still REFUSES (a lesson is not floor-exempt evidence)', () => {
|
|
720
|
-
const verdict =
|
|
757
|
+
const verdict = evaluateFloor({
|
|
721
758
|
...emptyContext(),
|
|
722
759
|
specs: [relevantHit(0.05, { filePath: 'docs/weak.md' })],
|
|
723
760
|
lessons: [relevantHit(undefined, { type: 'lesson' })],
|
|
@@ -728,7 +765,7 @@ describe('evaluateGroundingFloor', () => {
|
|
|
728
765
|
expect(verdict.withheld).toEqual([{ filePath: 'docs/weak.md', relevance: 0.05 }]);
|
|
729
766
|
});
|
|
730
767
|
it('an AT-floor spec beside lessons PROCEEDS, and the lessons are not counted as hits', () => {
|
|
731
|
-
const verdict =
|
|
768
|
+
const verdict = evaluateFloor({
|
|
732
769
|
...emptyContext(),
|
|
733
770
|
specs: [relevantHit(FLOOR)],
|
|
734
771
|
lessons: [relevantHit(undefined, { type: 'lesson' }), relevantHit(0.9, { type: 'lesson' })],
|
|
@@ -741,38 +778,102 @@ describe('evaluateGroundingFloor', () => {
|
|
|
741
778
|
expect(verdict.bestRelevance).toBeCloseTo(FLOOR, 10);
|
|
742
779
|
});
|
|
743
780
|
it('no relevance anywhere PROCEEDS — a pure-FTS corpus is never demoted', () => {
|
|
744
|
-
const verdict =
|
|
781
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(undefined), relevantHit(undefined)] }, FLOOR);
|
|
745
782
|
expect(verdict.refuse).toBe(false);
|
|
746
783
|
expect(verdict.bestRelevance).toBeNull();
|
|
747
784
|
expect(verdict.floorExempt).toBe(2);
|
|
748
785
|
});
|
|
749
|
-
// A
|
|
750
|
-
// carries no relevance at all,
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
786
|
+
// A relevance the CORE builder drops is one the floor must not read as
|
|
787
|
+
// signal: the item it writes carries no relevance at all, so counting it as
|
|
788
|
+
// a measurement would make the judgment and the artifact disagree about the
|
|
789
|
+
// same hit. The predicate is literally shared — `isRelevanceInRange` from
|
|
790
|
+
// `@mmnto/totem` (mmnto-ai/totem#2738 fold 2, F2) — which covers non-finite
|
|
791
|
+
// values AND finite ones outside [0, 1]. The range arm matters: before it, a
|
|
792
|
+
// relevance of `2` (a negative `_distance` under l2) set `bestRelevance`
|
|
793
|
+
// above the floor and DEFEATED the refusal, while the artifact write threw
|
|
794
|
+
// on that same value.
|
|
795
|
+
//
|
|
796
|
+
// But a fault is not a keyword-only hit either (the mmnto-ai/totem#2761 bot
|
|
797
|
+
// round, Greptile P1): counting it EXEMPT let it save a run. It is FAULTED —
|
|
798
|
+
// neither signal nor exemption — and a run whose every hit is faulted has
|
|
799
|
+
// nothing usable and refuses.
|
|
800
|
+
it.each([NaN, Infinity, -Infinity, 2, -1e-16])('a relevance outside [0, 1] (%s) is FAULTED — not signal, not exempt — and alone it REFUSES', (relevance) => {
|
|
801
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(relevance)] }, FLOOR);
|
|
802
|
+
expect(verdict.faulted).toBe(1);
|
|
803
|
+
expect(verdict.floorExempt).toBe(0);
|
|
755
804
|
expect(verdict.bestRelevance).toBeNull();
|
|
756
805
|
expect(verdict.withheld).toEqual([]);
|
|
757
|
-
expect(verdict.refuse).toBe(
|
|
806
|
+
expect(verdict.refuse).toBe(true);
|
|
758
807
|
});
|
|
759
|
-
it('a
|
|
760
|
-
|
|
808
|
+
it('a NEGATIVE relevance beside a weak one does NOT save the run — a fault is not evidence (Greptile P1)', () => {
|
|
809
|
+
// The build's earlier reading counted the fault as exempt, so this context
|
|
810
|
+
// proceeded into synthesis on the strength of an SDK fault. It refuses: the
|
|
811
|
+
// weak sibling is the only signal, it is below the floor, and nothing
|
|
812
|
+
// legitimately exempts it. The fault is disclosed by count, never withheld
|
|
813
|
+
// as a measurement.
|
|
814
|
+
const verdict = evaluateFloor({
|
|
815
|
+
...emptyContext(),
|
|
816
|
+
specs: [relevantHit(0.05, { filePath: 'docs/weak.md' }), relevantHit(-0.5)],
|
|
817
|
+
}, FLOOR);
|
|
818
|
+
expect(verdict.refuse).toBe(true);
|
|
819
|
+
expect(verdict.faulted).toBe(1);
|
|
820
|
+
expect(verdict.floorExempt).toBe(0);
|
|
821
|
+
expect(verdict.bestRelevance).toBeCloseTo(0.05, 10);
|
|
822
|
+
expect(verdict.withheld).toEqual([{ filePath: 'docs/weak.md', relevance: 0.05 }]);
|
|
823
|
+
});
|
|
824
|
+
it('a NaN hit beside a genuinely weak one is not disclosed as a withheld candidate, and does not save the run', () => {
|
|
825
|
+
const verdict = evaluateFloor({
|
|
761
826
|
...emptyContext(),
|
|
762
827
|
specs: [relevantHit(0.05, { filePath: 'docs/weak.md' }), relevantHit(NaN)],
|
|
763
828
|
}, FLOOR);
|
|
764
|
-
|
|
765
|
-
expect(verdict.floorExempt).toBe(
|
|
829
|
+
expect(verdict.faulted).toBe(1);
|
|
830
|
+
expect(verdict.floorExempt).toBe(0);
|
|
831
|
+
expect(verdict.refuse).toBe(true);
|
|
832
|
+
expect(verdict.withheld).toEqual([{ filePath: 'docs/weak.md', relevance: 0.05 }]);
|
|
833
|
+
});
|
|
834
|
+
it('a fault beside a hit AT or above the floor PROCEEDS on the real signal', () => {
|
|
835
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(-0.5), relevantHit(0.6)] }, FLOOR);
|
|
766
836
|
expect(verdict.refuse).toBe(false);
|
|
837
|
+
expect(verdict.faulted).toBe(1);
|
|
838
|
+
expect(verdict.bestRelevance).toBeCloseTo(0.6, 10);
|
|
767
839
|
expect(verdict.withheld).toEqual([]);
|
|
768
840
|
});
|
|
841
|
+
it('a fault beside a keyword-only hit PROCEEDS — the exemption is the keyword hit, never the fault', () => {
|
|
842
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(-0.5)], code: [relevantHit(undefined)] }, FLOOR);
|
|
843
|
+
expect(verdict.refuse).toBe(false);
|
|
844
|
+
expect(verdict.faulted).toBe(1);
|
|
845
|
+
expect(verdict.floorExempt).toBe(1);
|
|
846
|
+
expect(verdict.bestRelevance).toBeNull();
|
|
847
|
+
});
|
|
848
|
+
it('the refusal names an all-faulted retrieval and a faulted count beside a weak signal', () => {
|
|
849
|
+
const allFaulted = evaluateFloor({ ...emptyContext(), specs: [relevantHit(-0.5), relevantHit(2)] }, FLOOR);
|
|
850
|
+
expect(allFaulted.refuse).toBe(true);
|
|
851
|
+
const { message: allFaultedText } = formatGroundingRefusal('weak topic', allFaulted, FLOOR, 0);
|
|
852
|
+
expect(allFaultedText).toContain('Retrieval returned 2 hits, but every one carried a relevance that is not a finite number in [0, 1]');
|
|
853
|
+
expect(allFaultedText).toContain('nothing usable grounds this run');
|
|
854
|
+
// The cause is the search layer's to name (metric-specific); the refusal must
|
|
855
|
+
// not assert an l2 fault the running metric might not have (leg F3).
|
|
856
|
+
expect(allFaultedText).not.toContain('l2');
|
|
857
|
+
// The delivered-lessons disclosure the zero-hit arm carries (mmnto-ai/totem#2735).
|
|
858
|
+
const { message: withLessons } = formatGroundingRefusal('weak topic', allFaulted, FLOOR, 3);
|
|
859
|
+
expect(withLessons).toContain('3 lessons were retrieved, but lessons do not ground a run (ruled mmnto-ai/totem#2727).');
|
|
860
|
+
expect(allFaultedText).not.toContain('best relevance');
|
|
861
|
+
const mixed = evaluateFloor({
|
|
862
|
+
...emptyContext(),
|
|
863
|
+
specs: [relevantHit(0.05, { filePath: 'docs/weak.md' }), relevantHit(-0.5)],
|
|
864
|
+
}, FLOOR);
|
|
865
|
+
const { message: mixedText } = formatGroundingRefusal('weak topic', mixed, FLOOR, 0);
|
|
866
|
+
expect(mixedText).toContain('best relevance 0.050 is below the floor');
|
|
867
|
+
expect(mixedText).toContain('1 hit carried a relevance that is not a finite number in [0, 1] (tallied out of range by the search layer) and did not count as signal or as exemption.');
|
|
868
|
+
expect(mixedText).toContain('1. docs/weak.md — relevance 0.050');
|
|
869
|
+
});
|
|
769
870
|
// Was "counts hits across ALL FOUR partitions" when the lesson partition was
|
|
770
871
|
// structurally empty and could not change the count. mmnto-ai/totem#2735
|
|
771
872
|
// makes it populated, so the gate holds its ruled inputs: the three
|
|
772
873
|
// partitions it was exercised over. A delivered lesson is counted on the
|
|
773
874
|
// `Found:` line and in the artifact, never as a grounding hit.
|
|
774
875
|
it('counts hits across the spec, session and code partitions — never lessons', () => {
|
|
775
|
-
const verdict =
|
|
876
|
+
const verdict = evaluateFloor({
|
|
776
877
|
specs: [relevantHit(0.9)],
|
|
777
878
|
sessions: [relevantHit(0.8)],
|
|
778
879
|
code: [relevantHit(0.7)],
|
|
@@ -782,7 +883,7 @@ describe('evaluateGroundingFloor', () => {
|
|
|
782
883
|
expect(verdict.bestRelevance).toBeCloseTo(0.9, 10);
|
|
783
884
|
});
|
|
784
885
|
it('the withheld list carries every below-floor candidate as path + relevance (linked hits keep their store)', () => {
|
|
785
|
-
const verdict =
|
|
886
|
+
const verdict = evaluateFloor({
|
|
786
887
|
...emptyContext(),
|
|
787
888
|
specs: [
|
|
788
889
|
relevantHit(0.2, { filePath: 'docs/a.md' }),
|
|
@@ -794,15 +895,75 @@ describe('evaluateGroundingFloor', () => {
|
|
|
794
895
|
{ filePath: 'doctrine/b.md', sourceRepo: 'strategy', relevance: 0.1 },
|
|
795
896
|
]);
|
|
796
897
|
});
|
|
898
|
+
// --- No floor configured (mmnto-ai/totem#2727) ---
|
|
899
|
+
//
|
|
900
|
+
// `searchRelevanceFloor` lost its default, so `undefined` is the shape a
|
|
901
|
+
// repo that never set the key hands this function. The below-floor arm has
|
|
902
|
+
// to be unreachable then — not reachable at some fallback number.
|
|
903
|
+
it('with NO floor, hits far below any plausible floor still PROCEED and withhold nothing', () => {
|
|
904
|
+
const verdict = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(0.01), relevantHit(0.02)] }, isRelevanceInRange, undefined);
|
|
905
|
+
expect(verdict.refuse).toBe(false);
|
|
906
|
+
expect(verdict.withheld).toEqual([]);
|
|
907
|
+
// The MEASUREMENT is not conditional on a floor — it is still reported.
|
|
908
|
+
expect(verdict.hits).toBe(2);
|
|
909
|
+
expect(verdict.bestRelevance).toBeCloseTo(0.02, 10);
|
|
910
|
+
expect(verdict.floorExempt).toBe(0);
|
|
911
|
+
});
|
|
912
|
+
it('with NO floor, the ZERO-HIT arm still refuses (it is not a floor arm)', () => {
|
|
913
|
+
expect(evaluateGroundingFloor(emptyContext(), isRelevanceInRange, undefined)).toEqual({
|
|
914
|
+
refuse: true,
|
|
915
|
+
hits: 0,
|
|
916
|
+
bestRelevance: null,
|
|
917
|
+
withheld: [],
|
|
918
|
+
floorExempt: 0,
|
|
919
|
+
faulted: 0,
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
it('with NO floor, floorExempt is still counted for FTS-only hits', () => {
|
|
923
|
+
const verdict = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(undefined), relevantHit(0.01)] }, isRelevanceInRange, undefined);
|
|
924
|
+
expect(verdict.refuse).toBe(false);
|
|
925
|
+
expect(verdict.floorExempt).toBe(1);
|
|
926
|
+
expect(verdict.bestRelevance).toBeCloseTo(0.01, 10);
|
|
927
|
+
});
|
|
928
|
+
// The one state the mmnto-ai/totem#2758 rebase created: the all-faulted arm
|
|
929
|
+
// needs no floor (final leg, F12). A fault beside a real signal proceeds with
|
|
930
|
+
// no floor to judge; every hit faulted refuses regardless.
|
|
931
|
+
it('with NO floor, an all-faulted retrieval still REFUSES, and a fault beside real signal proceeds', () => {
|
|
932
|
+
const allFaulted = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(-0.5), relevantHit(2)] }, isRelevanceInRange, undefined);
|
|
933
|
+
expect(allFaulted).toEqual({
|
|
934
|
+
refuse: true,
|
|
935
|
+
hits: 2,
|
|
936
|
+
bestRelevance: null,
|
|
937
|
+
withheld: [],
|
|
938
|
+
floorExempt: 0,
|
|
939
|
+
faulted: 2,
|
|
940
|
+
});
|
|
941
|
+
const { message } = formatGroundingRefusal('an-unanchored-slug', allFaulted, undefined, 0);
|
|
942
|
+
expect(message).toBe([
|
|
943
|
+
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
944
|
+
'Retrieval returned 2 hits, but every one carried a relevance that is not a finite number in [0, 1] (tallied out of range by the search layer) — so nothing usable grounds this run.',
|
|
945
|
+
FLOOR_LINE_UNSET_TEXT,
|
|
946
|
+
].join('\n'));
|
|
947
|
+
const beside = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(-0.5), relevantHit(0.001)] }, isRelevanceInRange, undefined);
|
|
948
|
+
expect(beside.refuse).toBe(false);
|
|
949
|
+
expect(beside.faulted).toBe(1);
|
|
950
|
+
});
|
|
951
|
+
it('omitting the floor argument entirely is the same as passing undefined', () => {
|
|
952
|
+
const context = { ...emptyContext(), specs: [relevantHit(0.01)] };
|
|
953
|
+
expect(evaluateGroundingFloor(context, isRelevanceInRange)).toEqual(evaluateGroundingFloor(context, isRelevanceInRange, undefined));
|
|
954
|
+
});
|
|
797
955
|
});
|
|
798
956
|
// ─── formatGroundingRefusal (mmnto-ai/totem#2700) ────────
|
|
799
957
|
describe('formatGroundingRefusal', () => {
|
|
800
958
|
it('a 0-hit refusal names the topic, the 0 hits, and the floor VALUE and PLACE', () => {
|
|
801
|
-
const verdict =
|
|
959
|
+
const verdict = evaluateFloor(emptyContext(), FLOOR);
|
|
802
960
|
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, FLOOR, 0);
|
|
803
961
|
expect(message).toContain('an-unanchored-slug');
|
|
804
962
|
expect(message).toContain('0 hits');
|
|
805
|
-
|
|
963
|
+
// The WHOLE line, not a prefix of it: a `toContain` of the value-and-place
|
|
964
|
+
// text alone still passes if the retired "(schema default 0.25 when unset)"
|
|
965
|
+
// suffix is restored after it (mmnto-ai/totem#2727 fold, F3).
|
|
966
|
+
expect(floorLineOf(message)).toBe('floor 0.250 — searchRelevanceFloor in totem.config.ts');
|
|
806
967
|
});
|
|
807
968
|
// With lessons delivered, "nothing in the index grounds this run" sits beside
|
|
808
969
|
// a `Found: … N lessons` line. The message names the contradiction rather
|
|
@@ -811,26 +972,26 @@ describe('formatGroundingRefusal', () => {
|
|
|
811
972
|
// The seeded count IS the delivered count — the invariant production holds,
|
|
812
973
|
// where the caller passes `context.lessons.length`.
|
|
813
974
|
const lessons = [makeLesson(), makeLesson()];
|
|
814
|
-
const verdict =
|
|
975
|
+
const verdict = evaluateFloor({ ...emptyContext(), lessons }, FLOOR);
|
|
815
976
|
expect(verdict.hits).toBe(0);
|
|
816
977
|
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, FLOOR, lessons.length);
|
|
817
978
|
expect(message).toBe([
|
|
818
979
|
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
819
980
|
'Retrieval returned 0 grounding hits (specs, sessions, code) — nothing in the index grounds this run.',
|
|
820
|
-
'2 lessons were retrieved, but lessons do not ground a run (mmnto-ai/totem#2727
|
|
981
|
+
'2 lessons were retrieved, but lessons do not ground a run (ruled mmnto-ai/totem#2727).',
|
|
821
982
|
`floor 0.250 — ${FLOOR_PLACE_TEXT}`,
|
|
822
983
|
].join('\n'));
|
|
823
984
|
});
|
|
824
985
|
it('a single delivered lesson reads as one, not as "1 lessons"', () => {
|
|
825
|
-
const verdict =
|
|
986
|
+
const verdict = evaluateFloor({ ...emptyContext(), lessons: [makeLesson()] }, FLOOR);
|
|
826
987
|
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, FLOOR, 1);
|
|
827
|
-
expect(message).toContain('1 lesson was retrieved, but lessons do not ground a run (mmnto-ai/totem#2727
|
|
988
|
+
expect(message).toContain('1 lesson was retrieved, but lessons do not ground a run (ruled mmnto-ai/totem#2727).');
|
|
828
989
|
});
|
|
829
990
|
// Pinned against the LITERAL pre-mmnto-ai/totem#2735 message, not against a
|
|
830
991
|
// sibling call: the guarantee is that this text did not move, and only an
|
|
831
992
|
// exact comparison with the old bytes can say so.
|
|
832
993
|
it('with NO lessons delivered the 0-hit message is the pre-fold text, byte for byte', () => {
|
|
833
|
-
const verdict =
|
|
994
|
+
const verdict = evaluateFloor(emptyContext(), FLOOR);
|
|
834
995
|
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, FLOOR, 0);
|
|
835
996
|
expect(message).toBe([
|
|
836
997
|
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
@@ -838,8 +999,44 @@ describe('formatGroundingRefusal', () => {
|
|
|
838
999
|
`floor 0.250 — ${FLOOR_PLACE_TEXT}`,
|
|
839
1000
|
].join('\n'));
|
|
840
1001
|
});
|
|
1002
|
+
// --- The floor line's two forms (mmnto-ai/totem#2727) ---
|
|
1003
|
+
//
|
|
1004
|
+
// Byte-pinned in BOTH forms. The unset form must never render a number: a
|
|
1005
|
+
// refusal that reached here with no floor did so on an arm that needs none —
|
|
1006
|
+
// zero hits, or every hit faulted — and printing `floor 0.250` would claim a
|
|
1007
|
+
// judgment no floor made.
|
|
1008
|
+
it('with NO floor the 0-hit refusal names the floor as none, byte for byte', () => {
|
|
1009
|
+
const verdict = evaluateGroundingFloor(emptyContext(), isRelevanceInRange, undefined);
|
|
1010
|
+
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, undefined, 0);
|
|
1011
|
+
expect(message).toBe([
|
|
1012
|
+
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
1013
|
+
'Retrieval returned 0 hits — nothing in the index grounds this run.',
|
|
1014
|
+
FLOOR_LINE_UNSET_TEXT,
|
|
1015
|
+
].join('\n'));
|
|
1016
|
+
expect(message).not.toContain('0.250');
|
|
1017
|
+
});
|
|
1018
|
+
it('with a floor CONFIGURED the 0-hit refusal names the value and its place, byte for byte', () => {
|
|
1019
|
+
const verdict = evaluateGroundingFloor(emptyContext(), isRelevanceInRange, FLOOR);
|
|
1020
|
+
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, FLOOR, 0);
|
|
1021
|
+
expect(message).toBe([
|
|
1022
|
+
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
1023
|
+
'Retrieval returned 0 hits — nothing in the index grounds this run.',
|
|
1024
|
+
'floor 0.250 — searchRelevanceFloor in totem.config.ts',
|
|
1025
|
+
].join('\n'));
|
|
1026
|
+
});
|
|
1027
|
+
it('with NO floor the lessons clause still renders, above the none line', () => {
|
|
1028
|
+
const lessons = [makeLesson(), makeLesson()];
|
|
1029
|
+
const verdict = evaluateGroundingFloor({ ...emptyContext(), lessons }, isRelevanceInRange, undefined);
|
|
1030
|
+
const { message } = formatGroundingRefusal('an-unanchored-slug', verdict, undefined, lessons.length);
|
|
1031
|
+
expect(message).toBe([
|
|
1032
|
+
'Refusing to draft an unanchored spec for topic(s): an-unanchored-slug.',
|
|
1033
|
+
'Retrieval returned 0 grounding hits (specs, sessions, code) — nothing in the index grounds this run.',
|
|
1034
|
+
'2 lessons were retrieved, but lessons do not ground a run (ruled mmnto-ai/totem#2727).',
|
|
1035
|
+
FLOOR_LINE_UNSET_TEXT,
|
|
1036
|
+
].join('\n'));
|
|
1037
|
+
});
|
|
841
1038
|
it('the below-floor message is the pre-fold text whether or not lessons were delivered', () => {
|
|
842
|
-
const verdict =
|
|
1039
|
+
const verdict = evaluateFloor({ ...emptyContext(), specs: [relevantHit(0.2, { filePath: 'docs/a.md' })] }, FLOOR);
|
|
843
1040
|
const expected = [
|
|
844
1041
|
'Refusing to draft an unanchored spec for topic(s): weak topic.',
|
|
845
1042
|
'Retrieval returned 1 hits, but best relevance 0.200 is below the floor.',
|
|
@@ -851,7 +1048,7 @@ describe('formatGroundingRefusal', () => {
|
|
|
851
1048
|
expect(formatGroundingRefusal('weak topic', verdict, FLOOR, 0).message).toBe(expected);
|
|
852
1049
|
});
|
|
853
1050
|
it('a below-floor refusal names the best relevance and DISCLOSES every withheld candidate', () => {
|
|
854
|
-
const verdict =
|
|
1051
|
+
const verdict = evaluateFloor({
|
|
855
1052
|
...emptyContext(),
|
|
856
1053
|
specs: [
|
|
857
1054
|
relevantHit(0.2, { filePath: 'docs/a.md' }),
|
|
@@ -864,7 +1061,7 @@ describe('formatGroundingRefusal', () => {
|
|
|
864
1061
|
expect(message).toContain('2. [strategy] doctrine/b.md — relevance 0.100');
|
|
865
1062
|
});
|
|
866
1063
|
it('the hint names both cures and the --raw inspection path', () => {
|
|
867
|
-
const { recoveryHint } = formatGroundingRefusal('topic',
|
|
1064
|
+
const { recoveryHint } = formatGroundingRefusal('topic', evaluateFloor(emptyContext(), FLOOR), FLOOR, 0);
|
|
868
1065
|
expect(recoveryHint).toContain('totem spec <issue>');
|
|
869
1066
|
expect(recoveryHint).toContain('totem spec --from <record>');
|
|
870
1067
|
expect(recoveryHint).toContain('--raw');
|
|
@@ -1421,7 +1618,8 @@ describe('specCommand — anchored evidence, executed against stubbed seams', ()
|
|
|
1421
1618
|
const message = String(thrown.message);
|
|
1422
1619
|
expect(message).toContain('weak slug');
|
|
1423
1620
|
expect(message).toContain('best relevance 0.100');
|
|
1424
|
-
|
|
1621
|
+
// Full-line pin — see `floorLineOf` (mmnto-ai/totem#2727 fold, F3).
|
|
1622
|
+
expect(floorLineOf(message)).toBe('floor 0.250 — searchRelevanceFloor in totem.config.ts');
|
|
1425
1623
|
expect(message).toContain('docs/a.md — relevance 0.100');
|
|
1426
1624
|
expect(harness.orchestratorArgs).toEqual([]);
|
|
1427
1625
|
});
|
|
@@ -1445,5 +1643,40 @@ describe('specCommand — anchored evidence, executed against stubbed seams', ()
|
|
|
1445
1643
|
await specCommand(['2700'], { stdout: true });
|
|
1446
1644
|
expect(artifactRequest()['floor']).toBe(0.6);
|
|
1447
1645
|
});
|
|
1646
|
+
// --- No floor configured (mmnto-ai/totem#2727) ---
|
|
1647
|
+
it('with NO floor configured, a weak free-text run PROCEEDS instead of being refused', async () => {
|
|
1648
|
+
const { searchRelevanceFloor: _dropped, ...unfloored } = harness.config;
|
|
1649
|
+
harness.config = unfloored;
|
|
1650
|
+
// 0.1 is far below the 0.25 this suite otherwise configures — the sibling
|
|
1651
|
+
// test 'a free-text run entirely below the floor REFUSES' uses this exact
|
|
1652
|
+
// fixture WITH a floor and gets a GATE_INVALID. Same retrieval, no floor,
|
|
1653
|
+
// no refusal: that contrast is the invariant.
|
|
1654
|
+
harness.searchResults = { spec: [relevantHit(0.1, { filePath: 'docs/a.md' })] };
|
|
1655
|
+
await specCommand(['weak slug'], { stdout: true });
|
|
1656
|
+
expect(harness.orchestratorArgs.length).toBe(1);
|
|
1657
|
+
});
|
|
1658
|
+
it('with NO floor configured, the artifact OMITS grounding.floor rather than inventing one', async () => {
|
|
1659
|
+
const { searchRelevanceFloor: _dropped, ...unfloored } = harness.config;
|
|
1660
|
+
harness.config = unfloored;
|
|
1661
|
+
harness.searchResults = { spec: [relevantHit(0.9)] };
|
|
1662
|
+
await specCommand(['2700'], { stdout: true });
|
|
1663
|
+
expect(artifactRequest()['floor']).toBeUndefined();
|
|
1664
|
+
});
|
|
1665
|
+
it('with NO floor configured, a ZERO-HIT free-text run still refuses — with the none form', async () => {
|
|
1666
|
+
const { searchRelevanceFloor: _dropped, ...unfloored } = harness.config;
|
|
1667
|
+
harness.config = unfloored;
|
|
1668
|
+
let thrown;
|
|
1669
|
+
try {
|
|
1670
|
+
await specCommand(['nonsense slug'], { stdout: true });
|
|
1671
|
+
}
|
|
1672
|
+
catch (err) {
|
|
1673
|
+
thrown = err;
|
|
1674
|
+
}
|
|
1675
|
+
expect(thrown).toMatchObject({ code: 'GATE_INVALID' });
|
|
1676
|
+
const message = String(thrown.message);
|
|
1677
|
+
expect(message).toContain('Retrieval returned 0 hits');
|
|
1678
|
+
expect(message).toContain(FLOOR_LINE_UNSET_TEXT);
|
|
1679
|
+
expect(harness.orchestratorArgs).toEqual([]);
|
|
1680
|
+
});
|
|
1448
1681
|
});
|
|
1449
1682
|
//# sourceMappingURL=spec.test.js.map
|