fix(core): honour or refuse a stated timezone in normalize_date_to_iso (#252) - #259
Merged
Conversation
`created_at` values carrying a timezone were persisted as the wrong instant, silently. Three defects stacked on the same path (#252): 1. the "already ISO" guard was the substring test `"T" in raw`, and every US zone abbreviation contains a T — `8 May 2023 13:56 EST` was returned unparsed; 2. the built-in fast path matches the date at the START of the string and discards the rest, so `8 May 2023 13:56 +02:00` became midnight — time and offset both dropped; 3. on the dateutil path an unresolvable abbreviation is dropped behind an `UnknownTimezoneWarning` nobody reads, leaving a naive datetime that PostgreSQL's `timestamptz` cast and `compute_recency_boost` then read as UTC. Root cause: the function had no timezone policy at all. It now has one. `core/temporal_timezones.RFC5322ZoneResolver` is the `tzinfos` resolver dateutil parses under: a numeric offset passes through, an RFC 5322 §4.3 obs-zone abbreviation resolves through the cited table (the normative answer to "which EST?", cross-checked against CPython's `email._parseaddr._timezones`), and anything else is recorded and the value REFUSED with a warning naming the input, the abbreviation, the resolvable set and the fix. RFC 5322's own "read an unknown zone as -0000" default is deliberately not applied: storing that as an instant is the defect. Supplying a resolver also removes dateutil's host-dependent fallback (an abbreviation matching the machine's local zone name) and stops the warning being raised at all, so no `warnings.catch_warnings()` — which is process-global and not thread-safe — is taken on a store write path. The ISO guard is now an anchored ISO 8601 §5.4.2 pattern, and the date-only fast path is used only for strings that state no time of day; a string whose time is malformed salvages its date and says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
Three follow-ons to the #252 fix, each found by a check rather than by reading: * CI proved the parser was never there. `python-dateutil` was not a declared dependency and is absent from every `requirements/ci-*.txt`, so all four Test jobs ran with it missing — the dateutil branch of `normalize_date_to_iso` was dead code in CI, and any install that resolved without it silently kept dates it could not read (the LoCoMo shape the docstring itself cites). Declared, locked, and re-exported into the 9 hash-pinned requirement sets. * The guarantee now holds on the degraded path too. When the parse produces nothing and the string STATES a zone, the value is refused instead of salvaged: salvaging returns a naive date, and a naive date is read as UTC by exactly the consumers this issue is about. A string with no zone still salvages its date, as before. * `normalize_date_to_iso` moves to `core/temporal_normalize.py`. Storage normalization ("which instant does this string denote?") and retrieval scoring ("how close is this date to the query?") have different reasons to change — and the fix had pushed temporal.py to 302 lines, past this repo's 300-line cap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
The versions plugin installs bootstrap changed; that is observable from outside, so it belongs in the changelog rather than only in the commit that made it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
The store-level regression test for #252 failed on the first run, which is the point of writing one: the parser fix never reached the store. Both `PgMemoryStore._build_insert_params` and `SqliteMemoryStore._insert_memory_rows` guarded the normalization call with `"T" not in raw_created` — the same substring test as the "already ISO" guard the issue is about, one layer up, and one every US zone abbreviation trips. `8 May 2023 13:56 EST` was written to `memories.created_at` verbatim regardless of what normalize_date_to_iso did with it. The guard is removed from both sites rather than corrected in both: deciding whether a string is already ISO is normalize_date_to_iso's job, and it returns a real ISO datetime unchanged. sqlite_store's ImportError-guarded function-level import goes with it — pg_store has always imported the same first-party module at top level, so the "optional feature" it claimed to probe was never optional. Coverage now lands where the issue states the defect — on the stored row, on both backends, with a parity test asserting the two agree: tests_py/infrastructure/test_sqlite_backend.py ::TestCreatedAtTimezoneIsPersisted (4 tests, real store) tests_py/infrastructure/test_pg_store_created_at_timezone.py ::TestCreatedAtTimezone / ::TestBackendParity (11 tests, no PG needed) Restoring either guard fails 4 of them. I2's canonical-heat-writer allow-list is pinned by file:line and both edits shifted the sites below them (+5 / +2); re-pinned with the reason, same six writers, no new ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
The CI-SQLite job installs no psycopg, and `pg_store` hard-imports it at
module level, so importing the new test module interrupted collection
there — the test needs no live database, but it cannot import its
subject without the driver.
`pytest.importorskip("psycopg")` ahead of the import. That job still
asserts the SQLite half through test_sqlite_backend.py, and the
cross-backend parity assertion runs in the four PostgreSQL matrix jobs.
Verified in the driverless condition rather than assumed:
$ python -c "import sys; sys.modules['psycopg']=None; \
import pytest; pytest.main(['-q', '<the file>'])"
1 skipped in 0.01s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…5 migration Rebase-only bookkeeping; no behaviour change. Three commits were dropped as already upstream — #257 landed the launcher pin realignment and its test_launcher_pins_match_lock.py check against the CORRECT lock, and the test-count re-advertisement was superseded. The requirements/*.txt and launcher pins are taken from main, NOT from this branch: this branch predates #257 and still carried transformers==4.57.6 and huggingface-hub==0.36.2, so resolving toward it would have silently reverted the security fix. Regenerated from uv.lock instead, which keeps transformers==5.14.1 in all 9 files while adding the python-dateutil>=2.8.2 this branch legitimately declares. Test count: this machine collects 6426 on main where main advertises 6439, so the local absolute is NOT trustworthy (CI's environment collects differently). Applied the measured DELTA instead — paired collection, same venv, same moment: main 6426 -> this branch 6526, i.e. +100 -> 6539. CI's own gate is the arbiter and will fail loudly with the true number if this is off.
cdeust
force-pushed
the
fix-normalize-date-tz-252
branch
from
July 29, 2026 18:57
9fdb0c9 to
a539c36
Compare
CI rejected 6539: "assets/badge-tests.svg: tests badge says 6539, canonical is 6526". I derived 6539 as main's advertised 6439 plus a locally measured +100, on the assumption that this machine's collection does not match CI's. That assumption was wrong in this direction: the local collection of THIS branch (6526) matches CI's canonical exactly. What is actually stale is main's advertised 6439 — main under-reports its own suite, which is a separate defect in main, not a reason to offset from it. Using the measured absolute.
cdeust
added a commit
that referenced
this pull request
Jul 29, 2026
Rebase bookkeeping after #259 merged; no behaviour change. The earlier rebase resolved the six suite-size files to main's side wholesale, which deleted 49 lines of this branch's own documentation — including the '### Reproducing the pyright gate locally' section that tests_py/scripts/test_typecheck_env_parity.py asserts, failing 5 of its 9 cases. This time the branch's doc additions are re-applied as a PATCH against the new main (git apply --3way) so both sides' prose survives; the only conflicts were the hard-coded count, resolved to the current figure. Count is the measured absolute (6548), not a delta from main's advertised number: CI proved on #259 that this machine's collection matches CI's canonical exactly, so the delta arithmetic I used before was the wrong instrument. Verified: test_typecheck_env_parity.py 9 passed; check_doc_claims.py OK; generate_repo_badges.py --check OK; .bestpractices.json parses and carries no conflict markers (the class that shipped once before, now gated by check_no_conflict_markers).
cdeust
added a commit
that referenced
this pull request
Jul 29, 2026
…r().parse(bytes) works (#253) (#261) * fix(deps): pin tree-sitter-language-pack to the range where get_parser().parse(bytes) works (#253) The pyright gate's verdict depended on which installer built the environment: CI installs the hash-pinned export of uv.lock, the documented developer install resolved pyproject.toml's >=0.24.0,<1.14 range, and the two landed seven minor versions apart on tree-sitter-language-pack — one error in one environment, zero in the other, on the same commit. Probing every published wheel found a live defect the old floor admitted: 1.9.0-1.12.2 return a builtins.Parser whose parse(source: str) rejects bytes, so get_parser("python").parse(b"...") raises TypeError uncaught and codebase_analyze crashes. The floor moves to 1.12.5, the first release where the whole call chain is valid and type-checkable. - pyproject: floor 1.12.5, with the measured per-release type surface - ast_parser: _EXTRACTORS keyed by the pack's SupportedLanguage literal, AST_SUPPORTED derived from it (deleting an unreachable fallback arm), _is_ast_language TypeGuard narrows before get_parser - CONTRIBUTING/CLAUDE: both documented installs resolve from uv.lock - ci.yml: the Type Check job prints the versions its verdict depends on - tests: language contract (parses with every declared grammar) and typecheck-env parity (docs vs pip_constraint_sets vs ci.yml) Boy-scout (§14): four pre-existing shellcheck findings in the touched ci.yml (SC2015 x3, SC2034) fixed; the suite's 3 sqlite datetime DeprecationWarnings are outside this change's blast radius and filed as Closes #253 Closes #249 Closes #251 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u * chore: re-apply this branch's documentation over main and advertise 6548 Rebase bookkeeping after #259 merged; no behaviour change. The earlier rebase resolved the six suite-size files to main's side wholesale, which deleted 49 lines of this branch's own documentation — including the '### Reproducing the pyright gate locally' section that tests_py/scripts/test_typecheck_env_parity.py asserts, failing 5 of its 9 cases. This time the branch's doc additions are re-applied as a PATCH against the new main (git apply --3way) so both sides' prose survives; the only conflicts were the hard-coded count, resolved to the current figure. Count is the measured absolute (6548), not a delta from main's advertised number: CI proved on #259 that this machine's collection matches CI's canonical exactly, so the delta arithmetic I used before was the wrong instrument. Verified: test_typecheck_env_parity.py 9 passed; check_doc_claims.py OK; generate_repo_badges.py --check OK; .bestpractices.json parses and carries no conflict markers (the class that shipped once before, now gated by check_no_conflict_markers). --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #252.
What was wrong
normalize_date_to_isohad no timezone policy on any path, and threedefects stacked on top of that to make a stated zone vanish:
8 May 2023 13:56 EST'8 May 2023 13:56 EST'— returned unparsed, because the "already ISO" guard was the substring test"T" in rawandEST/PST/CST/MSTall contain a T2023-05-08T13:56:00-05:008 May 2023 13:56 +02:002023-05-08T00:00:00— the fast path matches the date at the start and discards the rest, so time and offset were dropped2023-05-08T13:56:00+02:008 May 2023 13:56 XYZ2023-05-08T00:00:00, silentlyNone+ a warning naming the abbreviationMeasured on
origin/main@ c1f449f withpython -W error::Warning.And the same substring test again, one layer up. Both write paths guarded
the call with
"T" not in raw_createdas a cheap "is it already ISO?" test:So
8 May 2023 13:56 ESTreachedmemories.created_atverbatim no matter whatnormalize_date_to_isodid with it. The store-level regression test failed onits first run for exactly this reason — which is why the issue's guarantee is now
asserted on the stored row and not only on the parser. The guard is deleted from
both sites rather than corrected twice: deciding what is already ISO belongs to
the one function that owns it.
And the parser was not installed. The first CI run on this PR failed in all
four Test jobs with
ModuleNotFoundError: No module named 'dateutil':python-dateutilwas never a declared dependency and is absent from everyrequirements/ci-*.txt, so the dateutil branch — the one the docstring's ownexample (
1:56 pm on 8 May, 2023, LoCoMo) depends on — had been dead code inCI since it was written, and any install resolving without it kept dates it
could not read. That is fixed here rather than logged (§15): the dependency is
declared, locked, and re-exported into the 9 hash-pinned requirement sets.
The fix (root cause, §6)
mcp_server/core/temporal_timezones.py— atzinfosresolver dateutilparses under:
+02:00,UTC,GMT,Z);the normative answer to "which EST?" (EST ≡ −0500), cross-checked row for row
against CPython's
Lib/email/_parseaddr.py::_timezones;input, the abbreviation, the resolvable set, the consequence avoided and the
fix. RFC 5322's own "read an unknown zone as
-0000" default is deliberatelynot applied — persisting that as an instant is the defect.
Supplying a resolver also (a) disables dateutil's host-dependent fallback to the
machine's local zone name, so parsing is identical on every machine, and (b)
means
UnknownTimezoneWarningis never raised, so nowarnings.catch_warnings()on a store write path (criterion 3) — verified by an AST assertion, not a grep.
One rule, applied everywhere: a stated zone is honoured or the value is
refused. That includes the degraded path — if the parse produces nothing and
the string states a zone, the date is not salvaged, because salvaging returns a
naive date and a naive date is exactly what gets read back as UTC. A string that
states no zone still salvages its date, and says so.
mcp_server/core/temporal_normalize.py—normalize_date_to_isomoves outof
temporal.py. Storage normalization ("which instant does this stringdenote?") may not lose precision; retrieval scoring ("how close is this date to
the query?") may. Different reasons to change, and the fix had pushed
temporal.pyto 302 lines, past this repo's 300-line cap. Both stores importfrom the new path. Final sizes:
temporal.py205,temporal_normalize.py132,temporal_timezones.py96.The ISO passthrough guard is now an anchored ISO 8601-1:2019 §5.4.2 pattern, and
the date-only fast path runs only for strings that state no time of day.
Acceptance criteria (#252)
tzinfosmapping, or is rejected — never re-anchored to UTCtest_est_resolves_to_the_rfc5322_offset,test_est_denotes_the_right_instant(asserts the instant is 18:56Z, not 13:56Z),test_unresolvable_abbreviation_is_refused; and on the parser-less pathtest_zone_bearing_date_is_refused_not_flattened,test_numeric_offset_is_refused_tootest_refusal_signal_is_actionable_verbatimasserts the rendered message whole;test_resolvable_abbreviation_is_quietandtest_fast_path_date_is_stored_without_a_degradation_signalassert the nominal paths stay silentwarnings.catch_warnings()on a store write pathtest_no_warnings_filter_mutation_on_the_write_path(AST walk over both modules — prose may still explain the ban),test_parse_emits_no_python_warning(no Python warning is raised at all)(Run before the split, when the tests still lived in
test_temporal.py;test_refusal_names_the_offending_inputwas then renamedtest_refusal_signal_is_actionable_verbatimas its assertion tightened to thewhole message, to kill the log-literal mutants.)
Completion Ledger
Path → test (every branch introduced by the diff)
normalize_date_to_iso: blank / whitespace-only →NoneTestNormalizeDateToIso::test_emptynormalize_date_to_iso: ISO 8601 date-time → verbatim passthroughtest_iso_datetime_passes_through_verbatim,test_iso_datetime_without_seconds_keeps_its_timenormalize_date_to_iso: no time of day, fast path hitstest_date_only_fast_path,test_fast_path_date_is_stored_without_a_degradation_signalnormalize_date_to_iso: no time of day, fast path misses → zone policytest_date_only_string_the_fast_path_missesnormalize_date_to_iso: time of day present → zone policytest_locomo_format_keeps_its_time_and_stays_naive,test_numeric_offset_is_preserved_parse_with_resolver:ImportErrorarm (parser absent)test_missing_dateutil_is_reported_not_swallowed(whole message),test_missing_dateutil_still_salvages_a_date,TestNormalizeDateToIsoWithoutDateutil(4 tests)_parse_with_resolver: parse succeedstest_est_resolves_to_the_rfc5322_offset_parse_with_resolver:ValueError/OverflowErrorarmtest_nothing_salvageable_returns_none_quietly,test_parser_overflow_is_not_propagated_normalize_under_zone_policy: unresolved abbreviation → warn +Nonetest_unresolvable_abbreviation_is_refused,test_refusal_signal_is_actionable_verbatim_normalize_under_zone_policy: parsed → isoformattest_rfc2822_style_with_abbreviation,test_utc_is_preserved_normalize_under_zone_policy: stated zone that could not be applied → warn +Nonetest_zone_bearing_date_is_refused_not_flattened(whole message),test_numeric_offset_is_refused_too_normalize_under_zone_policy: salvage succeeds → warn + date-onlytest_unparseable_time_of_day_salvages_the_date_and_says_so,test_zoneless_string_still_salvages_its_date_normalize_under_zone_policy: salvage fails →None, quiettest_nothing_salvageable_returns_none_quietly,test_unparseable_STATED_ZONE_RE(decides refuse vs salvage)TestStatedZoneDetection::test_zone_bearing_strings_are_detected,::test_zoneless_strings_are_notRFC5322ZoneResolver.__call__: numeric offset passes throughtest_numeric_offset_wins_over_the_table,test_zero_offset_is_not_confused_with_absent,test_offset_wins_even_for_an_unknown_nameRFC5322ZoneResolver.__call__: no zone token at all(None, None)test_no_zone_at_all_is_not_an_unresolved_zoneRFC5322ZoneResolver.__call__: table hit (all 10 rows)test_each_rfc5322_row(parametrised over an independently transcribed RFC table),test_abbreviation_is_case_insensitiveRFC5322ZoneResolver.__call__: table miss → recordedtest_unknown_abbreviation_is_recorded,test_recorded_name_keeps_its_original_case,test_zones_outside_rfc5322_are_refusedtest_state_is_per_instanceRFC5322_ZONE_NAMES(operator-facing set)test_exported_names_match_the_tablepg_store._build_insert_params: free-formcreated_at→ normalized bind valuetest_pg_store_created_at_timezone.py::TestCreatedAtTimezone(6 tests: offset, instant, refusal, ISO passthrough, LoCoMo shape, and theheat_base_set_atanchor derived from it)pytest.importorskip("psycopg")—pg_storehard-imports the driver, and the CI-SQLite job has none; verified in the driverless condition (sys.modules['psycopg']=None→1 skipped), not assumedsqlite_store._insert_memory_rows: same, on the stored rowtest_sqlite_backend.py::TestCreatedAtTimezoneIsPersisted(4 tests, against a real store)test_pg_store_created_at_timezone.py::TestBackendParity::test_sqlite_and_postgresql_agree(parametrised over 5 inputs)_BASE_PACKAGES/_ML_PACKAGESpins are versionsuv.lockrecordsTestLauncherPinsMatchTheLock::test_base_packages_are_the_locked_versions,::test_ml_packages_are_the_locked_versions, with::test_the_oracle_would_notice_a_drifted_pinas the negative control_NUMPY_VERSION's three marker branchesTestLauncherPinsMatchTheLock::test_the_date_parser_is_part_of_the_base_runtimepython-dateutildeclared → present in every install setscripts/generate_pip_constraints.py --check→requirements OK (13 checked); CI's own Test jobs are the end-to-end proof (they failed without it)§13.1 checklist
A. Correctness — A1 happy paths run end-to-end, output quoted. A2 edge cases:
empty, whitespace-only, ISO with/without seconds/offset/
Z, date-only,date+time, date+time+offset, date+time+resolvable abbreviation,
date+time+unknown abbreviation, malformed time-of-day, unparseable string, and
each of those again with the parser absent — all mapped above. A3 every failure
path asserts its observable signal, including the emission itself (four
warning sites, each asserted by its rendered message). A4 input is free-form text
from producers, validated at this store boundary; both callers guard with
isinstance(raw_created, str). A5 the function is pure and total — returns anISO string or
None, never raises, never partially writes. A6 idempotent: analready-ISO value passes through unchanged (
test_iso_datetime_passes_through_verbatim).B. Concurrency — no concurrency touched. Positively: the change removes the
only thread-unsafe option on this path by refusing
warnings.catch_warnings()(criterion 3), and the resolver's state lives on a per-parse instance
(
test_state_is_per_instance), shared with nothing.C. Resources — no new loops, collections or I/O. One small object per parse;
three module-level compiled regexes. The date-only fast path still short-circuits
before the dateutil import for the strings it handles.
D. Security — D1/D2 N/A: no query, command or path is built here; the input is
already-untrusted free text and the output is either
datetime.isoformat()orNone. D3 no secrets. One supply-chain note: the new dependency is hash-pinned inall 9 exported sets, per the repo's
--require-hashesrule.E. Interfaces — E1 the signature and return contract are unchanged
(
str -> str | None); the values change, which is the fix. The import pathchanges, which is why both consumers move in the same commit. E2 consumers
identified by name and read:
pg_store._build_insert_params,sqlite_store._insert_memory_rows(bothnormalize_date_to_iso(raw) or raw_created— a refusal falls back to the producer's literal text, thepre-existing behaviour for every unparseable date, which preserves information
instead of inventing an instant); benchmark producers checked — LoCoMo's
1:56 pm on 8 May, 2023is unchanged (test_locomo_format_keeps_its_time_and_stays_naive),LongMemEval never reaches this code (
_e2_loaders.pyuses its ownparse_longmemeval_date). E3 no persisted format change; stored ISO values stillpass through verbatim. E4 platform-independent; the one version-sensitive line is
datetime.fromisoformat, covered under mutation below.F. Observability — F1 all four degraded/failure modes emit a warning naming
the offending input, each asserted whole; the nominal paths are asserted
silent. F2 the degraded modes are named in the messages ("this value was NOT
normalized", "storing the date alone …, so its time is lost", "only date-only
formats are parseable").
G. Tests — G1 ledger complete. G2 the regression tests fail on the pre-fix
code (14, quoted). G3 deterministic: no sleeps, no shared paths, no order
dependence; the only clock-dependent tests compare against the formula with a
tolerance. G4 negative assertions present (
caplog.records == []on quiet paths,raised == []for Python warnings,not _STATED_ZONE_RE.search(...)). G5 fullsuite + lint + type check quoted below.
H. Quality — H1 SOLID/layering: both new modules are pure core (stdlib +
core), zero I/O; sizes 205 / 132 / 96 lines, longest new function 36 lines (under both the repo's 40-line and the standard's 50-line cap), ≤ 2
parameters. H2 no dead code (
_ISO_DATETIME_MIN_LENremoved with its guard), nodebug leftovers. H3 conventions match the neighbouring core modules; one test
module per source module. H4 CHANGELOG entries added;
docs/module-inventory.mdgains both modules and its measured counts are refreshed. H5 three commits: the
fix, the mechanical doc-count bump, the split + dependency. H6 CI status is
reported on this PR, not asserted here. H7 boy-scout below.
Mutation testing (§12)
scripts/mutation_check.sh, scoped to each changed file:pg_store.pyandsqlite_store.pywere not put through a full-file mutationrun: each is >1000 lines and the change to them is a deleted guard clause, so the
run would spend its time on a pre-existing backlog outside these lines (§12.1
scopes the blocking tier to the lines the change touched). The changed lines
carry the strongest form of the same evidence instead — the pre-change code IS
the mutant, and the new tests kill it:
scripts/launcher_deps.pycould not be mutated at all — mutmut refuses when thetests import the subject under a synthetic module name, which every
tests_py/scripts/module does. Reported, not worked around: filed as #262(see boy-scout below). The change there is a pin table whose contract is the new
uv.lockparity test, and that test's own detection power is demonstrated — itcaught 7 drifted pins on its first run, and carries a negative control.
0 survivors on the new code. The first scoped run reported 62 survivors:
24 in the new code (killed here — the log-message literals needed whole-message
assertions and the fast path needed a quiet-path assertion) and 38 pre-existing
ones in
temporal.py's untouched scoring functions, treated in this PR ratherthan deferred (§14). The 7 that remain are triaged, not ignored:
parse_date__mutmut_9/10,compute_recency_boost__mutmut_14/15— mutate the"Z"needle in.replace("Z", "+00:00"). Killed on Python 3.10 (a CI matrixjob), equivalent on ≥ 3.11 where
fromisoformataccepts the UTC designatornatively. Verified on the actual 3.10 interpreter rather than asserted:
test_space_separated_utc_designatorandtest_utc_designator_is_acceptedkillthem there (§12.3: pin the contract for the config where the line is
load-bearing).
compute_date_distance_score__mutmut_2(or→andin the guard) —provably equivalent: with a falsy
doc_date_str,parse_datereturnsNoneand the next guard returns
0.0; with emptytarget_date_hintsthe loop bodynever runs and
best_scoreis0.0. Both operands reach the same return inevery case.
compute_recency_boost__mutmut_28/30(< 0→<= 0,> cutoff→>= cutoff) — equivalent under a wall clock: they differ only whenage_daysis exactly0.0or exactlycutoff_days, andnowis sampled insidethe function at microsecond resolution, so neither is constructible from a test.
Killing them needs a clock seam, which would take the function to five
parameters (§4.4 caps it at four) and change a public core API used by
handlers/recall_helpers.pyand the benchmarks — out of proportion to aboundary no caller can reach.
§14 boy-scout — defects seen in touched material, all fixed here
"T" in rawsubstring guard (defect 1) — not part of normalize_date_to_iso silently re-anchors an unresolvable timezone abbreviation to UTC #252's stateddiagnosis, but on the same path, and the reason the issue's own reproduction
returns the input unchanged rather than a UTC value on current
main.ImportErrorarm returnedNonewith no signal at all — now reported.python-dateutilwas undeclared and missing from every CI install set —found by this PR's own first CI run, declared and pinned here.
refused now, so the guarantee holds without the optional parser too.
temporal.pycrossed the repo's 300-line cap (302) — split along theconcern boundary rather than trimmed.
temporal.py's scoring functions —killed (above), not deferred.
docs/module-inventory.md's measured layer counts had drifted (core 220 → 225,infrastructure 87 → 91) — re-measured and re-dated.
"T" not in raw_createdguard — the third instance of thesubstring defect, and the one that made the other two invisible from
outside. Found by writing the store-level test the issue's wording calls
for; removed rather than duplicated-and-corrected.
sqlite_store'stry: from ... import normalize_date_to_iso / except ImportError: pass— a first-party module is not an optional feature, andpg_storehad always imported the same one at module top. Hoisted; thedead arm is gone.
scripts/launcher_deps.py's pin table — which plugin installs bootstrapfrom — had drifted from
uv.lockon 7 of its 9 entries, includingpgvector 0.4.2against a locked0.5.0whose psycopg loader changepg_store._vector_to_bytesis written for, and anumpymarker split thetwo-branch constant no longer covered. Every
# source: uv.lockcommentthere was a claim nothing verified; a parity test against the lock now
fails on drift (it is what found this), and the pins are realigned.
docs/deployment-scenarios.mdrestated threecontainer versions (
torch==2.11.0against a locked 2.13.0) for an imagethat installs the hashed export instead, and
pyproject.toml's own commentclaimed
ST 5.4.1against a locked 5.6.1. Both now point at the lock.tests_py/invariants/test_I2_canonical_writer.pypins the canonicalheat-writer sites by
file:line; both store edits shifted the six sitesbelow them (+5 / +2). Re-pinned with the reason recorded, same six
writers, no new ones — the test was the oracle, as its own comments
describe for every prior re-pin.
site the doc-claim gate scans, plus the committed badge it derives.
One deferral, filed rather than described (§14.3): #262 — mutation testing
cannot attribute mutants for ANY module under
scripts/, because everytests_py/scripts/test path-imports its subject under a synthetic module name(
_cortex_launcher_deps), so mutmut matches no trampoline key. It fails loudlyrather than reporting a false zero, but the §12 gate is absent for that tree.
Fixing it is a test-infrastructure change across two test modules with its own
blast radius, outside this change's; the issue carries a reproduction and
acceptance criteria.
Verification run locally
Local numbers only — this machine has a PostgreSQL that CI's SQLite job does not,
and (until this PR) a
python-dateutilthat CI did not. CI's own numbers are theones that decide; see the checks on this PR.