scientify 1.10.3 → 1.12.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 +22 -8
- package/README.zh.md +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/src/cli/research.d.ts +10 -0
- package/dist/src/cli/research.d.ts.map +1 -0
- package/dist/src/cli/research.js +283 -0
- package/dist/src/cli/research.js.map +1 -0
- package/dist/src/commands/metabolism-status.d.ts +6 -0
- package/dist/src/commands/metabolism-status.d.ts.map +1 -0
- package/dist/src/commands/metabolism-status.js +100 -0
- package/dist/src/commands/metabolism-status.js.map +1 -0
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +63 -0
- package/dist/src/commands.js.map +1 -1
- package/dist/src/hooks/cron-skill-inject.d.ts +21 -0
- package/dist/src/hooks/cron-skill-inject.d.ts.map +1 -0
- package/dist/src/hooks/cron-skill-inject.js +53 -0
- package/dist/src/hooks/cron-skill-inject.js.map +1 -0
- package/dist/src/hooks/research-mode.d.ts.map +1 -1
- package/dist/src/hooks/research-mode.js +6 -1
- package/dist/src/hooks/research-mode.js.map +1 -1
- package/dist/src/knowledge-state/project.d.ts +13 -0
- package/dist/src/knowledge-state/project.d.ts.map +1 -0
- package/dist/src/knowledge-state/project.js +88 -0
- package/dist/src/knowledge-state/project.js.map +1 -0
- package/dist/src/knowledge-state/render.d.ts +63 -0
- package/dist/src/knowledge-state/render.d.ts.map +1 -0
- package/dist/src/knowledge-state/render.js +368 -0
- package/dist/src/knowledge-state/render.js.map +1 -0
- package/dist/src/knowledge-state/store.d.ts +19 -0
- package/dist/src/knowledge-state/store.d.ts.map +1 -0
- package/dist/src/knowledge-state/store.js +978 -0
- package/dist/src/knowledge-state/store.js.map +1 -0
- package/dist/src/knowledge-state/types.d.ts +182 -0
- package/dist/src/knowledge-state/types.d.ts.map +1 -0
- package/dist/src/knowledge-state/types.js +2 -0
- package/dist/src/knowledge-state/types.js.map +1 -0
- package/dist/src/literature/subscription-state.d.ts +11 -0
- package/dist/src/literature/subscription-state.d.ts.map +1 -1
- package/dist/src/literature/subscription-state.js +38 -2
- package/dist/src/literature/subscription-state.js.map +1 -1
- package/dist/src/research-subscriptions/handlers.d.ts.map +1 -1
- package/dist/src/research-subscriptions/handlers.js +1 -0
- package/dist/src/research-subscriptions/handlers.js.map +1 -1
- package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
- package/dist/src/research-subscriptions/parse.js +14 -0
- package/dist/src/research-subscriptions/parse.js.map +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
- package/dist/src/research-subscriptions/prompt.js +178 -23
- package/dist/src/research-subscriptions/prompt.js.map +1 -1
- package/dist/src/research-subscriptions/types.d.ts +1 -0
- package/dist/src/research-subscriptions/types.d.ts.map +1 -1
- package/dist/src/templates/bootstrap.d.ts +8 -0
- package/dist/src/templates/bootstrap.d.ts.map +1 -0
- package/dist/src/templates/bootstrap.js +153 -0
- package/dist/src/templates/bootstrap.js.map +1 -0
- package/dist/src/tools/arxiv-download.d.ts +2 -2
- package/dist/src/tools/arxiv-download.d.ts.map +1 -1
- package/dist/src/tools/arxiv-download.js +9 -11
- package/dist/src/tools/arxiv-download.js.map +1 -1
- package/dist/src/tools/scientify-cron.d.ts +2 -0
- package/dist/src/tools/scientify-cron.d.ts.map +1 -1
- package/dist/src/tools/scientify-cron.js +7 -0
- package/dist/src/tools/scientify-cron.js.map +1 -1
- package/dist/src/tools/scientify-literature-state.d.ts +234 -0
- package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
- package/dist/src/tools/scientify-literature-state.js +638 -3
- package/dist/src/tools/scientify-literature-state.js.map +1 -1
- package/dist/src/tools/unpaywall-download.d.ts +2 -2
- package/dist/src/tools/unpaywall-download.js +4 -4
- package/dist/src/tools/unpaywall-download.js.map +1 -1
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
- package/skills/idea-generation/SKILL.md +24 -29
- package/skills/metabolism/SKILL.md +118 -0
- package/skills/metabolism-init/SKILL.md +80 -0
- package/skills/{literature-survey → research-collect}/SKILL.md +23 -33
- package/skills/research-experiment/SKILL.md +1 -1
- package/skills/research-implement/SKILL.md +1 -1
- package/skills/research-pipeline/SKILL.md +6 -11
- package/skills/research-plan/SKILL.md +3 -3
- package/skills/research-review/SKILL.md +1 -1
- package/skills/research-subscription/SKILL.md +18 -3
- package/skills/research-survey/SKILL.md +6 -6
- package/skills/write-review-paper/SKILL.md +14 -14
- package/skills/_shared/workspace-spec.md +0 -152
- package/skills/install-scientify/SKILL.md +0 -106
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { appendFile, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { dayKeyFromTimestamp, renderDailyChangesMarkdown, renderExplorationLogMarkdown, renderHypothesisMarkdown, renderIngestLogMarkdown, renderKnowledgeIndexMarkdown, renderPaperNoteHeaderMarkdown, renderPaperNoteRunMarkdown, renderTopicUpdateMarkdown, slugifyTopic, } from "./render.js";
|
|
6
|
+
import { resolveProjectContext } from "./project.js";
|
|
7
|
+
const STATE_VERSION = 1;
|
|
8
|
+
const MAX_RECENT_RUN_IDS = 200;
|
|
9
|
+
const MAX_RECENT_HYPOTHESES = 50;
|
|
10
|
+
const MAX_RECENT_CHANGE_STATS = 30;
|
|
11
|
+
const MAX_LAST_TRACE = 20;
|
|
12
|
+
const MAX_RECENT_PAPERS = 50;
|
|
13
|
+
const MAX_PAPER_NOTES = 800;
|
|
14
|
+
const MIN_CORE_FULLTEXT_COVERAGE = 0.8;
|
|
15
|
+
const MIN_EVIDENCE_BINDING_RATE = 0.9;
|
|
16
|
+
const MAX_CITATION_ERROR_RATE = 0.02;
|
|
17
|
+
function defaultQualityGateState() {
|
|
18
|
+
return {
|
|
19
|
+
passed: false,
|
|
20
|
+
fullTextCoveragePct: 0,
|
|
21
|
+
evidenceBindingRatePct: 0,
|
|
22
|
+
citationErrorRatePct: 0,
|
|
23
|
+
reasons: ["quality gate not evaluated"],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function normalizeText(raw) {
|
|
27
|
+
return raw.trim().replace(/\s+/g, " ");
|
|
28
|
+
}
|
|
29
|
+
function sanitizeId(raw) {
|
|
30
|
+
return normalizeText(raw)
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
33
|
+
.replace(/-+/g, "-")
|
|
34
|
+
.replace(/^-|-$/g, "");
|
|
35
|
+
}
|
|
36
|
+
function normalizeScope(raw) {
|
|
37
|
+
const trimmed = normalizeText(raw).toLowerCase();
|
|
38
|
+
if (!trimmed)
|
|
39
|
+
return "global";
|
|
40
|
+
const parts = trimmed.split(":");
|
|
41
|
+
if (parts.length === 1)
|
|
42
|
+
return sanitizeId(parts[0]) || "global";
|
|
43
|
+
return `${sanitizeId(parts[0])}:${sanitizeId(parts.slice(1).join(":"))}`;
|
|
44
|
+
}
|
|
45
|
+
function buildRunFingerprint(args) {
|
|
46
|
+
const digest = createHash("sha1")
|
|
47
|
+
.update(args.scope)
|
|
48
|
+
.update("\n")
|
|
49
|
+
.update(args.topic)
|
|
50
|
+
.update("\n")
|
|
51
|
+
.update(args.status)
|
|
52
|
+
.update("\n")
|
|
53
|
+
.update(args.day)
|
|
54
|
+
.update("\n")
|
|
55
|
+
.update(args.paperIds.sort().join("|"))
|
|
56
|
+
.update("\n")
|
|
57
|
+
.update(args.note ?? "")
|
|
58
|
+
.digest("hex");
|
|
59
|
+
return `fp-${digest.slice(0, 20)}`;
|
|
60
|
+
}
|
|
61
|
+
function getKnowledgeStateRoot(projectPath) {
|
|
62
|
+
return path.join(projectPath, "knowledge_state");
|
|
63
|
+
}
|
|
64
|
+
function getStatePath(projectPath) {
|
|
65
|
+
return path.join(getKnowledgeStateRoot(projectPath), "state.json");
|
|
66
|
+
}
|
|
67
|
+
function getEventsPath(projectPath) {
|
|
68
|
+
return path.join(getKnowledgeStateRoot(projectPath), "events.jsonl");
|
|
69
|
+
}
|
|
70
|
+
function getLockPath(projectPath) {
|
|
71
|
+
return path.join(getKnowledgeStateRoot(projectPath), ".lock");
|
|
72
|
+
}
|
|
73
|
+
function buildDefaultState() {
|
|
74
|
+
return {
|
|
75
|
+
version: STATE_VERSION,
|
|
76
|
+
updatedAtMs: Date.now(),
|
|
77
|
+
streams: {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function ensureLayout(projectPath) {
|
|
81
|
+
const root = getKnowledgeStateRoot(projectPath);
|
|
82
|
+
await mkdir(root, { recursive: true });
|
|
83
|
+
await mkdir(path.join(root, "knowledge"), { recursive: true });
|
|
84
|
+
await mkdir(path.join(root, "paper_notes"), { recursive: true });
|
|
85
|
+
await mkdir(path.join(root, "daily_changes"), { recursive: true });
|
|
86
|
+
await mkdir(path.join(root, "hypotheses"), { recursive: true });
|
|
87
|
+
await mkdir(path.join(root, "logs"), { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
async function loadState(projectPath) {
|
|
90
|
+
const file = getStatePath(projectPath);
|
|
91
|
+
if (!existsSync(file))
|
|
92
|
+
return buildDefaultState();
|
|
93
|
+
try {
|
|
94
|
+
const raw = await readFile(file, "utf-8");
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (parsed.version !== STATE_VERSION || !parsed.streams || typeof parsed.streams !== "object") {
|
|
97
|
+
return buildDefaultState();
|
|
98
|
+
}
|
|
99
|
+
const streams = {};
|
|
100
|
+
for (const [key, rawStream] of Object.entries(parsed.streams)) {
|
|
101
|
+
if (!rawStream || typeof rawStream !== "object")
|
|
102
|
+
continue;
|
|
103
|
+
const topicKey = rawStream.topicKey && normalizeText(rawStream.topicKey) ? normalizeText(rawStream.topicKey) : key;
|
|
104
|
+
streams[key] = {
|
|
105
|
+
scope: normalizeScope(rawStream.scope ?? "global"),
|
|
106
|
+
topic: normalizeText(rawStream.topic ?? "topic"),
|
|
107
|
+
topicKey,
|
|
108
|
+
projectId: sanitizeId(rawStream.projectId ?? "auto-topic-global-000000") || "auto-topic-global-000000",
|
|
109
|
+
totalRuns: typeof rawStream.totalRuns === "number" ? Math.max(0, Math.floor(rawStream.totalRuns)) : 0,
|
|
110
|
+
totalHypotheses: typeof rawStream.totalHypotheses === "number" ? Math.max(0, Math.floor(rawStream.totalHypotheses)) : 0,
|
|
111
|
+
knowledgeTopics: Array.isArray(rawStream.knowledgeTopics)
|
|
112
|
+
? rawStream.knowledgeTopics.filter((item) => typeof item === "string").map((item) => normalizeText(item))
|
|
113
|
+
: [],
|
|
114
|
+
paperNotes: Array.isArray(rawStream.paperNotes)
|
|
115
|
+
? rawStream.paperNotes.filter((item) => typeof item === "string").map((item) => normalizeText(item))
|
|
116
|
+
: [],
|
|
117
|
+
recentFullTextReadCount: typeof rawStream.recentFullTextReadCount === "number"
|
|
118
|
+
? Math.max(0, Math.floor(rawStream.recentFullTextReadCount))
|
|
119
|
+
: 0,
|
|
120
|
+
recentNotFullTextReadCount: typeof rawStream.recentNotFullTextReadCount === "number"
|
|
121
|
+
? Math.max(0, Math.floor(rawStream.recentNotFullTextReadCount))
|
|
122
|
+
: 0,
|
|
123
|
+
lastQualityGate: rawStream.lastQualityGate &&
|
|
124
|
+
typeof rawStream.lastQualityGate === "object" &&
|
|
125
|
+
!Array.isArray(rawStream.lastQualityGate)
|
|
126
|
+
? {
|
|
127
|
+
passed: rawStream.lastQualityGate.passed === true,
|
|
128
|
+
fullTextCoveragePct: typeof rawStream.lastQualityGate.fullTextCoveragePct === "number" &&
|
|
129
|
+
Number.isFinite(rawStream.lastQualityGate.fullTextCoveragePct)
|
|
130
|
+
? Number(rawStream.lastQualityGate.fullTextCoveragePct.toFixed(2))
|
|
131
|
+
: 0,
|
|
132
|
+
evidenceBindingRatePct: typeof rawStream.lastQualityGate.evidenceBindingRatePct === "number" &&
|
|
133
|
+
Number.isFinite(rawStream.lastQualityGate.evidenceBindingRatePct)
|
|
134
|
+
? Number(rawStream.lastQualityGate.evidenceBindingRatePct.toFixed(2))
|
|
135
|
+
: 0,
|
|
136
|
+
citationErrorRatePct: typeof rawStream.lastQualityGate.citationErrorRatePct === "number" &&
|
|
137
|
+
Number.isFinite(rawStream.lastQualityGate.citationErrorRatePct)
|
|
138
|
+
? Number(rawStream.lastQualityGate.citationErrorRatePct.toFixed(2))
|
|
139
|
+
: 0,
|
|
140
|
+
reasons: Array.isArray(rawStream.lastQualityGate.reasons)
|
|
141
|
+
? rawStream.lastQualityGate.reasons
|
|
142
|
+
.filter((item) => typeof item === "string")
|
|
143
|
+
.map((item) => normalizeText(item))
|
|
144
|
+
.filter((item) => item.length > 0)
|
|
145
|
+
: [],
|
|
146
|
+
}
|
|
147
|
+
: defaultQualityGateState(),
|
|
148
|
+
lastUnreadCorePaperIds: Array.isArray(rawStream.lastUnreadCorePaperIds)
|
|
149
|
+
? rawStream.lastUnreadCorePaperIds
|
|
150
|
+
.filter((item) => typeof item === "string")
|
|
151
|
+
.map((item) => normalizeText(item))
|
|
152
|
+
.filter((item) => item.length > 0)
|
|
153
|
+
: [],
|
|
154
|
+
recentPapers: Array.isArray(rawStream.recentPapers)
|
|
155
|
+
? rawStream.recentPapers
|
|
156
|
+
.filter((item) => !!item && typeof item === "object")
|
|
157
|
+
.map(normalizePaper)
|
|
158
|
+
: [],
|
|
159
|
+
...(typeof rawStream.lastRunAtMs === "number" ? { lastRunAtMs: rawStream.lastRunAtMs } : {}),
|
|
160
|
+
...(rawStream.lastStatus ? { lastStatus: normalizeText(rawStream.lastStatus) } : {}),
|
|
161
|
+
recentRunIds: Array.isArray(rawStream.recentRunIds)
|
|
162
|
+
? rawStream.recentRunIds.filter((item) => typeof item === "string").map((item) => normalizeText(item))
|
|
163
|
+
: [],
|
|
164
|
+
recentHypothesisIds: Array.isArray(rawStream.recentHypothesisIds)
|
|
165
|
+
? rawStream.recentHypothesisIds
|
|
166
|
+
.filter((item) => typeof item === "string")
|
|
167
|
+
.map((item) => normalizeText(item))
|
|
168
|
+
: [],
|
|
169
|
+
recentHypotheses: Array.isArray(rawStream.recentHypotheses)
|
|
170
|
+
? rawStream.recentHypotheses.filter((item) => !!item && typeof item === "object")
|
|
171
|
+
: [],
|
|
172
|
+
recentChangeStats: Array.isArray(rawStream.recentChangeStats)
|
|
173
|
+
? rawStream.recentChangeStats.filter((item) => !!item && typeof item === "object")
|
|
174
|
+
: [],
|
|
175
|
+
lastExplorationTrace: Array.isArray(rawStream.lastExplorationTrace)
|
|
176
|
+
? rawStream.lastExplorationTrace
|
|
177
|
+
.filter((item) => !!item && typeof item === "object")
|
|
178
|
+
.map(normalizeTrace)
|
|
179
|
+
.filter((item) => Boolean(item))
|
|
180
|
+
: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
version: STATE_VERSION,
|
|
185
|
+
updatedAtMs: typeof parsed.updatedAtMs === "number" ? parsed.updatedAtMs : Date.now(),
|
|
186
|
+
streams,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return buildDefaultState();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function saveStateAtomic(projectPath, state) {
|
|
194
|
+
const file = getStatePath(projectPath);
|
|
195
|
+
const tmp = `${file}.tmp`;
|
|
196
|
+
state.updatedAtMs = Date.now();
|
|
197
|
+
await writeFile(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
198
|
+
await rename(tmp, file);
|
|
199
|
+
}
|
|
200
|
+
async function appendEvent(projectPath, event) {
|
|
201
|
+
await appendFile(getEventsPath(projectPath), `${JSON.stringify(event)}\n`, "utf-8");
|
|
202
|
+
}
|
|
203
|
+
async function appendMarkdown(filePath, block) {
|
|
204
|
+
await appendFile(filePath, `${block}\n`, "utf-8");
|
|
205
|
+
}
|
|
206
|
+
function normalizeStringArray(raw) {
|
|
207
|
+
if (!Array.isArray(raw))
|
|
208
|
+
return undefined;
|
|
209
|
+
const values = raw
|
|
210
|
+
.filter((item) => typeof item === "string")
|
|
211
|
+
.map((item) => normalizeText(item))
|
|
212
|
+
.filter((item) => item.length > 0);
|
|
213
|
+
return values.length > 0 ? values : undefined;
|
|
214
|
+
}
|
|
215
|
+
function normalizeEvidenceAnchors(raw) {
|
|
216
|
+
if (!Array.isArray(raw))
|
|
217
|
+
return undefined;
|
|
218
|
+
const anchors = raw
|
|
219
|
+
.filter((item) => !!item && typeof item === "object")
|
|
220
|
+
.map((item) => {
|
|
221
|
+
const claim = normalizeText(item.claim ?? "");
|
|
222
|
+
if (!claim)
|
|
223
|
+
return undefined;
|
|
224
|
+
return {
|
|
225
|
+
...(item.section ? { section: normalizeText(item.section) } : {}),
|
|
226
|
+
...(item.locator ? { locator: normalizeText(item.locator) } : {}),
|
|
227
|
+
claim,
|
|
228
|
+
...(item.quote ? { quote: normalizeText(item.quote) } : {}),
|
|
229
|
+
};
|
|
230
|
+
})
|
|
231
|
+
.filter((item) => Boolean(item));
|
|
232
|
+
return anchors.length > 0 ? anchors : undefined;
|
|
233
|
+
}
|
|
234
|
+
function toPaperNoteSlug(paper) {
|
|
235
|
+
const primary = paper.id ?? paper.url ?? paper.title ?? "";
|
|
236
|
+
const raw = normalizeText(primary);
|
|
237
|
+
const base = sanitizeId(raw.replace(/[:/.]+/g, "-")).slice(0, 72);
|
|
238
|
+
const digest = createHash("sha1")
|
|
239
|
+
.update([paper.id ?? "", paper.url ?? "", paper.title ?? ""].join("\n"))
|
|
240
|
+
.digest("hex")
|
|
241
|
+
.slice(0, 8);
|
|
242
|
+
return `${base || "paper"}-${digest}`;
|
|
243
|
+
}
|
|
244
|
+
function normalizePaper(input) {
|
|
245
|
+
const evidenceIds = Array.isArray(input.evidenceIds)
|
|
246
|
+
? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
|
|
247
|
+
: undefined;
|
|
248
|
+
const keyEvidenceSpans = normalizeStringArray(input.keyEvidenceSpans);
|
|
249
|
+
const subdomains = normalizeStringArray(input.subdomains);
|
|
250
|
+
const crossDomainLinks = normalizeStringArray(input.crossDomainLinks);
|
|
251
|
+
const keyContributions = normalizeStringArray(input.keyContributions);
|
|
252
|
+
const practicalInsights = normalizeStringArray(input.practicalInsights);
|
|
253
|
+
const mustUnderstandPoints = normalizeStringArray(input.mustUnderstandPoints);
|
|
254
|
+
const limitations = normalizeStringArray(input.limitations);
|
|
255
|
+
const evidenceAnchors = normalizeEvidenceAnchors(input.evidenceAnchors);
|
|
256
|
+
const readStatusRaw = input.readStatus?.trim().toLowerCase();
|
|
257
|
+
const readStatus = readStatusRaw && ["fulltext", "partial", "metadata", "unread"].includes(readStatusRaw)
|
|
258
|
+
? readStatusRaw
|
|
259
|
+
: undefined;
|
|
260
|
+
const fullTextRead = typeof input.fullTextRead === "boolean"
|
|
261
|
+
? input.fullTextRead
|
|
262
|
+
: readStatus === "fulltext"
|
|
263
|
+
? true
|
|
264
|
+
: readStatus
|
|
265
|
+
? false
|
|
266
|
+
: undefined;
|
|
267
|
+
const unreadReason = input.unreadReason ? normalizeText(input.unreadReason) : undefined;
|
|
268
|
+
return {
|
|
269
|
+
...(input.id ? { id: normalizeText(input.id) } : {}),
|
|
270
|
+
...(input.title ? { title: normalizeText(input.title) } : {}),
|
|
271
|
+
...(input.url ? { url: normalizeText(input.url) } : {}),
|
|
272
|
+
...(input.source ? { source: normalizeText(input.source) } : {}),
|
|
273
|
+
...(input.publishedAt ? { publishedAt: normalizeText(input.publishedAt) } : {}),
|
|
274
|
+
...(typeof input.score === "number" && Number.isFinite(input.score)
|
|
275
|
+
? { score: Number(input.score.toFixed(2)) }
|
|
276
|
+
: {}),
|
|
277
|
+
...(input.reason ? { reason: normalizeText(input.reason) } : {}),
|
|
278
|
+
...(input.summary ? { summary: normalizeText(input.summary) } : {}),
|
|
279
|
+
...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
|
|
280
|
+
...(typeof fullTextRead === "boolean" ? { fullTextRead } : {}),
|
|
281
|
+
...(readStatus ? { readStatus } : {}),
|
|
282
|
+
...(input.fullTextSource ? { fullTextSource: normalizeText(input.fullTextSource) } : {}),
|
|
283
|
+
...(input.fullTextRef ? { fullTextRef: normalizeText(input.fullTextRef) } : {}),
|
|
284
|
+
...(unreadReason ? { unreadReason } : {}),
|
|
285
|
+
...(keyEvidenceSpans && keyEvidenceSpans.length > 0 ? { keyEvidenceSpans } : {}),
|
|
286
|
+
...(input.domain ? { domain: normalizeText(input.domain) } : {}),
|
|
287
|
+
...(subdomains ? { subdomains } : {}),
|
|
288
|
+
...(crossDomainLinks ? { crossDomainLinks } : {}),
|
|
289
|
+
...(input.researchGoal ? { researchGoal: normalizeText(input.researchGoal) } : {}),
|
|
290
|
+
...(input.approach ? { approach: normalizeText(input.approach) } : {}),
|
|
291
|
+
...(input.methodologyDesign ? { methodologyDesign: normalizeText(input.methodologyDesign) } : {}),
|
|
292
|
+
...(keyContributions ? { keyContributions } : {}),
|
|
293
|
+
...(practicalInsights ? { practicalInsights } : {}),
|
|
294
|
+
...(mustUnderstandPoints ? { mustUnderstandPoints } : {}),
|
|
295
|
+
...(limitations ? { limitations } : {}),
|
|
296
|
+
...(evidenceAnchors ? { evidenceAnchors } : {}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function normalizeTrace(input) {
|
|
300
|
+
const query = normalizeText(input.query ?? "");
|
|
301
|
+
if (!query)
|
|
302
|
+
return undefined;
|
|
303
|
+
const filteredOutReasons = Array.isArray(input.filteredOutReasons)
|
|
304
|
+
? input.filteredOutReasons.map((item) => normalizeText(item)).filter((item) => item.length > 0)
|
|
305
|
+
: undefined;
|
|
306
|
+
return {
|
|
307
|
+
query,
|
|
308
|
+
...(input.reason ? { reason: normalizeText(input.reason) } : {}),
|
|
309
|
+
...(input.source ? { source: normalizeText(input.source) } : {}),
|
|
310
|
+
...(typeof input.candidates === "number" && Number.isFinite(input.candidates)
|
|
311
|
+
? { candidates: Math.max(0, Math.floor(input.candidates)) }
|
|
312
|
+
: {}),
|
|
313
|
+
...(typeof input.filteredTo === "number" && Number.isFinite(input.filteredTo)
|
|
314
|
+
? { filteredTo: Math.max(0, Math.floor(input.filteredTo)) }
|
|
315
|
+
: {}),
|
|
316
|
+
...(filteredOutReasons && filteredOutReasons.length > 0 ? { filteredOutReasons } : {}),
|
|
317
|
+
...(typeof input.resultCount === "number" && Number.isFinite(input.resultCount)
|
|
318
|
+
? { resultCount: Math.max(0, Math.floor(input.resultCount)) }
|
|
319
|
+
: {}),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function paperIdentity(input) {
|
|
323
|
+
const id = input.id ? normalizeText(input.id).toLowerCase() : "";
|
|
324
|
+
if (id)
|
|
325
|
+
return `id:${id}`;
|
|
326
|
+
const url = input.url ? normalizeText(input.url).toLowerCase() : "";
|
|
327
|
+
if (url)
|
|
328
|
+
return `url:${url}`;
|
|
329
|
+
const title = input.title ? normalizeText(input.title).toLowerCase() : "";
|
|
330
|
+
if (title)
|
|
331
|
+
return `title:${title}`;
|
|
332
|
+
return "";
|
|
333
|
+
}
|
|
334
|
+
function mergePapers(primary, secondary) {
|
|
335
|
+
const byId = new Map();
|
|
336
|
+
const upsert = (paper) => {
|
|
337
|
+
const normalized = normalizePaper(paper);
|
|
338
|
+
const key = paperIdentity(normalized);
|
|
339
|
+
if (!key)
|
|
340
|
+
return;
|
|
341
|
+
const existing = byId.get(key);
|
|
342
|
+
if (!existing) {
|
|
343
|
+
byId.set(key, normalized);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
byId.set(key, {
|
|
347
|
+
...existing,
|
|
348
|
+
...normalized,
|
|
349
|
+
evidenceIds: normalized.evidenceIds && normalized.evidenceIds.length > 0
|
|
350
|
+
? [...new Set([...(existing.evidenceIds ?? []), ...normalized.evidenceIds])]
|
|
351
|
+
: existing.evidenceIds,
|
|
352
|
+
keyEvidenceSpans: normalized.keyEvidenceSpans && normalized.keyEvidenceSpans.length > 0
|
|
353
|
+
? [...new Set([...(existing.keyEvidenceSpans ?? []), ...normalized.keyEvidenceSpans])]
|
|
354
|
+
: existing.keyEvidenceSpans,
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
for (const item of primary)
|
|
358
|
+
upsert(item);
|
|
359
|
+
for (const item of secondary)
|
|
360
|
+
upsert(item);
|
|
361
|
+
return [...byId.values()];
|
|
362
|
+
}
|
|
363
|
+
function withReadMark(paper, unreadFallback) {
|
|
364
|
+
const normalized = normalizePaper(paper);
|
|
365
|
+
const { unreadReason: existingUnreadReason, ...rest } = normalized;
|
|
366
|
+
const readStatus = normalized.readStatus ?? (normalized.fullTextRead ? "fulltext" : "metadata");
|
|
367
|
+
const fullTextRead = typeof normalized.fullTextRead === "boolean" ? normalized.fullTextRead : readStatus === "fulltext";
|
|
368
|
+
const unreadReason = fullTextRead || readStatus === "fulltext"
|
|
369
|
+
? undefined
|
|
370
|
+
: existingUnreadReason?.trim()
|
|
371
|
+
? existingUnreadReason.trim()
|
|
372
|
+
: unreadFallback;
|
|
373
|
+
return {
|
|
374
|
+
...rest,
|
|
375
|
+
readStatus,
|
|
376
|
+
fullTextRead,
|
|
377
|
+
...(unreadReason ? { unreadReason } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function countFullTextStats(papers) {
|
|
381
|
+
let fullTextReadCount = 0;
|
|
382
|
+
let notFullTextReadCount = 0;
|
|
383
|
+
for (const paper of papers) {
|
|
384
|
+
if (paper.fullTextRead === true || paper.readStatus === "fulltext")
|
|
385
|
+
fullTextReadCount += 1;
|
|
386
|
+
else
|
|
387
|
+
notFullTextReadCount += 1;
|
|
388
|
+
}
|
|
389
|
+
return { fullTextReadCount, notFullTextReadCount };
|
|
390
|
+
}
|
|
391
|
+
function hasStructuredProfile(paper) {
|
|
392
|
+
return Boolean((paper.domain && paper.domain.trim()) ||
|
|
393
|
+
(paper.subdomains && paper.subdomains.length > 0) ||
|
|
394
|
+
(paper.crossDomainLinks && paper.crossDomainLinks.length > 0) ||
|
|
395
|
+
(paper.researchGoal && paper.researchGoal.trim()) ||
|
|
396
|
+
(paper.approach && paper.approach.trim()) ||
|
|
397
|
+
(paper.methodologyDesign && paper.methodologyDesign.trim()) ||
|
|
398
|
+
(paper.keyContributions && paper.keyContributions.length > 0) ||
|
|
399
|
+
(paper.practicalInsights && paper.practicalInsights.length > 0) ||
|
|
400
|
+
(paper.mustUnderstandPoints && paper.mustUnderstandPoints.length > 0) ||
|
|
401
|
+
(paper.limitations && paper.limitations.length > 0) ||
|
|
402
|
+
(paper.evidenceAnchors && paper.evidenceAnchors.length > 0));
|
|
403
|
+
}
|
|
404
|
+
function isFullTextRead(paper) {
|
|
405
|
+
return paper.fullTextRead === true || paper.readStatus === "fulltext";
|
|
406
|
+
}
|
|
407
|
+
function normalizedCitationToken(raw) {
|
|
408
|
+
return normalizeText(raw).toLowerCase();
|
|
409
|
+
}
|
|
410
|
+
function isStrictEvidenceAnchor(anchor) {
|
|
411
|
+
return Boolean(anchor.section?.trim() && anchor.locator?.trim() && anchor.quote?.trim());
|
|
412
|
+
}
|
|
413
|
+
function hasStrictEvidenceAnchor(paper) {
|
|
414
|
+
const anchors = paper.evidenceAnchors ?? [];
|
|
415
|
+
return anchors.some((anchor) => isStrictEvidenceAnchor(anchor));
|
|
416
|
+
}
|
|
417
|
+
function buildPaperLookup(papers) {
|
|
418
|
+
const lookup = new Map();
|
|
419
|
+
for (const paper of papers) {
|
|
420
|
+
const candidates = [paper.id, paper.url, paper.title]
|
|
421
|
+
.filter((item) => typeof item === "string")
|
|
422
|
+
.map((item) => normalizedCitationToken(item));
|
|
423
|
+
for (const key of candidates) {
|
|
424
|
+
if (!key)
|
|
425
|
+
continue;
|
|
426
|
+
if (!lookup.has(key))
|
|
427
|
+
lookup.set(key, paper);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return lookup;
|
|
431
|
+
}
|
|
432
|
+
function dedupeText(items) {
|
|
433
|
+
const seen = new Set();
|
|
434
|
+
const result = [];
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const key = normalizedCitationToken(item);
|
|
437
|
+
if (!key || seen.has(key))
|
|
438
|
+
continue;
|
|
439
|
+
seen.add(key);
|
|
440
|
+
result.push(item);
|
|
441
|
+
}
|
|
442
|
+
return result;
|
|
443
|
+
}
|
|
444
|
+
function applyQualityGates(args) {
|
|
445
|
+
const corePapers = args.corePapers;
|
|
446
|
+
const coreCount = corePapers.length;
|
|
447
|
+
const fullTextCoreCount = corePapers.filter((paper) => isFullTextRead(paper)).length;
|
|
448
|
+
const fullTextCoverage = coreCount > 0 ? fullTextCoreCount / coreCount : 0;
|
|
449
|
+
const fullTextCoveragePct = Number((fullTextCoverage * 100).toFixed(2));
|
|
450
|
+
const unreadCorePaperIds = dedupeText(corePapers
|
|
451
|
+
.filter((paper) => !isFullTextRead(paper))
|
|
452
|
+
.map((paper) => paper.id?.trim() || paper.url?.trim() || paper.title?.trim() || "unknown-paper")).slice(0, 50);
|
|
453
|
+
const paperLookup = buildPaperLookup(args.allRunPapers);
|
|
454
|
+
const strictAnchorByKey = new Map();
|
|
455
|
+
for (const [key, paper] of paperLookup.entries()) {
|
|
456
|
+
strictAnchorByKey.set(key, hasStrictEvidenceAnchor(paper));
|
|
457
|
+
}
|
|
458
|
+
const conclusionEvidenceLists = [];
|
|
459
|
+
for (const change of args.knowledgeChanges) {
|
|
460
|
+
conclusionEvidenceLists.push(change.evidenceIds ?? []);
|
|
461
|
+
}
|
|
462
|
+
for (const update of args.knowledgeUpdates) {
|
|
463
|
+
conclusionEvidenceLists.push(update.evidenceIds ?? []);
|
|
464
|
+
}
|
|
465
|
+
let boundConclusions = 0;
|
|
466
|
+
for (const evidenceIds of conclusionEvidenceLists) {
|
|
467
|
+
const normalizedIds = evidenceIds.map((id) => normalizedCitationToken(id)).filter((id) => id.length > 0);
|
|
468
|
+
if (normalizedIds.length === 0)
|
|
469
|
+
continue;
|
|
470
|
+
let allResolvable = true;
|
|
471
|
+
let hasStrictAnchor = false;
|
|
472
|
+
for (const id of normalizedIds) {
|
|
473
|
+
const resolved = paperLookup.get(id);
|
|
474
|
+
if (!resolved) {
|
|
475
|
+
allResolvable = false;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (strictAnchorByKey.get(id))
|
|
479
|
+
hasStrictAnchor = true;
|
|
480
|
+
else if (hasStrictEvidenceAnchor(resolved))
|
|
481
|
+
hasStrictAnchor = true;
|
|
482
|
+
}
|
|
483
|
+
if (allResolvable && hasStrictAnchor)
|
|
484
|
+
boundConclusions += 1;
|
|
485
|
+
}
|
|
486
|
+
const conclusionCount = conclusionEvidenceLists.length;
|
|
487
|
+
const evidenceBindingRate = conclusionCount > 0 ? boundConclusions / conclusionCount : 1;
|
|
488
|
+
const evidenceBindingRatePct = Number((evidenceBindingRate * 100).toFixed(2));
|
|
489
|
+
const citationIds = [];
|
|
490
|
+
for (const change of args.knowledgeChanges)
|
|
491
|
+
citationIds.push(...(change.evidenceIds ?? []));
|
|
492
|
+
for (const update of args.knowledgeUpdates)
|
|
493
|
+
citationIds.push(...(update.evidenceIds ?? []));
|
|
494
|
+
for (const hypothesis of args.hypotheses)
|
|
495
|
+
citationIds.push(...(hypothesis.evidenceIds ?? []));
|
|
496
|
+
const normalizedCitationIds = citationIds.map((id) => normalizedCitationToken(id)).filter((id) => id.length > 0);
|
|
497
|
+
let citationErrors = 0;
|
|
498
|
+
for (const id of normalizedCitationIds) {
|
|
499
|
+
if (!paperLookup.has(id))
|
|
500
|
+
citationErrors += 1;
|
|
501
|
+
}
|
|
502
|
+
const citationErrorRate = normalizedCitationIds.length > 0 ? citationErrors / normalizedCitationIds.length : 0;
|
|
503
|
+
const citationErrorRatePct = Number((citationErrorRate * 100).toFixed(2));
|
|
504
|
+
let downgradedHighConfidenceCount = 0;
|
|
505
|
+
for (const update of args.knowledgeUpdates) {
|
|
506
|
+
if (update.confidence !== "high")
|
|
507
|
+
continue;
|
|
508
|
+
const refs = (update.evidenceIds ?? []).map((id) => normalizedCitationToken(id)).filter((id) => id.length > 0);
|
|
509
|
+
let canKeepHigh = refs.length > 0;
|
|
510
|
+
if (canKeepHigh) {
|
|
511
|
+
for (const ref of refs) {
|
|
512
|
+
const paper = paperLookup.get(ref);
|
|
513
|
+
if (!paper || !isFullTextRead(paper)) {
|
|
514
|
+
canKeepHigh = false;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!canKeepHigh) {
|
|
520
|
+
update.confidence = "medium";
|
|
521
|
+
downgradedHighConfidenceCount += 1;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const reasons = [];
|
|
525
|
+
if (fullTextCoverage < MIN_CORE_FULLTEXT_COVERAGE) {
|
|
526
|
+
reasons.push(`core_fulltext_coverage_below_threshold(${fullTextCoveragePct}% < ${Number((MIN_CORE_FULLTEXT_COVERAGE * 100).toFixed(0))}%)`);
|
|
527
|
+
}
|
|
528
|
+
if (evidenceBindingRate < MIN_EVIDENCE_BINDING_RATE) {
|
|
529
|
+
reasons.push(`evidence_binding_rate_below_threshold(${evidenceBindingRatePct}% < ${Number((MIN_EVIDENCE_BINDING_RATE * 100).toFixed(0))}%)`);
|
|
530
|
+
}
|
|
531
|
+
if (citationErrorRate >= MAX_CITATION_ERROR_RATE) {
|
|
532
|
+
reasons.push(`citation_error_rate_above_threshold(${citationErrorRatePct}% >= ${Number((MAX_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
|
|
533
|
+
}
|
|
534
|
+
if (downgradedHighConfidenceCount > 0) {
|
|
535
|
+
reasons.push(`high_confidence_downgraded(${downgradedHighConfidenceCount})`);
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
qualityGate: {
|
|
539
|
+
passed: reasons.length === 0,
|
|
540
|
+
fullTextCoveragePct,
|
|
541
|
+
evidenceBindingRatePct,
|
|
542
|
+
citationErrorRatePct,
|
|
543
|
+
reasons,
|
|
544
|
+
},
|
|
545
|
+
unreadCorePaperIds,
|
|
546
|
+
downgradedHighConfidenceCount,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function normalizeChange(input) {
|
|
550
|
+
const statement = normalizeText(input.statement ?? "");
|
|
551
|
+
if (!statement)
|
|
552
|
+
return undefined;
|
|
553
|
+
const type = ["NEW", "CONFIRM", "REVISE", "BRIDGE"].includes(input.type) ? input.type : "NEW";
|
|
554
|
+
const evidenceIds = Array.isArray(input.evidenceIds)
|
|
555
|
+
? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
|
|
556
|
+
: undefined;
|
|
557
|
+
return {
|
|
558
|
+
type,
|
|
559
|
+
statement,
|
|
560
|
+
...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
|
|
561
|
+
...(input.topic ? { topic: normalizeText(input.topic) } : {}),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function normalizeUpdate(input) {
|
|
565
|
+
const topic = normalizeText(input.topic ?? "");
|
|
566
|
+
const content = normalizeText(input.content ?? "");
|
|
567
|
+
if (!topic || !content)
|
|
568
|
+
return undefined;
|
|
569
|
+
const op = ["append", "revise", "confirm", "bridge"].includes(input.op) ? input.op : "append";
|
|
570
|
+
const evidenceIds = Array.isArray(input.evidenceIds)
|
|
571
|
+
? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
|
|
572
|
+
: undefined;
|
|
573
|
+
const confidence = input.confidence && ["low", "medium", "high"].includes(input.confidence) ? input.confidence : undefined;
|
|
574
|
+
return {
|
|
575
|
+
topic,
|
|
576
|
+
op,
|
|
577
|
+
content,
|
|
578
|
+
...(confidence ? { confidence } : {}),
|
|
579
|
+
...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function normalizeHypothesis(input) {
|
|
583
|
+
const statement = normalizeText(input.statement ?? "");
|
|
584
|
+
if (!statement)
|
|
585
|
+
return undefined;
|
|
586
|
+
const trigger = ["GAP", "BRIDGE", "TREND", "CONTRADICTION"].includes(input.trigger)
|
|
587
|
+
? input.trigger
|
|
588
|
+
: "TREND";
|
|
589
|
+
const dependencyPath = Array.isArray(input.dependencyPath)
|
|
590
|
+
? input.dependencyPath.map((step) => normalizeText(step)).filter((step) => step.length > 0)
|
|
591
|
+
: undefined;
|
|
592
|
+
const evidenceIds = Array.isArray(input.evidenceIds)
|
|
593
|
+
? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
|
|
594
|
+
: undefined;
|
|
595
|
+
const validationStatusRaw = input.validationStatus?.trim().toLowerCase();
|
|
596
|
+
const validationStatus = validationStatusRaw &&
|
|
597
|
+
["unchecked", "supporting", "conflicting", "openreview_related", "openreview_not_found"].includes(validationStatusRaw)
|
|
598
|
+
? validationStatusRaw
|
|
599
|
+
: undefined;
|
|
600
|
+
const validationEvidence = normalizeStringArray(input.validationEvidence);
|
|
601
|
+
const validationNotes = input.validationNotes ? normalizeText(input.validationNotes) : undefined;
|
|
602
|
+
const withScore = (value) => typeof value === "number" && Number.isFinite(value) ? Number(value.toFixed(2)) : undefined;
|
|
603
|
+
return {
|
|
604
|
+
...(input.id ? { id: sanitizeId(input.id) } : {}),
|
|
605
|
+
statement,
|
|
606
|
+
trigger,
|
|
607
|
+
...(dependencyPath && dependencyPath.length > 0 ? { dependencyPath } : {}),
|
|
608
|
+
...(typeof withScore(input.novelty) === "number" ? { novelty: withScore(input.novelty) } : {}),
|
|
609
|
+
...(typeof withScore(input.feasibility) === "number" ? { feasibility: withScore(input.feasibility) } : {}),
|
|
610
|
+
...(typeof withScore(input.impact) === "number" ? { impact: withScore(input.impact) } : {}),
|
|
611
|
+
...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
|
|
612
|
+
...(validationStatus ? { validationStatus } : {}),
|
|
613
|
+
...(validationNotes ? { validationNotes } : {}),
|
|
614
|
+
...(validationEvidence ? { validationEvidence } : {}),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
async function acquireLock(projectPath) {
|
|
618
|
+
const lockPath = getLockPath(projectPath);
|
|
619
|
+
const start = Date.now();
|
|
620
|
+
while (Date.now() - start < 8_000) {
|
|
621
|
+
try {
|
|
622
|
+
await writeFile(lockPath, String(process.pid), { flag: "wx" });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
throw new Error("knowledge_state lock timeout");
|
|
630
|
+
}
|
|
631
|
+
async function releaseLock(projectPath) {
|
|
632
|
+
const lockPath = getLockPath(projectPath);
|
|
633
|
+
await unlink(lockPath).catch(() => undefined);
|
|
634
|
+
}
|
|
635
|
+
function makeStreamKey(scope, topic, fallbackTopicKey) {
|
|
636
|
+
const normalizedScope = normalizeScope(scope);
|
|
637
|
+
const normalizedTopic = normalizeText(topic).toLowerCase();
|
|
638
|
+
const digest = createHash("sha1").update(`${normalizedScope}\n${normalizedTopic}`).digest("hex").slice(0, 20);
|
|
639
|
+
return fallbackTopicKey || digest;
|
|
640
|
+
}
|
|
641
|
+
function toSummary(stream) {
|
|
642
|
+
return {
|
|
643
|
+
projectId: stream.projectId,
|
|
644
|
+
streamKey: stream.topicKey,
|
|
645
|
+
totalRuns: stream.totalRuns,
|
|
646
|
+
totalHypotheses: stream.totalHypotheses,
|
|
647
|
+
knowledgeTopicsCount: stream.knowledgeTopics.length,
|
|
648
|
+
paperNotesCount: stream.paperNotes.length,
|
|
649
|
+
recentFullTextReadCount: stream.recentFullTextReadCount,
|
|
650
|
+
recentNotFullTextReadCount: stream.recentNotFullTextReadCount,
|
|
651
|
+
qualityGate: stream.lastQualityGate,
|
|
652
|
+
unreadCorePaperIds: stream.lastUnreadCorePaperIds,
|
|
653
|
+
recentPapers: stream.recentPapers,
|
|
654
|
+
...(stream.lastRunAtMs ? { lastRunAtMs: stream.lastRunAtMs } : {}),
|
|
655
|
+
...(stream.lastStatus ? { lastStatus: stream.lastStatus } : {}),
|
|
656
|
+
recentHypotheses: stream.recentHypotheses,
|
|
657
|
+
recentChangeStats: stream.recentChangeStats,
|
|
658
|
+
lastExplorationTrace: stream.lastExplorationTrace,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function countChangeStats(day, runId, changes) {
|
|
662
|
+
let newCount = 0;
|
|
663
|
+
let confirmCount = 0;
|
|
664
|
+
let reviseCount = 0;
|
|
665
|
+
let bridgeCount = 0;
|
|
666
|
+
for (const item of changes) {
|
|
667
|
+
if (item.type === "NEW")
|
|
668
|
+
newCount += 1;
|
|
669
|
+
else if (item.type === "CONFIRM")
|
|
670
|
+
confirmCount += 1;
|
|
671
|
+
else if (item.type === "REVISE")
|
|
672
|
+
reviseCount += 1;
|
|
673
|
+
else if (item.type === "BRIDGE")
|
|
674
|
+
bridgeCount += 1;
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
day,
|
|
678
|
+
runId,
|
|
679
|
+
newCount,
|
|
680
|
+
confirmCount,
|
|
681
|
+
reviseCount,
|
|
682
|
+
bridgeCount,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
export async function commitKnowledgeRun(input) {
|
|
686
|
+
const project = await resolveProjectContext({
|
|
687
|
+
projectId: input.projectId,
|
|
688
|
+
scope: input.scope,
|
|
689
|
+
topic: input.topic,
|
|
690
|
+
autoCreate: true,
|
|
691
|
+
});
|
|
692
|
+
await ensureLayout(project.projectPath);
|
|
693
|
+
await acquireLock(project.projectPath);
|
|
694
|
+
try {
|
|
695
|
+
const root = await loadState(project.projectPath);
|
|
696
|
+
const nowMs = Date.now();
|
|
697
|
+
const nowIso = new Date(nowMs).toISOString();
|
|
698
|
+
const dayKey = dayKeyFromTimestamp(nowMs);
|
|
699
|
+
const knowledgeState = input.knowledgeState ?? {};
|
|
700
|
+
const corePapersFromState = (knowledgeState.corePapers ?? [])
|
|
701
|
+
.filter((item) => item && typeof item === "object")
|
|
702
|
+
.map((item) => withReadMark(item, "Core paper was recorded without full-text evidence."));
|
|
703
|
+
const explorationPapers = (knowledgeState.explorationPapers ?? [])
|
|
704
|
+
.filter((item) => item && typeof item === "object")
|
|
705
|
+
.map((item) => withReadMark(item, "Exploration paper not fully read in this run."));
|
|
706
|
+
const explorationTrace = (knowledgeState.explorationTrace ?? [])
|
|
707
|
+
.map(normalizeTrace)
|
|
708
|
+
.filter((item) => Boolean(item));
|
|
709
|
+
const knowledgeChanges = (knowledgeState.knowledgeChanges ?? [])
|
|
710
|
+
.map(normalizeChange)
|
|
711
|
+
.filter((item) => Boolean(item));
|
|
712
|
+
const knowledgeUpdates = (knowledgeState.knowledgeUpdates ?? [])
|
|
713
|
+
.map(normalizeUpdate)
|
|
714
|
+
.filter((item) => Boolean(item));
|
|
715
|
+
const hypotheses = (knowledgeState.hypotheses ?? [])
|
|
716
|
+
.map(normalizeHypothesis)
|
|
717
|
+
.filter((item) => Boolean(item));
|
|
718
|
+
const selectedPapers = (input.papers ?? [])
|
|
719
|
+
.filter((paper) => paper && typeof paper === "object")
|
|
720
|
+
.map((paper) => withReadMark({
|
|
721
|
+
id: paper.id,
|
|
722
|
+
title: paper.title,
|
|
723
|
+
url: paper.url,
|
|
724
|
+
score: paper.score,
|
|
725
|
+
reason: paper.reason,
|
|
726
|
+
summary: paper.reason,
|
|
727
|
+
}, "Paper selected from ranking payload without full-text-read evidence."));
|
|
728
|
+
const corePapers = mergePapers(selectedPapers, corePapersFromState).map((paper) => withReadMark(paper, "Core paper missing explicit full-text-read evidence."));
|
|
729
|
+
const streamKey = makeStreamKey(input.scope, input.topic, input.topicKey);
|
|
730
|
+
const stream = root.streams[streamKey] ??
|
|
731
|
+
{
|
|
732
|
+
scope: normalizeScope(input.scope),
|
|
733
|
+
topic: normalizeText(input.topic),
|
|
734
|
+
topicKey: streamKey,
|
|
735
|
+
projectId: project.projectId,
|
|
736
|
+
totalRuns: 0,
|
|
737
|
+
totalHypotheses: 0,
|
|
738
|
+
knowledgeTopics: [],
|
|
739
|
+
paperNotes: [],
|
|
740
|
+
recentFullTextReadCount: 0,
|
|
741
|
+
recentNotFullTextReadCount: 0,
|
|
742
|
+
lastQualityGate: defaultQualityGateState(),
|
|
743
|
+
lastUnreadCorePaperIds: [],
|
|
744
|
+
recentPapers: [],
|
|
745
|
+
recentRunIds: [],
|
|
746
|
+
recentHypothesisIds: [],
|
|
747
|
+
recentHypotheses: [],
|
|
748
|
+
recentChangeStats: [],
|
|
749
|
+
lastExplorationTrace: [],
|
|
750
|
+
};
|
|
751
|
+
const paperIds = mergePapers(corePapers, explorationPapers)
|
|
752
|
+
.map((paper) => paper.id || paper.url || paper.title || "")
|
|
753
|
+
.map((value) => normalizeText(value))
|
|
754
|
+
.filter((value) => value.length > 0);
|
|
755
|
+
const runId = input.runId?.trim()
|
|
756
|
+
? sanitizeId(input.runId)
|
|
757
|
+
: buildRunFingerprint({
|
|
758
|
+
scope: stream.scope,
|
|
759
|
+
topic: stream.topic,
|
|
760
|
+
status: input.status,
|
|
761
|
+
day: dayKey,
|
|
762
|
+
paperIds,
|
|
763
|
+
note: input.note,
|
|
764
|
+
});
|
|
765
|
+
if (stream.recentRunIds.includes(runId)) {
|
|
766
|
+
root.streams[streamKey] = stream;
|
|
767
|
+
return {
|
|
768
|
+
projectId: project.projectId,
|
|
769
|
+
streamKey,
|
|
770
|
+
summary: toSummary(stream),
|
|
771
|
+
runId,
|
|
772
|
+
createdProject: project.created,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const rootPath = getKnowledgeStateRoot(project.projectPath);
|
|
776
|
+
const logDir = path.join(rootPath, "logs");
|
|
777
|
+
const dailyDir = path.join(rootPath, "daily_changes");
|
|
778
|
+
const knowledgeDir = path.join(rootPath, "knowledge");
|
|
779
|
+
const paperNotesDir = path.join(rootPath, "paper_notes");
|
|
780
|
+
const hypothesesDir = path.join(rootPath, "hypotheses");
|
|
781
|
+
await appendMarkdown(path.join(logDir, `day-${dayKey}-ingest.md`), renderIngestLogMarkdown({ now: nowIso, runId, scope: stream.scope, topic: stream.topic, papers: corePapers }));
|
|
782
|
+
await appendMarkdown(path.join(logDir, `day-${dayKey}-exploration.md`), renderExplorationLogMarkdown({
|
|
783
|
+
now: nowIso,
|
|
784
|
+
runId,
|
|
785
|
+
trace: explorationTrace,
|
|
786
|
+
papers: explorationPapers,
|
|
787
|
+
}));
|
|
788
|
+
await appendMarkdown(path.join(dailyDir, `day-${dayKey}.md`), renderDailyChangesMarkdown({ now: nowIso, runId, topic: stream.topic, changes: knowledgeChanges }));
|
|
789
|
+
const mergedRunPapers = mergePapers(corePapers, explorationPapers);
|
|
790
|
+
const qualityEval = applyQualityGates({
|
|
791
|
+
corePapers,
|
|
792
|
+
allRunPapers: mergedRunPapers,
|
|
793
|
+
knowledgeChanges,
|
|
794
|
+
knowledgeUpdates,
|
|
795
|
+
hypotheses,
|
|
796
|
+
});
|
|
797
|
+
const requestedStatus = normalizeText(input.status ?? "ok");
|
|
798
|
+
const qualitySensitiveStatus = requestedStatus === "ok" || requestedStatus === "fallback_representative";
|
|
799
|
+
const effectiveStatus = qualitySensitiveStatus && !qualityEval.qualityGate.passed ? "degraded_quality" : requestedStatus;
|
|
800
|
+
const topicToUpdates = new Map();
|
|
801
|
+
for (const update of knowledgeUpdates) {
|
|
802
|
+
const key = slugifyTopic(update.topic);
|
|
803
|
+
const list = topicToUpdates.get(key) ?? [];
|
|
804
|
+
list.push(update);
|
|
805
|
+
topicToUpdates.set(key, list);
|
|
806
|
+
}
|
|
807
|
+
for (const [topicSlug, updates] of topicToUpdates.entries()) {
|
|
808
|
+
const topicFile = `topic-${topicSlug}.md`;
|
|
809
|
+
const topicPath = path.join(knowledgeDir, topicFile);
|
|
810
|
+
if (!existsSync(topicPath)) {
|
|
811
|
+
await writeFile(topicPath, `# Topic: ${topicSlug}\n\n`, "utf-8");
|
|
812
|
+
}
|
|
813
|
+
await appendMarkdown(topicPath, renderTopicUpdateMarkdown({ now: nowIso, runId, updates }));
|
|
814
|
+
if (!stream.knowledgeTopics.includes(topicFile)) {
|
|
815
|
+
stream.knowledgeTopics.push(topicFile);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const coreKeys = new Set(corePapers.map((paper) => paperIdentity(paper)).filter((item) => item.length > 0));
|
|
819
|
+
const runPaperNoteFiles = [];
|
|
820
|
+
for (const paper of mergedRunPapers) {
|
|
821
|
+
const noteFile = `paper-${toPaperNoteSlug(paper)}.md`;
|
|
822
|
+
const notePath = path.join(paperNotesDir, noteFile);
|
|
823
|
+
const role = coreKeys.has(paperIdentity(paper)) ? "core" : "exploration";
|
|
824
|
+
if (!existsSync(notePath)) {
|
|
825
|
+
await writeFile(notePath, `${renderPaperNoteHeaderMarkdown({ paper, file: noteFile })}\n`, "utf-8");
|
|
826
|
+
}
|
|
827
|
+
await appendMarkdown(notePath, renderPaperNoteRunMarkdown({
|
|
828
|
+
now: nowIso,
|
|
829
|
+
runId,
|
|
830
|
+
role,
|
|
831
|
+
paper,
|
|
832
|
+
}));
|
|
833
|
+
runPaperNoteFiles.push(noteFile);
|
|
834
|
+
}
|
|
835
|
+
stream.paperNotes = [...new Set([...runPaperNoteFiles, ...stream.paperNotes])].slice(0, MAX_PAPER_NOTES);
|
|
836
|
+
const recentHypothesisSummaries = [];
|
|
837
|
+
let seq = stream.totalHypotheses;
|
|
838
|
+
const dayToken = dayKey.replace(/-/g, "");
|
|
839
|
+
for (const hypothesis of hypotheses) {
|
|
840
|
+
seq += 1;
|
|
841
|
+
const hypothesisId = hypothesis.id && hypothesis.id.length > 0 ? sanitizeId(hypothesis.id) : `hyp-${dayToken}-${String(seq).padStart(4, "0")}`;
|
|
842
|
+
const file = `${hypothesisId}.md`;
|
|
843
|
+
await writeFile(path.join(hypothesesDir, file), renderHypothesisMarkdown({ now: nowIso, hypothesisId, runId, hypothesis }), "utf-8");
|
|
844
|
+
recentHypothesisSummaries.push({
|
|
845
|
+
id: hypothesisId,
|
|
846
|
+
statement: hypothesis.statement,
|
|
847
|
+
trigger: hypothesis.trigger,
|
|
848
|
+
createdAtMs: nowMs,
|
|
849
|
+
file,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const fullTextStats = countFullTextStats(mergedRunPapers);
|
|
853
|
+
const structuredProfileCount = mergedRunPapers.filter(hasStructuredProfile).length;
|
|
854
|
+
await writeFile(path.join(knowledgeDir, "_index.md"), renderKnowledgeIndexMarkdown({
|
|
855
|
+
now: nowIso,
|
|
856
|
+
topic: stream.topic,
|
|
857
|
+
topicFiles: stream.knowledgeTopics,
|
|
858
|
+
paperNotesCount: stream.paperNotes.length,
|
|
859
|
+
totalHypotheses: stream.totalHypotheses + recentHypothesisSummaries.length,
|
|
860
|
+
recentPapers: mergedRunPapers,
|
|
861
|
+
fullTextReadCount: fullTextStats.fullTextReadCount,
|
|
862
|
+
notFullTextReadCount: fullTextStats.notFullTextReadCount,
|
|
863
|
+
qualityGate: qualityEval.qualityGate,
|
|
864
|
+
unreadCorePaperIds: qualityEval.unreadCorePaperIds,
|
|
865
|
+
lastStatus: effectiveStatus,
|
|
866
|
+
}), "utf-8");
|
|
867
|
+
const changeStat = countChangeStats(dayKey, runId, knowledgeChanges);
|
|
868
|
+
stream.projectId = project.projectId;
|
|
869
|
+
stream.totalRuns += 1;
|
|
870
|
+
stream.totalHypotheses += recentHypothesisSummaries.length;
|
|
871
|
+
stream.lastRunAtMs = nowMs;
|
|
872
|
+
stream.lastStatus = effectiveStatus;
|
|
873
|
+
stream.recentFullTextReadCount = fullTextStats.fullTextReadCount;
|
|
874
|
+
stream.recentNotFullTextReadCount = fullTextStats.notFullTextReadCount;
|
|
875
|
+
stream.lastQualityGate = qualityEval.qualityGate;
|
|
876
|
+
stream.lastUnreadCorePaperIds = qualityEval.unreadCorePaperIds;
|
|
877
|
+
stream.lastExplorationTrace = explorationTrace.slice(0, MAX_LAST_TRACE);
|
|
878
|
+
stream.recentPapers = mergePapers(mergedRunPapers, stream.recentPapers).slice(0, MAX_RECENT_PAPERS);
|
|
879
|
+
stream.recentRunIds = [runId, ...stream.recentRunIds.filter((id) => id !== runId)].slice(0, MAX_RECENT_RUN_IDS);
|
|
880
|
+
stream.recentHypothesisIds = [
|
|
881
|
+
...recentHypothesisSummaries.map((item) => item.id),
|
|
882
|
+
...stream.recentHypothesisIds,
|
|
883
|
+
].slice(0, MAX_RECENT_HYPOTHESES);
|
|
884
|
+
stream.recentHypotheses = [...recentHypothesisSummaries, ...stream.recentHypotheses].slice(0, MAX_RECENT_HYPOTHESES);
|
|
885
|
+
stream.recentChangeStats = [changeStat, ...stream.recentChangeStats].slice(0, MAX_RECENT_CHANGE_STATS);
|
|
886
|
+
root.streams[streamKey] = stream;
|
|
887
|
+
await saveStateAtomic(project.projectPath, root);
|
|
888
|
+
await appendFile(path.join(logDir, `day-${dayKey}-run-details.jsonl`), `${JSON.stringify({
|
|
889
|
+
ts: nowMs,
|
|
890
|
+
runId,
|
|
891
|
+
scope: stream.scope,
|
|
892
|
+
topic: stream.topic,
|
|
893
|
+
streamKey,
|
|
894
|
+
status: effectiveStatus,
|
|
895
|
+
corePapers,
|
|
896
|
+
explorationPapers,
|
|
897
|
+
explorationTrace,
|
|
898
|
+
knowledgeChanges,
|
|
899
|
+
knowledgeUpdates,
|
|
900
|
+
hypotheses,
|
|
901
|
+
paperNoteFiles: runPaperNoteFiles,
|
|
902
|
+
quality: {
|
|
903
|
+
fullTextReadCount: fullTextStats.fullTextReadCount,
|
|
904
|
+
notFullTextReadCount: fullTextStats.notFullTextReadCount,
|
|
905
|
+
paperNotesCount: stream.paperNotes.length,
|
|
906
|
+
structuredProfileCount,
|
|
907
|
+
qualityGate: qualityEval.qualityGate,
|
|
908
|
+
unreadCorePaperIds: qualityEval.unreadCorePaperIds,
|
|
909
|
+
downgradedHighConfidenceCount: qualityEval.downgradedHighConfidenceCount,
|
|
910
|
+
},
|
|
911
|
+
runLog: input.knowledgeState?.runLog ?? null,
|
|
912
|
+
note: input.note ?? null,
|
|
913
|
+
})}\n`, "utf-8");
|
|
914
|
+
await appendEvent(project.projectPath, {
|
|
915
|
+
ts: nowMs,
|
|
916
|
+
runId,
|
|
917
|
+
scope: stream.scope,
|
|
918
|
+
topic: stream.topic,
|
|
919
|
+
streamKey,
|
|
920
|
+
projectId: project.projectId,
|
|
921
|
+
status: effectiveStatus,
|
|
922
|
+
paperCount: corePapers.length,
|
|
923
|
+
explorationPaperCount: explorationPapers.length,
|
|
924
|
+
fullTextReadCount: fullTextStats.fullTextReadCount,
|
|
925
|
+
notFullTextReadCount: fullTextStats.notFullTextReadCount,
|
|
926
|
+
paperNotesCount: stream.paperNotes.length,
|
|
927
|
+
paperNoteFiles: runPaperNoteFiles,
|
|
928
|
+
structuredProfileCount,
|
|
929
|
+
qualityGate: qualityEval.qualityGate,
|
|
930
|
+
unreadCorePaperIds: qualityEval.unreadCorePaperIds,
|
|
931
|
+
downgradedHighConfidenceCount: qualityEval.downgradedHighConfidenceCount,
|
|
932
|
+
changeCount: knowledgeChanges.length,
|
|
933
|
+
hypothesisCount: recentHypothesisSummaries.length,
|
|
934
|
+
corePapers,
|
|
935
|
+
explorationPapers,
|
|
936
|
+
note: input.note,
|
|
937
|
+
runLog: input.knowledgeState?.runLog ?? null,
|
|
938
|
+
});
|
|
939
|
+
return {
|
|
940
|
+
projectId: project.projectId,
|
|
941
|
+
streamKey,
|
|
942
|
+
summary: toSummary(stream),
|
|
943
|
+
runId,
|
|
944
|
+
createdProject: project.created,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
finally {
|
|
948
|
+
await releaseLock(project.projectPath);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
export async function readKnowledgeSummary(args) {
|
|
952
|
+
let project;
|
|
953
|
+
try {
|
|
954
|
+
project = await resolveProjectContext({
|
|
955
|
+
projectId: args.projectId,
|
|
956
|
+
scope: args.scope,
|
|
957
|
+
topic: args.topic,
|
|
958
|
+
autoCreate: false,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
const statePath = getStatePath(project.projectPath);
|
|
965
|
+
if (!existsSync(statePath))
|
|
966
|
+
return undefined;
|
|
967
|
+
const root = await loadState(project.projectPath);
|
|
968
|
+
const streamKey = makeStreamKey(args.scope, args.topic, args.topicKey);
|
|
969
|
+
const stream = root.streams[streamKey];
|
|
970
|
+
if (!stream)
|
|
971
|
+
return undefined;
|
|
972
|
+
return {
|
|
973
|
+
projectId: project.projectId,
|
|
974
|
+
streamKey,
|
|
975
|
+
summary: toSummary(stream),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
//# sourceMappingURL=store.js.map
|