scientify 1.12.1 → 1.13.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.
Files changed (59) hide show
  1. package/README.md +6 -2
  2. package/README.zh.md +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -5
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/cli/research.d.ts +1 -1
  7. package/dist/src/cli/research.d.ts.map +1 -1
  8. package/dist/src/cli/research.js +123 -227
  9. package/dist/src/cli/research.js.map +1 -1
  10. package/dist/src/commands/metabolism-status.d.ts +2 -2
  11. package/dist/src/commands/metabolism-status.d.ts.map +1 -1
  12. package/dist/src/commands/metabolism-status.js +75 -72
  13. package/dist/src/commands/metabolism-status.js.map +1 -1
  14. package/dist/src/commands.d.ts.map +1 -1
  15. package/dist/src/commands.js +55 -0
  16. package/dist/src/commands.js.map +1 -1
  17. package/dist/src/hooks/research-mode.d.ts.map +1 -1
  18. package/dist/src/hooks/research-mode.js +55 -37
  19. package/dist/src/hooks/research-mode.js.map +1 -1
  20. package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
  21. package/dist/src/hooks/scientify-signature.js +5 -2
  22. package/dist/src/hooks/scientify-signature.js.map +1 -1
  23. package/dist/src/knowledge-state/render.d.ts +1 -0
  24. package/dist/src/knowledge-state/render.d.ts.map +1 -1
  25. package/dist/src/knowledge-state/render.js +101 -33
  26. package/dist/src/knowledge-state/render.js.map +1 -1
  27. package/dist/src/knowledge-state/store.d.ts.map +1 -1
  28. package/dist/src/knowledge-state/store.js +308 -66
  29. package/dist/src/knowledge-state/store.js.map +1 -1
  30. package/dist/src/knowledge-state/types.d.ts +40 -0
  31. package/dist/src/knowledge-state/types.d.ts.map +1 -1
  32. package/dist/src/literature/subscription-state.d.ts.map +1 -1
  33. package/dist/src/literature/subscription-state.js +949 -7
  34. package/dist/src/literature/subscription-state.js.map +1 -1
  35. package/dist/src/research-subscriptions/constants.d.ts +1 -1
  36. package/dist/src/research-subscriptions/constants.js +1 -1
  37. package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
  38. package/dist/src/research-subscriptions/parse.js +10 -0
  39. package/dist/src/research-subscriptions/parse.js.map +1 -1
  40. package/dist/src/research-subscriptions/prompt.d.ts +1 -1
  41. package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
  42. package/dist/src/research-subscriptions/prompt.js +146 -221
  43. package/dist/src/research-subscriptions/prompt.js.map +1 -1
  44. package/dist/src/research-subscriptions/types.d.ts +1 -0
  45. package/dist/src/research-subscriptions/types.d.ts.map +1 -1
  46. package/dist/src/templates/bootstrap.d.ts.map +1 -1
  47. package/dist/src/templates/bootstrap.js +19 -32
  48. package/dist/src/templates/bootstrap.js.map +1 -1
  49. package/dist/src/tools/scientify-cron.d.ts +4 -2
  50. package/dist/src/tools/scientify-cron.d.ts.map +1 -1
  51. package/dist/src/tools/scientify-cron.js +369 -17
  52. package/dist/src/tools/scientify-cron.js.map +1 -1
  53. package/dist/src/tools/scientify-literature-state.d.ts +40 -0
  54. package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
  55. package/dist/src/tools/scientify-literature-state.js +215 -72
  56. package/dist/src/tools/scientify-literature-state.js.map +1 -1
  57. package/openclaw.plugin.json +2 -4
  58. package/package.json +1 -1
  59. package/skills/research-subscription/SKILL.md +11 -3
@@ -16,11 +16,30 @@ const MAX_HYPOTHESIS_REJECTION_REASONS = 24;
16
16
  const MIN_CORE_FULLTEXT_COVERAGE = 0.8;
17
17
  const MIN_EVIDENCE_BINDING_RATE = 0.9;
18
18
  const MAX_CITATION_ERROR_RATE = 0.02;
19
+ const FATAL_CITATION_ERROR_RATE = 0.2;
20
+ const MIN_FULLTEXT_PROFILE_COMPLETENESS = 0.55;
19
21
  const MIN_HYPOTHESIS_EVIDENCE = 2;
20
22
  const MIN_HYPOTHESIS_DEPENDENCY_STEPS = 2;
21
23
  const MIN_HYPOTHESIS_STATEMENT_CHARS = 48;
24
+ const PLACEHOLDER_TEXT_RE = /^(?:n\/a|na|none|not provided|not available|unknown|tbd|todo|null|nil|未提供|暂无|未知|无)$/iu;
25
+ function defaultRunProfile() {
26
+ return "strict";
27
+ }
28
+ function defaultTriggerState(nowMs = Date.now()) {
29
+ return {
30
+ consecutiveNewReviseDays: 0,
31
+ bridgeCount7d: 0,
32
+ unreadCoreBacklog: 0,
33
+ lastUpdatedAtMs: nowMs,
34
+ };
35
+ }
22
36
  function defaultQualityGateState() {
23
37
  return {
38
+ mode: "soft",
39
+ severity: "warn",
40
+ warnings: ["quality gate not evaluated"],
41
+ fatalReasons: [],
42
+ blocking: false,
24
43
  passed: false,
25
44
  fullTextCoveragePct: 0,
26
45
  evidenceBindingRatePct: 0,
@@ -38,6 +57,16 @@ function defaultHypothesisGateState() {
38
57
  function normalizeText(raw) {
39
58
  return raw.trim().replace(/\s+/g, " ");
40
59
  }
60
+ function cleanOptionalText(raw) {
61
+ if (typeof raw !== "string")
62
+ return undefined;
63
+ const normalized = normalizeText(raw);
64
+ if (!normalized)
65
+ return undefined;
66
+ if (PLACEHOLDER_TEXT_RE.test(normalized))
67
+ return undefined;
68
+ return normalized;
69
+ }
41
70
  function sanitizeId(raw) {
42
71
  return normalizeText(raw)
43
72
  .toLowerCase()
@@ -118,6 +147,9 @@ async function loadState(projectPath) {
118
147
  topic: normalizeText(rawStream.topic ?? "topic"),
119
148
  topicKey,
120
149
  projectId: sanitizeId(rawStream.projectId ?? "auto-topic-global-000000") || "auto-topic-global-000000",
150
+ lastRunProfile: rawStream.lastRunProfile === "strict" || rawStream.lastRunProfile === "fast"
151
+ ? rawStream.lastRunProfile
152
+ : defaultRunProfile(),
121
153
  totalRuns: typeof rawStream.totalRuns === "number" ? Math.max(0, Math.floor(rawStream.totalRuns)) : 0,
122
154
  totalHypotheses: typeof rawStream.totalHypotheses === "number" ? Math.max(0, Math.floor(rawStream.totalHypotheses)) : 0,
123
155
  knowledgeTopics: Array.isArray(rawStream.knowledgeTopics)
@@ -126,6 +158,26 @@ async function loadState(projectPath) {
126
158
  paperNotes: Array.isArray(rawStream.paperNotes)
127
159
  ? rawStream.paperNotes.filter((item) => typeof item === "string").map((item) => normalizeText(item))
128
160
  : [],
161
+ triggerState: rawStream.triggerState && typeof rawStream.triggerState === "object" && !Array.isArray(rawStream.triggerState)
162
+ ? {
163
+ consecutiveNewReviseDays: typeof rawStream.triggerState.consecutiveNewReviseDays === "number" &&
164
+ Number.isFinite(rawStream.triggerState.consecutiveNewReviseDays)
165
+ ? Math.max(0, Math.floor(rawStream.triggerState.consecutiveNewReviseDays))
166
+ : 0,
167
+ bridgeCount7d: typeof rawStream.triggerState.bridgeCount7d === "number" &&
168
+ Number.isFinite(rawStream.triggerState.bridgeCount7d)
169
+ ? Math.max(0, Math.floor(rawStream.triggerState.bridgeCount7d))
170
+ : 0,
171
+ unreadCoreBacklog: typeof rawStream.triggerState.unreadCoreBacklog === "number" &&
172
+ Number.isFinite(rawStream.triggerState.unreadCoreBacklog)
173
+ ? Math.max(0, Math.floor(rawStream.triggerState.unreadCoreBacklog))
174
+ : 0,
175
+ lastUpdatedAtMs: typeof rawStream.triggerState.lastUpdatedAtMs === "number" &&
176
+ Number.isFinite(rawStream.triggerState.lastUpdatedAtMs)
177
+ ? Math.floor(rawStream.triggerState.lastUpdatedAtMs)
178
+ : Date.now(),
179
+ }
180
+ : defaultTriggerState(),
129
181
  recentFullTextReadCount: typeof rawStream.recentFullTextReadCount === "number"
130
182
  ? Math.max(0, Math.floor(rawStream.recentFullTextReadCount))
131
183
  : 0,
@@ -135,27 +187,60 @@ async function loadState(projectPath) {
135
187
  lastQualityGate: rawStream.lastQualityGate &&
136
188
  typeof rawStream.lastQualityGate === "object" &&
137
189
  !Array.isArray(rawStream.lastQualityGate)
138
- ? {
139
- passed: rawStream.lastQualityGate.passed === true,
140
- fullTextCoveragePct: typeof rawStream.lastQualityGate.fullTextCoveragePct === "number" &&
141
- Number.isFinite(rawStream.lastQualityGate.fullTextCoveragePct)
142
- ? Number(rawStream.lastQualityGate.fullTextCoveragePct.toFixed(2))
143
- : 0,
144
- evidenceBindingRatePct: typeof rawStream.lastQualityGate.evidenceBindingRatePct === "number" &&
145
- Number.isFinite(rawStream.lastQualityGate.evidenceBindingRatePct)
146
- ? Number(rawStream.lastQualityGate.evidenceBindingRatePct.toFixed(2))
147
- : 0,
148
- citationErrorRatePct: typeof rawStream.lastQualityGate.citationErrorRatePct === "number" &&
149
- Number.isFinite(rawStream.lastQualityGate.citationErrorRatePct)
150
- ? Number(rawStream.lastQualityGate.citationErrorRatePct.toFixed(2))
151
- : 0,
152
- reasons: Array.isArray(rawStream.lastQualityGate.reasons)
190
+ ? (() => {
191
+ const reasons = Array.isArray(rawStream.lastQualityGate.reasons)
153
192
  ? rawStream.lastQualityGate.reasons
154
193
  .filter((item) => typeof item === "string")
155
194
  .map((item) => normalizeText(item))
156
195
  .filter((item) => item.length > 0)
157
- : [],
158
- }
196
+ : [];
197
+ const warnings = Array.isArray(rawStream.lastQualityGate.warnings)
198
+ ? rawStream.lastQualityGate.warnings
199
+ .filter((item) => typeof item === "string")
200
+ .map((item) => normalizeText(item))
201
+ .filter((item) => item.length > 0)
202
+ : reasons;
203
+ const fatalReasons = Array.isArray(rawStream.lastQualityGate.fatalReasons)
204
+ ? rawStream.lastQualityGate.fatalReasons
205
+ .filter((item) => typeof item === "string")
206
+ .map((item) => normalizeText(item))
207
+ .filter((item) => item.length > 0)
208
+ : [];
209
+ const blocking = rawStream.lastQualityGate.blocking === true || fatalReasons.length > 0;
210
+ const severityRaw = typeof rawStream.lastQualityGate.severity === "string"
211
+ ? rawStream.lastQualityGate.severity.toLowerCase()
212
+ : undefined;
213
+ const severity = severityRaw === "fatal" || blocking
214
+ ? "fatal"
215
+ : severityRaw === "ok"
216
+ ? "ok"
217
+ : warnings.length > 0
218
+ ? "warn"
219
+ : "ok";
220
+ return {
221
+ mode: "soft",
222
+ severity,
223
+ warnings,
224
+ fatalReasons,
225
+ blocking,
226
+ passed: typeof rawStream.lastQualityGate.passed === "boolean"
227
+ ? rawStream.lastQualityGate.passed
228
+ : fatalReasons.length === 0,
229
+ fullTextCoveragePct: typeof rawStream.lastQualityGate.fullTextCoveragePct === "number" &&
230
+ Number.isFinite(rawStream.lastQualityGate.fullTextCoveragePct)
231
+ ? Number(rawStream.lastQualityGate.fullTextCoveragePct.toFixed(2))
232
+ : 0,
233
+ evidenceBindingRatePct: typeof rawStream.lastQualityGate.evidenceBindingRatePct === "number" &&
234
+ Number.isFinite(rawStream.lastQualityGate.evidenceBindingRatePct)
235
+ ? Number(rawStream.lastQualityGate.evidenceBindingRatePct.toFixed(2))
236
+ : 0,
237
+ citationErrorRatePct: typeof rawStream.lastQualityGate.citationErrorRatePct === "number" &&
238
+ Number.isFinite(rawStream.lastQualityGate.citationErrorRatePct)
239
+ ? Number(rawStream.lastQualityGate.citationErrorRatePct.toFixed(2))
240
+ : 0,
241
+ reasons: reasons.length > 0 ? reasons : [...warnings, ...fatalReasons],
242
+ };
243
+ })()
159
244
  : defaultQualityGateState(),
160
245
  lastUnreadCorePaperIds: Array.isArray(rawStream.lastUnreadCorePaperIds)
161
246
  ? rawStream.lastUnreadCorePaperIds
@@ -253,8 +338,8 @@ function normalizeStringArray(raw) {
253
338
  return undefined;
254
339
  const values = raw
255
340
  .filter((item) => typeof item === "string")
256
- .map((item) => normalizeText(item))
257
- .filter((item) => item.length > 0);
341
+ .map((item) => cleanOptionalText(item))
342
+ .filter((item) => Boolean(item));
258
343
  return values.length > 0 ? values : undefined;
259
344
  }
260
345
  function normalizeEvidenceAnchors(raw) {
@@ -263,14 +348,14 @@ function normalizeEvidenceAnchors(raw) {
263
348
  const anchors = raw
264
349
  .filter((item) => !!item && typeof item === "object")
265
350
  .map((item) => {
266
- const claim = normalizeText(item.claim ?? "");
351
+ const claim = cleanOptionalText(item.claim);
267
352
  if (!claim)
268
353
  return undefined;
269
354
  return {
270
- ...(item.section ? { section: normalizeText(item.section) } : {}),
271
- ...(item.locator ? { locator: normalizeText(item.locator) } : {}),
355
+ ...(cleanOptionalText(item.section) ? { section: cleanOptionalText(item.section) } : {}),
356
+ ...(cleanOptionalText(item.locator) ? { locator: cleanOptionalText(item.locator) } : {}),
272
357
  claim,
273
- ...(item.quote ? { quote: normalizeText(item.quote) } : {}),
358
+ ...(cleanOptionalText(item.quote) ? { quote: cleanOptionalText(item.quote) } : {}),
274
359
  };
275
360
  })
276
361
  .filter((item) => Boolean(item));
@@ -288,7 +373,7 @@ function toPaperNoteSlug(paper) {
288
373
  }
289
374
  function normalizePaper(input) {
290
375
  const evidenceIds = Array.isArray(input.evidenceIds)
291
- ? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
376
+ ? input.evidenceIds.map((id) => cleanOptionalText(id)).filter((id) => Boolean(id))
292
377
  : undefined;
293
378
  const keyEvidenceSpans = normalizeStringArray(input.keyEvidenceSpans);
294
379
  const subdomains = normalizeStringArray(input.subdomains);
@@ -309,31 +394,33 @@ function normalizePaper(input) {
309
394
  : readStatus
310
395
  ? false
311
396
  : undefined;
312
- const unreadReason = input.unreadReason ? normalizeText(input.unreadReason) : undefined;
397
+ const unreadReason = cleanOptionalText(input.unreadReason);
313
398
  return {
314
- ...(input.id ? { id: normalizeText(input.id) } : {}),
315
- ...(input.title ? { title: normalizeText(input.title) } : {}),
316
- ...(input.url ? { url: normalizeText(input.url) } : {}),
317
- ...(input.source ? { source: normalizeText(input.source) } : {}),
318
- ...(input.publishedAt ? { publishedAt: normalizeText(input.publishedAt) } : {}),
399
+ ...(cleanOptionalText(input.id) ? { id: cleanOptionalText(input.id) } : {}),
400
+ ...(cleanOptionalText(input.title) ? { title: cleanOptionalText(input.title) } : {}),
401
+ ...(cleanOptionalText(input.url) ? { url: cleanOptionalText(input.url) } : {}),
402
+ ...(cleanOptionalText(input.source) ? { source: cleanOptionalText(input.source) } : {}),
403
+ ...(cleanOptionalText(input.publishedAt) ? { publishedAt: cleanOptionalText(input.publishedAt) } : {}),
319
404
  ...(typeof input.score === "number" && Number.isFinite(input.score)
320
405
  ? { score: Number(input.score.toFixed(2)) }
321
406
  : {}),
322
- ...(input.reason ? { reason: normalizeText(input.reason) } : {}),
323
- ...(input.summary ? { summary: normalizeText(input.summary) } : {}),
407
+ ...(cleanOptionalText(input.reason) ? { reason: cleanOptionalText(input.reason) } : {}),
408
+ ...(cleanOptionalText(input.summary) ? { summary: cleanOptionalText(input.summary) } : {}),
324
409
  ...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
325
410
  ...(typeof fullTextRead === "boolean" ? { fullTextRead } : {}),
326
411
  ...(readStatus ? { readStatus } : {}),
327
- ...(input.fullTextSource ? { fullTextSource: normalizeText(input.fullTextSource) } : {}),
328
- ...(input.fullTextRef ? { fullTextRef: normalizeText(input.fullTextRef) } : {}),
412
+ ...(cleanOptionalText(input.fullTextSource) ? { fullTextSource: cleanOptionalText(input.fullTextSource) } : {}),
413
+ ...(cleanOptionalText(input.fullTextRef) ? { fullTextRef: cleanOptionalText(input.fullTextRef) } : {}),
329
414
  ...(unreadReason ? { unreadReason } : {}),
330
415
  ...(keyEvidenceSpans && keyEvidenceSpans.length > 0 ? { keyEvidenceSpans } : {}),
331
- ...(input.domain ? { domain: normalizeText(input.domain) } : {}),
416
+ ...(cleanOptionalText(input.domain) ? { domain: cleanOptionalText(input.domain) } : {}),
332
417
  ...(subdomains ? { subdomains } : {}),
333
418
  ...(crossDomainLinks ? { crossDomainLinks } : {}),
334
- ...(input.researchGoal ? { researchGoal: normalizeText(input.researchGoal) } : {}),
335
- ...(input.approach ? { approach: normalizeText(input.approach) } : {}),
336
- ...(input.methodologyDesign ? { methodologyDesign: normalizeText(input.methodologyDesign) } : {}),
419
+ ...(cleanOptionalText(input.researchGoal) ? { researchGoal: cleanOptionalText(input.researchGoal) } : {}),
420
+ ...(cleanOptionalText(input.approach) ? { approach: cleanOptionalText(input.approach) } : {}),
421
+ ...(cleanOptionalText(input.methodologyDesign)
422
+ ? { methodologyDesign: cleanOptionalText(input.methodologyDesign) }
423
+ : {}),
337
424
  ...(keyContributions ? { keyContributions } : {}),
338
425
  ...(practicalInsights ? { practicalInsights } : {}),
339
426
  ...(mustUnderstandPoints ? { mustUnderstandPoints } : {}),
@@ -446,6 +533,32 @@ function hasStructuredProfile(paper) {
446
533
  (paper.limitations && paper.limitations.length > 0) ||
447
534
  (paper.evidenceAnchors && paper.evidenceAnchors.length > 0));
448
535
  }
536
+ function countStructuredProfileFields(paper) {
537
+ let count = 0;
538
+ if (paper.domain && paper.domain.trim())
539
+ count += 1;
540
+ if (paper.subdomains && paper.subdomains.length > 0)
541
+ count += 1;
542
+ if (paper.crossDomainLinks && paper.crossDomainLinks.length > 0)
543
+ count += 1;
544
+ if (paper.researchGoal && paper.researchGoal.trim())
545
+ count += 1;
546
+ if (paper.approach && paper.approach.trim())
547
+ count += 1;
548
+ if (paper.methodologyDesign && paper.methodologyDesign.trim())
549
+ count += 1;
550
+ if (paper.keyContributions && paper.keyContributions.length > 0)
551
+ count += 1;
552
+ if (paper.practicalInsights && paper.practicalInsights.length > 0)
553
+ count += 1;
554
+ if (paper.mustUnderstandPoints && paper.mustUnderstandPoints.length > 0)
555
+ count += 1;
556
+ if (paper.limitations && paper.limitations.length > 0)
557
+ count += 1;
558
+ if (paper.evidenceAnchors && paper.evidenceAnchors.length > 0)
559
+ count += 1;
560
+ return count;
561
+ }
449
562
  function isFullTextRead(paper) {
450
563
  return paper.fullTextRead === true || paper.readStatus === "fulltext";
451
564
  }
@@ -492,6 +605,13 @@ function applyQualityGates(args) {
492
605
  const fullTextCoreCount = corePapers.filter((paper) => isFullTextRead(paper)).length;
493
606
  const fullTextCoverage = coreCount > 0 ? fullTextCoreCount / coreCount : 0;
494
607
  const fullTextCoveragePct = Number((fullTextCoverage * 100).toFixed(2));
608
+ const fullTextCorePapers = corePapers.filter((paper) => isFullTextRead(paper));
609
+ const structuredFieldTotal = 11;
610
+ const avgFullTextProfileCompleteness = fullTextCorePapers.length > 0
611
+ ? fullTextCorePapers.reduce((sum, paper) => sum + countStructuredProfileFields(paper) / structuredFieldTotal, 0) /
612
+ fullTextCorePapers.length
613
+ : 0;
614
+ const avgFullTextProfileCompletenessPct = Number((avgFullTextProfileCompleteness * 100).toFixed(2));
495
615
  const unreadCorePaperIds = dedupeText(corePapers
496
616
  .filter((paper) => !isFullTextRead(paper))
497
617
  .map((paper) => paper.id?.trim() || paper.url?.trim() || paper.title?.trim() || "unknown-paper")).slice(0, 50);
@@ -566,42 +686,68 @@ function applyQualityGates(args) {
566
686
  downgradedHighConfidenceCount += 1;
567
687
  }
568
688
  }
569
- const reasons = [];
570
- if (typeof args.requiredCorePapers === "number" &&
571
- Number.isFinite(args.requiredCorePapers) &&
572
- args.requiredCorePapers > 0 &&
573
- coreCount < args.requiredCorePapers) {
574
- reasons.push(`core_paper_count_below_required(${coreCount} < ${Math.floor(args.requiredCorePapers)})`);
689
+ const warnings = [];
690
+ const fatalReasons = [];
691
+ if (!args.hasAuditableArtifacts && !args.hasRunError) {
692
+ fatalReasons.push("no_auditable_artifacts_without_run_error");
693
+ }
694
+ if (typeof args.requiredCorePapers === "number" && Number.isFinite(args.requiredCorePapers) && args.requiredCorePapers > 0) {
695
+ const requiredCore = Math.floor(args.requiredCorePapers);
696
+ if (coreCount === 0) {
697
+ fatalReasons.push(`core_paper_count_below_required(${coreCount} < ${requiredCore})`);
698
+ }
699
+ else if (coreCount < requiredCore) {
700
+ warnings.push(`core_paper_count_below_required(${coreCount} < ${requiredCore})`);
701
+ }
575
702
  }
576
703
  if (fullTextCoverage < MIN_CORE_FULLTEXT_COVERAGE) {
577
- reasons.push(`core_fulltext_coverage_below_threshold(${fullTextCoveragePct}% < ${Number((MIN_CORE_FULLTEXT_COVERAGE * 100).toFixed(0))}%)`);
704
+ warnings.push(`core_fulltext_coverage_below_threshold(${fullTextCoveragePct}% < ${Number((MIN_CORE_FULLTEXT_COVERAGE * 100).toFixed(0))}%)`);
705
+ }
706
+ if (fullTextCorePapers.length > 0 && avgFullTextProfileCompleteness < MIN_FULLTEXT_PROFILE_COMPLETENESS) {
707
+ warnings.push(`fulltext_profile_completeness_below_threshold(${avgFullTextProfileCompletenessPct}% < ${Number((MIN_FULLTEXT_PROFILE_COMPLETENESS * 100).toFixed(0))}%)`);
578
708
  }
579
709
  if (typeof args.requiredFullTextCoveragePct === "number" &&
580
710
  Number.isFinite(args.requiredFullTextCoveragePct) &&
581
711
  args.requiredFullTextCoveragePct > 0 &&
582
712
  fullTextCoveragePct < args.requiredFullTextCoveragePct) {
583
- reasons.push(`core_fulltext_coverage_below_required(${fullTextCoveragePct}% < ${Number(args.requiredFullTextCoveragePct.toFixed(2))}%)`);
713
+ warnings.push(`core_fulltext_coverage_below_required(${fullTextCoveragePct}% < ${Number(args.requiredFullTextCoveragePct.toFixed(2))}%)`);
584
714
  }
585
715
  if (evidenceBindingRate < MIN_EVIDENCE_BINDING_RATE) {
586
- reasons.push(`evidence_binding_rate_below_threshold(${evidenceBindingRatePct}% < ${Number((MIN_EVIDENCE_BINDING_RATE * 100).toFixed(0))}%)`);
716
+ warnings.push(`evidence_binding_rate_below_threshold(${evidenceBindingRatePct}% < ${Number((MIN_EVIDENCE_BINDING_RATE * 100).toFixed(0))}%)`);
587
717
  }
588
- if (citationErrorRate >= MAX_CITATION_ERROR_RATE) {
589
- reasons.push(`citation_error_rate_above_threshold(${citationErrorRatePct}% >= ${Number((MAX_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
718
+ if (citationErrorRate >= FATAL_CITATION_ERROR_RATE) {
719
+ fatalReasons.push(`citation_error_rate_above_threshold(${citationErrorRatePct}% >= ${Number((FATAL_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
720
+ }
721
+ else if (citationErrorRate >= MAX_CITATION_ERROR_RATE) {
722
+ warnings.push(`citation_error_rate_above_warning_threshold(${citationErrorRatePct}% >= ${Number((MAX_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
590
723
  }
591
724
  const bridgeChangeCount = args.knowledgeChanges.filter((item) => item.type === "BRIDGE").length;
725
+ const reviseCount = args.knowledgeChanges.filter((item) => item.type === "REVISE").length;
726
+ const confirmCount = args.knowledgeChanges.filter((item) => item.type === "CONFIRM").length;
592
727
  const executedReflectionCount = args.reflectionTasks.filter((task) => task.status === "executed").length;
593
728
  if (bridgeChangeCount > 0 && executedReflectionCount === 0) {
594
- reasons.push(`reflection_missing_for_bridge(bridge_count=${bridgeChangeCount})`);
729
+ warnings.push(`reflection_missing_for_bridge(bridge_count=${bridgeChangeCount})`);
730
+ }
731
+ if (reviseCount > 0 && confirmCount > 0 && executedReflectionCount === 0) {
732
+ warnings.push(`reflection_missing_for_conflict(revise_count=${reviseCount},confirm_count=${confirmCount})`);
595
733
  }
596
734
  if (args.hypothesisGate.rejected > 0 && args.hypothesisGate.accepted === 0 && args.hypotheses.length > 0) {
597
- reasons.push(`hypothesis_gate_rejected_all(${args.hypothesisGate.rejected})`);
735
+ warnings.push(`hypothesis_gate_rejected_all(${args.hypothesisGate.rejected})`);
598
736
  }
599
737
  if (downgradedHighConfidenceCount > 0) {
600
- reasons.push(`high_confidence_downgraded(${downgradedHighConfidenceCount})`);
738
+ warnings.push(`high_confidence_downgraded(${downgradedHighConfidenceCount})`);
601
739
  }
740
+ const reasons = [...warnings, ...fatalReasons];
741
+ const blocking = fatalReasons.length > 0;
742
+ const severity = blocking ? "fatal" : warnings.length > 0 ? "warn" : "ok";
602
743
  return {
603
744
  qualityGate: {
604
- passed: reasons.length === 0,
745
+ mode: "soft",
746
+ severity,
747
+ warnings,
748
+ fatalReasons,
749
+ blocking,
750
+ passed: !blocking,
605
751
  fullTextCoveragePct,
606
752
  evidenceBindingRatePct,
607
753
  citationErrorRatePct,
@@ -707,10 +853,12 @@ function toSummary(stream) {
707
853
  return {
708
854
  projectId: stream.projectId,
709
855
  streamKey: stream.topicKey,
856
+ runProfile: stream.lastRunProfile,
710
857
  totalRuns: stream.totalRuns,
711
858
  totalHypotheses: stream.totalHypotheses,
712
859
  knowledgeTopicsCount: stream.knowledgeTopics.length,
713
860
  paperNotesCount: stream.paperNotes.length,
861
+ triggerState: stream.triggerState,
714
862
  recentFullTextReadCount: stream.recentFullTextReadCount,
715
863
  recentNotFullTextReadCount: stream.recentNotFullTextReadCount,
716
864
  qualityGate: stream.lastQualityGate,
@@ -931,9 +1079,12 @@ function applyHypothesisGate(args) {
931
1079
  if (resolvedEvidence < evidenceIds.length) {
932
1080
  reasons.push(`unresolved_evidence_ids(${evidenceIds.length - resolvedEvidence})`);
933
1081
  }
934
- if (fullTextSupported === 0) {
1082
+ if (fullTextSupported === 0 && args.runProfile === "strict") {
935
1083
  reasons.push("no_fulltext_backed_evidence");
936
1084
  }
1085
+ if (resolvedEvidence === 0) {
1086
+ reasons.push("no_resolved_evidence");
1087
+ }
937
1088
  const dependencyPathLength = hypothesis.dependencyPath?.length ?? 0;
938
1089
  if (dependencyPathLength < MIN_HYPOTHESIS_DEPENDENCY_STEPS) {
939
1090
  reasons.push(`dependency_path_too_short(${dependencyPathLength}<${MIN_HYPOTHESIS_DEPENDENCY_STEPS})`);
@@ -972,6 +1123,51 @@ function applyHypothesisGate(args) {
972
1123
  },
973
1124
  };
974
1125
  }
1126
+ function toDayStartMs(day) {
1127
+ const ts = Date.parse(`${day}T00:00:00.000Z`);
1128
+ return Number.isFinite(ts) ? ts : undefined;
1129
+ }
1130
+ function deriveTriggerState(args) {
1131
+ const sorted = [...args.recentChangeStats].sort((a, b) => b.day.localeCompare(a.day));
1132
+ // consecutive days from latest day with NEW/REVISE > 0
1133
+ let consecutiveNewReviseDays = 0;
1134
+ let expectedDayMs;
1135
+ for (const item of sorted) {
1136
+ const dayMs = toDayStartMs(item.day);
1137
+ if (dayMs === undefined)
1138
+ continue;
1139
+ const hasSignal = item.newCount > 0 || item.reviseCount > 0;
1140
+ if (!hasSignal)
1141
+ break;
1142
+ if (expectedDayMs === undefined) {
1143
+ consecutiveNewReviseDays = 1;
1144
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1145
+ continue;
1146
+ }
1147
+ if (dayMs === expectedDayMs) {
1148
+ consecutiveNewReviseDays += 1;
1149
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1150
+ continue;
1151
+ }
1152
+ break;
1153
+ }
1154
+ const sevenDaysAgo = args.nowMs - 7 * 24 * 60 * 60 * 1000;
1155
+ let bridgeCount7d = 0;
1156
+ for (const item of sorted) {
1157
+ const dayMs = toDayStartMs(item.day);
1158
+ if (dayMs === undefined)
1159
+ continue;
1160
+ if (dayMs < sevenDaysAgo)
1161
+ continue;
1162
+ bridgeCount7d += Math.max(0, Math.floor(item.bridgeCount));
1163
+ }
1164
+ return {
1165
+ consecutiveNewReviseDays,
1166
+ bridgeCount7d,
1167
+ unreadCoreBacklog: Math.max(0, Math.floor(args.unreadCoreBacklog)),
1168
+ lastUpdatedAtMs: args.nowMs,
1169
+ };
1170
+ }
975
1171
  export async function commitKnowledgeRun(input) {
976
1172
  const project = await resolveProjectContext({
977
1173
  projectId: input.projectId,
@@ -1023,10 +1219,12 @@ export async function commitKnowledgeRun(input) {
1023
1219
  topic: normalizeText(input.topic),
1024
1220
  topicKey: streamKey,
1025
1221
  projectId: project.projectId,
1222
+ lastRunProfile: defaultRunProfile(),
1026
1223
  totalRuns: 0,
1027
1224
  totalHypotheses: 0,
1028
1225
  knowledgeTopics: [],
1029
1226
  paperNotes: [],
1227
+ triggerState: defaultTriggerState(),
1030
1228
  recentFullTextReadCount: 0,
1031
1229
  recentNotFullTextReadCount: 0,
1032
1230
  lastQualityGate: defaultQualityGateState(),
@@ -1044,9 +1242,9 @@ export async function commitKnowledgeRun(input) {
1044
1242
  .map((paper) => paper.id || paper.url || paper.title || "")
1045
1243
  .map((value) => normalizeText(value))
1046
1244
  .filter((value) => value.length > 0);
1047
- const runId = input.runId?.trim()
1048
- ? sanitizeId(input.runId)
1049
- : buildRunFingerprint({
1245
+ const explicitRunId = input.runId?.trim() ? sanitizeId(input.runId) : undefined;
1246
+ let runId = explicitRunId ??
1247
+ buildRunFingerprint({
1050
1248
  scope: stream.scope,
1051
1249
  topic: stream.topic,
1052
1250
  status: input.status,
@@ -1054,15 +1252,30 @@ export async function commitKnowledgeRun(input) {
1054
1252
  paperIds,
1055
1253
  note: input.note,
1056
1254
  });
1255
+ const inferredRunProfile = input.knowledgeState?.runLog?.runProfile === "strict" || input.knowledgeState?.runLog?.runProfile === "fast"
1256
+ ? input.knowledgeState.runLog.runProfile
1257
+ : input.knowledgeState?.runLog?.requiredCorePapers !== undefined ||
1258
+ input.knowledgeState?.runLog?.requiredFullTextCoveragePct !== undefined
1259
+ ? "strict"
1260
+ : defaultRunProfile();
1057
1261
  if (stream.recentRunIds.includes(runId)) {
1058
- root.streams[streamKey] = stream;
1059
- return {
1060
- projectId: project.projectId,
1061
- streamKey,
1062
- summary: toSummary(stream),
1063
- runId,
1064
- createdProject: project.created,
1065
- };
1262
+ // Preserve idempotency for fingerprint-based runs, but avoid silently dropping
1263
+ // valid new cron cycles that accidentally reuse an explicit run_id.
1264
+ if (!explicitRunId) {
1265
+ root.streams[streamKey] = stream;
1266
+ return {
1267
+ projectId: project.projectId,
1268
+ streamKey,
1269
+ summary: toSummary(stream),
1270
+ runId,
1271
+ createdProject: project.created,
1272
+ };
1273
+ }
1274
+ const collisionTag = createHash("sha1")
1275
+ .update(`${nowMs}\n${paperIds.join("|")}\n${input.status ?? ""}\n${input.note ?? ""}`)
1276
+ .digest("hex")
1277
+ .slice(0, 8);
1278
+ runId = `${runId}-r${collisionTag}`;
1066
1279
  }
1067
1280
  const rootPath = getKnowledgeStateRoot(project.projectPath);
1068
1281
  const logDir = path.join(rootPath, "logs");
@@ -1100,8 +1313,16 @@ export async function commitKnowledgeRun(input) {
1100
1313
  hypotheses: submittedHypotheses,
1101
1314
  allRunPapers: mergedRunPapers,
1102
1315
  knowledgeChanges,
1316
+ runProfile: inferredRunProfile,
1103
1317
  });
1104
1318
  const acceptedHypotheses = hypothesisEval.acceptedHypotheses;
1319
+ const runArtifactCount = corePapers.length +
1320
+ explorationPapers.length +
1321
+ explorationTrace.length +
1322
+ knowledgeChanges.length +
1323
+ knowledgeUpdates.length +
1324
+ submittedHypotheses.length;
1325
+ const hasRunError = Boolean(input.knowledgeState?.runLog?.error && normalizeText(input.knowledgeState.runLog.error).length > 0);
1105
1326
  const qualityEval = applyQualityGates({
1106
1327
  corePapers,
1107
1328
  allRunPapers: mergedRunPapers,
@@ -1113,10 +1334,19 @@ export async function commitKnowledgeRun(input) {
1113
1334
  hypothesisGate: hypothesisEval.gate,
1114
1335
  requiredCorePapers: input.knowledgeState?.runLog?.requiredCorePapers,
1115
1336
  requiredFullTextCoveragePct: input.knowledgeState?.runLog?.requiredFullTextCoveragePct,
1337
+ hasAuditableArtifacts: runArtifactCount > 0,
1338
+ hasRunError,
1116
1339
  });
1340
+ if (changeSanitization.droppedBridgeCount > 0) {
1341
+ qualityEval.qualityGate.warnings.push(`bridge_dropped_due_to_ungrounded_evidence(${changeSanitization.droppedBridgeCount})`);
1342
+ qualityEval.qualityGate.reasons.push(`bridge_dropped_due_to_ungrounded_evidence(${changeSanitization.droppedBridgeCount})`);
1343
+ if (qualityEval.qualityGate.severity === "ok") {
1344
+ qualityEval.qualityGate.severity = "warn";
1345
+ }
1346
+ }
1117
1347
  const requestedStatus = normalizeText(input.status ?? "ok");
1118
1348
  const qualitySensitiveStatus = requestedStatus === "ok" || requestedStatus === "fallback_representative";
1119
- const effectiveStatus = qualitySensitiveStatus && !qualityEval.qualityGate.passed ? "degraded_quality" : requestedStatus;
1349
+ const effectiveStatus = qualitySensitiveStatus && qualityEval.qualityGate.blocking ? "degraded_quality" : requestedStatus;
1120
1350
  const topicToUpdates = new Map();
1121
1351
  for (const update of knowledgeUpdates) {
1122
1352
  const key = slugifyTopic(update.topic);
@@ -1174,6 +1404,7 @@ export async function commitKnowledgeRun(input) {
1174
1404
  await writeFile(path.join(knowledgeDir, "_index.md"), renderKnowledgeIndexMarkdown({
1175
1405
  now: nowIso,
1176
1406
  topic: stream.topic,
1407
+ runProfile: inferredRunProfile,
1177
1408
  topicFiles: stream.knowledgeTopics,
1178
1409
  paperNotesCount: stream.paperNotes.length,
1179
1410
  totalHypotheses: stream.totalHypotheses + recentHypothesisSummaries.length,
@@ -1188,6 +1419,7 @@ export async function commitKnowledgeRun(input) {
1188
1419
  }), "utf-8");
1189
1420
  const changeStat = countChangeStats(dayKey, runId, knowledgeChanges);
1190
1421
  stream.projectId = project.projectId;
1422
+ stream.lastRunProfile = inferredRunProfile;
1191
1423
  stream.totalRuns += 1;
1192
1424
  stream.totalHypotheses += recentHypothesisSummaries.length;
1193
1425
  stream.lastRunAtMs = nowMs;
@@ -1207,11 +1439,18 @@ export async function commitKnowledgeRun(input) {
1207
1439
  ].slice(0, MAX_RECENT_HYPOTHESES);
1208
1440
  stream.recentHypotheses = [...recentHypothesisSummaries, ...stream.recentHypotheses].slice(0, MAX_RECENT_HYPOTHESES);
1209
1441
  stream.recentChangeStats = [changeStat, ...stream.recentChangeStats].slice(0, MAX_RECENT_CHANGE_STATS);
1442
+ stream.triggerState = deriveTriggerState({
1443
+ recentChangeStats: stream.recentChangeStats,
1444
+ unreadCoreBacklog: qualityEval.unreadCorePaperIds.length,
1445
+ nowMs,
1446
+ });
1210
1447
  root.streams[streamKey] = stream;
1211
1448
  await saveStateAtomic(project.projectPath, root);
1212
1449
  await appendFile(path.join(logDir, `day-${dayKey}-run-details.jsonl`), `${JSON.stringify({
1213
1450
  ts: nowMs,
1451
+ run_id: runId,
1214
1452
  runId,
1453
+ run_profile: inferredRunProfile,
1215
1454
  scope: stream.scope,
1216
1455
  topic: stream.topic,
1217
1456
  streamKey,
@@ -1242,7 +1481,9 @@ export async function commitKnowledgeRun(input) {
1242
1481
  })}\n`, "utf-8");
1243
1482
  await appendEvent(project.projectPath, {
1244
1483
  ts: nowMs,
1484
+ run_id: runId,
1245
1485
  runId,
1486
+ run_profile: inferredRunProfile,
1246
1487
  scope: stream.scope,
1247
1488
  topic: stream.topic,
1248
1489
  streamKey,
@@ -1264,6 +1505,7 @@ export async function commitKnowledgeRun(input) {
1264
1505
  hypothesisCount: recentHypothesisSummaries.length,
1265
1506
  submittedHypothesisCount: submittedHypotheses.length,
1266
1507
  hypothesisGate: hypothesisEval.gate,
1508
+ triggerState: stream.triggerState,
1267
1509
  reflectionTasks,
1268
1510
  corePapers,
1269
1511
  explorationPapers,