@remnic/core 9.3.690 → 9.3.692

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 (294) hide show
  1. package/dist/access-boundary.d.ts +7 -6
  2. package/dist/access-boundary.js +21 -21
  3. package/dist/access-cli.js +49 -47
  4. package/dist/access-cli.js.map +1 -1
  5. package/dist/access-http.d.ts +7 -6
  6. package/dist/access-http.js +26 -26
  7. package/dist/access-mcp.d.ts +7 -6
  8. package/dist/access-mcp.js +23 -23
  9. package/dist/access-operations.d.ts +7 -6
  10. package/dist/access-operations.js +22 -22
  11. package/dist/{access-service-Dujr3MTm.d.ts → access-service-DvApf9LZ.d.ts} +2 -2
  12. package/dist/access-service.d.ts +7 -6
  13. package/dist/access-service.js +20 -20
  14. package/dist/access-surface-catalog.d.ts +7 -6
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-recall.d.ts +1 -1
  18. package/dist/active-recall.js +1 -1
  19. package/dist/adapters/index.js +4 -4
  20. package/dist/adapters/registry.js +2 -2
  21. package/dist/{auto-sync-NUQWSFQD.js → auto-sync-RWXT6P5M.js} +5 -5
  22. package/dist/behavior-learner.d.ts +1 -1
  23. package/dist/behavior-signals.d.ts +1 -1
  24. package/dist/bootstrap.d.ts +6 -5
  25. package/dist/briefing.d.ts +1 -1
  26. package/dist/briefing.js +5 -5
  27. package/dist/buffer-surprise-report.d.ts +1 -1
  28. package/dist/buffer.d.ts +1 -1
  29. package/dist/calibration.d.ts +1 -1
  30. package/dist/calibration.js +2 -2
  31. package/dist/capabilities.d.ts +1 -1
  32. package/dist/{catalog-CKPtJ114.d.ts → catalog-BEWLjdOr.d.ts} +1 -1
  33. package/dist/causal-behavior.d.ts +1 -1
  34. package/dist/causal-consolidation.d.ts +1 -1
  35. package/dist/causal-consolidation.js +8 -8
  36. package/dist/{chunk-GTDH3IUH.js → chunk-2D27ODNX.js} +2 -2
  37. package/dist/{chunk-46URPRE6.js → chunk-2EN5WZQB.js} +2 -2
  38. package/dist/{chunk-3K37VPM3.js → chunk-3BRA3N5A.js} +2 -2
  39. package/dist/{chunk-RTFAZOIR.js → chunk-3VVH6ZQW.js} +2 -2
  40. package/dist/{chunk-ZU7N3S6V.js → chunk-4IQNAOP3.js} +6 -142
  41. package/dist/chunk-4IQNAOP3.js.map +1 -0
  42. package/dist/{chunk-FIVDN2SM.js → chunk-4RGWCRIJ.js} +4 -4
  43. package/dist/{chunk-AGJH5ISO.js → chunk-5KBNNUXS.js} +2 -2
  44. package/dist/{chunk-X7RLU5CR.js → chunk-66HROLW5.js} +2 -2
  45. package/dist/chunk-6C2BMUZA.js +444 -0
  46. package/dist/chunk-6C2BMUZA.js.map +1 -0
  47. package/dist/{chunk-TYF3D4MS.js → chunk-6ZNIUGXB.js} +11 -11
  48. package/dist/{chunk-UPIBE2DK.js → chunk-ADUAF5EV.js} +2 -2
  49. package/dist/{chunk-LLONI6PY.js → chunk-AFWEUTMS.js} +2 -2
  50. package/dist/{chunk-ZUDM75KG.js → chunk-DCWIQFNA.js} +4 -4
  51. package/dist/{chunk-OMKUJTVJ.js → chunk-DO7MDZMU.js} +4 -4
  52. package/dist/{chunk-NGFEWFNK.js → chunk-DQHZA7Y3.js} +1 -1
  53. package/dist/chunk-DQHZA7Y3.js.map +1 -0
  54. package/dist/{chunk-LXIEXSHU.js → chunk-E6BOD3UJ.js} +2 -2
  55. package/dist/{chunk-OBXTMFZQ.js → chunk-ECZPWSB5.js} +2 -2
  56. package/dist/{chunk-CWE74HRG.js → chunk-FM7KLRKN.js} +3 -3
  57. package/dist/{chunk-BFVPIKDY.js → chunk-GCOSQBQ2.js} +3 -3
  58. package/dist/chunk-HXNMDPHN.js +149 -0
  59. package/dist/chunk-HXNMDPHN.js.map +1 -0
  60. package/dist/{chunk-MDYG7VI7.js → chunk-IREVVFTM.js} +10 -1
  61. package/dist/{chunk-MDYG7VI7.js.map → chunk-IREVVFTM.js.map} +1 -1
  62. package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
  63. package/dist/{chunk-CP2NZQLT.js → chunk-KYH53EFD.js} +6 -6
  64. package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
  65. package/dist/{chunk-SB6CQTKP.js → chunk-MCV2DD6F.js} +2 -2
  66. package/dist/{chunk-3FC6LW6T.js → chunk-MRFVMSTR.js} +2 -2
  67. package/dist/{chunk-WDXCNJSF.js → chunk-MXZWKOOR.js} +2 -2
  68. package/dist/{chunk-NSVXK7M5.js → chunk-NEILAC3H.js} +3 -3
  69. package/dist/{chunk-RGNEARXW.js → chunk-NUZ3N445.js} +7 -7
  70. package/dist/{chunk-WROKD3XC.js → chunk-O4ZIV62T.js} +35 -35
  71. package/dist/{chunk-PHZKALOE.js → chunk-QFG23CRE.js} +2 -2
  72. package/dist/{chunk-A4HH2EWA.js → chunk-QVG4LAQO.js} +2 -2
  73. package/dist/{chunk-54PVJDO5.js → chunk-SAC5V6JT.js} +2 -2
  74. package/dist/{chunk-XKUKJIOY.js → chunk-SEK4XIDN.js} +6 -6
  75. package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
  76. package/dist/{chunk-2N6UNJSX.js → chunk-U2OBZSGG.js} +2 -2
  77. package/dist/{chunk-BKAMHZYR.js → chunk-UOCLRM74.js} +2 -2
  78. package/dist/{chunk-A7UZR6RA.js → chunk-UTUYAPLY.js} +14 -1
  79. package/dist/chunk-UTUYAPLY.js.map +1 -0
  80. package/dist/{chunk-Z7KILAOU.js → chunk-V56S3A23.js} +19 -19
  81. package/dist/{chunk-CUNTLEJP.js → chunk-VFVG4NS2.js} +30 -9
  82. package/dist/chunk-VFVG4NS2.js.map +1 -0
  83. package/dist/{chunk-UD5OKH4J.js → chunk-W4ELOUNX.js} +2 -2
  84. package/dist/{chunk-CE6CBRCV.js → chunk-W4MICQIZ.js} +89 -407
  85. package/dist/chunk-W4MICQIZ.js.map +1 -0
  86. package/dist/{chunk-GR77Z2BM.js → chunk-WIKNVFBS.js} +2 -2
  87. package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
  88. package/dist/{chunk-67MMWC74.js → chunk-ZLXGFAML.js} +2 -2
  89. package/dist/{cli-BkDp6WNi.d.ts → cli-BjsY9Le7.d.ts} +3 -3
  90. package/dist/cli.d.ts +8 -7
  91. package/dist/cli.js +45 -44
  92. package/dist/compounding/engine.d.ts +1 -1
  93. package/dist/compounding/engine.js +5 -5
  94. package/dist/compounding/preference-consolidator.d.ts +1 -1
  95. package/dist/compression-optimizer.d.ts +1 -1
  96. package/dist/config.d.ts +1 -1
  97. package/dist/config.js +1 -1
  98. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  99. package/dist/connectors/codex-materialize-runner.js +5 -5
  100. package/dist/connectors/codex-materialize.d.ts +1 -1
  101. package/dist/connectors/index.d.ts +1 -1
  102. package/dist/connectors/index.js +5 -5
  103. package/dist/consolidation-provenance-check.d.ts +1 -1
  104. package/dist/consolidation-undo.d.ts +1 -1
  105. package/dist/contradiction/index.d.ts +1 -1
  106. package/dist/conversation-index/backend.d.ts +1 -1
  107. package/dist/conversation-index/backend.js +2 -2
  108. package/dist/conversation-index/chunker.d.ts +1 -1
  109. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  110. package/dist/conversation-index/indexer.d.ts +1 -1
  111. package/dist/conversation-index/search.d.ts +1 -1
  112. package/dist/day-summary.d.ts +1 -1
  113. package/dist/delinearize.d.ts +1 -1
  114. package/dist/direct-answer-wiring.d.ts +1 -1
  115. package/dist/direct-answer.d.ts +1 -1
  116. package/dist/embedding-fallback.d.ts +1 -1
  117. package/dist/enrichment/index.d.ts +1 -1
  118. package/dist/entity-retrieval.d.ts +1 -1
  119. package/dist/entity-retrieval.js +5 -5
  120. package/dist/entity-schema.d.ts +1 -1
  121. package/dist/event-time.d.ts +35 -0
  122. package/dist/event-time.js +325 -0
  123. package/dist/event-time.js.map +1 -0
  124. package/dist/explicit-capture.d.ts +6 -5
  125. package/dist/extraction-judge-telemetry.d.ts +1 -1
  126. package/dist/extraction-judge-training.d.ts +1 -1
  127. package/dist/extraction-judge.d.ts +1 -1
  128. package/dist/extraction-judge.js +3 -3
  129. package/dist/extraction.d.ts +1 -1
  130. package/dist/extraction.js +3 -3
  131. package/dist/fallback-llm.d.ts +1 -1
  132. package/dist/fallback-llm.js +2 -2
  133. package/dist/identity-continuity.d.ts +1 -1
  134. package/dist/importance.d.ts +1 -1
  135. package/dist/index.d.ts +11 -10
  136. package/dist/index.js +77 -75
  137. package/dist/index.js.map +1 -1
  138. package/dist/intent.d.ts +1 -1
  139. package/dist/lcm/engine.d.ts +1 -1
  140. package/dist/lcm/index.d.ts +1 -1
  141. package/dist/lcm/tools.d.ts +1 -1
  142. package/dist/lifecycle.d.ts +1 -1
  143. package/dist/live-connectors-runner.d.ts +1 -1
  144. package/dist/local-llm.d.ts +1 -1
  145. package/dist/maintenance/memory-governance.d.ts +1 -1
  146. package/dist/maintenance/memory-governance.js +5 -5
  147. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  148. package/dist/maintenance/rebuild-memory-projection.js +6 -6
  149. package/dist/mcp-memory-inspector-app.d.ts +7 -6
  150. package/dist/memory-action-policy.d.ts +1 -1
  151. package/dist/memory-cache.d.ts +1 -1
  152. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  153. package/dist/memory-projection-store.d.ts +1 -1
  154. package/dist/memory-provenance.d.ts +1 -1
  155. package/dist/memory-worth-outcomes.d.ts +1 -1
  156. package/dist/models-json.d.ts +1 -1
  157. package/dist/namespaces/migrate.d.ts +2 -2
  158. package/dist/namespaces/migrate.js +15 -15
  159. package/dist/namespaces/principal.d.ts +1 -1
  160. package/dist/namespaces/search.d.ts +1 -1
  161. package/dist/namespaces/search.js +9 -9
  162. package/dist/namespaces/storage.d.ts +2 -2
  163. package/dist/namespaces/storage.js +5 -5
  164. package/dist/native-knowledge.d.ts +1 -1
  165. package/dist/operator-toolkit.d.ts +1 -1
  166. package/dist/operator-toolkit.js +20 -19
  167. package/dist/orchestration/maintenance.d.ts +112 -0
  168. package/dist/orchestration/maintenance.js +39 -0
  169. package/dist/orchestration/maintenance.js.map +1 -0
  170. package/dist/{orchestrator-B7ixmUkP.d.ts → orchestrator-CNJCYjZ1.d.ts} +12 -36
  171. package/dist/orchestrator.d.ts +5 -4
  172. package/dist/orchestrator.js +42 -40
  173. package/dist/patterns-cli.d.ts +1 -1
  174. package/dist/policy-runtime.d.ts +1 -1
  175. package/dist/provenance.d.ts +1 -1
  176. package/dist/qmd-recall-cache.d.ts +1 -1
  177. package/dist/qmd.d.ts +1 -1
  178. package/dist/recall-disclosure-escalation.d.ts +1 -1
  179. package/dist/recall-explain-renderer.d.ts +1 -1
  180. package/dist/recall-explain-renderer.js +3 -3
  181. package/dist/recall-planner-llm.d.ts +1 -1
  182. package/dist/recall-planner-llm.js +2 -2
  183. package/dist/recall-state.d.ts +1 -1
  184. package/dist/recall-tag-filter.d.ts +1 -1
  185. package/dist/recall-xray-cli.d.ts +1 -1
  186. package/dist/recall-xray-cli.js +4 -4
  187. package/dist/recall-xray-renderer.d.ts +1 -1
  188. package/dist/recall-xray-renderer.js +3 -3
  189. package/dist/recall-xray.d.ts +1 -1
  190. package/dist/recall-xray.js +2 -2
  191. package/dist/resolve-auth-token.d.ts +1 -1
  192. package/dist/resume-bundles.js +2 -2
  193. package/dist/retrieval-agents.d.ts +1 -1
  194. package/dist/retrieval-tiers.d.ts +1 -1
  195. package/dist/routing/engine.d.ts +1 -1
  196. package/dist/routing/store.d.ts +1 -1
  197. package/dist/search/embed-helper.d.ts +1 -1
  198. package/dist/search/factory.d.ts +1 -1
  199. package/dist/search/factory.js +8 -8
  200. package/dist/search/index.d.ts +1 -1
  201. package/dist/search/index.js +13 -13
  202. package/dist/search/lancedb-backend.d.ts +1 -1
  203. package/dist/search/lancedb-backend.js +2 -2
  204. package/dist/search/meilisearch-backend.d.ts +1 -1
  205. package/dist/search/meilisearch-backend.js +2 -2
  206. package/dist/search/noop-backend.d.ts +1 -1
  207. package/dist/search/orama-backend.d.ts +1 -1
  208. package/dist/search/orama-backend.js +2 -2
  209. package/dist/search/port.d.ts +1 -1
  210. package/dist/search/remote-backend.d.ts +1 -1
  211. package/dist/{semantic-consolidation-CWch5uM7.d.ts → semantic-consolidation-D5U0Mrdb.d.ts} +1 -1
  212. package/dist/semantic-consolidation.d.ts +2 -2
  213. package/dist/semantic-consolidation.js +6 -6
  214. package/dist/semantic-rule-promotion.js +5 -5
  215. package/dist/semantic-rule-verifier.d.ts +1 -1
  216. package/dist/semantic-rule-verifier.js +5 -5
  217. package/dist/session-observer-bands.d.ts +1 -1
  218. package/dist/session-observer-state.d.ts +1 -1
  219. package/dist/shared-context/manager.d.ts +1 -1
  220. package/dist/signal.d.ts +1 -1
  221. package/dist/storage.d.ts +3 -1
  222. package/dist/storage.js +4 -4
  223. package/dist/summarizer.d.ts +1 -1
  224. package/dist/summarizer.js +3 -3
  225. package/dist/summary-snapshot.d.ts +1 -1
  226. package/dist/temporal-supersession.d.ts +1 -1
  227. package/dist/temporal-supersession.js +2 -2
  228. package/dist/temporal-validity.d.ts +22 -2
  229. package/dist/temporal-validity.js +3 -1
  230. package/dist/threading.d.ts +1 -1
  231. package/dist/tier-migration.d.ts +1 -1
  232. package/dist/tier-routing.d.ts +1 -1
  233. package/dist/topics.d.ts +1 -1
  234. package/dist/transcript.d.ts +1 -1
  235. package/dist/{types-Dm5xxVrr.d.ts → types-2NoBoRBM.d.ts} +38 -0
  236. package/dist/types.d.ts +1 -1
  237. package/dist/types.js +1 -1
  238. package/dist/utility-runtime.d.ts +1 -1
  239. package/dist/verified-recall.js +5 -5
  240. package/package.json +12 -2
  241. package/src/bitemporal-frontmatter.test.ts +177 -0
  242. package/src/config.ts +13 -0
  243. package/src/event-time.test.ts +514 -0
  244. package/src/event-time.ts +548 -0
  245. package/src/orchestration/maintenance.test.ts +738 -0
  246. package/src/orchestration/maintenance.ts +557 -0
  247. package/src/orchestrator-flush.test.ts +0 -599
  248. package/src/orchestrator.ts +60 -420
  249. package/src/storage.ts +34 -0
  250. package/src/temporal-validity.ts +34 -0
  251. package/src/types.ts +38 -0
  252. package/dist/chunk-A7UZR6RA.js.map +0 -1
  253. package/dist/chunk-CE6CBRCV.js.map +0 -1
  254. package/dist/chunk-CUNTLEJP.js.map +0 -1
  255. package/dist/chunk-NGFEWFNK.js.map +0 -1
  256. package/dist/chunk-ZU7N3S6V.js.map +0 -1
  257. /package/dist/{auto-sync-NUQWSFQD.js.map → auto-sync-RWXT6P5M.js.map} +0 -0
  258. /package/dist/{chunk-GTDH3IUH.js.map → chunk-2D27ODNX.js.map} +0 -0
  259. /package/dist/{chunk-46URPRE6.js.map → chunk-2EN5WZQB.js.map} +0 -0
  260. /package/dist/{chunk-3K37VPM3.js.map → chunk-3BRA3N5A.js.map} +0 -0
  261. /package/dist/{chunk-RTFAZOIR.js.map → chunk-3VVH6ZQW.js.map} +0 -0
  262. /package/dist/{chunk-FIVDN2SM.js.map → chunk-4RGWCRIJ.js.map} +0 -0
  263. /package/dist/{chunk-AGJH5ISO.js.map → chunk-5KBNNUXS.js.map} +0 -0
  264. /package/dist/{chunk-X7RLU5CR.js.map → chunk-66HROLW5.js.map} +0 -0
  265. /package/dist/{chunk-TYF3D4MS.js.map → chunk-6ZNIUGXB.js.map} +0 -0
  266. /package/dist/{chunk-UPIBE2DK.js.map → chunk-ADUAF5EV.js.map} +0 -0
  267. /package/dist/{chunk-LLONI6PY.js.map → chunk-AFWEUTMS.js.map} +0 -0
  268. /package/dist/{chunk-ZUDM75KG.js.map → chunk-DCWIQFNA.js.map} +0 -0
  269. /package/dist/{chunk-OMKUJTVJ.js.map → chunk-DO7MDZMU.js.map} +0 -0
  270. /package/dist/{chunk-LXIEXSHU.js.map → chunk-E6BOD3UJ.js.map} +0 -0
  271. /package/dist/{chunk-OBXTMFZQ.js.map → chunk-ECZPWSB5.js.map} +0 -0
  272. /package/dist/{chunk-CWE74HRG.js.map → chunk-FM7KLRKN.js.map} +0 -0
  273. /package/dist/{chunk-BFVPIKDY.js.map → chunk-GCOSQBQ2.js.map} +0 -0
  274. /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
  275. /package/dist/{chunk-CP2NZQLT.js.map → chunk-KYH53EFD.js.map} +0 -0
  276. /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
  277. /package/dist/{chunk-SB6CQTKP.js.map → chunk-MCV2DD6F.js.map} +0 -0
  278. /package/dist/{chunk-3FC6LW6T.js.map → chunk-MRFVMSTR.js.map} +0 -0
  279. /package/dist/{chunk-WDXCNJSF.js.map → chunk-MXZWKOOR.js.map} +0 -0
  280. /package/dist/{chunk-NSVXK7M5.js.map → chunk-NEILAC3H.js.map} +0 -0
  281. /package/dist/{chunk-RGNEARXW.js.map → chunk-NUZ3N445.js.map} +0 -0
  282. /package/dist/{chunk-WROKD3XC.js.map → chunk-O4ZIV62T.js.map} +0 -0
  283. /package/dist/{chunk-PHZKALOE.js.map → chunk-QFG23CRE.js.map} +0 -0
  284. /package/dist/{chunk-A4HH2EWA.js.map → chunk-QVG4LAQO.js.map} +0 -0
  285. /package/dist/{chunk-54PVJDO5.js.map → chunk-SAC5V6JT.js.map} +0 -0
  286. /package/dist/{chunk-XKUKJIOY.js.map → chunk-SEK4XIDN.js.map} +0 -0
  287. /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
  288. /package/dist/{chunk-2N6UNJSX.js.map → chunk-U2OBZSGG.js.map} +0 -0
  289. /package/dist/{chunk-BKAMHZYR.js.map → chunk-UOCLRM74.js.map} +0 -0
  290. /package/dist/{chunk-Z7KILAOU.js.map → chunk-V56S3A23.js.map} +0 -0
  291. /package/dist/{chunk-UD5OKH4J.js.map → chunk-W4ELOUNX.js.map} +0 -0
  292. /package/dist/{chunk-GR77Z2BM.js.map → chunk-WIKNVFBS.js.map} +0 -0
  293. /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
  294. /package/dist/{chunk-67MMWC74.js.map → chunk-ZLXGFAML.js.map} +0 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * Event-time resolver (issue #1578).
3
+ *
4
+ * Extraction can carry an optional per-fact `eventTime` expression — an ISO
5
+ * date or a relative phrase verbatim from the source turn ("last March",
6
+ * "yesterday", "since 2024"). This module resolves such expressions into a
7
+ * `[validFrom, validUntil)` event-time interval, **anchored to the source
8
+ * turn's timestamp** — never to `Date.now()`.
9
+ *
10
+ * Why anchor (not wall-clock): replay/import of old transcripts must resolve
11
+ * "yesterday" against the *old* turn's yesterday, not today. A relative
12
+ * expression is meaningless without its anchor; resolving it against the
13
+ * current time would silently rewrite history (AGENTS.md §39 — byte-identical
14
+ * when the feature is off; rule 35 — half-open intervals; rule 51 — never
15
+ * silently default invalid input).
16
+ *
17
+ * Resolution is **write-time-only**. The on-disk frontmatter stores absolute
18
+ * ISO timestamps strings (`valid_at` / `invalid_at`); readers never call back
19
+ * into this module. Anything unresolvable yields `ok: false` and the caller
20
+ * records `eventTimeSource: "assumed"` with `valid_at` copied from the
21
+ * ingestion anchor (`observedAt`).
22
+ *
23
+ * Interval semantics are `[validFrom, validUntil)` — inclusive start,
24
+ * exclusive end (AGENTS.md §23). A date-only expression resolving an *end*
25
+ * bound converts to start-of-next-day so the end date itself is excluded.
26
+ */
27
+ import { parseFlexibleIsoTimestamp } from "./utils/iso-timestamp.js";
28
+
29
+ /**
30
+ * Resolved event-time interval. All timestamps are UTC ISO strings.
31
+ *
32
+ * - `validFrom` / `validUntil` carry the half-open `[from, until)` event
33
+ * interval. Either may be absent when the expression only pinned one side
34
+ * ("since 2024" → validFrom only).
35
+ * - `ok` is false when the expression could not be resolved. The caller then
36
+ * falls back to `eventTimeSource: "assumed"`.
37
+ */
38
+ export interface ResolvedEventTime {
39
+ validFrom?: string;
40
+ validUntil?: string;
41
+ ok: boolean;
42
+ }
43
+
44
+ const UNITS = ["day", "week", "month", "quarter", "year"] as const;
45
+ type RelativeUnit = (typeof UNITS)[number];
46
+
47
+ function isRelativeUnit(unit: string): unit is RelativeUnit {
48
+ return (UNITS as readonly string[]).includes(unit);
49
+ }
50
+
51
+ /**
52
+ * Format a `Date` as a UTC ISO string with millisecond precision, matching the
53
+ * canonical frontmatter form. Returns `undefined` when the input is not
54
+ * finite (the resolver treats that as unresolvable rather than emitting
55
+ * `Invalid Date`).
56
+ */
57
+ function toIsoUtc(ms: number | null): string | undefined {
58
+ if (ms === null || !Number.isFinite(ms)) return undefined;
59
+ return new Date(ms).toISOString();
60
+ }
61
+
62
+ /**
63
+ * Compute the start of the day (UTC) for the given ms, used when a date-only
64
+ * expression resolves a *start* bound — the fact holds from midnight UTC.
65
+ */
66
+ function startOfDayUtc(ms: number): number {
67
+ const d = new Date(ms);
68
+ d.setUTCHours(0, 0, 0, 0);
69
+ return d.getTime();
70
+ }
71
+
72
+ /**
73
+ * Compute the start of the *next* day (UTC) for the given ms, used when a
74
+ * date-only expression resolves an *end* bound — the exclusive `validUntil`
75
+ * must land on the following midnight so the end date itself is excluded
76
+ * (AGENTS.md §23: date-only ends convert to start-of-next-day).
77
+ */
78
+ function startOfNextDayUtc(ms: number): number {
79
+ const d = new Date(startOfDayUtc(ms));
80
+ d.setUTCDate(d.getUTCDate() + 1);
81
+ return d.getTime();
82
+ }
83
+
84
+ const SEASON_TO_MONTH: Record<string, number> = {
85
+ winter: 12, // Dec–Feb anchor Dec (Northern Hemisphere convention)
86
+ spring: 3,
87
+ summer: 6,
88
+ fall: 9,
89
+ autumn: 9,
90
+ };
91
+
92
+ const MONTH_NAMES: Record<string, number> = {
93
+ january: 1, february: 2, march: 3, april: 4, may: 5, june: 6,
94
+ july: 7, august: 8, september: 9, october: 10, november: 11, december: 12,
95
+ jan: 1, feb: 2, mar: 3, apr: 4, jun: 6, jul: 7, aug: 8, sep: 9, sept: 9,
96
+ oct: 10, nov: 11, dec: 12,
97
+ };
98
+
99
+ const QUARTER_START_MONTH: Record<number, number> = {
100
+ 1: 1, 2: 4, 3: 7, 4: 10,
101
+ };
102
+
103
+ function monthIndex(token: string): number | undefined {
104
+ const idx = MONTH_NAMES[token.toLowerCase()];
105
+ return typeof idx === "number" ? idx : undefined;
106
+ }
107
+
108
+ /**
109
+ * Construct a UTC timestamp from year/month(1-12)/day with overflow validation.
110
+ * Returns `null` when the components do not form a real calendar date (e.g.
111
+ * Feb 30) rather than letting `Date` silently roll over.
112
+ */
113
+ function buildDateMs(year: number, month1: number, day: number): number | null {
114
+ if (
115
+ !Number.isInteger(year) ||
116
+ !Number.isInteger(month1) ||
117
+ month1 < 1 ||
118
+ month1 > 12 ||
119
+ !Number.isInteger(day) ||
120
+ day < 1 ||
121
+ day > daysInMonth(year, month1)
122
+ ) {
123
+ return null;
124
+ }
125
+ return Date.UTC(year, month1 - 1, day, 0, 0, 0, 0);
126
+ }
127
+
128
+ function daysInMonth(year: number, month1: number): number {
129
+ return new Date(Date.UTC(year, month1, 0)).getUTCDate();
130
+ }
131
+
132
+ /**
133
+ * Add `count` units to a UTC ms timestamp, returning the start-of-period for
134
+ * the resulting date. Used by "last/this/next <unit>" resolution.
135
+ */
136
+ function shiftUnit(anchorMs: number, unit: RelativeUnit, count: number): number {
137
+ const d = new Date(anchorMs);
138
+ switch (unit) {
139
+ case "day":
140
+ d.setUTCDate(d.getUTCDate() + count);
141
+ return startOfDayUtc(d.getTime());
142
+ case "week":
143
+ d.setUTCDate(d.getUTCDate() + count * 7);
144
+ return startOfDayUtc(d.getTime());
145
+ case "month":
146
+ d.setUTCMonth(d.getUTCMonth() + count, 1);
147
+ return startOfDayUtc(d.getTime());
148
+ case "quarter":
149
+ d.setUTCMonth(d.getUTCMonth() + count * 3, 1);
150
+ return startOfDayUtc(d.getTime());
151
+ case "year":
152
+ d.setUTCFullYear(d.getUTCFullYear() + count, 0, 1);
153
+ return startOfDayUtc(d.getTime());
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Resolve "last December" / "this March" / "next Q2" against the anchor.
159
+ * `direction` is -1 (last), 0 (this), +1 (next). Returns the start-of-period
160
+ * ms for the referenced month/season/quarter, or `null` if it cannot be
161
+ * placed (e.g. month token not recognized).
162
+ */
163
+ function resolveMonthYear(
164
+ anchorMs: number,
165
+ monthToken: string,
166
+ direction: number,
167
+ ): number | null {
168
+ const lower = monthToken.toLowerCase();
169
+ const targetMonth = monthIndex(lower);
170
+ if (typeof targetMonth === "number") {
171
+ const d = new Date(anchorMs);
172
+ let year = d.getUTCFullYear();
173
+ let m = targetMonth;
174
+ if (direction === -1 && m >= d.getUTCMonth() + 1) year -= 1;
175
+ else if (direction === +1 && m <= d.getUTCMonth() + 1) year += 1;
176
+ return buildDateMs(year, m, 1);
177
+ }
178
+ return null;
179
+ }
180
+ /**
181
+ * Resolve an explicit "<month|season> <year>" token (e.g. "march 2025",
182
+ * "dec 2024", "spring 2025") to a start-of-month ms. The explicit year is
183
+ * authoritative — it is never derived from the anchor. Returns null when
184
+ * the token is not in that shape or does not name a real month/season.
185
+ * Shared by the bare phrase path, "since <month> <year>", and the end bound
186
+ * so all three resolve identically (review: month-year phrases after since).
187
+ */
188
+ function resolveExplicitMonthYear(token: string): number | null {
189
+ const m = token.trim().toLowerCase().match(/^([a-z]+)\s+(\d{4})$/);
190
+ if (!m) return null;
191
+ const year = parseInt(m[2], 10);
192
+ if (!Number.isInteger(year)) return null;
193
+ const monthIdx = monthIndex(m[1]);
194
+ const baseMonth = typeof monthIdx === "number" ? monthIdx : SEASON_TO_MONTH[m[1]];
195
+ if (typeof baseMonth !== "number") return null;
196
+ return buildDateMs(year, baseMonth, 1);
197
+ }
198
+
199
+
200
+ /**
201
+ * Resolve a bare four-digit year token (e.g. "2024") to January 1 of that
202
+ * year. Used by "since 2024" and bare year expressions. Review #1578 r2:
203
+ * previously these fell through to ok:false, stamping the fact at ingestion
204
+ * time instead of the documented year start.
205
+ */
206
+ function resolveBareYear(token: string): number | null {
207
+ const m = token.trim().match(/^(\d{4})$/);
208
+ if (!m) return null;
209
+ return buildDateMs(Number(m[1]), 1, 1);
210
+ }
211
+
212
+
213
+ /**
214
+ * Check whether the anchor month falls inside a season's 3-month span.
215
+ * Seasons start at `baseMonth` and span 3 months (e.g. summer = Jun–Aug).
216
+ * Winter (Dec=12) wraps: Dec, Jan, Feb.
217
+ */
218
+ function isInSeason(baseMonth: number, anchorMonth: number): boolean {
219
+ for (let i = 0; i < 3; i++) {
220
+ if (((baseMonth - 1 + i) % 12) + 1 === anchorMonth) return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ function resolveSeasonYear(
226
+ anchorMs: number,
227
+ seasonToken: string,
228
+ direction: number,
229
+ ): number | null {
230
+ const baseMonth = SEASON_TO_MONTH[seasonToken.toLowerCase()];
231
+ if (typeof baseMonth !== "number") return null;
232
+ const d = new Date(anchorMs);
233
+ let year = d.getUTCFullYear();
234
+ const anchorMonth = d.getUTCMonth() + 1;
235
+
236
+ if (direction === -1) {
237
+ // "last <season>": go to the previous occurrence.
238
+ // If the season hasn't started yet this year (baseMonth > anchorMonth)
239
+ // or the anchor is currently INSIDE the season, roll back a year.
240
+ if (baseMonth > anchorMonth || isInSeason(baseMonth, anchorMonth)) year -= 1;
241
+ // Winter tail (Jan/Feb): current winter started prev Dec, so "last"
242
+ // needs one more year back.
243
+ if (baseMonth === 12 && anchorMonth <= 2) year -= 1;
244
+ } else if (direction === +1) {
245
+ // "next <season>": go to the next occurrence.
246
+ // Winter from Jan/Feb: current winter started prev Dec; next is this Dec.
247
+ if (baseMonth === 12 && anchorMonth <= 2) {
248
+ // no year change — next winter is December of the anchor year
249
+ } else if (baseMonth < anchorMonth || isInSeason(baseMonth, anchorMonth)) {
250
+ year += 1;
251
+ }
252
+ } else if (direction === 0 && baseMonth === 12 && anchorMonth <= 2) {
253
+ // "this winter" from Jan/Feb: winter started in the previous December.
254
+ year -= 1;
255
+ }
256
+ return buildDateMs(year, baseMonth, 1);
257
+ }
258
+
259
+ function resolveQuarter(
260
+ anchorMs: number,
261
+ qNumber: number,
262
+ direction: number,
263
+ ): number | null {
264
+ const startMonth = QUARTER_START_MONTH[qNumber];
265
+ if (typeof startMonth !== "number") return null;
266
+ const d = new Date(anchorMs);
267
+ let year = d.getUTCFullYear();
268
+ const anchorMonth = d.getUTCMonth() + 1;
269
+ const anchorQ = Math.floor((anchorMonth - 1) / 3) + 1;
270
+ // Roll back/forward when the target quarter hasn't started yet this year
271
+ // (startMonth past the anchor month) OR when the anchor is currently
272
+ // INSIDE the target quarter (review r2: "last Q2" from May returned 2026
273
+ // instead of 2025 because the quarter start month had already passed but
274
+ // the quarter was still ongoing).
275
+ if (direction === -1 && (startMonth >= anchorMonth || anchorQ === qNumber))
276
+ year -= 1;
277
+ else if (direction === +1 && (startMonth <= anchorMonth || anchorQ === qNumber))
278
+ year += 1;
279
+ return buildDateMs(year, startMonth, 1);
280
+ }
281
+
282
+ /**
283
+ * Resolve an absolute ISO date/datetime to ms. A date-only value yields
284
+ * start-of-day (UTC). Returns `null` when the string is not a well-formed,
285
+ * non-overflowed ISO timestamp.
286
+ */
287
+ function resolveAbsolute(raw: string): number | null {
288
+ const trimmed = raw.trim();
289
+ const ms = parseFlexibleIsoTimestamp(trimmed);
290
+ if (ms === null) return null;
291
+ // Date-only inputs (no T) anchor to start-of-day UTC.
292
+ if (!/[Tt]/.test(trimmed)) return startOfDayUtc(ms);
293
+ return ms;
294
+ }
295
+
296
+ /**
297
+ * Resolve a relative event-time expression against an anchor ISO timestamp.
298
+ *
299
+ * Supported shapes (case-insensitive, trimmed):
300
+ * - absolute ISO date / datetime: `"2026-03-01"`, `"2026-03-01T00:00:00Z"`
301
+ * - `"since <date|monthyear>"` → validFrom only
302
+ * - `"until <date|monthyear>"` / `"through ..."` / `"until end of ..."` → validUntil only
303
+ * - `"last <month|season|Qn|unit>"` → validFrom = start of referenced period
304
+ * - `"this <month|season|Qn|unit>"` → validFrom = start of current period
305
+ * - `"next <month|season|Qn|unit>"` → validFrom = start of referenced period
306
+ * - `"yesterday"` / `"today"` / `"tomorrow"` → validFrom = start of that day
307
+ *
308
+ * Anything else returns `{ ok: false }`. The caller then records
309
+ * `eventTimeSource: "assumed"`.
310
+ *
311
+ * @param expression the raw per-fact event-time phrase (may be empty/garbage)
312
+ * @param anchorIso the source turn's ISO timestamp — the resolution anchor
313
+ */
314
+ export function resolveEventTime(
315
+ expression: string | undefined | null,
316
+ anchorIso: string,
317
+ ): ResolvedEventTime {
318
+ const fallback: ResolvedEventTime = { ok: false };
319
+ if (typeof expression !== "string") return fallback;
320
+ const expr = expression.trim();
321
+ if (expr.length === 0) return fallback;
322
+
323
+ const anchorMs = parseFlexibleIsoTimestamp(anchorIso);
324
+ if (anchorMs === null || !Number.isFinite(anchorMs)) return fallback;
325
+
326
+ const lower = expr.toLowerCase();
327
+
328
+ // ── "since <x>" / "until <x>" / "through <x>" ──────────────────────────
329
+ const sinceMatch = lower.match(/^since\s+(.+)$/);
330
+ if (sinceMatch) {
331
+ const inner = sinceMatch[1].trim();
332
+ let fromMs =
333
+ resolveRelativePeriod(anchorMs, inner) ??
334
+ resolveExplicitMonthYear(inner) ??
335
+ resolveBareYear(inner) ??
336
+ resolveAbsolute(inner);
337
+ // "since <bare month/season>": if the resolved start is future and the
338
+ // expression carries no explicit 4-digit year, the fact meant the most
339
+ // recent PAST occurrence of that period (codex review r2: "since March"
340
+ // from January produced a future validFrom).
341
+ if (
342
+ fromMs !== null &&
343
+ fromMs > anchorMs &&
344
+ !/\d{4}/.test(inner)
345
+ ) {
346
+ const nd = new Date(fromMs);
347
+ nd.setUTCFullYear(nd.getUTCFullYear() - 1);
348
+ fromMs = nd.getTime();
349
+ }
350
+ const fromIso = toIsoUtc(fromMs);
351
+ if (!fromIso) return fallback;
352
+ return { validFrom: fromIso, ok: true };
353
+ }
354
+ const untilMatch = lower.match(/^(?:until|through|till|ending)\s+(.+)$/);
355
+ if (untilMatch) {
356
+ const inner = untilMatch[1].trim();
357
+ const untilMs = resolveEndBound(anchorMs, inner);
358
+ const untilIso = toIsoUtc(untilMs);
359
+ if (!untilIso) return fallback;
360
+ return { validUntil: untilIso, ok: true };
361
+ }
362
+
363
+ // ── "yesterday" / "today" / "tomorrow" ─────────────────────────────────
364
+ if (lower === "today") {
365
+ const fromIso = toIsoUtc(startOfDayUtc(anchorMs));
366
+ return fromIso ? { validFrom: fromIso, ok: true } : fallback;
367
+ }
368
+ if (lower === "yesterday") {
369
+ const fromIso = toIsoUtc(shiftUnit(anchorMs, "day", -1));
370
+ return fromIso ? { validFrom: fromIso, ok: true } : fallback;
371
+ }
372
+ if (lower === "tomorrow") {
373
+ const fromIso = toIsoUtc(shiftUnit(anchorMs, "day", +1));
374
+ return fromIso ? { validFrom: fromIso, ok: true } : fallback;
375
+ }
376
+
377
+ // ── "last/this/next <period>" ──────────────────────────────────────────
378
+ const relMatch = lower.match(/^(last|this|next)\s+(.+)$/);
379
+ if (relMatch) {
380
+ const direction = relMatch[1] === "last" ? -1 : relMatch[1] === "next" ? +1 : 0;
381
+ const period = relMatch[2].trim();
382
+ const ms = resolveRelativePeriod(anchorMs, period, direction);
383
+ const fromIso = toIsoUtc(ms);
384
+ return fromIso ? { validFrom: fromIso, ok: true } : fallback;
385
+ }
386
+
387
+ // ── bare month+year ("March 2025", "Dec 2024", "spring 2025") or
388
+ // bare four-digit year ("2024") ────────────────────────────────────────
389
+ // Explicit year is authoritative (never derived from the anchor). Shares
390
+ // resolveExplicitMonthYear / resolveBareYear with the since/until paths.
391
+ const explicitMs = resolveExplicitMonthYear(lower) ?? resolveBareYear(lower);
392
+ if (explicitMs !== null) {
393
+ const fromIso = toIsoUtc(explicitMs);
394
+ if (fromIso) return { validFrom: fromIso, ok: true };
395
+ }
396
+
397
+ // ── absolute ISO date / datetime ───────────────────────────────────────
398
+ const absMs = resolveAbsolute(expr);
399
+ if (absMs !== null) {
400
+ const fromIso = toIsoUtc(absMs);
401
+ return fromIso ? { validFrom: fromIso, ok: true } : fallback;
402
+ }
403
+
404
+ return fallback;
405
+ }
406
+
407
+ /**
408
+ * Resolve a period token (with optional direction) to a start-of-period ms.
409
+ * Handles month/season names, quarter tokens (`q1`..`q4`), and relative units
410
+ * (`day/week/month/quarter/year`) used by "last week", "this month", etc.
411
+ */
412
+ function resolveRelativePeriod(
413
+ anchorMs: number,
414
+ period: string,
415
+ direction: number = 0,
416
+ ): number | null {
417
+ const lower = period.trim().toLowerCase();
418
+
419
+ // Quarter token: "q1", "Q2", "quarter 3"
420
+ const qTok = lower.match(/^q(?:uarter)?\s?([1-4])$/);
421
+ if (qTok) {
422
+ return resolveQuarter(anchorMs, Number(qTok[1]), direction);
423
+ }
424
+ if (lower === "quarter") {
425
+ // "last quarter" / "next quarter" move the quarter INDEX (with year
426
+ // wraparound), not just the year — resolveQuarter only nudges the year
427
+ // for an explicit quarter number, so routing "last quarter" through it
428
+ // returned the current quarter's start (review: bare last/next quarter).
429
+ const d = new Date(anchorMs);
430
+ let q = Math.floor(d.getUTCMonth() / 3) + 1;
431
+ let year = d.getUTCFullYear();
432
+ if (direction === -1) {
433
+ q -= 1;
434
+ if (q < 1) { q = 4; year -= 1; }
435
+ } else if (direction === +1) {
436
+ q += 1;
437
+ if (q > 4) { q = 1; year += 1; }
438
+ }
439
+ return buildDateMs(year, QUARTER_START_MONTH[q], 1);
440
+ }
441
+
442
+ // Relative unit: "week", "month", "year", "day"
443
+ if (isRelativeUnit(lower)) {
444
+ return shiftUnit(anchorMs, lower, direction);
445
+ }
446
+
447
+ // Month or season name
448
+ const monthMs = resolveMonthYear(anchorMs, lower, direction);
449
+ if (monthMs !== null) return monthMs;
450
+ const seasonMs = resolveSeasonYear(anchorMs, lower, direction);
451
+ if (seasonMs !== null) return seasonMs;
452
+
453
+ return null;
454
+ }
455
+
456
+ /**
457
+ * Resolve an end-bound period to an *exclusive* upper bound ms. Date-only
458
+ * values convert to start-of-next-day (AGENTS.md §23); period names resolve to
459
+ * the start of the *following* period.
460
+ */
461
+ function resolveEndBound(anchorMs: number, period: string): number | null {
462
+ // Strip an optional "end of " prefix so "until end of March" resolves the
463
+ // same as "until March". Review #1578 r2: previously the prefix was passed
464
+ // unchanged and resolveEndBound returned null for the full phrase.
465
+ const trimmed = period.trim().replace(/^end\s+of\s+/i, "").trim();
466
+
467
+ // Bare four-digit year: exclusive end = January 1 of the FOLLOWING year.
468
+ if (/^\d{4}$/.test(trimmed)) {
469
+ return buildDateMs(Number(trimmed) + 1, 1, 1);
470
+ }
471
+
472
+ // Absolute date/datetime end bound.
473
+ const absMs = resolveAbsolute(trimmed);
474
+ if (absMs !== null) {
475
+ // Date-only → start of next day (exclusive end). Datetime → use as-is.
476
+ if (!/[Tt]/.test(trimmed)) return startOfNextDayUtc(absMs);
477
+ return absMs;
478
+ }
479
+ // Explicit "<month|season> <year>" end bound (e.g. "until March 2025",
480
+ // "through spring 2025"). The explicit year is authoritative, so the
481
+ // exclusive end = start of the FOLLOWING month (for a single month) or the
482
+ // start of the month AFTER the season ends (for a 3-month season).
483
+ // Review r2: seasons previously advanced only 1 month, cutting off the
484
+ // last 2 months of the season.
485
+ const explicitMs = resolveExplicitMonthYear(trimmed);
486
+ if (explicitMs !== null) {
487
+ const nd = new Date(explicitMs);
488
+ const firstWord = trimmed.toLowerCase().split(/\s+/)[0];
489
+ const monthAdvance = firstWord in SEASON_TO_MONTH ? 3 : 1;
490
+ nd.setUTCMonth(nd.getUTCMonth() + monthAdvance);
491
+ return startOfDayUtc(nd.getTime());
492
+ }
493
+
494
+ // Period-name end bound: month → exclusive end at start of the FOLLOWING
495
+ // month. "until <month>" is backwards-looking (the fact already stopped
496
+ // being true), so prefer the most recent PAST boundary: build it in the
497
+ // anchor year and roll back a year if that boundary is still future.
498
+ const lower = trimmed.toLowerCase();
499
+ const monthIdx = monthIndex(lower);
500
+ if (typeof monthIdx === "number") {
501
+ const d = new Date(anchorMs);
502
+ const startMs = buildDateMs(d.getUTCFullYear(), monthIdx, 1);
503
+ if (startMs === null) return null;
504
+ // Exclusive end = start of the month AFTER the named month.
505
+ const nd = new Date(startMs);
506
+ nd.setUTCMonth(nd.getUTCMonth() + 1);
507
+ let endMs = startOfDayUtc(nd.getTime());
508
+ // Only roll back when the named month boundary is genuinely future
509
+ // relative to the anchor cycle. When the anchor IS in the named month,
510
+ // the end of the current month is the right boundary — do not roll back
511
+ // (cursor review r2: "until March" from mid-March returned the prior year).
512
+ if (endMs > anchorMs && d.getUTCMonth() + 1 !== monthIdx) {
513
+ nd.setUTCFullYear(nd.getUTCFullYear() - 1);
514
+ endMs = startOfDayUtc(nd.getTime());
515
+ }
516
+ return endMs;
517
+ }
518
+ // Season end bound: advance by the full 3-month season. Same
519
+ // backwards-looking rollback logic as month names, but do not roll back
520
+ // when the anchor is currently inside the season (codex review r2: bare
521
+ // season end bounds like "until spring" / "through summer" returned null).
522
+ const seasonMonth = SEASON_TO_MONTH[lower];
523
+ if (typeof seasonMonth === "number") {
524
+ const d = new Date(anchorMs);
525
+ const anchorMonth = d.getUTCMonth() + 1;
526
+ let year = d.getUTCFullYear();
527
+ // Winter (Dec) from Jan/Feb: the current winter started in prev Dec,
528
+ // so build the season start from the previous year (codex review r2:
529
+ // bare winter end bounds used the anchor year's December, producing
530
+ // an exclusive end one year too far forward).
531
+ if (seasonMonth === 12 && anchorMonth <= 2) year -= 1;
532
+ const startMs = buildDateMs(year, seasonMonth, 1);
533
+ if (startMs === null) return null;
534
+ const nd = new Date(startMs);
535
+ nd.setUTCMonth(nd.getUTCMonth() + 3);
536
+ let endMs = startOfDayUtc(nd.getTime());
537
+ if (endMs > anchorMs && !isInSeason(seasonMonth, anchorMonth)) {
538
+ nd.setUTCFullYear(nd.getUTCFullYear() - 1);
539
+ endMs = startOfDayUtc(nd.getTime());
540
+ }
541
+ return endMs;
542
+ }
543
+ if (lower === "year") {
544
+ const d = new Date(anchorMs);
545
+ return buildDateMs(d.getUTCFullYear() + 1, 1, 1);
546
+ }
547
+ return null;
548
+ }