refactor(evidence): unify formulas with typed evidence

This commit is contained in:
2026-08-25 01:23:04 +02:00
parent d5d65f3659
commit cc30148b69
11 changed files with 342 additions and 38 deletions
+8
View File
@@ -348,6 +348,14 @@ Prerequisite: Phase 3 closed.
"<concept>"` (or derive it from the evidence/context), present it, and let the "<concept>"` (or derive it from the evidence/context), present it, and let the
reviewer approve/reject (`concept_formula_approved`/`concept_formula_rejected`). reviewer approve/reject (`concept_formula_approved`/`concept_formula_rejected`).
Reflect the approved formula in `schema_linking.json` (`concept_formulas`). Reflect the approved formula in `schema_linking.json` (`concept_formulas`).
4. **Formula proposals.** A `kind=formula` result from Evidence search is Published
Evidence and can be cited with its provenance. If no published formula is suitable and
you synthesize one for this question, present it to the reviewer and, after their F4
decision, include `{concept, columns, sql, sources}` in `concept_formulas`. This creates
a schema-versioned, **session-only Formula proposal**: it helps this session but is not
Published Evidence, has no `evidence:` ID, is not returned by runtime search, and never
writes to the workspace repository. A curator must separately import, review, and
publish it before another session can treat it as Evidence.
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's 5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
`write_schema_linking` tool — it validates the object against the `SchemaLinking` `write_schema_linking` tool — it validates the object against the `SchemaLinking`
model and writes the file deterministically (never hand-write it, never edit it model and writes the file deterministically (never hand-write it, never edit it
@@ -0,0 +1,8 @@
4. **Formula proposals.** A `kind=formula` result from Evidence search is Published
Evidence and can be cited with its provenance. If no published formula is suitable and
you synthesize one for this question, present it to the reviewer and, after their F4
decision, include `{concept, columns, sql, sources}` in `concept_formulas`. This creates
a schema-versioned, **session-only Formula proposal**: it helps this session but is not
Published Evidence, has no `evidence:` ID, is not returned by runtime search, and never
writes to the workspace repository. A curator must separately import, review, and
publish it before another session can treat it as Evidence.
@@ -216,6 +216,7 @@ Prerequisite: Phase 3 closed.
those to joins you derive yourself, and flag to the reviewer any join you need those to joins you derive yourself, and flag to the reviewer any join you need
that is NOT in the list. that is NOT in the list.
{{DISAMBIGUATION_SCHEMA_GROUNDING}} {{DISAMBIGUATION_SCHEMA_GROUNDING}}
{{EVIDENCE_FORMULA_PROPOSALS}}
5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's 5. Persist the **joins** (and any `concept_formulas`/`open_questions`) with the gate's
`write_schema_linking` tool — it validates the object against the `SchemaLinking` `write_schema_linking` tool — it validates the object against the `SchemaLinking`
model and writes the file deterministically (never hand-write it, never edit it model and writes the file deterministically (never hand-write it, never edit it
@@ -0,0 +1,26 @@
"""Migration boundary: legacy formulas become curated evidence or session proposals."""
from tht.evidence import formula_store
from tht.evidence.formula_store import ConceptFormula
def test_reviewed_formula_migration_has_deterministic_provenance_hash():
formula = ConceptFormula(
concept="fascia pediatrica",
columns=["clinical.patient.birth_date"],
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
status="reviewed",
sources=["Regola clinica approvata dal gruppo pediatrico."],
)
first = formula_store.legacy_formula_to_curated(
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
)
second = formula_store.legacy_formula_to_curated(
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
)
assert first is not None
assert second is not None
assert first.provenance.source_sha256 == second.provenance.source_sha256
assert first.provenance.source_sha256.startswith("sha256:")
+66
View File
@@ -8,7 +8,13 @@ part of the schema-linking artifact. The store is frontmatter-YAML + SQL body.
""" """
from datetime import UTC, datetime
from tht.decisions import DecisionRecord
from tht.evidence import formula_store
from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula from tht.evidence.formula_store import ConceptFormula, retrieve_formula, save_formula
from tht.evidence.session import project_session
from tht.session.models import SchemaLinking
def test_formula_retrieval_by_concept(tmp_path): def test_formula_retrieval_by_concept(tmp_path):
@@ -82,3 +88,63 @@ def test_concept_formula_default_status(tmp_path):
f = ConceptFormula(concept="x", columns=["c"], sql="SELECT 1") f = ConceptFormula(concept="x", columns=["c"], sql="SELECT 1")
assert f.status == "draft" # not yet reviewed assert f.status == "draft" # not yet reviewed
assert f.sources == [] assert f.sources == []
def test_reviewed_legacy_formula_becomes_curated_formula_with_stable_provenance():
formula = ConceptFormula(
concept="fascia pediatrica",
columns=["clinical.patient.birth_date"],
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
status="reviewed",
sources=["Regola clinica approvata dal gruppo pediatrico."],
)
migrated = formula_store.legacy_formula_to_curated(
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
)
assert migrated is not None
assert migrated.id == "evidence:fascia-pediatrica"
assert migrated.title == "Fascia pediatrica"
assert migrated.kind == "formula"
assert migrated.payload.concept == formula.concept
assert migrated.payload.columns == ("clinical.patient.birth_date",)
assert migrated.payload.sql == formula.sql
assert migrated.provenance.source_file == "source/formulas/fascia-pediatrica-1.sql.md"
assert migrated.provenance.supporting_excerpts == tuple(formula.sources)
assert migrated.review_items == ()
assert formula_store.legacy_formula_to_curated(
formula, legacy_path="formulas/fascia-pediatrica-1.sql.md",
).id == migrated.id
def test_session_formula_proposal_is_versioned_and_is_not_published_evidence(tmp_path):
linking = SchemaLinking(
question="Conta i pazienti pediatrici",
concept_formulas=[{
"concept": "fascia pediatrica",
"columns": ["clinical.patient.birth_date"],
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
"sources": ["Sintetizzata nella sessione"],
}],
)
decisions = [DecisionRecord(
seq=9,
ts=datetime(2026, 8, 25, tzinfo=UTC),
type="concept_formula_approved",
subject="phase:4",
detail="fascia pediatrica",
)]
projected = project_session(decisions, linking, tmp_path / "evidence")
assert projected == [{
"schema_version": 1,
"kind": "formula_proposal",
"publication": "session_only",
"concept": "fascia pediatrica",
"columns": ["clinical.patient.birth_date"],
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
"sources": ["Sintetizzata nella sessione"],
"decision_seq": 9,
}]
+82
View File
@@ -5,8 +5,16 @@ Completes the formula layer beyond the store: the `auto` status the spec require
and the guarantee that load_evidence_dir does NOT choke on *.sql.md formula files when and the guarantee that load_evidence_dir does NOT choke on *.sql.md formula files when
they live under the evidence root. they live under the evidence root.
""" """
import json
from types import SimpleNamespace
from typer.testing import CliRunner
from tht.cli import app, search_cmd
from tht.evidence import formula_store
from tht.evidence.formula_store import ConceptFormula, save_formula, search_formulas from tht.evidence.formula_store import ConceptFormula, save_formula, search_formulas
from tht.evidence.model import EvidenceDoc, load_evidence_dir from tht.evidence.model import EvidenceDoc, load_evidence_dir
from tht.evidence.search import EvidenceSearchOutcome
def test_status_auto_is_valid(): def test_status_auto_is_valid():
@@ -35,3 +43,77 @@ def test_load_evidence_dir_skips_formula_files(tmp_path):
ids = [d.id for d in docs] ids = [d.id for d in docs]
assert ids == ["ev1"] # the .sql.md formula file is skipped, no crash assert ids == ["ev1"] # the .sql.md formula file is skipped, no crash
assert all(isinstance(d, EvidenceDoc) for d in docs) assert all(isinstance(d, EvidenceDoc) for d in docs)
def test_unreviewed_legacy_formulas_cannot_become_curated_evidence():
for status in ("auto", "draft"):
formula = ConceptFormula(
concept="fascia pediatrica",
columns=["clinical.patient.birth_date"],
sql="CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
status=status,
)
assert formula_store.legacy_formula_to_curated(
formula, legacy_path="formulas/pediatric-1.sql.md",
) is None
def test_formula_search_uses_typed_evidence_with_a_formula_constraint():
class Searcher:
vector_generation = "gen:" + "a" * 32
def __init__(self):
self.calls = []
def search(self, embedding, **kwargs):
self.calls.append((embedding, kwargs))
return [SimpleNamespace(
id="fragment:formula", similarity=0.9, content="formula excerpt",
title="Fascia pediatrica", metadata={
"evidence_id": "evidence:fascia-pediatrica",
"evidence_kind": "formula",
"document_id": "doc:formula",
"ordinal": 0,
"source_uri": "file:///curated/formula/fascia-pediatrica.md",
"provenance": {},
},
)]
class Embedder:
def embed_query(self, query):
return [0.25]
searcher = Searcher()
outcome = search_cmd.search_formula_evidence(
"fascia pediatrica", searcher=searcher, embedder=Embedder(), top=3,
)
assert outcome.status == "available"
assert [result.evidence_id for result in outcome.results] == ["evidence:fascia-pediatrica"]
assert searcher.calls[0][1]["metadata_filter"]["required_kinds"] == ["formula"]
def test_formula_search_json_is_pristine_while_human_output_warns_about_legacy_store(monkeypatch):
monkeypatch.setattr(search_cmd, "_load_config_or_exit", lambda _path: SimpleNamespace(embeddings=object()))
monkeypatch.setattr(search_cmd, "workspace_id_for_config", lambda _cfg, _path: "workspace-a")
monkeypatch.setattr(search_cmd, "search_formula_evidence", lambda *args, **kwargs: EvidenceSearchOutcome(
"available", "gen:" + "a" * 32,
))
monkeypatch.setattr("tht.cli.vector_cmd.require_vector_cfg", lambda _cfg: None)
monkeypatch.setattr("tht.cli.vector_cmd.open_searcher", lambda _cfg: object())
monkeypatch.setattr("tht.cli.vector_cmd.make_embedder", lambda _cfg: object())
monkeypatch.setattr("tht.evidence.active_searcher", lambda *args, **kwargs: object())
monkeypatch.setattr("tht.evidence.validate_corpus_workspace", lambda _cfg, _workspace: None)
json_result = CliRunner().invoke(
app, ["search", "find", "fascia pediatrica", "--kind", "formula", "--top", "1", "--json"],
)
human_result = CliRunner().invoke(
app, ["search", "find", "fascia pediatrica", "--kind", "formula", "--top", "1"],
)
assert json_result.exit_code == 0
assert json.loads(json_result.stdout) == []
assert "ATTENZIONE" not in json_result.stdout
assert human_result.exit_code == 0
assert "ATTENZIONE" in human_result.output
+2 -1
View File
@@ -9,7 +9,7 @@ from tht.pi_skill_projection import (
render_projection, render_projection,
) )
BASELINE_SHA256 = "bb6daa6fe83d22f5e701025c5334d72ec9eb35349e90d24cb9d7f6290d0fecfe" BASELINE_SHA256 = "62bfa0dbc1179b43b2d80dc48155a6119421488a1ffc160e9c664f1fe280ce52"
def test_modular_pi_skill_renders_the_byte_identical_approved_projection(): def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
@@ -23,6 +23,7 @@ def test_modular_pi_skill_renders_the_byte_identical_approved_projection():
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"), ("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"), ("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.md"), ("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.md"),
("{{EVIDENCE_FORMULA_PROPOSALS}}", "evidence/formula-proposals.md"),
("{{MEMORY_SOLVED_SEARCH_F6}}", "memory/solved-search-f6.md"), ("{{MEMORY_SOLVED_SEARCH_F6}}", "memory/solved-search-f6.md"),
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"), ("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"), ("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),
+62 -23
View File
@@ -11,7 +11,7 @@ KIND_MAP = {
"evidence": ["evidence"], "evidence": ["evidence"],
"schema": ["schema_table", "schema_column"], "schema": ["schema_table", "schema_column"],
"values": [], # solo LSH "values": [], # solo LSH
"formula": [], # solo formula store (D14b), niente LSH/vector "formula": ["evidence"],
} }
_STAGE_PURPOSES = { _STAGE_PURPOSES = {
@@ -30,6 +30,20 @@ DEFAULT_TOP_FALLBACK = 10
search_app = typer.Typer(help="Ricerca semantica (evidence/schema/values) nel vectorstore") search_app = typer.Typer(help="Ricerca semantica (evidence/schema/values) nel vectorstore")
def search_formula_evidence(keyword: str, *, searcher, embedder, top: int):
"""Search only published Formula Evidence through the shared typed facade."""
from tht.evidence import EvidenceSearchContext, search_evidence
return search_evidence(
keyword,
"sql_generation",
EvidenceSearchContext(required_kinds=("formula",)),
searcher=searcher,
embedder=embedder,
top_n=top,
)
def _leased_dwh_snapshot(cfg, context: typer.Context): def _leased_dwh_snapshot(cfg, context: typer.Context):
from tht.jobs.dwh_pipeline import lease_dwh_snapshot from tht.jobs.dwh_pipeline import lease_dwh_snapshot
@@ -143,12 +157,6 @@ def search_cmd(
workspace_id = workspace_id_for_config(cfg, config) workspace_id = workspace_id_for_config(cfg, config)
validate_corpus_workspace(cfg, workspace_id) validate_corpus_workspace(cfg, workspace_id)
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
require_vector_cfg(cfg)
runtime_searcher = active_searcher(
cfg, open_searcher(cfg),
workspace_id=workspace_id,
)
if kind is not None and kind not in KIND_MAP: if kind is not None and kind not in KIND_MAP:
typer.secho( typer.secho(
f"ERRORE: --kind sconosciuto: {kind} (validi: {', '.join(KIND_MAP)})", f"ERRORE: --kind sconosciuto: {kind} (validi: {', '.join(KIND_MAP)})",
@@ -160,29 +168,60 @@ def search_cmd(
top = cfg.search.top_schema_tables if kind == "schema" else DEFAULT_TOP_FALLBACK top = cfg.search.top_schema_tables if kind == "schema" else DEFAULT_TOP_FALLBACK
if kind == "formula": if kind == "formula":
# D14b: recupero formule di concetto dallo store locale (niente LSH/vector). require_vector_cfg(cfg)
from tht.cli.evidence_cmd import evidence_root outcome = search_formula_evidence(
from tht.evidence.formula_store import search_formulas keyword,
searcher=active_searcher(cfg, open_searcher(cfg), workspace_id=workspace_id),
formulas = search_formulas(evidence_root(cfg), keyword)[:top] embedder=make_embedder(cfg.embeddings),
top=top,
)
if outcome.status == "unavailable":
payload = {"status": outcome.status, "code": outcome.code, "message": outcome.message}
if json_out: if json_out:
typer.echo(json.dumps( typer.echo(json.dumps(payload, ensure_ascii=False))
[f.model_dump(mode="json") for f in formulas], ensure_ascii=False, indent=2)) else:
typer.secho(f"ERRORE: {outcome.message}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
if json_out:
typer.echo(json.dumps([
{
"evidence_id": result.evidence_id,
"title": result.title,
"kind": result.kind,
"excerpts": list(result.excerpts),
"provenance": result.provenance,
"citation": result.citation,
"document_id": result.document_id,
}
for result in outcome.results
], ensure_ascii=False, indent=2))
return return
if not formulas: typer.secho(
"ATTENZIONE: lo store formule legacy non viene più consultato; "
"sono disponibili solo Formula Evidence pubblicate.",
fg=typer.colors.YELLOW,
err=True,
)
if not outcome.results:
typer.secho(f"Nessuna formula per '{keyword}'.", fg=typer.colors.YELLOW) typer.secho(f"Nessuna formula per '{keyword}'.", fg=typer.colors.YELLOW)
return return
table = Table(title=f"Formule per '{keyword}'") table = Table(title=f"Formula Evidence per '{keyword}'")
table.add_column("Concetto") table.add_column("Formula")
table.add_column("Status") table.add_column("Provenienza")
table.add_column("Colonne") table.add_column("Estratto")
table.add_column("SQL") for result in outcome.results:
for f in formulas: excerpt = result.excerpts[0] if result.excerpts else ""
sql_preview = (f.sql[:80] + "…") if len(f.sql) > 80 else f.sql table.add_row(result.title, result.citation, excerpt[:120])
table.add_row(f.concept, f.status, ", ".join(f.columns), sql_preview)
Console().print(table) Console().print(table)
return return
dwh_snapshot = _leased_dwh_snapshot(cfg, ctx)
require_vector_cfg(cfg)
runtime_searcher = active_searcher(
cfg, open_searcher(cfg),
workspace_id=workspace_id,
)
lsh_hits = None lsh_hits = None
try: try:
lsh, minhashes, meta = load_index( lsh, minhashes, meta = load_index(
+47 -13
View File
@@ -1,17 +1,12 @@
"""SQL concept->formula evidence store (spec D14b, §4.7). """Legacy ConceptFormula reader and one-way migration into Curated Evidence.
A concept (e.g. 'fascia pediatrica', 'ablazione') maps to a reusable SQL formula The ``formulas/*.sql.md`` store is retained only for the migration window. Runtime
(a CASE WHEN ...) that derives it from physical columns. These are reviewable lookup uses typed, published ``kind=formula`` Evidence instead. A session reviewer
units: the gate surfaces a candidate formula, the reviewer approves or rejects it may still approve a formula locally; that is a proposal, not publication.
(decision types concept_formula_approved / concept_formula_rejected), and approved
formulas travel with the schema-linking artifact.
Storage: one file per formula, frontmatter YAML + SQL body (same shape as
EvidenceDoc.parse). Directory layout: <root>/formulas/<slug>-<n>.sql.md.
Retrieve is by concept (may return several, e.g. competing drafts vs reviewed).
""" """
from __future__ import annotations from __future__ import annotations
import hashlib
import re import re
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
@@ -19,6 +14,8 @@ from typing import Literal
import yaml import yaml
from pydantic import BaseModel from pydantic import BaseModel
from tht.evidence.canonical import CuratedEvidence
FORMULAS_SUBDIR = "formulas" FORMULAS_SUBDIR = "formulas"
_SUFFIX_RE = re.compile(r"^(.*?)-(\d+)\.sql\.md$") _SUFFIX_RE = re.compile(r"^(.*?)-(\d+)\.sql\.md$")
@@ -104,8 +101,45 @@ def retrieve_formula(root: Path | str, concept: str) -> list[ConceptFormula]:
def search_formulas(root: Path | str, query: str) -> list[ConceptFormula]: def search_formulas(root: Path | str, query: str) -> list[ConceptFormula]:
"""Formulas whose concept contains `query` (case-insensitive). Used by """Read legacy formulas for migration tooling only (case-insensitive concept match)."""
`tht search find --kind formula` (D14b retrieval, §4.7.2): the reviewer searches a
concept term and gets the candidate formulas to approve before they reach the CTE."""
q = query.strip().lower() q = query.strip().lower()
return [f for f in _load_all(Path(root)) if q in f.concept.lower()] return [f for f in _load_all(Path(root)) if q in f.concept.lower()]
def legacy_formula_to_curated(
formula: ConceptFormula,
*,
legacy_path: str,
) -> CuratedEvidence | None:
"""Convert one reviewed legacy formula into its deterministic curated counterpart.
Drafts and model-generated formulas have no global publication status. Their
caller must project them as session-local Formula proposals instead.
"""
if formula.status != "reviewed":
return None
source_notes = tuple(formula.sources) or (
"Legacy formula migrated without a recorded provenance note.",
)
source_sha256 = hashlib.sha256(formula.dump().encode("utf-8")).hexdigest()
source_file = legacy_path if legacy_path.startswith("source/") else f"source/{legacy_path}"
return CuratedEvidence.model_validate({
"schema_version": 1,
"id": f"evidence:{formula._slug}",
"title": formula.concept[:1].upper() + formula.concept[1:],
"kind": "formula",
"purposes": ["schema_linking", "sql_generation"],
"applies_to": {"concepts": [formula.concept], "columns": formula.columns},
"language": "it",
"provenance": {
"source_file": source_file,
"source_sha256": f"sha256:{source_sha256}",
"supporting_excerpts": source_notes,
},
"review_items": [],
"payload": {
"concept": formula.concept,
"columns": formula.columns,
"sql": formula.sql,
},
})
+39 -1
View File
@@ -55,7 +55,45 @@ def project_session(
"esito": "accettata" if decision.type == "evidence_accepted" else "scartata", "esito": "accettata" if decision.type == "evidence_accepted" else "scartata",
"decision_seq": decision.seq, "decision_seq": decision.seq,
} }
return list(entries.values()) return list(entries.values()) + _formula_proposals(decisions, linking)
def _formula_proposals(decisions: list["DecisionRecord"], linking: "SchemaLinking") -> list[dict]:
"""Project locally approved F4 formulas without representing them as Evidence.
A proposal remains in the persisted session artifact until a separate curator
imports, reviews, and publishes it in the workspace repository.
"""
approved = {
decision.detail: decision.seq
for decision in decisions
if decision.type == "concept_formula_approved" and decision.detail
}
proposals = []
for formula in linking.concept_formulas:
if not isinstance(formula, dict):
continue
concept = formula.get("concept")
sql = formula.get("sql")
columns = formula.get("columns")
if not isinstance(concept, str) or not isinstance(sql, str) or not isinstance(columns, list):
continue
# A referenced published Formula Evidence is already represented by its
# Evidence receipt/citation, so it must not be reintroduced as a proposal.
evidence_id = formula.get("evidence_id", formula.get("id", ""))
if isinstance(evidence_id, str) and evidence_id.startswith("evidence:"):
continue
proposals.append({
"schema_version": 1,
"kind": "formula_proposal",
"publication": "session_only",
"concept": concept,
"columns": columns,
"sql": sql,
"sources": formula.get("sources", []),
"decision_seq": approved.get(concept),
})
return proposals
@dataclass(frozen=True) @dataclass(frozen=True)
+1
View File
@@ -19,6 +19,7 @@ FRAGMENT_ORDER = (
("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"), ("{{DISAMBIGUATION_REWRITING_INSTRUCTIONS}}", "disambiguation/phase-3.md"),
("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"), ("{{MEMORY_SOLVED_SEARCH_F4}}", "memory/solved-search-f4.md"),
("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.md"), ("{{DISAMBIGUATION_SCHEMA_GROUNDING}}", "disambiguation/schema-grounding.md"),
("{{EVIDENCE_FORMULA_PROPOSALS}}", "evidence/formula-proposals.md"),
("{{MEMORY_SOLVED_SEARCH_F6}}", "memory/solved-search-f6.md"), ("{{MEMORY_SOLVED_SEARCH_F6}}", "memory/solved-search-f6.md"),
("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"), ("{{MEMORY_SOLVED_SEARCH_F7}}", "memory/solved-search-f7.md"),
("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"), ("{{MEMORY_PROMOTION_F8}}", "memory/phase-8-promotion.md"),