@kirkchinese/dsh-citeciter 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Investigate any line in a DSH response—without stopping the Agent or changing
11
11
  ## Highlights
12
12
 
13
13
  - **Exact source binding.** CiteCiter rechecks the visible selection against the committed response before creating a Topic.
14
- - **No need to wait.** A committed model response can be investigated while the surrounding Agent turn continues.
14
+ - **No need to wait.** A committed intermediate model call can be investigated while the surrounding Agent turn continues, including its reasoning or a selection spanning reasoning and answer text.
15
15
  - **Durable investigations.** Private Topics support natural follow-up questions and reopen after a refresh or restart.
16
16
  - **Evidence with no write access.** CiteCiter can inspect committed source events and search or read project files, but cannot change the source Session or workspace.
17
17
  - **Inspectable work.** Questions, answers, source reads, and project-file checks remain together in the investigation panel.
@@ -19,23 +19,23 @@ Investigate any line in a DSH response—without stopping the Agent or changing
19
19
 
20
20
  ## Use
21
21
 
22
- 1. Select text inside a committed assistant model call. The surrounding Agent turn may still be running.
22
+ 1. Select answer or expanded reasoning text inside a committed assistant model call. The surrounding Agent turn may still be running.
23
23
  2. Right-click the selection, enter your first question, and choose `Citer!`.
24
24
  3. CiteCiter creates a new Topic in the investigation panel.
25
25
  4. Continue asking questions there, or reopen an earlier Topic from CiteCiter's Topic rail.
26
26
  5. Change the Topic model, reasoning effort, title, archive state, or panel width without changing the source Session.
27
27
 
28
- ## Install v0.4.1
28
+ ## Install v0.4.3
29
29
 
30
- CiteCiter 0.4.1 requires Node.js `^22.19.0 || >=24.0.0` and DSH `>=0.1.1-rc.1 <0.1.1-rc.3`.
30
+ CiteCiter 0.4.3 requires Node.js `^22.19.0 || >=24.0.0` and DSH `>=0.1.1-rc.1 <0.1.1-rc.3`.
31
31
 
32
32
  For DSH Web:
33
33
 
34
34
  ```sh
35
- dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.1
35
+ dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.3
36
36
  ```
37
37
 
38
- Restart DSH Web and refresh the page. Packaged Desktop adaptation is deferred; CiteCiter 0.4.1 does not provide a supported Desktop installation path.
38
+ Restart DSH Web and refresh the page. Packaged Desktop adaptation is deferred; CiteCiter 0.4.3 does not provide a supported Desktop installation path.
39
39
 
40
40
  ## Context modes
41
41
 
@@ -47,7 +47,7 @@ Exact Fork is an advanced mode for a source turn that has already ended. `exact-
47
47
 
48
48
  | Host | CiteCiter version | Status |
49
49
  |---|---|---|
50
- | DSH Web `0.1.1-rc.2` | `0.4.1` | Linux package, CI, upgrade, and assembled keyless browser checks passed; real-provider acceptance was not run |
50
+ | DSH Web `0.1.1-rc.2` | `0.4.3` | Linux package, regression tests, and real-browser selection checks passed; real-provider acceptance was not run |
51
51
  | [DSH Desktop 2.0.2](https://github.com/anywhere-labs/deepseek-harness-desktop) with bundled DSH `0.1.1-rc.2` | Future target | Adaptation and native acceptance are deferred; no packaged-Desktop compatibility claim |
52
52
  | [dataelement DSH Desktop](https://github.com/dataelement/dsh-desktop) development shell with DSH `0.1.1-rc.1` | `0.4.0` only | Historical conditional Linux source-shell evidence |
53
53
  | DSH Web `0.1.0-rc.7` | `0.3.2` | Previous stable line |
@@ -57,27 +57,27 @@ The demo uses a deterministic fixture to show the workflow; it is not real-provi
57
57
 
58
58
  ## Upgrade from an earlier version
59
59
 
60
- To install or upgrade v0.4.1 in Web:
60
+ To install or upgrade v0.4.3 in Web:
61
61
 
62
62
  ```sh
63
- dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.1
63
+ dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.3
64
64
  dsh plugin --profile web list --depth 0
65
65
  ```
66
66
 
67
- Packaged Desktop adaptation is deferred and is not part of the supported 0.4.1 upgrade path.
67
+ Packaged Desktop adaptation is deferred and is not part of the supported 0.4.3 upgrade path.
68
68
 
69
69
  Upgrading from v0.3 does not migrate or rewrite existing Topics, settings, or source Sessions. Users remaining on DSH `0.1.0-rc.7` should keep CiteCiter 0.3.2.
70
70
 
71
71
  ## Limitations
72
72
 
73
- - A selection must include at least one committed assistant model call. User-only ranges, tool-only ranges, and still-streaming fragments cannot anchor a Citation.
73
+ - A selection must include at least one committed assistant model call. User-only ranges, tool-only ranges, still-streaming fragments, and the generated reasoning disclosure header cannot anchor a Citation.
74
74
  - Renderer-generated KaTeX layout and footnote numbers cannot be cited directly because they lack stable source coordinates.
75
75
  - Exact Fork cannot start from an open source turn.
76
76
  - Source-file access depends on the running DSH filesystem service and remains read-only.
77
77
  - Read Frog translated selections are a best-effort compatibility path on DSH rc.1/rc.2 and activate only when its complete private marker set is present; ordinary DSH selection does not depend on Read Frog.
78
78
  - DSH does not yet expose a public additive right-dock contribution point. CiteCiter therefore uses a reversible private AppFrame grid adapter whenever the panel and conversation fit; it preserves the layout's details preference but temporarily hides that column while the investigation panel is open, and it must be retested for every supported DSH release.
79
79
  - Topics are Host-durable. When Desktop restarts on a different loopback port, CiteCiter falls back to the most recently updated Topic because browser local storage is origin-scoped; configure a fixed Desktop port to restore the exact last-viewed Topic.
80
- - CiteCiter 0.4.1 makes no packaged-Desktop compatibility claim; native Windows and macOS adaptation continues separately.
80
+ - CiteCiter 0.4.3 makes no packaged-Desktop compatibility claim; native Windows and macOS adaptation continues separately.
81
81
  - There is currently no TUI interaction adapter.
82
82
  - DSH is prerelease software; later DSH API versions may require a CiteCiter update.
83
83
 
package/README.zh.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ## 核心能力
12
12
 
13
13
  - **精确绑定来源。** 创建 Topic 前,CiteCiter 会把可见选区与已经提交的回答重新核对。
14
- - **不用等待主任务。** 一次模型回答提交后即可开始调查,外层 Agent 轮次可以继续运行。
14
+ - **不用等待主任务。** 一次中间模型调用提交后即可开始调查,外层 Agent 轮次可以继续运行;思考内容和跨越“思考+回答”的选区也可以使用。
15
15
  - **调查可以持续。** 私有 Topic 支持自然续问,刷新或重启后仍能重新打开。
16
16
  - **有证据、无写权限。** CiteCiter 能检查来源中已提交的事件,也能搜索和阅读项目文件,但不能改变来源 Session 或工作区。
17
17
  - **过程可检查。** 问题、回答、来源读取和项目文件核查都留在调查面板中。
@@ -19,23 +19,23 @@
19
19
 
20
20
  ## 使用
21
21
 
22
- 1. 在一次已经提交的助手模型调用中选择文字;外层 Agent 轮次可以仍在运行。
22
+ 1. 在一次已经提交的助手模型调用中选择回答或展开后的思考文字;外层 Agent 轮次可以仍在运行。
23
23
  2. 右键选区,输入第一个问题,然后点击 `Citer!`。
24
24
  3. CiteCiter 在调查面板中创建一个新 Topic。
25
25
  4. 在 Topic 中继续追问,或从 CiteCiter 的 Topic 栏恢复旧讨论。
26
26
  5. 独立调整模型、思考强度、标题、归档状态和面板宽度,不改变来源 Session。
27
27
 
28
- ## 安装 v0.4.1
28
+ ## 安装 v0.4.3
29
29
 
30
- CiteCiter 0.4.1 需要 Node.js `^22.19.0 || >=24.0.0` 与 DSH `>=0.1.1-rc.1 <0.1.1-rc.3`。
30
+ CiteCiter 0.4.3 需要 Node.js `^22.19.0 || >=24.0.0` 与 DSH `>=0.1.1-rc.1 <0.1.1-rc.3`。
31
31
 
32
32
  DSH Web 使用:
33
33
 
34
34
  ```sh
35
- dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.1
35
+ dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.3
36
36
  ```
37
37
 
38
- 安装后重启 DSH Web 并刷新页面。正式 Desktop 适配已经暂缓;CiteCiter 0.4.1 不提供受支持的 Desktop 安装路径。
38
+ 安装后重启 DSH Web 并刷新页面。正式 Desktop 适配已经暂缓;CiteCiter 0.4.3 不提供受支持的 Desktop 安装路径。
39
39
 
40
40
  ## 上下文模式
41
41
 
@@ -47,7 +47,7 @@ Exact Fork 是面向已结束来源轮次的高级模式。`exact-when-available
47
47
 
48
48
  | 宿主 | CiteCiter 版本 | 状态 |
49
49
  |---|---|---|
50
- | DSH Web `0.1.1-rc.2` | `0.4.1` | Linux 包、CI、升级与组装 keyless 浏览器检查通过;真实提供方验收未运行 |
50
+ | DSH Web `0.1.1-rc.2` | `0.4.3` | Linux 包、回归测试与真实浏览器选区检查通过;真实提供方验收未运行 |
51
51
  | [DSH Desktop 2.0.2](https://github.com/anywhere-labs/deepseek-harness-desktop)(内置 DSH `0.1.1-rc.2`) | 后续目标 | 适配与原生验收暂缓;不声明正式 Desktop 安装包兼容性 |
52
52
  | [dataelement DSH Desktop](https://github.com/dataelement/dsh-desktop) 源码开发壳(内置 DSH `0.1.1-rc.1`) | 仅 `0.4.0` | 历史上的有条件 Linux 源码壳证据 |
53
53
  | DSH Web `0.1.0-rc.7` | `0.3.2` | 旧版稳定线 |
@@ -57,27 +57,27 @@ Exact Fork 是面向已结束来源轮次的高级模式。`exact-when-available
57
57
 
58
58
  ## 从旧版本升级
59
59
 
60
- 在 Web 安装或升级 v0.4.1:
60
+ 在 Web 安装或升级 v0.4.3:
61
61
 
62
62
  ```sh
63
- dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.1
63
+ dsh plugin --profile web add @kirkchinese/dsh-citeciter@0.4.3
64
64
  dsh plugin --profile web list --depth 0
65
65
  ```
66
66
 
67
- 正式 Desktop 适配已经暂缓,不属于 0.4.1 的受支持升级路径。
67
+ 正式 Desktop 适配已经暂缓,不属于 0.4.3 的受支持升级路径。
68
68
 
69
69
  从 v0.3 升级不会迁移或重写现有 Topic、设置或来源 Session。仍在 DSH `0.1.0-rc.7` 上运行的用户应继续使用 CiteCiter 0.3.2。
70
70
 
71
71
  ## 已知限制
72
72
 
73
- - 选区中必须至少包含一个已经提交的助手模型回答;纯用户消息、纯工具行和仍在流式输出的片段不能作为引用锚点。
73
+ - 选区中必须至少包含一个已经提交的助手模型调用;纯用户消息、纯工具行、仍在流式输出的片段和自动生成的思考折叠标题不能作为引用锚点。
74
74
  - 渲染器生成的 KaTeX 排版和脚注编号没有稳定原文坐标,不能直接引用。
75
75
  - Exact Fork 不能从仍在运行的来源轮次开始。
76
76
  - 来源文件访问依赖当前 DSH 文件系统服务,并始终保持只读。
77
77
  - Read Frog 翻译选区是 DSH rc.1/rc.2 上的 best-effort 兼容路径,仅在其完整私有标记同时存在时启用;普通 DSH 选区不依赖 Read Frog。
78
78
  - DSH 尚未提供公开的增量右侧 dock 布局扩展点。CiteCiter 因此在面板与主对话能够同时容纳时使用可逆的私有 AppFrame grid 适配器;它保留布局的 details 偏好,但在调查面板打开期间临时隐藏该列,并且每个受支持的 DSH 版本都必须重新验证。
79
79
  - Topic 由 Host 持久保存。Desktop 重启到不同 loopback 端口时,浏览器 localStorage 会因 origin 改变而失去精确指针,CiteCiter 将回退到最近更新的 Topic;需要精确恢复“上次查看”时应配置固定 Desktop 端口。
80
- - CiteCiter 0.4.1 不声明正式 Desktop 安装包兼容性;Windows 与 macOS 原生适配将另行继续。
80
+ - CiteCiter 0.4.3 不声明正式 Desktop 安装包兼容性;Windows 与 macOS 原生适配将另行继续。
81
81
  - 当前没有 TUI 交互适配器。
82
82
  - DSH 仍处于预发布阶段,后续 API 版本可能要求同步更新 CiteCiter。
83
83
 
package/lib/client.js CHANGED
@@ -4443,6 +4443,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4443
4443
  }]
4444
4444
  };
4445
4445
  //#endregion
4446
+ //#region lib/types/assistant-content.js
4447
+ /**
4448
+ * Project committed reasoning and answer blocks in renderer order.
4449
+ *
4450
+ * @param blocks - DSH assistant content blocks.
4451
+ * @returns reasoning and answer text separated by the renderer's paragraph break.
4452
+ */
4453
+ function projectCitableAssistantContent(blocks) {
4454
+ let text = "";
4455
+ for (const block of blocks) {
4456
+ if (block === null || typeof block !== "object") continue;
4457
+ const candidate = block;
4458
+ const kind = candidate.kind ?? candidate.type;
4459
+ if (typeof candidate.text !== "string" || candidate.text === "") continue;
4460
+ if (kind === "reasoning") text += `${candidate.text}\n\n`;
4461
+ else if (kind === "text") text += candidate.text;
4462
+ }
4463
+ return text;
4464
+ }
4465
+ //#endregion
4446
4466
  //#region lib/types/client/answer.js
4447
4467
  /**
4448
4468
  * Read visible text from one assistant-step payload without retaining its live node.
@@ -4453,12 +4473,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4453
4473
  if (data === null || typeof data !== "object") return null;
4454
4474
  const record = data;
4455
4475
  if (record.status !== "running" && record.status !== "settled" && record.status !== "interrupted") return null;
4456
- let text = "";
4457
- for (const block of record.blocks ?? []) {
4458
- if (typeof block !== "object" || block === null) continue;
4459
- const candidate = block;
4460
- if (candidate.kind === "text" && typeof candidate.text === "string") text += candidate.text;
4461
- }
4476
+ const text = projectCitableAssistantContent(record.blocks ?? []);
4462
4477
  return text === "" ? null : {
4463
4478
  status: record.status,
4464
4479
  text
@@ -5365,6 +5380,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5365
5380
  const DSH_FLOW_SELECTOR = "[data-chat-flow-kind]";
5366
5381
  const DSH_ASSISTANT_ANCHOR_SELECTOR = "[data-chat-flow-kind=\"assistant-step\"][data-chat-anchor-key]";
5367
5382
  const DSH_REASONING_SELECTOR = "[data-variant=\"think\"]";
5383
+ const DSH_REASONING_HEADER_SELECTOR = "[data-disclosure-row]";
5368
5384
  const DSH_GENERATED_CONTENT_SELECTOR = "button, .katex, [data-footnotes], sup";
5369
5385
  const DSH_CODE_BLOCK_SELECTOR = ".md-code-block";
5370
5386
  const READ_FROG_TRANSLATION_SELECTOR = "[data-read-frog-translation-mode]";
@@ -5423,14 +5439,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5423
5439
  return anchors;
5424
5440
  }
5425
5441
  /**
5426
- * Detect DSH-rendered reasoning, generated controls, and collapsed code chrome.
5442
+ * Detect generated controls, reasoning summaries, and collapsed code chrome.
5427
5443
  *
5428
5444
  * @param range - current rendered selection range.
5429
5445
  * @param flow - assistant flow containing the range.
5430
5446
  * @returns whether the range touches content that CiteCiter must ignore.
5431
5447
  */
5432
5448
  function dshRangeTouchesExcludedContent(range, flow) {
5433
- for (const reasoning of flow.querySelectorAll(DSH_REASONING_SELECTOR)) if (range.intersectsNode(reasoning)) return true;
5449
+ for (const reasoning of flow.querySelectorAll(DSH_REASONING_SELECTOR)) for (const header of reasoning.querySelectorAll(DSH_REASONING_HEADER_SELECTOR)) if (range.intersectsNode(header)) return true;
5434
5450
  for (const generated of flow.querySelectorAll(DSH_GENERATED_CONTENT_SELECTOR)) if (range.intersectsNode(generated)) return true;
5435
5451
  for (const endpoint of [range.startContainer, range.endContainer]) {
5436
5452
  const element = elementForNode(endpoint);
@@ -5449,13 +5465,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5449
5465
  return null;
5450
5466
  }
5451
5467
  /**
5452
- * Determine whether a node is a Read Frog translated projection rather than source text.
5468
+ * Determine whether a node is generated UI rather than committed citable text.
5453
5469
  *
5454
5470
  * @param node - rendered node to classify.
5455
- * @returns whether Read Frog marks the node as translated content.
5471
+ * @returns whether the node must stay out of the citable projection.
5456
5472
  */
5457
- function isReadFrogTranslatedContent(node) {
5458
- return node.nodeType === Node.ELEMENT_NODE && node.matches(READ_FROG_TRANSLATION_SELECTOR);
5473
+ function isNonCitableProjection(node) {
5474
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
5475
+ const element = node;
5476
+ return element.matches(READ_FROG_TRANSLATION_SELECTOR) || element.matches(DSH_REASONING_HEADER_SELECTOR) && element.closest(DSH_REASONING_SELECTOR) !== null;
5477
+ }
5478
+ /**
5479
+ * Determine whether a node owns one DSH reasoning block.
5480
+ *
5481
+ * @param node - rendered node to classify.
5482
+ * @returns whether the node is a reasoning root.
5483
+ */
5484
+ function isDshReasoningContent(node) {
5485
+ return node.nodeType === Node.ELEMENT_NODE && node.matches(DSH_REASONING_SELECTOR);
5459
5486
  }
5460
5487
  /**
5461
5488
  * Resolve a selection wholly inside one Read Frog translation to its source paragraph.
@@ -6939,11 +6966,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6939
6966
  let targetStart;
6940
6967
  let targetEnd;
6941
6968
  const visit = (node) => {
6942
- if (isReadFrogTranslatedContent(node)) return;
6969
+ if (isNonCitableProjection(node)) return;
6970
+ const start = text.length;
6943
6971
  if (node === target) targetStart = text.length;
6944
6972
  if (node.nodeType === Node.TEXT_NODE) text += node.textContent ?? "";
6945
6973
  else for (const child of node.childNodes) visit(child);
6946
6974
  if (node === target) targetEnd = text.length;
6975
+ if (isDshReasoningContent(node) && text.length > start) text += "\n\n";
6947
6976
  };
6948
6977
  visit(root);
6949
6978
  return {
@@ -6956,7 +6985,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6956
6985
  let text = "";
6957
6986
  let found = false;
6958
6987
  const visit = (node) => {
6959
- if (found || isReadFrogTranslatedContent(node)) return;
6988
+ if (found || isNonCitableProjection(node)) return;
6960
6989
  if (node === boundary) {
6961
6990
  if (node.nodeType === Node.TEXT_NODE) text += (node.textContent ?? "").slice(0, offset);
6962
6991
  else for (let index = 0; index < offset; index++) {
@@ -6966,8 +6995,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6966
6995
  found = true;
6967
6996
  return;
6968
6997
  }
6998
+ const start = text.length;
6969
6999
  if (node.nodeType === Node.TEXT_NODE) text += node.textContent ?? "";
6970
- else for (const child of node.childNodes) visit(child);
7000
+ else for (const child of node.childNodes) {
7001
+ visit(child);
7002
+ if (found) return;
7003
+ }
7004
+ if (isDshReasoningContent(node) && text.length > start) text += "\n\n";
6971
7005
  };
6972
7006
  visit(root);
6973
7007
  return found ? text : null;
package/lib/index.js CHANGED
@@ -33,6 +33,26 @@ var __exportAll = (all, no_symbols) => {
33
33
  return target;
34
34
  };
35
35
  //#endregion
36
+ //#region lib/types/assistant-content.js
37
+ /**
38
+ * Project committed reasoning and answer blocks in renderer order.
39
+ *
40
+ * @param blocks - DSH assistant content blocks.
41
+ * @returns reasoning and answer text separated by the renderer's paragraph break.
42
+ */
43
+ function projectCitableAssistantContent(blocks) {
44
+ let text = "";
45
+ for (const block of blocks) {
46
+ if (block === null || typeof block !== "object") continue;
47
+ const candidate = block;
48
+ const kind = candidate.kind ?? candidate.type;
49
+ if (typeof candidate.text !== "string" || candidate.text === "") continue;
50
+ if (kind === "reasoning") text += `${candidate.text}\n\n`;
51
+ else if (kind === "text") text += candidate.text;
52
+ }
53
+ return text;
54
+ }
55
+ //#endregion
36
56
  //#region ../../node_modules/.pnpm/mdast-util-to-string@4.0.0/node_modules/mdast-util-to-string/lib/index.js
37
57
  /**
38
58
  * @typedef {import('mdast').Nodes} Nodes
@@ -14369,7 +14389,8 @@ function compactMapped(units) {
14369
14389
  });
14370
14390
  else compact[compact.length - 1] = {
14371
14391
  ...previous,
14372
- endOffset: unit.endOffset
14392
+ endOffset: unit.endOffset,
14393
+ ...previous.synthetic === true || unit.synthetic === true ? { synthetic: true } : {}
14373
14394
  };
14374
14395
  }
14375
14396
  return compact;
@@ -14384,7 +14405,8 @@ function joinMapped(parts, separator) {
14384
14405
  for (const text of separator) joined.push({
14385
14406
  text,
14386
14407
  startOffset: before,
14387
- endOffset: after
14408
+ endOffset: after,
14409
+ synthetic: true
14388
14410
  });
14389
14411
  }
14390
14412
  joined.push(...part);
@@ -14442,6 +14464,47 @@ function candidatesFromProjection(markdown, projection, needle) {
14442
14464
  }
14443
14465
  return candidates;
14444
14466
  }
14467
+ function candidatesIgnoringSyntheticWhitespace(markdown, projection, needle) {
14468
+ const rendered = visibleText(projection);
14469
+ const visibleOffsets = [0];
14470
+ for (const unit of projection) visibleOffsets.push(visibleOffsets.at(-1) + unit.text.length);
14471
+ const candidates = [];
14472
+ const seen = /* @__PURE__ */ new Set();
14473
+ for (let start = 0; start < projection.length; start++) {
14474
+ const first = projection[start];
14475
+ if (first.synthetic === true) continue;
14476
+ let unitIndex = start;
14477
+ let needleOffset = 0;
14478
+ let lastIndex = -1;
14479
+ while (needleOffset < needle.length && unitIndex < projection.length) {
14480
+ const unit = projection[unitIndex];
14481
+ if (unit.synthetic === true) {
14482
+ while (projection[unitIndex]?.synthetic === true) unitIndex++;
14483
+ needleOffset += /^\s+/u.exec(needle.slice(needleOffset))?.[0].length ?? 0;
14484
+ continue;
14485
+ }
14486
+ if (!needle.startsWith(unit.text, needleOffset)) break;
14487
+ needleOffset += unit.text.length;
14488
+ lastIndex = unitIndex;
14489
+ unitIndex++;
14490
+ }
14491
+ if (needleOffset !== needle.length || lastIndex < start) continue;
14492
+ const last = projection[lastIndex];
14493
+ const key = `${first.startOffset}:${last.endOffset}`;
14494
+ if (seen.has(key)) continue;
14495
+ seen.add(key);
14496
+ const visibleStart = visibleOffsets[start];
14497
+ const visibleEnd = visibleOffsets[lastIndex + 1];
14498
+ candidates.push({
14499
+ startOffset: first.startOffset,
14500
+ endOffset: last.endOffset,
14501
+ sourceText: markdown.slice(first.startOffset, last.endOffset),
14502
+ displayPrefix: rendered.slice(Math.max(0, visibleStart - 240), visibleStart),
14503
+ displaySuffix: rendered.slice(visibleEnd, visibleEnd + 240)
14504
+ });
14505
+ }
14506
+ return candidates;
14507
+ }
14445
14508
  /**
14446
14509
  * Locate every GFM source range that renders as one browser-visible selection.
14447
14510
  *
@@ -14458,7 +14521,11 @@ function markdownSourceCandidates(markdown, displayText) {
14458
14521
  if (needle === "") return [];
14459
14522
  const exact = candidatesFromProjection(markdown, projection, needle);
14460
14523
  if (exact.length > 0) return exact;
14461
- return candidatesFromProjection(markdown, compactMapped(projection), needle.replace(/\s+/gu, " "));
14524
+ const compactProjection = compactMapped(projection);
14525
+ const compactNeedle = needle.replace(/\s+/gu, " ");
14526
+ const compact = candidatesFromProjection(markdown, compactProjection, compactNeedle);
14527
+ if (compact.length > 0) return compact;
14528
+ return candidatesIgnoringSyntheticWhitespace(markdown, compactProjection, compactNeedle);
14462
14529
  }
14463
14530
  //#endregion
14464
14531
  //#region lib/types/citation-mapping.js
@@ -14519,23 +14586,37 @@ function committedAssistantText(source, sourceSessionId, anchorSeq) {
14519
14586
  if (sourceSessionId !== source.session.id) throw new Error("Citation sourceSessionId does not match the observed source Session");
14520
14587
  const anchor = source.events.find((event) => event.seq === anchorSeq);
14521
14588
  if (anchor?.type !== "assistant/message") throw new Error("Citation anchorSeq does not identify a committed assistant/message");
14522
- const text = messageText(anchor.data.message.content);
14523
- if (text === "") throw new Error("Citation assistant/message has no visible text");
14589
+ const citable = projectCitableAssistantContent(anchor.data.message.content);
14590
+ const answer = messageText(anchor.data.message.content);
14591
+ const projections = citable === answer ? [citable] : [citable, answer].filter((text) => text !== "");
14592
+ if (projections[0]?.trim() === "") throw new Error("Citation assistant/message has no citable text");
14524
14593
  return {
14525
14594
  seq: anchor.seq,
14526
- text
14595
+ projections
14527
14596
  };
14528
14597
  }
14598
+ function resolveProjectedRange(selection, projections) {
14599
+ let failure;
14600
+ for (const text of projections) try {
14601
+ return {
14602
+ range: resolveCitationRange(selection, text),
14603
+ text
14604
+ };
14605
+ } catch (error) {
14606
+ failure ??= error;
14607
+ }
14608
+ throw failure;
14609
+ }
14529
14610
  /** Resolve a browser selection claim against the authoritative committed assistant message. */
14530
14611
  function resolveObserverCitation(source, rawClaim) {
14531
14612
  const claim = citationSelectionClaimSchema.parse(rawClaim);
14532
14613
  const anchor = committedAssistantText(source, claim.sourceSessionId, claim.anchorSeq);
14533
- const range = resolveCitationRange({
14614
+ const { range, text } = resolveProjectedRange({
14534
14615
  displayText: claim.displayText,
14535
14616
  ...claim.sourceHintText === void 0 ? {} : { sourceHintText: claim.sourceHintText },
14536
14617
  prefixText: claim.prefixText,
14537
14618
  suffixText: claim.suffixText
14538
- }, anchor.text);
14619
+ }, anchor.projections);
14539
14620
  const identity = {
14540
14621
  sourceSessionId: claim.sourceSessionId,
14541
14622
  anchorSeq: anchor.seq,
@@ -14549,20 +14630,20 @@ function resolveObserverCitation(source, rawClaim) {
14549
14630
  selectionFingerprint
14550
14631
  },
14551
14632
  assistantMessageSeq: anchor.seq,
14552
- assistantVisibleText: anchor.text,
14633
+ assistantVisibleText: text,
14553
14634
  contentFingerprint: selectionFingerprint
14554
14635
  };
14555
14636
  }
14556
14637
  /**
14557
- * Validate one Citation against a committed assistant message in the observed source snapshot.
14638
+ * Validate one Citation against committed reasoning or answer text in the observed source snapshot.
14558
14639
  * A matching `assistant/message` is sufficient; its step and turn may remain open.
14559
14640
  */
14560
14641
  function validateObserverCitation(source, rawDraft) {
14561
14642
  const citation = citationDraftSchema.parse(rawDraft);
14562
14643
  const anchor = committedAssistantText(source, citation.sourceSessionId, citation.anchorSeq);
14563
- const visibleText = anchor.text;
14564
- if (citation.endOffset <= citation.startOffset || citation.endOffset > visibleText.length || citation.endOffset - citation.startOffset !== citation.sourceText.length || visibleText.slice(citation.startOffset, citation.endOffset) !== citation.sourceText) throw new Error("Citation UTF-16 offsets and sourceText do not match the assistant/message");
14565
- if (visibleText.slice(Math.max(0, citation.startOffset - citation.prefixText.length), citation.startOffset) !== citation.prefixText || visibleText.slice(citation.endOffset, citation.endOffset + citation.suffixText.length) !== citation.suffixText) throw new Error("Citation surrounding context does not match the assistant/message");
14644
+ if (anchor.projections.find((text) => citation.endOffset > citation.startOffset && citation.endOffset <= text.length && citation.endOffset - citation.startOffset === citation.sourceText.length && text.slice(citation.startOffset, citation.endOffset) === citation.sourceText) === void 0) throw new Error("Citation UTF-16 offsets and sourceText do not match the assistant/message");
14645
+ const visibleText = anchor.projections.find((text) => citation.endOffset > citation.startOffset && citation.endOffset <= text.length && citation.endOffset - citation.startOffset === citation.sourceText.length && text.slice(citation.startOffset, citation.endOffset) === citation.sourceText && text.slice(Math.max(0, citation.startOffset - citation.prefixText.length), citation.startOffset) === citation.prefixText && text.slice(citation.endOffset, citation.endOffset + citation.suffixText.length) === citation.suffixText);
14646
+ if (visibleText === void 0) throw new Error("Citation surrounding context does not match the assistant/message");
14566
14647
  const expectedFingerprint = fingerprintCitationDraft(citation);
14567
14648
  if (citation.selectionFingerprint !== expectedFingerprint) throw new Error("Citation content fingerprint does not match its evidence");
14568
14649
  return {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Project committed reasoning and answer blocks in renderer order.
3
+ *
4
+ * @param blocks - DSH assistant content blocks.
5
+ * @returns reasoning and answer text separated by the renderer's paragraph break.
6
+ */
7
+ export declare function projectCitableAssistantContent(blocks: readonly unknown[]): string;
@@ -1,4 +1,4 @@
1
- /** Visible assistant text and its stream lifecycle. */
1
+ /** Citable assistant reasoning and answer text with its stream lifecycle. */
2
2
  export type AssistantAnswer = {
3
3
  readonly status: 'running' | 'settled' | 'interrupted';
4
4
  readonly text: string;
@@ -41,7 +41,7 @@ export declare function dshAssistantAnchorForTarget(target: EventTarget | null):
41
41
  */
42
42
  export declare function dshIntersectedAssistantAnchors(range: Range): DshAssistantAnchor[];
43
43
  /**
44
- * Detect DSH-rendered reasoning, generated controls, and collapsed code chrome.
44
+ * Detect generated controls, reasoning summaries, and collapsed code chrome.
45
45
  *
46
46
  * @param range - current rendered selection range.
47
47
  * @param flow - assistant flow containing the range.
@@ -56,12 +56,19 @@ export declare function dshRangeTouchesExcludedContent(range: Range, flow: HTMLE
56
56
  */
57
57
  export declare function findDshAssistantAnchor(anchorKey: string): HTMLElement | null;
58
58
  /**
59
- * Determine whether a node is a Read Frog translated projection rather than source text.
59
+ * Determine whether a node is generated UI rather than committed citable text.
60
60
  *
61
61
  * @param node - rendered node to classify.
62
- * @returns whether Read Frog marks the node as translated content.
62
+ * @returns whether the node must stay out of the citable projection.
63
63
  */
64
- export declare function isReadFrogTranslatedContent(node: Node): boolean;
64
+ export declare function isNonCitableProjection(node: Node): boolean;
65
+ /**
66
+ * Determine whether a node owns one DSH reasoning block.
67
+ *
68
+ * @param node - rendered node to classify.
69
+ * @returns whether the node is a reasoning root.
70
+ */
71
+ export declare function isDshReasoningContent(node: Node): boolean;
65
72
  /**
66
73
  * Resolve a selection wholly inside one Read Frog translation to its source paragraph.
67
74
  *
@@ -38,7 +38,7 @@ export declare function fingerprintCitationDraft(draft: Omit<CitationDraft, 'sel
38
38
  /** Resolve a browser selection claim against the authoritative committed assistant message. */
39
39
  export declare function resolveObserverCitation(source: ObserverSourceSnapshot, rawClaim: CitationSelectionClaim): ValidatedObserverCitation;
40
40
  /**
41
- * Validate one Citation against a committed assistant message in the observed source snapshot.
41
+ * Validate one Citation against committed reasoning or answer text in the observed source snapshot.
42
42
  * A matching `assistant/message` is sufficient; its step and turn may remain open.
43
43
  */
44
44
  export declare function validateObserverCitation(source: ObserverSourceSnapshot, rawDraft: CitationDraft): ValidatedObserverCitation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirkchinese/dsh-citeciter",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Verifiable, source-bound, read-only investigations for DeepSeek Harness responses",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -80,6 +80,20 @@
80
80
  "cordis.patch.yml",
81
81
  "README.zh.md"
82
82
  ],
83
+ "scripts": {
84
+ "build": "npm run build:lib && npm run build:bundle",
85
+ "prebuild:lib": "node scripts/clean-types.mjs",
86
+ "build:lib": "tsc -p tsconfig.json",
87
+ "prebuild:bundle": "node scripts/clean-bundle.mjs",
88
+ "build:bundle": "tsdown --env.DSH_BUILD_FACE client",
89
+ "postbuild:bundle": "node scripts/normalize-bundle.mjs",
90
+ "prepack": "npm run build",
91
+ "dev": "npm run build && node dev/watch.mjs",
92
+ "smoke:seed": "node dev/seed-smoke-session.mjs",
93
+ "smoke:hmr": "node dev/hmr-smoke.mjs",
94
+ "typecheck": "tsc -p tsconfig.json --noEmit",
95
+ "test": "npm run build:lib && node --test tests/*.test.mjs"
96
+ },
83
97
  "peerDependencies": {
84
98
  "@deepseek-ai/cordis": "^4.0.1",
85
99
  "@deepseek-ai/dsh-agent": ">=0.1.1-rc.1 <0.1.1-rc.3",
@@ -159,18 +173,5 @@
159
173
  "tsdown": "^0.22.2",
160
174
  "typescript": "^6.0.3",
161
175
  "zod": "^4.4.3"
162
- },
163
- "scripts": {
164
- "build": "npm run build:lib && npm run build:bundle",
165
- "prebuild:lib": "node scripts/clean-types.mjs",
166
- "build:lib": "tsc -p tsconfig.json",
167
- "prebuild:bundle": "node scripts/clean-bundle.mjs",
168
- "build:bundle": "tsdown --env.DSH_BUILD_FACE client",
169
- "postbuild:bundle": "node scripts/normalize-bundle.mjs",
170
- "dev": "npm run build && node dev/watch.mjs",
171
- "smoke:seed": "node dev/seed-smoke-session.mjs",
172
- "smoke:hmr": "node dev/hmr-smoke.mjs",
173
- "typecheck": "tsc -p tsconfig.json --noEmit",
174
- "test": "npm run build:lib && node --test tests/*.test.mjs"
175
176
  }
176
- }
177
+ }