Skip to content

fix(external-evidence): reject duplicate admitted source refs - #5260

Open
Jim-jimu wants to merge 1 commit into
loopx-project:mainfrom
Jim-jimu:fix/external-evidence-duplicate-sources
Open

Jim-jimu wants to merge 1 commit into
loopx-project:mainfrom
Jim-jimu:fix/external-evidence-duplicate-sources

Conversation

@Jim-jimu

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

  • Outcome basis / optional anchor: A reproduced defect in the shipped external-evidence CLI.
  • Goal/source and gap: Repeating --admit-source returned a successful admission containing duplicate references, but retire rejected that same record as invalid even after downstream coverage was complete.
  • Observable before → after: Reject duplicate source references after whitespace normalization, before producing an admission. Corrected input still completes admission and retirement; distinct source subsets and ordering remain supported. See the regression and CLI validation rows.
  • Issue/task and intended base: Related to [Task][RFC]: Complete external-evidence research through real providers and user readback #5205; this fixes the duplicate-input defect only, not the parent provider-integration program. Base: main.

Scope And Continuation

  • Completed scope and remaining work: Add the uniqueness guard in the existing TypeScript admission owner, with TypeScript and real CLI regressions. Complete within this scope.
  • Slice boundary / successor: N/A; no follow-up is required for this defect. Adjacent-boundary review found no useful extraction: existing normalization, error transport and retirement validation already own the required behavior. No new abstraction or parallel Python rule is introduced.

Validation

  • Tested revision: 7553635b870beea0fc4ae69089fd0ce33207fb26 for the final focused rerun. The broader checks below ran before commit, on the same three-file patch over the explicitly named upstream baselines.
  • Run state: finished
  • Input classes: synthetic, public_fixture
Check kind Result Public-safe evidence / limitation
regression_parity passed On da5aac12 with the added tests but unchanged product code: TypeScript 2 failed / 14 passed; Python CLI 2 failed / 6 passed. Exact and whitespace-normalized duplicates were incorrectly accepted. Final commit rerun: 16 TypeScript and 8 Python tests passed.
real_entrypoint passed uv run --extra test python -m pytest tests/capabilities/test_external_evidence_cli.py -q: 8 passed on the final commit. Tests use python -m loopx.entrypoint and the managed TypeScript runtime with isolated temporary state. Duplicate input returns invalid_request without an admission id; corrected input reaches retire_ready. Receipts are synthetic; no external research provider is invoked.
integration passed Over final base cf759d32, before commit: 29 TypeScript tests across external evidence, manager return delivery and Todo authoring scope; 206 Python tests across the external-evidence CLI, canonical Todo health, collaboration Goal identity and manager-context roundtrips.
integration passed Over intermediate base 0730b6cc, before commit: full TypeScript suite 3,412 passed / 30 PostgreSQL cases skipped; 1,202 Python architecture, session, public-safety and CLI tests passed. These were followed by the affected checks on cf759d32 above.
integration failed Historical full Python run over da5aac12 plus this patch: 13,075 passed, 62 skipped, 4 failed. All four failures reproduced on the clean baseline: two registry I/O inventory assertions, and two scheduler-ack assertions in test_quota_settlement_cli.py. Upstream 0730b6cc fixed the inventory assertions; the architecture rerun passed. The two quota failures also reproduce on clean final base cf759d32.
static passed Over cf759d32 plus this patch: repository Ruff check, TypeScript typecheck, mypy (19 source files), CLI output-budget smoke and diff hygiene passed. The public-boundary scan of the three changed files reports zero errors or warnings.
integration passed loopx canary premerge --from-git-diff --git-diff-base cf759d323b755e2040200f976eac1eeb89383b4d: 13 selected catalog/risk checks passed, with no manual holds.
static failed Whole-repository public-boundary scan on cf759d32 reports two existing synthetic path fixtures in tests/control_plane/test_public_safe_text_classifier.py (lines 100 and 159). The clean final base gives the same two findings; neither file nor fixture is changed here.
  • Coverage and gaps: Covers duplicate normalization, correction through the real CLI, distinct-source subsets, reference order and retirement readback. Full Python was not rerun after the upstream updates; affected checks were rerun as listed. PostgreSQL and real external-provider execution were not exercised. Counts are separate runs, not additive coverage.

Frontend / Visual Evidence

  • UI impact: none. The changed public surface is CLI invalid-input validation; no rendered frontend or Lark view changes.
  • Before: N/A.
  • After: N/A.
  • States and viewports shown: N/A.
  • Source data: none.
  • Attention review: N/A; no visual surface changes.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

Shared-authority RFC fixture impact

  • Production-scale fixture schema: N/A.
  • Semantic dimensions changed, or reviewed no-impact rationale: N/A; this is not a TypeScript migration or shared Goal Authority contribution.
  • Provider conformance arms run: N/A; no authority-store behavior changes.
  • Read-only legacy/file/PostgreSQL three-arm rehearsal: N/A; no promotion, runtime routing or compatibility-projection changes.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths.
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

Signed-off-by: Jim-jimu <49069997+bmh201708@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结论:APPROVE。四行既有 TypeScript owner 校验,修复了“admit 产出的重复 source refs 到 retire 才失败”的真实续接缺陷;合法输入和既有拒绝/未激活分支的完整输出保持一致。

English verdict: APPROVE
Reviewed exact head: 7553635

动机

外部证据被父 Agent 明确采纳后,admission 必须能被后续 projection/retirement 正常读取。原 producer 允许选择同一个 source ref 多次,但既有 admission reader 又要求 refs 唯一,于是当前命令成功产出的回执,在下一步被自己拒绝。这里要修的是一个可复现的闭环缺陷,不是用更多 receipt 或更大的框架证明外部研究能力整体完成。

改动思路

把已有的唯一性要求前移到 evaluateExternalEvidenceAdmission:先执行现有 boundedStrings 的 trim/数量/长度校验,再用 Set cardinality 明确拒绝重复;之后继续走原 membership、disposition、projection 和 digest 构造。Python CLI 仍然只负责参数与结果运输,共享决策仍在 TypeScript owner。

我也比较了更短的替代方案:自动去重会悄悄改写父 Agent 的显式采纳意图;删掉 retirement reader 的防御则会接收外部/历史畸形 admission。这两种都不是等价修复。producer 和 reader 分属不同信任边界,保留两处检查是合理的,不需要为了四行规则抽出新框架。

具体改动

实际范围是三个文件、+134/-0:生产 TS 函数只增加四行;49 行 owner 回归和 81 行 CLI 回归验证新输入边界及 admit→retire。没有新模块、协议版本、CLI 参数、持久化字段或新的 Python 决策源。

关键代码讲解

  1. evaluateExternalEvidenceAdmission:admitted_source_refs 经原有 normalization 后执行 new Set(admittedRefs).size === admittedRefs.length。直接重复、首尾空白归一化后的重复、非相邻重复都会在 admission 构造前失败;不同来源的选择和原有顺序没有被改写。错误具体指向 decision.admitted_source_refs。
  2. normalizeAdmission:既有 reader 对输入 envelope/schema/digest/disposition/projection 和 refs 唯一性继续做独立校验。它必须处理随后由 caller 提供的历史或不可信数据;producer 新校验不能替它背书,所以保留不是第二个状态权威。
  3. projectExternalEvidenceRetirement:仍按已采纳来源是否全部投影判断 retained/retire_ready,部分覆盖或无关来源不能提前退休。现在合法新回执可以继续,非法重复选择则不会先产生一份毒回执再失败。retire_ready 也不是 Goal 完成或外部执行权限。

我本人用同一个独立合成 fixture 跑 production CLI→实际隔离 managed TS runtime,覆盖 14 个场景:单来源、子集、有序多来源、64 个唯一来源;直接/空白/非相邻重复;拒绝后的修正输入;未知来源、空 admit、空 reject、reject 带来源;部分/无关覆盖;声明但禁用的 provider。base 的三个重复场景先产出 admission,再在 retire 因唯一性失败;head 则 exit 1/invalid_request 且无 admission_id,改为唯一列表后完整到 retire_ready。其余 11 个场景的完整 normalized observation 在 base/head 逐字一致,包括错误细节、source order、id 和 coverage readback,不只比较通过数量。

对主干的风险

主要反向风险是过度拒绝合法多来源、静默重排/去重,或让“来源存在/安装”被误当成采纳/激活。上述独立真实 CLI 路径已经覆盖这些反例:不同来源和 64 边界通过,部分覆盖仍 retained,禁用 provider 仍未选中;没有新增自动调用、调度或外部副作用。语义变化只有重复选择在 producer 更早失败,这是修正原有不一致,已在 PR 与命名回归中披露。

本地验证:TS owner 16/16、CLI 8/8;TypeScript typecheck、配置范围 ruff/mypy、实际 PR diff check 全部通过;原生 TS 全量 3415 passed、30 既有条件 skip、0 fail;risk-selected premerge canary 13 项、0 fail;exact-scope change-quality 已记录 pass 并 verify。30 个条件 skip 没有冒充 PostgreSQL 实测,本 PR 不改 authority store/PG adapter。本次不查询、不轮询、不等待 GitHub CI。

语义与 CI 对齐

沿用了既有 disposition/schema 和唯一来源契约,匹配依据是明确归一化后的来源身份,不是 prose/substring denylist。错误文案保持领域中立;这是 enforced input validity,不是含糊的 guidance,也不增加 must_attempt_work。无 actor 生命周期/权限扩张,无自动加载 instruction 改动。证据与 provider 内容为合成输入,不宣称执行了真实外部服务、验证了来源内容质量或完成了 frontend/Lark 集成。

我的整体评价

这是完整、可逆、适当大小的旧 owner 修复:纠正了实际 producer/reader 不一致,也验证了随后能继续工作的路径。future-facing pass 的结论是无需额外抽象,保留独立 reader 防御即可。更大的 issue #5205 method/connector/frontend/Lark 集成仍是另一项边界,没有被这条 approve 当作完成。此次批准只评价这个 exact head;不代表自动 merge readiness,也没有执行合并。

This branch has not been deployed

No deployments
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.

2 participants