semdiff 0.1.0 → 0.1.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.
|
@@ -163,7 +163,7 @@ function parseVerdict(data) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// src/version.ts
|
|
166
|
-
var ENGINE_VERSION = "0.1.
|
|
166
|
+
var ENGINE_VERSION = "0.1.1";
|
|
167
167
|
var DEFAULT_PROMPT_VERSION = "0";
|
|
168
168
|
|
|
169
169
|
// src/pipeline/segment.ts
|
|
@@ -273,29 +273,62 @@ function tokenize(unit) {
|
|
|
273
273
|
return normalized.length === 0 ? [] : normalized.split(" ");
|
|
274
274
|
}
|
|
275
275
|
function lcsMatches(a, b) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
const matches = [];
|
|
277
|
+
hirschberg(a, 0, a.length, b, 0, b.length, matches);
|
|
278
|
+
return matches;
|
|
279
|
+
}
|
|
280
|
+
function hirschberg(a, a0, a1, b, b0, b1, out) {
|
|
281
|
+
const n = a1 - a0;
|
|
282
|
+
const m = b1 - b0;
|
|
283
|
+
if (n === 0 || m === 0) return;
|
|
284
|
+
if (n === 1) {
|
|
285
|
+
const key = a[a0];
|
|
286
|
+
for (let j = b0; j < b1; j++) {
|
|
287
|
+
if (b[j] === key) {
|
|
288
|
+
out.push([a0, j]);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
282
291
|
}
|
|
292
|
+
return;
|
|
283
293
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
} else {
|
|
295
|
-
j++;
|
|
294
|
+
const aMid = a0 + (n >> 1);
|
|
295
|
+
const scoreL = lcsRowForward(a, a0, aMid, b, b0, b1);
|
|
296
|
+
const scoreR = lcsRowBackward(a, aMid, a1, b, b0, b1);
|
|
297
|
+
let best = -1;
|
|
298
|
+
let split = 0;
|
|
299
|
+
for (let k = 0; k <= m; k++) {
|
|
300
|
+
const total = scoreL[k] + scoreR[k];
|
|
301
|
+
if (total > best) {
|
|
302
|
+
best = total;
|
|
303
|
+
split = k;
|
|
296
304
|
}
|
|
297
305
|
}
|
|
298
|
-
|
|
306
|
+
hirschberg(a, a0, aMid, b, b0, b0 + split, out);
|
|
307
|
+
hirschberg(a, aMid, a1, b, b0 + split, b1, out);
|
|
308
|
+
}
|
|
309
|
+
function lcsRowForward(a, a0, a1, b, b0, b1) {
|
|
310
|
+
const width = b1 - b0;
|
|
311
|
+
let prev = new Array(width + 1).fill(0);
|
|
312
|
+
let curr = new Array(width + 1).fill(0);
|
|
313
|
+
for (let i = a0; i < a1; i++) {
|
|
314
|
+
for (let k = 1; k <= width; k++) {
|
|
315
|
+
curr[k] = a[i] === b[b0 + k - 1] ? prev[k - 1] + 1 : Math.max(prev[k], curr[k - 1]);
|
|
316
|
+
}
|
|
317
|
+
[prev, curr] = [curr, prev];
|
|
318
|
+
}
|
|
319
|
+
return prev;
|
|
320
|
+
}
|
|
321
|
+
function lcsRowBackward(a, a0, a1, b, b0, b1) {
|
|
322
|
+
const width = b1 - b0;
|
|
323
|
+
let prev = new Array(width + 1).fill(0);
|
|
324
|
+
let curr = new Array(width + 1).fill(0);
|
|
325
|
+
for (let i = a1 - 1; i >= a0; i--) {
|
|
326
|
+
for (let k = width - 1; k >= 0; k--) {
|
|
327
|
+
curr[k] = a[i] === b[b0 + k] ? prev[k + 1] + 1 : Math.max(prev[k], curr[k + 1]);
|
|
328
|
+
}
|
|
329
|
+
[prev, curr] = [curr, prev];
|
|
330
|
+
}
|
|
331
|
+
return prev;
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
// src/pipeline/classify.ts
|
|
@@ -457,4 +490,4 @@ export {
|
|
|
457
490
|
cacheKey,
|
|
458
491
|
diff
|
|
459
492
|
};
|
|
460
|
-
//# sourceMappingURL=chunk-
|
|
493
|
+
//# sourceMappingURL=chunk-MQPL34VH.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema.ts","../src/classifier.ts","../src/classifiers/claude.ts","../src/version.ts","../src/pipeline/segment.ts","../src/pipeline/align.ts","../src/pipeline/classify.ts","../src/cache.ts","../src/index.ts"],"sourcesContent":["/**\r\n * The semdiff public contract (ADR-0006).\r\n *\r\n * A `StructuredDiff` is the engine's primary output. Every human-readable\r\n * rendering is a pure function of it, and machine consumers — notably the\r\n * downstream `sust-reg-reporter` application — integrate against these types\r\n * and their JSON form. This module is pure types and constants: no logic, no\r\n * imports, nothing domain-specific (ADR-0001).\r\n */\r\n\r\n/**\r\n * Version of the StructuredDiff contract. Additive-by-default (ADR-0006): a\r\n * backwards-compatible addition keeps the version; a breaking shape change\r\n * bumps it and gets its own ADR. `StructuredDiff.schemaVersion` is typed as a\r\n * plain `string` (not this literal) so an additive bump is not itself a\r\n * breaking type change for pinned consumers.\r\n */\r\nexport const SCHEMA_VERSION = \"1.0.0\";\r\n\r\n/**\r\n * A `Span` locates a change within ONE input by half-open `[start, end)`\r\n * CHARACTER OFFSETS (ADR-0007).\r\n *\r\n * INVARIANT (load-bearing for consumer citation integrity): offsets index into\r\n * the EXACT, LITERAL, UN-NORMALIZED input string the caller passed. For\r\n * `sust-reg-reporter` that string is the immutable content-addressed snapshot\r\n * text (its ADR-0004 citation integrity, ADR-0011 snapshot store), so the\r\n * offsets resolve against a stored snapshot. Normalization applied internally\r\n * for alignment (whitespace, casing, punctuation, numbering) MUST NOT shift the\r\n * reported offsets. These `{ start, end }` map field-for-field onto the\r\n * consumer's citation span (`@sust-reg/core` `SourceCitation.span`).\r\n */\r\nexport interface Span {\r\n /** Inclusive start character offset into the literal input. */\r\n readonly start: number;\r\n /** Exclusive end character offset into the literal input. */\r\n readonly end: number;\r\n /**\r\n * Optional id of the segmentation unit this span falls in (ADR-0003).\r\n * Additive metadata only — consumers anchor on `start`/`end`, never this.\r\n */\r\n readonly unitId?: string;\r\n}\r\n\r\n/** The kind of edit a change represents. */\r\nexport type ChangeType = \"insertion\" | \"deletion\" | \"modification\" | \"move\";\r\n\r\n/** Whether a change alters meaning (`substantive`) or not (`cosmetic`). */\r\nexport type Classification = \"substantive\" | \"cosmetic\";\r\n\r\n/** One classified change between input A and input B. */\r\nexport interface Change {\r\n readonly type: ChangeType;\r\n readonly classification: Classification;\r\n /** Location in input A; `null` for a pure insertion (absent from A). */\r\n readonly spanA: Span | null;\r\n /** Location in input B; `null` for a pure deletion (absent from B). */\r\n readonly spanB: Span | null;\r\n /**\r\n * Short description of what changed. Present only for substantive\r\n * modifications; the key is OMITTED otherwise (never set to `undefined`,\r\n * per `exactOptionalPropertyTypes`).\r\n */\r\n readonly description?: string;\r\n /** Classifier confidence in `[0, 1]`. */\r\n readonly confidence: number;\r\n /** Set for low-confidence or failed/degraded classifications (ADR-0004). */\r\n readonly needsReview: boolean;\r\n}\r\n\r\n/** The reproducibility stamp for a run (ADR-0004): identifies the model run. */\r\nexport interface Provenance {\r\n readonly modelId: string;\r\n readonly promptVersion: string;\r\n readonly engineVersion: string;\r\n}\r\n\r\n/** Aggregate counts for quick triage. */\r\nexport interface DiffSummary {\r\n readonly substantive: number;\r\n readonly cosmetic: number;\r\n /** Count per change type; all four keys are present (zeros allowed). */\r\n readonly byType: Readonly<Record<ChangeType, number>>;\r\n readonly needsReview: number;\r\n}\r\n\r\n/**\r\n * The engine's primary output (ADR-0006): a stable, versioned, JSON-\r\n * serializable diff. All human-readable views derive from it.\r\n */\r\nexport interface StructuredDiff {\r\n /** The `SCHEMA_VERSION` in effect at emit time; typed `string` for additive bumps. */\r\n readonly schemaVersion: string;\r\n readonly provenance: Provenance;\r\n readonly changes: readonly Change[];\r\n readonly summary: DiffSummary;\r\n}\r\n","/**\n * The classification boundary (ADR-0004).\n *\n * semdiff uses the LLM strictly as a gated, structured classifier behind a\n * small `Classifier` interface — never a free-form diff narrator, never a\n * hardwired SDK. The default provider is the latest capable Claude model,\n * injected via config, so consumers (e.g. `sust-reg-reporter`) are not forced\n * to own provider wiring.\n */\nimport type { Classification, Span } from \"./schema.ts\";\n\n/**\n * The structural kind of a candidate change. A subset of `ChangeType`: `move`\n * is detected deterministically before classification (ADR-0010), so it never\n * reaches the model.\n */\nexport type CandidateType = \"insertion\" | \"deletion\" | \"modification\";\n\n/**\n * A single changed pair handed to the classifier for a verdict. One side may be\n * absent: for an insertion `a` is `\"\"` and `spanA` is `null`; for a deletion\n * `b` is `\"\"` and `spanB` is `null` (ADR-0011).\n */\nexport interface CandidatePair {\n /** The structural kind of change. */\n readonly type: CandidateType;\n /** The unit text from input A; `\"\"` for an insertion. */\n readonly a: string;\n /** The unit text from input B; `\"\"` for a deletion. */\n readonly b: string;\n /** Location of `a` within input A (literal character offsets, ADR-0007); `null` for an insertion. */\n readonly spanA: Span | null;\n /** Location of `b` within input B (literal character offsets, ADR-0007); `null` for a deletion. */\n readonly spanB: Span | null;\n}\n\n/** The schema-validated verdict for one candidate pair. */\nexport interface ClassifierVerdict {\n readonly classification: Classification;\n /** Present only for a substantive verdict; OMITTED otherwise. */\n readonly description?: string;\n /** Provider/engine confidence in `[0, 1]`. */\n readonly confidence: number;\n}\n\n/** The injectable provider boundary. An implementation wraps one LLM provider. */\nexport interface Classifier {\n classify(pair: CandidatePair): Promise<ClassifierVerdict>;\n}\n\n/**\n * Default model id (the latest capable Claude, ADR-0004). Stamped into run\n * provenance, and used by the default classifier when the caller does not pin\n * one. Callers that inject their own provider should pass `modelId` for an\n * accurate provenance stamp.\n */\nexport const DEFAULT_MODEL_ID = \"claude-opus-4-8\";\n\n/**\n * The never-drop / never-fabricate fallback verdict (ADR-0004). When a model\n * response fails schema validation, retries are exhausted, or the provider\n * errors, the `classify` stage records this conservative verdict — `substantive`\n * (so the change is surfaced for review, not hidden) with zero confidence — and\n * flags the resulting change for review, rather than dropping the pair or\n * guessing a cosmetic/substantive call.\n */\nexport function needsReviewVerdict(): ClassifierVerdict {\n return { classification: \"substantive\", confidence: 0 };\n}\n","/**\r\n * Default classifier — calls the Anthropic Messages API to judge whether a\r\n * change is substantive or cosmetic (ADR-0004, ADR-0009).\r\n *\r\n * It uses the global `fetch` (no SDK), so the engine keeps ZERO runtime\r\n * dependencies; a consumer that needs a different provider or transport injects\r\n * its own `Classifier` instead. Determinism is steered by a pinned model, a\r\n * pinned prompt, and low effort where the model accepts it — Opus 4.8 removed the\r\n * `temperature` parameter, so there is no `temperature: 0`. The verdict is returned through a constrained\r\n * JSON schema and then RE-VALIDATED by the classify stage, so this module can\r\n * parse leniently: any malformed response surfaces as a thrown error that the\r\n * classify stage retries, then degrades to needs-review.\r\n *\r\n * Transport resilience (ADR-0012): each call has a timeout and retries transient\r\n * failures — HTTP 429/5xx, network errors, and abort timeouts — with exponential\r\n * backoff, honouring a `Retry-After` header. Non-transient errors (400, auth)\r\n * fail fast. This is distinct from the classify stage's verdict-level retry: the\r\n * provider exhausts its backoff first, and only if it still throws does the\r\n * stage's safety net degrade the pair to needs-review.\r\n */\r\nimport { DEFAULT_MODEL_ID, type CandidatePair, type Classifier, type ClassifierVerdict } from \"../classifier.ts\";\r\n\r\nconst MESSAGES_URL = \"https://api.anthropic.com/v1/messages\";\r\nconst ANTHROPIC_VERSION = \"2023-06-01\";\r\nconst MAX_TOKENS = 1024;\r\n\r\n/** Per-request timeout before the call is aborted and treated as transient (ADR-0012). */\r\nconst DEFAULT_TIMEOUT_MS = 60_000;\r\n/** Retries after the initial attempt on a transient failure (ADR-0012). */\r\nconst DEFAULT_MAX_RETRIES = 2;\r\n/** Backoff for retry n is BASE * 2**n plus jitter, capped at MAX (ADR-0012). */\r\nconst BASE_RETRY_DELAY_MS = 500;\r\nconst MAX_RETRY_DELAY_MS = 8_000;\r\n\r\n/**\r\n * Static classification instructions — the stable, cacheable prompt prefix\r\n * (ADR-0009). Domain-neutral (ADR-0001). Recall-biased per ADR-0005: when\r\n * uncertain, prefer \"substantive\" so a real change is surfaced, not hidden.\r\n *\r\n * Exported so a release-gating test can pin its hash to `DEFAULT_PROMPT_VERSION`\r\n * (ADR-0005): editing this text without bumping the version would let a persisted\r\n * verdict cache serve stale results. Not part of the package's public `exports`.\r\n */\r\nexport const SYSTEM_PROMPT = [\r\n \"You are a careful classifier inside a meaning-aware diff engine. You are given\",\r\n \"two versions of one short span of prose: version A (before) and version B\",\r\n \"(after). Decide whether the change from A to B is:\",\r\n \"\",\r\n '- \"substantive\": it alters the meaning — a changed value, number, date,',\r\n \" condition, scope, or any wording a careful reader would act on differently.\",\r\n '- \"cosmetic\": it preserves the meaning — formatting, punctuation, casing,',\r\n \" whitespace, renumbering, or a meaning-preserving rewording.\",\r\n \"\",\r\n \"One side may be empty: an empty A means the B text was newly inserted, and an\",\r\n \"empty B means the A text was removed. Judge whether that insertion or removal\",\r\n \"is substantive (it adds or removes meaning, an obligation, or a condition) or\",\r\n \"cosmetic (boilerplate, formatting, or duplicate content).\",\r\n \"\",\r\n \"Rules:\",\r\n \"- Judge only these two snippets; do not assume external context.\",\r\n '- When genuinely uncertain whether the meaning changed, choose \"substantive\":',\r\n \" it is safer to surface a real change than to hide one.\",\r\n '- For a substantive change, give a one-sentence factual \"description\" of what',\r\n \" changed — no advice and no judgement of how significant it is.\",\r\n '- Set \"confidence\" in [0, 1] for how sure you are of the classification.',\r\n].join(\"\\n\");\r\n\r\n/** Constrained output shape (structured outputs). Ranges are validated downstream. */\r\nconst VERDICT_SCHEMA = {\r\n type: \"object\",\r\n properties: {\r\n classification: { type: \"string\", enum: [\"substantive\", \"cosmetic\"] },\r\n description: { type: \"string\" },\r\n confidence: { type: \"number\" },\r\n },\r\n required: [\"classification\", \"confidence\"],\r\n additionalProperties: false,\r\n} as const;\r\n\r\n/** Configuration for the default Anthropic-backed classifier. */\r\nexport interface DefaultClassifierConfig {\r\n /** Model id; defaults to the latest capable Claude (ADR-0004). */\r\n readonly modelId?: string;\r\n /** API key; defaults to `process.env.ANTHROPIC_API_KEY`. */\r\n readonly apiKey?: string;\r\n /** Per-request timeout in ms before the call is aborted and retried (ADR-0012). Default 60000. */\r\n readonly timeoutMs?: number;\r\n /** Retries on a transient failure — 429, 5xx, network, or timeout (ADR-0012). Default 2. */\r\n readonly maxRetries?: number;\r\n}\r\n\r\n/**\r\n * Construct the default classifier. Throws immediately if no API key is\r\n * available, so a diff that needs the model fails at a clear boundary rather\r\n * than per-call.\r\n */\r\nexport function createDefaultClassifier(config: DefaultClassifierConfig): Classifier {\r\n const modelId = config.modelId ?? DEFAULT_MODEL_ID;\r\n const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;\r\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\r\n const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\r\n if (apiKey === undefined || apiKey === \"\") {\r\n throw new Error(\"createDefaultClassifier: no API key (set ANTHROPIC_API_KEY or pass config.apiKey)\");\r\n }\r\n\r\n return {\r\n classify: (pair: CandidatePair): Promise<ClassifierVerdict> => {\r\n const init: RequestInit = {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n \"x-api-key\": apiKey,\r\n \"anthropic-version\": ANTHROPIC_VERSION,\r\n },\r\n body: JSON.stringify(buildRequest(modelId, pair)),\r\n };\r\n return classifyWithRetry(() => classifyOnce(init, timeoutMs), maxRetries);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * A transient failure worth retrying: a 429/5xx response, a network error, or a\r\n * timeout. `retryAfterMs` is the server's hint (0 if none). Non-transient errors\r\n * are thrown as plain `Error`s and propagate without a retry.\r\n */\r\nclass TransientError extends Error {\r\n // A field declaration + assignment, not a constructor parameter property:\r\n // parameter properties are runtime syntax that Node's strip-only type removal\r\n // cannot handle, which would break the zero-build `node src/...` path (ADR-0002).\r\n readonly retryAfterMs: number;\r\n constructor(message: string, retryAfterMs: number) {\r\n super(message);\r\n this.retryAfterMs = retryAfterMs;\r\n }\r\n}\r\n\r\n/** Run `attempt`, retrying transient failures with backoff up to `maxRetries` (ADR-0012). */\r\nasync function classifyWithRetry(\r\n attempt: () => Promise<ClassifierVerdict>,\r\n maxRetries: number,\r\n): Promise<ClassifierVerdict> {\r\n for (let retry = 0; ; retry += 1) {\r\n try {\r\n return await attempt();\r\n } catch (error) {\r\n if (!(error instanceof TransientError) || retry >= maxRetries) throw error;\r\n await sleep(backoffMs(retry, error.retryAfterMs));\r\n }\r\n }\r\n}\r\n\r\n/** One request attempt: transient failures throw `TransientError`, others throw plainly. */\r\nasync function classifyOnce(init: RequestInit, timeoutMs: number): Promise<ClassifierVerdict> {\r\n let response: Response;\r\n try {\r\n response = await fetchWithTimeout(MESSAGES_URL, init, timeoutMs);\r\n } catch (cause) {\r\n // Aborted (timeout) or a network failure — both transient.\r\n throw new TransientError(`Anthropic API request failed: ${(cause as Error).message}`, 0);\r\n }\r\n if (!response.ok) {\r\n const message = `Anthropic API error ${response.status}: ${await response.text()}`;\r\n if (response.status === 429 || response.status >= 500) {\r\n throw new TransientError(message, retryAfterMs(response.headers));\r\n }\r\n throw new Error(message);\r\n }\r\n return parseVerdict(await response.json());\r\n}\r\n\r\n/** `fetch` with an abort-based timeout; the timer is always cleared. */\r\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\r\n const controller = new AbortController();\r\n const timer = setTimeout(() => controller.abort(), timeoutMs);\r\n try {\r\n return await fetch(url, { ...init, signal: controller.signal });\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n\r\n/** Backoff for retry `n`: the server's `Retry-After` if given, else exponential with jitter. */\r\nfunction backoffMs(retry: number, retryAfterMs: number): number {\r\n if (retryAfterMs > 0) return Math.min(retryAfterMs, MAX_RETRY_DELAY_MS);\r\n const exponential = BASE_RETRY_DELAY_MS * 2 ** retry;\r\n return Math.min(exponential + exponential * 0.25 * Math.random(), MAX_RETRY_DELAY_MS);\r\n}\r\n\r\n/** Parse `Retry-After` (seconds) into ms; 0 when absent or unparseable. */\r\nfunction retryAfterMs(headers: Headers): number {\r\n const seconds = Number(headers.get(\"retry-after\"));\r\n return seconds > 0 ? seconds * 1000 : 0;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n setTimeout(resolve, ms);\r\n });\r\n}\r\n\r\n/** Build the Messages API request body for one candidate pair. */\r\nfunction buildRequest(modelId: string, pair: CandidatePair): unknown {\r\n const outputConfig: Record<string, unknown> = {\r\n format: { type: \"json_schema\", schema: VERDICT_SCHEMA },\r\n };\r\n // `effort` steers determinism and cost, but only Opus and Sonnet 4.6 accept\r\n // it; Haiku and Sonnet 4.5 reject it with a 400. Include it only where\r\n // supported so overriding `modelId` to a cheaper model (an eval sweep, say)\r\n // does not fail. Omitting it is harmless; sending it where unsupported is not.\r\n if (modelSupportsEffort(modelId)) {\r\n outputConfig.effort = \"low\";\r\n }\r\n return {\r\n model: modelId,\r\n max_tokens: MAX_TOKENS,\r\n output_config: outputConfig,\r\n system: [{ type: \"text\", text: SYSTEM_PROMPT, cache_control: { type: \"ephemeral\" } }],\r\n messages: [{ role: \"user\", content: `Change type: ${pair.type}.\\nA:\\n${pair.a}\\n\\nB:\\n${pair.b}` }],\r\n };\r\n}\r\n\r\n/**\r\n * Whether `output_config.effort` is accepted by `modelId`. Opus (4.5+) and\r\n * Sonnet 4.6 support it; Haiku and Sonnet 4.5 return a 400. Biased to omit when\r\n * unsure — a missing effort still succeeds, an unsupported effort does not — so\r\n * a future model silently runs without effort rather than erroring.\r\n */\r\nfunction modelSupportsEffort(modelId: string): boolean {\r\n return modelId.startsWith(\"claude-opus-\") || modelId.startsWith(\"claude-sonnet-4-6\");\r\n}\r\n\r\n/** Extract the structured verdict from the Messages API response (lenient). */\r\nfunction parseVerdict(data: unknown): ClassifierVerdict {\r\n const message = data as { content?: ReadonlyArray<{ type?: string; text?: string }> };\r\n const text = message.content?.find((block) => block.type === \"text\")?.text;\r\n if (text === undefined) {\r\n throw new Error(\"Anthropic API returned no text content\");\r\n }\r\n return JSON.parse(text) as ClassifierVerdict;\r\n}\r\n","/**\r\n * Version constants feeding the reproducibility stamp (ADR-0004). A run is\r\n * stamped with the model id, the prompt version, and this engine version so\r\n * results are reproducible and cache keys are stable.\r\n */\r\n\r\n/**\r\n * semdiff engine version, stamped into `Provenance.engineVersion`. Kept in sync\r\n * with `package.json` `version`; `test/version.contract.test.ts` fails the build\r\n * if the two drift, so a published artifact never stamps a stale version.\r\n */\r\nexport const ENGINE_VERSION = \"0.1.0\";\r\n\r\n/** Default prompt-template version, stamped into `Provenance.promptVersion`. */\r\nexport const DEFAULT_PROMPT_VERSION = \"0\";\r\n","/**\r\n * Stage 1 — segment (ADR-0003). Local and deterministic; no model.\r\n *\r\n * Split an input into comparable units. At `sentence` granularity each unit is\r\n * a sentence; at `clause` granularity sentences are further divided at strong\r\n * intra-sentence separators. Each `Unit` carries the half-open `[start, end)`\r\n * CHARACTER OFFSETS of its text within the LITERAL input, so spans reported\r\n * downstream index the caller's exact input (the offset invariant, ADR-0007):\r\n * `input.slice(unit.span.start, unit.span.end) === unit.text` always holds.\r\n * Whitespace at unit boundaries is excluded from the span (and the text); no\r\n * other normalization is applied, so offsets never drift.\r\n */\r\nimport type { Span } from \"../schema.ts\";\r\n\r\n/** The granularity at which an input is segmented. */\r\nexport type SegmentGranularity = \"sentence\" | \"clause\";\r\n\r\n/** One comparable unit of an input, anchored to the literal input by offsets. */\r\nexport interface Unit {\r\n /** The unit's text, verbatim from the input (boundary whitespace trimmed). */\r\n readonly text: string;\r\n /** Half-open offsets of `text` within the literal input. */\r\n readonly span: Span;\r\n}\r\n\r\n/**\r\n * Sentence breaking is language-aware, and determinism is a core guarantee\r\n * (ADR-0005), so we pin the locale rather than use the ambient runtime locale.\r\n * Making the locale configurable is a later additive change.\r\n */\r\nconst SENTENCE_SEGMENTER = new Intl.Segmenter(\"en\", { granularity: \"sentence\" });\r\n\r\n/**\r\n * Strong intra-sentence clause separators. Comma-level splitting is deliberately\r\n * excluded — too unreliable to be deterministically useful — and enumerated-\r\n * clause structural cues are a future additive enhancement.\r\n */\r\nconst CLAUSE_DELIMITERS = \";:\";\r\n\r\n/**\r\n * Segment `text` into ordered `Unit`s at the given granularity. Deterministic;\r\n * no model. Empty and whitespace-only inputs yield no units.\r\n */\r\nexport function segment(text: string, granularity: SegmentGranularity): readonly Unit[] {\r\n const units: Unit[] = [];\r\n const delimiters = granularity === \"clause\" ? CLAUSE_DELIMITERS : \"\";\r\n for (const { segment: sentence, index } of SENTENCE_SEGMENTER.segment(text)) {\r\n emitUnits(sentence, index, delimiters, units);\r\n }\r\n return units;\r\n}\r\n\r\n/**\r\n * Emit trimmed units from a sentence `chunk` located at absolute offset `base`.\r\n * With no delimiters the chunk is a single unit; otherwise it is split at each\r\n * delimiter character, offsets staying absolute into the literal input.\r\n */\r\nfunction emitUnits(chunk: string, base: number, delimiters: string, out: Unit[]): void {\r\n if (delimiters.length === 0) {\r\n pushTrimmed(chunk, base, out);\r\n return;\r\n }\r\n let cursor = 0;\r\n for (let i = 0; i < chunk.length; i++) {\r\n if (delimiters.includes(chunk[i]!)) {\r\n pushTrimmed(chunk.slice(cursor, i), base + cursor, out);\r\n cursor = i + 1;\r\n }\r\n }\r\n pushTrimmed(chunk.slice(cursor), base + cursor, out);\r\n}\r\n\r\n/**\r\n * Trim boundary whitespace from `part` and, if non-empty, push a `Unit` whose\r\n * span points at the trimmed content within the literal input (`base` is the\r\n * absolute offset of `part`).\r\n */\r\nfunction pushTrimmed(part: string, base: number, out: Unit[]): void {\r\n const trimmed = part.trim();\r\n if (trimmed.length === 0) return;\r\n const start = base + (part.length - part.trimStart().length);\r\n out.push({ text: trimmed, span: { start, end: start + trimmed.length } });\r\n}\r\n","/**\r\n * Stage 2 — align (ADR-0003). Local and deterministic; no LLM.\r\n *\r\n * Match units across A and B and tag each pairing so the stage 2 -> 3 gate can\r\n * keep unchanged and cosmetic content away from the model:\r\n *\r\n * - `unchanged` — paired and textually identical.\r\n * - `trivial-change` — paired after normalization (whitespace, casing,\r\n * punctuation, and leading enumeration collapsed) but the\r\n * literal text differs. A cosmetic edit.\r\n * - `move` — a relocation of identical content (ADR-0010): a deletion\r\n * whose normalized content matches an insertion elsewhere,\r\n * re-paired into one change. Both old (`a`) and new (`b`)\r\n * positions are present; the text is unchanged.\r\n * - `candidate` — a genuine change needing downstream judgment: a paired\r\n * modification (both sides present), or a one-sided\r\n * insertion (`a === null`) or deletion (`b === null`).\r\n *\r\n * Pairing runs a longest-common-subsequence match over the normalized keys, then\r\n * pairs the survivors in each gap positionally when they share a token. A final\r\n * pass re-pairs content-identical deletion/insertion survivors into `move`s.\r\n *\r\n * Normalization is used ONLY to decide matches; it never touches the `Unit`\r\n * offsets, so the literal-input invariant (ADR-0007) is preserved untouched.\r\n */\r\nimport type { Unit } from \"./segment.ts\";\r\n\r\n/** How an aligned pairing relates its A and B units. */\r\nexport type AlignmentTag = \"unchanged\" | \"trivial-change\" | \"move\" | \"candidate\";\r\n\r\n/** A pairing of units across inputs; either side may be `null`. */\r\nexport interface AlignedPair {\r\n readonly tag: AlignmentTag;\r\n /** Unit from A, or `null` for an insertion. */\r\n readonly a: Unit | null;\r\n /** Unit from B, or `null` for a deletion. */\r\n readonly b: Unit | null;\r\n}\r\n\r\n/** Leading list/enumeration marker, e.g. \"1.\", \"1)\", \"(a)\", \"iv.\", or a bullet. */\r\nconst LEADING_ENUMERATOR = /^\\s*(?:[([]?\\s*(?:\\d{1,3}|[a-z]{1,2}|[ivxlcdm]{1,5})\\s*[)\\].]|[-*•·])\\s+/iu;\r\n\r\n/**\r\n * Unicode punctuation — quotes, dashes, periods, commas, parentheses, etc.\r\n * Symbols are deliberately KEPT: collapsing e.g. \"<\" and \">\" (or \"=\" / \"+\")\r\n * would mask a substantive change as cosmetic, and missing substance is the\r\n * costly error (ADR-0005).\r\n */\r\nconst PUNCTUATION = /\\p{P}/gu;\r\n\r\n/**\r\n * Align the segmented units of A and B into tagged pairings, in order.\r\n * Deterministic; no model.\r\n */\r\nexport function align(unitsA: readonly Unit[], unitsB: readonly Unit[]): readonly AlignedPair[] {\r\n const keysA = unitsA.map(normalize);\r\n const keysB = unitsB.map(normalize);\r\n const matches = lcsMatches(keysA, keysB);\r\n\r\n const out: AlignedPair[] = [];\r\n let i = 0;\r\n let j = 0;\r\n for (const [mi, mj] of matches) {\r\n emitGap(unitsA.slice(i, mi), unitsB.slice(j, mj), out);\r\n const a = unitsA[mi]!;\r\n const b = unitsB[mj]!;\r\n out.push({ tag: a.text === b.text ? \"unchanged\" : \"trivial-change\", a, b });\r\n i = mi + 1;\r\n j = mj + 1;\r\n }\r\n emitGap(unitsA.slice(i), unitsB.slice(j), out);\r\n return detectMoves(out);\r\n}\r\n\r\n/**\r\n * Pair the survivors in a gap: positionally, as a `candidate` modification when\r\n * the two units share a token, otherwise as a separate deletion and insertion.\r\n * Any leftover units are one-sided deletions (A) or insertions (B).\r\n */\r\nfunction emitGap(gapA: readonly Unit[], gapB: readonly Unit[], out: AlignedPair[]): void {\r\n const paired = Math.min(gapA.length, gapB.length);\r\n let k = 0;\r\n for (; k < paired; k++) {\r\n const a = gapA[k]!;\r\n const b = gapB[k]!;\r\n if (sharesToken(a, b)) {\r\n out.push({ tag: \"candidate\", a, b });\r\n } else {\r\n out.push({ tag: \"candidate\", a, b: null });\r\n out.push({ tag: \"candidate\", a: null, b });\r\n }\r\n }\r\n for (; k < gapA.length; k++) out.push({ tag: \"candidate\", a: gapA[k]!, b: null });\r\n for (; k < gapB.length; k++) out.push({ tag: \"candidate\", a: null, b: gapB[k]! });\r\n}\r\n\r\n/**\r\n * Re-pair content-identical deletion/insertion survivors into `move`s (ADR-0010).\r\n * A deletion is matched to an insertion with the same normalized key; the move\r\n * keeps the deletion's old position (`a`) and the insertion's new position (`b`).\r\n * Unmatched insertions/deletions are left as-is.\r\n *\r\n * Matching is content-only and 1:1 — it weighs neither distance nor document\r\n * structure (ADR-0010). Two consequences follow from keying on normalized text:\r\n * when several insertions share a key the LAST one wins (the map overwrites), and\r\n * if the same text genuinely appears as an unrelated deletion AND insertion they\r\n * collapse into one `move`. Acceptable at sentence/clause granularity, where\r\n * identical-content survivors are overwhelmingly true relocations.\r\n */\r\nfunction detectMoves(pairs: readonly AlignedPair[]): readonly AlignedPair[] {\r\n const insertionByKey = new Map<string, number>();\r\n pairs.forEach((pair, index) => {\r\n if (pair.tag === \"candidate\" && pair.a === null) {\r\n insertionByKey.set(normalize(pair.b!), index);\r\n }\r\n });\r\n\r\n const moveTo = new Map<number, number>();\r\n pairs.forEach((pair, index) => {\r\n if (pair.tag === \"candidate\" && pair.b === null) {\r\n const key = normalize(pair.a!);\r\n const insertionIndex = insertionByKey.get(key);\r\n if (insertionIndex !== undefined) {\r\n insertionByKey.delete(key);\r\n moveTo.set(index, insertionIndex);\r\n }\r\n }\r\n });\r\n\r\n if (moveTo.size === 0) return pairs;\r\n\r\n const movedInsertions = new Set(moveTo.values());\r\n return pairs.flatMap((pair, index) => {\r\n if (movedInsertions.has(index)) return [];\r\n const insertionIndex = moveTo.get(index);\r\n return insertionIndex === undefined ? [pair] : [{ tag: \"move\" as const, a: pair.a, b: pairs[insertionIndex]!.b }];\r\n });\r\n}\r\n\r\n/** Normalized match key: lower-cased, enumerator-stripped, punctuation-free. */\r\nfunction normalize(unit: Unit): string {\r\n return unit.text\r\n .toLowerCase()\r\n .replace(LEADING_ENUMERATOR, \"\")\r\n .replace(PUNCTUATION, \" \")\r\n .replace(/\\s+/g, \" \")\r\n .trim();\r\n}\r\n\r\n/** Whether two units share at least one normalized token (a weak similarity gate). */\r\nfunction sharesToken(a: Unit, b: Unit): boolean {\r\n const tokensB = new Set(tokenize(b));\r\n return tokenize(a).some((token) => tokensB.has(token));\r\n}\r\n\r\nfunction tokenize(unit: Unit): string[] {\r\n const normalized = normalize(unit);\r\n return normalized.length === 0 ? [] : normalized.split(\" \");\r\n}\r\n\r\n/**\r\n * Longest-common-subsequence match over two key sequences, returned as ordered\r\n * `[indexInA, indexInB]` pairs of equal keys.\r\n */\r\nfunction lcsMatches(a: readonly string[], b: readonly string[]): Array<readonly [number, number]> {\r\n const n = a.length;\r\n const m = b.length;\r\n const dp: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(m + 1).fill(0));\r\n for (let i = n - 1; i >= 0; i--) {\r\n for (let j = m - 1; j >= 0; j--) {\r\n dp[i]![j] = a[i] === b[j] ? dp[i + 1]![j + 1]! + 1 : Math.max(dp[i + 1]![j]!, dp[i]![j + 1]!);\r\n }\r\n }\r\n\r\n const matches: Array<readonly [number, number]> = [];\r\n let i = 0;\r\n let j = 0;\r\n while (i < n && j < m) {\r\n if (a[i] === b[j]) {\r\n matches.push([i, j]);\r\n i++;\r\n j++;\r\n } else if (dp[i + 1]![j]! >= dp[i]![j + 1]!) {\r\n i++;\r\n } else {\r\n j++;\r\n }\r\n }\r\n return matches;\r\n}\r\n","/**\n * Stage 3 — classify (ADR-0003, ADR-0004). The gated, structured LLM step.\n *\n * The caller passes only genuine `candidate` pairs — align has already kept\n * unchanged, trivial-change, and move content away from the model. A candidate\n * may be a modification (both sides present) or a one-sided insertion/deletion\n * (ADR-0011); for each, this stage asks the injected `Classifier` for a verdict,\n * validates it against the schema, retries once on a malformed response or a\n * provider error, and finally degrades to a flagged `needs-review` change —\n * never dropping a pair and never fabricating a verdict (ADR-0004). The provider\n * stays injected; this module imports no SDK, so the engine has no LLM-infra\n * dependency.\n *\n * Caching (ADR-0004's content-addressed cache) belongs with the provider\n * implementation, not this stage, and is out of scope here.\n */\nimport type { Change } from \"../schema.ts\";\nimport { needsReviewVerdict, type CandidatePair, type Classifier, type ClassifierVerdict } from \"../classifier.ts\";\n\n/** Attempts per pair: one initial call plus one retry (ADR-0004). */\nconst MAX_ATTEMPTS = 2;\n\n/** Verdicts below this confidence are flagged for review (ADR-0006). */\nconst MIN_TRUSTED_CONFIDENCE = 0.5;\n\n/**\n * Classify changed candidate pairs into `Change`s using the injected classifier.\n * Order is preserved; each change carries the candidate's type and spans untouched.\n */\nexport async function classify(\n candidates: readonly CandidatePair[],\n classifier: Classifier,\n): Promise<readonly Change[]> {\n const changes: Change[] = [];\n for (const pair of candidates) {\n changes.push(await classifyPair(pair, classifier));\n }\n return changes;\n}\n\nasync function classifyPair(pair: CandidatePair, classifier: Classifier): Promise<Change> {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n let verdict: unknown;\n try {\n verdict = await classifier.classify(pair);\n } catch {\n continue; // provider error / timeout / rate limit — retry, then needs-review\n }\n if (isValidVerdict(verdict)) {\n return toChange(pair, verdict);\n }\n }\n return needsReviewChange(pair);\n}\n\n/** Runtime guard: the model response is untrusted until validated (ADR-0004). */\nfunction isValidVerdict(value: unknown): value is ClassifierVerdict {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.classification !== \"substantive\" && v.classification !== \"cosmetic\") return false;\n if (typeof v.confidence !== \"number\" || !Number.isFinite(v.confidence)) return false;\n if (v.confidence < 0 || v.confidence > 1) return false;\n if (v.description !== undefined && typeof v.description !== \"string\") return false;\n return true;\n}\n\nfunction toChange(pair: CandidatePair, verdict: ClassifierVerdict): Change {\n const base = {\n type: pair.type,\n classification: verdict.classification,\n spanA: pair.spanA,\n spanB: pair.spanB,\n confidence: verdict.confidence,\n needsReview: verdict.confidence < MIN_TRUSTED_CONFIDENCE,\n };\n return verdict.description === undefined ? base : { ...base, description: verdict.description };\n}\n\nfunction needsReviewChange(pair: CandidatePair): Change {\n const { classification, confidence } = needsReviewVerdict();\n return {\n type: pair.type,\n classification,\n spanA: pair.spanA,\n spanB: pair.spanB,\n confidence,\n needsReview: true,\n };\n}\n","/**\r\n * Content-addressed verdict cache (ADR-0004). Identical classification inputs\r\n * return the cached verdict without a second model call — the primary\r\n * determinism and cost guarantee.\r\n *\r\n * The key is a hash of the normalized pair text plus the prompt version and\r\n * model id, so the same change under the same model/prompt is classified once.\r\n * Spans are deliberately NOT part of the key: the verdict (substantive/cosmetic\r\n * + description + confidence) describes the content change, not where it sits,\r\n * so a pair classified once applies wherever that text appears.\r\n *\r\n * The default cache is in-memory and process-local; inject a `VerdictCache` to\r\n * back it with a persistent store — the engine keeps no backend of its own\r\n * (ADR-0001). Reuse the wrapped classifier across `diff` calls to share it.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport type { CandidatePair, Classifier, ClassifierVerdict } from \"./classifier.ts\";\r\n\r\n/**\r\n * Field separator for the cache key. Normalization collapses whitespace and\r\n * never emits a NUL byte, so distinct field boundaries can never collide.\r\n */\r\nconst FIELD_SEPARATOR = String.fromCharCode(0);\r\n\r\n/** A store for classification verdicts, keyed by content hash. May be async. */\r\nexport interface VerdictCache {\r\n get(key: string): Promise<ClassifierVerdict | undefined>;\r\n set(key: string, verdict: ClassifierVerdict): Promise<void>;\r\n}\r\n\r\n/** An in-memory `VerdictCache` (process-local; the default). */\r\nexport function createMemoryCache(): VerdictCache {\r\n const store = new Map<string, ClassifierVerdict>();\r\n return {\r\n get: (key) => Promise.resolve(store.get(key)),\r\n set: (key, verdict) => {\r\n store.set(key, verdict);\r\n return Promise.resolve();\r\n },\r\n };\r\n}\r\n\r\n/** Options for `withCache`. `modelId` and `promptVersion` are part of the key. */\r\nexport interface CacheOptions {\r\n readonly modelId: string;\r\n readonly promptVersion: string;\r\n readonly cache?: VerdictCache;\r\n}\r\n\r\n/**\r\n * Wrap a `Classifier` so identical inputs are classified once. Reuse the\r\n * returned classifier across `diff` calls to share the cache.\r\n */\r\nexport function withCache(classifier: Classifier, options: CacheOptions): Classifier {\r\n const cache = options.cache ?? createMemoryCache();\r\n return {\r\n classify: async (pair: CandidatePair): Promise<ClassifierVerdict> => {\r\n const key = cacheKey(pair, options.modelId, options.promptVersion);\r\n const cached = await cache.get(key);\r\n if (cached !== undefined) return cached;\r\n const verdict = await classifier.classify(pair);\r\n await cache.set(key, verdict);\r\n return verdict;\r\n },\r\n };\r\n}\r\n\r\n/** Content-addressed key: a hash of (normalized a, normalized b, prompt, model). */\r\nexport function cacheKey(pair: CandidatePair, modelId: string, promptVersion: string): string {\r\n const parts = [normalize(pair.a), normalize(pair.b), promptVersion, modelId];\r\n return createHash(\"sha256\").update(parts.join(FIELD_SEPARATOR)).digest(\"hex\");\r\n}\r\n\r\nfunction normalize(text: string): string {\r\n return text.replace(/\\s+/g, \" \").trim();\r\n}\r\n","/**\n * semdiff — meaning-aware diff engine (library entry point).\n *\n * The library is the source of truth (ADR-0002); the CLI is a thin wrapper.\n * `diff` runs the segment -> align -> classify pipeline (ADR-0003) and assembles\n * the versioned `StructuredDiff` (ADR-0006), the engine's public contract.\n */\nexport * from \"./schema.ts\";\nexport * from \"./classifier.ts\";\n\nimport { SCHEMA_VERSION, type Change, type DiffSummary, type Provenance, type StructuredDiff } from \"./schema.ts\";\nimport { DEFAULT_MODEL_ID, type CandidatePair, type Classifier } from \"./classifier.ts\";\nimport { createDefaultClassifier } from \"./classifiers/claude.ts\";\nimport { ENGINE_VERSION, DEFAULT_PROMPT_VERSION } from \"./version.ts\";\nimport { segment, type SegmentGranularity, type Unit } from \"./pipeline/segment.ts\";\nimport { align } from \"./pipeline/align.ts\";\nimport { classify } from \"./pipeline/classify.ts\";\n\nexport { ENGINE_VERSION, DEFAULT_PROMPT_VERSION };\nexport { createDefaultClassifier, type DefaultClassifierConfig } from \"./classifiers/claude.ts\";\nexport { withCache, createMemoryCache, cacheKey, type VerdictCache, type CacheOptions } from \"./cache.ts\";\n\n/** Options for a `diff` run. Omit a field to take its default. */\nexport interface DiffOptions {\n /**\n * Provider used to classify changed pairs. Defaults to the latest capable\n * Claude model via `createDefaultClassifier` (ADR-0004) — which is only\n * constructed when there is a change to classify (a modification, insertion,\n * or deletion). Identical, cosmetic, and moved content needs no provider.\n */\n readonly classifier?: Classifier;\n /** Model id stamped into provenance; also passed to the default classifier. */\n readonly modelId?: string;\n /** Prompt-template version stamped into provenance. */\n readonly promptVersion?: string;\n /** Granularity at which inputs are segmented (ADR-0003). */\n readonly segmentGranularity?: SegmentGranularity;\n}\n\n/**\n * Produce a meaning-aware structured diff of two inputs. Runs\n * segment -> align -> classify, stamps run provenance, and assembles the\n * `StructuredDiff`. A classifier is constructed and called only when there is at\n * least one change to classify (a modification, insertion, or deletion), so\n * diffs of identical, cosmetic, or merely relocated content need no provider.\n */\nexport async function diff(a: string, b: string, options?: DiffOptions): Promise<StructuredDiff> {\n const granularity = options?.segmentGranularity ?? \"sentence\";\n const pairs = align(segment(a, granularity), segment(b, granularity));\n\n const candidates: CandidatePair[] = [];\n for (const pair of pairs) {\n if (pair.tag !== \"candidate\") continue;\n if (pair.a !== null && pair.b !== null) {\n candidates.push({ type: \"modification\", a: pair.a.text, b: pair.b.text, spanA: pair.a.span, spanB: pair.b.span });\n } else if (pair.b !== null) {\n candidates.push({ type: \"insertion\", a: \"\", b: pair.b.text, spanA: null, spanB: pair.b.span });\n } else {\n candidates.push({ type: \"deletion\", a: pair.a!.text, b: \"\", spanA: pair.a!.span, spanB: null });\n }\n }\n\n const modelId = options?.modelId ?? DEFAULT_MODEL_ID;\n const classified =\n candidates.length === 0\n ? []\n : await classify(candidates, options?.classifier ?? createDefaultClassifier({ modelId }));\n\n const changes: Change[] = [];\n let classifiedIndex = 0;\n for (const pair of pairs) {\n if (pair.tag === \"unchanged\") continue;\n if (pair.tag === \"trivial-change\") {\n changes.push(cosmeticModification(pair.a!, pair.b!));\n } else if (pair.tag === \"move\") {\n changes.push(moveChange(pair.a!, pair.b!));\n } else {\n changes.push(classified[classifiedIndex]!);\n classifiedIndex += 1;\n }\n }\n\n const provenance: Provenance = {\n modelId,\n promptVersion: options?.promptVersion ?? DEFAULT_PROMPT_VERSION,\n engineVersion: ENGINE_VERSION,\n };\n return { schemaVersion: SCHEMA_VERSION, provenance, changes, summary: summarize(changes) };\n}\n\n/** A cosmetic edit to a matched unit — determined deterministically, no model. */\nfunction cosmeticModification(a: Unit, b: Unit): Change {\n return { type: \"modification\", classification: \"cosmetic\", spanA: a.span, spanB: b.span, confidence: 1, needsReview: false };\n}\n\n/**\n * A relocation of identical content (ADR-0010) — deterministic and cosmetic: the\n * text did not change, only its position, so it is surfaced as a `move` rather\n * than a delete + insert. `a` is the old span, `b` the new one.\n */\nfunction moveChange(a: Unit, b: Unit): Change {\n return { type: \"move\", classification: \"cosmetic\", spanA: a.span, spanB: b.span, confidence: 1, needsReview: false };\n}\n\nfunction summarize(changes: readonly Change[]): DiffSummary {\n const byType = { insertion: 0, deletion: 0, modification: 0, move: 0 };\n let substantive = 0;\n let cosmetic = 0;\n let needsReview = 0;\n for (const change of changes) {\n byType[change.type] += 1;\n if (change.classification === \"substantive\") substantive += 1;\n else cosmetic += 1;\n if (change.needsReview) needsReview += 1;\n }\n return { substantive, cosmetic, byType, needsReview };\n}\n"],"mappings":"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","names":["retryAfterMs","i","j","normalize"]}
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts","../src/classifier.ts","../src/classifiers/claude.ts","../src/version.ts","../src/pipeline/segment.ts","../src/pipeline/align.ts","../src/pipeline/classify.ts","../src/cache.ts","../src/index.ts"],"sourcesContent":["/**\r\n * The semdiff public contract (ADR-0006).\r\n *\r\n * A `StructuredDiff` is the engine's primary output. Every human-readable\r\n * rendering is a pure function of it, and machine consumers — notably the\r\n * downstream `sust-reg-reporter` application — integrate against these types\r\n * and their JSON form. This module is pure types and constants: no logic, no\r\n * imports, nothing domain-specific (ADR-0001).\r\n */\r\n\r\n/**\r\n * Version of the StructuredDiff contract. Additive-by-default (ADR-0006): a\r\n * backwards-compatible addition keeps the version; a breaking shape change\r\n * bumps it and gets its own ADR. `StructuredDiff.schemaVersion` is typed as a\r\n * plain `string` (not this literal) so an additive bump is not itself a\r\n * breaking type change for pinned consumers.\r\n */\r\nexport const SCHEMA_VERSION = \"1.0.0\";\r\n\r\n/**\r\n * A `Span` locates a change within ONE input by half-open `[start, end)`\r\n * CHARACTER OFFSETS (ADR-0007).\r\n *\r\n * INVARIANT (load-bearing for consumer citation integrity): offsets index into\r\n * the EXACT, LITERAL, UN-NORMALIZED input string the caller passed. For\r\n * `sust-reg-reporter` that string is the immutable content-addressed snapshot\r\n * text (its ADR-0004 citation integrity, ADR-0011 snapshot store), so the\r\n * offsets resolve against a stored snapshot. Normalization applied internally\r\n * for alignment (whitespace, casing, punctuation, numbering) MUST NOT shift the\r\n * reported offsets. These `{ start, end }` map field-for-field onto the\r\n * consumer's citation span (`@sust-reg/core` `SourceCitation.span`).\r\n */\r\nexport interface Span {\r\n /** Inclusive start character offset into the literal input. */\r\n readonly start: number;\r\n /** Exclusive end character offset into the literal input. */\r\n readonly end: number;\r\n /**\r\n * Optional id of the segmentation unit this span falls in (ADR-0003).\r\n * Additive metadata only — consumers anchor on `start`/`end`, never this.\r\n */\r\n readonly unitId?: string;\r\n}\r\n\r\n/** The kind of edit a change represents. */\r\nexport type ChangeType = \"insertion\" | \"deletion\" | \"modification\" | \"move\";\r\n\r\n/** Whether a change alters meaning (`substantive`) or not (`cosmetic`). */\r\nexport type Classification = \"substantive\" | \"cosmetic\";\r\n\r\n/** One classified change between input A and input B. */\r\nexport interface Change {\r\n readonly type: ChangeType;\r\n readonly classification: Classification;\r\n /** Location in input A; `null` for a pure insertion (absent from A). */\r\n readonly spanA: Span | null;\r\n /** Location in input B; `null` for a pure deletion (absent from B). */\r\n readonly spanB: Span | null;\r\n /**\r\n * Short description of what changed. Present only for substantive\r\n * modifications; the key is OMITTED otherwise (never set to `undefined`,\r\n * per `exactOptionalPropertyTypes`).\r\n */\r\n readonly description?: string;\r\n /** Classifier confidence in `[0, 1]`. */\r\n readonly confidence: number;\r\n /** Set for low-confidence or failed/degraded classifications (ADR-0004). */\r\n readonly needsReview: boolean;\r\n}\r\n\r\n/** The reproducibility stamp for a run (ADR-0004): identifies the model run. */\r\nexport interface Provenance {\r\n readonly modelId: string;\r\n readonly promptVersion: string;\r\n readonly engineVersion: string;\r\n}\r\n\r\n/** Aggregate counts for quick triage. */\r\nexport interface DiffSummary {\r\n readonly substantive: number;\r\n readonly cosmetic: number;\r\n /** Count per change type; all four keys are present (zeros allowed). */\r\n readonly byType: Readonly<Record<ChangeType, number>>;\r\n readonly needsReview: number;\r\n}\r\n\r\n/**\r\n * The engine's primary output (ADR-0006): a stable, versioned, JSON-\r\n * serializable diff. All human-readable views derive from it.\r\n */\r\nexport interface StructuredDiff {\r\n /** The `SCHEMA_VERSION` in effect at emit time; typed `string` for additive bumps. */\r\n readonly schemaVersion: string;\r\n readonly provenance: Provenance;\r\n readonly changes: readonly Change[];\r\n readonly summary: DiffSummary;\r\n}\r\n","/**\n * The classification boundary (ADR-0004).\n *\n * semdiff uses the LLM strictly as a gated, structured classifier behind a\n * small `Classifier` interface — never a free-form diff narrator, never a\n * hardwired SDK. The default provider is the latest capable Claude model,\n * injected via config, so consumers (e.g. `sust-reg-reporter`) are not forced\n * to own provider wiring.\n */\nimport type { Classification, Span } from \"./schema.ts\";\n\n/**\n * The structural kind of a candidate change. A subset of `ChangeType`: `move`\n * is detected deterministically before classification (ADR-0010), so it never\n * reaches the model.\n */\nexport type CandidateType = \"insertion\" | \"deletion\" | \"modification\";\n\n/**\n * A single changed pair handed to the classifier for a verdict. One side may be\n * absent: for an insertion `a` is `\"\"` and `spanA` is `null`; for a deletion\n * `b` is `\"\"` and `spanB` is `null` (ADR-0011).\n */\nexport interface CandidatePair {\n /** The structural kind of change. */\n readonly type: CandidateType;\n /** The unit text from input A; `\"\"` for an insertion. */\n readonly a: string;\n /** The unit text from input B; `\"\"` for a deletion. */\n readonly b: string;\n /** Location of `a` within input A (literal character offsets, ADR-0007); `null` for an insertion. */\n readonly spanA: Span | null;\n /** Location of `b` within input B (literal character offsets, ADR-0007); `null` for a deletion. */\n readonly spanB: Span | null;\n}\n\n/** The schema-validated verdict for one candidate pair. */\nexport interface ClassifierVerdict {\n readonly classification: Classification;\n /** Present only for a substantive verdict; OMITTED otherwise. */\n readonly description?: string;\n /** Provider/engine confidence in `[0, 1]`. */\n readonly confidence: number;\n}\n\n/** The injectable provider boundary. An implementation wraps one LLM provider. */\nexport interface Classifier {\n classify(pair: CandidatePair): Promise<ClassifierVerdict>;\n}\n\n/**\n * Default model id (the latest capable Claude, ADR-0004). Stamped into run\n * provenance, and used by the default classifier when the caller does not pin\n * one. Callers that inject their own provider should pass `modelId` for an\n * accurate provenance stamp.\n */\nexport const DEFAULT_MODEL_ID = \"claude-opus-4-8\";\n\n/**\n * The never-drop / never-fabricate fallback verdict (ADR-0004). When a model\n * response fails schema validation, retries are exhausted, or the provider\n * errors, the `classify` stage records this conservative verdict — `substantive`\n * (so the change is surfaced for review, not hidden) with zero confidence — and\n * flags the resulting change for review, rather than dropping the pair or\n * guessing a cosmetic/substantive call.\n */\nexport function needsReviewVerdict(): ClassifierVerdict {\n return { classification: \"substantive\", confidence: 0 };\n}\n","/**\r\n * Default classifier — calls the Anthropic Messages API to judge whether a\r\n * change is substantive or cosmetic (ADR-0004, ADR-0009).\r\n *\r\n * It uses the global `fetch` (no SDK), so the engine keeps ZERO runtime\r\n * dependencies; a consumer that needs a different provider or transport injects\r\n * its own `Classifier` instead. Determinism is steered by a pinned model, a\r\n * pinned prompt, and low effort where the model accepts it — Opus 4.8 removed the\r\n * `temperature` parameter, so there is no `temperature: 0`. The verdict is returned through a constrained\r\n * JSON schema and then RE-VALIDATED by the classify stage, so this module can\r\n * parse leniently: any malformed response surfaces as a thrown error that the\r\n * classify stage retries, then degrades to needs-review.\r\n *\r\n * Transport resilience (ADR-0012): each call has a timeout and retries transient\r\n * failures — HTTP 429/5xx, network errors, and abort timeouts — with exponential\r\n * backoff, honouring a `Retry-After` header. Non-transient errors (400, auth)\r\n * fail fast. This is distinct from the classify stage's verdict-level retry: the\r\n * provider exhausts its backoff first, and only if it still throws does the\r\n * stage's safety net degrade the pair to needs-review.\r\n */\r\nimport { DEFAULT_MODEL_ID, type CandidatePair, type Classifier, type ClassifierVerdict } from \"../classifier.ts\";\r\n\r\nconst MESSAGES_URL = \"https://api.anthropic.com/v1/messages\";\r\nconst ANTHROPIC_VERSION = \"2023-06-01\";\r\nconst MAX_TOKENS = 1024;\r\n\r\n/** Per-request timeout before the call is aborted and treated as transient (ADR-0012). */\r\nconst DEFAULT_TIMEOUT_MS = 60_000;\r\n/** Retries after the initial attempt on a transient failure (ADR-0012). */\r\nconst DEFAULT_MAX_RETRIES = 2;\r\n/** Backoff for retry n is BASE * 2**n plus jitter, capped at MAX (ADR-0012). */\r\nconst BASE_RETRY_DELAY_MS = 500;\r\nconst MAX_RETRY_DELAY_MS = 8_000;\r\n\r\n/**\r\n * Static classification instructions — the stable, cacheable prompt prefix\r\n * (ADR-0009). Domain-neutral (ADR-0001). Recall-biased per ADR-0005: when\r\n * uncertain, prefer \"substantive\" so a real change is surfaced, not hidden.\r\n *\r\n * Exported so a release-gating test can pin its hash to `DEFAULT_PROMPT_VERSION`\r\n * (ADR-0005): editing this text without bumping the version would let a persisted\r\n * verdict cache serve stale results. Not part of the package's public `exports`.\r\n */\r\nexport const SYSTEM_PROMPT = [\r\n \"You are a careful classifier inside a meaning-aware diff engine. You are given\",\r\n \"two versions of one short span of prose: version A (before) and version B\",\r\n \"(after). Decide whether the change from A to B is:\",\r\n \"\",\r\n '- \"substantive\": it alters the meaning — a changed value, number, date,',\r\n \" condition, scope, or any wording a careful reader would act on differently.\",\r\n '- \"cosmetic\": it preserves the meaning — formatting, punctuation, casing,',\r\n \" whitespace, renumbering, or a meaning-preserving rewording.\",\r\n \"\",\r\n \"One side may be empty: an empty A means the B text was newly inserted, and an\",\r\n \"empty B means the A text was removed. Judge whether that insertion or removal\",\r\n \"is substantive (it adds or removes meaning, an obligation, or a condition) or\",\r\n \"cosmetic (boilerplate, formatting, or duplicate content).\",\r\n \"\",\r\n \"Rules:\",\r\n \"- Judge only these two snippets; do not assume external context.\",\r\n '- When genuinely uncertain whether the meaning changed, choose \"substantive\":',\r\n \" it is safer to surface a real change than to hide one.\",\r\n '- For a substantive change, give a one-sentence factual \"description\" of what',\r\n \" changed — no advice and no judgement of how significant it is.\",\r\n '- Set \"confidence\" in [0, 1] for how sure you are of the classification.',\r\n].join(\"\\n\");\r\n\r\n/** Constrained output shape (structured outputs). Ranges are validated downstream. */\r\nconst VERDICT_SCHEMA = {\r\n type: \"object\",\r\n properties: {\r\n classification: { type: \"string\", enum: [\"substantive\", \"cosmetic\"] },\r\n description: { type: \"string\" },\r\n confidence: { type: \"number\" },\r\n },\r\n required: [\"classification\", \"confidence\"],\r\n additionalProperties: false,\r\n} as const;\r\n\r\n/** Configuration for the default Anthropic-backed classifier. */\r\nexport interface DefaultClassifierConfig {\r\n /** Model id; defaults to the latest capable Claude (ADR-0004). */\r\n readonly modelId?: string;\r\n /** API key; defaults to `process.env.ANTHROPIC_API_KEY`. */\r\n readonly apiKey?: string;\r\n /** Per-request timeout in ms before the call is aborted and retried (ADR-0012). Default 60000. */\r\n readonly timeoutMs?: number;\r\n /** Retries on a transient failure — 429, 5xx, network, or timeout (ADR-0012). Default 2. */\r\n readonly maxRetries?: number;\r\n}\r\n\r\n/**\r\n * Construct the default classifier. Throws immediately if no API key is\r\n * available, so a diff that needs the model fails at a clear boundary rather\r\n * than per-call.\r\n */\r\nexport function createDefaultClassifier(config: DefaultClassifierConfig): Classifier {\r\n const modelId = config.modelId ?? DEFAULT_MODEL_ID;\r\n const apiKey = config.apiKey ?? process.env.ANTHROPIC_API_KEY;\r\n const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;\r\n const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;\r\n if (apiKey === undefined || apiKey === \"\") {\r\n throw new Error(\"createDefaultClassifier: no API key (set ANTHROPIC_API_KEY or pass config.apiKey)\");\r\n }\r\n\r\n return {\r\n classify: (pair: CandidatePair): Promise<ClassifierVerdict> => {\r\n const init: RequestInit = {\r\n method: \"POST\",\r\n headers: {\r\n \"content-type\": \"application/json\",\r\n \"x-api-key\": apiKey,\r\n \"anthropic-version\": ANTHROPIC_VERSION,\r\n },\r\n body: JSON.stringify(buildRequest(modelId, pair)),\r\n };\r\n return classifyWithRetry(() => classifyOnce(init, timeoutMs), maxRetries);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * A transient failure worth retrying: a 429/5xx response, a network error, or a\r\n * timeout. `retryAfterMs` is the server's hint (0 if none). Non-transient errors\r\n * are thrown as plain `Error`s and propagate without a retry.\r\n */\r\nclass TransientError extends Error {\r\n // A field declaration + assignment, not a constructor parameter property:\r\n // parameter properties are runtime syntax that Node's strip-only type removal\r\n // cannot handle, which would break the zero-build `node src/...` path (ADR-0002).\r\n readonly retryAfterMs: number;\r\n constructor(message: string, retryAfterMs: number) {\r\n super(message);\r\n this.retryAfterMs = retryAfterMs;\r\n }\r\n}\r\n\r\n/** Run `attempt`, retrying transient failures with backoff up to `maxRetries` (ADR-0012). */\r\nasync function classifyWithRetry(\r\n attempt: () => Promise<ClassifierVerdict>,\r\n maxRetries: number,\r\n): Promise<ClassifierVerdict> {\r\n for (let retry = 0; ; retry += 1) {\r\n try {\r\n return await attempt();\r\n } catch (error) {\r\n if (!(error instanceof TransientError) || retry >= maxRetries) throw error;\r\n await sleep(backoffMs(retry, error.retryAfterMs));\r\n }\r\n }\r\n}\r\n\r\n/** One request attempt: transient failures throw `TransientError`, others throw plainly. */\r\nasync function classifyOnce(init: RequestInit, timeoutMs: number): Promise<ClassifierVerdict> {\r\n let response: Response;\r\n try {\r\n response = await fetchWithTimeout(MESSAGES_URL, init, timeoutMs);\r\n } catch (cause) {\r\n // Aborted (timeout) or a network failure — both transient.\r\n throw new TransientError(`Anthropic API request failed: ${(cause as Error).message}`, 0);\r\n }\r\n if (!response.ok) {\r\n const message = `Anthropic API error ${response.status}: ${await response.text()}`;\r\n if (response.status === 429 || response.status >= 500) {\r\n throw new TransientError(message, retryAfterMs(response.headers));\r\n }\r\n throw new Error(message);\r\n }\r\n return parseVerdict(await response.json());\r\n}\r\n\r\n/** `fetch` with an abort-based timeout; the timer is always cleared. */\r\nasync function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number): Promise<Response> {\r\n const controller = new AbortController();\r\n const timer = setTimeout(() => controller.abort(), timeoutMs);\r\n try {\r\n return await fetch(url, { ...init, signal: controller.signal });\r\n } finally {\r\n clearTimeout(timer);\r\n }\r\n}\r\n\r\n/** Backoff for retry `n`: the server's `Retry-After` if given, else exponential with jitter. */\r\nfunction backoffMs(retry: number, retryAfterMs: number): number {\r\n if (retryAfterMs > 0) return Math.min(retryAfterMs, MAX_RETRY_DELAY_MS);\r\n const exponential = BASE_RETRY_DELAY_MS * 2 ** retry;\r\n return Math.min(exponential + exponential * 0.25 * Math.random(), MAX_RETRY_DELAY_MS);\r\n}\r\n\r\n/** Parse `Retry-After` (seconds) into ms; 0 when absent or unparseable. */\r\nfunction retryAfterMs(headers: Headers): number {\r\n const seconds = Number(headers.get(\"retry-after\"));\r\n return seconds > 0 ? seconds * 1000 : 0;\r\n}\r\n\r\nfunction sleep(ms: number): Promise<void> {\r\n return new Promise((resolve) => {\r\n setTimeout(resolve, ms);\r\n });\r\n}\r\n\r\n/** Build the Messages API request body for one candidate pair. */\r\nfunction buildRequest(modelId: string, pair: CandidatePair): unknown {\r\n const outputConfig: Record<string, unknown> = {\r\n format: { type: \"json_schema\", schema: VERDICT_SCHEMA },\r\n };\r\n // `effort` steers determinism and cost, but only Opus and Sonnet 4.6 accept\r\n // it; Haiku and Sonnet 4.5 reject it with a 400. Include it only where\r\n // supported so overriding `modelId` to a cheaper model (an eval sweep, say)\r\n // does not fail. Omitting it is harmless; sending it where unsupported is not.\r\n if (modelSupportsEffort(modelId)) {\r\n outputConfig.effort = \"low\";\r\n }\r\n return {\r\n model: modelId,\r\n max_tokens: MAX_TOKENS,\r\n output_config: outputConfig,\r\n system: [{ type: \"text\", text: SYSTEM_PROMPT, cache_control: { type: \"ephemeral\" } }],\r\n messages: [{ role: \"user\", content: `Change type: ${pair.type}.\\nA:\\n${pair.a}\\n\\nB:\\n${pair.b}` }],\r\n };\r\n}\r\n\r\n/**\r\n * Whether `output_config.effort` is accepted by `modelId`. Opus (4.5+) and\r\n * Sonnet 4.6 support it; Haiku and Sonnet 4.5 return a 400. Biased to omit when\r\n * unsure — a missing effort still succeeds, an unsupported effort does not — so\r\n * a future model silently runs without effort rather than erroring.\r\n */\r\nfunction modelSupportsEffort(modelId: string): boolean {\r\n return modelId.startsWith(\"claude-opus-\") || modelId.startsWith(\"claude-sonnet-4-6\");\r\n}\r\n\r\n/** Extract the structured verdict from the Messages API response (lenient). */\r\nfunction parseVerdict(data: unknown): ClassifierVerdict {\r\n const message = data as { content?: ReadonlyArray<{ type?: string; text?: string }> };\r\n const text = message.content?.find((block) => block.type === \"text\")?.text;\r\n if (text === undefined) {\r\n throw new Error(\"Anthropic API returned no text content\");\r\n }\r\n return JSON.parse(text) as ClassifierVerdict;\r\n}\r\n","/**\r\n * Version constants feeding the reproducibility stamp (ADR-0004). A run is\r\n * stamped with the model id, the prompt version, and this engine version so\r\n * results are reproducible and cache keys are stable.\r\n */\r\n\r\n/**\r\n * semdiff engine version, stamped into `Provenance.engineVersion`. Kept in sync\r\n * with `package.json` `version`; `test/version.contract.test.ts` fails the build\r\n * if the two drift, so a published artifact never stamps a stale version.\r\n */\r\nexport const ENGINE_VERSION = \"0.1.1\";\r\n\r\n/** Default prompt-template version, stamped into `Provenance.promptVersion`. */\r\nexport const DEFAULT_PROMPT_VERSION = \"0\";\r\n","/**\r\n * Stage 1 — segment (ADR-0003). Local and deterministic; no model.\r\n *\r\n * Split an input into comparable units. At `sentence` granularity each unit is\r\n * a sentence; at `clause` granularity sentences are further divided at strong\r\n * intra-sentence separators. Each `Unit` carries the half-open `[start, end)`\r\n * CHARACTER OFFSETS of its text within the LITERAL input, so spans reported\r\n * downstream index the caller's exact input (the offset invariant, ADR-0007):\r\n * `input.slice(unit.span.start, unit.span.end) === unit.text` always holds.\r\n * Whitespace at unit boundaries is excluded from the span (and the text); no\r\n * other normalization is applied, so offsets never drift.\r\n */\r\nimport type { Span } from \"../schema.ts\";\r\n\r\n/** The granularity at which an input is segmented. */\r\nexport type SegmentGranularity = \"sentence\" | \"clause\";\r\n\r\n/** One comparable unit of an input, anchored to the literal input by offsets. */\r\nexport interface Unit {\r\n /** The unit's text, verbatim from the input (boundary whitespace trimmed). */\r\n readonly text: string;\r\n /** Half-open offsets of `text` within the literal input. */\r\n readonly span: Span;\r\n}\r\n\r\n/**\r\n * Sentence breaking is language-aware, and determinism is a core guarantee\r\n * (ADR-0005), so we pin the locale rather than use the ambient runtime locale.\r\n * Making the locale configurable is a later additive change.\r\n */\r\nconst SENTENCE_SEGMENTER = new Intl.Segmenter(\"en\", { granularity: \"sentence\" });\r\n\r\n/**\r\n * Strong intra-sentence clause separators. Comma-level splitting is deliberately\r\n * excluded — too unreliable to be deterministically useful — and enumerated-\r\n * clause structural cues are a future additive enhancement.\r\n */\r\nconst CLAUSE_DELIMITERS = \";:\";\r\n\r\n/**\r\n * Segment `text` into ordered `Unit`s at the given granularity. Deterministic;\r\n * no model. Empty and whitespace-only inputs yield no units.\r\n */\r\nexport function segment(text: string, granularity: SegmentGranularity): readonly Unit[] {\r\n const units: Unit[] = [];\r\n const delimiters = granularity === \"clause\" ? CLAUSE_DELIMITERS : \"\";\r\n for (const { segment: sentence, index } of SENTENCE_SEGMENTER.segment(text)) {\r\n emitUnits(sentence, index, delimiters, units);\r\n }\r\n return units;\r\n}\r\n\r\n/**\r\n * Emit trimmed units from a sentence `chunk` located at absolute offset `base`.\r\n * With no delimiters the chunk is a single unit; otherwise it is split at each\r\n * delimiter character, offsets staying absolute into the literal input.\r\n */\r\nfunction emitUnits(chunk: string, base: number, delimiters: string, out: Unit[]): void {\r\n if (delimiters.length === 0) {\r\n pushTrimmed(chunk, base, out);\r\n return;\r\n }\r\n let cursor = 0;\r\n for (let i = 0; i < chunk.length; i++) {\r\n if (delimiters.includes(chunk[i]!)) {\r\n pushTrimmed(chunk.slice(cursor, i), base + cursor, out);\r\n cursor = i + 1;\r\n }\r\n }\r\n pushTrimmed(chunk.slice(cursor), base + cursor, out);\r\n}\r\n\r\n/**\r\n * Trim boundary whitespace from `part` and, if non-empty, push a `Unit` whose\r\n * span points at the trimmed content within the literal input (`base` is the\r\n * absolute offset of `part`).\r\n */\r\nfunction pushTrimmed(part: string, base: number, out: Unit[]): void {\r\n const trimmed = part.trim();\r\n if (trimmed.length === 0) return;\r\n const start = base + (part.length - part.trimStart().length);\r\n out.push({ text: trimmed, span: { start, end: start + trimmed.length } });\r\n}\r\n","/**\r\n * Stage 2 — align (ADR-0003). Local and deterministic; no LLM.\r\n *\r\n * Match units across A and B and tag each pairing so the stage 2 -> 3 gate can\r\n * keep unchanged and cosmetic content away from the model:\r\n *\r\n * - `unchanged` — paired and textually identical.\r\n * - `trivial-change` — paired after normalization (whitespace, casing,\r\n * punctuation, and leading enumeration collapsed) but the\r\n * literal text differs. A cosmetic edit.\r\n * - `move` — a relocation of identical content (ADR-0010): a deletion\r\n * whose normalized content matches an insertion elsewhere,\r\n * re-paired into one change. Both old (`a`) and new (`b`)\r\n * positions are present; the text is unchanged.\r\n * - `candidate` — a genuine change needing downstream judgment: a paired\r\n * modification (both sides present), or a one-sided\r\n * insertion (`a === null`) or deletion (`b === null`).\r\n *\r\n * Pairing runs a longest-common-subsequence match over the normalized keys, then\r\n * pairs the survivors in each gap positionally when they share a token. A final\r\n * pass re-pairs content-identical deletion/insertion survivors into `move`s.\r\n *\r\n * Normalization is used ONLY to decide matches; it never touches the `Unit`\r\n * offsets, so the literal-input invariant (ADR-0007) is preserved untouched.\r\n */\r\nimport type { Unit } from \"./segment.ts\";\r\n\r\n/** How an aligned pairing relates its A and B units. */\r\nexport type AlignmentTag = \"unchanged\" | \"trivial-change\" | \"move\" | \"candidate\";\r\n\r\n/** A pairing of units across inputs; either side may be `null`. */\r\nexport interface AlignedPair {\r\n readonly tag: AlignmentTag;\r\n /** Unit from A, or `null` for an insertion. */\r\n readonly a: Unit | null;\r\n /** Unit from B, or `null` for a deletion. */\r\n readonly b: Unit | null;\r\n}\r\n\r\n/** Leading list/enumeration marker, e.g. \"1.\", \"1)\", \"(a)\", \"iv.\", or a bullet. */\r\nconst LEADING_ENUMERATOR = /^\\s*(?:[([]?\\s*(?:\\d{1,3}|[a-z]{1,2}|[ivxlcdm]{1,5})\\s*[)\\].]|[-*•·])\\s+/iu;\r\n\r\n/**\r\n * Unicode punctuation — quotes, dashes, periods, commas, parentheses, etc.\r\n * Symbols are deliberately KEPT: collapsing e.g. \"<\" and \">\" (or \"=\" / \"+\")\r\n * would mask a substantive change as cosmetic, and missing substance is the\r\n * costly error (ADR-0005).\r\n */\r\nconst PUNCTUATION = /\\p{P}/gu;\r\n\r\n/**\r\n * Align the segmented units of A and B into tagged pairings, in order.\r\n * Deterministic; no model.\r\n */\r\nexport function align(unitsA: readonly Unit[], unitsB: readonly Unit[]): readonly AlignedPair[] {\r\n const keysA = unitsA.map(normalize);\r\n const keysB = unitsB.map(normalize);\r\n const matches = lcsMatches(keysA, keysB);\r\n\r\n const out: AlignedPair[] = [];\r\n let i = 0;\r\n let j = 0;\r\n for (const [mi, mj] of matches) {\r\n emitGap(unitsA.slice(i, mi), unitsB.slice(j, mj), out);\r\n const a = unitsA[mi]!;\r\n const b = unitsB[mj]!;\r\n out.push({ tag: a.text === b.text ? \"unchanged\" : \"trivial-change\", a, b });\r\n i = mi + 1;\r\n j = mj + 1;\r\n }\r\n emitGap(unitsA.slice(i), unitsB.slice(j), out);\r\n return detectMoves(out);\r\n}\r\n\r\n/**\r\n * Pair the survivors in a gap: positionally, as a `candidate` modification when\r\n * the two units share a token, otherwise as a separate deletion and insertion.\r\n * Any leftover units are one-sided deletions (A) or insertions (B).\r\n */\r\nfunction emitGap(gapA: readonly Unit[], gapB: readonly Unit[], out: AlignedPair[]): void {\r\n const paired = Math.min(gapA.length, gapB.length);\r\n let k = 0;\r\n for (; k < paired; k++) {\r\n const a = gapA[k]!;\r\n const b = gapB[k]!;\r\n if (sharesToken(a, b)) {\r\n out.push({ tag: \"candidate\", a, b });\r\n } else {\r\n out.push({ tag: \"candidate\", a, b: null });\r\n out.push({ tag: \"candidate\", a: null, b });\r\n }\r\n }\r\n for (; k < gapA.length; k++) out.push({ tag: \"candidate\", a: gapA[k]!, b: null });\r\n for (; k < gapB.length; k++) out.push({ tag: \"candidate\", a: null, b: gapB[k]! });\r\n}\r\n\r\n/**\r\n * Re-pair content-identical deletion/insertion survivors into `move`s (ADR-0010).\r\n * A deletion is matched to an insertion with the same normalized key; the move\r\n * keeps the deletion's old position (`a`) and the insertion's new position (`b`).\r\n * Unmatched insertions/deletions are left as-is.\r\n *\r\n * Matching is content-only and 1:1 — it weighs neither distance nor document\r\n * structure (ADR-0010). Two consequences follow from keying on normalized text:\r\n * when several insertions share a key the LAST one wins (the map overwrites), and\r\n * if the same text genuinely appears as an unrelated deletion AND insertion they\r\n * collapse into one `move`. Acceptable at sentence/clause granularity, where\r\n * identical-content survivors are overwhelmingly true relocations.\r\n */\r\nfunction detectMoves(pairs: readonly AlignedPair[]): readonly AlignedPair[] {\r\n const insertionByKey = new Map<string, number>();\r\n pairs.forEach((pair, index) => {\r\n if (pair.tag === \"candidate\" && pair.a === null) {\r\n insertionByKey.set(normalize(pair.b!), index);\r\n }\r\n });\r\n\r\n const moveTo = new Map<number, number>();\r\n pairs.forEach((pair, index) => {\r\n if (pair.tag === \"candidate\" && pair.b === null) {\r\n const key = normalize(pair.a!);\r\n const insertionIndex = insertionByKey.get(key);\r\n if (insertionIndex !== undefined) {\r\n insertionByKey.delete(key);\r\n moveTo.set(index, insertionIndex);\r\n }\r\n }\r\n });\r\n\r\n if (moveTo.size === 0) return pairs;\r\n\r\n const movedInsertions = new Set(moveTo.values());\r\n return pairs.flatMap((pair, index) => {\r\n if (movedInsertions.has(index)) return [];\r\n const insertionIndex = moveTo.get(index);\r\n return insertionIndex === undefined ? [pair] : [{ tag: \"move\" as const, a: pair.a, b: pairs[insertionIndex]!.b }];\r\n });\r\n}\r\n\r\n/** Normalized match key: lower-cased, enumerator-stripped, punctuation-free. */\r\nfunction normalize(unit: Unit): string {\r\n return unit.text\r\n .toLowerCase()\r\n .replace(LEADING_ENUMERATOR, \"\")\r\n .replace(PUNCTUATION, \" \")\r\n .replace(/\\s+/g, \" \")\r\n .trim();\r\n}\r\n\r\n/** Whether two units share at least one normalized token (a weak similarity gate). */\r\nfunction sharesToken(a: Unit, b: Unit): boolean {\r\n const tokensB = new Set(tokenize(b));\r\n return tokenize(a).some((token) => tokensB.has(token));\r\n}\r\n\r\nfunction tokenize(unit: Unit): string[] {\r\n const normalized = normalize(unit);\r\n return normalized.length === 0 ? [] : normalized.split(\" \");\r\n}\r\n\r\n/**\r\n * Longest-common-subsequence match over two key sequences, returned as ordered\r\n * `[indexInA, indexInB]` pairs of equal keys.\r\n *\r\n * Implemented with Hirschberg's divide-and-conquer LCS: O(n*m) time but only\r\n * O(min-of-the-two-lengths) space. The earlier version materialized the full\r\n * `(n+1) x (m+1)` length matrix, which is O(n*m) space and exhausts memory on\r\n * large inputs — two ~800 KB documents segment into tens of thousands of units,\r\n * so the matrix reaches hundreds of millions of cells (multiple GB) regardless\r\n * of how small the actual change is. This keeps two rolling rows instead, so a\r\n * large diff stays in tens of MB. The LCS it returns is identical for the\r\n * common unambiguous case (the golden align fixtures are unchanged).\r\n */\r\nfunction lcsMatches(a: readonly string[], b: readonly string[]): Array<readonly [number, number]> {\r\n const matches: Array<readonly [number, number]> = [];\r\n hirschberg(a, 0, a.length, b, 0, b.length, matches);\r\n return matches;\r\n}\r\n\r\n/** Append, in order, the matched absolute-index pairs for `a[a0:a1]`/`b[b0:b1]`. */\r\nfunction hirschberg(\r\n a: readonly string[],\r\n a0: number,\r\n a1: number,\r\n b: readonly string[],\r\n b0: number,\r\n b1: number,\r\n out: Array<readonly [number, number]>,\r\n): void {\r\n const n = a1 - a0;\r\n const m = b1 - b0;\r\n if (n === 0 || m === 0) return;\r\n if (n === 1) {\r\n const key = a[a0]!;\r\n for (let j = b0; j < b1; j++) {\r\n if (b[j] === key) {\r\n out.push([a0, j]);\r\n return;\r\n }\r\n }\r\n return;\r\n }\r\n\r\n const aMid = a0 + (n >> 1);\r\n // scoreL[k] = LCS(a[a0:aMid], b[b0:b0+k]); scoreR[k] = LCS(a[aMid:a1], b[b0+k:b1]).\r\n const scoreL = lcsRowForward(a, a0, aMid, b, b0, b1);\r\n const scoreR = lcsRowBackward(a, aMid, a1, b, b0, b1);\r\n // Split B where the two halves' LCS lengths sum highest (first max keeps the\r\n // result deterministic and left-biased, matching the prior forward walk).\r\n let best = -1;\r\n let split = 0;\r\n for (let k = 0; k <= m; k++) {\r\n const total = scoreL[k]! + scoreR[k]!;\r\n if (total > best) {\r\n best = total;\r\n split = k;\r\n }\r\n }\r\n\r\n hirschberg(a, a0, aMid, b, b0, b0 + split, out);\r\n hirschberg(a, aMid, a1, b, b0 + split, b1, out);\r\n}\r\n\r\n/** Forward LCS lengths: returns row where row[k] = LCS(a[a0:a1], b[b0:b0+k]). */\r\nfunction lcsRowForward(\r\n a: readonly string[],\r\n a0: number,\r\n a1: number,\r\n b: readonly string[],\r\n b0: number,\r\n b1: number,\r\n): number[] {\r\n const width = b1 - b0;\r\n let prev = new Array<number>(width + 1).fill(0);\r\n let curr = new Array<number>(width + 1).fill(0);\r\n for (let i = a0; i < a1; i++) {\r\n for (let k = 1; k <= width; k++) {\r\n curr[k] =\r\n a[i] === b[b0 + k - 1] ? prev[k - 1]! + 1 : Math.max(prev[k]!, curr[k - 1]!);\r\n }\r\n [prev, curr] = [curr, prev];\r\n }\r\n return prev;\r\n}\r\n\r\n/** Backward LCS lengths: returns row where row[k] = LCS(a[a0:a1], b[b0+k:b1]). */\r\nfunction lcsRowBackward(\r\n a: readonly string[],\r\n a0: number,\r\n a1: number,\r\n b: readonly string[],\r\n b0: number,\r\n b1: number,\r\n): number[] {\r\n const width = b1 - b0;\r\n let prev = new Array<number>(width + 1).fill(0);\r\n let curr = new Array<number>(width + 1).fill(0);\r\n for (let i = a1 - 1; i >= a0; i--) {\r\n for (let k = width - 1; k >= 0; k--) {\r\n curr[k] = a[i] === b[b0 + k] ? prev[k + 1]! + 1 : Math.max(prev[k]!, curr[k + 1]!);\r\n }\r\n [prev, curr] = [curr, prev];\r\n }\r\n return prev;\r\n}\r\n","/**\n * Stage 3 — classify (ADR-0003, ADR-0004). The gated, structured LLM step.\n *\n * The caller passes only genuine `candidate` pairs — align has already kept\n * unchanged, trivial-change, and move content away from the model. A candidate\n * may be a modification (both sides present) or a one-sided insertion/deletion\n * (ADR-0011); for each, this stage asks the injected `Classifier` for a verdict,\n * validates it against the schema, retries once on a malformed response or a\n * provider error, and finally degrades to a flagged `needs-review` change —\n * never dropping a pair and never fabricating a verdict (ADR-0004). The provider\n * stays injected; this module imports no SDK, so the engine has no LLM-infra\n * dependency.\n *\n * Caching (ADR-0004's content-addressed cache) belongs with the provider\n * implementation, not this stage, and is out of scope here.\n */\nimport type { Change } from \"../schema.ts\";\nimport { needsReviewVerdict, type CandidatePair, type Classifier, type ClassifierVerdict } from \"../classifier.ts\";\n\n/** Attempts per pair: one initial call plus one retry (ADR-0004). */\nconst MAX_ATTEMPTS = 2;\n\n/** Verdicts below this confidence are flagged for review (ADR-0006). */\nconst MIN_TRUSTED_CONFIDENCE = 0.5;\n\n/**\n * Classify changed candidate pairs into `Change`s using the injected classifier.\n * Order is preserved; each change carries the candidate's type and spans untouched.\n */\nexport async function classify(\n candidates: readonly CandidatePair[],\n classifier: Classifier,\n): Promise<readonly Change[]> {\n const changes: Change[] = [];\n for (const pair of candidates) {\n changes.push(await classifyPair(pair, classifier));\n }\n return changes;\n}\n\nasync function classifyPair(pair: CandidatePair, classifier: Classifier): Promise<Change> {\n for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {\n let verdict: unknown;\n try {\n verdict = await classifier.classify(pair);\n } catch {\n continue; // provider error / timeout / rate limit — retry, then needs-review\n }\n if (isValidVerdict(verdict)) {\n return toChange(pair, verdict);\n }\n }\n return needsReviewChange(pair);\n}\n\n/** Runtime guard: the model response is untrusted until validated (ADR-0004). */\nfunction isValidVerdict(value: unknown): value is ClassifierVerdict {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.classification !== \"substantive\" && v.classification !== \"cosmetic\") return false;\n if (typeof v.confidence !== \"number\" || !Number.isFinite(v.confidence)) return false;\n if (v.confidence < 0 || v.confidence > 1) return false;\n if (v.description !== undefined && typeof v.description !== \"string\") return false;\n return true;\n}\n\nfunction toChange(pair: CandidatePair, verdict: ClassifierVerdict): Change {\n const base = {\n type: pair.type,\n classification: verdict.classification,\n spanA: pair.spanA,\n spanB: pair.spanB,\n confidence: verdict.confidence,\n needsReview: verdict.confidence < MIN_TRUSTED_CONFIDENCE,\n };\n return verdict.description === undefined ? base : { ...base, description: verdict.description };\n}\n\nfunction needsReviewChange(pair: CandidatePair): Change {\n const { classification, confidence } = needsReviewVerdict();\n return {\n type: pair.type,\n classification,\n spanA: pair.spanA,\n spanB: pair.spanB,\n confidence,\n needsReview: true,\n };\n}\n","/**\r\n * Content-addressed verdict cache (ADR-0004). Identical classification inputs\r\n * return the cached verdict without a second model call — the primary\r\n * determinism and cost guarantee.\r\n *\r\n * The key is a hash of the normalized pair text plus the prompt version and\r\n * model id, so the same change under the same model/prompt is classified once.\r\n * Spans are deliberately NOT part of the key: the verdict (substantive/cosmetic\r\n * + description + confidence) describes the content change, not where it sits,\r\n * so a pair classified once applies wherever that text appears.\r\n *\r\n * The default cache is in-memory and process-local; inject a `VerdictCache` to\r\n * back it with a persistent store — the engine keeps no backend of its own\r\n * (ADR-0001). Reuse the wrapped classifier across `diff` calls to share it.\r\n */\r\nimport { createHash } from \"node:crypto\";\r\nimport type { CandidatePair, Classifier, ClassifierVerdict } from \"./classifier.ts\";\r\n\r\n/**\r\n * Field separator for the cache key. Normalization collapses whitespace and\r\n * never emits a NUL byte, so distinct field boundaries can never collide.\r\n */\r\nconst FIELD_SEPARATOR = String.fromCharCode(0);\r\n\r\n/** A store for classification verdicts, keyed by content hash. May be async. */\r\nexport interface VerdictCache {\r\n get(key: string): Promise<ClassifierVerdict | undefined>;\r\n set(key: string, verdict: ClassifierVerdict): Promise<void>;\r\n}\r\n\r\n/** An in-memory `VerdictCache` (process-local; the default). */\r\nexport function createMemoryCache(): VerdictCache {\r\n const store = new Map<string, ClassifierVerdict>();\r\n return {\r\n get: (key) => Promise.resolve(store.get(key)),\r\n set: (key, verdict) => {\r\n store.set(key, verdict);\r\n return Promise.resolve();\r\n },\r\n };\r\n}\r\n\r\n/** Options for `withCache`. `modelId` and `promptVersion` are part of the key. */\r\nexport interface CacheOptions {\r\n readonly modelId: string;\r\n readonly promptVersion: string;\r\n readonly cache?: VerdictCache;\r\n}\r\n\r\n/**\r\n * Wrap a `Classifier` so identical inputs are classified once. Reuse the\r\n * returned classifier across `diff` calls to share the cache.\r\n */\r\nexport function withCache(classifier: Classifier, options: CacheOptions): Classifier {\r\n const cache = options.cache ?? createMemoryCache();\r\n return {\r\n classify: async (pair: CandidatePair): Promise<ClassifierVerdict> => {\r\n const key = cacheKey(pair, options.modelId, options.promptVersion);\r\n const cached = await cache.get(key);\r\n if (cached !== undefined) return cached;\r\n const verdict = await classifier.classify(pair);\r\n await cache.set(key, verdict);\r\n return verdict;\r\n },\r\n };\r\n}\r\n\r\n/** Content-addressed key: a hash of (normalized a, normalized b, prompt, model). */\r\nexport function cacheKey(pair: CandidatePair, modelId: string, promptVersion: string): string {\r\n const parts = [normalize(pair.a), normalize(pair.b), promptVersion, modelId];\r\n return createHash(\"sha256\").update(parts.join(FIELD_SEPARATOR)).digest(\"hex\");\r\n}\r\n\r\nfunction normalize(text: string): string {\r\n return text.replace(/\\s+/g, \" \").trim();\r\n}\r\n","/**\n * semdiff — meaning-aware diff engine (library entry point).\n *\n * The library is the source of truth (ADR-0002); the CLI is a thin wrapper.\n * `diff` runs the segment -> align -> classify pipeline (ADR-0003) and assembles\n * the versioned `StructuredDiff` (ADR-0006), the engine's public contract.\n */\nexport * from \"./schema.ts\";\nexport * from \"./classifier.ts\";\n\nimport { SCHEMA_VERSION, type Change, type DiffSummary, type Provenance, type StructuredDiff } from \"./schema.ts\";\nimport { DEFAULT_MODEL_ID, type CandidatePair, type Classifier } from \"./classifier.ts\";\nimport { createDefaultClassifier } from \"./classifiers/claude.ts\";\nimport { ENGINE_VERSION, DEFAULT_PROMPT_VERSION } from \"./version.ts\";\nimport { segment, type SegmentGranularity, type Unit } from \"./pipeline/segment.ts\";\nimport { align } from \"./pipeline/align.ts\";\nimport { classify } from \"./pipeline/classify.ts\";\n\nexport { ENGINE_VERSION, DEFAULT_PROMPT_VERSION };\nexport { createDefaultClassifier, type DefaultClassifierConfig } from \"./classifiers/claude.ts\";\nexport { withCache, createMemoryCache, cacheKey, type VerdictCache, type CacheOptions } from \"./cache.ts\";\n\n/** Options for a `diff` run. Omit a field to take its default. */\nexport interface DiffOptions {\n /**\n * Provider used to classify changed pairs. Defaults to the latest capable\n * Claude model via `createDefaultClassifier` (ADR-0004) — which is only\n * constructed when there is a change to classify (a modification, insertion,\n * or deletion). Identical, cosmetic, and moved content needs no provider.\n */\n readonly classifier?: Classifier;\n /** Model id stamped into provenance; also passed to the default classifier. */\n readonly modelId?: string;\n /** Prompt-template version stamped into provenance. */\n readonly promptVersion?: string;\n /** Granularity at which inputs are segmented (ADR-0003). */\n readonly segmentGranularity?: SegmentGranularity;\n}\n\n/**\n * Produce a meaning-aware structured diff of two inputs. Runs\n * segment -> align -> classify, stamps run provenance, and assembles the\n * `StructuredDiff`. A classifier is constructed and called only when there is at\n * least one change to classify (a modification, insertion, or deletion), so\n * diffs of identical, cosmetic, or merely relocated content need no provider.\n */\nexport async function diff(a: string, b: string, options?: DiffOptions): Promise<StructuredDiff> {\n const granularity = options?.segmentGranularity ?? \"sentence\";\n const pairs = align(segment(a, granularity), segment(b, granularity));\n\n const candidates: CandidatePair[] = [];\n for (const pair of pairs) {\n if (pair.tag !== \"candidate\") continue;\n if (pair.a !== null && pair.b !== null) {\n candidates.push({ type: \"modification\", a: pair.a.text, b: pair.b.text, spanA: pair.a.span, spanB: pair.b.span });\n } else if (pair.b !== null) {\n candidates.push({ type: \"insertion\", a: \"\", b: pair.b.text, spanA: null, spanB: pair.b.span });\n } else {\n candidates.push({ type: \"deletion\", a: pair.a!.text, b: \"\", spanA: pair.a!.span, spanB: null });\n }\n }\n\n const modelId = options?.modelId ?? DEFAULT_MODEL_ID;\n const classified =\n candidates.length === 0\n ? []\n : await classify(candidates, options?.classifier ?? createDefaultClassifier({ modelId }));\n\n const changes: Change[] = [];\n let classifiedIndex = 0;\n for (const pair of pairs) {\n if (pair.tag === \"unchanged\") continue;\n if (pair.tag === \"trivial-change\") {\n changes.push(cosmeticModification(pair.a!, pair.b!));\n } else if (pair.tag === \"move\") {\n changes.push(moveChange(pair.a!, pair.b!));\n } else {\n changes.push(classified[classifiedIndex]!);\n classifiedIndex += 1;\n }\n }\n\n const provenance: Provenance = {\n modelId,\n promptVersion: options?.promptVersion ?? DEFAULT_PROMPT_VERSION,\n engineVersion: ENGINE_VERSION,\n };\n return { schemaVersion: SCHEMA_VERSION, provenance, changes, summary: summarize(changes) };\n}\n\n/** A cosmetic edit to a matched unit — determined deterministically, no model. */\nfunction cosmeticModification(a: Unit, b: Unit): Change {\n return { type: \"modification\", classification: \"cosmetic\", spanA: a.span, spanB: b.span, confidence: 1, needsReview: false };\n}\n\n/**\n * A relocation of identical content (ADR-0010) — deterministic and cosmetic: the\n * text did not change, only its position, so it is surfaced as a `move` rather\n * than a delete + insert. `a` is the old span, `b` the new one.\n */\nfunction moveChange(a: Unit, b: Unit): Change {\n return { type: \"move\", classification: \"cosmetic\", spanA: a.span, spanB: b.span, confidence: 1, needsReview: false };\n}\n\nfunction summarize(changes: readonly Change[]): DiffSummary {\n const byType = { insertion: 0, deletion: 0, modification: 0, move: 0 };\n let substantive = 0;\n let cosmetic = 0;\n let needsReview = 0;\n for (const change of changes) {\n byType[change.type] += 1;\n if (change.classification === \"substantive\") substantive += 1;\n else cosmetic += 1;\n if (change.needsReview) needsReview += 1;\n }\n return { substantive, cosmetic, byType, needsReview };\n}\n"],"mappings":"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","names":["retryAfterMs","normalize"]}
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -160,7 +160,7 @@ declare function needsReviewVerdict(): ClassifierVerdict;
|
|
|
160
160
|
* with `package.json` `version`; `test/version.contract.test.ts` fails the build
|
|
161
161
|
* if the two drift, so a published artifact never stamps a stale version.
|
|
162
162
|
*/
|
|
163
|
-
declare const ENGINE_VERSION = "0.1.
|
|
163
|
+
declare const ENGINE_VERSION = "0.1.1";
|
|
164
164
|
/** Default prompt-template version, stamped into `Provenance.promptVersion`. */
|
|
165
165
|
declare const DEFAULT_PROMPT_VERSION = "0";
|
|
166
166
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED