ucn 5.6.0 → 5.6.1

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.
@@ -54,6 +54,9 @@ const CASE_NODES = new Set(['switch_case', 'switch_default']);
54
54
  // after the block is reached is a property of the manager.
55
55
  const MANAGER_NODES = new Set(['with_statement', 'with_clause']);
56
56
  const CALL_NODES = new Set(['call_expression', 'call']);
57
+ // Invocation syntax across the parsers; member references are not calls.
58
+ const OUTGOING_CALL_NODES = new Set(['call_expression', 'call', 'method_invocation',
59
+ 'invocation_expression', 'object_creation_expression', 'new_expression']);
57
60
  const AWAIT_NODES = new Set(['await_expression', 'await']);
58
61
 
59
62
  // Nodes that hold declarations rather than being one: a callable's
@@ -120,6 +123,29 @@ function tokensOf(node, skip, out = []) {
120
123
  return out;
121
124
  }
122
125
 
126
+ // A baseline invocation is reused only when the whole callable still has
127
+ // the same written context, apart from direct primitive call arguments.
128
+ // This is a preservation premise, not a proof that an arbitrary new call
129
+ // is available (access, generics, receiver modes and result use included).
130
+ const DIRECT_LITERAL_NODES = new Set(['number', 'integer', 'float', 'number_literal',
131
+ 'integer_literal', 'int_literal', 'decimal_integer_literal', 'float_literal', 'real_literal',
132
+ 'string', 'string_literal', 'true', 'false', 'boolean_literal']);
133
+ function invocationTokens(node, out = []) {
134
+ if (!node || COMMENT_NODES.has(node.type)) return out;
135
+ const parent = node.parent;
136
+ const list = parent?.type === 'argument' && parent.childCount === 1 ? parent.parent : parent;
137
+ if (DIRECT_LITERAL_NODES.has(node.type) && list &&
138
+ ['arguments', 'argument_list'].includes(list.type)) {
139
+ const kind = ['true', 'false', 'boolean_literal'].includes(node.type) ? 'boolean'
140
+ : ['string', 'string_literal'].includes(node.type) && ['"', "'"].includes(node.text[0]) ? 'string'
141
+ : /^\d+$/.test(node.text) && Number(node.text) <= 127 ? 'smallInteger' : null;
142
+ if (kind) { out.push(['argument-literal', kind]); return out; }
143
+ }
144
+ if (node.childCount === 0) { out.push([node.type, node.text]); return out; }
145
+ for (const child of node.children) invocationTokens(child, out);
146
+ return out;
147
+ }
148
+
123
149
  function identifiersOf(node, out = new Set()) {
124
150
  if (!node) return out;
125
151
  if (node.childCount === 0) {
@@ -659,7 +685,102 @@ function callableContract(root, language, symbol, context = null) {
659
685
  }
660
686
  memberBindings = JSON.stringify(parts.sort());
661
687
  }
662
- return { header: JSON.stringify(header), generator, returnForm, inferred, derivation, memberBindings };
688
+ const outgoingCalls = [];
689
+ const contextStatements = [];
690
+ // Native comment queries preserve extra-node identity. The binding's
691
+ // ordinary children wrappers can expose a Python inline-suite comment
692
+ // as a zero-child block, so type-testing that walk loses directives.
693
+ const bodyComments = body ? body.descendantsOfType([...COMMENT_NODES]) : [];
694
+ const commentSpans = new Set(bodyComments.map(comment => `${comment.startIndex}:${comment.endIndex}`));
695
+ const comments = bodyComments.filter(comment => traits.checkDirectiveComment?.(comment.text));
696
+ const visit = candidate => {
697
+ if (OUTGOING_CALL_NODES.has(candidate.type)) outgoingCalls.push({ node: candidate,
698
+ start: candidate.startIndex, end: candidate.endIndex });
699
+ if (!sameNode(candidate, body) && (bindingTargets(candidate).length || STATEMENT_SHAPE.test(candidate.type))) {
700
+ contextStatements.push(candidate);
701
+ }
702
+ return true;
703
+ };
704
+ if (body) { visit(body); walkOwn(body, visit); }
705
+ const parameters = node.childForFieldName('parameters') || node.childForFieldName('parameter');
706
+ // A discard occupies syntax but introduces no binding. Its spelling
707
+ // must not connect otherwise independent statements (Go `_ = 0`).
708
+ // Keep all invocation/RHS tokens: discarding a result does not erase
709
+ // the call that produces it.
710
+ const dependencyNames = candidate => [...identifiersOf(candidate)]
711
+ .filter(name => !(traits.discardIdentifierNames || []).includes(name));
712
+ const contextFacts = contextStatements.map(statement => ({
713
+ names: dependencyNames(statement), tokens: tokensOf(statement),
714
+ }));
715
+ const statementsByName = new Map();
716
+ contextFacts.forEach((fact, position) => {
717
+ for (const name of fact.names) {
718
+ if (!statementsByName.has(name)) statementsByName.set(name, []);
719
+ statementsByName.get(name).push(position);
720
+ }
721
+ });
722
+ // Checker comment position is measured against code tokens, so blank
723
+ // lines and ordinary comments do not change the call's applicability.
724
+ const sourceTokens = [];
725
+ const collectTokens = candidate => {
726
+ if (!candidate || COMMENT_NODES.has(candidate.type) ||
727
+ commentSpans.has(`${candidate.startIndex}:${candidate.endIndex}`)) return;
728
+ if (candidate.childCount === 0) { sourceTokens.push(candidate); return; }
729
+ for (const child of candidate.children) collectTokens(child);
730
+ };
731
+ if (comments.length) collectTokens(body);
732
+ const tokenPosition = offset => {
733
+ let low = 0;
734
+ let high = sourceTokens.length;
735
+ while (low < high) {
736
+ const mid = (low + high) >>> 1;
737
+ if (sourceTokens[mid].startIndex < offset) low = mid + 1;
738
+ else high = mid;
739
+ }
740
+ return low;
741
+ };
742
+ const commentFacts = comments.map(comment => {
743
+ const at = tokenPosition(comment.startIndex);
744
+ return [comment.text, at,
745
+ sourceTokens[at - 1]?.endPosition.row === comment.startPosition.row,
746
+ sourceTokens[at]?.startPosition.row === comment.endPosition.row];
747
+ });
748
+ const invocationContext = JSON.stringify([invocationTokens(declaration), commentFacts]);
749
+ for (const call of outgoingCalls) {
750
+ // A call's own tokens do not establish its meaning. Follow the
751
+ // written bindings of its callee, receiver and arguments (including
752
+ // aliases), and retain the checker comments around it.
753
+ const names = dependencyNames(call.node);
754
+ const seenNames = new Set(names);
755
+ const selected = new Set();
756
+ for (let at = 0; at < names.length; at++) {
757
+ for (const position of statementsByName.get(names[at]) || []) {
758
+ if (selected.has(position)) continue;
759
+ selected.add(position);
760
+ for (const name of contextFacts[position].names) {
761
+ if (!seenNames.has(name)) { seenNames.add(name); names.push(name); }
762
+ }
763
+ }
764
+ }
765
+ const nesting = [];
766
+ for (let parent = call.node.parent; parent && !sameNode(parent, body); parent = parent.parent) {
767
+ nesting.push(parent.type);
768
+ }
769
+ call.invocationKey = JSON.stringify(invocationTokens(call.node));
770
+ const relevantParameters = (parameters?.namedChildren || []).filter(parameter =>
771
+ dependencyNames(parameter).some(name => seenNames.has(name)));
772
+ const returnsValue = sameNode(call.node, body) || nesting.some(type => RESULT_NODES.has(type));
773
+ call.key = JSON.stringify([tokensOf(call.node), relevantParameters.map(parameter => tokensOf(parameter)),
774
+ returnsValue ? symbol.returnType || null : null, nesting,
775
+ [...selected].sort((a, b) => a - b).map(position => contextFacts[position].tokens),
776
+ commentFacts.map(([text, at, prior, next]) => [text, at - tokenPosition(call.node.startIndex), prior, next])]);
777
+ delete call.node;
778
+ }
779
+ const contract = { header: JSON.stringify(header), generator, returnForm, inferred, derivation, memberBindings };
780
+ Object.defineProperties(contract, {
781
+ outgoingCalls: { value: outgoingCalls }, invocationContext: { value: invocationContext },
782
+ });
783
+ return contract;
663
784
  }
664
785
 
665
786
  /**
@@ -680,7 +801,20 @@ function classifyChange(before, after, language) {
680
801
  reasons.push('inferred-return-contract');
681
802
  }
682
803
  if ((before.memberBindings || null) !== (after.memberBindings || null)) reasons.push('inferred-member-type');
683
- return { kind: reasons.length > 0 ? 'contract' : 'inert', reasons };
804
+ const change = { kind: reasons.length > 0 ? 'contract' : 'inert', reasons };
805
+ // Moving or formatting a call does not make it a new call. Repeated
806
+ // calls are counted, so an extra occurrence is still checked.
807
+ const prior = new Map();
808
+ for (const call of before.outgoingCalls || []) prior.set(call.key, (prior.get(call.key) || 0) + 1);
809
+ const preserved = new Set((before.outgoingCalls || []).map(call => call.invocationKey));
810
+ const changedCalls = (after.outgoingCalls || []).filter(call => {
811
+ const left = prior.get(call.key) || 0;
812
+ if (left) { prior.set(call.key, left - 1); return false; }
813
+ call.stableInvocation = before.invocationContext === after.invocationContext && preserved.has(call.invocationKey);
814
+ return true;
815
+ });
816
+ Object.defineProperty(change, 'changedCalls', { value: changedCalls });
817
+ return change;
684
818
  }
685
819
 
686
820
  /** The declaration in the base revision that a changed one continues. */
@@ -821,6 +955,90 @@ function removedFrom(before, after) {
821
955
  return removed;
822
956
  }
823
957
 
958
+ /** Checker comments carry semantics even when no declaration changed. */
959
+ function checkingContext(root, traits) {
960
+ if (!root || !traits.checkDirectiveComment) return null;
961
+ const all = root.descendantsOfType([...COMMENT_NODES]);
962
+ const comments = all.filter(node => traits.checkDirectiveComment(node.text));
963
+ if (comments.length === 0) return null;
964
+ const barriers = all.filter(node => !traits.checkDirectiveComment(node.text) && traits.checkDirectiveBarrier?.(node.text));
965
+ const spans = new Set(all.map(node => `${node.startIndex}:${node.endIndex}`));
966
+ const code = [];
967
+ const scopeCounts = new Map();
968
+ const walk = (node, scope = []) => {
969
+ if (spans.has(`${node.startIndex}:${node.endIndex}`)) return;
970
+ if (CALLABLE_NODES.has(node.type) || ['class', 'class_declaration', 'class_definition'].includes(node.type)) {
971
+ const body = node.childForFieldName('body');
972
+ if (body) {
973
+ const header = tokensOf(node, body);
974
+ const key = JSON.stringify([scope, node.type, header]);
975
+ const ordinal = scopeCounts.get(key) || 0;
976
+ scopeCounts.set(key, ordinal + 1);
977
+ scope = [...scope, [node.type, header, ordinal]];
978
+ }
979
+ }
980
+ if (node.childCount === 0) code.push({ node, scope });
981
+ else for (const child of node.children) walk(child, scope);
982
+ };
983
+ walk(root);
984
+ // Identify the neighboring code by its line tokens and declaration,
985
+ // not by an absolute token offset. An inert edit in an earlier body
986
+ // cannot move a later directive onto different code. Repeated equal
987
+ // lines retain an occurrence number, so moving a suppression between
988
+ // identical calls still changes its attachment.
989
+ const lines = [];
990
+ for (const entry of code) {
991
+ let line = lines[lines.length - 1];
992
+ if (!line || line.row !== entry.node.startPosition.row) {
993
+ line = { row: entry.node.startPosition.row, entries: [] };
994
+ lines.push(line);
995
+ }
996
+ entry.line = line;
997
+ entry.column = line.entries.length;
998
+ line.entries.push(entry);
999
+ }
1000
+ const lineCounts = new Map();
1001
+ for (const line of lines) {
1002
+ line.key = JSON.stringify(line.entries.map(({ node, scope }) => [node.type, node.text, scope]));
1003
+ line.ordinal = lineCounts.get(line.key) || 0;
1004
+ lineCounts.set(line.key, line.ordinal + 1);
1005
+ }
1006
+ const anchor = entry => entry ? [entry.line.key, entry.line.ordinal, entry.column] : null;
1007
+ const position = node => {
1008
+ let low = 0;
1009
+ let high = code.length;
1010
+ while (low < high) {
1011
+ const middle = (low + high) >>> 1;
1012
+ if (code[middle].node.startIndex < node.startIndex) low = middle + 1;
1013
+ else high = middle;
1014
+ }
1015
+ const at = [anchor(code[low - 1]), anchor(code[low]),
1016
+ code[low - 1]?.node.endPosition.row === node.startPosition.row,
1017
+ code[low]?.node.startPosition.row === node.endPosition.row];
1018
+ return { at, nextRow: code[low]?.node.startPosition.row };
1019
+ };
1020
+ return JSON.stringify([
1021
+ comments.map(node => {
1022
+ const mode = traits.checkFileDirectiveComment?.(node.text) || 'local';
1023
+ const { at, nextRow } = position(node);
1024
+ let attachment;
1025
+ if (mode === 'column-zero-file') attachment = node.startPosition.column === 0;
1026
+ else if (mode === 'local' || mode === 'leading-or-local' && at[0] !== null) {
1027
+ if (traits.checkDirectiveTarget === 'next-line') {
1028
+ // A line directive applies forward. Earlier code is
1029
+ // not its target; a block comment matters only if it
1030
+ // occupies a line between the directive and its code.
1031
+ const blocked = barriers.some(barrier =>
1032
+ barrier.startPosition.row < nextRow &&
1033
+ barrier.endPosition.row > node.endPosition.row);
1034
+ attachment = [at[1], at[2], at[3], blocked];
1035
+ } else attachment = at;
1036
+ } else attachment = mode === 'file' || at[0] === null;
1037
+ return [node.text, mode, attachment];
1038
+ }),
1039
+ ]);
1040
+ }
1041
+
824
1042
  /**
825
1043
  * What a change did to a module outside its declarations.
826
1044
  *
@@ -852,6 +1070,7 @@ function moduleChange(before, after, traits) {
852
1070
  const exported = explicit(before.fileEntry);
853
1071
  return {
854
1072
  removedNames,
1073
+ checkingChanged: checkingContext(before.root, traits) !== checkingContext(after.root, traits),
855
1074
  removedExports: removedNames.filter(name => exported.has(name)),
856
1075
  // The module passed on everything another module exports, and no
857
1076
  // longer does: which names that took away is not written here.
package/core/check.js CHANGED
@@ -21,10 +21,13 @@
21
21
  * blocked a break was observed: a confirmed call site that no longer
22
22
  * fits the signature, an import of a name its module no
23
23
  * longer has, a bare call nothing is left to answer.
24
- * review the change can reach existing code and the engine cannot
25
- * settle whether it breaks: the reason is named and the sites
26
- * are listed.
27
- * ready nothing that existed before depends on what changed.
24
+ * review change exposure or call requirements are not settled:
25
+ * the reason is named and the sites are listed.
26
+ * ready no unresolved exposure remains in the modeled checks.
27
+ *
28
+ * A changed outgoing call is checked too. Positive argument fit preserves
29
+ * a baseline invocation only in an unchanged written context; it is not a
30
+ * compiler proof for arbitrary new calls or surrounding expressions.
28
31
  *
29
32
  * TRUST is the worst verdict among the items. BLOCKED means a definite
30
33
  * break, or that the check itself could not run. It never means "there is
@@ -35,14 +38,15 @@
35
38
 
36
39
  const { diffImpact, composeAccount, callNotResolvedEntries } = require('./analysis');
37
40
  const { providedNames } = require('./change-kinds');
41
+ const { outgoingChecker } = require('./outgoing-check');
38
42
  const fs = require('fs');
39
43
  const path = require('path');
40
44
  const { isValueDefinition, isTestPath } = require('./shared');
41
45
  const { langTraits } = require('../languages');
42
46
  const { assessDeletion, definiteDependentsOf, REASON_TEXT } = require('./deletion');
43
- const { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, HAZARD_TEXT } = require('./capture');
47
+ const { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, newOwnerUnreached, HAZARD_TEXT } = require('./capture');
44
48
  const { reviewItemsOf } = require('./review-items');
45
- const { createTestModel, linkFiles, commandsFor } = require('./test-units');
49
+ const { createTestModel, linkFiles, commandsFor, testSiteKey } = require('./test-units');
46
50
  // Reasons a file has no command that are about the file, not the runner.
47
51
  const FILE_WOULD_NOT_RUN = new Set(['declared-indirectly', 'collection-switched-off', 'conditional-tests']);
48
52
  const { resolveTarget, flaggedPaths } = require('./snapshot');
@@ -65,6 +69,7 @@ const MODULE_TEXT = Object.freeze({
65
69
  'no-longer-provided': 'other files import a name from this module that it no longer provides',
66
70
  'import-removed-still-used': 'an import was removed and the file still uses the name it bound',
67
71
  'imports-removed': 'a module the file imported or included without binding a name is no longer imported',
72
+ 'checking-context-changed': 'checker directives changed; previously unchecked source may now be checked',
68
73
  'module-level-code': 'code outside any declaration changed; what runs when the module loads is not modeled',
69
74
  'name-in-use': 'a new import binds a name the file already used',
70
75
  'downstream-exposure': 'a name left the exports of a library: consumers outside the repository may use it',
@@ -167,7 +172,10 @@ function callersOf(index, name, definition) {
167
172
  */
168
173
  function assessAddition(index, entry, definition, addedLinesByFile, account, judge, nonCallable = false) {
169
174
  if (!definition) return { verdict: 'review', reasons: ['declaration-not-indexed'] };
170
- const occurrences = occurrencesOutsideChange(index, entry.name, addedLinesByFile, definition);
175
+ const ownerExisted = ownerExistedBefore(index, definition, addedLinesByFile);
176
+ const occurrences = !ownerExisted && newOwnerUnreached(index, definition, addedLinesByFile)
177
+ ? { outside: [], total: 0 }
178
+ : occurrencesOutsideChange(index, entry.name, addedLinesByFile, definition);
171
179
  const summary = summarizeAccount(account);
172
180
  const reasons = [];
173
181
  if (occurrences.outside.length > 0) reasons.push('name-in-use');
@@ -182,7 +190,6 @@ function assessAddition(index, entry, definition, addedLinesByFile, account, jud
182
190
  try {
183
191
  contract = require('./deadcode').contractFactsOf(index, definition, index.files.get(definition.file)?.language);
184
192
  } catch (_) { contract = null; }
185
- const ownerExisted = ownerExistedBefore(index, definition, addedLinesByFile);
186
193
  let shape;
187
194
  try { shape = ownerExisted ? shapeChangeOf(index, definition, addedLinesByFile) : null; } catch (_) { shape = 'shape'; }
188
195
  const hazards = captureHazards(index, definition, { ownerExisted, contract, shield, nonCallable, shape });
@@ -403,6 +410,7 @@ function assessModuleChange(index, moved, judge) {
403
410
  // Loose code in a test file is the test: nothing imports it for
404
411
  // what it does when it loads.
405
412
  if (moved.codeChanged && !isTestPath(moved.relativePath)) reasons.push('module-level-code');
413
+ if (moved.checkingChanged) reasons.push('checking-context-changed');
406
414
  if ((moved.shadowed || []).length > 0) reasons.push('name-in-use');
407
415
  return {
408
416
  verdict: reasons.length > 0 ? 'review' : 'ready', reasons,
@@ -686,6 +694,7 @@ function check(live, options = {}) {
686
694
  }
687
695
  throw e;
688
696
  }
697
+ target.index._beginOp();
689
698
  try {
690
699
  const result = checkTarget(target.index, options, target.repoRoot);
691
700
  const notes = [...target.notes];
@@ -703,6 +712,7 @@ function check(live, options = {}) {
703
712
  if (notes.length > 0 && result && result.ok !== false) result.notes = notes;
704
713
  return result;
705
714
  } finally {
715
+ target.index._endOp();
706
716
  target.dispose();
707
717
  }
708
718
  }
@@ -848,6 +858,26 @@ function checkTarget(index, options, repoRoot) {
848
858
  const changed = limit ? allChanged.slice(0, limit) : allChanged;
849
859
 
850
860
  const items = [];
861
+ const outgoing = outgoingChecker(index, judge, addedLinesByFile);
862
+ const withOutgoing = (item, fn) => {
863
+ if (fn._kind === 'deleted') return item;
864
+ const result = outgoing(definitionOf(index, fn), fn.change, fn._kind === 'added');
865
+ if (!result.mismatches.length && !result.unsettled.length) return item;
866
+ const blocking = result.mismatches.filter(row => !row.unchecked).length;
867
+ item.signatureMismatches += blocking;
868
+ if (result.mismatches.length) item.mismatches = [...(item.mismatches || []), ...result.mismatches];
869
+ if (result.unsettled.length) item.outgoingReview = result.unsettled;
870
+ if (blocking) {
871
+ item.verdict = 'blocked';
872
+ item.reasons = [...new Set(['outgoing-signature-mismatch', ...item.reasons])];
873
+ } else {
874
+ if (item.verdict === 'ready') item.verdict = 'review';
875
+ item.reasons = [...new Set([...item.reasons,
876
+ ...(result.mismatches.length ? ['site-not-checked'] : []),
877
+ ...(result.unsettled.length ? ['outgoing-call-unverified'] : [])])];
878
+ }
879
+ return item;
880
+ };
851
881
 
852
882
  for (const fn of changed) {
853
883
  const filePath = fn.relativePath || fn.file || '';
@@ -897,7 +927,7 @@ function checkTarget(index, options, repoRoot) {
897
927
  item.verdict = 'review';
898
928
  item.reasons = [valueDeclaration ? 'value-change' : 'declaration-change'];
899
929
  }
900
- items.push(item);
930
+ items.push(withOutgoing(item, fn));
901
931
  continue;
902
932
  }
903
933
 
@@ -920,7 +950,7 @@ function checkTarget(index, options, repoRoot) {
920
950
  // The contract every caller relies on is untouched.
921
951
  item.verdict = 'ready';
922
952
  item.reasons = [];
923
- items.push(item);
953
+ items.push(withOutgoing(item, fn));
924
954
  continue;
925
955
  }
926
956
  // The base revision decides which call sites the changed
@@ -975,7 +1005,7 @@ function checkTarget(index, options, repoRoot) {
975
1005
  }
976
1006
  }
977
1007
  item.reviewItems = reviewItemsOf(unverifiedCallers);
978
- items.push(item);
1008
+ items.push(withOutgoing(item, fn));
979
1009
  continue;
980
1010
  }
981
1011
 
@@ -1010,7 +1040,7 @@ function checkTarget(index, options, repoRoot) {
1010
1040
  try { isEntry = isEntry || !!adapter?.isEntryPoint?.(definition); } catch (_) { /* keep */ }
1011
1041
  item.orphan = !isEntry;
1012
1042
  }
1013
- items.push(item);
1043
+ items.push(withOutgoing(item, fn));
1014
1044
  }
1015
1045
 
1016
1046
  // Deleted callables: the base revision's declaration, assessed against
@@ -1067,6 +1097,7 @@ function checkTarget(index, options, repoRoot) {
1067
1097
  className: fn.className || undefined,
1068
1098
  _callerCache: callerCache,
1069
1099
  _testModel: model,
1100
+ _testSitesOnly: true,
1070
1101
  });
1071
1102
  } catch (e) {
1072
1103
  t = null;
@@ -1074,12 +1105,14 @@ function checkTarget(index, options, repoRoot) {
1074
1105
  for (const tf of (t && t.testFiles) || []) {
1075
1106
  let held = linesByFile.get(tf.file);
1076
1107
  if (!held) {
1077
- held = { row: tf, lines: new Set(), linked: new Set() };
1108
+ held = { row: tf, lines: new Map(), linked: new Set() };
1078
1109
  linesByFile.set(tf.file, held);
1079
1110
  }
1080
1111
  for (const name of tf.linkedFunctions || []) held.linked.add(name);
1081
1112
  for (const match of tf.matches || []) {
1082
- if (match.matchType === 'call' || match.matchType === 'test-case') held.lines.add(match.line);
1113
+ if (match.matchType === 'call' || match.matchType === 'test-case') {
1114
+ held.lines.set(testSiteKey(match), match);
1115
+ }
1083
1116
  }
1084
1117
  }
1085
1118
  }
@@ -1092,7 +1125,7 @@ function checkTarget(index, options, repoRoot) {
1092
1125
  // no link stands in.
1093
1126
  const linesOf = new Map();
1094
1127
  for (const [rel, held] of linesByFile) {
1095
- if (absoluteTest.has(rel)) linesOf.set(absoluteTest.get(rel), [...held.lines]);
1128
+ if (absoluteTest.has(rel)) linesOf.set(absoluteTest.get(rel), [...held.lines.values()]);
1096
1129
  }
1097
1130
  testFiles = [];
1098
1131
  supportFiles = [];
@@ -52,7 +52,7 @@ const COMMAND_CONTRACTS = Object.freeze({
52
52
  { name: 'definition', when: 'Default or `type` is a symbol kind such as function/class.', answer: 'Matching indexed definitions with location and signature metadata.' },
53
53
  { name: 'type lookup', when: '`type=type`.', answer: 'Type-definition records rather than general name matches.' },
54
54
  ],
55
- defaults: ['Substring matching unless `exact=true`.', 'Tests excluded unless `includeTests=true`.', '`limit` is the single result cap; `withSource=true` attaches exact bodies.'],
55
+ defaults: ['Substring matching unless `exact=true`.', 'Test definitions included unless `includeTests=false`.', '`limit` is the single result cap; `withSource=true` attaches exact bodies.'],
56
56
  truth: 'Results are exact records from the static symbol index; usage counts are static indexed occurrences.',
57
57
  nonGoals: ['Text search inside comments or configuration.', 'Compiler overload resolution across external dependencies.'],
58
58
  invalidCombinations: ['A missing name is rejected.'],
@@ -183,7 +183,7 @@ const COMMAND_CONTRACTS = Object.freeze({
183
183
  target: 'Optional symbol/handle. Omitting it selects diff/precommit mode.',
184
184
  modes: [
185
185
  { name: 'symbol', when: 'A symbol target is supplied; `base` names the revision whose declaration its call sites were written against.', answer: 'Argument-count compatibility, uncertain sites, and the caller account.' },
186
- { name: 'diff', when: 'No target is supplied; `base` or `staged` selects changes.', answer: 'A verdict per changed symbol by what the change did: blocked (a break was observed, every site listed), review (can reach existing code, reason named) or ready (nothing that existed depends on it); TRUST is the worst of them. Affected tests.' },
186
+ { name: 'diff', when: 'No target is supplied; `base` or `staged` selects changes.', answer: 'A verdict per changed symbol by what the change did: blocked (a break was observed, every site listed), review (change exposure or call requirements not settled, reason named) or ready (no unresolved exposure in the modeled checks, not compiler or runtime proof); TRUST is the worst of them. Affected tests.' },
187
187
  ],
188
188
  defaults: ['Symbol mode checks static arity, not compiler type compatibility.', '`base` defaults to HEAD in both modes.', 'Diff mode: `staged` reads the staged tree from the git index, never the worktree; `strict` makes REVIEW_REQUIRED fail as BLOCKED does.'],
189
189
  truth: 'Findings are static diagnostics with explicit uncertainty; a clean result is not a compiler/test pass.',