diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f09005..496087e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ ### 新增 Added + - **Discovery-round tool face actually wired (ticket #217 = T3.5, spec-axis P1 backfill under umbrella #206; the three seams that previously had only selftest consumers are now the production consumption path)**: ① three-point wiring per the discovery-host.mjs wiring protocol — SHORTLIST (model step) gets host.tools direct install (faux.mjs runRealStep `ensureDiscoverHost(ctx)`, same injection shape as DEEP's microprobe host; ctx-level dupRegistry shared with model calls, round-level breaker instance bound to ops/breaker.json); MEASURE (deterministic, zero-model) issues real evidence-face probes via host.call("web_read") — existence stays host-recomputed, URL candidates get a live existence probe through the same guard chain (dup→breaker→cap) with measure-N call-ids and tool_seeds trace; computable criteria compute (measured when all three faces resolve), non-computable ones hold with an explicit uncertainty reason (no-URL candidate / host-refused probe — 被拒≠清白 — / missing SHORTLIST exclusivity estimate: the measure face has no re-estimation right and never fabricates); control_executable = the positive control of the probe itself (url_read_ok); exclusivity stays a passthrough of the SHORTLIST value. CLOSE gains the pure-increment readings key `discover_channels` (live host channelCounts preferred; host absent on resume/replay → recomputed from the d2 transcript registry_seeds + d3 tool_seeds artifacts, 宁少算 — rejected calls replay as nothing; health rounds byte-identical). readDiscoverKnobs gains the dupCallLimit key (same OPS_PIPELINE_DUP_CALL_LIMIT knob name and default as ops rounds — its own header comment had reserved this as the consumption point); faux face gets fixtureDiscoveryMethods (canned web_read table in fixtures/pipeline/measure.json; uncanned = honest io_error envelope, zero-network law). ② the end-to-end positive example finally exists (previously the full-round pin was zero-enqueue only): the faux six-step round now walks one candidate SHORTLIST→MEASURE→PROPOSE into a real queue.md row (fixture rDNS candidate gets exclusivity 0.42 + a canned ok web_read), pinned in both selftest-pipeline (full-round E2E: d3 measured/control_executable true, d4 1-enqueued/1-skipped, PROPOSE rdns row, discover_channels readings) and golden pipeline_discover_full_faux (same pins + queue_file row assertion); four new selftest examples (fixture-method three states incl. canned-fail passthrough / MEASURE tri-state incl. dup interception at the limit with explicit repeat_storm uncertainty / CLOSE live-host-vs-replay-and-health-round-equivalence / wiring mount-point sentinels). ③ queue.md frozen header sentence corrected — 「queue_action 唯一写口」 stopped being true the day ENQUEUE candidate rows and appendCandidateCard cards landed: the header now truthfully names the three write ports (ops-round queue_action · discovery-round ENQUEUE candidate row · discovery-round candidate card appendCandidateCard; the latter two action=PROPOSE one word one spelling), flipped in the same batch across the JS face (queue-lock.mjs QUEUE_HEADER), the python face (_ops_store.py QUEUE_HEADER), both python test fixtures (the northstar fixture's pre-existing missing-rationale-clause drift healed in passing), tools.mjs/discover-candidate.mjs notes — plus a new contract sentinel pinning the three copies (python literal ↔ node literal ↔ the header actually written by appendQueueRowLocked first-create) verbatim-equal and semantically three-write-port (a rollback to the old sentence reddens the sentinel). Gates (worktree-verified): selftest-pipeline 104/0 (base 100 + 4 new; red proof: reverting steps.mjs+faux.mjs wiring → exactly 100/4 with only the T3.5 examples red), golden 10/0, contract 19/0 (18 + header sentinel), t4 63/0, standard-check OK=56 FAIL=0 挂点=40, backend touched tests 48 passed (test_ops_store + test_eval_northstar), node --check all touched faces. + - **发现轮工具面真接线(票 #217 = T3.5,Spec 轴 P1 补票,伞 #206;原先只有 selftest 消费面的三接缝成为真生产消费路)**:①三点接线照 discovery-host.mjs 接线协议——SHORTLIST(模型步)host.tools 直装(faux.mjs runRealStep `ensureDiscoverHost(ctx)`,与 DEEP 微探针宿主同注入形;dupRegistry 复用 ctx 级同注册表,熔断实例轮级挂 ops/breaker.json);MEASURE(确定性零模型)经 host.call("web_read") 补发真证据面——存在性仍宿主重算,URL 候选补发实测探测同守卫链(dup→breaker→cap)+measure-N call-id+tool_seeds 留痕;能算的算出(三面全解=measured),不可算的显式 uncertainty 原因(无 URL 候选/宿主拒绝——被拒≠清白——/排他率缺 SHORTLIST 估值:测量面无改估权不虚构);control_executable=探测自身的阳性对照(url_read_ok);排他率仍透传 SHORTLIST 值。CLOSE 增纯增量读数键 `discover_channels`(活宿主 channelCounts 优先;resume/重放宿主缺席=从 d2 transcript registry_seeds+d3 tool_seeds 工件重算,宁少算——被拒不回放;健康轮字节面不变)。readDiscoverKnobs 增 dupCallLimit 键(与运营轮同旋钮名 OPS_PIPELINE_DUP_CALL_LIMIT 同缺省 3——其 header 注记本就预留此处为消费点);faux 面新增 fixtureDiscoveryMethods(fixtures/pipeline/measure.json 罐装 web_read 表;未罐装=诚实 io_error 信封,零网络律)。②端到端候选行正例终于存在(原全轮例只钉零入队):faux 六步全轮 1 候选走通 SHORTLIST→MEASURE→PROPOSE 落真 queue.md 行(fixture rDNS 候选排他率 0.42+罐装 ok web_read),selftest-pipeline(全轮 E2E:d3 measured/control_executable true、d4 1入队/1skip、PROPOSE rdns 行、discover_channels 读数)与 golden pipeline_discover_full_faux(同钉+queue_file 行断言)双面钉;新增四例(罐装方法三态含罐装失败面透传/MEASURE 三态含 limit 次 dup 拦截显式 repeat_storm uncertainty/CLOSE 活宿主优先↔回放重算↔健康轮位等价/接线挂点哨)。③queue.md 冻结表头句翻正——ENQUEUE 候选行与 appendCandidateCard 候选卡落地之日起「queue_action 唯一写口」即不再真:表头如实述三写口(ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼),JS 面(queue-lock.mjs QUEUE_HEADER)+python 面(_ops_store.py QUEUE_HEADER)+两件 python 测试 fixture(northstar fixture 预存 rationale 子句缺失漂移顺手对齐)+tools.mjs/discover-candidate.mjs 注记同批翻;新 contract 哨钉三抄(python 字面↔node 字面↔appendQueueRowLocked 首建实写串)逐字同+三写词语义哨(回退旧句即哨红)。门(worktree 实跑):selftest-pipeline 104/0(基 100+新 4;红证:steps.mjs+faux.mjs 接线回退→恰 100/4 仅 T3.5 例红)、golden 10/0、contract 19/0(18+表头哨)、t4 63/0、standard-check OK=56 FAIL=0 挂点=40、backend 触面测试 48 passed(test_ops_store+test_eval_northstar)、node --check 全触面。 + - **Discovery-round pipeline (umbrella #206, tickets #207-#210; ADR 0010; the discovery side goes from ad-hoc chat to the sixth round type under the same law as ops rounds)**: T1 (#207) — pipeline.mjs PIPELINES gains DISCOVER_PIPELINE (six data-driven steps SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE; SCAN/MEASURE deterministic zero-model, SHORTLIST model-produced, ENQUEUE write-only) + the OPS_DISCOVER_* knob family (30 calls/3600s/¥5 per round; monthly cap shared with ops rounds at ¥30 through the same admitOrRefuse face) + four discover artifacts into the ARTIFACTS table (22 total) + ledger tag pipeline-discover. T3 (#209) — structured candidate schema with a hand-written validator (CANDIDATE_FIELDS frozen; evidence-pointer existence failures drop the row with a trace, 宁少算, never fabricate), mkDiscoveryHost wiring the three web discovery tools (DISCOVERY_TOOLS frozen; guard chain dup→breaker→cap), queue.md candidate cards written PROPOSE under the #139 bilateral flock, per-channel quota counts as first-class readings. T2 (#208, not yet confluenced when this entry was authored) — G_SHORTLIST span-level verbatim citation (spanMatcher; URL hallucination or reworded quote rejects), G_MEASURE host-side number recomputation, G_ENQUEUE reconciliation, each with negative fixtures. T4 (#210, this entry) — the candidate-row action word unified to PROPOSE (steps.mjs ENQUEUE step consumes CANDIDATE_ACTION from discover-candidate.mjs, one word one spelling; deliberately distinct from the human-action closed set ENQUEUE/REJECT on the python write face), northstar gains two observation-only discovery legs (propose→landing conversion from queue.md PROPOSE rows vs the source registry + exclusivity distribution aggregated from d3-measure; zero-hold triple proven: no exit path, not in the regression family, CI untouched; missing artifacts render N/A sentinels, never fabricated), STANDARD.md D-7 发现与运营同律 with machine-checked mounts 挂点 31→40 (eight at authoring; spanMatcher's citation.mjs mount attached at confluence per the T4 handoff — pre-confluence it would have been a dead anchor = pseudo-standard), GLOSSARY coins PROPOSE, DESIGN §6 discovery wording refreshed. Gates (confluence finals, merger-verified live): selftest-pipeline 100/0 (T4's base 96 + T2's four span cases; red proof: reverting the ENQUEUE-step action word reddens the queue-row assertion 99/1), golden 10/0 (9 + T2's pipeline_discover_full_faux), backend tests/eval 493 passed (tests/core+tests/eval full 1224 passed 8 skipped; northstar 26/26; red-first via ImportError; loaders never fabricate on missing artifacts), standard-check OK=56 FAIL=0 挂点=40 条文=15 (red proof: deleting the D-7 mount row fails with 缺门挂点字段, exit 1). + - **发现轮管线化(伞 #206,票 #207-#210;ADR 0010;发现侧从临时聊天面升格为第六轮型,与运营轮同律)**:T1(#207)——pipeline.mjs PIPELINES 增 DISCOVER_PIPELINE(六步数据驱动 SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE;SCAN/MEASURE 确定性零模型,SHORTLIST 模型产,ENQUEUE 只写)+OPS_DISCOVER_* 旋钮族(每轮 30 调用/3600s/¥5;月帽与运营轮共享 ¥30,admitOrRefuse 同面)+四件发现工件入 ARTIFACTS 名表(总 22)+台账标签 pipeline-discover。T3(#209)——结构化候选 schema+手写校验器(CANDIDATE_FIELDS 冻结;证据指针存在性失败弃行留痕,宁少算,不虚构)、mkDiscoveryHost 接三件 web 发现工具(DISCOVERY_TOOLS 冻结;守卫链 dup→breaker→cap)、queue.md 候选卡以 PROPOSE 写入(#139 双边 flock)、逐通道配额计数一等公民读数。T2(#208,本条落笔时未合流)——G_SHORTLIST span 级逐字回引(spanMatcher;URL 幻觉/改写引文即拒)、G_MEASURE 宿主数字复算、G_ENQUEUE 入队对账,各带负例。T4(#210,本条)——候选行 action 词统一 PROPOSE(steps.mjs ENQUEUE 步消费 discover-candidate.mjs 的 CANDIDATE_ACTION,一词一拼;与 python 写面人裁动作闭集 ENQUEUE/REJECT 刻意分立),北星新增两腿发现观测(入队→落地转化率 queue.md PROPOSE 行 vs 源注册表落地+排他率分布 d3-measure 面聚合;零持门三重实证:零 exit/不进回归族/CI 零改;缺工件渲染 N/A 哨兵不虚构),STANDARD.md 新条文 D-7 发现与运营同律+机检挂点 31→40(落笔八挂点,spanMatcher 的 citation.mjs 挂点已随 #208 合流经移交件补挂——死锚即伪标准),GLOSSARY 铸词 PROPOSE,DESIGN §6 发现通道措辞翻正。门(合流终数,merger 亲跑):selftest-pipeline 100/0(T4 基 96+T2 span 例 4;红证:ENQUEUE 步 action 词回退即候选行断言真红 99/1)、golden 10/0(9+T2 pipeline_discover_full_faux)、backend tests/eval 493 passed(tests/core+tests/eval 全量 1224 passed 8 skipped;northstar 件 26/26,红先 ImportError 实证;loader 缺件不虚构)、standard-check OK=56 FAIL=0 挂点=40 条文=15(红证:删 D-7 挂点行即「缺门挂点字段」FAIL exit 1)。 - **Scanner self-declared ranges: source + eval truth-layer exclusion + label pinning (issues #212 #213 #214, docs closeout #215; scanner_ranges batch; census docs/research/2026-10-26-scanner-ranges-census.md, ADR 0011)**: new `backend/ipdb/_sources/scanner_ranges.py` (cdn_edges-shaped _FEEDS multi-publisher table) ingests 66 MISP warninglists non-nt scanning lists (CC0, CIRCL mirror, ~29,070 CIDR v4+v6 mixed, date-versioned daily) + Palo Alto Xpanse reference-page direct read (9 CIDR — the only major absent from MISP); OpenFilters/internet-scanners (same upstream) deliberately NOT wired — one route per truth, echo discipline; GreyNoise API-only stays out (red line); evidence face is the user-ruled accusation form: Evidence(classification="scanner", verdict="malicious", native_categories=["研究扫描 · <厂商/机构>(自报段)"], r=0.90, stale_days=7, provenance_tier B) — malicious is the behavioral-type assertion (词表 "aggressive scanning"), intent lives in the native label, adjudication stays with the user; independent two-vote corroboration with behavior witnesses (sw_scanners) is legal (observation line independent, no lineage overlap). Eval truth layer (ADR 0011, deliberate product/eval asymmetry = feature): assembly-side exclusion — `layer_builders.scanner_ranges_prefixes` reads prefixes from the raw middleware CSV (backend/data/scanner_ranges.csv, first column, never reverse-parsed from LMDB; missing CSV raises FileNotFoundError — the exclusion face must not silently shrink; empty merged table parks DEGRADED via truth_layer scanner_ok) feeding the existing `build_truth_layer(scanner_prefixes=)` hook; `corpus.build_benchmark` skips the scanner_ranges source and strains every stratum through `apply_scanner_filter` post-cap (宁少算, no backfill; no-source / missing-CSV degenerates bit-identical to the status quo); S12 corpus versioning and the S17 truth-fingerprint-change trend-reset note are pinned by tests (40.124.175.29 two-arm: member diff exact, removed_by_scanner_filter 0→1, both scanlist fingerprints change); the truth-side ops runbook rewire (legacy censys+Xpanse+Shadowserver hand-merged asset → scanner_ranges loader) is measured advice left for the ops round. Product surface: zero new UI mechanism — the native label rides the existing native_categories chip chain (_merge.py:306-331 → _api_models details → SourceDetailRow chip), pinned by double-ended contract tests (real lookup() full chain on 40.124.175.29: malicious verdict stands, label in details, corroborated=False, verdict_conflict stays dark; LookupResultOut outbound shape re-pinned; frontend chip full-literal visible); tamper-red proven both ends. Gates: source tests 11/11 offline (66+1 name-set verbatim vs spec; negative assertions: no OpenFilters, no -nt-, no GreyNoise), backend eval 489/0, backend targeted 1791/0 (source+core) and 111/0 (label chain), frontend vitest 11/11, full backend 2251 passed / 9 skipped (4 dispositions: 2 legit metadata-snapshot updates landed with the ticket, 1 pre-existing baseline failure unchanged, 1 load-flake single-run green). - **扫描器自报段三件套:源接入+eval 真值层排除+标签钉测(issues #212 #213 #214,文档收口 #215;scanner_ranges 批;普查 docs/research/2026-10-26-scanner-ranges-census.md,ADR 0011)**:新增 `backend/ipdb/_sources/scanner_ranges.py`(cdn_edges 同构 _FEEDS 多发布者表)接入 MISP warninglists 66 个非 nt 扫描列表(CC0,CIRCL 镜像,~29,070 条 CIDR v4/v6 混排,日期版本日更)+ Palo Alto Xpanse 参考页直读(9 CIDR——唯一不在 MISP 的大厂);OpenFilters/internet-scanners 同上游刻意不接——每真值一路,防回声;GreyNoise API-only 红线外;证据面=用户裁定的指控形态:Evidence(classification="scanner", verdict="malicious", native_categories=["研究扫描 · <厂商/机构>(自报段)"], r=0.90, stale_days=7, provenance_tier B)——malicious 是行为类型断言(词表 "aggressive scanning"),意图活在 native 标签里,改判权留用户;与行为证人(sw_scanners)独立两票 corroborated 合法(观测线独立,无谱系重叠)。eval 真值层(ADR 0011,产品/eval 刻意不对称=特性):装配侧排除——`layer_builders.scanner_ranges_prefixes` 从原始中间件 CSV(backend/data/scanner_ranges.csv,首列)取前缀表(勿从 LMDB 反解;缺 CSV raise FileNotFoundError——排除面不得无声缩水;合并表空由 truth_layer scanner_ok 落 DEGRADED),喂现成 `build_truth_layer(scanner_prefixes=)` 挂点;`corpus.build_benchmark` 跳过 scanner_ranges 源+封顶后逐分层过 `apply_scanner_filter`(宁少算不回补;无源/缺 CSV 与现状逐位一致退化);S12 语料版本与 S17 truth 指纹换版趋势重置注记以测试钉住(40.124.175.29 两臂:成员差精确、removed_by_scanner_filter 0→1、双 scanlist 指纹皆变);truth 侧 ops runbook 换源(legacy censys+Xpanse+Shadowserver 手合资产→scanner_ranges loader)为测量建言,留 ops 轮人裁。产品面:零新 UI 机制——native 标签走既有 native_categories chip 链(_merge.py:306-331→_api_models details→SourceDetailRow chip),双端契约测试钉死(40.124.175.29 真 lookup() 全链:malicious 判决照旧、标签在 details、corroborated=False、verdict_conflict 不点亮;LookupResultOut 出海形状复钉;前端 chip 全文字面量可见);双端 tamper-red 红证。门:源测试 11/11 offline(66+1 名单与 spec 逐字;负断言:无 OpenFilters/无 -nt-/无 GreyNoise)、后端 eval 489/0、后端定向 1791/0(源+core)与 111/0(标签链)、前端 vitest 11/11、后端全量 2251 过/9 跳(4 挂处置:2 快照合法更新随票落地、1 基线同挂、1 负载 flake 单跑绿)。 - **Artifact-name table single source (issue #202; C1-C3 refactor batch, umbrella #201; pure refactor, disk names byte-identical)**: artifact filenames were raw string literals in three consumption layers (gates.mjs ~45 sites, steps.mjs ~31, selftest-pipeline's 17-name hand-copied list) — adding 09-budget.json in #199 meant hand-syncing three layers (live example); new `ops/agent/artifacts.mjs` exports `ARTIFACTS` (18 fixed names, frozen) + `ARTIFACT_NAMES` + the DEEP parameterized families (`deepArtifactName`/`deepTranscriptName` factories + `DEEP_ARTIFACT_RE`/`DEEP_TRANSCRIPT_RE` matchers); all three layers now take names from the table; a two-way closure sentinel pins table<->disk-set coherence (table gains an entry without a writer = red; a step emits a new artifact not in the table = red); golden/cases.json and the canned-faux artifact-name set are bit-identical pre/post (md5-equal); umbrella closure folds in the fourth write surface — finding-exit.mjs's EXITS_ARTIFACT now takes `ARTIFACTS.ANCHOR_SWEEP_EXITS` (C1-merger find). Known non-swept literals by spec design: faux.mjs transcriptName x3 and pipeline.mjs HEALTH_PIPELINE out/SKIP_WHEN (outside the spec's three layers; the disk-level sentinel backstops). Gates: selftest-pipeline 78/0 (tamper proof: injected fake table entry -> closure sentinel FAIL "工件缺:10-c1-tamper.json" -> restored green), t4 63/0, contract 18/0, golden 9/0, standard-check 47 OK, backend ops subset 271 passed 1 skipped. diff --git a/DP-T1-REPORT.md b/DP-T1-REPORT.md new file mode 100644 index 00000000..be4fab31 --- /dev/null +++ b/DP-T1-REPORT.md @@ -0,0 +1,84 @@ +# DP-T1-REPORT — 发现轮管线骨架+旋钮族+台账(#207 T1) + +工位:worktree `.worktrees/dp-t1`(分支 `feature/dp-t1`,基=集成分支 `feature/discover-pipeline-v0` @ b8825774)。 +spec:/tmp/discover-pipeline-spec.md T1 节(权威);ADR 0010;票 #207(伞 #206)。 +状态:**全部 T1 交付物落地,验收门全绿;禁 push 已遵守(未 push)。** + +## 交付物清单(spec T1 节逐条) + +1. **DISCOVER_PIPELINE 六步表**(`ops/agent/pipeline.mjs`):SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE(+FAILED 终态书挡),数据驱动同 HEALTH_PIPELINE 形制。`PIPELINES={health,discover}` 单源;byName/runnable/STAGE_PREFIX 一律 per-type 派生(`byNameOf`/`runnableOf`/`STAGE_PREFIXES`)。 + - SCAN(script,零模型零网络):候选宇宙快照 `d1-scan.json`=**源注册表面**(backend/ipdb/_sources/*.py 非_前缀,evidence_ptr=repo 相对路径)+**NEEDS.md 缺口行面**(状态∈{open,候选},evidence_ptr=`NEEDS.md:L`)+web 面占位(`channels.web.status="placeholder"`,T3 接线)。 + - SHORTLIST(model `discover/shortlist`,fail retry1/terminal model_face):结构化候选解析,**失败=降级空清单留痕不炸轮**(`d2-shortlist.json={candidates:[],degraded:"shortlist_parse:…"}`,步仍 done,零候选合法,同 TRIAGE 零假设判例)。 + - MEASURE(script):T1=本地确定性存在性面(候选 evidence_ptr ∈ d1-scan 指针集 → evidence_local_ok);未测/超时=`status:"uncertainty"` hold 行留痕(T3 接探针后消除)。工件 `d3-measure.json`。 + - ENQUEUE(script):**只写不动作**——经 `ops/agent/tools/queue-lock.mjs appendQueueRowLocked`(#139 flock 真路径,node/python 同锁同址)写 `ops/queue.md`;入队三判据=证据存在性全过+排他率预估≥`ENQUEUE_EXCLUSIVITY_GATE`(0.25,常量)+阳性对照可执行,**判据在代码不在 prompt(模型无权改,ADR 0010)**;不过=skipped 留痕;queue 写败=fail-closed infra。工件 `d4-enqueue.json`。 + - RETRO/CLOSE:同 health 律(台账幂等/sandbox 红线/09-budget.json 同形快照)。 +2. **round-id 形制**:`^\d{4}-\d{2}-\d{2}-\d{4}-discover(-r\d+)?$`——mkCtx 正则 per-type 构造(后缀集=轮型集);`typeOfRoundId(id)` 后缀推断(显式 type 优先,不符即拦);resume/replay 正则同步双后缀;replay 前缀复制 discover=d1-d4。**selftest 既有 faux 面同构新增**(见红测/绿测节)。 +3. **旋钮族**(`ops/agent/pipeline/budget.mjs`):`readDiscoverKnobs`={OPS_DISCOVER_MAX_MODEL_CALLS=30, OPS_DISCOVER_WALL_CLOCK_S=3600, OPS_DISCOVER_SPEND_CAP_CNY=5, **monthlyCapCny=OPS_PIPELINE_MONTHLY_CAP_CNY(共享同键同缺省 ¥30)**};mkCtx 入口校验(非法=usage 错);**月帽共享=admitOrRefuse 同一函数**(runPipeline 传 `round:ctx.type`,只影响留痕轮次列,裁决真源 admitMonthly 不变);dup 哈希沿用(canonicalJson/dupKey/dupRecord 原样)。引擎四界 per-type:发现轮报错文案指认 OPS_DISCOVER_*;**token 界=发现轮跳过**(tokenBudget undefined 判)。 +4. **artifacts.mjs 名表扩容**:DISCOVER_SCAN/DISCOVER_SHORTLIST/DISCOVER_MEASURE/DISCOVER_ENQUEUE 四名入 ARTIFACTS(总 22);新增派生导出 `HEALTH_ARTIFACT_NAMES`(原 18)与 `discoverArtifactNames()`(7:四名+09-budget/gates/state);d2 transcript 由步面管不入名表(任务明令四名;发现轮闭包断言=固定面∪transcript 字面)。闭包哨三层自动覆盖。 +5. **台账标签**:`LEDGER_TAGS={health:"pipeline",discover:"pipeline-discover"}` 单源(steps.mjs 导出,steps 写面+gates 读回检查同源消费)。 +6. **接线点占位(T3 填)**:SHORTLIST 真面经 runModelStep 通用机零工具注入;MEASURE 本地面;web 渠道 placeholder;prompt `prompts/discover/shortlist.md`(输出契约/span 逐字回引 T2 强化/回声防线/模型无权改判据);canned fixture `fixtures/pipeline/shortlist.json`(两真候选:L14 rDNS parquet+L17 carrier,queue 零入队=排他率未测故 skip,设计使然)+faux.mjs STEP_ROUND/transcriptName SHORTLIST 接线+README 行。 +7. **CLI**:`node ops/agent/pipeline.mjs discover [--round-id ID] [--fixtures DIR]`(main usage/emitInfra 轮名 per-type)。 + +## 验收门数字 + +| 门 | 数字 | 基线 | 判 | +|---|---|---|---| +| selftest-pipeline | **PASS=89 FAIL=0** | 81/0 不降 | ✅(81 基线全绿+8 新例) | +| selftest-t4 | **63/0** | 63/0 | ✅ | +| selftest-contract | **18/0** | 18/0 | ✅(OPS_DISCOVER_* 三名入 SPAWN_CONTRACT_ENV+测试名集) | +| golden | **9/0** | 9/0 | ✅(health 罐装位等价;run-golden.mjs 门数改 HEALTH_PIPELINE 派生单源) | +| standard-check | **OK=47 FAIL=0(挂点 31)** | 47 | ✅ | +| pytest ops 子集(backend venv) | **276 passed, 1 skipped** | — | ✅(82.9s) | +| health 轮罐装面位等价 | golden 9/0+基线 81 全绿+pytest 全绿 | 零变 | ✅:health 可见面零变(门序 10 门/门记录/台账行/env 7 键/报错文案同字符串),仅内部派生单源化 | + +## commit 链(worktree,未 push) + +1. `174aa199` chore(#207) 开工占位(空) +2. `41662317` 旋钮族单源+月帽共享+SPAWN_CONTRACT_ENV 三名 +3. `615a648c` 步/门/faux 面+四门+canned fixture+prompt+台账标签 +4. `f52669fc` DISCOVER_PIPELINE 六步表+引擎 per-type 化+selftest 8 新例+健康轮断言 per-type 化 +5. `1a9f102d` golden 门数 per-type 派生+本报告 + +## 红测真红记载(实现前捕获,PASS=81 FAIL=8) + +| 例 | 红证(逐字) | 注 | +|---|---|---| +| ①旋钮族(含负值拒) | `TypeError: readDiscoverKnobs is not a function` | 结构红:负值/非数字拒行为由 readKnob 单源既有合同测试钉死(#199 判例);绿面新增断言(负值/非数字/env 覆盖/缺省/月帽共享同值)全绿 | +| ②台账标签 | 断言败:留痕行轮次列=discover(实际落成 health) | appendRefusalTrace 硬编码 round="health" 时期 | +| ③月帽共享拒 discover | wiring 红:`round_id 形状须 YYYY-MM-DD-HHMM-health:…-discover`(mkCtx bad_round 先于 admission 面) | 接线后真行为绿:¥30 植入→拒 budget_monthly+留痕 round=discover+轮目录不建 | +| ④round-id 后缀双面 | wiring 红:rejects 消息不匹配(/形状须…-discover/ 不中,P0 拦截面) | 绿后双面真拒 | +| ⑤全 faux 六步 | 同③ wiring 红(轮未起) | 绿后六步/信封/09-budget env 面 5 键/台账行/门序全验 | +| ⑥工件闭包+SCAN 确定性 | `Cannot read properties of undefined`(runD 未成) | 绿后含独立树重扫逐字节同 | +| ⑦SHORTLIST 降级 | 同③ wiring 红(降级断言不可达) | 绿后坏 fixture 真跑解析失败路径:candidates[]+degraded+d3 零行+d4 零入队 | +| ⑧ENQUEUE 写路 | `The "path" argument must be of type string`(ENQUEUE 步未实现) | 绿后含 flock 真写+幂等去重 | + +**四例法定红对应**:旋钮负值拒=①(结构红+readKnob 合同注)/月帽共享=③/round-id 后缀=④/SHORTLIST 降级=⑦——各一例真红在案。 + +## 自决清单(实现者拍板项,评审可翻) + +1. **轮型推断**:typeOfRoundId 由 round-id 后缀推断(显式 type 优先)——测试/agent 侧可只给 id;两者不符由 per-type 正则拦。 +2. **LEDGER_TAGS 与 ENQUEUE_EXCLUSIVITY_GATE 放 steps.mjs 导出**(步写面/门读面同源单源),gates.mjs import 消费。 +3. **SCAN 不加 NEEDS_FILE env 缝**(repo 相对路径直读):避免 OPS_* 表闭包噪音;T2/T3 需要时再加。 +4. **d2 transcript 不入名表**(任务明令四名);发现轮闭包=discoverArtifactNames()∪transcript 字面。 +5. **健康轮既有断言 per-type 化**:门契约=GATE_SEQUENCE↔双管线执行序**并集**双射;健康轮门数/门序从 HEALTH_PIPELINE 派生(HEALTH_GATES_SEQ/HEALTH_GATE_COUNT);golden/run-golden.mjs 同判例(GATE_COUNT→HEALTH_PIPELINE.filter)。 +6. **faux canned 两候选=queue 零入队**(排他率 null→skip):罐装轮不写 queue(测试树内也无 queue 副作用),ENQUEUE 真写路由独立例⑧直调步验证。 +7. **replay 末步工件 per-type**(health=07-synth-report.md/discover=d4-enqueue.json);resume 正则/旋钮回退读面/pending 步序 per-type。 +8. mkCtx 的「P0 仅健康轮管线」throw 由 PIPELINES 闭集校验替代(历史使命完成)。 + +## 给 T2(#208)/T3(#209)的接线点说明 + +**T3 已交卷(0cc76b1d),按其接线协议对接**(DP-T3-REPORT): +- SHORTLIST:模型步真面工具注入=`discovery-host.mjs mkDiscoveryHost() → {tools,seeds,channelCounts,call}` 直装(runRealStep 的 DEEP 判例位:mkDiscoveryHost 替代 mkMicroprobeHost);ctx 级 dupRegistry 沿用(旋钮族 dupCallLimit 同读);轮级 breaker 同面。 +- MEASURE:探针调用=`host.call`(T1 的 uncertainty hold 行在 T3 后由真实读数替换,超时面仍 hold)。 +- CLOSE:`readings.discover_channels=discoverChannelReadings(host.channelCounts)`(09-budget.json 增读数面;G_CLOSE 的 BUDGET_ENV_KEYS per-type 已就位)。 +- 候选 schema 校验=`discover-candidate.mjs`(12 导出面)——G_SHORTLIST/G_MEASURE 行形状校验升级时消费(T1 最小形状版由 gates.mjs 内联,T2 #208 强化时收口)。 +- SCAN web 面:`channels.web` 从 placeholder 升真读(host.tools 的 web 三工具=候选宇宙第三渠道),渠道闭集 `[source_registry,needs,web]`。 +- T2 #208 四门强化:G_SHORTLIST span 逐字回引(spanMatcher)、G_MEASURE 数字复算、G_ENQUEUE 对账;T1 已把四门名+序+最小形状钉进 GATE_SEQUENCE 双射面(单侧改门即红)。 + +## 余项(非 T1 面) + +- T2 #208:四门强化+spanMatcher+selftest 罐装篡改红。 +- T4:STANDARD/GLOSSARY/北星/CHANGELOG(本票未动 CHANGELOG——伞 #206 收口批处理)。 +- 生产激活面:OPS_DISCOVER_* 三旋钮缺省值(30/3600/¥5)即生效,无需 env;与 #199 判例同(agent.mjs SPAWN_CONTRACT_ENV 已含三名)。 + +*报告人:T1 实现者(worker);2026-10-26;40min 顶内完成,无撞墙。* diff --git a/DP-T2-REPORT.md b/DP-T2-REPORT.md new file mode 100644 index 00000000..9faeb41a --- /dev/null +++ b/DP-T2-REPORT.md @@ -0,0 +1,92 @@ +# DP-T2 交卷报告 — #208 四门强化 + span 级回引 + 哨 + +工位:`.worktrees/dp-t2`(分支 `feature/dp-t2`,基 = 集成分支 `feature/discover-pipeline-v0` @ `4b1c8720`,无 merge 需求) +纪律:40min 墙两段(原跑被截断→续作存量入库 `ae96a298`);禁 push 已守;dp-t1/dp-t3 worktree 零触碰。 + +## 交付件(对照任务书 1-5) + +1. **spanMatcher 纯函数**(新 `ops/agent/pipeline/citation.mjs`):`(quote, evidenceText)→bool`, + 基律=朴素 `String.includes` 逐字律(零归一化零 trim)+ **URL 边界律**(自铸判例):quote 中 + URL 记号须在 evidence 中**定界在场**(相邻字符∉ URL 字符集,逗/分号=散文定界符)——杀前缀子串 + (`https://ip.thc` ⊂ `https://ip.thc.org`)而恰等仍收。 + 三负例判例全钉:**幽灵 URL**(引不存在的 URL)/ **半真 URL**(前缀子串+超集串两侧)/ **改写引文** + (大小写/标点改写)全 must-reject;恰等+真 span must-accept;空串/非串拒。 + 同文件:`scanEvidenceFace(scan, ptr)` 三形解析(见自决③)、`ptrForm` 本地分类器、 + `canonicalJson` 稳定序列化;`containsExactString` 同律复刻(T3 未导出,单源判例注记)。 +2. **G_SHORTLIST 升格**:T1 形状检查→span 级语义门。7 键闭集形状(name/quote/evidence_ptr/source_channel + 串;url null|http(s);exclusivity null|[0,1];未知键拒)→ `scanEvidenceFace` 解析 → `spanMatcher` + 逐字对账;门败 reason 带候选名+证据指针;degraded 串合法(span 面 N/A,零候选合法)。 +3. **G_MEASURE 强化**:宿主重算三面——排他率透传(d3 行值 ≡ shortlist 候选值 ?? null)、存在性计数 + (`evidence_local_ok` 经 `scanEvidenceFace` 重算,不一致=FAIL 带两值)、hold_n(原有)。行须按名映射到 + shortlist 候选(无源读数=FAIL)。 +4. **G_SCAN/G_ENQUEUE 查漏**:G_SCAN += 条目自洽(needs 指针 `^NEEDS\.md:L\d+$` + text 非空; + registry 指针 `^backend/ipdb/_sources/\.py$` + name≡stem)。G_ENQUEUE += 入队行三判据复算 + (evidence_local_ok===true ∧ exclusivity≥0.25 ∧ control_executable===true,违者 FAIL 带数字) + + **穷尽哨**(每 shortlist 候选必在 enqueued∪skipped——证据永不灭失)。GATE_SEQUENCE 派生注册面 + 零手抄(selftest-pipeline ~L176 双表并集双射哨既有,已核在场)。 +5. **selftest/golden 扩例**:selftest-pipeline 四门篡改红期例(每门一例真红:G_SCAN 指针自洽/ + G_SHORTLIST 标点改写 U+FF08 半角(→全角(/G_MEASURE 数字漂移/G_ENQUEUE 幽灵入队;未篡改原轮四门 + 全绿对照)+ spanMatcher 三负例例 + scanEvidenceFace 三形例 + SCAN 真面直调例(registry≥40 + + needs text 面 + 真面工件过 G_SCAN);golden 新例 `pipeline_discover_full_faux`(真跑),run-golden + 轮型/roundId/门数从用例+轮型定义表派生(缺省=健康,位等价);standard-check 挂点行=T4 未动。 + +## 自决(裁定记录) + +- **①SCAN faux 缝**:`steps.mjs` SCAN 增罐装宇宙缝(modelFace==="faux" ∧ fixturesDir 有 + `d1-scan.json` → 轮工件候选宇宙=fixture 钉死)。理由:span 门把罐装 quote 与真仓 NEEDS.md 文本 + 耦合,仓文演进会破门 CI 假红;faux=罐装 I/O 判例(#170);真轮(缺省)零影响。真面由「SCAN 真面 + 直调」例独立钉。 +- **②needs 条目 text 面**:真面 SCAN needs 行增 `text`=原文行快照(registry 行结构性无 text)。 + 理由=证据永不灭失:门重放从轮工件回算,quote span 对账面必须入轮工件(G_VERIFY 快照判例同律); + 现薄形(name/url/ptr)无引文可引。 +- **③T3 文件不动 + 双协议分立**(父会话裁定确认):`discover-candidate.mjs` 的 + validateCandidate 保严律(url 必填/exclusivity 必填/两形指针)——那是独立候选面(PROPose 卡)的必填律; + 发现轮回引面(needs 行合法无 URL/null 排他率/透传指针)在 G_SHORTLIST 门面自检 + citation.mjs + `ptrForm` 本地三形分类器(url/artifact=复用 T3 导出面,scan 形=透传 repo 路径/行锚)收口。两闭集 + 分立非冲突;selftest T3 钉例(url:null 拒等)未动,加注记。 +- **④URL 边界律自铸**:纯 includes 律下半真 URL 前缀子串会假绿,定界判(邻字符∉URL 字符集)杀之; + 判例注记在 citation.mjs 头注。 +- **⑤T1 工件闭包例改指罐装计数**(registry 1/needs 2 + text 面),原真面断言(≥40)移至「SCAN + 真面直调」例——职责分立:闭包例钉 faux 轮固定面,直调例钉真面扫描。 +- **⑥golden 09 断言初值 2/1/900 为我臆写,首跑红(实际缺省 30/5/3600/0.05)即改钉真缺省面**—— + golden 钉缺省值不动旋钮,红即纠。 + +## 验收门(全绿) + +| 门 | 数字 | 基线 | +|---|---|---| +| selftest-pipeline | **100/0** | 96 不降 + Δ4 新例(spanMatcher 三负例/scanEvidenceFace 三形/四门篡改红期/SCAN 真面直调) | +| selftest-t4 | **63/0** | 63 ✓ | +| selftest-contract | **18/0** | 18 ✓ | +| golden(真跑 run-golden) | **10/0** | 9+Δ1(`pipeline_discover_full_faux`)✓ | +| standard-check | **47/0**(挂点 31) | 47 不降 ✓ | +| node --check | 全过(citation/gates/steps/selftest-pipeline/run-golden) | ✓ | + +红测真红记录:实现前首跑 94/6(citation 缺失/闭包计数/真面 text/GATES 作用域)+ 95/5(透传形三形外 +主根因)→ 修复后 100/0;四门篡改例在无强化门实现期即红(gates.mjs 改前 G_SHORTLIST 篡改例真红已录)。 + +## commit 链(基 4b1c8720) + +- `afff7252` 占位(#208 开工) +- `ae96a298` 续作存量入库:citation.mjs + gates.mjs 四门 + steps.mjs 三件 + fixture + selftest 新例 +- `3d1ab1f5` golden 扩发现轮用例(run-golden 轮型派生) +- `b042de51` shortlist prompt span 律名面(幽灵/半真/改写三门败——faux 不读 prompt,真轮模型需此告知) + +## 与 T1/T3 接线对拍 + +- **T1**(dp-t1-report 接线协议):①d1 形状微扩(text 键新增)——G_SCAN 自洽门兼容两代工件 + (text 非空串才查,结构性 registry 行无 text 合法);②T1 自测「全 faux 六步」原断言未破(计数面 + 改指罐装已在自决⑤注记);③**回放注意**:T1 时代已存的发现轮工件(若 d1 无 text、d2 quote 非逐字) + 过不了新 G_SHORTLIST/G_SCAN——生产尚无发现轮(T1 #207 起未跑真轮),无回放损失。 +- **T3**(dp-t3-report 接线协议):①discovery-host 的 `filterCandidatesByEvidence`(T3 纯函数面) + 与本轮 `scanEvidenceFace` 分工——前者=web 三工具读数的证据存在性(独立候选面),后者=发现轮 + d1 宇宙回引面(轮工件重放律);②T3 validator 严律保留,双协议分立(自决③);③T3 web 渠道接线 + (channels.web placeholder)留 PR3a,本轮零动;④T3 的 `candidateSourceSlug`/queue 卡行在 G_ENQUEUE + 穷尽哨语义下兼容(enqueued 行 source=slug ↔ shortlist name 小写化映射已核)。 + +## 残留/移交 + +- T4(STANDARD.md 挂点行典章)未动——四门注册面 GATE_SEQUENCE 派生零手抄已核,挂点行由 T4 录。 +- 真轮首跑时 prompts/discover/shortlist.md span 律已名面落字;模型实测改写率待冷启动验收面。 +- golden `pipeline_discover_full_faux` 的 09 断言钉的是**缺省旋钮面**(30/5/3600/0.05),旋钮改缺省 + 须同步此例(与 health 例同维护律)。 diff --git a/DP-T3-REPORT.md b/DP-T3-REPORT.md new file mode 100644 index 00000000..24ee6a1b --- /dev/null +++ b/DP-T3-REPORT.md @@ -0,0 +1,63 @@ +# DP-T3-REPORT — 发现管线化批 T3(#209)交卷 + +工位:`.worktrees/dp-t3`(分支 `feature/dp-t3`,基 b8825774 = 集成分支 feature/discover-pipeline-v0 顶)。commit:46ea92cc(占位)→ 0a810184(交付物 ①②④)→ 5169dc38(交付物 ③⑤)。未 push;未动主仓/其他 worktree。 + +## 交付清单(票 #209 五项) + +### ① 结构化候选 schema+手写校验器(零新 npm 依赖) +`ops/agent/pipeline/discover-candidate.mjs`(新文件,165 行): +- `CANDIDATE_FIELDS`(7 键)/`SOURCE_CHANNELS = ['registry','needs','web']`(SCAN 三来源闭集,与 T1 d1-scan.json 协议面同集,合流时同批扩)/`CANDIDATE_MAX` 长度帽(name 200/url 500/quote 1000/notes 2000/evidence_ptr 600)。 +- `validateCandidate(c)` → `{ok:true,candidate}`|`{ok:false,code:'shape',reason}`:六键必填(name/url/source_channel/quote/evidence_ptr/exclusivity_estimate),notes 可缺省(备注非证据),未知键=拒,source_channel 闭集,exclusivity_estimate∈[0,1] 数字。**排他率≥0.25 硬门裁决不在此**(属 T1 ENQUEUE 判据,模型无权改判据,ADR 0010 §3)。 +- `validateCandidates(list)` → `{valid, dropped:[{index,name,url,reason}]}` 留痕清单。 +- 供 T1 SHORTLIST 消费(候选形状=模型输出契约)。 + +### ② 证据存在性校验(纯函数,宁少算) +同文件:`evidencePtrForm(ptr)` 判两形——`<工件名>#`(RFC6901-lite,~0/~1 转义;拒空指针=整工件指认无法对账)或 http(s) URL(工件集合内 `containsExactString` 深度逐字搜,不规范化——**半真 URL 不算在场**);`resolveEvidencePtr` / `filterCandidatesByEvidence(candidates, artifacts)` → `{kept, dropped:[{index,name,url,evidence_ptr,reason}]}`——缺=候选丢弃+留痕,不静默。 + +### ③ 发现轮工具宿主(microprobes 同形制注入面) +`ops/agent/pipeline/discovery-host.mjs`(新文件,150 行):`mkDiscoveryHost({dupRegistry, dupLimit, budgets, breaker, methods})` → `{tools, seeds, channelCounts, call}`。 +- 工具=web_read/web_search/rss_parse(`DISCOVERY_TOOLS` 冻结闭集;**gh_search 留 legacy 面**,spec T3 节)。实现零复制:`discoveryMethods`(#179)经 `Object.assign` 挂适配器,`this.finish(callId,name,structured,contentText)` 收口——真网络/解析/信封面单源(tools/discovery.mjs)。 +- 守卫链 **dup→breaker→cap**,与 tools.mjs `ToolHost.beforeToolCall` 同序同义(自决已录判例);被拒=零执行零 seed,入 blocked 观察面。breaker 回写仅 `BREAKER_CHANNELS∩布尔 ok` 面(信封码单源)。 +- `call(name,args)`:MEASURE 侧确定性直调,补发 `measure-N` call-id(与运行时模型 id 不撞),同样过守卫链+登记。 +- AgentTool 面(name/label/description/annotations/parameters/outputSchema)= initialState.tools 直装形(microprobes 同);seeds=chronological `{call_id,tool,sc}`(registry_seed 行型,G_* 门对账面)。 + +### ④ 入队行:候选卡(ENQUEUE 只写不动作,#0006 人裁交互律不动) +同 discover-candidate.mjs:`CANDIDATE_ACTION='PROPOSE'`;`candidateSourceSlug(url)` = host slug+md5(url)[:6](同候选跨轮幂等判重键 action+source,同 host 异 url 不互撞);`candidateCardEvidence` = 五字段指针卡(name/url/source_channel/exclusivity_estimate/evidence_ptr;**quote 不入行**——全文在 shortlist 工件);`appendCandidateCard(candidate,{round,queueFile})` 走 `appendQueueRowLocked`(#139 flock 路径单源;坏候选/缺 round=拒写零文件)。7 列冻结表头不动(status 列语义=proposal)。 + +**action 词裁决:PROPOSE(新词)非 ENQUEUE**——ENQUEUE/REJECT 闭集(python `_ops_store.QUEUE_ACTIONS`+registry.mjs queueAction schema)是运营轮**已测量源**动作词(语义门独占 exclusive_probe 实测);候选卡=发现轮 pre-验证提案,语义不同层。queue.md 无程序解析行消费方(grep 证实:py 侧只写不读行;contract 测试只对拍锁路径),7 列形状保住即可。**改词须 python 侧同批**(若合流裁决改用 ENQUEUE)——留合流批。 + +### ⑤ 配额一等公民(通道分列调用计数) +同 discovery-host.mjs:host 内建 `channelCounts`(executed/ok/failed/blocked 分列);`aggregateChannelCalls(rows)` 纯聚合(种子行/扁平行两形;坏行掷错不静默吞——对账面要响);`discoverChannelReadings(channelCounts)` 构造器→`{channels:{tool:{calls,ok,failed,blocked}}, calls_total, blocked_total}`(frozen;不变量 ok+failed=calls 违者/非非负整数=构造期掷错)。**breaker 态快照沿用 steps.mjs 既有 readings.breaker 面,不重造**(票面明示)。缺省帽=tools/shared.mjs `B` 常量单源(与 legacy discover 轮 ToolHost budgets 同源)。 + +## 与 T1 的接线协议(合流面) + +T1(SHORTLIST/MEASURE/CLOSE 步)按此接,不新增 T3 侧改动: + +1. **SHORTLIST(模型步)**:`mkDiscoveryHost({dupRegistry: ctx.dupRegistry, dupLimit: ctx.budgetKnobs?.dupCallLimit, breaker: <轮级 mkBreaker()>, budgets: })`,`host.tools` 直装 initialState.tools(仿 faux.mjs DEEP 真面 mkMicroprobeHost 装配);`host.seeds` 逐条发 registry_seed transcript 行(形制同 microprobes)。 +2. **dupRegistry 传 ctx 级 Map**(与模型调用同 registry,dup 判据 #199 A4 同律);breaker 传**轮级** mkBreaker() 实例(熔断态跨步共享)。 +3. **MEASURE(零模型)**:`host.call(name, args)` 宿主直调,call-id=measure-N。 +4. **CLOSE**:09-budget.json readings 增 `discover_channels = discoverChannelReadings(host.channelCounts)`(键名 T1 定;构造器只出值);breaker 快照仍走既有 readings.breaker。 +5. **候选流**:模型 SHORTLIST 输出候选(①schema)→`filterCandidatesByEvidence(candidates, scanArtifacts)`(②)→T1 排他率硬门(≥0.25,代码常量)→`appendCandidateCard`(④)。全文候选+quote 落 shortlist 工件(名从 T1 扩的 ARTIFACTS.DISCOVER_* 取);queue.md 卡行=指针卡。 + +## 红证(TDD) + +- **①②④**:红 81/3(模块缺,discover-candidate.mjs 未落盘)→绿 84/0。负例:缺键/错型/未知键/闭集外通道/exclusivity 越界/空指针/半真 URL 各拒。 +- **③⑤**:红 84/4(模块缺,discovery-host.mjs 未落盘)→绿 88/0;**反证红 87/1**(临时破坏 blocked 计数 `+=0` → 宿主例「被拒入 blocked 观察面」即败,验毕还原)——断言咬行为,非只咬模块存在。 + +## 验收门(2026-10-26 实跑) + +| 门 | 结果 | +|---|---| +| selftest-pipeline | **PASS=88 FAIL=0**(基线 81 不降+7 新例绿) | +| selftest-t4 | PASS=63 FAIL=0 | +| selftest-contract | PASS=18 FAIL=0 | +| standard-check | OK=47 FAIL=0(不降) | +| node --check | discover-candidate.mjs / discovery-host.mjs / selftest-pipeline.mjs 全过 | + +## 余项/风险(合流批裁决) + +1. **queue.md 表头注释陈旧**:「queue_action 唯一写口」自本写入器起过时(PROPOSE 走 appendQueueRowLocked);表头本身不动(跨语言双源对拍哨)。改词须 python 同批。 +2. **readings 键名 discover_channels 为建议**,T1 CLOSE 落地时定名。 +3. **OPS_DISCOVER_* 旋钮族**未 env 化(ADR 0010 §4):宿主经 opts.budgets 注入即可,旋钮读取器属 T1 宿主装配面。 +4. **dupe-seeds 面注意**:被拒调用不入 seeds=不计聚合(宁少算);若 T1 需要被拒观察面,用 host.channelCounts.blocked(readings 构造器已含 blocked_total)。 +5. 测试对真实现面零网络装载断言(AgentTool 面齐);真网络路径继承 #179 既有测试,不在本票重测。 diff --git a/DP-T35-REPORT.md b/DP-T35-REPORT.md new file mode 100644 index 00000000..06b41def --- /dev/null +++ b/DP-T35-REPORT.md @@ -0,0 +1,81 @@ +# DP-T35-REPORT — T3.5 发现轮工具面真接线(#217)实现交卷 + +- 工位:`.worktrees/dp-t35`,分支 `feature/dp-t35`,基 8611e82b(feature/discover-pipeline-v0 tip),**未 push**。 +- 补票来源:双轴终审 Spec 轴 P1——mkDiscoveryHost/discoverChannelReadings/appendCandidateCard 生产消费方仅 selftest;SHORTLIST 零工具注入、MEASURE 恒 uncertainty+control_executable:null、CLOSE 无 discover_channels 读数;净效应=真面发现轮永不过 ENQUEUE 三判据、零候选行。 + +## 一、commit 链(逐件) + +| commit | 内容 | +|---|---| +| 620720b6 | chore(#217) 占位 commit | +| 6e306c1a | D1:readDiscoverKnobs 追 dupCallLimit 键(OPS_PIPELINE_DUP_CALL_LIMIT 同键同缺省 3,其 header 本就预留此处为消费点);selftest 旋钮族例扩断言红先绿 | +| 6683c2f5 | D2+D3 三点接线:faux.mjs SHORTLIST 注入 ensureDiscoverHost / steps.mjs MEASURE 重写 host.call("web_read") 真证据面 / CLOSE readings.discover_channels 纯增量键;ctxShape 增 discoverHost 键;fixtures shortlist.json rDNS 排他率 0.42 + 新 measure.json 罐装;selftest 全轮 E2E 正例 + 新四例;接线回退红证 100/4 | +| 87bc10c3 | 表头三写口翻正:queue-lock.mjs/_ops_store.py QUEUE_HEADER + 两件 python 测试 fixture(northstar fixture 预存 rationale 子句漂移顺手对齐)+tools.mjs/discover-candidate.mjs 注记;新 contract 表头对拍哨(python 字面↔node 字面↔首建实写串逐字同+三写词语义哨) | +| 0b0804b2 | wip(#217 续作入库防丢):golden pipeline_discover_full_faux 扩端到端正例 + prompts/discover/shortlist.md 工具面句翻正(截断现场存量,续作验证后收口) | +| (本次) | CHANGELOG 双语条 #217 + DP-T35-REPORT.md | + +## 二、四条任务逐项 + +### ① 三点接线(照 discovery-host.mjs 顶部接线协议) + +- **SHORTLIST**(模型步):faux.mjs runRealStep probes 三元扩展——`def.name === "SHORTLIST" ? ensureDiscoverHost(ctx) : (DEEP 原路)`,host.tools 直装 initialState.tools;dupRegistry=ctx 级(与模型调用同注册表,#199 A4 判例);breaker=轮级 mkBreaker 实例(ops/breaker.json,ctx.breakerKnobs 优先)。faux 面 tools 保持 [](罐装宇宙,零网络律不变)。 +- **MEASURE**(确定性零模型):逐候选 host.call("web_read",{url}) 补发实测探测,同守卫链(dup→breaker→cap)、measure-N call-id、tool_seeds 落 d3 工件留痕。判据语义: + - evidence_local_ok=scanEvidenceFace 宿主重算(原样); + - url_read_ok 三态:true(结构面 ok)/false(探测执行且明确失败,code 透传)/null(守卫拒绝或无结构面——被拒≠清白,不充当证据); + - control_executable=url_read_ok——阳性对照=探测自身可执行且读到结果;无 URL 候选=「控制面不可算」显式 uncertainty; + - exclusivity_estimate=SHORTLIST 值透传(测量面无改估权,勿虚构;G_MEASURE 钉 passthrough); + - 三面全解=measured;任一不可算=uncertainty+显式原因串(url_read_code 留痕)。 +- **CLOSE**:readings.discover_channels=discoverChannelReadings(活宿主 channelCounts 优先含 blocked;宿主缺席(resume/重放)=aggregateChannelCalls(replayDiscoverSeeds(ctx)) 从 d2 transcript registry_seed 行+d3 tool_seeds 重算,宁少算——被拒调用零种子不回放);健康轮零改动字节面不变(键仅 isDiscover 分支展开)。 +- **接线消费点**:readDiscoverKnobs.dupCallLimit(OPS_PIPELINE_DUP_CALL_LIMIT,缺省 3)→ensureDiscoverHost dupLimit;fixtureDiscoveryMethods(fixtures/pipeline/measure.json 罐装 web_read 表;未罐装=诚实 io_error 信封)。 + +### ② 端到端候选行正例(不再只钉零入队) + +- fixture:rDNS 主机名候选 exclusivity_estimate null→0.42 + measure.json 罐装 ok web_read(https://ip.thc.org)。 +- selftest-pipeline 全轮 E2E:faux 六步全绿,d3 rDNS 行 measured/evidence_local_ok true/url_read_ok true/control_executable true/exclusivity 0.42/hold_n 1/tool_seeds[0]=measure-1;d4 n_enqueued 1/n_skipped 1;queue.md 实写 `| | PROPOSE | rdns |` 行;09-budget readings.discover_channels.channels.web_read.calls=1。 +- golden pipeline_discover_full_faux 同钉:d3 pins+matches(measure-1)/d4 {1,1}/queue_file state:row PROPOSE rdns 行/09-budget discover_channels 读数。 +- 新四例:罐装方法三态(ok/罐装失败 code 透传/未罐装 io_error)、MEASURE 三态(4 同 URL dupLimit 3:第 3 次起 repeat_storm 显式 uncertainty 宁少算)、CLOSE 读数面(活宿主优先↔回放重算↔健康轮无此键位等价)、接线挂点哨(源面 grep 挂点在位)。 + +### ③ queue.md 表头句翻正(一词一拼) + +- 新句:`# 源生命周期动作队列(三写口:ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼;行由 host 模板渲染,rationale 只落工件)` +- 落点:ops/agent/tools/queue-lock.mjs QUEUE_HEADER、backend/ipdb/_ops_store.py QUEUE_HEADER、backend/tests/test_ops_store.py fixture、backend/tests/eval/test_eval_northstar.py fixture(预存缺 rationale 子句漂移顺手对齐)、ops/agent/tools.mjs:8 注记、discover-candidate.mjs 注记。 +- 新 contract 哨(§14 同形):python 字面↔node 字面(JSON.parse 逐字面)逐字同 + 三写词语义正则 + appendQueueRowLocked 首建实写串 startsWith 断言(18→19)。 +- 刻意不动:prompts/discover-system.md:2「唯一写口」(模型写动作面语境仍真)、DESIGN.md:39(异语境)。 + +### ④ 门全绿不降 + 红测真红 + +见下节。 + +## 三、门数字(worktree 实跑,续作收口后终跑) + +| 门 | 结果 | +|---|---| +| node ops/agent/selftest-pipeline.mjs | **PASS=104 FAIL=0**(基线 100 全在 + 新 4 例) | +| 红证(接线回退) | git show 6683c2f5^ 的 steps.mjs+faux.mjs 覆盖 → **PASS=100 FAIL=4**(恰全轮 E2E 扩钉+MEASURE 三态+CLOSE 读数面+挂点哨四例红,基线零损)→ 还原回 104/0 | +| node ops/agent/golden/run-golden.mjs | **PASS=10 FAIL=0**(discover 例扩正例断言) | +| node ops/agent/selftest-contract.mjs | **PASS=19 FAIL=0**(18 基线 + 新表头对拍哨) | +| node ops/agent/selftest-t4.mjs | **PASS=63 FAIL=0** | +| node ops/agent/standard-check.mjs | **OK=56 FAIL=0 条文=15 金标=3 建议=10 挂点=40 原语=22** RC=0 | +| backend 触面 pytest(判例命令) | tests/test_ops_store.py + tests/eval/test_eval_northstar.py **48 passed** | +| node --check | 全部 11 个触面 mjs OK;python ast.parse 三件 OK | + +## 四、自决与依据(代码内注释已落) + +1. **control_executable 语义**=测量通道阳性对照(url_read_ok):探测自身执行且拿到结构面结果才 true;无 URL 候选=null+「控制面不可算」。未采「上一轮通道历史」(那是熔断面,不是本轮控制面)。 +2. **被拒≠清白**:守卫拒绝(repeat_storm/channel_open/budget)→ url_read_ok=null + code 留痕 + uncertainty,绝不把拒绝当成失败证据(fail≠missing)。 +3. **CLOSE 回放重算宁少算**:活宿主优先(含 blocked 面);resume/重放无宿主时从工件回放,被拒调用零种子不可回放——读数少计不多计(回声不是印证)。 +4. **健康轮位等价**:discover_channels 键仅 isDiscover 分支展开,健康轮 09-budget 字节面不变(CLOSE 例直钉健康轮无此键)。 +5. **faux 面零网络律**:fixtureDiscoveryMethods 未罐装工具=io_error 信封,不降级为真网络调用。 +6. **northstar fixture 漂移顺手对齐**:test_eval_northstar.py 的 queue 头 fixture 原缺「rationale 只落工件」子句(预存漂移),本批统一为完整新句——同一冻结串三抄一致是哨的存在前提。 +7. **表头哨形式**沿 §14 既有锁名对拍哨形制(python 正则抽字面↔node 正则抽字面↔首建实写),不新造哨形。 + +## 五、余项/风险 + +- **真面(MeASURE 真网络)未在本机实跑**:接线面与守卫链由罐装/三态/挂点例钉死,真面 web_read=r.jina.ai 网络路径与 #199 交付 B 熔断同代码路,离线不可验;首真轮观察点=d3 tool_seeds+09-budget discover_channels 读数。 +- appendCandidateCard(候选卡第三写口)仍无管线内调用方(T3 遗面):本票范围=表头如实+哨钉死,不伪造消费方;接线留 #206 后续票裁。 +- prompts/discover/shortlist.md 句已翻正(web_read/web_search/rss_parse 三只读工具可调),属文案面无门。 +- breaker.json 在本批所有绿门下不产生新写路径(罐装/回放均零计数码),B-snapshot 健康例(breaker.json 不创建)仍绿。 + +## 六、交割 + +- 未 push;worktree 留待 merger 独立复算(SDD 纪律:不信自报,merger 重跑全门+红证重放)。 diff --git a/DP-T4-REPORT.md b/DP-T4-REPORT.md new file mode 100644 index 00000000..4ef6ad3f --- /dev/null +++ b/DP-T4-REPORT.md @@ -0,0 +1,67 @@ +# DP-T4 交卷报告(#210 典章+北星+CHANGELOG) + +工位:.worktrees/dp-t4,分支 feature/dp-t4,基 4b1c8720(feature/discover-pipeline-v0 = T1 bfe9bc62 + T3 4b1c8720 已合流;T2 #208 未合流——citation.mjs 不在本树,挂点决策见 §自决③)。未 push、未动集成分支、未关票。 + +## 五件交付 + +### ① action 词统一 PROPOSE(0e34a57f) + +- `ops/agent/pipeline/steps.mjs` ENQUEUE 步:字面量 `action: "ENQUEUE"` → `CANDIDATE_ACTION`(自 discover-candidate.mjs 导入,单源一词一拼,与 T3 appendCandidateCard 同词)。T1 面 MEASURE 恒写 `control_executable:null` → 全 skip,appendQueueRowLocked 是死路——仍先对齐,防 T2/T3 活化后拼写分叉。 +- python 面(**零改**,T3 merger 已核实读面兼容):`_ops_store.py` QUEUE_ACTIONS=("ENQUEUE","REJECT") 是**人裁动作闭集**(python 写面),PROPOSE=提案非动作,分立不合并。 +- `selftest-pipeline.mjs` ENQUEUE 写路钉面同步断言 `\| PROPOSE \| rdns_hf_parquet \|`。 +- 红证:sed 回改 ENQUEUE → PASS=95 FAIL=1(候选行断言真红);还原 → 96/0。 + +### ② STANDARD.md D-7 + GLOSSARY(7f55c7f8) + +- 新条文 **D-7 发现与运营同律**(判例链出处 ADR 0010):第六轮型同宿主/同台账/同工件树;六步 SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE;四门全机检(G_SCAN 形状/G_SHORTLIST span 级逐字回引/G_MEASURE 数字复算/G_ENQUEUE 对账);**模型零裁决**(入队判据=代码常量不在 prompt);**ENQUEUE 只写不动作**(#139 双边 flock,开票/接源全人裁);**PROPOSE 词**(与 ENQUEUE/REJECT 人裁闭集分立);北星两腿同 E-5 零持门。 +- 原语组成:状态机骨架;阶段门;三层职责线;工具白名单(全部既有原语,零新词入原语表——22 词冻结不动)。 +- 门挂点 8 条新增(挂点 31→39,全机检 existsSync+fragment):pipeline.mjs#DISCOVER_PIPELINE;gates.mjs#G_SCAN/#G_SHORTLIST/#G_MEASURE/#G_ENQUEUE;discovery-host.mjs#mkDiscoveryHost;discover-candidate.mjs#CANDIDATE_ACTION;monthly.py#load_discover_proposals。 +- **序言计数零硬编码**(#187 判例:计数以原语表为准,不重复计数)。 +- GLOSSARY.md 铸词 **PROPOSE(提案动作词)** 一行式(发现轮/发现管线已在伞 charter 铸,未重复)。 +- 红证:删 D-7 门挂点行 → FAIL=1「D-7 缺门挂点字段(条文五件套不全=伪标准形态)」挂点回落 31;复原 → OK=55 FAIL=0 挂点=39。 + +### ③ monthly.py 北星两腿(2d71cc1f) + +- `backend/ipdb/_eval/monthly.py` 新纯函数:`discover_conversion(proposed, landed)`(queue.md PROPOSE 去重行 vs 注册表源名逐字匹配;宁少算:候选名≠模块名=漏匹配不算落地;无提案→None 不虚构 0%)+`exclusivity_distribution(values)`(四桶 [0,0.25)/[0.25,0.5)/[0.5,0.75)/[0.75,1],0.25 桶界=ENQUEUE 门槛,ge_gate 计数;零读数→None)。 +- loader:`load_discover_proposals(ops_dir)`(queue.md PROPOSE 行去重 slug,缺文件/零行→None)+`load_discover_exclusivity(ops_dir)`(rounds/*-discover*/d3-measure.json 聚合 results[].exclusivity_estimate,坏件跳过)。 +- `northstar_record` 扩三注入 kwarg(proposed/landed/exclusivity),record 增 `discover` 节;`render_northstar` 第二行「发现轨北星(ADR 0010, 只观测不持门)」N/A 哨兵三态分立(无 PROPOSE 记录/零排他读数/全量两腿)。 +- 接线 `__main__.py` run_readout:ops_root() 单源(_ops_store),registry 源名集合=landed;生产 P2 桥(node DOCS_ROOT/ops)与 python ops_root 同址同文件。 +- 月报行:monthly-*.md 头块发现轨行 + northstar-*.json discover 节。 +- **零持门三重实证**:①零 exit(monthly.py 全文无 exit 调用)②不进回归族(fusion_replay_gate.py/monthly_eval.sh 零 discover 消费;gates.mjs 排他率命中=T1 环内四门,非北星持门)③CI 零改(.github/ 无 diff)。 +- 测试:`tests/eval/test_eval_northstar.py` 12 新例(纯函数手算真值:3 distinct/2 landed/2÷3;n=5 mean=0.43 桶 1/2/1/1 ge_gate=4;loader 只收 -discover 轮、ENQUEUE 行不入、缺件→None)。红先:实现前 ImportError 真红。 + +### ④ CHANGELOG 伞条(17ae289b) + +Unreleased/新增 一条双语伞(#206-#210):T1 六步骨架+OPS_DISCOVER_*(月帽共享 admitOrRefuse)/T3 校验器+discovery-host+PROPOSE 候选卡+配额通道计数/T2 span 三面(spanMatcher+数字复算+对账,注明落笔时未合流)/T4 PROPOSE 统一+北星两腿+D-7 八挂点+GLOSSARY+DESIGN。门数字=各工位已证数,终数留合流终审(dispatcher 更新)。 + +### ⑤ DESIGN.md §6 翻正(dd012d77) + +预算四界(OPS_DISCOVER_* 30/3600s/¥5,月帽共享 ¥30)/北极星(发现侧两腿零持门)/实绩回流(discover 找源已升格发现轮管线,ENQUEUE 只写+PROPOSE+全人裁)三处措辞滞后翻正,一词一拼。 + +## 自决记录(超出票面字面、需 merger 知悉) + +1. **spanMatcher 挂点不挂 citation.mjs**(T2 未合流,挂了即死锚=伪标准 FAIL):D-7 挂活面四门+discovery-host+CANDIDATE_ACTION+load_discover_proposals;判例链注「spanMatcher 挂点随 #208 合流补挂」——**合流 merger 动作项**:T2 合入后补 `ops/agent/pipeline/citation.mjs#spanMatcher` 入 D-7 门挂点行(挂点 39→40)。 +2. **PROPOSE↔落地匹配口径**:候选 slug(≤32 snake)vs 注册表模块名逐字相等=落地;宁少算,名不符=漏报不虚增。真实转化率下界。 +3. **排他率桶界取 0.25**:ENQUEUE_EXCLUSIVITY_GATE 常量同值;桶界即门槛语义,不改门槛。 +4. 北星接线 dev 面:worktree 无 docs/ 仓,loaders 自然 miss→None(N/A 哨兵),不虚构。 + +## 验收门(全绿) + +| 门 | 数字 | 基线 | 结果 | +|---|---|---|---| +| selftest-pipeline | **PASS=96 FAIL=0** | 96/0 不降 | ✅ | +| standard-check | **OK=55 FAIL=0(挂点 39/条文 15)** | 挂点增+FAIL=0 | ✅(31→39) | +| pytest(tests/core+tests/eval 超集,主仓 venv) | **1224 passed, 8 skipped** | 全绿 | ✅(含 northstar 26/26,12 新) | +| selftest.sh 全伞 | **PASS=115 FAIL=0** | 全绿 | ✅ | +| node --check | steps.mjs/selftest-pipeline.mjs | — | ✅(运行即装载面) | + +## commit 链(worktree,未 push) + +``` +17ae289b docs(#210 T4): CHANGELOG 双语伞条 +dd012d77 docs(#210 T4): DESIGN.md §6 三处翻正 +7f55c7f8 feat(#210 T4): STANDARD D-7+八挂点+GLOSSARY PROPOSE(红证 FAIL=1→复原) +2d71cc1f feat(#210 T4): 北星两腿+零持门三重实证(红证 ImportError→26 passed) +0e34a57f feat(#210 T4): ENQUEUE 步 action 词统一 PROPOSE(红证 95/1→96/0) +01b3c6b2 chore(#210 T4): 占位 commit +``` diff --git a/GLOSSARY.md b/GLOSSARY.md index 8b2afb96..277ce04d 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -127,6 +127,9 @@ - **判例升格通道**:同因人裁留痕 ≥2 且 ruling_agreement 一致(键并集:文本归一全等 ∨ 源身份词相交;superseded 链不计数防谱系回声)→ retro/promotion-candidates/ 候选件(仅立项:三门槛=可机检/可证伪/非环境特异性以 pending 起步待人裁逐项核);人裁采纳(adopt_rule)才入概念判例,lineage 必列来源留痕(谱系可回算);检测器永不写判例区。锚:ops/agent/promote-detect.mjs(B稿 §3.2) - **通道信封四码**:外部通道(gh/rg)失败的结构化分类码——channel_missing(spawn ENOENT/rc=127,通道不在运行环境)/channel_auth(凭证缺失失效:not logged in/401/403)/quota(rate limit 限流)/transient(超时/网络,唯一可重试族,同参数 ≤1 次);判定优先级 channel_missing>channel_auth>quota>transient>原兑底(gh_error/rg_infra 兼容保留);每码文案自带「下一步合法动作」(对齐 MCP isError 语义 2026-07-28:错误要能引导模型自纠或升级)——前三码属基础设施/部署面不可轮内自纠,直接人裁区;trace.rc 恒 Number(ENOENT→127/超时杀→124)。判例:2026-10-24-discover-dual-channel-dead(容器无 gh 双通道全灭,失败不可诊断面)。锚:ops/agent/tools/envelope.mjs(classifyChannelError)、prompts/discover-system.md(信封处置)、GLOSSARY 同批录入律 - **发现通道**:discover 轮的读向补全三薄工具 web_read(r.jina.ai markdown 正文,恒无鉴权头——实测 2026-10-24:裸调用正常,带欠费 key 反 402)/ web_search(pi-web-access 多 provider 网络搜索,2026-10-24 用户令;缺省路由 keyless duckduckgo 零配置一发即活,jina 经 agent-config tool_keys 扩槽配 key 后入回退链;路由配置单源 = web-search.json 物化进 ops 树 + PI_CODING_AGENT_DIR 指路,auth.json 同型判例)/ rss_parse(本地零依赖 RSS2.0/Atom 结构解析,新鲜门 RSS_FRESH_DAYS=30);与测量工具(fetch_url/gh_search/exclusive_probe/matrix)的边界=数据形状天然隔离:产出发现线索,不进谓词硬门数字与 MEASUREMENT_TOOLS(evidence 语义门不因此放宽);agent-reach 整只不进镜像(异构故障面违 few, sharp tools)。锚:ops/agent/tools/discovery.mjs(normalizeJinaRead/parseMcpSearchText/parseRss)、prompts/discover-system.md(动作面表)、DESIGN §6(2026-10-24 裁定) +- **发现轮**:发现侧(新情报源发现)的管线化轮型,round-id 后缀 `-discover`;六步 SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE 与运营轮同律(同宿主/同台账/同工件树形制)——SCAN/MEASURE 确定性零模型,SHORTLIST 模型产结构化候选,ENQUEUE 只写不动作(入 queue.md 候选卡,人裁终局);MEASURE 超时/不可达=uncertainty hold 不判死;入队判据(排他率≥0.25+证据存在性+阳性对照)是代码常量,模型无权改(ADR 0010)。锚:ops/agent/pipeline.mjs(DISCOVER_PIPELINE)、docs/adr/0010 +- **发现管线**:发现轮的六步四门管线本体——四门 G_SCAN(形状)/G_SHORTLIST(span 级逐字回引:quote 必须逐字见于 SCAN 工件 evidence 面,幽灵 URL/改写引文即拒)/G_MEASURE(宿主数字复算)/G_ENQUEUE(入队行⊆shortlist∩measure 合格面对账);span 级回引开源界零实现(2026-10 调研判语),判例自铸;OPS_DISCOVER_* 旋钮族独立(30 调用/3600s/¥5),月帽与运营轮共享(admitOrRefuse 同面)。锚:ops/agent/pipeline/gates.mjs(四门)、ops/agent/pipeline/citation.mjs(spanMatcher) +- **PROPOSE(提案动作词)**:queue.md 候选行的 action 词——发现轮管线产出的是提案非动作:ENQUEUE 步与候选卡统一写 PROPOSE(单源 ops/agent/pipeline/discover-candidate.mjs CANDIDATE_ACTION,一词一拼),与运营轮人裁动作词 ENQUEUE/REJECT(python _ops_store.QUEUE_ACTIONS 写面闭集)分立;发现轮写口不得复用 ENQUEUE 混淆「已入队动作」语义(#210 T4 统一判例)。锚:ops/agent/pipeline/discover-candidate.mjs(CANDIDATE_ACTION)、ops/agent/pipeline/steps.mjs(ENQUEUE 步消费) - **黄金标准(四层)**:情报运营 agent 开发验收典章(ADR 0008,2026-10-24 用户确认;真源 STANDARD.md v0 = #181)——原语层(22 词,条文必须声明由哪些原语组成)→条文层(编号+判例链,EDD 开发环=头条)→门挂点层(CI/ops 轮/selftest 机检挂口)→金标层(回归语料);**挂不上验证手段的条目只能称建议,不得称标准**(反面教材=锚门死件;机检面=standard-check.mjs,挂 selftest 链)。结构词:条文五件套=编号+主张+原语组成+门挂点+判例链(缺一=伪标准形态);挂口族=门挂点层注册表(CI 腿/selftest 伞/golden 回归/ops 轮门四族,挂点须落族内载体,嵌套 docs 仓路径禁作挂点)。外部依据只采 2026 源。Avoid:与金族他词混用(金轨迹=空转保活、golden fixtures=字节等价基线、裁决级锚=anchors.py 锚集,均非本词)。锚:ops/agent/STANDARD.md(典章真源)、ops/agent/standard-check.mjs(门挂点核对器)、docs/adr/0008(嵌套 docs 仓)、notes/ops-agent-golden-standard-research-2026-10-24.md - **回归轨 / 能力轨**:评测双轨语义(ADR 0008)——回归轨=已知答案金标+行为探针,≈100% 通过才放行,持 CI+运营轮巡检(回答「agent 是否还能做会的事」);能力轨=北星指标(整体情报更准)爬坡观测,不持门;两轨阈值语义不同,不得互相原谅。出处:Anthropic evals 2026-01。锚:docs/adr/0008、backend/ipdb/_eval/anchors.py(回归轨既有件,接线=租户批) - **安静失败**:监控面板全绿而系统行为已错——标准可观测性(存活/延迟/错误率)抓不住语义级退化,须行为探针(活路径已知答案查询)代查(IEEE Spectrum 2026-04)。判例:锚门死件(8.8.8.8 被打恶而门不响)。锚:notes/ops-agent-golden-standard-research-2026-10-24.md(工业腿③) diff --git a/backend/ipdb/_eval/__main__.py b/backend/ipdb/_eval/__main__.py index ff505f54..70975bc2 100644 --- a/backend/ipdb/_eval/__main__.py +++ b/backend/ipdb/_eval/__main__.py @@ -362,12 +362,14 @@ def run_readout(registry, out_dir, *, assets_dir=None, keysets_dir=None, from .layer_builders import PER_SOURCE_N, build_all_monthly, non_vpn_resolvers from .layered import assemble_four_layers, epsilon_sensitivity, layered_readout from .monthly import (compose_action_block, compose_monthly_md, - load_anchor_green, load_dispositions, + load_anchor_green, load_discover_exclusivity, + load_discover_proposals, load_dispositions, load_leg_status, load_prev_northstar, northstar_record, render_anchor_oc, render_consensus_diag, render_leg_status, render_northstar, render_scorecard, render_steal_parking_diag) + from .._ops_store import ops_root # #210 T4:发现轨北星读面 ops 树根(单源) out_dir = Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) @@ -445,7 +447,15 @@ def run_readout(registry, out_dir, *, assets_dir=None, keysets_dir=None, ns = northstar_record( readout, load_anchor_green(out_dir / "model"), load_prev_northstar(out_dir, run_id), - run_id=run_id, eval_env=eval_env(), today=today) + run_id=run_id, eval_env=eval_env(), today=today, + # #210 T4 发现轨两腿注入(ADR 0010 决策⑤, 同 E-5 零持门): + # 分子分母读 ops 树(queue.md PROPOSE 行 / rounds/*-discover*/ + # d3-measure.json;ops_root 单源 = 生产 P2 桥与 node DOCS_ROOT/ops + # 同址同文件), 落地面 = 本轮真注册表源名集合; 读面只读永不写, + # 缺件 → None → N/A 哨兵(无话不虚构)。 + proposed=load_discover_proposals(ops_root()), + landed=set(srcs), + exclusivity=load_discover_exclusivity(ops_root())) (out_dir / f"northstar-{run_id}.json").write_text( json.dumps(ns, indent=1, ensure_ascii=False), encoding="utf-8") north_star_lines = render_northstar(ns) diff --git a/backend/ipdb/_eval/monthly.py b/backend/ipdb/_eval/monthly.py index 140a55a4..45242e49 100644 --- a/backend/ipdb/_eval/monthly.py +++ b/backend/ipdb/_eval/monthly.py @@ -14,6 +14,7 @@ import gzip import json +import re from dataclasses import dataclass from datetime import date, timedelta from pathlib import Path @@ -294,6 +295,61 @@ def load_prev_northstar(out_dir: Path, run_id: str) -> dict | None: return prev +# 发现轮候选行判式(#210 T4): 行形状 = T4 冻结 7 列, 判重键 action+source +# 与写面(_ops_store.append_queue / queue-lock.mjs appendQueueRowCore)同构; +# source 形状 ^[a-z0-9_]{1,32}$ 同写面 —— 严格行正则 = 宁少算: 表头/分隔 +# 行/散文行不入读数。 +_PROPOSE_ROW_RE = re.compile( + r"^\|\s*\d{4}-\d{2}-\d{2}\s*\|[^|]+\|\s*PROPOSE\s*\|" + r"\s*([a-z0-9_]{1,32})\s*\|") + + +def load_discover_proposals(ops_dir: Path) -> list[str] | None: + """发现轮北星①分母: queue.md PROPOSE 候选行 source 读数。 + + /queue.md = node 写面同件(#139 flock 域); 只读永不写 + (ENQUEUE 只写不动作的读侧影子)。判重键 action+source 同写面 —— + 同 slug 跨轮多行只取一员(转化的分母是「提案过多少源」非行数); + 运营轮 ENQUEUE/REJECT 行不入(闭集分立, discover-candidate.mjs + CANDIDATE_ACTION 注)。缺件或无 PROPOSE 行 → None(无提案记录 ≠ + 零提案 ≠ 转化率 0%, 三态分立不虚构)。""" + qpath = Path(ops_dir) / "queue.md" + if not qpath.is_file(): + return None + rows = [] + for line in qpath.read_text(encoding="utf-8").splitlines(): + m = _PROPOSE_ROW_RE.match(line) + if m: + rows.append(m.group(1)) + # 判重键 action+source 同写面: 同 slug 跨轮多行只取一员(首现序, + # 人工维护面/历史行双保险; discover_conversion 内再独立去重一次) + return list(dict.fromkeys(rows)) or None + + +def load_discover_exclusivity(ops_dir: Path) -> list[float]: + """发现轮北星②输入: rounds/*-discover*/d3-measure.json 的 + results[].exclusivity_estimate 读数袋(逐件升序, 轮序稳定)。 + + round-id 后缀 -discover(-rN)? 同 pipeline.mjs 轮型派生; 非发现轮 + 目录不入(宁少算)。None/缺键/非数读数跳过(缺读数 ≠ 0); 坏件跳过 + 不炸(同 load_prev_northstar 宽容律)。无 rounds/无件 → [](分布面 + N/A 哨兵)。只读永不写。""" + rounds = Path(ops_dir) / "rounds" + if not rounds.is_dir(): + return [] + xs: list[float] = [] + for mf in sorted(rounds.glob("*-discover*/d3-measure.json")): + try: + data = json.loads(mf.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError, ValueError): + continue + for r in (data.get("results") or []): + v = r.get("exclusivity_estimate") if isinstance(r, dict) else None + if isinstance(v, (int, float)) and not isinstance(v, bool): + xs.append(float(v)) + return xs + + def load_leg_status(out_dir: Path) -> dict | None: """腿级状态件(#193 C5): /leg-status-*.json 最新一件; 月轮壳在 readout 腿前写入(腿名→rc, readout=null=本腿在飞), @@ -333,8 +389,9 @@ def render_leg_status(leg: dict | None) -> list[str]: def northstar_record(readout: dict, anchor_hist: list[dict], prev_ns: dict | None, *, run_id: str, eval_env: str, - today: date) -> dict: - """北星两腿纯计算(接线方传 readout + 工件读数, 本函数不碰文件)。 + today: date, proposed: list[str] | None = None, + landed=None, exclusivity: list[float] | None = None) -> dict: + """北星纯计算(接线方传 readout + 工件读数, 本函数不碰文件)。 ①锚集全绿率 = anchor_hist 最新一件(回归轨工件直读); ②谱系去重后 corroborated 精度: OK 层 k_all 直方图 k≥2 桶求和 @@ -343,8 +400,12 @@ def northstar_record(readout: dict, anchor_hist: list[dict], 事故面 = 聚合器互证假印证)折减 → precision = (n − derived_only)/n; n=0 → None(零读数≠精度 0, 无话不虚构)。月环比 = 本轮 − 前轮 (百分点), 任一侧值缺席(首轮件无/N/A)→ None。直方图键 int 强转 - (str 键 JSON 往返同容), 非数键跳过。只观测不持门: 本函数及调用方 - 均不设阈值、不产退出码。""" + (str 键 JSON 往返同容), 非数键跳过。 + ③发现轨两腿(#210 T4, ADR 0010): proposed/landed/exclusivity 三读数 + 由接线方注入(queue.md PROPOSE 行 / 源注册表 / d3-measure 工件), + 本函数只做 discover_conversion / exclusivity_distribution 纯计算; + 读数缺席 → 各腿 None(无话不虚构), 不设阈值不产退出码。 + 只观测不持门: 本函数及调用方均不设阈值、不产退出码。""" kcorr = 0 for block in (readout.get("layers") or {}).values(): if block.get("status") != "OK": @@ -377,9 +438,52 @@ def northstar_record(readout: dict, anchor_hist: list[dict], (precision - prev_prec) * 100 if precision is not None and prev_prec is not None else None, "prev_northstar_present": prev_ns is not None, + # #210 T4 发现轨两腿(ADR 0010): 零持门同 E-5 —— 只进观测件, + # 不设阈值/不进回归族/CI 零改; 读数缺席 = None(N/A 哨兵)。 + "discover": { + "conversion": discover_conversion(proposed, landed), + "exclusivity": exclusivity_distribution(exclusivity), + }, } +# 发现轨北星①(#210 T4, ADR 0010 决策⑤): 入队→落地转化率 = +# queue.md PROPOSE 提案(去重 action+source)vs 源接入落地(注册表源名)。 +# 提案 slug 与注册表模块名同名(如 blocklist_de)即算落地 —— 逐字匹配, +# 名不同 = 漏匹配(宁少算); 无提案记录 → None(无话不虚构, ≠ 0%)。 +def discover_conversion(proposed, landed) -> dict | None: + distinct = list(dict.fromkeys(proposed or [])) + if not distinct: + return None + landed_set = set(landed or ()) + hits = sum(1 for s in distinct if s in landed_set) + return {"proposed": len(distinct), "landed": hits, + "rate": hits / len(distinct)} + + +# 发现轨北星②(#210 T4, ADR 0010 决策⑤): 排他率分布 = 发现轮 +# d3-measure 工件 results[].exclusivity_estimate 聚合; 桶沿含 ENQUEUE +# 门槛 0.25(steps.mjs ENQUEUE_EXCLUSIVITY_GATE 同值, 跨语言双抄形状 +# = QUEUE_HEADER 判例; 判据常量, 改动需 ADR 0010 同批)。零读数 → None。 +def exclusivity_distribution(values) -> dict | None: + xs = [float(v) for v in (values or []) if v is not None] + if not xs: + return None + buckets = {"[0,0.25)": 0, "[0.25,0.5)": 0, "[0.5,0.75)": 0, "[0.75,1]": 0} + for x in xs: + if x < 0.25: + buckets["[0,0.25)"] += 1 + elif x < 0.5: + buckets["[0.25,0.5)"] += 1 + elif x < 0.75: + buckets["[0.5,0.75)"] += 1 + else: + buckets["[0.75,1]"] += 1 + return {"n": len(xs), "mean": sum(xs) / len(xs), "buckets": buckets, + "ge_gate": buckets["[0.25,0.5)"] + buckets["[0.5,0.75)"] + + buckets["[0.75,1]"]} + + def render_northstar(ns: dict) -> list[str]: """北星行(恒一行;「只观测不持门」写进行文)。N/A 哨兵分立: 锚读数缺席 / 零 corroborated 读数 / 首轮 / 前轮 N/A 各自显式措辞, @@ -410,7 +514,26 @@ def render_northstar(ns: dict) -> list[str]: prec = (f"corroborated 精度 {corr['precision'] * 100:.1f}%" f"(corroborated {corr['n']} 读数, 派生独证 " f"{corr['derived_only']}, {pmom})") - return [f"- 能力轨北星(E-5, 只观测不持门): {anchor}|{prec}"] + # 发现轨行(#210 T4, ADR 0010): 两腿各自 N/A 哨兵 —— 转化率无提案记录 + # ≠ 0%; 分布零读数 ≠ 全零桶(无话不虚构, 与能力轨同律)。 + disc = ns.get("discover") or {} + conv = disc.get("conversion") + if conv is None: + conv_s = "入队→落地转化率 N/A(无 PROPOSE 提案记录)" + else: + conv_s = (f"入队→落地转化率 {conv['landed']}/{conv['proposed']}" + f"({conv['rate'] * 100:.1f}%)") + ex = disc.get("exclusivity") + if ex is None: + ex_s = "排他率分布 N/A(零 d3-measure 排他率读数)" + else: + b = ex["buckets"] + ex_s = (f"排他率分布 n={ex['n']} mean={ex['mean']:.2f} " + f"(0–0.25:{b['[0,0.25)']} 0.25–0.5:{b['[0.25,0.5)']} " + f"0.5–0.75:{b['[0.5,0.75)']} 0.75–1:{b['[0.75,1]']} " + f"≥gate(0.25){ex['ge_gate']}/{ex['n']})") + return [f"- 能力轨北星(E-5, 只观测不持门): {anchor}|{prec}", + f"- 发现轨北星(ADR 0010, 只观测不持门): {conv_s}|{ex_s}"] def _fmt(v) -> str: diff --git a/backend/ipdb/_ops_store.py b/backend/ipdb/_ops_store.py index f54298b2..6c256565 100644 --- a/backend/ipdb/_ops_store.py +++ b/backend/ipdb/_ops_store.py @@ -75,8 +75,12 @@ "| 日期 | 轮次 | model | 用量 | 备注 |\n" "|---|---|---|---|---|\n" ) +# 表头(T4 冻结列面;首行句 #217 T3.5 翻正:三写口如实描述——ops 轮 queue_action +# (append_queue/queueAction 同路)·发现轮 ENQUEUE 候选行·发现轮候选卡 +# appendCandidateCard,后两路 action=PROPOSE 一词一拼;与 node 侧 +# ops/agent/tools/queue-lock.mjs QUEUE_HEADER 跨语言双抄,对拍哨钉死不漂移) QUEUE_HEADER = ( - "# 源生命周期动作队列(queue_action 唯一写口;行由 host 模板渲染,rationale 只落工件)\n\n" + "# 源生命周期动作队列(三写口:ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼;行由 host 模板渲染,rationale 只落工件)\n\n" "| 日期 | 轮次 | action | source | predicate | evidence | vetoable |\n" "|---|---|---|---|---|---|---|\n" ) diff --git a/backend/tests/eval/test_eval_northstar.py b/backend/tests/eval/test_eval_northstar.py index 3bf2f6d7..0a0f6151 100644 --- a/backend/tests/eval/test_eval_northstar.py +++ b/backend/tests/eval/test_eval_northstar.py @@ -18,7 +18,11 @@ from ipdb._eval.monthly import ( compose_monthly_md, + discover_conversion, + exclusivity_distribution, load_anchor_green, + load_discover_exclusivity, + load_discover_proposals, load_prev_northstar, northstar_record, render_northstar, @@ -167,7 +171,7 @@ def test_render_full_values_one_line(): run_id="r2", eval_env="monthly", today=date(2026, 11, 1)) lines = render_northstar(ns) - assert len(lines) == 1 # 能力轨记录 = 一行 + assert len(lines) == 2 # 能力轨行 + 发现轨行(#210 T4 后恒两行) ln = lines[0] assert ln.startswith("- ") assert "能力轨北星" in ln and "E-5" in ln and "只观测不持门" in ln @@ -241,3 +245,150 @@ def test_md_places_north_star_line_in_header_before_layers(): def test_md_without_north_star_lines_is_schema_stable(): assert "能力轨北星" not in _md_min() + + +# ── #210 T4 发现轨北星:PROPOSE→落地转化率 + 排他率分布(ADR 0010;零持门)── +# fixtures 手算独立真值(勿用被测实现回算): +# - 提案输入含重复行(foo_bar ×2)+未落地 nope_src → distinct 3 员, +# 落地命中 {foo_bar,baz_qux} 2 员 → 转化率 2/3。 +# - 排他率读数 [0.1, 0.25, 0.3, 0.6, 0.9, None] → n=5(Null 跳过), +# mean=(0.1+0.25+0.3+0.6+0.9)/5=0.43;桶 [0,0.25)=1、[0.25,0.5)=2、 +# [0.5,0.75)=1、[0.75,1]=1(0.25 归 ≥gate 桶 = ENQUEUE 门槛同沿)→ +# ge_gate = 2+1+1 = 4。 + +def test_discover_conversion_pure_hand_computed(): + assert discover_conversion( + ["foo_bar", "baz_qux", "foo_bar", "nope_src"], + {"foo_bar", "baz_qux", "other"}) == \ + {"proposed": 3, "landed": 2, "rate": 2 / 3} + + +def test_discover_conversion_no_proposals_is_none_not_fabricated(): + # 无 PROPOSE 提案记录 → None(无话不虚构,≠ 转化率 0%) + assert discover_conversion(None, {"foo_bar"}) is None + assert discover_conversion([], {"foo_bar"}) is None + + +def test_exclusivity_distribution_pure_hand_computed(): + got = exclusivity_distribution([0.1, 0.25, 0.3, 0.6, 0.9, None]) + assert got["n"] == 5 # Null 读数跳过 ≠ 0 + assert got["mean"] == pytest.approx(0.43) + assert got["buckets"] == {"[0,0.25)": 1, "[0.25,0.5)": 2, + "[0.5,0.75)": 1, "[0.75,1]": 1} + assert got["ge_gate"] == 4 # ≥0.25(ENQUEUE 门槛沿):0.25/0.3/0.6/0.9 + + +def test_exclusivity_distribution_empty_is_none(): + # 零 d3-measure 排他率读数 → None(分布 N/A,不虚构全零桶) + assert exclusivity_distribution(None) is None + assert exclusivity_distribution([]) is None + + +def test_record_discover_section_defaults_to_na_sentinels(): + ns = northstar_record(_readout(), [_ANCHOR_CUR], None, + run_id="r1", eval_env="monthly", + today=date(2026, 11, 1)) + assert ns["discover"] == {"conversion": None, "exclusivity": None} + + +def test_record_discover_section_with_readings(): + ns = northstar_record(_readout(), [_ANCHOR_CUR], None, + run_id="r1", eval_env="monthly", + today=date(2026, 11, 1), + proposed=["foo_bar", "baz_qux"], + landed={"foo_bar"}, + exclusivity=[0.3, 0.9]) + assert ns["discover"]["conversion"] == \ + {"proposed": 2, "landed": 1, "rate": 0.5} + assert ns["discover"]["exclusivity"]["n"] == 2 + + +def test_render_discover_line_sentinels_never_fabricate(): + ns = northstar_record(_readout_no_layers(), [], None, + run_id="r1", eval_env="monthly", + today=date(2026, 10, 1)) + lines = render_northstar(ns) + assert len(lines) == 2 # 能力轨行 + 发现轨行 + assert "发现轨北星" in lines[1] and "ADR 0010" in lines[1] + assert "只观测不持门" in lines[1] + assert "入队→落地转化率 N/A(无 PROPOSE 提案记录)" in lines[1] + assert "排他率分布 N/A(零 d3-measure 排他率读数)" in lines[1] + + +def test_render_discover_line_full_values(): + ns = northstar_record(_readout(), [_ANCHOR_CUR], None, + run_id="r1", eval_env="monthly", + today=date(2026, 11, 1), + proposed=["foo_bar", "baz_qux"], + landed={"foo_bar"}, + exclusivity=[0.1, 0.25, 0.3, 0.6, 0.9]) + ln = render_northstar(ns)[1] + assert "入队→落地转化率 1/2(50.0%)" in ln + assert "排他率分布 n=5 mean=0.43" in ln + assert "≥gate(0.25)4/5" in ln + + +# ── load_discover_proposals / load_discover_exclusivity: 工件读面 ── + + +def _mk_queue(ops_dir, rows): + ops_dir.mkdir(parents=True, exist_ok=True) + (ops_dir / "queue.md").write_text( + "# 源生命周期动作队列(三写口:ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼;行由 host 模板渲染,rationale 只落工件)\n\n" + "| 日期 | 轮次 | action | source | predicate | evidence | vetoable |\n" + "|---|---|---|---|---|---|---|\n" + "".join(rows), encoding="utf-8") + + +def test_load_discover_proposals_dedup_and_missing(tmp_path): + ops = tmp_path / "ops" + _mk_queue(ops, [ + "| 2026-10-26 | 2026-10-26-0900-discover | PROPOSE | foo_bar | — " + "| candidate:name=\"Foo Bar\" | 是(否决=删除本行) |\n", + "| 2026-10-26 | 2026-10-26-0900-discover | PROPOSE | baz_qux | — " + "| candidate:name=\"Baz\" | 是(否决=删除本行) |\n", + "| 2026-10-27 | 2026-10-27-0900-discover | PROPOSE | foo_bar | — " + "| candidate:name=\"Foo Bar\" | 是(否决=删除本行) |\n", + "| 2026-10-27 | r-ops | ENQUEUE | legacy_src | sunset_frozen | x | 是 |\n", + ]) + # 判重键 action+source(写面同键)→ foo_bar 一员;ENQUEUE(运营轮动作词)不入 + assert load_discover_proposals(ops) == ["foo_bar", "baz_qux"] + + +def test_load_discover_proposals_missing_or_none(tmp_path): + # 缺件 → None(无提案记录 ≠ 空列表:前者是不出节,后者是零提案) + assert load_discover_proposals(tmp_path / "ops") is None + ops = tmp_path / "ops2" + _mk_queue(ops, [ + "| 2026-10-27 | r-ops | ENQUEUE | legacy_src | sunset_frozen | x | 是 |\n", + ]) + assert load_discover_proposals(ops) is None # 有 queue.md 无 PROPOSE 行 + + +def test_load_discover_exclusivity_aggregates_discover_rounds(tmp_path): + ops = tmp_path / "ops" + d1 = ops / "rounds" / "2026-10-26-0900-discover" + d2 = ops / "rounds" / "2026-10-27-0900-discover-r1" + d3 = ops / "rounds" / "2026-10-27-1000" # 非发现轮:不入 + for d in (d1, d2, d3): + d.mkdir(parents=True) + (d1 / "d3-measure.json").write_text(json.dumps( + {"round_id": "2026-10-26-0900-discover", "results": [ + {"candidate": "foo_bar", "exclusivity_estimate": 0.42}, + {"candidate": "baz_qux", "exclusivity_estimate": None}]}), + encoding="utf-8") + (d2 / "d3-measure.json").write_text(json.dumps( + {"round_id": "2026-10-27-0900-discover-r1", "results": [ + {"candidate": "foo_bar", "exclusivity_estimate": 0.9}]}), + encoding="utf-8") + (d3 / "d3-measure.json").write_text(json.dumps( + {"results": [{"exclusivity_estimate": 0.99}]}), encoding="utf-8") + assert sorted(load_discover_exclusivity(ops)) == pytest.approx([0.42, 0.9]) + + +def test_load_discover_exclusivity_bad_artifact_skipped(tmp_path): + ops = tmp_path / "ops" + d = ops / "rounds" / "2026-10-26-0900-discover" + d.mkdir(parents=True) + (d / "d3-measure.json").write_text("{corrupt", encoding="utf-8") + assert load_discover_exclusivity(ops) == [] # 坏件跳过不虚构 + assert load_discover_exclusivity(tmp_path / "no-such") == [] diff --git a/backend/tests/test_ops_store.py b/backend/tests/test_ops_store.py index 9f832d52..cb5bd632 100644 --- a/backend/tests/test_ops_store.py +++ b/backend/tests/test_ops_store.py @@ -29,9 +29,9 @@ "| 日期 | 轮次 | model | 用量 | 备注 |\n" "|---|---|---|---|---|\n" ) -# T4 tools.mjs queueAction 首建表头(逐字) +# 表头(逐字;首行句 #217 T3.5 翻正三写口——与 _ops_store.QUEUE_HEADER/node QUEUE_HEADER 三抄对拍) QUEUE_HEADER = ( - "# 源生命周期动作队列(queue_action 唯一写口;行由 host 模板渲染,rationale 只落工件)\n\n" + "# 源生命周期动作队列(三写口:ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼;行由 host 模板渲染,rationale 只落工件)\n\n" "| 日期 | 轮次 | action | source | predicate | evidence | vetoable |\n" "|---|---|---|---|---|---|---|\n" ) diff --git a/ops/agent/DESIGN.md b/ops/agent/DESIGN.md index 00f9995f..436e4b25 100644 --- a/ops/agent/DESIGN.md +++ b/ops/agent/DESIGN.md @@ -71,10 +71,10 @@ |---|---| | 围栏观 | 剪的是打断/审批摩擦,不剪边界执法:写口 queue_action 语义门/沙箱断言/防失控界全保留;防线集中后,深围栏(写门/evidence 选择对/SSRF)随注入面扩大**加强测试** | | 工具面 | 原生薄工具四件套:gh_search(镜像装 gh 二进制;GH_TOKEN 经 compose override env 注入不入镜像层,#139 提前收)/ web_read(r.jina.ai,无 key 限速可用)/ **web_search(pi-web-access 0.44.0 MCP 多 provider 回退链,2026-10-24 用户裁定取代 s.jina.ai 直连;缺省路由 duckduckgo keyless,jina key 经 agent-config tool_keys 扩槽入回退链)**/ rss_parse(本地解析);agent-reach 整只**不进镜像**(13 通道×异构故障面违「few, sharp tools」);错误语义对齐 MCP isError 二分:channel_missing/channel_auth/quota/transient 四码,每码带「下一步合法动作」文案 | -| 预算四界 | **【状态:已落地——#199 交付A:pipeline/budget.mjs readBudgetKnobs 单源(六 OPS_PIPELINE_* 旋钮),双点执法(步前准入+环内)+同参哈希拦,G_CLOSE 回声哨对拍 09-budget.json 读数】**迭代上限(补整轮模型调用数帽)/ 轨迹 token 预算 / 墙钟超时 / 花费帽(每轮 ¥3 ∧ 月度 ¥30,同台账并发安全双点执法:准入+环内)+ 重复检测(工具调用哈希);单发线降为告警线;取代 §4 预算卡行闸值面(8h 节奏不变) | +| 预算四界 | **【状态:已落地——#199 交付A:pipeline/budget.mjs readBudgetKnobs 单源(六 OPS_PIPELINE_* 旋钮),双点执法(步前准入+环内)+同参哈希拦,G_CLOSE 回声哨对拍 09-budget.json 读数;发现轮管线化后(ADR 0010,#207 T1)增 OPS_DISCOVER_* 独立旋钮族 30 调用/3600s/¥5,月帽与运营轮共享 ¥30(admitOrRefuse 同面)】**迭代上限(补整轮模型调用数帽)/ 轨迹 token 预算 / 墙钟超时 / 花费帽(每轮 ¥3 ∧ 月度 ¥30,同台账并发安全双点执法:准入+环内)+ 重复检测(工具调用哈希);单发线降为告警线;取代 §4 预算卡行闸值面(8h 节奏不变) | | 熔断器 | 跨轮熔断(连续失败达阈开断 → 快速结构化 tool-unavailable 信封 → 半开探活恢复),状态持久化跨轮;宿主层职责,取代静态轮前探测;判例 01:59 重试白烧的直接解。已落地 #199 交付B(tools/breaker.mjs:五通道闭集/计数码恰三员宁少算/fail-closed 坏文件重置闭路/半开探活/channel_open 信封零 spawn,快照入 09-budget.json) | -| 北极星 | 三联指标:truth 检出率主星 ∧ 误报面∧ 回声面约束;约束破线则检出率提升不算数(防堆回声源刷分,哲学③同构) | -| 实绩回流 | eval 月轮 → 实绩摘要工件 + 逐源待审条目(MC/CG/fp_proxy 实绩 vs 入池排他率预测,偏差大=建议降权/退役只进人裁);目标卡自动草稿+人审定稿注入每轮 prompt;discover 默认对失分轴(truth 层 k=0 清单/缺失 ctype)找源;判例库红线不动(host 永不自动写) | +| 北极星 | 三联指标:truth 检出率主星 ∧ 误报面∧ 回声面约束;约束破线则检出率提升不算数(防堆回声源刷分,哲学③同构);发现侧两腿(入队→落地转化率+排他率分布,ADR 0010 决策⑤)同零持门律落月轮观测件(#210 T4:northstar discover 节+发现轨行) | +| 实绩回流 | eval 月轮 → 实绩摘要工件 + 逐源待审条目(MC/CG/fp_proxy 实绩 vs 入池排他率预测,偏差大=建议降权/退役只进人裁);目标卡自动草稿+人审定稿注入每轮 prompt;discover 默认对失分轴(truth 层 k=0 清单/缺失 ctype)找源——已升格为发现轮管线(六步四门,ADR 0010;ENQUEUE 只写不动作,候选行 PROPOSE,开票/接源全人裁);判例库红线不动(host 永不自动写) | | 对齐节奏 | 注意力预算制(arXiv 2026-08 倒 U:升级率过高反而更不安全):goal card 周更 + eval 月轮 + **积压才推**(active>10 或 >48h 未审 → lark digest);digest 含疲劳遥测(批准量/平均批准耗时/override 率),耗时随量单调降 = 控制表演化警报 | ## 附录 A:运营提示词审阅清单(SDD task review 执审;新/改提示词 PR 必过) diff --git a/ops/agent/STANDARD.md b/ops/agent/STANDARD.md index 99ecbd79..8a1f551c 100644 --- a/ops/agent/STANDARD.md +++ b/ops/agent/STANDARD.md @@ -111,6 +111,12 @@ - 门挂点:ops/agent/tools/envelope.mjs#classifyChannelError;ops/agent/error-codes.mjs - 判例链:FlowGuard 2026-07(语义信号≠证据);EpiSEM-Bench 2026-10(每句都真也可能不安全);判例 2026-10-24-discover-dual-channel-dead;PR #179 wire 漂移哨(非空无标记文本归 io_error 不冒充零命中) +#### D-7 发现与运营同律 +- 主张:发现轮=第六种轮型,与运营轮同宿主/同台账/同工件树(六步 SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE),四门全机检——G_SCAN 候选宇宙形状/G_SHORTLIST span 级逐字回引(URL 幻觉即拒)/G_MEASURE 宿主数字复算/G_ENQUEUE 入队对账;模型零裁决:入队判据(排他率≥0.25+证据存在性+阳性对照)是代码常量不在 prompt,模型无权改;ENQUEUE 只写不动作——候选行经 #139 双边 flock 写 queue.md,开票/接源全人裁,action 词统一 PROPOSE(提案非动作,与运营轮 ENQUEUE/REJECT 人裁闭集分立,一词一拼);北星两腿(入队→落地转化率+排他率分布)同 E-5 只观测不持门 +- 原语组成:状态机骨架;阶段门;三层职责线;工具白名单 +- 门挂点:ops/agent/pipeline.mjs#DISCOVER_PIPELINE;ops/agent/pipeline/gates.mjs#G_SCAN;ops/agent/pipeline/gates.mjs#G_SHORTLIST;ops/agent/pipeline/gates.mjs#G_MEASURE;ops/agent/pipeline/gates.mjs#G_ENQUEUE;ops/agent/pipeline/citation.mjs#spanMatcher;ops/agent/pipeline/discovery-host.mjs#mkDiscoveryHost;ops/agent/pipeline/discover-candidate.mjs#CANDIDATE_ACTION;backend/ipdb/_eval/monthly.py#load_discover_proposals +- 判例链:ADR 0010(发现轮管线化七决策);伞 #206 与 #207-#210;判例 2026-10-26-scanner-ranges-census(排他率口径实测);#210 T4(PROPOSE 统一+北星两腿);spanMatcher 挂点(citation.mjs)已随 #208 合流补挂(T4 移交件①;死锚即伪标准判例存照) + ## 三、门挂点层 挂口族注册表(条文门挂点必须落在族内载体上;挂点路径一律主仓相对,嵌套 docs 仓文件禁作挂点——CI checkout 无 docs/;`#锚词` fragment 须在文件文本内;空挂点/死锚=核对器非零退出): diff --git a/ops/agent/agent.mjs b/ops/agent/agent.mjs index d3c3f59c..5a89e80f 100644 --- a/ops/agent/agent.mjs +++ b/ops/agent/agent.mjs @@ -87,6 +87,12 @@ export const SPAWN_CONTRACT_ENV = [ { name: "OPS_PIPELINE_SPEND_CAP_CNY", default: "3", consumer: "pipeline/budget.mjs(四界④整轮花费帽,结算后查)", phase: "call-time" }, { name: "OPS_PIPELINE_MONTHLY_CAP_CNY", default: "30", consumer: "pipeline/budget.mjs(月度 kill switch 帽,双面准入拒跑 code=budget_monthly)", phase: "call-time" }, { name: "OPS_PIPELINE_DUP_CALL_LIMIT", default: "3", consumer: "pipeline/budget.mjs(同参调用拦截线,双面工具注入拦 code=repeat_storm;0=停用)", phase: "call-time" }, + // #207 T1(发现轮管线骨架):三旋钮独立真读方=pipeline/budget.mjs readDiscoverKnobs(单源读面; + // 月帽不另立行——OPS_PIPELINE_MONTHLY_CAP_CNY 同键共享,admitOrRefuse 同一函数); + // dup 面沿 OPS_PIPELINE_DUP_CALL_LIMIT(T3 #209 工具注入接线时消费)。 + { name: "OPS_DISCOVER_MAX_MODEL_CALLS", default: "30", consumer: "pipeline/budget.mjs readDiscoverKnobs(发现轮①整轮模型调用数帽,runModelStage attempt 前查)", phase: "call-time" }, + { name: "OPS_DISCOVER_WALL_CLOCK_S", default: "3600", consumer: "pipeline/budget.mjs readDiscoverKnobs(发现轮③整轮墙钟秒,步边界+逐候选前查)", phase: "call-time" }, + { name: "OPS_DISCOVER_SPEND_CAP_CNY", default: "5", consumer: "pipeline/budget.mjs readDiscoverKnobs(发现轮④整轮花费帽,结算后查)", phase: "call-time" }, // #199 交付B(P16 跨轮熔断):tools/breaker.mjs 旋钮(构造期读=轮次调用期); // state 面=DOCS_ROOT/ops/breaker.json 由 DOCS_ROOT 行派生,不另立行。 { name: "OPS_BREAKER_THRESHOLD", default: "3", consumer: "tools/breaker.mjs(通道连续计数码失败开断阈;计数码闭集=channel_missing/channel_auth/quota;0=停用),拦 code=channel_open", phase: "call-time" }, diff --git a/ops/agent/artifacts.mjs b/ops/agent/artifacts.mjs index 84786194..cc735f4c 100644 --- a/ops/agent/artifacts.mjs +++ b/ops/agent/artifacts.mjs @@ -1,4 +1,4 @@ -// 工件名表单源(#202 C1 / spec C1):健康轮轮目录工件名的唯一真源。 +// 工件名表单源(#202 C1 / spec C1;#207 T1 扩双管线):轮目录工件名的唯一真源(健康轮+发现轮)。 // 为什么:gates.mjs(inputs 声明/读面)、steps.mjs(写出面)、selftest-pipeline.mjs(清单面)三层 // 曾各持手抄字符串(同名字符串 ~70 处),漂移即静默错位;单源后漂移由闭包哨 // (selftest「工件名表单源」测)双向拦截:名表加项不更新写出面=红;步面新写工件不入表=红。 @@ -26,11 +26,29 @@ export const ARTIFACTS = Object.freeze({ BUDGET: "09-budget.json", GATES: "gates.json", STATE: "state.json", + // 发现轮四步工件(#207 T1;spec /tmp/discover-pipeline-spec.md T1 节): + // d1 候选宇宙快照/ d2 模型结构化候选/ d3 确定性测量/ d4 入队留痕。 + // d2-shortlist-transcript.jsonl 是步侧产物不入名表(faux.mjs transcriptName 同 health TRIAGE_TRANSCRIPT 面:名表=轮目录固定业务面,transcript 由步面管)。 + DISCOVER_SCAN: "d1-scan.json", + DISCOVER_SHORTLIST: "d2-shortlist.json", + DISCOVER_MEASURE: "d3-measure.json", + DISCOVER_ENQUEUE: "d4-enqueue.json", }); -// 名表闭包面(清单遍历/闭包哨差集都用它;禁止 Object.keys 当名字用)。 +// 名表闭包面(清单遍历/闭包哨差集都用它;禁止 Object.keys 当名字用)。全量 22 名。 export const ARTIFACT_NAMES = Object.freeze(Object.values(ARTIFACTS)); +// 轮型固定面派生(#207 T1):闭包哨/门对拍按轮型取面,不拿全量 22 名错拍他轮目录。 +const DISCOVER_FIXED = ["DISCOVER_SCAN", "DISCOVER_SHORTLIST", "DISCOVER_MEASURE", "DISCOVER_ENQUEUE"]; +export const HEALTH_ARTIFACT_NAMES = Object.freeze(ARTIFACT_NAMES.filter((n) => !DISCOVER_FIXED.some((k) => ARTIFACTS[k] === n))); // 健康轮 18 固定名(位等价:与 #202 C1 表逐字节同) +export const discoverArtifactNames = () => DISCOVER_FIXED.map((k) => ARTIFACTS[k]).concat([ARTIFACTS.BUDGET, ARTIFACTS.GATES, ARTIFACTS.STATE]); // 发现轮 7 固定名(d1-d4+09/gates/state) + +// 发现轮 SHORTLIST transcript 名(#217 T3.5):real/faux 同名;不入 ARTIFACTS 固定表(表=健康轮 18 名 +// 位等价冻结面)也不入 discoverArtifactNames(闭包哨测试以额外名单独钉)。单源理由:faux.mjs +// transcriptName(写出面)与 discovery-host.mjs replayDiscoverSeeds(CLOSE 回放读面)共用, +// 两处手抄=回放读不到留痕。字节冻结(磁盘名位等价判例同固定表)。 +export const DISCOVER_SHORTLIST_TRANSCRIPT = "d2-shortlist-transcript.jsonl"; + // 逐假设 DEEP 参数族:名字由假设 slug 拼出(slug=hidSlug(url)),不入固定表。 export const deepArtifactName = (slug) => `05-deep-${slug}.json`; export const deepTranscriptName = (slug) => `05-deep-${slug}-transcript.jsonl`; diff --git a/ops/agent/fixtures/pipeline/README.md b/ops/agent/fixtures/pipeline/README.md index cb0fafea..a20705ff 100644 --- a/ops/agent/fixtures/pipeline/README.md +++ b/ops/agent/fixtures/pipeline/README.md @@ -49,6 +49,7 @@ tools.mjs `matrixScanCli` 真扫描(OPS_EVAL_PY stub)。ANCHOR_SWEEP(#182)同样 | `deep/https-www.binarydefense.com-banlist.txt.json` | DEEP | refuted;seed c-bd reprobe_ua age_days=5 | | `deep/golden-track-canned.json` | DEEP 金轨迹 | unresolved 直通(零 need_deep 轮保活,#162 审 P2-2 裁定①)| | `synth.json` | SYNTH | 三节 canned 报告(计数行 + 深验行 `confirmed=1 refuted=1 unresolved=0`)| +| `shortlist.json` | SHORTLIST(发现轮,#207 T1) | 2 结构化候选(NEEDS.md:L14 rDNS parquet 候选/L17 carrier open;exclusivity=null→ENQUEUE 恒 skip,罐装轮零入队;prompts/discover/shortlist.md) | P3 桩件 `deep/_stub.json` 已随真开启退役(#166);`review_freeze` 动作词随 confirmed→human_ruling 裁决退役(P4:人裁终局,建议面恒交人)。 diff --git a/ops/agent/fixtures/pipeline/d1-scan.json b/ops/agent/fixtures/pipeline/d1-scan.json new file mode 100644 index 00000000..d4d2eadc --- /dev/null +++ b/ops/agent/fixtures/pipeline/d1-scan.json @@ -0,0 +1,13 @@ +{ + "_comment": "#208 T2:发现轮 faux 面 SCAN 宇宙罐装(steps.mjs SCAN faux 缝消费;缺省真面不受影响)。needs 条目带 text=原文行快照(span 门对账面入轮工件——证据永不灭失);registry 条目结构性(无 text,canonicalJson 面)。text 面须与 shortlist.json 罐装 quote 构成逐字 span(改 quote 或 text 即 span 门红——篡改哨自检)。", + "candidates": [ + { "name": "feodo_tracker", "url": null, "evidence_ptr": "backend/ipdb/_sources/feodo_tracker.py", "source_channel": "source_registry", "status": "integrated" }, + { "name": "rDNS 主机名", "url": "https://ip.thc.org", "evidence_ptr": "NEEDS.md:L14", "source_channel": "needs", "status": "候选", "text": "| rDNS 主机名 | 反查 IP→域名,现链路零覆盖 | 候选 | ip.thc.org 月度全量 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁) 2026-10-24 记 |" }, + { "name": "carrier / is_isp 出中国段", "url": null, "evidence_ptr": "NEEDS.md:L17", "source_channel": "needs", "status": "open", "text": "| carrier / is_isp 出中国段 | ASN/运营商属性标注 | open | IPinfo carrier 批量库 1.18MB CSV 存在但 standard 付费 token 档,无免费路径 |" } + ], + "channels": { + "registry": { "status": "ok", "n": 1 }, + "needs": { "status": "ok", "n": 2 }, + "web": { "status": "placeholder", "n": 0, "note": "T3 #209 接线(PR3a web_search 渠道;T1 只留占位留痕)" } + } +} diff --git a/ops/agent/fixtures/pipeline/measure.json b/ops/agent/fixtures/pipeline/measure.json new file mode 100644 index 00000000..9913fc2e --- /dev/null +++ b/ops/agent/fixtures/pipeline/measure.json @@ -0,0 +1,11 @@ +{ + "_comment": "#217 T3.5:faux 面 MEASURE 罐装读数表(web_read 探测面;零网络律)。web_reads 按 url 罐装:ok=true 行=罐装成功读数;ok=false 行=罐装失败面(死链/鉴权模拟);未罐装 url=io_error 未罐装信封(诚实面,不虚构)。本罐只装 d2 shortlist.json 出现的 rDNS 候选 URL,供全轮例走通 SHORTLIST→MEASURE→PROPOSE 正路。", + "web_reads": [ + { + "url": "https://ip.thc.org", + "ok": true, + "title": "ip.thc.org rDNS 数据页(faux 罐装)", + "content": "ip.thc.org rDNS 月度全量 parquet 发布页(faux 罐装正文,零网络):每月全量反向解析主机名数据,49.5GB parquet,HF+镜像双源实测活;商用条款模糊+小机 1B 行重建未裁(NEEDS.md:L14 同文)。" + } + ] +} diff --git a/ops/agent/fixtures/pipeline/shortlist.json b/ops/agent/fixtures/pipeline/shortlist.json new file mode 100644 index 00000000..b6495e26 --- /dev/null +++ b/ops/agent/fixtures/pipeline/shortlist.json @@ -0,0 +1,14 @@ +{ + "turns": [ + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "{\"candidates\":[{\"name\":\"rDNS 主机名\",\"url\":\"https://ip.thc.org\",\"quote\":\"ip.thc.org 月度全量 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁)\",\"evidence_ptr\":\"NEEDS.md:L14\",\"source_channel\":\"needs\",\"exclusivity_estimate\":0.42,\"notes\":\"#217 T3.5 罐装:模型估值 0.42(≥门槛 0.25);URL 探测/阳性对照走 measure.json 罐装 web_read\"},{\"name\":\"carrier / is_isp 出中国段\",\"url\":null,\"quote\":\"IPinfo carrier 批量库 1.18MB CSV 存在但 standard 付费 token 档,无免费路径\",\"evidence_ptr\":\"NEEDS.md:L17\",\"source_channel\":\"needs\",\"exclusivity_estimate\":null,\"notes\":\"#217 T3.5 罐装:无 URL+排他率 null=双面不可算,钉 uncertainty hold 面\"}]}" + } + ], + "stopReason": "stop" + } + ] +} diff --git a/ops/agent/golden/cases.json b/ops/agent/golden/cases.json index e4dfc0b1..85617eea 100644 --- a/ops/agent/golden/cases.json +++ b/ops/agent/golden/cases.json @@ -29,244 +29,794 @@ "id": "health_summary_format", "tier": "faux", "round": "health", - "tags": ["positive", "boundary-near-band-179-180", "boundary-token-required"], + "tags": [ + "positive", + "boundary-near-band-179-180", + "boundary-token-required" + ], "desc": "正例:健康快照摘要格式(输出契约三节)+ near_band 179/180 边界 + P1 T2 TOKEN_REQUIRED 分桶——binarydefense 179 恰在近带(90-179),api.abuseipdb.com 403 → 需令牌·正常不进 unreachable,其余全源恰 180 过冻结门线,由真实 health_snapshot 分桶消费 stub 读数。", "input": "健康轮金轨迹回放:全源 feed 冻结检测 + 语义矩阵扫描。", "stubs": { "curl_age_days": [ - { "match": "api.abuseipdb.com", "status": 403 }, - { "match": "binarydefense", "days": 179 }, - { "match": "*", "days": 180 } + { + "match": "api.abuseipdb.com", + "status": 403 + }, + { + "match": "binarydefense", + "days": 179 + }, + { + "match": "*", + "days": 180 + } ], "matrix": { "exit_code": 0, - "counts": { "violation": 0, "pending": 0, "agreement": 4 }, + "counts": { + "violation": 0, + "pending": 0, + "agreement": 4 + }, "layers": { - "truth": { "status": "OK", "members": 4, "sampled": 4, "notes": [] }, - "infra": { "status": "OK", "members": 8, "sampled": 8, "notes": [] } + "truth": { + "status": "OK", + "members": 4, + "sampled": 4, + "notes": [] + }, + "infra": { + "status": "OK", + "members": 8, + "sampled": 8, + "notes": [] + } }, - "buckets": { "violation": [], "pending": [], "agreement": [] } + "buckets": { + "violation": [], + "pending": [], + "agreement": [] + } } }, "registrySeed": [], "turns": [ - { "toolCall": { "name": "health_snapshot", "id": "call_hs1", "args": {} } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 异常清单\n- 死键嫌疑·建议复核:全源 age_days=180(恰过 180 门线),逐 URL 读数见工件 TSV(Last-Modified 与 age_days 实测)。\n- 近带(90-179):https://www.binarydefense.com/banlist.txt(179 天,未过冻结门线,建议观察)。\n- 需令牌(401/403):https://api.abuseipdb.com/api/v2/blacklist(需令牌·正常,可达但匿名无凭据;不算异常不推动作)。\n- 不可达:无;NO-LM:无。\n- 矩阵:exit_code=0(干净),violation=0 pending=0 agreement=4。\n\n## 摘要\n金轨迹回放:health_snapshot 结构化返回 frozen/near_band/unreachable/no_lm/token_required 计数与矩阵读数;179/180 边界两侧同时钉住——179 在近带内不标死键,180 恰过门线入冻结;403 需令牌归 TOKEN_REQUIRED 不入不可达。无自动动作,仅建议复核留人裁。" } + { + "toolCall": { + "name": "health_snapshot", + "id": "call_hs1", + "args": {} + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 异常清单\n- 死键嫌疑·建议复核:全源 age_days=180(恰过 180 门线),逐 URL 读数见工件 TSV(Last-Modified 与 age_days 实测)。\n- 近带(90-179):https://www.binarydefense.com/banlist.txt(179 天,未过冻结门线,建议观察)。\n- 需令牌(401/403):https://api.abuseipdb.com/api/v2/blacklist(需令牌·正常,可达但匿名无凭据;不算异常不推动作)。\n- 不可达:无;NO-LM:无。\n- 矩阵:exit_code=0(干净),violation=0 pending=0 agreement=4。\n\n## 摘要\n金轨迹回放:health_snapshot 结构化返回 frozen/near_band/unreachable/no_lm/token_required 计数与矩阵读数;179/180 边界两侧同时钉住——179 在近带内不标死键,180 恰过门线入冻结;403 需令牌归 TOKEN_REQUIRED 不入不可达。无自动动作,仅建议复核留人裁。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["health_snapshot"] }, - { "kind": "registry", "id": "call_hs1", "path": "tool", "equals": "health_snapshot" }, - { "kind": "registry", "id": "call_hs1", "path": "sc.ok", "equals": true }, - { "kind": "registry", "id": "call_hs1", "path": "sc.urls_n", "at_least": 50 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.near_band_n", "equals": 1 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.near_band.0.age_days", "equals": 179 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.near_band.0.url", "matches": "binarydefense" }, - { "kind": "registry", "id": "call_hs1", "path": "sc.frozen_n", "equals_field_minus": ["sc.urls_n", 2] }, - { "kind": "registry", "id": "call_hs1", "path": "sc.unreachable_n", "equals": 0 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.no_lm_n", "equals": 0 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.token_required_n", "equals": 1 }, - { "kind": "registry", "id": "call_hs1", "path": "sc.token_required.0", "matches": "api\\.abuseipdb\\.com" }, - { "kind": "registry", "id": "call_hs1", "path": "sc.matrix_ok", "equals": true }, - { "kind": "registry", "id": "call_hs1", "path": "sc.matrix_exit_code", "equals": 0 }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "ledger", "min_rows": 1, "last_matches": "\\| health \\| faux-golden \\|" }, - { "kind": "package", "exists": true, "matches": [ - "## 注入判例", "## 异常清单", "## 摘要", - "- round: health / model: faux-golden", - "- tool calls: health_snapshot×1", - "零内建工具证明", - "- 队列动作: 无", - "2026-06-01-golden-aggregator.md" - ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "health_snapshot" + ] + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "tool", + "equals": "health_snapshot" + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.ok", + "equals": true + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.urls_n", + "at_least": 50 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.near_band_n", + "equals": 1 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.near_band.0.age_days", + "equals": 179 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.near_band.0.url", + "matches": "binarydefense" + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.frozen_n", + "equals_field_minus": [ + "sc.urls_n", + 2 + ] + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.unreachable_n", + "equals": 0 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.no_lm_n", + "equals": 0 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.token_required_n", + "equals": 1 + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.token_required.0", + "matches": "api\\.abuseipdb\\.com" + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.matrix_ok", + "equals": true + }, + { + "kind": "registry", + "id": "call_hs1", + "path": "sc.matrix_exit_code", + "equals": 0 + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| health \\| faux-golden \\|" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "## 注入判例", + "## 异常清单", + "## 摘要", + "- round: health / model: faux-golden", + "- tool calls: health_snapshot×1", + "零内建工具证明", + "- 队列动作: 无", + "2026-06-01-golden-aggregator.md" + ] + } ] }, { "id": "discover_predicate_hit", "tier": "faux", "round": "discover", - "tags": ["positive"], + "tags": [ + "positive" + ], "desc": "正例:discover 谓词命中路径——真实 gh_search(gh CLI 替身)产出 size_kb 实测 → 模型入队 REJECT zero_asset → 写前语义门从登记表重算放行 → 队列行落盘 + 恒可否决。", "input": "discover 金轨迹回放:候选 cand_zerohub(fixture/empty-hub)评估。", "stubs": { "gh": { "repos": [ - { "fullName": "fixture/empty-hub", "description": "fixture zero-asset repo", "pushedAt": "2026-05-01T00:00:00Z", "updatedAt": "2026-05-01T00:00:00Z", "stargazersCount": 3, "language": "Shell", "isArchived": false, "size": 8, "url": "https://github.com/fixture/empty-hub" } + { + "fullName": "fixture/empty-hub", + "description": "fixture zero-asset repo", + "pushedAt": "2026-05-01T00:00:00Z", + "updatedAt": "2026-05-01T00:00:00Z", + "stargazersCount": 3, + "language": "Shell", + "isArchived": false, + "size": 8, + "url": "https://github.com/fixture/empty-hub" + } ] } }, "registrySeed": [], "turns": [ - { "toolCall": { "name": "gh_search", "id": "call_gh1", "args": { "query": "fixture empty-hub", "kind": "repos", "limit": 3 } } }, - { "toolCall": { "name": "queue_action", "id": "call_q1", "args": { "source": "cand_zerohub", "action": "REJECT", "predicate": "zero_asset", "rationale": "金轨迹:仓零数据资产,size_kb 全部≤32。", "evidence": [ { "call_id": "call_gh1", "field": "results.0.size_kb", "value": 8 } ] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_zerohub:GitHub 仓 fixture/empty-hub;来源=call_gh1 gh_search 实测。\n\n## 硬门数字\n- call_gh1:results.0.size_kb=8(仓体积,全结果 ≤32KB)\n\n## 建议\n- REJECT cand_zerohub(zero_asset):仓零数据资产,本会话 gh_search 实测支持。\n\n## 自动执行区\n- REJECT cand_zerohub 已入队(call_q1,恒可否决)。\n\n## 人裁区\n- 无。\n\n## 摘要\n金轨迹回放:zero_asset 谓词命中路径——gh_search 实测 → 语义门重算放行 → 入队;rationale 只落工件不进行列。" } + { + "toolCall": { + "name": "gh_search", + "id": "call_gh1", + "args": { + "query": "fixture empty-hub", + "kind": "repos", + "limit": 3 + } + } + }, + { + "toolCall": { + "name": "queue_action", + "id": "call_q1", + "args": { + "source": "cand_zerohub", + "action": "REJECT", + "predicate": "zero_asset", + "rationale": "金轨迹:仓零数据资产,size_kb 全部≤32。", + "evidence": [ + { + "call_id": "call_gh1", + "field": "results.0.size_kb", + "value": 8 + } + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_zerohub:GitHub 仓 fixture/empty-hub;来源=call_gh1 gh_search 实测。\n\n## 硬门数字\n- call_gh1:results.0.size_kb=8(仓体积,全结果 ≤32KB)\n\n## 建议\n- REJECT cand_zerohub(zero_asset):仓零数据资产,本会话 gh_search 实测支持。\n\n## 自动执行区\n- REJECT cand_zerohub 已入队(call_q1,恒可否决)。\n\n## 人裁区\n- 无。\n\n## 摘要\n金轨迹回放:zero_asset 谓词命中路径——gh_search 实测 → 语义门重算放行 → 入队;rationale 只落工件不进行列。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["gh_search", "queue_action"] }, - { "kind": "registry", "id": "call_gh1", "path": "tool", "equals": "gh_search" }, - { "kind": "registry", "id": "call_gh1", "path": "sc.ok", "equals": true }, - { "kind": "registry", "id": "call_gh1", "path": "sc.kind", "equals": "repos" }, - { "kind": "registry", "id": "call_gh1", "path": "sc.results.0.size_kb", "equals": 8 }, - { "kind": "queue_writes", "count": 1, "actions": ["REJECT"] }, - { "kind": "queue_file", "state": "row", "pattern": "\\| REJECT \\| cand_zerohub \\| zero_asset \\|" }, - { "kind": "queue_file", "state": "row", "pattern": "call_gh1:results.0.size_kb=8" }, - { "kind": "ledger", "min_rows": 1, "last_matches": "\\| discover \\| faux-golden \\|" }, - { "kind": "package", "exists": true, "matches": [ - "- round: discover / model: faux-golden", - "- tool calls: gh_search×1 queue_action×1", - "- 队列动作: REJECT:cand_zerohub", - "2026-06-01-golden-aggregator.md" - ], "not_matches": [ "金轨迹:仓零数据资产" ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "gh_search", + "queue_action" + ] + }, + { + "kind": "registry", + "id": "call_gh1", + "path": "tool", + "equals": "gh_search" + }, + { + "kind": "registry", + "id": "call_gh1", + "path": "sc.ok", + "equals": true + }, + { + "kind": "registry", + "id": "call_gh1", + "path": "sc.kind", + "equals": "repos" + }, + { + "kind": "registry", + "id": "call_gh1", + "path": "sc.results.0.size_kb", + "equals": 8 + }, + { + "kind": "queue_writes", + "count": 1, + "actions": [ + "REJECT" + ] + }, + { + "kind": "queue_file", + "state": "row", + "pattern": "\\| REJECT \\| cand_zerohub \\| zero_asset \\|" + }, + { + "kind": "queue_file", + "state": "row", + "pattern": "call_gh1:results.0.size_kb=8" + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| discover \\| faux-golden \\|" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- round: discover / model: faux-golden", + "- tool calls: gh_search×1 queue_action×1", + "- 队列动作: REJECT:cand_zerohub", + "2026-06-01-golden-aggregator.md" + ], + "not_matches": [ + "金轨迹:仓零数据资产" + ] + } ] }, { "id": "discover_midround_search_citation", "tier": "faux", "round": "discover", - "tags": ["positive", "midround-search-citation"], + "tags": [ + "positive", + "midround-search-citation" + ], "desc": "正例③(评审修正①钉子):轮中 search_precedents 命中引用放行——终稿引用非预注入、但经本会话检索命中的判例(B),收口闭集活时并集必须接纳(旧时序缺陷下被误杀);neg3 为拒侧反面。", "input": "discover 金轨迹回放:候选 cand_m(轮中检索补证)。", "stubs": {}, "registrySeed": [], "turns": [ - { "toolCall": { "name": "search_precedents", "id": "call_sp1", "args": { "keywords": ["NO-LAYER", "读数约定"] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n- 2026-06-02-golden-matrix-nolayer.md(call_sp1 轮中检索命中)\n\n## 候选表\n- cand_m:语义矩阵层读数疑点,轮中检索判例库补证(检索词 NO-LAYER/读数约定)。\n\n## 硬门数字\n- 无测量需求(本例只钉闭集活时并集)。\n\n## 建议\n- 人裁 cand_m。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_m:层读数约定引 call_sp1 命中判例,留人裁。\n\n## 摘要\n金轨迹回放:轮中 search_precedents 命中(非预注入)的判例引用,收口闭集=预注入∪本会话命中(活时并集)必须放行;neg3 为拒侧反面。" } + { + "toolCall": { + "name": "search_precedents", + "id": "call_sp1", + "args": { + "keywords": [ + "NO-LAYER", + "读数约定" + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n- 2026-06-02-golden-matrix-nolayer.md(call_sp1 轮中检索命中)\n\n## 候选表\n- cand_m:语义矩阵层读数疑点,轮中检索判例库补证(检索词 NO-LAYER/读数约定)。\n\n## 硬门数字\n- 无测量需求(本例只钉闭集活时并集)。\n\n## 建议\n- 人裁 cand_m。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_m:层读数约定引 call_sp1 命中判例,留人裁。\n\n## 摘要\n金轨迹回放:轮中 search_precedents 命中(非预注入)的判例引用,收口闭集=预注入∪本会话命中(活时并集)必须放行;neg3 为拒侧反面。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["search_precedents"] }, - { "kind": "registry", "id": "call_sp1", "path": "tool", "equals": "search_precedents" }, - { "kind": "registry", "id": "call_sp1", "path": "sc.ok", "equals": true }, - { "kind": "registry", "id": "call_sp1", "path": "sc.hit_n", "equals": 1 }, - { "kind": "registry", "id": "call_sp1", "path": "sc.hits.0", "equals": "2026-06-02-golden-matrix-nolayer.md" }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "ledger", "min_rows": 1, "last_matches": "\\| discover \\| faux-golden \\|" }, - { "kind": "package", "exists": true, "matches": [ - "- tool calls: search_precedents×1", - "- 队列动作: 无", - "2026-06-02-golden-matrix-nolayer.md" - ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "search_precedents" + ] + }, + { + "kind": "registry", + "id": "call_sp1", + "path": "tool", + "equals": "search_precedents" + }, + { + "kind": "registry", + "id": "call_sp1", + "path": "sc.ok", + "equals": true + }, + { + "kind": "registry", + "id": "call_sp1", + "path": "sc.hit_n", + "equals": 1 + }, + { + "kind": "registry", + "id": "call_sp1", + "path": "sc.hits.0", + "equals": "2026-06-02-golden-matrix-nolayer.md" + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| discover \\| faux-golden \\|" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- tool calls: search_precedents×1", + "- 队列动作: 无", + "2026-06-02-golden-matrix-nolayer.md" + ] + } ] }, { "id": "neg1_enqueue_without_measurement", "tier": "faux", "round": "discover", - "tags": ["negative-1-no-measurement"], + "tags": [ + "negative-1-no-measurement" + ], "desc": "负例①:无实测 ENQUEUE 被门挡——evidence 引用编造 call_id,写前语义门拒执行(错误工具结果回灌模型),轮次照常收口、零写队列。", "input": "discover 金轨迹回放:候选 cand_a 评估(无任何测量)。", "stubs": {}, "registrySeed": [], "turns": [ - { "toolCall": { "name": "queue_action", "id": "call_q1", "args": { "source": "cand_a", "action": "ENQUEUE", "rationale": "金轨迹:无实测直接入队。", "evidence": [ { "call_id": "call_ghost", "field": "exclusive_rate", "value": 0.587 } ] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_a:无本会话测量数据。\n\n## 硬门数字\n- 待实测(ENQUEUE 尝试被写前语义门拦截)。\n\n## 建议\n- FLAG cand_a:入人裁区。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_a:无实测 ENQUEUE 被门挡(证据引用未知调用 call_ghost),升级人裁。\n\n## 摘要\n金轨迹回放:无实测 ENQUEUE 被写前语义门拦截,零写队列。" } + { + "toolCall": { + "name": "queue_action", + "id": "call_q1", + "args": { + "source": "cand_a", + "action": "ENQUEUE", + "rationale": "金轨迹:无实测直接入队。", + "evidence": [ + { + "call_id": "call_ghost", + "field": "exclusive_rate", + "value": 0.587 + } + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_a:无本会话测量数据。\n\n## 硬门数字\n- 待实测(ENQUEUE 尝试被写前语义门拦截)。\n\n## 建议\n- FLAG cand_a:入人裁区。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_a:无实测 ENQUEUE 被门挡(证据引用未知调用 call_ghost),升级人裁。\n\n## 摘要\n金轨迹回放:无实测 ENQUEUE 被写前语义门拦截,零写队列。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["queue_action"] }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "ledger", "min_rows": 1 }, - { "kind": "package", "exists": true, "matches": [ - "- tool calls: queue_action×1", - "- 队列动作: 无" - ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "queue_action" + ] + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "ledger", + "min_rows": 1 + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- tool calls: queue_action×1", + "- 队列动作: 无" + ] + } ] }, { "id": "neg2_failed_envelope_as_evidence", "tier": "faux", "round": "discover", - "tags": ["negative-2-failed-envelope"], + "tags": [ + "negative-2-failed-envelope" + ], "desc": "负例②:失败信封入 evidence 被拒——真实 fetch_url 探测环回字面地址产出真实 SSRF 失败信封(ok:false),以其入 evidence 自动入队,门拒;降级人裁、零写队列。", "input": "discover 金轨迹回放:候选 cand_a 探测(环回地址)。", "stubs": {}, "registrySeed": [], "turns": [ - { "toolCall": { "name": "fetch_url", "id": "call_ff1", "args": { "url": "http://127.0.0.1:9/x", "method": "HEAD" } } }, - { "toolCall": { "name": "queue_action", "id": "call_q1", "args": { "source": "cand_a", "action": "ENQUEUE", "rationale": "金轨迹:以失败信封字段充当实测。", "evidence": [ { "call_id": "call_ff1", "field": "code", "value": "ssrf" } ] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_a:fetch_url 探测环回字面地址,实测失败信封。\n\n## 硬门数字\n- call_ff1:code=ssrf(失败信封,非读数)。\n\n## 建议\n- 人裁 cand_a:探测失败信封不得转述为读数(无证据≠清白)。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_a:失败信封作 evidence 被门拒,降级人裁。\n\n## 摘要\n金轨迹回放:失败信封入 evidence 被写前语义门拒绝。" } + { + "toolCall": { + "name": "fetch_url", + "id": "call_ff1", + "args": { + "url": "http://127.0.0.1:9/x", + "method": "HEAD" + } + } + }, + { + "toolCall": { + "name": "queue_action", + "id": "call_q1", + "args": { + "source": "cand_a", + "action": "ENQUEUE", + "rationale": "金轨迹:以失败信封字段充当实测。", + "evidence": [ + { + "call_id": "call_ff1", + "field": "code", + "value": "ssrf" + } + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_a:fetch_url 探测环回字面地址,实测失败信封。\n\n## 硬门数字\n- call_ff1:code=ssrf(失败信封,非读数)。\n\n## 建议\n- 人裁 cand_a:探测失败信封不得转述为读数(无证据≠清白)。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_a:失败信封作 evidence 被门拒,降级人裁。\n\n## 摘要\n金轨迹回放:失败信封入 evidence 被写前语义门拒绝。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["fetch_url", "queue_action"] }, - { "kind": "registry", "id": "call_ff1", "path": "tool", "equals": "fetch_url" }, - { "kind": "registry", "id": "call_ff1", "path": "sc.ok", "equals": false }, - { "kind": "registry", "id": "call_ff1", "path": "sc.code", "equals": "ssrf" }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "package", "exists": true, "matches": [ "- 队列动作: 无" ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "fetch_url", + "queue_action" + ] + }, + { + "kind": "registry", + "id": "call_ff1", + "path": "tool", + "equals": "fetch_url" + }, + { + "kind": "registry", + "id": "call_ff1", + "path": "sc.ok", + "equals": false + }, + { + "kind": "registry", + "id": "call_ff1", + "path": "sc.code", + "equals": "ssrf" + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- 队列动作: 无" + ] + } ] }, { "id": "neg3_fabricated_precedent_citation", "tier": "faux", "round": "discover", - "tags": ["negative-3-fabricated-citation"], + "tags": [ + "negative-3-fabricated-citation" + ], "desc": "负例③:编造判例引用被闭合校验拦——终稿引用真实存在于库但不在(预注入∪检索命中)闭集的判例文件:存在性校验(lib.sh)通过,闭集成员校验(tools.mjs)抛错,轮次失败;台账已先记账(九裁决 9:校验失败的轮次也花真钱)。", "input": "discover 金轨迹回放:候选 cand_c(引用未注入判例)。", "stubs": {}, "registrySeed": [], "turns": [ - { "text": "## 注入判例\n- 2026-06-02-golden-matrix-nolayer.md\n\n## 候选表\n- cand_c:无测量(本例只测闭合校验)。\n\n## 硬门数字\n- 待实测。\n\n## 建议\n- 人裁 cand_c。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_c:金轨迹负例③载体。\n\n## 摘要\n金轨迹回放:引用库内存在但未注入的判例文件,闭合校验(闭集成员,非存在性)必须拦下,轮次失败。" } + { + "text": "## 注入判例\n- 2026-06-02-golden-matrix-nolayer.md\n\n## 候选表\n- cand_c:无测量(本例只测闭合校验)。\n\n## 硬门数字\n- 待实测。\n\n## 建议\n- 人裁 cand_c。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_c:金轨迹负例③载体。\n\n## 摘要\n金轨迹回放:引用库内存在但未注入的判例文件,闭合校验(闭集成员,非存在性)必须拦下,轮次失败。" + } ], "assertions": [ - { "kind": "round_ok", "expected": false }, - { "kind": "error_matches", "pattern": "闭合校验.*不在.*闭集.*golden-matrix-nolayer" }, - { "kind": "ledger", "min_rows": 1, "last_matches": "\\| discover \\| faux-golden \\|" }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "package", "exists": true, "matches": [ "2026-06-02-golden-matrix-nolayer.md" ] } + { + "kind": "round_ok", + "expected": false + }, + { + "kind": "error_matches", + "pattern": "闭合校验.*不在.*闭集.*golden-matrix-nolayer" + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| discover \\| faux-golden \\|" + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "2026-06-02-golden-matrix-nolayer.md" + ] + } ] }, { "id": "boundary_enqueue_rate_24_9", "tier": "faux", "round": "discover", - "tags": ["boundary-exclusive-24.9"], + "tags": [ + "boundary-exclusive-24.9" + ], "desc": "边界:排他率 24.9% 恰低于 0.25 门线——exclusive_probe 冻结契约(registrySeed,门从登记表真算)+ 值逐字绑定,ENQUEUE 谓词不成立被挡,零写队列。", "input": "discover 金轨迹回放:候选 cand_lo(排他 24.9%)。", "stubs": {}, "registrySeed": [ - { "id": "call_probe_lo", "tool": "exclusive_probe", "sc": { "ok": true, "url": "https://fixture.example/feed.txt", "seed": 20261018, "exclusive_rate": 0.249, "exclusive": 74, "sampled": 300, "feed_unique_keys": 120510, "control_pass": true, "covering_sources": [ { "source": "binarydefense", "n": 20 } ], "artifact": "(fixture-seed)", "trace": { "cmd": "(frozen contract)", "rc": 0 } } } + { + "id": "call_probe_lo", + "tool": "exclusive_probe", + "sc": { + "ok": true, + "url": "https://fixture.example/feed.txt", + "seed": 20261018, + "exclusive_rate": 0.249, + "exclusive": 74, + "sampled": 300, + "feed_unique_keys": 120510, + "control_pass": true, + "covering_sources": [ + { + "source": "binarydefense", + "n": 20 + } + ], + "artifact": "(fixture-seed)", + "trace": { + "cmd": "(frozen contract)", + "rc": 0 + } + } + } ], "turns": [ - { "toolCall": { "name": "queue_action", "id": "call_q1", "args": { "source": "cand_lo", "action": "ENQUEUE", "rationale": "金轨迹:24.9% 逐字复述。", "evidence": [ { "call_id": "call_probe_lo", "field": "exclusive_rate", "value": 0.249 } ] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_lo:探针实测 74/300=24.9%(对照闸过)。\n\n## 硬门数字\n- call_probe_lo:exclusive_rate=0.249(exclusive=74 sampled=300)\n\n## 建议\n- FLAG cand_lo:24.9% 恰低于 0.25 门线,不自动入队,边界升级人裁。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_lo:排他率边界值,人裁。\n\n## 摘要\n金轨迹回放:24.9%<0.25 边界不越界——ENQUEUE 被写前语义门挡下。" } + { + "toolCall": { + "name": "queue_action", + "id": "call_q1", + "args": { + "source": "cand_lo", + "action": "ENQUEUE", + "rationale": "金轨迹:24.9% 逐字复述。", + "evidence": [ + { + "call_id": "call_probe_lo", + "field": "exclusive_rate", + "value": 0.249 + } + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_lo:探针实测 74/300=24.9%(对照闸过)。\n\n## 硬门数字\n- call_probe_lo:exclusive_rate=0.249(exclusive=74 sampled=300)\n\n## 建议\n- FLAG cand_lo:24.9% 恰低于 0.25 门线,不自动入队,边界升级人裁。\n\n## 自动执行区\n- 无。\n\n## 人裁区\n- cand_lo:排他率边界值,人裁。\n\n## 摘要\n金轨迹回放:24.9%<0.25 边界不越界——ENQUEUE 被写前语义门挡下。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["queue_action"] }, - { "kind": "queue_writes", "count": 0 }, - { "kind": "queue_file", "state": "absent" }, - { "kind": "package", "exists": true, "matches": [ "- 队列动作: 无" ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "queue_action" + ] + }, + { + "kind": "queue_writes", + "count": 0 + }, + { + "kind": "queue_file", + "state": "absent" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- 队列动作: 无" + ] + } ] }, { "id": "boundary_enqueue_rate_25_0", "tier": "faux", "round": "discover", - "tags": ["boundary-exclusive-25.0"], + "tags": [ + "boundary-exclusive-25.0" + ], "desc": "边界:排他率 25.0% 恰过 0.25 门线——同冻结契约,ENQUEUE 谓词成立放行,队列行落盘 + 恒可否决 + 幂等语义由 host 执行面保证。", "input": "discover 金轨迹回放:候选 cand_hi(排他 25.0%)。", "stubs": {}, "registrySeed": [ - { "id": "call_probe_hi", "tool": "exclusive_probe", "sc": { "ok": true, "url": "https://fixture.example/feed2.txt", "seed": 20261018, "exclusive_rate": 0.25, "exclusive": 75, "sampled": 300, "feed_unique_keys": 90210, "control_pass": true, "covering_sources": [ { "source": "binarydefense", "n": 20 } ], "artifact": "(fixture-seed)", "trace": { "cmd": "(frozen contract)", "rc": 0 } } } + { + "id": "call_probe_hi", + "tool": "exclusive_probe", + "sc": { + "ok": true, + "url": "https://fixture.example/feed2.txt", + "seed": 20261018, + "exclusive_rate": 0.25, + "exclusive": 75, + "sampled": 300, + "feed_unique_keys": 90210, + "control_pass": true, + "covering_sources": [ + { + "source": "binarydefense", + "n": 20 + } + ], + "artifact": "(fixture-seed)", + "trace": { + "cmd": "(frozen contract)", + "rc": 0 + } + } + } ], "turns": [ - { "toolCall": { "name": "queue_action", "id": "call_q1", "args": { "source": "cand_hi", "action": "ENQUEUE", "rationale": "金轨迹:25.0% 逐字复述,对照闸过。", "evidence": [ { "call_id": "call_probe_hi", "field": "exclusive_rate", "value": 0.25 } ] } } }, - { "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_hi:探针实测 75/300=25.0%(对照闸过)。\n\n## 硬门数字\n- call_probe_hi:exclusive_rate=0.25(exclusive=75 sampled=300)\n\n## 建议\n- ENQUEUE cand_hi:恰过 0.25 门线且对照闸过,自动入队区。\n\n## 自动执行区\n- ENQUEUE cand_hi 已入队(call_q1,恒可否决)。\n\n## 人裁区\n- 无。\n\n## 摘要\n金轨迹回放:25.0%≥0.25 恰过线——写前语义门放行入队。" } + { + "toolCall": { + "name": "queue_action", + "id": "call_q1", + "args": { + "source": "cand_hi", + "action": "ENQUEUE", + "rationale": "金轨迹:25.0% 逐字复述,对照闸过。", + "evidence": [ + { + "call_id": "call_probe_hi", + "field": "exclusive_rate", + "value": 0.25 + } + ] + } + } + }, + { + "text": "## 注入判例\n- 2026-06-01-golden-aggregator.md\n\n## 候选表\n- cand_hi:探针实测 75/300=25.0%(对照闸过)。\n\n## 硬门数字\n- call_probe_hi:exclusive_rate=0.25(exclusive=75 sampled=300)\n\n## 建议\n- ENQUEUE cand_hi:恰过 0.25 门线且对照闸过,自动入队区。\n\n## 自动执行区\n- ENQUEUE cand_hi 已入队(call_q1,恒可否决)。\n\n## 人裁区\n- 无。\n\n## 摘要\n金轨迹回放:25.0%≥0.25 恰过线——写前语义门放行入队。" + } ], "assertions": [ - { "kind": "round_ok" }, - { "kind": "tool_calls", "equals": ["queue_action"] }, - { "kind": "queue_writes", "count": 1, "actions": ["ENQUEUE"] }, - { "kind": "queue_file", "state": "row", "pattern": "\\| ENQUEUE \\| cand_hi \\|" }, - { "kind": "queue_file", "state": "row", "pattern": "call_probe_hi:exclusive_rate=0.25" }, - { "kind": "package", "exists": true, "matches": [ "- 队列动作: ENQUEUE:cand_hi" ] } + { + "kind": "round_ok" + }, + { + "kind": "tool_calls", + "equals": [ + "queue_action" + ] + }, + { + "kind": "queue_writes", + "count": 1, + "actions": [ + "ENQUEUE" + ] + }, + { + "kind": "queue_file", + "state": "row", + "pattern": "\\| ENQUEUE \\| cand_hi \\|" + }, + { + "kind": "queue_file", + "state": "row", + "pattern": "call_probe_hi:exclusive_rate=0.25" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "- 队列动作: ENQUEUE:cand_hi" + ] + } ] }, { @@ -274,97 +824,406 @@ "kind": "pipeline", "tier": "faux", "round": "health", - "tags": ["p0-skeleton", "positive", "pipeline"], + "tags": [ + "p0-skeleton", + "positive", + "pipeline" + ], "desc": "轮次状态机全状态(#182 增 ANCHOR_SWEEP 十一态)faux 跑通到 CLOSE(spec #161 P0 骨架 + P1 位等价面 #163 + P4 DEEP/ITEMIZE 真开启 #166):真 extract_feed_urls+feed_freshness(curl 替身零网络)+真 matrix 扫描(OPS_EVAL_PY 替身,tools.mjs matrixScanCli 单源,01-probe-matrix.json 与 legacy -matrix-scan.json 逐字节同文)+真锚巡检(#182:同 OPS_EVAL_PY 替身旗标分发,罐装零 finding 全集投影,08-anchor-sweep.json 落件)→真抑制分区(R1 压 dshield)→faux TRIAGE/DEEP/SYNTH(仓内 fixtures/pipeline,DEEP 逐假设微探针 seed=registrySeed 测量腿,A 一律宿主回填)→真收编/条目库桥落 ops/items/台账/沙箱;十门全机检留 gates.json;信封只增 pipeline 扩展字段。", "stubs": { "curl_age_days": [ - { "match": "cinsscore", "days": 220 }, - { "match": "binarydefense", "days": 210 }, - { "match": "dshield", "days": 200 }, - { "match": "*", "days": 30 } + { + "match": "cinsscore", + "days": 220 + }, + { + "match": "binarydefense", + "days": 210 + }, + { + "match": "dshield", + "days": 200 + }, + { + "match": "*", + "days": 30 + } ], "matrix": { "exit_code": 0, - "counts": { "violation": 0, "pending": 0, "agreement": 4 }, + "counts": { + "violation": 0, + "pending": 0, + "agreement": 4 + }, "layers": { - "truth": { "status": "OK", "members": 4, "sampled": 4, "notes": [] }, - "infra": { "status": "OK", "members": 8, "sampled": 8, "notes": [] } + "truth": { + "status": "OK", + "members": 4, + "sampled": 4, + "notes": [] + }, + "infra": { + "status": "OK", + "members": 8, + "sampled": 8, + "notes": [] + } }, - "buckets": { "violation": [], "pending": [], "agreement": [] } + "buckets": { + "violation": [], + "pending": [], + "agreement": [] + } } }, "pipeline": { "rules": { "version": 1, "rules": [ - { "id": "R1", "adopted": "2026-10-24", "adopted_via": "item-fixture", - "match": { "bucket": "frozen", "url_shape": "*dshield.org*" }, - "action": "suppress", "note": "fixture:dshield 冻结读数为已知探测伪迹", "review_at": "2027-01-24" } + { + "id": "R1", + "adopted": "2026-10-24", + "adopted_via": "item-fixture", + "match": { + "bucket": "frozen", + "url_shape": "*dshield.org*" + }, + "action": "suppress", + "note": "fixture:dshield 冻结读数为已知探测伪迹", + "review_at": "2027-01-24" + } + ] + } + }, + "assertions": [ + { + "kind": "pipeline_ok" + }, + { + "kind": "pipeline_stage", + "stage": "PROBE", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "DEEP", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "CLOSE", + "status": "done" + }, + { + "kind": "pipeline_gates" + }, + { + "kind": "pipeline_artifact", + "file": "01-probe-summary.json", + "json": { + "urls_n_at_least": 30, + "flags.length": 3, + "buckets.frozen": 3, + "attempted_n_at_least": 30, + "matrix.ok": true, + "matrix.exit_code": 0, + "matrix.violation_n": 0 + } + }, + { + "kind": "pipeline_artifact", + "file": "01-probe-matrix.json", + "json": { + "exit_code": 0, + "counts.violation": 0 + }, + "matches": [ + "\"layers\":", + "\"buckets\":" + ] + }, + { + "kind": "pipeline_artifact", + "file": "02-verify-survivors.json", + "json": { + "survivors.length": 2 + } + }, + { + "kind": "pipeline_artifact", + "file": "02-verify-suppressed.json", + "json": { + "suppressed.0.rule_id": "R1" + } + }, + { + "kind": "pipeline_artifact", + "file": "05-deep-verdicts.json", + "json": { + "verdicts.length": 2, + "degraded.length": 0 + } + }, + { + "kind": "pipeline_artifact", + "file": "06-itemize-manifest.json", + "json": { + "created": 2, + "reopened": 0, + "deduped": 0, + "suppressed": 1 + } + }, + { + "kind": "pipeline_artifact", + "file": "07-synth-report.md", + "matches": [ + "## 注入判例", + "## 异常清单", + "## 摘要", + "created=2 reopened=0 deduped=0 suppressed=1", + "confirmed=1 refuted=1 unresolved=0", + "2026-06-01-golden-aggregator.md", + "human_ruling" + ] + }, + { + "kind": "pipeline_artifact", + "file": "gates.json", + "matches": [ + "G_DEEP", + "G_CLOSE", + "\"pass\": true" ] + }, + { + "kind": "pipeline_artifact", + "file": "09-budget.json", + "json": { + "round_id": "2026-06-01-0400-health", + "env.OPS_PIPELINE_MAX_MODEL_CALLS": 12, + "env.OPS_PIPELINE_TOKEN_BUDGET": 200000, + "env.OPS_PIPELINE_WALL_CLOCK_S": 1800, + "env.OPS_PIPELINE_SPEND_CAP_CNY": 3, + "env.OPS_PIPELINE_MONTHLY_CAP_CNY": 30, + "env.OPS_PIPELINE_DUP_CALL_LIMIT": 3, + "readings.model_calls": 4, + "readings.monthly.ok": true + }, + "matches": [ + "\"usage\":", + "\"dup_top3\":", + "\"wall_s\":" + ] + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| health \\| faux-pipeline \\|.*pipeline:2026-06-01-0400-health" + } + ] + }, + { + "id": "pipeline_discover_full_faux", + "kind": "pipeline", + "tier": "faux", + "round": "discover", + "tags": [ + "p0-skeleton", + "positive", + "pipeline", + "discover" + ], + "desc": "发现轮状态机全链 faux 跑通到 CLOSE(spec 发现管线化 T1 #207 + T2 #208 + T3.5 #217 工具面真接线):真 SCAN(源注册表+NEEDS 缺口行,needs 条目带 text 原文行快照=证据永不灭失)→faux SHORTLIST(罐装 2 候选,quote=NEEDS 行逐字 span)→真 MEASURE(#217 接线:存在性重算+宿主补发 web_read 罐装读数——rDNS 候选三面全算出 measured/控制面阳性对照 ok,carrier 无 URL+排他率 null=uncertainty 显式原因勿虚构)→真 ENQUEUE(#217 正例:三判据活路 1 入队 PROPOSE 候选行落 queue.md,1 skip 留痕)→RETRO/CLOSE(#217 readings.discover_channels 读数第三键);四门强化面(G_SCAN 指针自洽/G_SHORTLIST span 级逐字回引/G_MEASURE 数字复算/G_ENQUEUE 三判据+穷尽哨)全机检留 gates.json。", + "stubs": {}, + "pipeline": { + "round": "discover", + "rules": { + "version": 1, + "rules": [] } }, "assertions": [ - { "kind": "pipeline_ok" }, - { "kind": "pipeline_stage", "stage": "PROBE", "status": "done" }, - { "kind": "pipeline_stage", "stage": "DEEP", "status": "done" }, - { "kind": "pipeline_stage", "stage": "CLOSE", "status": "done" }, - { "kind": "pipeline_gates" }, - { "kind": "pipeline_artifact", "file": "01-probe-summary.json", - "json": { "urls_n_at_least": 30, "flags.length": 3, "buckets.frozen": 3, "attempted_n_at_least": 30, - "matrix.ok": true, "matrix.exit_code": 0, "matrix.violation_n": 0 } }, - { "kind": "pipeline_artifact", "file": "01-probe-matrix.json", - "json": { "exit_code": 0, "counts.violation": 0 }, - "matches": [ "\"layers\":", "\"buckets\":" ] }, - { "kind": "pipeline_artifact", "file": "02-verify-survivors.json", "json": { "survivors.length": 2 } }, - { "kind": "pipeline_artifact", "file": "02-verify-suppressed.json", "json": { "suppressed.0.rule_id": "R1" } }, - { "kind": "pipeline_artifact", "file": "05-deep-verdicts.json", "json": { "verdicts.length": 2, "degraded.length": 0 } }, - { "kind": "pipeline_artifact", "file": "06-itemize-manifest.json", "json": { "created": 2, "reopened": 0, "deduped": 0, "suppressed": 1 } }, - { "kind": "pipeline_artifact", "file": "07-synth-report.md", "matches": [ - "## 注入判例", "## 异常清单", "## 摘要", - "created=2 reopened=0 deduped=0 suppressed=1", - "confirmed=1 refuted=1 unresolved=0", - "2026-06-01-golden-aggregator.md", - "human_ruling" - ] }, - { "kind": "pipeline_artifact", "file": "gates.json", "matches": [ "G_DEEP", "G_CLOSE", "\"pass\": true" ] }, - { "kind": "pipeline_artifact", "file": "09-budget.json", - "json": { "round_id": "2026-06-01-0400-health", "env.OPS_PIPELINE_MAX_MODEL_CALLS": 12, "env.OPS_PIPELINE_TOKEN_BUDGET": 200000, - "env.OPS_PIPELINE_WALL_CLOCK_S": 1800, "env.OPS_PIPELINE_SPEND_CAP_CNY": 3, "env.OPS_PIPELINE_MONTHLY_CAP_CNY": 30, - "env.OPS_PIPELINE_DUP_CALL_LIMIT": 3, "readings.model_calls": 4, "readings.monthly.ok": true }, - "matches": [ "\"usage\":", "\"dup_top3\":", "\"wall_s\":" ] }, - { "kind": "ledger", "min_rows": 1, "last_matches": "\\| health \\| faux-pipeline \\|.*pipeline:2026-06-01-0400-health" } + { + "kind": "pipeline_ok" + }, + { + "kind": "pipeline_stage", + "stage": "SCAN", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "SHORTLIST", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "MEASURE", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "ENQUEUE", + "status": "done" + }, + { + "kind": "pipeline_stage", + "stage": "CLOSE", + "status": "done" + }, + { + "kind": "pipeline_gates" + }, + { + "kind": "pipeline_artifact", + "file": "d1-scan.json", + "json": { + "round_id": "2026-06-01-0400-discover", + "channels.registry.n": 1, + "channels.needs.n": 2, + "channels.web.status": "placeholder" + }, + "matches": [ + "rDNS 主机名", + "NEEDS.md:L14", + "parquet", + "carrier" + ] + }, + { + "kind": "pipeline_artifact", + "file": "d2-shortlist.json", + "json": { + "candidates.length": 2 + }, + "matches": [ + "ip.thc.org" + ] + }, + { + "kind": "pipeline_artifact", + "file": "d3-measure.json", + "json": { + "hold_n": 1, + "results.length": 2, + "results.0.evidence_local_ok": true, + "results.0.status": "measured", + "results.0.control_executable": true, + "results.0.exclusivity_estimate": 0.42 + }, + "matches": [ + "rDNS 主机名", + "measure-1" + ] + }, + { + "kind": "pipeline_artifact", + "file": "d4-enqueue.json", + "json": { + "gate_exclusivity": 0.25, + "n_enqueued": 1, + "n_skipped": 1 + } + }, + { + "kind": "queue_file", + "state": "row", + "pattern": "\\| 2026-06-01-0400-discover \\| PROPOSE \\| rdns \\|" + }, + { + "kind": "pipeline_artifact", + "file": "09-budget.json", + "json": { + "round_id": "2026-06-01-0400-discover", + "env.OPS_DISCOVER_MAX_MODEL_CALLS": 30, + "env.OPS_DISCOVER_SPEND_CAP_CNY": 5, + "env.OPS_DISCOVER_WALL_CLOCK_S": 3600, + "env.OPS_PIPELINE_BUDGET_CNY": 0.05, + "env.OPS_PIPELINE_MONTHLY_CAP_CNY": 30, + "readings.discover_channels.channels.web_read.calls": 1, + "readings.discover_channels.calls_total": 1, + "readings.discover_channels.blocked_total": 0 + }, + "matches": [ + "\"usage\":" + ] + }, + { + "kind": "pipeline_artifact", + "file": "gates.json", + "matches": [ + "G_SCAN", + "G_SHORTLIST", + "G_MEASURE", + "G_ENQUEUE", + "\"pass\": true" + ] + }, + { + "kind": "ledger", + "min_rows": 1, + "last_matches": "\\| discover \\| faux-pipeline \\|.*pipeline-discover:2026-06-01-0400-discover" + } ] }, { "id": "smoke_health", "tier": "smoke", "round": "health", - "tags": ["smoke-real-model"], + "tags": [ + "smoke-real-model" + ], "desc": "冒烟层(真模型 glm-5.3-flash,花 API,手跑 --smoke):真实健康轮——判例注入/快照工具/收口四件全真;断言仅回路与收口,判读质量归冷启动手动验收。", "input": "健康轮冒烟:全源冻结检测 + 语义矩阵扫描,产健康包。", "stubs": {}, "registrySeed": [], "turns": [], "assertions": [ - { "kind": "round_ok" }, - { "kind": "package", "exists": true, "matches": [ "## 注入判例" ] }, - { "kind": "ledger", "min_rows": 1 } + { + "kind": "round_ok" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "## 注入判例" + ] + }, + { + "kind": "ledger", + "min_rows": 1 + } ] }, { "id": "smoke_discover_threatview_replay", "tier": "smoke", "round": "discover", - "tags": ["smoke-real-model"], + "tags": [ + "smoke-real-model" + ], "desc": "冒烟层(真模型 glm-5.3 主力,花 API,手跑 --smoke):threatview 重放(T4 验收 b 同源场景)——sunset_frozen 谓词真网络实测路径;建议与人裁一致性核对仍属手动验收,本冒烟只断言回路与收口。", "input": "discover 冒烟:重放 threatview(https://threatview.io/Downloads/Daily-IOC-Lists/latest.txt 等),评估是否 sunset_frozen REJECT。", "stubs": {}, "registrySeed": [], "turns": [], "assertions": [ - { "kind": "round_ok" }, - { "kind": "package", "exists": true, "matches": [ "## 注入判例" ] }, - { "kind": "ledger", "min_rows": 1 } + { + "kind": "round_ok" + }, + { + "kind": "package", + "exists": true, + "matches": [ + "## 注入判例" + ] + }, + { + "kind": "ledger", + "min_rows": 1 + } ] } ] diff --git a/ops/agent/golden/run-golden.mjs b/ops/agent/golden/run-golden.mjs index 1a6358eb..fdeb8640 100644 --- a/ops/agent/golden/run-golden.mjs +++ b/ops/agent/golden/run-golden.mjs @@ -93,7 +93,10 @@ async function runCase(c, allowSmoke) { const { runRound } = await import("../agent.mjs"); const { resolveField } = await import("../tools.mjs"); - const { GATE_COUNT } = await import("../pipeline/gates.mjs"); // 门数单源(#194 C3):禁手抄 10(评审D P2 硬编码判例);env 后动态 import 同上两行 + const { HEALTH_PIPELINE, DISCOVER_PIPELINE } = await import("../pipeline.mjs"); // #207 T1:健康轮门数从轮型定义表派生(gates.GATE_COUNT 已含发现轮四门,非健康轮面;与 selftest-pipeline 同判例);env 后动态 import 同上两行 + // #208 T2:轮型从用例派生(缺省=健康;pipeline.discover=发现轮全 faux 例),门数仍单源=对应轮型表可跑步门数,零手抄 + const roundType = c.kind === "pipeline" && c.pipeline?.round === "discover" ? "discover" : "health"; + const GATE_COUNT = (roundType === "discover" ? DISCOVER_PIPELINE : HEALTH_PIPELINE).filter((d) => d.gate).length; let result = null, err = null; if (c.kind === "pipeline") { // P0 轮次状态机例(spec #161):真引擎跑全 faux 轮(模型步=faux provider+仓内 fixtures/pipeline, @@ -103,7 +106,8 @@ async function runCase(c, allowSmoke) { const buf = { stdout: "", stderr: "" }; const { runPipeline, DEFAULT_FIXTURES_DIR } = await import("../pipeline.mjs"); try { - result = await runPipeline({ type: "health", roundId: "2026-06-01-0400-health", + const roundId = `2026-06-01-0400-${roundType}`; // #208 T2:发现轮例用 -discover 后缀(轮 id 律 ^…-discover$);健康例原字面不变(位等价) + result = await runPipeline({ type: roundType, roundId, fixturesDir: DEFAULT_FIXTURES_DIR, // 显式 fixtures=faux 面(#170;golden 恒零网络,同缺省路径行为不变) stdout: { write: (s) => { buf.stdout += s; return true; } }, stderr: { write: (s) => { buf.stderr += s; return true; } } }); diff --git a/ops/agent/pipeline.mjs b/ops/agent/pipeline.mjs index 12b416c5..b94c5832 100644 --- a/ops/agent/pipeline.mjs +++ b/ops/agent/pipeline.mjs @@ -46,6 +46,7 @@ export const ctxShape = ({ type, roundId, opsRoot, env, log, fixturesDir, modelF ...ctxPaths(opsRoot, roundId), // 派生路径单源(含 retroAuditKey;replay 面事后覆写不受影响) ledgerFile: env.LEDGER_FILE ?? process.env.LEDGER_FILE ?? path.join(DOCS_ROOT, "ops", "ledger.md"), pkg: null, verify: null, envelope: null, + discoverHost: null, // #217 T3.5:轮级发现宿主惰性单例位(SHORTLIST 直装/MEASURE 补发共用;discover 轮专用,health 恒 null) usage: { input: 0, output: 0, totalTokens: 0, cost: 0 }, state, }); @@ -96,9 +97,41 @@ export const HEALTH_PIPELINE = [ { name: "FAILED", kind: "terminal", model: null, out: [], gate: null, artifact: null, fail: { retry: 0, terminal: null } }, // 终态书挡:工件全保留,replay 可救 ]; -const byName = (n) => HEALTH_PIPELINE.find((d) => d.name === n); -const RUNNABLE = HEALTH_PIPELINE.filter((d) => d.kind === "script" || d.kind === "model").map((d) => d.name); // replay 段闭集 -const STAGE_PREFIX = { PROBE: "01-", VERIFY: "02-", TRIAGE: "03-", DISPATCH: "04-", DEEP: "05-", ITEMIZE: "06-", SYNTH: "07-", ANCHOR_SWEEP: "08-" }; // 工件序号前缀固定步序(C稿 §4.1;replay 前置工件复制消费,#182 补 08-) +// 发现轮管线定义表(#207 T1;spec /tmp/discover-pipeline-spec.md T1 节;ADR 0010 六步骨架): +// SCAN(确定性候选宇宙快照,零模型零网络)→SHORTLIST(模型结构化候选,解析失败=降级空清单 +// 留痕不炸轮,spec T1)→MEASURE(确定性存在性测量,超时/未测=uncertainty hold 行,不炸轮)→ +// ENQUEUE(只写不动作:#139 flock 路径写 queue.md,入队判据=代码常量模型无权改)→ +// RETRO/CLOSE(同 health 律:台账幂等/sandbox 红线/09-budget.json 同形快照;月帽共享同门)。 +// T2 #208 强化四门(span 逐字回引/数字复算/对账);T3 #209 接模型调用与工具注入面(本票只留接线点)。 +export const DISCOVER_PIPELINE = [ + { name: "SCAN", kind: "script", model: null, + out: ["d1-scan.json"], gate: "G_SCAN", artifact: "d1-scan.json", + fail: { retry: 0, terminal: "infra" } }, // 候选宇宙=registry+needs 两面(web=T3 占位);读败=infra + { name: "SHORTLIST", kind: "model", model: "discover/shortlist", + out: ["d2-shortlist.json", "d2-shortlist-transcript.jsonl"], gate: "G_SHORTLIST", artifact: "d2-shortlist.json", + fail: { retry: 1, terminal: "model_face" } }, // 面级失败重试≤1;JSON 解析失败=consume 内降级空清单(步 done,零候选合法);span 逐字回引=T2 #208 + { name: "MEASURE", kind: "script", model: null, + out: ["d3-measure.json"], gate: "G_MEASURE", artifact: "d3-measure.json", + fail: { retry: 0, terminal: "infra" } }, // T1 本地存在性面(测量面死/超时=uncertainty hold 留痕);探针接线=T3 #209 + { name: "ENQUEUE", kind: "script", model: null, + out: ["d4-enqueue.json"], gate: "G_ENQUEUE", artifact: "d4-enqueue.json", + fail: { retry: 0, terminal: "infra" } }, // 只写不动作(#139);queue 写败=fail-closed infra(证据不灭失) + { name: "RETRO", kind: "script", model: null, + out: ["../retro/.json"], gate: "G_RETRO", artifact: "retro/.json", + fail: { retry: 0, degrade: "skip_retro", terminal: "ledger_failed" } }, // 同 health 律:候选写败=降级;台账/sandbox=终态;台账标签 pipeline-discover(LEDGER_TAGS 单源) + { name: "CLOSE", kind: "bookend", model: null, out: [], gate: "G_CLOSE", artifact: "(envelope)", + fail: { retry: 0, terminal: "infra" } }, // 09-budget.json 同形快照(env 面 per-type=三旋钮+est+共享月帽) + { name: "FAILED", kind: "terminal", model: null, out: [], gate: null, artifact: null, + fail: { retry: 0, terminal: null } }, // 终态书挡:工件全保留,replay 可救 +]; +export const PIPELINES = { health: HEALTH_PIPELINE, discover: DISCOVER_PIPELINE }; // 轮型→定义表(#207 T1);byName/RUNNABLE/STAGE_PREFIX 一律 per-type 派生 +const byNameOf = (type) => (n) => PIPELINES[type ?? "health"].find((d) => d.name === n); +const runnableOf = (type) => PIPELINES[type ?? "health"].filter((d) => d.kind === "script" || d.kind === "model").map((d) => d.name); // replay 段闭集(per-type) +const STAGE_PREFIXES = { // 工件序号前缀固定步序(C稿 §4.1;replay 前置工件复制消费;per-type #207 T1:发现轮=d1-d4) + health: { PROBE: "01-", VERIFY: "02-", TRIAGE: "03-", DISPATCH: "04-", DEEP: "05-", ITEMIZE: "06-", SYNTH: "07-", ANCHOR_SWEEP: "08-" }, + discover: { SCAN: "d1-", SHORTLIST: "d2-", MEASURE: "d3-", ENQUEUE: "d4-" }, // RETRO 工件在 ops/retro/ 无轮内前缀 +}; +export const typeOfRoundId = (id) => /-discover(-r\d+)?$/.test(String(id)) ? "discover" : "health"; // 后缀集=轮型集(#207 T1);round-id 后缀即轮型声明面 // ══ 引擎(通用代码,≤300 行纪律:下段至文件尾只做定义表驱动的编排,不判任何具体步)══ const nowIso = () => new Date().toISOString(); @@ -116,16 +149,16 @@ const SKIP_WHEN = { // 命名谓词闭集(定义表数据引用;新增=spec 变 }; function mkCtx(opts, io) { - const type = opts.type ?? "health"; - if (type !== "health") throw Object.assign(new Error(`P0 仅健康轮管线(discover 立项后自持定义表):${type}`), { code: "bad_round" }); + const type = opts.type ?? typeOfRoundId(opts.roundId ?? ""); // 轮型:显式优先,缺省由 round-id 后缀推断(#207 T1);两者不符由下方正则拦 + if (!PIPELINES[type]) throw Object.assign(new Error(`轮型须 health|discover(后缀集=轮型集):${type}`), { code: "bad_round" }); const roundId = opts.roundId ?? roundIdNow(type); - if (!/^\d{4}-\d{2}-\d{2}-\d{4}-health(-r\d+)?$/.test(roundId)) - throw Object.assign(new Error(`round_id 形状须 YYYY-MM-DD-HHMM-health:${roundId}`), { code: "bad_round" }); + if (!new RegExp(`^\\d{4}-\\d{2}-\\d{2}-\\d{4}-${type}(-r\\d+)?$`).test(roundId)) + throw Object.assign(new Error(`round_id 形状须 YYYY-MM-DD-HHMM-${type}:${roundId}`), { code: "bad_round" }); const budgetRaw = io.env.OPS_PIPELINE_BUDGET_CNY ?? process.env.OPS_PIPELINE_BUDGET_CNY ?? "0.05"; const budgetCny = +budgetRaw; // NaN 静默放行=审后修 P2-4:非法值退出 2 不带病跑轮 if (!Number.isFinite(budgetCny) || budgetCny < 0) throw Object.assign(new Error(`OPS_PIPELINE_BUDGET_CNY 须为非负数,得到:${budgetRaw}`), { usage: true }); - const budgetKnobs = budget.readBudgetKnobs(io.env); // 四界旋钮(#199 A1;校验同 BUDGET_CNY 判例:非法=用法错 exit 2 不带病跑轮) + const budgetKnobs = type === "discover" ? budget.readDiscoverKnobs(io.env) : budget.readBudgetKnobs(io.env); // #207 T1:发现轮旋钮族单源读面(月帽共享同键);非法=用法错 exit 2 不带病跑轮(同 #199 判例) const breakerKnobs = readBreakerKnobs(io.env); // #199 B:熔断旋钮轮始校验(非法=用法错 exit 2 同判例;快照消费=CLOSE) const fixturesDir = path.resolve(opts.fixturesDir ?? DEFAULT_FIXTURES_DIR); const modelFace = selectModelFace({ fixturesExplicit: opts.fixturesDir != null, inherited: opts.inheritedModelFace ?? null }); // 面选择(#170:显式 fixtures=faux/继承面/缺省 real;纯函数真源在 faux.mjs) @@ -163,8 +196,11 @@ function failStage(ctx, def, rec, e, gateRec) { // 单发步(TRIAGE/SYNTH)的门评估入重试圈:C稿 §2.2「契约违例重试≤1(同提示词重发)」——门败与 // schema 违例同面重试;逐假设步(DEEP forEach)的门评收编尾后一次(部分态评门=假阴性嗓音)。 async function runModelStage(def, ctx, rec) { - ctx.budgetKnobs ??= budget.readBudgetKnobs(ctx.env ?? {}); // 手撞 ctx/resume 重建面兑底(#199 A1;幂等) + ctx.budgetKnobs ??= (ctx.type === "discover" ? budget.readDiscoverKnobs : budget.readBudgetKnobs)(ctx.env ?? {}); // 手撞 ctx/resume 重建面兑底(#199 A1 幂等;per-type #207 T1) ctx.modelCalls ??= 0; + const knobSrc = ctx.type === "discover" // #207 T1:报错文案指认 per-type 旋钮名(发现轮=OPS_DISCOVER_*;月帽共享同键) + ? { calls: "OPS_DISCOVER_MAX_MODEL_CALLS", wall: "OPS_DISCOVER_WALL_CLOCK_S", spend: "OPS_DISCOVER_SPEND_CAP_CNY" } + : { calls: "OPS_PIPELINE_MAX_MODEL_CALLS", wall: "OPS_PIPELINE_WALL_CLOCK_S", spend: "OPS_PIPELINE_SPEND_CAP_CNY" }; const io = steps.modelIO(def); const items = def.forEach ? steps.forEachItems(def, ctx) : [null]; const usage = { input: 0, output: 0, totalTokens: 0, cost: 0 }; @@ -177,9 +213,9 @@ async function runModelStage(def, ctx, rec) { rec.attempts += 1; // #199 A1 四界检查点①③:attempt 前查调用数帽与墙钟(违界即停:不重试不降级,哲学①已写工件照留) if (budget.overCallsCap(ctx.modelCalls, ctx.budgetKnobs.maxModelCalls)) - throw Object.assign(new Error(`预算四界①:模型调用将超帽(已计 ${ctx.modelCalls}+1 > ${ctx.budgetKnobs.maxModelCalls},OPS_PIPELINE_MAX_MODEL_CALLS)`), { code: "budget_exceeded", fatalBudget: true }); + throw Object.assign(new Error(`预算四界①:模型调用将超帽(已计 ${ctx.modelCalls}+1 > ${ctx.budgetKnobs.maxModelCalls},${knobSrc.calls})`), { code: "budget_exceeded", fatalBudget: true }); if (budget.overWallClock(ctx.state.started_at, ctx.budgetKnobs.wallClockS)) - throw Object.assign(new Error(`预算四界③:墙钟 ${budget.elapsedWallS(ctx.state.started_at).toFixed(1)}s 超帽 ${ctx.budgetKnobs.wallClockS}s(OPS_PIPELINE_WALL_CLOCK_S)`), { code: "wall_clock_exceeded", fatalBudget: true }); + throw Object.assign(new Error(`预算四界③:墙钟 ${budget.elapsedWallS(ctx.state.started_at).toFixed(1)}s 超帽 ${ctx.budgetKnobs.wallClockS}s(${knobSrc.wall})`), { code: "wall_clock_exceeded", fatalBudget: true }); ctx.modelCalls += 1; try { const r = await runModelStep(def, ctx, item); @@ -189,10 +225,11 @@ async function runModelStage(def, ctx, rec) { // #199 A1 四界检查点②④:结算后查 token 轨迹与花费帽(轮内累计=已入库 ctx.usage+本步未收编 usage) const roundTokens = ctx.usage.totalTokens + usage.totalTokens; const roundCost = ctx.usage.cost + usage.cost; - if (budget.overTokenBudget(roundTokens, ctx.budgetKnobs.tokenBudget)) + // #207 T1:发现轮无 token 界(旋钮族只有调用数/墙钟/花费三界;tokenBudget undefined 即跳过) + if (ctx.budgetKnobs.tokenBudget !== undefined && budget.overTokenBudget(roundTokens, ctx.budgetKnobs.tokenBudget)) throw Object.assign(new Error(`预算四界②:token 轨迹 ${roundTokens} 超预算 ${ctx.budgetKnobs.tokenBudget}(OPS_PIPELINE_TOKEN_BUDGET)`), { code: "budget_exceeded", fatalBudget: true }); if (budget.overSpendCap(roundCost, ctx.budgetKnobs.spendCapCny)) - throw Object.assign(new Error(`预算四界④:花费 ¥${roundCost.toFixed(8)} 超帽 ¥${ctx.budgetKnobs.spendCapCny}(OPS_PIPELINE_SPEND_CAP_CNY)`), { code: "budget_exceeded", fatalBudget: true }); + throw Object.assign(new Error(`预算四界④:花费 ¥${roundCost.toFixed(8)} 超帽 ¥${ctx.budgetKnobs.spendCapCny}(${knobSrc.spend})`), { code: "budget_exceeded", fatalBudget: true }); if (!def.forEach) { const gateRec = gates.evalStageGate(def, ctx); gates.appendGate(ctx.dir, ctx.roundId, gateRec); @@ -218,10 +255,12 @@ async function runModelStage(def, ctx, rec) { // 段执行器(runPipeline 与 replay 共用):dry=只跑门不调模型不跑步(C稿 §4.5); // skippedInOriginal=原件轮里被 SKIP_WHEN 跳过的步——dry 审计对这些步跳门(审后修 P2-3): // 缺席工件判门败是假阴性,静轮 dry 首审即触发;原件 stages[].status=skipped 即不评其门。 -async function runStages(ctx, { fromIdx = 0, toIdx = RUNNABLE.length - 1, dry = false, skippedInOriginal = [], resume = false } = {}) { +async function runStages(ctx, { fromIdx = 0, toIdx = null, dry = false, skippedInOriginal = [], resume = false } = {}) { + const runnable = runnableOf(ctx.type), byname = byNameOf(ctx.type); // per-type 步序(#207 T1;health 面不变) + if (toIdx === null) toIdx = runnable.length - 1; const recordedAs = (name) => ctx.state.stages.find((r) => r.name === name && ["done", "skipped", "degraded", "dry"].includes(r.status)); for (let i = fromIdx; i <= toIdx; i++) { - const def = byName(RUNNABLE[i]); + const def = byname(runnable[i]); if (resume && recordedAs(def.name)) { // resume:已记录步零重入(幂等;state 与工件一致以工件+门为准的例外=已过关步) ctx.state.stage = def.name; ctx.log(`[stage] ${def.name} done gate=SKIP(resume:已记录${recordedAs(def.name).status}) dur=0s artifact=-`); @@ -237,10 +276,11 @@ async function runStages(ctx, { fromIdx = 0, toIdx = RUNNABLE.length - 1, dry = } // #199 A1 四界检查点③(步边界):墙钟超=终态先于步体(已记录步零重入/dry 零调用不查; // resume 沿原 started_at 续算——恢复晚于帽即拒,同 16h 陈旧防线语义) - ctx.budgetKnobs ??= budget.readBudgetKnobs(ctx.env ?? {}); + ctx.budgetKnobs ??= (ctx.type === "discover" ? budget.readDiscoverKnobs : budget.readBudgetKnobs)(ctx.env ?? {}); if (!dry && budget.overWallClock(ctx.state.started_at, ctx.budgetKnobs.wallClockS)) { + const wallKnob = ctx.type === "discover" ? "OPS_DISCOVER_WALL_CLOCK_S" : "OPS_PIPELINE_WALL_CLOCK_S"; // #207 T1 文案 per-type const wrec = { name: def.name, status: "done", attempts: 0, dur_ms: 0, usage: null }; - return failStage(ctx, def, wrec, Object.assign(new Error(`预算四界③:墙钟 ${budget.elapsedWallS(ctx.state.started_at).toFixed(1)}s 超帽 ${ctx.budgetKnobs.wallClockS}s(OPS_PIPELINE_WALL_CLOCK_S)`), { code: "wall_clock_exceeded" }), null); + return failStage(ctx, def, wrec, Object.assign(new Error(`预算四界③:墙钟 ${budget.elapsedWallS(ctx.state.started_at).toFixed(1)}s 超帽 ${ctx.budgetKnobs.wallClockS}s(${wallKnob})`), { code: "wall_clock_exceeded" }), null); } const rec = { name: def.name, status: dry ? "dry" : "done", attempts: 0, dur_ms: 0, usage: null }; const t0 = performance.now(); @@ -310,7 +350,7 @@ async function runStages(ctx, { fromIdx = 0, toIdx = RUNNABLE.length - 1, dry = } async function finishClose(ctx, io) { // CLOSE 书挡:存在性校验→信封门→state 终写→stdout 末行(重放不入此步) - const def = byName("CLOSE"); + const def = byNameOf(ctx.type)("CLOSE"); // per-type(#207 T1;两轮型同书挡形制) const rec = { name: "CLOSE", status: "done", attempts: 1, dur_ms: 0, usage: null }; const t0 = performance.now(); ctx.log("[stage] CLOSE start"); @@ -351,7 +391,7 @@ export async function runPipeline(opts = {}) { // 读数入 state(A5 CLOSE 工件面)。est=本轮预算(BUDGET_CNY 准告警线上界,宁少算口径)。 // #203 C2:拒跑块(文案/[admission] log/台账留痕行)收口 budget.mjs admitOrRefuse 留痕外壳 // (与 agent.mjs legacy 面同壳同文案);本面只映射出口=emitFail 信封(exit 1)。 - const admission = await budget.admitOrRefuse({ ledgerFile: ctx.ledgerFile, capCny: ctx.budgetKnobs.monthlyCapCny, estCny: ctx.budgetCny, env: io.env, log: ctx.log }); + const admission = await budget.admitOrRefuse({ ledgerFile: ctx.ledgerFile, capCny: ctx.budgetKnobs.monthlyCapCny, estCny: ctx.budgetCny, env: io.env, log: ctx.log, round: ctx.type }); // #207 T1:月帽共享 OPS_PIPELINE_MONTHLY_CAP_CNY 同一裁决函数,只留痕轮次列 per-type if (admission.refused) return emitFail(admission.code, admission.message); ctx.state.monthly_admission = admission.admission; // A5 CLOSE 工件读数面(观察非裁决) if (fs.existsSync(path.join(ctx.dir, "state.json"))) return emitFail("bad_round", `rounds/${ctx.roundId} 已存在(同 id 重跑走 replay)`); @@ -373,10 +413,13 @@ const copyTree = (src, dst, skip) => { // 原件零改写:只读复制(哲学① }; export async function runReplay(opts = {}) { const io = bindIo(opts); - const { roundId, from = "PROBE", to = "RETRO", dry = false } = opts; - for (const s of [from, to]) if (!RUNNABLE.includes(s)) + const { roundId, from = null, to = "RETRO", dry = false } = opts; + const type = typeOfRoundId(roundId); // 后缀集=轮型集(#207 T1);发现轮重放闭集同构 + const RUNNABLE = runnableOf(type), STAGE_PREFIX = STAGE_PREFIXES[type]; + const fromStage = from ?? RUNNABLE[0]; // 缺省=首步(health=PROBE/discover=SCAN) + for (const s of [fromStage, to]) if (!RUNNABLE.includes(s)) throw Object.assign(new Error(`replay 步名须 ∈ {${RUNNABLE.join("|")}}(CLOSE 属原轮时序不入重放;FAILED 非可跑步),得到:${s}`), { usage: true }); - if (RUNNABLE.indexOf(from) > RUNNABLE.indexOf(to)) throw Object.assign(new Error(`--from(${from}) 不得晚于 --to(${to})`), { usage: true }); + if (RUNNABLE.indexOf(fromStage) > RUNNABLE.indexOf(to)) throw Object.assign(new Error(`--from(${fromStage}) 不得晚于 --to(${to})`), { usage: true }); const OPS_ROOT = opsRootOf(io.env); const srcDir = path.join(OPS_ROOT, "rounds", String(roundId)); if (!fs.existsSync(path.join(srcDir, "state.json"))) throw Object.assign(new Error(`rounds/${roundId} 无 state.json(无可重放轮次)`), { usage: true }); @@ -392,7 +435,7 @@ export async function runReplay(opts = {}) { // 不属轮次工件面;ITEMIZE 重放走原轮 manifest 回声(零写,replay 确定性)。 if (dry) copyTree(srcDir, dir, ["state.json", "gates.json"]); else { - const prefixes = RUNNABLE.slice(0, RUNNABLE.indexOf(from)).map((n) => STAGE_PREFIX[n]).filter(Boolean); + const prefixes = RUNNABLE.slice(0, RUNNABLE.indexOf(fromStage)).map((n) => STAGE_PREFIX[n]).filter(Boolean); for (const f of fs.readdirSync(srcDir)) { if (f === "state.json" || f === "gates.json" || fs.statSync(path.join(srcDir, f)).isDirectory()) continue; if (prefixes.some((p) => f.startsWith(p))) fs.copyFileSync(path.join(srcDir, f), path.join(dir, f)); @@ -407,10 +450,10 @@ export async function runReplay(opts = {}) { const ctx = mkCtx({ ...opts, roundId: replayId, replayOf: roundId, inheritedModelFace: origFace ?? "faux" }, io); ctx.retroAuditKey = dry ? roundId : replayId; // dry 审原件工件;重执行审重放自产 writeState(ctx); - const failure = await runStages(ctx, { fromIdx: RUNNABLE.indexOf(from), toIdx: RUNNABLE.indexOf(to), dry, skippedInOriginal: origSkipped }); + const failure = await runStages(ctx, { fromIdx: RUNNABLE.indexOf(fromStage), toIdx: RUNNABLE.indexOf(to), dry, skippedInOriginal: origSkipped }); ctx.state.status = failure ? "failed" : "done"; // 重放目录必终态(#167:boot 恢复扫描不把 -rN 目录误当活轮) writeState(ctx); - const rp = path.join(dir, "07-synth-report.md"); + const rp = path.join(dir, ctx.type === "discover" ? "d4-enqueue.json" : "07-synth-report.md"); // per-type 末步工件(#207 T1;发现轮无报告步) if (fs.existsSync(rp)) ctx.pkg = rp; // 重放段产物或复制件 const envelope = failure ? composeEnvelope(ctx, { ok: false, ...failure }) : composeEnvelope(ctx, { ok: true }); io.stdout.write(JSON.stringify(envelope) + "\n"); @@ -424,8 +467,8 @@ export async function runReplay(opts = {}) { export async function runResume(opts = {}) { const io = bindIo(opts); const { roundId } = opts; - if (!/^\d{4}-\d{2}-\d{2}-\d{4}-health$/.test(String(roundId))) - throw Object.assign(new Error(`resume 目标须原件轮 id(YYYY-MM-DD-HHMM-health,-rN 重放目录不入恢复):${roundId}`), { usage: true }); + if (!/^\d{4}-\d{2}-\d{2}-\d{4}-(health|discover)$/.test(String(roundId))) + throw Object.assign(new Error(`resume 目标须原件轮 id(YYYY-MM-DD-HHMM-health|discover,-rN 重放目录不入恢复):${roundId}`), { usage: true }); const OPS_ROOT = opsRootOf(io.env); const dir = path.join(OPS_ROOT, "rounds", String(roundId)); const state = j(path.join(dir, "state.json")); @@ -442,9 +485,9 @@ export async function runResume(opts = {}) { // 形状=ctxShape 单源(#204 C3,与 mkCtx 同构造);值面=state 复原+缺旋钮补 env 读取 // (budget_knobs/model_face 缺省回读零漂移;budget_cny 不校验不补——旧轮 undefined 保持 undefined)。 const ctx = ctxShape({ type: state.type, roundId, opsRoot: OPS_ROOT, env: io.env, log: io.log, - budgetCny: state.budget_cny, budgetKnobs: state.budget_knobs ?? budget.readBudgetKnobs(io.env), breakerKnobs: readBreakerKnobs(io.env), + budgetCny: state.budget_cny, budgetKnobs: state.budget_knobs ?? (state.type === "discover" ? budget.readDiscoverKnobs(io.env) : budget.readBudgetKnobs(io.env)), breakerKnobs: readBreakerKnobs(io.env), // #207 T1:旋钮回退读面 per-type fixturesDir: state.fixtures_dir, modelFace: state.model_face ?? "faux", state }); - const pending = RUNNABLE.filter((n) => !state.stages.some((r) => r.name === n && ["done", "skipped", "degraded", "dry"].includes(r.status))); + const pending = runnableOf(state.type).filter((n) => !state.stages.some((r) => r.name === n && ["done", "skipped", "degraded", "dry"].includes(r.status))); // 步序 per-type(#207 T1) ctx.resumedFrom = pending[0] ?? null; // 信封 resumed_from(恢复起点;全记录齐=仅 CLOSE 收口) ctx.resuming = true; // 引擎旗:模型步工件齐者补门不重跑(runModelStage/runStages 消费) state.resume_count = (state.resume_count ?? 0) + 1; @@ -463,19 +506,25 @@ export async function runResume(opts = {}) { export async function main(argv, io = {}) { const env = io.env ?? process.env; const stdout = io.stdout ?? process.stdout, stderr = io.stderr ?? process.stderr; - const usage = "用法: node ops/agent/pipeline.mjs health [--round-id ID] [--fixtures DIR]\n node ops/agent/pipeline.mjs replay [--from STAGE] [--to STAGE] [--dry] [--fixtures DIR]\n node ops/agent/pipeline.mjs resume (crash 恢复,C稿 §4.6;boot 扫描/运维手用)"; + const usage = "用法: node ops/agent/pipeline.mjs health [--round-id ID] [--fixtures DIR]\n node ops/agent/pipeline.mjs discover [--round-id ID] [--fixtures DIR](发现轮 #207 T1)\n node ops/agent/pipeline.mjs replay [--from STAGE] [--to STAGE] [--dry] [--fixtures DIR]\n node ops/agent/pipeline.mjs resume (crash 恢复,C稿 §4.6;boot 扫描/运维手用)"; const flag = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : undefined; }; const bad = (m) => { stderr.write(`${m}\n${usage}\n`); return 2; }; const mode = env.OPS_PIPELINE ?? process.env.OPS_PIPELINE ?? "legacy"; // 迁移开关(P2 桥消费;本 CLI 校验值闭集并记入 state.json) if (!["legacy", "pipeline"].includes(mode)) return bad(`OPS_PIPELINE 须 legacy|pipeline,得到:${mode}`); const [cmd, pos] = argv; + let curRound = "health"; // emitInfra 面轮型(#207 T1:discover 子命令报错不再误报 health) const emitInfra = (e) => { const code = ERROR_CODES.includes(e?.code) ? e.code : "infra"; - stdout.write(JSON.stringify({ ok: false, round: "health", error: { code, message: e?.message ?? String(e) } }) + "\n"); return 1; }; + stdout.write(JSON.stringify({ ok: false, round: curRound, error: { code, message: e?.message ?? String(e) } }) + "\n"); return 1; }; try { if (cmd === "health") { const r = await runPipeline({ type: "health", roundId: flag("--round-id"), fixturesDir: flag("--fixtures"), env, stdout, stderr }); return r.ok ? 0 : 1; } + if (cmd === "discover") { // #207 T1:发现轮 CLI 入口(--round-id 缺省=当日 HHMM-discover 自造) + curRound = "discover"; + const r = await runPipeline({ type: "discover", roundId: flag("--round-id"), fixturesDir: flag("--fixtures"), env, stdout, stderr }); + return r.ok ? 0 : 1; + } if (cmd === "resume") { if (!pos || pos.startsWith("-")) return bad("resume 缺 "); const r = await runResume({ roundId: pos, env, stdout, stderr }); diff --git a/ops/agent/pipeline/budget.mjs b/ops/agent/pipeline/budget.mjs index b1a139db..7ada6501 100644 --- a/ops/agent/pipeline/budget.mjs +++ b/ops/agent/pipeline/budget.mjs @@ -40,6 +40,22 @@ export function readBudgetKnobs(env = process.env) { }; } +// 发现轮旋钮族(#207 T1;ADR 0010;spec /tmp/discover-pipeline-spec.md T4 首月缺省):三旋钮独立 +// (轮内调用帽/墙钟/花费帽,宁少算:发现轮无 token 界——不预设轨迹预算,四界只执①③④);月帽 +// 共享 OPS_PIPELINE_MONTHLY_CAP_CNY 同键同源(月帽是全 agent 面共享底线,不另立月门:admitOrRefuse +// 同一函数消费,双拒跑同码同文案)。dup 面沿 OPS_PIPELINE_DUP_CALL_LIMIT 同键同缺省 +// (#217 T3.5 接线消费:发现工具宿主 mkDiscoveryHost dupLimit;原「T3 接线时消费」注记兑现); +// 哈希 dupKey 沿用不另立。 +export function readDiscoverKnobs(env = process.env) { + return { + maxModelCalls: readKnob("OPS_DISCOVER_MAX_MODEL_CALLS", env.OPS_DISCOVER_MAX_MODEL_CALLS ?? process.env.OPS_DISCOVER_MAX_MODEL_CALLS, "30"), // ①整轮模型调用数帽(30 调用) + wallClockS: readKnob("OPS_DISCOVER_WALL_CLOCK_S", env.OPS_DISCOVER_WALL_CLOCK_S ?? process.env.OPS_DISCOVER_WALL_CLOCK_S, "3600"), // ③整轮墙钟秒 + spendCapCny: readKnob("OPS_DISCOVER_SPEND_CAP_CNY", env.OPS_DISCOVER_SPEND_CAP_CNY ?? process.env.OPS_DISCOVER_SPEND_CAP_CNY, "5"), // ④整轮花费帽 ¥ + monthlyCapCny: readKnob("OPS_PIPELINE_MONTHLY_CAP_CNY", env.OPS_PIPELINE_MONTHLY_CAP_CNY ?? process.env.OPS_PIPELINE_MONTHLY_CAP_CNY, "30"), // 月帽共享(与 readBudgetKnobs 同键同缺省) + dupCallLimit: readKnob("OPS_PIPELINE_DUP_CALL_LIMIT", env.OPS_PIPELINE_DUP_CALL_LIMIT ?? process.env.OPS_PIPELINE_DUP_CALL_LIMIT, "3"), // 同参调用拦截线(#217 T3.5:发现宿主 dupLimit 消费;同键同缺省不另立) + }; +} + // —— 四界环内执法(纯函数;pipeline.mjs runModelStage/runStages 检查点调用)—— // ①调用数帽:attempt 前查(calls=已计调用数;calls+1=本次将成第几次;> 帽即违界)。 export const overCallsCap = (calls, cap) => calls + 1 > cap; @@ -109,8 +125,7 @@ export async function monthlyAdmission({ ledgerFile, capCny, estCny, now = new D } // 拒跑留痕行(经 lib.sh ledger_append 写,表头自建/A3 后自带锁;model/用量列=—,备注列=拒跑读数)。 -export async function appendRefusalTrace(ledgerFile, { est, cap }, env = process.env) { - const round = "health"; // 双面拒跑都发生在 health 轮(discover 未立项) +export async function appendRefusalTrace(ledgerFile, { est, cap }, env = process.env, round = "health") { // round 参数化(#207):发现轮立项后拒跑留痕轮次列随轮型,不再硬编码 return execFileP("bash", ["-c", `. "$0"; ledger_append "$@"`, LIBSH, round, "—", "—", `budget_monthly refused est=${est} cap=${cap}`], { timeout: 120_000, maxBuffer: 32 * 1024 * 1024, env: { ...env, LEDGER_FILE: ledgerFile } }); } @@ -128,11 +143,11 @@ export const refusalOf = (a) => ({ // 留痕外壳:monthlyAdmission 读→裁;拒跑则 [admission] log + 台账留痕行(写败只 log 不 // 翻转裁决——宁拒不跑),返回 {refused:true, code, message};放行返回 {refused:false, // admission}(读数对象原样回传,state.monthly_admission/09-budget.json 观察面字节不变)。 -export async function admitOrRefuse({ ledgerFile, capCny, estCny, env, log }) { +export async function admitOrRefuse({ ledgerFile, capCny, estCny, env, log, round = "health" }) { // round 只影响留痕轮次列(#207);裁决真源 admitMonthly 与两面出口形状不变 const admission = await monthlyAdmission({ ledgerFile, capCny, estCny }); if (admission.ok) return { refused: false, admission }; log(`[admission] budget_monthly refused month=${admission.month} spend=${admission.spend} est=${admission.est} cap=${admission.cap} skipped=${admission.skipped}`); - try { await appendRefusalTrace(ledgerFile, { est: estCny, cap: capCny }, env); } + try { await appendRefusalTrace(ledgerFile, { est: estCny, cap: capCny }, env, round); } catch (e) { log(`[admission] 拒跑留痕行写败(不翻转裁决):${e.message}`); } // 台账写败不吞拒跑:宁拒不跑 return { refused: true, ...refusalOf(admission) }; } diff --git a/ops/agent/pipeline/citation.mjs b/ops/agent/pipeline/citation.mjs new file mode 100644 index 00000000..ef7b3c8a --- /dev/null +++ b/ops/agent/pipeline/citation.mjs @@ -0,0 +1,110 @@ +// citation.mjs — #208 T2:span 级逐字回引(发现轮)。 +// 定位:纯函数面,零 IO/零模型/零网络。三律: +// ① 逐字律:quote 必须是证据面的原样子串(不 trim/不规范化/不改写); +// ② URL 边界律(判例自铸,spec T2 三负例):quote 里的 URL 形 token 须在证据面以 +// 「定界整 token」在场——前缀子串(https://ip.thc vs 面内 https://ip.thc.org)与 +// 超集串皆非在场(半真 URL ≠ 在场);定界=URL 合法字符集两侧不续(逗/分号按散文截断, +// 与 token 提取停集同源); +// ③ 面入工件律:证据面取自轮工件(d1-scan 快照),不读活仓文件——回放可回算 +// (G_VERIFY 快照判例同源)。 +// 与 T3 discover-candidate.mjs containsExactString 的分工:彼为「指针所指值逐字在 +// 工件集合在场」(深度值等价);此为「quote 文本在单证据面逐字 span」——两律同源 +// (皆零规范化),作用面不同。 + +import { evidencePtrForm, resolveEvidencePtr } from "./discover-candidate.mjs"; // 形律/指针解析=T3 导出面复用(单源);containsExactString 未导出→此处同律复刻 +import { ARTIFACTS } from "../artifacts.mjs"; + +// 指针形分类(#208 T2 统一律):T3 两形(url/artifact)优先;其 null 再验透传形(scan)—— +// 输入候选原样回传的 repo 相对路径/行锚(NEEDS.md:L14、backend/ipdb/_sources/x.py)。 +// T3 discover-candidate.mjs 的两形闭集不动(2026-10-26 #208 处置裁定:彼=独立候选面必填律, +// 此=发现轮回引面透传律);透传形在 T3 侧归 null 形,在此归 scan——两闭集分立非冲突。 +const SCAN_PTR_RE = /^[A-Za-z0-9_][A-Za-z0-9_.\/-]*(?::L\d+)?$/; // 无空白/无 #(否则前两形已收);可带 :L<行号> 行锚尾 +function ptrForm(ptr) { + const f = evidencePtrForm(ptr); + if (f) return f; + if (typeof ptr === "string" && SCAN_PTR_RE.test(ptr)) return "scan"; + return null; +} + +const URL_TOKEN_RE = /https?:\/\/[^\s"'<>()[\]{},;]+/g; // token 停集=空白/引号/括类/中西逗分号 +const URL_CHAR = /[A-Za-z0-9._~:/?#\[\]@!$&'()*+=%-]/; // RFC3986 保留+未保留;逗/分号除外(散文定界) + +// containsExactString:与 T3 discover-candidate.mjs 私有同名函数同律复刻(深度逐字值搜, +// 零规范化/零前缀匹配——幽灵 URL 判例:半真 URL 不算在场)。T3 侧私有未导出(2026-10-26 +// #208 处置裁定:T3 文件不动,同律单源锚定此处)。 +function containsExactString(v, s) { + if (v === s) return true; + if (Array.isArray(v)) return v.some((x) => containsExactString(x, s)); + if (v && typeof v === "object") return Object.values(v).some((x) => containsExactString(x, s)); + return false; +} + +// URL token 在证据面是否「定界在场」(任一次出现两侧皆非 URL 续字符) +function urlDelimited(hay, tok) { + let i = hay.indexOf(tok); + while (i !== -1) { + const before = i === 0 ? "" : hay[i - 1]; + const after = hay[i + tok.length] ?? ""; + if (!URL_CHAR.test(before) && !URL_CHAR.test(after)) return true; + i = hay.indexOf(tok, i + 1); + } + return false; +} + +// spanMatcher(quote, evidenceText) → bool:quote 是否为证据面的合法回引。 +// 拒:幽灵 URL / 半真 URL(前缀子串、超集)/ 大小写或标点改写 / 非串或空 quote。 +export function spanMatcher(quote, evidenceText) { + if (typeof quote !== "string" || quote.length === 0 || typeof evidenceText !== "string") return false; + if (!evidenceText.includes(quote)) return false; // 逐字律(第一律:includes 是必要条件) + const toks = quote.match(URL_TOKEN_RE) ?? []; + for (const tok of toks) if (!urlDelimited(evidenceText, tok)) return false; // URL 边界律(第二律) + return true; +} + +// canonicalJson:键序稳定序列化(容器面=text 键缺省时的确定性证据面;SCAN 确定性判例同源) +export function canonicalJson(v) { + if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null"; + if (Array.isArray(v)) return "[" + v.map(canonicalJson).join(",") + "]"; + const keys = Object.keys(v).sort(); + return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(v[k])).join(",") + "}"; +} + +// entryFace(scan 条目)→ 证据面文本:needs/web 条目带 text(原文行快照)优先; +// 缺省=条目 canonicalJson(结构性面,registry 面=此形)。 +function entryFace(entry) { + if (typeof entry?.text === "string" && entry.text.length > 0) return entry.text; + return canonicalJson(entry); +} + +// scanEvidenceFace(scan, ptr) → {ok:true,text,entry} | {ok:false,reason} +// 三形指针(T2 #208 统一律,prompt 透传契约同源): +// ① 透传形(scan 形,如 NEEDS.md:L14 / backend/ipdb/_sources/x.py):条目 evidence_ptr +// 逐字等于 ptr → 该条目面; +// ② 工件形(d1-scan.json#/candidates/N):工件名必须=d1-scan,walkPointer 后取所指值 +// (值=串则原样,容器则条目面/canonicalJson); +// ③ URL 形:URL 须在宇宙某条目逐字在场(T3 containsExactString 同律)→ 该条目面。 +export function scanEvidenceFace(scan, ptr) { + if (!scan || !Array.isArray(scan.candidates)) return { ok: false, reason: "d1-scan 候选宇宙不可读" }; + if (typeof ptr !== "string" || ptr.length === 0) return { ok: false, reason: "指针为空" }; + const form = ptrForm(ptr); + if (form === "scan") { + const entry = scan.candidates.find((c) => c && c.evidence_ptr === ptr); + if (!entry) return { ok: false, reason: `透传指针 ${ptr} 不在 d1-scan 候选宇宙(幽灵指针)` }; + return { ok: true, text: entryFace(entry), entry }; + } + if (form === "artifact") { + const file = ptr.split("#")[0]; + if (file !== ARTIFACTS.DISCOVER_SCAN) return { ok: false, reason: `工件形指针须指 ${ARTIFACTS.DISCOVER_SCAN}(得 ${file})` }; + const r = resolveEvidencePtr(ptr, { [ARTIFACTS.DISCOVER_SCAN]: scan }); + if (!r.ok) return { ok: false, reason: `指针解析失败:${r.reason}` }; + if (typeof r.value === "string" && r.value.length > 0) return { ok: true, text: r.value, entry: null }; + const entry = scan.candidates.find((c) => canonicalJson(c) === canonicalJson(r.value)); + return { ok: true, text: entry ? entryFace(entry) : canonicalJson(r.value), entry: entry ?? null }; + } + if (form === "url") { + const entry = scan.candidates.find((c) => containsExactString(c, ptr)); + if (!entry) return { ok: false, reason: `URL ${ptr} 未在 d1-scan 候选宇宙逐字在场(幽灵 URL)` }; + return { ok: true, text: entryFace(entry), entry }; + } + return { ok: false, reason: `指针三形外:${ptr}` }; +} diff --git a/ops/agent/pipeline/discover-candidate.mjs b/ops/agent/pipeline/discover-candidate.mjs new file mode 100644 index 00000000..02bc5ac4 --- /dev/null +++ b/ops/agent/pipeline/discover-candidate.mjs @@ -0,0 +1,167 @@ +// pipeline/discover-candidate.mjs — 发现轮结构化候选面(票 #209/T3,ADR 0010;spec +// /tmp/discover-pipeline-spec.md T3 节)。三件:①candidate 形状校验(zod-free 手写 +// 校验器——仓律零新 npm 依赖;SHORTLIST 模型产出的结构化候选必须过形才进下游) +// ②证据存在性校验(evidence_ptr 指向的工件键/URL 必须在给定工件集合真在场,缺= +// 候选丢弃+留痕清单——哲学③宁少算:证据不在场的候选不修不猜,只丢并留痕) +// ③候选卡行写入器(queue.md 三写口之一:action=PROPOSE 候选卡行,走 #139 flock +// 路径 appendQueueRowLocked;表头首行句 #217 T3.5 已翻正三写口如实描述—— +// 「queue_action 唯一写口」旧句随本写口+ENQUEUE 候选行两新写口退役; +// #0006 人裁交互律不动——只写不动作,开票/接源全人裁)。 +// 消费方:T1(发现轮管线骨架,并行工位)SHORTLIST/ENQUEUE 步;接线协议见 worktree +// 根 DP-T3-REPORT.md。本文件零仓依赖(queue-lock 是 tools/ 自含零 node_modules 面)。 +import { createHash } from "node:crypto"; +import { appendQueueRowLocked } from "../tools/queue-lock.mjs"; + +// 候选字段闭集(spec T3 节钉死的七键;未知键=拒——模型输出漂移要在形状层响,不静默吞) +export const CANDIDATE_FIELDS = Object.freeze( + ["name", "url", "source_channel", "quote", "evidence_ptr", "exclusivity_estimate", "notes"]); +// 来源渠道闭集=SCAN 三来源(spec T1 节:源注册表现状/NEEDS.md 缺口行/外部目录面)。 +// 协议面:与 T1 d1-scan.json 条目的来源渠道字段同闭集——T1/T3 合流时若 SCAN 扩来源, +// 此闭集与 T1 侧同批扩(单源裁决留合流批,此处先钉三员)。 +export const SOURCE_CHANNELS = Object.freeze(["registry", "needs", "web"]); +// 长度帽(表格/工件面防失控;与工具面 url maxLength=500 同量级) +export const CANDIDATE_MAX = Object.freeze({ name: 200, url: 500, quote: 1000, notes: 2000, evidence_ptr: 600 }); + +// evidence_ptr 两形(spec T3:指向工件键/URL): +// artifact 形:<工件名># —— 工件名=裸文件名(无路径分隔),指针 / 起步、 +// 逐段走对象键/数组下标(RFC6901-lite,~0/~1 转义支持);不收空指针(整工件指认 +// 无法对账到具体证据条目——宁少算)。 +// url 形:http(s) URL —— 必须在给定工件集合里逐字在场(防幽灵 URL;span 级语义 +// 面在 T2 spanMatcher,T3 只管「在场」存在性)。 +const URL_RE = /^https?:\/\/\S+$/; +const ARTIFACT_PTR_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*#\/.*$/; +export function evidencePtrForm(ptr) { + if (typeof ptr !== "string" || ptr.length > CANDIDATE_MAX.evidence_ptr) return null; + if (URL_RE.test(ptr)) return "url"; + if (ARTIFACT_PTR_RE.test(ptr)) return "artifact"; + return null; +} + +// 手写校验器(零依赖;返回 {ok:true,candidate} | {ok:false,code:"shape",reason})。 +// notes 可缺省(备注非证据);其余六键必填。错型/缺键/闭集外/未知键=拒,reason 点名。 +export function validateCandidate(c) { + const no = (reason) => ({ ok: false, code: "shape", reason }); + if (c == null || typeof c !== "object" || Array.isArray(c)) return no("candidate 必须是非数组对象"); + const unknown = Object.keys(c).filter((k) => !CANDIDATE_FIELDS.includes(k)); + if (unknown.length) return no(`未知键 ${unknown.join(",")}(闭集:${CANDIDATE_FIELDS.join("/")})`); + for (const k of ["name", "url", "source_channel", "quote", "evidence_ptr"]) { + if (typeof c[k] !== "string" || !c[k].trim()) return no(`${k} 缺失或非非空字符串`); + } + if (c.name.length > CANDIDATE_MAX.name) return no(`name 超 ${CANDIDATE_MAX.name} 字`); + if (!URL_RE.test(c.url) || c.url.length > CANDIDATE_MAX.url) return no(`url 须 http(s) 形且 ≤${CANDIDATE_MAX.url} 字`); + if (!SOURCE_CHANNELS.includes(c.source_channel)) + return no(`source_channel 必须在闭集内:${SOURCE_CHANNELS.join("/")}`); + if (c.quote.length > CANDIDATE_MAX.quote) return no(`quote 超 ${CANDIDATE_MAX.quote} 字(span 级回引面=T2)`); + if (!evidencePtrForm(c.evidence_ptr)) + return no("evidence_ptr 须两形之一:<工件名># 或 http(s) URL"); + const ex = c.exclusivity_estimate; + if (typeof ex !== "number" || !Number.isFinite(ex) || ex < 0 || ex > 1) + return no("exclusivity_estimate 须为 [0,1] 数(入队硬门 ≥0.25 的裁决在 T1 ENQUEUE,不在此)"); + if (c.notes !== undefined && (typeof c.notes !== "string" || c.notes.length > CANDIDATE_MAX.notes)) + return no(`notes 须字符串且 ≤${CANDIDATE_MAX.notes} 字(可缺省)`); + return { ok: true, candidate: c }; +} + +// 批面:形状分拣(valid 进下游,dropped 留痕——与证据存在性同律:只丢不修)。 +export function validateCandidates(list) { + const valid = [], dropped = []; + (Array.isArray(list) ? list : []).forEach((c, i) => { + const r = validateCandidate(c); + if (r.ok) valid.push(c); + else dropped.push({ index: i, name: c?.name ?? null, url: c?.url ?? null, reason: r.reason }); + }); + return { valid, dropped }; +} + +// ── 证据存在性(纯函数)────────────────────────────────────────────────── +// artifacts: { <工件名>: 已解析 JSON } —— 调用方(T1)从工件树读入后传入;本函数 +// 零 IO(回放可算)。 +function walkPointer(doc, ptr) { + let cur = doc; + if (ptr === "") return { ok: true, value: cur }; + for (const raw of ptr.slice(1).split("/")) { + const seg = raw.replace(/~1/g, "/").replace(/~0/g, "~"); + if (cur == null || typeof cur !== "object") return { ok: false, reason: `指针段 «${seg}» 前已非容器` }; + if (Array.isArray(cur)) { + if (!/^\d+$/.test(seg) || Number(seg) >= cur.length) return { ok: false, reason: `数组下标 «${seg}» 不在场` }; + cur = cur[Number(seg)]; + } else { + if (!Object.prototype.hasOwnProperty.call(cur, seg)) return { ok: false, reason: `键 «${seg}» 不在场` }; + cur = cur[seg]; + } + } + return { ok: true, value: cur }; +} +function containsExactString(v, s) { // 深度逐字值搜(不做规范化/前缀匹配——幽灵 URL 判例:半真 URL 不算在场) + if (v === s) return true; + if (Array.isArray(v)) return v.some((x) => containsExactString(x, s)); + if (v && typeof v === "object") return Object.values(v).some((x) => containsExactString(x, s)); + return false; +} +export function resolveEvidencePtr(ptr, artifacts) { + const form = evidencePtrForm(ptr); + if (!form) return { ok: false, reason: "evidence_ptr 形状坏(须 <工件名># 或 http(s) URL)" }; + if (form === "url") { + const hit = Object.keys(artifacts ?? {}).some((f) => containsExactString(artifacts[f], ptr)); + return hit ? { ok: true } : { ok: false, reason: `URL 未在给定工件集合逐字在场:${ptr}` }; + } + const h = ptr.indexOf("#"); + const file = ptr.slice(0, h), rest = ptr.slice(h + 1); + if (!Object.prototype.hasOwnProperty.call(artifacts ?? {}, file)) + return { ok: false, reason: `工件不在集合:${file}` }; + const w = walkPointer(artifacts[file], rest); + return w.ok ? { ok: true, value: w.value } : { ok: false, reason: `${file} 内指针 ${rest} 不在场:${w.reason}` }; +} +// 证据存在性过滤:缺=丢+留痕(dropped 行带 index/name/url/evidence_ptr/reason, +// 供 T1 ENQUEUE 工件与 G_ENQUEUE 对账面消费——证据永不灭失)。 +export function filterCandidatesByEvidence(candidates, artifacts) { + const kept = [], dropped = []; + (Array.isArray(candidates) ? candidates : []).forEach((c, i) => { + const r = resolveEvidencePtr(c?.evidence_ptr, artifacts); + if (r.ok) kept.push(c); + else dropped.push({ index: i, name: c?.name ?? null, url: c?.url ?? null, + evidence_ptr: c?.evidence_ptr ?? null, reason: r.reason }); + }); + return { kept, dropped }; +} + +// ── 候选卡行(ENQUEUE 面行形制)─────────────────────────────────────────── +// action=PROPOSE(spec T3 钉「status=proposal」):与 queue_action 工具的 ENQUEUE/REJECT +// 闭集分立——那两词是运营轮已测量源的动作词(其语义门独占 exclusive_probe 实测); +// 候选卡=发现轮 pre-验证提案(排他率是预估非实测)。#0006 人裁交互律:只写不动作, +// 人裁否决=删行(veto_cmd 同形)。queue.md 冻结表头不动;表头注释「queue_action 唯一 +// 写口」措辞自本写入器起过时——表头是跨语言双源冻结形状(#139 哨对拍),改词须 +// python 侧同批;留 T4/合流批裁决(见 DP-T3-REPORT.md 余项)。 +export const CANDIDATE_ACTION = "PROPOSE"; + +// source 列(slug):host slug + url md5 前 6 位。判重键=action+source(appendQueueRowCore +// 冻结语义)→ 同候选跨轮幂等(重提案=dedup 跳过),同 host 异 url 候选不互撞。 +export function candidateSourceSlug(url) { + let host = ""; + try { host = new URL(url).hostname; } catch { /* 形状面已拦,兜底哈希支住唯一性 */ } + const slug = host.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 20) || "site"; + const hash = createHash("md5").update(String(url)).digest("hex").slice(0, 6); + return `${slug}_${hash}`; +} +// 卡行 evidence 面=指针卡字段(全文候选+quote 在 shortlist 工件;行=人裁索引,不重复 +// 全文——宁少算)。call_id 命名空间 "candidate":候选卡的证据是工件指针不是本会话 +// 测量调用,evidence_ptr 即指针。 +export function candidateCardEvidence(candidate) { + return ["name", "url", "source_channel", "exclusivity_estimate", "evidence_ptr"] + .map((f) => ({ call_id: "candidate", field: f, value: candidate[f] })); +} +// 写入器:先形状校验(坏候选=零写,宁少算),后走 #139 flock 路径(与 python +// append_queue 同锁域互斥);幂等由 appendQueueRowCore 判重面收口。 +export async function appendCandidateCard(candidate, { round, queueFile } = {}) { + if (!round || typeof round !== "string") return { ok: false, code: "shape", reason: "round 缺失(发现轮 round-id)" }; + if (!queueFile || typeof queueFile !== "string") return { ok: false, code: "shape", reason: "queueFile 缺失" }; + const v = validateCandidate(candidate); + if (!v.ok) return { ok: false, code: "shape", reason: `候选卡拒写(形状):${v.reason}` }; + return appendQueueRowLocked({ + queueFile, + action: CANDIDATE_ACTION, + source: candidateSourceSlug(candidate.url), + round, + evidence: candidateCardEvidence(candidate), + }); +} diff --git a/ops/agent/pipeline/discovery-host.mjs b/ops/agent/pipeline/discovery-host.mjs new file mode 100644 index 00000000..162b2ddb --- /dev/null +++ b/ops/agent/pipeline/discovery-host.mjs @@ -0,0 +1,248 @@ +// pipeline/discovery-host.mjs — 发现轮 web 三工具宿主(票 #209/T3,ADR 0010;spec +// T3 节):SHORTLIST/MEASURE 步的 web_read/web_search/rss_parse 注入面,microprobes +// (DEEP 真面,#173)同形制——工具=pi-agent-core AgentTool 面(initialState.tools 直装), +// seeds=chronological 登记行 {call_id,tool,sc}(faux.mjs 发 registry_seed 行,G_* 门 +// 对账面)。工具实现零复制:discoveryMethods(tools/discovery.mjs,#179)经 Object.assign +// 挂适配器复用——真网络/解析/信封面单源;守卫链 dup→breaker→cap 与 tools.mjs +// ToolHost.beforeToolCall 同序同义(自决已录,#199 B 判例),被拒=零执行零登记(microprobes +// 头注律)但入 blocked 观察面。配额一等公民:通道分列计数 host 内建(channelCounts), +// 纯聚合 aggregateChannelCalls(种子行→计数,回放/对账可算)+构造器 +// discoverChannelReadings(T1 CLOSE 进 09-budget.json readings 用;breaker 态快照沿用 +// steps.mjs 既有 readings.breaker 面,不重造)。 +// 接线协议(T1/T3 合流面,详见 worktree 根 DP-T3-REPORT.md): +// mkDiscoveryHost({dupRegistry, dupLimit, budgets, breaker, methods}) → {tools, seeds, channelCounts, call} +// - SHORTLIST(模型步):tools 直装 initialState.tools;dupRegistry 传 ctx 级 Map( +// 与模型调用同 registry,dup 判例 #199 A4 同律);breaker 传轮级 mkBreaker() 实例。 +// - MEASURE(确定性步,零模型):host.call(name, args) 宿主补发 measure-N call-id +// 直调(执行同样过守卫链、同样登记 seed)。 +// - CLOSE:readings 面 discover_channels=discoverChannelReadings(host.channelCounts) +// (键名由 T1 定,构造器只出值);breaker 快照仍走既有 readings.breaker。 +import fs from "node:fs"; +import path from "node:path"; +import { Type } from "@earendil-works/pi-ai"; +import { discoveryMethods } from "../tools/discovery.mjs"; +import { BREAKER_CHANNELS, mkBreaker, readBreakerKnobs } from "../tools/breaker.mjs"; +import { B } from "../tools/shared.mjs"; +import { dupRecord } from "./budget.mjs"; +import { ARTIFACTS as A, DISCOVER_SHORTLIST_TRANSCRIPT } from "../artifacts.mjs"; // 工件名单源(C1 判例:回放读面禁手抄磁盘名) + +// 发现宿主工具闭集(=BREAKER_CHANNELS 的 web 三员;gh_search 留 legacy 面,spec T3) +export const DISCOVERY_TOOLS = Object.freeze(["web_read", "web_search", "rss_parse"]); +// 每轮帽缺省=tools/shared.mjs B 常量单源(与 legacy discover 轮 ToolHost budgets 同源, +// tools.mjs:31——两宿主同一事实,不另立数字);T1 合流时如需 OPS_DISCOVER_* 旋钮化, +// 经 opts.budgets 注入,缺省面不动。 +export const DEFAULT_DISCOVERY_BUDGETS = Object.freeze({ + web_read: B.webReadPerRound, + web_search: B.webSearchPerRound, + rss_parse: B.rssParsePerRound, +}); + +export function mkDiscoveryHost({ + dupRegistry = new Map(), + dupLimit = 3, + budgets = DEFAULT_DISCOVERY_BUDGETS, + breaker = null, // mkBreaker() 产物(可选;管线面由 T1 传轮级实例,测试可注假面) + methods = discoveryMethods, // 测试缝:注入假实现面(真面走真网络/spawn 不可离线测) +} = {}) { + const seeds = []; + const channelCounts = {}; // {[tool]: {executed, ok, failed, blocked}} 惰性建 + const cell = (t) => channelCounts[t] ?? (channelCounts[t] = { executed: 0, ok: 0, failed: 0, blocked: 0 }); + let seq = 0; + + const reject = (callId, tool, code, reason) => { // 被拒=零执行零登记;入 blocked 观察面 + cell(tool).blocked += 1; + return { + content: [{ type: "text", text: `[call_id=${callId}] 拒绝信封(code=${code}):${reason}` }], + structuredContent: { ok: false, code, reason: String(reason).slice(0, 200) }, + }; + }; + // 适配器:this.finish=登记收口(失败信封也登记——被引时门内识别 ok:false)+ + // 通道计数+熔断回写+call_id 上翻(tools.mjs finish 同判例:模型看不到运行时 id) + const adapter = { + finish(callId, name, structured, contentText) { + seeds.push({ call_id: callId, tool: name, sc: structured }); + const c = cell(name); + c.executed += 1; + if (structured?.ok === true) c.ok += 1; else c.failed += 1; + if (breaker && BREAKER_CHANNELS.includes(name) && structured && typeof structured.ok === "boolean") + breaker.record(name, { ok: structured.ok, code: structured.code }); + return { content: [{ type: "text", text: `[call_id=${callId}] ${contentText}` }], structuredContent: structured }; + }, + }; + Object.assign(adapter, methods); + // 守卫链(与 tools.mjs beforeToolCall 同序:dup→breaker→cap;被拦零执行零花费不占帽) + const guardDup = (name, p) => { + if (!(dupLimit > 0)) return null; + const rec = dupRecord(dupRegistry, name, p, dupLimit); + return rec.blocked ? { code: "repeat_storm", + reason: `同参调用第 ${rec.count} 次≥拦截线 ${dupLimit}(OPS_PIPELINE_DUP_CALL_LIMIT);同参数重发不会出新读数——改参数(换 URL/检索词)或升级人裁` } : null; + }; + const guardBreaker = (name) => { + if (!breaker || !BREAKER_CHANNELS.includes(name)) return null; + const v = breaker.verdict(name); + return v.action === "block" ? { code: "channel_open", + reason: `通道 ${name} 熔断开断中(冷却剩 ${v.probe_in_flight ? "探活在飞" : `${v.retryAfterS}s`});开断期该通道零调用——改用本地读面或升级人裁` } : null; + }; + const guardCap = (name) => { + const max = budgets[name]; + if (max !== undefined && cell(name).executed >= max) + return { code: "budget", reason: `预算超限:${name} 每轮上限 ${max} 次已用满;升级人裁区` }; + return null; + }; + const METHOD_OF = Object.freeze({ web_read: "webRead", web_search: "webSearch", rss_parse: "rssParse" }); + const wrap = (tool) => ({ + ...tool, + execute: async (callId, p) => { + const dj = guardDup(tool.name, p); if (dj) return reject(callId, tool.name, dj.code, dj.reason); + const bj = guardBreaker(tool.name); if (bj) return reject(callId, tool.name, bj.code, bj.reason); + const cj = guardCap(tool.name); if (cj) return reject(callId, tool.name, cj.code, cj.reason); + // this=adapter(实现面 this.finish 收口;箭头 execute 词法 this 已绑,常规函数面 + // .call 兜底——注入协议要求 methods 为适配器方法形) + return tool.execute.call(adapter, callId, p); + }, + }); + const tools = DISCOVERY_TOOLS.map((n) => wrap(adapter[METHOD_OF[n]]())); + // MEASURE 侧确定性直调(T1 占位对齐):宿主补发 measure-N(与运行时模型 call-id + // 不撞);同样过守卫链、同样登记。 + async function call(name, args) { + const tool = tools.find((x) => x.name === name); + if (!tool) throw new Error(`未知工具 ${name}(发现宿主闭集:${DISCOVERY_TOOLS.join("/")})`); + return tool.execute(`measure-${(seq += 1)}`, args); + } + return { tools, seeds, channelCounts, call }; +} + +// ── 接线面(#217 T3.5;头注接线协议的落地消费点)──────────────────────── +// breaker 态文件路径单源:CLOSE 观察快照(steps.mjs)与发现宿主 record 读写共用同一文件 +// ——两处各自手拼=漂移即双态(一文件一拼)。ctx 面与 steps.mjs CLOSE 原判例同形。 +export const breakerFileOf = (ctx) => path.join(ctx.opsRoot ?? path.dirname(ctx.dir), "breaker.json"); + +// 轮级发现宿主单例:惰性建于 ctx.discoverHost——SHORTLIST(真面工具直装 initialState.tools) +// 与 MEASURE(host.call 补发 measure-N)共用同一实例=同守卫链同配额面。dupRegistry=ctx 级 +// Map(与模型调用同 registry,dup 判例 #199 A4 同律);dupLimit=发现旋钮 dupCallLimit +// (readDiscoverKnobs,#217 T3.5 追键);breaker=轮级 mkBreaker() 绑 ops/breaker.json(与 +// CLOSE 快照同文件同旋钮)。resume 后宿主缺席=常态(已录步跳过),CLOSE 读数走工件回放 +// (replayDiscoverSeeds)。测试缝:methods 注假实现面(缺省真面=网络/spawn 不可离线测)。 +export function ensureDiscoverHost(ctx, { methods = discoveryMethods } = {}) { + if (ctx.discoverHost) return ctx.discoverHost; + ctx.discoverHost = mkDiscoveryHost({ + dupRegistry: ctx.dupRegistry ?? new Map(), + dupLimit: ctx.budgetKnobs?.dupCallLimit ?? 3, + breaker: mkBreaker({ + file: breakerFileOf(ctx), env: ctx.env ?? {}, round: ctx.roundId ?? "discover", + knobs: ctx.breakerKnobs ?? readBreakerKnobs(ctx.env ?? {}), ledgerFile: ctx.ledgerFile, log: ctx.log, + }), + methods, + }); + return ctx.discoverHost; +} + +// faux 罐装方法面(零网络律):fixtures measure.json web_reads 表→web_read 罐装读数 +// (未罐装 URL=io_error 未罐装信封;罐 ok:false 行=罐装失败面,供测试确定性模拟死链/鉴权); +// web_search/rss_parse faux 面恒 io_error 未罐装——faux 宇宙只罐 MEASURE 消费的读面, +// 不罐不虚构。方法形状与 discoveryMethods 同构(name/label/description/annotations/ +// parameters/execute;this.finish 收口同注入协议)。 +export function fixtureDiscoveryMethods(fx = {}) { + const reads = new Map((Array.isArray(fx.web_reads) ? fx.web_reads : []).map((r) => [String(r?.url ?? ""), r])); + const miss = (code, reason) => ({ ok: false, code, reason }); + const cannedFail = (r, url) => ({ ok: false, code: String(r?.code ?? "channel_missing"), reason: String(r?.reason ?? "faux 罐装失败面(measure.json web_reads ok:false)"), url, trace: { channel: "faux", status: Number(r?.status ?? 0) } }); + const uncanned = (url) => ({ ok: false, code: "io_error", reason: `faux 未罐装该 URL(零网络律;fixtures measure.json web_reads 补)`, url, trace: { channel: "faux", status: 0 } }); + // 注意方法形:this=适配器(Object.assign 挂 adapter 后经 adapter.() 调用, + // execute 箭头捕获方法 this——与 discoveryMethods 同注入协议,勿改成脱对象工厂函数; + // notCannedTool 须常规函数:.call(this) 传导 adapter 进闭包(箭头函数 .call 无效) + function notCannedTool(tool) { + return { + name: tool, label: `${tool}(faux 未罐装)`, + description: "faux 面恒 io_error 未罐装(零网络;MEASURE 面不消费本工具)", + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + parameters: Type.Object({ query: Type.String({ maxLength: 500 }) }), + execute: async (callId) => this.finish(callId, tool, miss("io_error", `faux 未罐装 ${tool}(零网络律)`), + `${tool} 失败信封(faux):io_error: 未罐装`), + }; + } + return { + webRead() { + return { + name: "web_read", label: "网页正文读(faux 罐装)", + description: "faux 面罐装读数(measure.json web_reads 表;未罐装=io_error,罐 ok:false=罐装失败面;零网络)", + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + parameters: Type.Object({ url: Type.String({ pattern: "^https?://", maxLength: 500 }) }), + execute: async (callId, p) => { + const r = reads.get(p.url); + if (!r) return this.finish(callId, "web_read", uncanned(p.url), `web_read 失败信封(faux):io_error: 未罐装 ${p.url}`); + if (r.ok !== true) return this.finish(callId, "web_read", cannedFail(r, p.url), `web_read 失败信封(faux):${r.code}: ${r.reason}`); + const content = String(r.content ?? ""); + return this.finish(callId, "web_read", + { ok: true, url: p.url, title: r.title ?? null, content, chars_n: content.length, truncated: Boolean(r.truncated ?? false), trace: { channel: "faux", status: 200 } }, + `读(faux 罐装) ${p.url};title=${r.title ?? "(无)"};chars=${content.length}\n${content}`); + }, + }; + }, + webSearch() { return notCannedTool.call(this, "web_search"); }, + rssParse() { return notCannedTool.call(this, "rss_parse"); }, + }; +} + +// 工件回放重算(证据永不灭失):宿主缺席(resume 后 MEASURE 已录跳过)时 CLOSE 读数面的 +// 回放源=d2 transcript registry_seed 行(真面 SHORTLIST 工具调用留痕)+ d3-measure.json +// tool_seeds(MEASURE 补发留痕)。被拒不入种子(头注律)→回放面恒宁少算(blocked 观察面 +// 不回放);坏行跳过不掷错(回放是补面非门面——门面吞坏行才是假读数,回放面宁少算)。 +// 工具名过滤=DISCOVERY_TOOLS 闭集(健康轮 probe 工具不混入)。 +export function replayDiscoverSeeds(ctx) { + const rows = []; + try { + for (const line of fs.readFileSync(path.join(ctx.dir, DISCOVER_SHORTLIST_TRANSCRIPT), "utf8").split("\n")) { + if (!line.trim()) continue; + let o; try { o = JSON.parse(line); } catch { continue; } // 坏行跳过(宁少算) + if (o?.type === "registry_seed" && DISCOVERY_TOOLS.includes(o.tool) && o.sc && typeof o.sc.ok === "boolean") + rows.push({ tool: o.tool, ok: o.sc.ok }); + } + } catch { /* transcript 缺席=零真面工具调用(faux 轮常态) */ } + let m = null; + try { m = JSON.parse(fs.readFileSync(path.join(ctx.dir, A.DISCOVER_MEASURE), "utf8")); } catch { /* 缺席=步未达 */ } + if (m && Array.isArray(m.tool_seeds)) + for (const s of m.tool_seeds) + if (s && DISCOVERY_TOOLS.includes(s.tool) && s.sc && typeof s.sc.ok === "boolean") rows.push({ tool: s.tool, ok: s.sc.ok }); + return rows; +} + +// 纯聚合:种子行(registry_seed 面 {tool, sc:{ok}})或扁平行 {tool, ok} → 通道分列 +// 调用计数。只聚已执行面(被拒不入种子,宁少算);行形状坏(缺 tool/ok 非布尔)掷错 +// 不静默吞——本面供 G_ENQUEUE/RETRO 对账与 CLOSE 回放重算,吞坏行=假读数。 +export function aggregateChannelCalls(rows) { + const out = {}; + for (const r of rows ?? []) { + if (!r || typeof r.tool !== "string") + throw new Error(`计数行缺 tool:${JSON.stringify(r)?.slice(0, 120)}`); + const ok = typeof r.ok === "boolean" ? r.ok + : r.sc && typeof r.sc.ok === "boolean" ? r.sc.ok : undefined; + if (ok === undefined) + throw new Error(`计数行 ${r.tool} 的 ok 非布尔(种子行须带 sc.ok):${JSON.stringify(r)?.slice(0, 120)}`); + const c = out[r.tool] ?? (out[r.tool] = { calls: 0, ok: 0, failed: 0 }); + c.calls += 1; + if (ok) c.ok += 1; else c.failed += 1; + } + return out; +} + +// 09-budget.json readings 构造器(T1 CLOSE 消费):host 面 channelCounts(executed+ +// blocked)或 aggregateChannelCalls 输出(calls,无 blocked)皆可入——两词一义=已执行。 +// 不变量 ok+failed=calls 违者/非非负整数=掷错(构造期响,不落错读数进工件)。 +// breaker 态快照沿用 steps.mjs 既有 readings.breaker 面——本构造器不重造(票面明示)。 +export function discoverChannelReadings(channelCounts) { + const channels = {}; + let calls_total = 0, blocked_total = 0; + for (const [tool, raw] of Object.entries(channelCounts ?? {})) { + const c = raw ?? {}; + const calls = c.calls ?? c.executed ?? 0; + const ok = c.ok ?? 0, failed = c.failed ?? 0, blocked = c.blocked ?? 0; + if (![calls, ok, failed, blocked].every((n) => Number.isInteger(n) && n >= 0)) + throw new Error(`通道 ${tool} 计数非非负整数:${JSON.stringify(c)}`); + if (ok + failed !== calls) + throw new Error(`通道 ${tool} 不变量破:ok(${ok})+failed(${failed})≠calls(${calls})`); + channels[tool] = Object.freeze({ calls, ok, failed, blocked }); + calls_total += calls; + blocked_total += blocked; + } + return Object.freeze({ channels: Object.freeze(channels), calls_total, blocked_total }); +} diff --git a/ops/agent/pipeline/faux.mjs b/ops/agent/pipeline/faux.mjs index 83e6454c..2bb47536 100644 --- a/ops/agent/pipeline/faux.mjs +++ b/ops/agent/pipeline/faux.mjs @@ -20,6 +20,8 @@ import path from "node:path"; import crypto from "node:crypto"; import { loadModels } from "../models.mjs"; import { mkMicroprobeHost } from "./microprobes.mjs"; +import { ensureDiscoverHost } from "./discovery-host.mjs"; // #217 T3.5:SHORTLIST 真面工具注入(发现宿主=ctx 级单例,与 MEASURE 同守卫链同配额面) +import { DISCOVER_SHORTLIST_TRANSCRIPT } from "../artifacts.mjs"; // 工件名单源(回放读面共用) import { hidSlug, modelIO } from "./steps.mjs"; const sha = (s) => crypto.createHash("sha256").update(s).digest("hex").slice(0, 16); @@ -32,11 +34,12 @@ export function selectModelFace({ fixturesExplicit = false, inherited = null } = // 步→models.mjs 轮键映射(#170):TRIAGE/SYNTH=health 轻模型(flash),DEEP=discover 重模型(glm-5.3); // env 旋钮 OPS_MODEL_HEALTH/OPS_MODEL_DISCOVER 与 legacy 轮同一拼写(models.mjs resolve 闭集 health|discover)。 -export const STEP_ROUND = { TRIAGE: "health", SYNTH: "health", DEEP: "discover" }; +export const STEP_ROUND = { TRIAGE: "health", SYNTH: "health", DEEP: "discover", SHORTLIST: "discover" }; // #207 T1:发现轮 SHORTLIST 同 OPS_MODEL_DISCOVER 键 const transcriptName = (def, item) => def.name === "TRIAGE" ? "03-triage-transcript.jsonl" : def.name === "DEEP" ? `05-deep-${hidSlug(item.hid)}-transcript.jsonl` - : def.name === "SYNTH" ? "07-synth-transcript.jsonl" : null; + : def.name === "SYNTH" ? "07-synth-transcript.jsonl" + : def.name === "SHORTLIST" ? DISCOVER_SHORTLIST_TRANSCRIPT : null; // #207 T1:发现轮模型步留痕(#217 T3.5 起名单源 artifacts.mjs——CLOSE 回放读面共用,不入 ARTIFACTS 固定表同 health transcript 面) function loadFixture(def, ctx, item) { // DEEP 逐假设 fixture(P4 #166 真开启):deep/.json,slug=hidSlug(hid);金轨迹轮=golden-track-canned。 @@ -112,7 +115,9 @@ async function runRealStep(def, ctx, item, input) { fetchImpl: ctx.probeFetch, dupRegistry: ctx.dupRegistry ?? new Map(), dupLimit: ctx.budgetKnobs?.dupCallLimit ?? 3 }) - : null; + : def.name === "SHORTLIST" + ? ensureDiscoverHost(ctx) // #217 T3.5:SHORTLIST 真面直装发现工具三件(头注接线协议;dupRegistry=ctx 级同 registry、breaker=轮级同文件——与 MEASURE 同宿主单例;faux 面工具恒 [] 不注入,罐装宇宙零网络) + : null; const agent = new Agent({ initialState: { systemPrompt: input.systemPrompt, model, tools: probes?.tools ?? [] }, streamFn: bundle.models.streamSimple.bind(bundle.models) }); const rec = { usage: { input: 0, output: 0, totalTokens: 0, cost: 0 }, messages: [] }; diff --git a/ops/agent/pipeline/gates.mjs b/ops/agent/pipeline/gates.mjs index cb0999f2..b60a4972 100644 --- a/ops/agent/pipeline/gates.mjs +++ b/ops/agent/pipeline/gates.mjs @@ -7,10 +7,11 @@ import fs from "node:fs"; import path from "node:path"; import { resolveField, closureCheck } from "../tools.mjs"; -import { normalizeRules, readInFlightItems, inFlightKeys, deepSeeds, RULE_PREDICATES, RULE_ACTIONS } from "./steps.mjs"; +import { normalizeRules, readInFlightItems, inFlightKeys, deepSeeds, RULE_PREDICATES, RULE_ACTIONS, LEDGER_TAGS, ENQUEUE_EXCLUSIVITY_GATE } from "./steps.mjs"; // LEDGER_TAGS/ENQUEUE_EXCLUSIVITY_GATE=步/门同源(#207 T1:台账标签与入队门槛单源) import { fullDedupKey } from "./ops-items.mjs"; // 全签同因面(P4 #166;步/门同源) import { parseItem } from "./ops-items.mjs"; // 条目树读回(单一解析语义) import { ARTIFACTS as A, DEEP_TRANSCRIPT_RE, DEEP_TRANSCRIPT_GLOB } from "../artifacts.mjs"; // 工件名表单源(#202 C1):门读面/inputs 声明禁手抄工件名(磁盘名字节冻结,位等价判例) +import { scanEvidenceFace, spanMatcher } from "./citation.mjs"; // #208 T2:证据面三形解析+span 逐字回引(G_SHORTLIST/G_MEASURE 消费) export const CLOSED_SETS = { cause_class: ["frozen_feed", "dead_endpoint", "auth_wall", "rate_limit", "probe_artifact", "matrix_violation", "unknown"], // C稿 §3.2 草案 1 @@ -262,11 +263,21 @@ export const GATES = { G_RETRO: (ctx) => { const key = ctx.retroAuditKey; // replay --dry 审原件;重执行审重放自产 const retroFile = path.join(ctx.opsRoot, "retro", `${key}.json`); - const inputs = [`retro/${key}.json`, `${A.ANCHOR_SWEEP}(#184 出口③摘要源)`, "(ledger)"]; + const isDiscover = ctx.type === "discover"; // #207 T1:发现轮 retro 体=计数面(无规则/条目 diff 面) + const inputs = [`retro/${key}.json`, ...(isDiscover ? [] : [`${A.ANCHOR_SWEEP}(#184 出口③摘要源)`]), "(ledger)"]; const r = j(retroFile); if (!r) return fail(`retro/${key}.json 不可解析`, inputs); if (r.round_id !== key) return fail(`retro.round_id=${r.round_id}≠${key}`, inputs); if (!Array.isArray(r.candidates)) return fail("retro.candidates 非数组(候选只产条目,永不自动生效)", inputs); + if (isDiscover) { // 发现轮面(#207 T1):计数回算+沙箱+台账标签;规则/候选/出口面不适用 + const cnt = r.counts ?? {}; + for (const k of ["scan_n", "shortlist_n", "measure_hold_n", "enqueue_n"]) + if (!Number.isFinite(cnt[k])) return fail(`retro.counts.${k} 缺席/非数字(计数面=d1-d4 工件回算)`, inputs); + if (r.sandbox_assert_pass !== true) return fail(`sandbox_assert rc≠0(${r.sandbox_assert_rc})`, inputs); + const ledger = txt(ctx.ledgerFile); + if (ledger === null || !ledger.includes(`${LEDGER_TAGS.discover}:${key}`)) return fail("台账读回缺本轮行(pipeline-discover)", inputs); + return pass(`发现轮 retro 计数面过(scan=${cnt.scan_n}/shortlist=${cnt.shortlist_n}/hold=${cnt.measure_hold_n}/enqueue=${cnt.enqueue_n});台账行读回(pipeline-discover);sandbox rc=0`, inputs); + } for (const c of r.candidates ?? []) { if (!c || typeof c !== "object" || !c.match || typeof c.match !== "object") return fail("候选缺 match 对象", inputs); const keys = Object.keys(c.match); @@ -293,7 +304,7 @@ export const GATES = { } else if (sweepArt && (!r.anchor_sweep || r.anchor_sweep.status !== "degraded")) return fail("retro.anchor_sweep 摘要未记巡检降级态(08 工件=失败信封;出口③机检)", inputs); const ledger = txt(ctx.ledgerFile); - if (ledger === null || !ledger.includes(`pipeline:${key}`)) return fail("台账读回缺本轮行", inputs); + if (ledger === null || !ledger.includes(`${LEDGER_TAGS[ctx.type ?? "health"]}:${key}`)) return fail("台账读回缺本轮行", inputs); return pass(`retro schema 过(${(r.candidates ?? []).length} 候选,到点复审 ${(r.rules_due_review ?? []).length},通道矛盾 ${(r.channel_conflicts ?? []).length});台账行读回;sandbox rc=0`, inputs); }, // G_CLOSE:信封单行 JSON 落 stdout(书挡门;重放不入 CLOSE) @@ -305,8 +316,15 @@ export const GATES = { if (JSON.parse(s).ok !== ctx.envelope.ok) return fail("信封不可往返解析", inputs); // #199 A5:预算四界回声哨——环内已执法(runModelStage/runStages 检查点,违界即停), // 门只对拍 09-budget.json 读数 ≤ 生效帽(防工件漂移/篡改;非二次执法)。 - const BUDGET_ENV_KEYS = ["OPS_PIPELINE_BUDGET_CNY", "OPS_PIPELINE_MAX_MODEL_CALLS", "OPS_PIPELINE_TOKEN_BUDGET", - "OPS_PIPELINE_WALL_CLOCK_S", "OPS_PIPELINE_SPEND_CAP_CNY", "OPS_PIPELINE_MONTHLY_CAP_CNY", "OPS_PIPELINE_DUP_CALL_LIMIT"]; + // #207 T1:发现轮 env 面=三旋钮+est+共享月帽(无 token 界;键集 per-type 与 steps 写面互锁)。 + const isDiscover = ctx.type === "discover"; + const BUDGET_ENV_KEYS = isDiscover + ? ["OPS_PIPELINE_BUDGET_CNY", "OPS_DISCOVER_MAX_MODEL_CALLS", "OPS_DISCOVER_WALL_CLOCK_S", "OPS_DISCOVER_SPEND_CAP_CNY", "OPS_PIPELINE_MONTHLY_CAP_CNY"] + : ["OPS_PIPELINE_BUDGET_CNY", "OPS_PIPELINE_MAX_MODEL_CALLS", "OPS_PIPELINE_TOKEN_BUDGET", + "OPS_PIPELINE_WALL_CLOCK_S", "OPS_PIPELINE_SPEND_CAP_CNY", "OPS_PIPELINE_MONTHLY_CAP_CNY", "OPS_PIPELINE_DUP_CALL_LIMIT"]; + const CAP = isDiscover // 读数≤帽对拍键 per-type(发现轮帽=OPS_DISCOVER_* 同源) + ? { calls: "OPS_DISCOVER_MAX_MODEL_CALLS", spend: "OPS_DISCOVER_SPEND_CAP_CNY", wall: "OPS_DISCOVER_WALL_CLOCK_S", month: "OPS_PIPELINE_MONTHLY_CAP_CNY" } + : { calls: "OPS_PIPELINE_MAX_MODEL_CALLS", spend: "OPS_PIPELINE_SPEND_CAP_CNY", wall: "OPS_PIPELINE_WALL_CLOCK_S", month: "OPS_PIPELINE_MONTHLY_CAP_CNY" }; const b = j(path.join(ctx.dir, A.BUDGET)); if (!b || !b.env || !b.readings) return fail(`${A.BUDGET} 缺席/形状坏(env+readings 必在场)`, inputs); const missEnv = BUDGET_ENV_KEYS.filter((k) => !(k in b.env) || !Number.isFinite(b.env[k])); @@ -315,16 +333,16 @@ export const GATES = { if (!r.usage || !Number.isFinite(r.usage.totalTokens) || !Number.isFinite(r.usage.cost) || !Number.isFinite(r.model_calls) || !Number.isFinite(r.wall_s) || !Array.isArray(r.dup_top3)) return fail("预算读数形状坏(usage/model_calls/wall_s/dup_top3)", inputs); - if (r.model_calls > b.env.OPS_PIPELINE_MAX_MODEL_CALLS) - return fail(`读数超帽:model_calls=${r.model_calls} > ${b.env.OPS_PIPELINE_MAX_MODEL_CALLS}(OPS_PIPELINE_MAX_MODEL_CALLS)`, inputs); - if (r.usage.totalTokens > b.env.OPS_PIPELINE_TOKEN_BUDGET) + if (r.model_calls > b.env[CAP.calls]) + return fail(`读数超帽:model_calls=${r.model_calls} > ${b.env[CAP.calls]}(${CAP.calls})`, inputs); + if (!isDiscover && r.usage.totalTokens > b.env.OPS_PIPELINE_TOKEN_BUDGET) // 发现轮无 token 界(不预设轨迹预算) return fail(`读数超帽:totalTokens=${r.usage.totalTokens} > ${b.env.OPS_PIPELINE_TOKEN_BUDGET}(OPS_PIPELINE_TOKEN_BUDGET)`, inputs); - if (r.usage.cost > b.env.OPS_PIPELINE_SPEND_CAP_CNY) - return fail(`读数超帽:cost=${r.usage.cost} > ${b.env.OPS_PIPELINE_SPEND_CAP_CNY}(OPS_PIPELINE_SPEND_CAP_CNY)`, inputs); - if (r.wall_s > b.env.OPS_PIPELINE_WALL_CLOCK_S) - return fail(`读数超帽:wall_s=${r.wall_s} > ${b.env.OPS_PIPELINE_WALL_CLOCK_S}(OPS_PIPELINE_WALL_CLOCK_S)`, inputs); - if (r.monthly && Number.isFinite(r.monthly.projected) && r.monthly.projected > b.env.OPS_PIPELINE_MONTHLY_CAP_CNY) - return fail(`读数超帽:月度投影 ${r.monthly.projected} > ${b.env.OPS_PIPELINE_MONTHLY_CAP_CNY}(OPS_PIPELINE_MONTHLY_CAP_CNY;replay 面 monthly=null 不查)`, inputs); + if (r.usage.cost > b.env[CAP.spend]) + return fail(`读数超帽:cost=${r.usage.cost} > ${b.env[CAP.spend]}(${CAP.spend})`, inputs); + if (r.wall_s > b.env[CAP.wall]) + return fail(`读数超帽:wall_s=${r.wall_s} > ${b.env[CAP.wall]}(${CAP.wall})`, inputs); + if (r.monthly && Number.isFinite(r.monthly.projected) && r.monthly.projected > b.env[CAP.month]) + return fail(`读数超帽:月度投影 ${r.monthly.projected} > ${b.env[CAP.month]}(${CAP.month};replay 面 monthly=null 不查)`, inputs); // #184:finding 出口三件套信封留痕机检——ANCHOR_SWEEP 已跑(done/degraded)则 // pipeline.finding_exits 必在场(gh_issue/webhook 状态面;未配置=skip 痕是票面语义非可选) const sweepStage = (ctx.envelope.pipeline?.stages ?? []).find((x) => x.name === "ANCHOR_SWEEP"); @@ -335,6 +353,139 @@ export const GATES = { } return pass(`信封单行 JSON 落 stdout 末行(${s.length} 字节,${ctx.envelope.pipeline?.stages?.length ?? 0} stages)`, inputs); }, + + // ── 发现轮四门(#207 T1 骨架;#208 T2 强化:span 逐字回引/数字复算/对账收严/条目自洽哨)── + // G_SCAN:候选宇宙快照可解析+行形状+channels 计数+#208 条目自洽哨(指针形与渠道自洽—— + // needs 面=NEEDS.md 行锚+text 原文面在场;registry 面=repo 相对模块路径+name=文件词干)。 + G_SCAN: (ctx) => { + const inputs = [A.DISCOVER_SCAN]; + const d = j(path.join(ctx.dir, A.DISCOVER_SCAN)); + if (!d) return fail(`${A.DISCOVER_SCAN} 不可解析`, inputs); + if (!Array.isArray(d.candidates)) return fail("candidates 非数组", inputs); + const CH = ["source_registry", "needs"]; // T1 渠道闭集(web=T3 #209 接线后入集) + for (const c of d.candidates) { + if (!c || typeof c.name !== "string" || !c.name) return fail("候选缺 name", inputs); + if (typeof c.evidence_ptr !== "string" || !c.evidence_ptr) return fail(`候选 ${c.name} 缺 evidence_ptr(证据永不灭失:候选须可原样回算)`, inputs); + if (!CH.includes(c.source_channel)) return fail(`候选 ${c.name} 渠道闭集外:${c.source_channel}`, inputs); + if (typeof c.status !== "string" || !c.status) return fail(`候选 ${c.name} 缺 status`, inputs); + // #208 指针形自洽哨(同律子集于 citation.mjs 三形,但此处钉「形与渠道」互证: + // needs 条目行锚指向原文面、registry 条目路径指向真模块——防篡改面漂移) + if (c.source_channel === "needs") { + if (!/^NEEDS\.md:L\d+$/.test(c.evidence_ptr)) + return fail(`needs 条目 ${c.name} 指针形不自洽:${c.evidence_ptr}(须 NEEDS.md:L<行号>)`, inputs); + if (typeof c.text !== "string" || !c.text.trim()) + return fail(`needs 条目 ${c.name} 缺 text 原文面(#208:span 回引对账面须入轮工件)`, inputs); + } else if (c.source_channel === "source_registry") { + const stem = /^backend\/ipdb\/_sources\/([A-Za-z0-9_-]+)\.py$/.exec(c.evidence_ptr); + if (!stem || stem[1] !== c.name) + return fail(`registry 条目指针形不自洽:${c.evidence_ptr}(须 backend/ipdb/_sources/.py 且 name=文件词干)`, inputs); + } + } + if (!d.channels || !d.channels.registry || !d.channels.needs || d.channels.web?.status !== "placeholder") + return fail("channels 面缺席/坏形(registry/needs/web 三渠道留痕必在场;web=T3 占位)", inputs); + const regN = d.candidates.filter((c) => c.source_channel === "source_registry").length; + const needsN = d.candidates.filter((c) => c.source_channel === "needs").length; + if (d.channels.registry.n !== regN || d.channels.needs.n !== needsN) + return fail(`渠道计数不符(registry 工件=${d.channels.registry.n}/实数=${regN};needs 工件=${d.channels.needs.n}/实数=${needsN})`, inputs); + return pass(`候选宇宙 ${d.candidates.length} 条(registry ${regN}+needs ${needsN},web=placeholder);行形状+指针自洽哨全过`, inputs); + }, + // G_SHORTLIST(#208 T2 span 语义门):结构化候选可解析+数组(空合法:零候选/降级留痕不炸轮); + // 每条候选 shape 面(七键闭集)+quote 对证据面(scanEvidenceFace 经 evidence_ptr 解析, + // 三形:透传/工件/URL)逐字 span(spanMatcher:改写/幽灵 URL/半真 URL=门败,reason 带 + // 候选名+指针)。needs 面 url:null/排他率:null 合法(透传契约;T3 校验器分立不碰)。 + G_SHORTLIST: (ctx) => { + const inputs = [A.DISCOVER_SHORTLIST, A.DISCOVER_SCAN, "(d2-shortlist-transcript.jsonl)"]; + const d = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)); + const scan = j(path.join(ctx.dir, A.DISCOVER_SCAN)); + if (!d) return fail(`${A.DISCOVER_SHORTLIST} 不可解析`, inputs); + if (!Array.isArray(d.candidates)) return fail("candidates 非数组", inputs); + if (d.degraded !== undefined && typeof d.degraded !== "string") return fail("degraded 留痕非字符串", inputs); + const FIELDS = ["name", "url", "source_channel", "quote", "evidence_ptr", "exclusivity_estimate", "notes"]; // 七键闭集(T3 同名面;门面独立校验不 import T3 私有律) + for (const c of d.candidates) { + if (!c || typeof c !== "object") return fail("候选非对象", inputs); + const unknown = Object.keys(c).filter((k) => !FIELDS.includes(k)); + if (unknown.length) return fail(`候选 ${c?.name} 未知键 ${unknown.join(",")}(闭集:${FIELDS.join("/")})`, inputs); + for (const k of ["name", "quote", "evidence_ptr", "source_channel"]) + if (typeof c[k] !== "string" || !c[k].trim()) return fail(`候选 ${c?.name} ${k} 缺失或非非空串`, inputs); + if (!(c.url === null || (typeof c.url === "string" && /^https?:\/\/\S+$/.test(c.url)))) + return fail(`候选 ${c.name} url 须 http(s) 或 null(needs 无 URL 候选合法;强造=幽灵温床)`, inputs); + if (!(c.exclusivity_estimate === null || (typeof c.exclusivity_estimate === "number" && Number.isFinite(c.exclusivity_estimate) && c.exclusivity_estimate >= 0 && c.exclusivity_estimate <= 1))) + return fail(`候选 ${c.name} exclusivity_estimate 须 [0,1] 数或 null(宁少算勿多算)`, inputs); + // span 语义门(核心):证据面解析+逐字回引 + if (!scan) return fail(`${A.DISCOVER_SCAN} 不可解析(span 对账面缺席)`, inputs); + const face = scanEvidenceFace(scan, c.evidence_ptr); + if (!face.ok) return fail(`候选 ${c.name}:证据指针 ${c.evidence_ptr} 在 d1-scan 候选宇宙不可解析(${face.reason})`, inputs); + if (!spanMatcher(c.quote, face.text)) + return fail(`候选 ${c.name}:quote 非证据面逐字 span(指针 ${c.evidence_ptr};改写/幽灵 URL/半真 URL=门败——spec T2 三负例)`, inputs); + } + return pass(`结构化候选 ${d.candidates.length} 条${d.degraded ? `(降级留痕:${d.degraded.slice(0, 60)})` : ""};span 逐字回引全过(#208 T2)`, inputs); + }, + // G_MEASURE(#208 T2 数字复算):测量面可解析+行形状;行⊆shortlist 对账+存在性宿主重算 + // (scanEvidenceFace)+排他率透传复算(shortlist 值??null)+hold 计数——工件漂移即门败。 + G_MEASURE: (ctx) => { + const inputs = [A.DISCOVER_MEASURE, A.DISCOVER_SHORTLIST, A.DISCOVER_SCAN]; + const d = j(path.join(ctx.dir, A.DISCOVER_MEASURE)); + const sh = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)); + const scan = j(path.join(ctx.dir, A.DISCOVER_SCAN)); + if (!d) return fail(`${A.DISCOVER_MEASURE} 不可解析`, inputs); + if (!Array.isArray(d.results)) return fail("results 非数组", inputs); + const byName = new Map((sh?.candidates ?? []).map((c) => [String(c?.name ?? "").toLowerCase(), c])); + for (const m of d.results) { + if (!m || typeof m.candidate !== "string" || !m.candidate) return fail("测量行缺 candidate", inputs); + if (typeof m.evidence_local_ok !== "boolean") return fail(`测量行 ${m.candidate} 缺 evidence_local_ok 布尔`, inputs); + if (m.status !== "uncertainty" && m.status !== "measured") return fail(`测量行 ${m.candidate} 状态闭集外:${m.status}`, inputs); + if (!(m.exclusivity_estimate === null || Number.isFinite(m.exclusivity_estimate))) return fail(`测量行 ${m.candidate} exclusivity_estimate 非 number|null`, inputs); + if (!(m.control_executable === null || typeof m.control_executable === "boolean")) return fail(`测量行 ${m.candidate} control_executable 非 boolean|null`, inputs); + // #208 对账:测量行 ⊄ shortlist = 门败(无源读数) + const sc = byName.get(String(m.name ?? "").toLowerCase()); + if (!sc) return fail(`测量行 ${m.candidate} ⊄ shortlist(name=${m.name} 对不上——无源读数)`, inputs); + // #208 存在性宿主重算:工件布 vs scanEvidenceFace 真值 + if (scan) { + const want = scanEvidenceFace(scan, sc.evidence_ptr).ok; + if (m.evidence_local_ok !== want) + return fail(`存在性复算不符:行 ${m.candidate} 工件=${m.evidence_local_ok}/宿主重算=${want}(指针 ${sc.evidence_ptr})`, inputs); + } + // #208 排他率透传复算:测量行=shortlist 候选值??null(模型无权在测量面改估) + const wantEx = sc.exclusivity_estimate ?? null; + if (m.exclusivity_estimate !== wantEx) + return fail(`排他率透传不符:行 ${m.candidate} 工件=${m.exclusivity_estimate}/shortlist=${wantEx}(测量面无改估权)`, inputs); + } + const holdN = d.results.filter((m) => m.status === "uncertainty").length; + if (d.hold_n !== holdN) return fail(`hold 计数不符(工件=${d.hold_n}/实数=${holdN})`, inputs); + return pass(`测量 ${d.results.length} 行(uncertainty hold ${holdN});存在性+排他率宿主复算全符(#208 T2)`, inputs); + }, + // G_ENQUEUE(#208 T2 对账收严):入队行⊆shortlist∩measure 合格面(三判据宿主重算)+ + // 穷尽哨(shortlist 候选必须在 enqueued∪skipped 有行——证据永不灭失:不静默丢候选)+ + // 计数回算+门槛常量在场(判据在代码不在 prompt)。 + G_ENQUEUE: (ctx) => { + const inputs = [A.DISCOVER_ENQUEUE, A.DISCOVER_SHORTLIST, A.DISCOVER_MEASURE, "(queue.md #139)"]; + const d = j(path.join(ctx.dir, A.DISCOVER_ENQUEUE)); + const sh = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)); + const me = j(path.join(ctx.dir, A.DISCOVER_MEASURE)); + if (!d) return fail(`${A.DISCOVER_ENQUEUE} 不可解析`, inputs); + if (!Array.isArray(d.enqueued) || !Array.isArray(d.skipped)) return fail("enqueued/skipped 非数组", inputs); + if (d.n_enqueued !== d.enqueued.length || d.n_skipped !== d.skipped.length) + return fail(`计数不符(enqueued ${d.n_enqueued}≠${d.enqueued.length};skipped ${d.n_skipped}≠${d.skipped.length})`, inputs); + if (d.gate_exclusivity !== ENQUEUE_EXCLUSIVITY_GATE) return fail(`门槛常量不符:工件=${d.gate_exclusivity}/代码=${ENQUEUE_EXCLUSIVITY_GATE}(判据单源在代码)`, inputs); + const shSet = new Set((sh?.candidates ?? []).map((c) => String(c.name).toLowerCase())); + const mByCand = new Map((me?.results ?? []).map((r) => [String(r.candidate), r])); + for (const row of d.enqueued) { + if (!shSet.has(String(row.name ?? "").toLowerCase())) return fail(`入队行 ${row.candidate} ⊄ shortlist 候选(对账;T2 #208 强化)`, inputs); + if (!Array.isArray(row.reasons) || row.reasons.length) return fail(`入队行 ${row.candidate} 带拒因(矛盾面)`, inputs); + // #208 三判据宿主重算(入队⊆shortlist∩measure 合格面) + const m = mByCand.get(String(row.candidate ?? "")); + if (!m) return fail(`入队行 ${row.candidate} 无测量行(入队⊆shortlist∩measure 合格面)`, inputs); + if (m.evidence_local_ok !== true || !((m.exclusivity_estimate ?? -1) >= ENQUEUE_EXCLUSIVITY_GATE) || m.control_executable !== true) + return fail(`入队行 ${row.candidate} 判据复算不过(证据=${m.evidence_local_ok}/排他率=${m.exclusivity_estimate}(门≥${ENQUEUE_EXCLUSIVITY_GATE})/对照=${m.control_executable}——三判据宿主重算)`, inputs); + } + for (const row of d.skipped) if (!Array.isArray(row.reasons) || !row.reasons.length) return fail(`skipped 行 ${row.candidate} 缺拒因(无证据即无话:不入队须留理由)`, inputs); + // #208 穷尽哨:shortlist 候选必须在 enqueued∪skipped 有行(不静默丢候选) + const covered = new Set([...d.enqueued, ...d.skipped].map((r) => String(r.name ?? "").toLowerCase())); + for (const c of sh?.candidates ?? []) + if (!covered.has(String(c?.name ?? "").toLowerCase())) + return fail(`shortlist 候选 ${c?.name} 在 ${A.DISCOVER_ENQUEUE} 无行(enqueued∪skipped 须穷尽——证据永不灭失:不静默丢候选)`, inputs); + return pass(`入队 ${d.n_enqueued}/skipped ${d.n_skipped};门槛=排他率≥${ENQUEUE_EXCLUSIVITY_GATE}(常量);⊆shortlist∩measure 合格面+穷尽哨过(#208 T2)`, inputs); + }, }; // 门契约单源(#194 C3,评审D P2):门清单/门数/门序一律从上方 GATES 注册键序派生——加门/删门/ diff --git a/ops/agent/pipeline/steps.mjs b/ops/agent/pipeline/steps.mjs index 449f51b0..660acc12 100644 --- a/ops/agent/pipeline/steps.mjs +++ b/ops/agent/pipeline/steps.mjs @@ -19,6 +19,10 @@ import * as budget from "./budget.mjs"; // #199 A5:09-budget.json 读数面(零 import { mkBreaker, readBreakerKnobs } from "../tools/breaker.mjs"; // #199 B:breaker 观察快照(只读零写;零环:breaker 只依赖 node 内置+lib.sh) import { itemSignature, fullDedupKey, createOpsItem, parseItemsTree, CAUSE_GROUP_BY_BUCKET } from "./ops-items.mjs"; // 条目库桥(P4 #166:接 _ops_items 同空间) import { ARTIFACTS as A, deepArtifactName, deepTranscriptName, DEEP_TRANSCRIPT_RE } from "../artifacts.mjs"; // 工件名表单源(#202 C1):步写出/读面禁手抄工件名(磁盘名字节冻结,位等价判例) +import { appendQueueRowLocked } from "../tools/queue-lock.mjs"; // #139 flock 互锁队列写口(#207 T1 ENQUEUE 消费;与 python 侧 append_queue 同构互锁) +import { CANDIDATE_ACTION } from "./discover-candidate.mjs"; // #210 T4:action 词单源 PROPOSE(一词一拼,与 T3 appendCandidateCard 同源;#217 T3.5 接线后 MEASURE 可出 control_executable:true——三判据活路,候选行真写入) +import { scanEvidenceFace } from "./citation.mjs"; // #208 T2:证据面三形解析(透传/工件/URL)→span 级存在性 +import { ensureDiscoverHost, fixtureDiscoveryMethods, discoverChannelReadings, aggregateChannelCalls, replayDiscoverSeeds, breakerFileOf } from "./discovery-host.mjs"; // #217 T3.5:MEASURE 宿主补发/faux 罐装方法面/CLOSE 读数构造+回放重算/breaker 文件名单源 const execFileP = promisify(execFileCb); const AGENT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -32,6 +36,9 @@ export const RULE_PREDICATES = ["bucket", "url_shape", "source", "error_family"] export const RULE_ACTIONS = ["suppress", "annotate"]; // 规则动作闭集(C稿 §4.2) const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; const infra = (msg) => Object.assign(new Error(msg), { code: "infra" }); +// 台账行标签单源(#207 T1):steps RETRO 写面与 gates G_RETRO 读回检查同源,禁各自手抄 +// (health=pipeline 历史名字节冻结;discover=pipeline-discover,票面铸词)。 +export const LEDGER_TAGS = { health: "pipeline", discover: "pipeline-discover" }; // review_at 缺省 = 采纳日 +90 天(C稿 §4.2:防规则磨烂成新噪音;RETRO P5 到点报人复审) const addDaysIso = (iso, n) => { const d = new Date(`${iso}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); }; @@ -318,14 +325,29 @@ function adoptRuleItem(ctx, cand) { async function RETRO(def, ctx) { const usage = `in=${ctx.usage.input} out=${ctx.usage.output} total=${ctx.usage.totalTokens} cost=${ctx.usage.cost.toFixed(8)}`; // 幂等重入(resume #167):台账已有本轮行则零追加(crash 后恢复不双记账——回声防线) + const ledgerTag = LEDGER_TAGS[ctx.type ?? "health"]; // #207 T1:轮型→标签单源 let ledgerNow = null; try { ledgerNow = fs.readFileSync(ctx.ledgerFile, "utf8"); } catch { /* 首轮无台账文件=正常 */ } - if (!(ledgerNow ?? "").includes(`pipeline:${ctx.roundId}`)) - try { await sh("ledger_append", ["health", ctx.state?.model_face === "real" ? "pipeline-real" : "faux-pipeline", usage, `pipeline:${ctx.roundId}`], 120_000, ctx.env); } catch (e) { throw Object.assign(new Error(`台账写败:${e.message}`), { code: "ledger_failed" }); } // 台账先记(#170 起真面记 pipeline-real,真 usage 数字入台账;faux 面标签不变向后兼容) + if (!(ledgerNow ?? "").includes(`${ledgerTag}:${ctx.roundId}`)) + try { await sh("ledger_append", [ctx.type ?? "health", ctx.state?.model_face === "real" ? "pipeline-real" : "faux-pipeline", usage, `${ledgerTag}:${ctx.roundId}`], 120_000, ctx.env); } catch (e) { throw Object.assign(new Error(`台账写败:${e.message}`), { code: "ledger_failed" }); } // 台账先记(#170 起真面记 pipeline-real;faux 面标签不变向后兼容;轮次列=轮型) try { await sh("sandbox_assert", [], 60_000, ctx.env); } // 沙箱红线真执行(backend/frontend 零改动) catch (e) { throw Object.assign(new Error("sandbox_assert rc≠0(backend/frontend 污染)"), { code: "sandbox_violation" }); } let degraded; try { // 候选产出失败=降级(跳过 retro,轮仍成功);台账/sandbox 失败已先行终态(C稿 §2.2 语义序) + fs.mkdirSync(path.dirname(ctx.retroFile), { recursive: true }); + if (ctx.type === "discover") { // 发现轮 retro 体(#207 T1):计数面=d1-d4 工件回算;无规则/条目 diff 面(健康轮律不适用) + const scan = j(path.join(ctx.dir, A.DISCOVER_SCAN)); + const shortlist = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)); + const measure = j(path.join(ctx.dir, A.DISCOVER_MEASURE)); + const enqueue = j(path.join(ctx.dir, A.DISCOVER_ENQUEUE)); + writeJson(ctx.retroFile, { round_id: ctx.roundId, type: "discover", generated_at: new Date().toISOString(), + counts: { scan_n: scan?.candidates?.length ?? 0, shortlist_n: shortlist?.candidates?.length ?? 0, + measure_hold_n: measure?.hold_n ?? 0, enqueue_n: enqueue?.n_enqueued ?? 0 }, + degraded_traces: [shortlist?.degraded].filter(Boolean), // 降级留痕集中面(SHORTLIST 解析失败等) + candidates: [], // T1 无 retro 候选(T2 #208 强化门时随面扩展) + sandbox_assert_pass: true, sandbox_assert_rc: 0, degraded: false }); + return undefined; + } const s = j(path.join(ctx.dir, A.PROBE_SUMMARY)); const survivors = j(path.join(ctx.dir, A.VERIFY_SURVIVORS))?.survivors ?? []; const suppressedF = j(path.join(ctx.dir, A.VERIFY_SUPPRESSED)); @@ -374,26 +396,31 @@ async function CLOSE(def, ctx) { // 书挡步:存在性校验(闭合双层的外 // #199 A5:预算四界读数工件(09-budget.json)。先于 pkg 存在性早退——预算面必落: // 报告缺席轮的谱系/审计面不随早退丢。读数=环内执法同一 ctx 计数面(usage 快照/调用数/ // 墙钟/dup_top3 观察+月度准入投影);门面 G_CLOSE 做回声哨对拍(环内已执法,门只防工件漂移)。 - const knobs = ctx.budgetKnobs ?? budget.readBudgetKnobs(ctx.env ?? {}); + const isDiscover = ctx.type === "discover"; + const knobs = ctx.budgetKnobs ?? (isDiscover ? budget.readDiscoverKnobs(ctx.env ?? {}) : budget.readBudgetKnobs(ctx.env ?? {})); // #207 T1:发现轮旋钮族同面(轮始已校验,此为回退面) // #199 B:breaker 快照只读观察面(回声律:不造文件——状态文件仅在通道面首次结算时创建, // 生产首轮自动建 CHANGELOG 注记);旋钮生效值随快照落盘=可证伪。轮始校验同 budget 判例 // (mkCtx 读一次,非法=用法错 exit 2 不带病跑轮)。 const breakerFace = mkBreaker({ - file: path.join(ctx.opsRoot ?? path.dirname(ctx.dir), "breaker.json"), + file: breakerFileOf(ctx), // #217 T3.5:与发现宿主同文件名单源(discovery-host.mjs breakerFileOf;原步内手拼表达式同形收口) env: ctx.env ?? {}, round: ctx.roundId ?? "health", knobs: ctx.breakerKnobs ?? readBreakerKnobs(ctx.env ?? {}), }); writeJson(path.join(ctx.dir, A.BUDGET), { round_id: ctx.roundId, generated_at: new Date().toISOString(), - env: { // 生效值(含 OPS_PIPELINE_BUDGET_CNY:语义=DISPATCH 准告警线,真执法=A1 四界) - OPS_PIPELINE_BUDGET_CNY: ctx.budgetCny, - OPS_PIPELINE_MAX_MODEL_CALLS: knobs.maxModelCalls, - OPS_PIPELINE_TOKEN_BUDGET: knobs.tokenBudget, - OPS_PIPELINE_WALL_CLOCK_S: knobs.wallClockS, - OPS_PIPELINE_SPEND_CAP_CNY: knobs.spendCapCny, - OPS_PIPELINE_MONTHLY_CAP_CNY: knobs.monthlyCapCny, - OPS_PIPELINE_DUP_CALL_LIMIT: knobs.dupCallLimit, - }, + env: isDiscover // 生效值 per-type(#207 T1):发现轮=三旋钮+est+共享月帽(无 token 界——发现轮不预设轨迹预算);健康轮 7 键面字节不变(位等价) + ? { OPS_PIPELINE_BUDGET_CNY: ctx.budgetCny, OPS_DISCOVER_MAX_MODEL_CALLS: knobs.maxModelCalls, + OPS_DISCOVER_WALL_CLOCK_S: knobs.wallClockS, OPS_DISCOVER_SPEND_CAP_CNY: knobs.spendCapCny, + OPS_PIPELINE_MONTHLY_CAP_CNY: knobs.monthlyCapCny } + : { // 生效值(含 OPS_PIPELINE_BUDGET_CNY:语义=DISPATCH 准告警线,真执法=A1 四界) + OPS_PIPELINE_BUDGET_CNY: ctx.budgetCny, + OPS_PIPELINE_MAX_MODEL_CALLS: knobs.maxModelCalls, + OPS_PIPELINE_TOKEN_BUDGET: knobs.tokenBudget, + OPS_PIPELINE_WALL_CLOCK_S: knobs.wallClockS, + OPS_PIPELINE_SPEND_CAP_CNY: knobs.spendCapCny, + OPS_PIPELINE_MONTHLY_CAP_CNY: knobs.monthlyCapCny, + OPS_PIPELINE_DUP_CALL_LIMIT: knobs.dupCallLimit, + }, readings: { usage: { ...ctx.usage }, model_calls: ctx.modelCalls ?? 0, @@ -401,8 +428,15 @@ async function CLOSE(def, ctx) { // 书挡步:存在性校验(闭合双层的外 dup_top3: budget.dupTop(ctx.dupRegistry ?? new Map(), 3), monthly: ctx.state?.monthly_admission ?? null, // A2 准入留痕(replay 面无准入=null) breaker: breakerFace.snapshot(), // #199 B:五通道闭集态+生效旋钮(观察不裁决,G_CLOSE 不对拍帽——无帽可拍) + // #217 T3.5:发现轮配额第三读数键(纯增量;健康轮不展开=字节不变位等价)。活宿主优先 + // (含 blocked 观察面);宿主缺席(resume 后 MEASURE 已录跳过)=工件回放重算(d2 transcript + // registry_seed+d3 tool_seeds;被拒不入种子=回放面恒宁少算;证据永不灭失:读数可回算)。 + ...(isDiscover ? { discover_channels: ctx.discoverHost + ? discoverChannelReadings(ctx.discoverHost.channelCounts) + : discoverChannelReadings(aggregateChannelCalls(replayDiscoverSeeds(ctx))) } : {}), }, }); + if (isDiscover) { ctx.pkg = null; return; } // 发现轮无报告步/无判例校验面(#207 T1;信封 pkg=null 恒) const report = path.join(ctx.dir, A.SYNTH_REPORT); ctx.pkg = fs.existsSync(report) ? report : null; if (!ctx.pkg) return; @@ -542,6 +576,27 @@ function synthConsume(ctx, _item, { text }) { fs.writeFileSync(path.join(ctx.dir, A.SYNTH_REPORT), text); } +// 发现轮 SHORTLIST(#207 T1):输入=SCAN 候选宇宙;输出=结构化候选(JSON,span 级引用=T2 #208 强化)。 +// 解析失败=降级空清单留痕不炸轮(spec T1 明律:零候选合法面,同 TRIAGE 零假设;不重试不 model_face)。 +function shortlistInput(ctx) { + const scan = j(path.join(ctx.dir, A.DISCOVER_SCAN)) ?? { candidates: [] }; + return { systemPrompt: fs.readFileSync(promptFile("discover/shortlist"), "utf8"), + userMsg: JSON.stringify({ candidates: scan.candidates, note: "T1 候选宇宙=registry+needs 两面(web 面 T3 #209 接线);产出结构化候选 JSON" }) }; +} +function shortlistConsume(ctx, _item, { text }) { + let out; + try { + const body = (text.match(/\{[\s\S]*\}/) ?? [])[0]; // 散文/围栏包裹的 JSON 体宽容提取(提取不出仍走降级) + if (!body) throw new Error("无 JSON 体"); + const parsed = JSON.parse(body); + if (!Array.isArray(parsed?.candidates)) throw new Error("candidates 非数组"); + out = { round_id: ctx.roundId, candidates: parsed.candidates }; + } catch (e) { + out = { round_id: ctx.roundId, candidates: [], degraded: `shortlist_parse:${e.message}(降级空清单留痕,轮不炸;spec T1)` }; + } + writeJson(path.join(ctx.dir, A.DISCOVER_SHORTLIST), out); +} + // 模型步工件完备判定(#167 resume 三规则②):「有 transcript(=本步工件集齐)→ 只补门 // 评估,不重花 token」。单发步=def.out 全在场;逐假设步(DEEP)=该假设 05-deep-.json // + transcript 在场(聚合 verdicts 由 finalize 从逐假设工件重建,不算完备判据)。 @@ -553,7 +608,130 @@ export function modelItemComplete(def, ctx, item) { return def.out.every((f) => fs.existsSync(path.join(ctx.dir, f))); } -const SCRIPT = { PROBE, VERIFY, DISPATCH, ITEMIZE, RETRO, ANCHOR_SWEEP, CLOSE }; +// ── 发现轮四步(#207 T1;spec /tmp/discover-pipeline-spec.md T1 节;ADR 0010 六步骨架)────────── +// SCAN:确定性候选宇宙快照(零模型零网络):两面=已接源注册表(backend/ipdb/_sources/*.py,下划线 +// 前缀=内部模块非源)+ NEEDS.md 缺口行(AGENTS.md:NEEDS=「想问而无槽」index;状态 open/候选=缺口)。 +// web 渠道=T3 #209 接线占位(留痕不炸轮)。证据永不灭失:registry 面指针=repo 相对模块路径, +// needs 面=NEEDS.md 行锚——候选可原样回算。同 commit 重扫逐字节同(确定性,无网络无时钟入候选面)。 +const NEEDS_STATUS = ["open", "候选"]; // 缺口行状态闭集(已立项/已闭不入候选宇宙) +async function SCAN(def, ctx) { + // #208 T2 faux 缝:显式 faux 面(缺省真面)且 fixture 在场→候选宇宙=罐装钉死。理由: + // span 门使罐装 quote 与 NEEDS.md 原文耦合,真仓演进会破门(ops CI 随文案红); + // faux=罐装 I/O 是仓律(#170 面选择);真轮(缺省)不受影响。证据永不灭失不破: + // 罐装宇宙自带 text 面,span 回引在轮工件内可回算。 + if (ctx.modelFace === "faux" && typeof ctx.fixturesDir === "string") { + const fxPath = path.join(ctx.fixturesDir, A.DISCOVER_SCAN); + if (fs.existsSync(fxPath)) { + const fx = JSON.parse(fs.readFileSync(fxPath, "utf8")); + writeJson(path.join(ctx.dir, A.DISCOVER_SCAN), { round_id: ctx.roundId, generated_at: new Date().toISOString(), + candidates: fx.candidates, channels: fx.channels }); + return; + } + } + const repoRoot = path.resolve(AGENT_DIR, "..", ".."); + const registry = fs.existsSync(path.join(repoRoot, "backend", "ipdb", "_sources")) + ? fs.readdirSync(path.join(repoRoot, "backend", "ipdb", "_sources")).filter((f) => f.endsWith(".py") && !f.startsWith("_")).sort() + .map((f) => ({ name: f.replace(/\.py$/, ""), url: null, evidence_ptr: `backend/ipdb/_sources/${f}`, source_channel: "source_registry", status: "integrated" })) + : []; + const needsRows = []; + const needsFile = path.join(repoRoot, "NEEDS.md"); + if (fs.existsSync(needsFile)) { + const lines = fs.readFileSync(needsFile, "utf8").split("\n"); + for (let i = 0; i < lines.length; i++) { + const cells = lines[i].split("|").map((c) => c.trim()); + if (cells.length < 5 || !NEEDS_STATUS.includes(cells[3])) continue; + const url = (cells[1].match(/https?:\/\/\S+/) ?? [null])[0]; + needsRows.push({ name: cells[1].replace(/https?:\/\/\S+/g, "").replace(/\s+/g, " ").trim() || url, + url, evidence_ptr: `NEEDS.md:L${i + 1}`, source_channel: "needs", status: cells[3], + text: lines[i].trim() }); // #208 T2:原文行快照入工件——span 门(逐字回引)对账面在轮工件内,回放可回算;registry 面结构性(canonicalJson 面),不带 text + } + } + writeJson(path.join(ctx.dir, A.DISCOVER_SCAN), { round_id: ctx.roundId, generated_at: new Date().toISOString(), + candidates: [...registry, ...needsRows], + channels: { registry: { status: "ok", n: registry.length }, needs: { status: "ok", n: needsRows.length }, + web: { status: "placeholder", n: 0, note: "T3 #209 接线(PR3a web_search 渠道;T1 只留占位留痕)" } } }); +} + +// MEASURE(#217 T3.5 真接线):确定性测量步(零模型),两测量面: +// ①证据存在性:scanEvidenceFace 本地重算(d1 工件零网络;#208 T2 三形); +// ②URL 探测:发现宿主 host.call("web_read") 真证据面补发(有 url 候选;faux 面=measure.json +// 罐装读数零网络,未罐装=io_error 诚实面,永不真联网)。 +// 排他率=SHORTLIST 估值透传(G_MEASURE 钉:测量面无改估权——勿虚构);control_executable= +// 该候选自身探测的阳性对照:web_read 执行且 ok=true→true;执行且 ok=false(死链/鉴权/4xx) +// →false(实测量非不可算);宿主拒绝(dup/breaker/cap)或无 url→null=不可算。三面全可算才 +// status=measured;任一不可算=uncertainty hold+显式原因(无证据即无话:不入队也不除名, +// 被拒≠清白),不炸轮。tool_seeds=宿主种子快照(CLOSE 回放重算面;被拒不入种子=宁少算)。 +const MEASURE_GUARD_CODES = new Set(["repeat_storm", "channel_open", "budget"]); // 宿主拒绝码(ERROR_CODES 三员);执行失败面另有分类码(channel_missing/channel_auth/io_error 等) +async function MEASURE(def, ctx) { + const scan = j(path.join(ctx.dir, A.DISCOVER_SCAN)) ?? { candidates: [] }; + const shortlist = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)) ?? { candidates: [] }; + // faux 面方法恒罐装(零网络律:fixture 缺 measure.json=全 io_error 未罐装,不落真网络); + // 真面=discoveryMethods 真网络(spawn/fetch)。 + const fauxFace = ctx.modelFace === "faux" && typeof ctx.fixturesDir === "string"; + const host = ensureDiscoverHost(ctx, fauxFace + ? { methods: fixtureDiscoveryMethods(j(path.join(ctx.fixturesDir, "measure.json")) ?? {}) } : {}); + const results = []; + for (const c of shortlist.candidates) { + const evidence_local_ok = scanEvidenceFace(scan, c.evidence_ptr).ok; // ①本地重算(幽灵指针/幽灵 URL=not ok) + let url_read_ok = null, url_read_code = null; // ②探测三态:true/false=实测量,null=不可算(无 url/被拒/无结构面) + if (c.url) { + const sc = (await host.call("web_read", { url: c.url }))?.structuredContent; + if (sc?.ok === true) url_read_ok = true; + else if (sc && !MEASURE_GUARD_CODES.has(sc.code)) { url_read_ok = false; url_read_code = sc.code ?? null; } + else url_read_code = sc?.code ?? "no_structured"; // 守卫拒绝/无结构面=不可算非清白 + } + const unresolved = []; + if (!c.url) unresolved.push("无 URL 候选:控制面不可算(不发探测)"); + else if (url_read_ok === null) unresolved.push(`URL 探测不可算(宿主拒绝/无结构面 code=${url_read_code};被拒≠清白)`); + if (c.exclusivity_estimate == null) unresolved.push("排他率缺 SHORTLIST 估值(测量面无改估权,勿虚构)"); + results.push({ candidate: candidateSlug(c.name), name: c.name, evidence_local_ok, + url_read_ok, url_read_code, control_executable: url_read_ok, + status: unresolved.length ? "uncertainty" : "measured", + uncertainty_reason: unresolved.length ? unresolved.join(";") : null, + exclusivity_estimate: c.exclusivity_estimate ?? null }); + } + writeJson(path.join(ctx.dir, A.DISCOVER_MEASURE), { round_id: ctx.roundId, generated_at: new Date().toISOString(), + results, n: results.length, hold_n: results.filter((r) => r.status === "uncertainty").length, + tool_seeds: [...host.seeds] }); +} + +// ENQUEUE:只写不动作(#139):过三判据(证据存在性全过+排他率≥0.25+阳性对照可执行)的候选 +// 经 queue-lock(#139 flock 路径,与 python append_queue 同构互锁)写 queue.md 一行候选卡; +// 不开票不接源。判据=常量(spec:判据在代码不在 prompt,模型无权改)。写败=fail-closed infra +// (证据不灭失优先于轮成功)。 +export const ENQUEUE_EXCLUSIVITY_GATE = 0.25; // spec T1:排他率预估门槛(常量,步/门同源导出;模型无权改判据——判据在代码不在 prompt;T4 北星校准走旋钮前不动) +const candidateSlug = (name) => String(name).toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "cand"; // queue source 面 ^[a-z0-9_]{1,32}$ 适配 +async function ENQUEUE(def, ctx) { + const shortlist = j(path.join(ctx.dir, A.DISCOVER_SHORTLIST)) ?? { candidates: [] }; + const measure = j(path.join(ctx.dir, A.DISCOVER_MEASURE)) ?? { results: [] }; + const byCand = new Map(measure.results.map((r) => [r.candidate, r])); + const enqueued = []; const skipped = []; + for (const c of shortlist.candidates) { + const m = byCand.get(candidateSlug(c.name)); + const reasons = []; + if (!m || m.evidence_local_ok !== true) reasons.push("evidence"); + if (!m || !((m.exclusivity_estimate ?? -1) >= ENQUEUE_EXCLUSIVITY_GATE)) reasons.push("exclusivity"); + if (!m || m.control_executable !== true) reasons.push("control"); + (reasons.length ? skipped : enqueued).push({ candidate: candidateSlug(c.name), name: c.name, reasons }); + } + const queueFile = path.join(ctx.opsRoot, "queue.md"); + for (const row of enqueued) { + const m = byCand.get(row.candidate); + // #210 T4:action 词统一 PROPOSE(死路面也先对齐)——运营轮 ENQUEUE/REJECT 闭集是 + // python 写面(_ops_store.QUEUE_ACTIONS)的人裁动作词;发现轮候选行=提案非动作,与 + // T3 appendCandidateCard 一词一拼,python 读面无程序解析行消费方(PROPOSE 行无破坏)。 + const r = await appendQueueRowLocked({ queueFile, round: ctx.roundId, action: CANDIDATE_ACTION, source: row.candidate, predicate: null, + evidence: [ + { call_id: "d3-measure", field: "evidence_local_ok", value: true }, + { call_id: "d3-measure", field: "exclusivity_estimate", value: m.exclusivity_estimate }, + { call_id: "d3-measure", field: "control_executable", value: true }] }, { env: ctx.env ?? {} }); + if (!r.ok) throw infra(`queue 写败(${row.candidate}):${r.code ?? "io_error"}`); // fail-closed + } + writeJson(path.join(ctx.dir, A.DISCOVER_ENQUEUE), { round_id: ctx.roundId, generated_at: new Date().toISOString(), + gate_exclusivity: ENQUEUE_EXCLUSIVITY_GATE, enqueued, skipped, n_enqueued: enqueued.length, n_skipped: skipped.length }); +} + +const SCRIPT = { PROBE, VERIFY, DISPATCH, ITEMIZE, RETRO, ANCHOR_SWEEP, CLOSE, SCAN, MEASURE, ENQUEUE }; export async function runStage(def, ctx) { const fn = SCRIPT[def.name]; if (!fn) throw Object.assign(new Error(`脚本步未实现:${def.name}(定义表与实现漂移)`), { code: "infra" }); @@ -563,6 +741,7 @@ const MODEL_IO = { TRIAGE: { input: triageInput, consume: triageConsume, finalize: null }, DEEP: { input: deepInput, consume: deepConsume, finalize: deepFinalize, items: deepItems }, SYNTH: { input: synthInput, consume: synthConsume, finalize: null }, + SHORTLIST: { input: shortlistInput, consume: shortlistConsume, finalize: null }, // #207 T1:发现轮唯一模型步;#217 T3.5 真面工具注入=发现宿主(faux.mjs runRealStep 直装,与 MEASURE 同单例) }; export const modelIO = (def) => { const io = MODEL_IO[def.name]; diff --git a/ops/agent/prompts/discover/shortlist.md b/ops/agent/prompts/discover/shortlist.md new file mode 100644 index 00000000..9be9bfc1 --- /dev/null +++ b/ops/agent/prompts/discover/shortlist.md @@ -0,0 +1,33 @@ +# 发现轮 SHORTLIST(#207 T1 接线;span 级逐字回引强化=T2 #208;工具面真接线=T3.5 #217) + +你是 IP Radar 的情报源发现轮候选筛选器。输入是一份**候选宇宙快照**(d1-scan):已接源注册表 + +NEEDS.md 缺口行(T1 无 web 面;T3 #209 接线)。你的任务:从候选宇宙中选出值得进入确定性测量(MEASURE) +的候选,输出**结构化候选清单**。 + +## 输出契约 + +输出单个 JSON 对象(不要围栏、不要散文): + +```json +{"candidates":[{"name":"...","url":"...","quote":"...","evidence_ptr":"NEEDS.md:L14","source_channel":"needs","exclusivity_estimate":null,"notes":"..."}]} +``` + +字段: +- `name`:候选名(与输入候选同名,不改写)。 +- `url`:数据源 URL(输入有的透传;无则 null)。 +- `quote`:**逐字引用**的原文 span(从输入候选的行文/首记列原样摘录,禁改写禁拼接)——T2 #208 已落地 + G_SHORTLIST span 级逐字回引门:quote 须在 d1-scan 对应证据面(经 evidence_ptr 三形解析)中找到逐字 span; + 幽灵 URL(引不存在的 URL)/半真 URL(前缀子串或超集串)/改写引文(大小写或标点改写)均门败。 +- `evidence_ptr`:输入候选的 evidence_ptr 原样透传(registry 面=repo 相对模块路径;needs 面=NEEDS.md 行锚)。 +- `source_channel`:输入候选同值透传。 +- `exclusivity_estimate`:排他率预估(0-1;信息不足以估则 null——**宁少算勿多算**)。 +- `notes`:一句理由(为什么值得测量/缺口在哪)。 + +## 边界(铁律) + +1. **你只筛选,不裁决。** 入队判据(证据存在性/排他率≥门槛/阳性对照)是代码常量,不在本 prompt; + 模型无权改判据、无权入队、无权开票。 +2. **零候选合法。** 宇宙中无可测候选时输出空清单 `{"candidates":[]}`,诚实留痕优于硬凑。 +3. **回声防线。** 与已接源(注册表行)功能重叠的候选不选——同源重复不是印证。 +4. 工具面(T3.5 #217 真面接线):本轮可调三个只读工具 web_read/web_search/rss_parse(同守卫链: + 同参重复拦截/通道熔断/每轮上限);测量(MEASURE)由宿主确定性补发,不在本步。 diff --git a/ops/agent/selftest-contract.mjs b/ops/agent/selftest-contract.mjs index c4400524..9a6b59ed 100644 --- a/ops/agent/selftest-contract.mjs +++ b/ops/agent/selftest-contract.mjs @@ -112,7 +112,7 @@ await t("表对拍:扫描器收录的 env 读取面(计算模板无未知形)", assert.equal(READS.unknownTemplate, null, `未收录的 process.env[··] 计算读取模板,对拍器需同步`); }); -await t("表:SPAWN_CONTRACT_ENV 名集=规格 16 项+…(增补链见 agent.mjs 注;#199 交付A 尾部再追 OPS_PIPELINE_MAX_MODEL_CALLS/OPS_PIPELINE_TOKEN_BUDGET/OPS_PIPELINE_WALL_CLOCK_S/OPS_PIPELINE_SPEND_CAP_CNY/OPS_PIPELINE_MONTHLY_CAP_CNY/OPS_PIPELINE_DUP_CALL_LIMIT——pipeline/budget.mjs readBudgetKnobs 单源实读)", () => { +await t("表:SPAWN_CONTRACT_ENV 名集=规格 16 项+…(增补链见 agent.mjs 注;#199 交付A 尾部再追 OPS_PIPELINE_MAX_MODEL_CALLS/OPS_PIPELINE_TOKEN_BUDGET/OPS_PIPELINE_WALL_CLOCK_S/OPS_PIPELINE_SPEND_CAP_CNY/OPS_PIPELINE_MONTHLY_CAP_CNY/OPS_PIPELINE_DUP_CALL_LIMIT——pipeline/budget.mjs readBudgetKnobs 单源实读;#207 T1 再追 OPS_DISCOVER_MAX_MODEL_CALLS/OPS_DISCOVER_WALL_CLOCK_S/OPS_DISCOVER_SPEND_CAP_CNY——readDiscoverKnobs 单源实读,月帽共享不另立行)", () => { assert.deepEqual([...tableNames()].sort(), ["DOCS_ROOT", "GH_TOKEN", "IP_RADAR_ALERT_URLS", "JINA_API_KEY", "LEDGER_FILE", "OPS_ANCHOR_SWEEP_TIMEOUT_MS", "OPS_AUTH", "OPS_DATA_DIR", "OPS_EVAL_PY", "OPS_FETCH_MAX_BYTES", "OPS_MATRIX_TIMEOUT_MS", "OPS_MODELS", "OPS_MODEL_DISCOVER", "OPS_MODEL_HEALTH", "OPS_MODELS_STORE", "OPS_PIPELINE", "OPS_PIPELINE_BUDGET_CNY", "OPS_PROBE_CLI_TIMEOUT_MS", @@ -120,6 +120,7 @@ await t("表:SPAWN_CONTRACT_ENV 名集=规格 16 项+…(增补链见 agent.mjs "PRECEDENTS_DIR", "ZAI_CODING_CN_API_KEY", "OPS_PIPELINE_MAX_MODEL_CALLS", "OPS_PIPELINE_TOKEN_BUDGET", "OPS_PIPELINE_WALL_CLOCK_S", "OPS_PIPELINE_SPEND_CAP_CNY", "OPS_PIPELINE_MONTHLY_CAP_CNY", "OPS_PIPELINE_DUP_CALL_LIMIT", + "OPS_DISCOVER_MAX_MODEL_CALLS", "OPS_DISCOVER_WALL_CLOCK_S", "OPS_DISCOVER_SPEND_CAP_CNY", // #207 T1 发现轮三旋钮 "OPS_BREAKER_THRESHOLD", "OPS_BREAKER_COOLDOWN_S"].sort()); // #199 交付B:tools/breaker.mjs readBreakerKnobs 单源实读 }); @@ -351,6 +352,32 @@ await t("锁路径对拍:python LEDGER_LOCK_NAME ↔ node ledgerLockPath 同源 "python 写面 append_ledger 须持 _ledger_lock"); }); +// ── #217 T3.5:queue.md 表头跨语言三抄对拍哨(形制同锁路径哨)── +// 表头首行句随三写口翻正(queue_action / 发现轮 ENQUEUE 候选行 / 候选卡 +// appendCandidateCard)后,"唯一写口"旧句不再是事实——三抄面(python +// _ops_store.QUEUE_HEADER / node queue-lock.mjs QUEUE_HEADER / node 侧首建实写串) +// 任一漂移=表头撒谎或双源分叉,哨红。 +await t("表头对拍:python QUEUE_HEADER ↔ node QUEUE_HEADER ↔ 首建实写串三抄逐字同(#217 T3.5)", async () => { + const py = fs.readFileSync(path.join(AGENT, "..", "..", "backend", "ipdb", "_ops_store.py"), "utf8"); + const pyBlock = py.match(/QUEUE_HEADER = \(([\s\S]*?)\n\)/); + assert.ok(pyBlock, "_ops_store.py 缺 QUEUE_HEADER 拼串(表头契约漂移)"); + const pyHeader = (pyBlock[1].match(/"((?:[^"\\]|\\.)*)"/g) ?? []).map((x) => JSON.parse(x)).join(""); + const nodeSrc = fs.readFileSync(path.join(AGENT, "tools/queue-lock.mjs"), "utf8"); + const nodeBlock = nodeSrc.match(/const QUEUE_HEADER =\s*\n?\s*((?:"(?:[^"\\]|\\.)*"\s*\+?\s*\n?\s*)+);/); + assert.ok(nodeBlock, "queue-lock.mjs 缺 QUEUE_HEADER 常量(表头契约漂移)"); + const nodeHeader = (nodeBlock[1].match(/"((?:[^"\\]|\\.)*)"/g) ?? []).map((x) => JSON.parse(x)).join(""); + assert.equal(pyHeader, nodeHeader, "python/node 表头双抄分叉(任一侧改句未同批)"); + assert.match(pyHeader, /三写口.*queue_action.*ENQUEUE.*appendCandidateCard/s, + "表头首行句须如实描述三写口(#217;回退到「唯一写口」旧句=哨红)"); + // 首建实写面:appendQueueRowLocked 惰性首建写出的文件头 = 双抄头(实跑非只源) + const { appendQueueRowLocked } = await import("./tools/queue-lock.mjs"); + const qf = path.join(TMP, "hdr", "queue.md"); + fs.mkdirSync(path.dirname(qf), { recursive: true }); // flock 先开锁文件:父目录须在场(生产 ops/ 恒在) + await appendQueueRowLocked({ queueFile: qf, round: "r", action: "PROPOSE", source: "hdr_probe", predicate: null, evidence: [{ call_id: "measure-1", field: "evidence_local_ok", value: true }] }); + const written = fs.readFileSync(qf, "utf8"); + assert.ok(written.startsWith(nodeHeader), "首建实写串与 QUEUE_HEADER 常量分叉(渲染面漂移)"); +}); + fs.rmSync(TMP, { recursive: true, force: true }); console.log(`contract-selftest: PASS=${pass} FAIL=${fail}`); process.exit(fail ? 1 : 0); diff --git a/ops/agent/selftest-pipeline.mjs b/ops/agent/selftest-pipeline.mjs index c89d71ec..81ddb966 100644 --- a/ops/agent/selftest-pipeline.mjs +++ b/ops/agent/selftest-pipeline.mjs @@ -96,7 +96,13 @@ process.env.PATH = `${STUBS}:${process.env.PATH}`; // env 后动态 import(同 tools.mjs 面:先落位 env 再装载)。 const { GATE_SEQUENCE, GATE_COUNT } = await import("./pipeline/gates.mjs"); -const { runPipeline, HEALTH_PIPELINE, DEFAULT_FIXTURES_DIR } = await import("./pipeline.mjs"); +const { runPipeline, HEALTH_PIPELINE, DISCOVER_PIPELINE, DEFAULT_FIXTURES_DIR } = await import("./pipeline.mjs"); +// #207 T1:门契约 per-type 化——GATE_SEQUENCE=双管线执行序并集(注册表键序双射面);健康轮门数/ +// 门序不再从 GATE_COUNT/GATE_SEQUENCE 取(发现轮四门入注册表后两者已非健康轮面),改从 +// HEALTH_PIPELINE 派生单源(与 golden/run-golden.mjs 同判例)。 +const SEQ_OF = (pipe) => pipe.map((d) => d.gate).filter(Boolean); +const HEALTH_GATES_SEQ = SEQ_OF(HEALTH_PIPELINE); +const HEALTH_GATE_COUNT = HEALTH_GATES_SEQ.length; const { runModelStep, selectModelFace, STEP_ROUND } = await import("./pipeline/faux.mjs"); // #170 面选择/真面注入缝直调单测面 const { ERROR_CODES } = await import("./agent.mjs"); const steps = await import("./pipeline/steps.mjs"); // P2 #164 直调 VERIFY 步(规则库/在飞面单元例) @@ -164,30 +170,30 @@ await t("全 faux 轮:十执行态到 CLOSE,信封 ok+形状只增 pipeline 扩 assert.equal(env.verify.cited, 1, "canned 报告引 1 fixture 判例"); }); -await t("门契约单源(#194 C3):GATES 注册键序=HEALTH_PIPELINE 执行序双射;GATE_COUNT 派生(单侧加/删门即红=门面假绿判例)", async () => { +await t("门契约单源(#194 C3;#207 T1 per-type):GATES 注册键序=双管线执行序并集双射;GATE_COUNT 派生(单侧加/删门即红=门面假绿判例)", async () => { const { GATES } = await import("./pipeline/gates.mjs"); const { HEALTH_PIPELINE } = await import("./pipeline.mjs"); - assert.deepEqual(GATE_SEQUENCE, HEALTH_PIPELINE.map((d) => d.gate).filter(Boolean), "注册键序=执行序"); + assert.deepEqual(GATE_SEQUENCE, [...new Set([...SEQ_OF(HEALTH_PIPELINE), ...SEQ_OF(DISCOVER_PIPELINE)])], "注册键序=双管线执行序并集(health 先 discover 后)"); assert.equal(GATE_COUNT, GATE_SEQUENCE.length, "门数=名单长(禁手写数字)"); assert.equal(Object.keys(GATES).length, GATE_COUNT); }); await t("工件名表单源(#202 C1):ARTIFACT_NAMES 双向闭包——清单全落盘∧轮目录实际工件集合=名表∪DEEP 参数族(名表加项不更新写出面=红;步面新写工件不入表=红;09-budget.json 手抄三层判例收口)+gates.json 十记录全 PASS+state.json 终态 done/CLOSE+原子写无 .tmp 残留", async () => { - const { ARTIFACTS, ARTIFACT_NAMES, DEEP_ARTIFACT_RE, DEEP_TRANSCRIPT_RE } = await import("./artifacts.mjs"); // 工件名单一真源(禁手抄清单) + const { ARTIFACTS, ARTIFACT_NAMES, HEALTH_ARTIFACT_NAMES, DEEP_ARTIFACT_RE, DEEP_TRANSCRIPT_RE } = await import("./artifacts.mjs"); // 工件名单一真源(禁手抄清单);#207 T1:健康轮闭包面=HEALTH_ARTIFACT_NAMES(ARTIFACT_NAMES 已含发现轮四名) const d = roundDir(R1); - for (const f of ARTIFACT_NAMES) + for (const f of HEALTH_ARTIFACT_NAMES) assert.ok(fs.existsSync(path.join(d, f)), `工件缺:${f}`); - const parametric = fs.readdirSync(d).filter((f) => !ARTIFACT_NAMES.includes(f)); // 名表外=逐假设参数族闭包面 + const parametric = fs.readdirSync(d).filter((f) => !HEALTH_ARTIFACT_NAMES.includes(f)); // 名表外=逐假设参数族闭包面 for (const f of parametric) assert.ok(DEEP_ARTIFACT_RE.test(f) || DEEP_TRANSCRIPT_RE.test(f), `轮目录工件不在名表∪参数族闭包:${f}(新工件须先入 artifacts.mjs——名表与写出面双向哨)`); assert.ok(parametric.some((f) => DEEP_ARTIFACT_RE.test(f) && f !== ARTIFACTS.DEEP_VERDICTS), "逐假设工件"); assert.ok(parametric.some((f) => DEEP_TRANSCRIPT_RE.test(f)), "逐假设 transcript"); const g = j(path.join(d, "gates.json")); assert.equal(g.round_id, R1); - assert.equal(g.records.length, GATE_COUNT, "9 可跑步门(含 #182 ANCHOR_SWEEP)+G_CLOSE"); + assert.equal(g.records.length, HEALTH_GATE_COUNT, "9 可跑步门(含 #182 ANCHOR_SWEEP)+G_CLOSE(#207 T1 起从 HEALTH_PIPELINE 派生)"); assert.ok(g.records.every((r) => r.pass === true), `门败:${g.records.filter((r) => !r.pass).map((r) => r.name).join(",")}`); assert.ok(g.records.every((r) => typeof r.reason === "string" && /\d/.test(r.reason)), "reason 带数字可回算"); - assert.deepEqual(g.records.map((r) => r.name), GATE_SEQUENCE); + assert.deepEqual(g.records.map((r) => r.name), HEALTH_GATES_SEQ); const st = j(path.join(d, "state.json")); assert.equal(st.status, "done"); assert.equal(st.stage, "CLOSE"); @@ -635,7 +641,7 @@ await t("空转金轨迹轮(#162 审 P2-2 裁定①):零 need_deep → DISPATCH const m = j(path.join(d, "06-itemize-manifest.json")); assert.deepEqual([m.created, m.reopened, m.deduped, m.suppressed], [2, 0, 0, 1], "watch 条目 2 + 抑制 1(P4:真树落盘)"); const g = j(path.join(d, "gates.json")); - assert.equal(g.records.length, GATE_COUNT - 1, "skipped 步无门记录(PROBE..CLOSE 除 DISPATCH;含 #182 G_ANCHOR_SWEEP)"); + assert.equal(g.records.length, HEALTH_GATE_COUNT - 1, "skipped 步无门记录(PROBE..CLOSE 除 DISPATCH;含 #182 G_ANCHOR_SWEEP)"); assert.ok(g.records.every((x) => x.pass), "G_DEEP 机检金轨迹在场面"); assert.ok(g.records.find((x) => x.name === "G_DEEP").reason.includes("golden=1"), "门 reason 带数字可回算"); }); @@ -900,7 +906,7 @@ await t("matrix 裸失败负例(#163 review P2-1):信封件落 01-probe-matrix.j assert.equal(s.matrix.exit_code, null); assert.equal(s.matrix.violation_n, null); const g = j(path.join(d, "gates.json")); - assert.equal(g.records.length, GATE_COUNT); + assert.equal(g.records.length, HEALTH_GATE_COUNT); assert.ok(g.records.every((x) => x.pass), "门不受 matrix 裸失败牵连(legacy 失败只回嵌套信封,不阻轮语义两侧一致)"); // 该 stub 无旗标分发→sweep 面同死:#182 降级旁路(stage degraded+门旁路留痕,轮仍成功) const envD = JSON.parse(r.stdout.trim().split("\n").pop()); @@ -1111,10 +1117,10 @@ await t("replay --dry 全程可跑(CLI 子进程):门全重算 PASS,产物写 -r assert.equal(env.ok, true); assert.equal(env.pipeline.replayed_of, R1); assert.equal(env.pipeline.round_id, `${R1}-r1`); - assert.equal(env.pipeline.stages.length, GATE_COUNT - 1, "dry 段=PROBE..RETRO(CLOSE 不入重放;含 #182 ANCHOR_SWEEP)"); + assert.equal(env.pipeline.stages.length, HEALTH_GATE_COUNT - 1, "dry 段=PROBE..RETRO(CLOSE 不入重放;含 #182 ANCHOR_SWEEP)"); assert.ok(env.pipeline.stages.every((s) => s.status === "dry"), "dry 状态词"); const g = j(path.join(roundDir(`${R1}-r1`), "gates.json")); - assert.equal(g.records.length, GATE_COUNT - 1, "可跑步门全重算(含 G_ANCHOR_SWEEP;CLOSE 不入重放)"); + assert.equal(g.records.length, HEALTH_GATE_COUNT - 1, "可跑步门全重算(含 G_ANCHOR_SWEEP;CLOSE 不入重放)"); assert.ok(g.records.every((x) => x.pass), "原件工件上全门重算 PASS(自愈:以工件+门为准)"); assert.equal(treeMd5(roundDir(R1)), before, "原件零改写"); }); @@ -1503,7 +1509,7 @@ await t("⑫-面选择纯函数:显式 fixtures→faux/继承面优先/缺省→ assert.equal(selectModelFace({ fixturesExplicit: true }), "faux", "显式 fixtures=faux 面(自测/golden)"); assert.equal(selectModelFace({ fixturesExplicit: true, inherited: "real" }), "faux", "显式 fixtures 赢过继承面(零成本审计 override)"); assert.equal(selectModelFace({ inherited: "faux" }), "faux", "继承面(replay 保真)"); - assert.deepEqual(STEP_ROUND, { TRIAGE: "health", SYNTH: "health", DEEP: "discover" }, "步→models.mjs 轮键映射(flash/glm-5.3)"); + assert.deepEqual(STEP_ROUND, { TRIAGE: "health", SYNTH: "health", DEEP: "discover", SHORTLIST: "discover" }, "步→models.mjs 轮键映射(flash/glm-5.3;SHORTLIST=#207 T1 发现轮)"); const st = j(path.join(roundDir(R1), "state.json")); assert.equal(st.model_face, "faux", "显式 fixtures 轮 state.model_face=faux(面入 state 审计面)"); }); @@ -1841,7 +1847,7 @@ await t("C3-ctx 形状单源(#204):ctxShape 闭集键钉死(mkCtx 与 runResume const { ctxShape } = await import("./pipeline.mjs"); const state = { schema: 1 }; const c = ctxShape({ type: "health", roundId: "2026-10-25-2110-health", opsRoot: "/ops", env: {}, log: () => {}, fixturesDir: "/fx", modelFace: "faux", budgetCny: 0.05, budgetKnobs: { monthlyCapCny: 30 }, breakerKnobs: {}, state }); - assert.deepEqual(Object.keys(c).sort(), ["breakerKnobs", "budgetCny", "budgetKnobs", "dir", "dupRegistry", "envelope", "env", "fixturesDir", "itemsDir", "ledgerFile", "log", "modelCalls", "modelFace", "opsRoot", "pkg", "retroAuditKey", "retroFile", "roundId", "rulesFile", "state", "type", "usage", "verify"].sort(), "键集=两消费面(gates/steps)所读闭集+ctxPaths 四键;手抄重建回潮=键漂移即红"); + assert.deepEqual(Object.keys(c).sort(), ["breakerKnobs", "budgetCny", "budgetKnobs", "dir", "discoverHost", "dupRegistry", "envelope", "env", "fixturesDir", "itemsDir", "ledgerFile", "log", "modelCalls", "modelFace", "opsRoot", "pkg", "retroAuditKey", "retroFile", "roundId", "rulesFile", "state", "type", "usage", "verify"].sort(), "键集=两消费面(gates/steps)所读闭集+ctxPaths 四键;手抄重建回潮=键漂移即红(#217 T3.5 追 discoverHost)"); assert.ok(c.dir === path.join("/ops", "rounds", "2026-10-25-2110-health"), "dir 自算(resume argv String 面与 mkCtx 字符串面同算)"); assert.equal(c.modelCalls, 0); assert.ok(c.dupRegistry instanceof Map); assert.deepEqual(c.usage, { input: 0, output: 0, totalTokens: 0, cost: 0 }, "调用登记/用量每次新建"); @@ -1974,6 +1980,466 @@ await t("A5-回声哨:篡改 09-budget.json 读数超帽 → G_CLOSE 复评门 assert.ok(rec.inputs.includes("09-budget.json")); }); +// ── 发现轮 T1(#207;spec /tmp/discover-pipeline-spec.md T1 节;六步骨架+旋钮族+台账)── +const RD = "2026-10-26-0940-discover"; +const budget = await import("./pipeline/budget.mjs"); + +await t("发现轮旋钮族(#207 T1+#217 T3.5):readDiscoverKnobs 缺省 30 调用/3600s/¥5/dup 3;月帽共享 OPS_PIPELINE_MONTHLY_CAP_CNY(同键同值,不另立月门);dup 拦截线沿 OPS_PIPELINE_DUP_CALL_LIMIT 同键同值(宿主接线消费点);负值/非数字 usage 拒", async () => { + const k = budget.readDiscoverKnobs({}); + assert.deepEqual([k.maxModelCalls, k.wallClockS, k.spendCapCny, k.dupCallLimit], [30, 3600, 5, 3], "spec T4 首月缺省(dup 面=#217 T3.5 追键,沿 health 同键同缺省)"); + assert.equal(k.monthlyCapCny, budget.readBudgetKnobs({}).monthlyCapCny, "月帽共享:与 health 同键同源"); + const ov = { OPS_DISCOVER_MAX_MODEL_CALLS: "3", OPS_PIPELINE_MONTHLY_CAP_CNY: "0.5", OPS_PIPELINE_DUP_CALL_LIMIT: "5" }; + assert.deepEqual([budget.readDiscoverKnobs(ov).maxModelCalls, budget.readDiscoverKnobs(ov).monthlyCapCny, budget.readDiscoverKnobs(ov).dupCallLimit], [3, 0.5, 5], "env 覆盖生效(月帽键=OPS_PIPELINE_MONTHLY_CAP_CNY;dup 键=OPS_PIPELINE_DUP_CALL_LIMIT 不另立)"); + assert.throws(() => budget.readDiscoverKnobs({ OPS_DISCOVER_MAX_MODEL_CALLS: "-1" }), /OPS_DISCOVER_MAX_MODEL_CALLS 须为非负数/, "负值拒"); + assert.throws(() => budget.readDiscoverKnobs({ OPS_DISCOVER_SPEND_CAP_CNY: "abc" }), /OPS_DISCOVER_SPEND_CAP_CNY/, "非数字拒"); + assert.throws(() => budget.readDiscoverKnobs({ OPS_PIPELINE_DUP_CALL_LIMIT: "x" }), /OPS_PIPELINE_DUP_CALL_LIMIT 须为非负数/, "dup 旋钮非数字拒(#217 T3.5)"); +}); + +await t("双面拒跑留痕轮标签(#207 T1):appendRefusalTrace round 参数化——discover 轮留痕行轮次列=discover(原硬编码 health 注释「discover 未立项」随立项退役)", async () => { + const tr = mkTree("refusal-round"); + fs.writeFileSync(tr.env.LEDGER_FILE, "| 日期 | 轮次 | model | 用量 | 备注 |\n|---|---|---|---|---|\n"); + await budget.appendRefusalTrace(tr.env.LEDGER_FILE, { est: 0.05, cap: 30 }, tr.env, "discover"); + assert.match(fs.readFileSync(tr.env.LEDGER_FILE, "utf8"), /\| discover \| — \| — \| [^|]*budget_monthly refused/, "留痕行轮次列=discover"); +}); + +await t("发现轮月帽共享执法(#207 T1):台账当月已花 ¥30+est>0 → discover 轮拒跑 budget_monthly(同一 admitOrRefuse),留痕行 round=discover,轮目录不建", async () => { + const tr = mkTree("discover-monthly"); + fs.writeFileSync(tr.env.LEDGER_FILE, "| 日期 | 轮次 | model | 用量 | 备注 |\n|---|---|---|---|---|\n| 2026-10-26 | discover | glm-5.3 | in=1 out=1 total=2 cost=30 | 满帽 |\n"); + const r = await runPipeline({ roundId: RD, fixturesDir: DEFAULT_FIXTURES_DIR, env: tr.env, ...mkIo() }); + assert.equal(r.ok, false); + assert.equal(r.envelope.error.code, "budget_monthly", "拒跑码与 health 同源(refusalOf 单源)"); + assert.match(fs.readFileSync(tr.env.LEDGER_FILE, "utf8"), /\| discover \| — \| — \| [^|]*budget_monthly refused/, "拒跑留痕 round=discover"); + assert.ok(!fs.existsSync(path.join(tr.root, "ops", "rounds", RD)), "拒跑不开轮目录"); +}); + +await t("round-id 后缀校验(#207 T1):discover 轮型拒 -health 后缀 id;health 轮型拒 -discover 后缀 id(双面 bad_round,后缀集=轮型集)", async () => { + const tr = mkTree("roundid-suffix"); + await assert.rejects(() => runPipeline({ type: "discover", roundId: "2026-10-26-0941-health", env: tr.env, ...mkIo() }), /形状须 YYYY-MM-DD-HHMM-discover/); + await assert.rejects(() => runPipeline({ type: "health", roundId: "2026-10-26-0941-discover", env: tr.env, ...mkIo() }), /形状须 YYYY-MM-DD-HHMM-health/); // 显式 type:缺省由后缀推断(推断面已由上例反证) +}); + +let runD; +await t("发现轮全 faux 六步(#207 T1):SCAN..CLOSE 全绿,信封 round=discover,台账行标签 pipeline-discover:,09-budget.json env 面=三旋钮+est+共享月帽(无 token 界),gates 记录=发现轮门序全 PASS,信封 pkg=null(无报告步)", async () => { + const tr = mkTree("discover-full"); + runD = { tr, r: await runPipeline({ roundId: RD, fixturesDir: DEFAULT_FIXTURES_DIR, env: tr.env, ...mkIo() }) }; + const r = runD.r; + assert.equal(r.ok, true, r.envelope?.error?.message ?? ""); + assert.equal(r.envelope.round, "discover"); + assert.deepEqual(r.envelope.pipeline.stages.map((s) => s.name), ["SCAN", "SHORTLIST", "MEASURE", "ENQUEUE", "RETRO", "CLOSE"], "六步执行态"); + assert.equal(j(path.join(tr.root, "ops", "rounds", RD, "state.json")).status, "done"); + assert.ok(fs.readFileSync(tr.env.LEDGER_FILE, "utf8").includes(`pipeline-discover:${RD}`), "台账标签 pipeline-discover(#207)"); + const b = j(path.join(tr.root, "ops", "rounds", RD, "09-budget.json")); + assert.deepEqual(Object.keys(b.env).sort(), ["OPS_DISCOVER_MAX_MODEL_CALLS", "OPS_DISCOVER_SPEND_CAP_CNY", "OPS_DISCOVER_WALL_CLOCK_S", "OPS_PIPELINE_BUDGET_CNY", "OPS_PIPELINE_MONTHLY_CAP_CNY"], "env 面 5 键"); + assert.equal(b.env.OPS_DISCOVER_MAX_MODEL_CALLS, 30, "旋钮读数落盘可证伪"); + const g = j(path.join(tr.root, "ops", "rounds", RD, "gates.json")); + assert.ok(g.records.every((x) => x.pass === true), g.records.filter((x) => !x.pass).map((x) => `${x.name}:${x.reason}`).join(";")); + assert.deepEqual(g.records.map((x) => x.name), ["G_SCAN", "G_SHORTLIST", "G_MEASURE", "G_ENQUEUE", "G_RETRO", "G_CLOSE"], "发现轮门序"); + assert.equal(r.envelope.pkg, null, "发现轮无报告步"); + assert.ok(fs.existsSync(path.join(tr.root, "ops", "retro", `${RD}.json`)), "retro 工件(ops/retro/,同 health 律)"); + // #217 T3.5 E2E 正例:罐装 rDNS 候选(url+排他率 0.42+measure.json 罐装读数)走通 + // SHORTLIST→MEASURE→PROPOSE 全链——不再只钉零入队;carrier(无 url+排他率 null)钉 hold 面。 + const d3 = j(path.join(tr.root, "ops", "rounds", RD, "d3-measure.json")); + const rdns = d3.results.find((x) => x.name === "rDNS 主机名"); + assert.equal(rdns.status, "measured", "三面全可算=measured(接线后不再恒 uncertainty)"); + assert.equal(rdns.evidence_local_ok, true, "存在性:本地重算过"); + assert.equal(rdns.url_read_ok, true, "URL 探测:宿主补发真执行(罐装面零网络)"); + assert.equal(rdns.control_executable, true, "阳性对照:探测 ok=true 才 true"); + assert.equal(rdns.exclusivity_estimate, 0.42, "排他率透传(SHORTLIST 值)"); + assert.equal(d3.hold_n, 1, "carrier 双面不可算=hold 1(#217 后不再全 hold)"); + assert.ok(Array.isArray(d3.tool_seeds) && d3.tool_seeds.length === 1 + && d3.tool_seeds[0].call_id === "measure-1" && d3.tool_seeds[0].tool === "web_read" + && d3.tool_seeds[0].sc.ok === true, "宿主补发留痕 tool_seeds(measure-N call_id)"); + const d4 = j(path.join(tr.root, "ops", "rounds", RD, "d4-enqueue.json")); + assert.deepEqual([d4.n_enqueued, d4.n_skipped], [1, 1], "三判据活路:1 入队 1 skip"); + const q = fs.readFileSync(path.join(tr.root, "ops", "queue.md"), "utf8"); + assert.ok(q.includes(`| ${RD} | PROPOSE | rdns |`), "queue.md PROPOSE 候选行真写入(端到端正例)"); + assert.equal(b.readings.discover_channels.channels.web_read.calls, 1, "CLOSE 读数第三键:web_read 实测 1 调(活宿主面)"); + assert.deepEqual([b.readings.discover_channels.calls_total, b.readings.discover_channels.blocked_total], [1, 0], "读数总面"); +}); + +await t("发现轮工件闭包+SCAN 确定性(#207 T1):轮目录=发现轮固定面(d1-d4+09/gates/state)∪shortlist transcript;候选宇宙两面=源注册表(≥40 已接源,repo 相对 evidence 指针)+NEEDS 缺口行(open/候选),web 面占位留痕;独立树重扫候选名单逐字节同", async () => { + const d = path.join(runD.tr.root, "ops", "rounds", RD); + const { ARTIFACTS, discoverArtifactNames } = await import("./artifacts.mjs"); + assert.deepEqual(fs.readdirSync(d).sort(), [...discoverArtifactNames(), "d2-shortlist-transcript.jsonl"].sort(), "固定面闭包"); + const scan = j(path.join(d, ARTIFACTS.DISCOVER_SCAN)); + const reg = scan.candidates.filter((c) => c.source_channel === "source_registry"); + const needs = scan.candidates.filter((c) => c.source_channel === "needs"); + // #208 T2:faux 轮宇宙=fixtures/d1-scan.json 钉死(seam 仅 modelFace=faux 生效)——罐装 span 例 + // 不随真仓 NEEDS.md 演进破门;真面 SCAN(≥40)由下方「SCAN 真面直调」例钉。 + assert.equal(reg.length, 1, "faux 轮候选宇宙=fixture 钉死(registry 1 条)"); + assert.equal(needs.length, 2, "faux 轮 needs 2 条"); + assert.ok(needs.every((c) => typeof c.text === "string" && c.text.includes("|")), "needs 条目带 text 原文行证据面(#208 T2:证据永不灭失,面入 d1)"); + assert.ok(reg.every((c) => /^backend\/ipdb\/_sources\/[a-z0-9_]+\.py$/.test(c.evidence_ptr)), "registry 指针=repo 相对模块路径"); + assert.ok(needs.every((c) => /^NEEDS\.md:L\d+$/.test(c.evidence_ptr) && ["open", "候选"].includes(c.status)), "needs 指针=NEEDS.md 行锚+缺口状态闭集"); + assert.equal(scan.channels.web.status, "placeholder", "web 面占位留痕(T3 #209 接线)"); + const tr2 = mkTree("discover-scan-det"); + await runPipeline({ roundId: "2026-10-26-0942-discover", fixturesDir: DEFAULT_FIXTURES_DIR, env: tr2.env, ...mkIo() }); + const s2 = j(path.join(tr2.root, "ops", "rounds", "2026-10-26-0942-discover", ARTIFACTS.DISCOVER_SCAN)); + assert.equal(JSON.stringify(s2.candidates), JSON.stringify(scan.candidates), "SCAN 确定性(零模型零网络,同 commit 名单同)"); +}); + +await t("T2-SCAN 真面直调(#208):modelFace 非 faux=扫真仓(registry ≥40+needs 行带 text 原文面+G_SCAN 过);text 面=行原文快照(证据永不灭失:quote span 回引在轮工件内可回算)", async () => { + const tr = mkTree("scan-realfaced"); + const dir = path.join(tr.root, "ops", "rounds", "2026-10-26-0944-discover"); fs.mkdirSync(dir, { recursive: true }); + const ctx = { type: "discover", roundId: "2026-10-26-0944-discover", dir, opsRoot: path.join(tr.root, "ops"), env: tr.env, fixturesDir: DEFAULT_FIXTURES_DIR, log: () => {}, usage: { input: 0, output: 0, totalTokens: 0, cost: 0 } }; // modelFace 缺席≠faux → 真面扫描(判例:faux seam 须显式面才生效) + await (await import("./pipeline/steps.mjs")).runStage({ name: "SCAN" }, ctx); + const { ARTIFACTS } = await import("./artifacts.mjs"); + const s = j(path.join(dir, ARTIFACTS.DISCOVER_SCAN)); + const regN = s.candidates.filter((c) => c.source_channel === "source_registry").length; + const needs = s.candidates.filter((c) => c.source_channel === "needs"); + assert.ok(regN >= 40, `真面 registry ≥40(实际 ${regN})`); + assert.ok(needs.length >= 1 && needs.every((c) => typeof c.text === "string" && c.text.trim().length > 0), "真面 needs 行全带原文 text 面"); + const { GATES } = await import("./pipeline/gates.mjs"); + assert.equal(GATES.G_SCAN(ctx).pass, true, "真面工件过 G_SCAN"); +}); + +await t("SHORTLIST 解析失败降级(#207 T1):canned 文本非 JSON → 轮仍全绿(不炸轮),d2-shortlist 候选空+degraded 留痕,MEASURE 零行,ENQUEUE 零入队", async () => { + const fx = path.join(TMP, "fx-degrade"); fs.mkdirSync(fx, { recursive: true }); + fs.writeFileSync(path.join(fx, "shortlist.json"), JSON.stringify({ turns: [{ role: "assistant", content: [{ type: "text", text: "本回合模型输出非 JSON(罐装解析失败面)" }], stopReason: "stop" }] })); + const tr = mkTree("discover-degrade"); + const r = await runPipeline({ roundId: "2026-10-26-0943-discover", fixturesDir: fx, env: tr.env, ...mkIo() }); + assert.equal(r.ok, true, "解析失败不炸轮(spec T1)"); + const d = path.join(tr.root, "ops", "rounds", "2026-10-26-0943-discover"); + const sh = j(path.join(d, "d2-shortlist.json")); + assert.deepEqual(sh.candidates, [], "降级空清单"); + assert.match(sh.degraded, /shortlist_parse/, "降级留痕"); + assert.deepEqual(j(path.join(d, "d3-measure.json")).results, []); + assert.equal(j(path.join(d, "d4-enqueue.json")).n_enqueued, 0); +}); + +await t("ENQUEUE 写路(#207 T1;#210 T4 action 词统一 PROPOSE):直调步——三判据全过候选经 #139 flock 路径落 queue.md 行(action=PROPOSE 与 T3 候选卡一词一拼,source 形 ^[a-z0-9_]{1,32}$,evidence 带 d3 读数);判据不过=skipped 留痕不落行;重跑=幂等去重", async () => { + const tr = mkTree("enqueue-unit"); + const { ARTIFACTS } = await import("./artifacts.mjs"); + const dir = path.join(tr.root, "ops", "rounds", RD); fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, ARTIFACTS.DISCOVER_SHORTLIST), JSON.stringify({ round_id: RD, candidates: [ + { name: "rdns_hf_parquet", url: "https://ip.thc.org", evidence_ptr: "NEEDS.md:L14" }, + { name: "skip_no_ev", url: null, evidence_ptr: "NEEDS.md:L999" }] })); + fs.writeFileSync(path.join(dir, ARTIFACTS.DISCOVER_MEASURE), JSON.stringify({ round_id: RD, results: [ + { candidate: "rdns_hf_parquet", evidence_local_ok: true, exclusivity_estimate: 0.42, control_executable: true }, + { candidate: "skip_no_ev", evidence_local_ok: false, exclusivity_estimate: 0.9, control_executable: true }] })); + const ctx = { type: "discover", roundId: RD, dir, opsRoot: path.join(tr.root, "ops"), env: tr.env, log: () => {}, usage: { input: 0, output: 0, totalTokens: 0, cost: 0 } }; + const steps2 = await import("./pipeline/steps.mjs"); + await steps2.runStage({ name: "ENQUEUE" }, ctx); + const q = fs.readFileSync(path.join(tr.root, "ops", "queue.md"), "utf8"); + assert.match(q, /\| PROPOSE \| rdns_hf_parquet \|/, "行落(action=PROPOSE+source;#210 T4 统一词)"); + assert.ok(!q.includes("skip_no_ev"), "证据未过候选不落行"); + await steps2.runStage({ name: "ENQUEUE" }, ctx); + assert.equal(fs.readFileSync(path.join(tr.root, "ops", "queue.md"), "utf8").split("| PROPOSE | rdns_hf_parquet |").length - 1, 1, "重跑去重不双写"); + const e4 = j(path.join(dir, ARTIFACTS.DISCOVER_ENQUEUE)); + assert.equal(e4.n_enqueued, 1); + assert.equal(e4.n_skipped, 1); +}); + +// ── T2(#208):四门强化+span 级回引+哨 ───────────────────────────────── +await t("T2-spanMatcher 三负例判例(#208):幽灵 URL/半真 URL(前缀子串+超集)/改写引文(大小写+标点)=全拒;恰等与逐字 span=收;非串/空串=拒", async () => { + const { spanMatcher } = await import("./pipeline/citation.mjs"); + const FACE = "| rDNS 主机名 | 反查 IP→域名 | 候选 | ip.thc.org 月度全量 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁) 2026-10-24 记 |"; + // 负例① 幽灵 URL:quote 引不在证据面的 URL + assert.equal(spanMatcher("见 https://ghost.example.com/x 提供月度 parquet", FACE), false, "幽灵 URL=拒"); + // 负例② 半真 URL 之(a) 前缀子串:面内是 https://ip.thc.org,quote 只引 https://ip.thc + assert.equal(spanMatcher("https://ip.thc 提供月度全量 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁)", FACE), false, "前缀子串半真 URL=拒(URL 边界律)"); + // 负例② 之(b) 超集串:面内 https://ip.thc.org,quote 引 https://ip.thc.org/extra + assert.equal(spanMatcher("https://ip.thc.org/extra 提供 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁)", FACE), false, "超集半真 URL=拒"); + // 负例③ 改写引文:大小写改写 + assert.equal(spanMatcher("IP.THC.ORG 月度全量 PARQUET(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁)", FACE), false, "大小写改写=拒"); + // 负例③ 改写引文:标点改写(中文逗号替空格) + assert.equal(spanMatcher("ip.thc.org 月度全量 parquet(49.5GB,HF+镜像双源实测活,FLAG=商用条款模糊+小机 1B 行重建未裁)", FACE), false, "标点改写=拒"); + // 正例:恰等整句 + 逐字 span(句中片段,URL 后随中文=定界在场) + assert.equal(spanMatcher(FACE, FACE), true, "恰等=收"); + assert.equal(spanMatcher("ip.thc.org 月度全量 parquet(49.5GB,HF+镜像双源实测活;FLAG=商用条款模糊+小机 1B 行重建未裁)", FACE), true, "逐字 span=收"); + // 非串/空串拒 + assert.equal(spanMatcher("", FACE), false, "空 quote=拒"); + assert.equal(spanMatcher(null, FACE), false, "非串 quote=拒"); + assert.equal(spanMatcher(FACE, null), false, "非串证据面=拒"); +}); + +await t("T2-scanEvidenceFace 三形解析(#208):透传形(NEEDS.md:L14/registry 路径)→条目 text 面;工件形(d1-scan.json#/candidates/N)→所指值/条目面;URL 形→URL 逐字在场的条目面;幽灵指针/外件/幽灵 URL=拒", async () => { + const { scanEvidenceFace } = await import("./pipeline/citation.mjs"); + const scan = { candidates: [ + { name: "feodo_tracker", evidence_ptr: "backend/ipdb/_sources/feodo_tracker.py", source_channel: "source_registry", status: "integrated" }, + { name: "rDNS 主机名", url: "https://ip.thc.org", evidence_ptr: "NEEDS.md:L14", source_channel: "needs", status: "候选", text: "| rDNS 主机名 | 反查 | 候选 | ip.thc.org 月度全量 parquet |" }, + { name: "Feodo Tracker", url: "https://feodotracker.abuse.ch/", evidence_ptr: "https://feodotracker.abuse.ch/", source_channel: "web", status: "候选" }] }; + const f1 = scanEvidenceFace(scan, "NEEDS.md:L14"); + assert.equal(f1.ok, true); assert.ok(f1.text.includes("ip.thc.org 月度全量 parquet"), "透传形→条目 text 面"); + const f1b = scanEvidenceFace(scan, "backend/ipdb/_sources/feodo_tracker.py"); + assert.equal(f1b.ok, true); assert.ok(!f1b.text.includes("text"), "registry 透传形→canonicalJson 面(无 text 键)"); + const f2 = scanEvidenceFace(scan, "d1-scan.json#/candidates/1"); + assert.equal(f2.ok, true); assert.ok(f2.text.includes("rDNS 主机名"), "工件形→所指值面"); + const f3 = scanEvidenceFace(scan, "https://feodotracker.abuse.ch/"); + assert.equal(f3.ok, true); assert.ok(f3.text.includes("Feodo Tracker"), "URL 形→URL 在场条目面"); + assert.equal(scanEvidenceFace(scan, "NEEDS.md:L999").ok, false, "透传形不在宇宙=拒"); + assert.equal(scanEvidenceFace(scan, "d9-nope.json#/x").ok, false, "外件指针=拒"); + assert.equal(scanEvidenceFace(scan, "https://ghost.example.com/").ok, false, "幽灵 URL=拒"); + assert.equal(scanEvidenceFace(scan, "指 针 含空白").ok, false, "三形外=拒"); +}); + +await t("T2-四门篡改红期(#208):G_SCAN 指针自洽/G_SHORTLIST span 改写/G_MEASURE 数字漂移/G_ENQUEUE 幽灵入队——篡改即门败(reason 带候选名+数字);未篡改原轮四门全绿对照", async () => { + const { GATES } = await import("./pipeline/gates.mjs"); + const base = path.join(runD.tr.root, "ops", "rounds", RD); + const { ARTIFACTS } = await import("./artifacts.mjs"); + const ctxLike = (dir) => ({ type: "discover", roundId: RD, dir, opsRoot: path.join(runD.tr.root, "ops"), envelope: runD.r.envelope }); + const tamper = (name, gate, doctor, why) => { + const dir = path.join(TMP, `t2-tamper-${name}`); + fs.rmSync(dir, { recursive: true, force: true }); fs.cpSync(base, dir, { recursive: true }); + doctor(dir); + const rec = GATES[gate](ctxLike(dir)); + assert.equal(rec.pass, false, `${name} 篡改须门败(得 ${rec.reason})`); + assert.ok(why.test(rec.reason), `${name} reason 面具名(得 ${rec.reason})`); + }; + // 对照:未篡改轮四门全绿(闭包绿先钉) + for (const g of ["G_SCAN", "G_SHORTLIST", "G_MEASURE", "G_ENQUEUE"]) assert.equal(GATES[g](ctxLike(base)).pass, true, `对照:${g} 原轮须绿`); + // ① G_SCAN:needs 条目指针改非行锚形(自洽破) + tamper("scan", "G_SCAN", (dir) => { const p = path.join(dir, ARTIFACTS.DISCOVER_SCAN); const s = j(p); s.candidates.find((c) => c.source_channel === "needs").evidence_ptr = "NEEDS.md:行14"; fs.writeFileSync(p, JSON.stringify(s, null, 2)); }, /指针形|自洽/); + // ② G_SHORTLIST:quote 标点改写(逐字 span 破) + tamper("shortlist", "G_SHORTLIST", (dir) => { const p = path.join(dir, ARTIFACTS.DISCOVER_SHORTLIST); const s = j(p); s.candidates[0].quote = s.candidates[0].quote.replace("(", "\uff08"); // 标点改写:半角→全角 U+FF08 + fs.writeFileSync(p, JSON.stringify(s, null, 2)); }, /逐字|span/); + // ③ G_MEASURE:排他率数字漂移(工件 0.9 vs shortlist null) + tamper("measure", "G_MEASURE", (dir) => { const p = path.join(dir, ARTIFACTS.DISCOVER_MEASURE); const s = j(p); s.results[0].exclusivity_estimate = 0.9; fs.writeFileSync(p, JSON.stringify(s, null, 2)); }, /排他率/); + // ④ G_ENQUEUE:幽灵入队行(不在 shortlist) + tamper("enqueue", "G_ENQUEUE", (dir) => { const p = path.join(dir, ARTIFACTS.DISCOVER_ENQUEUE); const s = j(p); s.enqueued.unshift({ candidate: "ghost_cand", name: "幽灵候选", reasons: [] }); s.n_enqueued += 1; fs.writeFileSync(p, JSON.stringify(s, null, 2)); }, /shortlist|测量/); +}); + +// ── T3(#209):发现轮结构化候选面(schema+证据存在性+候选卡行)────────────────── +await t("T3-候选schema:缺键/错型/未知键/坏通道词/坏evidence_ptr形状=拒;正例过(notes可缺省)", async () => { + const cand = await import("./pipeline/discover-candidate.mjs"); + const CANDIDATE_OK = { name: "Feodo Tracker", url: "https://feodotracker.abuse.ch/", source_channel: "web", + quote: "Tracking botnet C2 servers", evidence_ptr: "d1-scan.json#/entries/0", exclusivity_estimate: 0.4, notes: "" }; + assert.equal(cand.validateCandidate(CANDIDATE_OK).ok, true, "正例须过"); + const noUrl = { ...CANDIDATE_OK }; delete noUrl.url; + const r1 = cand.validateCandidate(noUrl); + assert.equal(r1.ok, false, "缺 url=拒"); + assert.ok(r1.reason.includes("url"), `拒因须点名 url:${r1.reason}`); + assert.equal(cand.validateCandidate({ ...CANDIDATE_OK, exclusivity_estimate: "0.5" }).ok, false, "排他率字符串=错型拒"); + assert.equal(cand.validateCandidate({ ...CANDIDATE_OK, exclusivity_estimate: 1.5 }).ok, false, "排他率>1=拒"); + assert.equal(cand.validateCandidate({ ...CANDIDATE_OK, source_channel: "manual" }).ok, false, "通道词闭集外=拒"); + assert.equal(cand.validateCandidate({ ...CANDIDATE_OK, score: 9 }).ok, false, "未知键=拒"); + assert.equal(cand.validateCandidate({ ...CANDIDATE_OK, evidence_ptr: "d1-scan.json 条目0" }).ok, false, "evidence_ptr 非两形=拒"); + const noNotes = { ...CANDIDATE_OK }; delete noNotes.notes; + assert.equal(cand.validateCandidate(noNotes).ok, true, "notes 缺省可过"); + // #208 T2 处置裁定(2026-10-26 交管):T3 discover-candidate.mjs 不动(其校验器=独立候选面, + // url/排他率必填律保留);发现轮 needs 面(url:null/排他率:null/透传形指针 NEEDS.md:L14) + // 的收留裁决在 gates.mjs G_SHORTLIST 门面(四门篡改例+全链六步例钉)与 citation.mjs + // scanEvidenceFace(span 解析)——两协议分立,非冲突。 +}); +await t("T3-证据存在性:指针缺段/工件不在集合/URL不在场=丢弃留痕;在场=留", async () => { + const cand = await import("./pipeline/discover-candidate.mjs"); + const CANDIDATE_OK = { name: "Feodo Tracker", url: "https://feodotracker.abuse.ch/", source_channel: "web", + quote: "Tracking botnet C2 servers", evidence_ptr: "d1-scan.json#/entries/0", exclusivity_estimate: 0.4, notes: "" }; + const artifacts = { "d1-scan.json": { entries: [ + { name: "Feodo Tracker", url: "https://feodotracker.abuse.ch/", channel: "web" }, + { name: "SSL Blacklist", url: "https://sslbl.abuse.ch/", channel: "web" }] } }; + const cs = [ + CANDIDATE_OK, + { ...CANDIDATE_OK, name: "幽灵条目", evidence_ptr: "d1-scan.json#/entries/9" }, + { ...CANDIDATE_OK, name: "外件", evidence_ptr: "d9-nope.json#/x" }, + { ...CANDIDATE_OK, name: "URL在场", evidence_ptr: "https://feodotracker.abuse.ch/" }, + { ...CANDIDATE_OK, name: "URL幽灵", evidence_ptr: "https://ghost.example.com/" }, + ]; + const r = cand.filterCandidatesByEvidence(cs, artifacts); + assert.deepEqual(r.kept.map((c) => c.name), ["Feodo Tracker", "URL在场"], "宁少算:缺证据只丢不修"); + assert.equal(r.dropped.length, 3, "丢弃=3 且留痕"); + assert.ok(r.dropped.every((d) => d.reason && typeof d.evidence_ptr === "string"), "留痕行须带 reason+evidence_ptr"); + assert.ok(r.dropped.some((d) => d.reason.includes("不在场")), `留痕须言明不在场:${JSON.stringify(r.dropped)}`); +}); +await t("T3-候选卡行:PROPOSE 落 queue.md(#139 flock 路径)幂等;坏候选=零写", async () => { + const cand = await import("./pipeline/discover-candidate.mjs"); + const CANDIDATE_OK = { name: "Feodo Tracker", url: "https://feodotracker.abuse.ch/", source_channel: "web", + quote: "Tracking botnet C2 servers", evidence_ptr: "d1-scan.json#/entries/0", exclusivity_estimate: 0.4, notes: "" }; + const queueFile = path.join(TMP, "docs", "ops", "queue-card.md"); + const bad = await cand.appendCandidateCard({ ...CANDIDATE_OK, quote: "" }, { round: "2026-10-26-0001-discover", queueFile }); + assert.equal(bad.ok, false, "坏候选拒写"); + assert.equal(fs.existsSync(queueFile), false, "拒写=零文件(宁少算)"); + const r1 = await cand.appendCandidateCard(CANDIDATE_OK, { round: "2026-10-26-0001-discover", queueFile }); + assert.deepEqual([r1.ok, r1.appended], [true, true], "首写落行"); + const r2 = await cand.appendCandidateCard(CANDIDATE_OK, { round: "2026-10-26-0001-discover", queueFile }); + assert.deepEqual([r2.ok, r2.appended, r2.dedup], [true, false, true], "同候选再写=幂等 dedup(action+source 判重)"); + const rows = fs.readFileSync(queueFile, "utf8").split("\n").filter((l) => l.startsWith("| 20")); + assert.equal(rows.length, 1, "恰一行候选卡"); + assert.match(rows[0], /\| PROPOSE \| [a-z0-9_]{1,32} \| — \|/, "action=PROPOSE+source slug 形+predicate 恒 —"); + assert.ok(rows[0].includes('candidate:name="Feodo Tracker"') && rows[0].includes("exclusivity_estimate=0.4"), "evidence 面=指针卡字段"); + assert.ok(rows[0].endsWith("| 是(否决=删除本行) |"), "vetoable 列冻结形"); + const r3 = await cand.appendCandidateCard({ ...CANDIDATE_OK, name: "SSL Blacklist", url: "https://sslbl.abuse.ch/" }, + { round: "2026-10-26-0001-discover", queueFile }); + assert.equal(r3.appended, true, "同 host 异 url 候选不互撞(slug 含 url 短哈希)"); +}); + +// ── T3(#209):发现轮工具宿主+配额一等公民(microprobes 同形制注入面)──────────── +await t("T3-发现宿主:守卫链 dup→breaker→cap 同序同义;被拒=零 seed 零执行;执行=登记 seed+通道计数+熔断回写", async () => { + const { mkDiscoveryHost, DISCOVERY_TOOLS } = await import("./pipeline/discovery-host.mjs"); + // 假实现面(测试缝:真面 discoveryMethods 走真网络/spawn 不可离线测;守卫/登记层全离线钉) + const exec = []; + const mkMethods = () => ({ + webRead() { return { name: "web_read", execute: async (id, p) => { exec.push(["web_read", p]); return this.finish(id, "web_read", { ok: p.fail !== true, n: 1 }, "fake read"); } }; }, + webSearch() { return { name: "web_search", execute: async (id, p) => { exec.push(["web_search", p]); return this.finish(id, "web_search", { ok: true, n: 2 }, "fake search"); } }; }, + rssParse() { return { name: "rss_parse", execute: async (id, p) => { exec.push(["rss_parse", p]); return this.finish(id, "rss_parse", { ok: false, code: "io_error" }, "fake rss"); } }; }, + }); + // ① dup 拦:同参第 2 次(dupLimit=2)→repeat_storm,零执行零 seed + const h1 = mkDiscoveryHost({ methods: mkMethods(), dupLimit: 2 }); + await h1.tools[0].execute("c1", { url: "https://a.example/" }); + const d = await h1.tools[0].execute("c2", { url: "https://a.example/" }); + assert.equal(d.structuredContent.code, "repeat_storm", "同参第 dupLimit 次=repeat_storm"); + assert.equal(exec.length, 1, "被拒=零执行"); + assert.equal(h1.seeds.length, 1, "被拒不入登记行"); + assert.equal(h1.channelCounts.web_read.blocked, 1, "被拒入 blocked 观察面"); + // ② breaker 拦:开断中→channel_open(先于 cap;dup 放行后) + const recorded = []; + const openBreaker = { verdict: () => ({ action: "block", retryAfterS: 99 }), record: (...a) => recorded.push(a) }; + const h2 = mkDiscoveryHost({ methods: mkMethods(), dupLimit: 0, breaker: openBreaker, budgets: { web_read: 5, web_search: 5, rss_parse: 5 } }); + const b = await h2.tools[0].execute("c3", { url: "https://b.example/" }); + assert.equal(b.structuredContent.code, "channel_open", "开断=channel_open 信封"); + assert.equal(exec.length, 1, "开断零执行"); + // ③ cap 拦:每轮帽用满→budget 信封 + const h3 = mkDiscoveryHost({ methods: mkMethods(), dupLimit: 0, budgets: { web_read: 1, web_search: 5, rss_parse: 5 } }); + await h3.tools[0].execute("c4", { url: "https://c.example/" }); + const c = await h3.tools[0].execute("c5", { url: "https://d.example/" }); + assert.equal(c.structuredContent.code, "budget", "帽满=budget 信封"); + assert.equal(h3.seeds.length, 1, "帽满拒不入登记"); + // ④ 执行面:seed 逐字登记+call_id 上翻+通道计数分列+熔断回写(仅通道工具/布尔 ok 面) + await h3.tools[1].execute("c6", { query: "q" }); + await h3.tools[2].execute("c7", { url: "https://e.example/feed" }); + assert.deepEqual(h3.seeds.map((s) => [s.call_id, s.tool]), [["c4", "web_read"], ["c6", "web_search"], ["c7", "rss_parse"]], "seeds=chronological 登记行(registry_seed 同形)"); + assert.deepEqual(h3.channelCounts, { web_read: { executed: 1, ok: 1, failed: 0, blocked: 1 }, + web_search: { executed: 1, ok: 1, failed: 0, blocked: 0 }, rss_parse: { executed: 1, ok: 0, failed: 1, blocked: 0 } }, "通道分列计数(ok/failed 分裂)"); + const passBreaker = { verdict: () => ({ action: "pass" }), record: (...a) => recorded.push(a) }; + const hb = mkDiscoveryHost({ methods: mkMethods(), dupLimit: 0, breaker: passBreaker }); + await hb.tools[2].execute("c8", { url: "https://f.example/feed" }); + assert.deepEqual(recorded, [["rss_parse", { ok: false, code: "io_error" }]], "熔断回写=信封码单源(仅布尔 ok 通道面)"); + assert.deepEqual(hb.tools.map((t) => t.name), DISCOVERY_TOOLS, "工具闭集序=web 三件"); + // ⑤ MEASURE 侧确定性直调:宿主补发 call-id(measure- 前缀,与运行时模型 id 不撞) + const r = await h3.call("web_search", { query: "direct" }); + assert.equal(r.structuredContent.ok, true, "直调可执行"); + assert.ok(h3.seeds.at(-1).call_id.startsWith("measure-"), "直调补发 measure-N"); + await assert.rejects(() => h3.call("gh_search", {}), /未知工具/, "闭集外直调=挪错"); +}); +await t("T3-配额聚合:aggregateChannelCalls 分列聚 ok/failed(种子行/扁平行两形);坏行掷错不静默吞", async () => { + const { aggregateChannelCalls } = await import("./pipeline/discovery-host.mjs"); + const rows = [ + { tool: "web_read", sc: { ok: true } }, + { tool: "web_read", sc: { ok: false, code: "quota" } }, + { tool: "web_search", ok: true }, + ]; + assert.deepEqual(aggregateChannelCalls(rows), + { web_read: { calls: 2, ok: 1, failed: 1 }, web_search: { calls: 1, ok: 1, failed: 0 } }, + "分列聚已执行面(被拒不入种子=不计——宁少算)"); + assert.throws(() => aggregateChannelCalls([{ tool: "web_read", sc: { code: "quota" } }]), /ok 非布尔/, "ok 缺失=掷错(回声对账面要响)"); + assert.throws(() => aggregateChannelCalls([{ ok: true }]), /缺 tool/, "缺 tool=掷错"); + assert.deepEqual(aggregateChannelCalls([]), {}, "空行集=空计数"); +}); +await t("T3-配额构造器:discoverChannelReadings 不变量 ok+failed=calls 违者掷错;正例形状进 readings", async () => { + const { discoverChannelReadings } = await import("./pipeline/discovery-host.mjs"); + const r = discoverChannelReadings({ web_read: { executed: 2, ok: 1, failed: 1, blocked: 1 }, + web_search: { calls: 1, ok: 1, failed: 0 } }); + assert.deepEqual(r, { channels: { web_read: { calls: 2, ok: 1, failed: 1, blocked: 1 }, + web_search: { calls: 1, ok: 1, failed: 0, blocked: 0 } }, calls_total: 3, blocked_total: 1 }, + "host 面(executed)与聚合面(calls)两词一义皆可入;blocked 缺省 0"); + assert.throws(() => discoverChannelReadings({ web_read: { calls: 2, ok: 1, failed: 0 } }), /不变量破/, "ok+failed≠calls=掷错"); + assert.throws(() => discoverChannelReadings({ web_read: { calls: 1.5, ok: 1, failed: 0 } }), /非非负整数/, "非整数计数=掷错"); + assert.throws(() => discoverChannelReadings({ web_read: { calls: -1, ok: 0, failed: -1 } }), /非非负整数/, "负数=掷错"); +}); +await t("T3-发现宿主真面形:默认 mkDiscoveryHost()=discoveryMethods 真工具(AgentTool 面零网络装载)", async () => { + const { mkDiscoveryHost } = await import("./pipeline/discovery-host.mjs"); + const h = mkDiscoveryHost(); // 缺省=真实现面(discovery.mjs 单源,零复制) + assert.deepEqual(h.tools.map((t) => t.name), ["web_read", "web_search", "rss_parse"], "真面三工具"); + for (const t of h.tools) { + assert.ok(t.label && t.description && t.parameters && t.outputSchema && t.annotations, `${t.name} AgentTool 面齐(label/parameters/outputSchema/annotations)`); + assert.ok(Object.keys(t.parameters.properties).length >= 1, `${t.name} 参数面非空`); + } +}); + +await t("T3.5-faux 罐装方法面(#217):fixtureDiscoveryMethods 三态——罐 ok:true 读数/罐 ok:false 失败面(分类码透传)/未罐装 io_error;web_search 未罐装恒 io_error;零网络(表查找无 fetch)", async () => { + const { fixtureDiscoveryMethods, mkDiscoveryHost } = await import("./pipeline/discovery-host.mjs"); + const fx = { web_reads: [ + { url: "https://a.example/ok", ok: true, title: "A", content: "罐装正文" }, + { url: "https://a.example/dead", ok: false, code: "channel_missing", reason: "404(faux 罐装失败面)" }, + ] }; + const h = mkDiscoveryHost({ methods: fixtureDiscoveryMethods(fx) }); + const ok = (await h.call("web_read", { url: "https://a.example/ok" })).structuredContent; + assert.equal(ok.ok, true); assert.equal(ok.title, "A"); assert.ok(ok.content.includes("罐装正文")); + const dead = (await h.call("web_read", { url: "https://a.example/dead" })).structuredContent; + assert.equal(dead.ok, false); assert.equal(dead.code, "channel_missing", "罐装失败面分类码透传"); + const miss = (await h.call("web_read", { url: "https://a.example/uncanned" })).structuredContent; + assert.equal(miss.ok, false); assert.equal(miss.code, "io_error", "未罐装=诚实未罐装信封非虚构"); + const sr = (await h.call("web_search", { query: "x" })).structuredContent; + assert.equal(sr.ok, false); assert.equal(sr.code, "io_error", "faux 面未罐装工具恒 io_error(零网络律)"); + assert.equal(h.seeds.length, 4, "全部执行入种子留痕"); + assert.deepEqual([h.channelCounts.web_read.executed, h.channelCounts.web_read.blocked], [3, 0]); +}); + +await t("T3.5-MEASURE 接线三态(#217):直调步=罐装 ok→measured/control true;dup 拦截(第 limit 次同参即拦)→uncertainty 显式不可算原因非清白;被拒零种子留痕宁少算", async () => { + const { runStage } = await import("./pipeline/steps.mjs"); + const tr = mkTree("t35-measure"); + const dir = path.join(tr.root, "ops", "rounds", "2026-10-26-1000-discover"); + fs.mkdirSync(dir, { recursive: true }); + const fxDir = path.join(TMP, "t35-measure-fx"); + fs.mkdirSync(fxDir, { recursive: true }); + fs.cpSync(path.join(AGENT, "fixtures/pipeline/d1-scan.json"), path.join(fxDir, "d1-scan.json")); + fs.writeFileSync(path.join(fxDir, "shortlist.json"), JSON.stringify({ turns: [] })); // MEASURE 不读;占位 + fs.writeFileSync(path.join(fxDir, "measure.json"), JSON.stringify({ web_reads: [ + { url: "https://a.example/x", ok: true, content: "同参四次试 dup 拦截" }] })); + const scan = j(path.join(AGENT, "fixtures/pipeline/d1-scan.json")); + fs.writeFileSync(path.join(dir, "d1-scan.json"), JSON.stringify(scan)); + const mk = (n) => ({ name: `dup-cand-${n}`, url: "https://a.example/x", quote: "x", evidence_ptr: "NEEDS.md:L14", source_channel: "needs", exclusivity_estimate: 0.5, notes: "" }); + fs.writeFileSync(path.join(dir, "d2-shortlist.json"), JSON.stringify({ round_id: "r", candidates: [mk(1), mk(2), mk(3), mk(4)] })); + const ctx = { type: "discover", roundId: "2026-10-26-1000-discover", dir, opsRoot: path.join(tr.root, "ops"), + env: tr.env, modelFace: "faux", fixturesDir: fxDir, budgetKnobs: { maxModelCalls: 30, wallClockS: 3600, spendCapCny: 5, monthlyCapCny: 30, dupCallLimit: 3 }, + breakerKnobs: {}, dupRegistry: new Map(), state: { started_at: new Date().toISOString() }, usage: { input: 0, output: 0, totalTokens: 0, cost: 0 }, modelCalls: 0, log: () => {}, discoverHost: null }; + await runStage({ name: "MEASURE", gate: "G_MEASURE" }, ctx); + const d3 = j(path.join(dir, "d3-measure.json")); + assert.equal(d3.results.length, 4); + assert.ok(d3.results.slice(0, 2).every((r) => r.status === "measured" && r.control_executable === true && r.url_read_ok === true), "第 1-2 次:罐装真读数=measured"); + assert.ok(d3.results.slice(2).every((r) => r.status === "uncertainty" && r.control_executable === null && /repeat_storm/.test(r.uncertainty_reason)), "第 3(limit)次起 dup 拦截=显式不可算(被拒≠清白)"); + assert.equal(d3.hold_n, 2); + assert.equal(d3.tool_seeds.length, 2, "被拒零种子:宁少算留痕面=2"); + assert.deepEqual([ctx.discoverHost.channelCounts.web_read.executed, ctx.discoverHost.channelCounts.web_read.blocked], [2, 2], "blocked 观察面照计"); + // G_MEASURE 同工件过门(接线后语义仍在门管) + const gates = await import("./pipeline/gates.mjs"); + assert.equal(gates.GATES.G_MEASURE(ctx).pass, true, "新行型(三态+tool_seeds)过 G_MEASURE"); +}); + +await t("T3.5-CLOSE 读数面(#217):活宿主优先(含 blocked);宿主缺席=工件回放重算(d2 transcript registry_seed+d3 tool_seeds;被拒不回放)——发现轮第三键,健康轮无此键", async () => { + const { runStage } = await import("./pipeline/steps.mjs"); + const { replayDiscoverSeeds, aggregateChannelCalls, discoverChannelReadings } = await import("./pipeline/discovery-host.mjs"); + const tr = mkTree("t35-close"); + const dir = path.join(tr.root, "ops", "rounds", "2026-10-26-1001-discover"); + fs.mkdirSync(dir, { recursive: true }); + // 工件面:d2 transcript 真面 registry_seed 留痕×1(web_read ok)+d3 tool_seeds×2(web_read ok + rss_parse fail) + fs.writeFileSync(path.join(dir, "d2-shortlist-transcript.jsonl"), [ + JSON.stringify({ type: "input", stage: "SHORTLIST", system_sha: "x", user_sha: "y", meta: null }), + JSON.stringify({ type: "registry_seed", call_id: "c1", tool: "web_read", sc: { ok: true, url: "https://a.example/x" } }), + JSON.stringify({ type: "registry_seed", call_id: "c2", tool: "probe_head", sc: { ok: true } }), // 健康轮 probe 工具不混入 + "{ 坏行不入回放(宁少算)", + "", + ].join("\n") + "\n"); + fs.writeFileSync(path.join(dir, "d3-measure.json"), JSON.stringify({ tool_seeds: [ + { call_id: "measure-1", tool: "web_read", sc: { ok: true } }, + { call_id: "measure-2", tool: "rss_parse", sc: { ok: false, code: "io_error" } }] })); + const rows = replayDiscoverSeeds({ dir }); + assert.deepEqual(rows, [{ tool: "web_read", ok: true }, { tool: "web_read", ok: true }, { tool: "rss_parse", ok: false }], "回放=d2 真面+d3 补发;probe 工具/坏行不混入"); + const readings = discoverChannelReadings(aggregateChannelCalls(rows)); + assert.deepEqual(readings.channels.web_read, { calls: 2, ok: 2, failed: 0, blocked: 0 }); + assert.equal(readings.calls_total, 3); + const mkCtx = (host) => ({ type: "discover", roundId: "2026-10-26-1001-discover", dir, opsRoot: path.join(tr.root, "ops"), + env: tr.env, modelFace: "faux", budgetKnobs: { maxModelCalls: 30, wallClockS: 3600, spendCapCny: 5, monthlyCapCny: 30, dupCallLimit: 3 }, + breakerKnobs: {}, dupRegistry: new Map(), state: { started_at: new Date().toISOString() }, usage: { input: 0, output: 0, totalTokens: 0, cost: 0 }, modelCalls: 0, log: () => {}, discoverHost: host }); + await runStage({ name: "CLOSE", bookend: true }, mkCtx(null)); + const b = j(path.join(dir, "09-budget.json")); + assert.deepEqual(b.readings.discover_channels.channels, { web_read: { calls: 2, ok: 2, failed: 0, blocked: 0 }, rss_parse: { calls: 1, ok: 0, failed: 1, blocked: 0 } }, "宿主缺席=回放重算面入 readings"); + assert.equal("discover_channels" in b.readings, true); + // 健康轮位等价:同 ctx type=health → 无第三键 + fs.rmSync(path.join(dir, "09-budget.json")); + const hc = mkCtx(null); hc.type = "health"; hc.roundId = "2026-10-26-1001-health"; + await runStage({ name: "CLOSE", bookend: true }, hc); + assert.equal("discover_channels" in j(path.join(dir, "09-budget.json")).readings, false, "健康轮字节面不变(纯增量键)"); +}); + +await t("T3.5-接线挂点哨(#217):SHORTLIST 真面工具直装/MEASURE 宿主补发/CLOSE 回放读数的源面挂点各在位(拆接线回退=哨红;行为面由 E2E/直调例钉)", async () => { + const fauxSrc = fs.readFileSync(path.join(AGENT, "pipeline/faux.mjs"), "utf8"); + assert.ok(fauxSrc.includes('def.name === "SHORTLIST"') && fauxSrc.includes("ensureDiscoverHost(ctx)"), "SHORTLIST 注入面在 faux.mjs runRealStep"); + const stepsSrc = fs.readFileSync(path.join(AGENT, "pipeline/steps.mjs"), "utf8"); + assert.ok(stepsSrc.includes('host.call("web_read", { url: c.url })'), "MEASURE 宿主补发在 steps.mjs"); + assert.ok(stepsSrc.includes("replayDiscoverSeeds(ctx)"), "CLOSE 回放读数面在 steps.mjs"); +}); + fs.rmSync(TMP, { recursive: true, force: true }); console.log(`pipeline-selftest: PASS=${pass} FAIL=${fail}`); process.exit(fail ? 1 : 0); diff --git a/ops/agent/tools.mjs b/ops/agent/tools.mjs index bce1ecae..89a10d4f 100644 --- a/ops/agent/tools.mjs +++ b/ops/agent/tools.mjs @@ -5,7 +5,10 @@ // 2026-10-24 裁定;融合必保 6 条 = tool-critic-1/2 并集;铁律①-⑦ 见各面文件注释。 // // 结构性红线(铁律⑥):零内建工具——本面只注册下列领域工具,agent 无 bash/write/edit; -// 唯一写口 = queue_action(append-only,恒 vetoable,幂等去重),包/台账/判例写回全在 host。 +// 写口注记:queue_action 是 ops 轮动作词面;发现轮两写口(ENQUEUE 候选行/候选卡 +// appendCandidateCard,均 PROPOSE)在 pipeline 侧另路同入 queue.md——表头三写口句 +// 见 queue-lock.mjs QUEUE_HEADER(#217 T3.5 翻正);本文件消费 appendQueueRowLocked +// 同 #139 flock 路径,包/台账/判例写回全在 host。 // 警察函数(校验/沙箱/台账)不注册为工具(被审计者不持审计工具)。 // // 失败面统一(铁律②):一切失败 = 结构化信封 {ok:false,code,reason},不抛裸栈、不静默; diff --git a/ops/agent/tools/queue-lock.mjs b/ops/agent/tools/queue-lock.mjs index a9f47b54..5ac35da0 100644 --- a/ops/agent/tools/queue-lock.mjs +++ b/ops/agent/tools/queue-lock.mjs @@ -47,10 +47,11 @@ export function queueLockPath(queueFile) { return queueFile + QUEUE_LOCK_SUFFIX; } -// T4 冻结表头(逐字;与 python _ops_store.QUEUE_HEADER 跨语言双抄是冻结形状的 -// 既定双源,§14 哨对拍钉死不漂移) +// 表头(T4 冻结列面;首行句 #217 T3.5 翻正:原「queue_action 唯一写口」随发现轮 +// 两新写口(ENQUEUE 候选行/appendCandidateCard 候选卡,均 PROPOSE)不再真——三写口 +// 如实描述;与 python _ops_store.QUEUE_HEADER 跨语言双抄,§14 形制对拍哨钉死不漂移) const QUEUE_HEADER = - "# 源生命周期动作队列(queue_action 唯一写口;行由 host 模板渲染,rationale 只落工件)\n\n" + "# 源生命周期动作队列(三写口:ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼;行由 host 模板渲染,rationale 只落工件)\n\n" + "| 日期 | 轮次 | action | source | predicate | evidence | vetoable |\n" + "|---|---|---|---|---|---|---|\n";