@hraness/wordcell 0.23.0 → 0.24.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.
Files changed (45) hide show
  1. package/README.md +29 -16
  2. package/dist/authoring.js +2 -1
  3. package/dist/cli.js +10 -9
  4. package/dist/clip/jobs.js +5 -1
  5. package/dist/evaluation-builder.js +5 -4
  6. package/dist/evaluation-kb.js +5 -4
  7. package/dist/graph-authority.js +2 -1
  8. package/dist/graph-percolation.js +3 -2
  9. package/dist/{index-1er88ckw.js → index-3gc2yk4k.js} +10 -5
  10. package/dist/{index-mnq2wy34.js → index-59bcxqee.js} +1 -1
  11. package/dist/{index-p70t8j13.js → index-7drercgk.js} +1 -1
  12. package/dist/{index-tcaq1c7f.js → index-e26mdwxz.js} +1 -1
  13. package/dist/{index-r4qs6vq4.js → index-hz2wyggk.js} +1 -1
  14. package/dist/{index-vtwbapjr.js → index-jqkkb4fr.js} +72 -12
  15. package/dist/{index-tvd2sqrx.js → index-mcpppwbq.js} +439 -4
  16. package/dist/index-qkesh4c6.js +7 -0
  17. package/dist/{index-b0khj0h5.js → index-qt2zwza8.js} +6 -1
  18. package/dist/{index-0eacgvpv.js → index-t0fh8a1q.js} +7 -3
  19. package/dist/index.js +6 -5
  20. package/dist/portfolio.js +5 -4
  21. package/dist/sdk.js +4 -3
  22. package/dist/{support-c8hy7qfa.js → support-07jpt1z4.js} +9 -8
  23. package/dist/url-intelligence.js +2 -1
  24. package/dist/workflows/decision-context.js +5 -4
  25. package/dist/workflows/index.js +5 -4
  26. package/package.json +12 -8
  27. package/skills/wordcell/SKILL.md +1 -1
  28. package/skills/wordcell/references/publish.md +2 -1
  29. package/skills/wordcell/references/query.md +17 -3
  30. package/src/authoring-platform.ts +3 -0
  31. package/src/cli-help.ts +1 -0
  32. package/src/cli-program.ts +41 -3
  33. package/src/clip/jobs.ts +2 -1
  34. package/src/clip/metadata-search-tool/Cargo.lock +2 -0
  35. package/src/clip/metadata-search-tool/Cargo.toml +2 -0
  36. package/src/clip/metadata-search-tool/runner.ts +4 -1
  37. package/src/clip/metadata-search-tool/src/duckduckgo.rs +534 -0
  38. package/src/clip/metadata-search-tool/src/main.rs +31 -18
  39. package/src/clip/metadata-search.ts +4 -2
  40. package/src/clip/url-metadata.ts +2 -1
  41. package/src/directory-sync.ts +9 -0
  42. package/src/graph-authority.ts +5 -2
  43. package/src/mcp-tools.ts +17 -1
  44. package/src/sdk.ts +149 -1
  45. package/src/selected-passage.ts +409 -0
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { syncsDirectories } from "./directory-sync.js";
2
3
  import { constants } from "node:fs";
3
4
  import { mkdir, mkdtemp, realpath, rename, rm } from "node:fs/promises";
4
5
  import { join, resolve } from "node:path";
@@ -197,8 +198,10 @@ export async function rebuildGraph(rootInput: string, options: GraphRebuildOptio
197
198
  const handle = await fs.open(stagedPath, constants.O_RDONLY | constants.O_NOFOLLOW);
198
199
  try { await handle.sync(); } finally { await handle.close(); }
199
200
  await rename(stagedPath, path);
200
- const directory = await fs.open(cache, constants.O_RDONLY);
201
- try { await directory.sync(); } finally { await directory.close(); }
201
+ if (syncsDirectories()) {
202
+ const directory = await fs.open(cache, constants.O_RDONLY);
203
+ try { await directory.sync(); } finally { await directory.close(); }
204
+ }
202
205
  return result;
203
206
  } finally {
204
207
  try {
package/src/mcp-tools.ts CHANGED
@@ -229,6 +229,15 @@ export function optionalText(arguments_: Arguments, key: string, maxBytes: numbe
229
229
  return has(arguments_, key) ? checkedText(arguments_[key], key, maxBytes) : undefined;
230
230
  }
231
231
 
232
+ function optionalBoolean(arguments_: Arguments, key: string): boolean | undefined {
233
+ if (!has(arguments_, key)) return undefined;
234
+ const value = arguments_[key];
235
+ if (typeof value !== "boolean") {
236
+ throw new ToolArgumentError(`Invalid argument "${key}": expected a boolean.`);
237
+ }
238
+ return value;
239
+ }
240
+
232
241
  export function optionalInteger(
233
242
  arguments_: Arguments,
234
243
  key: string,
@@ -631,6 +640,11 @@ export function readTools(root: string, sessions: VaultSessions): readonly ToolE
631
640
  properties: {
632
641
  query: { type: "string", minLength: 1, maxLength: MAX_QUERY_TEXT_UTF8_BYTES },
633
642
  mode: { type: "string", enum: SEARCH_MODES, default: "hybrid" },
643
+ selectedPassage: {
644
+ type: "boolean",
645
+ default: false,
646
+ description: "Include a local source excerpt with line references and section headings. Ranking is unchanged.",
647
+ },
634
648
  limit: { type: "integer", minimum: 1, maximum: MAX_SEARCH_RESULTS, default: DEFAULT_SEARCH_RESULTS },
635
649
  ...filterProperties,
636
650
  },
@@ -638,10 +652,11 @@ export function readTools(root: string, sessions: VaultSessions): readonly ToolE
638
652
  annotations: { title: "Search notes", ...READ_ONLY },
639
653
  },
640
654
  handler: async (arguments_) => {
641
- allowOnly(arguments_, ["query", "mode", "limit", "tags", "where", "has", "scope"]);
655
+ allowOnly(arguments_, ["query", "mode", "limit", "tags", "where", "has", "scope", "selectedPassage"]);
642
656
  const query = requiredText(arguments_, "query", MAX_QUERY_TEXT_UTF8_BYTES);
643
657
  const mode = optionalChoice(arguments_, "mode", SEARCH_MODES) ?? "hybrid";
644
658
  const limit = optionalInteger(arguments_, "limit", 1, MAX_SEARCH_RESULTS) ?? DEFAULT_SEARCH_RESULTS;
659
+ const selectedPassage = optionalBoolean(arguments_, "selectedPassage");
645
660
  const filters = metadataFilters(arguments_);
646
661
  const tags = optionalTextList(arguments_, "tags", MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES);
647
662
  const scopes = optionalTextList(arguments_, "scope", MAX_REPOSITORY_SCOPES, MAX_QUERY_TEXT_UTF8_BYTES);
@@ -650,6 +665,7 @@ export function readTools(root: string, sessions: VaultSessions): readonly ToolE
650
665
  query,
651
666
  mode,
652
667
  limit,
668
+ ...(selectedPassage === undefined ? {} : { selectedPassage }),
653
669
  ...(filters.length === 0 ? {} : { filters }),
654
670
  ...(tags === undefined ? {} : { tags }),
655
671
  ...(scopes === undefined ? {} : { repositoryScopes: scopes }),
package/src/sdk.ts CHANGED
@@ -52,6 +52,15 @@ import {
52
52
  type SearchRuleRequest,
53
53
  type SearchRulesV1,
54
54
  } from "./search-rules.js";
55
+ import {
56
+ createSelectedPassageBudget,
57
+ SELECTED_PASSAGE_MAX_BYTES,
58
+ SELECTED_PASSAGE_MAX_HEADING_BYTES,
59
+ SELECTED_PASSAGE_NONE_REASONS,
60
+ SELECTED_PASSAGE_UNAVAILABLE_REASONS,
61
+ selectSearchPassage,
62
+ type SelectedPassageResult,
63
+ } from "./selected-passage.js";
55
64
  import {
56
65
  applyRerank,
57
66
  MAX_RERANK_CANDIDATES,
@@ -82,6 +91,14 @@ import {
82
91
  type UntrustedStructuredContent,
83
92
  } from "./untrusted-content.js";
84
93
 
94
+ export type {
95
+ SelectedPassage,
96
+ SelectedPassageHeading,
97
+ SelectedPassageNone,
98
+ SelectedPassageResult,
99
+ SelectedPassageUnavailable,
100
+ } from "./selected-passage.js";
101
+
85
102
  export const MAX_SEARCH_RESULTS = 100;
86
103
  export const MAX_SEARCH_CANDIDATES = 500;
87
104
  export const DEFAULT_SEARCH_RESULTS = 10;
@@ -131,6 +148,8 @@ export type KnowledgeBaseSearchOptions = {
131
148
  readonly history?: false | "auto" | "required" | KnowledgeBaseHistoryOptions;
132
149
  /** Opt-in rerank engine over a bounded result window. */
133
150
  readonly rerank?: KnowledgeBaseSearchRerankOptions;
151
+ /** Add one local query-selected source passage after ranking; never changes reranker input. */
152
+ readonly selectedPassage?: boolean;
134
153
  };
135
154
 
136
155
  export type KnowledgeBaseExactEvidence = {
@@ -179,6 +198,8 @@ export type KnowledgeBaseSearchHit = {
179
198
  readonly identity: boolean;
180
199
  readonly line?: number;
181
200
  readonly snippet: string;
201
+ /** Present only when requested; legacy snippet, line, and retrieval evidence retain their meanings. */
202
+ readonly selectedPassage?: SelectedPassageResult;
182
203
  readonly tags: readonly string[];
183
204
  readonly metadata: MetadataObject;
184
205
  readonly evidence: readonly KnowledgeBaseSearchEvidence[];
@@ -729,6 +750,10 @@ export async function openKnowledgeBase(
729
750
  searchRules,
730
751
  );
731
752
  const effectiveOptions = expansion.request as KnowledgeBaseSearchOptions;
753
+ if (effectiveOptions.selectedPassage !== undefined
754
+ && typeof effectiveOptions.selectedPassage !== "boolean") {
755
+ throw new TypeError("Search selectedPassage must be a boolean.");
756
+ }
732
757
  const { query: effectiveQuery } = validateSearchQuery(effectiveOptions.query);
733
758
  const mode = checkedSearchMode(effectiveOptions.mode);
734
759
  const ordering = checkedSearchOrdering(effectiveOptions.ordering);
@@ -1029,6 +1054,17 @@ export async function openKnowledgeBase(
1029
1054
  hit.rank === index + 1 ? hit : { ...hit, rank: index + 1 }));
1030
1055
  }
1031
1056
  }
1057
+ if (effectiveOptions.selectedPassage === true) {
1058
+ const budget = createSelectedPassageBudget();
1059
+ results = Object.freeze(results.map((hit) => {
1060
+ const note = notesById.get(hit.id);
1061
+ if (note === undefined) throw new Error("Selected search hit has no snapshot note.");
1062
+ return Object.freeze({
1063
+ ...hit,
1064
+ selectedPassage: selectSearchPassage({ query: effectiveQuery, text: note.content, budget }),
1065
+ });
1066
+ }));
1067
+ }
1032
1068
  let graph: GraphContext | null = null;
1033
1069
  if (graphOptions !== null) {
1034
1070
  try {
@@ -1273,7 +1309,16 @@ export function packSearchContext(
1273
1309
  if (!append(String(searchEvidenceRank(item)))) return writer.result();
1274
1310
  }
1275
1311
  if (!append("\n\n")) return writer.result();
1276
- if (!append(hit.snippet)) return writer.result();
1312
+ const selected = selectedPassageForContext(hit, `Knowledge-base search result ${hit.rank}`);
1313
+ if (selected?.status === "selected") {
1314
+ for (const heading of selected.headings) {
1315
+ if (!append(`Section (level ${heading.level}, lines ${heading.startLine}-${heading.endLine}${heading.truncated ? ", shortened" : ""}):\n`)) return writer.result();
1316
+ if (!append(heading.text)) return writer.result();
1317
+ if (!append("\n\n")) return writer.result();
1318
+ }
1319
+ if (!append(`Selected source (UTF-8 snapshot ${selected.sourceSha256}; bytes ${selected.startByte}-${selected.endByte}; lines ${selected.startLine}-${selected.endLine}${selected.clippedStart || selected.clippedEnd ? "; excerpt clipped" : ""}):\n`)) return writer.result();
1320
+ if (!append(selected.text)) return writer.result();
1321
+ } else if (!append(hit.snippet)) return writer.result();
1277
1322
  }
1278
1323
 
1279
1324
  const graph = result.graph;
@@ -1420,6 +1465,107 @@ function optionalDataNumber(
1420
1465
  return dataNumber(record[key], `${label}.${key}`);
1421
1466
  }
1422
1467
 
1468
+ function dataCount(value: unknown, label: string, minimum = 0): number {
1469
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) {
1470
+ throw new TypeError(`${label} must be an integer of at least ${minimum}.`);
1471
+ }
1472
+ return value;
1473
+ }
1474
+
1475
+ function dataSourceText(value: unknown, label: string, maximumBytes: number): string {
1476
+ const text = dataString(value, label);
1477
+ if (/\p{Surrogate}/u.test(text)) throw new TypeError(`${label} must be well-formed Unicode.`);
1478
+ if (Buffer.byteLength(text, "utf8") > maximumBytes) {
1479
+ throw new RangeError(`${label} exceeds ${maximumBytes} UTF-8 bytes.`);
1480
+ }
1481
+ return text;
1482
+ }
1483
+
1484
+ function sourceSpan(
1485
+ record: InspectedDataRecord,
1486
+ label: string,
1487
+ text: string,
1488
+ ): { startByte: number; endByte: number; startLine: number; endLine: number } {
1489
+ const startByte = dataCount(requiredData(record, "startByte", label), `${label}.startByte`);
1490
+ const endByte = dataCount(requiredData(record, "endByte", label), `${label}.endByte`);
1491
+ const startLine = dataCount(requiredData(record, "startLine", label), `${label}.startLine`, 1);
1492
+ const endLine = dataCount(requiredData(record, "endLine", label), `${label}.endLine`, startLine);
1493
+ if (endByte - startByte !== Buffer.byteLength(text, "utf8")) {
1494
+ throw new TypeError(`${label} byte span must match its text.`);
1495
+ }
1496
+ return { startByte, endByte, startLine, endLine };
1497
+ }
1498
+
1499
+ function selectedPassageData(value: unknown, label: string): SelectedPassageResult {
1500
+ const record = inspectedDataRecord(value, label);
1501
+ const status = requiredData(record, "status", label);
1502
+ if (status === "none" || status === "unavailable") {
1503
+ const reason = requiredData(record, "reason", label);
1504
+ const reasons: readonly string[] = status === "none"
1505
+ ? SELECTED_PASSAGE_NONE_REASONS
1506
+ : SELECTED_PASSAGE_UNAVAILABLE_REASONS;
1507
+ if (typeof reason !== "string" || !reasons.includes(reason)) {
1508
+ throw new TypeError(`${label}.reason is not a known ${status} reason.`);
1509
+ }
1510
+ return Object.freeze({ status, reason }) as SelectedPassageResult;
1511
+ }
1512
+ if (status !== "selected") throw new TypeError(`${label}.status is not a known selected-passage status.`);
1513
+ const text = dataSourceText(requiredData(record, "text", label), `${label}.text`, SELECTED_PASSAGE_MAX_BYTES);
1514
+ if (text === "") throw new TypeError(`${label}.text must not be empty.`);
1515
+ const span = sourceSpan(record, label, text);
1516
+ const sourceSha256 = dataString(requiredData(record, "sourceSha256", label), `${label}.sourceSha256`);
1517
+ if (!/^[0-9a-f]{64}$/u.test(sourceSha256)) throw new TypeError(`${label}.sourceSha256 must be a SHA-256 digest.`);
1518
+ if (requiredData(record, "sourceEncoding", label) !== "utf8-snapshot") {
1519
+ throw new TypeError(`${label}.sourceEncoding must be utf8-snapshot.`);
1520
+ }
1521
+ let previousLevel = 0;
1522
+ let previousEnd = 0;
1523
+ const headings = inspectedDataArray(requiredData(record, "headings", label), `${label}.headings`, 6)
1524
+ .map((entry, index) => {
1525
+ const headingLabel = `${label}.headings[${index}]`;
1526
+ const heading = inspectedDataRecord(entry, headingLabel);
1527
+ const level = dataCount(requiredData(heading, "level", headingLabel), `${headingLabel}.level`, previousLevel + 1);
1528
+ if (level > 6) throw new TypeError(`${headingLabel}.level must be at most 6.`);
1529
+ const headingText = dataSourceText(
1530
+ requiredData(heading, "text", headingLabel),
1531
+ `${headingLabel}.text`,
1532
+ SELECTED_PASSAGE_MAX_HEADING_BYTES,
1533
+ );
1534
+ const headingSpan = sourceSpan(heading, headingLabel, headingText);
1535
+ if (headingSpan.startByte < previousEnd || headingSpan.endByte > span.startByte) {
1536
+ throw new TypeError(`${headingLabel} must precede the selected passage in source order.`);
1537
+ }
1538
+ previousLevel = level;
1539
+ previousEnd = headingSpan.endByte;
1540
+ return Object.freeze({
1541
+ level,
1542
+ text: headingText,
1543
+ ...headingSpan,
1544
+ truncated: dataBoolean(requiredData(heading, "truncated", headingLabel), `${headingLabel}.truncated`),
1545
+ });
1546
+ });
1547
+ return Object.freeze({
1548
+ status,
1549
+ text,
1550
+ ...span,
1551
+ sourceSha256,
1552
+ sourceEncoding: "utf8-snapshot",
1553
+ clippedStart: dataBoolean(requiredData(record, "clippedStart", label), `${label}.clippedStart`),
1554
+ clippedEnd: dataBoolean(requiredData(record, "clippedEnd", label), `${label}.clippedEnd`),
1555
+ headings: Object.freeze(headings),
1556
+ });
1557
+ }
1558
+
1559
+ /** Read an optional hit passage as untrusted data without invoking accessors. */
1560
+ function selectedPassageForContext(hit: object, label: string): SelectedPassageResult | undefined {
1561
+ const descriptor = Object.getOwnPropertyDescriptor(hit, "selectedPassage");
1562
+ if (descriptor === undefined) return undefined;
1563
+ if (!("value" in descriptor)) throw new TypeError(`${label} contains an accessor property.`);
1564
+ return descriptor.value === undefined
1565
+ ? undefined
1566
+ : selectedPassageData(descriptor.value, `${label}.selectedPassage`);
1567
+ }
1568
+
1423
1569
  function searchEvidenceSummary(value: unknown, label: string): Readonly<Record<string, unknown>> {
1424
1570
  const evidence = inspectedDataRecord(value, label);
1425
1571
  return Object.freeze({
@@ -1438,6 +1584,7 @@ function searchHitContextRecord(value: unknown, index: number): Readonly<Record<
1438
1584
  ).map((entry, evidenceIndex) =>
1439
1585
  searchEvidenceSummary(entry, `${label}.evidence[${evidenceIndex}]`));
1440
1586
  const line = optionalDataNumber(hit, "line", label);
1587
+ const selectedPassage = selectedPassageForContext(hit, label);
1441
1588
  return Object.freeze({
1442
1589
  kind: "search-result",
1443
1590
  id: dataString(requiredData(hit, "id", label), `${label}.id`),
@@ -1449,6 +1596,7 @@ function searchHitContextRecord(value: unknown, index: number): Readonly<Record<
1449
1596
  ...(line === undefined ? {} : { line }),
1450
1597
  evidence: Object.freeze(evidence),
1451
1598
  snippet: dataString(requiredData(hit, "snippet", label), `${label}.snippet`),
1599
+ ...(selectedPassage === undefined ? {} : { selectedPassage }),
1452
1600
  });
1453
1601
  }
1454
1602