@mmnto/cli 1.124.0 → 2.0.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.
Files changed (37) hide show
  1. package/dist/commands/doctor.d.ts +16 -10
  2. package/dist/commands/doctor.d.ts.map +1 -1
  3. package/dist/commands/doctor.js +162 -54
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/doctor.test.js +444 -28
  6. package/dist/commands/doctor.test.js.map +1 -1
  7. package/dist/commands/hook-totemdir-render.test.js +55 -1
  8. package/dist/commands/hook-totemdir-render.test.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +39 -0
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/init.test.js +32 -0
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/install-hooks-exit-contract.test.js +32 -1
  15. package/dist/commands/install-hooks-exit-contract.test.js.map +1 -1
  16. package/dist/commands/install-hooks.d.ts +143 -10
  17. package/dist/commands/install-hooks.d.ts.map +1 -1
  18. package/dist/commands/install-hooks.js +441 -69
  19. package/dist/commands/install-hooks.js.map +1 -1
  20. package/dist/commands/install-hooks.test.js +1075 -7
  21. package/dist/commands/install-hooks.test.js.map +1 -1
  22. package/dist/commands/spec-templates.d.ts +17 -4
  23. package/dist/commands/spec-templates.d.ts.map +1 -1
  24. package/dist/commands/spec-templates.js +23 -3
  25. package/dist/commands/spec-templates.js.map +1 -1
  26. package/dist/commands/spec.d.ts +57 -8
  27. package/dist/commands/spec.d.ts.map +1 -1
  28. package/dist/commands/spec.js +124 -28
  29. package/dist/commands/spec.js.map +1 -1
  30. package/dist/commands/spec.test.js +277 -44
  31. package/dist/commands/spec.test.js.map +1 -1
  32. package/dist/utils.d.ts +5 -3
  33. package/dist/utils.d.ts.map +1 -1
  34. package/dist/utils.js.map +1 -1
  35. package/dist/utils.test.js +8 -3
  36. package/dist/utils.test.js.map +1 -1
  37. 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, hasUnrenderableHookChar, PROMPT_SOURCE_BUILTIN, PROMPT_SOURCE_OVERRIDE, TotemConfigError, } from '@mmnto/totem';
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 (the mmnto-ai/totem#2692 C4 predicate)', () => {
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, control character or non-ASCII
537
- // byte would break the `sh` single-quoted word, the JS string literal
538
- // inside it, or both and could forge hook lines.
539
- expect(hasUnrenderableHookChar(section), `${section} cannot be rendered`).toBe(false);
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 both promised sections, in prompt order', () => {
543
- expect([...SPEC_REQUIRED_SECTIONS]).toEqual([
544
- '### Problem Statement',
545
- '### Implementation Tasks',
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 (schema default 0.25 when unset)';
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 = evaluateGroundingFloor(emptyContext(), FLOOR);
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 = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(0.2), relevantHit(0.11)] }, FLOOR);
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 = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(0.1), relevantHit(FLOOR)] }, FLOOR);
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 = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(0.05)], code: [relevantHit(undefined)] }, FLOOR);
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 = evaluateGroundingFloor({ ...emptyContext(), lessons: [relevantHit(undefined, { type: 'lesson' })] }, FLOOR);
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 = evaluateGroundingFloor({
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 = evaluateGroundingFloor({
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 = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(undefined), relevantHit(undefined)] }, FLOOR);
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 non-finite relevance is what the CORE builder drops: the item it writes
750
- // carries no relevance at all, exactly an FTS-only hit's shape. The floor
751
- // must read it the same way, or the artifact and the judgment disagree.
752
- it.each([NaN, Infinity, -Infinity])('a non-finite relevance (%s) is floor-EXEMPT, never counted as signal', (relevance) => {
753
- const verdict = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(relevance)] }, FLOOR);
754
- expect(verdict.floorExempt).toBe(1);
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(false);
806
+ expect(verdict.refuse).toBe(true);
758
807
  });
759
- it('a NaN hit beside a genuinely weak one is not disclosed as a withheld candidate', () => {
760
- const verdict = evaluateGroundingFloor({
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
- // One exempt hit is enough to proceed — and nothing is withheld.
765
- expect(verdict.floorExempt).toBe(1);
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 = evaluateGroundingFloor({
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 = evaluateGroundingFloor({
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 = evaluateGroundingFloor(emptyContext(), FLOOR);
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
- expect(message).toContain('floor 0.250 searchRelevanceFloor in totem.config.ts (schema default 0.25 when unset)');
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 = evaluateGroundingFloor({ ...emptyContext(), lessons }, FLOOR);
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 rules whether they may).',
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 = evaluateGroundingFloor({ ...emptyContext(), lessons: [makeLesson()] }, FLOOR);
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 rules whether they may).');
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 = evaluateGroundingFloor(emptyContext(), FLOOR);
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 = evaluateGroundingFloor({ ...emptyContext(), specs: [relevantHit(0.2, { filePath: 'docs/a.md' })] }, FLOOR);
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 = evaluateGroundingFloor({
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', evaluateGroundingFloor(emptyContext(), FLOOR), FLOOR, 0);
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
- expect(message).toContain('floor 0.250searchRelevanceFloor in totem.config.ts (schema default 0.25 when unset)');
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