Skip to content

feat(F257): #4 签名 lint O2→O1 — message-signature detection layer (observe-only) - #47

Merged
mindfn merged 3 commits into
develop_basefrom
feat/f257-console-sig-lint
Jul 21, 2026
Merged

feat(F257): #4 签名 lint O2→O1 — message-signature detection layer (observe-only)#47
mindfn merged 3 commits into
develop_basefrom
feat/f257-console-sig-lint

Conversation

@mindfn

@mindfn mindfn commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What

F257 修复清单 #4 — message 末行签名 [昵称/模型🐾]O2→O1 结构升级(检测层)。把"签名必须含模型型号"从纯 prompt 约定(O2 观测层;dev-7a882ba0 靠 operator 人工发现)升级为 regex-decidable 结构断言(O1 结构强制),结果落 message.extra.signatureLint,供 Console / eval 消费。

Why

四层 harness 里签名约定零结构覆盖(dev-7a882ba0 实锤:Fable 漏签靠 operator 人工发现)。#4 = 自动检测缺签 / 不合规签名,替代人工发现。

Design

  • 严格契约 matcher[nickname/model🐾],首斜杠分隔 nickname/model,model 允许 provider-qualified(含斜杠,如 opencode 运行时 codex-for-me/gpt-5.4),两段 trim 后非空必需。不复用 routing 的 permissive strip matcher(isCatSignatureLine 是 strip 用,容忍无模型 [Spark🐾]/无爪 [砚砚/GPT-5.5],作 compliance 会漏判)。presence-only;identity-correctness(签名匹配发帖猫)deferred。
  • observe-only 非阻断:记录 miss,绝不 reject 已持久化的用户可见消息(逼近铁律5/硬排除)。契合 F257「触发可观测」。
  • 全链 reachabilityextra.signatureLint 在 persist(callback×2 + serial/parallel stream-final)→ Redis serialize/parse round-trip → GET + socket broadcast(primary + duplicate-recovery)→ web 投影链(cold-hydration / merge-reconcile / background+active callback side-patch 全体)逐点透传。§16e 全量 sweep(34 点)确认无遗漏;pickSignatureLint 1-line forwarder per-package DRY(异构 allowlist 不可统一 projector)。
  • 两阶段(AC 完成 ≠ feature 完成):本 PR = 检测层账本闭环(miss 自动 emit deviation 归因 obj-identity-integrity,自动化 dev-7a882ba0 的手动 report)deferred 到 feat(#476): unify callback auth — headers replace body/query #3(objective registry)——harness ledger 读 DeviationEventLog/eval verdict 不扫 message.extra;正确 deviation 需 registered objective。详见 spec feat(#603): .local / .local-override rule overlay support #4 + cat-signature-lint.ts SCOPE 注释。

Review

跨家族 review(缅因猫 sol / gpt-5.6-sol)R1→R5,APPROVE @ f8d3c33da。含 §16e failure-mode sweep(extra allowlist 全链投影漏字段的重复失效模式,逮到初版 checklist 外的 3 个漏点)。

Tests

  • 纯函数:strict matcher(provider-qualified / 空段 / 契约形态 / FP)+ signatureLintExtra + pickSignatureLint + mergeMessageExtra(web,含 signatureLint-only guard 不变式)。
  • 集成:serial/parallel stream-final 入分母;callback broadcast + GET cold-hydration;Redis serialize→parse round-trip(valid/malformed/其他字段不受污染)。
  • gate:基线定性法(对 baseline 0 新失败);biome clean、callbacks 复杂度对基线 0 delta、无 any、web tsc clean。

Scope

检测层完成;账本闭环 deferred 到切片 2(#3 objective registry)。两阶段已在 spec #4 显式在案。


🤖 Generated with Claude Code

mindfn and others added 3 commits July 20, 2026 22:00
Why: 消息末行 [昵称/模型🐾] 签名此前仅靠 prompt 文本约定(O2 观测层),零
结构覆盖 → dev-7a882ba0(Fable 漏签靠 operator 人工发现)。本切片交付 #4 的
**检测层**(O1 结构强制):agent 消息末行是否以被识别的猫签名结尾。

两阶段(owner vision-guardian 校准,AC 完成 ≠ feature 完成):
- ① 检测层(本切片):复用 isCatSignatureLine(F167,不重造正则)→ post-seam
  记 message.extra.signatureLint(message 级可观测、denominator-bearing、
  observe-only 非阻断,绝不 reject 已持久化用户可见消息)。
- ② 账本闭环(deferred 到切片 2 / #3 后):检测到 miss 自动 emit deviation
  归因 obj-identity-integrity 进 harness ledger,自动化 dev-7a882ba0 的手动
  report_harness_signal。deferred 原因:ledger 读 DeviationEventLog/
  GuardRejectionEventLog/eval verdict 不扫 message.extra;正确 deviation 需
  registered objective(否则重蹈 #3 修的 free-string 考古)+ segment/condition
  归因基建(condition_hit 需 condition registry+typed fact,均属 #2/#3 数据根)。
  extra-only ≠ #4 完成——已在 spec #4 + 代码 SCOPE 注释显式在案。

设计自决:observe-only 非阻断(阻断逼近铁律5/硬排除,风险高);#4 检测 ⊥ #3
(不依赖 registry);signatureLintExtra helper 提取使 post-message 巨型 handler
biome 复杂度对基线 0 delta。

证据:19 单测(纯函数+helper)+ 2 wiring e2e(extra.signatureLint signed
true/false)+ 79 blast-radius 回归全绿;tsc/biome clean;新文件 65 行、无 any。

[宪宪/claude-opus-4-8🐾]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
R3 review 四项全部核实成立(读码验证,非反射接受),本 commit 逐项修复 + red→green:

P1-3 严格契约 matcher(correctness):lintCatSignature 之前复用 isCatSignatureLine
(routing 的 permissive STRIP matcher,容忍 [Spark🐾] 无模型 / [砚砚/GPT-5.5] 无爪),
把契约违规签名误判为 compliant。改用严格 `[nickname/model🐾]` 正则(slash+model+paw
必需),断言 governance-l0「签名必须含模型型号」。strip=permissive(routing) 与
lint=strict(compliance) 分离。两个原正例 flip 成反例 + 保留严格正例。

P1-1 覆盖(coverage):签名 lint 之前只接 callback post-message 两支,漏掉 serial/
parallel 以 origin:'stream' 持久化的普通 agent 最终文本 → 从不 post_message 的猫不入
分母,签名率系统性偏差。route-serial.ts:2805 + route-parallel.ts:1248 各接
signatureLintExtra(storedContent)。加 serial+parallel 集成测试(signed/unsigned 均入分母)。

P1-2 持久化(durability):serializeExtra 写入但 safeParseExtra 白名单重建时丢弃
signatureLint → Redis 回读后字段消失(in-memory 测试测不到)。parser 加 signatureLint
strict 支持(typeof signed === 'boolean')+ serialize→parse round-trip 测试(true/false/
malformed drop/其他字段不受影响)。

P2-1 可达(reachability):GET /api/messages 白名单、两处 callback socket broadcast、
web ChatMessage.extra、server AgentMessage.extra 均漏 signatureLint → Console 读不到。
逐一透传(broadcast 转发 storedMsg.extra.signatureLint,单一真相源)+ cold-hydration
(GET)+ live-broadcast 契约测试。无 UI 渲染(#6 消费)。

证据:F257 slice-1 全套 82 tests + blast-radius 回归 60 tests 全绿;tsc/web-tsc clean;
biome 新文件 clean,route-serial/parallel/callbacks 复杂度对基线 0 delta;safeParseExtra
48→50、messages GET 70→73(+2/+3,逐字段 established pattern,函数本已远超限,无新增
erroring function)。两阶段账本闭环仍 deferred(sol 已 accept)。

[宪宪/claude-opus-4-8🐾]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sol R4 REQUEST CHANGES 两项核实成立(读码 + runtime 真相源),逐项修 + §16e 全量 sweep:

P1 严格 matcher 拒真实模型标识(correctness):STRICT_SIGNATURE_LINE_RE 假设单斜杠,但
opencode 运行时 defaultModel="codex-for-me/gpt-5.4"(provider-qualified,含斜杠)→
[金渐层/codex-for-me/gpt-5.4🐾] 被误判 unsigned、按 cohort 污染分母。改:首斜杠分隔
nickname/model,model 允许 provider-qualified 斜杠,两段 trim 后非空必需(拒 [ /m🐾] /
[n/ 🐾])。flip multi-slash 为正例 + roster provider-qualified 正例 + 空段负例。

P2 §16e 全链 reachability(补锅匠 → failure-mode sweep):R3 只补 server 侧 + web
ChatMessage 类型,漏 web 投影链全体 + 2 处 dup-recovery broadcast。做全量 sweep(api 20
点 + web 14 点)逐点核 fixed/N-A,修复 14 点:
- API: A13/A14 dup-recovery broadcast forward(+ A11-A14 抽 pickSignatureLint helper,
  callbacks 复杂度对基线 0 delta)
- Web 类型: W3 AgentMsg / W4 BackgroundAgentMessage / W5 history-response
- Web runtime: W6 cold-hydration(guard+emit) / W7 mergeMessageExtra(sweep 发现的 uncited
  点) / W8-W14 useAgentMessages callback side-patch×7(pickSignatureLint 统一 forward)
- sweep 发现 sol 未列的 3 个 drop 点(W7/W13/W14)——证明逐点补 sol 列表会再 bounce

Divergence: 6+ 异构 allowlist(不同 key 子集),不可统一 projector;pickSignatureLint
1-line forwarder per-package DRY(sweep 建议)。

证据:api F257+回归 105 全绿;web reachability vitest 8 + web tsc clean;biome clean、
callbacks 复杂度 0-delta;无 any。A18/A19(F246 dispatch origination,fresh extra 无
drop)+ A16/A17(full passthrough)文档化 N-A。

[宪宪/claude-opus-4-8🐾]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mindfn
mindfn merged commit b256842 into develop_base Jul 21, 2026
mindfn added a commit that referenced this pull request Jul 21, 2026
…rity + fail-closed) (#48)

* feat(F257): #3 objective registry — 只读发现(取代 report_harness_signal objectiveId 考古)

修复清单 #3:report_harness_signal 的 objectiveId 是自由 string,零 list/schema/注入清单
→ 三次上报三次考古。本 PR 建 objective registry 只读发现层:

- registry YAML(docs/harness-feedback/objectives/registry.yaml,KD-3 registry-as-data)=
  定义层(id/statement/segments)。**只登 canonized 目标**——redesign §2 名义 8 objectives
  但只 canonize 了 2 个 id(obj-routing-delivery + obj-identity-integrity);不 invent 另
  7 个 id(会伪造 V2 依赖的契约),registry 随 canonize 增长。
- loader(objective-registry.ts):parseObjectiveRegistry 纯函数(malformed 行丢弃不抛、
  malformed YAML → 空)+ loadObjectiveRegistry fail-safe。
- GET /api/callbacks/objectives(镜像 rich-block-rules 只读路由)。
- cat_cafe_list_objectives MCP tool(镜像 rich-block-rules-tool;A_READ_LOCAL + readonly
  allowlist 注册)。
- report_harness_signal objectiveId description 同步指向 list_objectives。

边界:只做定义层 + 发现,不做 V2(evaluator/metrics/condition registry)。objectiveId 仍
自由(discovery-only,软校验留 V2)。

证据:9 tests(parseObjectiveRegistry 4 + loadObjectiveRegistry 2 + 路由 e2e + shipped
registry 验证)全绿;api + mcp-server build clean;biome clean。切片 2a,独立 #47。

[宪宪/claude-opus-4-8🐾]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(F257): #3 (2a) R1 — single authority + fail-closed + contract tests + map delta

Addresses sol's slice-2a R1 REQUEST CHANGES (all four, no pushback).

## P1-1 — remove `segments` (second writable authority)
The registry authored `id/statement/segments`, duplicating the unit→objective
membership that frozen redesign §4.8 keeps in the versioned UnitEvaluationManifest.
Dropped `segments` from YAML + loader type + API output + MCP tool output; the
registry is now the canonical `id + statement` definition layer only. Attachments
become a V2 derived read-model from UnitEvaluationManifest — one authority.

## P1-2 — fail-closed loader (no silent empty), strict validation, lower complexity
parseObjectiveRegistry/loadObjectiveRegistry now return a discriminated
ObjectiveRegistryResult ({ok:true,registry} | {ok:false,error}). Unreadable file /
malformed YAML / non-mapping root / non-positive-integer version / non-array
objectives / any invalid row / duplicate id → explicit failure, never
{version:1,objectives:[]}. Validates positive-integer version, trimmed non-empty id
matching ^obj-[a-z0-9]+(?:-[a-z0-9]+)*$, trimmed non-empty statement, unique ids
(sol repros now rejected: version -2.5, whitespace id/statement, dup ids). Route
returns 503 (uncached) on failure; MCP tool surfaces it via its existing !ok error
path. Validation extracted into helpers → below the Biome complexity-17 warning.
Route gains an optional injectable registry path (test seam).

## P2-1 — MCP contract tests restored to true green
Added cat_cafe_list_objectives to EXPECTED_TOOLS / EXPECTED_COLLAB_TOOLS /
EXPECTED_READONLY_TOOLS, and cat_cafe_report_harness_signal to TOOLS/COLLAB (the
PR #42 leftover that made the suite baseline-red 385/387) — not masking the new
mismatch behind the old failure name. Added focused handler tests (success /
honest empty / 503 HTTP failure / network error). Full mcp-server suite 391/391.

## P2-2 — architecture map delta
harness-eval cell: added objective-registry.ts / callback-docs-routes.ts /
list-objectives-tool.ts code anchors + registry.yaml doc anchor + scan hints + why
this is the definition layer of the SAME control plane, not a parallel registry.

## Evidence
- api: f257-objective-registry 18/0, callback-docs-route 4/0 (200 + 503 fail-closed).
- mcp-server full suite 391/391 (was 385/387).
- biome: changed files clean (useLiteralKeys + parser complexity + format resolved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(F257): #3 (2a) R2 — path-free 503 + strict key-set (reject unknown, not strip)

Addresses sol's 2a R2 REQUEST CHANGES (both small, no scope change). R1's four
findings were confirmed CLOSED.

## P2-1 — unauthenticated 503 leaked internal path / fs errno
callback-docs-routes.ts /api/callbacks/objectives is unauthenticated but returned the
loader's raw error, which contains the registry path + Node fs errno (e.g.
"...registry unreadable at /private/secret/install/objectives.yaml: ENOENT..."), forwarded
onward via the MCP tool's response.text(). Now: log the full internal reason server-side
(request.log.error) and return a stable, path-free 503 body `{ error: 'Objective registry
unavailable' }`. Test asserts the body contains neither the passed path nor ENOENT/errno,
and is uncached; the MCP tool still recognizes 503 as an errorResult.

## P2-2 — "strict + single authority" schema was still fail-open on forbidden fields
The loader read only id/statement and silently dropped everything else — so a stray
`segments` (or a typo / future unversioned field) could reappear in the YAML, mislead a
human into treating it as membership authority, while the runtime ignored it. Worse, the R1
test asserted that strip-behavior as "valid". Now registryVersion=1 fail-closes on the key
set: root allows only registryVersion/objectives; each entry allows only id/statement. A
`segments` key REJECTS (message points to the UnitEvaluationManifest §4.8 authority), not
strips; any unknown key rejects. Future fields must bump the schema. Removed the fail-open
"ignores segments" test; added red→green rejections for segments, unknown entry key, and
unknown root key.

## Evidence
- api: f257-objective-registry 21/0 (now includes segments/unknown-key rejections +
  descriptive-message assertion), callback-docs-route 4/0 (path-free 503 asserted).
- shipped registry.yaml still validates (id/statement only). biome changed files clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(F257): #3 (2a) R3 — pin registryVersion to exactly 1 (fail closed on unsupported)

Addresses sol's 2a R3 sole finding (R2's two remain CLOSED).

## P2-1 — unsupported schema versions were accepted under v1 semantics
The key-set contract advertised "registryVersion=1 allows only … / future fields must bump
schema", but parseObjectiveRegistry accepted EVERY positive integer and applied the v1
parser (repro: v1/v2/v999 all ok:true). That advertises an unimplemented version back to
discovery clients as supported. Now the version gate requires exactly SUPPORTED_REGISTRY_
VERSION (=1); anything else fails closed with a clear message. A future v2 must ship an
explicit versioned parser + bump the constant, not ride the positive-integer gate.

Tests: changed the version-2 "valid" fixture to 1; added v2 + v999 rejection cases
(the existing -2.5 / 0 / 1.5 / missing cases still reject as ≠1).

## Evidence
- f257-objective-registry 23/0. biome changed files clean; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@mindfn
mindfn deleted the feat/f257-console-sig-lint branch July 21, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant