@hraness/sponge 0.3.3 → 0.3.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.
@@ -20,6 +20,7 @@ export declare function textQuote(source: string, start: number, end: number): T
20
20
  export declare function locateQuote(source: string, quote: string): TextQuote | undefined;
21
21
  /** A stored selector still names its exact text in `source`. */
22
22
  export declare function anchorHolds(source: string, anchor: TextQuote): boolean;
23
+ /** Word count: space-separated tokens, with each run of Han or kana counted as half its characters, rounded up. */
23
24
  export declare function words(value: string): number;
24
25
  /**
25
26
  * Every occurrence of `quote` in `source` ignoring inline Markdown and case, within optional raw bounds.
@@ -107,9 +107,19 @@ export function anchorHolds(source, anchor) {
107
107
  anchor.end > anchor.start &&
108
108
  source.slice(anchor.start, anchor.end) === anchor.exact);
109
109
  }
110
+ /** Han and kana are written without spaces; about two characters make a word. */
111
+ const unspaced = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu;
112
+ /** Word count: space-separated tokens, with each run of Han or kana counted as half its characters, rounded up. */
110
113
  export function words(value) {
111
114
  const t = foldText(value);
112
- return t ? t.split(' ').length : 0;
115
+ if (!t)
116
+ return 0;
117
+ let n = 0;
118
+ for (const token of t.split(' ')) {
119
+ const cjk = token.match(unspaced)?.length ?? 0;
120
+ n += cjk ? Math.ceil(cjk / 2) + (token.replace(unspaced, '').match(/[\p{L}\p{N}]/u) ? 1 : 0) : 1;
121
+ }
122
+ return n;
113
123
  }
114
124
  /**
115
125
  * Rendered-text projection for snapping a near-verbatim quote: drops inline Markdown syntax (link targets, brackets,
@@ -116,7 +116,7 @@ async function complete(origin, request, model, messages, schema, o) {
116
116
  });
117
117
  if (!response.ok) {
118
118
  await response.body?.cancel().catch(() => undefined);
119
- throw new SpongeError('unavailable');
119
+ throw new SpongeError('model-unavailable', { detail: { status: response.status } });
120
120
  }
121
121
  let parsed;
122
122
  try {
@@ -125,14 +125,14 @@ async function complete(origin, request, model, messages, schema, o) {
125
125
  catch (error) {
126
126
  if (error instanceof SpongeError)
127
127
  throw error;
128
- throw new SpongeError('unavailable');
128
+ throw new SpongeError('model-unavailable');
129
129
  }
130
130
  const choice = parsed?.choices?.[0];
131
131
  if (choice?.finish_reason === 'length')
132
132
  throw new SpongeError('limit-exceeded');
133
133
  const content = choice?.message?.content;
134
134
  if (typeof content !== 'string' || !content.trim())
135
- throw new SpongeError('unavailable');
135
+ throw new SpongeError('model-unavailable');
136
136
  if (Buffer.byteLength(content) > o.maxOutputBytes)
137
137
  throw new SpongeError('limit-exceeded');
138
138
  return content;
@@ -317,10 +317,13 @@ export function snapQuote(source, text, within) {
317
317
  export function trimQuote(text, max = L.quoteWords) {
318
318
  if (words(text) <= max)
319
319
  return text;
320
- const ends = [...text.matchAll(/\S+/g)].map((m) => m.index + m[0].length);
321
- let n = Math.min(max, ends.length), out = text;
320
+ // Cut after a space-separated token or, inside unspaced Han and kana, after any character.
321
+ const ends = [
322
+ ...new Set([...text.matchAll(/\S+/g), ...text.matchAll(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu)].map((m) => m.index + m[0].length)),
323
+ ].sort((a, b) => a - b);
324
+ let n = ends.length, out = text;
322
325
  do {
323
- out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013-]+$/, '');
326
+ out = text.slice(0, ends[n - 1]).replace(/[\s,;:(\[\u2014\u2013\u3001\uff0c-]+$/, '');
324
327
  n--;
325
328
  } while (n > 0 && words(out) > max);
326
329
  return out;
@@ -486,7 +489,7 @@ export async function draftDigest(input) {
486
489
  throw new SpongeError('limit-exceeded');
487
490
  if (error instanceof SpongeError)
488
491
  throw error;
489
- throw new SpongeError('unavailable');
492
+ throw new SpongeError('model-unavailable', { cause: error });
490
493
  }
491
494
  if (userSignal.aborted)
492
495
  throw new SpongeError('cancelled');
@@ -49,8 +49,12 @@ const keys = (v, allowed) => Object.keys(v).every((k) => allowed.includes(k));
49
49
  const str = (v, max = 2000) => typeof v === 'string' && v.trim().length > 0 && v.length <= max && !/[\u0000-\u0008\u000b-\u001f\u007f]/.test(v);
50
50
  /** v1: sentence terminators followed by more text; abbreviations are rare enough in a 155-character lead to reject. */
51
51
  const sentencesV1 = (v) => foldText(v).split(/(?<=[.!?])\s+(?=\S)/).length;
52
- /** v2: a terminator (and closing quotes) followed by a capital or digit, the same split Jungle's first-sentence lead uses, so `e.g. this` stays one sentence. */
53
- const sentences = (v) => foldText(v).split(/(?<=[.!?]["')]*)\s+(?=["(]?[A-Z0-9])/).length;
52
+ /**
53
+ * v2: a terminator (and closing quotes) followed by a capital or digit, the split Jungle's first-sentence lead uses, so
54
+ * `e.g. this` stays one sentence. A period after an initial or a common title does not end a sentence: `Lucas C.
55
+ * Cordeiro`, `F. Zakaria`, `U.S. Senate`, `Dr. Lind`.
56
+ */
57
+ const sentences = (v) => foldText(v).split(/(?<=(?:(?<!\b(?:[A-Z]|Mr|Mrs|Ms|Dr|Prof|St|Jr|Sr|vs|No|Inc|Ltd|Co))\.|[!?])["')]*)\s+(?=["(]?[A-Z0-9])/).length;
54
58
  /** Parse an untrusted digest (v1 or v2) into its shape, or report why not. */
55
59
  export function parseDigest(value) {
56
60
  const issues = [], bad = (path, code = 'shape') => {
@@ -17,6 +17,7 @@ declare const parents: {
17
17
  readonly malformed: "invalid-input";
18
18
  readonly 'no-capture': "invalid-input";
19
19
  readonly 'draft-rejected': "unavailable";
20
+ readonly 'model-unavailable': "unavailable";
20
21
  readonly 'parent-missing': "invalid-input";
21
22
  readonly 'target-exists': "invalid-input";
22
23
  };
@@ -31,6 +31,7 @@ const parents = {
31
31
  malformed: 'invalid-input',
32
32
  'no-capture': 'invalid-input',
33
33
  'draft-rejected': 'unavailable',
34
+ 'model-unavailable': 'unavailable',
34
35
  'parent-missing': 'invalid-input',
35
36
  'target-exists': 'invalid-input',
36
37
  };
@@ -80,7 +81,8 @@ export const hints = {
80
81
  };
81
82
  /** Hints for detail codes whose public code's hint would mislead. Fixed text only, like `hints`. */
82
83
  export const detailHints = {
83
- 'draft-rejected': 'The model draft failed the digest checks listed above, even after one repair. Run it again; drafts vary.',
84
+ 'draft-rejected': 'The model draft failed the digest checks listed above, even after one repair. Drafting is deterministic, so the same capture and model fail again; refresh the item or use another model.',
85
+ 'model-unavailable': 'The model server did not answer or returned an error. Check that it is still running and has free GPU memory (another loaded model can exhaust it), then retry.',
84
86
  };
85
87
  /** The one error type. `message` is the code, so bare `String(error)` never carries detail, paths or causes. */
86
88
  export class SpongeError extends Error {
@@ -27,14 +27,14 @@
27
27
  }
28
28
  ],
29
29
  "build": {
30
- "asrSourceSha256": "e2e38c4beb276bf8e625e89f4694dcf6a89526621b0a056b590325bc7fb92314",
30
+ "asrSourceSha256": "3b32718d4626aff97f2c3e043c9fd020f237adc7f7e5508e7d7b99a555762982",
31
31
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
32
- "embeddingSourceSha256": "38ba64989463d38bf6100636a2b1569e43cc32fbe640da6cc4bf0d8b24409c8a",
32
+ "embeddingSourceSha256": "66f008ea29050d114b0df74337124bac8f6cdba9732e257cc70ad85eb9ebee6f",
33
33
  "swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
34
34
  },
35
35
  "minimumMacOS": "26.0",
36
- "packageVersion": "0.3.3",
36
+ "packageVersion": "0.3.5",
37
37
  "platform": "darwin",
38
38
  "schemaVersion": 1,
39
- "sourceCommit": "b1de42fa724be73e352c34357d42a1ca3946d5b5"
39
+ "sourceCommit": "49b3f7f100cdbcbf5f52418d28c53dc13f198424"
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "A local library for captured articles and verified offline assets.",
5
5
  "repository": {
6
6
  "type": "git",