Skip to content

v0.121.0

Choose a tag to compare

@resoltico resoltico released this 22 Feb 01:09
· 80 commits to main since this release

Changed (BREAKING)

  • with_read_lock and with_write_lock decorators removed (DEAD-RWLOCK-DECORATORS-001):

    • Both decorators were unreachable dead code: absent from __all__ = ["RWLock"], not imported by any module in the codebase, not covered by any test
    • Deleted from runtime/rwlock.py; Callable and wraps imports removed alongside; the file now exports only RWLock
    • Callers must use the context-manager API directly: with lock.read(): / with lock.write():, which provide identical semantics without decorator indirection
    • Location: runtime/rwlock.py
  • ErrorCategory base changed from Enum to StrEnum (CLARITY-ERRORCATEGORY-STRENUM-001):

    • ErrorCategory(Enum) produced str(ErrorCategory.REFERENCE) == "ErrorCategory.REFERENCE"; .value access was required for serialization and log aggregation, and the str() output was not the plain value string
    • Changed to ErrorCategory(StrEnum): str(ErrorCategory.REFERENCE) == "reference"; direct equality ErrorCategory.REFERENCE == "reference" is now True (StrEnum inherits from str); .value and .name attributes are unchanged
    • Code comparing str(category) against "ErrorCategory.X" strings must update to compare against the plain value strings ("reference", "resolution", "cyclic", "parse", "formatting")
    • Location: diagnostics/codes.py

Fixed

  • TextElement expansion budget exhaustion abandoned entire parts accumulator (DEFECT-RESOLVER-TEXTELEMENT-BUDGET-001):

    • _resolve_pattern handled Placeable budget exhaustion by catching FrozenFluentError, appending the error, and breaking — preserving previously accumulated parts; TextElement budget exhaustion propagated the exception upward via track_expansion, abandoning the entire parts accumulator and silently discarding all previously resolved elements
    • Fixed: TextElement branch now checks context.total_chars > context.max_expansion_size after calling track_expansion, appends EXPANSION_BUDGET_EXCEEDED error, and breaks — symmetric behavior with the Placeable path; partial output is preserved regardless of which element type triggers the limit
    • Location: runtime/resolver.py
  • ResolutionContext._seen was injectable via dataclass __init__ (SECURITY-RESOLUTION-CONTEXT-SEEN-001):

    • _seen: set[str] = field(default_factory=set) was a public __init__ parameter; any caller could construct ResolutionContext(_seen={"target-message"}) to pre-populate the cycle-detection set, making context.contains("target-message") return True immediately and silently bypassing cycle detection for an arbitrary message ID without any error
    • Fixed: changed to field(init=False, default_factory=set); excluded from __init__; pre-population is structurally impossible
    • Location: runtime/resolution_context.py
  • Data race on _modified_in_context in FluentBundle and FluentLocalization (RACE-MODIFIED-IN-CONTEXT-001):

    • _modified_in_context was reset to False in __enter__ and read + reset in __exit__ without holding _rwlock; concurrent add_function and add_resource calls write _modified_in_context = True under a write lock; in Python 3.13 free-threaded mode __exit__ could observe a stale False and skip cache invalidation despite concurrent mutations having occurred
    • Fixed: __enter__ resets _modified_in_context under _rwlock.write() (bundle) or _lock.write() (orchestrator); __exit__ reads and resets the flag atomically under the same write lock, then performs cache invalidation outside the lock (cache operations are independently thread-safe)
    • Location: runtime/bundle.py, localization/orchestrator.py
  • _clear_tracebacks destroyed forensic audit trail on every resolver return (FORENSIC-CLEAR-TRACEBACKS-001):

    • _clear_tracebacks set err.__traceback__ = None on every FrozenFluentError before resolve_message returned; stack traces were unconditionally destroyed at the resolver boundary, making post-mortem incident analysis impossible unless the caller logged the exception before the error tuple was discarded; this contradicted the forensic intent of FrozenFluentError's BLAKE2b content hashing design
    • Fixed: _clear_tracebacks deleted; all six return (result, _clear_tracebacks(errors)) call sites changed to return (result, tuple(errors)); tracebacks are preserved for the full lifetime of the error objects
    • Location: runtime/resolver.py

Changed

  • ResolutionContext.track_expansion no longer raises on budget exceeded (ARCH-TRACK-EXPANSION-SOC-001):

    • track_expansion both mutated _total_chars and generated FrozenFluentError via ErrorTemplate, coupling state tracking to error policy in an infrastructure object; the resolver already had a pre-loop budget check that generated the same diagnostic via different code, making budget exhaustion handling inconsistent across TextElement and Placeable paths
    • track_expansion(char_count) now only mutates _total_chars and never raises; callers check context.total_chars > context.max_expansion_size after each call and generate FrozenFluentError directly; error construction is unified in _resolve_pattern for both element paths
    • Location: runtime/resolution_context.py, runtime/resolver.py
  • IntegrityCache._lock changed from RLock to Lock (PERF-CACHE-LOCK-001):

    • IntegrityCache._lock was threading.RLock(); no call path in IntegrityCache acquires the lock re-entrantly — every public method acquires at entry and _audit is documented to run while the lock is already held without re-acquiring; RLock tracked the owning thread ID and acquire count on every acquisition, adding measurable overhead in the hot path of format_pattern()
    • Changed to threading.Lock(); lock semantics unchanged for all current call paths; any future inadvertent re-entry would raise RuntimeError immediately rather than silently succeeding
    • Location: runtime/cache.py
  • GlobalDepthGuard thread-spawning limitation documented (CLARITY-DEPTH-GUARD-THREAD-LIMIT-001):

    • The class docstring documented protection against same-thread re-entry via ContextVar but did not document the known limitation: custom functions that spawn new threads bypass the guard entirely — each new thread receives ContextVar default (depth 0) and can initiate a full-depth resolution chain independent of the spawning thread's guard state
    • Added explicit "Thread Spawning Limitation" section to the docstring; no code changes; the limitation is an accepted consequence of ContextVar semantics
    • Location: runtime/resolution_context.py
  • resolve_message except clause comment corrected (CLARITY-RESOLVER-EXCEPT-COMMENT-001):

    • The except FrozenFluentError block carried the comment "Global depth exceeded — collect error and return fallback"; the block catches all FrozenFluentError instances regardless of DiagnosticCode — including EXPANSION_BUDGET_EXCEEDED and EXPRESSION_DEPTH_EXCEEDED from the expression depth guard — not only global depth violations; the misleading comment could cause developers to incorrectly narrow the guard's scope during maintenance
    • Replaced with: "Resolution limit exceeded (global depth, expression depth, or expansion budget). Collect error and return fallback — prevents partial output from reaching the caller."
    • Location: runtime/resolver.py