{
  "version": 1,
  "id": "Circe-postmerge-audit",
  "repo": "/home/abstergo/Programming/Circe",
  "repoName": "Circe",
  "scope": "postmerge-audit",
  "title": "Circe merged implementation audit",
  "sinceCommit": "a685f92bede7523193c62616e03dd62b3f7b537c",
  "lastReviewCommit": "1f21302cf7783b5eaa0017c1328b31f750047e6b",
  "reviewRound": 1,
  "loc": {
    "added": 383,
    "removed": 72
  },
  "updatedAt": "2026-09-23T22:17:19.008880+00:00",
  "htmlPath": "/home/abstergo/Documents/reviews/Circe-postmerge-audit-abbyview.html",
  "jsonPath": "/home/abstergo/Documents/reviews/Circe-postmerge-audit-abbyview.json",
  "verdict": "COMMENT",
  "actionableOpen": 0,
  "mergeBlocking": 0,
  "prUrl": "https://github.com/Absterrg0/circe/pull/8",
  "assessment": "Five confirmed defect groups fixed in PR #8; semantic product remains incomplete.",
  "baseline": {
    "frozenAt": "2026-09-23T22:17:19.008880+00:00",
    "title": "Circe merged implementation audit",
    "lede": "The boundaries are sound; five confirmed implementation gaps remained after the PR stack merged. They are fixed in PR #8.",
    "walkthrough": {
      "prose": "Whole-implementation audit and corrective pass, not a re-review of only the final PR.",
      "bullets": [
        "Reviewed merged main a685f92 across command compilation, runtime transitions, recovery, file journal, context assembly/invalidation, perception decoding, Jev adapter and inspector wiring.",
        "Ran the unchanged 396-test baseline, typechecking and package-boundary checks before making fixes.",
        "Used three requested GPT-6 Luna agents for bounded kernel, context and perception audits. They reached their usage limit; the primary agent reviewed their partial changes, finished the fixes and performed final verification.",
        "Added 14 retained eval regressions: 12 fail on untouched merged main and two are passing controls. Added one disk-format regression. All 15 additions pass after the fixes.",
        "Created PR #8 at 1f21302. It is open and has not been merged. Original checkout remains untouched; changes were made in an isolated worktree."
      ]
    },
    "filesChanged": [
      {
        "path": "ARCHITECTURE.md",
        "added": 4,
        "removed": 3,
        "delta": "+4 \u22123"
      },
      {
        "path": "README.md",
        "added": 26,
        "removed": 7,
        "delta": "+26 \u22127"
      },
      {
        "path": "docs/SEMANTIC-CONTEXT-MODEL.md",
        "added": 7,
        "removed": 4,
        "delta": "+7 \u22124"
      },
      {
        "path": "packages/context/src/assembly.ts",
        "added": 13,
        "removed": 2,
        "delta": "+13 \u22122"
      },
      {
        "path": "packages/context/src/store.ts",
        "added": 16,
        "removed": 5,
        "delta": "+16 \u22125"
      },
      {
        "path": "packages/core/src/journal-recovery.ts",
        "added": 1,
        "removed": 0,
        "delta": "+1 \u22120"
      },
      {
        "path": "packages/core/src/journal.ts",
        "added": 3,
        "removed": 1,
        "delta": "+3 \u22121"
      },
      {
        "path": "packages/core/src/runtime.ts",
        "added": 14,
        "removed": 12,
        "delta": "+14 \u221212"
      },
      {
        "path": "packages/core/src/types.ts",
        "added": 3,
        "removed": 0,
        "delta": "+3 \u22120"
      },
      {
        "path": "packages/evals/src/postmerge-context.test.ts",
        "added": 110,
        "removed": 0,
        "delta": "+110 \u22120"
      },
      {
        "path": "packages/evals/src/postmerge-kernel.test.ts",
        "added": 52,
        "removed": 0,
        "delta": "+52 \u22120"
      },
      {
        "path": "packages/evals/src/postmerge-perception.test.ts",
        "added": 73,
        "removed": 0,
        "delta": "+73 \u22120"
      },
      {
        "path": "packages/journal-file/src/file-journal-durability.test.ts",
        "added": 13,
        "removed": 0,
        "delta": "+13 \u22120"
      },
      {
        "path": "packages/perception-jev/src/perception-jev.test.ts",
        "added": 5,
        "removed": 3,
        "delta": "+5 \u22123"
      },
      {
        "path": "packages/perception/src/validation.ts",
        "added": 43,
        "removed": 35,
        "delta": "+43 \u221235"
      }
    ],
    "flowDiagram": "Authority evidence \u2192 immutable context frame \u2192 decoded decisions \u2192 acceptance policy \u2192 accepted-turn compiler \u2192 kernel authorization / frozen effects \u2192 executor\n\nKernel state \u2192 durable journal \u2192 recovery\nSemantic store \u2192 in-memory only (persistent host still required)",
    "sinceCommit": "a685f92bede7523193c62616e03dd62b3f7b537c",
    "loc": {
      "added": 383,
      "removed": 72
    }
  },
  "roundDeltas": [],
  "findings": [
    {
      "id": "F72",
      "title": "Complete a command atomically with its recovered discourse state",
      "file": "packages/core/src/runtime.ts",
      "lines": "2680-2699",
      "severity": "major",
      "pBadge": "P1",
      "category": "data-integrity",
      "bodyParagraphs": [
        "On merged main, finalize wrote command-applied before discourse-snapshot. Recovery treated the command as complete even when a crash left only the previous discourse snapshot. This is a completion-order defect, not an executor retry issue.",
        "The retained test selects A and then selects B or clears the selection. It restarts after every journal prefix and redelivers the second input. At the old applied-marker cut, recovery keeps A and suppresses the second input. Merely reversing two writes creates another gap: a new selection could be applied twice if its snapshot survived but its completion marker did not.",
        "The fix records the reservation key inside the discourse snapshot, so recovery sees the state and its completion together. The legacy command-applied record follows for format compatibility, and the in-memory result cache is populated afterward. Existing older journals still load; the change cannot recover state that an older writer never persisted."
      ],
      "fix": "Add optional commandKey to discourse-snapshot, validate it on disk, recognize it during recovery, and place the legacy completion marker after that snapshot.",
      "tests": "postmerge-kernel.test.ts: both select and clear-selection across every crash prefix; file-journal-durability.test.ts: completion identity round trip and invalid identity rejection.",
      "status": "fixed",
      "fixedAt": "2026-09-23T22:17:19.008880+00:00",
      "reviewRound": 1,
      "type": "logic",
      "section": "actionable",
      "followUp": false,
      "suggestedDiff": "diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts\nindex e8942b5..d22ef0d 100644\n--- a/packages/core/src/runtime.ts\n+++ b/packages/core/src/runtime.ts\n@@ -2629,17 +2629,6 @@ export function createRuntime(deps: RuntimeDeps): SemanticRuntime {\n   }\n \n   function finalize(ctx: TurnCtx): TurnResult {\n-    // Completing the turn is the durable proof of application; selection\n-    // alone is not. Recorded once, under the actual reservation key.\n-    if (ctx.rememberKey && !ctx.appliedLogged) {\n-      ctx.appliedLogged = true;\n-      log({\n-        kind: \"command-applied\",\n-        interactionId: state.interactionId,\n-        key: ctx.rememberKey,\n-        at: ctx.now,\n-      });\n-    }\n     const scope = activeScope();\n     const allTransactions = [...state.transactions.values()].filter((tx) => !isTerminal(tx));\n     const transactionIds = new Set([...ctx.transactions, ...allTransactions.map((tx) => tx.id)]);\n@@ -2666,7 +2655,6 @@ export function createRuntime(deps: RuntimeDeps): SemanticRuntime {\n       reply: ctx.reply ? { kind: ctx.reply } : undefined,\n       errors: ctx.errors.length > 0 ? [...ctx.errors] : undefined,\n     };\n-    if (ctx.rememberKey) state.commandResults.set(ctx.rememberKey, structuredClone(result));\n     const snapshot: DiscourseSnapshot = {\n       scopes: [...state.scopes.values()].map((scope) => structuredClone(scope)),\n       scopeStack: [...state.scopeStack],\n@@ -2693,8 +2681,22 @@ export function createRuntime(deps: RuntimeDeps): SemanticRuntime {\n       kind: \"discourse-snapshot\",\n       interactionId: state.interactionId,\n       state: snapshot,\n+      ...(ctx.rememberKey ? { commandKey: ctx.rememberKey } : {}),\n       at: ctx.now,\n     });\n+    // The snapshot certifies completion atomically with its discourse state.\n+    // Retain the explicit marker for readers of the earlier journal format,\n+    // which must also see that state before suppressing replay.\n+    if (ctx.rememberKey && !ctx.appliedLogged) {\n+      log({\n+        kind: \"command-applied\",\n+        interactionId: state.interactionId,\n+        key: ctx.rememberKey,\n+        at: ctx.now,\n+      });\n+      ctx.appliedLogged = true;\n+    }\n+    if (ctx.rememberKey) state.commandResults.set(ctx.rememberKey, structuredClone(result));\n     return result;\n   }\n \n",
      "prompt": "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and retain behavioral regression coverage.\n\nF72 was fixed in 1f21302. Verify packages/core/src/runtime.ts:2680-2699 against its recorded baseline at a685f92. On merged main, finalize wrote command-applied before discourse-snapshot. Recovery treated the command as complete even when a crash left only the previous discourse snapshot. This is a completion-order defect, not an executor retry issue. Add optional commandKey to discourse-snapshot, validate it on disk, recognize it during recovery, and place the legacy completion marker after that snapshot. Exercise postmerge-kernel.test.ts: both select and clear-selection across every crash prefix; file-journal-durability.test.ts: completion identity round trip and invalid identity rejection. Preserve both passing controls and legacy journal compatibility where applicable. Do not add a new service, recovery framework, or second interpretation path. If the fix still holds, report that result rather than rewriting it."
    },
    {
      "id": "F73",
      "title": "Invalidate beliefs when another candidate in their evidence frame changes",
      "file": "packages/context/src/store.ts",
      "lines": "280-327",
      "severity": "major",
      "pBadge": "P1",
      "category": "correctness",
      "bodyParagraphs": [
        "Annotations were linked to the current generations of their subject refs, although a semantic judgment had received an entire recorded frame. A belief about A could depend on comparing A with B while naming only A as its subject. Updating B left that belief current.",
        "The same hole existed when an asynchronous result arrived after B had already changed: the freshness check only compared the annotation subjects. The retained regressions exercise both publication orders, with controls for a ref outside the frame and an annotation subject absent from the frame.",
        "The fix links every candidate generation captured by the recorded frame and checks the whole candidate read set at publication. Subject membership checks remain intact. This is intentionally conservative: until selective semantic dependencies exist, a frame-based judgment may require recomputation when any candidate it saw changes."
      ],
      "fix": "Use recorded frame candidate generations for dependency links and late-result checks; retain subject membership and explicit dependency validation.",
      "tests": "postmerge-context.test.ts: changed peer invalidation, late arrival, unrelated-ref control, and outside-frame subject rejection.",
      "status": "fixed",
      "fixedAt": "2026-09-23T22:17:19.008880+00:00",
      "reviewRound": 1,
      "type": "logic",
      "section": "actionable",
      "followUp": false,
      "suggestedDiff": "diff --git a/packages/context/src/store.ts b/packages/context/src/store.ts\nindex 1a291e4..1c28903 100644\n--- a/packages/context/src/store.ts\n+++ b/packages/context/src/store.ts\n@@ -282,7 +282,16 @@ class InMemoryContextStore implements SemanticContextStoreContract {\n     const record = snapshot({ ...input, id } as SemanticAnnotation);\n     this.annotationRecords.set(id, record);\n     for (const ref of record.refs) this.push(this.annotationsByRef, entityRefKey(ref), id);\n-    const generations = record.refs.map((ref) => this.currentGeneration(ref));\n+    const frame = this.frameRecords.get(record.evidence.contextFrameId);\n+    // A semantic decision is grounded in the complete frame it received.\n+    // Track every candidate generation so changes to evidence outside the\n+    // annotation's subject tuple still invalidate that exact-frame judgment.\n+    const generations = new Set(record.refs.map((ref) => this.currentGeneration(ref)));\n+    if (frame && (!record.evidence.contextHash || frame.hash === record.evidence.contextHash)) {\n+      for (const candidate of frame.candidates) {\n+        generations.add(candidate.knowledgeGeneration);\n+      }\n+    }\n     this.linkDependencies(id, [...record.dependsOn, ...generations]);\n     // A result published after its evidence changed is stale on arrival; keep\n     // it as history but never let it masquerade as current. The check covers\n@@ -308,10 +317,12 @@ class InMemoryContextStore implements SemanticContextStoreContract {\n     if (annotation.evidence.contextHash && frame.hash !== annotation.evidence.contextHash) {\n       return false;\n     }\n-    for (const ref of annotation.refs) {\n-      const key = entityRefKey(ref);\n-      const candidate = frame.candidates.find((entry) => entityRefKey(entry.ref) === key);\n-      if (!candidate) return false;\n+    const candidateKeys = new Set(frame.candidates.map((candidate) => entityRefKey(candidate.ref)));\n+    if (annotation.refs.some((ref) => !candidateKeys.has(entityRefKey(ref)))) return false;\n+    // The trace records the exact frame hash, so the full frame read set must\n+    // still be current when its result is published.\n+    for (const candidate of frame.candidates) {\n+      const key = entityRefKey(candidate.ref);\n       if (candidate.knowledgeGeneration !== this.generationByRef.get(key)) return false;\n     }\n     return true;\n",
      "prompt": "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and retain behavioral regression coverage.\n\nF73 was fixed in 1f21302. Verify packages/context/src/store.ts:280-327 against its recorded baseline at a685f92. Annotations were linked to the current generations of their subject refs, although a semantic judgment had received an entire recorded frame. A belief about A could depend on comparing A with B while naming only A as its subject. Updating B left that belief current. Use recorded frame candidate generations for dependency links and late-result checks; retain subject membership and explicit dependency validation. Exercise postmerge-context.test.ts: changed peer invalidation, late arrival, unrelated-ref control, and outside-frame subject rejection. Preserve both passing controls and legacy journal compatibility where applicable. Do not add a new service, recovery framework, or second interpretation path. If the fix still holds, report that result rather than rewriting it."
    },
    {
      "id": "F74",
      "title": "Decode backend values once and require actual probability evidence",
      "file": "packages/perception/src/validation.ts",
      "lines": "181-246",
      "severity": "major",
      "pBadge": "P1",
      "category": "correctness",
      "bodyParagraphs": [
        "The shared decoder claimed to read untrusted output once, but read status and answer identities multiple times. A changing status getter could be AMBIGUOUS during validation and RESOLVED in the returned result. Earlier independent PR #5 probes exposed this inherited limitation; the whole-product pass closes it.",
        "Throwing accessors also escaped the public analysis API. Score-array entries were inspected and then copied through a second read. Probability copying mishandled __proto__ option IDs, and a missing chosen probability could be mistaken for an inherited object property. These are boundary defects even though ordinary JSON responses rarely exercise changing getters.",
        "The fix snapshots each value before checking it, contains unreadable responses as structured failures, preserves own probability entries, and checks chosen-option ownership explicitly. The workflow regression verifies that an ambiguous response cannot enter accepted state. This adds no alternate decoder or vendor-specific path."
      ],
      "fix": "Reuse captured status and id values, copy score entries once, catch unreadable output, build own probability entries with Object.fromEntries, and check Object.hasOwn for the chosen option.",
      "tests": "postmerge-perception.test.ts: six decoder cases; previous external Jev probes N2 and N3 now pass.",
      "status": "fixed",
      "fixedAt": "2026-09-23T22:17:19.008880+00:00",
      "reviewRound": 1,
      "type": "logic",
      "section": "actionable",
      "followUp": false,
      "suggestedDiff": "diff --git a/packages/perception/src/validation.ts b/packages/perception/src/validation.ts\nindex 878a17b..ed00cf3 100644\n--- a/packages/perception/src/validation.ts\n+++ b/packages/perception/src/validation.ts\n@@ -179,14 +179,13 @@ export function validateDecisionSpecs(specs: DecisionSpec[]): SpecValidationIssu\n }\n \n function decodeAnswer(\n-  spec: DecisionSpec,\n+  id: string,\n   answer: Record<string, unknown>,\n ): DecisionAnswer | undefined {\n   // Each field is read exactly once: a getter cannot present one value to the\n   // validator and another to policy.\n-  const id = answer.id;\n   const kind = answer.kind;\n-  if (typeof id !== \"string\" || typeof kind !== \"string\") return undefined;\n+  if (typeof kind !== \"string\") return undefined;\n   if (kind === \"binary\") {\n     const probability = answer.probability;\n     return { id, kind: \"binary\", probability: probability as number };\n@@ -196,25 +195,25 @@ function decodeAnswer(\n     const choice = answer.choice;\n     const confidence = answer.confidence;\n     if (!isRecord(rawProbabilities)) return undefined;\n-    const probabilities: Record<string, number> = {};\n-    for (const [optionId, value] of Object.entries(rawProbabilities)) {\n+    const entries = Object.entries(rawProbabilities);\n+    for (const [, value] of entries) {\n       if (typeof value !== \"number\") return undefined;\n-      probabilities[optionId] = value;\n     }\n+    const probabilities = Object.fromEntries(entries) as Record<string, number>;\n     return { id, kind: \"choice\", choice: choice as string, probabilities, confidence: confidence as number };\n   }\n   if (kind === \"score\") {\n     const rawProbabilities = answer.probabilities;\n     const score = answer.score;\n     const confidence = answer.confidence;\n-    if (!Array.isArray(rawProbabilities) || rawProbabilities.some((value) => typeof value !== \"number\")) {\n-      return undefined;\n-    }\n+    if (!Array.isArray(rawProbabilities)) return undefined;\n+    const probabilities: unknown[] = [...rawProbabilities];\n+    if (probabilities.some((value) => typeof value !== \"number\")) return undefined;\n     return {\n       id,\n       kind: \"score\",\n       score: score as number,\n-      probabilities: [...(rawProbabilities as number[])],\n+      probabilities: probabilities as number[],\n       confidence: confidence as number,\n     };\n   }\n@@ -236,6 +235,16 @@ export function analyzePerceptionResult(\n   specs: DecisionSpec[],\n   result: unknown,\n ): DecodedPerceptionResult {\n+  try {\n+    return analyzeResult(specs, result);\n+  } catch {\n+    // Accessors and proxies are still untrusted output. Do not invoke their\n+    // string conversion while reporting an unreadable result.\n+    return { issues: [{ specId: \"result\", kind: \"malformed-result\", detail: \"backend result could not be read\" }] };\n+  }\n+}\n+\n+function analyzeResult(specs: DecisionSpec[], result: unknown): DecodedPerceptionResult {\n   const issues: PerceptionValidationIssue[] = [];\n \n   if (!isRecord(result)) {\n@@ -249,17 +258,16 @@ export function analyzePerceptionResult(\n       ],\n     };\n   }\n-  if (\n-    typeof result.status !== \"string\" ||\n-    !PERCEPTION_STATUSES.includes(result.status as PerceptionStatus)\n-  ) {\n+  const status = result.status;\n+  const rawAnswers = result.answers;\n+  if (typeof status !== \"string\" || !PERCEPTION_STATUSES.includes(status as PerceptionStatus)) {\n     issues.push({\n       specId: specs[0]?.id ?? \"result\",\n       kind: \"malformed-status\",\n-      detail: `backend status ${String(result.status)} is not a recognized perception status`,\n+      detail: `backend status ${String(status)} is not a recognized perception status`,\n     });\n   }\n-  if (!Array.isArray(result.answers)) {\n+  if (!Array.isArray(rawAnswers)) {\n     issues.push({\n       specId: specs[0]?.id ?? \"result\",\n       kind: \"malformed-answers\",\n@@ -268,13 +276,13 @@ export function analyzePerceptionResult(\n     return { issues };\n   }\n   const MAX_ANSWERS = 256;\n-  if (result.answers.length > MAX_ANSWERS) {\n+  if (rawAnswers.length > MAX_ANSWERS) {\n     return {\n       issues: [\n         {\n           specId: specs[0]?.id ?? \"result\",\n           kind: \"malformed-answers\",\n-          detail: `backend returned ${result.answers.length} answers (limit ${MAX_ANSWERS})`,\n+          detail: `backend returned too many answers (limit ${MAX_ANSWERS})`,\n         },\n       ],\n     };\n@@ -283,7 +291,7 @@ export function analyzePerceptionResult(\n   const requestedIds = new Set(specs.map((spec) => spec.id));\n   const byId = new Map<string, DecisionAnswer>();\n   const malformedIds = new Set<string>();\n-  for (const raw of result.answers) {\n+  for (const raw of rawAnswers) {\n     if (!isRecord(raw)) {\n       issues.push({\n         specId: specs[0]?.id ?? \"result\",\n@@ -292,23 +300,23 @@ export function analyzePerceptionResult(\n       });\n       continue;\n     }\n-    if (typeof raw.id === \"string\" && !requestedIds.has(raw.id)) {\n+    const id = raw.id;\n+    if (typeof id === \"string\" && !requestedIds.has(id)) {\n       issues.push({\n-        specId: raw.id,\n+        specId: id,\n         kind: \"unexpected-answer\",\n-        detail: `backend returned an answer for unrequested decision ${raw.id}`,\n+        detail: `backend returned an answer for unrequested decision ${id}`,\n       });\n       continue;\n     }\n-    const spec = specs.find((candidate) => candidate.id === raw.id);\n-    const decoded = spec ? decodeAnswer(spec, raw) : undefined;\n+    const decoded = typeof id === \"string\" ? decodeAnswer(id, raw) : undefined;\n     if (!decoded) {\n       issues.push({\n-        specId: typeof raw.id === \"string\" ? raw.id : (specs[0]?.id ?? \"result\"),\n+        specId: typeof id === \"string\" ? id : (specs[0]?.id ?? \"result\"),\n         kind: \"malformed-answer\",\n-        detail: `answer ${String(raw.id)} has an unrecognized shape`,\n+        detail: `answer ${String(id)} has an unrecognized shape`,\n       });\n-      if (typeof raw.id === \"string\") malformedIds.add(raw.id);\n+      if (typeof id === \"string\") malformedIds.add(id);\n       continue;\n     }\n     if (byId.has(decoded.id)) {\n@@ -390,7 +398,7 @@ export function analyzePerceptionResult(\n           });\n         }\n       }\n-      if (optionIds.has(answer.choice) && answer.probabilities[answer.choice] === undefined) {\n+      if (optionIds.has(answer.choice) && !Object.hasOwn(answer.probabilities, answer.choice)) {\n         issues.push({\n           specId: spec.id,\n           kind: \"invalid-choice\",\n@@ -475,17 +483,17 @@ export function analyzePerceptionResult(\n     }\n     answers.push(answer);\n   }\n+  const backend = result.backend;\n+  const model = result.model;\n+  const latencyMs = result.latencyMs;\n   return {\n     issues: [],\n     decoded: {\n-      status: result.status as PerceptionStatus,\n+      status: status as PerceptionStatus,\n       answers,\n-      backend: typeof result.backend === \"string\" ? result.backend : \"unknown\",\n-      model: typeof result.model === \"string\" ? result.model : \"unknown\",\n-      latencyMs:\n-        typeof result.latencyMs === \"number\" && Number.isFinite(result.latencyMs)\n-          ? result.latencyMs\n-          : 0,\n+      backend: typeof backend === \"string\" ? backend : \"unknown\",\n+      model: typeof model === \"string\" ? model : \"unknown\",\n+      latencyMs: typeof latencyMs === \"number\" && Number.isFinite(latencyMs) ? latencyMs : 0,\n     },\n   };\n }\n",
      "prompt": "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and retain behavioral regression coverage.\n\nF74 was fixed in 1f21302. Verify packages/perception/src/validation.ts:181-246 against its recorded baseline at a685f92. The shared decoder claimed to read untrusted output once, but read status and answer identities multiple times. A changing status getter could be AMBIGUOUS during validation and RESOLVED in the returned result. Earlier independent PR #5 probes exposed this inherited limitation; the whole-product pass closes it. Reuse captured status and id values, copy score entries once, catch unreadable output, build own probability entries with Object.fromEntries, and check Object.hasOwn for the chosen option. Exercise postmerge-perception.test.ts: six decoder cases; previous external Jev probes N2 and N3 now pass. Preserve both passing controls and legacy journal compatibility where applicable. Do not add a new service, recovery framework, or second interpretation path. If the fix still holds, report that result rather than rewriting it."
    },
    {
      "id": "F75",
      "title": "Return the same immutable frame contract that the store records",
      "file": "packages/context/src/assembly.ts",
      "lines": "637-659",
      "severity": "minor",
      "pBadge": "P2",
      "category": "data-integrity",
      "bodyParagraphs": [
        "Assembly persisted an immutable frame copy but returned a mutable frame to its caller. A caller could change candidate attributes or coverage while retaining the original frame ID and hash. The returned object would then disagree with the stored evidence.",
        "Assembly now returns a detached, deeply frozen snapshot. The regression checks nested candidate attributes and equality with the stored frame. One existing adversarial Jev test intentionally modifies a fixture; it now makes a detached copy first. Consumers that construct alternate frames must likewise construct a new value rather than mutate an assembled one."
      ],
      "fix": "Clone and deep-freeze the constructed frame before recording and returning it.",
      "tests": "postmerge-context.test.ts: nested returned frame immutability and stored evidence consistency.",
      "status": "fixed",
      "fixedAt": "2026-09-23T22:17:19.008880+00:00",
      "reviewRound": 1,
      "type": "logic",
      "section": "actionable",
      "followUp": false,
      "suggestedDiff": "diff --git a/packages/context/src/assembly.ts b/packages/context/src/assembly.ts\nindex 4fba3dd..4208b1a 100644\n--- a/packages/context/src/assembly.ts\n+++ b/packages/context/src/assembly.ts\n@@ -135,6 +135,17 @@ function round4(value: number): number {\n   return Math.round(value * 10000) / 10000;\n }\n \n+function immutableSnapshot<T>(value: T): T {\n+  const copy = structuredClone(value);\n+  const freeze = (entry: unknown): void => {\n+    if (!entry || typeof entry !== \"object\" || Object.isFrozen(entry)) return;\n+    Object.freeze(entry);\n+    for (const child of Object.values(entry as Record<string, unknown>)) freeze(child);\n+  };\n+  freeze(copy);\n+  return copy;\n+}\n+\n function resolveBudget(policy: AssemblyPolicy, expansionLevel: number): { level: number; budget: AssemblyBudget } {\n   const level = Math.max(0, Math.min(expansionLevel, policy.expansions.length - 1));\n   return { level, budget: policy.expansions[level] ?? policy.defaults };\n@@ -623,7 +634,7 @@ export async function assembleFrame(\n   const hash = hashOf(payload);\n \n   const retrievedBy = [...new Set(candidates.flatMap((candidate) => candidate.reasons))].sort();\n-  const frame: ContextFrame = {\n+  const frame: ContextFrame = immutableSnapshot({\n     id: `frame:${request.interactionId}:${request.observationId}:${level}:${store.version()}:${hash.slice(0, 8)}`,\n     version: store.version(),\n     hash,\n@@ -642,7 +653,7 @@ export async function assembleFrame(\n       generatedAt: request.now,\n       completeness,\n     },\n-  };\n+  });\n   // Persist the immutable input frame so an interpretation can later prove\n   // which read set and coverage it was based on.\n   store.recordFrame(frame);\n",
      "prompt": "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and retain behavioral regression coverage.\n\nF75 was fixed in 1f21302. Verify packages/context/src/assembly.ts:637-659 against its recorded baseline at a685f92. Assembly persisted an immutable frame copy but returned a mutable frame to its caller. A caller could change candidate attributes or coverage while retaining the original frame ID and hash. The returned object would then disagree with the stored evidence. Clone and deep-freeze the constructed frame before recording and returning it. Exercise postmerge-context.test.ts: nested returned frame immutability and stored evidence consistency. Preserve both passing controls and legacy journal compatibility where applicable. Do not add a new service, recovery framework, or second interpretation path. If the fix still holds, report that result rather than rewriting it."
    },
    {
      "id": "F76",
      "title": "Do not expose the in-memory journal\u2019s backing records",
      "file": "packages/core/src/journal.ts",
      "lines": "15-18",
      "severity": "minor",
      "pBadge": "P2",
      "category": "data-integrity",
      "bodyParagraphs": [
        "The in-memory journal cloned appended entries but returned its backing array from entries(). TypeScript readonly protected only the type surface: JavaScript callers or nested object mutations could rewrite accepted history.",
        "The regression mutates an input ID through the returned history and appends another entry. Both changes previously affected the journal itself. Returning a structured clone preserves the reference journal\u2019s ownership boundary and keeps recovery dependent on recorded history."
      ],
      "fix": "Return a detached snapshot from entries(), preserving snapshot-on-append behavior.",
      "tests": "postmerge-kernel.test.ts: returned array and nested entry mutations do not rewrite journal history.",
      "status": "fixed",
      "fixedAt": "2026-09-23T22:17:19.008880+00:00",
      "reviewRound": 1,
      "type": "logic",
      "section": "actionable",
      "followUp": false,
      "suggestedDiff": "diff --git a/packages/core/src/journal.ts b/packages/core/src/journal.ts\nindex f77e692..7baaeda 100644\n--- a/packages/core/src/journal.ts\n+++ b/packages/core/src/journal.ts\n@@ -13,7 +13,9 @@ export function createInMemoryJournal(): InteractionJournal {\n       entries.push(structuredClone(entry));\n     },\n     entries(): readonly InteractionLogEntry[] {\n-      return entries;\n+      // The readonly type is only a compile-time promise. Return detached\n+      // records so a consumer cannot rewrite the journal's accepted history.\n+      return structuredClone(entries);\n     },\n   };\n }\n",
      "prompt": "Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and retain behavioral regression coverage.\n\nF76 was fixed in 1f21302. Verify packages/core/src/journal.ts:15-18 against its recorded baseline at a685f92. The in-memory journal cloned appended entries but returned its backing array from entries(). TypeScript readonly protected only the type surface: JavaScript callers or nested object mutations could rewrite accepted history. Return a detached snapshot from entries(), preserving snapshot-on-append behavior. Exercise postmerge-kernel.test.ts: returned array and nested entry mutations do not rewrite journal history. Preserve both passing controls and legacy journal compatibility where applicable. Do not add a new service, recovery framework, or second interpretation path. If the fix still holds, report that result rather than rewriting it."
    }
  ],
  "architecture": [
    "The architecture is credible for a contract-focused runtime. Core owns authorization, intent revisions, effects and recovery. Context owns observations, materialized frames and evidence dependencies. Perception produces accepted decisions and propositions; the compiler routes both public entry points through the same evaluation. The injected Jev adapter stays outside the mutation kernel. These boundaries solve real ownership problems and should be kept.",
    "The implementation is becoming difficult to reason about inside those boundaries. runtime.ts is 2,723 lines, core/types.ts is 1,231, and the accepted-turn compiler is 883. Repeated fixes have concentrated around shared state, completion ordering, evidence ownership and alternate entry points. Those are concrete maintenance risks. A line-count-driven rewrite would not improve correctness by itself; future changes should make transaction/effect transitions and their durable records easier to inspect together.",
    "The new fixes use existing contracts and add no runtime dependencies, queues, database layers or orchestration framework. The optional completion identity is the only journal shape addition. Whole-frame invalidation costs more recomputation than subject-only invalidation, but correctly represents what the model read. Narrowing it later requires recorded selective dependencies, not an assumption that the subject tuple is the entire input.",
    "I would continue from this implementation after the fixes are reviewed. I would not treat passing regression suites as proof that Circe already delivers its semantic product thesis. The next useful work is one real continuing interaction with restart and held-out language evaluation, rather than more abstractions around the scripted A/B demo."
  ],
  "limitations": [
    {
      "area": "Semantic durability",
      "detail": "Kernel interactions have a file journal. The semantic store is still an in-memory implementation: observations, relations, frames, decision traces and beliefs do not survive a host restart through that store. A persistent semantic-store adapter remains product work."
    },
    {
      "area": "Continuing interaction",
      "detail": "runInteractionPipeline creates a fresh context store and starts frame assembly with empty recentReferents on every call. It is useful integration scaffolding, not a continuing conversation host with durable semantic history."
    },
    {
      "area": "Real interpretation",
      "detail": "The inspector uses scripted responses for a fixed scenario. The Jev adapter accepts an injected client; a live SDK/transport host, representative held-out evaluation and outcome-calibrated policy thresholds are still absent."
    },
    {
      "area": "Operational scope",
      "detail": "The file journal is suitable for its current single-process contract. Multi-process coordination and compaction remain deferred. No load benchmark, live-model accuracy study or formal verification was performed in this pass."
    }
  ],
  "verification": {
    "baselineTests": 396,
    "finalTests": 411,
    "externalChecks": 147,
    "newEvalBaseline": {
      "failed": 12,
      "passed": 2
    },
    "newTestsAdded": 15,
    "typecheck": "pass",
    "boundaries": "pass",
    "inspector": "pass",
    "diffCheck": "pass"
  },
  "history": [
    {
      "round": 1,
      "at": "2026-09-23T22:17:19.008880+00:00",
      "actionableOpen": 0,
      "note": "Audited merged implementation, reproduced defects against baseline, fixed and verified in PR #8."
    }
  ],
  "syncMeta": {
    "instructions": "Keep baseline and finding bodies unchanged on subsequent reviews; append new findings and update fixed status. No numeric confidence score, per user preference."
  }
}
