dsh-trivium 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -279,6 +279,10 @@ Off by default. After you turn **Chips (memory whitelist)** on in Settings, the
279
279
 
280
280
  ## Changelog
281
281
 
282
+ **0.7.0** — Observability release. Settings → Recall check now lists pending and recently delivered corrections per session, scoped to the workspace, and adds a **Check version chains** button that runs the same read-only check as `dsh-trivium-check`. New read-only endpoints: `GET /api/dsh-trivium/corrections` and `GET /api/dsh-trivium/integrity`. A fault-injection regression now proves a sidecar write failure is reported by health and repaired by `bootstrap-write` on reopen, and the README documents the host-side desktop `EPIPE`. Memory format, defaults and the four-tool surface are unchanged.
283
+
284
+ **0.6.0** — Maintenance release. The read-only revision checker and the stale-deadline cleaner now ship with the package as `dsh-trivium-check` and `dsh-trivium-clear-until` (dry run by default; `--apply` clears only deadlines also present in the body, `--all` clears every stale one). The checker previously existed only in the Git checkout even though the 0.5.1 notes told users to run it. This release also pins two verifications: turn identity comes from the host's own user-message id (checked in a real session log), and a genuine second-window attempt with a stale ETag returns 409 and succeeds after refresh. No defaults or data formats changed.
285
+
282
286
  **0.5.1** — Correctness release for 0.5.0. A user turn is now booked by the text actually rendered, so three long chips can no longer ship 3 × ~217 tokens against one 300 allowance; commit is idempotent and an explicit zero budget stays zero. Version resolution returns the current node's own body, type, deadline and state instead of only its id, so a query matching a superseded decision no longer shows the old instruction under the new id. Imports plan URI identity and validate the whole resulting graph before one native transaction: two versions are never merged by similar text, no `supersedes` self-loop is written, and a failed transaction leaves no node, body or edge behind. When a delivered fact changes, the session that saw it gets a short correction notice (never the old body), scoped per workspace and consumed only after delivery. Kernel: `triviumdb@0.9.0-beta.3` (pinned prerelease). Windows are supported from 900 px upward; at 390 px the host's 188 px settings sidebar leaves ~154 px for the panel, which is not readable (it does not scroll horizontally). Upgrade note: `until` values written by 0.4.21 from prose dates are not rewritten automatically — clear them with **Show expired decisions** or `ctx_remember` with `until: ""`.
283
287
 
284
288
  **0.5.0** — Memory you can audit. Chips and recall now share **one** 300-token allowance per user turn instead of clipping to 300 each, the same node can no longer appear twice, and a tool loop no longer re-injects inside one turn. The delivery ledger is keyed by host message ids, so a failed send is retried and compaction rebuilds, while a fork inherits neither the ledger nor the pins. `ctx_find` is capped at ~1200 estimated tokens with whole-record truncation, and `ctx_read` projects a field allowlist so new metadata cannot grow its output.
@@ -350,6 +354,12 @@ The peer ranges now explicitly admit the supported DSH `0.1.5`, `0.2.0`, and `0.
350
354
  | Memory still there after uninstall | Stop `dsh web` before `dsh plugin remove`. Leftover `.tdb` is a workspace never opened on 0.4.10+; delete `.dsh/trivium.tdb` in that folder. Committed `trivium.jsonl` comes back with `git checkout`. Disable-in-place does not wipe. |
351
355
  | Check for updates fails | That request only hits the npm registry for the latest version. Memory is unaffected; try again later. |
352
356
  | Old conversations will not open after rc.8 | That is DSH’s own session-store format change, not a broken `.tdb`. Graph memory still works via `ctx_find`; leftover boxes on unmatched old sessions are plot residue. |
357
+ | Desktop shows “A JavaScript error occurred in the main process” with `EPIPE: broken pipe, write` | Host-side, not the plugin: `electron-updater` writes a log line to a console pipe whose parent process has exited (typical when the app was started by a test launcher with `--inspect`). No `dsh-trivium` frame is in that stack. Click OK and start the app from the Start menu; avoid launching it from a terminal or harness that closes. |
358
+
359
+ **Maintenance commands** (shipped with the package since 0.6.0):
360
+
361
+ - `npx dsh-trivium-check <workspace>` — opens the graph **read-only** and reports `supersedes` self loops, cycles, forks, duplicate URIs, dangling endpoints and unresolvable chains. Exit code 0 clean, 2 issues, 1 cannot open.
362
+ - `npx dsh-trivium-clear-until <workspace>` — dry run: lists stale decisions that still carry a deadline. `--apply` clears only deadlines whose text also appears in the body (the shape 0.4.21 produced from prose dates); `--apply --all` clears every stale deadline, including explicit ones. Close the host first — the kernel is single-writer.
353
363
 
354
364
  Local source checkout: `npm install`, then `node scripts/link-dsh.mjs`, then restart `dsh web`. `triviumdb` is a native module; if it will not install, check Node against `package.json` `engines` (`^22.19.0 || >=24`).
355
365
 
@@ -358,8 +368,8 @@ Local source checkout: `npm install`, then `node scripts/link-dsh.mjs`, then res
358
368
  ## Release info
359
369
 
360
370
  - GitHub: https://github.com/QWQcool/dsh-trivium
361
- - Package version: `0.4.22` · [npm](https://www.npmjs.com/package/dsh-trivium)
362
- - Tested host: `@deepseek-ai/dsh@0.1.5-rc.1` (also `0.1.1-rc.2` / `0.1.0-rc.8`)
371
+ - Package version: `0.7.0` · [npm](https://www.npmjs.com/package/dsh-trivium)
372
+ - Tested host: `@deepseek-ai/dsh@0.2.1-rc.4` (the peer range also admits the 0.1.x lines)
363
373
  - License: MIT (depends on [TriviumDB](https://github.com/YoKONCy/TriviumDB), Apache-2.0)
364
374
 
365
375
  ---
package/README.zh-CN.md CHANGED
@@ -275,6 +275,10 @@ Trivium 是记忆内核,不是日记、日历或聊天伴侣。它按**节点
275
275
 
276
276
  ## 更新说明
277
277
 
278
+ **0.7.0** — 可观测性版本。设置 → 召回检查现在按会话列出待发送与最近已发送的更正(按工作区过滤),并新增「检查版本链」按钮,复用 `dsh-trivium-check` 的只读检查。新增只读接口:`GET /api/dsh-trivium/corrections`、`GET /api/dsh-trivium/integrity`。新增故障注入回归:旁路写入失败会被 health 报告,重开时由 `bootstrap-write` 修复;README 补充桌面端 `EPIPE` 的宿主侧说明。记忆格式、默认值与四工具面均未变。
279
+
280
+ **0.6.0** — 维护版本。只读版本链体检与过期期限清理现在随包分发:`dsh-trivium-check`、`dsh-trivium-clear-until`(默认只列不改;`--apply` 只清「期限同时出现在正文里」的那些,`--all` 清所有过期期限)。此前 0.5.1 的说明让用户运行 `scripts/check-integrity.mjs`,但该脚本并不在 npm 包里。本版同时固化两项验收:轮标识取自宿主自己的用户消息 id(已在真实会话日志中确认),以及真实第二窗口带过期 ETag 会得到 409、刷新后成功。默认值与数据格式均未变。
281
+
278
282
  **0.5.1** — 0.5.0 的正确性修复版。一轮的预算改为按**实际渲染出的文本**记账,三条长芯片不可能再在同一轮送出 3 × 约 217 token;`commit` 幂等,显式 0 预算保持为 0。版本解析返回当前节点自己的正文、类型、期限与状态,而不是只换 id——查询命中被替代的决策时,不会再把旧指令挂在新 id 下。导入先按 URI 规划身份、校验映射后的完整图,再用一次原生事务提交:两个版本不会因正文相近被合并,不会写出 `supersedes` 自环,事务失败也不留下节点、正文或边。已投递的事实被修改后,看过它的会话会收到一条短更正(不重发旧正文),按工作区隔离,且只有真正送达才消费。内核升到 `triviumdb@0.9.0-beta.3`(精确锁定预发布版)。界面支持宽度为 **≥900px**;390px 时宿主固定 188px 侧栏只给面板留下约 154px,不可读(但不会横向滚动)。升级说明:0.4.21 从正文日期误写的 `until` 不会自动清除,请在「显示过期决策」里清空,或用 `ctx_remember` 传 `until: ""`。
279
283
 
280
284
  **0.5.0** — 记忆可审计。芯片与召回改为**共用**每轮 300 估算 token(原先各 300),同一节点不会出现两次,一轮内的工具循环也不再重复注入。投递账本按宿主消息 id 记账:失败可重试、压缩后可重建,fork 既不继承账本也不继承 pins。`ctx_find` 默认上限约 1200 估算 token,超预算丢弃整条记录;`ctx_read` 改为字段白名单,新元数据不会撑大默认输出。
@@ -346,6 +350,12 @@ peer 范围显式接受已支持的 DSH `0.1.5`、`0.2.0`、`0.2.1` RC 版本线
346
350
  | 卸载后记忆还在 | 先停 `dsh web` 再 `dsh plugin remove`。剩下的 `.tdb` 是从未用 0.4.10 打开过的工作区,到该文件夹删 `.dsh/trivium.tdb`。已提交的 `trivium.jsonl` 用 `git checkout` 找回。只在配置里 disabled 不会清文件。 |
347
351
  | 检查更新失败 | 只请求 npm registry 拿最新版本号,不影响记忆;过一会儿再点即可。 |
348
352
  | rc.8 之后旧对话打不开 | 那是 DSH 自己的会话库格式变了,不是 `.tdb` 坏了。图记忆仍可 `ctx_find`;对不上号的旧方框可当情节残留。 |
353
+ | 桌面端弹出「A JavaScript error occurred in the main process / EPIPE: broken pipe, write」 | 宿主侧问题,不是插件:`electron-updater` 往一个已经断开的控制台管道写日志(常见于用 `--inspect` 的测试启动器拉起、父进程随后退出)。栈里没有 `dsh-trivium`。点确定后从开始菜单正常启动;不要从会关闭的终端/测试器拉起。 |
354
+
355
+ **维护命令**(0.6.0 起随包分发):
356
+
357
+ - `npx dsh-trivium-check <工作区>` —— 以**只读**方式打开图,报告 `supersedes` 自环、循环、分叉、重复 URI、悬空端点与无法解析的链。退出码:0 干净,2 有问题,1 打不开。
358
+ - `npx dsh-trivium-clear-until <工作区>` —— 默认只列出仍带期限的过期决策;`--apply` 只清「期限文本同时出现在正文里」的那些(0.4.21 从正文抓日期的形态);`--apply --all` 清掉所有过期期限,包括用户显式填的。先关宿主,内核是单写者。
349
359
 
350
360
  本地联调:仓库里 `npm install` 后 `node scripts/link-dsh.mjs`,再重启 `dsh web`。`triviumdb` 是原生模块,装不上时先看 Node 是否满足 `package.json` 的 `engines`(`^22.19.0 || >=24`)。
351
361
 
@@ -354,8 +364,8 @@ peer 范围显式接受已支持的 DSH `0.1.5`、`0.2.0`、`0.2.1` RC 版本线
354
364
  ## 发布信息
355
365
 
356
366
  - GitHub: https://github.com/QWQcool/dsh-trivium
357
- - 包版本:`0.4.22` · [npm](https://www.npmjs.com/package/dsh-trivium)
358
- - 测试宿主:`@deepseek-ai/dsh@0.1.5-rc.1`(兼 `0.1.1-rc.2` / `0.1.0-rc.8`)
367
+ - 包版本:`0.7.0` · [npm](https://www.npmjs.com/package/dsh-trivium)
368
+ - 测试宿主:`@deepseek-ai/dsh@0.2.1-rc.4`(peer 范围同时接受 0.1.x 各线)
359
369
  - License: MIT(依赖 [TriviumDB](https://github.com/YoKONCy/TriviumDB) 为 Apache-2.0)
360
370
 
361
371
  ---
package/lib/client.js CHANGED
@@ -84,6 +84,17 @@ window.__ModuleLoader__.load({
84
84
  recallSemanticOff: "语义通道未运行:只读检查不请求 embedding。",
85
85
  recallBudget: "本轮预算",
86
86
  recallBudgetHint: "估算 token,不是模型计费。芯片、更正与召回共用这 300。",
87
+ correctionsTitle: "更正",
88
+ correctionsHint: "事实被修改后,看过它的会话会收到一条短更正;只有真正送达才从待发送里移除。",
89
+ correctionsPending: "下一条消息生效",
90
+ correctionsSent: "最近已发送",
91
+ correctionsNone: "没有待发送或最近发送的更正。",
92
+ correctionsSession: "会话",
93
+ integrityTitle: "版本链体检",
94
+ integrityCheck: "检查版本链",
95
+ integrityOk: "版本链正常。",
96
+ integrityIssues: "发现问题",
97
+ integrityScanned: "已扫描",
87
98
  recallWillSend: "将发送给模型的文本",
88
99
  recallEmptyText: "本轮没有可注入的内容。",
89
100
  recallReasons: "入选与过滤原因",
@@ -311,6 +322,17 @@ window.__ModuleLoader__.load({
311
322
  recallSemanticOff: "Semantic channel did not run: the read-only check never requests an embedding.",
312
323
  recallBudget: "Turn budget",
313
324
  recallBudgetHint: "Estimated tokens, not model billing. Chips, corrections and recall share this 300.",
325
+ correctionsTitle: "Corrections",
326
+ correctionsHint: "When a delivered fact changes, the session that saw it gets a short notice; it leaves this list only after delivery.",
327
+ correctionsPending: "applies with the next message",
328
+ correctionsSent: "Recently sent",
329
+ correctionsNone: "No pending or recently sent corrections.",
330
+ correctionsSession: "Session",
331
+ integrityTitle: "Version-chain check",
332
+ integrityCheck: "Check version chains",
333
+ integrityOk: "Version chains are clean.",
334
+ integrityIssues: "Issues found",
335
+ integrityScanned: "Scanned",
314
336
  recallWillSend: "Text that would be sent to the model",
315
337
  recallEmptyText: "Nothing would be injected this turn.",
316
338
  recallReasons: "Selected and filtered reasons",
@@ -748,6 +770,9 @@ window.__ModuleLoader__.load({
748
770
  const [previewBusy, setPreviewBusy] = react.useState(false);
749
771
  const [traces, setTraces] = react.useState([]);
750
772
  const [traceError, setTraceError] = react.useState("");
773
+ const [corrections, setCorrections] = react.useState(null);
774
+ const [integrity, setIntegrity] = react.useState(null);
775
+ const [integrityBusy, setIntegrityBusy] = react.useState(false);
751
776
  const [showSuperseded, setShowSuperseded] = react.useState(false);
752
777
  const [revisionDialog, setRevisionDialog] = react.useState(null);
753
778
  const [revisionBusy, setRevisionBusy] = react.useState(false);
@@ -950,9 +975,33 @@ window.__ModuleLoader__.load({
950
975
  }
951
976
  };
952
977
 
978
+ const loadCorrections = react.useCallback(async () => {
979
+ try {
980
+ const q = cwdQuery();
981
+ setCorrections(await api("/api/dsh-trivium/corrections" + (q ? "?" + q : "")));
982
+ } catch (err) {
983
+ setCorrections({ ok: false, error: String(err.message || err), sessions: [] });
984
+ }
985
+ }, [status]);
986
+
987
+ const runIntegrity = async () => {
988
+ setIntegrityBusy(true);
989
+ try {
990
+ const q = cwdQuery();
991
+ setIntegrity(await api("/api/dsh-trivium/integrity" + (q ? "?" + q : "")));
992
+ } catch (err) {
993
+ setIntegrity({ ok: false, error: String(err.message || err) });
994
+ } finally {
995
+ setIntegrityBusy(false);
996
+ }
997
+ };
998
+
953
999
  react.useEffect(() => {
954
- if (tab === "recall") loadTraces();
955
- }, [tab, loadTraces]);
1000
+ if (tab === "recall") {
1001
+ loadTraces();
1002
+ loadCorrections();
1003
+ }
1004
+ }, [tab, loadTraces, loadCorrections]);
956
1005
 
957
1006
  /*
958
1007
  * Dialog accessibility: focus moves into the panel, Escape closes it, and
@@ -1380,6 +1429,83 @@ window.__ModuleLoader__.load({
1380
1429
  ],
1381
1430
  })
1382
1431
  : null,
1432
+ jsx("div", { style: ui.fieldLabel, children: L.integrityTitle }),
1433
+ jsxs("div", {
1434
+ style: ui.toolbar,
1435
+ children: [
1436
+ jsx(Button, {
1437
+ variant: "outline",
1438
+ size: "sm",
1439
+ disabled: integrityBusy,
1440
+ onClick: runIntegrity,
1441
+ children: integrityBusy ? L.loading : L.integrityCheck,
1442
+ }),
1443
+ integrity
1444
+ ? jsx("span", { style: ui.muted, children: integrity.ok ? L.integrityOk : L.integrityIssues })
1445
+ : null,
1446
+ ],
1447
+ }),
1448
+ integrity && !integrity.ok && (integrity.issues || []).length
1449
+ ? jsx("div", {
1450
+ style: ui.error,
1451
+ children: integrity.issues.map((issue) => "#" + (issue.id ?? "?") + " " + issue.code).join(" · "),
1452
+ })
1453
+ : null,
1454
+ integrity && integrity.ok
1455
+ ? jsx("div", { style: ui.muted, children: L.integrityScanned + " " + integrity.nodes + " / " + integrity.edges })
1456
+ : null,
1457
+ jsx("div", { style: ui.fieldLabel, children: L.correctionsTitle }),
1458
+ jsx("div", { style: ui.hint, children: L.correctionsHint }),
1459
+ corrections === null
1460
+ ? null
1461
+ : !(corrections.sessions || []).length
1462
+ ? jsx("div", { style: ui.muted, children: L.correctionsNone })
1463
+ : jsxs("div", {
1464
+ style: ui.stack,
1465
+ children: corrections.sessions.map((session) =>
1466
+ jsxs(
1467
+ "div",
1468
+ {
1469
+ style: ui.stack,
1470
+ children: [
1471
+ jsx("div", {
1472
+ style: ui.muted,
1473
+ children: L.correctionsSession + " " + String(session.sessionId || "").slice(0, 12),
1474
+ }),
1475
+ ...(session.pending || []).map((row) =>
1476
+ jsxs(
1477
+ "div",
1478
+ {
1479
+ style: { ...ui.muted, display: "flex", gap: 6, flexWrap: "wrap" },
1480
+ children: [
1481
+ jsx("span", { style: ui.tag, children: "#" + row.id }),
1482
+ jsx("span", { children: row.note || "" }),
1483
+ jsx("span", { children: L.correctionsPending }),
1484
+ ],
1485
+ },
1486
+ "cp-" + session.sessionId + "-" + row.id,
1487
+ ),
1488
+ ),
1489
+ ...(session.sent || []).slice(-3).map((row, i) =>
1490
+ jsxs(
1491
+ "div",
1492
+ {
1493
+ style: { ...ui.muted, display: "flex", gap: 6, flexWrap: "wrap" },
1494
+ children: [
1495
+ jsx("span", { style: ui.tag, children: "✓ #" + row.id }),
1496
+ jsx("span", { children: row.note || "" }),
1497
+ jsx("span", { children: L.correctionsSent }),
1498
+ ],
1499
+ },
1500
+ "cs-" + session.sessionId + "-" + i,
1501
+ ),
1502
+ ),
1503
+ ],
1504
+ },
1505
+ "corr-" + session.sessionId,
1506
+ ),
1507
+ ),
1508
+ }),
1383
1509
  jsx("div", { style: ui.fieldLabel, children: L.recallRecent }),
1384
1510
  !traces.length
1385
1511
  ? jsx("div", { style: ui.muted, children: L.recallNoTrace })
package/lib/server.js CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  import { embeddingPublicStatus } from "./embed.js";
22
22
  import { chipsEnabledOf, configPanelOpenOf, gitSidecarEnabledOf, listedWorkspaces, pluginLocaleOf, recallModeOf, sessionLayerEnabledOf, writeUiSettings } from "./settings.js";
23
23
  import { TURN_ADDITIONAL_BUDGET } from "./context-budget.js";
24
+ import { checkRevisionIntegrity } from "./integrity.js";
25
+ import { correctionSessions } from "./turn-ledger.js";
24
26
  import { discoverWorkbuddyFiles, discoverExternalFiles, exportMarkdown, importWorkbuddy, importExternal } from "./markdown.js";
25
27
  import { checkNpmUpdate, installedVersion } from "./update.js";
26
28
  import { writeGateMessage } from "./hygiene.js";
@@ -202,6 +204,28 @@ async function handle(req, res, { liveOptions, status }) {
202
204
  return;
203
205
  }
204
206
 
207
+ if (path === "/api/dsh-trivium/corrections" && method === "GET") {
208
+ const paramCwd = url.searchParams.get("cwd");
209
+ const cwd = paramCwd ? (isKnownCwd(paramCwd) ? paramCwd : "") : getActiveCwd();
210
+ sendJson(res, 200, {
211
+ ok: true,
212
+ cwd: cwd || "",
213
+ sessions: correctionSessions(cwd || ""),
214
+ });
215
+ return;
216
+ }
217
+
218
+ if (path === "/api/dsh-trivium/integrity" && method === "GET") {
219
+ const { cwd, db } = await dbForRequest(url);
220
+ if (!db) {
221
+ sendJson(res, 400, { ok: false, message: "no workspace" });
222
+ return;
223
+ }
224
+ const report = checkRevisionIntegrity(db);
225
+ sendJson(res, 200, { ok: true, cwd, ...report });
226
+ return;
227
+ }
228
+
205
229
  if (path === "/api/dsh-trivium/settings" && method === "POST") {
206
230
  const body = JSON.parse((await readBody(req)) || "{}");
207
231
  const saved = writeUiSettings(body);
@@ -35,6 +35,8 @@ function stateOf(sessionId) {
35
35
  // correction must not cross workspaces.
36
36
  cwd: "",
37
37
  delivered: new Set(),
38
+ // Last corrections actually handed to the host, for the settings panel.
39
+ sentCorrections: [],
38
40
  /*
39
41
  * What this session was actually shown, per node: body, state and the
40
42
  * fingerprint it was shown under. A later write consults this table to
@@ -120,6 +122,32 @@ export function pendingCorrections(sessionId) {
120
122
  return [...stateOf(sessionId).corrections];
121
123
  }
122
124
 
125
+ /**
126
+ * Pending and recently delivered corrections, grouped by session.
127
+ *
128
+ * The panel is not tied to one session, so it asks per workspace and shows the
129
+ * short session id. A session that never received a fact never appears.
130
+ */
131
+ export function correctionSessions(cwd = "") {
132
+ const want = cwd ? resolve(String(cwd)) : "";
133
+ const out = [];
134
+ for (const state of sessions.values()) {
135
+ if (want && state.cwd && state.cwd !== want) continue;
136
+ if (!state.corrections.length && !state.sentCorrections.length) continue;
137
+ out.push({
138
+ sessionId: state.sessionId,
139
+ cwd: state.cwd,
140
+ pending: state.corrections.map((c) => ({
141
+ id: c.id,
142
+ note: String(c.l0 || ""),
143
+ queuedAt: c.queuedAt || null,
144
+ })),
145
+ sent: [...state.sentCorrections].slice(-10),
146
+ });
147
+ }
148
+ return out;
149
+ }
150
+
123
151
  /**
124
152
  * A fact changed after a session was shown it. Queue a short correction for
125
153
  * every session that actually received this node — "delivered once, never
@@ -220,6 +248,12 @@ export function commit(reservation) {
220
248
  // Consumed only now: an over-budget correction stays pending for the
221
249
  // next turn instead of being silently dropped.
222
250
  deliveredCorrections.add(record.id);
251
+ state.sentCorrections.push({
252
+ id: record.id,
253
+ note: String(record.l0 || ""),
254
+ at: new Date().toISOString(),
255
+ });
256
+ if (state.sentCorrections.length > 20) state.sentCorrections.shift();
223
257
  continue;
224
258
  }
225
259
  if (!record.fingerprint) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trivium",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "In-process graph memory for DeepSeek Harness, backed by TriviumDB.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,11 +12,17 @@
12
12
  },
13
13
  "files": [
14
14
  "lib",
15
+ "scripts/check-integrity.mjs",
16
+ "scripts/clear-expired-until.mjs",
15
17
  "cordis.patch.yml",
16
18
  "README.md",
17
19
  "README.zh-CN.md",
18
20
  "LICENSE"
19
21
  ],
22
+ "bin": {
23
+ "dsh-trivium-check": "scripts/check-integrity.mjs",
24
+ "dsh-trivium-clear-until": "scripts/clear-expired-until.mjs"
25
+ },
20
26
  "dsh": {
21
27
  "bundle": {
22
28
  "patch": "./cordis.patch.yml"
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read-only check for decision revision integrity.
4
+ *
5
+ * node scripts/check-integrity.mjs <workspace> [--json]
6
+ *
7
+ * Exit codes: 0 clean, 2 issues found, 1 cannot open.
8
+ *
9
+ * The database is opened with accessMode "readOnly": no WAL, no text index
10
+ * rebuild, no workspace registration. If a host is holding the file, the open
11
+ * is refused — close the host or run this on a copy, never let a checker modify
12
+ * the file it is judging.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+
17
+ import { checkRevisionIntegrity } from "../lib/integrity.js";
18
+ import { dbPathFor, openWorkspaceDbReadOnly } from "../lib/store.js";
19
+
20
+ const args = process.argv.slice(2);
21
+ const asJson = args.includes("--json");
22
+ const cwd = resolve(args.find((arg) => !arg.startsWith("--")) || process.cwd());
23
+ const file = dbPathFor(cwd);
24
+
25
+ if (!existsSync(file)) {
26
+ console.error(`no memory database at ${file}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ let db;
31
+ try {
32
+ db = openWorkspaceDbReadOnly(cwd);
33
+ } catch (err) {
34
+ console.error(`cannot open read-only (is another host using it?): ${err.message}`);
35
+ process.exit(1);
36
+ }
37
+
38
+ try {
39
+ const report = checkRevisionIntegrity(db);
40
+ if (asJson) {
41
+ console.log(JSON.stringify({ cwd, file, ...report }, null, 2));
42
+ } else {
43
+ console.log(`workspace: ${cwd}`);
44
+ console.log(`database: ${file}`);
45
+ console.log(`nodes: ${report.nodes} revision edges: ${report.edges}`);
46
+ if (report.ok) {
47
+ console.log("integrity: ok");
48
+ } else {
49
+ console.log(`integrity: ${report.issues.length} issue(s)`);
50
+ for (const issue of report.issues) console.log(`- ${issue.code}: ${JSON.stringify(issue)}`);
51
+ }
52
+ }
53
+ process.exitCode = report.ok ? 0 : 2;
54
+ } finally {
55
+ try {
56
+ db.close?.();
57
+ } catch {
58
+ // closing a read-only handle is best effort
59
+ }
60
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * List or clear stale decision deadlines.
4
+ *
5
+ * node scripts/clear-expired-until.mjs <workspace> [--json]
6
+ * node scripts/clear-expired-until.mjs <workspace> --apply [--all]
7
+ *
8
+ * Default is a read-only listing. `--apply` clears deadlines only on decisions
9
+ * that are already stale, and by default only when the deadline text is also
10
+ * present in the body — that is the shape 0.4.21 produced when it parsed dates
11
+ * out of prose. Pass `--all` to clear every stale decision deadline, including
12
+ * ones the user typed explicitly; that is a deliberate, destructive choice.
13
+ *
14
+ * The workspace must not be open in another process (the kernel is single
15
+ * writer). Back up `.dsh/trivium.tdb` if you are not sure.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { resolve } from "node:path";
19
+
20
+ import {
21
+ closeAll,
22
+ dbPathFor,
23
+ describeDbFailure,
24
+ openWorkspaceDb,
25
+ updateNode,
26
+ } from "../lib/store.js";
27
+ import { isStalePayload } from "../lib/until.js";
28
+
29
+ const args = process.argv.slice(2);
30
+ const asJson = args.includes("--json");
31
+ const apply = args.includes("--apply");
32
+ const all = args.includes("--all");
33
+ const cwd = resolve(args.find((arg) => !arg.startsWith("--")) || process.cwd());
34
+ const file = dbPathFor(cwd);
35
+
36
+ function compact(value) {
37
+ return String(value ?? "").replace(/\s+/g, "");
38
+ }
39
+
40
+ /** 0.4.21 turned a date in the prose into `until`; that date is still in the body. */
41
+ function textMentionsUntil(text, until) {
42
+ const raw = String(until || "").trim();
43
+ if (!raw) return false;
44
+ const hay = compact(text);
45
+ const variants = [raw, raw.replace(/-/g, "."), raw.replace(/-/g, "/")];
46
+ return variants.some((value) => hay.includes(compact(value)));
47
+ }
48
+
49
+ function fail(message, code = 1) {
50
+ if (asJson) console.log(JSON.stringify({ ok: false, message }, null, 2));
51
+ else console.error(message);
52
+ process.exit(code);
53
+ }
54
+
55
+ if (!existsSync(file)) {
56
+ fail(`No database at ${file}. Pass the workspace directory that contains .dsh/trivium.tdb.`);
57
+ }
58
+
59
+ let db;
60
+ try {
61
+ db = await openWorkspaceDb(cwd);
62
+ } catch (err) {
63
+ const lock = describeDbFailure(err);
64
+ fail(lock ? lock.message : `Cannot open ${file}: ${err.message || err}`);
65
+ }
66
+
67
+ const rows = [];
68
+ for (const id of db.allNodeIds?.() || []) {
69
+ const payload = db.get(id)?.payload || {};
70
+ if (payload.type !== "decision") continue;
71
+ if (!String(payload.until || "").trim()) continue;
72
+ if (!isStalePayload(payload)) continue;
73
+ rows.push({
74
+ id: Number(id),
75
+ name: String(payload.name || ""),
76
+ until: String(payload.until || ""),
77
+ untilAt: String(payload.untilAt || ""),
78
+ textMentionsUntil: textMentionsUntil(payload.text, payload.until),
79
+ });
80
+ }
81
+
82
+ const selected = apply ? (all ? rows : rows.filter((row) => row.textMentionsUntil)) : [];
83
+ const cleared = [];
84
+ for (const row of selected) {
85
+ try {
86
+ updateNode(db, row.id, { until: "" });
87
+ cleared.push(row.id);
88
+ } catch (err) {
89
+ fail(`#${row.id} could not be cleared: ${err.message || err}`);
90
+ }
91
+ }
92
+ if (cleared.length) db.flush();
93
+
94
+ const report = {
95
+ ok: true,
96
+ cwd,
97
+ file,
98
+ applied: apply,
99
+ all,
100
+ staleDecisions: rows.length,
101
+ textMentionsUntil: rows.filter((row) => row.textMentionsUntil).length,
102
+ cleared,
103
+ rows,
104
+ };
105
+ closeAll();
106
+
107
+ if (asJson) {
108
+ console.log(JSON.stringify(report, null, 2));
109
+ } else {
110
+ console.log(`${rows.length} stale decision(s) with a deadline in ${file}`);
111
+ for (const row of rows) {
112
+ const mark = row.textMentionsUntil ? "date in body" : "explicit deadline";
113
+ console.log(` #${row.id} ${row.name || "(no name)"} until=${row.until} (${mark})`);
114
+ }
115
+ if (!apply) {
116
+ console.log("\nDry run. Re-run with --apply to clear dates that also appear in the body, or --apply --all for every stale deadline.");
117
+ } else {
118
+ console.log(`\nCleared ${cleared.length} deadline(s).`);
119
+ }
120
+ }