@company-semantics/contracts 97.0.0 → 99.0.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 +62 -8
- package/src/chat/README.md +31 -2
- package/src/chat/__tests__/alternatives.test.ts +121 -0
- package/src/chat/__tests__/chat-exports.test.ts +73 -1
- package/src/chat/__tests__/feedback-reasons.test.ts +99 -0
- package/src/chat/__tests__/message-actions.test.ts +41 -9
- package/src/chat/__tests__/message-mentions.test.ts +1 -0
- package/src/chat/__tests__/multi-party.test.ts +1 -0
- package/src/chat/__tests__/runtime-profile.test.ts +23 -5
- package/src/chat/__tests__/turn-wire.test.ts +43 -4
- package/src/chat/feedback.ts +80 -0
- package/src/chat/index.ts +26 -0
- package/src/chat/message-actions.ts +36 -30
- package/src/chat/schemas.ts +51 -1
- package/src/chat/types.ts +15 -0
- package/src/generated/openapi-routes.ts +1 -0
- package/src/ingestion/README.md +51 -21
- package/src/ingestion/__tests__/providers.test.ts +59 -0
- package/src/ingestion/__tests__/registry.test.ts +18 -18
- package/src/ingestion/__tests__/surfaces.test.ts +68 -40
- package/src/ingestion/index.ts +20 -2
- package/src/ingestion/providers.ts +141 -0
- package/src/ingestion/registry.ts +44 -25
- package/src/ingestion/surfaces.ts +81 -28
- package/src/ingestion/types.ts +80 -15
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 = '935ee995d53d' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '935ee995d53d56538f0566bb3c1372041d5cb1def41f3b28db02716ad4d8c591' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -421,6 +421,23 @@ export interface paths {
|
|
|
421
421
|
patch?: never;
|
|
422
422
|
trace?: never;
|
|
423
423
|
};
|
|
424
|
+
"/api/chats/{id}/branch": {
|
|
425
|
+
parameters: {
|
|
426
|
+
query?: never;
|
|
427
|
+
header?: never;
|
|
428
|
+
path?: never;
|
|
429
|
+
cookie?: never;
|
|
430
|
+
};
|
|
431
|
+
get?: never;
|
|
432
|
+
put?: never;
|
|
433
|
+
/** Make one version of a turn the active branch */
|
|
434
|
+
post: operations["selectChatBranch"];
|
|
435
|
+
delete?: never;
|
|
436
|
+
options?: never;
|
|
437
|
+
head?: never;
|
|
438
|
+
patch?: never;
|
|
439
|
+
trace?: never;
|
|
440
|
+
};
|
|
424
441
|
"/api/chats/{id}/pin": {
|
|
425
442
|
parameters: {
|
|
426
443
|
query?: never;
|
|
@@ -1712,7 +1729,7 @@ export interface paths {
|
|
|
1712
1729
|
};
|
|
1713
1730
|
/**
|
|
1714
1731
|
* Read one analysis workflow’s generic lifecycle envelope
|
|
1715
|
-
* @description The domain-agnostic projection a client polls: identity, registered type, activity-free lifecycle state, start and completion times, the opaque domain progress and result payloads (null until the domain adapter supplies them), a closed failure class on a failed workflow, the frozen typed outcome of the current execution epoch once it settles (null until then), the execution affordances offered now (the contracts offer table over state, the type’s execution support and the frozen outcome, filtered by the viewer’s policy), the workflow it was relaunched from, the current execution epoch’s start and completion times
|
|
1732
|
+
* @description The domain-agnostic projection a client polls: identity, registered type, activity-free lifecycle state, start and completion times, the opaque domain progress and result payloads (null until the domain adapter supplies them), a closed failure class on a failed workflow, the frozen typed outcome of the current execution epoch once it settles (null until then), the execution affordances offered now (the contracts offer table over state, the type’s execution support and the frozen outcome, filtered by the viewer’s policy), the workflow it was relaunched from, and the current execution epoch’s start and completion times. Org-scoped: a workflow in another workspace answers 404.
|
|
1716
1733
|
*/
|
|
1717
1734
|
get: operations["getAnalysisWorkflow"];
|
|
1718
1735
|
put?: never;
|
|
@@ -5652,7 +5669,16 @@ export interface components {
|
|
|
5652
5669
|
presentationGroupId: string | null;
|
|
5653
5670
|
/** @enum {string} */
|
|
5654
5671
|
turnKind: "conversation" | "workflow_launch" | "workflow_settlement" | "input_request_launch" | "proactive" | "refusal";
|
|
5655
|
-
|
|
5672
|
+
alternatives: {
|
|
5673
|
+
position: number;
|
|
5674
|
+
total: number;
|
|
5675
|
+
ids: string[];
|
|
5676
|
+
} | null;
|
|
5677
|
+
myFeedback: {
|
|
5678
|
+
/** @enum {string} */
|
|
5679
|
+
rating: "up" | "down";
|
|
5680
|
+
reason: ("incorrect" | "didnt_follow_instructions" | "not_helpful" | "missing_context" | "tool_or_workflow_issue" | "other") | null;
|
|
5681
|
+
} | null;
|
|
5656
5682
|
}[];
|
|
5657
5683
|
};
|
|
5658
5684
|
SuccessResponse: {
|
|
@@ -5710,6 +5736,10 @@ export interface components {
|
|
|
5710
5736
|
title: string;
|
|
5711
5737
|
isAutoGenerated: boolean;
|
|
5712
5738
|
};
|
|
5739
|
+
ChatBranchSelectRequest: {
|
|
5740
|
+
/** Format: uuid */
|
|
5741
|
+
messageId: string;
|
|
5742
|
+
};
|
|
5713
5743
|
ChatSseEvent: {
|
|
5714
5744
|
/** @constant */
|
|
5715
5745
|
v: 1;
|
|
@@ -5978,14 +6008,16 @@ export interface components {
|
|
|
5978
6008
|
ChatMessageFeedbackResponse: {
|
|
5979
6009
|
/** @enum {string} */
|
|
5980
6010
|
rating: "up" | "down";
|
|
5981
|
-
reason:
|
|
6011
|
+
reason: ("incorrect" | "didnt_follow_instructions" | "not_helpful" | "missing_context" | "tool_or_workflow_issue" | "other") | null;
|
|
5982
6012
|
/** Format: date-time */
|
|
5983
6013
|
updatedAt: string;
|
|
5984
6014
|
};
|
|
5985
6015
|
PutChatMessageFeedbackRequest: {
|
|
5986
6016
|
/** @enum {string} */
|
|
5987
6017
|
rating: "up" | "down";
|
|
5988
|
-
|
|
6018
|
+
/** @enum {string} */
|
|
6019
|
+
reason?: "incorrect" | "didnt_follow_instructions" | "not_helpful" | "missing_context" | "tool_or_workflow_issue" | "other";
|
|
6020
|
+
comment?: string;
|
|
5989
6021
|
};
|
|
5990
6022
|
WorkspaceAccessResponse: {
|
|
5991
6023
|
hasAccess: boolean;
|
|
@@ -7756,10 +7788,6 @@ export interface components {
|
|
|
7756
7788
|
})[];
|
|
7757
7789
|
}) | null;
|
|
7758
7790
|
error?: string;
|
|
7759
|
-
capabilities: {
|
|
7760
|
-
canStop: boolean;
|
|
7761
|
-
canContinue: boolean;
|
|
7762
|
-
};
|
|
7763
7791
|
executionAffordances: ({
|
|
7764
7792
|
/** @constant */
|
|
7765
7793
|
id: "workflow.stop";
|
|
@@ -12458,6 +12486,32 @@ export interface operations {
|
|
|
12458
12486
|
};
|
|
12459
12487
|
};
|
|
12460
12488
|
};
|
|
12489
|
+
selectChatBranch: {
|
|
12490
|
+
parameters: {
|
|
12491
|
+
query?: never;
|
|
12492
|
+
header?: never;
|
|
12493
|
+
path: {
|
|
12494
|
+
id: string;
|
|
12495
|
+
};
|
|
12496
|
+
cookie?: never;
|
|
12497
|
+
};
|
|
12498
|
+
requestBody: {
|
|
12499
|
+
content: {
|
|
12500
|
+
"application/json": components["schemas"]["ChatBranchSelectRequest"];
|
|
12501
|
+
};
|
|
12502
|
+
};
|
|
12503
|
+
responses: {
|
|
12504
|
+
/** @description The chat with its new active branch */
|
|
12505
|
+
200: {
|
|
12506
|
+
headers: {
|
|
12507
|
+
[name: string]: unknown;
|
|
12508
|
+
};
|
|
12509
|
+
content: {
|
|
12510
|
+
"application/json": components["schemas"]["GetChatResponse"];
|
|
12511
|
+
};
|
|
12512
|
+
};
|
|
12513
|
+
};
|
|
12514
|
+
};
|
|
12461
12515
|
pinChat: {
|
|
12462
12516
|
parameters: {
|
|
12463
12517
|
query?: never;
|
package/src/chat/README.md
CHANGED
|
@@ -59,16 +59,30 @@ A chat can belong to a SUBJECT — the durable case a hand-off to chat is about
|
|
|
59
59
|
|
|
60
60
|
One turn is one PRESENTATION GROUP whose action row appears once, when the turn settles (control doctrine `chat-action-system`; ADR slug `chat-turn-identity-and-message-actions`).
|
|
61
61
|
|
|
62
|
-
- `ChatMessage.presentationGroupId`, `turnKind` and `myFeedback` are REQUIRED: null for no group, a kind for every row, null for no rating. They shipped in a MAJOR so no consumer holds a second opinion about a missing key
|
|
62
|
+
- `ChatMessage.presentationGroupId`, `turnKind` and `myFeedback` are REQUIRED: null for no group, a kind for every row, null for no rating (otherwise `{ rating, reason }` — see "Versions and feedback"). They shipped in a MAJOR so no consumer holds a second opinion about a missing key
|
|
63
63
|
- `TURN_KINDS` (`turn-kind.ts`) is CLOSED and append-only. The server derives the kind from what persisted the row; a client never infers it from content. A turn kind is not a principal kind and never changes `author`
|
|
64
64
|
- `myFeedback` is the READER's own rating, per-reader like `myRole`
|
|
65
65
|
- `ChatStreamMessageMetadataSchema` (`stream-metadata.ts`) rides the AI SDK `start` chunk. The live message adopts `messageId`, so a streaming turn already IS the persisted row, already grouped; every field means what the same-named `ChatMessage` field means
|
|
66
66
|
- `messageActionsFor` (`message-actions.ts`) is the ONE eligibility function: the app renders the action row from it and the backend enforces the same function (`isMessageActionAllowed`) on every regenerate, edit and feedback request. Hiding a button is never the boundary
|
|
67
|
-
- `messageActionsFor` is PURE and TOTAL. An unsettled turn offers nothing; input that does not parse offers nothing — unknown is never permissive. Regenerate is offered
|
|
67
|
+
- `messageActionsFor` is PURE and TOTAL. An unsettled turn offers nothing; input that does not parse offers nothing — unknown is never permissive. Regenerate is offered on ANY settled `conversation` answer and edit on ANY settled `conversation` prompt the reader authored — not only the latest: both add a sibling version (a branch) and never delete. `isLatestOnActiveBranch` stays on the context but gates nothing. Launch, settlement, input-request, proactive and refusal turns never offer regenerate
|
|
68
68
|
- Its per-kind table is `satisfies Record<TurnKind, …>`, so a new kind fails to compile until its eligibility is decided
|
|
69
69
|
- Execution actions (cancel, continue, retry, run again) are NOT message actions and are not here — they reach a person as affordances
|
|
70
70
|
- Load order: `turn-kind.ts` is a LEAF (zod only). `schemas.ts`, `stream-metadata.ts` and `message-actions.ts` import it; the latter two never import `schemas.ts`, and `index.ts` re-exports all three directly
|
|
71
71
|
|
|
72
|
+
## Versions and feedback
|
|
73
|
+
|
|
74
|
+
Regenerating an answer or editing a prompt ADDS A SIBLING VERSION of the turn — a branch in a message tree with an active leaf — and never deletes anything; a reader's rating of a turn carries a CLOSED reason (ADR slug `response-versions-and-feedback-reasons`).
|
|
75
|
+
|
|
76
|
+
- `ChatMessage.alternatives` is REQUIRED and NULLABLE: null means the turn has no siblings. Otherwise `{ position, total, ids }` — `position` is 1-based, `ids` lists every sibling (this one included) ORDERED BY CREATION, and the schema refuses `ids.length !== total` or `position > total`, so a client never reconciles fields that disagree
|
|
77
|
+
- The transcript carries the ACTIVE branch only. Other versions are reachable by id, never inlined
|
|
78
|
+
- `ChatBranchSelectRequestSchema` (`{ messageId }`) is the body that makes one sibling the active branch. Naming an id is a lookup, not authority — the server re-checks it is a version of a turn in this chat
|
|
79
|
+
- `FEEDBACK_RATINGS` is `up` / `down`. `FEEDBACK_REASONS` (`feedback.ts`) is CLOSED and append-only: a reason is a category the product can count, and free text belongs in `comment` (at most `FEEDBACK_COMMENT_MAX` = 2000 characters), never in a reason
|
|
80
|
+
- `PutChatMessageFeedbackRequestSchema` is the body of `putChatMessageFeedback`, named after the generated operation's component so the backend can replace its route-local schema of the same name. A reason or a comment rides only a `down` rating; the schema refuses either on `up`. It is a deliberate exception to "request schemas live in backend": the closed reason list is shared vocabulary the app's picker and the backend's check must agree on
|
|
81
|
+
- `ChatMessage.myFeedback` is `{ rating, reason } | null` — the READER's own rating; `reason` is null on an up rating and on a down rating given without one. The comment is write-only and never projected back onto the transcript
|
|
82
|
+
- Feedback MEASURES a turn. It is NOT APPROVAL of anything the turn proposed, and never authorizes, confirms or executes
|
|
83
|
+
- Load order: `feedback.ts` is a LEAF (zod only). `schemas.ts` imports it, it never imports `schemas.ts`, and `index.ts` re-exports it directly
|
|
84
|
+
- `openapi/backend.yaml` deliberately lags (contracts-first): its `ChatMessage` has no `alternatives`, its `myFeedback` is a bare rating, and its feedback `reason` is free text. It catches up when the backend regenerates its spec
|
|
85
|
+
|
|
72
86
|
<!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
|
|
73
87
|
|
|
74
88
|
## Public API
|
|
@@ -83,6 +97,8 @@ One turn is one PRESENTATION GROUP whose action row appears once, when the turn
|
|
|
83
97
|
- `CHAT_PAGE_VIEWS` — The app route parser's fifteen `type` values, projected onto the wire.
|
|
84
98
|
- `CHAT_RUNTIME_PROFILES` — Ordered list of available profiles for UI rendering.
|
|
85
99
|
- `CONVERSATION_SUBJECT_KINDS` — The kinds of subject a chat can continue.
|
|
100
|
+
- `ChatBranchSelectRequest` _(type)_ — Request body that makes one sibling version the active branch.
|
|
101
|
+
- `ChatBranchSelectRequestSchema` — Request body that makes one version the active branch (response-versions-and-feedback-reasons).
|
|
86
102
|
- `ChatByInteractionResponse` _(type)_ — Response for GET /api/chats/by-interaction/:interactionId
|
|
87
103
|
- `ChatByInteractionResponseSchema` — Response for GET /api/chats/by-interaction/:interactionId
|
|
88
104
|
- `ChatChangedField` _(type)_ — Fields that can change on a chat, used in changed array.
|
|
@@ -111,8 +127,12 @@ One turn is one PRESENTATION GROUP whose action row appears once, when the turn
|
|
|
111
127
|
- `ChatMentionableResponse` _(type)_ — Response for the chat composer's mentionable-candidate endpoint.
|
|
112
128
|
- `ChatMentionableResponseSchema` — Who the chat composer may offer as a mention target.
|
|
113
129
|
- `ChatMessage` _(type)_ — Individual message in a chat response (GET /api/chats/:id).
|
|
130
|
+
- `ChatMessageAlternatives` _(type)_ — The sibling versions of one turn — 1-based `position`, `total`, and every sibling id ordered by creation…
|
|
131
|
+
- `ChatMessageAlternativesSchema` — The VERSIONS of one turn (response-versions-and-feedback-reasons, ).
|
|
114
132
|
- `ChatMessageMention` _(type)_ — One mention on a chat turn — a `PrincipalRef` target, a nullable UTF-16 code-unit range into `content`, and a…
|
|
115
133
|
- `ChatMessageMentionSchema` — ONE mention on a chat turn — field-for-field `CommentMentionSchema` (`../comments/schemas`), and deliberately…
|
|
134
|
+
- `ChatMessageMyFeedback` _(type)_
|
|
135
|
+
- `ChatMessageMyFeedbackSchema` — The reader's own rating as `ChatMessage.myFeedback` carries it.
|
|
116
136
|
- `ChatMessageSchema`
|
|
117
137
|
- `ChatOrigin` _(type)_ — How a chat was born: `user` or `proactive` (the system spoke first).
|
|
118
138
|
- `ChatOriginSchema` — How a chat was born: by the user, or by the system speaking first (a proactive occurrence projected onto the…
|
|
@@ -143,6 +163,13 @@ One turn is one PRESENTATION GROUP whose action row appears once, when the turn
|
|
|
143
163
|
- `CreateChatResponse` _(type)_ — Response for POST /api/chats
|
|
144
164
|
- `CreateChatResponseSchema` — Response for POST /api/chats
|
|
145
165
|
- `DEFAULT_CHAT_RUNTIME_PROFILE` — Default profile when none is specified in the request.
|
|
166
|
+
- `FEEDBACK_COMMENT_MAX` — Longest free-text comment a down rating may carry.
|
|
167
|
+
- `FEEDBACK_RATINGS` — The two ratings a reader can give a turn.
|
|
168
|
+
- `FEEDBACK_REASONS` — Why a reader rated a turn down.
|
|
169
|
+
- `FeedbackRating` _(type)_
|
|
170
|
+
- `FeedbackRatingSchema`
|
|
171
|
+
- `FeedbackReason` _(type)_
|
|
172
|
+
- `FeedbackReasonSchema`
|
|
146
173
|
- `GetChatResponse` _(type)_ — Response for GET /api/chats/:id
|
|
147
174
|
- `GetChatResponseSchema` — Response for GET /api/chats/:id
|
|
148
175
|
- `InvalidateChatEvent` _(type)_ — Single-entity staleness signal.
|
|
@@ -164,6 +191,8 @@ One turn is one PRESENTATION GROUP whose action row appears once, when the turn
|
|
|
164
191
|
- `MessageActionIdSchema`
|
|
165
192
|
- `ProactiveChatResolution` _(type)_ — Resolving an occurrence id to the CURRENT reader's chat — one of the four bounded states `ready` (with…
|
|
166
193
|
- `ProactiveChatResolutionSchema` — Resolving an occurrence id to THIS reader's chat (INV-PROACTIVE-LINK).
|
|
194
|
+
- `PutChatMessageFeedbackRequest` _(type)_
|
|
195
|
+
- `PutChatMessageFeedbackRequestSchema` — Body of PUT /api/chats/{id}/messages/{messageId}/feedback (operationId putChatMessageFeedback).
|
|
167
196
|
- `RecognizedProactiveKind` _(type)_ — A wire `proactiveKind` after recognition: a kind this contracts version knows, or the raw string it did not —…
|
|
168
197
|
- `ResolveLaunchResponse` _(type)_
|
|
169
198
|
- `ResolveLaunchResponseSchema` — Response of the read-only launch resolution.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sibling versions of one turn (response-versions-and-feedback-reasons,
|
|
3
|
+
* ADR-CONTRACTS-228).
|
|
4
|
+
*
|
|
5
|
+
* INVARIANTS TESTED:
|
|
6
|
+
* - `ChatMessage.alternatives` is null (no siblings) or a version position
|
|
7
|
+
* within the ordered sibling ids.
|
|
8
|
+
* - `ids` are uuids, `ids.length === total` and `1 <= position <= total`.
|
|
9
|
+
* - The branch-select request names one sibling by uuid `messageId`.
|
|
10
|
+
* - Both schemas and their types are reachable from the `./chat` barrel.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
ChatBranchSelectRequestSchema,
|
|
15
|
+
ChatMessageAlternativesSchema,
|
|
16
|
+
ChatMessageSchema,
|
|
17
|
+
} from "../schemas.js";
|
|
18
|
+
import type {
|
|
19
|
+
ChatBranchSelectRequest,
|
|
20
|
+
ChatMessageAlternatives,
|
|
21
|
+
} from "../index.js";
|
|
22
|
+
import * as barrel from "../index.js";
|
|
23
|
+
|
|
24
|
+
const A = "11111111-1111-4111-8111-111111111111";
|
|
25
|
+
const B = "22222222-2222-4222-8222-222222222222";
|
|
26
|
+
|
|
27
|
+
const makeMessage = (alternatives: unknown) => ({
|
|
28
|
+
id: "msg_1",
|
|
29
|
+
role: "assistant" as const,
|
|
30
|
+
content: "Done.",
|
|
31
|
+
sequenceNumber: 1,
|
|
32
|
+
createdAt: "2026-10-08T00:00:00.000Z",
|
|
33
|
+
mentions: [],
|
|
34
|
+
author: { kind: "agent" as const },
|
|
35
|
+
presentationGroupId: null,
|
|
36
|
+
turnKind: "conversation" as const,
|
|
37
|
+
alternatives,
|
|
38
|
+
myFeedback: null,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("ChatMessageAlternativesSchema", () => {
|
|
42
|
+
it("parses null alternatives on a message: the turn has no siblings", () => {
|
|
43
|
+
expect(ChatMessageSchema.parse(makeMessage(null)).alternatives).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("accepts a version position within ordered sibling ids", () => {
|
|
47
|
+
const alternatives: ChatMessageAlternatives = {
|
|
48
|
+
position: 2,
|
|
49
|
+
total: 2,
|
|
50
|
+
ids: [A, B],
|
|
51
|
+
};
|
|
52
|
+
expect(
|
|
53
|
+
ChatMessageSchema.parse(makeMessage(alternatives)).alternatives,
|
|
54
|
+
).toEqual(alternatives);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("refuses ids whose length disagrees with total", () => {
|
|
58
|
+
expect(
|
|
59
|
+
ChatMessageAlternativesSchema.safeParse({
|
|
60
|
+
position: 1,
|
|
61
|
+
total: 3,
|
|
62
|
+
ids: [A, B],
|
|
63
|
+
}).success,
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("refuses a position beyond total", () => {
|
|
68
|
+
expect(
|
|
69
|
+
ChatMessageAlternativesSchema.safeParse({
|
|
70
|
+
position: 3,
|
|
71
|
+
total: 2,
|
|
72
|
+
ids: [A, B],
|
|
73
|
+
}).success,
|
|
74
|
+
).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("refuses a position below 1", () => {
|
|
78
|
+
expect(
|
|
79
|
+
ChatMessageAlternativesSchema.safeParse({
|
|
80
|
+
position: 0,
|
|
81
|
+
total: 2,
|
|
82
|
+
ids: [A, B],
|
|
83
|
+
}).success,
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("refuses a non-uuid sibling id", () => {
|
|
88
|
+
expect(
|
|
89
|
+
ChatMessageAlternativesSchema.safeParse({
|
|
90
|
+
position: 1,
|
|
91
|
+
total: 1,
|
|
92
|
+
ids: ["msg_1"],
|
|
93
|
+
}).success,
|
|
94
|
+
).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("ChatBranchSelectRequestSchema", () => {
|
|
99
|
+
it("accepts a uuid messageId", () => {
|
|
100
|
+
const body: ChatBranchSelectRequest = { messageId: A };
|
|
101
|
+
expect(ChatBranchSelectRequestSchema.safeParse(body).success).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("refuses a missing or non-uuid messageId", () => {
|
|
105
|
+
expect(ChatBranchSelectRequestSchema.safeParse({}).success).toBe(false);
|
|
106
|
+
expect(
|
|
107
|
+
ChatBranchSelectRequestSchema.safeParse({ messageId: "msg_1" }).success,
|
|
108
|
+
).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("chat barrel exports the alternatives vocabulary", () => {
|
|
113
|
+
it("re-exports the same schemas the module defines", () => {
|
|
114
|
+
expect(barrel.ChatMessageAlternativesSchema).toBe(
|
|
115
|
+
ChatMessageAlternativesSchema,
|
|
116
|
+
);
|
|
117
|
+
expect(barrel.ChatBranchSelectRequestSchema).toBe(
|
|
118
|
+
ChatBranchSelectRequestSchema,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -11,19 +11,37 @@
|
|
|
11
11
|
* MessageActionContextSchema, messageActionsFor and isMessageActionAllowed
|
|
12
12
|
* are exported from the chat barrel
|
|
13
13
|
* - messageActionsFor called through the barrel is the same function the
|
|
14
|
-
* backend enforces: a settled
|
|
14
|
+
* backend enforces: a settled conversation answer offers regenerate, latest
|
|
15
|
+
* or not
|
|
16
|
+
* - The versions and feedback vocabulary (response-versions-and-feedback-reasons)
|
|
17
|
+
* is exported from the chat barrel: ChatMessageAlternativesSchema,
|
|
18
|
+
* ChatBranchSelectRequestSchema, FEEDBACK_RATINGS, FeedbackRatingSchema,
|
|
19
|
+
* FEEDBACK_REASONS, FeedbackReasonSchema, FEEDBACK_COMMENT_MAX (2000),
|
|
20
|
+
* PutChatMessageFeedbackRequestSchema and ChatMessageMyFeedbackSchema
|
|
15
21
|
*/
|
|
16
22
|
import { describe, expect, it } from "vitest";
|
|
17
23
|
import {
|
|
24
|
+
ChatBranchSelectRequestSchema,
|
|
25
|
+
ChatMessageAlternativesSchema,
|
|
26
|
+
ChatMessageMyFeedbackSchema,
|
|
18
27
|
ChatStreamMessageMetadataSchema,
|
|
28
|
+
FEEDBACK_COMMENT_MAX,
|
|
29
|
+
FEEDBACK_RATINGS,
|
|
30
|
+
FEEDBACK_REASONS,
|
|
31
|
+
FeedbackRatingSchema,
|
|
32
|
+
FeedbackReasonSchema,
|
|
19
33
|
isMessageActionAllowed,
|
|
20
34
|
MESSAGE_ACTION_IDS,
|
|
21
35
|
MessageActionContextSchema,
|
|
22
36
|
messageActionsFor,
|
|
37
|
+
PutChatMessageFeedbackRequestSchema,
|
|
23
38
|
TURN_KINDS,
|
|
24
39
|
TurnKindSchema,
|
|
25
40
|
} from "../index.js";
|
|
26
41
|
|
|
42
|
+
const ID_A = "11111111-1111-4111-8111-111111111111";
|
|
43
|
+
const ID_B = "22222222-2222-4222-8222-222222222222";
|
|
44
|
+
|
|
27
45
|
describe("chat barrel exports the turn on the wire", () => {
|
|
28
46
|
it("exports the closed vocabularies", () => {
|
|
29
47
|
expect(TURN_KINDS).toContain("conversation");
|
|
@@ -58,4 +76,58 @@ describe("chat barrel exports the turn on the wire", () => {
|
|
|
58
76
|
expect(isMessageActionAllowed(ctx, "regenerate")).toBe(true);
|
|
59
77
|
expect(isMessageActionAllowed(ctx, "edit")).toBe(false);
|
|
60
78
|
});
|
|
79
|
+
|
|
80
|
+
it("offers regenerate on an older answer through the barrel", () => {
|
|
81
|
+
const ctx = MessageActionContextSchema.parse({
|
|
82
|
+
role: "assistant",
|
|
83
|
+
turnKind: "conversation",
|
|
84
|
+
settled: true,
|
|
85
|
+
isLatestOnActiveBranch: false,
|
|
86
|
+
authoredByReader: false,
|
|
87
|
+
});
|
|
88
|
+
expect(isMessageActionAllowed(ctx, "regenerate")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("chat barrel exports the versions and feedback vocabulary", () => {
|
|
93
|
+
it("exports the alternatives and branch-select schemas", () => {
|
|
94
|
+
expect(
|
|
95
|
+
ChatMessageAlternativesSchema.safeParse({
|
|
96
|
+
position: 2,
|
|
97
|
+
total: 2,
|
|
98
|
+
ids: [ID_A, ID_B],
|
|
99
|
+
}).success,
|
|
100
|
+
).toBe(true);
|
|
101
|
+
expect(
|
|
102
|
+
ChatBranchSelectRequestSchema.safeParse({ messageId: ID_B }).success,
|
|
103
|
+
).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("exports the closed feedback vocabularies", () => {
|
|
107
|
+
expect(FEEDBACK_RATINGS).toEqual(["up", "down"]);
|
|
108
|
+
expect(FeedbackRatingSchema.options).toEqual([...FEEDBACK_RATINGS]);
|
|
109
|
+
expect(FEEDBACK_REASONS).toContain("other");
|
|
110
|
+
expect(FeedbackReasonSchema.options).toEqual([...FEEDBACK_REASONS]);
|
|
111
|
+
expect(FEEDBACK_COMMENT_MAX).toBe(2000);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("exports the feedback request and myFeedback schemas", () => {
|
|
115
|
+
expect(
|
|
116
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
117
|
+
rating: "down",
|
|
118
|
+
reason: "incorrect",
|
|
119
|
+
comment: "wrong date",
|
|
120
|
+
}).success,
|
|
121
|
+
).toBe(true);
|
|
122
|
+
expect(
|
|
123
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
124
|
+
rating: "up",
|
|
125
|
+
reason: "incorrect",
|
|
126
|
+
}).success,
|
|
127
|
+
).toBe(false);
|
|
128
|
+
expect(
|
|
129
|
+
ChatMessageMyFeedbackSchema.safeParse({ rating: "up", reason: null })
|
|
130
|
+
.success,
|
|
131
|
+
).toBe(true);
|
|
132
|
+
});
|
|
61
133
|
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat message feedback (response-versions-and-feedback-reasons).
|
|
3
|
+
*
|
|
4
|
+
* INVARIANTS TESTED:
|
|
5
|
+
* - A reason or a comment rides only a "down" rating; an up rating carrying
|
|
6
|
+
* either is refused.
|
|
7
|
+
* - A comment is at most FEEDBACK_COMMENT_MAX (2000) characters.
|
|
8
|
+
* - FEEDBACK_REASONS is CLOSED: a reason outside it is refused.
|
|
9
|
+
* - ChatMessageMyFeedback is `{ rating, reason }` with a nullable reason.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
ChatMessageMyFeedbackSchema,
|
|
14
|
+
FEEDBACK_COMMENT_MAX,
|
|
15
|
+
FEEDBACK_RATINGS,
|
|
16
|
+
FEEDBACK_REASONS,
|
|
17
|
+
PutChatMessageFeedbackRequestSchema,
|
|
18
|
+
} from "../feedback.js";
|
|
19
|
+
|
|
20
|
+
describe("PutChatMessageFeedbackRequestSchema", () => {
|
|
21
|
+
it("accepts a bare up or down rating", () => {
|
|
22
|
+
for (const rating of FEEDBACK_RATINGS) {
|
|
23
|
+
expect(
|
|
24
|
+
PutChatMessageFeedbackRequestSchema.safeParse({ rating }).success,
|
|
25
|
+
).toBe(true);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("accepts a down rating with a reason and a comment", () => {
|
|
30
|
+
expect(
|
|
31
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
32
|
+
rating: "down",
|
|
33
|
+
reason: "incorrect",
|
|
34
|
+
comment: "wrong date",
|
|
35
|
+
}).success,
|
|
36
|
+
).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("refuses a reason with rating up", () => {
|
|
40
|
+
expect(
|
|
41
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
42
|
+
rating: "up",
|
|
43
|
+
reason: "other",
|
|
44
|
+
}).success,
|
|
45
|
+
).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("refuses a comment with rating up", () => {
|
|
49
|
+
expect(
|
|
50
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
51
|
+
rating: "up",
|
|
52
|
+
comment: "nice",
|
|
53
|
+
}).success,
|
|
54
|
+
).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("refuses a comment over 2000 characters", () => {
|
|
58
|
+
expect(FEEDBACK_COMMENT_MAX).toBe(2000);
|
|
59
|
+
expect(
|
|
60
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
61
|
+
rating: "down",
|
|
62
|
+
comment: "x".repeat(FEEDBACK_COMMENT_MAX + 1),
|
|
63
|
+
}).success,
|
|
64
|
+
).toBe(false);
|
|
65
|
+
expect(
|
|
66
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
67
|
+
rating: "down",
|
|
68
|
+
comment: "x".repeat(FEEDBACK_COMMENT_MAX),
|
|
69
|
+
}).success,
|
|
70
|
+
).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("refuses a reason outside FEEDBACK_REASONS", () => {
|
|
74
|
+
expect(
|
|
75
|
+
PutChatMessageFeedbackRequestSchema.safeParse({
|
|
76
|
+
rating: "down",
|
|
77
|
+
reason: "rude",
|
|
78
|
+
}).success,
|
|
79
|
+
).toBe(false);
|
|
80
|
+
expect(FEEDBACK_REASONS).toContain("other");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("ChatMessageMyFeedbackSchema", () => {
|
|
85
|
+
it("carries a rating and a nullable reason", () => {
|
|
86
|
+
expect(
|
|
87
|
+
ChatMessageMyFeedbackSchema.parse({ rating: "up", reason: null }),
|
|
88
|
+
).toEqual({ rating: "up", reason: null });
|
|
89
|
+
expect(
|
|
90
|
+
ChatMessageMyFeedbackSchema.parse({ rating: "down", reason: "other" }),
|
|
91
|
+
).toEqual({ rating: "down", reason: "other" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("requires the reason key", () => {
|
|
95
|
+
expect(
|
|
96
|
+
ChatMessageMyFeedbackSchema.safeParse({ rating: "up" }).success,
|
|
97
|
+
).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* - One test per row of the doctrine matrix.
|
|
7
7
|
* - An unsettled turn offers nothing.
|
|
8
8
|
* - Regenerate is never offered on a launch, settlement or input-request turn.
|
|
9
|
+
* - Regenerate and edit are offered on older turns too — position gates
|
|
10
|
+
* nothing, because both branch rather than delete.
|
|
9
11
|
* - Total over TURN_KINDS; an unknown kind or role is never permissive.
|
|
10
12
|
* - isMessageActionAllowed agrees with messageActionsFor.
|
|
11
13
|
*/
|
|
@@ -74,7 +76,7 @@ describe("messageActionsFor", () => {
|
|
|
74
76
|
).toEqual(["copy", "edit"]);
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
it("
|
|
79
|
+
it("an older user turn that is the reader's own still offers copy and edit (edit branches)", () => {
|
|
78
80
|
expect(
|
|
79
81
|
messageActionsFor(
|
|
80
82
|
makeCtx({
|
|
@@ -83,10 +85,21 @@ describe("messageActionsFor", () => {
|
|
|
83
85
|
isLatestOnActiveBranch: false,
|
|
84
86
|
}),
|
|
85
87
|
),
|
|
86
|
-
).toEqual(["copy"]);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
).toEqual(["copy", "edit"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("a user turn that is not the reader's own offers copy only, latest or not", () => {
|
|
92
|
+
for (const isLatestOnActiveBranch of [true, false]) {
|
|
93
|
+
expect(
|
|
94
|
+
messageActionsFor(
|
|
95
|
+
makeCtx({
|
|
96
|
+
role: "user",
|
|
97
|
+
authoredByReader: false,
|
|
98
|
+
isLatestOnActiveBranch,
|
|
99
|
+
}),
|
|
100
|
+
),
|
|
101
|
+
).toEqual(["copy"]);
|
|
102
|
+
}
|
|
90
103
|
});
|
|
91
104
|
|
|
92
105
|
it("the latest assistant conversation turn offers copy, feedback and regenerate", () => {
|
|
@@ -97,10 +110,25 @@ describe("messageActionsFor", () => {
|
|
|
97
110
|
]);
|
|
98
111
|
});
|
|
99
112
|
|
|
100
|
-
it("
|
|
113
|
+
it("offers regenerate on a historical conversation answer", () => {
|
|
101
114
|
expect(
|
|
102
115
|
messageActionsFor(makeCtx({ isLatestOnActiveBranch: false })),
|
|
103
|
-
).toEqual(["copy", "feedback"]);
|
|
116
|
+
).toEqual(["copy", "feedback", "regenerate"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("an older unsettled conversation turn still offers nothing", () => {
|
|
120
|
+
for (const role of ["user", "assistant"] as const) {
|
|
121
|
+
expect(
|
|
122
|
+
messageActionsFor(
|
|
123
|
+
makeCtx({
|
|
124
|
+
role,
|
|
125
|
+
settled: false,
|
|
126
|
+
isLatestOnActiveBranch: false,
|
|
127
|
+
authoredByReader: true,
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
).toEqual([]);
|
|
131
|
+
}
|
|
104
132
|
});
|
|
105
133
|
|
|
106
134
|
it("never offers regenerate on a workflow_launch, workflow_settlement or input_request_launch turn", () => {
|
|
@@ -117,9 +145,13 @@ describe("messageActionsFor", () => {
|
|
|
117
145
|
}
|
|
118
146
|
});
|
|
119
147
|
|
|
120
|
-
it("a proactive or refusal turn offers copy only", () => {
|
|
148
|
+
it("a proactive or refusal turn offers copy only, latest or not", () => {
|
|
121
149
|
for (const turnKind of ["proactive", "refusal"] as const) {
|
|
122
|
-
|
|
150
|
+
for (const isLatestOnActiveBranch of [true, false]) {
|
|
151
|
+
expect(
|
|
152
|
+
messageActionsFor(makeCtx({ turnKind, isLatestOnActiveBranch })),
|
|
153
|
+
).toEqual(["copy"]);
|
|
154
|
+
}
|
|
123
155
|
}
|
|
124
156
|
});
|
|
125
157
|
|