scientify 1.12.1 → 1.12.2

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 +3 -1
  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 +54 -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 +206 -33
  29. package/dist/src/knowledge-state/store.js.map +1 -1
  30. package/dist/src/knowledge-state/types.d.ts +12 -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 +579 -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 +142 -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 +8 -0
  54. package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
  55. package/dist/src/tools/scientify-literature-state.js +140 -71
  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 +7 -0
@@ -16,9 +16,22 @@ 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 MIN_FULLTEXT_PROFILE_COMPLETENESS = 0.55;
19
20
  const MIN_HYPOTHESIS_EVIDENCE = 2;
20
21
  const MIN_HYPOTHESIS_DEPENDENCY_STEPS = 2;
21
22
  const MIN_HYPOTHESIS_STATEMENT_CHARS = 48;
23
+ const PLACEHOLDER_TEXT_RE = /^(?:n\/a|na|none|not provided|not available|unknown|tbd|todo|null|nil|未提供|暂无|未知|无)$/iu;
24
+ function defaultRunProfile() {
25
+ return "strict";
26
+ }
27
+ function defaultTriggerState(nowMs = Date.now()) {
28
+ return {
29
+ consecutiveNewReviseDays: 0,
30
+ bridgeCount7d: 0,
31
+ unreadCoreBacklog: 0,
32
+ lastUpdatedAtMs: nowMs,
33
+ };
34
+ }
22
35
  function defaultQualityGateState() {
23
36
  return {
24
37
  passed: false,
@@ -38,6 +51,16 @@ function defaultHypothesisGateState() {
38
51
  function normalizeText(raw) {
39
52
  return raw.trim().replace(/\s+/g, " ");
40
53
  }
54
+ function cleanOptionalText(raw) {
55
+ if (typeof raw !== "string")
56
+ return undefined;
57
+ const normalized = normalizeText(raw);
58
+ if (!normalized)
59
+ return undefined;
60
+ if (PLACEHOLDER_TEXT_RE.test(normalized))
61
+ return undefined;
62
+ return normalized;
63
+ }
41
64
  function sanitizeId(raw) {
42
65
  return normalizeText(raw)
43
66
  .toLowerCase()
@@ -118,6 +141,9 @@ async function loadState(projectPath) {
118
141
  topic: normalizeText(rawStream.topic ?? "topic"),
119
142
  topicKey,
120
143
  projectId: sanitizeId(rawStream.projectId ?? "auto-topic-global-000000") || "auto-topic-global-000000",
144
+ lastRunProfile: rawStream.lastRunProfile === "strict" || rawStream.lastRunProfile === "fast"
145
+ ? rawStream.lastRunProfile
146
+ : defaultRunProfile(),
121
147
  totalRuns: typeof rawStream.totalRuns === "number" ? Math.max(0, Math.floor(rawStream.totalRuns)) : 0,
122
148
  totalHypotheses: typeof rawStream.totalHypotheses === "number" ? Math.max(0, Math.floor(rawStream.totalHypotheses)) : 0,
123
149
  knowledgeTopics: Array.isArray(rawStream.knowledgeTopics)
@@ -126,6 +152,26 @@ async function loadState(projectPath) {
126
152
  paperNotes: Array.isArray(rawStream.paperNotes)
127
153
  ? rawStream.paperNotes.filter((item) => typeof item === "string").map((item) => normalizeText(item))
128
154
  : [],
155
+ triggerState: rawStream.triggerState && typeof rawStream.triggerState === "object" && !Array.isArray(rawStream.triggerState)
156
+ ? {
157
+ consecutiveNewReviseDays: typeof rawStream.triggerState.consecutiveNewReviseDays === "number" &&
158
+ Number.isFinite(rawStream.triggerState.consecutiveNewReviseDays)
159
+ ? Math.max(0, Math.floor(rawStream.triggerState.consecutiveNewReviseDays))
160
+ : 0,
161
+ bridgeCount7d: typeof rawStream.triggerState.bridgeCount7d === "number" &&
162
+ Number.isFinite(rawStream.triggerState.bridgeCount7d)
163
+ ? Math.max(0, Math.floor(rawStream.triggerState.bridgeCount7d))
164
+ : 0,
165
+ unreadCoreBacklog: typeof rawStream.triggerState.unreadCoreBacklog === "number" &&
166
+ Number.isFinite(rawStream.triggerState.unreadCoreBacklog)
167
+ ? Math.max(0, Math.floor(rawStream.triggerState.unreadCoreBacklog))
168
+ : 0,
169
+ lastUpdatedAtMs: typeof rawStream.triggerState.lastUpdatedAtMs === "number" &&
170
+ Number.isFinite(rawStream.triggerState.lastUpdatedAtMs)
171
+ ? Math.floor(rawStream.triggerState.lastUpdatedAtMs)
172
+ : Date.now(),
173
+ }
174
+ : defaultTriggerState(),
129
175
  recentFullTextReadCount: typeof rawStream.recentFullTextReadCount === "number"
130
176
  ? Math.max(0, Math.floor(rawStream.recentFullTextReadCount))
131
177
  : 0,
@@ -253,8 +299,8 @@ function normalizeStringArray(raw) {
253
299
  return undefined;
254
300
  const values = raw
255
301
  .filter((item) => typeof item === "string")
256
- .map((item) => normalizeText(item))
257
- .filter((item) => item.length > 0);
302
+ .map((item) => cleanOptionalText(item))
303
+ .filter((item) => Boolean(item));
258
304
  return values.length > 0 ? values : undefined;
259
305
  }
260
306
  function normalizeEvidenceAnchors(raw) {
@@ -263,14 +309,14 @@ function normalizeEvidenceAnchors(raw) {
263
309
  const anchors = raw
264
310
  .filter((item) => !!item && typeof item === "object")
265
311
  .map((item) => {
266
- const claim = normalizeText(item.claim ?? "");
312
+ const claim = cleanOptionalText(item.claim);
267
313
  if (!claim)
268
314
  return undefined;
269
315
  return {
270
- ...(item.section ? { section: normalizeText(item.section) } : {}),
271
- ...(item.locator ? { locator: normalizeText(item.locator) } : {}),
316
+ ...(cleanOptionalText(item.section) ? { section: cleanOptionalText(item.section) } : {}),
317
+ ...(cleanOptionalText(item.locator) ? { locator: cleanOptionalText(item.locator) } : {}),
272
318
  claim,
273
- ...(item.quote ? { quote: normalizeText(item.quote) } : {}),
319
+ ...(cleanOptionalText(item.quote) ? { quote: cleanOptionalText(item.quote) } : {}),
274
320
  };
275
321
  })
276
322
  .filter((item) => Boolean(item));
@@ -288,7 +334,7 @@ function toPaperNoteSlug(paper) {
288
334
  }
289
335
  function normalizePaper(input) {
290
336
  const evidenceIds = Array.isArray(input.evidenceIds)
291
- ? input.evidenceIds.map((id) => normalizeText(id)).filter((id) => id.length > 0)
337
+ ? input.evidenceIds.map((id) => cleanOptionalText(id)).filter((id) => Boolean(id))
292
338
  : undefined;
293
339
  const keyEvidenceSpans = normalizeStringArray(input.keyEvidenceSpans);
294
340
  const subdomains = normalizeStringArray(input.subdomains);
@@ -309,31 +355,33 @@ function normalizePaper(input) {
309
355
  : readStatus
310
356
  ? false
311
357
  : undefined;
312
- const unreadReason = input.unreadReason ? normalizeText(input.unreadReason) : undefined;
358
+ const unreadReason = cleanOptionalText(input.unreadReason);
313
359
  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) } : {}),
360
+ ...(cleanOptionalText(input.id) ? { id: cleanOptionalText(input.id) } : {}),
361
+ ...(cleanOptionalText(input.title) ? { title: cleanOptionalText(input.title) } : {}),
362
+ ...(cleanOptionalText(input.url) ? { url: cleanOptionalText(input.url) } : {}),
363
+ ...(cleanOptionalText(input.source) ? { source: cleanOptionalText(input.source) } : {}),
364
+ ...(cleanOptionalText(input.publishedAt) ? { publishedAt: cleanOptionalText(input.publishedAt) } : {}),
319
365
  ...(typeof input.score === "number" && Number.isFinite(input.score)
320
366
  ? { score: Number(input.score.toFixed(2)) }
321
367
  : {}),
322
- ...(input.reason ? { reason: normalizeText(input.reason) } : {}),
323
- ...(input.summary ? { summary: normalizeText(input.summary) } : {}),
368
+ ...(cleanOptionalText(input.reason) ? { reason: cleanOptionalText(input.reason) } : {}),
369
+ ...(cleanOptionalText(input.summary) ? { summary: cleanOptionalText(input.summary) } : {}),
324
370
  ...(evidenceIds && evidenceIds.length > 0 ? { evidenceIds } : {}),
325
371
  ...(typeof fullTextRead === "boolean" ? { fullTextRead } : {}),
326
372
  ...(readStatus ? { readStatus } : {}),
327
- ...(input.fullTextSource ? { fullTextSource: normalizeText(input.fullTextSource) } : {}),
328
- ...(input.fullTextRef ? { fullTextRef: normalizeText(input.fullTextRef) } : {}),
373
+ ...(cleanOptionalText(input.fullTextSource) ? { fullTextSource: cleanOptionalText(input.fullTextSource) } : {}),
374
+ ...(cleanOptionalText(input.fullTextRef) ? { fullTextRef: cleanOptionalText(input.fullTextRef) } : {}),
329
375
  ...(unreadReason ? { unreadReason } : {}),
330
376
  ...(keyEvidenceSpans && keyEvidenceSpans.length > 0 ? { keyEvidenceSpans } : {}),
331
- ...(input.domain ? { domain: normalizeText(input.domain) } : {}),
377
+ ...(cleanOptionalText(input.domain) ? { domain: cleanOptionalText(input.domain) } : {}),
332
378
  ...(subdomains ? { subdomains } : {}),
333
379
  ...(crossDomainLinks ? { crossDomainLinks } : {}),
334
- ...(input.researchGoal ? { researchGoal: normalizeText(input.researchGoal) } : {}),
335
- ...(input.approach ? { approach: normalizeText(input.approach) } : {}),
336
- ...(input.methodologyDesign ? { methodologyDesign: normalizeText(input.methodologyDesign) } : {}),
380
+ ...(cleanOptionalText(input.researchGoal) ? { researchGoal: cleanOptionalText(input.researchGoal) } : {}),
381
+ ...(cleanOptionalText(input.approach) ? { approach: cleanOptionalText(input.approach) } : {}),
382
+ ...(cleanOptionalText(input.methodologyDesign)
383
+ ? { methodologyDesign: cleanOptionalText(input.methodologyDesign) }
384
+ : {}),
337
385
  ...(keyContributions ? { keyContributions } : {}),
338
386
  ...(practicalInsights ? { practicalInsights } : {}),
339
387
  ...(mustUnderstandPoints ? { mustUnderstandPoints } : {}),
@@ -446,6 +494,32 @@ function hasStructuredProfile(paper) {
446
494
  (paper.limitations && paper.limitations.length > 0) ||
447
495
  (paper.evidenceAnchors && paper.evidenceAnchors.length > 0));
448
496
  }
497
+ function countStructuredProfileFields(paper) {
498
+ let count = 0;
499
+ if (paper.domain && paper.domain.trim())
500
+ count += 1;
501
+ if (paper.subdomains && paper.subdomains.length > 0)
502
+ count += 1;
503
+ if (paper.crossDomainLinks && paper.crossDomainLinks.length > 0)
504
+ count += 1;
505
+ if (paper.researchGoal && paper.researchGoal.trim())
506
+ count += 1;
507
+ if (paper.approach && paper.approach.trim())
508
+ count += 1;
509
+ if (paper.methodologyDesign && paper.methodologyDesign.trim())
510
+ count += 1;
511
+ if (paper.keyContributions && paper.keyContributions.length > 0)
512
+ count += 1;
513
+ if (paper.practicalInsights && paper.practicalInsights.length > 0)
514
+ count += 1;
515
+ if (paper.mustUnderstandPoints && paper.mustUnderstandPoints.length > 0)
516
+ count += 1;
517
+ if (paper.limitations && paper.limitations.length > 0)
518
+ count += 1;
519
+ if (paper.evidenceAnchors && paper.evidenceAnchors.length > 0)
520
+ count += 1;
521
+ return count;
522
+ }
449
523
  function isFullTextRead(paper) {
450
524
  return paper.fullTextRead === true || paper.readStatus === "fulltext";
451
525
  }
@@ -492,6 +566,13 @@ function applyQualityGates(args) {
492
566
  const fullTextCoreCount = corePapers.filter((paper) => isFullTextRead(paper)).length;
493
567
  const fullTextCoverage = coreCount > 0 ? fullTextCoreCount / coreCount : 0;
494
568
  const fullTextCoveragePct = Number((fullTextCoverage * 100).toFixed(2));
569
+ const fullTextCorePapers = corePapers.filter((paper) => isFullTextRead(paper));
570
+ const structuredFieldTotal = 11;
571
+ const avgFullTextProfileCompleteness = fullTextCorePapers.length > 0
572
+ ? fullTextCorePapers.reduce((sum, paper) => sum + countStructuredProfileFields(paper) / structuredFieldTotal, 0) /
573
+ fullTextCorePapers.length
574
+ : 0;
575
+ const avgFullTextProfileCompletenessPct = Number((avgFullTextProfileCompleteness * 100).toFixed(2));
495
576
  const unreadCorePaperIds = dedupeText(corePapers
496
577
  .filter((paper) => !isFullTextRead(paper))
497
578
  .map((paper) => paper.id?.trim() || paper.url?.trim() || paper.title?.trim() || "unknown-paper")).slice(0, 50);
@@ -576,6 +657,9 @@ function applyQualityGates(args) {
576
657
  if (fullTextCoverage < MIN_CORE_FULLTEXT_COVERAGE) {
577
658
  reasons.push(`core_fulltext_coverage_below_threshold(${fullTextCoveragePct}% < ${Number((MIN_CORE_FULLTEXT_COVERAGE * 100).toFixed(0))}%)`);
578
659
  }
660
+ if (fullTextCorePapers.length > 0 && avgFullTextProfileCompleteness < MIN_FULLTEXT_PROFILE_COMPLETENESS) {
661
+ reasons.push(`fulltext_profile_completeness_below_threshold(${avgFullTextProfileCompletenessPct}% < ${Number((MIN_FULLTEXT_PROFILE_COMPLETENESS * 100).toFixed(0))}%)`);
662
+ }
579
663
  if (typeof args.requiredFullTextCoveragePct === "number" &&
580
664
  Number.isFinite(args.requiredFullTextCoveragePct) &&
581
665
  args.requiredFullTextCoveragePct > 0 &&
@@ -589,10 +673,15 @@ function applyQualityGates(args) {
589
673
  reasons.push(`citation_error_rate_above_threshold(${citationErrorRatePct}% >= ${Number((MAX_CITATION_ERROR_RATE * 100).toFixed(0))}%)`);
590
674
  }
591
675
  const bridgeChangeCount = args.knowledgeChanges.filter((item) => item.type === "BRIDGE").length;
676
+ const reviseCount = args.knowledgeChanges.filter((item) => item.type === "REVISE").length;
677
+ const confirmCount = args.knowledgeChanges.filter((item) => item.type === "CONFIRM").length;
592
678
  const executedReflectionCount = args.reflectionTasks.filter((task) => task.status === "executed").length;
593
679
  if (bridgeChangeCount > 0 && executedReflectionCount === 0) {
594
680
  reasons.push(`reflection_missing_for_bridge(bridge_count=${bridgeChangeCount})`);
595
681
  }
682
+ if (reviseCount > 0 && confirmCount > 0 && executedReflectionCount === 0) {
683
+ reasons.push(`reflection_missing_for_conflict(revise_count=${reviseCount},confirm_count=${confirmCount})`);
684
+ }
596
685
  if (args.hypothesisGate.rejected > 0 && args.hypothesisGate.accepted === 0 && args.hypotheses.length > 0) {
597
686
  reasons.push(`hypothesis_gate_rejected_all(${args.hypothesisGate.rejected})`);
598
687
  }
@@ -707,10 +796,12 @@ function toSummary(stream) {
707
796
  return {
708
797
  projectId: stream.projectId,
709
798
  streamKey: stream.topicKey,
799
+ runProfile: stream.lastRunProfile,
710
800
  totalRuns: stream.totalRuns,
711
801
  totalHypotheses: stream.totalHypotheses,
712
802
  knowledgeTopicsCount: stream.knowledgeTopics.length,
713
803
  paperNotesCount: stream.paperNotes.length,
804
+ triggerState: stream.triggerState,
714
805
  recentFullTextReadCount: stream.recentFullTextReadCount,
715
806
  recentNotFullTextReadCount: stream.recentNotFullTextReadCount,
716
807
  qualityGate: stream.lastQualityGate,
@@ -931,9 +1022,12 @@ function applyHypothesisGate(args) {
931
1022
  if (resolvedEvidence < evidenceIds.length) {
932
1023
  reasons.push(`unresolved_evidence_ids(${evidenceIds.length - resolvedEvidence})`);
933
1024
  }
934
- if (fullTextSupported === 0) {
1025
+ if (fullTextSupported === 0 && args.runProfile === "strict") {
935
1026
  reasons.push("no_fulltext_backed_evidence");
936
1027
  }
1028
+ if (resolvedEvidence === 0) {
1029
+ reasons.push("no_resolved_evidence");
1030
+ }
937
1031
  const dependencyPathLength = hypothesis.dependencyPath?.length ?? 0;
938
1032
  if (dependencyPathLength < MIN_HYPOTHESIS_DEPENDENCY_STEPS) {
939
1033
  reasons.push(`dependency_path_too_short(${dependencyPathLength}<${MIN_HYPOTHESIS_DEPENDENCY_STEPS})`);
@@ -972,6 +1066,51 @@ function applyHypothesisGate(args) {
972
1066
  },
973
1067
  };
974
1068
  }
1069
+ function toDayStartMs(day) {
1070
+ const ts = Date.parse(`${day}T00:00:00.000Z`);
1071
+ return Number.isFinite(ts) ? ts : undefined;
1072
+ }
1073
+ function deriveTriggerState(args) {
1074
+ const sorted = [...args.recentChangeStats].sort((a, b) => b.day.localeCompare(a.day));
1075
+ // consecutive days from latest day with NEW/REVISE > 0
1076
+ let consecutiveNewReviseDays = 0;
1077
+ let expectedDayMs;
1078
+ for (const item of sorted) {
1079
+ const dayMs = toDayStartMs(item.day);
1080
+ if (dayMs === undefined)
1081
+ continue;
1082
+ const hasSignal = item.newCount > 0 || item.reviseCount > 0;
1083
+ if (!hasSignal)
1084
+ break;
1085
+ if (expectedDayMs === undefined) {
1086
+ consecutiveNewReviseDays = 1;
1087
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1088
+ continue;
1089
+ }
1090
+ if (dayMs === expectedDayMs) {
1091
+ consecutiveNewReviseDays += 1;
1092
+ expectedDayMs = dayMs - 24 * 60 * 60 * 1000;
1093
+ continue;
1094
+ }
1095
+ break;
1096
+ }
1097
+ const sevenDaysAgo = args.nowMs - 7 * 24 * 60 * 60 * 1000;
1098
+ let bridgeCount7d = 0;
1099
+ for (const item of sorted) {
1100
+ const dayMs = toDayStartMs(item.day);
1101
+ if (dayMs === undefined)
1102
+ continue;
1103
+ if (dayMs < sevenDaysAgo)
1104
+ continue;
1105
+ bridgeCount7d += Math.max(0, Math.floor(item.bridgeCount));
1106
+ }
1107
+ return {
1108
+ consecutiveNewReviseDays,
1109
+ bridgeCount7d,
1110
+ unreadCoreBacklog: Math.max(0, Math.floor(args.unreadCoreBacklog)),
1111
+ lastUpdatedAtMs: args.nowMs,
1112
+ };
1113
+ }
975
1114
  export async function commitKnowledgeRun(input) {
976
1115
  const project = await resolveProjectContext({
977
1116
  projectId: input.projectId,
@@ -1023,10 +1162,12 @@ export async function commitKnowledgeRun(input) {
1023
1162
  topic: normalizeText(input.topic),
1024
1163
  topicKey: streamKey,
1025
1164
  projectId: project.projectId,
1165
+ lastRunProfile: defaultRunProfile(),
1026
1166
  totalRuns: 0,
1027
1167
  totalHypotheses: 0,
1028
1168
  knowledgeTopics: [],
1029
1169
  paperNotes: [],
1170
+ triggerState: defaultTriggerState(),
1030
1171
  recentFullTextReadCount: 0,
1031
1172
  recentNotFullTextReadCount: 0,
1032
1173
  lastQualityGate: defaultQualityGateState(),
@@ -1044,9 +1185,9 @@ export async function commitKnowledgeRun(input) {
1044
1185
  .map((paper) => paper.id || paper.url || paper.title || "")
1045
1186
  .map((value) => normalizeText(value))
1046
1187
  .filter((value) => value.length > 0);
1047
- const runId = input.runId?.trim()
1048
- ? sanitizeId(input.runId)
1049
- : buildRunFingerprint({
1188
+ const explicitRunId = input.runId?.trim() ? sanitizeId(input.runId) : undefined;
1189
+ let runId = explicitRunId ??
1190
+ buildRunFingerprint({
1050
1191
  scope: stream.scope,
1051
1192
  topic: stream.topic,
1052
1193
  status: input.status,
@@ -1054,15 +1195,30 @@ export async function commitKnowledgeRun(input) {
1054
1195
  paperIds,
1055
1196
  note: input.note,
1056
1197
  });
1198
+ const inferredRunProfile = input.knowledgeState?.runLog?.runProfile === "strict" || input.knowledgeState?.runLog?.runProfile === "fast"
1199
+ ? input.knowledgeState.runLog.runProfile
1200
+ : input.knowledgeState?.runLog?.requiredCorePapers !== undefined ||
1201
+ input.knowledgeState?.runLog?.requiredFullTextCoveragePct !== undefined
1202
+ ? "strict"
1203
+ : defaultRunProfile();
1057
1204
  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
- };
1205
+ // Preserve idempotency for fingerprint-based runs, but avoid silently dropping
1206
+ // valid new cron cycles that accidentally reuse an explicit run_id.
1207
+ if (!explicitRunId) {
1208
+ root.streams[streamKey] = stream;
1209
+ return {
1210
+ projectId: project.projectId,
1211
+ streamKey,
1212
+ summary: toSummary(stream),
1213
+ runId,
1214
+ createdProject: project.created,
1215
+ };
1216
+ }
1217
+ const collisionTag = createHash("sha1")
1218
+ .update(`${nowMs}\n${paperIds.join("|")}\n${input.status ?? ""}\n${input.note ?? ""}`)
1219
+ .digest("hex")
1220
+ .slice(0, 8);
1221
+ runId = `${runId}-r${collisionTag}`;
1066
1222
  }
1067
1223
  const rootPath = getKnowledgeStateRoot(project.projectPath);
1068
1224
  const logDir = path.join(rootPath, "logs");
@@ -1100,6 +1256,7 @@ export async function commitKnowledgeRun(input) {
1100
1256
  hypotheses: submittedHypotheses,
1101
1257
  allRunPapers: mergedRunPapers,
1102
1258
  knowledgeChanges,
1259
+ runProfile: inferredRunProfile,
1103
1260
  });
1104
1261
  const acceptedHypotheses = hypothesisEval.acceptedHypotheses;
1105
1262
  const qualityEval = applyQualityGates({
@@ -1114,6 +1271,10 @@ export async function commitKnowledgeRun(input) {
1114
1271
  requiredCorePapers: input.knowledgeState?.runLog?.requiredCorePapers,
1115
1272
  requiredFullTextCoveragePct: input.knowledgeState?.runLog?.requiredFullTextCoveragePct,
1116
1273
  });
1274
+ if (changeSanitization.droppedBridgeCount > 0) {
1275
+ qualityEval.qualityGate.passed = false;
1276
+ qualityEval.qualityGate.reasons.push(`bridge_dropped_due_to_ungrounded_evidence(${changeSanitization.droppedBridgeCount})`);
1277
+ }
1117
1278
  const requestedStatus = normalizeText(input.status ?? "ok");
1118
1279
  const qualitySensitiveStatus = requestedStatus === "ok" || requestedStatus === "fallback_representative";
1119
1280
  const effectiveStatus = qualitySensitiveStatus && !qualityEval.qualityGate.passed ? "degraded_quality" : requestedStatus;
@@ -1174,6 +1335,7 @@ export async function commitKnowledgeRun(input) {
1174
1335
  await writeFile(path.join(knowledgeDir, "_index.md"), renderKnowledgeIndexMarkdown({
1175
1336
  now: nowIso,
1176
1337
  topic: stream.topic,
1338
+ runProfile: inferredRunProfile,
1177
1339
  topicFiles: stream.knowledgeTopics,
1178
1340
  paperNotesCount: stream.paperNotes.length,
1179
1341
  totalHypotheses: stream.totalHypotheses + recentHypothesisSummaries.length,
@@ -1188,6 +1350,7 @@ export async function commitKnowledgeRun(input) {
1188
1350
  }), "utf-8");
1189
1351
  const changeStat = countChangeStats(dayKey, runId, knowledgeChanges);
1190
1352
  stream.projectId = project.projectId;
1353
+ stream.lastRunProfile = inferredRunProfile;
1191
1354
  stream.totalRuns += 1;
1192
1355
  stream.totalHypotheses += recentHypothesisSummaries.length;
1193
1356
  stream.lastRunAtMs = nowMs;
@@ -1207,11 +1370,18 @@ export async function commitKnowledgeRun(input) {
1207
1370
  ].slice(0, MAX_RECENT_HYPOTHESES);
1208
1371
  stream.recentHypotheses = [...recentHypothesisSummaries, ...stream.recentHypotheses].slice(0, MAX_RECENT_HYPOTHESES);
1209
1372
  stream.recentChangeStats = [changeStat, ...stream.recentChangeStats].slice(0, MAX_RECENT_CHANGE_STATS);
1373
+ stream.triggerState = deriveTriggerState({
1374
+ recentChangeStats: stream.recentChangeStats,
1375
+ unreadCoreBacklog: qualityEval.unreadCorePaperIds.length,
1376
+ nowMs,
1377
+ });
1210
1378
  root.streams[streamKey] = stream;
1211
1379
  await saveStateAtomic(project.projectPath, root);
1212
1380
  await appendFile(path.join(logDir, `day-${dayKey}-run-details.jsonl`), `${JSON.stringify({
1213
1381
  ts: nowMs,
1382
+ run_id: runId,
1214
1383
  runId,
1384
+ run_profile: inferredRunProfile,
1215
1385
  scope: stream.scope,
1216
1386
  topic: stream.topic,
1217
1387
  streamKey,
@@ -1242,7 +1412,9 @@ export async function commitKnowledgeRun(input) {
1242
1412
  })}\n`, "utf-8");
1243
1413
  await appendEvent(project.projectPath, {
1244
1414
  ts: nowMs,
1415
+ run_id: runId,
1245
1416
  runId,
1417
+ run_profile: inferredRunProfile,
1246
1418
  scope: stream.scope,
1247
1419
  topic: stream.topic,
1248
1420
  streamKey,
@@ -1264,6 +1436,7 @@ export async function commitKnowledgeRun(input) {
1264
1436
  hypothesisCount: recentHypothesisSummaries.length,
1265
1437
  submittedHypothesisCount: submittedHypotheses.length,
1266
1438
  hypothesisGate: hypothesisEval.gate,
1439
+ triggerState: stream.triggerState,
1267
1440
  reflectionTasks,
1268
1441
  corePapers,
1269
1442
  explorationPapers,