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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
if
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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 =
|
|
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),
|
|
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":
|
|
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):
|