@gajae-code/coding-agent 0.18.6 → 0.18.8

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 (302) hide show
  1. package/CHANGELOG.md +318 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +6 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. package/src/web/search/providers/codex.ts +3 -2
@@ -0,0 +1,1814 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export type CrystalClassification = "confirmed" | "inferred" | "disputed";
4
+ export type CrystalItemKind = "goal" | "constraint" | "decision" | "acceptance_criterion" | "non_goal";
5
+
6
+ export interface CrystalMessage {
7
+ index: number;
8
+ role: "user" | "assistant" | "system" | "tool" | "toolResult" | "developer";
9
+ content: string;
10
+ }
11
+
12
+ export interface CrystalSnapshot {
13
+ revision: number;
14
+ start: number;
15
+ end: number;
16
+ digest: string;
17
+ messages: CrystalMessage[];
18
+ }
19
+
20
+ export interface CrystalItem {
21
+ id: string;
22
+ kind: CrystalItemKind;
23
+ classification: CrystalClassification;
24
+ statement: string;
25
+ anchor?: { message_index: number; quote: string };
26
+ }
27
+
28
+ export interface CrystalInput {
29
+ snapshot: CrystalSnapshot;
30
+ current_revision: number;
31
+ items: CrystalItem[];
32
+ removed_ids?: string[];
33
+ removed_item_anchors?: CrystalResolutionAnchor[];
34
+ open_gaps?: string[];
35
+ conflicts?: string[];
36
+ resolved_open_gaps?: string[];
37
+ resolved_conflicts?: string[];
38
+ resolved_open_gap_anchors?: CrystalResolutionAnchor[];
39
+ resolved_conflict_anchors?: CrystalResolutionAnchor[];
40
+ prior?: DeepInterviewCrystal;
41
+ }
42
+
43
+ export interface CrystalResolutionAnchor {
44
+ item: string;
45
+ message_index: number;
46
+ quote: string;
47
+ resolution: string;
48
+ }
49
+
50
+ export interface CrystalDelta {
51
+ kind: "none" | "additive" | "intent-changed" | "goal-replaced" | "stale";
52
+ changed_ids: string[];
53
+ added_ids: string[];
54
+ preserved_ids: string[];
55
+ approval_invalidated: boolean;
56
+ }
57
+
58
+ export interface DeepInterviewCrystal {
59
+ schema_version: 1;
60
+ spec_version: number;
61
+ lifecycle: "ready" | "needs-questions" | "stale" | "superseded";
62
+ source: { revision: number; start: number; end: number; digest: string; messages: CrystalMessage[] };
63
+ items: CrystalItem[];
64
+ removed_ids?: string[];
65
+ removed_item_anchors?: CrystalResolutionAnchor[];
66
+ pending_removals?: string[];
67
+ open_gaps: string[];
68
+ conflicts: string[];
69
+ delta: CrystalDelta;
70
+ execution_approval: "not-approved";
71
+ }
72
+
73
+ const MAX_MESSAGES = 200;
74
+ const MAX_ITEMS = 128;
75
+ const MAX_TEXT = 10_000;
76
+ const ITEM_KINDS: readonly CrystalItemKind[] = ["goal", "constraint", "decision", "acceptance_criterion", "non_goal"];
77
+ const CLASSIFICATIONS: readonly CrystalClassification[] = ["confirmed", "inferred", "disputed"];
78
+ const EVIDENCE_SEGMENTER = new Intl.Segmenter("en", { granularity: "word" });
79
+ const CJK_NEGATOR_TERMS = new Set(["不", "無", "无", "没", "沒", "未", "否", "勿", "毋", "别", "別", "莫"]);
80
+ const SEMANTIC_EVIDENCE_TERMS = new Set([
81
+ "no",
82
+ "not",
83
+ "never",
84
+ "neither",
85
+ "without",
86
+ "cannot",
87
+ "can't",
88
+ "won't",
89
+ "don't",
90
+ "doesn't",
91
+ "isn't",
92
+ "aren't",
93
+ "wasn't",
94
+ "weren't",
95
+ "shouldn't",
96
+ "mustn't",
97
+ "needn't",
98
+ "must",
99
+ "should",
100
+ "need",
101
+ "needs",
102
+ "required",
103
+ "require",
104
+ "shall",
105
+ "ought",
106
+ "may",
107
+ "might",
108
+ "could",
109
+ "would",
110
+ "if",
111
+ "unless",
112
+ "provided",
113
+ "assuming",
114
+ "maybe",
115
+ "perhaps",
116
+ "possibly",
117
+ "probably",
118
+ "likely",
119
+ "unlikely",
120
+ "seems",
121
+ "seem",
122
+ "apparently",
123
+ "or",
124
+ "either",
125
+ "alternatively",
126
+ "instead",
127
+ "rather",
128
+ "but",
129
+ "however",
130
+ "although",
131
+ "though",
132
+ "except",
133
+ "refuse",
134
+ "refused",
135
+ "reject",
136
+ "rejected",
137
+ "defer",
138
+ "deferred",
139
+ "pending",
140
+ "uncertain",
141
+ "unknown",
142
+ "undecided",
143
+ "unresolved",
144
+ "unclear",
145
+ ]);
146
+ const RESOLUTION_GENERIC_TERMS = new Set([
147
+ "ok",
148
+ "yes",
149
+ "done",
150
+ "resolved",
151
+ "confirmed",
152
+ "accepted",
153
+ "fine",
154
+ "selected",
155
+ "select",
156
+ "chosen",
157
+ "choose",
158
+ "set",
159
+ "sets",
160
+ "follow",
161
+ "follows",
162
+ "remain",
163
+ "remains",
164
+ "stays",
165
+ "stay",
166
+ "unchanged",
167
+ "same",
168
+ "different",
169
+ "fixed",
170
+ "defined",
171
+ "available",
172
+ "possible",
173
+ "valid",
174
+ "invalid",
175
+ "answer",
176
+ "answers",
177
+ "decision",
178
+ "decisions",
179
+ "choice",
180
+ "choices",
181
+ "selection",
182
+ "selections",
183
+ "resolution",
184
+ "resolutions",
185
+ "determined",
186
+ "specified",
187
+ ]);
188
+ const RESOLUTION_META_TERMS = new Set([
189
+ "answer",
190
+ "answers",
191
+ "decision",
192
+ "decisions",
193
+ "choice",
194
+ "choices",
195
+ "selection",
196
+ "selections",
197
+ "resolution",
198
+ "resolutions",
199
+ "determination",
200
+ "determinations",
201
+ "still",
202
+ "decide",
203
+ ]);
204
+ const RESOLUTION_QUESTION_TERMS = new Set([
205
+ "what",
206
+ "which",
207
+ "when",
208
+ "where",
209
+ "why",
210
+ "how",
211
+ "whether",
212
+ "is",
213
+ "are",
214
+ "does",
215
+ "do",
216
+ "can",
217
+ "could",
218
+ "should",
219
+ "would",
220
+ "will",
221
+ "얼마",
222
+ "몇",
223
+ "무엇",
224
+ "무엇을",
225
+ "어떤",
226
+ "언제",
227
+ "어디",
228
+ "왜",
229
+ "어떻게",
230
+ "인지",
231
+ "인가",
232
+ "多少",
233
+ "幾",
234
+ "什么",
235
+ "什麼",
236
+ "哪个",
237
+ "哪個",
238
+ "为何",
239
+ "為何",
240
+ "怎么",
241
+ "怎麼",
242
+ "如何",
243
+ "何时",
244
+ "何時",
245
+ "哪里",
246
+ "哪裡",
247
+ "是否",
248
+ "いくら",
249
+ "何",
250
+ "なに",
251
+ "どの",
252
+ "いつ",
253
+ "どこ",
254
+ "なぜ",
255
+ "どう",
256
+ "ですか",
257
+ ]);
258
+ const CONFLICT_STATUS_TERMS = new Set([
259
+ "dispute",
260
+ "disputed",
261
+ "disputes",
262
+ "conflict",
263
+ "conflicts",
264
+ "contradiction",
265
+ "contradictory",
266
+ "uncertain",
267
+ "unknown",
268
+ "unresolved",
269
+ "unclear",
270
+ "undecided",
271
+ "pending",
272
+ "contested",
273
+ "disagreement",
274
+ "争议",
275
+ "爭議",
276
+ "冲突",
277
+ "衝突",
278
+ "분쟁",
279
+ "충돌",
280
+ "모순",
281
+ ]);
282
+
283
+ type CrystalSemanticProfile = {
284
+ negative: boolean;
285
+ interrogative: boolean;
286
+ conditional: boolean;
287
+ hedged: boolean;
288
+ alternative: boolean;
289
+ contradictory: boolean;
290
+ refusal: boolean;
291
+ unresolved: boolean;
292
+ obligation: string;
293
+ comparison: string;
294
+ exclusive: boolean;
295
+ stateValue: string;
296
+ };
297
+
298
+ function isRecord(value: unknown): value is Record<string, unknown> {
299
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
300
+ }
301
+
302
+ function text(value: unknown, name: string, max = MAX_TEXT): string {
303
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} must be non-empty text`);
304
+ const result = value.normalize("NFC").trim();
305
+ if ([...result].length > max) throw new Error(`${name} exceeds max length ${max}`);
306
+ return result;
307
+ }
308
+
309
+ function snapshotText(value: unknown, name: string): string {
310
+ if (typeof value !== "string") throw new Error(`${name} must be text`);
311
+ const result = value.normalize("NFC").trim();
312
+ if ([...result].length > MAX_TEXT) throw new Error(`${name} exceeds max length ${MAX_TEXT}`);
313
+ return result;
314
+ }
315
+
316
+ function integer(value: unknown, name: string): number {
317
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
318
+ throw new Error(`${name} must be a non-negative integer`);
319
+ return value;
320
+ }
321
+
322
+ function containsNonTextMarker(value: string): boolean {
323
+ return (
324
+ /(?:\[(?:image|audio|video|file|content|toolCall|thinking|sticker|attachment|document)\]?|(?:image|audio|video|file|content|toolCall|thinking|sticker|attachment|document)\])/i.test(
325
+ value,
326
+ ) ||
327
+ /^(?:\[?(?:image|audio|video|file|content|toolCall|thinking|sticker|attachment|document)\]?)+$/i.test(
328
+ value.trim(),
329
+ ) ||
330
+ /^(?:\[[^\]]+\])+$/.test(value.trim())
331
+ );
332
+ }
333
+
334
+ // Projection markers are evidence boundaries, never authored text. Keep offsets in
335
+ // the authenticated message so coverage and correction ordering use the same source.
336
+ function authoredTextSpans(content: string): Array<{ text: string; offset: number }> {
337
+ const spans: Array<{ text: string; offset: number }> = [];
338
+ let offset = 0;
339
+ for (const marker of content.matchAll(
340
+ /\[(?:image|audio|video|file|content|toolCall|thinking|redactedThinking|sticker|attachment|document)\]/giu,
341
+ )) {
342
+ spans.push({ text: content.slice(offset, marker.index), offset });
343
+ offset = marker.index + marker[0].length;
344
+ }
345
+ spans.push({ text: content.slice(offset), offset });
346
+ return spans.filter(span => span.text.trim() !== "");
347
+ }
348
+
349
+ function anchoredTextSpan(content: string, quote: string): string | undefined {
350
+ if (containsNonTextMarker(quote)) return undefined;
351
+ return authoredTextSpans(content).find(span => !containsNonTextMarker(span.text) && span.text.includes(quote))?.text;
352
+ }
353
+
354
+ function anchoredClause(content: string, quote: string): string {
355
+ const span = anchoredTextSpan(content, quote);
356
+ if (span === undefined) return content;
357
+ content = span;
358
+ const quoteIndex = content.indexOf(quote);
359
+ const before = content.slice(0, quoteIndex);
360
+ if (/[.!?。!?]["'”’]?$/.test(quote.trim()) && (quoteIndex === 0 || /[.!?。!?]["'”’]?\s*$/u.test(before)))
361
+ return quote.trim();
362
+ const boundaryPattern = /(?:[!?]["'”’]?\s+|\.["'”’]?\s+(?=[\p{Lu}"'])|\n+)/gu;
363
+ let boundary = -1;
364
+ for (const match of before.matchAll(boundaryPattern)) boundary = match.index + match[0].length - 1;
365
+ const afterStart = quoteIndex + quote.length;
366
+ const after = content.slice(afterStart);
367
+ const endMatch = /(?:[!?]["'”’]?(?:\s|$)|\.["'”’]?(?=\s+[\p{Lu}"']|$)|\n)/u.exec(after);
368
+ const end = endMatch ? afterStart + endMatch.index + 1 : content.length;
369
+ return content.slice(boundary + 1, end).trim();
370
+ }
371
+
372
+ function hasVerbatimTokenBoundaries(content: string, quote: string): boolean {
373
+ const span = anchoredTextSpan(content, quote);
374
+ if (span === undefined) return false;
375
+ content = span;
376
+ const quoteIndex = content.indexOf(quote);
377
+ const word = /[\p{L}\p{M}\p{N}_\u200C\u200D]/u;
378
+ const first = [...quote][0];
379
+ const last = [...quote].at(-1);
380
+ const before = quoteIndex > 0 ? content.slice(0, quoteIndex).match(/.$/u)?.[0] : undefined;
381
+ const after = content.slice(quoteIndex + quote.length).match(/^./u)?.[0];
382
+ return !(
383
+ (first && before && word.test(first) && word.test(before)) ||
384
+ (last && after && word.test(last) && word.test(after))
385
+ );
386
+ }
387
+
388
+ function hasLaterSupersedingCorrection(content: string, quote: string, statement: string): boolean {
389
+ const quoteIndex = content.indexOf(quote);
390
+ if (quoteIndex < 0) return false;
391
+ const later = authoredTextSpans(content.slice(quoteIndex + quote.length))
392
+ .map(span => span.text)
393
+ .join("\n");
394
+ const following = later.replace(/^\s*[.!?。!?]+\s*/u, "");
395
+ const followingBoundary = /(?:[!?。!?;]|\.(?=\s|$)|\n)/u.exec(following);
396
+ const followingClause = followingBoundary ? following.slice(0, followingBoundary.index + 1) : following;
397
+ const topicOverlap = [...topicTerms(statement, false)].some(term => evidenceTerms(followingClause).has(term));
398
+ const directNegativeReplacement =
399
+ /^\s*[.!?。!?]*\s*(?:no|not)\s*[,;:.!?。!?]?\s*(?:use|make|choose|select|switch|change|replace)\b/i.test(
400
+ later,
401
+ );
402
+ const followingClauses = following
403
+ .split(/(?:[!?。!?;]|\.(?=\s|$)|\n)+\s*/u)
404
+ .filter(clause => clause.trim() !== "");
405
+ const negativeCorrection =
406
+ /\b(?:no|not|never|cannot|can['’]t|won['’]t|don['’]t|doesn['’]t|isn['’]t|aren['’]t|wasn['’]t|weren['’]t|shouldn['’]t|mustn['’]t|needn['’]t|do\s+not)\b/i;
407
+ const explicitNegativeReplacement =
408
+ directNegativeReplacement ||
409
+ followingClauses.some((clause, index) => {
410
+ if (!negativeCorrection.test(clause)) return false;
411
+ const clauseTerms = evidenceTerms(clause);
412
+ if ([...topicTerms(statement, false)].some(term => clauseTerms.has(term))) return true;
413
+ const previousClause = index > 0 ? followingClauses[index - 1]! : "";
414
+ return (
415
+ /\b(?:it|this|that|them|these|those)\b/i.test(clause) &&
416
+ [...topicTerms(statement, false)].some(term => evidenceTerms(previousClause).has(term))
417
+ );
418
+ });
419
+ const positiveReplacementMatch =
420
+ /(?:(?:switch|change)\s+to\s+([A-Za-z0-9][A-Za-z0-9+#.-]*)|replace\b[^.!?。!?]*\bwith\s+([A-Za-z0-9][A-Za-z0-9+#.-]*))/i.exec(
421
+ followingClause,
422
+ );
423
+ const directPositiveReplacement =
424
+ positiveReplacementMatch !== null &&
425
+ (topicOverlap ||
426
+ [positiveReplacementMatch[1], positiveReplacementMatch[2]]
427
+ .map(term => term?.replace(/[.!?。!?]+$/u, ""))
428
+ .filter((term): term is string => typeof term === "string")
429
+ .some(
430
+ term =>
431
+ isShortTechnicalIdentifier(term) ||
432
+ /^(?:C\+\+|C#|F#)$/i.test(term) ||
433
+ /(?:[a-z][A-Z]|[A-Z].*[A-Z])/.test(term),
434
+ ));
435
+ const standaloneConditionalQualifier =
436
+ /^\s*[.!?。!?]*\s*(?:only\s+)?(?:if|unless|until|when|assuming|in\s+case|contingent|provided(?:\s+that)?|depending\s+on)\b[^,;.!?。!?]*[.!?。!?]?\s*$/i.test(
437
+ later,
438
+ );
439
+ const statementHasShortTechnical = (statement.match(/[A-Za-z0-9][A-Za-z0-9+#.-]*/g) ?? []).some(
440
+ isShortTechnicalIdentifier,
441
+ );
442
+ const statementUseTarget = /\buse\s+([A-Za-z0-9][A-Za-z0-9+#.-]*)/i.exec(statement)?.[1];
443
+ const isTechnicalSubstitutionTarget = (term: string): boolean =>
444
+ isShortTechnicalIdentifier(term) ||
445
+ /^(?:C\+\+|C#|F#)$/i.test(term) ||
446
+ /(?:[a-z][A-Z]|[A-Z].*[A-Z])/.test(term) ||
447
+ LONG_TECHNICAL_TERMS.has(term.toLowerCase());
448
+ const genericCorrection = followingClauses.some(clause => {
449
+ const marker =
450
+ /\b(?:actually|instead|rather|correction|on\s+second\s+thought|make\s+that)\b/i.test(clause) ||
451
+ /(?:사실|대신|정정|다시\s+생각|実際|代わり|訂正|やはり|实际上|實際上|改为|改為|更正)/u.test(clause);
452
+ if (!marker) return false;
453
+ if (/\b(?:also|too|additionally|as\s+well|in\s+addition)\b/i.test(clause)) return false;
454
+ const clauseTerms = evidenceTerms(clause);
455
+ return (
456
+ [...topicTerms(statement, false)].some(
457
+ term => !/^(?:build|make|create|implement|support|actually|please)$/.test(term) && clauseTerms.has(term),
458
+ ) ||
459
+ (statementHasShortTechnical &&
460
+ (clause.match(/[A-Za-z0-9][A-Za-z0-9+#.-]*/g) ?? []).some(isShortTechnicalIdentifier)) ||
461
+ Boolean(
462
+ statementUseTarget &&
463
+ isTechnicalSubstitutionTarget(statementUseTarget) &&
464
+ (() => {
465
+ const target = /\buse\s+([A-Za-z0-9][A-Za-z0-9+#.-]*)/i.exec(clause)?.[1];
466
+ return Boolean(target && isTechnicalSubstitutionTarget(target));
467
+ })(),
468
+ )
469
+ );
470
+ });
471
+ return (
472
+ genericCorrection || standaloneConditionalQualifier || explicitNegativeReplacement || directPositiveReplacement
473
+ );
474
+ }
475
+
476
+ function semanticProfile(value: string): CrystalSemanticProfile {
477
+ const normalized = value.normalize("NFC").replaceAll("’", "'").toLowerCase();
478
+ const obligationTerms = new Set<string>();
479
+ if (
480
+ /\b(?:must|have\s+to|required|required\s+to|require|shall)\b/i.test(normalized) ||
481
+ /(?:반드시|필수|해야|필요|必須|必要|なければなら|必须|必須|需要|应当|應當)/u.test(normalized)
482
+ )
483
+ obligationTerms.add("required");
484
+ if (
485
+ /\b(?:should|ought\s+to|recommended|recommend)\b/i.test(normalized) ||
486
+ /(?:권장|하는\s+것이\s+좋|べき|推荐|推薦|应该|應該)/u.test(normalized)
487
+ )
488
+ obligationTerms.add("recommended");
489
+ if (/\bneed(?:s|ed)?\b/i.test(normalized) || /(?:필요하다|필요한|必要だ|需要)/u.test(normalized))
490
+ obligationTerms.add("need");
491
+ if (
492
+ /\b(?:can|may|might|could|would)\b/i.test(normalized) ||
493
+ /(?:할\s+수|가능|てもよい|かもしれ|可以|可能)/u.test(normalized)
494
+ )
495
+ obligationTerms.add("permissive");
496
+ const negative =
497
+ /\b(?:no|not|never|neither|without|cannot|can't|won't|don['’]t|doesn't|isn't|aren't|wasn't|weren't|shouldn't|mustn't|needn't|avoid|prohibit(?:s|ed)?|forbid(?:s|den)?|ban(?:s|ned)?)\b/i.test(
498
+ normalized,
499
+ ) ||
500
+ /(?:안|않|못|없|아니|하지\s*마|마세요|말자|금지|禁止|ない|ません|ぬ|ず|たくない|不|無|无|没|沒|未|否|勿|毋|别|別|莫)/u.test(
501
+ normalized,
502
+ );
503
+ // Clause-initial auxiliary inversion is unsafe regardless of the actor or action.
504
+ // Exclude direct negative imperatives such as "Do not export reports".
505
+ const interrogative =
506
+ /[??]/u.test(normalized) ||
507
+ /\b(?:whether|wonder(?:s|ing)?|question(?:s|ed|ing)?)\b/i.test(normalized) ||
508
+ /(?:^|[.!?。!?;\n]\s*)\s*["'“‘]?(?:can|could|should|would|will|shall|must|may|do|does|did|is|are|was|were|has|have|had)\s+(?!not\b|never\b)[\p{L}][\p{L}\p{N}_'-]*/iu.test(
509
+ normalized,
510
+ ) ||
511
+ /(?:吗|嗎|呢|나요|습니까|습니까|인가요|인가|ㄹ까요|을까요|까요|궁금|어떻게|무엇|무엇을|왜|언제|어디|얼마|몇|多少|什么|什麼|哪个|哪個|为何|為何|怎么|怎麼|如何|何时|何時|哪里|哪裡|是否|疑問|かどうか)/u.test(
512
+ normalized,
513
+ ) ||
514
+ /(?:か|かな)(?:[??。.!!]|$)/u.test(normalized);
515
+ const conditional =
516
+ /\b(?:if|unless|until|before|after|once|provided(?:\s+that)?|assuming|in\s+case|contingent|when|depending\s+on)\b/i.test(
517
+ normalized,
518
+ ) ||
519
+ /(?:만약|하면|라면|다면|으면|이면|경우|조건|경우에\s+따라|もし|なら|れば|たら|場合|条件|次第|如果|若|假如|倘若|除非|只要|情况下|取决于|取決於)/u.test(
520
+ normalized,
521
+ );
522
+ // Only explicit actors performing deliberate actions establish permission.
523
+ // Other uses of may (including a second uncertain clause) remain hedges.
524
+ const withoutPermissions = normalized.replace(
525
+ /\b(?:(?:the\s+)?(?:admins?|administrators?|users?|operators?|owners?|members?|guests?|clients?|customers?|editors?|viewers?)|we|you|they)\s+may\s+(?:not\s+)?(?:export|import|read|write|create|delete|update|view|access|manage|edit|download|upload|share|use|run|configure|approve|submit|invite)\b/g,
526
+ "",
527
+ );
528
+ const hedged =
529
+ /\b(?:maybe|perhaps|possibly|probably|might|may|could|would|likely|unlikely|seems?|apparently|approximately|around|roughly|tentative(?:ly)?|prefer(?:ably)?|i\s+think|i\s+guess|i\s+believe|believe(?:s|d)?)\b/i.test(
530
+ withoutPermissions,
531
+ ) ||
532
+ /(?:아마|어쩌면|가능성|수도|것\s+같|같습니다|추정|대략|たぶん|おそらく|かもしれ|可能性|と思|思われ|だろう|でしょう|也许|也許|可能|或许|大概|似乎|大約|估计|估計|据说|據說)/u.test(
533
+ normalized,
534
+ );
535
+ const alternative =
536
+ /\b(?:or|either|alternatively|one\s+of|option(?:s)?|versus|vs)\b/i.test(normalized) ||
537
+ /\s\/\s/.test(normalized) ||
538
+ /[\p{L}\p{N}]\/[\p{L}\p{N}]/u.test(normalized) ||
539
+ /(?:또는|혹은|아니면|대안|または|もしくは|いずれか|或者|或是|或|还是|替代)/u.test(normalized);
540
+ const contradictory =
541
+ /\b(?:but|however|instead|rather\s+than|although|though|except|contradict(?:s|ed|ion)?|disagree(?:s|d)?|versus|vs)\b/i.test(
542
+ normalized,
543
+ ) || /(?:하지만|그러나|반면|대신|모순|矛盾|但是|不过|不過|然而|しかし|だが|ところが|代わりに)/u.test(normalized);
544
+ const refusal =
545
+ /\b(?:refus(?:e|ed|es|ing)|declin(?:e|ed|es|ing)|won't|can't|cannot|unable\s+to|not\s+able\s+to)\b/i.test(
546
+ normalized,
547
+ ) ||
548
+ /(?:답변할\s+수\s+없|대답할\s+수\s+없|말할\s+수\s+없|하지\s+않|않겠|거부|回答できない|答えられない|答えない|したくない|拒否|无法回答|不能回答|不愿回答|不願回答|不愿|不願|拒绝|拒絕|无法决定|不能决定)/u.test(
549
+ normalized,
550
+ );
551
+ const comparison = [
552
+ ...new Set(
553
+ [
554
+ ...normalized.matchAll(
555
+ /(?:>=|<=|>|<)|\b(?:at\s+least|at\s+most|no\s+less\s+than|no\s+more\s+than|less\s+than|more\s+than|under|over|below|above|exactly)\b/g,
556
+ ),
557
+ ].map(match => match[0]),
558
+ ),
559
+ ]
560
+ .sort()
561
+ .join(",");
562
+ const exclusive = /\b(?:only|exclusively|solely)\b/i.test(normalized);
563
+ const stateValue = [...new Set([...normalized.matchAll(/\b(?:on|off|up|down)\b/g)].map(match => match[0]))]
564
+ .sort()
565
+ .join(",");
566
+ return {
567
+ negative,
568
+ interrogative,
569
+ conditional,
570
+ hedged,
571
+ alternative,
572
+ contradictory,
573
+ refusal,
574
+ unresolved: isExplicitlyUnresolved(normalized),
575
+ obligation: [...obligationTerms].sort().join(","),
576
+ comparison,
577
+ exclusive,
578
+ stateValue,
579
+ };
580
+ }
581
+
582
+ function sameSemanticIntent(left: CrystalSemanticProfile, right: CrystalSemanticProfile): boolean {
583
+ return (
584
+ left.negative === right.negative &&
585
+ left.interrogative === right.interrogative &&
586
+ left.conditional === right.conditional &&
587
+ left.hedged === right.hedged &&
588
+ left.alternative === right.alternative &&
589
+ left.contradictory === right.contradictory &&
590
+ left.refusal === right.refusal &&
591
+ left.unresolved === right.unresolved &&
592
+ left.obligation === right.obligation &&
593
+ left.comparison === right.comparison &&
594
+ left.exclusive === right.exclusive &&
595
+ left.stateValue === right.stateValue
596
+ );
597
+ }
598
+
599
+ function isUnsafeConfirmedStatement(profile: CrystalSemanticProfile): boolean {
600
+ return (
601
+ profile.interrogative ||
602
+ profile.conditional ||
603
+ profile.hedged ||
604
+ profile.alternative ||
605
+ profile.refusal ||
606
+ profile.unresolved
607
+ );
608
+ }
609
+
610
+ /** A terse acknowledgement has no requirement context of its own and cannot anchor a Crystal item. */
611
+ function isContextFreeAcknowledgement(value: string): boolean {
612
+ const normalized = value.replace(/[\p{P}\s]+$/gu, "").trim();
613
+ return /^(?:yes|no|yep|yeah|yup|sure|okay|ok|affirmative|true|false|correct|incorrect|agreed|absolutely|done|finished|confirmed|confirm|acknowledged|understood|got\s+it|sounds\s+good|looks\s+good|great|fine|thanks|thank\s+you|네|예|맞아|맞습니다|是|对|對|はい|ええ)$/iu.test(
614
+ normalized,
615
+ );
616
+ }
617
+
618
+ export function crystalSnapshotDigest(
619
+ snapshot: Pick<CrystalSnapshot, "revision" | "start" | "end" | "messages">,
620
+ ): string {
621
+ return createHash("sha256")
622
+ .update(
623
+ JSON.stringify({
624
+ revision: snapshot.revision,
625
+ start: snapshot.start,
626
+ end: snapshot.end,
627
+ messages: snapshot.messages.map(message => ({
628
+ ...message,
629
+ content: message.content.normalize("NFC").trim(),
630
+ })),
631
+ }),
632
+ )
633
+ .digest("hex");
634
+ }
635
+
636
+ function validateSnapshot(value: unknown): CrystalSnapshot {
637
+ if (!isRecord(value)) throw new Error("crystallize snapshot is required");
638
+ const revision = integer(value.revision, "snapshot.revision");
639
+ const start = integer(value.start, "snapshot.start");
640
+ const end = integer(value.end, "snapshot.end");
641
+ if (end < start) throw new Error("snapshot.end must be >= snapshot.start");
642
+ if (!Array.isArray(value.messages) || value.messages.length > MAX_MESSAGES)
643
+ throw new Error("snapshot.messages must be bounded");
644
+ const messages = value.messages.map((entry, index) => {
645
+ if (!isRecord(entry)) throw new Error(`snapshot.messages[${index}] is invalid`);
646
+ const messageIndex = integer(entry.index, `snapshot.messages[${index}].index`);
647
+ if (messageIndex < start || messageIndex > end) throw new Error("snapshot message is outside its declared range");
648
+ const role = entry.role as CrystalMessage["role"];
649
+ if (
650
+ role !== "user" &&
651
+ role !== "assistant" &&
652
+ role !== "system" &&
653
+ role !== "tool" &&
654
+ role !== "toolResult" &&
655
+ role !== "developer"
656
+ )
657
+ throw new Error("snapshot message role is invalid");
658
+ return { index: messageIndex, role, content: snapshotText(entry.content, `snapshot.messages[${index}].content`) };
659
+ });
660
+ if (messages.some((message, index) => index > 0 && message.index <= messages[index - 1]!.index))
661
+ throw new Error("snapshot messages must be ordered and unique");
662
+ if (end - start + 1 > MAX_MESSAGES) throw new Error("snapshot range is too large");
663
+ if (messages.length !== end - start + 1 || messages.some((message, index) => message.index !== start + index))
664
+ throw new Error("snapshot messages must cover the declared range");
665
+ const digest = text(value.digest, "snapshot.digest", 64);
666
+ if (!/^[a-f0-9]{64}$/.test(digest) || digest !== crystalSnapshotDigest({ revision, start, end, messages }))
667
+ throw new Error("snapshot digest mismatch");
668
+ return { revision, start, end, digest, messages };
669
+ }
670
+
671
+ function validateItems(value: unknown, snapshot?: CrystalSnapshot): CrystalItem[] {
672
+ if (!Array.isArray(value) || value.length > MAX_ITEMS) throw new Error("crystallize items must be a bounded array");
673
+ const ids = new Set<string>();
674
+ return value.map((entry, index) => {
675
+ if (!isRecord(entry)) throw new Error(`crystallize item ${index} is invalid`);
676
+ const id = text(entry.id, `items[${index}].id`, 128);
677
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(id)) throw new Error(`items[${index}].id is invalid`);
678
+ if (ids.has(id)) throw new Error(`duplicate crystallize item: ${id}`);
679
+ ids.add(id);
680
+ if (!ITEM_KINDS.includes(entry.kind as CrystalItemKind)) throw new Error(`items[${index}].kind is invalid`);
681
+ if (!CLASSIFICATIONS.includes(entry.classification as CrystalClassification))
682
+ throw new Error(`items[${index}].classification is invalid`);
683
+ const item: CrystalItem = {
684
+ id,
685
+ kind: entry.kind as CrystalItemKind,
686
+ classification: entry.classification as CrystalClassification,
687
+ statement: text(entry.statement, `items[${index}].statement`),
688
+ };
689
+ if (item.classification === "confirmed") {
690
+ if (!isRecord(entry.anchor)) throw new Error(`confirmed item ${id} requires a verbatim anchor`);
691
+ item.anchor = {
692
+ message_index: integer(entry.anchor.message_index, `items[${index}].anchor.message_index`),
693
+ quote: text(entry.anchor.quote, `items[${index}].anchor.quote`),
694
+ };
695
+ if (snapshot) {
696
+ const anchorMessage = snapshot.messages.find(
697
+ message => String(message.index) === String(item.anchor!.message_index),
698
+ );
699
+ if (!anchorMessage)
700
+ throw new Error(`confirmed item ${id} anchor message ${item.anchor!.message_index} is missing`);
701
+ const statementTerms = evidenceTerms(item.statement);
702
+ const quoteTerms = evidenceTerms(item.anchor.quote);
703
+ const statementQuantitative = quantitativeEvidenceTerms(item.statement);
704
+ const quoteQuantitative = quantitativeEvidenceTerms(item.anchor.quote);
705
+ const statementIdentifiers = caseSensitiveIdentifierTerms(item.statement);
706
+ const quoteIdentifiers = caseSensitiveIdentifierTerms(item.anchor.quote);
707
+
708
+ const statementSemantics = semanticProfile(item.statement);
709
+ const quoteSemantics = semanticProfile(anchoredClause(anchorMessage.content, item.anchor.quote));
710
+ const authenticatedNonGoal =
711
+ item.kind !== "non_goal" ||
712
+ quoteSemantics.negative ||
713
+ /\b(?:non[- ]?goal|out\s+of\s+scope|excluded?|do\s+not|don't|dont|won't|will\s+not|must\s+not)\b/i.test(
714
+ item.anchor.quote,
715
+ );
716
+ const fullAnchorClause = anchoredClause(anchorMessage.content, item.anchor.quote);
717
+ const quoteClauses = fullAnchorClause.split(/(?:[!?。!?;]|\.(?=\s|$)|\n)+\s*/u).filter(Boolean);
718
+ const partialMultiDirectiveAnchor =
719
+ fullAnchorClause !== item.anchor.quote.trim() &&
720
+ (quoteClauses.length > 1 || /:|\b(?:and|plus|as\s+well\s+as)\b/i.test(fullAnchorClause));
721
+ const mixedClausePolarity =
722
+ quoteClauses.length > 1 &&
723
+ new Set(quoteClauses.map(clause => semanticProfile(clause).negative)).size > 1;
724
+ if (
725
+ anchorMessage.role !== "user" ||
726
+ containsNonTextMarker(item.anchor.quote) ||
727
+ !anchorMessage.content.includes(item.anchor.quote) ||
728
+ !hasVerbatimTokenBoundaries(anchorMessage.content, item.anchor.quote) ||
729
+ hasLaterSupersedingCorrection(anchorMessage.content, item.anchor.quote, item.statement) ||
730
+ quoteTerms.size === 0 ||
731
+ isContextFreeAcknowledgement(item.statement) ||
732
+ isContextFreeAcknowledgement(item.anchor.quote) ||
733
+ statementTerms.size === 0 ||
734
+ [...statementTerms].some(term => !quoteTerms.has(term)) ||
735
+ [...quoteTerms].some(term => !statementTerms.has(term)) ||
736
+ statementQuantitative.size !== quoteQuantitative.size ||
737
+ [...statementQuantitative].some(term => !quoteQuantitative.has(term)) ||
738
+ statementIdentifiers.size !== quoteIdentifiers.size ||
739
+ [...statementIdentifiers].some(term => !quoteIdentifiers.has(term)) ||
740
+ (statementQuantitative.size > 0 &&
741
+ JSON.stringify(quantitativeEvidenceSequence(item.statement)) !==
742
+ JSON.stringify(quantitativeEvidenceSequence(item.anchor.quote))) ||
743
+ mixedClausePolarity ||
744
+ partialMultiDirectiveAnchor ||
745
+ !authenticatedNonGoal ||
746
+ !preservesEvidenceOrder(item.statement, item.anchor.quote) ||
747
+ isUnsafeConfirmedStatement(statementSemantics) ||
748
+ !sameSemanticIntent(statementSemantics, quoteSemantics)
749
+ )
750
+ throw new Error(
751
+ `confirmed item ${id} has no statement-bound verbatim user anchor (conservative derivation failed)`,
752
+ );
753
+ }
754
+ }
755
+ return item;
756
+ });
757
+ }
758
+
759
+ function normalizedGaps(value: unknown): string[] {
760
+ if (value === undefined) return [];
761
+ if (!Array.isArray(value) || value.length > 16) throw new Error("open_gaps must be a bounded array");
762
+ return value.map((gap, index) => text(gap, `open_gaps[${index}]`, 500));
763
+ }
764
+
765
+ function isMeaningfulEvidenceWord(value: string): boolean {
766
+ const codePointLength = [...value].length;
767
+ return (
768
+ codePointLength >= 3 ||
769
+ (codePointLength >= 2 && /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(value)) ||
770
+ SEMANTIC_EVIDENCE_TERMS.has(value.toLowerCase()) ||
771
+ [...value].some(codePoint => CJK_NEGATOR_TERMS.has(codePoint))
772
+ );
773
+ }
774
+
775
+ const SHORT_EVIDENCE_STOPWORDS = new Set([
776
+ "a",
777
+ "an",
778
+ "as",
779
+ "at",
780
+ "be",
781
+ "by",
782
+ "do",
783
+ "he",
784
+ "i",
785
+ "if",
786
+ "in",
787
+ "is",
788
+ "it",
789
+ "me",
790
+ "my",
791
+ "of",
792
+ "on",
793
+ "or",
794
+ "so",
795
+ "to",
796
+ "up",
797
+ "us",
798
+ "we",
799
+ ]);
800
+ const LOWERCASE_SHORT_TECHNICAL_TERMS = new Set([
801
+ "ai",
802
+ "cd",
803
+ "ci",
804
+ "db",
805
+ "gb",
806
+ "go",
807
+ "id",
808
+ "io",
809
+ "ip",
810
+ "js",
811
+ "kb",
812
+ "mb",
813
+ "ml",
814
+ "ms",
815
+ "ns",
816
+ "os",
817
+ "s",
818
+ "tb",
819
+ "ts",
820
+ "ui",
821
+ "us",
822
+ "vm",
823
+ ]);
824
+ const LONG_TECHNICAL_TERMS = new Set([
825
+ "rust",
826
+ "python",
827
+ "java",
828
+ "swift",
829
+ "kotlin",
830
+ "ruby",
831
+ "scala",
832
+ "elixir",
833
+ "erlang",
834
+ ]);
835
+
836
+ function isShortTechnicalIdentifier(value: string): boolean {
837
+ const length = [...value].length;
838
+ const uppercaseTechnical = /^[A-Z0-9]/.test(value) && value !== "A" && value !== "I";
839
+ return (
840
+ length >= 1 &&
841
+ length <= 2 &&
842
+ /^[A-Za-z0-9][A-Za-z0-9+#.-]*$/.test(value) &&
843
+ (uppercaseTechnical ||
844
+ (!SHORT_EVIDENCE_STOPWORDS.has(value.toLowerCase()) &&
845
+ LOWERCASE_SHORT_TECHNICAL_TERMS.has(value.toLowerCase())))
846
+ );
847
+ }
848
+
849
+ function evidenceTerms(value: string): Set<string> {
850
+ const ignored = new Set([
851
+ "and",
852
+ "are",
853
+ "can",
854
+ "does",
855
+ "for",
856
+ "how",
857
+ "needs",
858
+ "requirement",
859
+ "requirements",
860
+ "that",
861
+ "the",
862
+ "this",
863
+ "was",
864
+ "were",
865
+ "what",
866
+ "when",
867
+ "where",
868
+ "which",
869
+ "why",
870
+ "will",
871
+ "with",
872
+ "want",
873
+ "use",
874
+ "using",
875
+ ]);
876
+ const canonical = value.normalize("NFC");
877
+ const normalized = canonical.toLowerCase();
878
+ const terms = new Set(
879
+ [...EVIDENCE_SEGMENTER.segment(canonical)]
880
+ .filter(part => part.isWordLike)
881
+ .filter(part => isMeaningfulEvidenceWord(part.segment) || isShortTechnicalIdentifier(part.segment))
882
+ .map(part => part.segment.toLowerCase())
883
+ .filter(term => !ignored.has(term) || SEMANTIC_EVIDENCE_TERMS.has(term)),
884
+ );
885
+ for (const match of canonical.matchAll(/(?:\b[A-Za-z][A-Za-z0-9]*\.[A-Za-z0-9.]+\b|\b[A-Za-z][+#]{1,2})/g))
886
+ terms.add(match[0].toLowerCase());
887
+ for (const match of canonical.matchAll(/\p{Sc}/gu)) terms.add(match[0]);
888
+ for (const match of canonical.matchAll(/\b\d+(?:\.\d+)?\s*([A-Za-z]{1,3})\b/g)) terms.add(match[1]!.toLowerCase());
889
+ for (const negator of CJK_NEGATOR_TERMS) if (normalized.includes(negator)) terms.add(negator);
890
+ return terms;
891
+ }
892
+
893
+ function quantitativeEvidenceTerms(value: string): Set<string> {
894
+ const canonical = value.normalize("NFC");
895
+ const terms = new Set<string>();
896
+ for (const match of canonical.matchAll(/\p{Sc}|%|>=|<=|>|</gu)) terms.add(match[0]);
897
+ for (const match of canonical
898
+ .toLowerCase()
899
+ .matchAll(
900
+ /\b(?:zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|hundred|thousand|million|billion)\b/g,
901
+ ))
902
+ terms.add(match[0]);
903
+ if (/\d\s*:\s*\d/u.test(canonical)) terms.add(":");
904
+ for (const match of canonical.matchAll(/\b(\d+(?:\.\d+)?)\s*([A-Za-z]+)?\b/g)) {
905
+ terms.add(match[1]!);
906
+ if (match[2]) terms.add(match[2].length <= 3 && /[A-Z]/.test(match[2]) ? match[2] : match[2].toLowerCase());
907
+ }
908
+ return terms;
909
+ }
910
+
911
+ function quantitativeEvidenceSequence(value: string): string[] {
912
+ return [...value.normalize("NFC").matchAll(/\p{Sc}|%|>=|<=|>|<|:|\d+(?:\.\d+)?|[A-Za-z]+/gu)].map(match => {
913
+ const token = match[0];
914
+ return token.length <= 3 && /[A-Z]/.test(token) ? token : token.toLowerCase();
915
+ });
916
+ }
917
+
918
+ function caseSensitiveIdentifierTerms(value: string): Set<string> {
919
+ return new Set(
920
+ [...value.normalize("NFC").matchAll(/\b[A-Za-z][A-Za-z0-9_]*\b/g)]
921
+ .map(match => match[0])
922
+ .filter(
923
+ term =>
924
+ term.includes("_") ||
925
+ (term.length > 1 && term === term.toUpperCase()) ||
926
+ /[a-z0-9][A-Z]/.test(term) ||
927
+ (term.match(/[A-Z]/g)?.length ?? 0) >= 2,
928
+ ),
929
+ );
930
+ }
931
+
932
+ function requirementBearingClauses(
933
+ snapshot: CrystalSnapshot,
934
+ ): Array<{ messageIndex: number; clause: string; clauseOffset: number }> {
935
+ const clauses: Array<{ messageIndex: number; clause: string; clauseOffset: number }> = [];
936
+ for (const message of snapshot.messages) {
937
+ if (message.role !== "user") continue;
938
+ for (const span of authoredTextSpans(message.content)) {
939
+ let searchOffset = 0;
940
+ for (const raw of span.text.split(
941
+ /(?:(?:[!?。!?;]|\.(?=\s|$)|\n)+|,(?=\s*(?:actually|instead|rather|replace|replaced|no longer|not)\b))\s*/iu,
942
+ )) {
943
+ const clause = raw.trim();
944
+ const localOffset = clause ? span.text.indexOf(clause, searchOffset) : searchOffset;
945
+ searchOffset = Math.max(searchOffset, localOffset + clause.length);
946
+ if (
947
+ !clause ||
948
+ /^(?:what|why|how|when|where|who|which)\b/i.test(clause) ||
949
+ /\basked\b[^"“”]*["“”][^"“”]*(?:should|could|would|can|will)\b/i.test(clause) ||
950
+ /^(?:no further changes|nothing else|another question remains|the ambiguity remains open|continue (?:again|with the remaining goal)|yes|no|ok|okay|done|acknowledged|understood|got it|thanks|thank you)$/i.test(
951
+ clause,
952
+ )
953
+ )
954
+ continue;
955
+ if (evidenceTerms(clause).size >= 1)
956
+ clauses.push({ messageIndex: message.index, clause, clauseOffset: span.offset + localOffset });
957
+ }
958
+ }
959
+ }
960
+ return clauses;
961
+ }
962
+
963
+ function evidenceTermSequence(value: string): string[] {
964
+ const terms = evidenceTerms(value);
965
+ const canonical = value.normalize("NFC");
966
+ const technical = [...canonical.matchAll(/(?:\b[A-Za-z][A-Za-z0-9]*\.[A-Za-z0-9.]+\b|\b[A-Za-z][+#]{1,2})/g)].map(
967
+ match => ({ index: match.index, end: match.index + match[0].length, term: match[0].toLowerCase() }),
968
+ );
969
+ for (const match of canonical.matchAll(/\p{Sc}/gu))
970
+ technical.push({ index: match.index, end: match.index + match[0].length, term: match[0] });
971
+ for (const match of canonical.matchAll(/\b\d+(?:\.\d+)?\s*([A-Za-z]{1,3})\b/g)) {
972
+ const unit = match[1]!;
973
+ const index = match.index + match[0].lastIndexOf(unit);
974
+ technical.push({ index, end: index + unit.length, term: unit.toLowerCase() });
975
+ }
976
+ const segmented = [...EVIDENCE_SEGMENTER.segment(canonical)]
977
+ .filter(
978
+ part =>
979
+ part.isWordLike &&
980
+ !technical.some(match => part.index >= match.index && part.index < match.end) &&
981
+ terms.has(part.segment.toLowerCase()),
982
+ )
983
+ .map(part => ({ index: part.index, term: part.segment.toLowerCase() }));
984
+ return [...segmented, ...technical]
985
+ .filter(part => terms.has(part.term))
986
+ .sort((left, right) => left.index - right.index)
987
+ .map(part => part.term);
988
+ }
989
+
990
+ function preservesEvidenceOrder(statement: string, quote: string): boolean {
991
+ const statementTerms = evidenceTermSequence(statement);
992
+ const quoteTerms = evidenceTermSequence(quote);
993
+ return (
994
+ statementTerms.length === quoteTerms.length && statementTerms.every((term, index) => quoteTerms[index] === term)
995
+ );
996
+ }
997
+
998
+ function topicTerms(value: string, conflict: boolean): Set<string> {
999
+ const terms = evidenceTerms(value);
1000
+ for (const term of [...terms]) {
1001
+ if (
1002
+ SEMANTIC_EVIDENCE_TERMS.has(term) ||
1003
+ CJK_NEGATOR_TERMS.has(term) ||
1004
+ RESOLUTION_QUESTION_TERMS.has(term) ||
1005
+ RESOLUTION_META_TERMS.has(term) ||
1006
+ (conflict && CONFLICT_STATUS_TERMS.has(term))
1007
+ )
1008
+ terms.delete(term);
1009
+ }
1010
+ return terms;
1011
+ }
1012
+
1013
+ function hasConcreteResolutionValue(value: string, item: string, conflict: boolean): boolean {
1014
+ const itemTerms = evidenceTerms(item);
1015
+ const resolutionTerms = evidenceTerms(value);
1016
+ const newTerms = [...resolutionTerms].filter(
1017
+ term =>
1018
+ !itemTerms.has(term) &&
1019
+ !SEMANTIC_EVIDENCE_TERMS.has(term) &&
1020
+ !RESOLUTION_GENERIC_TERMS.has(term) &&
1021
+ !(conflict && CONFLICT_STATUS_TERMS.has(term)),
1022
+ );
1023
+ return (
1024
+ newTerms.length > 0 ||
1025
+ /\b(?:yes|no|on|off|true|false|enabled|disabled)\b/i.test(value) ||
1026
+ /(?:예|네|아니요|아니|是|否|はい|いいえ)/u.test(value) ||
1027
+ /\d/.test(value)
1028
+ );
1029
+ }
1030
+
1031
+ function hasCjkTopicOverlap(item: string, resolution: string): boolean {
1032
+ const cjk = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
1033
+ const itemChars = [...item.normalize("NFC").toLowerCase()].filter(character => cjk.test(character));
1034
+ const resolutionText = [...resolution.normalize("NFC").toLowerCase()]
1035
+ .filter(character => cjk.test(character))
1036
+ .join("");
1037
+ if (itemChars.length < 4 || resolutionText.length < 4) return false;
1038
+ for (let length = itemChars.length; length >= 4; length--)
1039
+ for (let start = 0; start + length <= itemChars.length; start++)
1040
+ if (resolutionText.includes(itemChars.slice(start, start + length).join(""))) return true;
1041
+ return false;
1042
+ }
1043
+
1044
+ function isUnsafeResolution(value: string, item: string, conflict: boolean): boolean {
1045
+ const profile = semanticProfile(value);
1046
+ const standaloneBinary =
1047
+ /^(?:yes|no|true|false|enabled|disabled|예|네|아니요|아니|是|否|はい|いいえ)[.!。!?]?$/iu.test(value.trim());
1048
+ const itemTerms = topicTerms(item, conflict);
1049
+ const valueTerms = evidenceTerms(value);
1050
+ const statementBoundNegativeDecision =
1051
+ profile.negative &&
1052
+ itemTerms.size > 0 &&
1053
+ [...itemTerms].every(term => valueTerms.has(term)) &&
1054
+ /\b(?:should|must|will|is|are)\s+not\s+(?:be\s+)?(?:enabled|disabled|allowed|permitted|required|needed|used|supported)\b/i.test(
1055
+ value,
1056
+ );
1057
+ return (
1058
+ (profile.negative && !standaloneBinary && !statementBoundNegativeDecision) ||
1059
+ /\b(?:different\s+from|other\s+than|not\s+equal\s+to|anything\s+but|except\s+for)\b/i.test(value) ||
1060
+ ((/\b(?:less\s+than|greater\s+than|at\s+most|at\s+least|no\s+more\s+than|no\s+less\s+than|under|over|below|above)\b/i.test(
1061
+ value,
1062
+ ) ||
1063
+ /(?:<=|>=|<|>)/.test(value)) &&
1064
+ !/[0-9]/.test(value)) ||
1065
+ profile.interrogative ||
1066
+ profile.conditional ||
1067
+ profile.hedged ||
1068
+ profile.alternative ||
1069
+ profile.contradictory ||
1070
+ profile.refusal ||
1071
+ profile.unresolved
1072
+ );
1073
+ }
1074
+
1075
+ function validateResolutionAnchors(
1076
+ resolutions: readonly string[],
1077
+ value: unknown,
1078
+ snapshot: CrystalSnapshot,
1079
+ field: string,
1080
+ afterIndex: number,
1081
+ ): CrystalResolutionAnchor[] {
1082
+ if (resolutions.length === 0 && value === undefined) return [];
1083
+ if (!Array.isArray(value) || value.length !== resolutions.length)
1084
+ throw new Error(`${field} must contain one anchor per resolution`);
1085
+ const seen = new Set<string>();
1086
+ const anchors: CrystalResolutionAnchor[] = [];
1087
+ for (const [index, raw] of value.entries()) {
1088
+ if (!isRecord(raw)) throw new Error(`${field}[${index}] must be an object`);
1089
+ const item = text(raw.item, `${field}[${index}].item`, 500);
1090
+ const messageIndex = integer(raw.message_index, `${field}[${index}].message_index`);
1091
+ const quote = text(raw.quote, `${field}[${index}].quote`, 500);
1092
+ const resolution = text(raw.resolution, `${field}[${index}].resolution`, 500);
1093
+ const message = snapshot.messages.find(candidate => candidate.index === messageIndex);
1094
+ const conflict = field === "resolved_conflict_anchors";
1095
+ const itemTerms = topicTerms(item, conflict);
1096
+ const resolutionTerms = evidenceTerms(resolution);
1097
+ const conciseBoolean =
1098
+ /^(?:yes|no|true|false)[.!]?$/i.test(resolution.trim()) &&
1099
+ /^(?:should|is|are|can|could|do|does|did|will|would)\b/i.test(item.trim());
1100
+ const conciseState =
1101
+ /^(?:on|off)[.!]?$/i.test(resolution.trim()) &&
1102
+ /\b(?:enabled|disabled|enable|disable|state|toggle)\b/i.test(item);
1103
+ const conciseAnswer = conciseBoolean || conciseState;
1104
+ const addressesItem =
1105
+ (itemTerms.size > 0 && [...itemTerms].every(term => resolutionTerms.has(term))) ||
1106
+ hasCjkTopicOverlap(item, resolution) ||
1107
+ (resolutions.length === 1 && conciseAnswer);
1108
+ const concreteAnswer = hasConcreteResolutionValue(resolution, item, conflict);
1109
+ const unsafeResolution =
1110
+ isUnsafeResolution(resolution, item, conflict) ||
1111
+ isUnsafeResolution(quote, item, conflict) ||
1112
+ (message ? isUnsafeResolution(anchoredClause(message.content, quote), item, conflict) : true) ||
1113
+ (message ? isUnsafeResolution(anchoredClause(message.content, resolution), item, conflict) : true);
1114
+ if (
1115
+ !resolutions.includes(item) ||
1116
+ seen.has(item) ||
1117
+ messageIndex <= afterIndex ||
1118
+ !message ||
1119
+ message.role !== "user" ||
1120
+ containsNonTextMarker(quote) ||
1121
+ containsNonTextMarker(resolution) ||
1122
+ !message.content.includes(quote) ||
1123
+ anchoredTextSpan(message.content, quote)?.includes(resolution) !== true ||
1124
+ !hasVerbatimTokenBoundaries(message.content, quote) ||
1125
+ !hasVerbatimTokenBoundaries(message.content, resolution) ||
1126
+ hasLaterSupersedingCorrection(message.content, quote, resolution) ||
1127
+ hasLaterSupersedingCorrection(message.content, resolution, resolution) ||
1128
+ unsafeResolution ||
1129
+ resolution === item
1130
+ )
1131
+ throw new Error(`${field}[${index}] has no fresh verbatim user anchor`);
1132
+ if (!addressesItem) throw new Error(`${field}[${index}] has no relevant verbatim user anchor`);
1133
+ if (!concreteAnswer) throw new Error(`${field}[${index}] has no fresh verbatim user anchor`);
1134
+ seen.add(item);
1135
+ anchors.push({ item, message_index: messageIndex, quote, resolution });
1136
+ }
1137
+ return anchors;
1138
+ }
1139
+
1140
+ function isExplicitlyUnresolved(value: string): boolean {
1141
+ const normalized = value.normalize("NFC").toLowerCase();
1142
+ return (
1143
+ /\b(?:undecided|undetermined|unresolved|unclear|unknown|tbd|pending|defer(?:red|ring)?|uncertain|disputed|contested|conflict(?:s|ing)?|disagreement(?:s)?)\b/i.test(
1144
+ normalized,
1145
+ ) ||
1146
+ /\b(?:still\s+(?:deciding|undecided|unresolved|unknown)|not\s+(?:yet\s+)?(?:decided|determined|settled|made|chosen|selected|specified|known)|to\s+be\s+decided)\b/i.test(
1147
+ normalized,
1148
+ ) ||
1149
+ /\b(?:will\s+be\s+decided|decide\s+(?:later|eventually)|to\s+decide\s+later|follow[- ]?up|not\s+sure|unsure|later|eventually|afterwards|in\s+the\s+future|next\s+(?:week|month|time))\b/i.test(
1150
+ normalized,
1151
+ ) ||
1152
+ /\b(?:no|not|never)\s+(?:\w+\s+){0,3}(?:decision|choice|answer|selection|determination|resolution|agreement)\b/i.test(
1153
+ normalized,
1154
+ ) ||
1155
+ /(?:미정|미결정|미해결|보류|불확실|아직\s*(?:결정|정해|선택)|결정되지|결정\s*안\s*(?:됨|됐|되었습니다|안됨)|정해지지|나중에\s*결정|답(?:변)?이\s*없|모르겠|알\s*수\s*없|미합의|분쟁|충돌|모순|未定|未決定|未解決|保留|不明|不確定|まだ\s*(?:決ま|決め)|決まっていない|決まっていません|決定していない|決定していません|答えがない|回答がない|後で\s*決め|わからない|分からない|争議|矛盾|尚未决定|尚未确定|还没决定|還沒決定|没有答案|沒有答案|没有决定|沒有決定|以后决定|以後決定|未解决|未解決|不明确|不明確|不清楚|不知道|待定|暂缓|暫緩|争议|爭議|冲突|衝突|矛盾)/u.test(
1156
+ normalized,
1157
+ )
1158
+ );
1159
+ }
1160
+
1161
+ function validateRemovalAnchors(
1162
+ value: unknown,
1163
+ requestedIds: readonly string[],
1164
+ priorPendingIds: readonly string[],
1165
+ priorItems: ReadonlyMap<string, CrystalItem>,
1166
+ snapshot: CrystalSnapshot,
1167
+ afterIndex: number,
1168
+ ): CrystalResolutionAnchor[] {
1169
+ const removalIds = [...new Set([...requestedIds, ...priorPendingIds])].sort();
1170
+ if (value === undefined) return [];
1171
+ if (!Array.isArray(value) || value.length > removalIds.length)
1172
+ throw new Error("removed_item_anchors must contain one anchor per resolved removal");
1173
+ const seen = new Set<string>();
1174
+ const anchors: CrystalResolutionAnchor[] = [];
1175
+ for (const [index, raw] of value.entries()) {
1176
+ if (!isRecord(raw)) throw new Error(`removed_item_anchors[${index}] must be an object`);
1177
+ const itemId = text(raw.item, `removed_item_anchors[${index}].item`, 128);
1178
+ if (!removalIds.includes(itemId) || seen.has(itemId))
1179
+ throw new Error(`removed_item_anchors[${index}] does not identify a pending removal`);
1180
+ const previous = priorItems.get(itemId);
1181
+ if (!previous) throw new Error(`removed_item_anchors[${index}] references unknown prior material`);
1182
+ const messageIndex = integer(raw.message_index, `removed_item_anchors[${index}].message_index`);
1183
+ const quote = text(raw.quote, `removed_item_anchors[${index}].quote`, 500);
1184
+ const resolution = text(raw.resolution, `removed_item_anchors[${index}].resolution`, 500);
1185
+ const message = snapshot.messages.find(candidate => candidate.index === messageIndex);
1186
+ const statementTerms = topicTerms(previous.statement, false);
1187
+ const resolutionTerms = evidenceTerms(resolution);
1188
+ const referencesStatement =
1189
+ statementTerms.size > 0 && [...statementTerms].every(term => resolutionTerms.has(term));
1190
+ const removalLanguage =
1191
+ /\b(?:remove|drop|delete|discard|omit|exclude|retire|cancel|stop|no\s+longer|do\s+not\s+need|don't\s+need|not\s+needed)\b/i.test(
1192
+ resolution,
1193
+ ) ||
1194
+ /(?:삭제|제거|버리|제외|취소|중단|더\s+이상\s+필요\s+없|없애|削除|取り除|除外|破棄|取り消|停止|不要|删除|移除|去掉|丢弃|丟棄|排除|取消|停止|不再需要)/u.test(
1195
+ resolution,
1196
+ );
1197
+ const explicitNegatedRemoval =
1198
+ /\b(?:no\s+longer|do\s+not\s+need|don't\s+need|not\s+needed)\b/i.test(resolution) ||
1199
+ /(?:더\s+이상\s+필요\s+없|不要|不再需要)/u.test(resolution);
1200
+ const containsCompetingKeepDirective = (value: string): boolean =>
1201
+ value.split(/[.!?。!?;\n]+/u).some(clause => {
1202
+ const directive =
1203
+ /\b(?:do\s+not|don['’]t|never)\s+(?:remove|drop|delete|discard|omit|exclude|retire|cancel|stop)\b/i.test(
1204
+ clause,
1205
+ ) ||
1206
+ /\b(?:keep|retain|restore|preserve|maintain)\b/i.test(clause) ||
1207
+ /(?:삭제|제거|제외|취소|중단)(?:하지\s*마|하지\s*않)|(?:유지|보존|복원|保持|保留|恢复|恢復|復元)/u.test(
1208
+ clause,
1209
+ );
1210
+ if (!directive) return false;
1211
+ if (/\b(?:it|this|that|them|these|those)\b/i.test(clause)) return true;
1212
+ const clauseTerms = evidenceTerms(clause);
1213
+ return (
1214
+ [...statementTerms].some(term => clauseTerms.has(term)) || hasCjkTopicOverlap(previous.statement, clause)
1215
+ );
1216
+ });
1217
+ const competingKeepDirective = [resolution, quote, message?.content ?? ""].some(containsCompetingKeepDirective);
1218
+ const removalSemantics = semanticProfile(resolution);
1219
+ const quotedRemovalSemantics = semanticProfile(quote);
1220
+ const messageRemovalSemantics = message ? semanticProfile(anchoredClause(message.content, quote)) : undefined;
1221
+ if (
1222
+ message?.role !== "user" ||
1223
+ messageIndex <= afterIndex ||
1224
+ containsNonTextMarker(quote) ||
1225
+ containsNonTextMarker(resolution) ||
1226
+ !message.content.includes(quote) ||
1227
+ anchoredTextSpan(message.content, quote)?.includes(resolution) !== true ||
1228
+ !hasVerbatimTokenBoundaries(message.content, quote) ||
1229
+ !hasVerbatimTokenBoundaries(message.content, resolution) ||
1230
+ resolution === previous.statement ||
1231
+ !removalLanguage ||
1232
+ competingKeepDirective ||
1233
+ !referencesStatement ||
1234
+ (removalSemantics.negative && !explicitNegatedRemoval) ||
1235
+ removalSemantics.interrogative ||
1236
+ removalSemantics.conditional ||
1237
+ removalSemantics.hedged ||
1238
+ removalSemantics.refusal ||
1239
+ removalSemantics.alternative ||
1240
+ removalSemantics.contradictory ||
1241
+ removalSemantics.unresolved ||
1242
+ (quotedRemovalSemantics.negative && !explicitNegatedRemoval) ||
1243
+ quotedRemovalSemantics.interrogative ||
1244
+ quotedRemovalSemantics.conditional ||
1245
+ quotedRemovalSemantics.hedged ||
1246
+ quotedRemovalSemantics.refusal ||
1247
+ quotedRemovalSemantics.alternative ||
1248
+ quotedRemovalSemantics.contradictory ||
1249
+ quotedRemovalSemantics.unresolved ||
1250
+ !messageRemovalSemantics ||
1251
+ (messageRemovalSemantics.negative && !explicitNegatedRemoval) ||
1252
+ messageRemovalSemantics.interrogative ||
1253
+ messageRemovalSemantics.conditional ||
1254
+ messageRemovalSemantics.hedged ||
1255
+ messageRemovalSemantics.refusal ||
1256
+ messageRemovalSemantics.alternative ||
1257
+ messageRemovalSemantics.contradictory ||
1258
+ messageRemovalSemantics.unresolved
1259
+ )
1260
+ throw new Error(`removed_item_anchors[${index}] has no fresh statement-bound user removal evidence`);
1261
+ seen.add(itemId);
1262
+ anchors.push({ item: itemId, message_index: messageIndex, quote, resolution });
1263
+ }
1264
+ return anchors.sort((left, right) => left.item.localeCompare(right.item));
1265
+ }
1266
+
1267
+ function validateStoredRemovalAnchors(
1268
+ value: unknown,
1269
+ removedIds: readonly string[],
1270
+ priorSnapshot: CrystalSnapshot,
1271
+ ): CrystalResolutionAnchor[] {
1272
+ if (removedIds.length === 0 && value === undefined) return [];
1273
+ if (!Array.isArray(value) || value.length !== removedIds.length)
1274
+ throw new Error("prior crystal removal evidence is invalid");
1275
+ const anchors = value.map((raw, index) => {
1276
+ if (!isRecord(raw)) throw new Error("prior crystal removal evidence is invalid");
1277
+ const item = text(raw.item, `prior.removed_item_anchors[${index}].item`, 128);
1278
+ const messageIndex = integer(raw.message_index, `prior.removed_item_anchors[${index}].message_index`);
1279
+ const quote = text(raw.quote, `prior.removed_item_anchors[${index}].quote`, 500);
1280
+ const resolution = text(raw.resolution, `prior.removed_item_anchors[${index}].resolution`, 500);
1281
+ const message = priorSnapshot.messages.find(candidate => candidate.index === messageIndex);
1282
+ const rolledOut = messageIndex < priorSnapshot.start;
1283
+ if (
1284
+ !removedIds.includes(item) ||
1285
+ messageIndex > priorSnapshot.end ||
1286
+ (!rolledOut && (message?.role !== "user" || !message.content.includes(quote)))
1287
+ )
1288
+ throw new Error("prior crystal removal evidence is invalid");
1289
+ return { item, message_index: messageIndex, quote, resolution };
1290
+ });
1291
+ if (new Set(anchors.map(anchor => anchor.item)).size !== anchors.length)
1292
+ throw new Error("prior crystal removal evidence is invalid");
1293
+ return anchors.sort((left, right) => left.item.localeCompare(right.item));
1294
+ }
1295
+
1296
+ function validateRemovedIds(value: unknown): string[] {
1297
+ if (!Array.isArray(value) || value.length > MAX_ITEMS) throw new Error("removed_ids must be a bounded array");
1298
+ const ids = value.map((id, index) => text(id, `removed_ids[${index}]`, 128));
1299
+ if (ids.some(id => !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(id)))
1300
+ throw new Error("removed_ids must contain valid item identifiers");
1301
+ if (new Set(ids).size !== ids.length) throw new Error("removed_ids must be unique");
1302
+ return ids.sort();
1303
+ }
1304
+
1305
+ export function crystallizeDeepInterview(value: unknown): DeepInterviewCrystal {
1306
+ if (!isRecord(value)) throw new Error("crystallize input must be an object");
1307
+ const snapshot = validateSnapshot(value.snapshot);
1308
+ if (value.current_revision === undefined) throw new Error("authoritative current revision is required");
1309
+ if (integer(value.current_revision, "current_revision") !== snapshot.revision)
1310
+ throw new Error("conversation snapshot is stale");
1311
+ const items = validateItems(value.items, snapshot);
1312
+ if (snapshot.messages.length === 0) throw new Error("crystallize requires material conversation evidence");
1313
+ const requestedRemovedIds = value.removed_ids === undefined ? [] : validateRemovedIds(value.removed_ids);
1314
+ if (requestedRemovedIds.some(id => items.some(item => item.id === id)))
1315
+ throw new Error("removed_ids must be disjoint from submitted items");
1316
+ const gaps = normalizedGaps(value.open_gaps);
1317
+ if (gaps.length > 2) throw new Error("broad ambiguity requires the full deep-interview flow");
1318
+ const conflicts = normalizedGaps(value.conflicts);
1319
+ const resolvedGaps = normalizedGaps(value.resolved_open_gaps);
1320
+ const resolvedConflicts = normalizedGaps(value.resolved_conflicts);
1321
+ const prior = value.prior;
1322
+ if (prior !== undefined && !isRecord(prior)) throw new Error("prior crystal is invalid");
1323
+ const priorCrystal = prior as DeepInterviewCrystal | undefined;
1324
+ let canonicalPriorItems: CrystalItem[] = [];
1325
+ let priorSnapshot: CrystalSnapshot | undefined;
1326
+ if (priorCrystal) {
1327
+ if (
1328
+ priorCrystal.schema_version !== 1 ||
1329
+ !Number.isSafeInteger(priorCrystal.spec_version) ||
1330
+ priorCrystal.spec_version < 1
1331
+ )
1332
+ throw new Error("prior crystal is invalid");
1333
+ if (priorCrystal.spec_version >= Number.MAX_SAFE_INTEGER)
1334
+ throw new Error("prior crystal spec_version cannot be safely incremented");
1335
+ if (!isRecord(priorCrystal.source) || snapshot.revision <= priorCrystal.source.revision)
1336
+ throw new Error("conversation snapshot is stale");
1337
+ if (!Array.isArray(priorCrystal.source.messages)) throw new Error("prior crystal source evidence is missing");
1338
+ priorSnapshot = validateSnapshot({ ...priorCrystal.source, messages: priorCrystal.source.messages });
1339
+ if (!Array.isArray(priorCrystal.items)) throw new Error("prior crystal is invalid");
1340
+ if (
1341
+ !Number.isSafeInteger(priorCrystal.source.revision) ||
1342
+ !Number.isSafeInteger(priorCrystal.source.start) ||
1343
+ !Number.isSafeInteger(priorCrystal.source.end) ||
1344
+ typeof priorCrystal.source.digest !== "string" ||
1345
+ !/^[a-f0-9]{64}$/.test(priorCrystal.source.digest) ||
1346
+ !isRecord(priorCrystal.delta) ||
1347
+ priorCrystal.execution_approval !== "not-approved"
1348
+ )
1349
+ throw new Error("prior crystal is invalid");
1350
+ canonicalPriorItems = validateItems(priorCrystal.items);
1351
+ const inWindowPriorItems = priorCrystal.items.filter(
1352
+ item =>
1353
+ item.anchor &&
1354
+ item.anchor.message_index >= priorSnapshot!.start &&
1355
+ item.anchor.message_index <= priorSnapshot!.end,
1356
+ );
1357
+ validateItems(inWindowPriorItems, priorSnapshot);
1358
+ }
1359
+ const priorEnd =
1360
+ priorCrystal && isRecord(priorCrystal.source) && typeof priorCrystal.source.end === "number"
1361
+ ? priorCrystal.source.end
1362
+ : -1;
1363
+ if (priorCrystal && snapshot.start > priorEnd + 1)
1364
+ throw new Error("conversation snapshot must include every message after the prior Crystal boundary");
1365
+ if (gaps.some(gap => resolvedGaps.includes(gap)))
1366
+ throw new Error("open_gaps and resolved_open_gaps must be disjoint");
1367
+ if (conflicts.some(conflict => resolvedConflicts.includes(conflict)))
1368
+ throw new Error("conflicts and resolved_conflicts must be disjoint");
1369
+ const resolvedGapAnchors = validateResolutionAnchors(
1370
+ resolvedGaps,
1371
+ value.resolved_open_gap_anchors,
1372
+ snapshot,
1373
+ "resolved_open_gap_anchors",
1374
+ priorEnd,
1375
+ );
1376
+ const resolvedConflictAnchors = validateResolutionAnchors(
1377
+ resolvedConflicts,
1378
+ value.resolved_conflict_anchors,
1379
+ snapshot,
1380
+ "resolved_conflict_anchors",
1381
+ priorEnd,
1382
+ );
1383
+ const priorItems = new Map(canonicalPriorItems.map(item => [item.id, item]));
1384
+ const priorRemovedIds = priorCrystal?.removed_ids === undefined ? [] : validateRemovedIds(priorCrystal.removed_ids);
1385
+ const priorRemovedAnchors = priorCrystal
1386
+ ? validateStoredRemovalAnchors(priorCrystal.removed_item_anchors, priorRemovedIds, priorSnapshot!)
1387
+ : [];
1388
+ const priorPendingRemovals =
1389
+ priorCrystal?.pending_removals === undefined ? [] : validateRemovedIds(priorCrystal.pending_removals);
1390
+ if (priorPendingRemovals.some(id => !priorItems.has(id)))
1391
+ throw new Error("prior crystal contains an unresolved removal for unknown material");
1392
+ if (priorRemovedIds.some(id => priorItems.has(id)))
1393
+ throw new Error("prior crystal contains removed material in its active items");
1394
+ if (priorRemovedIds.some(id => priorPendingRemovals.includes(id)))
1395
+ throw new Error("prior crystal contains both resolved and pending removal tombstones");
1396
+ if (items.some(item => priorRemovedIds.includes(item.id)))
1397
+ throw new Error("submitted items must not resurrect a permanently removed item");
1398
+ if (requestedRemovedIds.some(id => priorRemovedIds.includes(id)))
1399
+ throw new Error("removed crystallize item is permanently removed");
1400
+ if (requestedRemovedIds.some(id => !priorItems.has(id)))
1401
+ throw new Error("removed crystallize item is not present in prior crystal");
1402
+ const resolvedRemovalAnchors = validateRemovalAnchors(
1403
+ value.removed_item_anchors,
1404
+ requestedRemovedIds,
1405
+ priorPendingRemovals,
1406
+ priorItems,
1407
+ snapshot,
1408
+ priorEnd,
1409
+ );
1410
+ const resolvedRemovedIds = resolvedRemovalAnchors.map(anchor => anchor.item);
1411
+ const cancelledPendingRemovalIds = priorPendingRemovals.filter(id => {
1412
+ const submitted = items.find(item => item.id === id);
1413
+ const previous = priorItems.get(id);
1414
+ const anchor = submitted?.anchor;
1415
+ const message = anchor
1416
+ ? snapshot.messages.find(candidate => candidate.index === anchor.message_index)
1417
+ : undefined;
1418
+ const keepClause =
1419
+ anchor && message
1420
+ ? (anchoredClause(message.content, anchor.quote)
1421
+ .split(/[,,、]/u)
1422
+ .find(clause => clause.includes(anchor.quote)) ?? anchoredClause(message.content, anchor.quote))
1423
+ : "";
1424
+ return Boolean(
1425
+ submitted?.classification === "confirmed" &&
1426
+ previous &&
1427
+ anchor &&
1428
+ message?.role === "user" &&
1429
+ anchor.message_index > priorEnd &&
1430
+ message.content.includes(anchor.quote) &&
1431
+ message.content.indexOf(anchor.quote) === message.content.lastIndexOf(anchor.quote) &&
1432
+ hasVerbatimTokenBoundaries(message.content, anchor.quote) &&
1433
+ (/\b(?:keep|retain|restore|preserve|maintain)\b/i.test(keepClause) ||
1434
+ /(?:유지|보존|복원|保持|保留|恢复|恢復|復元)/u.test(keepClause)) &&
1435
+ ([...topicTerms(previous.statement, false)].some(term => evidenceTerms(keepClause).has(term)) ||
1436
+ hasCjkTopicOverlap(previous.statement, keepClause)),
1437
+ );
1438
+ });
1439
+ const unresolvedRemovalIds = [
1440
+ ...new Set(
1441
+ [...priorPendingRemovals, ...requestedRemovedIds].filter(
1442
+ id => !resolvedRemovedIds.includes(id) && !cancelledPendingRemovalIds.includes(id),
1443
+ ),
1444
+ ),
1445
+ ].sort();
1446
+ if (items.some(item => unresolvedRemovalIds.includes(item.id) || resolvedRemovedIds.includes(item.id)))
1447
+ throw new Error("submitted items must not silently resurrect unresolved removals");
1448
+ const allRemovedIds = [...new Set([...priorRemovedIds, ...resolvedRemovedIds])].sort();
1449
+ const sameIntent = (left: CrystalItem, right: CrystalItem): boolean =>
1450
+ left.id === right.id &&
1451
+ left.kind === right.kind &&
1452
+ left.statement === right.statement &&
1453
+ left.classification === right.classification;
1454
+ for (const item of items) {
1455
+ const previous = priorItems.get(item.id);
1456
+ if (
1457
+ item.classification === "confirmed" &&
1458
+ (previous === undefined || !sameIntent(item, previous)) &&
1459
+ (item.anchor === undefined || item.anchor.message_index <= priorEnd)
1460
+ )
1461
+ throw new Error(`changed confirmed item ${item.id} requires fresh user evidence`);
1462
+ }
1463
+ const mergedItems = [...items];
1464
+ const crossIdSupersededIds = new Set(
1465
+ canonicalPriorItems
1466
+ .filter(previous =>
1467
+ items.some(candidate => {
1468
+ if (candidate.id === previous.id || candidate.kind !== previous.kind || !candidate.anchor) return false;
1469
+ const message = snapshot.messages.find(entry => entry.index === candidate.anchor!.message_index);
1470
+ return (
1471
+ candidate.anchor.message_index > priorEnd &&
1472
+ message?.role === "user" &&
1473
+ /\b(?:actually\s+use|instead\s+use|switch|change|replace)\b/i.test(candidate.anchor.quote) &&
1474
+ /\buse\b/i.test(previous.statement) &&
1475
+ /\buse\b/i.test(candidate.statement) &&
1476
+ [...topicTerms(previous.statement, false)].some(term => evidenceTerms(candidate.statement).has(term))
1477
+ );
1478
+ }),
1479
+ )
1480
+ .map(item => item.id),
1481
+ );
1482
+ for (const removedId of allRemovedIds) crossIdSupersededIds.delete(removedId);
1483
+ if (crossIdSupersededIds.size > 0)
1484
+ throw new Error("cross-ID replacement requires explicit removal of the superseded item");
1485
+ for (const item of canonicalPriorItems)
1486
+ if (!allRemovedIds.includes(item.id) && !mergedItems.some(candidate => candidate.id === item.id))
1487
+ mergedItems.push(item);
1488
+ for (const anchor of [...resolvedGapAnchors, ...resolvedConflictAnchors]) {
1489
+ const answerTerms = evidenceTerms(anchor.resolution);
1490
+ const resolutionId = `resolution:${createHash("sha256")
1491
+ .update(`${anchor.item}\0${anchor.resolution}`)
1492
+ .digest("hex")
1493
+ .slice(0, 16)}`;
1494
+ const answerSemantics = semanticProfile(anchor.resolution);
1495
+ const answerQuantitative = quantitativeEvidenceTerms(anchor.resolution);
1496
+ const answerQuantitativeSequence = quantitativeEvidenceSequence(anchor.resolution);
1497
+ const represented = mergedItems.some(item => {
1498
+ const itemQuantitative = quantitativeEvidenceTerms(item.statement);
1499
+ return (
1500
+ item.classification === "confirmed" &&
1501
+ [...answerTerms].every(term => evidenceTerms(item.statement).has(term)) &&
1502
+ sameSemanticIntent(answerSemantics, semanticProfile(item.statement)) &&
1503
+ answerQuantitative.size === itemQuantitative.size &&
1504
+ [...answerQuantitative].every(term => itemQuantitative.has(term)) &&
1505
+ JSON.stringify(answerQuantitativeSequence) === JSON.stringify(quantitativeEvidenceSequence(item.statement))
1506
+ );
1507
+ });
1508
+ if (!represented)
1509
+ if (mergedItems.some(item => item.id === resolutionId))
1510
+ throw new Error(`promoted resolution item ID collides with submitted item: ${resolutionId}`);
1511
+ else
1512
+ mergedItems.push({
1513
+ id: resolutionId,
1514
+ kind: "constraint",
1515
+ classification: "confirmed",
1516
+ statement: anchor.resolution,
1517
+ anchor: { message_index: anchor.message_index, quote: anchor.resolution },
1518
+ });
1519
+ }
1520
+ const currentItems = mergedItems;
1521
+ if (currentItems.length > MAX_ITEMS) throw new Error("merged crystallize items exceed the bounded limit");
1522
+ if (currentItems.length === 0) throw new Error("crystallize requires material conversation evidence");
1523
+ for (const directive of requirementBearingClauses(snapshot)) {
1524
+ const directiveTerms = topicTerms(directive.clause, false);
1525
+ const represented = currentItems.some(
1526
+ item =>
1527
+ [...directiveTerms].every(term => evidenceTerms(item.statement).has(term)) &&
1528
+ preservesEvidenceOrder(item.statement, directive.clause) &&
1529
+ sameSemanticIntent(semanticProfile(directive.clause), semanticProfile(item.statement)),
1530
+ );
1531
+ const snapshotReplacement = items.some(replacement => {
1532
+ const anchor = replacement.anchor;
1533
+ if (replacement.classification !== "confirmed" || !anchor) return false;
1534
+ const message = snapshot.messages.find(entry => entry.index === anchor.message_index);
1535
+ if (
1536
+ !message ||
1537
+ anchor.message_index < directive.messageIndex ||
1538
+ (anchor.message_index === directive.messageIndex &&
1539
+ message.content.indexOf(anchor.quote) <= directive.clauseOffset) ||
1540
+ !/\b(?:actually|instead|rather|correction|replace|switch|change)\b/i.test(anchor.quote) ||
1541
+ /\b(?:also|too|additionally|as\s+well|in\s+addition)\b/i.test(anchor.quote)
1542
+ )
1543
+ return false;
1544
+ // A use-target correction may replace that target, not other directives
1545
+ // attached to it. The ordered correction helper must also recognize it.
1546
+ const target = /\buse\s+([A-Za-z0-9][A-Za-z0-9+#.-]*)/i.exec(directive.clause)?.[1];
1547
+ if (!target || !/\buse\s+/i.test(anchor.quote)) return false;
1548
+ const directiveSemantics = semanticProfile(directive.clause);
1549
+ const replacementSemantics = semanticProfile(replacement.statement);
1550
+ if (
1551
+ !sameSemanticIntent(
1552
+ { ...directiveSemantics, contradictory: replacementSemantics.contradictory },
1553
+ replacementSemantics,
1554
+ )
1555
+ )
1556
+ return false;
1557
+ const remainingTerms = topicTerms(directive.clause, false);
1558
+ remainingTerms.delete(target.toLowerCase());
1559
+ for (const term of ["actually", "correction"]) remainingTerms.delete(term);
1560
+ const replacementTerms = evidenceTerms(anchor.quote);
1561
+ return (
1562
+ [...remainingTerms].every(term => replacementTerms.has(term)) &&
1563
+ hasLaterSupersedingCorrection(`${directive.clause}. ${anchor.quote}`, directive.clause, directive.clause)
1564
+ );
1565
+ });
1566
+ const explicitlySuperseded =
1567
+ snapshotReplacement ||
1568
+ canonicalPriorItems.some(previous => {
1569
+ const replacement = items.find(item => item.id === previous.id);
1570
+ return (
1571
+ replacement !== undefined &&
1572
+ !sameIntent(replacement, previous) &&
1573
+ replacement.anchor !== undefined &&
1574
+ (directive.messageIndex < replacement.anchor.message_index ||
1575
+ (directive.messageIndex === replacement.anchor.message_index &&
1576
+ (snapshot.messages
1577
+ .find(message => message.index === directive.messageIndex)
1578
+ ?.content.indexOf(replacement.anchor.quote) ?? -1) > directive.clauseOffset)) &&
1579
+ /\b(?:actually|instead|rather|replace|replaced|no longer|not)\b/i.test(replacement.anchor.quote) &&
1580
+ [...directiveTerms].every(term => evidenceTerms(previous.statement).has(term))
1581
+ );
1582
+ });
1583
+ const explicitlyPreserved =
1584
+ /(?:\b(?:keep|retain|restore|preserve|maintain)\b|유지|보존|복원|保持|保留|恢复|恢復|復元)/iu.test(
1585
+ directive.clause,
1586
+ ) &&
1587
+ currentItems.some(
1588
+ item =>
1589
+ item.anchor?.message_index === directive.messageIndex &&
1590
+ ([...topicTerms(item.statement, false)].some(term => directiveTerms.has(term)) ||
1591
+ hasCjkTopicOverlap(item.statement, directive.clause)),
1592
+ );
1593
+ const representedRemoval = [...priorRemovedAnchors, ...resolvedRemovalAnchors].some(anchor => {
1594
+ const quote = anchor.quote.replace(/[.!?。!?]+$/u, "");
1595
+ const resolution = anchor.resolution.replace(/[.!?。!?]+$/u, "");
1596
+ const resolutionTerms = evidenceTerms(resolution);
1597
+ return (
1598
+ anchor.message_index === directive.messageIndex &&
1599
+ (directive.clause.includes(quote) ||
1600
+ directive.clause.includes(resolution) ||
1601
+ [...directiveTerms].every(term => resolutionTerms.has(term)))
1602
+ );
1603
+ });
1604
+ const unresolved = [...gaps, ...conflicts].some(item => {
1605
+ const itemTerms = topicTerms(item, false);
1606
+ return directiveTerms.size === itemTerms.size && [...directiveTerms].every(term => itemTerms.has(term));
1607
+ });
1608
+ if (!represented && !explicitlySuperseded && !explicitlyPreserved && !representedRemoval && !unresolved)
1609
+ throw new Error(`unrepresented user directive requires an item or unresolved gap: ${directive.clause}`);
1610
+ }
1611
+ if (!currentItems.some(item => item.classification === "confirmed" && item.kind !== "non_goal"))
1612
+ throw new Error("crystallize requires a confirmed user requirement");
1613
+ const confirmedItems = currentItems.filter(item => item.classification === "confirmed");
1614
+ const opposingActions: ReadonlyArray<readonly [readonly string[], readonly string[]]> = [
1615
+ [
1616
+ ["enable", "enables", "enabled"],
1617
+ ["disable", "disables", "disabled"],
1618
+ ],
1619
+ [
1620
+ ["allow", "allows", "allowed", "permit", "permits", "permitted"],
1621
+ ["deny", "denies", "denied", "prohibit", "prohibits", "prohibited", "forbid", "forbids", "forbidden"],
1622
+ ],
1623
+ [
1624
+ ["include", "includes", "included"],
1625
+ ["exclude", "excludes", "excluded"],
1626
+ ],
1627
+ [
1628
+ ["start", "starts", "started"],
1629
+ ["stop", "stops", "stopped"],
1630
+ ],
1631
+ [
1632
+ ["add", "adds", "added"],
1633
+ ["remove", "removes", "removed"],
1634
+ ],
1635
+ [
1636
+ ["require", "requires", "required"],
1637
+ ["forbid", "forbids", "forbidden", "prohibit", "prohibits", "prohibited"],
1638
+ ],
1639
+ ];
1640
+ for (let leftIndex = 0; leftIndex < confirmedItems.length; leftIndex++) {
1641
+ const left = confirmedItems[leftIndex]!;
1642
+ const leftTerms = topicTerms(left.statement, false);
1643
+ for (const right of confirmedItems.slice(leftIndex + 1)) {
1644
+ const rightTerms = topicTerms(right.statement, false);
1645
+ const opposingAction = opposingActions.some(([positiveTerms, negativeTerms]) => {
1646
+ const signed = (
1647
+ statement: string,
1648
+ terms: Set<string>,
1649
+ ): { sign: number; subject: Set<string> } | undefined => {
1650
+ const actionTerms = evidenceTerms(statement);
1651
+ const positive = positiveTerms.some(term => actionTerms.has(term));
1652
+ const negative = negativeTerms.some(term => actionTerms.has(term));
1653
+ if (positive === negative) return undefined;
1654
+ const explicitlyNegated = /\b(?:do\s+not|don['’]t|never|not)\b/i.test(statement);
1655
+ return {
1656
+ sign: (positive ? 1 : -1) * (explicitlyNegated ? -1 : 1),
1657
+ subject: new Set(
1658
+ [...terms].filter(term => !positiveTerms.includes(term) && !negativeTerms.includes(term)),
1659
+ ),
1660
+ };
1661
+ };
1662
+ const leftAction = signed(left.statement, leftTerms);
1663
+ const rightAction = signed(right.statement, rightTerms);
1664
+ if (!leftAction || !rightAction || leftAction.sign === rightAction.sign) return false;
1665
+ return (
1666
+ leftAction.subject.size > 0 &&
1667
+ leftAction.subject.size === rightAction.subject.size &&
1668
+ [...leftAction.subject].every(term => rightAction.subject.has(term))
1669
+ );
1670
+ });
1671
+ if (
1672
+ opposingAction ||
1673
+ (leftTerms.size > 0 &&
1674
+ leftTerms.size === rightTerms.size &&
1675
+ [...leftTerms].every(term => rightTerms.has(term)) &&
1676
+ semanticProfile(left.statement).negative !== semanticProfile(right.statement).negative)
1677
+ )
1678
+ throw new Error(`contradictory confirmed items require an explicit conflict: ${left.id}, ${right.id}`);
1679
+ }
1680
+ }
1681
+ const changed = currentItems
1682
+ .filter(item => {
1683
+ const previous = priorItems.get(item.id);
1684
+ return previous !== undefined && !sameIntent(item, previous);
1685
+ })
1686
+ .map(item => item.id)
1687
+ .sort();
1688
+ const added = currentItems
1689
+ .filter(item => !priorItems.has(item.id))
1690
+ .map(item => item.id)
1691
+ .sort();
1692
+ const preserved = currentItems
1693
+ .filter(item => priorItems.has(item.id) && !changed.includes(item.id))
1694
+ .map(item => item.id)
1695
+ .sort();
1696
+ const goalChanged = changed.some(id => {
1697
+ const current = currentItems.find(item => item.id === id);
1698
+ const previous = priorItems.get(id);
1699
+ return current?.kind === "goal" || previous?.kind === "goal";
1700
+ });
1701
+ const removedGoal = resolvedRemovedIds.some(id => priorItems.get(id)?.kind === "goal");
1702
+ const removedIntent = resolvedRemovedIds.length > 0 || unresolvedRemovalIds.length > 0;
1703
+ const allGaps = [
1704
+ ...new Set([...(priorCrystal?.open_gaps ?? []), ...gaps].filter(gap => !resolvedGaps.includes(gap))),
1705
+ ];
1706
+ if (allGaps.length > 2) throw new Error("broad ambiguity requires the full deep-interview flow");
1707
+ const allConflicts = [
1708
+ ...new Set(
1709
+ [...(priorCrystal?.conflicts ?? []), ...conflicts].filter(conflict => !resolvedConflicts.includes(conflict)),
1710
+ ),
1711
+ ];
1712
+ const hasDisputed = currentItems.some(item => item.classification === "disputed");
1713
+ const hasInferred = currentItems.some(item => item.classification === "inferred");
1714
+ const intentChanged =
1715
+ goalChanged ||
1716
+ changed.some(id =>
1717
+ ["constraint", "decision", "acceptance_criterion", "non_goal"].includes(
1718
+ items.find(item => item.id === id)?.kind ?? "",
1719
+ ),
1720
+ );
1721
+ const delta: CrystalDelta = {
1722
+ kind:
1723
+ allConflicts.length > 0 || hasDisputed
1724
+ ? "stale"
1725
+ : goalChanged || removedGoal
1726
+ ? "goal-replaced"
1727
+ : intentChanged || removedIntent || hasInferred
1728
+ ? "intent-changed"
1729
+ : added.length > 0
1730
+ ? "additive"
1731
+ : "none",
1732
+ changed_ids: changed,
1733
+ added_ids: added,
1734
+ preserved_ids: preserved,
1735
+ approval_invalidated:
1736
+ Boolean(priorCrystal && added.length > 0) ||
1737
+ intentChanged ||
1738
+ removedIntent ||
1739
+ hasInferred ||
1740
+ allConflicts.length > 0 ||
1741
+ hasDisputed,
1742
+ };
1743
+ const allRemovedAnchors = [...priorRemovedAnchors];
1744
+ for (const anchor of resolvedRemovalAnchors)
1745
+ if (!allRemovedAnchors.some(existing => existing.item === anchor.item)) allRemovedAnchors.push(anchor);
1746
+ allRemovedAnchors.sort((left, right) => left.item.localeCompare(right.item));
1747
+ const lifecycle =
1748
+ allConflicts.length > 0 || hasDisputed
1749
+ ? "stale"
1750
+ : goalChanged || removedGoal
1751
+ ? "superseded"
1752
+ : intentChanged || removedIntent || hasInferred || allGaps.length > 0
1753
+ ? "needs-questions"
1754
+ : "ready";
1755
+ return {
1756
+ schema_version: 1,
1757
+ spec_version: (priorCrystal?.spec_version ?? 0) + 1,
1758
+ lifecycle,
1759
+ source: {
1760
+ revision: snapshot.revision,
1761
+ start: snapshot.start,
1762
+ end: snapshot.end,
1763
+ digest: snapshot.digest,
1764
+ messages: snapshot.messages,
1765
+ },
1766
+ items: currentItems,
1767
+ ...(allRemovedIds.length > 0 ? { removed_ids: allRemovedIds } : {}),
1768
+ ...(allRemovedAnchors.length > 0 ? { removed_item_anchors: allRemovedAnchors } : {}),
1769
+ ...(unresolvedRemovalIds.length > 0 ? { pending_removals: unresolvedRemovalIds } : {}),
1770
+ open_gaps: allGaps,
1771
+ conflicts: allConflicts,
1772
+ delta,
1773
+ execution_approval: "not-approved",
1774
+ };
1775
+ }
1776
+
1777
+ export function crystalMarkdown(crystal: DeepInterviewCrystal): string {
1778
+ const lines = [
1779
+ `# Deep Interview Crystal v${crystal.spec_version}`,
1780
+ "",
1781
+ `- Readiness: ${crystal.lifecycle}`,
1782
+ `- Source: revision ${crystal.source.revision}, messages ${crystal.source.start}–${crystal.source.end}, digest ${crystal.source.digest}`,
1783
+ `- Execution approval: ${crystal.execution_approval}`,
1784
+ "",
1785
+ "## Delta",
1786
+ `- Kind: ${crystal.delta.kind}`,
1787
+ `- Changed IDs: ${crystal.delta.changed_ids.join(", ") || "none"}`,
1788
+ `- Added IDs: ${crystal.delta.added_ids.join(", ") || "none"}`,
1789
+ `- Preserved IDs: ${crystal.delta.preserved_ids.join(", ") || "none"}`,
1790
+ `- Removed IDs: ${crystal.removed_ids?.join(", ") || "none"}`,
1791
+ `- Removal evidence: ${crystal.removed_item_anchors?.map(anchor => `${anchor.item}@${anchor.message_index}: ${anchor.quote}`).join("; ") || "none"}`,
1792
+ `- Pending removals: ${crystal.pending_removals?.join(", ") || "none"}`,
1793
+ `- Approval invalidated: ${crystal.delta.approval_invalidated}`,
1794
+ "",
1795
+ "## Classified material",
1796
+ ];
1797
+ for (const classification of CLASSIFICATIONS) {
1798
+ lines.push(`### ${classification}`);
1799
+ for (const item of crystal.items.filter(candidate => candidate.classification === classification))
1800
+ lines.push(
1801
+ `- **${item.kind}** (${item.id}): ${item.statement}${item.anchor ? ` _(verbatim anchor ${item.anchor.message_index}: ${item.anchor.quote})_` : ""}`,
1802
+ );
1803
+ }
1804
+ lines.push(
1805
+ "",
1806
+ "## Open gaps",
1807
+ ...(crystal.open_gaps.length > 0 ? crystal.open_gaps.map(gap => `- ${gap}`) : ["- None"]),
1808
+ "",
1809
+ "## Conflicts",
1810
+ ...(crystal.conflicts.length > 0 ? crystal.conflicts.map(conflict => `- ${conflict}`) : ["- None"]),
1811
+ "",
1812
+ );
1813
+ return `${lines.join("\n")}\n`;
1814
+ }