@productbrain/cli 0.1.0-beta.6417 → 0.1.0-beta.6419

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.
@@ -3,7 +3,7 @@
3
3
  * TEN-341 test contract: capture --link calls createEntryRelation, capture --json outputs JSON.
4
4
  * WP-319 S3: grounding fires in parallel, fail-open, --suggest-only, --strict, PB_GROUNDING_MODE.
5
5
  */
6
- import { beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
  import { runCapture } from '../commands/capture.js';
8
8
  import { CLIError, ErrorCode } from '../lib/errors.js';
9
9
  import { setOutputMode } from '../lib/runner.js';
@@ -30,8 +30,8 @@ describe('runCapture --link', () => {
30
30
  // list the source entry's legal verbs in the refusal message.
31
31
  it('--link without --link-type refuses (no default), preserving the created entry', async () => {
32
32
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
33
- if (fn === 'chain.suggestLinksForCapture')
34
- return Promise.resolve([]);
33
+ if (fn === 'chain.capture')
34
+ return Promise.resolve(null);
35
35
  if (fn === 'chain.createEntry')
36
36
  return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-710' });
37
37
  if (fn === 'chain.getEntry') {
@@ -66,8 +66,8 @@ describe('runCapture --link', () => {
66
66
  });
67
67
  it('--link without --link-type: refusal message lists the source entry\'s declared relation types', async () => {
68
68
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
69
- if (fn === 'chain.suggestLinksForCapture')
70
- return Promise.resolve([]);
69
+ if (fn === 'chain.capture')
70
+ return Promise.resolve(null);
71
71
  if (fn === 'chain.createEntry')
72
72
  return Promise.resolve({ docId: 'doc-1b', entryId: 'TEN-711' });
73
73
  if (fn === 'chain.getEntry') {
@@ -104,8 +104,8 @@ describe('runCapture --link', () => {
104
104
  // the entry still gets created, but the link itself must be explicitly refused.
105
105
  it('--link " " (whitespace-only) refuses instead of silently dropping the link', async () => {
106
106
  kernelCallWithSessionMock.mockImplementation((fn) => {
107
- if (fn === 'chain.suggestLinksForCapture')
108
- return Promise.resolve([]);
107
+ if (fn === 'chain.capture')
108
+ return Promise.resolve(null);
109
109
  if (fn === 'chain.createEntry')
110
110
  return Promise.resolve({ docId: 'doc-ws', entryId: 'TEN-720' });
111
111
  return Promise.resolve({});
@@ -128,8 +128,8 @@ describe('runCapture --link', () => {
128
128
  // must check `!== undefined` instead, or this whole block (and the bcRUML refusal) never runs.
129
129
  it('--link "" (empty string) refuses instead of silently dropping the link', async () => {
130
130
  kernelCallWithSessionMock.mockImplementation((fn) => {
131
- if (fn === 'chain.suggestLinksForCapture')
132
- return Promise.resolve([]);
131
+ if (fn === 'chain.capture')
132
+ return Promise.resolve(null);
133
133
  if (fn === 'chain.createEntry')
134
134
  return Promise.resolve({ docId: 'doc-empty', entryId: 'TEN-722' });
135
135
  return Promise.resolve({});
@@ -151,8 +151,8 @@ describe('runCapture --link', () => {
151
151
  // guidance must say so rather than recommending `pb relate <verb> <missing-id>`.
152
152
  it('--link <missing-id> without --link-type reports the target was not found, not a verb-recovery command', async () => {
153
153
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
154
- if (fn === 'chain.suggestLinksForCapture')
155
- return Promise.resolve([]);
154
+ if (fn === 'chain.capture')
155
+ return Promise.resolve(null);
156
156
  if (fn === 'chain.createEntry')
157
157
  return Promise.resolve({ docId: 'doc-miss', entryId: 'TEN-721' });
158
158
  if (fn === 'chain.getEntry') {
@@ -185,8 +185,8 @@ describe('runCapture --link', () => {
185
185
  // `pb relate` would name a guaranteed-to-fail re-run.
186
186
  it('excludes replaces and do_not_confuse_with from the refusal\'s legal-verb list (both are door-blocked)', async () => {
187
187
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
188
- if (fn === 'chain.suggestLinksForCapture')
189
- return Promise.resolve([]);
188
+ if (fn === 'chain.capture')
189
+ return Promise.resolve(null);
190
190
  if (fn === 'chain.createEntry')
191
191
  return Promise.resolve({ docId: 'doc-1c', entryId: 'DEC-1' });
192
192
  if (fn === 'chain.getEntry') {
@@ -219,8 +219,8 @@ describe('runCapture --link', () => {
219
219
  // so it must never be listed as a `pb relate`-usable recovery verb.
220
220
  it('excludes a declared verb outside the kernel allowlist from the refusal\'s legal-verb list', async () => {
221
221
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
222
- if (fn === 'chain.suggestLinksForCapture')
223
- return Promise.resolve([]);
222
+ if (fn === 'chain.capture')
223
+ return Promise.resolve(null);
224
224
  if (fn === 'chain.createEntry')
225
225
  return Promise.resolve({ docId: 'doc-1d', entryId: 'GOAL-1' });
226
226
  if (fn === 'chain.getEntry') {
@@ -251,8 +251,8 @@ describe('runCapture --link', () => {
251
251
  // supply (guards.ts:349-363) — never recommend them as recovery verbs for such a target.
252
252
  it('excludes domain-tag verbs from the refusal\'s legal-verb list when the target is a domain', async () => {
253
253
  kernelCallWithSessionMock.mockImplementation((fn, args) => {
254
- if (fn === 'chain.suggestLinksForCapture')
255
- return Promise.resolve([]);
254
+ if (fn === 'chain.capture')
255
+ return Promise.resolve(null);
256
256
  if (fn === 'chain.createEntry')
257
257
  return Promise.resolve({ docId: 'doc-1e', entryId: 'INS-1' });
258
258
  if (fn === 'chain.getEntry') {
@@ -284,8 +284,8 @@ describe('runCapture --link', () => {
284
284
  it('uses the supplied relation type when --link-type provided (no probe, real write)', async () => {
285
285
  // Prefix "INS:" is extracted → skips classification, routes directly to insights
286
286
  kernelCallWithSessionMock.mockImplementation((fn) => {
287
- if (fn === 'chain.suggestLinksForCapture')
288
- return Promise.resolve([]);
287
+ if (fn === 'chain.capture')
288
+ return Promise.resolve(null);
289
289
  if (fn === 'chain.createEntry')
290
290
  return Promise.resolve({ docId: 'doc-2', entryId: 'INS-50' });
291
291
  if (fn === 'chain.createEntryRelation')
@@ -311,8 +311,8 @@ describe('runCapture --link', () => {
311
311
  // --link-without---link-type branch above).
312
312
  it('a relate-door refusal during --link is deferred, naming the persisted entry — never an unhandled rejection', async () => {
313
313
  kernelCallWithSessionMock.mockImplementation((fn) => {
314
- if (fn === 'chain.suggestLinksForCapture')
315
- return Promise.resolve([]);
314
+ if (fn === 'chain.capture')
315
+ return Promise.resolve(null);
316
316
  if (fn === 'chain.createEntry')
317
317
  return Promise.resolve({ docId: 'doc-wp', entryId: 'WP-6' });
318
318
  if (fn === 'chain.createEntryRelation') {
@@ -338,8 +338,8 @@ describe('runCapture --link', () => {
338
338
  // real error's identity.
339
339
  it('a non-validation relate-door failure during --link preserves its own code/category, not a relabeled VALIDATION_FAILED', async () => {
340
340
  kernelCallWithSessionMock.mockImplementation((fn) => {
341
- if (fn === 'chain.suggestLinksForCapture')
342
- return Promise.resolve([]);
341
+ if (fn === 'chain.capture')
342
+ return Promise.resolve(null);
343
343
  if (fn === 'chain.createEntry')
344
344
  return Promise.resolve({ docId: 'doc-wp7', entryId: 'WP-7' });
345
345
  if (fn === 'chain.createEntryRelation') {
@@ -367,8 +367,8 @@ describe('runCapture --link', () => {
367
367
  // the "was refused, re-run" template, which would prompt a blind duplicate-risking retry.
368
368
  it('a NETWORK_TIMEOUT relate-door failure during --link says outcome-unknown, never "was refused"', async () => {
369
369
  kernelCallWithSessionMock.mockImplementation((fn) => {
370
- if (fn === 'chain.suggestLinksForCapture')
371
- return Promise.resolve([]);
370
+ if (fn === 'chain.capture')
371
+ return Promise.resolve(null);
372
372
  if (fn === 'chain.createEntry')
373
373
  return Promise.resolve({ docId: 'doc-wp9', entryId: 'WP-9' });
374
374
  if (fn === 'chain.createEntryRelation') {
@@ -397,8 +397,8 @@ describe('runCapture --link', () => {
397
397
  // scheduled.
398
398
  it('the quality re-eval still runs after a relate-door refusal — the entry is not left permanently deferred', async () => {
399
399
  kernelCallWithSessionMock.mockImplementation((fn) => {
400
- if (fn === 'chain.suggestLinksForCapture')
401
- return Promise.resolve([]);
400
+ if (fn === 'chain.capture')
401
+ return Promise.resolve(null);
402
402
  if (fn === 'chain.createEntry')
403
403
  return Promise.resolve({ docId: 'doc-wp8', entryId: 'WP-8' });
404
404
  if (fn === 'chain.createEntryRelation') {
@@ -424,8 +424,8 @@ describe('runCapture --link', () => {
424
424
  // fallback (formatters/capture.ts:105).
425
425
  it('the human-readable receipt prints the real relation type, never the dead surfaces_tension_in fallback', async () => {
426
426
  kernelCallWithSessionMock.mockImplementation((fn) => {
427
- if (fn === 'chain.suggestLinksForCapture')
428
- return Promise.resolve([]);
427
+ if (fn === 'chain.capture')
428
+ return Promise.resolve(null);
429
429
  if (fn === 'chain.createEntry')
430
430
  return Promise.resolve({ docId: 'doc-2b', entryId: 'INS-51' });
431
431
  if (fn === 'chain.createEntryRelation')
@@ -453,8 +453,8 @@ describe('runCapture --link', () => {
453
453
  // persisted write may claim `linkedTo`/`relationType`.
454
454
  it('a governs-consent proposal diversion (no relationId) is never reported as a persisted link', async () => {
455
455
  kernelCallWithSessionMock.mockImplementation((fn) => {
456
- if (fn === 'chain.suggestLinksForCapture')
457
- return Promise.resolve([]);
456
+ if (fn === 'chain.capture')
457
+ return Promise.resolve(null);
458
458
  if (fn === 'chain.createEntry')
459
459
  return Promise.resolve({ docId: 'doc-3', entryId: 'WPKG-1' });
460
460
  if (fn === 'chain.createEntryRelation') {
@@ -493,8 +493,8 @@ describe('runCapture --link', () => {
493
493
  // deferred-then-never-run schedule would leave the entry permanently stuck.
494
494
  it('--link without --link-type does NOT defer the LLM schedule (nothing will ever un-defer it)', async () => {
495
495
  kernelCallWithSessionMock.mockImplementation((fn) => {
496
- if (fn === 'chain.suggestLinksForCapture')
497
- return Promise.resolve([]);
496
+ if (fn === 'chain.capture')
497
+ return Promise.resolve(null);
498
498
  if (fn === 'chain.createEntry')
499
499
  return Promise.resolve({ docId: 'doc-4', entryId: 'TEN-20' });
500
500
  if (fn === 'chain.createEntryRelation')
@@ -512,8 +512,8 @@ describe('runCapture --link', () => {
512
512
  // instructions must reach the operator, not just whichever check runs first.
513
513
  it('surfaces BOTH the fields-rejected error and the link-type refusal when they co-occur', async () => {
514
514
  kernelCallWithSessionMock.mockImplementation((fn) => {
515
- if (fn === 'chain.suggestLinksForCapture')
516
- return Promise.resolve([]);
515
+ if (fn === 'chain.capture')
516
+ return Promise.resolve(null);
517
517
  if (fn === 'chain.createEntry') {
518
518
  return Promise.resolve({
519
519
  docId: 'doc-5', entryId: 'TEN-21',
@@ -545,8 +545,8 @@ describe('runCapture --link', () => {
545
545
  // error code must still see the timeout, not a generic validation failure.
546
546
  it('the combined fields-rejected + link-refused error preserves the link failure\'s own code (NETWORK_TIMEOUT), not VALIDATION_FAILED', async () => {
547
547
  kernelCallWithSessionMock.mockImplementation((fn) => {
548
- if (fn === 'chain.suggestLinksForCapture')
549
- return Promise.resolve([]);
548
+ if (fn === 'chain.capture')
549
+ return Promise.resolve(null);
550
550
  if (fn === 'chain.createEntry') {
551
551
  return Promise.resolve({
552
552
  docId: 'doc-10', entryId: 'TEN-22',
@@ -583,8 +583,8 @@ describe('runCapture --entry-type', () => {
583
583
  });
584
584
  it('forwards --entry-type as canonicalKey on the chain.createEntry payload', async () => {
585
585
  kernelCallWithSessionMock.mockImplementation((fn) => {
586
- if (fn === 'chain.suggestLinksForCapture')
587
- return Promise.resolve([]);
586
+ if (fn === 'chain.capture')
587
+ return Promise.resolve(null);
588
588
  if (fn === 'chain.createEntry')
589
589
  return Promise.resolve({ docId: 'doc-3', entryId: 'OBJ-1' });
590
590
  return Promise.resolve({});
@@ -599,8 +599,8 @@ describe('runCapture --entry-type', () => {
599
599
  });
600
600
  it('omits canonicalKey entirely when --entry-type is absent (byte-identical default path)', async () => {
601
601
  kernelCallWithSessionMock.mockImplementation((fn) => {
602
- if (fn === 'chain.suggestLinksForCapture')
603
- return Promise.resolve([]);
602
+ if (fn === 'chain.capture')
603
+ return Promise.resolve(null);
604
604
  if (fn === 'chain.createEntry')
605
605
  return Promise.resolve({ docId: 'doc-4', entryId: 'OBJ-1' });
606
606
  return Promise.resolve({});
@@ -618,8 +618,8 @@ describe('runCapture --entry-type', () => {
618
618
  // straight to the shared Objectives drawer with no classification call at all.
619
619
  it('--entry-type objective alone (no --collection) skips classification and routes to the Objectives drawer (bcI42)', async () => {
620
620
  kernelCallWithSessionMock.mockImplementation((fn) => {
621
- if (fn === 'chain.suggestLinksForCapture')
622
- return Promise.resolve([]);
621
+ if (fn === 'chain.capture')
622
+ return Promise.resolve(null);
623
623
  if (fn === 'chain.createEntry')
624
624
  return Promise.resolve({ docId: 'doc-5', entryId: 'OBJ-2' });
625
625
  return Promise.resolve({});
@@ -640,8 +640,8 @@ describe('runCapture --entry-type', () => {
640
640
  // OWN 1:1 drawer (key-results, idPrefix KR) — it no longer shares the Objectives drawer.
641
641
  it('--entry-type key_result alone (no --collection) routes to the Key Results drawer (DEC-1622)', async () => {
642
642
  kernelCallWithSessionMock.mockImplementation((fn) => {
643
- if (fn === 'chain.suggestLinksForCapture')
644
- return Promise.resolve([]);
643
+ if (fn === 'chain.capture')
644
+ return Promise.resolve(null);
645
645
  if (fn === 'chain.createEntry')
646
646
  return Promise.resolve({ docId: 'doc-6', entryId: 'KR-2' });
647
647
  return Promise.resolve({});
@@ -661,8 +661,8 @@ describe('runCapture --entry-type', () => {
661
661
  // classification (often landing in 'strategy') and the placement guard then rejects it.
662
662
  it('--entry-type with mixed case/whitespace still routes to the Objectives drawer, no classification call (bclZ9)', async () => {
663
663
  kernelCallWithSessionMock.mockImplementation((fn) => {
664
- if (fn === 'chain.suggestLinksForCapture')
665
- return Promise.resolve([]);
664
+ if (fn === 'chain.capture')
665
+ return Promise.resolve(null);
666
666
  if (fn === 'chain.createEntry')
667
667
  return Promise.resolve({ docId: 'doc-7', entryId: 'OBJ-3' });
668
668
  return Promise.resolve({});
@@ -681,8 +681,8 @@ describe('runCapture --entry-type', () => {
681
681
  // branches (strategy) still skips classification and routes to its own drawer.
682
682
  it('--entry-type strategy alone (no --collection) skips classification and routes to the Strategy drawer — proves the derivation is general, not a hardcoded pair', async () => {
683
683
  kernelCallWithSessionMock.mockImplementation((fn) => {
684
- if (fn === 'chain.suggestLinksForCapture')
685
- return Promise.resolve([]);
684
+ if (fn === 'chain.capture')
685
+ return Promise.resolve(null);
686
686
  if (fn === 'chain.createEntry')
687
687
  return Promise.resolve({ docId: 'doc-8', entryId: 'STR-1' });
688
688
  return Promise.resolve({});
@@ -701,8 +701,8 @@ describe('runCapture --entry-type', () => {
701
701
  // --entry-type derivation silently can't resolve them during a chain.listCollections outage.
702
702
  it('--entry-type vision alone (no --collection) skips classification and routes to the Vision drawer, even offline', async () => {
703
703
  kernelCallWithSessionMock.mockImplementation((fn) => {
704
- if (fn === 'chain.suggestLinksForCapture')
705
- return Promise.resolve([]);
704
+ if (fn === 'chain.capture')
705
+ return Promise.resolve(null);
706
706
  if (fn === 'chain.createEntry')
707
707
  return Promise.resolve({ docId: 'doc-9', entryId: 'VIS-1' });
708
708
  return Promise.resolve({});
@@ -725,8 +725,8 @@ describe('runCapture --json', () => {
725
725
  it('outputs valid JSON with id field and no other stdout', async () => {
726
726
  // Prefix "TEN:" is extracted → skips classification
727
727
  kernelCallWithSessionMock.mockImplementation((fn) => {
728
- if (fn === 'chain.suggestLinksForCapture')
729
- return Promise.resolve([]);
728
+ if (fn === 'chain.capture')
729
+ return Promise.resolve(null);
730
730
  if (fn === 'chain.createEntry')
731
731
  return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-711' });
732
732
  return Promise.resolve({});
@@ -777,8 +777,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
777
777
  };
778
778
  it('--json output includes the raw coaching object from the write response', async () => {
779
779
  kernelCallWithSessionMock.mockImplementation((fn) => {
780
- if (fn === 'chain.suggestLinksForCapture')
781
- return Promise.resolve([]);
780
+ if (fn === 'chain.capture')
781
+ return Promise.resolve(null);
782
782
  if (fn === 'chain.createEntry')
783
783
  return Promise.resolve({ docId: 'doc-c1', entryId: 'DEC-77', coaching });
784
784
  return Promise.resolve({});
@@ -805,8 +805,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
805
805
  });
806
806
  it('--json output omits coaching when the write response has none (backward compat)', async () => {
807
807
  kernelCallWithSessionMock.mockImplementation((fn) => {
808
- if (fn === 'chain.suggestLinksForCapture')
809
- return Promise.resolve([]);
808
+ if (fn === 'chain.capture')
809
+ return Promise.resolve(null);
810
810
  if (fn === 'chain.createEntry')
811
811
  return Promise.resolve({ docId: 'doc-c2', entryId: 'DEC-78' });
812
812
  return Promise.resolve({});
@@ -846,8 +846,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
846
846
  };
847
847
  it('--link: re-evaluates after the relation lands and renders the post-relation verdict', async () => {
848
848
  kernelCallWithSessionMock.mockImplementation((fn) => {
849
- if (fn === 'chain.suggestLinksForCapture')
850
- return Promise.resolve([]);
849
+ if (fn === 'chain.capture')
850
+ return Promise.resolve(null);
851
851
  if (fn === 'chain.createEntry')
852
852
  return Promise.resolve({ docId: 'doc-c3', entryId: 'DEC-79', coaching });
853
853
  if (fn === 'chain.createEntryRelation')
@@ -890,8 +890,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
890
890
  });
891
891
  it('--link: keeps the create-response verdict when the re-eval fails (fail-open, BR-144)', async () => {
892
892
  kernelCallWithSessionMock.mockImplementation((fn) => {
893
- if (fn === 'chain.suggestLinksForCapture')
894
- return Promise.resolve([]);
893
+ if (fn === 'chain.capture')
894
+ return Promise.resolve(null);
895
895
  if (fn === 'chain.createEntry')
896
896
  return Promise.resolve({ docId: 'doc-c4', entryId: 'DEC-80', coaching });
897
897
  if (fn === 'chain.createEntryRelation')
@@ -921,8 +921,8 @@ describe('runCapture coaching passthrough (WP-475 E1)', () => {
921
921
  });
922
922
  it('no --link: does NOT call the re-evaluation and does NOT defer the LLM schedule (plain capture unchanged)', async () => {
923
923
  kernelCallWithSessionMock.mockImplementation((fn) => {
924
- if (fn === 'chain.suggestLinksForCapture')
925
- return Promise.resolve([]);
924
+ if (fn === 'chain.capture')
925
+ return Promise.resolve(null);
926
926
  if (fn === 'chain.createEntry')
927
927
  return Promise.resolve({ docId: 'doc-c5', entryId: 'DEC-81', coaching });
928
928
  return Promise.resolve({});
@@ -950,8 +950,8 @@ describe('runCapture --field passthrough', () => {
950
950
  });
951
951
  it('merges --field pairs into data', async () => {
952
952
  kernelCallWithSessionMock.mockImplementation((fn) => {
953
- if (fn === 'chain.suggestLinksForCapture')
954
- return Promise.resolve([]);
953
+ if (fn === 'chain.capture')
954
+ return Promise.resolve(null);
955
955
  if (fn === 'chain.createEntry')
956
956
  return Promise.resolve({ docId: 'doc-6', entryId: 'STD-1' });
957
957
  return Promise.resolve({});
@@ -972,8 +972,8 @@ describe('runCapture --field passthrough', () => {
972
972
  });
973
973
  it('--field description=X overrides the derived description', async () => {
974
974
  kernelCallWithSessionMock.mockImplementation((fn) => {
975
- if (fn === 'chain.suggestLinksForCapture')
976
- return Promise.resolve([]);
975
+ if (fn === 'chain.capture')
976
+ return Promise.resolve(null);
977
977
  if (fn === 'chain.createEntry')
978
978
  return Promise.resolve({ docId: 'doc-7', entryId: 'TEN-1' });
979
979
  return Promise.resolve({});
@@ -997,8 +997,8 @@ describe('runCapture WHY pre-flight', () => {
997
997
  });
998
998
  it('blocks with guidance when governed and whyField is missing', async () => {
999
999
  kernelCallWithSessionMock.mockImplementation((fn) => {
1000
- if (fn === 'chain.suggestLinksForCapture')
1001
- return Promise.resolve([]);
1000
+ if (fn === 'chain.capture')
1001
+ return Promise.resolve(null);
1002
1002
  if (fn === 'chain.getCollectionFields') {
1003
1003
  return Promise.resolve({ governed: true, whyField: 'rationale', minChars: 12, fields: [] });
1004
1004
  }
@@ -1011,8 +1011,8 @@ describe('runCapture WHY pre-flight', () => {
1011
1011
  });
1012
1012
  it('does not block when whyField is description', async () => {
1013
1013
  kernelCallWithSessionMock.mockImplementation((fn) => {
1014
- if (fn === 'chain.suggestLinksForCapture')
1015
- return Promise.resolve([]);
1014
+ if (fn === 'chain.capture')
1015
+ return Promise.resolve(null);
1016
1016
  if (fn === 'chain.getCollectionFields') {
1017
1017
  return Promise.resolve({ governed: true, whyField: 'description', minChars: 12, fields: [] });
1018
1018
  }
@@ -1030,8 +1030,8 @@ describe('runCapture WHY pre-flight', () => {
1030
1030
  // remaps data.description → rationale before the WHY gate. A bare `pb capture "DEC: …"`
1031
1031
  // (description set, no --field rationale) must NOT be false-blocked client-side.
1032
1032
  kernelCallWithSessionMock.mockImplementation((fn) => {
1033
- if (fn === 'chain.suggestLinksForCapture')
1034
- return Promise.resolve([]);
1033
+ if (fn === 'chain.capture')
1034
+ return Promise.resolve(null);
1035
1035
  if (fn === 'chain.getCollectionFields') {
1036
1036
  return Promise.resolve({
1037
1037
  governed: true,
@@ -1054,8 +1054,8 @@ describe('runCapture WHY pre-flight', () => {
1054
1054
  // proof_governed has whyField='rationale' but no descriptionFieldKey remap, so a bare
1055
1055
  // capture (no --field rationale) is correctly blocked — the description will not satisfy it.
1056
1056
  kernelCallWithSessionMock.mockImplementation((fn) => {
1057
- if (fn === 'chain.suggestLinksForCapture')
1058
- return Promise.resolve([]);
1057
+ if (fn === 'chain.capture')
1058
+ return Promise.resolve(null);
1059
1059
  if (fn === 'chain.getCollectionFields') {
1060
1060
  return Promise.resolve({
1061
1061
  governed: true,
@@ -1074,8 +1074,8 @@ describe('runCapture WHY pre-flight', () => {
1074
1074
  });
1075
1075
  it('fails open and proceeds when the contract fetch fails', async () => {
1076
1076
  kernelCallWithSessionMock.mockImplementation((fn) => {
1077
- if (fn === 'chain.suggestLinksForCapture')
1078
- return Promise.resolve([]);
1077
+ if (fn === 'chain.capture')
1078
+ return Promise.resolve(null);
1079
1079
  if (fn === 'chain.getCollectionFields')
1080
1080
  return Promise.reject(new Error('network'));
1081
1081
  if (fn === 'chain.createEntry')
@@ -1093,8 +1093,8 @@ describe('runCapture WHY pre-flight', () => {
1093
1093
  });
1094
1094
  it('fails open when the contract fetch rejects with a CLIError (server 4xx/5xx surfaces as McpError, an instanceof CLIError)', async () => {
1095
1095
  kernelCallWithSessionMock.mockImplementation((fn) => {
1096
- if (fn === 'chain.suggestLinksForCapture')
1097
- return Promise.resolve([]);
1096
+ if (fn === 'chain.capture')
1097
+ return Promise.resolve(null);
1098
1098
  if (fn === 'chain.getCollectionFields') {
1099
1099
  return Promise.reject(new CLIError('Could not reach test.convex.site.', {
1100
1100
  code: ErrorCode.NETWORK_UNREACHABLE,
@@ -1117,8 +1117,8 @@ describe('runCapture WHY pre-flight', () => {
1117
1117
  it('surfaces the server WHY-gate reason on rejection', async () => {
1118
1118
  const serverReason = 'Governance entries (standard) require an explicit WHY before they can be created. Set "rationale" to at least 12 characters.';
1119
1119
  kernelCallWithSessionMock.mockImplementation((fn) => {
1120
- if (fn === 'chain.suggestLinksForCapture')
1121
- return Promise.resolve([]);
1120
+ if (fn === 'chain.capture')
1121
+ return Promise.resolve(null);
1122
1122
  if (fn === 'chain.getCollectionFields') {
1123
1123
  return Promise.resolve({ governed: true, whyField: 'rationale', minChars: 12, fields: [] });
1124
1124
  }
@@ -1150,8 +1150,8 @@ describe('runCapture date inference', () => {
1150
1150
  });
1151
1151
  it('does not default date to today when no source date is present', async () => {
1152
1152
  kernelCallWithSessionMock.mockImplementation((fn) => {
1153
- if (fn === 'chain.suggestLinksForCapture')
1154
- return Promise.resolve([]);
1153
+ if (fn === 'chain.capture')
1154
+ return Promise.resolve(null);
1155
1155
  if (fn === 'chain.resolveCollection')
1156
1156
  return Promise.resolve({ collection: 'decisions', confidence: 0.9, tier: 'high' });
1157
1157
  if (fn === 'chain.createEntry')
@@ -1170,8 +1170,8 @@ describe('runCapture date inference', () => {
1170
1170
  });
1171
1171
  it('uses inferred source date when the capture text includes one', async () => {
1172
1172
  kernelCallWithSessionMock.mockImplementation((fn) => {
1173
- if (fn === 'chain.suggestLinksForCapture')
1174
- return Promise.resolve([]);
1173
+ if (fn === 'chain.capture')
1174
+ return Promise.resolve(null);
1175
1175
  if (fn === 'chain.resolveCollection')
1176
1176
  return Promise.resolve({ collection: 'decisions', confidence: 0.9, tier: 'high' });
1177
1177
  if (fn === 'chain.createEntry')
@@ -1186,28 +1186,32 @@ describe('runCapture date inference', () => {
1186
1186
  });
1187
1187
  });
1188
1188
  // ---------------------------------------------------------------------------
1189
- // WP-319 S3: Pre-write grounding tests
1189
+ // WP-319 S3 / WP-755 C1a: Pre-write grounding tests (the one capture door)
1190
1190
  // ---------------------------------------------------------------------------
1191
- describe('runCapture WP-319 grounding', () => {
1192
- const emptyGrounding = { index: 0, related: [], duplicates: [], governance: [], scanTruncated: false, v: 1 };
1193
- const groundingWithDuplicate = {
1194
- index: 0,
1195
- related: [],
1196
- duplicates: [{ entryId: 'TEN-5', name: 'Similar tension', collectionSlug: 'tensions', matchType: 'possible_duplicate', overlapRatio: 0.9 }],
1197
- governance: [],
1198
- v: 1,
1191
+ describe('runCapture WP-319/WP-755 grounding', () => {
1192
+ const emptyVerdict = { kind: 'create_and_link', candidates: [], scanTruncated: false };
1193
+ const askVerdict = {
1194
+ kind: 'ask',
1195
+ candidates: [{ entryId: 'TEN-5', name: 'Similar tension', collectionSlug: 'tensions', score: 0.6 }],
1196
+ scanTruncated: false,
1199
1197
  };
1200
1198
  beforeEach(() => {
1201
1199
  kernelCallWithSessionMock.mockReset();
1202
1200
  mockSession = { sessionId: 'sess-test' };
1203
1201
  delete process.env.PB_GROUNDING_MODE;
1202
+ // Pre-existing gap this fix round's new --strict tests surfaced: runner.ts's outputMode is
1203
+ // module-level `let` state, set permanently by any earlier test in this file that passed
1204
+ // `json: true` (setOutputMode is never test-scoped). Reset it here so a later test's
1205
+ // `!json` branches (the --strict prompt gate) are never silently skipped by an earlier
1206
+ // test's leaked JSON mode.
1207
+ setOutputMode('auto');
1204
1208
  });
1205
- it('fires chain.suggestLinksForCapture in parallel and still calls createEntry', async () => {
1209
+ it('fires chain.capture in parallel and still calls createEntry', async () => {
1206
1210
  // Prefix-based route skips classification; grounding fires for TEN prefix
1207
1211
  kernelCallWithSessionMock
1208
1212
  .mockImplementation((fn) => {
1209
- if (fn === 'chain.suggestLinksForCapture')
1210
- return Promise.resolve([emptyGrounding]);
1213
+ if (fn === 'chain.capture')
1214
+ return Promise.resolve(emptyVerdict);
1211
1215
  if (fn === 'chain.createEntry')
1212
1216
  return Promise.resolve({ docId: 'doc-1', entryId: 'TEN-720' });
1213
1217
  return Promise.resolve({});
@@ -1215,8 +1219,8 @@ describe('runCapture WP-319 grounding', () => {
1215
1219
  const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
1216
1220
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1217
1221
  await runCapture({ text: 'TEN: grounding test tension' });
1218
- expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.suggestLinksForCapture', expect.objectContaining({
1219
- entries: expect.arrayContaining([expect.objectContaining({ name: 'grounding test tension' })]),
1222
+ expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.capture', expect.objectContaining({
1223
+ name: 'grounding test tension',
1220
1224
  }));
1221
1225
  expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
1222
1226
  writeSpy.mockRestore();
@@ -1225,7 +1229,7 @@ describe('runCapture WP-319 grounding', () => {
1225
1229
  it('does NOT block capture when grounding call fails', async () => {
1226
1230
  kernelCallWithSessionMock
1227
1231
  .mockImplementation((fn) => {
1228
- if (fn === 'chain.suggestLinksForCapture')
1232
+ if (fn === 'chain.capture')
1229
1233
  return Promise.reject(new Error('network error'));
1230
1234
  if (fn === 'chain.createEntry')
1231
1235
  return Promise.resolve({ docId: 'doc-2', entryId: 'TEN-721' });
@@ -1239,12 +1243,14 @@ describe('runCapture WP-319 grounding', () => {
1239
1243
  writeSpy.mockRestore();
1240
1244
  stderrSpy.mockRestore();
1241
1245
  });
1242
- it('--suggest-only does NOT call createEntry and outputs grounding data', async () => {
1243
- // In test env (non-TTY), isJsonMode() returns true → JSON path is taken.
1246
+ it('--suggest-only does NOT call createEntry and outputs the verdict', async () => {
1247
+ // Explicit json:true — isJsonMode() is module-level state (runner.ts) with no TTY inference
1248
+ // since WP-744/1e-B; this test wants the JSON preview shape, so it asks for it directly
1249
+ // rather than relying on another test's leaked global mode.
1244
1250
  kernelCallWithSessionMock
1245
1251
  .mockImplementation((fn) => {
1246
- if (fn === 'chain.suggestLinksForCapture')
1247
- return Promise.resolve([groundingWithDuplicate]);
1252
+ if (fn === 'chain.capture')
1253
+ return Promise.resolve(askVerdict);
1248
1254
  return Promise.resolve({});
1249
1255
  });
1250
1256
  let stdoutOutput = '';
@@ -1253,9 +1259,9 @@ describe('runCapture WP-319 grounding', () => {
1253
1259
  return true;
1254
1260
  });
1255
1261
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1256
- await runCapture({ text: 'TEN: duplicate candidate', suggestOnly: true });
1262
+ await runCapture({ text: 'TEN: possible match candidate', suggestOnly: true, json: true });
1257
1263
  expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
1258
- // JSON output contains suggestOnly flag and grounding report
1264
+ // JSON output contains suggestOnly flag and the verdict report
1259
1265
  const jsonLines = stdoutOutput.split('\n').filter((l) => { try {
1260
1266
  JSON.parse(l);
1261
1267
  return true;
@@ -1273,8 +1279,8 @@ describe('runCapture WP-319 grounding', () => {
1273
1279
  it('--suggest-only with --json outputs machine-readable object without calling createEntry', async () => {
1274
1280
  kernelCallWithSessionMock
1275
1281
  .mockImplementation((fn) => {
1276
- if (fn === 'chain.suggestLinksForCapture')
1277
- return Promise.resolve([emptyGrounding]);
1282
+ if (fn === 'chain.capture')
1283
+ return Promise.resolve(emptyVerdict);
1278
1284
  return Promise.resolve({});
1279
1285
  });
1280
1286
  let stdoutOutput = '';
@@ -1298,11 +1304,11 @@ describe('runCapture WP-319 grounding', () => {
1298
1304
  writeSpy.mockRestore();
1299
1305
  stderrSpy.mockRestore();
1300
1306
  });
1301
- it('banner is suppressed when grounding has no matches (zero-noise floor)', async () => {
1307
+ it('banner is suppressed when the verdict has no candidates (zero-noise floor)', async () => {
1302
1308
  kernelCallWithSessionMock
1303
1309
  .mockImplementation((fn) => {
1304
- if (fn === 'chain.suggestLinksForCapture')
1305
- return Promise.resolve([emptyGrounding]);
1310
+ if (fn === 'chain.capture')
1311
+ return Promise.resolve(emptyVerdict);
1306
1312
  if (fn === 'chain.createEntry')
1307
1313
  return Promise.resolve({ docId: 'doc-3', entryId: 'TEN-722' });
1308
1314
  return Promise.resolve({});
@@ -1314,13 +1320,13 @@ describe('runCapture WP-319 grounding', () => {
1314
1320
  return true;
1315
1321
  });
1316
1322
  await runCapture({ text: 'TEN: no matches tension' });
1317
- // No "Grounding" banner in stderr when zero matches
1318
- const groundingInStderr = stderrLines.some((l) => l.includes('Grounding'));
1323
+ // No "Related"/"Possible match" banner in stderr when the verdict has zero candidates.
1324
+ const groundingInStderr = stderrLines.some((l) => l.includes('Related') || l.includes('Possible match'));
1319
1325
  expect(groundingInStderr).toBe(false);
1320
1326
  writeSpy.mockRestore();
1321
1327
  stderrSpy.mockRestore();
1322
1328
  });
1323
- it('PB_GROUNDING_MODE=off skips grounding call entirely', async () => {
1329
+ it('PB_GROUNDING_MODE=off skips the door entirely', async () => {
1324
1330
  process.env.PB_GROUNDING_MODE = 'off';
1325
1331
  kernelCallWithSessionMock
1326
1332
  .mockImplementation((fn) => {
@@ -1331,7 +1337,7 @@ describe('runCapture WP-319 grounding', () => {
1331
1337
  const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
1332
1338
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1333
1339
  await runCapture({ text: 'TEN: no grounding env off' });
1334
- expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.suggestLinksForCapture', expect.anything());
1340
+ expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.capture', expect.anything());
1335
1341
  expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
1336
1342
  delete process.env.PB_GROUNDING_MODE;
1337
1343
  writeSpy.mockRestore();
@@ -1340,8 +1346,8 @@ describe('runCapture WP-319 grounding', () => {
1340
1346
  it('--json output stdout is clean (no grounding on stdout)', async () => {
1341
1347
  kernelCallWithSessionMock
1342
1348
  .mockImplementation((fn) => {
1343
- if (fn === 'chain.suggestLinksForCapture')
1344
- return Promise.resolve([groundingWithDuplicate]);
1349
+ if (fn === 'chain.capture')
1350
+ return Promise.resolve(askVerdict);
1345
1351
  if (fn === 'chain.createEntry')
1346
1352
  return Promise.resolve({ docId: 'doc-5', entryId: 'TEN-724' });
1347
1353
  return Promise.resolve({});
@@ -1366,9 +1372,90 @@ describe('runCapture WP-319 grounding', () => {
1366
1372
  expect(parsed.id).toBe('TEN-724');
1367
1373
  // No grounding content in stdout
1368
1374
  expect(stdoutOutput).not.toContain('Grounding');
1369
- expect(stdoutOutput).not.toContain('duplicate');
1375
+ expect(stdoutOutput).not.toContain('Possible match');
1370
1376
  writeSpy.mockRestore();
1371
1377
  stderrSpy.mockRestore();
1372
1378
  });
1379
+ // WP-755 C1a fix round 1 (item 2): --strict must prompt on "version" too, not just "ask" — a
1380
+ // 0.9-overlap duplicate returns "version" (silent-confidence band), and until C2 lands, that
1381
+ // verdict's write is still a plain create; --strict must not silently create it unconfirmed.
1382
+ const versionVerdict = {
1383
+ kind: 'version',
1384
+ entryId: 'TEN-9',
1385
+ score: 0.95,
1386
+ name: 'Existing near-duplicate tension',
1387
+ collectionSlug: 'tensions',
1388
+ docId: 'doc-9',
1389
+ scanTruncated: false,
1390
+ };
1391
+ const originalStdinIsTTY = process.stdin.isTTY;
1392
+ function mockInteractiveStdin(answer) {
1393
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
1394
+ vi.spyOn(process.stdin, 'once').mockImplementation((_event, cb) => {
1395
+ cb(Buffer.from(answer));
1396
+ return process.stdin;
1397
+ });
1398
+ }
1399
+ afterEach(() => {
1400
+ // vitest's own hook, not a manual end-of-test call — runs even when an assertion above threw,
1401
+ // so one failing case in this block can never leak its stdin mock into the next test.
1402
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
1403
+ vi.restoreAllMocks();
1404
+ });
1405
+ it('--strict prompts on a "version" verdict and proceeds on "y"', async () => {
1406
+ kernelCallWithSessionMock
1407
+ .mockImplementation((fn) => {
1408
+ if (fn === 'chain.capture')
1409
+ return Promise.resolve(versionVerdict);
1410
+ if (fn === 'chain.createEntry')
1411
+ return Promise.resolve({ docId: 'doc-10', entryId: 'TEN-726' });
1412
+ return Promise.resolve({});
1413
+ });
1414
+ mockInteractiveStdin('y');
1415
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
1416
+ const stderrLines = [];
1417
+ vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1418
+ stderrLines.push(String(chunk));
1419
+ return true;
1420
+ });
1421
+ await runCapture({ text: 'TEN: near duplicate strict version', strict: true });
1422
+ expect(stderrLines.some((l) => l.includes('Proceed with capture?'))).toBe(true);
1423
+ expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
1424
+ });
1425
+ it('--strict prompts on a "version" verdict and cancels on "n" (no createEntry call)', async () => {
1426
+ kernelCallWithSessionMock
1427
+ .mockImplementation((fn) => {
1428
+ if (fn === 'chain.capture')
1429
+ return Promise.resolve(versionVerdict);
1430
+ if (fn === 'chain.createEntry')
1431
+ return Promise.resolve({ docId: 'doc-11', entryId: 'TEN-727' });
1432
+ return Promise.resolve({});
1433
+ });
1434
+ mockInteractiveStdin('n');
1435
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
1436
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1437
+ await runCapture({ text: 'TEN: near duplicate strict version cancel', strict: true });
1438
+ expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.createEntry', expect.anything());
1439
+ });
1440
+ it('--strict prompts on an "ask" verdict too (unchanged from before this fix round)', async () => {
1441
+ kernelCallWithSessionMock
1442
+ .mockImplementation((fn) => {
1443
+ if (fn === 'chain.capture')
1444
+ return Promise.resolve(askVerdict);
1445
+ if (fn === 'chain.createEntry')
1446
+ return Promise.resolve({ docId: 'doc-12', entryId: 'TEN-728' });
1447
+ return Promise.resolve({});
1448
+ });
1449
+ mockInteractiveStdin('y');
1450
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
1451
+ const stderrLines = [];
1452
+ vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
1453
+ stderrLines.push(String(chunk));
1454
+ return true;
1455
+ });
1456
+ await runCapture({ text: 'TEN: ask strict prompt', strict: true });
1457
+ expect(stderrLines.some((l) => l.includes('Proceed with capture?'))).toBe(true);
1458
+ expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.anything());
1459
+ });
1373
1460
  });
1374
1461
  //# sourceMappingURL=capture.test.js.map