@wirecat/cli-core 0.376.0 → 0.377.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.377.0 — 20.10.2026
11
+
12
+ ### Added
13
+
14
+ - **A document hit says how many other paragraphs match.** `ProjectEvidenceItem.moreMatches` (on a document item
15
+ with `around`) counts the paragraphs after the one `around` centres on that also name a lane word. See
16
+ [project gathering](docs/dev/PROJECT-GATHERING.md).
17
+
18
+ ### Fixed
19
+
20
+ - **A document hit's `around` followed the lane that found it first, not the document.** It centred on that lane's
21
+ first matching paragraph (in a measured 145 KB document, paragraph 60 found by the key, not paragraph 4 that
22
+ names the project). It now centres on the first paragraph that names a word of any lane, in document order, as
23
+ the guide says. No extra reads or charged bytes.
24
+
10
25
  ## 0.376.0 — 20.10.2026
11
26
 
12
27
  ### Added
@@ -20,6 +20,20 @@ export interface ProjectNeighbourReader {
20
20
  }[]>;
21
21
  }
22
22
  export declare const storedMessageNeighbours: (messages: Pick<MessagesStore, "around">) => ProjectNeighbourReader;
23
+ /**
24
+ * The paragraphs around the first one that names a word of any query, in document order, with the hit's own marked
25
+ * found, and how many later paragraphs name one too.
26
+ */
27
+ export declare const documentMatches: (text: string, queries: string[], window: {
28
+ before: number;
29
+ after: number;
30
+ }) => {
31
+ around: {
32
+ text: string;
33
+ found: boolean;
34
+ }[];
35
+ moreMatches: number;
36
+ };
23
37
  /** The paragraphs around the first one that names the query, in document order; the hit's own is marked found. */
24
38
  export declare const documentPassage: (text: string, query: string, window: {
25
39
  before: number;
@@ -19,22 +19,30 @@ export const storedMessageNeighbours = (messages) => ({
19
19
  },
20
20
  });
21
21
  const wordsOf = (text) => (text.match(/[\p{L}\p{N}\p{M}]+/gu) ?? []).map(fold);
22
- /** The paragraphs around the first one that names the query, in document order; the hit's own is marked found. */
23
- export const documentPassage = (text, query, window) => {
22
+ /**
23
+ * The paragraphs around the first one that names a word of any query, in document order, with the hit's own marked
24
+ * found, and how many later paragraphs name one too.
25
+ */
26
+ export const documentMatches = (text, queries, window) => {
24
27
  const paragraphs = text
25
28
  .split(/\n\s*\n/)
26
29
  .map((part) => part.replace(/\s+/g, " ").trim())
27
30
  .filter(Boolean);
31
+ const query = queries.join(" ");
28
32
  const words = new Set(wordsOf(query));
29
- const hit = paragraphs.findIndex((part) => wordsOf(part).some((word) => words.has(word)));
30
- if (hit < 0 || paragraphs.length < 2)
31
- return [];
33
+ const matching = paragraphs.flatMap((part, index) => (wordsOf(part).some((word) => words.has(word)) ? [index] : []));
34
+ const hit = matching[0];
35
+ if (hit === undefined || paragraphs.length < 2)
36
+ return { around: [], moreMatches: 0 };
32
37
  const from = Math.max(0, hit - window.before);
33
- return paragraphs.slice(from, hit + window.after + 1).map((part, index) => ({
38
+ const around = paragraphs.slice(from, hit + window.after + 1).map((part, index) => ({
34
39
  text: excerpt(part, query, 200, { literal: true }),
35
40
  found: from + index === hit,
36
41
  }));
42
+ return { around, moreMatches: matching.length - 1 };
37
43
  };
44
+ /** The paragraphs around the first one that names the query, in document order; the hit's own is marked found. */
45
+ export const documentPassage = (text, query, window) => documentMatches(text, [query], window).around;
38
46
  /**
39
47
  * Neighbours are context for reading a hit, not matches: they never join `items`, and their text counts against
40
48
  * the run's read bytes.
@@ -36,6 +36,8 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
36
36
  fingerprint: string;
37
37
  around?: ProjectNeighbour[] | null;
38
38
  aroundIssue?: string | null;
39
+ /** A document hit with `around`: how many paragraphs after the one it centres on also name a lane word. */
40
+ moreMatches?: number;
39
41
  }
40
42
  /** Strongest first: a hit that names the project, then one only from a linked chat, then one naming only a person. */
41
43
  export type ProjectEvidenceContext = "project" | "linked-chat" | "person-only";
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { CONTEXT_MAX_READ_BYTES, contextEvidenceFingerprint } from "../context/evidence.js";
3
3
  import { createContextEvidenceReader } from "./context-evidence.js";
4
4
  import { excerpt } from "./excerpt.js";
5
- import { attachNeighbours, documentPassage, } from "./project-gathering-around.js";
5
+ import { attachNeighbours, documentMatches, } from "./project-gathering-around.js";
6
6
  import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
7
7
  const terminal = new Set(["done", "unavailable", "failed"]);
8
8
  const CONTEXTS = ["person-only", "linked-chat", "project"];
@@ -31,6 +31,7 @@ export const gatherProjectEvidence = async (ports, request) => {
31
31
  plan.sources.map(() => ({ state: "not-requested", history: null, code: null }));
32
32
  const seen = new Set(previous?.seen ?? []);
33
33
  const signal = plan.signal;
34
+ const queries = plan.lanes.flatMap((lane) => (lane.query === null ? [] : [lane.query]));
34
35
  if (plan.fetch === "before-read" && !ports.fetcher)
35
36
  throw new CliError("configuration_error", "this host cannot fetch source history; use fetch none", {
36
37
  param: "fetch",
@@ -114,8 +115,10 @@ export const gatherProjectEvidence = async (ports, request) => {
114
115
  context: contextOf(lane),
115
116
  };
116
117
  if (kind === "document" && lane.query !== null && plan.around.before + plan.around.after) {
118
+ const passage = documentMatches(item.text, queries, plan.around);
117
119
  entry.around = [];
118
- for (const part of documentPassage(item.text, lane.query, plan.around)) {
120
+ entry.moreMatches = passage.moreMatches;
121
+ for (const part of passage.around) {
119
122
  const bytes = Buffer.byteLength(part.text);
120
123
  if (bytes > plan.budgets.maxReadBytes - usage.readBytes - usage.aroundBytes) {
121
124
  entry.aroundIssue = "read-bytes";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.376.0",
3
+ "version": "0.377.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",