dsh-zotero-skill 0.5.1 → 0.5.4

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/package.json CHANGED
@@ -1,60 +1,60 @@
1
- {
2
- "name": "dsh-zotero-skill",
3
- "version": "0.5.1",
4
- "description": "Zotero skill for DeepSeek Harness: find papers on OpenAlex or Crossref, import Chinese CNKI export files, track research fronts, screen core papers, manage and classify the library, batch-read, map the landscape, and surface research gaps. Also exports BibTeX and cites into LaTeX/Markdown.",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./package.json": "./package.json"
10
- },
11
- "files": [
12
- "lib/**/*.js",
13
- "zotero/**",
14
- "docs/**",
15
- "!**/__pycache__/**",
16
- "!**/*.pyc",
17
- "cordis.patch.yml",
18
- "README.md",
19
- "LICENSE",
20
- "NOTICE.md"
21
- ],
22
- "publishConfig": {
23
- "access": "public"
24
- },
25
- "dsh": {
26
- "bundle": {
27
- "patch": "./cordis.patch.yml"
28
- }
29
- },
30
- "scripts": {
31
- "test": "node --test test/*.test.mjs",
32
- "prepublishOnly": "node --test test/*.test.mjs"
33
- },
34
- "engines": {
35
- "node": ">=20"
36
- },
37
- "license": "MIT",
38
- "repository": {
39
- "type": "git",
40
- "url": "git+https://github.com/Eason-0712/dsh-zotero-skill.git"
41
- },
42
- "homepage": "https://github.com/Eason-0712/dsh-zotero-skill#readme",
43
- "bugs": {
44
- "url": "https://github.com/Eason-0712/dsh-zotero-skill/issues"
45
- },
46
- "keywords": [
47
- "dsh",
48
- "dsh-plugin",
49
- "deepseek-harness",
50
- "zotero",
51
- "skill",
52
- "bibtex",
53
- "citation",
54
- "literature-review",
55
- "openalex",
56
- "crossref",
57
- "cnki",
58
- "research"
59
- ]
60
- }
1
+ {
2
+ "name": "dsh-zotero-skill",
3
+ "version": "0.5.4",
4
+ "description": "Zotero skill for DeepSeek Harness: find papers on OpenAlex or Crossref, import Chinese CNKI export files, track research fronts, screen core papers, manage and classify the library, batch-read, map the landscape, and surface research gaps. Also exports BibTeX and cites into LaTeX/Markdown.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "files": [
12
+ "lib/**/*.js",
13
+ "zotero/**",
14
+ "docs/**",
15
+ "!**/__pycache__/**",
16
+ "!**/*.pyc",
17
+ "cordis.patch.yml",
18
+ "README.md",
19
+ "LICENSE",
20
+ "NOTICE.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "test": "node --test test/*.test.mjs",
32
+ "prepublishOnly": "node --test test/*.test.mjs"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/Eason-0712/dsh-zotero-skill.git"
41
+ },
42
+ "homepage": "https://github.com/Eason-0712/dsh-zotero-skill#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/Eason-0712/dsh-zotero-skill/issues"
45
+ },
46
+ "keywords": [
47
+ "dsh",
48
+ "dsh-plugin",
49
+ "deepseek-harness",
50
+ "zotero",
51
+ "skill",
52
+ "bibtex",
53
+ "citation",
54
+ "literature-review",
55
+ "openalex",
56
+ "crossref",
57
+ "cnki",
58
+ "research"
59
+ ]
60
+ }
@@ -51,7 +51,7 @@ read ask --jobs read-A read-B --question "两个方法的泛化设定有什么
51
51
  }
52
52
  ```
53
53
 
54
- `read audit <job>` 检查已有译文的术语、数字和疑似未翻译段。数字检查会正规化百分比、千分位和小数;对引用号变化或主动单位换算可能需要人工判断。`read accept --strict` 可要求这些线索均处理完再接受。一般接受仍会记录线索,不能因此宣称科学正确性已经验证。
54
+ `read audit <job>` 检查已有译文的术语、数字和疑似未翻译段。数字检查会正规化百分比、千分位和小数;对引用号变化或主动单位换算可能需要人工判断。`read status <job>` 只内联前 40 条线索并给出 `translation_audit_total`,完整列表由 `read audit` 提供——线索多时先看总数再决定是否逐条核对。`read accept --strict` 可要求这些线索均处理完再接受。一般接受仍会记录线索,不能因此宣称科学正确性已经验证。
55
55
 
56
56
  ## 纠错与历史
57
57
 
@@ -31,6 +31,8 @@
31
31
 
32
32
  将结果写入一个 UTF-8 JSON 文件,然后执行 `read accept <job> --input result.json`。可分批接受并恢复;普通接受不可覆盖旧结果,新增解释用新的 claim id。需要更正已有译文或结论时使用带 expected-revision 和原因的 `read revise`,并保留历史,详见 focused-reading.md。
33
33
 
34
+ `read status <job>` 与 `read accept` 统计的是「已接受快照 + 之后写入的分块结果」:子任务写出的 `t-*.json` 即使还没单独提交,也会被算进译文与结论数量,不会被上一次快照盖住,因此可以分批交付、边写边看。已接受的内容始终优先,后写入的分块不会覆盖它;两个尚未接受的分块互相矛盾时仍会报 `Conflicting translations`,需要先改掉其中一个再接受。
35
+
34
36
  ```json
35
37
  {
36
38
  "source_hash": "prepare返回的精确SHA256",
@@ -161,7 +161,8 @@ def run(action, args, api):
161
161
  if action == "history":
162
162
  return jobs.history(args.job)
163
163
  if action == "audit":
164
- return {"issues": jobs.inspect(args.job)["translation_audit"], "note": "Deterministic checks identify review candidates, not scientific correctness"}
164
+ issues = jobs.audit(args.job)
165
+ return {"issues": issues, "total": len(issues), "note": "Deterministic checks identify review candidates, not scientific correctness"}
165
166
  if action == "outline":
166
167
  return jobs.outline(args.job)
167
168
  if action == "annotations":
@@ -17,6 +17,7 @@ CATEGORIES = {"overview": "研究问题与章节逻辑", "method": "方法与结
17
17
  "implementation": "实现与复现"}
18
18
  KINDS = {"reported", "interpretation", "proposal"}
19
19
  KIND_LABELS = {"reported": "原文报告", "interpretation": "分析解释", "proposal": "待验证建议"}
20
+ AUDIT_INLINE_LIMIT = 40
20
21
 
21
22
 
22
23
  def now():
@@ -176,34 +177,73 @@ def result_store(directory):
176
177
 
177
178
 
178
179
  def aggregate(directory):
179
- current = s.read(result_store(directory) / "current.json")
180
+ """Merge the accepted snapshot with the chunk results written since it.
181
+
182
+ The snapshot stays authoritative: it is what someone explicitly accepted, so
183
+ a later chunk row never silently replaces an accepted translation or claim.
184
+ Chunk rows only fill in what the snapshot does not cover yet, which is the
185
+ normal flow after `read accept` was called with partial results — ignoring
186
+ them made `read accept` report `translations: 0` for work that had already
187
+ been written to disk.
188
+ """
189
+ store = result_store(directory)
190
+ current = s.read(store / "current.json")
191
+ translations, claims, missing = {}, {}, {}
192
+ accepted_blocks, accepted_claims = set(), set()
180
193
  if current is not None:
181
194
  payload = current["payload"]
182
195
  content_hash = s.digest(payload)
183
196
  if content_hash != current.get("content_hash") or s.digest({"content_hash": content_hash, "previous": current.get("previous")}) != current.get("revision"):
184
197
  raise ValueError("Accepted result snapshot was modified; restore a verified revision")
185
- return ({r["block_id"]: r["text"] for r in payload["translations"]}, payload["claims"], payload["missing"])
186
- translations, claims, missing = {}, {}, {}
187
- for path in sorted(result_store(directory).glob("*.json")):
198
+ translations = {r["block_id"]: r["text"] for r in payload["translations"]}
199
+ claims = {c["id"]: c for c in payload["claims"]}
200
+ missing = dict(payload["missing"])
201
+ accepted_blocks.update(translations)
202
+ accepted_claims.update(claims)
203
+ for path in sorted(store.glob("*.json")):
204
+ if path.name == "current.json":
205
+ continue
188
206
  record = s.read(path)
207
+ if not isinstance(record, dict):
208
+ continue
189
209
  for row in record.get("translations", []):
190
- old = translations.get(row["block_id"])
191
- if old is not None and old != row["text"]:
192
- raise ValueError(f"Conflicting translations for {row['block_id']}")
193
- translations[row["block_id"]] = row["text"]
210
+ block_id = row["block_id"]
211
+ if block_id in translations:
212
+ if block_id not in accepted_blocks and translations[block_id] != row["text"]:
213
+ raise ValueError(f"Conflicting translations for {block_id}")
214
+ continue
215
+ translations[block_id] = row["text"]
194
216
  for row in record.get("claims", []):
195
- if row["id"] in claims and claims[row["id"]] != row:
196
- raise ValueError(f"Conflicting claim id: {row['id']}")
197
- claims[row["id"]] = row
198
- missing.update(record.get("missing", {}))
217
+ key = row["id"]
218
+ if key in claims:
219
+ if key not in accepted_claims and claims[key] != row:
220
+ raise ValueError(f"Conflicting claim id: {key}")
221
+ continue
222
+ claims[key] = row
223
+ for category, reason in record.get("missing", {}).items():
224
+ missing.setdefault(category, reason)
199
225
  return translations, list(claims.values()), missing
200
226
 
201
227
 
202
228
  def result_revision(directory):
203
229
  translations, claims, missing = aggregate(directory)
204
230
  current = s.read(result_store(directory) / "current.json")
231
+ payload = result_payload(translations, claims, missing)
232
+ if current is not None and s.digest(payload) == current.get("content_hash"):
233
+ return current["revision"]
234
+ return s.digest(payload)
235
+
236
+
237
+ def base_revision(directory):
238
+ """The revision a new snapshot should build on.
239
+
240
+ Pending chunk results are not a revision yet, so the chain keeps pointing at
241
+ the last accepted snapshot instead of at a hash that was never committed.
242
+ """
243
+ current = s.read(result_store(directory) / "current.json")
205
244
  if current is not None:
206
245
  return current["revision"]
246
+ translations, claims, missing = aggregate(directory)
207
247
  return s.digest(result_payload(translations, claims, missing))
208
248
 
209
249
 
@@ -228,7 +268,10 @@ def commit_results(directory, payload, previous, reason):
228
268
  def history(job_id):
229
269
  directory, _ = load(job_id)
230
270
  store = result_store(directory)
231
- revision = result_revision(directory)
271
+ # Walk the accepted snapshots only: pending chunk rows are merged by aggregate()
272
+ # but are not revisions, so starting from result_revision() would look for a
273
+ # history file that does not exist and return an empty chain.
274
+ revision = base_revision(directory)
232
275
  result, visited = [], set()
233
276
  while revision not in visited:
234
277
  visited.add(revision)
@@ -292,7 +335,7 @@ def accept(job_id, record, *, strict=False):
292
335
  result = validate(record, manifest, document)
293
336
  with s.lock(directory), s.lock(result_store(directory)):
294
337
  previous_t, previous_c, previous_missing = aggregate(directory)
295
- previous_revision = result_revision(directory)
338
+ previous_revision = base_revision(directory)
296
339
  old_claims = {c["id"]: c for c in previous_c}
297
340
  for row in result["translations"]:
298
341
  if row["block_id"] in previous_t and previous_t[row["block_id"]] != row["text"]:
@@ -327,7 +370,7 @@ def revise(job_id, record, expected_revision, reason):
327
370
  missing.update(result["missing"])
328
371
  for claim in by_id.values():
329
372
  missing.pop(claim["category"], None)
330
- commit_results(directory, result_payload(translations, list(by_id.values()), missing), expected_revision, reason)
373
+ commit_results(directory, result_payload(translations, list(by_id.values()), missing), base_revision(directory), reason)
331
374
  return inspect(job_id)
332
375
 
333
376
 
@@ -341,10 +384,21 @@ def inspect(job_id):
341
384
  categories = set(CATEGORIES) if manifest["settings"]["mode"] == "deep" else {"overview", "method", "experiment", "contribution", "limitation"}
342
385
  absent = sorted(categories - {c["category"] for c in claims} - set(missing)) if outputs & {"notes", "zotero-note", "annotated-pdf"} else []
343
386
  unreadable = [p["number"] for p in document["pages"] if not p["readable"]]
387
+ issues = quality.audit(document, translations, manifest["profile"])
344
388
  return {"id": job_id, "revision": result_revision(directory), "translations": len(translations), "claims": len(claims),
345
389
  "untranslated_blocks": untranslated, "uncovered_categories": absent,
346
390
  "unreadable_pages": unreadable, "ready": not (untranslated or absent or unreadable),
347
- "warnings": document["warnings"], "translation_audit": quality.audit(document, translations, manifest["profile"])}
391
+ "warnings": document["warnings"], "translation_audit": issues[:AUDIT_INLINE_LIMIT],
392
+ "translation_audit_total": len(issues),
393
+ "translation_audit_note": "status 只列出前 %d 条,完整列表用 read audit" % AUDIT_INLINE_LIMIT if len(issues) > AUDIT_INLINE_LIMIT else ""}
394
+
395
+
396
+ def audit(job_id):
397
+ """Every deterministic review hint, for `read audit`."""
398
+ directory, manifest = load(job_id)
399
+ document = s.read(manifest["document"])
400
+ translations, _, _ = aggregate(directory)
401
+ return quality.audit(document, translations, manifest["profile"])
348
402
 
349
403
 
350
404
  def source_link(manifest, page, annotation_key=None):