@yadsh/dsh-qa-surface 0.9.0 → 0.11.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 (250) hide show
  1. package/README.md +269 -2
  2. package/lib/access/service.js +49 -10
  3. package/lib/access/service.js.map +1 -1
  4. package/lib/account-remotes.js +41 -0
  5. package/lib/account-remotes.js.map +1 -1
  6. package/lib/accounts/database.js +140 -0
  7. package/lib/accounts/database.js.map +1 -1
  8. package/lib/accounts/errors.js.map +1 -1
  9. package/lib/accounts/service-token.js +103 -0
  10. package/lib/accounts/service-token.js.map +1 -0
  11. package/lib/accounts/store.js +291 -5
  12. package/lib/accounts/store.js.map +1 -1
  13. package/lib/admin/conversation-log.js +14 -8
  14. package/lib/admin/conversation-log.js.map +1 -1
  15. package/lib/admin/permissions.js +5 -0
  16. package/lib/admin/permissions.js.map +1 -1
  17. package/lib/admin/quality-store.js +4 -0
  18. package/lib/admin/quality-store.js.map +1 -1
  19. package/lib/admin/service.js +180 -7
  20. package/lib/admin/service.js.map +1 -1
  21. package/lib/admin/session-log.js +39 -8
  22. package/lib/admin/session-log.js.map +1 -1
  23. package/lib/attachment-rules.js +39 -6
  24. package/lib/attachment-rules.js.map +1 -1
  25. package/lib/cli.js +83 -1
  26. package/lib/cli.js.map +1 -1
  27. package/lib/client/QaAccountsController.js +213 -4
  28. package/lib/client/QaAccountsController.js.map +1 -1
  29. package/lib/client/QaSessionController.js +49 -21
  30. package/lib/client/QaSessionController.js.map +1 -1
  31. package/lib/client/QaSurface.js +51 -9
  32. package/lib/client/QaSurface.js.map +1 -1
  33. package/lib/client/QaSurfaceGuard.js +12 -5
  34. package/lib/client/QaSurfaceGuard.js.map +1 -1
  35. package/lib/client/admin/QaAdmin.js +14 -2
  36. package/lib/client/admin/QaAdmin.js.map +1 -1
  37. package/lib/client/admin/pages/SkillFiles.js +78 -0
  38. package/lib/client/admin/pages/SkillFiles.js.map +1 -0
  39. package/lib/client/admin/pages/Users.js +74 -3
  40. package/lib/client/admin/pages/Users.js.map +1 -1
  41. package/lib/client/admin/routes.js +8 -1
  42. package/lib/client/admin/routes.js.map +1 -1
  43. package/lib/client/admin/shared.js +5 -1
  44. package/lib/client/admin/shared.js.map +1 -1
  45. package/lib/client/admin/styles.js +2 -0
  46. package/lib/client/admin/styles.js.map +1 -1
  47. package/lib/client/attachments.js +41 -11
  48. package/lib/client/attachments.js.map +1 -1
  49. package/lib/client/base64.js +22 -1
  50. package/lib/client/base64.js.map +1 -1
  51. package/lib/client/components/QaAgentsDrawer.js +9 -6
  52. package/lib/client/components/QaAgentsDrawer.js.map +1 -1
  53. package/lib/client/components/QaApproval.js +1 -1
  54. package/lib/client/components/QaApproval.js.map +1 -1
  55. package/lib/client/components/QaAuthGate.js +19 -10
  56. package/lib/client/components/QaAuthGate.js.map +1 -1
  57. package/lib/client/components/QaChangelog.js +56 -0
  58. package/lib/client/components/QaChangelog.js.map +1 -1
  59. package/lib/client/components/QaFilesPanel.js +17 -11
  60. package/lib/client/components/QaFilesPanel.js.map +1 -1
  61. package/lib/client/components/QaHeader.js +3 -11
  62. package/lib/client/components/QaHeader.js.map +1 -1
  63. package/lib/client/components/QaModal.js.map +1 -1
  64. package/lib/client/components/QaQuestions.js +4 -4
  65. package/lib/client/components/QaQuestions.js.map +1 -1
  66. package/lib/client/components/QaSidebar.js +10 -2
  67. package/lib/client/components/QaSidebar.js.map +1 -1
  68. package/lib/client/components/QaWidthHandle.js +111 -5
  69. package/lib/client/components/QaWidthHandle.js.map +1 -1
  70. package/lib/client/components/QaWorkspaceBrowser.js +231 -0
  71. package/lib/client/components/QaWorkspaceBrowser.js.map +1 -0
  72. package/lib/client/index.js +122 -53
  73. package/lib/client/index.js.map +1 -1
  74. package/lib/client/kiosk.js +51 -0
  75. package/lib/client/kiosk.js.map +1 -0
  76. package/lib/client/lineage.js +64 -0
  77. package/lib/client/lineage.js.map +1 -1
  78. package/lib/client/markdown/blocks.js +122 -7
  79. package/lib/client/markdown/blocks.js.map +1 -1
  80. package/lib/client/markdown/inline.js +108 -12
  81. package/lib/client/markdown/inline.js.map +1 -1
  82. package/lib/client/markdown/katex-css.js +12 -0
  83. package/lib/client/markdown/katex-css.js.map +1 -0
  84. package/lib/client/markdown/math.js +84 -0
  85. package/lib/client/markdown/math.js.map +1 -0
  86. package/lib/client/markdown/render.js +74 -5
  87. package/lib/client/markdown/render.js.map +1 -1
  88. package/lib/client/settings/card.js +2 -2
  89. package/lib/client/settings/card.js.map +1 -1
  90. package/lib/client/settings/sections/attachments.js +6 -5
  91. package/lib/client/settings/sections/attachments.js.map +1 -1
  92. package/lib/client/settings/sections/notes.js +64 -0
  93. package/lib/client/settings/sections/notes.js.map +1 -0
  94. package/lib/client/settings/sections/sources.js +4 -1
  95. package/lib/client/settings/sections/sources.js.map +1 -1
  96. package/lib/client/settings/sections.js +1 -0
  97. package/lib/client/settings/sections.js.map +1 -1
  98. package/lib/client/settlement.js +1 -1
  99. package/lib/client/settlement.js.map +1 -1
  100. package/lib/client/source-preview.js +3 -0
  101. package/lib/client/source-preview.js.map +1 -1
  102. package/lib/client/styles.js +106 -18
  103. package/lib/client/styles.js.map +1 -1
  104. package/lib/client/types.js.map +1 -1
  105. package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
  106. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
  107. package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
  108. package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
  109. package/lib/client/user-settings/SkillCatalog.js +2 -2
  110. package/lib/client/user-settings/SkillCatalog.js.map +1 -1
  111. package/lib/client/user-settings/UserSettingsDialog.js +17 -2
  112. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  113. package/lib/client/user-settings/copy.js +20 -0
  114. package/lib/client/user-settings/copy.js.map +1 -1
  115. package/lib/client.js +31007 -686
  116. package/lib/client.js.map +1 -1
  117. package/lib/config-resolvers/attachments.js +6 -5
  118. package/lib/config-resolvers/attachments.js.map +1 -1
  119. package/lib/config-resolvers/defaults.js +33 -2
  120. package/lib/config-resolvers/defaults.js.map +1 -1
  121. package/lib/config-resolvers/integration.js +94 -0
  122. package/lib/config-resolvers/integration.js.map +1 -0
  123. package/lib/config-resolvers/notes.js +26 -0
  124. package/lib/config-resolvers/notes.js.map +1 -0
  125. package/lib/config-resolvers/sources.js +4 -0
  126. package/lib/config-resolvers/sources.js.map +1 -1
  127. package/lib/config.js +107 -0
  128. package/lib/config.js.map +1 -1
  129. package/lib/index.js +348 -12
  130. package/lib/index.js.map +1 -1
  131. package/lib/integration/answer.js +100 -0
  132. package/lib/integration/answer.js.map +1 -0
  133. package/lib/integration/attachments.js +231 -0
  134. package/lib/integration/attachments.js.map +1 -0
  135. package/lib/integration/contract.js +53 -0
  136. package/lib/integration/contract.js.map +1 -0
  137. package/lib/integration/host-runner.js +243 -0
  138. package/lib/integration/host-runner.js.map +1 -0
  139. package/lib/integration/http.js +538 -0
  140. package/lib/integration/http.js.map +1 -0
  141. package/lib/integration/service.js +331 -0
  142. package/lib/integration/service.js.map +1 -0
  143. package/lib/integration/transcript-reader.js +202 -0
  144. package/lib/integration/transcript-reader.js.map +1 -0
  145. package/lib/integration/transcript.js +148 -0
  146. package/lib/integration/transcript.js.map +1 -0
  147. package/lib/personal-skills/index.js +2 -2
  148. package/lib/personal-skills/index.js.map +1 -1
  149. package/lib/personal-skills/paths.js +14 -8
  150. package/lib/personal-skills/paths.js.map +1 -1
  151. package/lib/personal-skills/provider.js +16 -4
  152. package/lib/personal-skills/provider.js.map +1 -1
  153. package/lib/personal-skills/service.js +240 -30
  154. package/lib/personal-skills/service.js.map +1 -1
  155. package/lib/prompt-notes.js +103 -6
  156. package/lib/prompt-notes.js.map +1 -1
  157. package/lib/provenance/document-preview.js +60 -0
  158. package/lib/provenance/document-preview.js.map +1 -0
  159. package/lib/provenance/file-preview.js +191 -19
  160. package/lib/provenance/file-preview.js.map +1 -1
  161. package/lib/qa-tools/catalog.js +18 -1
  162. package/lib/qa-tools/catalog.js.map +1 -1
  163. package/lib/qa-tools/docs-tools.js +563 -0
  164. package/lib/qa-tools/docs-tools.js.map +1 -0
  165. package/lib/qa-tools/file-delete-gate.js +60 -0
  166. package/lib/qa-tools/file-delete-gate.js.map +1 -0
  167. package/lib/qa-tools/file-delete.js +156 -0
  168. package/lib/qa-tools/file-delete.js.map +1 -0
  169. package/lib/resolve-config.js +6 -0
  170. package/lib/resolve-config.js.map +1 -1
  171. package/lib/secure-session.js +83 -3
  172. package/lib/secure-session.js.map +1 -1
  173. package/lib/shared/documents.js +22 -0
  174. package/lib/shared/documents.js.map +1 -0
  175. package/lib/shared/integration-tokens.js +23 -0
  176. package/lib/shared/integration-tokens.js.map +1 -0
  177. package/lib/typert.host.js +1320 -134
  178. package/lib/typert.remote-client.d.ts +33 -1
  179. package/lib/typert.remote-client.js +1102 -74
  180. package/lib/types/access/service.d.ts +33 -2
  181. package/lib/types/account-remotes.d.ts +30 -1
  182. package/lib/types/accounts/database.d.ts +53 -0
  183. package/lib/types/accounts/errors.d.ts +5 -1
  184. package/lib/types/accounts/service-token.d.ts +70 -0
  185. package/lib/types/accounts/store.d.ts +131 -1
  186. package/lib/types/admin/conversation-log.d.ts +12 -0
  187. package/lib/types/admin/service.d.ts +56 -1
  188. package/lib/types/admin/session-log.d.ts +21 -0
  189. package/lib/types/attachment-rules.d.ts +26 -3
  190. package/lib/types/client/QaAccountsController.d.ts +64 -5
  191. package/lib/types/client/QaSessionController.d.ts +24 -2
  192. package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
  193. package/lib/types/client/admin/routes.d.ts +2 -0
  194. package/lib/types/client/admin/shared.d.ts +2 -0
  195. package/lib/types/client/attachments.d.ts +21 -4
  196. package/lib/types/client/base64.d.ts +13 -1
  197. package/lib/types/client/components/QaAgentsDrawer.d.ts +2 -1
  198. package/lib/types/client/components/QaFilesPanel.d.ts +16 -4
  199. package/lib/types/client/components/QaHeader.d.ts +9 -3
  200. package/lib/types/client/components/QaModal.d.ts +4 -3
  201. package/lib/types/client/components/QaWidthHandle.d.ts +48 -2
  202. package/lib/types/client/components/QaWorkspaceBrowser.d.ts +15 -0
  203. package/lib/types/client/index.d.ts +1 -1
  204. package/lib/types/client/kiosk.d.ts +34 -0
  205. package/lib/types/client/lineage.d.ts +42 -0
  206. package/lib/types/client/markdown/blocks.d.ts +14 -1
  207. package/lib/types/client/markdown/inline.d.ts +23 -7
  208. package/lib/types/client/markdown/katex-css.d.ts +11 -0
  209. package/lib/types/client/markdown/math.d.ts +23 -0
  210. package/lib/types/client/markdown/render.d.ts +2 -1
  211. package/lib/types/client/settings/sections/notes.d.ts +12 -0
  212. package/lib/types/client/settings/sections.d.ts +1 -0
  213. package/lib/types/client/settlement.d.ts +4 -1
  214. package/lib/types/client/styles.d.ts +15 -2
  215. package/lib/types/client/types.d.ts +120 -1
  216. package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
  217. package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
  218. package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
  219. package/lib/types/client/user-settings/copy.d.ts +10 -1
  220. package/lib/types/config-resolvers/integration.d.ts +47 -0
  221. package/lib/types/config-resolvers/notes.d.ts +10 -0
  222. package/lib/types/config.d.ts +10 -0
  223. package/lib/types/index.d.ts +111 -1
  224. package/lib/types/integration/answer.d.ts +65 -0
  225. package/lib/types/integration/attachments.d.ts +85 -0
  226. package/lib/types/integration/contract.d.ts +196 -0
  227. package/lib/types/integration/host-runner.d.ts +44 -0
  228. package/lib/types/integration/http.d.ts +79 -0
  229. package/lib/types/integration/service.d.ts +120 -0
  230. package/lib/types/integration/transcript-reader.d.ts +100 -0
  231. package/lib/types/integration/transcript.d.ts +59 -0
  232. package/lib/types/personal-skills/index.d.ts +2 -2
  233. package/lib/types/personal-skills/paths.d.ts +15 -3
  234. package/lib/types/personal-skills/provider.d.ts +14 -2
  235. package/lib/types/personal-skills/service.d.ts +89 -14
  236. package/lib/types/prompt-notes.d.ts +37 -0
  237. package/lib/types/provenance/document-preview.d.ts +28 -0
  238. package/lib/types/provenance/file-preview.d.ts +69 -2
  239. package/lib/types/qa-tools/catalog.d.ts +1 -1
  240. package/lib/types/qa-tools/docs-tools.d.ts +160 -0
  241. package/lib/types/qa-tools/file-delete-gate.d.ts +42 -0
  242. package/lib/types/qa-tools/file-delete.d.ts +32 -0
  243. package/lib/types/secure-session.d.ts +52 -1
  244. package/lib/types/shared/documents.d.ts +20 -0
  245. package/lib/types/shared/integration-tokens.d.ts +24 -0
  246. package/lib/types/types.d.ts +263 -3
  247. package/lib/types/ui-mode.d.ts +18 -0
  248. package/lib/ui-mode.js +22 -0
  249. package/lib/ui-mode.js.map +1 -0
  250. package/package.json +12 -10
@@ -0,0 +1,65 @@
1
+ import type { StoredSessionEvent } from "../admin/conversation-log.js";
2
+ /**
3
+ * Reading one answered turn out of a session log.
4
+ *
5
+ * The log is the durable record of the conversation, and it is what the
6
+ * integration API publishes: the live assistant stream is a presentation
7
+ * channel that can be abandoned mid-attempt, while `assistant/message` events
8
+ * are what was actually committed. A turn with tools in it commits several
9
+ * assistant messages — the intermediate steps carry tool calls and usually no
10
+ * prose — so the answer is the *last one that has text*, not simply the last
11
+ * one written. If a later empty message follows a text one, publishing the
12
+ * empty message would answer the bridge with nothing.
13
+ */
14
+ /** The answer of one settled turn, plus how the turn ended. */
15
+ export interface QaTurnAnswer {
16
+ /** The publishable text; empty when the turn committed no prose at all. */
17
+ readonly answer: string;
18
+ /** True when the last committed assistant message was interrupted. */
19
+ readonly interrupted: boolean;
20
+ /** Durable seq of the message the answer came from, when there was one. */
21
+ readonly seq: number | null;
22
+ }
23
+ /** The empty answer: what a turn that committed nothing readable looks like. */
24
+ export declare const QA_EMPTY_ANSWER: QaTurnAnswer;
25
+ /**
26
+ * Project the answer of the turn that starts after `afterSeq`.
27
+ *
28
+ * Only events after the caller's own prompt are read: a chat is continued
29
+ * across questions, and picking the last assistant message of the whole log
30
+ * would hand question #2 the answer to question #1 whenever the new turn
31
+ * committed nothing.
32
+ *
33
+ * @param events - durable events, in any order.
34
+ * @param afterSeq - the seq of the prompt whose answer is wanted.
35
+ * @returns the publishable answer and whether the turn was interrupted.
36
+ */
37
+ export declare function answerAfter(events: readonly StoredSessionEvent[], afterSeq: number): QaTurnAnswer;
38
+ /**
39
+ * Cut an answer down to the size the caller publishes.
40
+ *
41
+ * The bridge posts the answer into a ticket comment, which has a size budget
42
+ * of its own: an over-long comment is not rejected there, it is cut — and a cut
43
+ * that lands in the middle of a sentence, or worse inside a code block, reads
44
+ * like a broken answer. The cut therefore prefers the last paragraph break,
45
+ * then the last line, then the last sentence, and only then an arbitrary word
46
+ * boundary, and it appends an ellipsis so the reader sees that text is missing
47
+ * rather than finished. A boundary that would leave less than half the budget
48
+ * used is refused: a single early paragraph break must not turn a full answer
49
+ * into one line.
50
+ * @param answer - the settled answer, already trimmed.
51
+ * @param maxCharacters - the publish budget; a non-positive value never cuts.
52
+ * @returns the answer, or its bounded head.
53
+ */
54
+ export declare function boundAnswer(answer: string, maxCharacters: number): string;
55
+ /**
56
+ * The seq of the newest user-authored message in a log, which is the floor
57
+ * `answerAfter` reads from when the caller has just submitted a prompt.
58
+ * Synthetic context (injected notes, skill payloads) is skipped: it is model
59
+ * input recorded as a user message, and one arriving after the answer would
60
+ * otherwise hide it.
61
+ * @param events - durable events, in any order.
62
+ * @returns the seq, or 0 when the log holds no human prompt yet.
63
+ */
64
+ export declare function lastPromptSeq(events: readonly StoredSessionEvent[]): number;
65
+ //# sourceMappingURL=answer.d.ts.map
@@ -0,0 +1,85 @@
1
+ import type { PromptContentPart } from "@deepseek-ai/dsh-api-session-controller";
2
+ import type { DocumentsFace } from "@yadsh/dsh-documents";
3
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
+ import { QaIntegrationAttachmentError, type QaFileAttachment } from "./contract.js";
5
+ /**
6
+ * Bringing a ticket's attachments into the prompt.
7
+ *
8
+ * An image rides the prompt inline, because that is what the harness carries.
9
+ * A file does not: `PromptContentPart` names one as an opaque receipt minted by
10
+ * the harness's own upload service, which an external caller has no session to
11
+ * reach — and that service is what stores the copy the model later reads with
12
+ * its file tools. The bridge's non-image attachments are therefore read *here*
13
+ * and arrive as text: a text file is decoded, a document is extracted through
14
+ * the deployment's own document pipeline (the same runtime, providers and
15
+ * limits the `document_*` tools use), and the result joins the prompt as one
16
+ * more text part.
17
+ *
18
+ * That is a deliberate trade. It costs the fidelity of a format the pipeline
19
+ * cannot read — such a request is refused with the bridge's own fallback signal
20
+ * rather than answered with a question the model cannot see the attachment for.
21
+ * It buys a path that works in every deployment shape: nothing is written into
22
+ * a shared workspace, the staging directory is temporary and removed after the
23
+ * extraction, and no second converter is grown inside this plugin.
24
+ */
25
+ /**
26
+ * Plain-text formats the bridge sends; their bytes *are* the content, so
27
+ * decoding them needs no pipeline and cannot fail on a deployment.
28
+ */
29
+ export declare const QA_INTEGRATION_TEXT_MEDIA_TYPES: readonly string[];
30
+ /**
31
+ * Documents the deployment's pipeline is asked to extract. PDF and the OOXML
32
+ * family are what the bridge's own filter lets through; whether a particular
33
+ * one is readable is the pipeline's answer, not a list kept here.
34
+ */
35
+ export declare const QA_INTEGRATION_DOCUMENT_MEDIA_TYPES: readonly string[];
36
+ /** Total inlined attachment text per question, across every attachment. */
37
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS = 120000;
38
+ /** No single attachment may claim the whole budget. */
39
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_CHARS_PER_FILE = 60000;
40
+ /** A file name is a label in the prompt, not a path. */
41
+ export declare const QA_INTEGRATION_MAX_ATTACHMENT_NAME = 120;
42
+ /** Whether the bytes are the content: decoding needs only UTF-8. */
43
+ export declare function isIntegrationTextMediaType(mediaType: string): boolean;
44
+ /** Whether the attachment has to go through the document pipeline. */
45
+ export declare function isIntegrationDocumentMediaType(mediaType: string): boolean;
46
+ /**
47
+ * Reduce a caller-supplied file name to the label a prompt can carry.
48
+ *
49
+ * The name arrives from another system and is used as a log field, a heading in
50
+ * the prompt and the file name of one temporary file — so directory parts,
51
+ * traversal, control characters and an unbounded length all have to go before
52
+ * it is any of those. An empty result is named by its media type.
53
+ * @param raw - the name the caller sent.
54
+ * @param mediaType - its media type, for the fallback name.
55
+ * @returns a safe, non-empty label.
56
+ */
57
+ export declare function safeAttachmentName(raw: string, mediaType: string): string;
58
+ /** What the extraction needs from the Host, as a port. */
59
+ export interface QaAttachmentDependencies {
60
+ /** The document pipeline, or undefined on a deployment without it. */
61
+ readonly documents: DocumentsFace | undefined;
62
+ /** The chat the prompt belongs to; the pipeline records artifacts against it. */
63
+ readonly sessionId: string;
64
+ readonly signal: AbortSignal;
65
+ readonly logger: PluginLogger;
66
+ /** Total inlined characters for this question. */
67
+ readonly maxChars?: number;
68
+ }
69
+ /**
70
+ * Turn the request's file attachments into prompt text parts.
71
+ *
72
+ * The parts are appended after the question, in the order the caller sent them,
73
+ * and the question itself is untouched. A file the Host cannot read refuses the
74
+ * whole request: answering with the question but not its attachments would hand
75
+ * the model a question about material it never received, which is exactly what
76
+ * the bridge's 415 fallback exists to avoid.
77
+ *
78
+ * @param files - the non-image attachments of one request.
79
+ * @param deps - pipeline, session and budgets.
80
+ * @returns one text part per readable attachment, plus a note when the budget cut the list.
81
+ */
82
+ export declare function attachmentPromptParts(files: readonly QaFileAttachment[], deps: QaAttachmentDependencies): Promise<readonly PromptContentPart[]>;
83
+ /** Re-exported so the runner does not reach into the contract for one name. */
84
+ export { QaIntegrationAttachmentError };
85
+ //# sourceMappingURL=attachments.d.ts.map
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The integration API's wire contract: what an external application may post to
3
+ * `/qa/api/ask`, what it gets back, and the refusal vocabulary the HTTP layer
4
+ * maps onto status codes.
5
+ *
6
+ * The shape follows the ticket bridge specification the deployment was asked to
7
+ * implement (issue #177): a Markdown `answer`, the `sources` that back it, a
8
+ * `confidence` bucket, an `escalate` flag for "the assistant could not answer,
9
+ * send it to a specialist", and a `reason` for the logs. `chat_id` is the DSH
10
+ * session identity: sending it back as `session_id` continues the same
11
+ * conversation, which is the whole point of the bridge keeping it.
12
+ */
13
+ import type { QaSourceReference } from "../provenance/types.js";
14
+ /** Why one integration request was refused, in the bridge's own vocabulary. */
15
+ export type QaIntegrationReason = "unauthorized" | "forbidden" | "not-found" | "invalid-request" | "unsupported-media" | "payload-too-large" | "rate-limited" | "busy" | "unavailable" | "timeout";
16
+ /**
17
+ * HTTP status per refusal. The mapping is part of the contract, not a detail:
18
+ * the bridge retries on 5xx and 429, falls back to a bodyless JSON request on
19
+ * 415, and treats 401/403 as a configuration error it must not spin on.
20
+ */
21
+ export declare const QA_INTEGRATION_STATUS: Readonly<Record<QaIntegrationReason, number>>;
22
+ /** One refusal, carrying the status the HTTP layer answers with. */
23
+ export declare class QaIntegrationError extends Error {
24
+ readonly reason: QaIntegrationReason;
25
+ constructor(reason: QaIntegrationReason, message: string, options?: {
26
+ readonly cause?: unknown;
27
+ });
28
+ get status(): number;
29
+ }
30
+ /** One image the caller attached, already bounded and decoded to base64. */
31
+ export interface QaInlineImageAttachment {
32
+ readonly kind: "image";
33
+ /** A raster media type; anything else is refused before it gets here. */
34
+ readonly mediaType: string;
35
+ /** base64 bytes, exactly as the harness prompt content carries them. */
36
+ readonly data: string;
37
+ readonly name?: string;
38
+ }
39
+ /**
40
+ * One non-image file the caller attached, still as bytes.
41
+ *
42
+ * Files cannot ride a prompt the way images do: the harness carries a file as
43
+ * an opaque receipt minted by its own upload service, which an external caller
44
+ * has no session to reach. What the bridge sends instead is read on the Host —
45
+ * text files decoded, documents extracted through the deployment's document
46
+ * pipeline — and the resulting text joins the prompt, which is the same
47
+ * material the model would have read out of the stored copy.
48
+ */
49
+ export interface QaFileAttachment {
50
+ readonly kind: "file";
51
+ readonly mediaType: string;
52
+ readonly name: string;
53
+ /** Raw bytes, bounded by the configured per-attachment ceiling. */
54
+ readonly bytes: Buffer;
55
+ }
56
+ /** One attachment of a parsed request, in the order the caller sent it. */
57
+ export type QaIntegrationAttachment = QaInlineImageAttachment | QaFileAttachment;
58
+ /**
59
+ * Why one attachment could not become prompt content, in the vocabulary the
60
+ * service maps onto HTTP.
61
+ *
62
+ * `unsupported` is the bridge's own fallback signal (415): repeat the question
63
+ * without attachments. `unavailable` is the same answer for a document the
64
+ * deployment cannot read right now, and the two are deliberately distinct only
65
+ * in the log — a caller sees one status either way.
66
+ */
67
+ export type QaIntegrationAttachmentReason = "unsupported" | "unavailable";
68
+ /** One attachment the Host could not turn into prompt content. */
69
+ export declare class QaIntegrationAttachmentError extends Error {
70
+ readonly attachment: string;
71
+ readonly reason: QaIntegrationAttachmentReason;
72
+ constructor(attachment: string, reason: QaIntegrationAttachmentReason, message: string);
73
+ }
74
+ /** A parsed `POST /qa/api/ask` body, whichever encoding carried it. */
75
+ export interface QaAskRequest {
76
+ /** The question; at least one non-whitespace character. */
77
+ readonly message: string;
78
+ /** Product version the bridge extracted from the ticket, when it knew one. */
79
+ readonly version: string | null;
80
+ /** The chat to continue, or null for a new one. */
81
+ readonly sessionId: string | null;
82
+ /** Ticket metadata the operator sees in the log line. */
83
+ readonly context: QaAskContext;
84
+ readonly attachments: readonly QaIntegrationAttachment[];
85
+ }
86
+ /** The ticket metadata the bridge sends along; all of it is optional. */
87
+ export interface QaAskContext {
88
+ readonly ticketKey: string | null;
89
+ readonly reporter: string | null;
90
+ readonly reporterName: string | null;
91
+ }
92
+ /** A successful answer, in the exact shape the bridge publishes. */
93
+ export interface QaAskAnswer {
94
+ readonly chatId: string;
95
+ readonly answer: string;
96
+ readonly sources: readonly string[];
97
+ readonly confidence: "high" | "medium" | "low";
98
+ /** True when the assistant produced nothing worth publishing. */
99
+ readonly escalate: boolean;
100
+ /** Why it escalated — for the bridge's log, never for a client. */
101
+ readonly reason: string;
102
+ }
103
+ /**
104
+ * One message of a conversation as an external caller reads it.
105
+ *
106
+ * The text is the same flattening the answer itself uses (prose only, with
107
+ * attachments named), so a caller that keeps asking questions and a caller
108
+ * that reads back the transcript never see two versions of one message.
109
+ */
110
+ export interface QaIntegrationMessage {
111
+ /** Durable position of the message; the cursor for the next read. */
112
+ readonly seq: number;
113
+ readonly role: "user" | "assistant";
114
+ readonly text: string;
115
+ /** ISO timestamp, when the log recorded one for the event. */
116
+ readonly at?: string;
117
+ }
118
+ /**
119
+ * A conversation as the caller that owns it reads it back.
120
+ *
121
+ * `sessions:read` exists for exactly this: a bridge that escalated a question,
122
+ * or that wants to show the specialist what was already said, reads the chat
123
+ * it started without asking it again. The read is bounded from both ends —
124
+ * `after` is the caller's own cursor, the newest `limit` messages are the
125
+ * window — so a long conversation costs one page, not one transcript per call.
126
+ */
127
+ export interface QaIntegrationTranscript {
128
+ readonly chatId: string;
129
+ /** The window, oldest first. */
130
+ readonly messages: readonly QaIntegrationMessage[];
131
+ /** Newest seq the caller has now seen; 0 when the chat holds no messages. */
132
+ readonly lastSeq: number;
133
+ /** True when older messages exist below the window. */
134
+ readonly truncated: boolean;
135
+ }
136
+ /** `GET /qa/api/health` payload; the bridge only needs `ok`. */
137
+ export interface QaHealth {
138
+ readonly ok: boolean;
139
+ readonly version: string;
140
+ readonly models: readonly string[];
141
+ readonly uptimeS: number;
142
+ }
143
+ /**
144
+ * What one answered turn hands back to the service: the answer text, the
145
+ * sources that back it and whether the turn was cut short. The runner owns the
146
+ * session mechanics; nothing about sessions leaks into the wire contract.
147
+ */
148
+ export interface QaIntegrationTurn {
149
+ readonly chatId: string;
150
+ readonly answer: string;
151
+ readonly sources: readonly QaSourceReference[];
152
+ /** True when the turn was cancelled or the log recorded an interruption. */
153
+ readonly interrupted: boolean;
154
+ }
155
+ /** The one host capability the integration service needs, as a port. */
156
+ export interface QaIntegrationRunner {
157
+ /**
158
+ * Send one question into a chat and resolve once its turn has settled.
159
+ *
160
+ * Implementations must enforce that `userId` may use `chatId` — the service
161
+ * has already checked the credential, and whoever owns session creation owns
162
+ * the ownership boundary that goes with it.
163
+ */
164
+ run(input: {
165
+ readonly userId: string;
166
+ /** The chat to continue, or null for a new conversation. */
167
+ readonly chatId: string | null;
168
+ readonly message: string;
169
+ readonly attachments: readonly QaIntegrationAttachment[];
170
+ readonly ticketKey: string | null;
171
+ readonly signal: AbortSignal;
172
+ /**
173
+ * Called as soon as the chat exists, before the turn settles. The service
174
+ * needs the identity of a chat whose answer it may never see: a timed-out
175
+ * question still has to hand the bridge a chat it can continue.
176
+ */
177
+ readonly onChat?: (chatId: string) => void;
178
+ }): Promise<QaIntegrationTurn>;
179
+ /**
180
+ * Read back the messages of one conversation.
181
+ *
182
+ * The service has already authenticated the credential and established that
183
+ * the chat belongs to that account; the implementation reads the log and
184
+ * projects it, and reports an unreadable log rather than an empty answer.
185
+ */
186
+ transcript(input: {
187
+ readonly chatId: string;
188
+ /** Exclusive lower bound: only messages after this seq are returned. */
189
+ readonly after: number;
190
+ /** Newest messages to return; older ones are reported as truncated. */
191
+ readonly limit: number;
192
+ }): Promise<QaIntegrationTranscript>;
193
+ /** Model ids the deployment can route right now, for the health payload. */
194
+ models(): Promise<readonly string[]>;
195
+ }
196
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,44 @@
1
+ import type { Context } from "@deepseek-ai/cordis";
2
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
3
+ import type { QaAccessService } from "../access/service.js";
4
+ import type { QaAccounts } from "../accounts/store.js";
5
+ import type { QaSessionLogReader } from "../admin/session-log.js";
6
+ import type { QaProvenanceHost } from "../provenance/host-store.js";
7
+ import type { QaPolicyAdmission } from "../secure-session.js";
8
+ import type { ResolvedQaSurfaceConfig } from "../types.js";
9
+ import type { DocumentsFace } from "@yadsh/dsh-documents";
10
+ import type { QaIntegrationRunner } from "./contract.js";
11
+ /**
12
+ * The session half of the integration API: open a chat, send one question,
13
+ * wait for the turn, read the answer.
14
+ *
15
+ * It is deliberately not a new turn engine. Opening a chat goes through the
16
+ * same three steps the browser's `createSession` takes — the deployment
17
+ * preflight, the per-user workspace, then `secureSession` — because that last
18
+ * call is what pins the QA tool policy, attaches the QA catalog and records
19
+ * the chat as attested. A question is admitted with
20
+ * `sessionController.prompt`, the same call the browser makes; the wait is
21
+ * `agent.whenIdle()`, which is the harness's own definition of "this turn is
22
+ * over"; the answer is read back from the durable log rather than from the
23
+ * live assistant stream, so what the bridge publishes is what was committed.
24
+ */
25
+ export interface QaIntegrationRunnerOptions {
26
+ ctx: Context;
27
+ getConfig(): ResolvedQaSurfaceConfig;
28
+ /** The accounts store, or undefined while accounts are disabled. */
29
+ accounts(): QaAccounts | undefined;
30
+ admission: QaPolicyAdmission;
31
+ access: QaAccessService;
32
+ sessionLog: QaSessionLogReader;
33
+ provenance: QaProvenanceHost;
34
+ /**
35
+ * The deployment's document pipeline, resolved per call so a provider that
36
+ * installs later still counts. Absent on a deployment that has none, where
37
+ * document attachments are refused with the caller's own fallback signal.
38
+ */
39
+ documents(): DocumentsFace | undefined;
40
+ logger: PluginLogger;
41
+ }
42
+ /** Build the runner for one plugin instance. */
43
+ export declare function createQaIntegrationRunner(options: QaIntegrationRunnerOptions): QaIntegrationRunner;
44
+ //# sourceMappingURL=host-runner.d.ts.map
@@ -0,0 +1,79 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { WebServer } from "@deepseek-ai/dsh-host-webserver";
3
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
+ import type { ResolvedQaSurfaceConfig } from "../types.js";
5
+ import { type QaAskRequest } from "./contract.js";
6
+ import type { QaIntegrationService } from "./service.js";
7
+ /** The bridge's own ceiling: five attachments per question. */
8
+ export declare const QA_INTEGRATION_MAX_FILES = 5;
9
+ export interface QaIntegrationRouteOptions {
10
+ config: ResolvedQaSurfaceConfig;
11
+ service: QaIntegrationService;
12
+ logger: PluginLogger;
13
+ }
14
+ /**
15
+ * Register the integration routes on the host web server.
16
+ * @param webServer - the route table owner.
17
+ * @param options - resolved config, the service and the plugin logger.
18
+ * @returns the disposer removing every route this call registered.
19
+ */
20
+ export declare function registerQaIntegrationRoutes(webServer: Pick<WebServer, "register">, options: QaIntegrationRouteOptions): () => void;
21
+ /** The page size a caller gets when it does not ask for one. */
22
+ export declare const QA_INTEGRATION_DEFAULT_TRANSCRIPT_LIMIT = 50;
23
+ /**
24
+ * Parse one `POST /ask` body out of whichever encoding carried it.
25
+ *
26
+ * JSON is the primary form. Multipart exists because the bridge sends the
27
+ * ticket's attachments through it, and images ride a prompt inline in this
28
+ * harness — so an image-only multipart request is answered like a JSON one. A
29
+ * non-image attachment has nowhere to go: files reach a prompt as a receipt
30
+ * minted by the browser upload path, which an external caller has no session
31
+ * to mint. That request is refused with 415, which is exactly the signal the
32
+ * bridge's own fallback waits for: it repeats the question without
33
+ * attachments rather than failing.
34
+ *
35
+ * @param body - the raw request body.
36
+ * @param contentType - the request's content type, parameters included.
37
+ * @param config - resolved config, for the attachment ceilings.
38
+ * @returns the normalized request.
39
+ */
40
+ export declare function parseAskBody(body: Buffer, contentType: string | undefined, config: ResolvedQaSurfaceConfig): QaAskRequest;
41
+ /** One image part of a multipart body, bounded as it was read. */
42
+ interface MultipartFile {
43
+ readonly filename: string;
44
+ readonly mediaType: string;
45
+ readonly bytes: Buffer;
46
+ }
47
+ interface MultipartBody {
48
+ readonly fields: ReadonlyMap<string, string>;
49
+ readonly files: readonly MultipartFile[];
50
+ }
51
+ /**
52
+ * Split a multipart body into its text fields and files.
53
+ *
54
+ * Hand-written and deliberately minimal: one request shape (a flat form of
55
+ * scalar fields and `files` parts), one boundary, no nested multiparts. The
56
+ * body is already bounded by the caller, so no part can grow without limit.
57
+ *
58
+ * @param body - the raw request body.
59
+ * @param boundary - the boundary parameter of the content type.
60
+ * @returns text fields and file parts, in the order they appeared.
61
+ */
62
+ export declare function parseMultipart(body: Buffer, boundary: string): MultipartBody;
63
+ /**
64
+ * Read the whole request body under a byte ceiling.
65
+ * @param request - the incoming request.
66
+ * @param limit - accepted body size in bytes.
67
+ * @returns the body.
68
+ */
69
+ export declare function readBody(request: IncomingMessage, limit: number): Promise<Buffer>;
70
+ /**
71
+ * One refusal, mapped onto its status.
72
+ *
73
+ * An unexpected error is answered as `unavailable` rather than leaking a stack
74
+ * trace: the bridge retries a 5xx, and the deployment's own log holds the
75
+ * detail.
76
+ */
77
+ export declare function sendRefusal(response: ServerResponse, error: unknown): void;
78
+ export {};
79
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1,120 @@
1
+ import type { PluginLogger } from "@yadsh/dsh-plugin-log";
2
+ import type { QaAccounts } from "../accounts/store.js";
3
+ import { type QaServiceTokenScope } from "../accounts/service-token.js";
4
+ import type { QaSourceReference } from "../provenance/types.js";
5
+ import type { ResolvedQaSurfaceConfig } from "../types.js";
6
+ import { type QaAskAnswer, type QaAskRequest, type QaHealth, type QaIntegrationRunner, type QaIntegrationTranscript } from "./contract.js";
7
+ /**
8
+ * The integration API's brain: credential, budget, and one answered turn.
9
+ *
10
+ * The service owns everything that must be identical for every transport —
11
+ * who is allowed to ask, how often, how many at once, what a refusal means and
12
+ * how an answered turn is shaped into the contract. The HTTP layer owns bytes
13
+ * and status codes, the runner owns sessions and agents. That split is what
14
+ * lets the mechanics be tested without a web server, a model or a database.
15
+ *
16
+ * Two limits, deliberately different in kind. Concurrency (`maxConcurrent`)
17
+ * protects the deployment: a bridge that fires a hundred questions must not
18
+ * open a hundred agent turns. Rate (`requestsPerMinute`) protects the account:
19
+ * one token cannot spend the deployment's whole budget. Both refuse with 429,
20
+ * which the bridge already retries.
21
+ */
22
+ /** The bridge publishes a handful of citations; more is noise in a Jira note. */
23
+ export declare const QA_INTEGRATION_MAX_CITATIONS = 5;
24
+ /**
25
+ * One citation string per source, in the bridge's own style: the document plus
26
+ * where in it the evidence sits, so a reader can open the right page.
27
+ *
28
+ * @param sources - the turn's evidence, best first.
29
+ * @returns bounded citation strings.
30
+ */
31
+ export declare function sourceCitations(sources: readonly QaSourceReference[]): readonly string[];
32
+ /** Who a presented credential turned out to be. */
33
+ export interface QaIntegrationIdentity {
34
+ readonly tokenId: string;
35
+ readonly userId: string;
36
+ readonly scopes: readonly QaServiceTokenScope[];
37
+ }
38
+ export interface QaIntegrationServiceOptions {
39
+ getConfig(): ResolvedQaSurfaceConfig;
40
+ /** The accounts store, or undefined while accounts are disabled. */
41
+ accounts(): QaAccounts | undefined;
42
+ runner: QaIntegrationRunner;
43
+ logger: PluginLogger;
44
+ /** Package version the health payload reports. */
45
+ version: string;
46
+ now?(): number;
47
+ }
48
+ export declare class QaIntegrationService {
49
+ private readonly deps;
50
+ private readonly now;
51
+ private readonly startedAt;
52
+ /** Recent request timestamps per token id, pruned to the rolling minute. */
53
+ private readonly recent;
54
+ private inFlight;
55
+ constructor(deps: QaIntegrationServiceOptions);
56
+ /** Requests currently inside `ask`; the console shows this as load. */
57
+ get busy(): number;
58
+ /**
59
+ * Resolve a bearer credential to its token, or refuse.
60
+ *
61
+ * A malformed, unknown, expired, revoked or disabled-account token is one
62
+ * answer — 401 — because telling them apart would let an unauthenticated
63
+ * caller enumerate token ids. A valid token missing the scope is 403: the
64
+ * caller proved who it is, and the refusal is about rights, not identity.
65
+ */
66
+ authenticate(authorization: string | undefined, required: QaServiceTokenScope | null): QaIntegrationIdentity;
67
+ /**
68
+ * Answer one question, or refuse before a turn is ever opened.
69
+ *
70
+ * @param authorization - the raw `Authorization` header value.
71
+ * @param request - the parsed body.
72
+ * @param signal - the request's own lifetime; a dropped connection stops the wait.
73
+ * @returns the answer the bridge publishes.
74
+ */
75
+ ask(authorization: string | undefined, request: QaAskRequest, signal: AbortSignal): Promise<QaAskAnswer>;
76
+ /**
77
+ * Read one conversation back for the application that started it.
78
+ *
79
+ * This is what `sessions:read` is for. A bridge that escalated a question,
80
+ * or that has to show a specialist what was already said, reads the chat it
81
+ * owns instead of asking the same question again — which would spend a turn
82
+ * and produce a second answer to a question that already has one.
83
+ *
84
+ * The ownership rule is the same one `ask` applies when it continues a chat:
85
+ * the account the token belongs to has to own the conversation. A chat that
86
+ * does not exist and a chat that belongs to somebody else get one answer, so
87
+ * an id alone never confirms that another account's conversation exists.
88
+ *
89
+ * @param authorization - the raw `Authorization` header value.
90
+ * @param query - the chat, the caller's cursor and the page size.
91
+ * @returns the window of messages, with the caller's new cursor.
92
+ */
93
+ session(authorization: string | undefined, query: {
94
+ readonly chatId: string;
95
+ readonly after: number;
96
+ readonly limit: number;
97
+ }): Promise<QaIntegrationTranscript>;
98
+ /**
99
+ * Liveness and routing facts for the bridge's own UI.
100
+ * @param authorization - the raw `Authorization` header value.
101
+ * @returns the health payload; `ok` is true whenever it is answered at all.
102
+ */
103
+ health(authorization: string | undefined): Promise<QaHealth>;
104
+ /** One refusal shaped as an answer: empty text, escalated, no citations. */
105
+ private escalated;
106
+ /** Refuse with `rate-limited` once a token spends its minute's budget. */
107
+ private assertRate;
108
+ }
109
+ /**
110
+ * The credential in one `Authorization` value, or null.
111
+ *
112
+ * `Bearer` is matched case-insensitively, as HTTP authentication schemes are,
113
+ * and the token itself is never trimmed: whitespace inside a credential is
114
+ * part of it, and accepting a padded variant would accept a token the store
115
+ * never minted.
116
+ * @param authorization - the raw header, when the caller sent one.
117
+ * @returns the token, or null.
118
+ */
119
+ export declare function bearerToken(authorization: string | undefined): string | null;
120
+ //# sourceMappingURL=service.d.ts.map