Skip to content

Add a pure is_load_bearing_memory(item) predicate for curated memory #427

Description

@hadamrd

Problem

The curated-memory compaction arc (epic #426) needs a single, authoritative
classifier deciding which MemoryItems compaction may never drop. Today no
such predicate exists in the memory package: the only load-bearing logic is
forge_loop.eventlog.models.is_load_bearing, which operates on EventKind, not
on curated MemoryItems. Without a mirror predicate, the upcoming compaction
step would have to inline ad-hoc kind == .../tag checks, scattering the
"what's sacred" rule across call sites and risking silent erasure of decisions.

Concrete example: a maestro resumes forge-loop after context loss. Its store
holds a SEMANTIC decision, a rejected-path-tagged item, a PROCEDURAL
validated skill, and dozens of EPISODIC per-issue failed/merged episodes.
Compaction should bound the episodic noise but keep all three load-bearing
items. There is currently no is_load_bearing_memory(item) to consult, so the
compaction arc (#426) has nothing to call.

Acceptance criteria

  • A pure function is_load_bearing_memory(item: MemoryItem) -> bool exists
    in src/forge_loop/memory/models.py, with no I/O and no store access.
  • Returns True for MemoryKind.SEMANTIC items (strategic decisions).
  • Returns True for MemoryKind.PROCEDURAL items (validated skills).
  • Returns True for any item whose tags contain REJECTED_PATH_TAG,
    regardless of kind (a rejected path is sacred even if filed differently).
  • Returns False for MemoryKind.EPISODIC items that are NOT
    rejected-path-tagged (the prunable per-issue failed/merged episodes).
  • The function has a docstring naming it the single source of truth for the
    compaction prune guard, mirroring eventlog.models.is_load_bearing.
  • is_load_bearing_memory is added to memory/__init__.py's __all__
    export list (alphabetical position) and importable as
    from forge_loop.memory import is_load_bearing_memory.
  • All existing tests still pass; ruff/type checks clean on touched files.

Test matrix

Unit tests (new file tests/test_memory_load_bearing.py, mirroring
tests/test_eventlog_load_bearing.py):

  • Parametrized table asserting a deliberate verdict for every MemoryKind:
    SEMANTIC -> True, PROCEDURAL -> True, EPISODIC -> False. Use a
    dict[MemoryKind, bool] keyed by every enum member and assert
    set(EXPECTED) == set(MemoryKind) so a future MemoryKind fails CI rather
    than silently inheriting a default.
  • A SEMANTIC decision item returns True.
  • A PROCEDURAL skill item returns True.
  • An EPISODIC item tagged ("failed",) returns False.
  • An EPISODIC item tagged ("merged",) returns False.

Adversarial / sad-path tests:

  • An EPISODIC item that ALSO carries REJECTED_PATH_TAG returns True
    (tag wins over prunable kind — the sacred-path rule must not be defeated by
    kind). This is the key adversarial case.
  • An item with empty tags=() is classified purely by kind.
  • A rejected-path-tagged item alongside an unrelated tag
    ((REJECTED_PATH_TAG, "axis:foo")) still returns True.

Integration: assert the public import path
from forge_loop.memory import is_load_bearing_memory resolves (guards the
__all__ wiring).

No e2e tests required — this is a pure leaf predicate with no runtime wiring
yet (the compaction arc that consumes it lands in a sibling issue under #426).

Out of scope

  • Do NOT implement the compaction arc, pruning loop, or any store mutation —
    this issue delivers only the classifier (those live elsewhere under Bound curated memory with a load-bearing-preserving compaction arc #426).
  • Do NOT add any I/O, DB access, or MemoryStore calls.
  • Do NOT change MemoryItem, MemoryKind, or REJECTED_PATH_TAG definitions.
  • Do NOT touch eventlog.models.is_load_bearing or its tests.
  • Do NOT introduce a "fail-safe unknown kind" branch — MemoryKind is a closed
    StrEnum with three members; enumerate them explicitly (a CI guard test
    catches any future addition). Keep net size ~40 LOC incl. tests.
  • Do NOT add config flags, CLI commands, or events.

File pointers

  • src/forge_loop/memory/models.py — add is_load_bearing_memory(item) here,
    near MemoryKind/REJECTED_PATH_TAG (lines ~13–21). This is the mirror of
    src/forge_loop/eventlog/models.py:167 (is_load_bearing).
  • src/forge_loop/memory/__init__.py — import the new symbol from
    memory.models (the existing import block, lines 7–17) and add it to
    __all__ (lines 40–54).
  • tests/test_memory_load_bearing.py — NEW test file; model its structure on
    tests/test_eventlog_load_bearing.py (the EXPECTED_VERDICTS table + edge
    classes pattern).

Original report

Parent: #426

Part of epic: "Bound curated memory with a load-bearing-preserving compaction arc"

Add a pure predicate in the memory package (mirroring eventlog/models.is_load_bearing) that classifies a MemoryItem as load-bearing vs prunable. Load-bearing = MemoryKind.SEMANTIC (decisions), any item carrying REJECTED_PATH_TAG, and MemoryKind.PROCEDURAL (validated skills). Prunable = MemoryKind.EPISODIC items (the per-issue 'failed'/'merged' episodes). No I/O, no store access — just the classifier the compaction arc will consume. Primary acceptance: a semantic decision, a rejected-path-tagged item, and a procedural skill each return True; an episodic 'failed' item returns False. ~40 net LOC incl. tests.

Customer story

A maestro resuming forge-loop after context loss needs its load-bearing decisions and rejected paths kept; this predicate is the single point that decides what compaction may never drop.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions