@company-semantics/contracts 62.3.0 → 62.4.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/package.json +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +1 -0
- package/src/chat/README.md +2 -0
- package/src/chat/__tests__/compaction-boundary.test.ts +121 -0
- package/src/chat/schemas.ts +20 -0
- package/src/index.ts +3 -0
- package/src/message-parts/README.md +3 -0
- package/src/message-parts/__tests__/structure-review.test.ts +34 -0
- package/src/message-parts/index.ts +7 -0
- package/src/message-parts/structure-review.ts +64 -0
- package/src/message-parts/types.ts +2 -0
- package/src/message-parts/wire.ts +26 -0
package/package.json
CHANGED
|
@@ -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 = 'bd8b060aedb2' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'bd8b060aedb2cd35af45ee8c8b0e1dc97da92305f985757cd19a57fbd44866e3' as const;
|
package/src/api/generated.ts
CHANGED
package/src/chat/README.md
CHANGED
|
@@ -22,6 +22,8 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
|
|
|
22
22
|
- Page context carries IDENTIFIERS ONLY. Content reaches the model through tools, which apply resource-native read authority per the backend ADR slug `retrieval-source-authority-contract`; a body injected here would bypass the source's own check for a reader who may not be allowed to see it. There is deliberately no display-name field either — where a human-readable name is wanted, the server resolves it from the id through the owning source (ADR-CONTRACTS-153)
|
|
23
23
|
- Page context carries NO FREE-FORM TEXT. Its values are caller-controlled — they arrive in an HTTP request body — and the server renders them into a `{role:"system"}` message it authored. `.strict()` rejects unknown KEYS and says nothing about adversarial VALUES, so every field is an enum, a bounded pattern, or an opaque id. The channel carries operator authority; the values in it do not inherit it
|
|
24
24
|
- Page context carries NO ORG IDENTITY, though the app's org routes have one in the URL. Org scope is derived from the session; a request-supplied org identifier would be an authority claim in a field the server otherwise reads for itself
|
|
25
|
+
- `compactedThroughSequence` on the chat detail is ABSENT or NULL exactly when the conversation has never been compacted. It is never coerced to `0` — sequence numbers are zero-indexed, so `0` is a boundary a chat can really have, and a zero default would make "never compacted" indistinguishable from "compacted through sequence 0". It is optional for the same reason `proactiveKind` is a bare string: a client that does not know the field renders the transcript exactly as before (ADR-CONTRACTS-154)
|
|
26
|
+
- `compactedThroughSequence` is a BOUNDARY MARKER, not a context gauge. It is the one sequence number the summary covers through — never a token count, never a percentage of the context window, and never the summary text itself. The reader's question it answers is "why did it forget the beginning", not "how full is the window" (ADR-CONTRACTS-154)
|
|
25
27
|
- `CHAT_PAGE_VIEWS` and `CHAT_PAGE_SCOPES` are a projection of the app's `Route` union (`company-semantics-app/src/platform/route-parser.ts`), not a parallel vocabulary. A view the parser can emit but the schema rejects fails the send outright rather than degrading
|
|
26
28
|
|
|
27
29
|
<!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ChatDetail.compactedThroughSequence` — a boundary marker, not a gauge.
|
|
3
|
+
*
|
|
4
|
+
* INVARIANTS TESTED:
|
|
5
|
+
* - ABSENT or NULL means the conversation has never been compacted. Both
|
|
6
|
+
* spellings are accepted and both parse to "no boundary": a serializer may
|
|
7
|
+
* omit the key or send an explicit null, and neither may become `0`.
|
|
8
|
+
* - `0` is a REAL boundary, distinguishable from "never compacted". Sequence
|
|
9
|
+
* numbers are zero-indexed, so a chat compacted through its first message
|
|
10
|
+
* carries `0` — which is exactly why the field is nullable rather than
|
|
11
|
+
* defaulted (ADR-CONTRACTS-154).
|
|
12
|
+
* - The field is OPTIONAL, so a detail produced by an older backend still
|
|
13
|
+
* parses and a client that ignores it renders the transcript as before.
|
|
14
|
+
* - It is an INTEGER sequence number and nothing else. A fractional value is
|
|
15
|
+
* refused, and no token-count / percentage sibling exists to be read instead.
|
|
16
|
+
*
|
|
17
|
+
* Why this is asserted on the schema rather than left to the serializer:
|
|
18
|
+
* the difference between `null` and `0` is invisible at a glance and fails
|
|
19
|
+
* silently in the direction that looks correct — every never-compacted chat
|
|
20
|
+
* grows a marker. A default of `0` anywhere on this path would be caught by
|
|
21
|
+
* nothing else, so the wire shape states the distinction itself.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
import { ChatDetailSchema, GetChatResponseSchema } from "../schemas.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Fixtures
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** A chat detail with every REQUIRED field and no compaction boundary. */
|
|
31
|
+
const BASE_DETAIL = {
|
|
32
|
+
id: "chat_1",
|
|
33
|
+
title: "A long conversation",
|
|
34
|
+
interactionId: "int_1",
|
|
35
|
+
createdAt: "2026-08-31T00:00:00.000Z",
|
|
36
|
+
updatedAt: "2026-08-31T01:00:00.000Z",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("ChatDetail compaction boundary", () => {
|
|
40
|
+
it("an absent compaction boundary means never compacted", () => {
|
|
41
|
+
const parsed = ChatDetailSchema.parse(BASE_DETAIL);
|
|
42
|
+
|
|
43
|
+
// Absent, not zero. A `0` here would be indistinguishable from a chat
|
|
44
|
+
// compacted through sequence 0, which is a state that really exists.
|
|
45
|
+
expect(parsed.compactedThroughSequence).toBeUndefined();
|
|
46
|
+
expect(parsed.compactedThroughSequence ?? null).toBeNull();
|
|
47
|
+
expect(parsed.compactedThroughSequence).not.toBe(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("an explicit null means never compacted too", () => {
|
|
51
|
+
const parsed = ChatDetailSchema.parse({
|
|
52
|
+
...BASE_DETAIL,
|
|
53
|
+
compactedThroughSequence: null,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(parsed.compactedThroughSequence).toBeNull();
|
|
57
|
+
expect(parsed.compactedThroughSequence ?? null).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("sequence zero is a real boundary, not an absence", () => {
|
|
61
|
+
const parsed = ChatDetailSchema.parse({
|
|
62
|
+
...BASE_DETAIL,
|
|
63
|
+
compactedThroughSequence: 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(parsed.compactedThroughSequence).toBe(0);
|
|
67
|
+
// The distinction the nullability exists to preserve.
|
|
68
|
+
expect(parsed.compactedThroughSequence).not.toBeNull();
|
|
69
|
+
expect(parsed.compactedThroughSequence).not.toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("carries the boundary sequence number when the chat has been compacted", () => {
|
|
73
|
+
const parsed = ChatDetailSchema.parse({
|
|
74
|
+
...BASE_DETAIL,
|
|
75
|
+
compactedThroughSequence: 42,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(parsed.compactedThroughSequence).toBe(42);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("is an integer sequence number, never a fraction", () => {
|
|
82
|
+
expect(
|
|
83
|
+
ChatDetailSchema.safeParse({
|
|
84
|
+
...BASE_DETAIL,
|
|
85
|
+
compactedThroughSequence: 1.5,
|
|
86
|
+
}).success,
|
|
87
|
+
).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("is a boundary marker, not a context gauge", () => {
|
|
91
|
+
const shape = Object.keys(ChatDetailSchema.shape);
|
|
92
|
+
|
|
93
|
+
// The published fact is one sequence number. No token count, no percentage
|
|
94
|
+
// of the context window, and not the summary text (ADR-CONTRACTS-154).
|
|
95
|
+
expect(shape).toContain("compactedThroughSequence");
|
|
96
|
+
for (const gauge of [
|
|
97
|
+
"contextTokens",
|
|
98
|
+
"contextUsage",
|
|
99
|
+
"contextPercent",
|
|
100
|
+
"tokenCount",
|
|
101
|
+
"contextSummary",
|
|
102
|
+
]) {
|
|
103
|
+
expect(shape).not.toContain(gauge);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("a detail from an older backend still parses", () => {
|
|
108
|
+
// The field is optional precisely so this stays true: a client that does
|
|
109
|
+
// not know it renders the transcript exactly as it did before.
|
|
110
|
+
expect(ChatDetailSchema.safeParse(BASE_DETAIL).success).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rides the chat detail in GET /api/chats/:id", () => {
|
|
114
|
+
const parsed = GetChatResponseSchema.parse({
|
|
115
|
+
chat: { ...BASE_DETAIL, compactedThroughSequence: 7 },
|
|
116
|
+
messages: [],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(parsed.chat.compactedThroughSequence).toBe(7);
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/chat/schemas.ts
CHANGED
|
@@ -335,6 +335,26 @@ export const ChatDetailSchema = z.object({
|
|
|
335
335
|
// a list concern (the sidebar row) and does not ride the detail.
|
|
336
336
|
origin: proactiveChatFields.origin,
|
|
337
337
|
unread: proactiveChatFields.unread,
|
|
338
|
+
/**
|
|
339
|
+
* The message sequence number the conversation's compacted summary covers
|
|
340
|
+
* THROUGH — a boundary marker, not a context gauge.
|
|
341
|
+
*
|
|
342
|
+
* ABSENT or NULL means the conversation has never been compacted. The
|
|
343
|
+
* nullability is load-bearing and must survive to the wire: a zero default
|
|
344
|
+
* would make "never compacted" indistinguishable from "compacted through
|
|
345
|
+
* sequence 0", and sequence numbers are zero-indexed, so 0 is a real
|
|
346
|
+
* boundary a chat can actually have.
|
|
347
|
+
*
|
|
348
|
+
* OPTIONAL for the same forward-compatibility reason `proactiveKind` is a
|
|
349
|
+
* bare string: a client that does not know this field renders the transcript
|
|
350
|
+
* exactly as it did before, rather than rejecting the whole detail.
|
|
351
|
+
*
|
|
352
|
+
* Deliberately the boundary and NOTHING else — no token count, no
|
|
353
|
+
* percentage-of-window, and not the summary text. The reader's question is
|
|
354
|
+
* why the model forgot the beginning, not how full the window is
|
|
355
|
+
* (ADR-CONTRACTS-154).
|
|
356
|
+
*/
|
|
357
|
+
compactedThroughSequence: z.number().int().nullable().optional(),
|
|
338
358
|
});
|
|
339
359
|
|
|
340
360
|
/** Response for GET /api/chats/:id */
|
package/src/index.ts
CHANGED
|
@@ -1024,6 +1024,9 @@ export type {
|
|
|
1024
1024
|
InteractiveTaskData,
|
|
1025
1025
|
InteractiveTaskPart,
|
|
1026
1026
|
InteractiveTaskDataPart,
|
|
1027
|
+
StructureReviewData,
|
|
1028
|
+
StructureReviewPart,
|
|
1029
|
+
StructureReviewDataPart,
|
|
1027
1030
|
// Suggested replies surface types (non-governed chips, PRD-00958)
|
|
1028
1031
|
SuggestedReply,
|
|
1029
1032
|
SuggestedRepliesData,
|
|
@@ -66,6 +66,9 @@ Canonical vocabulary for structured assistant message output. Defines the type s
|
|
|
66
66
|
- `StatusPanelEntry` _(type)_ — Single entry in a status panel.
|
|
67
67
|
- `StatusPanelPart` _(type)_ — Status panel surface part.
|
|
68
68
|
- `StreamPhase` _(type)_ — Derived stream phase for UI state management.
|
|
69
|
+
- `StructureReviewData` _(type)_ — Structure review surface data payload — the anchor's handle and its intro-coherence counts.
|
|
70
|
+
- `StructureReviewDataPart` _(type)_ — Structure review data part (wire format).
|
|
71
|
+
- `StructureReviewPart` _(type)_ — Structure review message part (semantic type).
|
|
69
72
|
- `SuggestedRepliesData` _(type)_ — Suggested replies surface data payload.
|
|
70
73
|
- `SuggestedRepliesDataPart` _(type)_ — Wire form, as persisted in `chat_messages.parts`.
|
|
71
74
|
- `SuggestedRepliesPart` _(type)_ — Normalized form, as rendered.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { WireSurfaceBuilder } from "../wire";
|
|
3
|
+
import type { StructureReviewData } from "../structure-review";
|
|
4
|
+
|
|
5
|
+
describe("WireSurfaceBuilder.structureReview", () => {
|
|
6
|
+
const data: StructureReviewData = {
|
|
7
|
+
proposalId: "42202e9f-0000-0000-0000-000000000000",
|
|
8
|
+
stepCount: 4,
|
|
9
|
+
withheldCount: 5,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
it("returns data part with type data-structure-review", () => {
|
|
13
|
+
const result = WireSurfaceBuilder.structureReview(data);
|
|
14
|
+
expect(result.type).toBe("data-structure-review");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("data matches input StructureReviewData exactly", () => {
|
|
18
|
+
const result = WireSurfaceBuilder.structureReview(data);
|
|
19
|
+
expect(result.data).toEqual(data);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// The part is an ANCHOR: a handle plus intro-coherence counts and nothing
|
|
23
|
+
// else. Pinning the key set catches the two documented regressions — a
|
|
24
|
+
// mutation precondition (expectedInputHash/expectedStructureRevision) or an
|
|
25
|
+
// endpoint creeping onto a durably persisted, share-visible payload.
|
|
26
|
+
it("carries only the anchor fields (no hashes, no endpoints, no cards)", () => {
|
|
27
|
+
const result = WireSurfaceBuilder.structureReview(data);
|
|
28
|
+
expect(Object.keys(result.data).sort()).toEqual([
|
|
29
|
+
"proposalId",
|
|
30
|
+
"stepCount",
|
|
31
|
+
"withheldCount",
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -56,6 +56,13 @@ export type {
|
|
|
56
56
|
InteractiveTaskDataPart,
|
|
57
57
|
} from "./interactive";
|
|
58
58
|
|
|
59
|
+
// Structure review surface types (governed anchor; sibling of interactive)
|
|
60
|
+
export type {
|
|
61
|
+
StructureReviewData,
|
|
62
|
+
StructureReviewPart,
|
|
63
|
+
StructureReviewDataPart,
|
|
64
|
+
} from "./structure-review";
|
|
65
|
+
|
|
59
66
|
// Suggested replies surface types (non-governed chips)
|
|
60
67
|
export type {
|
|
61
68
|
SuggestedReply,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structure Review Surface Types
|
|
3
|
+
*
|
|
4
|
+
* The chat anchor for the org-structure review stepper: analysis completes,
|
|
5
|
+
* the assistant introduces what it found, and this part is where the
|
|
6
|
+
* interactive review lives. It is a SIBLING of `preview` / `confirmation` /
|
|
7
|
+
* `interactive` — governed (it counts against the at-most-one-governed-surface
|
|
8
|
+
* -per-turn rule) but deliberately NOT an InteractiveTaskKind, because its
|
|
9
|
+
* submit is a domain apply with its own receipt and concurrency fences, not a
|
|
10
|
+
* governance-engine execution.
|
|
11
|
+
*
|
|
12
|
+
* STRUCTURE REVIEW INVARIANTS:
|
|
13
|
+
* - The part is an ANCHOR, not the stepper. It carries a handle; the surface
|
|
14
|
+
* is assembled client-side from a live, authorized proposal read. Never
|
|
15
|
+
* inline the presented cards here: chat parts are durably persisted and
|
|
16
|
+
* reach share viewers verbatim, while presentations are recomputed at read
|
|
17
|
+
* time and scope-gated.
|
|
18
|
+
* - The durable part contains NO mutation preconditions. `expectedInputHash`
|
|
19
|
+
* and `expectedStructureRevision` always come from the same live read that
|
|
20
|
+
* produced the cards being acted on — never from this part.
|
|
21
|
+
* - No endpoints. The consuming app's domain API module owns them; an
|
|
22
|
+
* `applyEndpoint` here would imply generic-surface semantics this part does
|
|
23
|
+
* not have (its submit is a domain route, not a governance submit).
|
|
24
|
+
* - `stepCount` / `withheldCount` are intro-coherence hints fixed at emission
|
|
25
|
+
* time: a differing live count means "the decision count changed since the
|
|
26
|
+
* introduction" (a display notice), while a differing proposalId means the
|
|
27
|
+
* review was superseded. They are counts, and cannot detect N questions
|
|
28
|
+
* becoming N different questions — the surface's questionId-keyed
|
|
29
|
+
* reconciliation handles that.
|
|
30
|
+
* - Emission is orchestration-driven (a typed launch), never a model choice.
|
|
31
|
+
*
|
|
32
|
+
* @see the contracts ADR for this surface for design rationale
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Structure review surface data payload — the anchor's handle and its
|
|
37
|
+
* intro-coherence counts. All fields required: the counts are non-sensitive
|
|
38
|
+
* and fixed at emission time, and the coherence check depends on them.
|
|
39
|
+
*/
|
|
40
|
+
export interface StructureReviewData {
|
|
41
|
+
/** The org-structure proposal this review addresses. */
|
|
42
|
+
proposalId: string;
|
|
43
|
+
/** Answerable questions at emit time (defective and withheld items excluded). */
|
|
44
|
+
stepCount: number;
|
|
45
|
+
/** Questions withheld or defective at emit time — disclosed, never hidden. */
|
|
46
|
+
withheldCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Structure review message part (semantic type).
|
|
51
|
+
*/
|
|
52
|
+
export interface StructureReviewPart {
|
|
53
|
+
type: "structure-review";
|
|
54
|
+
data: StructureReviewData;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Structure review data part (wire format).
|
|
59
|
+
* Uses AI SDK's data-{name} convention.
|
|
60
|
+
*/
|
|
61
|
+
export interface StructureReviewDataPart {
|
|
62
|
+
type: "data-structure-review";
|
|
63
|
+
data: StructureReviewData;
|
|
64
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { ToolListMessagePart } from "../mcp/index";
|
|
|
17
17
|
import type { PreviewPart } from "./preview";
|
|
18
18
|
import type { ConfirmationPart } from "./confirmation";
|
|
19
19
|
import type { InteractiveTaskPart } from "./interactive";
|
|
20
|
+
import type { StructureReviewPart } from "./structure-review";
|
|
20
21
|
import type { SuggestedRepliesPart } from "./suggested-replies";
|
|
21
22
|
|
|
22
23
|
// =============================================================================
|
|
@@ -126,6 +127,7 @@ export type SurfacePart =
|
|
|
126
127
|
| ConfirmationPart
|
|
127
128
|
| PreviewPart
|
|
128
129
|
| InteractiveTaskPart
|
|
130
|
+
| StructureReviewPart
|
|
129
131
|
| SuggestedRepliesPart;
|
|
130
132
|
|
|
131
133
|
/**
|
|
@@ -17,6 +17,10 @@ import type {
|
|
|
17
17
|
InteractiveTaskData,
|
|
18
18
|
InteractiveTaskDataPart,
|
|
19
19
|
} from "./interactive";
|
|
20
|
+
import type {
|
|
21
|
+
StructureReviewData,
|
|
22
|
+
StructureReviewDataPart,
|
|
23
|
+
} from "./structure-review";
|
|
20
24
|
import type {
|
|
21
25
|
SuggestedRepliesData,
|
|
22
26
|
SuggestedRepliesDataPart,
|
|
@@ -116,6 +120,28 @@ export const WireSurfaceBuilder = {
|
|
|
116
120
|
};
|
|
117
121
|
},
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Build a structure-review data part for streaming.
|
|
125
|
+
* The anchor for the org-structure review stepper: a handle plus
|
|
126
|
+
* intro-coherence counts, assembled into a surface client-side from a live
|
|
127
|
+
* authorized proposal read.
|
|
128
|
+
*
|
|
129
|
+
* INVARIANTS:
|
|
130
|
+
* - Governed: counts against the at-most-one-governed-surface-per-turn rule
|
|
131
|
+
* - Anchor only: never inline presented cards, endpoints, or staleness
|
|
132
|
+
* hashes (mutation preconditions come from the live read)
|
|
133
|
+
* - Emission is orchestration-driven (a typed launch), never a model choice
|
|
134
|
+
*
|
|
135
|
+
* @param data - Structure review data (proposalId + counts)
|
|
136
|
+
* @returns Wire-format structure review part ready for stream
|
|
137
|
+
*/
|
|
138
|
+
structureReview(data: StructureReviewData): StructureReviewDataPart {
|
|
139
|
+
return {
|
|
140
|
+
type: "data-structure-review",
|
|
141
|
+
data,
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
|
|
119
145
|
/**
|
|
120
146
|
* Build a suggested-replies data part for streaming.
|
|
121
147
|
* Chips beneath an assistant turn that each fire an ORDINARY user turn.
|