recallnest 2.3.0 → 2.6.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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Freshness — 记忆新鲜度四态判定
3
+ *
4
+ * 出处:通用Wiki上下文记忆Agent方案借鉴审计(2026-07-21,Alice 拍板落地)。
5
+ *
6
+ * 召回的记忆按"依赖状态"判定有效性,四个离散档:
7
+ * - exact 依赖原封未动,可直接复用
8
+ * - compatible 依赖变了,但落在声明的兼容集内,不影响结论
9
+ * - uncertain 依赖变了且无法判定是否影响结论,复用前需验证
10
+ * - invalid 依赖对象已失效(文件删了 / 仓库没了)
11
+ *
12
+ * 判据是"依赖状态"不是"时间":依赖对象在不在 / revision 变没变。
13
+ * 失效颗粒度是结论级:每条记忆绑自己的 dependsOn,某个来源变了只降那一条。
14
+ *
15
+ * 硬边界(承重约束):
16
+ * 1. 判定必须廉价 —— 单条依赖 = 一次 existsSync/statSync(O(1) syscall)
17
+ * 或一次 `git rev-parse`(单进程、无网络、按 repo 目录 memoize)。
18
+ * 绝不全量扫描、绝不调模型、绝不网络请求。
19
+ * 2. opt-in —— 只对显式声明了 dependsOn 的记忆生效。无声明的记忆
20
+ * evaluateEntryFreshness 返回 null,零开销、行为与之前完全一致。
21
+ * 3. 向后兼容 —— dependsOn 存活在既有 metadata JSON 列里(照 emotion /
22
+ * privacyTier 惯例),不新增数据库字段、不改破坏性 schema。
23
+ */
24
+
25
+ import { z } from "zod";
26
+ import { execFileSync } from "node:child_process";
27
+ import { existsSync, statSync } from "node:fs";
28
+ import { dirname } from "node:path";
29
+
30
+ export const FRESHNESS_STATES = ["exact", "compatible", "uncertain", "invalid"] as const;
31
+ export type Freshness = (typeof FRESHNESS_STATES)[number];
32
+ export const FreshnessSchema = z.enum(FRESHNESS_STATES);
33
+
34
+ export const DEPENDENCY_KINDS = ["file", "git-rev"] as const;
35
+ export type DependencyKind = (typeof DEPENDENCY_KINDS)[number];
36
+
37
+ /**
38
+ * 一条依赖声明。首版只支持 file / git-rev(不支持任意 cmd 执行,安全优先)。
39
+ * kind='file' ref = 文件路径;expected = 文件 mtime(毫秒时间戳的整数字符串)
40
+ * 作为廉价变更指纹,缺省时只做存在性检查。内容级精确请用 git-rev。
41
+ * kind='git-rev' ref = git 仓库内的路径(文件或目录,取其所在仓库 HEAD);
42
+ * expected = 期望 commit hash(支持短 hash 前缀),缺省时只做仓库存在性检查。
43
+ * expected 单值或数组:数组首值匹配 = exact,其余值匹配 = compatible(= 声明的兼容集)。
44
+ */
45
+ export const DependencySchema = z.object({
46
+ kind: z.enum(DEPENDENCY_KINDS),
47
+ ref: z.string().min(1).max(500),
48
+ expected: z
49
+ .union([z.string().min(1).max(200), z.array(z.string().min(1).max(200)).min(1).max(8)])
50
+ .optional(),
51
+ });
52
+ export type Dependency = z.infer<typeof DependencySchema>;
53
+
54
+ export const DependsOnSchema = z.array(DependencySchema).min(1).max(8);
55
+
56
+ /** 判定结果按严重度排序,聚合时取最差。 */
57
+ const SEVERITY: Record<Freshness, number> = { exact: 0, compatible: 1, uncertain: 2, invalid: 3 };
58
+
59
+ /** git rev-parse 结果按 repo 目录缓存:一次判定批次内同一仓库只 spawn 一次 git。 */
60
+ export type FreshnessCache = Map<string, string | null>;
61
+
62
+ export function createFreshnessCache(): FreshnessCache {
63
+ return new Map();
64
+ }
65
+
66
+ /** 从记忆 metadata JSON 解析 dependsOn 声明;缺失或非法返回 null(照 parseEmotion 惯例)。 */
67
+ export function parseDependsOn(metadata?: string): Dependency[] | null {
68
+ if (!metadata) return null;
69
+ let parsed: unknown;
70
+ try {
71
+ parsed = JSON.parse(metadata);
72
+ } catch {
73
+ return null;
74
+ }
75
+ if (parsed === null || typeof parsed !== "object") return null;
76
+ const raw = (parsed as Record<string, unknown>).dependsOn;
77
+ if (raw == null) return null;
78
+ const result = DependsOnSchema.safeParse(raw);
79
+ return result.success ? result.data : null;
80
+ }
81
+
82
+ /** 校验来自 store 输入侧的 dependsOn(unknown → Dependency[] | null)。 */
83
+ export function parseDependsOnInput(raw: unknown): Dependency[] | null {
84
+ if (raw == null) return null;
85
+ const result = DependsOnSchema.safeParse(raw);
86
+ return result.success ? result.data : null;
87
+ }
88
+
89
+ function toExpectedList(expected: Dependency["expected"]): string[] {
90
+ if (expected == null) return [];
91
+ return Array.isArray(expected) ? expected : [expected];
92
+ }
93
+
94
+ /**
95
+ * 把"当前值"归类到 exact / compatible / uncertain。
96
+ * - 无 expected:只做存在性检查,存在即 exact。
97
+ * - 匹配 expected[0](首选/最新)→ exact
98
+ * - 匹配 expected[1..](声明的其他兼容值)→ compatible
99
+ * - 都不匹配 → uncertain(变了,且无法判定是否影响结论)
100
+ */
101
+ function classifyAgainstExpected(
102
+ current: string,
103
+ expected: string[],
104
+ matches: (current: string, candidate: string) => boolean,
105
+ ): Freshness {
106
+ if (expected.length === 0) return "exact";
107
+ if (matches(current, expected[0])) return "exact";
108
+ for (let i = 1; i < expected.length; i++) {
109
+ if (matches(current, expected[i])) return "compatible";
110
+ }
111
+ return "uncertain";
112
+ }
113
+
114
+ function exactMatch(current: string, candidate: string): boolean {
115
+ return current === candidate;
116
+ }
117
+
118
+ /** 短 hash 友好:current 是完整 HEAD(40 hex),candidate 可为完整或短前缀。 */
119
+ function gitRevMatch(current: string, candidate: string): boolean {
120
+ if (!candidate) return false;
121
+ const a = current.toLowerCase();
122
+ const b = candidate.toLowerCase();
123
+ return a === b || a.startsWith(b);
124
+ }
125
+
126
+ function resolveHeadRev(ref: string, cache: FreshnessCache): string | null {
127
+ let dir: string;
128
+ try {
129
+ dir = existsSync(ref) && statSync(ref).isDirectory() ? ref : dirname(ref);
130
+ } catch {
131
+ dir = dirname(ref);
132
+ }
133
+ const cached = cache.get(dir);
134
+ if (cached !== undefined) return cached;
135
+ let rev: string | null = null;
136
+ try {
137
+ const out = execFileSync("git", ["-C", dir, "rev-parse", "HEAD"], {
138
+ encoding: "utf8",
139
+ timeout: 2000,
140
+ stdio: ["ignore", "pipe", "ignore"],
141
+ }).trim();
142
+ rev = out.length > 0 ? out : null;
143
+ } catch {
144
+ rev = null; // 非 git 仓库 / git 不可用 / 超时 → 依赖失效
145
+ }
146
+ cache.set(dir, rev);
147
+ return rev;
148
+ }
149
+
150
+ /** 单条依赖的廉价判定。 */
151
+ export function evaluateDependency(dep: Dependency, cache: FreshnessCache): Freshness {
152
+ const expected = toExpectedList(dep.expected);
153
+ if (dep.kind === "file") {
154
+ if (!existsSync(dep.ref)) return "invalid";
155
+ if (expected.length === 0) return "exact";
156
+ let mtime: string;
157
+ try {
158
+ mtime = String(Math.floor(statSync(dep.ref).mtimeMs));
159
+ } catch {
160
+ return "invalid"; // 存在性检查与 stat 之间被删
161
+ }
162
+ return classifyAgainstExpected(mtime, expected, exactMatch);
163
+ }
164
+ // git-rev
165
+ const head = resolveHeadRev(dep.ref, cache);
166
+ if (head === null) return "invalid";
167
+ return classifyAgainstExpected(head, expected, gitRevMatch);
168
+ }
169
+
170
+ /** 多条依赖聚合:取最差(worst-case)——任一依赖失效,整条记忆就不可全信。 */
171
+ export function evaluateFreshness(deps: Dependency[], cache?: FreshnessCache): Freshness {
172
+ if (deps.length === 0) return "exact";
173
+ const revCache = cache ?? createFreshnessCache();
174
+ let worst: Freshness = "exact";
175
+ for (const dep of deps) {
176
+ const f = evaluateDependency(dep, revCache);
177
+ if (SEVERITY[f] > SEVERITY[worst]) worst = f;
178
+ if (worst === "invalid") break; // 已是最差档,无需继续
179
+ }
180
+ return worst;
181
+ }
182
+
183
+ /**
184
+ * 便捷入口:从记忆 metadata 判定新鲜度。
185
+ * opt-in —— 无 dependsOn 声明返回 null(调用方据此跳过展示,零开销)。
186
+ * @param cache 可选的跨条目共享缓存(渲染一批结果时传入,令同仓库只 spawn 一次 git)。
187
+ */
188
+ export function evaluateEntryFreshness(metadata?: string, cache?: FreshnessCache): Freshness | null {
189
+ const deps = parseDependsOn(metadata);
190
+ if (!deps) return null;
191
+ return evaluateFreshness(deps, cache);
192
+ }
@@ -0,0 +1,148 @@
1
+ export const LOCAL_HTTP_HOSTNAME = "127.0.0.1";
2
+ export const LOCAL_JSON_BODY_LIMIT_BYTES = 1024 * 1024;
3
+
4
+ const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
5
+ const DEFAULT_JSON_METHODS = ["POST"] as const;
6
+
7
+ export interface LocalHttpRequestPolicyOptions {
8
+ jsonMethods?: readonly string[];
9
+ maxJsonBodyBytes?: number;
10
+ }
11
+
12
+ export type LocalHttpRequestPolicyResult =
13
+ | { allowed: true }
14
+ | { allowed: false; status: 400 | 403 | 413 | 415; message: string };
15
+
16
+ function reject(
17
+ status: 400 | 403 | 413 | 415,
18
+ message: string,
19
+ ): LocalHttpRequestPolicyResult {
20
+ return { allowed: false, status, message };
21
+ }
22
+
23
+ function normalizeHostname(hostname: string): string {
24
+ const normalized = hostname.trim().toLowerCase();
25
+ if (normalized.startsWith("[") && normalized.endsWith("]")) {
26
+ return normalized.slice(1, -1);
27
+ }
28
+ return normalized;
29
+ }
30
+
31
+ function hostnameFromHostHeader(hostHeader: string): string | null {
32
+ const authority = hostHeader.trim();
33
+ if (!authority || /[\s/@?#\\]/.test(authority)) return null;
34
+
35
+ try {
36
+ return normalizeHostname(new URL(`http://${authority}`).hostname);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function isLocalHostname(hostname: string): boolean {
43
+ return LOCAL_HOSTNAMES.has(normalizeHostname(hostname));
44
+ }
45
+
46
+ function readContentLength(request: Request): number | null {
47
+ const raw = request.headers.get("content-length");
48
+ if (raw == null) return null;
49
+ const trimmed = raw.trim();
50
+ if (!/^\d+$/.test(trimmed)) return Number.NaN;
51
+ return Number(trimmed);
52
+ }
53
+
54
+ async function bodyExceedsLimit(request: Request, maxBytes: number): Promise<boolean> {
55
+ if (!request.body) return false;
56
+
57
+ const reader = request.clone().body?.getReader();
58
+ if (!reader) return false;
59
+
60
+ let totalBytes = 0;
61
+ try {
62
+ while (true) {
63
+ const chunk = await reader.read();
64
+ if (chunk.done) return false;
65
+ totalBytes += chunk.value.byteLength;
66
+ if (totalBytes > maxBytes) {
67
+ void reader.cancel("request body too large").catch(() => {});
68
+ return true;
69
+ }
70
+ }
71
+ } finally {
72
+ reader.releaseLock();
73
+ }
74
+ }
75
+
76
+ export async function enforceLocalHttpRequestPolicy(
77
+ request: Request,
78
+ options: LocalHttpRequestPolicyOptions = {},
79
+ ): Promise<LocalHttpRequestPolicyResult> {
80
+ let requestUrl: URL;
81
+ try {
82
+ requestUrl = new URL(request.url);
83
+ } catch {
84
+ return reject(400, "Invalid request URL.");
85
+ }
86
+
87
+ if (!isLocalHostname(requestUrl.hostname)) {
88
+ return reject(403, "Host is not allowed.");
89
+ }
90
+
91
+ const hostHeader = request.headers.get("host");
92
+ if (hostHeader != null) {
93
+ const hostname = hostnameFromHostHeader(hostHeader);
94
+ if (!hostname || !isLocalHostname(hostname)) {
95
+ return reject(403, "Host is not allowed.");
96
+ }
97
+ }
98
+
99
+ const fetchSite = request.headers.get("sec-fetch-site")?.trim().toLowerCase();
100
+ if (fetchSite && fetchSite !== "same-origin" && fetchSite !== "none") {
101
+ return reject(403, "Cross-site requests are not allowed.");
102
+ }
103
+
104
+ const origin = request.headers.get("origin");
105
+ if (origin != null) {
106
+ let originUrl: URL;
107
+ try {
108
+ originUrl = new URL(origin);
109
+ } catch {
110
+ return reject(403, "Origin is not allowed.");
111
+ }
112
+ if (originUrl.origin !== requestUrl.origin) {
113
+ return reject(403, "Origin is not allowed.");
114
+ }
115
+ }
116
+
117
+ const jsonMethods = options.jsonMethods ?? DEFAULT_JSON_METHODS;
118
+ if (!jsonMethods.some((method) => method.toUpperCase() === request.method.toUpperCase())) {
119
+ return { allowed: true };
120
+ }
121
+
122
+ const mediaType = request.headers.get("content-type")
123
+ ?.split(";", 1)[0]
124
+ ?.trim()
125
+ .toLowerCase();
126
+ if (mediaType !== "application/json") {
127
+ return reject(415, "Content-Type must be application/json.");
128
+ }
129
+
130
+ const maxJsonBodyBytes = options.maxJsonBodyBytes ?? LOCAL_JSON_BODY_LIMIT_BYTES;
131
+ const contentLength = readContentLength(request);
132
+ if (contentLength != null && (!Number.isSafeInteger(contentLength) || contentLength < 0)) {
133
+ return reject(400, "Invalid Content-Length header.");
134
+ }
135
+ if (contentLength != null && contentLength > maxJsonBodyBytes) {
136
+ return reject(413, `Request body exceeds ${maxJsonBodyBytes} bytes.`);
137
+ }
138
+
139
+ try {
140
+ if (await bodyExceedsLimit(request, maxJsonBodyBytes)) {
141
+ return reject(413, `Request body exceeds ${maxJsonBodyBytes} bytes.`);
142
+ }
143
+ } catch {
144
+ return reject(400, "Unable to read request body.");
145
+ }
146
+
147
+ return { allowed: true };
148
+ }
@@ -0,0 +1,8 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function buildIdempotentRecordId(prefix: string, key?: string): string | undefined {
4
+ const normalized = key?.replace(/\s+/g, " ").trim();
5
+ if (!normalized) return undefined;
6
+ const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 32);
7
+ return `${prefix}-${digest}`;
8
+ }