Skip to content

feat(F171): migrate weixin-mp to plugin framework - #6

Closed
mindfn wants to merge 6 commits into
feat/f171-weixin-mpfrom
feat/f197-weixin-mp-plugin
Closed

feat(F171): migrate weixin-mp to plugin framework#6
mindfn wants to merge 6 commits into
feat/f171-weixin-mpfrom
feat/f197-weixin-mp-plugin

Conversation

@mindfn

@mindfn mindfn commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Migrates WeChat MP from hardcoded index.ts limb registration to declarative plugins/weixin-mp/plugin.yaml
  • Moves limbs/weixin-mp.ymlplugins/weixin-mp/limbs/weixin-mp.yml
  • Adds plugins/weixin-mp/skills/weixin-mp/SKILL.md
  • Platform-aware limbAdapterFactory: weixin-mp → WeixinMpLimbNode (with Redis), others → PluginLimbNode
  • Removes weixin-mp from BUILTIN_PLUGIN_IDS (env vars match WEIXIN_MP_* prefix, builtin trust not needed)

Depends on

Test plan

  • Registry scan: ['test-skill-only', 'weixin-mp']
  • 17 backend security tests pass
  • 1 frontend test pass
  • End-to-end: enable weixin-mp plugin → limb registered → healthCheck works

🐾 [宪宪/Opus-46]
🤖 Generated with Claude Code

[宪宪/Opus-4.6🐾]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mindfn
mindfn force-pushed the feat/f197-weixin-mp-plugin branch from 6245bd6 to d3d6b08 Compare May 12, 2026 01:25
mindfn and others added 4 commits May 12, 2026 09:27
Phase 1: Declarative plugin framework with manifest-based discovery,
three-layer env safety, loopback+identity guards on all write endpoints,
limb startup rehydration, and skill UI gating.

- PluginRegistry: scan plugins/, validate manifests, reject reserved IDs
- PluginResourceActivator: per-resource lifecycle (skill/mcp/limb)
- PluginLimbNode: generic ILimbNode adapter for plugin-declared limbs
- limb-yaml-loader: YAML → LimbDeclaration parser
- plugin-routes: CRUD + healthCheck with loopback/identity/audit guards
- Startup rehydration of enabled limb plugins from capabilities.json
- PerCatToggles disabled when parent plugin is disabled
- 17 security tests for manifest parsing + env safety

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P1: reject envName values that aren't valid shell variable names
(blocks newline injection into .env.local via crafted manifest).

P2: resolve limb nodeId from YAML declaration instead of using plugin
id for healthCheck lookup; return 501 for unimplemented mcpProbe.

20/20 security tests pass.

[宪宪/Opus-46🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
healthCheck.limbCommand must exist in the plugin's limb capability
commands whitelist — prevents using the test endpoint to probe
arbitrary commands outside the plugin's declared scope.

[宪宪/Opus-46🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2: WeChat MP article publishing moved from hardcoded to
declarative plugin framework.

- plugins/weixin-mp/plugin.yaml — manifest with config, limb, skill, healthCheck
- plugins/weixin-mp/limbs/weixin-mp.yml — capability declaration
- plugins/weixin-mp/skills/weixin-mp/SKILL.md — skill documentation
- WeixinMpLimbNode + weixin-mp domain code (token, client, markdown converter)
- limbAdapterFactory: weixin-mp → WeixinMpLimbNode (Redis), others → PluginLimbNode
- Startup rehydration uses platform-aware factory

weixin-mp not in BUILTIN_PLUGIN_IDS — env prefix WEIXIN_MP_* matches naturally.

[宪宪/Opus-46🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mindfn
mindfn force-pushed the feat/f197-weixin-mp-plugin branch from d3d6b08 to f946af5 Compare May 12, 2026 01:27
…tion

P1: Add URL validation (protocol/private-IP/localhost/metadata blocking)
and content-type/size checks to image fetch in weixin-mp-client.

P1: Split limb capabilities — read-only ops (check_status, list_drafts,
publish_status) as authLevel: free; write ops (publish_article,
upload_image) as authLevel: gated.

P2: Escape HTML entities in markdown converter, validate URL schemes
(block javascript:/data:), prevent attribute injection via quote
escaping.

18 new security tests for URL safety + HTML sanitization.

[宪宪/Opus-46🐾]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mindfn

mindfn commented May 12, 2026

Copy link
Copy Markdown
Owner Author

Superseded by upstream PR zts212653#688 (zts212653/clowder-ai) — clean Phase 2 based on new Phase 1 branch.

@mindfn mindfn closed this May 12, 2026
mindfn pushed a commit that referenced this pull request May 16, 2026
…ed wiring (zts212653#695)

* fix(embedding): batch splitting + reprobe + collection embed deps

Bug #1: Extract embedIndexedItems from IndexBuilder into shared
embed-utils.ts with 64-item batch splitting. IndexBuilder's private
version sent all items at once — embed-api MAX_BATCH_SIZE=64 caused
>64 items to fail with HTTP 400, silently swallowed by fail-open.

Bug #2: Add reprobeIfNeeded() to EmbeddingService. load() probes
/health once at startup; if embed-api starts later, isReady() stays
false forever. reprobeIfNeeded() re-probes when not ready.

Bug #6: Wire embedDeps into CollectionIndexBuilder and library.ts
rebuild route. Collection rebuild now produces vector embeddings
(was FTS-only). Also pass force param from request body (was ignored).

Upstream: zts212653#693

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[宪宪/Opus-46🐾]

* fix(embedding): wire production call sites for reprobeIfNeeded and per-collection VectorStore

P1 fixes from gpt55 review:
- embed-utils.ts: call reprobeIfNeeded() before isReady() check so all
  callers (IndexBuilder + CollectionIndexBuilder) get late-start recovery
- library.ts: accept embeddingService instead of static embedDeps;
  dynamically load sqlite-vec and create per-collection VectorStore at
  rebuild time (prevents cross-DB vector pollution)
- index.ts: pass memoryServices.embeddingService to libraryRoutes

Tests: add reprobe recovery test, update all embedding mocks with
reprobeIfNeeded method. 68/68 embedding tests pass.

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

* fix(embedding): wire setEmbedDeps on collection store for search-time hybrid

P1 from gpt55 R2: rebuild writes vectors via CollectionIndexBuilder but
the collection's SqliteEvidenceStore never gets setEmbedDeps, so search()
stays lexical-only. Now the rebuild route calls store.setEmbedDeps() after
constructing the per-collection VectorStore, using the system embedMode.

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

---------

Co-authored-by: tianyiliang <tianyiliang@tianyiliangdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.com>
mindfn pushed a commit that referenced this pull request May 18, 2026
…ed wiring (zts212653#695)

* fix(embedding): batch splitting + reprobe + collection embed deps

Bug #1: Extract embedIndexedItems from IndexBuilder into shared
embed-utils.ts with 64-item batch splitting. IndexBuilder's private
version sent all items at once — embed-api MAX_BATCH_SIZE=64 caused
>64 items to fail with HTTP 400, silently swallowed by fail-open.

Bug #2: Add reprobeIfNeeded() to EmbeddingService. load() probes
/health once at startup; if embed-api starts later, isReady() stays
false forever. reprobeIfNeeded() re-probes when not ready.

Bug #6: Wire embedDeps into CollectionIndexBuilder and library.ts
rebuild route. Collection rebuild now produces vector embeddings
(was FTS-only). Also pass force param from request body (was ignored).

Upstream: zts212653#693

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[宪宪/Opus-46🐾]

* fix(embedding): wire production call sites for reprobeIfNeeded and per-collection VectorStore

P1 fixes from gpt55 review:
- embed-utils.ts: call reprobeIfNeeded() before isReady() check so all
  callers (IndexBuilder + CollectionIndexBuilder) get late-start recovery
- library.ts: accept embeddingService instead of static embedDeps;
  dynamically load sqlite-vec and create per-collection VectorStore at
  rebuild time (prevents cross-DB vector pollution)
- index.ts: pass memoryServices.embeddingService to libraryRoutes

Tests: add reprobe recovery test, update all embedding mocks with
reprobeIfNeeded method. 68/68 embedding tests pass.

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

* fix(embedding): wire setEmbedDeps on collection store for search-time hybrid

P1 from gpt55 R2: rebuild writes vectors via CollectionIndexBuilder but
the collection's SqliteEvidenceStore never gets setEmbedDeps, so search()
stays lexical-only. Now the rebuild route calls store.setEmbedDeps() after
constructing the per-collection VectorStore, using the system embedMode.

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

---------

Co-authored-by: tianyiliang <tianyiliang@tianyiliangdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.com>
mindfn pushed a commit that referenced this pull request May 18, 2026
…ed wiring (zts212653#695)

* fix(embedding): batch splitting + reprobe + collection embed deps

Bug #1: Extract embedIndexedItems from IndexBuilder into shared
embed-utils.ts with 64-item batch splitting. IndexBuilder's private
version sent all items at once — embed-api MAX_BATCH_SIZE=64 caused
>64 items to fail with HTTP 400, silently swallowed by fail-open.

Bug #2: Add reprobeIfNeeded() to EmbeddingService. load() probes
/health once at startup; if embed-api starts later, isReady() stays
false forever. reprobeIfNeeded() re-probes when not ready.

Bug #6: Wire embedDeps into CollectionIndexBuilder and library.ts
rebuild route. Collection rebuild now produces vector embeddings
(was FTS-only). Also pass force param from request body (was ignored).

Upstream: zts212653#693

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[宪宪/Opus-46🐾]

* fix(embedding): wire production call sites for reprobeIfNeeded and per-collection VectorStore

P1 fixes from gpt55 review:
- embed-utils.ts: call reprobeIfNeeded() before isReady() check so all
  callers (IndexBuilder + CollectionIndexBuilder) get late-start recovery
- library.ts: accept embeddingService instead of static embedDeps;
  dynamically load sqlite-vec and create per-collection VectorStore at
  rebuild time (prevents cross-DB vector pollution)
- index.ts: pass memoryServices.embeddingService to libraryRoutes

Tests: add reprobe recovery test, update all embedding mocks with
reprobeIfNeeded method. 68/68 embedding tests pass.

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

* fix(embedding): wire setEmbedDeps on collection store for search-time hybrid

P1 from gpt55 R2: rebuild writes vectors via CollectionIndexBuilder but
the collection's SqliteEvidenceStore never gets setEmbedDeps, so search()
stays lexical-only. Now the rebuild route calls store.setEmbedDeps() after
constructing the per-collection VectorStore, using the system embedMode.

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

---------

Co-authored-by: tianyiliang <tianyiliang@tianyiliangdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.com>
mindfn pushed a commit that referenced this pull request May 19, 2026
…ed wiring (zts212653#695)

* fix(embedding): batch splitting + reprobe + collection embed deps

Bug #1: Extract embedIndexedItems from IndexBuilder into shared
embed-utils.ts with 64-item batch splitting. IndexBuilder's private
version sent all items at once — embed-api MAX_BATCH_SIZE=64 caused
>64 items to fail with HTTP 400, silently swallowed by fail-open.

Bug #2: Add reprobeIfNeeded() to EmbeddingService. load() probes
/health once at startup; if embed-api starts later, isReady() stays
false forever. reprobeIfNeeded() re-probes when not ready.

Bug #6: Wire embedDeps into CollectionIndexBuilder and library.ts
rebuild route. Collection rebuild now produces vector embeddings
(was FTS-only). Also pass force param from request body (was ignored).

Upstream: zts212653#693

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[宪宪/Opus-46🐾]

* fix(embedding): wire production call sites for reprobeIfNeeded and per-collection VectorStore

P1 fixes from gpt55 review:
- embed-utils.ts: call reprobeIfNeeded() before isReady() check so all
  callers (IndexBuilder + CollectionIndexBuilder) get late-start recovery
- library.ts: accept embeddingService instead of static embedDeps;
  dynamically load sqlite-vec and create per-collection VectorStore at
  rebuild time (prevents cross-DB vector pollution)
- index.ts: pass memoryServices.embeddingService to libraryRoutes

Tests: add reprobe recovery test, update all embedding mocks with
reprobeIfNeeded method. 68/68 embedding tests pass.

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

* fix(embedding): wire setEmbedDeps on collection store for search-time hybrid

P1 from gpt55 R2: rebuild writes vectors via CollectionIndexBuilder but
the collection's SqliteEvidenceStore never gets setEmbedDeps, so search()
stays lexical-only. Now the rebuild route calls store.setEmbedDeps() after
constructing the per-collection VectorStore, using the system embedMode.

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

---------

Co-authored-by: tianyiliang <tianyiliang@tianyiliangdeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lysander Su <773678591@qq.com>
mindfn pushed a commit that referenced this pull request May 23, 2026
…12653#753)

Phase F (trace persistence via pointer association) actually shipped via
zts212653#579 (merged 2026-04-28, commit 8cc6f9a) but the
spec header was never updated. AC-F1..F7 are all [x] in the AC section
already; only the section/header status was stale (still "设计中"/"spec").

Changes (docs-only):
- Phase F section header: "(设计中)" → "✅" + status block with
  Provenance (zts212653#592), Implementation PR (zts212653#579), and Scope note
  clarifying AC-F8 is intentionally deferred to Phase H.
- AC section header: "Phase F(...)" → "Phase F(...)✅" for parity
  with Phase B/C/D/E/G/I that already have ✅ markers.

No code, no AC changes. Source of truth: implementation already in
production on both zts/main and clowder-labs/main.

[宪宪/Opus-47🐾]

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 13, 2026
…e, MemoryQuery, sunset fix

Comprehensive revision based on Sol's CHANGES REQUESTED (5 P1s) and
Fable's follow-up (instance boundary retraction + acceptance criteria):

P1-1 recall three-mode coverage:
- Added MemoryQuery discriminated union (relevance | recent)
- related() stays as separate method (key-based graph traversal)
- scope → content filter, namespace → library selector: two independent
  small tables replace the old 30-cell matrix

P1-2 rebuild/maintain boundary:
- rebuild = host replays full ingest (scanner → remember() × N)
- maintain() = component-internal index health only
- Phase 1 updated: IndexBuilder scanner stays OUTSIDE component
- This split resolves 16 getDb escapes (not new work, fixing known leak)

P1-3 sunset lifecycle contradiction fixed:
- §5.1 signal table refined: "observed source absence → dormant" vs
  "explicit user delete → hard delete + cascade" (Sol precision)
- Phase 2 rewritten: no more cascade delete as default; 188 orphans
  classified by deletion reason, not bulk-cleared
- Cascade delete violates Iron Law #5 (Fable: user data default persistent)
- Doc-type lifecycle downgraded from "基本完整" to "⚠️ no executor"
- Denial #11 added: cascade delete as sunset fix

P1-4 old remnants cleaned:
- §2 problem statement: "can't replace conversation backend" →
  "no unified capability boundary, 16 getDb escapes"
- Phase 2: ConversationProvider/两个Provider removed

P1-5 Wire = transport adapter:
- §4.6: Wire changed from "删除" to "降级为 transport adapter"
- §8: MemoryComponent = logical contract; HTTP/socket = optional transport
- Parent ADR sync obligation explicitly noted

Instance boundary (Sol ruling, Fable agreed):
- One Clowder runtime = one MemoryComponent instance
- Federation (cross-namespace RRF) is component-internal capability
- Architecture diagram: Federation Layer removed from external
- MemoryKey.namespace = library identifier (project/global/collection-id)
- Denial #10 added: per-namespace replacement = coordinator resurrection

13-capability mapping refined (Fable acceptance criteria):
- #6 entity resolution: internal, not on contract
- #7 materialization: host-side governance, component only ingests output
- #11 provenance: two coverage points (MemoryInput + recall filter)
- #13 federation: now component-internal (was external)

File renamed: echomem-collaboration-design.md → memory-component-abstraction.md

465 → 505 lines. All three reviewers' inputs consolidated.

[布偶猫/claude-opus-4-6🐾]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 21, 2026
…erve-only) (#47)

* feat(F257): #4 签名 lint O2→O1 — 检测层(observe-only)+ 账本闭环显式 deferred

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>

* fix(F257): #4 检测层完整性 — sol R3 REQUEST CHANGES 全部修复(P1×3 + P2×1)

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>

* fix(F257): #4 R4 — P1 provider-qualified model + P2 §16e 全链 reachability

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>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 21, 2026
…d canonical tuple (sol R1)

P1-1: verdict vocabulary was inferred from usage sites, not the authoritative SegmentVerdict type — corrected to the frozen 6-term set; removed keep_observe (Eval Hub verdict-handoff domain, not SegmentVerdict); added observability-debt + needs-denominator.
P1-2: rewrote unmeasurable (无分母/injectionCount=0/anti-miskill) + dormant (有分母+连续2周期零触发零违规) to judgment-schema-v1 §2 frozen semantics.
P2-1: moved canonical SEGMENT_VERDICTS tuple + SegmentVerdict to shared (SSOT); engine consumes it; UI map uses satisfies Record<SegmentVerdict,…> (compile-time fail-closed); test iterates shared tuple + asserts domain separation; tightened shared EvalStageSummary.verdict.
Follow-up (out of scope): UI VersionEpoch/EvalStageSummary local 'verdict: string' mirrors across ChainView/Modal/EvalStagePanel — dedup to shared type is a separate cleanup.
Gate: shared/api/web tsc 0, vitest 62 pass, biome clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 21, 2026
…contract (sol P2-2)

Three explanations drifted from the actual SegmentJudgment producer:
- alive: producer is injectionCount>0 → alive even with violationCount===0 (segment-judgment-engine.ts:299-308). Removed '违规仍有下降空间' (contradicts a displayed 违规次数 0); now states zero-violation is a valid alive measurement.
- observability-debt: dropped the '连续2周期 observabilityDeadline' clock — that pressure rule binds to 2 consecutive unmeasurable periods, not this verdict's definition.
- needs-denominator: denominator contract is fired-count | session-count | none; replaced 'typed fact' (objective-metrics plane) with the 注入/会话 denominator.
Added 3 regression tests (alive+zero-violations anchor + 2 lock tests). Full 6-word sweep; dormant/unmeasurable/retire-candidate unchanged (sol OK).

Gate: web tsc 0, vitest 65 pass, biome clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 21, 2026
… Console (判据③) (#52)

* feat(F257): #6 slice 6a — verdict vocabulary explanations in lifeline Console (判据③)

Why: operator screenshot showed eval(unmeasurable) rendered as amber with no meaning — LifelineChainView/EvalStagePanel only hard-coded alive/dormant/retire-candidate, so real verdicts (unmeasurable/keep_observe) fell through unexplained. Adds a single source of truth (verdict-explanations.ts: verdict→{label,explanation,tone} covering alive/keep_observe/unmeasurable/dormant/retire-candidate + safe fallback), wired into the eval badge (tone + hover tooltip) and the 判定 detail row (label + explanation).

Gate: vitest 60 pass (8 new incl. wiring contract + 52 lifeline regression), biome clean, tsc 0.

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

* fix(F257): #6 slice 6a R1 — correct SegmentVerdict vocabulary + shared canonical tuple (sol R1)

P1-1: verdict vocabulary was inferred from usage sites, not the authoritative SegmentVerdict type — corrected to the frozen 6-term set; removed keep_observe (Eval Hub verdict-handoff domain, not SegmentVerdict); added observability-debt + needs-denominator.
P1-2: rewrote unmeasurable (无分母/injectionCount=0/anti-miskill) + dormant (有分母+连续2周期零触发零违规) to judgment-schema-v1 §2 frozen semantics.
P2-1: moved canonical SEGMENT_VERDICTS tuple + SegmentVerdict to shared (SSOT); engine consumes it; UI map uses satisfies Record<SegmentVerdict,…> (compile-time fail-closed); test iterates shared tuple + asserts domain separation; tightened shared EvalStageSummary.verdict.
Follow-up (out of scope): UI VersionEpoch/EvalStageSummary local 'verdict: string' mirrors across ChainView/Modal/EvalStagePanel — dedup to shared type is a separate cleanup.
Gate: shared/api/web tsc 0, vitest 62 pass, biome clean.

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

* fix(F257): #6 slice 6a R2 — align 3 verdict explanations to producer contract (sol P2-2)

Three explanations drifted from the actual SegmentJudgment producer:
- alive: producer is injectionCount>0 → alive even with violationCount===0 (segment-judgment-engine.ts:299-308). Removed '违规仍有下降空间' (contradicts a displayed 违规次数 0); now states zero-violation is a valid alive measurement.
- observability-debt: dropped the '连续2周期 observabilityDeadline' clock — that pressure rule binds to 2 consecutive unmeasurable periods, not this verdict's definition.
- needs-denominator: denominator contract is fired-count | session-count | none; replaced 'typed fact' (objective-metrics plane) with the 注入/会话 denominator.
Added 3 regression tests (alive+zero-violations anchor + 2 lock tests). Full 6-word sweep; dormant/unmeasurable/retire-candidate unchanged (sol OK).

Gate: web tsc 0, vitest 65 pass, biome clean.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 22, 2026
…onable (判据①, sol R1)

Why: sol R1 caught that 494f90f implemented actionability from the
SYNTHESIZED governance.pending — the exact false signal of the original
incident (operator saw 'pending' with no Candidate and asked '是要我审批吗').
This reworks per the 固化 boundary (main msg 0001784469935300-000115) and
operator's option B (root-cause, staged):

- Read model (API): deriveActiveStage() — loop model, not one-way pipeline.
  unmeasurable/observability-debt/needs-denominator/retire-candidate →
  tracing; only conclusive alive/dormant parks at governance.
- actionable derived ONLY from real pending Candidate count via a new
  resolvePendingCandidateCount seam; projection unwired → response honestly
  reports source:'unavailable' + candidateCount:null (never guessed).
- Shared types: ActiveStage + ActionableInfo on SegmentLifecycleResponse.
- Web: loop marker ◈ on the active epoch's real stage; governance.pending
  rendered informationally (slate) — amber dot / N 待审 only with real
  candidate count; empty governance says 未进入治理环节 (not the misleading
  等待治理决策); detail panel shows provenance-gap text when unwired.

Evidence: API 81/81 (13 new contract tests incl. sol R1 regressions 1-4),
web vitest targeted 78/78 (13 jsdom behavior tests, not source-regex),
full web vitest 22 failures = identical to pre-change baseline (8 files,
verified via stash A/B), tsc 0, biome clean.

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 22, 2026
…rmational

Why: same failure mode as sol R1 P1-2 in a sibling surface — VersionDetail's
STATUS_MAP rendered the synthesized governance-pending as amber 待治理
(attention), the same class of false signal as the original incident.
Now informational slate '评估已过·治理环节'; actionability remains gated on
real Candidate count only. Behavior test added (no amber 待治理).

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 22, 2026
Why (sol R2):
- P1-4: UI still gated real Candidates behind synthesized governance.pending,
  hiding the decisive cross-state (retire-candidate: active=tracing +
  actionable=governance(N>0) + epoch.governance=null). Now Candidate
  projection is the sole actionability authority: chain label/tone/title and
  Governance detail show N 待审 + CTA independently of epoch.governance.
- P2-2: dormant ≠ pass — wording now verdict-neutral '评估完成' everywhere
  (chain tooltip, GovPanel, epoch STATUS_MAP).
- P2-3: resolveActionableInfo extracted, fail-closed: provider
  throw/negative/fractional/NaN → source:'unavailable' + server-side warn,
  never a guessed count. Route handler/GovPanel/fetchData refactored so this
  delta adds zero complexity warnings (only pre-existing chain 43 + 2 a11y).

Evidence: API 87/87 (incl. R2 cross-state + 4 fail-safe cases); web jsdom
82/82 targeted (17 判据① behavior tests); full web vitest 22 fails identical
to pre-change baseline (8 files, stash A/B); tsc 0; biome no new warnings.
Isolated-chromium evidence (vite harness + playwright):
/tmp/f257-6b-browser/state-a-unavailable.png + state-b-cross.png,
10/10 DOM assertions pass in real browser.

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 22, 2026
* feat(F257): #6 slice 6b — activeStage/actionableStage 分离 (判据①)

The lifeline chain conflated 'active' (which version is live) with 'needs an operator decision'. Adds an actionable-stage channel distinct from the active-version marker.

Grounded in the producer: segment-lifeline-chain.ts sets governance=pending exactly when the winning verdict is alive/dormant (lines 304-306), so governance-pending is the sole operator-gated stage in v1.
- exported isActionableStage(governance, stage) — governance-pending only
- StageBadge: actionable prop → amber attention dot (separate visual channel from the blue active ● + selection ring)
- governance badge wired; pending label → 待处理
- TDD: actionable-stage unit + wiring contract tests; loosened the 6a title contract regex to accept the actionable title override (intent preserved: eval title still flows through)

Gate: web tsc 0, vitest 71 pass, biome clean.

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

* feat(F257): #6 slice 6b rework — activeStage read-model + honest actionable (判据①, sol R1)

Why: sol R1 caught that 494f90f implemented actionability from the
SYNTHESIZED governance.pending — the exact false signal of the original
incident (operator saw 'pending' with no Candidate and asked '是要我审批吗').
This reworks per the 固化 boundary (main msg 0001784469935300-000115) and
operator's option B (root-cause, staged):

- Read model (API): deriveActiveStage() — loop model, not one-way pipeline.
  unmeasurable/observability-debt/needs-denominator/retire-candidate →
  tracing; only conclusive alive/dormant parks at governance.
- actionable derived ONLY from real pending Candidate count via a new
  resolvePendingCandidateCount seam; projection unwired → response honestly
  reports source:'unavailable' + candidateCount:null (never guessed).
- Shared types: ActiveStage + ActionableInfo on SegmentLifecycleResponse.
- Web: loop marker ◈ on the active epoch's real stage; governance.pending
  rendered informationally (slate) — amber dot / N 待审 only with real
  candidate count; empty governance says 未进入治理环节 (not the misleading
  等待治理决策); detail panel shows provenance-gap text when unwired.

Evidence: API 81/81 (13 new contract tests incl. sol R1 regressions 1-4),
web vitest targeted 78/78 (13 jsdom behavior tests, not source-regex),
full web vitest 22 failures = identical to pre-change baseline (8 files,
verified via stash A/B), tsc 0, biome clean.

[小狸/k3🐾]

* fix(F257): #6 判据① §16e sweep — epoch status 'governance-pending' informational

Why: same failure mode as sol R1 P1-2 in a sibling surface — VersionDetail's
STATUS_MAP rendered the synthesized governance-pending as amber 待治理
(attention), the same class of false signal as the original incident.
Now informational slate '评估已过·治理环节'; actionability remains gated on
real Candidate count only. Behavior test added (no amber 待治理).

[小狸/k3🐾]

* fix(F257): #6 判据① R2 — candidate-first UI + fail-safe provider + wording

Why (sol R2):
- P1-4: UI still gated real Candidates behind synthesized governance.pending,
  hiding the decisive cross-state (retire-candidate: active=tracing +
  actionable=governance(N>0) + epoch.governance=null). Now Candidate
  projection is the sole actionability authority: chain label/tone/title and
  Governance detail show N 待审 + CTA independently of epoch.governance.
- P2-2: dormant ≠ pass — wording now verdict-neutral '评估完成' everywhere
  (chain tooltip, GovPanel, epoch STATUS_MAP).
- P2-3: resolveActionableInfo extracted, fail-closed: provider
  throw/negative/fractional/NaN → source:'unavailable' + server-side warn,
  never a guessed count. Route handler/GovPanel/fetchData refactored so this
  delta adds zero complexity warnings (only pre-existing chain 43 + 2 a11y).

Evidence: API 87/87 (incl. R2 cross-state + 4 fail-safe cases); web jsdom
82/82 targeted (17 判据① behavior tests); full web vitest 22 fails identical
to pre-change baseline (8 files, stash A/B); tsc 0; biome no new warnings.
Isolated-chromium evidence (vite harness + playwright):
/tmp/f257-6b-browser/state-a-unavailable.png + state-b-cross.png,
10/10 DOM assertions pass in real browser.

[小狸/k3🐾]

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mindfn added a commit that referenced this pull request Jul 23, 2026
Why: Console lifeline projected incomparable metrics into one context —
tracing(18) came from the CURRENT query window while eval injectionCount=0
came from the judgment's OWN historical eval window. CachedJudgment dropped
window + denominatorKind at persistence, so the query window silently
substituted for the eval coordinate.

- CachedJudgment: window + denominatorKind required on producer writes
  (SegmentJudgment always has both); legacy Redis JSON reads normalize to
  explicit null (fail-visible, never guessed from evaluatedAt)
- read model: EvalStageSummary.evalWindow/denominatorKind; response.window
  documented as the QUERY window (distinct coordinate)
- chain builder: toEvalStageSummary propagates both (extracted helper —
  keeps attachJudgments cognitive complexity at baseline)
- Console: eval panel shows 评估窗口(采样区间) + 分母; tracing panel labels
  当前查询窗口; legacy → 评估窗口未知/分母未知 fail-visible

Red→green: cache round-trip/legacy (4) + chain/route attribution (6) +
jsdom render 18-vs-0 (6). API full-suite A/B vs base 231f960: failing
list byte-identical (90) — zero new failures. Web vitest A/B: same 22
baseline failures, +6 new green. tsc 0 errors; biome 0 new warnings.

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 23, 2026
…nce fail-closed (sol P1-1/P2-1/P2-2)

Why: sol R1 REQUEST CHANGES — (P1-1) query window and eval window were
still in mutually exclusive panels, so tracing(18) vs eval(0) was NOT
visible as two coordinates in one viewport (the stale test even encoded
the bug by asserting the query window absent from the eval panel);
(P2-1) the Redis read boundary trusted any PRESENT window/denominatorKind
JSON, letting forged coordinates reach the UI as "Invalid Date ~ Invalid
Date"; (P2-2) no real-browser evidence.

- P1-1: EvalStagePanel gains queryWindow prop + compact 坐标对照 block —
  current observation count + CURRENT query window rendered next to the
  historical eval window/denominator in ONE viewport, explicitly labeled
  ≠ 评估窗口; LifelineStageDetail wires queryWindow through. The stale
  mutual-exclusion test is rewritten: query window may appear only as
  labeled contrast, never as the eval window. New composed render test
  (LifelineChainView + eval LifelineStageDetail) asserts one DOM holds
  tracing(18), 0, QUERY start/end, EVAL start/end, fired-count.
- P2-1: normalizeCachedJudgment fails closed — window must be a
  non-array record with finite startMs/endMs in legal [start,end order;
  denominatorKind restricted to the closed union
  fired-count|session-count|none; non-record raw (e.g. JSON array)
  rejected outright. Malformed-present regressions added for
  get/getBatch/getHistory; legitimate legacy-missing still → null.
- P2-2: replayable browser harness — showcase page
  /showcase/f257-eval-window (repo precedent: f052/f11-review) +
  packages/web/scripts/capture-f257-eval-window.mjs (production build +
  real chromium, fail-closed waits). Composed screenshot delivered in
  review handoff thread.

Red→green: cache malformed 5 red → 25/25 green (file 20 + f257-eval-window 5);
web composed 1 red → 7/7 green. API full-suite: 90 unique failures, zero
touching judgment/lifeline/eval-window surface (baseline-count identical).
Web vitest: same 22 baseline failures across same 8 unrelated files.
tsc 0 errors; biome clean.

[小狸/k3🐾]
EOF
)
mindfn added a commit that referenced this pull request Jul 23, 2026
…an evidence (sol P1-2/P2-3/P2-4)

Why: sol R2 — the R1 test/showcase locked an IMPOSSIBLE judgment state
(alive + injectionCount=0 + fired-count): segment-judgment-engine
produceVerdict maps injectionCount=0 → unmeasurable + none, so the
screenshot visibly contradicted the product contract (活跃:injectionCount>0
above 注入次数 0) and regressed the 6b loop model (unmeasurable returns the
cycle to tracing, no governance). The committed capture script was
machine-specific (user/OS/arch paths), and review-evidence/ dirtied the
worktree.

- P1-2: fixtures now reproduce the REAL incident — eval unmeasurable /
  injectionCount=0 / violationCount=0 / denominatorKind 'none', epoch
  status eval-pending, activeStage 'tracing', governance null (cycle back
  to tracing per 6b loop model). jsdom contract + showcase updated;
  composed test now asserts 无分母(不可计算比率)and the ABSENCE of
  fired-count for an unmeasurable judgment. Legacy case uses a
  producer-valid alive judgment with missing provenance fields.
- P2-3: committed capture script REMOVED — the showcase route
  (/showcase/f257-eval-window) is the replayable artifact; evidence is
  captured with local browser tooling.
- P2-4: review evidence moved outside the worktree (/tmp/f257-6c-eval-window);
  git status clean.

Red→green: eval-window-provenance 7/7 green (producer-contract guard
included); web tsc 0; biome clean; next build includes the showcase route.
Fresh production-styled chromium capture attached in review thread.

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 23, 2026
… P2-1)

Why: sol R4 REQUEST CHANGES — normalizeWindow only rejected startMs > endMs,
so a forged {startMs: t, endMs: t} survived the Redis read boundary and the
Console rendered `t ~ t` as a trusted eval coordinate. judgment-schema-v1
defines a [start,end sampling window (empty interval has no sampleable
instant), and every canonical selector/adapter already rejects
windowEndMs <= windowStartMs. The cache seam now enforces the same
invariant: startMs >= endMs → null.

Same-type audit (requested with the finding): scanned all provenance
validators on the slice surface for the interval invariant —
- publish-verdict/validation.ts ×3 selector validators: already  reject
- harness-ledger-generator-adapter.ts: already  reject
- segment-lifeline-chain.ts toEvalStageSummary: pure propagation, no
  re-validation — cache read boundary is the sole seam (now closed)
- web EvalStagePanel: renders null as 评估窗口未知 (no own invariant needed)
normalizeWindow was the only gap; no sibling instances remain.
Integer constraint not added at this seam — producer window comes from
epoch ms timestamps (finite numbers); end > start is the blocking
requirement per the finding.

Red→green: zero-length window test red (1 fail) → cache 21/21 +
f257-eval-window 5/5 green; getHistory seam covered. Web jsdom 7/7;
web tsc 0; biome clean.

[小狸/k3🐾]
EOF
)
mindfn added a commit that referenced this pull request Jul 23, 2026
…ol P1/P2)

Why: sol R5 REQUEST CHANGES (second-pass integration review) —
(P1) the 6c contrast block rendered tracing observationCount (ALL observed
rows, incl. observe-only, capped at MAX_OBSERVATIONS=100) as "当前观测 N
次注入" — reproducing the 18-vs-0 fake contradiction even with perfect
window provenance, and presenting capped lower bounds as totals.
(P2) legacy-missing and present-but-malformed provenance both normalized
to null while the UI asserted "历史缓存缺字段" for both — a false cause
for corrupted data.

P1 — real metric + completeness:
- TracingStageSummary gains firedCount + capped (shared contract)
- collectObservations computes fired per row via segment-judgment-engine
  isFired (exported — single predicate source, producer semantics);
  capped=true ONLY when a matching row exists beyond MAX_OBSERVATIONS
  (exactly-100 is not "capped")
- attachObservations counts both metrics + propagates capped
- EvalStagePanel contrast block: 当前注入 = fired-count (≥N when capped,
  下限 labeled); observe-only surplus shown as 观测行数 (≠ 注入次数);
  pending panel same split; 评估方式 keyed on firedCount
- stale source-regex contract test asserting the old mislabel rewritten

P2 — gap kind preserved end-to-end:
- ProvenanceGapKind 'legacy-missing' | 'invalid-present' (shared)
- cache read seam classifies both fields (absent=legacy, malformed=invalid),
  write path records gap null
- toEvalStageSummary propagates; UI renders invalid-present as
  评估窗口不可用(缓存数据损坏)/分母不可用 — never mislabeled legacy

Red→green: API 5 red (observe-only / 101-cap / gap-kind chain+route+cache)
→ 99/99; web 3 red (contrast observe-only / capped / invalid-present render)
→ 93/93 across 4 affected files. Full API public suite: only the 4 known
KD-17 stale assertions (17,421 pass); full web vitest: same 22 baseline
failures in the same 8 files (5,169 pass, +50 new green). tsc 0 both
packages; biome clean.

[小狸/k3🐾]
mindfn added a commit that referenced this pull request Jul 23, 2026
…ap kind (sol P1/P2)

Why: sol R6 REQUEST CHANGES (third-pass focused review @ edfa6d9) —
(P1) the R5 global 100-row cap could project an active epoch with real
observations as tracing:null (unsampled ≠ zero-data), and UI siblings
still rendered bare capped counts; lower-bound provenance on counts was
the wrong coordinate — completeness belongs to the DETAIL list only.
(P2) raw == null cannot distinguish absent (legacy-missing) from
present-null (invalid-present; the producer never writes null) — gap
classification must be by own-property presence.

P1 — aggregate counts exact, detail list capped:
- collectObservations: full-window scan for counting; detail rows =
  100 most recent + observationsCapped completeness flag (route response)
- TracingStageSummary drops capped (counts are exact, never lower bounds)
- EvalStagePanel/LifelineChainView/LifelineStageDetail: exact counts,
  no ≥/下限 markers; tracing panel shows 明细仅显示最近 100 条 note when
  observationsCapped

P2 — presence-based gap classification:
- SegmentJudgmentCache: Object.hasOwn presence → absent=legacy-missing,
  present-null/malformed=invalid-present (validatePresentWindow /
  validatePresentDenominatorKind)

R6 failure-mode gate — two minimal matrices, every cell tested:
- presence: absent / explicit-null / valid / invalid × {window,
  denominatorKind} × {get, getBatch, getHistory, route}
- completeness: <100 / =100 / >100 × single/multi-epoch ×
  fired/observe-only — incl. sol's multi-thread repro (101st row on
  active v2 → v2 tracing exact, never null)

Evidence: API focused 126/126; web focused 94/94; web tsc 0; biome
clean (1 pre-existing complexity + 2 pre-existing a11y warnings = baseline)
mindfn added a commit that referenced this pull request Jul 23, 2026
…54)

* feat(F257): #6 slice 6c — eval window + denominator provenance (判据②)

Why: Console lifeline projected incomparable metrics into one context —
tracing(18) came from the CURRENT query window while eval injectionCount=0
came from the judgment's OWN historical eval window. CachedJudgment dropped
window + denominatorKind at persistence, so the query window silently
substituted for the eval coordinate.

- CachedJudgment: window + denominatorKind required on producer writes
  (SegmentJudgment always has both); legacy Redis JSON reads normalize to
  explicit null (fail-visible, never guessed from evaluatedAt)
- read model: EvalStageSummary.evalWindow/denominatorKind; response.window
  documented as the QUERY window (distinct coordinate)
- chain builder: toEvalStageSummary propagates both (extracted helper —
  keeps attachJudgments cognitive complexity at baseline)
- Console: eval panel shows 评估窗口(采样区间) + 分母; tracing panel labels
  当前查询窗口; legacy → 评估窗口未知/分母未知 fail-visible

Red→green: cache round-trip/legacy (4) + chain/route attribution (6) +
jsdom render 18-vs-0 (6). API full-suite A/B vs base 231f960: failing
list byte-identical (90) — zero new failures. Web vitest A/B: same 22
baseline failures, +6 new green. tsc 0 errors; biome 0 new warnings.

[小狸/k3🐾]

* fix(F257): #6 slice 6c R1 — composed 18-vs-0 viewport + Redis provenance fail-closed (sol P1-1/P2-1/P2-2)

Why: sol R1 REQUEST CHANGES — (P1-1) query window and eval window were
still in mutually exclusive panels, so tracing(18) vs eval(0) was NOT
visible as two coordinates in one viewport (the stale test even encoded
the bug by asserting the query window absent from the eval panel);
(P2-1) the Redis read boundary trusted any PRESENT window/denominatorKind
JSON, letting forged coordinates reach the UI as "Invalid Date ~ Invalid
Date"; (P2-2) no real-browser evidence.

- P1-1: EvalStagePanel gains queryWindow prop + compact 坐标对照 block —
  current observation count + CURRENT query window rendered next to the
  historical eval window/denominator in ONE viewport, explicitly labeled
  ≠ 评估窗口; LifelineStageDetail wires queryWindow through. The stale
  mutual-exclusion test is rewritten: query window may appear only as
  labeled contrast, never as the eval window. New composed render test
  (LifelineChainView + eval LifelineStageDetail) asserts one DOM holds
  tracing(18), 0, QUERY start/end, EVAL start/end, fired-count.
- P2-1: normalizeCachedJudgment fails closed — window must be a
  non-array record with finite startMs/endMs in legal [start,end order;
  denominatorKind restricted to the closed union
  fired-count|session-count|none; non-record raw (e.g. JSON array)
  rejected outright. Malformed-present regressions added for
  get/getBatch/getHistory; legitimate legacy-missing still → null.
- P2-2: replayable browser harness — showcase page
  /showcase/f257-eval-window (repo precedent: f052/f11-review) +
  packages/web/scripts/capture-f257-eval-window.mjs (production build +
  real chromium, fail-closed waits). Composed screenshot delivered in
  review handoff thread.

Red→green: cache malformed 5 red → 25/25 green (file 20 + f257-eval-window 5);
web composed 1 red → 7/7 green. API full-suite: 90 unique failures, zero
touching judgment/lifeline/eval-window surface (baseline-count identical).
Web vitest: same 22 baseline failures across same 8 unrelated files.
tsc 0 errors; biome clean.

[小狸/k3🐾]
EOF
)

* fix(F257): #6 slice 6c R2 — producer-reachable fixture + portable/clean evidence (sol P1-2/P2-3/P2-4)

Why: sol R2 — the R1 test/showcase locked an IMPOSSIBLE judgment state
(alive + injectionCount=0 + fired-count): segment-judgment-engine
produceVerdict maps injectionCount=0 → unmeasurable + none, so the
screenshot visibly contradicted the product contract (活跃:injectionCount>0
above 注入次数 0) and regressed the 6b loop model (unmeasurable returns the
cycle to tracing, no governance). The committed capture script was
machine-specific (user/OS/arch paths), and review-evidence/ dirtied the
worktree.

- P1-2: fixtures now reproduce the REAL incident — eval unmeasurable /
  injectionCount=0 / violationCount=0 / denominatorKind 'none', epoch
  status eval-pending, activeStage 'tracing', governance null (cycle back
  to tracing per 6b loop model). jsdom contract + showcase updated;
  composed test now asserts 无分母(不可计算比率)and the ABSENCE of
  fired-count for an unmeasurable judgment. Legacy case uses a
  producer-valid alive judgment with missing provenance fields.
- P2-3: committed capture script REMOVED — the showcase route
  (/showcase/f257-eval-window) is the replayable artifact; evidence is
  captured with local browser tooling.
- P2-4: review evidence moved outside the worktree (/tmp/f257-6c-eval-window);
  git status clean.

Red→green: eval-window-provenance 7/7 green (producer-contract guard
included); web tsc 0; biome clean; next build includes the showcase route.
Fresh production-styled chromium capture attached in review thread.

[小狸/k3🐾]

* fix(F257): #6 slice 6c R4 — zero-length eval window fails closed (sol P2-1)

Why: sol R4 REQUEST CHANGES — normalizeWindow only rejected startMs > endMs,
so a forged {startMs: t, endMs: t} survived the Redis read boundary and the
Console rendered `t ~ t` as a trusted eval coordinate. judgment-schema-v1
defines a [start,end sampling window (empty interval has no sampleable
instant), and every canonical selector/adapter already rejects
windowEndMs <= windowStartMs. The cache seam now enforces the same
invariant: startMs >= endMs → null.

Same-type audit (requested with the finding): scanned all provenance
validators on the slice surface for the interval invariant —
- publish-verdict/validation.ts ×3 selector validators: already  reject
- harness-ledger-generator-adapter.ts: already  reject
- segment-lifeline-chain.ts toEvalStageSummary: pure propagation, no
  re-validation — cache read boundary is the sole seam (now closed)
- web EvalStagePanel: renders null as 评估窗口未知 (no own invariant needed)
normalizeWindow was the only gap; no sibling instances remain.
Integer constraint not added at this seam — producer window comes from
epoch ms timestamps (finite numbers); end > start is the blocking
requirement per the finding.

Red→green: zero-length window test red (1 fail) → cache 21/21 +
f257-eval-window 5/5 green; getHistory seam covered. Web jsdom 7/7;
web tsc 0; biome clean.

[小狸/k3🐾]
EOF
)

* fix(F257): #6 slice 6c R5 — current-side metric honesty + gap kind (sol P1/P2)

Why: sol R5 REQUEST CHANGES (second-pass integration review) —
(P1) the 6c contrast block rendered tracing observationCount (ALL observed
rows, incl. observe-only, capped at MAX_OBSERVATIONS=100) as "当前观测 N
次注入" — reproducing the 18-vs-0 fake contradiction even with perfect
window provenance, and presenting capped lower bounds as totals.
(P2) legacy-missing and present-but-malformed provenance both normalized
to null while the UI asserted "历史缓存缺字段" for both — a false cause
for corrupted data.

P1 — real metric + completeness:
- TracingStageSummary gains firedCount + capped (shared contract)
- collectObservations computes fired per row via segment-judgment-engine
  isFired (exported — single predicate source, producer semantics);
  capped=true ONLY when a matching row exists beyond MAX_OBSERVATIONS
  (exactly-100 is not "capped")
- attachObservations counts both metrics + propagates capped
- EvalStagePanel contrast block: 当前注入 = fired-count (≥N when capped,
  下限 labeled); observe-only surplus shown as 观测行数 (≠ 注入次数);
  pending panel same split; 评估方式 keyed on firedCount
- stale source-regex contract test asserting the old mislabel rewritten

P2 — gap kind preserved end-to-end:
- ProvenanceGapKind 'legacy-missing' | 'invalid-present' (shared)
- cache read seam classifies both fields (absent=legacy, malformed=invalid),
  write path records gap null
- toEvalStageSummary propagates; UI renders invalid-present as
  评估窗口不可用(缓存数据损坏)/分母不可用 — never mislabeled legacy

Red→green: API 5 red (observe-only / 101-cap / gap-kind chain+route+cache)
→ 99/99; web 3 red (contrast observe-only / capped / invalid-present render)
→ 93/93 across 4 affected files. Full API public suite: only the 4 known
KD-17 stale assertions (17,421 pass); full web vitest: same 22 baseline
failures in the same 8 files (5,169 pass, +50 new green). tsc 0 both
packages; biome clean.

[小狸/k3🐾]

* fix(F257): #6 slice 6c R6 — exact aggregate counts + presence-based gap kind (sol P1/P2)

Why: sol R6 REQUEST CHANGES (third-pass focused review @ edfa6d9) —
(P1) the R5 global 100-row cap could project an active epoch with real
observations as tracing:null (unsampled ≠ zero-data), and UI siblings
still rendered bare capped counts; lower-bound provenance on counts was
the wrong coordinate — completeness belongs to the DETAIL list only.
(P2) raw == null cannot distinguish absent (legacy-missing) from
present-null (invalid-present; the producer never writes null) — gap
classification must be by own-property presence.

P1 — aggregate counts exact, detail list capped:
- collectObservations: full-window scan for counting; detail rows =
  100 most recent + observationsCapped completeness flag (route response)
- TracingStageSummary drops capped (counts are exact, never lower bounds)
- EvalStagePanel/LifelineChainView/LifelineStageDetail: exact counts,
  no ≥/下限 markers; tracing panel shows 明细仅显示最近 100 条 note when
  observationsCapped

P2 — presence-based gap classification:
- SegmentJudgmentCache: Object.hasOwn presence → absent=legacy-missing,
  present-null/malformed=invalid-present (validatePresentWindow /
  validatePresentDenominatorKind)

R6 failure-mode gate — two minimal matrices, every cell tested:
- presence: absent / explicit-null / valid / invalid × {window,
  denominatorKind} × {get, getBatch, getHistory, route}
- completeness: <100 / =100 / >100 × single/multi-epoch ×
  fired/observe-only — incl. sol's multi-thread repro (101st row on
  active v2 → v2 tracing exact, never null)

Evidence: API focused 126/126; web focused 94/94; web tsc 0; biome
clean (1 pre-existing complexity + 2 pre-existing a11y warnings = baseline)
mindfn added a commit that referenced this pull request Jul 28, 2026
Closes F257 Console #6 slice 判据⑤ variable segment presentation.

Internal review: @sol R4 APPROVE. Gate passed at SHA 60628ea.
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