@hraness/sponge 0.3.1 → 0.3.3

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.
@@ -7,6 +7,8 @@ export interface TextQuote {
7
7
  start: number;
8
8
  end: number;
9
9
  }
10
+ /** Spoken text without transcript cues, whitespace collapsed: what a reader quotes from a transcript. */
11
+ export declare function stripCues(raw: string): string;
10
12
  export declare function foldText(value: string): string;
11
13
  /** Every occurrence of `quote` in `source`, compared after folding. */
12
14
  export declare function locateAll(source: string, quote: string): {
@@ -1,9 +1,20 @@
1
1
  const context = 32;
2
2
  const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
3
3
  const escapable = /^[\\`*_{}[\]()#+\-.!|>~<"']$/;
4
+ /** A transcript cue at a line start: `- [00:02] ` (clipped captions) or `[00:00:02] ` (Sponge transcripts). */
5
+ const cue = /^[ \t]*(?:[-*+][ \t]+)?\[(?:\d{1,2}:)?\d{1,2}:\d{2}\][ \t]+/;
6
+ /** Spoken text without transcript cues, whitespace collapsed: what a reader quotes from a transcript. */
7
+ export function stripCues(raw) {
8
+ return raw
9
+ .split('\n')
10
+ .map((line) => line.replace(cue, ''))
11
+ .join('\n')
12
+ .replace(/\s+/g, ' ')
13
+ .trim();
14
+ }
4
15
  /**
5
16
  * Fold the differences a reader cannot see: Markdown escapes, curly quotes, dashes, runs of
6
- * whitespace and Unicode normalization. Folding works on grapheme clusters, each NFC-normalized;
17
+ * whitespace, line-start transcript cues and Unicode normalization. Folding works on grapheme clusters, each NFC-normalized;
7
18
  * every folded code unit of a cluster maps to the cluster's raw bounds, so `map[i]` and `ends[i]`
8
19
  * bound the raw text of folded unit i and matches map back exactly.
9
20
  */
@@ -19,6 +30,17 @@ function fold(raw) {
19
30
  };
20
31
  for (let c = 0; c < clusters.length; c++) {
21
32
  const { segment, index } = clusters[c];
33
+ // A transcript cue reads as the space between caption lines, so a quote of the speech spans them.
34
+ const marker = (index === 0 || raw[index - 1] === '\n') && cue.exec(raw.slice(index, index + 32));
35
+ if (marker) {
36
+ const stop = index + marker[0].length;
37
+ if (!space && text.length)
38
+ emit(' ', index, stop);
39
+ space = true;
40
+ while (c + 1 < clusters.length && clusters[c + 1].index < stop)
41
+ c++;
42
+ continue;
43
+ }
22
44
  // An escaped character maps to its backslash, so a match starting there keeps the escape.
23
45
  let ch = segment, end = index + segment.length;
24
46
  if (segment === '\\' && c + 1 < clusters.length && escapable.test(clusters[c + 1].segment)) {
@@ -98,6 +120,12 @@ function rendered(raw) {
98
120
  for (let i = 0; i < raw.length; i++) {
99
121
  let ch = raw[i], at = i;
100
122
  if (lineStart) {
123
+ const c = cue.exec(raw.slice(i, i + 32));
124
+ if (c) {
125
+ i += c[0].length - 1;
126
+ lineStart = false;
127
+ continue;
128
+ }
101
129
  const m = /^[ \t]*(?:>[ \t]?|#{1,6}[ \t]+|[-*+][ \t]+|\d{1,3}[.)][ \t]+)*/.exec(raw.slice(i, i + 64));
102
130
  if (m[0].length) {
103
131
  i += m[0].length - 1;
@@ -791,5 +791,5 @@ export const usageOf = (name) => command(name)
791
791
  .join('\n');
792
792
  export function commandHelp(name) {
793
793
  const c = command(name), flags = Object.keys(c.flags);
794
- return `sponge ${c.name} — ${c.summary}\n\nUsage:\n${c.usage.map(row).join('').replace(/^ /gm, ' sponge ')}${flags.length ? `\nFlags: ${flags.map((f) => '--' + f).join(' ')}\n` : ''}\nGlobal flags: --library /absolute/path --json --debug\n`;
794
+ return `sponge ${c.name} — ${c.summary}\n\nUsage:\n${c.usage.map(([synopsis, description]) => row([`sponge ${synopsis}`, description])).join('')}${flags.length ? `\nFlags: ${flags.map((f) => '--' + f).join(' ')}\n` : ''}\nGlobal flags: --library /absolute/path --json --debug\n`;
795
795
  }
@@ -8,7 +8,7 @@ import { command, usageOf } from './commands.js';
8
8
  import { openLibrary } from '../store/index.js';
9
9
  import { defaultCapturePins } from '../acquisition/index.js';
10
10
  import { failure } from '../acquisition/errors.js';
11
- import { SpongeError, codeOf, publicCode, exitFor, hints, debugChain } from '../errors.js';
11
+ import { SpongeError, codeOf, publicCode, exitFor, hints, detailHints, debugChain, } from '../errors.js';
12
12
  /** Failures print one categorical JSON line: the public code, its specific `reason` when narrower, and a fixed hint.
13
13
  * Causes, details, paths and upstream messages appear only with --debug or SPONGE_DEBUG=1. */
14
14
  function report(error, requestId, debug) {
@@ -17,7 +17,7 @@ function report(error, requestId, debug) {
17
17
  process.stderr.write(JSON.stringify({ issues }) + '\n');
18
18
  const hint = code === 'invalid-input' && typeof usage === 'string' && command(usage)
19
19
  ? `Usage: ${usageOf(usage).replace(/\n/g, ' | ')}`
20
- : hints[code];
20
+ : (detailHints[reason] ?? hints[code]);
21
21
  process.stderr.write(JSON.stringify({ ...failure(requestId, code), ...(reason !== code ? { reason } : {}), hint }) + '\n');
22
22
  if (debug)
23
23
  process.stderr.write(debugChain(error));
@@ -1,4 +1,4 @@
1
- import { foldText, locateAll, locateLoose, words } from '../annotations/quote.js';
1
+ import { foldText, locateAll, locateLoose, stripCues, words } from '../annotations/quote.js';
2
2
  import { checkDigest, digestLimits as L, forms, parseDigest, } from './schema.js';
3
3
  import { SpongeError } from '../errors.js';
4
4
  import { sha256 } from '../util/hash.js';
@@ -97,7 +97,9 @@ function lastSentenceEnd(window) {
97
97
  best = m.index + m[0].length;
98
98
  return best;
99
99
  }
100
- const display = (p) => `[${p.id}] ${collapse(p.text)}`;
100
+ const display = (p) => `[${p.id}] ${stripCues(p.text)}`;
101
+ /** A raw source span as a quote: transcript cues inside it are dropped, so the quote reads as the speech. */
102
+ const spoken = (raw) => (raw.includes('\n') && stripCues(raw) !== collapse(raw) ? stripCues(raw) : raw);
101
103
  /** Pack passages into sections of at most `budget` display characters, preferring heading boundaries past half a budget. */
102
104
  export function sectionsOf(list, budget) {
103
105
  const out = [];
@@ -295,7 +297,7 @@ export function snapQuote(source, text, within) {
295
297
  const folded = locateAll(source, text);
296
298
  if (folded.length) {
297
299
  const m = pick(folded);
298
- return { text: m ? source.slice(m.start, m.end) : text, match: 'folded' };
300
+ return { text: m ? spoken(source.slice(m.start, m.end)) : text, match: 'folded' };
299
301
  }
300
302
  const loose = (within && pick(locateLoose(source, text, within))) || pick(locateLoose(source, text));
301
303
  if (!loose)
@@ -306,7 +308,7 @@ export function snapQuote(source, text, within) {
306
308
  start--;
307
309
  while (end < source.length && /[*_`]/.test(source[end]))
308
310
  end++;
309
- return { text: source.slice(start, end), match: 'snapped' };
311
+ return { text: spoken(source.slice(start, end)), match: 'snapped' };
310
312
  }
311
313
  /**
312
314
  * A located quote over the word limit keeps its leading words: a prefix of a source span is still verbatim. Trailing
@@ -608,7 +610,7 @@ export async function draftDigest(input) {
608
610
  result = await attempt(await call([...messages, { role: 'assistant', content: first }, { role: 'user', content: repairPrompt(result.issues) }], schema));
609
611
  }
610
612
  if (!result.draft)
611
- throw new SpongeError('unavailable', { issues: result.issues });
613
+ throw new SpongeError('draft-rejected', { issues: result.issues });
612
614
  let issues = result.issues;
613
615
  if (issues.length) {
614
616
  trace.dropped = prune(result.draft, issues, source, options);
@@ -616,7 +618,7 @@ export async function draftDigest(input) {
616
618
  if (result.draft.digest.ideas.length < L.minIdeas)
617
619
  issues = [...issues.filter((i) => !(i.path === 'ideas' && i.code === 'count')), { path: 'ideas', code: 'count' }];
618
620
  if (issues.length)
619
- throw new SpongeError('unavailable', { issues });
621
+ throw new SpongeError('draft-rejected', { issues });
620
622
  }
621
623
  const producer = {
622
624
  pipeline: 'sponge-digest',
@@ -16,6 +16,9 @@ declare const parents: {
16
16
  readonly 'not-digest': "unsupported";
17
17
  readonly malformed: "invalid-input";
18
18
  readonly 'no-capture': "invalid-input";
19
+ readonly 'draft-rejected': "unavailable";
20
+ readonly 'parent-missing': "invalid-input";
21
+ readonly 'target-exists': "invalid-input";
19
22
  };
20
23
  export type DetailCode = keyof typeof parents;
21
24
  export type SpongeCode = ErrorCode | DetailCode;
@@ -25,6 +28,8 @@ export declare const isSpongeCode: (value: unknown) => value is SpongeCode;
25
28
  export declare const exitStatus: Readonly<Record<ErrorCode, number>>;
26
29
  /** The one code→user-facing hint table. Fixed text only: never paths, sources, credentials or upstream prose. */
27
30
  export declare const hints: Readonly<Record<ErrorCode, string>>;
31
+ /** Hints for detail codes whose public code's hint would mislead. Fixed text only, like `hints`. */
32
+ export declare const detailHints: Readonly<Partial<Record<DetailCode, string>>>;
28
33
  export interface SpongeErrorOptions {
29
34
  detail?: Record<string, unknown>;
30
35
  cause?: unknown;
@@ -30,6 +30,9 @@ const parents = {
30
30
  'not-digest': 'unsupported',
31
31
  malformed: 'invalid-input',
32
32
  'no-capture': 'invalid-input',
33
+ 'draft-rejected': 'unavailable',
34
+ 'parent-missing': 'invalid-input',
35
+ 'target-exists': 'invalid-input',
33
36
  };
34
37
  export const indexCodes = [
35
38
  'invalid-index',
@@ -75,6 +78,10 @@ export const hints = {
75
78
  'schema-newer': 'This library was written by a newer Sponge. Upgrade Sponge.',
76
79
  'owner-busy': 'Another Sponge process owns the library. Wait for it, or run `sponge queue recover` after it stops.',
77
80
  };
81
+ /** Hints for detail codes whose public code's hint would mislead. Fixed text only, like `hints`. */
82
+ 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
+ };
78
85
  /** The one error type. `message` is the code, so bare `String(error)` never carries detail, paths or causes. */
79
86
  export class SpongeError extends Error {
80
87
  code;
@@ -112,8 +112,18 @@ export function copyVerified(source, target, record) {
112
112
  export function newDirectory(path) {
113
113
  if (resolve(path) !== path)
114
114
  throw new SpongeError('invalid-input');
115
- safeDirectory(dirname(path));
116
- mkdirSync(path, { mode: 0o700 });
115
+ try {
116
+ safeDirectory(dirname(path));
117
+ mkdirSync(path, { mode: 0o700 });
118
+ }
119
+ catch (error) {
120
+ const code = error.code;
121
+ if (code === 'ENOENT')
122
+ throw new SpongeError('parent-missing', { cause: error });
123
+ if (code === 'EEXIST')
124
+ throw new SpongeError('target-exists', { cause: error });
125
+ throw error;
126
+ }
117
127
  const stat = lstatSync(path);
118
128
  return { root: path, device: stat.dev, inode: stat.ino };
119
129
  }
@@ -27,14 +27,14 @@
27
27
  }
28
28
  ],
29
29
  "build": {
30
- "asrSourceSha256": "94b9303afcf9304e695c37411cef065e9cc45a31e0e51cd171819cbfc9ee3955",
30
+ "asrSourceSha256": "e2e38c4beb276bf8e625e89f4694dcf6a89526621b0a056b590325bc7fb92314",
31
31
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
32
- "embeddingSourceSha256": "eed7e110bc888af99e0d6e614275cea7a7dbaa56f88d3de9bdd04cc9ff4cfa45",
32
+ "embeddingSourceSha256": "38ba64989463d38bf6100636a2b1569e43cc32fbe640da6cc4bf0d8b24409c8a",
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.1",
36
+ "packageVersion": "0.3.3",
37
37
  "platform": "darwin",
38
38
  "schemaVersion": 1,
39
- "sourceCommit": "b8975aff1dc06e398c3f78811e76f4ff875729a1"
39
+ "sourceCommit": "b1de42fa724be73e352c34357d42a1ca3946d5b5"
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "A local library for captured articles and verified offline assets.",
5
5
  "repository": {
6
6
  "type": "git",