@company-semantics/contracts 45.1.0 → 45.3.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.
Files changed (32) hide show
  1. package/package.json +10 -3
  2. package/src/__tests__/resource-keys.test.ts +59 -0
  3. package/src/api/generated-spec-hash.ts +2 -2
  4. package/src/api/generated.ts +540 -1
  5. package/src/comments/README.md +115 -0
  6. package/src/comments/__tests__/README.md +49 -0
  7. package/src/comments/__tests__/anchor-corpus.test.ts +202 -0
  8. package/src/comments/__tests__/fixtures/README.md +58 -0
  9. package/src/comments/__tests__/fixtures/comment-anchors.json +94 -0
  10. package/src/comments/__tests__/fixtures/comment-anchors.provenance.json +9 -0
  11. package/src/comments/__tests__/schemas.test.ts +274 -0
  12. package/src/comments/anchor.ts +120 -0
  13. package/src/comments/index.ts +43 -0
  14. package/src/comments/schemas.ts +227 -0
  15. package/src/generated/openapi-routes.ts +7 -0
  16. package/src/index.ts +42 -0
  17. package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +4 -0
  18. package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +741 -0
  19. package/src/notifications/__tests__/definition.test.ts +4 -3
  20. package/src/notifications/__tests__/fixtures.ts +34 -0
  21. package/src/notifications/__tests__/kinds.test.ts +11 -4
  22. package/src/notifications/__tests__/registry.test.ts +7 -5
  23. package/src/notifications/__tests__/render-snapshot.test.ts +36 -0
  24. package/src/notifications/kinds/comment-mention.ts +82 -0
  25. package/src/notifications/kinds/comment-reply.ts +79 -0
  26. package/src/notifications/kinds/index.ts +2 -0
  27. package/src/notifications/kinds.ts +12 -3
  28. package/src/notifications/payloads.ts +49 -0
  29. package/src/notifications/registry.ts +6 -2
  30. package/src/org/schemas.ts +23 -0
  31. package/src/resource-keys.ts +68 -0
  32. package/src/user-notifications/kinds.ts +13 -1
@@ -0,0 +1,115 @@
1
+ # comments/
2
+
3
+ ## Purpose
4
+
5
+ The published **read** vocabulary of the comment surface: where a thread hangs
6
+ (`CommentAnchorSchema`), what a thread and its comments look like on the wire
7
+ (`CommentThreadSchema`, `CommentSchema`, `CommentMentionSchema`), and who a
8
+ comment may name (`MentionableResponseSchema`).
9
+
10
+ This exists as a published contract rather than an app-local type because two
11
+ independent clients have to agree about it without a server in the middle to
12
+ arbitrate — see the first invariant.
13
+
14
+ ## Invariants
15
+
16
+ - **The anchor is the contract between the client that WRITES it and the client
17
+ that later RESOLVES it, and the backend is a passthrough that cannot catch
18
+ drift in it.** The route boundary validates the anchor's SHAPE and stores the
19
+ result as opaque jsonb; the service, the database and every projection treat
20
+ it as bytes and interpret nothing. So if the writing client and the resolving
21
+ client disagree about what these fields mean, the write succeeds, the read
22
+ succeeds, and the comment silently lands on the wrong text. There is no
23
+ server-side tripwire, and there cannot be one — which is precisely why the
24
+ shape is published here instead of being declared twice.
25
+ - **`v` is the evolution hatch, and it is the only thing that bounds that
26
+ risk.** A client that meets an anchor it cannot resolve sees an unfamiliar `v`
27
+ and degrades on purpose instead of guessing at fields it half-recognises. New
28
+ anchor shapes arrive as new `v` variants, never as a migration of an existing
29
+ one.
30
+ - **`quote` is required on a text anchor; `relStart`/`relEnd` are not.** The
31
+ quote is the durable fallback a resolving client re-finds the passage by once
32
+ the relative positions stop resolving. A comment authored from a card has no
33
+ attached collab session and can only produce the quote path — optional there
34
+ means "this client could not", never "this client chose not to".
35
+ - **The anchor variants are STRICT.** A document anchor carrying `quote` or
36
+ `relStart` is refused rather than accepted with the extras dropped: a client
37
+ that sent them meant to anchor to text and got the discriminator wrong, and
38
+ storing the mistake yields a thread that can never be placed.
39
+ - **A comment's `body` is null exactly when `deletedAt` is set, and such a
40
+ comment carries no mentions.** A soft-deleted comment is a REDACTED
41
+ PROJECTION, not a removed row — the tombstone keeps its position in the
42
+ `(createdAt ASC, id ASC)` sequence so the surrounding replies still read in
43
+ order, and a thread whose comments were all deleted stays distinguishable from
44
+ one that never had any. The rule is documented and writer-enforced rather than
45
+ expressed as a Zod refinement: a refinement would make this package refuse a
46
+ response the server is willing to emit, and it cannot appear in the OpenAPI
47
+ surface, so it would become a third description of the wire shape that no
48
+ parity guard can check.
49
+ - **Mention offsets are ZERO-BASED, EXCLUSIVE-END UTF-16 CODE UNITS** into the
50
+ exact body string — plain JavaScript string coordinates. An emoji counts as
51
+ two units, so a client measuring in code points slides off by one per
52
+ preceding emoji, and **no Unicode normalization happens anywhere on this
53
+ path**: normalising before measuring changes the length and desynchronises
54
+ every offset after the first composed character.
55
+ - **Mention identity renders from the mention row, never from the body
56
+ characters under the range.** A stale or forged range can only
57
+ mis-**highlight**; it can never misattribute a mention to the wrong person.
58
+ Offsets are nullable (and null on every row today, pending the offset
59
+ columns) — treat null as "highlight nothing", not as an error.
60
+ - **These shapes are MIRRORED from the backend route boundary, not authored
61
+ here.** Every bound matches
62
+ `company-semantics-backend/src/api/http/routes/comments/comments.schemas.ts`,
63
+ and therefore matches the generated `components["schemas"]` view in `../api`.
64
+ The accept/reject corpus in `__tests__/fixtures/comment-anchors.json` is
65
+ copied **byte-for-byte** from that repo's `tests/fixtures/`. Only this side of
66
+ that copy is gated: its digest is pinned in
67
+ `__tests__/fixtures/comment-anchors.provenance.json` and re-derived on every
68
+ test run, so the corpus cannot be edited here quietly. A change made at the
69
+ boundary is NOT caught from this repo — nothing in a single-repo CI run can
70
+ see that file, and this package must not depend on the backend to look. See
71
+ ADR-CONTRACTS-116 for the split and for where the upstream gate belongs.
72
+ - **Request bodies are NOT here** (ADR-CONT-029), and one of them must never
73
+ be: the mention set on a write is validated against a candidate list the
74
+ server derives from `effective_acl_grants`, so nothing a client sends may
75
+ widen who can be mentioned.
76
+
77
+ ## Public API
78
+
79
+ | Export | Description |
80
+ | --------------------------------- | --------------------------------------------------------------------------- |
81
+ | `CommentAnchorSchema` | `{document,v:1}` \| `{text,v:1,…}`, strict, discriminated on `type` |
82
+ | `COMMENT_ANCHOR_TYPES` | The anchor discriminants, as a vocabulary tuple |
83
+ | `CommentAnchorTypeSchema` | Zod mirror of `COMMENT_ANCHOR_TYPES` — what `anchorType` is checked against |
84
+ | `COMMENT_THREAD_STATUSES` | `open` \| `resolved` — there is no third state |
85
+ | `CommentThreadStatusSchema` | Zod mirror of `COMMENT_THREAD_STATUSES` |
86
+ | `COMMENT_SUBJECT_TYPES` | What a thread can hang off — closed, sized to what admits comments |
87
+ | `CommentSubjectTypeSchema` | Zod mirror of `COMMENT_SUBJECT_TYPES` |
88
+ | `CommentMentionSchema` | One mention: user id, nullable UTF-16 range, read-time display name |
89
+ | `CommentSchema` | One comment; `body` null iff soft-deleted (infers `CommentProjection`) |
90
+ | `CommentThreadSummarySchema` | A thread without its comments — the resolve/reopen response |
91
+ | `CommentThreadSchema` | A thread with all its comments, oldest-first, tombstones included |
92
+ | `CommentThreadListResponseSchema` | `GET /api/comments` |
93
+ | `MentionableCandidateSchema` | One offerable mention target — no email, no access level |
94
+ | `MentionableResponseSchema` | `GET /api/company-md/docs/{id}/mentionable` |
95
+
96
+ ## Dependencies
97
+
98
+ - `zod` — schemas are canonical, types are inferred.
99
+ - `./anchor` ← `./schemas`. Nothing outside this directory is imported: the
100
+ comment vocabulary binds to the existing `commenter` band of
101
+ `../permissions`'s `AccessLevel` and introduces no access level of its own, so
102
+ it needs no import to say so.
103
+
104
+ ## Related vocabulary elsewhere in this package
105
+
106
+ - `../resource-keys` carries `commentThreads` — the SSE coalescer's key for "the
107
+ threads on this subject". It lives there rather than app-locally because the
108
+ key registry **is** the coalescer's vocabulary; an unregistered key cannot be
109
+ routed through it.
110
+ - `../user-notifications` carries `comment.mention` and `comment.reply` — the
111
+ durable inbox rows a comment produces.
112
+ - `../notifications` carries the same two names as composable notifications —
113
+ the message that leaves the building. The overlap is expected and is the same
114
+ one `companyMd.access_requested` already has; the two unions are not derived
115
+ from each other.
@@ -0,0 +1,49 @@
1
+ # comments/\_\_tests\_\_/
2
+
3
+ ## Purpose
4
+
5
+ Contract tests for the published comment vocabulary: the anchor union against
6
+ the mirrored corpus (`anchor-corpus.test.ts`), and the thread/comment/mention
7
+ projections against the invariants their JSDoc claims (`schemas.test.ts`).
8
+
9
+ ## Invariants
10
+
11
+ - **The anchor suite is DRIVEN BY THE CORPUS, not by hand-copied cases.** It
12
+ iterates every `accept` and every `reject` entry in
13
+ `./fixtures/comment-anchors.json` — a byte-for-byte copy of the backend's
14
+ fixture file — so a case added at the boundary is covered here as soon as the
15
+ file is re-copied, with no test edit. Hand-copying the cases would let the two
16
+ suites diverge silently, which is exactly the failure this domain exists to
17
+ prevent.
18
+ - **The corpus's digest is pinned, and that pin is the only mechanical part of
19
+ the anti-drift story that runs in this repo's CI.** `the corpus is still the
20
+ backend's bytes` re-derives the sha256 of `./fixtures/comment-anchors.json`
21
+ and compares it to `./fixtures/comment-anchors.provenance.json`, so editing
22
+ the corpus here — rather than upstream, then re-copying — fails. It does not
23
+ and cannot notice a change made in the backend file: a single-repo CI checkout
24
+ never sees it. `pnpm anchor-corpus:check` covers that half only where a
25
+ backend working tree is reachable, and reports when it is not.
26
+ - **The named rejection tests are additional, not a substitute.** `rejects a
27
+ text anchor with no quote` and `rejects a document anchor carrying
28
+ text-anchor fields` name the two failure modes the strict variants exist for,
29
+ in prose, so a reader of the test list can see them without decoding fixture
30
+ names. The corpus covers the same ground mechanically; both are wanted.
31
+ - **The corpus file is read with `readFileSync` + `JSON.parse`, not imported.**
32
+ `src/tsconfig.json` does not set `resolveJsonModule`, so a JSON import from
33
+ anything the typechecker reaches would fail the build. Reading it keeps the
34
+ fixture inert regardless of which file loads it. `node:fs` is fine in here —
35
+ the vocabulary-guard skips `__tests__/`.
36
+ - **Negative tests mutate ONE field of a well-formed factory result.** A
37
+ hand-built broken object can pass for the wrong reason: it fails because of
38
+ the field nobody was testing.
39
+
40
+ ## Public API
41
+
42
+ None — test-only.
43
+
44
+ ## Dependencies
45
+
46
+ - `vitest` — `describe` / `it` / `expect`.
47
+ - `node:fs`, `node:path`, `node:url` — to read the corpus and its pin from disk.
48
+ - `node:crypto` — to re-derive the corpus digest.
49
+ - `../anchor`, `../schemas` — the schemas under test.
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The anchor union, held to the SAME corpus the backend route boundary is held
3
+ * to (ADR-CONTRACTS-116).
4
+ *
5
+ * WHY THIS SUITE IS FIXTURE-DRIVEN RATHER THAN HAND-WRITTEN. The backend
6
+ * validates an anchor's shape once, at the route boundary, and then stores
7
+ * opaque jsonb it never interprets. So nothing on the server can notice if this
8
+ * package's idea of a valid anchor drifts from that boundary's — the write
9
+ * succeeds, the read succeeds, and a comment lands on the wrong text.
10
+ * `./fixtures/comment-anchors.json` is therefore a byte-for-byte copy of the
11
+ * backend's fixture file, and this file runs `CommentAnchorSchema` over every
12
+ * case in it. A case added at the boundary is covered here the moment the file
13
+ * is re-copied. Hand-copying the cases would reintroduce exactly the drift the
14
+ * copy exists to prevent.
15
+ *
16
+ * The pinned-digest test below is the part of that defence that is MECHANICAL
17
+ * in this repo's CI: it re-derives the corpus's sha256 and compares it to
18
+ * `./fixtures/comment-anchors.provenance.json`, so an edit made on THIS side
19
+ * fails here. Drift introduced on the BACKEND side is NOT gated from contracts
20
+ * CI — a single-repo checkout cannot see that file, and this package must not
21
+ * depend on the backend to look. `pnpm anchor-corpus:check` diffs the live
22
+ * upstream file where a backend tree is reachable, `pnpm anchor-corpus:sync`
23
+ * re-copies, and ADR-CONTRACTS-116 records that the upstream half is still a
24
+ * documented promise rather than a gate.
25
+ *
26
+ * The named rejection tests below are additional rather than a substitute: the
27
+ * corpus covers the same ground by fixture name, and these say in prose what
28
+ * the strict variants are FOR.
29
+ */
30
+ import { createHash } from "node:crypto";
31
+ import { readFileSync } from "node:fs";
32
+ import { dirname, join } from "node:path";
33
+ import { fileURLToPath } from "node:url";
34
+ import { describe, expect, it } from "vitest";
35
+
36
+ import { CommentAnchorSchema } from "../anchor.js";
37
+
38
+ /** One named case from the corpus. */
39
+ interface AnchorCase {
40
+ name: string;
41
+ anchor: unknown;
42
+ }
43
+
44
+ /** The subset of `comment-anchors.provenance.json` this suite enforces. */
45
+ interface CorpusPin {
46
+ source: { repo: string; path: string };
47
+ bytes: number;
48
+ sha256: string;
49
+ }
50
+
51
+ /**
52
+ * Read from disk rather than imported: `src/tsconfig.json` sets no
53
+ * `resolveJsonModule`, so a JSON import would fail the build the moment
54
+ * anything the typechecker reaches loaded it.
55
+ */
56
+ const CORPUS_PATH = join(
57
+ dirname(fileURLToPath(import.meta.url)),
58
+ "fixtures",
59
+ "comment-anchors.json",
60
+ );
61
+ const corpus = JSON.parse(readFileSync(CORPUS_PATH, "utf8")) as {
62
+ accept: AnchorCase[];
63
+ reject: AnchorCase[];
64
+ };
65
+
66
+ const PIN_PATH = join(
67
+ dirname(fileURLToPath(import.meta.url)),
68
+ "fixtures",
69
+ "comment-anchors.provenance.json",
70
+ );
71
+
72
+ describe("the corpus is still the backend's bytes", () => {
73
+ it("matches the digest pinned when it was copied", () => {
74
+ // The one check in this repo that notices the corpus being EDITED rather
75
+ // than re-copied. Without it, "byte-for-byte copy" is a comment: a case
76
+ // hand-tuned here to make a red test green would pass, and the published
77
+ // contract would quietly become its own authority instead of the
78
+ // boundary's mirror. Bytes, not parsed JSON — a reflow that preserves the
79
+ // parse still breaks the copy, which is why the file is in /.prettierignore.
80
+ //
81
+ // This does NOT see a change made upstream; nothing in a single-repo CI run
82
+ // can. See the file header and ADR-CONTRACTS-116.
83
+ const pin = JSON.parse(readFileSync(PIN_PATH, "utf8")) as CorpusPin;
84
+ const bytes = readFileSync(CORPUS_PATH);
85
+
86
+ expect(bytes.byteLength).toBe(pin.bytes);
87
+ expect(createHash("sha256").update(bytes).digest("hex")).toBe(pin.sha256);
88
+ });
89
+
90
+ it("names the upstream file it was copied from", () => {
91
+ // The pin is also the pointer a reader needs to run the diff by hand, so an
92
+ // emptied or reshaped source block is a failure, not a formality.
93
+ const pin = JSON.parse(readFileSync(PIN_PATH, "utf8")) as CorpusPin;
94
+
95
+ expect(pin.source.repo).toBe("company-semantics-backend");
96
+ expect(pin.source.path).toBe("tests/fixtures/comment-anchors.json");
97
+ });
98
+ });
99
+
100
+ describe("CommentAnchorSchema against the mirrored corpus", () => {
101
+ it("has both halves of the corpus, so neither loop can pass vacuously", () => {
102
+ // An empty array iterates cleanly and reports as a passing suite. This is
103
+ // the only assertion that notices a corpus copied as an empty shell.
104
+ expect(corpus.accept.length).toBeGreaterThan(0);
105
+ expect(corpus.reject.length).toBeGreaterThan(0);
106
+ });
107
+
108
+ for (const { name, anchor } of corpus.accept) {
109
+ it(`accepts ${name}`, () => {
110
+ expect(CommentAnchorSchema.safeParse(anchor).success).toBe(true);
111
+ });
112
+ }
113
+
114
+ for (const { name, anchor } of corpus.reject) {
115
+ it(`rejects ${name}`, () => {
116
+ expect(CommentAnchorSchema.safeParse(anchor).success).toBe(false);
117
+ });
118
+ }
119
+ });
120
+
121
+ describe("the anchor's load-bearing refusals, named", () => {
122
+ it("rejects a text anchor with no quote", () => {
123
+ // `quote` is the DURABLE FALLBACK — what a resolving client re-finds the
124
+ // passage by once the relative positions stop resolving, and the composer's
125
+ // `Comment on "…"` label. An anchor without one is unplaceable the first
126
+ // time the document is edited, so it must never reach storage.
127
+ expect(
128
+ CommentAnchorSchema.safeParse({
129
+ type: "text",
130
+ v: 1,
131
+ prefix: "agreed to ",
132
+ suffix: " by March",
133
+ }).success,
134
+ ).toBe(false);
135
+ });
136
+
137
+ it("rejects a document anchor carrying text-anchor fields", () => {
138
+ // The variants are STRICT on purpose. A client that sent `quote` on a
139
+ // document anchor meant to anchor to text and got the discriminator wrong;
140
+ // accepting it with the extras dropped stores a thread that can never be
141
+ // placed, and nothing downstream would ever flag it.
142
+ expect(
143
+ CommentAnchorSchema.safeParse({
144
+ type: "document",
145
+ v: 1,
146
+ quote: "stray quote",
147
+ prefix: "",
148
+ suffix: "",
149
+ }).success,
150
+ ).toBe(false);
151
+ });
152
+
153
+ it("rejects an unknown version on either variant", () => {
154
+ // `v` is the evolution hatch: a NEW shape is a new `v`, so a client must
155
+ // never accept a `v` it does not know and guess at the fields it
156
+ // half-recognises.
157
+ expect(
158
+ CommentAnchorSchema.safeParse({ type: "document", v: 2 }).success,
159
+ ).toBe(false);
160
+ expect(
161
+ CommentAnchorSchema.safeParse({
162
+ type: "text",
163
+ v: 2,
164
+ quote: "q",
165
+ prefix: "",
166
+ suffix: "",
167
+ }).success,
168
+ ).toBe(false);
169
+ });
170
+
171
+ it("rejects a relative position that is not base64", () => {
172
+ // The backend never decodes these, so the charset bound is the ONLY thing
173
+ // standing between a malformed position and a client that cannot decode it
174
+ // either. This is the case a looser `z.string().max(4096)` would let
175
+ // through.
176
+ expect(
177
+ CommentAnchorSchema.safeParse({
178
+ type: "text",
179
+ v: 1,
180
+ relStart: "not base64!!",
181
+ quote: "q",
182
+ prefix: "",
183
+ suffix: "",
184
+ }).success,
185
+ ).toBe(false);
186
+ });
187
+
188
+ it("accepts a text anchor with only the quote path", () => {
189
+ // The positive twin of the rule above: a comment authored from a card has
190
+ // no attached collab session and can produce no relative position at all.
191
+ // If this ever stops parsing, commenting outside the editor breaks.
192
+ expect(
193
+ CommentAnchorSchema.safeParse({
194
+ type: "text",
195
+ v: 1,
196
+ quote: "ship the quarterly goals",
197
+ prefix: "",
198
+ suffix: "",
199
+ }).success,
200
+ ).toBe(true);
201
+ });
202
+ });
@@ -0,0 +1,58 @@
1
+ # comments/\_\_tests\_\_/fixtures/
2
+
3
+ ## Purpose
4
+
5
+ Holds `comment-anchors.json`, the **stable contract corpus** for the comment
6
+ anchor — named accept and reject cases that `CommentAnchorSchema` is run over by
7
+ `../anchor-corpus.test.ts`.
8
+
9
+ The file is a directory of its own rather than a `fixtures.ts` module (the shape
10
+ used elsewhere under `src/`) for one reason: it is **not this repo's file**. It
11
+ is a byte-for-byte copy of
12
+ `company-semantics-backend/tests/fixtures/comment-anchors.json`, and keeping it
13
+ as inert data in a directory of its own is what makes "copied, never edited"
14
+ obvious to the next reader.
15
+
16
+ ## Invariants
17
+
18
+ - **The file is copied BYTE-FOR-BYTE from the backend suite.** The backend route
19
+ boundary is the only place a comment anchor is ever validated (it stores
20
+ opaque jsonb and interprets nothing), so the corpus the boundary is tested
21
+ with and the corpus the published contract is tested with must be the same
22
+ bytes. If they were merely "kept in sync by hand", the one risk this whole
23
+ domain exists to manage — silent app/contracts drift on a shape no server
24
+ checks — would be back.
25
+ - **Only ONE direction of that copy is gated, and it is this one.**
26
+ `comment-anchors.provenance.json` pins the copy's sha256 and byte length, and
27
+ `../anchor-corpus.test.ts` re-derives both on every run — so an edit made
28
+ here fails CI. A change made in the BACKEND file is invisible from this repo:
29
+ contracts CI checks out one repo and must not depend on the backend to look.
30
+ `pnpm anchor-corpus:check` diffs the live upstream file when a backend tree is
31
+ reachable and says so when it is not. See ADR-CONTRACTS-116 for where the
32
+ upstream gate belongs (backend CI, via `node_modules`).
33
+ - **Never hand-tune this file.** Change the anchor schema and the corpus
34
+ together, in the backend, then re-copy with `pnpm anchor-corpus:sync`, which
35
+ rewrites the pin in the same step. Editing this side to make a test pass fails
36
+ the pin; editing it and re-pinning by hand defeats the check, and shows up as
37
+ exactly that in the diff.
38
+ - **It is listed in `/.prettierignore`.** Prettier reflows the long anchor
39
+ objects, which would break the byte identity (and fail the pin). The file
40
+ carries the backend's formatting, not this repo's. The pin beside it is NOT
41
+ ignored — it is this repo's file and Prettier owns its formatting.
42
+ - **Cases are addressed by `name`, not by index.** Both tests iterate the
43
+ `accept` and `reject` arrays and use each case's `name` as its test label, so
44
+ a case added upstream is covered here the moment it is copied over, with no
45
+ edit to the test.
46
+
47
+ ## Public API
48
+
49
+ None — test-only fixture data. `comment-anchors.provenance.json` is generated;
50
+ regenerate it with `pnpm anchor-corpus:sync` rather than editing it.
51
+
52
+ ## Dependencies
53
+
54
+ - Upstream source: `company-semantics-backend/tests/fixtures/comment-anchors.json`
55
+ (authored alongside the anchor boundary; see backend ADR slug
56
+ `polymorphic-comment-threads`).
57
+ - `scripts/anchor-corpus.ts` — writes the pin (`--sync`) and verifies it
58
+ (`--check`, the default).
@@ -0,0 +1,94 @@
1
+ {
2
+ "$comment": "STABLE CONTRACT CORPUS for the comment-anchor route boundary (PRD-00920 / ADR-BE-512 §4). Named accept/reject cases for CommentAnchorSchema (src/api/http/routes/comments/comments.schemas.ts). PRD-00922 copies this file into @company-semantics/contracts and mechanically diffs the two copies, so schema drift between the backend boundary and the published contract is caught by a file comparison. Change cases here only together with the schema; never hand-tune one side.",
3
+ "accept": [
4
+ {
5
+ "name": "document-anchor",
6
+ "anchor": { "type": "document", "v": 1 }
7
+ },
8
+ {
9
+ "name": "text-anchor-with-relative-positions",
10
+ "anchor": {
11
+ "type": "text",
12
+ "v": 1,
13
+ "relStart": "AAECAwQ=",
14
+ "relEnd": "BQYHCAk=",
15
+ "quote": "ship the quarterly goals",
16
+ "prefix": "agreed to ",
17
+ "suffix": " by March"
18
+ }
19
+ },
20
+ {
21
+ "name": "text-anchor-quote-only",
22
+ "anchor": {
23
+ "type": "text",
24
+ "v": 1,
25
+ "quote": "ship the quarterly goals",
26
+ "prefix": "",
27
+ "suffix": ""
28
+ }
29
+ }
30
+ ],
31
+ "reject": [
32
+ {
33
+ "name": "text-anchor-missing-quote",
34
+ "anchor": { "type": "text", "v": 1, "prefix": "agreed to ", "suffix": " by March" }
35
+ },
36
+ {
37
+ "name": "document-anchor-carrying-text-fields",
38
+ "anchor": {
39
+ "type": "document",
40
+ "v": 1,
41
+ "quote": "stray quote",
42
+ "prefix": "",
43
+ "suffix": ""
44
+ }
45
+ },
46
+ {
47
+ "name": "unknown-version-document",
48
+ "anchor": { "type": "document", "v": 2 }
49
+ },
50
+ {
51
+ "name": "unknown-version-text",
52
+ "anchor": { "type": "text", "v": 2, "quote": "q", "prefix": "", "suffix": "" }
53
+ },
54
+ {
55
+ "name": "unknown-anchor-type",
56
+ "anchor": { "type": "region", "v": 1 }
57
+ },
58
+ {
59
+ "name": "oversized-prefix",
60
+ "anchor": {
61
+ "type": "text",
62
+ "v": 1,
63
+ "quote": "q",
64
+ "prefix": "ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp",
65
+ "suffix": ""
66
+ }
67
+ },
68
+ {
69
+ "name": "oversized-suffix",
70
+ "anchor": {
71
+ "type": "text",
72
+ "v": 1,
73
+ "quote": "q",
74
+ "prefix": "",
75
+ "suffix": "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss"
76
+ }
77
+ },
78
+ {
79
+ "name": "empty-quote",
80
+ "anchor": { "type": "text", "v": 1, "quote": "", "prefix": "", "suffix": "" }
81
+ },
82
+ {
83
+ "name": "relstart-not-base64",
84
+ "anchor": {
85
+ "type": "text",
86
+ "v": 1,
87
+ "relStart": "not base64!!",
88
+ "quote": "q",
89
+ "prefix": "",
90
+ "suffix": ""
91
+ }
92
+ }
93
+ ]
94
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$comment": "Provenance pin for ./comment-anchors.json, which is a byte-for-byte copy of company-semantics-backend/tests/fixtures/comment-anchors.json (ADR-CONTRACTS-116). anchor-corpus.test.ts re-derives this digest from the file's bytes on every run, so an edit made on THIS side fails CI instead of quietly making the corpus this repo's own. Drift introduced on the BACKEND side is not observable from contracts CI and is not gated here. Regenerate with `pnpm anchor-corpus:sync`; never hand-edit this file or the corpus.",
3
+ "source": {
4
+ "repo": "company-semantics-backend",
5
+ "path": "tests/fixtures/comment-anchors.json"
6
+ },
7
+ "bytes": 2649,
8
+ "sha256": "84fa0040c9acc166a5551185950e5dc8afffcc5fd63a1f17080422318b405c8c"
9
+ }