@yadsh/dsh-qa-surface 0.7.3 → 0.8.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 (146) hide show
  1. package/README.md +56 -9
  2. package/lib/access/model.js +24 -4
  3. package/lib/access/model.js.map +1 -1
  4. package/lib/access/role-repository.js +199 -91
  5. package/lib/access/role-repository.js.map +1 -1
  6. package/lib/access/service.js +194 -6
  7. package/lib/access/service.js.map +1 -1
  8. package/lib/account-remotes.js +2 -4
  9. package/lib/account-remotes.js.map +1 -1
  10. package/lib/accounts/database.js +447 -0
  11. package/lib/accounts/database.js.map +1 -0
  12. package/lib/accounts/file.js +27 -90
  13. package/lib/accounts/file.js.map +1 -1
  14. package/lib/accounts/store.js +210 -59
  15. package/lib/accounts/store.js.map +1 -1
  16. package/lib/admin/quality-store.js +199 -54
  17. package/lib/admin/quality-store.js.map +1 -1
  18. package/lib/admin/service.js +39 -6
  19. package/lib/admin/service.js.map +1 -1
  20. package/lib/cli.js +2 -1
  21. package/lib/cli.js.map +1 -1
  22. package/lib/client/QaSessionController.js +20 -2
  23. package/lib/client/QaSessionController.js.map +1 -1
  24. package/lib/client/QaSurface.js +69 -31
  25. package/lib/client/QaSurface.js.map +1 -1
  26. package/lib/client/QaTranscriptAdapter.js +4 -0
  27. package/lib/client/QaTranscriptAdapter.js.map +1 -1
  28. package/lib/client/admin/QaAdmin.js +17 -7
  29. package/lib/client/admin/QaAdmin.js.map +1 -1
  30. package/lib/client/admin/{format.js → copy.js} +1 -1
  31. package/lib/client/admin/copy.js.map +1 -0
  32. package/lib/client/admin/pages/Conversations.js +1 -1
  33. package/lib/client/admin/pages/Conversations.js.map +1 -1
  34. package/lib/client/admin/pages/Overview.js +1 -1
  35. package/lib/client/admin/pages/Overview.js.map +1 -1
  36. package/lib/client/admin/pages/Quality.js +1 -1
  37. package/lib/client/admin/pages/Quality.js.map +1 -1
  38. package/lib/client/admin/pages/Review.js +1 -1
  39. package/lib/client/admin/pages/Review.js.map +1 -1
  40. package/lib/client/admin/pages/Users.js +6 -2
  41. package/lib/client/admin/pages/Users.js.map +1 -1
  42. package/lib/client/admin/shared.js +1 -1
  43. package/lib/client/admin/shared.js.map +1 -1
  44. package/lib/client/components/Markdown.js +14 -206
  45. package/lib/client/components/Markdown.js.map +1 -1
  46. package/lib/client/components/QaChangelog.js +60 -0
  47. package/lib/client/components/QaChangelog.js.map +1 -1
  48. package/lib/client/components/QaHeader.js +1 -3
  49. package/lib/client/components/QaHeader.js.map +1 -1
  50. package/lib/client/components/QaMessage.js +9 -4
  51. package/lib/client/components/QaMessage.js.map +1 -1
  52. package/lib/client/components/QaSidebar.js +5 -2
  53. package/lib/client/components/QaSidebar.js.map +1 -1
  54. package/lib/client/components/QaSourcesPanel.js +3 -18
  55. package/lib/client/components/QaSourcesPanel.js.map +1 -1
  56. package/lib/client/components/QaWorkGroup.js +4 -8
  57. package/lib/client/components/QaWorkGroup.js.map +1 -1
  58. package/lib/client/components/source-icon.js +26 -0
  59. package/lib/client/components/source-icon.js.map +1 -0
  60. package/lib/client/components/thinking-phrases.js +2 -11
  61. package/lib/client/components/thinking-phrases.js.map +1 -1
  62. package/lib/client/components/use-now.js +21 -0
  63. package/lib/client/components/use-now.js.map +1 -0
  64. package/lib/client/index.js +11 -3
  65. package/lib/client/index.js.map +1 -1
  66. package/lib/client/lineage.js +18 -0
  67. package/lib/client/lineage.js.map +1 -0
  68. package/lib/client/markdown/CodeBlock.js +28 -0
  69. package/lib/client/markdown/CodeBlock.js.map +1 -0
  70. package/lib/client/markdown/blocks.js +342 -0
  71. package/lib/client/markdown/blocks.js.map +1 -0
  72. package/lib/client/markdown/highlight.js +1089 -0
  73. package/lib/client/markdown/highlight.js.map +1 -0
  74. package/lib/client/markdown/inline.js +377 -0
  75. package/lib/client/markdown/inline.js.map +1 -0
  76. package/lib/client/markdown/render.js +147 -0
  77. package/lib/client/markdown/render.js.map +1 -0
  78. package/lib/client/markdown/source-chip.js +55 -0
  79. package/lib/client/markdown/source-chip.js.map +1 -0
  80. package/lib/client/markdown/use-transient-flag.js +27 -0
  81. package/lib/client/markdown/use-transient-flag.js.map +1 -0
  82. package/lib/client/role/RoleSelector.js +11 -2
  83. package/lib/client/role/RoleSelector.js.map +1 -1
  84. package/lib/client/role/preview.js +123 -0
  85. package/lib/client/role/preview.js.map +1 -0
  86. package/lib/client/styles.js +73 -19
  87. package/lib/client/styles.js.map +1 -1
  88. package/lib/client.js +3137 -761
  89. package/lib/client.js.map +1 -1
  90. package/lib/config-resolvers/accounts.js +15 -0
  91. package/lib/config-resolvers/accounts.js.map +1 -1
  92. package/lib/config-resolvers/defaults.js +8 -0
  93. package/lib/config-resolvers/defaults.js.map +1 -1
  94. package/lib/config-resolvers/sources.js +16 -0
  95. package/lib/config-resolvers/sources.js.map +1 -1
  96. package/lib/config.js +57 -0
  97. package/lib/config.js.map +1 -1
  98. package/lib/index.js +25 -10
  99. package/lib/index.js.map +1 -1
  100. package/lib/lockdown-policy.js +19 -0
  101. package/lib/lockdown-policy.js.map +1 -1
  102. package/lib/provenance/host-store.js +7 -3
  103. package/lib/provenance/host-store.js.map +1 -1
  104. package/lib/provenance/retention.js +25 -0
  105. package/lib/provenance/retention.js.map +1 -0
  106. package/lib/provenance/snapshot-store.js +290 -35
  107. package/lib/provenance/snapshot-store.js.map +1 -1
  108. package/lib/questions.js +16 -3
  109. package/lib/questions.js.map +1 -1
  110. package/lib/secure-session.js +136 -23
  111. package/lib/secure-session.js.map +1 -1
  112. package/lib/typert.host.js +79 -52
  113. package/lib/typert.remote-client.js +75 -48
  114. package/lib/types/access/role-repository.d.ts +38 -7
  115. package/lib/types/access/service.d.ts +77 -3
  116. package/lib/types/account-remotes.d.ts +8 -4
  117. package/lib/types/accounts/database.d.ts +84 -0
  118. package/lib/types/accounts/file.d.ts +17 -39
  119. package/lib/types/accounts/store.d.ts +98 -19
  120. package/lib/types/admin/quality-store.d.ts +20 -2
  121. package/lib/types/client/admin/{format.d.ts → copy.d.ts} +1 -1
  122. package/lib/types/client/components/Markdown.d.ts +7 -8
  123. package/lib/types/client/components/QaSidebar.d.ts +3 -1
  124. package/lib/types/client/components/QaSourcesPanel.d.ts +1 -3
  125. package/lib/types/client/components/source-icon.d.ts +12 -0
  126. package/lib/types/client/components/use-now.d.ts +8 -0
  127. package/lib/types/client/lineage.d.ts +15 -0
  128. package/lib/types/client/markdown/CodeBlock.d.ts +14 -0
  129. package/lib/types/client/markdown/blocks.d.ts +74 -0
  130. package/lib/types/client/markdown/highlight.d.ts +29 -0
  131. package/lib/types/client/markdown/inline.d.ts +40 -0
  132. package/lib/types/client/markdown/render.d.ts +13 -0
  133. package/lib/types/client/markdown/source-chip.d.ts +32 -0
  134. package/lib/types/client/markdown/use-transient-flag.d.ts +12 -0
  135. package/lib/types/client/role/RoleSelector.d.ts +11 -1
  136. package/lib/types/client/role/preview.d.ts +109 -0
  137. package/lib/types/index.d.ts +5 -1
  138. package/lib/types/lockdown-policy.d.ts +16 -0
  139. package/lib/types/provenance/host-store.d.ts +7 -3
  140. package/lib/types/provenance/retention.d.ts +46 -0
  141. package/lib/types/provenance/snapshot-store.d.ts +63 -7
  142. package/lib/types/questions.d.ts +5 -0
  143. package/lib/types/secure-session.d.ts +27 -1
  144. package/lib/types/types.d.ts +54 -0
  145. package/package.json +3 -3
  146. package/lib/client/admin/format.js.map +0 -1
@@ -0,0 +1,74 @@
1
+ /**
2
+ * GFM block grammar for the chat renderer: the container and leaf blocks the
3
+ * assistant's answers actually use, parsed here and handed to `render.tsx` as
4
+ * plain data. Text inside a leaf block stays raw Markdown — `parseInline`
5
+ * resolves it at render time — so one block pass feeds every inline consumer.
6
+ *
7
+ * The grammar is deliberately a subset of CommonMark: nested containers and
8
+ * lazy continuation are supported, while the pathological corners (HTML
9
+ * blocks, setext inside lists, link reference definitions spanning lines) stay
10
+ * out. Raw HTML never becomes a block: a tag line is a paragraph whose inline
11
+ * pass leaves the tag as literal text.
12
+ */
13
+ /** `#`-through-`######` heading levels. */
14
+ export type HeadingDepth = 1 | 2 | 3 | 4 | 5 | 6;
15
+ /** Column alignment carried by a table's delimiter row. */
16
+ export type TableAlign = "left" | "center" | "right";
17
+ /** One list item: its own blocks plus the checkbox a task item declares. */
18
+ export interface MarkdownListItem {
19
+ readonly blocks: readonly MarkdownBlock[];
20
+ readonly task: boolean;
21
+ readonly checked: boolean;
22
+ }
23
+ /** A block-level node; `text` fields hold raw inline Markdown. */
24
+ export type MarkdownBlock = {
25
+ readonly kind: "paragraph";
26
+ readonly text: string;
27
+ } | {
28
+ readonly kind: "heading";
29
+ readonly depth: HeadingDepth;
30
+ readonly text: string;
31
+ } | {
32
+ readonly kind: "code";
33
+ readonly text: string;
34
+ readonly lang?: string;
35
+ } | {
36
+ readonly kind: "quote";
37
+ readonly blocks: readonly MarkdownBlock[];
38
+ } | {
39
+ readonly kind: "list";
40
+ readonly ordered: boolean;
41
+ readonly start: number;
42
+ readonly loose: boolean;
43
+ readonly items: readonly MarkdownListItem[];
44
+ } | {
45
+ readonly kind: "table";
46
+ readonly align: readonly TableAlign[];
47
+ readonly head: readonly string[];
48
+ readonly rows: readonly (readonly string[])[];
49
+ } | {
50
+ readonly kind: "rule";
51
+ };
52
+ /** A parsed document: its blocks plus the reference targets they resolve. */
53
+ export interface ParsedMarkdown {
54
+ readonly blocks: readonly MarkdownBlock[];
55
+ /** Link/image definitions by upper-cased identifier. */
56
+ readonly definitions: ReadonlyMap<string, string>;
57
+ }
58
+ /** Split one table row into trimmed cells, honoring escaped pipes. */
59
+ export declare function splitTableRow(line: string): string[];
60
+ /**
61
+ * Parse a whole document.
62
+ * @param text - Markdown source with any line endings.
63
+ * @returns Blocks plus the definitions the inline pass resolves against.
64
+ */
65
+ export declare function parseMarkdown(text: string): ParsedMarkdown;
66
+ /**
67
+ * Parse the blocks of one container (document, quote, or list item).
68
+ * @param lines - Container lines, already de-indented by the caller.
69
+ * @param definitions - Document-wide definition accumulator.
70
+ * @param depth - Container nesting depth.
71
+ * @returns The container's blocks.
72
+ */
73
+ export declare function parseBlocks(lines: readonly string[], definitions: Map<string, string>, depth: number): MarkdownBlock[];
74
+ //# sourceMappingURL=blocks.d.ts.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The chat renderer's syntax highlighter: a small tokenizer over per-language
3
+ * keyword/comment/string tables, not a grammar engine. The original UI runs
4
+ * shiki; its grammar modules alone cost ~1.6 MB, which a self-contained plugin
5
+ * bundle cannot carry, so this scanner covers the shapes that carry meaning in
6
+ * an answer — comments, strings, numbers, keywords — and leaves the rest of the
7
+ * line in the foreground color. Colors come from the host theme's
8
+ * `--shiki-token-*` custom properties, so a fenced block here matches a fenced
9
+ * block in the assistant's own transcript in both light and dark themes.
10
+ *
11
+ * An unknown or absent language renders plain: never an error, never a guess.
12
+ */
13
+ /** Token categories the stylesheet colors. */
14
+ export type HighlightClass = "comment" | "string" | "keyword" | "number" | "constant" | "meta" | "tag" | "attr" | "key" | "insert" | "delete" | "hunk";
15
+ /** One colored run of a code line. */
16
+ export interface HighlightSpan {
17
+ readonly text: string;
18
+ readonly cls?: HighlightClass;
19
+ }
20
+ /** One code line as colored runs. */
21
+ export type HighlightLine = readonly HighlightSpan[];
22
+ /**
23
+ * Highlight one fenced block.
24
+ * @param code - The fence's source text.
25
+ * @param lang - The fence's info string.
26
+ * @returns One {@link HighlightLine} per source line.
27
+ */
28
+ export declare function highlightLines(code: string, lang: string | undefined): HighlightLine[];
29
+ //# sourceMappingURL=highlight.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GFM inline grammar for the chat renderer: code spans, emphasis, links,
3
+ * images, autolinks, and the two line-break flavors. Emphasis follows the
4
+ * CommonMark intent (a closer must match its opener's delimiter run, `_` never
5
+ * opens inside a word) rather than the full delimiter-stack algorithm — the
6
+ * shapes this grammar cannot express are the pathological ones, and they
7
+ * degrade to literal text instead of mangling the sentence.
8
+ */
9
+ /** One inline node; `children` nodes nest. */
10
+ export type MarkdownInline = {
11
+ readonly kind: "text";
12
+ readonly value: string;
13
+ } | {
14
+ readonly kind: "softbreak";
15
+ } | {
16
+ readonly kind: "break";
17
+ } | {
18
+ readonly kind: "code";
19
+ readonly value: string;
20
+ } | {
21
+ readonly kind: "strong" | "em" | "del";
22
+ readonly children: readonly MarkdownInline[];
23
+ } | {
24
+ readonly kind: "link";
25
+ readonly href: string;
26
+ readonly children: readonly MarkdownInline[];
27
+ } | {
28
+ readonly kind: "image";
29
+ readonly src: string;
30
+ readonly alt: string;
31
+ };
32
+ /**
33
+ * Parse the inline content of one leaf block.
34
+ * @param text - Raw inline Markdown, newlines included.
35
+ * @param definitions - Link/image definitions collected by the block pass.
36
+ * @param depth - Inline nesting depth.
37
+ * @returns The inline nodes in document order.
38
+ */
39
+ export declare function parseInline(text: string, definitions: ReadonlyMap<string, string>, depth?: number): MarkdownInline[];
40
+ //# sourceMappingURL=inline.d.ts.map
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ import { type MarkdownSourceContext } from "./source-chip.js";
3
+ /**
4
+ * Render markdown source as semantic React elements. Raw HTML never enters the
5
+ * DOM (a tag is literal text), link and image destinations pass a protocol
6
+ * allowlist, and a source the owning message knows about becomes a chip
7
+ * instead of a plain link or code token.
8
+ * @param text - Assistant-authored markdown.
9
+ * @param context - Source resolution handed down by the owning message.
10
+ * @returns The document's blocks.
11
+ */
12
+ export declare function renderMarkdown(text: string, context: MarkdownSourceContext): ReactNode[];
13
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ import type { QaSource } from "../../types.js";
3
+ import { type QaSourceRefs } from "../components/source-refs.js";
4
+ /** Inline source awareness handed down from the owning assistant message. */
5
+ export interface MarkdownSourceContext {
6
+ readonly refs?: QaSourceRefs;
7
+ /** Open a path-backed source in the sources drawer's detail view. */
8
+ readonly onSourceOpen?: (source: QaSource) => void;
9
+ }
10
+ /**
11
+ * One inline source footnote: a pill over the matched link or path with a
12
+ * hover/focus preview card (title, target, snippet). A URL-backed source is
13
+ * an anchor that opens right away; a path-backed one is a button whose click
14
+ * the owner routes to the sources drawer. The hover card stays out of the
15
+ * chip's accessible name and rides along as its description instead.
16
+ */
17
+ export declare function SourceChip({ source, label, href, onOpen, }: {
18
+ readonly source: QaSource;
19
+ readonly label: string;
20
+ readonly href?: string;
21
+ readonly onOpen?: (source: QaSource) => void;
22
+ }): ReactNode;
23
+ /**
24
+ * The chip a path-backed inline-code token earns, or undefined when the token
25
+ * names no known source. A URL-backed token is a chip that opens right away.
26
+ */
27
+ export declare function codeChip(value: string, context: MarkdownSourceContext): ReactNode | undefined;
28
+ /** The chip a matched markdown link earns, or undefined when it matches none. */
29
+ export declare function linkChip(href: string, label: string, context: MarkdownSourceContext): ReactNode | undefined;
30
+ /** http(s)/mailto destinations only; images are narrowed further to http(s). */
31
+ export declare function safeHref(value: string): string | undefined;
32
+ //# sourceMappingURL=source-chip.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A boolean that turns on for a fixed window and drops back off — the
3
+ * "Скопировано" flash and its siblings. The pending timer lives in a ref and
4
+ * is cleared by the unmount cleanup, so a pulse fired right before the block
5
+ * leaves the DOM never schedules a state update on it, and re-pulsing restarts
6
+ * the window instead of stacking timers.
7
+ */
8
+ export declare function useTransientFlag(windowMs: number): {
9
+ readonly on: boolean;
10
+ readonly pulse: () => void;
11
+ };
12
+ //# sourceMappingURL=use-transient-flag.d.ts.map
@@ -8,7 +8,17 @@ export interface QaRoleSelectorProps {
8
8
  }
9
9
  /** Visible only when the account can actually choose between profiles. */
10
10
  export declare function QaRoleSelector(props: QaRoleSelectorProps): import("react/jsx-runtime").JSX.Element | null;
11
- export declare function QaAdminPreviewBanner({ role }: {
11
+ /**
12
+ * The banner that keeps a preview from being mistaken for an ordinary chat.
13
+ *
14
+ * It carries the way out as well: while previewing, the header's role selector
15
+ * is hidden — the previewed profile need not be one the account holds — so
16
+ * without a control here the only exit was the browser's Back button, and a new
17
+ * chat meanwhile ran as the previewed profile instead of the account's default.
18
+ * @param props - the previewed role's name, and how to leave the preview.
19
+ */
20
+ export declare function QaAdminPreviewBanner(props: {
12
21
  readonly role: string;
22
+ readonly onExit?: () => void;
13
23
  }): import("react/jsx-runtime").JSX.Element;
14
24
  //# sourceMappingURL=RoleSelector.d.ts.map
@@ -0,0 +1,109 @@
1
+ import type { QaSubrole } from "../../types.js";
2
+ /**
3
+ * The administrator preview mode, as a property of the history entry.
4
+ *
5
+ * "Preview as role" navigates to the chat route carrying a marker in the
6
+ * history state. Deriving the mode from the entry — instead of latching it in
7
+ * component state — is what lets the browser's own Back button leave the
8
+ * preview, and what keeps a preview from outliving its entry: while the mode
9
+ * was latched, every later new chat in that browser tab was created as a
10
+ * preview of the previewed profile, silently ignoring the account's default
11
+ * profile, with the role selector hidden and only a corner banner to say so.
12
+ */
13
+ /** One preview in force: the role and the name it was previewed under. */
14
+ export interface QaAdminPreview {
15
+ readonly roleId: string;
16
+ readonly name: string;
17
+ }
18
+ interface PreviewState {
19
+ readonly qaPreview: string;
20
+ readonly qaPreviewName: string;
21
+ }
22
+ /** The history state a "Preview as role" navigation carries. */
23
+ export declare function previewEntryState(role: Pick<QaSubrole, "id" | "name">): PreviewState;
24
+ /**
25
+ * The preview one history entry asks for, or undefined for an ordinary entry.
26
+ *
27
+ * Home of an older Host or of a cleared entry carries no marker at all, and a
28
+ * marker whose role id cannot be read is ignored rather than guessed: the name
29
+ * is cosmetic, so an entry written before names travelled with the marker
30
+ * still previews, under its role id.
31
+ */
32
+ export declare function readPreviewEntry(state: unknown): QaAdminPreview | undefined;
33
+ export type QaPreviewTransition = {
34
+ readonly kind: "enter";
35
+ readonly preview: QaAdminPreview;
36
+ } | {
37
+ readonly kind: "leave";
38
+ } | {
39
+ readonly kind: "keep";
40
+ };
41
+ /**
42
+ * What the surface should do with the preview mode for one history entry.
43
+ *
44
+ * Only an administrator can preview, so an entry that asks for one while an
45
+ * ordinary account is signed in leaves the mode instead of entering it — the
46
+ * marker survives in the browser's history across a sign-out, and latching it
47
+ * again would hand the next account a chat under a profile it was never
48
+ * assigned.
49
+ * @param input - the entry's state, the preview in force, and who is signed in.
50
+ * @returns enter, leave, or keep, so the caller only writes state it must.
51
+ */
52
+ export declare function planPreviewTransition(input: {
53
+ readonly state: unknown;
54
+ readonly preview: QaAdminPreview | null;
55
+ readonly isAdmin: boolean;
56
+ }): QaPreviewTransition;
57
+ /** The slice of the browser the preview needs; a test injects its own. */
58
+ export interface QaPreviewBrowser {
59
+ readonly state: unknown;
60
+ pushState(data: unknown, unused: string, url?: string | null): void;
61
+ replaceState(data: unknown, unused: string, url?: string | null): void;
62
+ /** Subscribe to browser navigation; returns the unsubscribe. */
63
+ onPopState(listener: () => void): () => void;
64
+ }
65
+ export interface QaAdminPreviewOptions {
66
+ /** Only an administrator may preview; anyone else leaves the mode. */
67
+ readonly isAdmin: boolean;
68
+ /** The account's own default profile, restored when a preview ends. */
69
+ readonly fallbackSubrole: string | null;
70
+ /** The chat route a preview navigation returns to. */
71
+ readonly previewUrl: string;
72
+ /** Called with the profile the next chat must use; null when none is known. */
73
+ readonly onSelect: (roleId: string | null) => void;
74
+ /**
75
+ * Re-checked on every browser navigation AND whenever this changes: an
76
+ * in-app navigation to the chat route arrives without a `popstate` event.
77
+ */
78
+ readonly routeKey: string;
79
+ readonly target?: QaPreviewBrowser;
80
+ }
81
+ export interface QaAdminPreviewMode {
82
+ /** The preview in force right now, if any. */
83
+ readonly preview: QaAdminPreview | null;
84
+ /** Enter the preview of one role, as the administration console asks. */
85
+ readonly enter: (role: Pick<QaSubrole, "id" | "name">) => void;
86
+ /**
87
+ * End the mode because the chat on screen is not a preview, leaving the
88
+ * history entry alone: going Back to it is entering the preview again.
89
+ */
90
+ readonly clear: () => void;
91
+ /** End the mode for good, as the administrator asked from the banner. */
92
+ readonly leave: () => void;
93
+ }
94
+ /**
95
+ * The administrator preview mode of one browser tab.
96
+ *
97
+ * The mode is a property of the history entry, so entering and leaving it
98
+ * follows the browser: the console pushes an entry that carries the preview,
99
+ * every navigation back to an entry without one ends the mode, and only an
100
+ * administrator can hold it. The alternative — reading the marker once and
101
+ * latching it in component state — kept the mode alive for the whole tab, and
102
+ * since the role selector is hidden while previewing, later new chats silently
103
+ * ran as the previewed profile instead of the account's default one.
104
+ * @param options - who is signed in, the account default, and the history.
105
+ * @returns the preview in force plus the two explicit transitions.
106
+ */
107
+ export declare function useQaAdminPreview(options: QaAdminPreviewOptions): QaAdminPreviewMode;
108
+ export {};
109
+ //# sourceMappingURL=preview.d.ts.map
@@ -68,7 +68,11 @@ export declare class QaSurface extends TypertRemoteService {
68
68
  accountsLogin(email: string, password: string): QaAccountSession;
69
69
  /** Identity probe; safe to call with an empty or expired token. */
70
70
  accountsWhoami(token: string): QaWhoamiResult;
71
- /** Migrate a browser's local chat index into server-side ownership. */
71
+ /**
72
+ * Migrate a browser's local chat index into server-side ownership. The
73
+ * access service owns the filter: a delegated session in that index is not a
74
+ * chat and is never claimed.
75
+ */
72
76
  accountsClaimSessions(token: string, sessionIds: readonly string[]): QaClaimResult;
73
77
  /** The token user's owned session ids; the sidebar list authority. */
74
78
  accountsOwnedSessions(token: string): {
@@ -25,4 +25,20 @@ export declare function qaToolPolicyPlan(allow: readonly string[], isMounted: (t
25
25
  * stays as unchecked as any allow-listed one, and no less.
26
26
  */
27
27
  export declare function qaToolDenial(allowed: ReadonlySet<string>, toolName: string, dynamicNames?: readonly string[]): string | undefined;
28
+ /**
29
+ * The conversation-wide ceiling, applied to every agent of an attested chat.
30
+ *
31
+ * A scoped restriction and a scoped guard cover the scope that owns them and
32
+ * that scope's descendants. A delegated child is composed from the parent's
33
+ * PRESET, so the parent agent's own layers never enter the child's chain: the
34
+ * child is bounded by its preset `toolFilter` alone, and an expert can
35
+ * therefore hold — and call — a tool the subrole never granted the chat. This
36
+ * check is the ceiling the whole conversation shares: the subrole's reach,
37
+ * visible tools plus the grantable ones, so a skill may still hand a tool to a
38
+ * delegated assistant, but nothing may exceed what the role could ever grant.
39
+ * @param ceiling - names reachable anywhere in this conversation.
40
+ * @param toolName - the tool the model is trying to call.
41
+ * @returns the denial reason, or `undefined` when the call is within reach.
42
+ */
43
+ export declare function qaCeilingDenial(ceiling: ReadonlySet<string>, toolName: string): string | undefined;
28
44
  //# sourceMappingURL=lockdown-policy.d.ts.map
@@ -35,9 +35,13 @@ export declare class QaProvenanceHost {
35
35
  constructor(ctx: Context, config: () => ResolvedQaSurfaceConfig, store?: QaProvenanceSnapshotStore, onPersistenceError?: (error: unknown) => void);
36
36
  dispose(): void;
37
37
  /**
38
- * Drop every per-session record of one chat. Long-lived servers outlive
39
- * their sessions by weeks; without this the collector, snapshot, call and
40
- * lineage maps grow with every chat and turn forever.
38
+ * Drop the in-memory per-session records of one chat. Long-lived servers
39
+ * outlive their sessions by weeks; without this the collector, snapshot,
40
+ * call and lineage maps grow with every chat and turn forever. The durable
41
+ * snapshot file is intentionally kept: `session/disposed` also fires for
42
+ * runtime teardown of a chat that still exists and will be reopened, and
43
+ * its stored turns are what let the transcript show sources after a Host
44
+ * restart. Growth of the file is bounded by the snapshot store's caps.
41
45
  */
42
46
  private forget;
43
47
  private forgetAll;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Retention contract for durable provenance.
3
+ *
4
+ * This module is deliberately free of Node imports: the resolved default
5
+ * config reaches the browser bundle through `resolve-config`, and a single
6
+ * `node:fs` import here would break that bundle at runtime.
7
+ */
8
+ /**
9
+ * What a sharded provenance store keeps. Every bound counts turns, shards or
10
+ * days; zero means "keep everything" for that bound.
11
+ */
12
+ export interface QaProvenanceRetention {
13
+ /** Newest turns kept per chat. */
14
+ readonly maxTurnsPerSession: number;
15
+ /** Chats kept, most recently written first. */
16
+ readonly maxSessions: number;
17
+ /** Chats untouched for longer than this are dropped. */
18
+ readonly maxAgeDays: number;
19
+ /** How often a write may trigger the directory sweep. */
20
+ readonly sweepIntervalMinutes: number;
21
+ }
22
+ /**
23
+ * Defaults sized for a QA deployment: one chat is worth a few hundred turns, a
24
+ * fortnight of chats is worth keeping, and an untouched month is finished.
25
+ */
26
+ export declare const DEFAULT_QA_PROVENANCE_RETENTION: QaProvenanceRetention;
27
+ /** Bounds the configuration validator enforces, mirrored for the CLI. */
28
+ export declare const QA_PROVENANCE_RETENTION_LIMITS: Readonly<{
29
+ maxTurnsPerSession: {
30
+ min: number;
31
+ max: number;
32
+ };
33
+ maxSessions: {
34
+ min: number;
35
+ max: number;
36
+ };
37
+ maxAgeDays: {
38
+ min: number;
39
+ max: number;
40
+ };
41
+ sweepIntervalMinutes: {
42
+ min: number;
43
+ max: number;
44
+ };
45
+ }>;
46
+ //# sourceMappingURL=retention.d.ts.map
@@ -1,23 +1,79 @@
1
+ import { type QaProvenanceRetention } from "./retention.js";
1
2
  import type { QaTurnSources } from "./types.js";
2
3
  export interface QaProvenanceSnapshotStore {
3
4
  list(sessionId: string): readonly QaTurnSources[];
4
5
  put(bundle: QaTurnSources): void;
6
+ /**
7
+ * Drop one session's snapshots. The host store wires this to the session's
8
+ * disposal, which is what keeps the durable file from growing with every
9
+ * chat a long-lived server ever served.
10
+ */
11
+ drop(sessionId: string): void;
12
+ }
13
+ /**
14
+ * Retention bounds and their defaults live in a Node-free module: the resolved
15
+ * default config reaches the browser bundle, and this file does not.
16
+ */
17
+ export { DEFAULT_QA_PROVENANCE_RETENTION, QA_PROVENANCE_RETENTION_LIMITS, } from "./retention.js";
18
+ export type { QaProvenanceRetention } from "./retention.js";
19
+ /** What one directory sweep did, for the maintenance command and the tests. */
20
+ export interface QaProvenanceSweepResult {
21
+ readonly scanned: number;
22
+ readonly droppedByAge: number;
23
+ readonly droppedByCount: number;
5
24
  }
6
25
  /** Process-local store used by tests and explicit ephemeral deployments. */
7
26
  export declare class MemoryQaProvenanceSnapshotStore implements QaProvenanceSnapshotStore {
8
27
  private readonly sessions;
9
28
  list(sessionId: string): readonly QaTurnSources[];
10
29
  put(bundle: QaTurnSources): void;
30
+ drop(sessionId: string): void;
11
31
  }
12
- /** Default durable provenance path, deliberately outside Harness journals. */
13
- export declare function defaultQaProvenanceFilePath(): string;
14
- /** Atomically persists per-turn QA source snapshots in plugin-owned storage. */
32
+ /** Default durable provenance directory, deliberately outside Harness journals. */
33
+ export declare function defaultQaProvenanceDirectoryPath(): string;
34
+ /** The pre-sharding monolith, migrated on first use and then left alone. */
35
+ export declare function defaultLegacyProvenanceFilePath(): string;
36
+ /**
37
+ * Durable provenance, one file per chat.
38
+ *
39
+ * The read API is per-session and nothing enumerates every chat, so a shard is
40
+ * exactly the unit a writer touches: a turn rewrites one chat's history rather
41
+ * than every chat's. Empty turns collapse to their numbers and each shard is
42
+ * bounded by `QaProvenanceRetention`, so neither a long chat nor a long-lived
43
+ * deployment can make a single write grow without limit.
44
+ *
45
+ * The store is the only writer of its directory, and no cache sits in front of
46
+ * it: every read goes to disk.
47
+ */
15
48
  export declare class FileQaProvenanceSnapshotStore implements QaProvenanceSnapshotStore {
16
- private readonly filePath;
17
- constructor(filePath?: string);
49
+ private readonly directoryPath;
50
+ private readonly retention;
51
+ private readonly legacyFilePath;
52
+ private migrated;
53
+ private lastSweepAt;
54
+ constructor(directoryPath?: string, retention?: () => QaProvenanceRetention, legacyFilePath?: string);
18
55
  list(sessionId: string): readonly QaTurnSources[];
19
56
  put(bundle: QaTurnSources): void;
20
- private load;
21
- private persist;
57
+ drop(sessionId: string): void;
58
+ /**
59
+ * Drop shards the retention policy no longer covers: untouched for
60
+ * `maxAgeDays`, then everything past the `maxSessions` most recent ones.
61
+ *
62
+ * `keep` names the chat whose shard must survive the sweep — a caller that
63
+ * swept right after writing it would otherwise be able to delete the very
64
+ * bundle it just stored.
65
+ */
66
+ sweep(keep?: string): QaProvenanceSweepResult;
67
+ /**
68
+ * Split the pre-sharding monolith into shards exactly once, then rename it
69
+ * out of the way. A shard that already exists wins the merge, so a retried
70
+ * migration cannot lose turns that arrived after the first attempt.
71
+ */
72
+ private migrateLegacyOnce;
73
+ private shardPath;
74
+ private readShard;
75
+ private writeShard;
76
+ private removeShard;
77
+ private maybeSweep;
22
78
  }
23
79
  //# sourceMappingURL=snapshot-store.d.ts.map
@@ -68,6 +68,11 @@ export declare class QaQuestionGate {
68
68
  * parked request is authoritative: a label the question never offered is
69
69
  * dropped, and a free-text answer replaces the choice in a single-select
70
70
  * rather than accompanying it, so the model only ever reads a real answer.
71
+ *
72
+ * The payload arrives over the wire from the browser, so its shape is
73
+ * untrusted: a non-array, a non-object entry, or a non-array/non-string
74
+ * selection is folded into the refusal/skip semantics instead of crashing
75
+ * the gate with a TypeError.
71
76
  */
72
77
  answer(sessionId: string, requestId: string, answers: readonly QaQuestionAnswerItem[]): boolean;
73
78
  /** Close a request without answering it. */
@@ -1,6 +1,7 @@
1
1
  import type { Agent } from "@deepseek-ai/dsh-agent";
2
2
  import type { Context } from "@deepseek-ai/cordis";
3
3
  import type { PluginLogger } from "@yadsh/dsh-plugin-log";
4
+ import { type QaSessionFacts } from "./accounts/store.js";
4
5
  import type { QaResolvedSessionPolicy } from "./access/service.js";
5
6
  import type { QaLockdownProof, ResolvedQaSurfaceConfig } from "./types.js";
6
7
  /**
@@ -9,7 +10,7 @@ import type { QaLockdownProof, ResolvedQaSurfaceConfig } from "./types.js";
9
10
  * pair or throw the coarse `QaAttestationError`; see {@link QaAccounts}.
10
11
  */
11
12
  export interface QaAccountsGate {
12
- enforceSessionAccess(token: string, sessionId: string): {
13
+ enforceSessionAccess(token: string, sessionId: string, facts?: QaSessionFacts): {
13
14
  readonly id: string;
14
15
  } | undefined;
15
16
  userWorkspace(userId: string, registeredWorkspacePath: string): string;
@@ -47,7 +48,17 @@ export declare class QaPolicyAdmission {
47
48
  */
48
49
  private readonly attested;
49
50
  private readonly workspaceAccess;
51
+ /**
52
+ * Every name reachable anywhere in one QA conversation, keyed by session id.
53
+ *
54
+ * The chat's agent and the agents delegated from it share one entry —
55
+ * children inherit it when their session is created — because the ceiling is
56
+ * a property of the conversation, not of one scope. See
57
+ * {@link qaCeilingDenial} for why a scoped restriction cannot express it.
58
+ */
59
+ private readonly ceilings;
50
60
  private readonly disposeWorkspaceGuard;
61
+ private readonly disposeCeilingGuard;
51
62
  constructor(ctx: Context, config: () => ResolvedQaSurfaceConfig, logger: PluginLogger, accounts?: QaAccountsGate | undefined,
52
63
  /**
53
64
  * Names the QA tool catalog attached to one agent after its activation
@@ -100,6 +111,21 @@ export declare class QaPolicyAdmission {
100
111
  * mismatch"), and the adoption and permission checks stay the gate.
101
112
  */
102
113
  private liveAgent;
114
+ /**
115
+ * The Host-registered facts of one session, for the accounts gate's bounded
116
+ * auto-claim. A session this process has not materialized (a chat of an
117
+ * earlier Host run, reached before its agent exists) yields no facts and
118
+ * keeps the historical unbounded claim.
119
+ */
120
+ private sessionFacts;
121
+ /**
122
+ * Account identity and ownership are checked before any policy work: an
123
+ * invalid token must not learn whether a session exists or how the
124
+ * deployment composes. Independent of lockdown — a deployment may gate
125
+ * users without pinning the policy, and the gate itself no-ops while
126
+ * accounts are disabled.
127
+ */
128
+ private accessOwner;
103
129
  secureSession(token: string, sessionId: string): Promise<QaLockdownProof>;
104
130
  /**
105
131
  * Mask one agent down to the policy's tool names, for the account-free path