@company-semantics/contracts 51.0.0 → 51.2.0

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.
@@ -0,0 +1,391 @@
1
+ /**
2
+ * The published resolver, held to the corpus it must agree with.
3
+ *
4
+ * WHAT THIS SUITE EXISTS TO KILL. The plausible-looking wrong resolver returns
5
+ * the FIRST match. It passes every "the quote is still there" test, it never
6
+ * throws, and it is wrong precisely when the document is repetitive — which is
7
+ * when comments matter most. Its symptom is not a broken highlight but a
8
+ * confident one, on a paragraph the comment's author never saw. Every
9
+ * `orphaned` expectation below is a case where that resolver returns a
10
+ * well-formed range, so a suite that only asserted the happy path would sit
11
+ * green over it.
12
+ *
13
+ * The second wrong resolver collapses `document` into `orphaned` — a thread on
14
+ * the subject AS A WHOLE draws no highlight either way, so the bug is invisible
15
+ * until the rail files it behind "this text is gone".
16
+ *
17
+ * THE TEXT-INSERTION BLOCK IS DRIVEN BY THE PINNED CORPUS, not by hand-copied
18
+ * cases. `./fixtures/comment-anchors.json` carries a `document` and an
19
+ * `expectedResolution` on each text-insertion accept row: ground truth for
20
+ * exactly this module, recorded contracts-first in PRD-00933. `anchor-corpus`
21
+ * re-derives those outcomes with its own seam scan; this suite drives the
22
+ * SHIPPED resolver over the same rows, which is what turns that hand-rolled
23
+ * cross-check into a check of the implementation rather than of itself. The
24
+ * corpus is read, never edited — its digest is pinned beside it.
25
+ */
26
+ import { readFileSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { describe, expect, it } from "vitest";
30
+
31
+ import { ANCHOR_CONTEXT_CHARS } from "../anchor-constants.js";
32
+ import { CommentAnchorSchema } from "../anchor.js";
33
+ import {
34
+ contextAfter,
35
+ contextBefore,
36
+ contextsMatchAt,
37
+ findSoleOccurrence,
38
+ resolveAnchorFromText,
39
+ } from "../resolve.js";
40
+
41
+ /** One named case from the corpus. */
42
+ interface AnchorCase {
43
+ name: string;
44
+ anchor: unknown;
45
+ }
46
+
47
+ /** The additive fields the text-insertion rows carry beside `anchor`. */
48
+ interface InsertionFixture extends AnchorCase {
49
+ anchor: { type: "text-insertion" };
50
+ document: string;
51
+ expectedResolution:
52
+ { outcome: "resolved"; offset: number } | { outcome: "orphaned" };
53
+ }
54
+
55
+ const isInsertionFixture = (c: AnchorCase): c is InsertionFixture => {
56
+ const anchor = c.anchor as { type?: unknown } | null;
57
+ return anchor !== null && anchor?.type === "text-insertion";
58
+ };
59
+
60
+ /**
61
+ * Read from disk rather than imported: `src/tsconfig.json` sets no
62
+ * `resolveJsonModule`, so a JSON import would fail the build the moment
63
+ * anything the typechecker reaches loaded it.
64
+ */
65
+ const CORPUS_PATH = join(
66
+ dirname(fileURLToPath(import.meta.url)),
67
+ "fixtures",
68
+ "comment-anchors.json",
69
+ );
70
+ const corpus = JSON.parse(readFileSync(CORPUS_PATH, "utf8")) as {
71
+ accept: AnchorCase[];
72
+ reject: AnchorCase[];
73
+ };
74
+
75
+ /** Parse through the published schema, so no fixture reaches the resolver
76
+ * having skipped the boundary a real anchor cannot skip. */
77
+ const parsed = (anchor: unknown) => CommentAnchorSchema.parse(anchor);
78
+
79
+ const textAnchor = (fields: {
80
+ quote: string;
81
+ prefix: string;
82
+ suffix: string;
83
+ }) => parsed({ type: "text", v: 1, ...fields });
84
+
85
+ const insertionAnchor = (fields: {
86
+ leftContext: string;
87
+ rightContext: string;
88
+ }) => parsed({ type: "text-insertion", v: 1, ...fields });
89
+
90
+ describe("resolveAnchorFromText against the pinned insertion corpus", () => {
91
+ const insertionCases = corpus.accept.filter(isInsertionFixture);
92
+
93
+ it("has the four recorded insertion fixtures, so the loop cannot pass vacuously", () => {
94
+ // An empty array iterates cleanly and reports as a passing suite. Position
95
+ // 0, document end, ambiguous repeated seam, surrogate-pair flanked.
96
+ expect(insertionCases.length).toBeGreaterThanOrEqual(4);
97
+ });
98
+
99
+ for (const { name, anchor, document, expectedResolution } of insertionCases) {
100
+ it(`resolves ${name} exactly as the corpus records`, () => {
101
+ const resolution = resolveAnchorFromText(parsed(anchor), document);
102
+
103
+ if (expectedResolution.outcome === "resolved") {
104
+ // Whole-object, including the rung: a rung-2 answer reported as rung 1
105
+ // is the bug, and an insertion resolution is always zero-width.
106
+ expect(resolution).toEqual({
107
+ status: "resolved",
108
+ start: expectedResolution.offset,
109
+ end: expectedResolution.offset,
110
+ rung: 2,
111
+ });
112
+ } else {
113
+ expect(resolution).toEqual({ status: "orphaned" });
114
+ }
115
+ });
116
+ }
117
+
118
+ it("resolves the surrogate-flanked seam between the pairs, not inside one", () => {
119
+ // The corpus row carries a `relPos` encoded against another document. This
120
+ // module has no Y.Text in reach and correctly ignores it, so the seam has
121
+ // to carry the anchor on its own — and 11 is a UTF-16 code-unit offset
122
+ // BETWEEN two astral pairs, where a code-point walk would land on 9.
123
+ const document = "Q3 goals 🎯🚀 shipped early";
124
+ const anchor = parsed({
125
+ type: "text-insertion",
126
+ v: 1,
127
+ relPos: "AQLmzsvNAgA=",
128
+ leftContext: "goals 🎯",
129
+ rightContext: "🚀 shipped",
130
+ });
131
+
132
+ expect(resolveAnchorFromText(anchor, document)).toEqual({
133
+ status: "resolved",
134
+ start: 11,
135
+ end: 11,
136
+ rung: 2,
137
+ });
138
+ expect(document.slice(0, 11)).toBe("Q3 goals 🎯");
139
+ });
140
+ });
141
+
142
+ describe("resolveAnchorFromText — ambiguity resolves orphaned, never the first match", () => {
143
+ it("orphans an anchor whose quote occurs ambiguously", () => {
144
+ // No context to disambiguate with, and the quote repeats. A first-match
145
+ // implementation returns index 3 here and is silently wrong half the time.
146
+ const anchor = textAnchor({ quote: "the plan", prefix: "", suffix: "" });
147
+ const text = "Is the plan agreed? We ship the plan on Friday.";
148
+
149
+ expect(resolveAnchorFromText(anchor, text)).toEqual({ status: "orphaned" });
150
+ });
151
+
152
+ it("orphans when even the context-qualified match is ambiguous", () => {
153
+ const anchor = textAnchor({
154
+ quote: "beta",
155
+ prefix: "alpha ",
156
+ suffix: " gamma",
157
+ });
158
+ const text = "alpha beta gamma / alpha beta gamma";
159
+
160
+ // Two context matches mean the CONTEXT failed to identify a candidate.
161
+ // Binding to the first occurrence — or quietly retrying the bare quote,
162
+ // which is equally ambiguous — is the shortcut this ladder forbids.
163
+ expect(resolveAnchorFromText(anchor, text)).toEqual({ status: "orphaned" });
164
+ });
165
+
166
+ it("resolves via context when the bare quote alone would be ambiguous", () => {
167
+ const anchor = textAnchor({ quote: "beta", prefix: "alpha ", suffix: "" });
168
+ const text = "zeta beta gamma / alpha beta gamma";
169
+
170
+ // The SECOND "beta" — the one the context identifies. This is the positive
171
+ // twin of the rule above: context DISAMBIGUATES, it does not merely veto.
172
+ expect(resolveAnchorFromText(anchor, text)).toEqual({
173
+ status: "resolved",
174
+ start: text.lastIndexOf("beta"),
175
+ end: text.lastIndexOf("beta") + "beta".length,
176
+ rung: 2,
177
+ });
178
+ });
179
+
180
+ it("falls back to the bare quote when the surrounding sentence was rewritten", () => {
181
+ const anchor = textAnchor({
182
+ quote: "materially understated",
183
+ prefix: "Q3 revenue was ",
184
+ suffix: " in the board pack.",
185
+ });
186
+ const text = "The figure was materially understated, we now believe.";
187
+
188
+ // Context ABSENT is not context AMBIGUOUS: the sentence moved, the quote
189
+ // survived, and a unique bare quote is still evidence enough.
190
+ expect(resolveAnchorFromText(anchor, text)).toEqual({
191
+ status: "resolved",
192
+ start: text.indexOf("materially understated"),
193
+ end:
194
+ text.indexOf("materially understated") +
195
+ "materially understated".length,
196
+ rung: 2,
197
+ });
198
+ });
199
+
200
+ it("counts overlapping occurrences as ambiguous", () => {
201
+ // "aa" occurs at 0 AND at 1. Resuming the second search at
202
+ // `first + needle.length` would miss the overlap and call this unique.
203
+ const anchor = textAnchor({ quote: "aa", prefix: "", suffix: "" });
204
+
205
+ expect(resolveAnchorFromText(anchor, "aaa")).toEqual({
206
+ status: "orphaned",
207
+ });
208
+ });
209
+
210
+ it("orphans when the quote is gone entirely", () => {
211
+ const anchor = textAnchor({
212
+ quote: "the original wording",
213
+ prefix: "before ",
214
+ suffix: " after",
215
+ });
216
+
217
+ expect(
218
+ resolveAnchorFromText(anchor, "Nothing of the sort remains."),
219
+ ).toEqual({ status: "orphaned" });
220
+ });
221
+ });
222
+
223
+ describe("resolveAnchorFromText — the insertion seam refuses ambiguity", () => {
224
+ it("orphans a repeated seam rather than taking the first row", () => {
225
+ const document = "item done, item done, item done";
226
+ const anchor = insertionAnchor({
227
+ leftContext: "item ",
228
+ rightContext: "done",
229
+ });
230
+
231
+ // A first-match resolver returns a perfectly well-formed caret at 5 here,
232
+ // two rows away from where the suggestion was written.
233
+ expect(document.indexOf("item done")).toBe(0);
234
+ expect(resolveAnchorFromText(anchor, document)).toEqual({
235
+ status: "orphaned",
236
+ });
237
+ });
238
+
239
+ it("orphans when the seam is gone entirely", () => {
240
+ const anchor = insertionAnchor({
241
+ leftContext: "approved ",
242
+ rightContext: "the plan.",
243
+ });
244
+
245
+ expect(
246
+ resolveAnchorFromText(anchor, "Nothing here resembles that sentence."),
247
+ ).toEqual({ status: "orphaned" });
248
+ });
249
+
250
+ it("resolves the empty seam on an empty document and orphans it once there is text", () => {
251
+ const anchor = insertionAnchor({ leftContext: "", rightContext: "" });
252
+
253
+ // On the empty document the anchor is complete and 0 is the only answer —
254
+ // the one case an empty seam is authorable at all.
255
+ expect(resolveAnchorFromText(anchor, "")).toEqual({
256
+ status: "resolved",
257
+ start: 0,
258
+ end: 0,
259
+ rung: 2,
260
+ });
261
+ // Once there is text it says nothing about WHERE among it the caret goes.
262
+ expect(
263
+ resolveAnchorFromText(anchor, "The board approved the plan."),
264
+ ).toEqual({ status: "orphaned" });
265
+ });
266
+
267
+ it("always resolves zero-width", () => {
268
+ const document = "We agreed to ship the quarterly goals.";
269
+ const anchor = insertionAnchor({
270
+ leftContext: "ship ",
271
+ rightContext: "the quarterly",
272
+ });
273
+
274
+ const resolution = resolveAnchorFromText(anchor, document);
275
+ expect(resolution.status).toBe("resolved");
276
+ if (resolution.status !== "resolved") return;
277
+ // A zero-width point that resolved to a RANGE would paint a highlight over
278
+ // text the suggestion never claimed.
279
+ expect(resolution.start).toBe(resolution.end);
280
+ });
281
+ });
282
+
283
+ describe("resolveAnchorFromText — the document anchor", () => {
284
+ it("reports a document anchor as document, never orphaned", () => {
285
+ const anchor = parsed({ type: "document", v: 1 });
286
+
287
+ // A thread on the subject AS A WHOLE draws no highlight, but it is
288
+ // correctly anchored — calling it orphaned would put it behind the rail's
289
+ // "this text is gone" affordance.
290
+ expect(resolveAnchorFromText(anchor, "any text at all")).toEqual({
291
+ status: "document",
292
+ });
293
+ expect(resolveAnchorFromText(anchor, "")).toEqual({ status: "document" });
294
+ });
295
+ });
296
+
297
+ describe("findSoleOccurrence", () => {
298
+ it("reports a lone occurrence with its index", () => {
299
+ expect(findSoleOccurrence("the plan is agreed", "plan")).toEqual({
300
+ kind: "unique",
301
+ index: 4,
302
+ });
303
+ });
304
+
305
+ it("reports an absent needle as absent, distinctly from ambiguous", () => {
306
+ // The resolver treats these two differently at the context level: absent
307
+ // falls through to the bare quote, ambiguous orphans immediately.
308
+ expect(findSoleOccurrence("the plan", "budget")).toEqual({
309
+ kind: "absent",
310
+ });
311
+ });
312
+
313
+ it("treats an empty needle as ambiguous, never as a match at 0", () => {
314
+ // `indexOf("")` is 0 on every string, so an implementation without this
315
+ // guard reports a confident unique match at the start of the document.
316
+ expect(findSoleOccurrence("the plan", "")).toEqual({ kind: "ambiguous" });
317
+ expect(findSoleOccurrence("", "")).toEqual({ kind: "ambiguous" });
318
+ });
319
+ });
320
+
321
+ describe("contextsMatchAt", () => {
322
+ const text = "The board approved the plan.";
323
+
324
+ it("confirms the contexts that actually surround the point", () => {
325
+ expect(contextsMatchAt(text, 19, "approved ", "the plan.")).toBe(true);
326
+ });
327
+
328
+ it("refuses a context that cannot FIT before the point", () => {
329
+ // THE LOAD-BEARING GUARD. `slice` reads a negative start from the END of
330
+ // the string, so without it "approved " would be compared against a
331
+ // substring taken from the far end and could answer true.
332
+ expect(contextsMatchAt(text, 2, "approved ", "e board")).toBe(false);
333
+ });
334
+
335
+ it("refuses a context that cannot FIT after the point", () => {
336
+ // The mirror case: `slice` CLAMPS a long end rather than failing.
337
+ expect(contextsMatchAt(text, text.length - 2, "n.", "the plan.")).toBe(
338
+ false,
339
+ );
340
+ });
341
+
342
+ it("refuses an out-of-bounds point outright", () => {
343
+ expect(contextsMatchAt(text, -1, "", "")).toBe(false);
344
+ expect(contextsMatchAt(text, text.length + 1, "", "")).toBe(false);
345
+ });
346
+
347
+ it("answers true at every in-bounds point for two empty contexts", () => {
348
+ // Not a bug — a fact about the question. It is why a consumer's
349
+ // relative-position rung must refuse an anchor with no contexts BEFORE it
350
+ // decodes anything, rather than treating this check as evidence.
351
+ expect(contextsMatchAt(text, 0, "", "")).toBe(true);
352
+ expect(contextsMatchAt(text, 7, "", "")).toBe(true);
353
+ });
354
+ });
355
+
356
+ describe("contextBefore / contextAfter", () => {
357
+ it("captures at most ANCHOR_CONTEXT_CHARS, the capture width", () => {
358
+ const text = "x".repeat(200);
359
+
360
+ // The CAPTURE width, deliberately narrower than what the schema ACCEPTS —
361
+ // capturing to the accept bound would leave a constructor no headroom.
362
+ expect(contextBefore(text, 150)).toHaveLength(ANCHOR_CONTEXT_CHARS);
363
+ expect(contextAfter(text, 50)).toHaveLength(ANCHOR_CONTEXT_CHARS);
364
+ });
365
+
366
+ it("clamps at the document edges instead of running off them", () => {
367
+ expect(contextBefore("short", 3)).toBe("sho");
368
+ expect(contextAfter("short", 3)).toBe("rt");
369
+ expect(contextBefore("short", 0)).toBe("");
370
+ expect(contextAfter("short", 5)).toBe("");
371
+ });
372
+
373
+ it("never opens on half a character", () => {
374
+ // The fixed-width slice would start exactly between the pair's two code
375
+ // units; the lone low surrogate that results is ill-formed on the wire.
376
+ const text = "🎯" + "a".repeat(ANCHOR_CONTEXT_CHARS - 1);
377
+ const before = contextBefore(text, text.length);
378
+
379
+ expect(before).toBe("a".repeat(ANCHOR_CONTEXT_CHARS - 1));
380
+ expect(before.charCodeAt(0)).toBeLessThan(0xd800);
381
+ });
382
+
383
+ it("never closes on half a character", () => {
384
+ const text = "a".repeat(ANCHOR_CONTEXT_CHARS - 1) + "🎯tail";
385
+ const after = contextAfter(text, 0);
386
+
387
+ // Trimming costs one code unit of context and changes nothing about
388
+ // matching — the document holds the same code units either way.
389
+ expect(after).toBe("a".repeat(ANCHOR_CONTEXT_CHARS - 1));
390
+ });
391
+ });
@@ -17,6 +17,7 @@ import {
17
17
  CommentThreadListResponseSchema,
18
18
  CommentThreadSchema,
19
19
  CommentThreadStatusSchema,
20
+ CommentThreadSummarySchema,
20
21
  MentionableResponseSchema,
21
22
  } from "../schemas.js";
22
23
 
@@ -68,6 +69,7 @@ function makeThread(over: Record<string, unknown> = {}) {
68
69
  createdByUserId: USER_ID,
69
70
  resolvedByUserId: null,
70
71
  resolvedAt: null,
72
+ lastAnchoredOffset: null,
71
73
  createdAt: "2026-08-01T12:00:00.000Z",
72
74
  updatedAt: "2026-08-01T12:00:00.000Z",
73
75
  comments: [makeComment()],
@@ -224,7 +226,13 @@ describe("CommentThreadSchema", () => {
224
226
  ).toBe(false);
225
227
  });
226
228
 
227
- it("keeps tombstones in the comment list rather than filtering them", () => {
229
+ it("parses a redacted comment inside a thread the schema still admits one", () => {
230
+ // This test once asserted that a reader SEES tombstones in a conversation.
231
+ // ADR-BE-545 ended that: the list read omits deleted comments outright. What
232
+ // survives is the SHAPE — the redacted projection is still a legal
233
+ // `CommentProjection`, because it is what the DELETE acknowledgement returns
234
+ // to the caller who performed the delete. So this pins the parse, not a read
235
+ // behaviour the server no longer has.
228
236
  const thread = CommentThreadSchema.parse(
229
237
  makeThread({
230
238
  comments: [
@@ -243,6 +251,26 @@ describe("CommentThreadSchema", () => {
243
251
  });
244
252
 
245
253
  describe("CommentThreadSummarySchema kind and suggestion", () => {
254
+ it("carries a lastAnchoredOffset, and null is the ordinary answer", () => {
255
+ // An ORDERING HINT and nothing else (ADR-BE-548): where an orphaned thread's
256
+ // passage used to be, so a client can keep the card in the reader's list
257
+ // rather than piling every orphan at the top. It must never yield a range.
258
+ expect(
259
+ CommentThreadSummarySchema.parse(makeThread()).lastAnchoredOffset,
260
+ "a thread nobody has watched lose its passage carries null",
261
+ ).toBeNull();
262
+ expect(
263
+ CommentThreadSummarySchema.parse(makeThread({ lastAnchoredOffset: 412 }))
264
+ .lastAnchoredOffset,
265
+ ).toBe(412);
266
+ expect(
267
+ CommentThreadSummarySchema.safeParse(
268
+ makeThread({ lastAnchoredOffset: 3.5 }),
269
+ ).success,
270
+ "a fractional offset is not a source position",
271
+ ).toBe(false);
272
+ });
273
+
246
274
  it("a zero-comment suggestion thread parses through both response shapes", () => {
247
275
  // The full suggestion projection: kind `suggestion`, a v1 payload, a
248
276
  // zero-width text-insertion anchor, and NO comments. The backend creates a