From 8af7ad4bf7d798d0a18dffe892685dc8e033585d Mon Sep 17 00:00:00 2001 From: Mother Seara Date: Fri, 28 Aug 2026 16:25:44 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=9B=90=EC=9E=A5=EC=9D=84=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A0=20=ED=98=B8=EC=B6=9C=EC=9D=B4=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EC=97=88=EB=8B=A4=EA=B3=A0=20=EB=A7=90=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=20(=EB=A7=89=EC=A7=80=20=EC=95=8A=EA=B3=A0=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B2=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 08-26 에 신고된 두 건은 **MCP 경로**였다. 0.41.0 이 세운 문(없는 원장 생성 거부)은 `main()` 안에 있어 CLI 에만 걸린다 — 이 서버의 모든 툴은 파이썬 API 로 가므로 그 문을 통째로 지나친다. 그래서 수리가 정작 신고된 경로에 닿지 않았다. API 를 막는 건 답이 아니다. 대부분의 호출자가 안 하는 실수 때문에 **전 레인 봉인이 멈춘다**. 그래서 막지 않고 **보이게** 한다. `mm_preregister`·`mm_retract`·`am_record`·`am_witness`·`pm_verify` 가 응답에 `⚠️ new_ledger_created` 를 실어 **절대경로**를 찍는다. `mm_preregister` 는 추가로 `lint` 안에 `㉘ ledger-birth` WARN 을 낸다 — 압축이 WARN 은 원문 그대로 두고 나머지는 개수로 접기 때문에 이 줄만 살아남는다. ★ 이건 스택의 다른 무엇도 못 메우는 구멍이다. 갓 태어난 원장의 체인은 **완벽히 성하다**. 그래서 무결성 검사는 전부 초록이고, 어떤 프로브도 이 파일이 새것이라고 말할 이유가 없다. 그 정보가 존재하는 순간은 **만드는 그 호출 하나뿐**이다. 시험 4건 — 그중 둘이 **음성대조**다(이어 쓸 때는 안 뜬다). 항상 뜨는 알림은 소음이고, 소음은 읽는 쪽이 건너뛰는 법을 배운다. pytest 66 통과(파이프 없이 · exit 0). Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 20 ++++++++++++++ mirror_stack_mcp/server.py | 56 ++++++++++++++++++++++++++++++++++---- tests/test_server.py | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f83d95..fffdecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,26 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). green even if they validated nothing. Both were confirmed to fail on the old hint before this was committed. +- **The response now says when a call CREATED the ledger instead of appending to one.** + measure-mirror 0.41.0 taught its CLI to refuse creating a ledger without `--new-ledger`. + That gate lives in `main()`; every tool here goes through the Python API and misses it + entirely — so the two lanes reported on 2026-08-26 sealing into unaudited ledgers were + both on *this* path, and the fix never reached them. + + Blocking the API is not the answer: it would stop every lane mid-seal for a mistake most + callers are not making. So this does not block. `mm_preregister`, `mm_retract`, + `am_record`, `am_witness` and `pm_verify` add a `⚠️ new_ledger_created` field naming the + **absolute** path, and `mm_preregister` also raises a `㉘ ledger-birth` WARN inside `lint` + (WARN survives output compaction; everything else is collapsed to a count). + + This is the gap nothing else in the stack can close: the chain of a brand-new ledger is + perfectly intact, so every integrity check is green and no probe has any reason to + mention that the file is new. The only moment the information exists is the call that + creates it. + + A negative control ships with it — the notice must NOT fire when appending, or it is + noise, and noise is what a reader learns to skip. + --- ## [0.2.12] — 2026-08-28 diff --git a/mirror_stack_mcp/server.py b/mirror_stack_mcp/server.py index 87f4806..4c1153e 100644 --- a/mirror_stack_mcp/server.py +++ b/mirror_stack_mcp/server.py @@ -181,6 +181,34 @@ def _remind(tool, result): return f"{result}\n\n{msg}" +# ── a ledger that did not exist is being born, not appended to ─────────────── +# measure-mirror 0.41.0 taught the CLI to refuse creating a ledger without +# `--new-ledger`. The Python API did not change, and every tool here goes through +# the API — so the whole gate misses this server. Two lanes were reported on +# 2026-08-26 sealing into ledgers nobody audits, both via this path. +# +# Blocking the API would stop every lane mid-seal for a mistake most callers are +# not making, so this does not block. It makes the birth VISIBLE in the response +# the caller already reads — which is the part that was missing: the chain is +# intact either way, so every integrity check stays green and nothing else in the +# stack will ever mention that this file is new. +_BIRTH_KEY = "⚠️ new_ledger_created" + + +def _birth_msg(path: str) -> str: + return (f"{os.path.abspath(path)} did not exist — this call CREATED it rather than " + "appending to an existing ledger. If you meant an existing ledger, what you " + "just sealed is in a file no audit covers, and its chain will verify green " + "forever. Check the path (and which directory you are in), then re-seal in " + "the right ledger — append-only means this entry stays where it is.") + + +def _birth(path: str, existed: bool) -> dict: + """The response field that distinguishes 'appended' from 'created'. Empty when + the ledger already existed, so a normal call carries no extra noise.""" + return {} if existed else {_BIRTH_KEY: _birth_msg(path)} + + # ───────────────────────── 🪞 measure-mirror (claims) ───────────────────────── @mcp.tool() def mm_preregister(ledger_path: str, claim_id: str, metric: str, min_n: int = 200, @@ -203,13 +231,20 @@ def mm_preregister(ledger_path: str, claim_id: str, metric: str, min_n: int = 20 The response carries an automatic seal-quality lint (`lint` key): a FAIL there means the compute gate will BLOCK this claim — fix and re-seal under a NEW claim_id.""" + existed = os.path.exists(ledger_path) entry = mm.preregister( ledger_path, claim_id, metric=metric, min_n=min_n, baseline=baseline, pass_threshold=pass_threshold, kill_condition=kill_condition, kill_threshold=kill_threshold, depends_on=depends_on, metric_range=metric_range, chance=chance, pre_seal_checks=pre_seal_checks) - lint = _compact(_findings(mm._preseal_lint(entry))) - return _remind("mm_preregister", {**entry, "lint": lint}) + findings = list(mm._preseal_lint(entry)) + if not existed: + # WARN level on purpose: compaction keeps WARN verbatim, and this is the one + # line that a green-everywhere response would otherwise never carry. + findings.append(mm.Finding("㉘ ledger-birth", "WARN", _birth_msg(ledger_path))) + lint = _compact(_findings(findings)) + return _remind("mm_preregister", {**entry, "lint": lint, + **_birth(ledger_path, existed)}) @mcp.tool() @@ -291,7 +326,9 @@ def mm_multiseed_check(seed_results: list[float], baseline: float = 0.5) -> str: @mcp.tool() def mm_retract(ledger_path: str, claim_id: str, reason: str) -> dict: """Append a chain-linked retraction (cannot be silently deleted; dependents go STALE).""" - return _remind("mm_retract", mm.retract(ledger_path, claim_id, reason)) + existed = os.path.exists(ledger_path) + return _remind("mm_retract", {**mm.retract(ledger_path, claim_id, reason), + **_birth(ledger_path, existed)}) @mcp.tool() @@ -352,14 +389,19 @@ def mm_preflight(ledger_path: str, claim_id: str, gate: str = "compute", def am_record(ledger_path: str, agent: str, action: str, target: str | None = None, payload: dict | None = None) -> dict: """Seal one agent action. Set target= to tie the action to a claim (J1).""" + existed = os.path.exists(ledger_path) return _remind("am_record", - am.record(ledger_path, agent=agent, action=action, target=target, payload=payload)) + {**am.record(ledger_path, agent=agent, action=action, + target=target, payload=payload), + **_birth(ledger_path, existed)}) @mcp.tool() def am_witness(my_ledger: str, peer_ledger: str, peer_name: str) -> dict: """Pin a peer ledger's head into mine (J3). Catches whole-ledger replacement that chains miss.""" - return am.witness_peer(my_ledger, peer_ledger, peer_name=peer_name) + existed = os.path.exists(my_ledger) + return {**am.witness_peer(my_ledger, peer_ledger, peer_name=peer_name), + **_birth(my_ledger, existed)} @mcp.tool() @@ -373,7 +415,9 @@ def am_verify(ledger_path: str) -> list[str]: def pm_verify(file_path: str, ledger_path: str = "pm_ledger.jsonl", origin: str | None = None) -> dict: """Verify a content file's provenance/integrity across 5 signals (a verifier, not a detector).""" - return pm.verify(file_path, ledger_path=ledger_path, origin=origin) + existed = os.path.exists(ledger_path) + return {**pm.verify(file_path, ledger_path=ledger_path, origin=origin), + **_birth(ledger_path, existed)} # ───────────────────────── 🪞🔎🪪 stack-level ───────────────────────────────── diff --git a/tests/test_server.py b/tests/test_server.py index 27e5b26..fcd698f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -307,3 +307,51 @@ def narrow(ledger_path: str, claim_id: str, metric: str, func_metadata(narrow).arg_model.model_validate( {"ledger_path": "x", "claim_id": "c", "metric": "acc", "pre_seal_checks": _STRUCTURED}) + + +# ── created vs appended ────────────────────────────────────────────────────── +# The CLI gate added in measure-mirror 0.41.0 lives in main(); every tool here +# goes through the Python API and misses it entirely. Blocking the API would stop +# every lane mid-seal, so the response says so instead. What matters is that it +# says so ONLY when the ledger is actually new — a notice that always fires is +# noise, and noise is what a reader learns to skip. + +def test_preregister_says_so_when_it_creates_the_ledger(tmp_path): + led = tmp_path / "typo.jsonl" + assert not led.exists() + r = s.mm_preregister(str(led), "c1", metric="acc", min_n=240, + kill_threshold={"metric": "acc", "threshold": 0.55, + "direction": "below"}, + pre_seal_checks=[{"name": "neutral-control", + "result": "not_fired"}]) + assert s._BIRTH_KEY in r + assert str(led.resolve()) in r[s._BIRTH_KEY] + # and it survives compaction, which keeps WARN verbatim and collapses the rest + assert any("ledger-birth" in line and "WARN" in line for line in r["lint"]) + + +def test_preregister_is_silent_when_it_appends(tmp_path): + # negative control: the notice must not fire on the normal path, or it means nothing. + led = tmp_path / "real.jsonl" + s.mm_preregister(str(led), "c1", metric="acc", min_n=240, + kill_threshold={"metric": "acc", "threshold": 0.55, + "direction": "below"}) + r = s.mm_preregister(str(led), "c2", metric="acc", min_n=240, + kill_threshold={"metric": "acc", "threshold": 0.55, + "direction": "below"}) + assert s._BIRTH_KEY not in r + assert not any("ledger-birth" in line for line in r["lint"]) + + +def test_am_record_says_so_when_it_creates_the_ledger(tmp_path): + led = tmp_path / "actions.jsonl" + r = s.am_record(str(led), agent="t", action="a") + assert s._BIRTH_KEY in r + r2 = s.am_record(str(led), agent="t", action="b") + assert s._BIRTH_KEY not in r2 # negative control on the same file + + +def test_retract_says_so_when_it_creates_the_ledger(tmp_path): + led = tmp_path / "r.jsonl" + r = s.mm_retract(str(led), "never-registered", reason="wrong ledger") + assert s._BIRTH_KEY in r From f3cee7c6afab4bb7ac8d2dac1ce7c668184b6943 Mon Sep 17 00:00:00 2001 From: Mother Seara Date: Sun, 30 Aug 2026 11:34:13 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EC=83=88=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EB=B8=8C=20=EB=B2=88=ED=98=B8=EB=A5=BC=20=E3=89=99=20=EB=A1=9C?= =?UTF-8?q?=20=E2=80=94=20=E3=89=98=20=EB=8A=94=20subspace=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=EA=B8=B0=EA=B0=80=20=EC=9D=B4=EB=AF=B8=20=EC=93=B4?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카드 171 에 대한 대장님 답 b(기호만 안 겹치는 것으로 바꾼 뒤 머지) 집행이다. measure-mirror 의 mm.py 는 ㉑~㉘ 를 한 등록부로 쓰고, 그 ㉘ 는 이미 Subspace claim — declaration auditor 다(하위 항목 schema / no-anchor / energy-not-matched / null-ladder). 이 PR 이 붙인 ledger-birth 가 같은 번호를 두 번째 뜻으로 쓰고 있었다. 다음 빈 번호인 ㉙ 로 옮긴다. 바꾼 것은 기호 두 자리뿐이다 — server.py 의 Finding 이름과 0.2.13 절 한 줄. 0.2.10 절의 옛 ㉘ 두 줄은 진짜 ㉘ 를 가리키므로 그대로 뒀다. 시험은 기호가 아니라 ledger-birth 문자열로 검사하므로 안 깨진다. pytest 66 통과. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- mirror_stack_mcp/server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fffdecc..aceb71a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Blocking the API is not the answer: it would stop every lane mid-seal for a mistake most callers are not making. So this does not block. `mm_preregister`, `mm_retract`, `am_record`, `am_witness` and `pm_verify` add a `⚠️ new_ledger_created` field naming the - **absolute** path, and `mm_preregister` also raises a `㉘ ledger-birth` WARN inside `lint` + **absolute** path, and `mm_preregister` also raises a `㉙ ledger-birth` WARN inside `lint` (WARN survives output compaction; everything else is collapsed to a count). This is the gap nothing else in the stack can close: the chain of a brand-new ledger is diff --git a/mirror_stack_mcp/server.py b/mirror_stack_mcp/server.py index 4c1153e..7578d46 100644 --- a/mirror_stack_mcp/server.py +++ b/mirror_stack_mcp/server.py @@ -241,7 +241,7 @@ def mm_preregister(ledger_path: str, claim_id: str, metric: str, min_n: int = 20 if not existed: # WARN level on purpose: compaction keeps WARN verbatim, and this is the one # line that a green-everywhere response would otherwise never carry. - findings.append(mm.Finding("㉘ ledger-birth", "WARN", _birth_msg(ledger_path))) + findings.append(mm.Finding("㉙ ledger-birth", "WARN", _birth_msg(ledger_path))) lint = _compact(_findings(findings)) return _remind("mm_preregister", {**entry, "lint": lint, **_birth(ledger_path, existed)})