@remnic/core 9.3.690 → 9.3.691
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +18 -18
- package/dist/access-cli.js +44 -44
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +24 -24
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +21 -21
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +20 -20
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-Dujr3MTm.d.ts → access-service-CKnFdxH-.d.ts} +2 -2
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +17 -17
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-NUQWSFQD.js → auto-sync-RWXT6P5M.js} +5 -5
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +2 -2
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-CKPtJ114.d.ts → catalog-BEWLjdOr.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-GTDH3IUH.js → chunk-2D27ODNX.js} +2 -2
- package/dist/{chunk-3K37VPM3.js → chunk-3BRA3N5A.js} +2 -2
- package/dist/{chunk-RTFAZOIR.js → chunk-3VVH6ZQW.js} +2 -2
- package/dist/{chunk-FIVDN2SM.js → chunk-4RGWCRIJ.js} +4 -4
- package/dist/{chunk-AGJH5ISO.js → chunk-5KBNNUXS.js} +2 -2
- package/dist/{chunk-X7RLU5CR.js → chunk-66HROLW5.js} +2 -2
- package/dist/{chunk-NSVXK7M5.js → chunk-7UVDIWLL.js} +4 -4
- package/dist/{chunk-UPIBE2DK.js → chunk-ADUAF5EV.js} +2 -2
- package/dist/{chunk-LLONI6PY.js → chunk-AFWEUTMS.js} +2 -2
- package/dist/{chunk-CWE74HRG.js → chunk-D3DNGQNQ.js} +3 -3
- package/dist/{chunk-ZUDM75KG.js → chunk-DCWIQFNA.js} +4 -4
- package/dist/{chunk-NGFEWFNK.js → chunk-DQHZA7Y3.js} +1 -1
- package/dist/chunk-DQHZA7Y3.js.map +1 -0
- package/dist/{chunk-OBXTMFZQ.js → chunk-ECZPWSB5.js} +2 -2
- package/dist/{chunk-BFVPIKDY.js → chunk-GCOSQBQ2.js} +3 -3
- package/dist/{chunk-LXIEXSHU.js → chunk-IPJD7WJH.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-MDYG7VI7.js → chunk-IREVVFTM.js} +10 -1
- package/dist/{chunk-MDYG7VI7.js.map → chunk-IREVVFTM.js.map} +1 -1
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-WROKD3XC.js → chunk-K7I24ZO2.js} +24 -24
- package/dist/{chunk-Z7KILAOU.js → chunk-KO6OL4I7.js} +20 -20
- package/dist/{chunk-SB6CQTKP.js → chunk-MCV2DD6F.js} +2 -2
- package/dist/{chunk-3FC6LW6T.js → chunk-MRFVMSTR.js} +2 -2
- package/dist/{chunk-WDXCNJSF.js → chunk-MXZWKOOR.js} +2 -2
- package/dist/{chunk-XKUKJIOY.js → chunk-NCRDCTEJ.js} +7 -7
- package/dist/{chunk-RGNEARXW.js → chunk-NUZ3N445.js} +7 -7
- package/dist/{chunk-CP2NZQLT.js → chunk-OOO7ZKOD.js} +6 -6
- package/dist/{chunk-PHZKALOE.js → chunk-QFG23CRE.js} +2 -2
- package/dist/{chunk-UDJLF3BO.js → chunk-QLRYXOAD.js} +2 -2
- package/dist/{chunk-ZU7N3S6V.js → chunk-QNG35H27.js} +2 -2
- package/dist/{chunk-A4HH2EWA.js → chunk-QVG4LAQO.js} +2 -2
- package/dist/{chunk-54PVJDO5.js → chunk-SAC5V6JT.js} +2 -2
- package/dist/{chunk-CE6CBRCV.js → chunk-TJ5IIQIR.js} +56 -45
- package/dist/chunk-TJ5IIQIR.js.map +1 -0
- package/dist/{chunk-OMKUJTVJ.js → chunk-TSGREOYY.js} +5 -5
- package/dist/{chunk-2N6UNJSX.js → chunk-U2OBZSGG.js} +2 -2
- package/dist/{chunk-BKAMHZYR.js → chunk-UOCLRM74.js} +2 -2
- package/dist/{chunk-A7UZR6RA.js → chunk-UTUYAPLY.js} +14 -1
- package/dist/chunk-UTUYAPLY.js.map +1 -0
- package/dist/{chunk-TYF3D4MS.js → chunk-UUMYB3FR.js} +2 -2
- package/dist/{chunk-CUNTLEJP.js → chunk-VFVG4NS2.js} +30 -9
- package/dist/chunk-VFVG4NS2.js.map +1 -0
- package/dist/{chunk-46URPRE6.js → chunk-VH2DHBHG.js} +2 -2
- package/dist/{chunk-UD5OKH4J.js → chunk-W4ELOUNX.js} +2 -2
- package/dist/{chunk-GR77Z2BM.js → chunk-WIKNVFBS.js} +2 -2
- package/dist/{chunk-67MMWC74.js → chunk-ZLXGFAML.js} +2 -2
- package/dist/{cli-BkDp6WNi.d.ts → cli-CrSW_KRq.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +38 -38
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +5 -5
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +5 -5
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +5 -5
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/contradiction/index.js +4 -4
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/backend.js +2 -2
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-retrieval.js +5 -5
- package/dist/entity-schema.d.ts +1 -1
- package/dist/event-time.d.ts +35 -0
- package/dist/event-time.js +325 -0
- package/dist/event-time.js.map +1 -0
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.d.ts +1 -1
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/fallback-llm.js +2 -2
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +72 -72
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/memory-governance.js +5 -5
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +6 -6
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/migrate.js +10 -10
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/namespaces/storage.js +5 -5
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +14 -14
- package/dist/{orchestrator-B7ixmUkP.d.ts → orchestrator-DvswGK76.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +34 -34
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-planner-llm.js +2 -2
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +2 -2
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +3 -3
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +3 -3
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-CWch5uM7.d.ts → semantic-consolidation-D5U0Mrdb.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +6 -6
- package/dist/semantic-rule-promotion.js +5 -5
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -5
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +3 -1
- package/dist/storage.js +4 -4
- package/dist/summarizer.d.ts +1 -1
- package/dist/summarizer.js +3 -3
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-supersession.js +2 -2
- package/dist/temporal-validity.d.ts +22 -2
- package/dist/temporal-validity.js +3 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/{types-Dm5xxVrr.d.ts → types-2NoBoRBM.d.ts} +38 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -5
- package/package.json +2 -2
- package/src/bitemporal-frontmatter.test.ts +177 -0
- package/src/config.ts +13 -0
- package/src/event-time.test.ts +514 -0
- package/src/event-time.ts +548 -0
- package/src/orchestrator.ts +27 -1
- package/src/storage.ts +34 -0
- package/src/temporal-validity.ts +34 -0
- package/src/types.ts +38 -0
- package/dist/chunk-A7UZR6RA.js.map +0 -1
- package/dist/chunk-CE6CBRCV.js.map +0 -1
- package/dist/chunk-CUNTLEJP.js.map +0 -1
- package/dist/chunk-NGFEWFNK.js.map +0 -1
- /package/dist/{auto-sync-NUQWSFQD.js.map → auto-sync-RWXT6P5M.js.map} +0 -0
- /package/dist/{chunk-GTDH3IUH.js.map → chunk-2D27ODNX.js.map} +0 -0
- /package/dist/{chunk-3K37VPM3.js.map → chunk-3BRA3N5A.js.map} +0 -0
- /package/dist/{chunk-RTFAZOIR.js.map → chunk-3VVH6ZQW.js.map} +0 -0
- /package/dist/{chunk-FIVDN2SM.js.map → chunk-4RGWCRIJ.js.map} +0 -0
- /package/dist/{chunk-AGJH5ISO.js.map → chunk-5KBNNUXS.js.map} +0 -0
- /package/dist/{chunk-X7RLU5CR.js.map → chunk-66HROLW5.js.map} +0 -0
- /package/dist/{chunk-NSVXK7M5.js.map → chunk-7UVDIWLL.js.map} +0 -0
- /package/dist/{chunk-UPIBE2DK.js.map → chunk-ADUAF5EV.js.map} +0 -0
- /package/dist/{chunk-LLONI6PY.js.map → chunk-AFWEUTMS.js.map} +0 -0
- /package/dist/{chunk-CWE74HRG.js.map → chunk-D3DNGQNQ.js.map} +0 -0
- /package/dist/{chunk-ZUDM75KG.js.map → chunk-DCWIQFNA.js.map} +0 -0
- /package/dist/{chunk-OBXTMFZQ.js.map → chunk-ECZPWSB5.js.map} +0 -0
- /package/dist/{chunk-BFVPIKDY.js.map → chunk-GCOSQBQ2.js.map} +0 -0
- /package/dist/{chunk-LXIEXSHU.js.map → chunk-IPJD7WJH.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-WROKD3XC.js.map → chunk-K7I24ZO2.js.map} +0 -0
- /package/dist/{chunk-Z7KILAOU.js.map → chunk-KO6OL4I7.js.map} +0 -0
- /package/dist/{chunk-SB6CQTKP.js.map → chunk-MCV2DD6F.js.map} +0 -0
- /package/dist/{chunk-3FC6LW6T.js.map → chunk-MRFVMSTR.js.map} +0 -0
- /package/dist/{chunk-WDXCNJSF.js.map → chunk-MXZWKOOR.js.map} +0 -0
- /package/dist/{chunk-XKUKJIOY.js.map → chunk-NCRDCTEJ.js.map} +0 -0
- /package/dist/{chunk-RGNEARXW.js.map → chunk-NUZ3N445.js.map} +0 -0
- /package/dist/{chunk-CP2NZQLT.js.map → chunk-OOO7ZKOD.js.map} +0 -0
- /package/dist/{chunk-PHZKALOE.js.map → chunk-QFG23CRE.js.map} +0 -0
- /package/dist/{chunk-UDJLF3BO.js.map → chunk-QLRYXOAD.js.map} +0 -0
- /package/dist/{chunk-ZU7N3S6V.js.map → chunk-QNG35H27.js.map} +0 -0
- /package/dist/{chunk-A4HH2EWA.js.map → chunk-QVG4LAQO.js.map} +0 -0
- /package/dist/{chunk-54PVJDO5.js.map → chunk-SAC5V6JT.js.map} +0 -0
- /package/dist/{chunk-OMKUJTVJ.js.map → chunk-TSGREOYY.js.map} +0 -0
- /package/dist/{chunk-2N6UNJSX.js.map → chunk-U2OBZSGG.js.map} +0 -0
- /package/dist/{chunk-BKAMHZYR.js.map → chunk-UOCLRM74.js.map} +0 -0
- /package/dist/{chunk-TYF3D4MS.js.map → chunk-UUMYB3FR.js.map} +0 -0
- /package/dist/{chunk-46URPRE6.js.map → chunk-VH2DHBHG.js.map} +0 -0
- /package/dist/{chunk-UD5OKH4J.js.map → chunk-W4ELOUNX.js.map} +0 -0
- /package/dist/{chunk-GR77Z2BM.js.map → chunk-WIKNVFBS.js.map} +0 -0
- /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
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -122,7 +122,7 @@ import {
|
|
|
122
122
|
normalizeSupersessionKey,
|
|
123
123
|
shouldFilterSupersededFromRecall,
|
|
124
124
|
} from "./temporal-supersession.js";
|
|
125
|
-
import { isValidAsOf } from "./temporal-validity.js";
|
|
125
|
+
import { isValidAsOf, isValidityExpiredNow } from "./temporal-validity.js";
|
|
126
126
|
import { RelevanceStore } from "./relevance.js";
|
|
127
127
|
import { NegativeExampleStore } from "./negative.js";
|
|
128
128
|
import {
|
|
@@ -19339,6 +19339,7 @@ export class Orchestrator {
|
|
|
19339
19339
|
): QmdSearchResult[] {
|
|
19340
19340
|
let lifecycleFilteredCount = 0;
|
|
19341
19341
|
let temporalSupersededFilteredCount = 0;
|
|
19342
|
+
let biTemporalExpiredFilteredCount = 0;
|
|
19342
19343
|
let dedicatedSurfaceFilteredCount = 0;
|
|
19343
19344
|
let forgottenFilteredCount = 0;
|
|
19344
19345
|
let blockedPathFilteredCount = 0;
|
|
@@ -19401,6 +19402,26 @@ export class Orchestrator {
|
|
|
19401
19402
|
temporalSupersededFilteredCount += 1;
|
|
19402
19403
|
continue;
|
|
19403
19404
|
}
|
|
19405
|
+
// Bi-temporal INJECTION filter (issue #1578): when the master gate
|
|
19406
|
+
// is on and the caller did NOT pin `as_of`, drop facts whose event-
|
|
19407
|
+
// time interval has ended before now. This lives ONLY in the recall
|
|
19408
|
+
// injection path (filterSearchResultsByRecallSafety) — explicit
|
|
19409
|
+
// search (access-service.memorySearch → searchAcrossNamespaces /
|
|
19410
|
+
// qmd.search) never routes through here, so expired-validity facts
|
|
19411
|
+
// remain findable by memory_search and `as_of` queries (escape
|
|
19412
|
+
// hatch: the as_of branch above also admits historically-valid
|
|
19413
|
+
// records). Gated off entirely when `temporalExpiredInInjection` is
|
|
19414
|
+
// set. Status-orthogonal: an `active` fact can be validity-expired;
|
|
19415
|
+
// a `superseded` one may still be within its window.
|
|
19416
|
+
if (
|
|
19417
|
+
!asOfActive &&
|
|
19418
|
+
this.config.temporalBiTemporal &&
|
|
19419
|
+
!this.config.temporalExpiredInInjection &&
|
|
19420
|
+
isValidityExpiredNow(memory.frontmatter, Date.now())
|
|
19421
|
+
) {
|
|
19422
|
+
biTemporalExpiredFilteredCount += 1;
|
|
19423
|
+
continue;
|
|
19424
|
+
}
|
|
19404
19425
|
|
|
19405
19426
|
if (
|
|
19406
19427
|
options?.allowDedicatedSurface !== true &&
|
|
@@ -19423,6 +19444,11 @@ export class Orchestrator {
|
|
|
19423
19444
|
`temporal supersession filter removed ${temporalSupersededFilteredCount} superseded candidates`,
|
|
19424
19445
|
);
|
|
19425
19446
|
}
|
|
19447
|
+
if (biTemporalExpiredFilteredCount > 0) {
|
|
19448
|
+
log.debug(
|
|
19449
|
+
`bi-temporal validity filter removed ${biTemporalExpiredFilteredCount} expired-validity candidates from injection (temporal.biTemporal on)`,
|
|
19450
|
+
);
|
|
19451
|
+
}
|
|
19426
19452
|
if (dedicatedSurfaceFilteredCount > 0) {
|
|
19427
19453
|
log.debug(
|
|
19428
19454
|
`dedicated surface filter removed ${dedicatedSurfaceFilteredCount} dream/procedural candidates from generic recall`,
|
package/src/storage.ts
CHANGED
|
@@ -321,6 +321,22 @@ function serializeFrontmatter(fm: MemoryFrontmatter): string {
|
|
|
321
321
|
// memories round-trip unchanged; readers default `valid_at` to `created`.
|
|
322
322
|
if (fm.valid_at) lines.push(`validAt: ${fm.valid_at}`);
|
|
323
323
|
if (fm.invalid_at) lines.push(`invalidAt: ${fm.invalid_at}`);
|
|
324
|
+
// Issue #1578 — bi-temporal ingestion provenance. Validate on write so a
|
|
325
|
+
// corrupt `observedAt` (non-ISO / overflowed) cannot leak onto disk; reads
|
|
326
|
+
// are permissive, mirroring the `valid_at` precedent. `eventTimeSource` is
|
|
327
|
+
// a closed enum — reject anything outside it rather than persisting garbage.
|
|
328
|
+
if (fm.observedAt) {
|
|
329
|
+
const validated = normalizeMemoryWriteTimestamp("observedAt", fm.observedAt);
|
|
330
|
+
if (validated) lines.push(`observedAt: ${validated}`);
|
|
331
|
+
}
|
|
332
|
+
if (fm.eventTimeSource) {
|
|
333
|
+
if (fm.eventTimeSource !== "extracted" && fm.eventTimeSource !== "assumed") {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`serializeFrontmatter: invalid eventTimeSource ${JSON.stringify(fm.eventTimeSource)} — expected "extracted" | "assumed"`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
lines.push(`eventTimeSource: ${fm.eventTimeSource}`);
|
|
339
|
+
}
|
|
324
340
|
if (fm.forgottenAt) lines.push(`forgottenAt: ${fm.forgottenAt}`);
|
|
325
341
|
if (fm.forgottenReason) lines.push(`forgottenReason: ${JSON.stringify(fm.forgottenReason)}`);
|
|
326
342
|
// Lifecycle policy fields
|
|
@@ -808,6 +824,12 @@ function parseFrontmatter(
|
|
|
808
824
|
// Issue #680 — explicit fact lifecycle round-trip.
|
|
809
825
|
valid_at: fm.validAt || undefined,
|
|
810
826
|
invalid_at: fm.invalidAt || undefined,
|
|
827
|
+
// Issue #1578 — bi-temporal ingestion provenance round-trip.
|
|
828
|
+
observedAt: fm.observedAt || undefined,
|
|
829
|
+
eventTimeSource:
|
|
830
|
+
fm.eventTimeSource === "extracted" || fm.eventTimeSource === "assumed"
|
|
831
|
+
? fm.eventTimeSource
|
|
832
|
+
: undefined,
|
|
811
833
|
forgottenAt: fm.forgottenAt || undefined,
|
|
812
834
|
forgottenReason: parseFrontmatterStringValue(fm.forgottenReason),
|
|
813
835
|
lifecycleState: (fm.lifecycleState as LifecycleState) || undefined,
|
|
@@ -3333,6 +3355,12 @@ export class StorageManager {
|
|
|
3333
3355
|
memoryKind?: MemoryFrontmatter["memoryKind"];
|
|
3334
3356
|
expiresAt?: string;
|
|
3335
3357
|
validAt?: string;
|
|
3358
|
+
// Issue #1578 — bi-temporal ingestion provenance. `observedAt` is the
|
|
3359
|
+
// ingestion time (when Remnic learned the fact); `eventTimeSource`
|
|
3360
|
+
// records whether `validAt` was resolved from an extracted expression
|
|
3361
|
+
// or assumed from the ingestion anchor. Both validate on serialize.
|
|
3362
|
+
observedAt?: string;
|
|
3363
|
+
eventTimeSource?: "extracted" | "assumed";
|
|
3336
3364
|
structuredAttributes?: Record<string, string>;
|
|
3337
3365
|
/**
|
|
3338
3366
|
* When provided, this string is used as the source for the fact-content
|
|
@@ -3365,6 +3393,10 @@ export class StorageManager {
|
|
|
3365
3393
|
const conf = options.confidence ?? 0.8;
|
|
3366
3394
|
const tier = confidenceTier(conf);
|
|
3367
3395
|
const validAt = normalizeMemoryWriteTimestamp("validAt", options.validAt);
|
|
3396
|
+
const observedAt = normalizeMemoryWriteTimestamp(
|
|
3397
|
+
"observedAt",
|
|
3398
|
+
options.observedAt,
|
|
3399
|
+
);
|
|
3368
3400
|
|
|
3369
3401
|
// Auto-set TTL for speculative memories
|
|
3370
3402
|
let expiresAt: string | undefined;
|
|
@@ -3398,6 +3430,8 @@ export class StorageManager {
|
|
|
3398
3430
|
sourceTurnId: options.sourceTurnId,
|
|
3399
3431
|
memoryKind: options.memoryKind,
|
|
3400
3432
|
valid_at: validAt,
|
|
3433
|
+
observedAt,
|
|
3434
|
+
eventTimeSource: options.eventTimeSource,
|
|
3401
3435
|
structuredAttributes: options.structuredAttributes,
|
|
3402
3436
|
};
|
|
3403
3437
|
if (options.status !== undefined) {
|