Skip to content

symfonic.core.prompt.blocks.memory_lane_facts

memory_lane_facts

Turning one scanned graph node into the facts it carries.

The node-to-fact half of :mod:symfonic.core.prompt.blocks.memory_lane. That module owns the scan -- which nodes to read and what revision the result carries; this one owns the reading of a single node: which of its parts carry a statement, in what order, and with whose provenance.

Provenance a node did not record stays None. Nothing here invents a source or a timestamp, and nothing here ranks: :func:_scan_order is a deterministic sort key, not a score, so two scans over an unchanged lane emit the same bytes and an unchanged block keeps the cached prompt prefix.

node_to_facts

node_to_facts(node: Any, prefix: str, profile_fields: frozenset[str]) -> list[BlockFact]

The node's label fact, then one fact per declared profile field.

Fields are emitted in sorted order so two scans of an unchanged lane still produce identical bytes -- the property that lets an unchanged block keep the cached prompt prefix. Set iteration order would not.

Each field carries its own provenance when the writer recorded one under _field_provenance. A role corrected today must not inherit the onboarding form's timestamp from three months ago: the node-level stamp describes when the node was written, and once one node carries several independently-sourced facts that is no longer the same question as when each fact was recorded. Falling back to the node-level stamp keeps a node written before this existed rendering exactly as it did.

Source code in src/symfonic/core/prompt/blocks/memory_lane_facts.py
def node_to_facts(
    node: Any, prefix: str, profile_fields: frozenset[str]
) -> list[BlockFact]:
    """The node's label fact, then one fact per declared profile field.

    Fields are emitted in sorted order so two scans of an unchanged lane
    still produce identical bytes -- the property that lets an unchanged
    block keep the cached prompt prefix. Set iteration order would not.

    Each field carries **its own** provenance when the writer recorded
    one under ``_field_provenance``. A role corrected today must not
    inherit the onboarding form's timestamp from three months ago: the
    node-level stamp describes when the node was written, and once one
    node carries several independently-sourced facts that is no longer
    the same question as when each fact was recorded. Falling back to
    the node-level stamp keeps a node written before this existed
    rendering exactly as it did.
    """
    facts = []
    label_fact = _node_to_fact(node, prefix)
    if label_fact is not None:
        facts.append(label_fact)

    properties = _properties_of(node)
    provenance = properties.get("_field_provenance")
    provenance = provenance if isinstance(provenance, dict) else {}

    for field in sorted(profile_fields):
        if field not in properties:
            continue
        raw = properties[field]
        if not isinstance(raw, str):
            # A non-string profile value has no honest one-line rendering
            # and str() would happily print a dict or a list of them.
            continue
        value = _WHITESPACE_RUN.sub(" ", raw).strip()
        if not value:
            continue
        recorded = provenance.get(field)
        recorded = recorded if isinstance(recorded, dict) else {}
        facts.append(
            BlockFact(
                value=f"{field}: {value}",
                source=_source_of(recorded) or _source_of(properties),
                recorded_at=(
                    _recorded_at(node, recorded)
                    if "recorded_at" in recorded
                    else _recorded_at(node, properties)
                ),
            )
        )
    return facts

scan_order

scan_order(node: Any) -> tuple[str, str, str]

Deterministic scan order: label, then recorded time, then id.

Not relevance -- there is no score to order by, and that is the point. Two scans over an unchanged lane emit the same bytes, so an unchanged block does not invalidate the cached prompt prefix.

Review fix (LOW, per-task t7-resolver): the tiebreak used to be node.created_at -- the row-creation time -- while :func:_node_to_fact resolves the displayed provenance from properties['recorded_at'] first, falling back to created_at only when the node recorded nothing. Two nodes sharing a label sort by row-creation order here but can carry recorded_at values in the opposite order, so the rendered facts read out of chronological sequence even though both this function's own docstring and the module's provenance rule call for "recorded time". Reusing :func:_recorded_at -- the same resolution :func:_node_to_fact performs -- keeps the two in agreement.

Source code in src/symfonic/core/prompt/blocks/memory_lane_facts.py
def scan_order(node: Any) -> tuple[str, str, str]:
    """Deterministic scan order: label, then recorded time, then id.

    Not relevance -- there is no score to order by, and that is the
    point. Two scans over an unchanged lane emit the same bytes, so an
    unchanged block does not invalidate the cached prompt prefix.

    Review fix (LOW, per-task t7-resolver): the tiebreak used to be
    ``node.created_at`` -- the row-creation time -- while
    :func:`_node_to_fact` resolves the *displayed* provenance from
    ``properties['recorded_at']`` first, falling back to ``created_at``
    only when the node recorded nothing. Two nodes sharing a label sort
    by row-creation order here but can carry ``recorded_at`` values in
    the opposite order, so the rendered facts read out of chronological
    sequence even though both this function's own docstring and the
    module's provenance rule call for "recorded time". Reusing
    :func:`_recorded_at` -- the same resolution :func:`_node_to_fact`
    performs -- keeps the two in agreement.
    """
    properties = _properties_of(node)
    return (
        str(getattr(node, "label", "") or ""),
        _stamp(_recorded_at(node, properties)),
        str(getattr(node, "id", "") or ""),
    )