Skip to content

ID-132 SPEC — G-MEMO-DELTA: per-concept memo delta-determinism ({132.38})

ID-132 SPEC — G-MEMO-DELTA per-concept memo delta-determinism

Section titled “ID-132 SPEC — G-MEMO-DELTA per-concept memo delta-determinism”

Kind: Spec slice — a standalone, ratifiable slice that defines numbered, testable memo-determinism invariants (MD-N) the Orchestrator can ratify and later dispatch implementation from. It does not implement; it specifies the per-concept content-versioning mechanism that makes BI-18 (delta-only determinism) actually satisfiable against the installed cocoindex==1.0.7 engine. Authored: 12/07/2026 · Task Planner (fresh dispatch, {132.38} DESIGN-FIRST slice). Sibling dependency: {132.35} depends on this slice ({132.38}) — Task-132-internal, sibling-only; no cross-Task dependency, no escalation. Governing rulings to respect (cite, not re-litigate): DR-047 (pipeline degrade posture is scoped narrowly — never a silent correctness loss; this slice’s over-invalidation bias is the DR-047-compliant choice, MD-6), DR-054 / DR-027 (the landed client-overlay path — the EffectiveOntology threads through the concept-write gate; MD-9 rules it OUT of the Pass-1 draft fingerprint), DR-055 (deploy topology — the forced-run surface that gives the memo its ComponentContext, MD-1). This slice formalises; it does not re-open any of those.

Owner ratification (S469 — READ FIRST; binds over the OQ recommendations + DR-intent below where they differ)

Section titled “Owner ratification (S469 — READ FIRST; binds over the OQ recommendations + DR-intent below where they differ)”

Both open questions were owner-ratified at S469 close; the core mechanism (MD-1..MD-7, MD-9..MD-11) is ratified as written. Two decisions reverse the Planner’s in-line recommendations — where this block and the text below disagree, this block wins (now also binding as DR-060):

  • OQ-MD-1 → MANUAL version= bump, NOT deps= auto-invalidation. Drafting-config (prompt/model/max-tokens) changes do not fold into the logic fingerprint and do not auto-invalidate the corpus. A config-driven re-draft is a deliberate operator version= bump, recorded in the bundle’s OKF §7 log.md (already emitted by bundle_writer.append_log_entry). Rationale (OKF-aligned): OKF treats concepts as durable, curated, git-versioned artifacts (“indefinite curation, not regeneration”); silently re-billing a full-corpus draft on a prompt tweak is contrary to OKF intent and does not scale to a live client corpus. A prompt bug-fix that genuinely needs a re-draft is still one explicit, logged version= bump. Implementation impact: MD-8 → do not add deps={...} to the decorator; keep version: as the documented manual lever; data-change staleness is still auto-handled per-concept by content_version (MD-3/6/7, unchanged).
  • OQ-MD-2 → id-131 schema migration (updated_at + ON UPDATE trigger), NOT the producer-side content-hash. entity_mentions/entity_relationships get a real updated_at column + trigger; the version aggregate then uses count(*) + max(updated_at) uniformly across all tables. Implementation impact: MD-7’s “MUST contribute a content hash” for those two tables is superseded — they use updated_at like the rest. Sequencing: {132.38} implementation now depends on that id-131 migration landing first — it is no longer a single self-contained producer dispatch (the spec’s original single-dispatch framing is void; the migration is the gating predecessor).

Empirical-verification note (pre-ratification, external library cocoindex==1.0.7). Every engine claim below was import-and-call verified against the installed package on 12/07/2026; file:line cites are into the installed source (~/Library/Python/3.14/lib/python/site-packages/cocoindex/_internal/). Results:

Symbol (pinned cocoindex==1.0.7)Claim verifiedResult
_internal.function._AutoFunctionBuilder.__init__@coco.fn exposes memo, memo_key, version, logic_tracking, deps (NO ttl)PRESENT
_internal.function._apply_memo_key(args, kwargs, spec)a param mapped to None is excluded from the fingerprint input (('KEY','SRC') → ('KEY',))PRESENT
_internal.memo_fingerprint._canonicalize(<frozen dataclass>)fingerprints all fields in definition order; adding/changing content_version changes the canonical form; equal value → identicalPRESENT
_internal.memo_fingerprint._canonicalize(<obj holding a lock/pool>)pickle fallback raises TypeError: Unsupported type for memoization key: … (reproduces staging RUN 1’s LRecordsSource failure)PRESENT (confirms the bug)
_internal.function._compute_logic_fingerprint(fn, deps=…) / version=…deps change → logic-fp change (stable when unchanged); version bump → logic-fp changePRESENT

No ABSENT / SIGNATURE_DRIFT. All other symbols cited (ConceptKey, enrich_concept, list_concepts, LRecordsSource) are internal Canonical Platform surfaces — import-and-call N/A.

Code-intelligence orientation (verbatim; repo:'canonical'). Per the Planner code-intel binding rule; tool outputs quoted verbatim and cross-checked against direct file reads.

  • gitnexus_query({query:'concept memoisation enrich fingerprint', repo:'canonical'})"processes": [], "process_symbols": []. definitions surfaced the exact symbols this slice touches, verbatim: Function:…/producer/enrich.py:enrich_concept (startLine 572, endLine 638, module "Producer" — line extent re-pinned to HEAD c3e8f734 after the a997fb3a docstring expansion shifted the module by ~32 lines; C-audit Amendment B), plus the composition context Function:…/producer/flow_def.py:_draft_concepts (197–232) and …/producer/flow_def.py:run_producer_flow (304–469). No memoisation/fingerprint/content-version process exists — confirming the delta signal is net-new capability over the existing draft path.
  • gitnexus_context({name:'enrich_concept', repo:'canonical'})"uid": "Function:…/producer/enrich.py:enrich_concept" (572–638 re-pinned, Amendment B); "incoming": { "calls": [ { "name": "_draft_concepts" } ] } (the sole caller — the plain for loop, NOT a cocoindex mount_each); "outgoing": { "calls": [ … "list_concepts", "read_concept", "run_tool_use_loop", "build_concept_frontmatter" … ] }; "processes": [].
  • gitnexus_context({name:'list_concepts', repo:'canonical'})"status": "ambiguous", 6 candidates; the production symbol is Method:…/sources/l_records.py:LRecordsSource.list_concepts#0 (451–463), "outgoing": { "calls": [ "_list_topic_concepts", "_list_product_concepts", "_list_company_concepts", "_list_certification_concepts", "_list_case_study_concepts", "_list_won_bid_case_study_concepts" ] }, "incoming": { "calls": [ "find", "run_producer_flow", … the TestListConcepts* suite ] }. The six hard-coded per-type enumeration methods are exactly the surface the content-version aggregate attaches to.

Direct reads (cross-check) — the seams this slice governs:

  • producer/enrich.py:572 @coco.fn(memo=True) on async def enrich_concept(key: ConceptKey, source: Source, *, model=ANTHROPIC_MODEL, max_tokens=_MAX_TOKENS_PASS1). The current module docstring (:50-59) asserts source … is not part of the memo fingerprint’s data-varying surface” — this is false against the installed engine (the engine fingerprints every arg; that is the RUN-1 bug). enrich_concept calls await source.read_concept(key) (:594) with the SAME key object list_concepts returned.
  • sources/l_records.py:102-181 @dataclass(frozen=True) class ConceptKey — 7 identity/locator fields (rel_path, concept_type, scope_tag, domain, subtopic, entity_id, workspace_id); __post_init__ validates membership. It carries no content-hash / updated_at / version field, so two enumerations of the same concept over CHANGED content produce an identical key.
  • sources/l_records.py:452-557 list_concepts() fans out to the six _list_* methods; each SELECT already carries a deterministic ORDER BY (:234-236 module note). The enumeration queries select only identity (DISTINCT scope_tag, DISTINCT canonical_name, buyer, …) — no content-version column.
  • sources/l_records.py:324-341 — the entity_mentions / entity_relationships column lists carry created_at only, no updated_at; and (flow.py :2598, :2666, :3366-3414) these two tables are written by row-level UPSERT (ON CONFLICT), so an in-place edit to an existing mention (e.g. confidence, context_snippet, metadata) does not move created_at. A count(*) + max(created_at) signal is therefore blind to in-place mention edits (MD-7).
  • producer/flow_def.py:215-217enrich_concept is invoked from a plain for key in concepts loop (draft = await enrich_concept(key, source)), not a cocoindex-composed component.
  • server.py:1051-1072 (verified engine note) + :1091-1138 _build_forced_producer_report — the forced-run surface constructs a dedicated coco.App and runs run_producer_flow inside its main_fn under update_blocking(), which gives enrich_concept’s @coco.fn(memo=True) a live ComponentContext. The engine’s own gate (function.py:741-746 sync / :1231 async: parent_ctx is None → execute UNMEMOISED) means the memo is inert without that context — the forced-run App and the post-walk hook are what make it engage (MD-1).
  • producer/bundle_writer.py:242-250 bundle_write_path_for_key(key) derives the physical path from key.rel_path (+ concept_type/workspace_id for the won-bid redirect) — never a content-version. Confirms a new content_version field will not leak into the write path (MD-4).
  • producer/prompts.py:61 PASS1_INSTRUCTION_PROMPT = """…""" and extraction.py:119 ANTHROPIC_MODEL = "claude-opus-4-6" — module-level constants imported by enrich.py (snapshot-safe for deps=, MD-8). Neither appears in enrich_concept’s own body AST (they enter via _cached_system() / the default kwarg), so the AST logic fingerprint does not track them.
  • Effective ontology reach. grep -rn "EffectiveOntology|effective_ontology|overlay" over enrich.py, agent_loop.py, prompts.pyzero hits. The DR-054/DR-027 overlay threads into the concept-write gate ({132.34}/{132.35}), downstream of the Pass-1 draft — it does not shape the draft (MD-9).
  • ccc search fallback: not required — gitnexus + direct read both returned the real surface. No greenfield disclaimer applies.

BI-18 (delta-only determinism) is UNSATISFIABLE today in both directions, empirically established in staging RUN 1 (18/18 concepts) and confirmed against the installed engine:

  1. The source arg is unfingerprintable → every draft aborts. enrich_concept is @coco.fn(memo=True); the engine fingerprints every positional arg (memo_fingerprint.py:372-401 _make_call_canonicalfingerprint_call :412-440). source is an LRecordsSource holding an asyncpg pool; it has no __coco_memo_key__ and is unpickleable, so _canonicalize hits the pickle fallback and raises TypeError: Unsupported type for memoization key: LRecordsSource (memo_fingerprint.py:360-369). RUN 1: 18/18 failed with exactly this. Blocker 1.
  2. Even with source fixed, the fingerprint is IDENTITY-ONLY → stale drafts served silently. ConceptKey is a frozen dataclass, so _canonicalize_dataclass (memo_fingerprint.py:131-151) keys on its field values — but every field is a locator/identity token. list_concepts’s six sub-queries select no content-hash / updated_at / version. A record edit that leaves concept identity unchanged (e.g. an answer rewrite under the same scope_tag, a confidence bump on an existing entity mention) memo-hits → the stale prior draft is returned silently. That is a direct violation of BI-18 direction 2 and of DR-047’s no-silent-correctness-degrade posture. Blocker 2.

A canary test scripts/tests/test_l_records_source.py::TestMemoKeyProtocolEscalation (commit a997fb3a, in-flight) pins the CURRENT unfixed state green against the installed engine; this slice names its required evolution (MD-11).

Non-goals.

  • Making the memo engage. The ComponentContext precondition is already satisfied by the forced-run App and the post-walk hook (server.py:1091-1138, MD-1). This slice does not touch that wiring; it states the precondition so the Checker verifies the fix is exercised where the memo is live.
  • Composing the effective ontology into the Pass-1 draft. Ruled OUT (MD-9); the overlay is a write-gate concern (DR-054/DR-027), not a drafting input.
  • Changing the enumeration grain / concept identity (BI-2). content_version is additive; the identity fields, __post_init__ validation, dedup, routing and physical write path are unchanged (MD-4).
  • A schema migration on entity_mentions / entity_relationships. Closing their updated_at gap producer-side (content-hash) is the in-scope choice; an id-131 schema addition is an OQ, not a requirement (OQ-MD-2).
  • Invalidating a concept when a cross-linked concept’s content changes (BI-9). read_concept_raw/sample_rows can target any catalogue ref for cross-linking narrative; content_version does not track this transitively (MD-7a). Named as an accepted residual limitation, not solved here.

Behaviour — memo-determinism invariants (MD-N)

Section titled “Behaviour — memo-determinism invariants (MD-N)”

Each invariant carries an inline (per DR-NNN) tag where a ruling drives it, and a *Test:* clause the Checker uses as its acceptance criterion. MD-N numbers are append-only. The empirical tests (MD-2/3/8) run at the _canonicalize / _compute_logic_fingerprint level against the installed engine — no Rust/LMDB boot required, mirroring the existing canary.

  1. MD-1 (memo-engagement precondition — DR-055). The content-version mechanism is load-bearing only where the memo engages: a live cocoindex ComponentContext. The forced-run App (server.py:1091-1138) and the post-walk hook run run_producer_flow under update_blocking(), which supplies that context; the bare _draft_concepts loop (unit tests, cocoindex stubbed) runs unmemoised by design (function.py:741-746 / :1231: parent_ctx is None → execute raw). No change is made to this wiring; it is stated so the BI-18 Run-1 proof (MD-10) is run through a memo-live surface, never the bare loop. Test: the forced-run App path establishes a ComponentContext (existing server.py coverage); a direct enrich_concept(key, source) call with no ambient context executes without attempting a fingerprint (no Unsupported type raise) — the pre-fix escape hatch that must not be mistaken for a pass.

  2. MD-2 (the source arg is excluded from the fingerprint). enrich_concept is decorated @coco.fn(memo=True, memo_key={'source': None}, …). Per function.py:418-448 a parameter mapped to None is dropped from the fingerprint input, so the unpickleable LRecordsSource never reaches _canonicalize and the RUN-1 TypeError: Unsupported type for memoization key: LRecordsSource cannot recur. sources/l_records.py stays cocoindex-free (no memo protocol added to the data class — a hard property of that module). Test: fingerprinting an enrich_concept-shaped call with memo_key={'source': None} succeeds where the same call without it raises the RUN-1 TypeError (empirical, cocoindex==1.0.7).

  3. MD-3 (a per-concept content_version drives the fingerprint — the BI-18 delta lever). A content_version: str = "" field is added to the frozen ConceptKey. Because _canonicalize_dataclass fingerprints all fields (memo_fingerprint.py:131-151), two keys with identical identity but different content_version produce different fingerprints (re-draft), and identical content_version produces the same fingerprint (memo-hit). Test (empirical): _canonicalize(ConceptKey(id…, content_version="a")) != _canonicalize(ConceptKey(id…, content_version="b")) and == _canonicalize(ConceptKey(id…, content_version="a")).

  4. MD-4 (content_version participates ONLY in the memo fingerprint). It is excluded from every identity / routing / dedup / physical-write-path / provenance consumer: ConceptKey.__post_init__ validation, read_concept type routing, bundle_write_path / bundle_write_path_for_key, the won-bid buyer dedup (_list_won_bid_case_study_concepts), catalogue_paths, and find()’s _concept_haystack all ignore it. Rationale: concept identity is rel_path (BI-2); a content change must re-draft the same concept, not mint a new one. Test: two keys differing only in content_version yield identical bundle_write_path_for_key, identical read_concept routing, and identical find() membership.

  1. MD-5 (computed at list_concepts() SQL time; one cheap set-based aggregate per enumeration query). content_version is populated inside the six _list_* methods from a single set-based aggregate query per method, grouped by the concept’s identity — never a per-concept round-trip and never a per-record round-trip. Enumerating N concepts issues O(number-of-enumeration-methods) aggregate queries, not O(N). (Cost lens: the whole point of BI-18 is to skip LLM drafting calls; a handful of grouped aggregates over the bounded first-client corpus is negligible against even one avoided draft.) Test: enumerating a fixture corpus of N concepts issues a bounded, N-independent number of DB fetch calls for the version signal (assert against FakePool.calls).

  2. MD-6 (sensitivity contract — second-order determinism, DR-047). content_version changes if and only if a backing row of that concept is inserted, deleted, or edited (including in-place); it does not change on byte-identical backing content. It is deterministic: derived from sorted row content, carrying no wall-clock / run timestamp. The bias is explicit and DR-047-mandated: over-invalidation is safe (a redundant re-draft costs LLM spend but never serves stale), under-invalidation is a forbidden silent correctness loss. A record shared by several concepts (e.g. one source_document feeding a topic and the company concept) legitimately re-drafts every concept whose backing set it belongs to — that is “exactly the affected concept(s)”, not over-invalidation. Test: mutating one backing row changes that concept’s content_version (and only concepts whose read grid includes that row); a no-op re-enumeration yields a byte-identical content_version.

  3. MD-7 (backing-set coverage per enumeration method — the read grid; updated_at-less tables need a content hash). For each of the six methods, the version aggregate covers the same tables read_concept reads for that type (the TECH per-type join grid):

    • topicq_a_pairs (by scope_tag or domain/subtopic) + parent source_documents + reference_items + record_lifecycle + entity_mentions + entity_relationships;
    • productsource_documents (filename match) + q_a_pairs (by those docs or entity_id) + reference_items;
    • companysource_documents (company/team) + reference_items + entity_mentions;
    • certificationsource_documents (compliance) + reference_items + entity_mentions (by entity_type='certification' + canonical_name, across all docs);
    • case_study (named-clients) → source_documents (named-clients) + q_a_pairs + reference_items;
    • case_study (won-bid) → workspaces + derived_from_form_response q_a_pairs (by source_workspace_id, published) + won form_templates.

    For the two tables with no updated_at that are UPSERT-written (entity_mentions, entity_relationships), a count(*) + max(created_at) signal is insufficient (blind to in-place edits) — those tables MUST contribute a content hash (e.g. md5(string_agg(id || confidence || context_snippet || metadata ORDER BY id))). Tables with updated_at MAY use the cheaper count(*) + max(updated_at); a uniform content-hash over every table is the robust default (see Design, Option (i)). Test: for each concept type, an in-place edit to any table in its read grid — including a confidence change on an existing entity_mentions row — changes that concept’s content_version.

7a. MD-7a (cross-link read surface — accepted residual limitation, not solved by this slice). enrich_concept’s Pass-1 tool loop exposes read_concept_raw/sample_rows against ANY catalogue ref (producer/enrich.py:_build_tool_executors, BI-9 cross-linking), not only the concept’s own key.rel_path. A concept’s draft may therefore incorporate content read from ANOTHER concept’s backing rows. content_version (MD-3/MD-7) is scoped to the concept’s OWN read grid only and does not detect a change to a cross-linked concept’s backing data — a concept whose draft narratively referenced another concept can serve a stale cross-reference indefinitely after that other concept’s content changes, without the referencing concept’s own content_version moving. This is a genuine, DR-047-relevant under-invalidation gap this slice does not close: resolving it deterministically at list_concepts() time is not tractable in one dispatch (which concepts get cross-linked is only known AFTER Pass-1 runs — a circular dependency), so it is named here as an accepted, bounded limitation rather than silently left unaddressed. Mitigation / escape hatch: the version: manual bump lever (MD-8, Design (iv)) remains available for an operator-triggered full re-draft if stale cross-links are observed in practice; a future slice may explore a two-pass fixed-point scheme (re-check content_version of any concept actually cross-linked into a prior draft) if this proves materially stale at real-corpus scale.

  1. MD-8 (drafting-config folded into the logic fingerprint via deps=). The Pass-1 draft is shaped by PASS1_INSTRUCTION_PROMPT, ANTHROPIC_MODEL, and _MAX_TOKENS_PASS1, none of which the AST logic fingerprint tracks (imported constants / default kwargs; _compute_logic_fingerprint dumps only enrich_concept’s own body, function.py:596-640). They are passed as @coco.fn(…, deps={"prompt": PASS1_INSTRUCTION_PROMPT, "model": ANTHROPIC_MODEL, "max_tokens": _MAX_TOKENS_PASS1}), which folds them into the logic fingerprint (snapshotted at decoration; function.py:1861-1882) so a drafting-config change invalidates all concepts — correct, because every prior draft was shaped by the old config. Test (empirical): _compute_logic_fingerprint(enrich_concept, deps=A) != deps=B and == deps=A (cocoindex==1.0.7).

  2. MD-9 (the effective ontology is NOT in the Pass-1 fingerprint — DR-054/DR-027). The EffectiveOntology overlay governs the concept-write gate, not the Pass-1 draft (verified: zero ontology imports in enrich.py/agent_loop.py/prompts.py). An overlay change must not re-draft concepts (the draft is identical; only admission at write time differs). Forward guard: if a future change threads the ontology into Pass-1 prompting, it MUST then join deps= (MD-8). Test: a change to the effective ontology triggers zero enrich_concept re-drafts; a grep asserts no ontology symbol is imported into the Pass-1 modules.

  1. MD-10 (deterministic memoised output — the BI-18 Run-1 proof). The ConceptDraft return is deterministic in its inputs: the frontmatter per-run timestamp (build_concept_frontmatter(timestamp=datetime.now(...)), enrich.py:601) is an output, never fingerprinted, so a memo-hit returns the cached draft verbatim (old timestamp) and a memo-miss mints a fresh one. read_concept’s existing per-query ORDER BY keeps reads reproducible. Two consecutive producer runs over unchanged L-records make zero Anthropic drafting calls (the memo-hit no-op). Test: through one memo-live surface (two /producer-run calls, or a walk-triggered run then a no-delta walk), run-1 populates the memo and run-2 over identical records issues zero Anthropic drafting calls and produces a no-op bundle diff.

  2. MD-11 (canary evolution — same PR). test_l_records_source.py::TestMemoKeyProtocolEscalation (commit a997fb3a) currently pins the unfixed state green (source-arg TypeError; identity-only key). It MUST evolve in the same PR to pin the fixed contract: source excluded (MD-2), content_version drives the fingerprint (MD-3), config via deps= (MD-8). It must not be left asserting pre-fix behaviour. Test: the evolved canary asserts MD-2/MD-3/MD-8 against the installed engine and would fail against the pre-fix decorator.

Design — options evaluated, positions taken

Section titled “Design — options evaluated, positions taken”

Adopted: (i) content-version on ConceptKey computed at list_concepts() SQL time. This is the core. It is the only option that gives per-concept delta granularity computable cheaply, once, at enumeration — exactly where the identity is already derived. Realisation position: a per-backing-table content signal combined in fixed table order into one content_version string; a uniform md5(string_agg(id || <mutable content cols> ORDER BY id)) per table is the robust default (no updated_at dependency, catches inserts/updates/deletes/in-place-edits), and count(*) + max(updated_at) is an acceptable cheaper substitute only for tables that have updated_at. The two updated_at-less UPSERT tables (entity_mentions, entity_relationships) MUST use the hash (MD-7). Empirically grounded: _canonicalize fingerprints the added field (MD-3 probe).

Adopted (fix, not the delta lever): memo_key={'source': None} over a __coco_memo_key__ on LRecordsSource. Option (iii) proposed a stable __coco_memo_key__ on the source class plus an explicit content-version arg. The content-version half is adopted via (i). The source half is refined: memo_key={'source': None} (function.py:418-448, empirically excludes the arg) is cleaner than a class dunder because (a) it keeps sources/l_records.py cocoindex-free — a hard, documented property of that module; (b) it localises the entire memo concern to the one decorator site; (c) a __coco_memo_key__ returning a constant still contributes a constant to the fingerprint, whereas None excludes the arg entirely (the honest model: source carries no draft-shaping data). No content lives on source, so nothing is lost by exclusion.

Adopted: (iv) deps= for config surfaces. deps (function.py:596-640, :1861-1882; empirically confirmed) folds the prompt/model/max-tokens into the logic fingerprint (MD-8). Also under (iv): version: int is a manual global-invalidation lever (bump to force a full re-draft when a change is invisible to both AST and deps — e.g. a model behaviour shift under a pinned id, or a memo-store reset); it is noted as an available escape hatch, not wired by default. No TTL exists in cocoindex==1.0.7 (decorator params are memo, memo_key, batching, max_batch_size, runner, version, logic_tracking, deps — verified empirically); time-based expiry is not an option and is not needed (the content-version is the invalidation signal).

Rejected: (ii) the native memo-state / validator mechanism. The engine does support hit-time state validation — __coco_memo_state__ / register_memo_key_function(typ, key_fn, state_fn=…) (memo_fingerprint.py:196-207, :42-51, :287-303 “shook” tag) drives a can_reuse / states_changed decision at memo-hit time (function.py:165-368). It is rejected as unnecessary complexity: state-validators exist for signals you cannot compute at key-construction time (they re-check external state when a hit is proposed). Here the invalidation signal is computable at list_concepts() time, so embedding it in the key (option i) is strictly simpler, needs no state storage/registry, and is unit-testable at the _canonicalize level. Documented so a future maintainer knows the mechanism was considered and why it was not used.

Position: content_version lives on ConceptKey (a field), not as a separate enrich_concept argument. The _draft_concepts loop and run_producer_flow’s source_workspace_ids comprehension already pass the ConceptKey through; a field rides that plumbing with zero signature churn, and the default "" keeps every existing ConceptKey(...) construction (tests, the agent cross-link path) valid. The conceptual contract is clarified, not muddied: the memo key is (which concept) + (what content) — precisely BI-18. MD-4 fences the field out of every identity consumer so BI-2 identity semantics are preserved.

Implementation checklist (dispatch order — scoped to one executor dispatch)

Section titled “Implementation checklist (dispatch order — scoped to one executor dispatch)”
  1. sources/l_records.pyConceptKey.content_version. Add content_version: str = "" as the last field (after workspace_id); leave __post_init__ unchanged (MD-4). Docstring: state it is a memo-fingerprint-only field, excluded from identity (BI-2).
  2. sources/l_records.py — the six enumeration aggregates. Add one content-version aggregate SQL per _list_* method, grouped by identity, covering that type’s read grid (MD-7); content-hash for entity_mentions/entity_relationships, count+max(updated_at) permitted elsewhere. Populate content_version on each constructed ConceptKey. Keep every query ORDER BY-deterministic and the module cocoindex-free (MD-2/MD-5/MD-6). Prefer folding the aggregate into (or beside) the existing enumeration query per method to hold the round-trip count (MD-5).
  3. producer/enrich.py — the decorator. Change to @coco.fn(memo=True, memo_key={'source': None}, deps={"prompt": PASS1_INSTRUCTION_PROMPT, "model": ANTHROPIC_MODEL, "max_tokens": _MAX_TOKENS_PASS1}) (MD-2/MD-8). Rewrite the docstring’s memo note (:50-59): the old claim that the engine ignores source is false — state that source is excluded via memo_key, and that content_version on the key is the BI-18 delta signal.
  4. Canary evolution — scripts/tests/test_l_records_source.py. Evolve TestMemoKeyProtocolEscalation from the unfixed-state pin to the fixed contract (MD-11: MD-2/MD-3/MD-8).
  5. Tests (behaviour-first, test-philosophy.md). In scripts/tests/test_l_records_source.py: content_version sensitivity per type incl. the entity_mentions in-place-edit case (MD-6/MD-7). In scripts/tests/test_producer_enrich.py: memo_key exclusion + deps at the _canonicalize/_compute_logic_fingerprint level (MD-2/MD-3/MD-8); MD-4 non-leak. In scripts/tests/test_cocoindex_server.py::TestForcedProducerReportWiring: the MD-10 Run-1 no-op proof (extend the existing forced-run coverage — do not boot the real engine in unit tests).

testStrategy (the slice must make this satisfiable): After the fix, enrich_concept’s call fingerprints without raising on source; a record edit that leaves concept identity unchanged (e.g. an in-place entity_mentions.confidence change, or an answer rewrite under the same scope_tag) changes exactly that concept’s content_version and re-drafts only it; an unchanged corpus re-run makes zero Anthropic drafting calls; a prompt/model change re-drafts all concepts; an effective-ontology change re-drafts none. — satisfied by MD-2 (source), MD-3/6/7 (delta), MD-10 (no-op proof), MD-8 (config), MD-9 (ontology out).

Coordination notes (implementation-time, not owner questions)

Section titled “Coordination notes (implementation-time, not owner questions)”
  • C-1 (memo-live proof surface). Run the MD-10 proof through one cocoindex-App surface end to end (server.py:1091-1138 forced-run twice, or walk→no-delta-walk) — a memo populated under one App namespace is not guaranteed hit under another (server.py:1074-1086). Do not compare a walk-triggered run against a forced run and expect a cross-App hit.
  • C-2 (fixture corpus for the sensitivity tests). The existing FakePool (test_l_records_source.py) matches queries by marker substring; the new aggregate queries need FakePool.when(...) rules. The in-place-edit test (MD-7) is expressible purely at the fixture level (two enumerations with the same identity rows but a changed confidence), no DB needed.
  • C-3 (docstring drift). enrich.py:50-59 currently mis-states the engine’s source handling; fixing the behaviour without fixing the docstring would leave an actively misleading comment — treat the docstring edit as part of the same change (item 3).

Open questions (OQ-MD-N) — recommendations; owner ratifies

Section titled “Open questions (OQ-MD-N) — recommendations; owner ratifies”
  • OQ-MD-1 [RESOLVED S469 → MANUAL version= bump + log.md; NOT deps=. See Owner ratification / DR-060. The recommendation below was NOT taken.] (drafting-config invalidation policy — cost vs correctness). Should a change to PASS1_INSTRUCTION_PROMPT / ANTHROPIC_MODEL auto-invalidate every concept (via deps=, MD-8 — correct, but costs a full-corpus re-draft on any prompt tweak), or be a manual operator decision (via a version: bump, cheaper but risks silent drift between the prompt change and each concept’s next content change)? Recommendation: deps= (auto-invalidate). A changed drafting prompt makes every prior draft stale by construction; leaving them is exactly the silent drift DR-047 forbids in spirit, and the full re-draft is an owner-visible, intended consequence of editing the prompt. The owner ratifies accepting that cost posture.
  • OQ-MD-2 [RESOLVED S469 → id-131 schema updated_at + ON UPDATE trigger; NOT the producer-side hash. See Owner ratification / DR-060. The recommendation below was NOT taken.] (entity_mentions / entity_relationships updated_at gap — producer hash vs id-131 schema).** Close the in-place-edit blind spot producer-side with a content-hash aggregate (MD-7 — self-contained, no migration, marginally heavier read), or request id-131 to add updated_at (+ an ON UPDATE trigger) to those two tables (cleaner long-term signal, but a cross-Task migration
    • coordination)? Recommendation: producer-side content-hash now. It keeps {132.38} a single producer-scoped dispatch and removes the blind spot immediately; a schema addition is worth raising to id-131 only if the hash aggregate proves too costly at real corpus scale. The owner/id-131 owner ratifies whether to also schedule the schema change.

DR-intent (for the Orchestrator, on main — not written in-branch)

Section titled “DR-intent (for the Orchestrator, on main — not written in-branch)”

A new binding ruling is warranted to record the producer memo delta-determinism contract, refining DR-047’s application to the producer memo path:

  • source (and any non-data adapter arg) is excluded from the enrich_concept fingerprint via memo_key, keeping the source adapter cocoindex-free;
  • the BI-18 delta signal is a per-concept content_version computed at list_concepts() time as a deterministic content aggregate over the concept’s read grid, over-invalidation-biased (a redundant re-draft is safe; a silent stale draft is not — DR-047);
  • entity_mentions / entity_relationships get a real updated_at column + ON UPDATE trigger (id-131 schema migration, OQ-MD-2 ratified) so the version aggregate uses count+max(updated_at) uniformly — the producer-side content-hash was rejected;
  • drafting-config (prompt/model/max-tokens) is NOT folded via deps=; a config re-draft is a manual version= bump recorded in log.md (OQ-MD-1 ratified — DR-060);
  • the effective ontology is excluded from the Pass-1 fingerprint (write-gate concern only — DR-054/DR-027);
  • the mechanism does not transitively invalidate a concept when a cross-linked concept’s backing content changes (MD-7a) — an accepted, bounded residual limitation, with the version: manual bump as the operator escape hatch.

Surfaced as intent only — the Orchestrator writes it on main per the decision-register discipline (pending OQ-MD-1/OQ-MD-2 ratification).