@company-semantics/contracts 50.0.0 → 51.1.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.
- package/README.md +6 -1
- package/package.json +17 -17
- package/src/__tests__/resource-keys.test.ts +8 -8
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +116 -9
- package/src/comments/__tests__/schemas.test.ts +29 -1
- package/src/comments/schemas.ts +27 -8
- package/src/generated/openapi-routes.ts +2 -1
- package/src/resource-key-types.ts +9 -6
- package/src/resource-keys.ts +2 -2
- package/src/resource-registry.ts +5 -4
package/README.md
CHANGED
|
@@ -11,7 +11,10 @@ until they are proven stable.
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
IntentCategory,
|
|
16
|
+
InsightConfidence,
|
|
17
|
+
} from "@company-semantics/contracts";
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
## What belongs here
|
|
@@ -33,6 +36,7 @@ It does not emit `.d.ts` files. Consumers are expected to compile TS.
|
|
|
33
36
|
## Versioning
|
|
34
37
|
|
|
35
38
|
Follows semver:
|
|
39
|
+
|
|
36
40
|
- Additive vocabulary → minor bump
|
|
37
41
|
- Renaming or removal → major bump
|
|
38
42
|
|
|
@@ -41,6 +45,7 @@ Follows semver:
|
|
|
41
45
|
Releases are published automatically via GitHub Actions.
|
|
42
46
|
|
|
43
47
|
Steps:
|
|
48
|
+
|
|
44
49
|
1. `npm version patch`
|
|
45
50
|
2. `git push origin main --tags`
|
|
46
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "51.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -88,8 +88,8 @@
|
|
|
88
88
|
"typecheck:ci": "NODE_OPTIONS='--max-old-space-size=4096' tsc -p scripts/ci/tsconfig.json",
|
|
89
89
|
"lint": "eslint src/ scripts/ tests/ __tests__/ vitest.config.ts",
|
|
90
90
|
"lint:md": "markdownlint-cli2 '**/*.md' '#node_modules' '#.ralph/worktrees' '#.claude/worktrees'",
|
|
91
|
-
"format": "prettier --write
|
|
92
|
-
"format:check": "prettier --check
|
|
91
|
+
"format": "prettier --write .",
|
|
92
|
+
"format:check": "prettier --check .",
|
|
93
93
|
"lint:json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\"",
|
|
94
94
|
"prepare": "husky",
|
|
95
95
|
"guard:export": "npx tsx scripts/ci/export-guard.ts",
|
|
@@ -101,9 +101,9 @@
|
|
|
101
101
|
"guard:version-tag:json": "npx tsx scripts/ci/version-tag-guard.ts --json",
|
|
102
102
|
"guard:decisions-deprecation": "npx tsx scripts/ci/decisions-deprecation-guard.ts",
|
|
103
103
|
"guard:decisions-deprecation:json": "npx tsx scripts/ci/decisions-deprecation-guard.ts --json",
|
|
104
|
-
"guard:quick": "
|
|
105
|
-
"ci:local": "pnpm typecheck && pnpm lint:md && pnpm guard:test &&
|
|
106
|
-
"guard": "
|
|
104
|
+
"guard:quick": "sh scripts/ci/run-guard.sh --quick",
|
|
105
|
+
"ci:local": "pnpm typecheck && pnpm lint:md && pnpm guard:test && pnpm guard",
|
|
106
|
+
"guard": "sh scripts/ci/run-guard.sh",
|
|
107
107
|
"guard:test": "NODE_OPTIONS='--max-old-space-size=4096' vitest run scripts/ci/__tests__",
|
|
108
108
|
"release": "npx tsx scripts/release.ts",
|
|
109
109
|
"prepublishOnly": "echo 'ERROR: Publishing is CI-only via tag push. Use pnpm release instead.' && exit 1",
|
|
@@ -120,13 +120,13 @@
|
|
|
120
120
|
"generate:openapi-routes": "tsx scripts/generate-openapi-routes.ts",
|
|
121
121
|
"generate:openapi-routes:check": "tsx scripts/generate-openapi-routes.ts --check",
|
|
122
122
|
"generate:api-types:check": "openapi-typescript openapi/backend.yaml -o /tmp/cs-api-types-check.ts && diff -q src/api/generated.ts /tmp/cs-api-types-check.ts",
|
|
123
|
-
"generate:current": "tsx
|
|
124
|
-
"adr:new": "tsx
|
|
125
|
-
"repo-map": "tsx
|
|
126
|
-
"sync:workflow-pins": "tsx
|
|
127
|
-
"repo-map:check": "tsx
|
|
128
|
-
"readme-api": "tsx
|
|
129
|
-
"readme-api:check": "tsx
|
|
123
|
+
"generate:current": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/generate-current.ts\"",
|
|
124
|
+
"adr:new": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-control/scripts/adr/adr-new-cli.ts\"",
|
|
125
|
+
"repo-map": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/generate-repo-map.ts\" --roots src --name company-semantics-contracts --write",
|
|
126
|
+
"sync:workflow-pins": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/sync-workflow-pins.ts\" --write",
|
|
127
|
+
"repo-map:check": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/generate-repo-map.ts\" --roots src --name company-semantics-contracts --check",
|
|
128
|
+
"readme-api": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/generate-readme-api.ts\" --roots src --write --jsdoc",
|
|
129
|
+
"readme-api:check": "tsx \"$(git rev-parse --path-format=absolute --git-common-dir)/../../company-semantics-ci/scripts/generate-readme-api.ts\" --roots src --check --jsdoc",
|
|
130
130
|
"validate:amp": "tsx scripts/validate-amp.ts"
|
|
131
131
|
},
|
|
132
132
|
"packageManager": "pnpm@10.25.0",
|
|
@@ -148,9 +148,9 @@
|
|
|
148
148
|
"markdownlint-cli2": "^0.23.2",
|
|
149
149
|
"openapi-typescript": "^7.13.0",
|
|
150
150
|
"prettier": "^3.9.6",
|
|
151
|
-
"tsx": "^4.23.
|
|
151
|
+
"tsx": "^4.23.12",
|
|
152
152
|
"typescript": "^5.8.3",
|
|
153
|
-
"typescript-eslint": "^8.
|
|
153
|
+
"typescript-eslint": "^8.67.0",
|
|
154
154
|
"vite": "^8.2.1",
|
|
155
155
|
"vitest": "^4.1.10",
|
|
156
156
|
"yaml": "^2.9.0"
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"overrides": {
|
|
160
160
|
"brace-expansion@<5.0.9": ">=5.0.9",
|
|
161
161
|
"minimatch@<10.2.3": ">=10.2.3",
|
|
162
|
-
"nanoid@<3.3.
|
|
162
|
+
"nanoid@<3.3.18": ">=3.3.18",
|
|
163
163
|
"js-yaml@>=4.0.0 <4.3.1": ">=4.3.1 <5.0.0",
|
|
164
164
|
"js-yaml@>=5.0.0 <5.2.2": ">=5.2.2 <6.0.0",
|
|
165
165
|
"ajv@<6.14.0": ">=6.14.0",
|
|
@@ -176,5 +176,5 @@
|
|
|
176
176
|
"*.md": "markdownlint-cli2",
|
|
177
177
|
"package.json": "node -e \"JSON.parse(require('fs').readFileSync('package.json'))\""
|
|
178
178
|
},
|
|
179
|
-
"securityRequirementsVersion": "
|
|
179
|
+
"securityRequirementsVersion": "2dcea336317c1136c8d94798b1cadc9c115febf574d041a173974a5f7959de0b"
|
|
180
180
|
}
|
|
@@ -234,16 +234,16 @@ describe("resource-keys: companyMdAccessRequests (per-doc identity)", () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
describe("resource-keys:
|
|
237
|
+
describe("resource-keys: companyMdDocHistory (per-doc history list)", () => {
|
|
238
238
|
const DOC_ID = "44444444-4444-4444-8444-444444444444";
|
|
239
239
|
const key: ResourceKey = {
|
|
240
|
-
type: "
|
|
240
|
+
type: "companyMdDocHistory",
|
|
241
241
|
orgId: ORG_ID,
|
|
242
242
|
docId: DOC_ID,
|
|
243
243
|
};
|
|
244
244
|
|
|
245
245
|
it("serialises to [type, orgId, docId]", () => {
|
|
246
|
-
expect(toQueryKey(key)).toEqual(["
|
|
246
|
+
expect(toQueryKey(key)).toEqual(["companyMdDocHistory", ORG_ID, DOC_ID]);
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
it("round-trips through fromQueryKey", () => {
|
|
@@ -261,7 +261,7 @@ describe("resource-keys: companyMdDocVersions (per-doc version list)", () => {
|
|
|
261
261
|
|
|
262
262
|
it("does NOT collide with companyMdAccessRequests, which shares its shape", () => {
|
|
263
263
|
// Same segments, same field name, same document — only the type tag tells
|
|
264
|
-
// the two apart. An owner's access-request inbox and a
|
|
264
|
+
// the two apart. An owner's access-request inbox and a history list must
|
|
265
265
|
// never invalidate each other.
|
|
266
266
|
const requestsKey: ResourceKey = {
|
|
267
267
|
type: "companyMdAccessRequests",
|
|
@@ -277,7 +277,7 @@ describe("resource-keys: companyMdDocVersion (composite version identity)", () =
|
|
|
277
277
|
// The second composite member. Its identity is (docId, versionId), so it
|
|
278
278
|
// exercises the same four-segment path `commentThreads` opened — and, unlike
|
|
279
279
|
// that key, it has a SIBLING of the same doc scope one segment shorter
|
|
280
|
-
// (
|
|
280
|
+
// (companyMdDocHistory), which is the collision the split exists to avoid.
|
|
281
281
|
const DOC_ID = "44444444-4444-4444-8444-444444444444";
|
|
282
282
|
const VERSION_ID = "66666666-6666-4666-8666-666666666666";
|
|
283
283
|
const key: ResourceKey = {
|
|
@@ -319,12 +319,12 @@ describe("resource-keys: companyMdDocVersion (composite version identity)", () =
|
|
|
319
319
|
expect(matchesResourceKey(toQueryKey(key), other)).toBe(false);
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
-
it("is NOT invalidated by a sweep of the document's
|
|
322
|
+
it("is NOT invalidated by a sweep of the document's history LIST", () => {
|
|
323
323
|
// The whole reason the two members are distinct. A restore appends to the
|
|
324
|
-
// history and so invalidates `
|
|
324
|
+
// history and so invalidates `companyMdDocHistory`; the sealed bodies
|
|
325
325
|
// already fetched are immutable and must survive it.
|
|
326
326
|
const listKey: ResourceKey = {
|
|
327
|
-
type: "
|
|
327
|
+
type: "companyMdDocHistory",
|
|
328
328
|
orgId: ORG_ID,
|
|
329
329
|
docId: DOC_ID,
|
|
330
330
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '544058ec2d0f' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '544058ec2d0ffc4b77cd02a9121088dadc5618d5bc04ed33707298a8a1ce6114' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -1548,15 +1548,15 @@ export interface paths {
|
|
|
1548
1548
|
patch?: never;
|
|
1549
1549
|
trace?: never;
|
|
1550
1550
|
};
|
|
1551
|
-
"/api/company-md/docs/{id}/
|
|
1551
|
+
"/api/company-md/docs/{id}/history": {
|
|
1552
1552
|
parameters: {
|
|
1553
1553
|
query?: never;
|
|
1554
1554
|
header?: never;
|
|
1555
1555
|
path?: never;
|
|
1556
1556
|
cookie?: never;
|
|
1557
1557
|
};
|
|
1558
|
-
/** List a company.md document’s
|
|
1559
|
-
get: operations["
|
|
1558
|
+
/** List a company.md document’s merged history (versions and lifecycle events) */
|
|
1559
|
+
get: operations["listCompanyMdDocHistory"];
|
|
1560
1560
|
put?: never;
|
|
1561
1561
|
post?: never;
|
|
1562
1562
|
delete?: never;
|
|
@@ -3565,6 +3565,23 @@ export interface paths {
|
|
|
3565
3565
|
patch?: never;
|
|
3566
3566
|
trace?: never;
|
|
3567
3567
|
};
|
|
3568
|
+
"/api/comments/threads/{threadId}/anchor-trace": {
|
|
3569
|
+
parameters: {
|
|
3570
|
+
query?: never;
|
|
3571
|
+
header?: never;
|
|
3572
|
+
path?: never;
|
|
3573
|
+
cookie?: never;
|
|
3574
|
+
};
|
|
3575
|
+
get?: never;
|
|
3576
|
+
put?: never;
|
|
3577
|
+
/** Report where a thread's anchored passage used to be */
|
|
3578
|
+
post: operations["stampCommentAnchorTrace"];
|
|
3579
|
+
delete?: never;
|
|
3580
|
+
options?: never;
|
|
3581
|
+
head?: never;
|
|
3582
|
+
patch?: never;
|
|
3583
|
+
trace?: never;
|
|
3584
|
+
};
|
|
3568
3585
|
"/api/comments/threads/{threadId}/accept-intent": {
|
|
3569
3586
|
parameters: {
|
|
3570
3587
|
query?: never;
|
|
@@ -5034,10 +5051,11 @@ export interface components {
|
|
|
5034
5051
|
CompanyMdMentionableResponse: {
|
|
5035
5052
|
items: components["schemas"]["CompanyMdMentionableCandidate"][];
|
|
5036
5053
|
};
|
|
5037
|
-
/** @description A page of a company.md document’s
|
|
5038
|
-
|
|
5054
|
+
/** @description A page of a company.md document’s merged History — versions and lifecycle events under one cursor — plus the opaque token a restore is gated on. */
|
|
5055
|
+
CompanyMdDocHistoryResponse: {
|
|
5039
5056
|
currentRevision: string;
|
|
5040
|
-
|
|
5057
|
+
items: components["schemas"]["CompanyMdDocHistoryItem"][];
|
|
5058
|
+
withheldCategories: ("version" | "source" | "lifecycle" | "access" | "sync")[];
|
|
5041
5059
|
nextCursor: string | null;
|
|
5042
5060
|
};
|
|
5043
5061
|
/** @description One sealed company.md version — its metadata and the body it sealed. */
|
|
@@ -6513,6 +6531,9 @@ export interface components {
|
|
|
6513
6531
|
CommentThreadSummaryResponse: {
|
|
6514
6532
|
thread: components["schemas"]["CommentThreadSummary"];
|
|
6515
6533
|
};
|
|
6534
|
+
AnchorTraceRequest: {
|
|
6535
|
+
lastAnchoredOffset: number;
|
|
6536
|
+
};
|
|
6516
6537
|
/** @description The acceptance lease: the opaque claim token that alone can complete the accept, and how long the lease lives. */
|
|
6517
6538
|
SuggestionClaimResponse: {
|
|
6518
6539
|
claimToken: string;
|
|
@@ -6532,12 +6553,56 @@ export interface components {
|
|
|
6532
6553
|
displayName: string;
|
|
6533
6554
|
avatarUrl: string | null;
|
|
6534
6555
|
};
|
|
6556
|
+
/** @description One item in a document’s merged History: a sealed version (or the in-progress session) nested as `version`, or a lifecycle event. */
|
|
6557
|
+
CompanyMdDocHistoryItem: {
|
|
6558
|
+
/** @constant */
|
|
6559
|
+
kind: "version";
|
|
6560
|
+
mergeId: string | null;
|
|
6561
|
+
occurredAt: string;
|
|
6562
|
+
/** @constant */
|
|
6563
|
+
category: "version";
|
|
6564
|
+
version: components["schemas"]["CompanyMdDocVersionEntry"];
|
|
6565
|
+
} | {
|
|
6566
|
+
/** @constant */
|
|
6567
|
+
kind: "event";
|
|
6568
|
+
mergeId: string;
|
|
6569
|
+
occurredAt: string;
|
|
6570
|
+
/** @enum {string} */
|
|
6571
|
+
category: "version" | "source" | "lifecycle" | "access" | "sync";
|
|
6572
|
+
/** @enum {string} */
|
|
6573
|
+
eventType: "doc_created" | "doc_renamed" | "doc_moved" | "source_added" | "source_attached" | "source_detached" | "source_removed" | "source_extraction_started" | "source_extraction_succeeded" | "source_extraction_failed" | "content_generated" | "external_sync_completed" | "access_granted" | "access_revoked" | "access_updated" | "sharing_policy_changed" | "visibility_changed" | "owner_transferred" | "discoverability_changed";
|
|
6574
|
+
actor: components["schemas"]["CompanyMdDocVersionActor"] | null;
|
|
6575
|
+
subject: string | null;
|
|
6576
|
+
target: components["schemas"]["CompanyMdDocHistoryTarget"] | null;
|
|
6577
|
+
previousValue: string | null;
|
|
6578
|
+
newValue: string | null;
|
|
6579
|
+
correlationId: string | null;
|
|
6580
|
+
};
|
|
6535
6581
|
/** @description The singular actor a version is attributed to, in product vocabulary. */
|
|
6536
6582
|
CompanyMdDocVersionActor: {
|
|
6537
6583
|
/** @enum {string} */
|
|
6538
6584
|
type: "member" | "agent" | "system";
|
|
6539
6585
|
userId: string;
|
|
6540
6586
|
};
|
|
6587
|
+
/** @description What a history event acted on: an ACL principal (member, unit, or the whole org) or the extracted source artifact. */
|
|
6588
|
+
CompanyMdDocHistoryTarget: {
|
|
6589
|
+
/** @constant */
|
|
6590
|
+
type: "member";
|
|
6591
|
+
userId: string;
|
|
6592
|
+
} | {
|
|
6593
|
+
/** @constant */
|
|
6594
|
+
type: "unit";
|
|
6595
|
+
unitId: string;
|
|
6596
|
+
} | {
|
|
6597
|
+
/** @constant */
|
|
6598
|
+
type: "org";
|
|
6599
|
+
} | {
|
|
6600
|
+
/** @constant */
|
|
6601
|
+
type: "source";
|
|
6602
|
+
filename: string | null;
|
|
6603
|
+
mime: string;
|
|
6604
|
+
byteSize: number;
|
|
6605
|
+
};
|
|
6541
6606
|
/** @description An ACL-admitted context-doc discovery hit. */
|
|
6542
6607
|
ContextDocDiscoveryHit: {
|
|
6543
6608
|
id: string;
|
|
@@ -6568,6 +6633,7 @@ export interface components {
|
|
|
6568
6633
|
createdByUserId: string | null;
|
|
6569
6634
|
resolvedByUserId: string | null;
|
|
6570
6635
|
resolvedAt: string | null;
|
|
6636
|
+
lastAnchoredOffset: number | null;
|
|
6571
6637
|
createdAt: string;
|
|
6572
6638
|
updatedAt: string;
|
|
6573
6639
|
comments: components["schemas"]["Comment"][];
|
|
@@ -6642,6 +6708,7 @@ export interface components {
|
|
|
6642
6708
|
createdByUserId: string | null;
|
|
6643
6709
|
resolvedByUserId: string | null;
|
|
6644
6710
|
resolvedAt: string | null;
|
|
6711
|
+
lastAnchoredOffset: number | null;
|
|
6645
6712
|
createdAt: string;
|
|
6646
6713
|
updatedAt: string;
|
|
6647
6714
|
};
|
|
@@ -9079,7 +9146,7 @@ export interface operations {
|
|
|
9079
9146
|
};
|
|
9080
9147
|
};
|
|
9081
9148
|
};
|
|
9082
|
-
|
|
9149
|
+
listCompanyMdDocHistory: {
|
|
9083
9150
|
parameters: {
|
|
9084
9151
|
query?: {
|
|
9085
9152
|
cursor?: string;
|
|
@@ -9093,13 +9160,13 @@ export interface operations {
|
|
|
9093
9160
|
};
|
|
9094
9161
|
requestBody?: never;
|
|
9095
9162
|
responses: {
|
|
9096
|
-
/** @description A page of
|
|
9163
|
+
/** @description A page of merged history items, plus the document’s live revision token and the categories withheld for this reader */
|
|
9097
9164
|
200: {
|
|
9098
9165
|
headers: {
|
|
9099
9166
|
[name: string]: unknown;
|
|
9100
9167
|
};
|
|
9101
9168
|
content: {
|
|
9102
|
-
"application/json": components["schemas"]["
|
|
9169
|
+
"application/json": components["schemas"]["CompanyMdDocHistoryResponse"];
|
|
9103
9170
|
};
|
|
9104
9171
|
};
|
|
9105
9172
|
/** @description Invalid cursor or limit */
|
|
@@ -12789,6 +12856,46 @@ export interface operations {
|
|
|
12789
12856
|
};
|
|
12790
12857
|
};
|
|
12791
12858
|
};
|
|
12859
|
+
stampCommentAnchorTrace: {
|
|
12860
|
+
parameters: {
|
|
12861
|
+
query?: never;
|
|
12862
|
+
header?: never;
|
|
12863
|
+
path: {
|
|
12864
|
+
threadId: string;
|
|
12865
|
+
};
|
|
12866
|
+
cookie?: never;
|
|
12867
|
+
};
|
|
12868
|
+
requestBody: {
|
|
12869
|
+
content: {
|
|
12870
|
+
"application/json": components["schemas"]["AnchorTraceRequest"];
|
|
12871
|
+
};
|
|
12872
|
+
};
|
|
12873
|
+
responses: {
|
|
12874
|
+
/** @description Offset recorded, or the already-recorded thread returned unchanged (first write wins) */
|
|
12875
|
+
200: {
|
|
12876
|
+
headers: {
|
|
12877
|
+
[name: string]: unknown;
|
|
12878
|
+
};
|
|
12879
|
+
content: {
|
|
12880
|
+
"application/json": components["schemas"]["CommentThreadSummaryResponse"];
|
|
12881
|
+
};
|
|
12882
|
+
};
|
|
12883
|
+
/** @description Invalid body */
|
|
12884
|
+
400: {
|
|
12885
|
+
headers: {
|
|
12886
|
+
[name: string]: unknown;
|
|
12887
|
+
};
|
|
12888
|
+
content?: never;
|
|
12889
|
+
};
|
|
12890
|
+
/** @description Thread not found, or the caller may not comment on its subject */
|
|
12891
|
+
404: {
|
|
12892
|
+
headers: {
|
|
12893
|
+
[name: string]: unknown;
|
|
12894
|
+
};
|
|
12895
|
+
content?: never;
|
|
12896
|
+
};
|
|
12897
|
+
};
|
|
12898
|
+
};
|
|
12792
12899
|
acceptSuggestionIntent: {
|
|
12793
12900
|
parameters: {
|
|
12794
12901
|
query?: never;
|
|
@@ -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("
|
|
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
|
package/src/comments/schemas.ts
CHANGED
|
@@ -110,11 +110,14 @@ export type CommentMention = z.infer<typeof CommentMentionSchema>;
|
|
|
110
110
|
* One comment as the app renders it.
|
|
111
111
|
*
|
|
112
112
|
* THE REDACTION INVARIANT: `body` is null EXACTLY WHEN `deletedAt` is set, and a
|
|
113
|
-
* soft-deleted comment also arrives with `mentions` empty.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
113
|
+
* soft-deleted comment also arrives with `mentions` empty.
|
|
114
|
+
*
|
|
115
|
+
* WHERE A REDACTED COMMENT ACTUALLY APPEARS is narrower than it once was, and
|
|
116
|
+
* this docblock used to overstate it. The redacted projection is the DELETE
|
|
117
|
+
* acknowledgement — the response to the caller who performed the delete, telling
|
|
118
|
+
* them what they destroyed. It is NOT a row in a thread listing: ADR-BE-545 has
|
|
119
|
+
* the read omit deleted comments outright, and drop a comment-kind thread once
|
|
120
|
+
* nothing live is left on it. A reader never sees a tombstone in a conversation.
|
|
118
121
|
*
|
|
119
122
|
* The invariant is DOCUMENTED here and enforced by the writer, not by a Zod
|
|
120
123
|
* refinement. A refinement would make this package refuse a response the server
|
|
@@ -166,6 +169,18 @@ export type CommentProjection = z.infer<typeof CommentSchema>;
|
|
|
166
169
|
* would make this package refuse a response the server is willing to emit and
|
|
167
170
|
* would be a third wire description no parity guard can check.
|
|
168
171
|
*
|
|
172
|
+
* `lastAnchoredOffset` IS NOT AN ANCHOR. It is a source offset recording where
|
|
173
|
+
* this thread's passage USED TO BE, reported by the client that watched the
|
|
174
|
+
* passage disappear (ADR-BE-548). It exists so a client can keep an ORPHANED
|
|
175
|
+
* thread in the reader's list where the conversation happened, instead of piling
|
|
176
|
+
* every one of them at the top — and it may be used for NOTHING ELSE. It must
|
|
177
|
+
* never yield a range, and nothing may place a highlight, a decoration or a
|
|
178
|
+
* leader line from it: a comment shown against the wrong sentence is strictly
|
|
179
|
+
* worse than one shown as unanchored (ADR-APP-120), and that ordering is what
|
|
180
|
+
* bounds this field. `null` is ordinary — it means no client has watched this
|
|
181
|
+
* thread's passage go — and the value is WRITE-ONCE on the server, so it records
|
|
182
|
+
* the first sighting and never a correction.
|
|
183
|
+
*
|
|
169
184
|
* THE TWO-LEVEL STATUS MODEL: `status` stays exactly `open | resolved`
|
|
170
185
|
* (`COMMENT_THREAD_STATUSES` is NOT widened — widening a closed published enum
|
|
171
186
|
* is a parse outage for every deployed client). A decided suggestion arrives as
|
|
@@ -185,6 +200,7 @@ export const CommentThreadSummarySchema = z.object({
|
|
|
185
200
|
createdByUserId: z.string().nullable(),
|
|
186
201
|
resolvedByUserId: z.string().nullable(),
|
|
187
202
|
resolvedAt: z.string().nullable(),
|
|
203
|
+
lastAnchoredOffset: z.number().int().nullable(),
|
|
188
204
|
createdAt: z.string(),
|
|
189
205
|
updatedAt: z.string(),
|
|
190
206
|
});
|
|
@@ -193,9 +209,12 @@ export type CommentThreadSummary = z.infer<typeof CommentThreadSummarySchema>;
|
|
|
193
209
|
/**
|
|
194
210
|
* A thread with ALL its comments, oldest-first by (createdAt ASC, id ASC).
|
|
195
211
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* comment
|
|
212
|
+
* DELETED COMMENTS ARE OMITTED, NOT REDACTED. This once said the opposite, and
|
|
213
|
+
* the prose outlived the rule: ADR-BE-545 made the read drop deleted comments
|
|
214
|
+
* entirely, and drop a comment-kind thread once nothing live is left on it —
|
|
215
|
+
* deleting the comment that IS a thread deletes the thread with it, so the reply
|
|
216
|
+
* order a redacted row used to hold open no longer has anything to hold. The
|
|
217
|
+
* wire SHAPE is unchanged either way; what changed is which rows arrive.
|
|
199
218
|
*/
|
|
200
219
|
export const CommentThreadSchema = CommentThreadSummarySchema.extend({
|
|
201
220
|
comments: z.array(CommentSchema),
|
|
@@ -26,6 +26,7 @@ export const openApiRoutes = {
|
|
|
26
26
|
'/api/comments/threads/{threadId}/accept-complete': ['POST'],
|
|
27
27
|
'/api/comments/threads/{threadId}/accept-intent': ['POST'],
|
|
28
28
|
'/api/comments/threads/{threadId}/accept-release': ['POST'],
|
|
29
|
+
'/api/comments/threads/{threadId}/anchor-trace': ['POST'],
|
|
29
30
|
'/api/comments/threads/{threadId}/comments': ['POST'],
|
|
30
31
|
'/api/comments/threads/{threadId}/reject': ['POST'],
|
|
31
32
|
'/api/comments/threads/{threadId}/reopen': ['POST'],
|
|
@@ -50,6 +51,7 @@ export const openApiRoutes = {
|
|
|
50
51
|
'/api/company-md/docs/{id}/context-bank/upload': ['POST'],
|
|
51
52
|
'/api/company-md/docs/{id}/context-bank/{contextDocId}': ['DELETE'],
|
|
52
53
|
'/api/company-md/docs/{id}/context-bank/{contextDocId}/order': ['PATCH'],
|
|
54
|
+
'/api/company-md/docs/{id}/history': ['GET'],
|
|
53
55
|
'/api/company-md/docs/{id}/mentionable': ['GET'],
|
|
54
56
|
'/api/company-md/docs/{id}/sharing': ['GET'],
|
|
55
57
|
'/api/company-md/docs/{id}/sharing/acl': ['POST'],
|
|
@@ -57,7 +59,6 @@ export const openApiRoutes = {
|
|
|
57
59
|
'/api/company-md/docs/{id}/sharing/policy': ['PUT'],
|
|
58
60
|
'/api/company-md/docs/{id}/title': ['PUT'],
|
|
59
61
|
'/api/company-md/docs/{id}/transfer-owner': ['POST'],
|
|
60
|
-
'/api/company-md/docs/{id}/versions': ['GET'],
|
|
61
62
|
'/api/company-md/docs/{id}/versions/{versionId}': ['GET'],
|
|
62
63
|
'/api/company-md/docs/{id}/versions/{versionId}/restore': ['POST'],
|
|
63
64
|
'/api/company-md/extract': ['POST'],
|
|
@@ -38,16 +38,19 @@ export type ResourceKey =
|
|
|
38
38
|
// a bare slug does not identify a document. That field name is a wart to be
|
|
39
39
|
// contained, not propagated — a new key gets the honest name.
|
|
40
40
|
| { type: "companyMdAccessRequests"; orgId: string; docId: string }
|
|
41
|
-
// The
|
|
42
|
-
// list the history panel pages through.
|
|
43
|
-
// `
|
|
44
|
-
//
|
|
45
|
-
|
|
41
|
+
// The merged history feed of ONE document (PRD-00938) — sealed versions plus
|
|
42
|
+
// lifecycle events, the metadata-only list the history panel pages through.
|
|
43
|
+
// Named `companyMdDocHistory`, not `companyMdDocVersions`: the history is the
|
|
44
|
+
// list, a version is a thing in it (see `companyMdDocVersion` below). Keyed by
|
|
45
|
+
// `docId` for the same reason `companyMdAccessRequests` is: the honest name
|
|
46
|
+
// for the stable document id, rather than propagating `companyMdDoc`'s legacy
|
|
47
|
+
// `slug` field name.
|
|
48
|
+
| { type: "companyMdDocHistory"; orgId: string; docId: string }
|
|
46
49
|
// ONE sealed version, body included.
|
|
47
50
|
//
|
|
48
51
|
// A member of its own rather than a `versionId` segment folded into the list
|
|
49
52
|
// key above, and the split is load-bearing. A restore APPENDS to the history,
|
|
50
|
-
// so it must invalidate `
|
|
53
|
+
// so it must invalidate `companyMdDocHistory` — and because a sealed version
|
|
51
54
|
// is immutable, every already-fetched body must SURVIVE that invalidation.
|
|
52
55
|
// Fold the two together and each restore evicts the whole set of historical
|
|
53
56
|
// bodies the reader just paged through, to re-fetch content that cannot have
|
package/src/resource-keys.ts
CHANGED
|
@@ -96,7 +96,7 @@ const IDENTITY_FIELDS = {
|
|
|
96
96
|
companyMdDoc: "slug",
|
|
97
97
|
companyMdContextBank: "slug",
|
|
98
98
|
companyMdAccessRequests: "docId",
|
|
99
|
-
|
|
99
|
+
companyMdDocHistory: "docId",
|
|
100
100
|
orgUnit: "unitId",
|
|
101
101
|
orgUnitChildren: "unitId",
|
|
102
102
|
orgUnitAncestors: "unitId",
|
|
@@ -226,7 +226,7 @@ export function toQueryKey(key: ResourceKey): readonly string[] {
|
|
|
226
226
|
case "companyMdContextBank":
|
|
227
227
|
return [key.type, key.orgId, key.slug] as const;
|
|
228
228
|
case "companyMdAccessRequests":
|
|
229
|
-
case "
|
|
229
|
+
case "companyMdDocHistory":
|
|
230
230
|
return [key.type, key.orgId, key.docId] as const;
|
|
231
231
|
|
|
232
232
|
// Composite identity — subject CLASS then subject id, in that order, so the
|
package/src/resource-registry.ts
CHANGED
|
@@ -60,7 +60,7 @@ export type ResourceEntryInput = z.input<typeof ResourceEntrySchema>;
|
|
|
60
60
|
*
|
|
61
61
|
* TWO VOCABULARY CAVEATS, both deliberate and both for ADR-CONTRACTS-120:
|
|
62
62
|
*
|
|
63
|
-
* 1. `resource` here is the {@link ResourceKey} type TAG (`
|
|
63
|
+
* 1. `resource` here is the {@link ResourceKey} type TAG (`companyMdDocHistory`).
|
|
64
64
|
* The registry that currently holds live rows is the app's copy, and its
|
|
65
65
|
* `resource` values are PATH-shaped (`company-md/docs`, `orgs/:orgId/billing`).
|
|
66
66
|
* The two do not line up, and reconciling them is not this PRD's work —
|
|
@@ -73,8 +73,9 @@ export const COMPANY_MD_VERSION_RESOURCE_ENTRIES = [
|
|
|
73
73
|
{
|
|
74
74
|
// The history panel is opened, never hydrated with the page — hence
|
|
75
75
|
// background/P3. A restore appends to this list, so it is the row that
|
|
76
|
-
// takes the invalidation.
|
|
77
|
-
|
|
76
|
+
// takes the invalidation. The history is the list, a version is a thing
|
|
77
|
+
// in it — hence `companyMdDocHistory` here and `companyMdDocVersion` below.
|
|
78
|
+
resource: "companyMdDocHistory",
|
|
78
79
|
priority: "P3",
|
|
79
80
|
hydrationPhase: "background",
|
|
80
81
|
hydrationDepends: [],
|
|
@@ -96,7 +97,7 @@ export const COMPANY_MD_VERSION_RESOURCE_ENTRIES = [
|
|
|
96
97
|
resource: "companyMdDocVersion",
|
|
97
98
|
priority: "P3",
|
|
98
99
|
hydrationPhase: "background",
|
|
99
|
-
hydrationDepends: ["
|
|
100
|
+
hydrationDepends: ["companyMdDocHistory"],
|
|
100
101
|
mutationBehavior: "serial",
|
|
101
102
|
staleTimeMs: 300_000,
|
|
102
103
|
degradationSection: "core",
|