@hraness/wordcell 0.22.3 → 0.22.5

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 (53) hide show
  1. package/README.md +20 -17
  2. package/dist/authoring.js +2 -2
  3. package/dist/{authority-06eqhqam.js → authority-3ga47syb.js} +2 -2
  4. package/dist/benchmark.js +3 -3
  5. package/dist/cli.js +16 -16
  6. package/dist/evaluation-builder.js +10 -10
  7. package/dist/evaluation-kb.js +10 -10
  8. package/dist/graph-authority.js +4 -4
  9. package/dist/graph-percolation.js +6 -6
  10. package/dist/graph.js +3 -1
  11. package/dist/{index-1k595z6v.js → index-1r5rnkfr.js} +2 -2
  12. package/dist/{index-3agn8scn.js → index-23m6bbjt.js} +47 -11
  13. package/dist/{index-q7tbg07z.js → index-42tb8qqp.js} +7 -7
  14. package/dist/{index-yx6p1vzc.js → index-8bgnhjgq.js} +17 -16
  15. package/dist/{index-233z9wmn.js → index-8v6k9h4r.js} +13 -5
  16. package/dist/{index-vnybgywh.js → index-aer0jdrq.js} +1 -1
  17. package/dist/{index-xv80vzfp.js → index-f75zmmdr.js} +4 -4
  18. package/dist/{index-ecqpgr5y.js → index-f7fnww7a.js} +1 -1
  19. package/dist/{index-djrcc1yf.js → index-gtwqye5a.js} +4 -4
  20. package/dist/{index-bqtqeak3.js → index-h2qn3y3f.js} +4 -4
  21. package/dist/{index-rr9kzq4n.js → index-n57tewfr.js} +2 -2
  22. package/dist/{index-76dbn6pe.js → index-n9kwenpt.js} +1 -1
  23. package/dist/{index-n386evxj.js → index-p96kb6t2.js} +1 -1
  24. package/dist/{index-tf4wzjew.js → index-pz2b2x0y.js} +1 -1
  25. package/dist/{index-jk1sgx36.js → index-qdb8f8va.js} +1 -1
  26. package/dist/{index-a3j61wz9.js → index-xzvcw9ga.js} +201 -131
  27. package/dist/{index-qbssx940.js → index-zy7an84p.js} +223 -8
  28. package/dist/index.js +21 -15
  29. package/dist/percolate.js +2 -2
  30. package/dist/portfolio.js +10 -10
  31. package/dist/publish-reader/reader.js +48 -12
  32. package/dist/publish-search.js +1 -1
  33. package/dist/publish.js +4 -4
  34. package/dist/sdk.js +9 -9
  35. package/dist/search.js +2 -2
  36. package/dist/semantic.js +4 -4
  37. package/dist/{support-vm8fw4yc.js → support-jf9v10j7.js} +15 -15
  38. package/dist/workflows/decision-context.js +10 -10
  39. package/dist/workflows/index.js +10 -10
  40. package/package.json +5 -4
  41. package/skills/wordcell/SKILL.md +1 -1
  42. package/skills/wordcell/references/plan-structure.md +1 -1
  43. package/skills/wordcell/references/url-platforms.md +1 -1
  44. package/src/cli-program.ts +4 -2
  45. package/src/graph.ts +79 -9
  46. package/src/mention-index.ts +194 -0
  47. package/src/publish-index.ts +3 -2
  48. package/src/publish-markdown.ts +55 -0
  49. package/src/publish-model.ts +2 -2
  50. package/src/publish-reader/reader.ts +3 -1
  51. package/src/publish-search.ts +44 -13
  52. package/src/publish.ts +4 -1
  53. package/src/vault.ts +26 -2
@@ -47,7 +47,7 @@ export const WORDCELL_SITE_LIMITS_V1 = Object.freeze({
47
47
  * exact lane's substring semantics; larger corpora use postings shards.
48
48
  */
49
49
  inlineTotalBytes: 2 * 1_024 * 1_024,
50
- /** Normalized content text retained per note payload for hydration. */
50
+ /** Normalized readable content retained per note payload for snippet hydration. */
51
51
  noteTextBytes: 32 * 1_024,
52
52
  /** Site title and description. */
53
53
  titleBytes: 512,
@@ -185,7 +185,7 @@ export type WordcellSiteNoteV1 = {
185
185
  readonly type?: string;
186
186
  readonly tags: readonly string[];
187
187
  readonly summary: string;
188
- /** Bounded normalized content text for search hydration. */
188
+ /** Bounded normalized readable content for snippet hydration, not ranking. */
189
189
  readonly text: string;
190
190
  readonly textTruncated: boolean;
191
191
  readonly links: readonly WordcellSiteNoteLinkV1[];
@@ -321,7 +321,9 @@ function resultHref(base: string, slug: string): string {
321
321
 
322
322
  function snippetFor(index: SiteIndex, doc: WordcellSiteDocV1, query: PublishQuery): string {
323
323
  const hydrated = index.notes.get(doc.s);
324
- const text = hydrated?.text ?? doc.x;
324
+ // doc.x is a normalized ranking basis, not display text. The eager preview
325
+ // remains readable while the existing bounded hydration fetch is pending.
326
+ const text = hydrated?.text;
325
327
  return text === undefined ? doc.p : publishSnippet(text, query, doc.p);
326
328
  }
327
329
 
@@ -392,8 +392,10 @@ export function comparePublishScores(
392
392
  }
393
393
 
394
394
  /**
395
- * Bounded context window around the first query match in normalized text.
396
- * Mirrors `exactSnippet`: collapse whitespace, ellipsize edges.
395
+ * Bounded display context around the first normalized query match. Match
396
+ * offsets are mapped back to NFC display text before taking a UTF-8 byte
397
+ * window, so case expansion and supplementary characters cannot split it.
398
+ * The fallback retains the separate document-preview byte contract.
397
399
  */
398
400
  export function publishSnippet(
399
401
  text: string,
@@ -401,18 +403,47 @@ export function publishSnippet(
401
403
  fallback: string,
402
404
  windowBytes = 160,
403
405
  ): string {
404
- const normalized = publishNormalize(text);
405
- let offset = query.normalized === "" ? -1 : normalized.indexOf(query.normalized);
406
- if (offset < 0) {
406
+ if (!Number.isSafeInteger(windowBytes) || windowBytes < 0) throw new RangeError("Snippet window must be a nonnegative safe byte count.");
407
+ const display = text.normalize("NFC");
408
+ const normalized = publishNormalize(display);
409
+ let match = query.normalized === "" ? -1 : normalized.indexOf(query.normalized);
410
+ if (match < 0) {
407
411
  for (const term of query.terms) {
408
- offset = normalized.indexOf(term);
409
- if (offset >= 0) break;
412
+ match = normalized.indexOf(term);
413
+ if (match >= 0) break;
410
414
  }
411
415
  }
412
- if (offset < 0) return fallback;
413
- const half = Math.floor(windowBytes / 2);
414
- const start = Math.max(0, offset - half);
415
- const end = Math.min(text.length, offset + half);
416
- const snippet = text.slice(start, end).replace(/\s+/gu, " ").trim();
417
- return `${start > 0 ? "…" : ""}${snippet}${end < text.length ? "…" : ""}`;
416
+ if (match < 0) return fallback;
417
+ let offset = 0, normalizedOffset = 0;
418
+ for (const character of display) {
419
+ const width = character.toLocaleLowerCase("en-US").length;
420
+ if (normalizedOffset + width > match) break;
421
+ normalizedOffset += width;
422
+ offset += character.length;
423
+ }
424
+ const edge = windowBytes >= 6 ? "…" : "";
425
+ const budget = windowBytes - 2 * publishUtf8Bytes(edge);
426
+ const previous = (end: number): number => {
427
+ const last = display.charCodeAt(end - 1);
428
+ return end > 1 && last >= 0xdc00 && last <= 0xdfff && display.charCodeAt(end - 2) >= 0xd800 && display.charCodeAt(end - 2) <= 0xdbff ? end - 2 : end - 1;
429
+ };
430
+ let start = offset, end = offset, used = 0;
431
+ while (start > 0) {
432
+ const before = previous(start), bytes = publishUtf8Bytes(display.slice(before, start));
433
+ if (used + bytes > Math.floor(budget / 2)) break;
434
+ start = before; used += bytes;
435
+ }
436
+ while (end < display.length) {
437
+ const character = String.fromCodePoint(display.codePointAt(end) ?? 0), bytes = publishUtf8Bytes(character);
438
+ if (used + bytes > budget) break;
439
+ end += character.length; used += bytes;
440
+ }
441
+ // Near the end, spend the remaining budget on earlier whole characters.
442
+ while (start > 0) {
443
+ const before = previous(start), bytes = publishUtf8Bytes(display.slice(before, start));
444
+ if (used + bytes > budget) break;
445
+ start = before; used += bytes;
446
+ }
447
+ const snippet = display.slice(start, end).replace(/\s+/gu, " ").trim();
448
+ return `${start > 0 ? edge : ""}${snippet}${end < display.length ? edge : ""}`;
418
449
  }
package/src/publish.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  } from "./graph.js";
17
17
  import { buildSiteIndex } from "./publish-index.js";
18
18
  import {
19
+ projectMarkdownText,
19
20
  renderMarkdownToHtml,
20
21
  type PublishRenderContext,
21
22
  } from "./publish-markdown.js";
@@ -307,7 +308,9 @@ export async function projectVault(
307
308
  let anyTextTruncated = false;
308
309
  for (const note of selection.notes) {
309
310
  const slug = slugFor(note.id);
310
- const text = boundedNormalized(note.searchableText, WORDCELL_SITE_LIMITS_V1.noteTextBytes);
311
+ // Display grammar owns citations before normalization can fold distinct
312
+ // labels together. Ranking continues to use untouched searchableText.
313
+ const text = boundedNormalized(projectMarkdownText(note.content).text, WORDCELL_SITE_LIMITS_V1.noteTextBytes);
311
314
  anyTextTruncated ||= text.truncated;
312
315
  const payload: WordcellSiteNoteV1 = {
313
316
  format: WORDCELL_SITE_NOTE_FORMAT_V1,
package/src/vault.ts CHANGED
@@ -13,6 +13,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
13
13
 
14
14
  import {
15
15
  analyzeVault,
16
+ analyzeVaultComplete,
16
17
  isCanonicalNoteId,
17
18
  lookupNote,
18
19
  MAX_ANALYZED_NOTES,
@@ -21,6 +22,7 @@ import {
21
22
  renderCatalog,
22
23
  replaceCatalog,
23
24
  type AnalyzeVaultOptions,
25
+ type CompleteAnalyzeVaultOptions,
24
26
  type Note,
25
27
  type VaultAnalysis,
26
28
  } from "./graph.js";
@@ -90,6 +92,8 @@ export type ScanVaultOptions = Omit<AnalyzeVaultOptions, "mentionScope"> & {
90
92
  readonly mentionScope?: string | false;
91
93
  };
92
94
 
95
+ export type CompleteScanVaultOptions = ScanVaultOptions & Pick<CompleteAnalyzeVaultOptions, "mentionIndexLimits">;
96
+
93
97
  export async function markdownFiles(
94
98
  directory: string,
95
99
  ignoredDirectories: ReadonlySet<string> = defaultIgnoredDirectories,
@@ -520,8 +524,9 @@ async function atomicReplace(
520
524
 
521
525
  async function snapshot(
522
526
  rootInput: string,
523
- options: ScanVaultOptions,
527
+ options: CompleteScanVaultOptions,
524
528
  writeIndex: boolean,
529
+ completeMentions = false,
525
530
  ): Promise<VaultSnapshot> {
526
531
  const requestedRoot = resolve(rootInput);
527
532
  const root = await realpath(requestedRoot);
@@ -613,7 +618,7 @@ async function snapshot(
613
618
  catalogMode,
614
619
  index,
615
620
  notes,
616
- analysis: analyzeVault(notes, {
621
+ analysis: (completeMentions ? analyzeVaultComplete : analyzeVault)(notes, {
617
622
  catalogNoteId,
618
623
  ...(options.includeInSuggestions === undefined
619
624
  ? {}
@@ -631,6 +636,9 @@ async function snapshot(
631
636
  ...(options.maxMentions === undefined
632
637
  ? {}
633
638
  : { maxMentions: options.maxMentions }),
639
+ ...(!completeMentions || options.mentionIndexLimits === undefined
640
+ ? {}
641
+ : { mentionIndexLimits: options.mentionIndexLimits }),
634
642
  }),
635
643
  };
636
644
  }
@@ -648,3 +656,19 @@ export async function refreshVault(
648
656
  ): Promise<VaultSnapshot> {
649
657
  return snapshot(root, options, true);
650
658
  }
659
+
660
+ /** Read every admitted note and compute the full graph through the phrase index. */
661
+ export async function scanVaultComplete(
662
+ root = ".",
663
+ options: CompleteScanVaultOptions = {},
664
+ ): Promise<VaultSnapshot> {
665
+ return snapshot(root, options, false, true);
666
+ }
667
+
668
+ /** Refresh the configured catalog while retaining complete indexed analysis. */
669
+ export async function refreshVaultComplete(
670
+ root = ".",
671
+ options: CompleteScanVaultOptions = {},
672
+ ): Promise<VaultSnapshot> {
673
+ return snapshot(root, options, true, true);
674
+ }