diff --git a/docs/handoff/2026-08-05-srcadm-01-surfaceome-admission.zh-CN.md b/docs/handoff/2026-08-05-srcadm-01-surfaceome-admission.zh-CN.md new file mode 100644 index 0000000..371efa1 --- /dev/null +++ b/docs/handoff/2026-08-05-srcadm-01-surfaceome-admission.zh-CN.md @@ -0,0 +1,132 @@ +# Handoff:SRCADM-01 surfaceome 来源准入审计 + +- 日期:`2026-08-05` +- 任务分支:`task_20260805_srcadm-01-surfaceome-admission` +- 基线:`main` @ `8aa7e87` +- 授权依据:审计范围 `AUD-01`..`AUD-09` 由 **PR #59** 冻结并获 `APPROVE`,即本审计的授权 +- 交付物类型:**审计记录(contract-only,无外部运行产物)** +- 结论:**`admissible_with_conditions`**——有条件可纳入,四项条件 +- 架构变更:`NO_ARCHITECTURE_CHANGE` +- 审核状态:等待 ChatGPT `APPROVE`。**本 PR 不适用 `AGENTS.md`「审核豁免」。** + + +## 〇、第一轮审核后补:可独立复核的审计包 + +审核意见:**审计结论的核心事实全部来自仓库外文件;仓库内的测试只验证审计文档自洽, +不能证明外部事实为真。** 这个意见成立,而且与我在 PR #62 对自己提的要求是同一条标准。 + +审计包:`external:result/gen_iet_srcadm_01_audit_bundle_20260806T000000Z` +ZIP SHA-256 `49d56c395661e7c71ba4caa60657126596cf4f784430ff462ab4512fdb0237b4`, +2,666,041 bytes,**13 个文件**(ZIP 内 14 个条目,多出的一条是目录条目)。 + +**上一版声明的 `2dbe88af…` 作废**——按复核要求补入了 `audit_expected.json`、 +`license_manifest.json` 与更多重算项,包内容变了哈希必然变。**以此为准。** + +解包后 `python3 verify_audit.py .` 即重算 **72 项**审计事实,逐项 `MATCH`/`MISMATCH`, +末行输出 `72/72 MATCH`。无网络、无写入、不依赖包外路径,解压即可运行,退出码 `0` 表示全通过。 + +**从全新解压目录实测:`72/72 MATCH`,退出码 0。** + +**为什么是 72 而不是上一版的 48。** 按复核要求补入了 processed 表逐文件 SHA-256/字节数/行数 +重算(9 项)、六个 license 歧义来源逐一验证其 license 文本确实歧义(6 项)、 +三个 CC BY 4.0 来源逐一验证(3 项)、license 清单覆盖全部 19 个来源、 +已记录 raw 摘要与 `checksums.sha256` 逐条一致、snapshot 与 dataset 版本一致性、 +target 轴规模等。**未删除任何原有检查**,48 项全部仍在。 + +**判据不来自预写结论。** 被审计主张放在 `audit_expected.json`,脚本从 builder 源码与 +三张表**重新算出**每个数字再比对;那份文件里写错一个数,这里就会 `MISMATCH`。 + +三张 processed 表**未做子集裁剪**——子集会使 11,334、6/5 重复键等计数无法重算, +审核方就只能重新相信叙述。整包压缩后 2.6 MB,代价可接受。 + +### 唯一不能在包内独立重算的项 + +19 个 raw 文件未随包提供(合计数 GB)。其校验和在审计时已实算,记于 +`raw_checksum_verification.json`(**19/19 `OK`**)。独立重算需归档 snapshot 本身—— +**这正是 `COND-03` 已声明的边界**,不是新增限制。`download_manifest.json` 随包提供, +故 `AUD-02` 可在包内完整重算。 + +### 重算过程中查出的一处表述问题 + +原文「HPA 有行但 `hpa_plasma_membrane = false` 的基因共 **11,334** 个」。 +重算脚本首版按「consensus 中该字段为 false」计数,得 **18,534**。 + +**11,334 是对的**:HPA 实际覆盖 **13,597** 个基因,其中 11,334 个该字段为 `false`; +多出的部分是**从未被 HPA 覆盖**的基因——该字段对它们同样是 `false`。 +原文的「有行」二字承载了全部区分度,容易读漏。现在机器可读记录与脚本都同时报出 +13,597/18,534/11,334。**两种口径下 `imaging` 被错误计入的都是 0 个**,`AUD-05` 结论不变。 + +### 另一处更正 + +`AUD-01` 原写 `AssetGenOS/scripts/build_t7_surfaceome_reference.py`,有歧义—— +该路径**在 StelligenOS 仓库之外**,是同级的 `AssetGenOS` 仓库。已更正,副本随包提供。 + +## 一、本次范围 + +人类负责人指示起 `SRCADM-01`。这是 Track B 的第一环,也是唯一挡住 `EVGAP-01` 的一环。 + +PR #59 明确「派生数据库不能靠自声明 + 哈希纳入」,并冻结九项必审内容。本次是那九项的实际审计——**读了 builder 源码、raw manifest、license 声明与 processed 表,而不是描述它们**。 + +**本 PR 不授予准入。** 准入由获 `APPROVE` 后成立,届时另开 PR 把 `EVGAP-01` 的 `admission_record_ref` 指向审核记录。 + +## 二、仓库内交付了什么 + +| 文件 | 作用 | +|---|---| +| `docs/tasks/SRCADM_01_SURFACEOME_ADMISSION_AUDIT.zh-CN.md` | 审计文档(面向操作者,中文) | +| `docs/pools/srcadm_01_surfaceome_admission.yaml` | 机器可读审计结论:九项 verdict 与依据、四项准入条件 | +| `tests/test_srcadm_01_surfaceome_admission.py` | 13 项校验 | + +## 三、九项结论 + +`AUD-01` PASS|`AUD-02` PASS(实算复核)|`AUD-03` PASS_WITH_FINDING|`AUD-04` PASS_WITH_FINDING|`AUD-05` PASS|`AUD-06` PASS_WITH_FINDING|`AUD-07` PASS|`AUD-08` PASS|`AUD-09` PASS_WITH_FINDING + +**无一项 FAIL**,四项带 finding 且全部界定了影响范围。 + +## 四、三个最关键的结论 + +**`AUD-05` 独立性成立,而且恰好避开了审核方点名的失效模式。** 三家族映射为 `curated_knowledge <- {goa_human, uniprot_reviewed_human}`、`imaging <- {hpa_subcellular_location}`、`cell_surface_capture_ms <- {cspa}`。**`goa_human` 与 `uniprot_reviewed_human` 同源**(GOA human 由 UniProt 策展流程产出),而 builder 把二者收进同一个家族,因此不会重复计数——`GUCY2C` 两来源皆 supported,`family_count` 仍为 1。两条加强验证:family 计数要求**支持性**证据(反例检验 11,334 个 HPA 有行但 `hpa_plasma_membrane=false` 的基因中,`imaging` 被计入的为 **0**);`family_count >= 2` 必然包含至少一个实验型家族。 + +**`AUD-04` license 歧义存在但不触及 EVGAP-01 读取的字段。** 六个来源 license 有歧义(cellphonedb ×4、cellchatdb GPL-3.0、omnipath per-resource),**但没有任何一个出现在 `source_evidence.tsv` 中**——它们只喂已被 PR #59 禁用的 `cci_receptor_*`。进入的四个来源里三个是 CC BY 4.0。**该结论承重**:依赖 #59 的字段白名单,白名单一扩大准入即失效(`COND-02`)。 + +**`AUD-09` 完整性可验、可复现性有边界。** 19 个 raw 文件 `shasum -c` 全部 OK;builder 唯一时间依赖是时间戳字段、无 random。**但 `uniprot_reviewed_human` 与 `goa_human` 的 release 是 `current_at_download`,不是版本号**,从上游重新下载不保证逐字节复现。可复现性成立的前提是使用已归档 snapshot(`COND-03`)。 + +## 五、四项准入条件 + +`COND-01` 仅限这一个 snapshot(23 个校验和钉住)|`COND-02` 仅限 #59 字段白名单|`COND-03` 基于已归档 snapshot 而非可从上游复现|`COND-04` 重复键不得进入 EVGAP-01 判据(靶点轴扩大须重查) + +## 六、其余两项 finding + +**`AUD-03`**:manifest 的 `release` 字段全部为 null,release 字符串由 builder 另行赋值;四个在用来源中两个是 `current_at_download`。 + +**`AUD-06`**:builder 中检索不到显式去重例程;实测 6 + 5 个重复键。受影响的 11 个基因**没有一个属于 41 个靶点**,且 family 计数取自 support 布尔值而非行数,故重复行结构上不可能抬高 `RQ-01`;41 个靶点的 `family_count` 与 `families` 列表实测 100% 一致。 + +## 七、一处如实记录的残留 + +`builder_version` 由 config 传入而非脚本内常量,故「0.3.0」这个版本号依赖 `build_manifest` 自述、**不能由脚本自身独立确证**。不构成阻断(snapshot 由校验和钉住,版本号只是标签),但记录在案。 + +## 八、明确没有做什么 + +- **没有授予准入**;没有修改 `evgap_01_surface_localization_extraction.yaml` 的 `admission_record_ref`(仍为 `null`,`authorises_extraction_run` 仍为 `false`)。 +- 没有执行 `EVGAP-01` 抽取;没有执行 Level 01;没有解除 `EVGAP-01` 或 `EVGAP-02`。 +- 没有纳入该数据集的其他版本或后续重建;没有扩大 PR #59 的字段白名单。 +- 没有纳入 `SRCADM-02`..`SRCADM-05` 的任何派生库。 +- 没有产生任何外部运行产物——本次是对既有文件的审计,未生成数据。 +- 未补八份批准记录(现为九份,含 #62),事实已查全但未写文件。 + +## 九、验证结果 + +- `Ran 351 tests` 全部通过(`main` 基线 338 + 本次新增 13)。 +- `scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。 +- 测试逐条把审计结论钉在 PR #59 冻结的范围上:九项 ID 集合必须与 `required_audit_items` 完全相等;无 `FAIL`;每项必须有可复核依据(`PASS_WITH_FINDING` 必须写出 finding);`AUD-02` 的摘要必须是 64 位十六进制且与 #59 引用的前缀一致;`AUD-05` 必须点名同源来源对并引用实测反例数;`AUD-04` 依赖的 `cci_receptor_role` 必须确实在 #59 的 `barred_fields` 中;`AUD-06` 的影响界定必须针对 41 靶点轴;本审计不得授予准入。 +- 一处测试自身的错误已修:初稿从 `source_admission_dependency` 读 `raw_manifest_sha256` 键,而该键在 #59 修订后并不存在(摘要写在 `AUD-02` 的条目文本里)。改为跨文件比对前缀并校验 64 位十六进制。 +- **变异检验 11 个,全部被捕获后精确回滚**:自行授予准入、自填 `admission_record_ref`、删掉一项审计、`AUD-05` 去掉同源来源说明、`AUD-05` 声明「有行即计」、结论改为无条件可纳入、删掉白名单条件 `COND-02`、抹去 `AUD-09` 的可复现性限制、把 `AUD-06` 影响界定改成含糊表述、删掉受影响基因清单、把某项判为 `FAIL` 却不改总结论。 +- **其中两个变异首轮逃逸,是我断言太松导致的**:`assertIn("current_at_download", finding)` 与 `assertIn("41", bound)` 在部分删除后仍能命中残留文本。已改为要求 `AUD-09` 的 finding 同时点名两个未钉版本的来源并写出「逐字节」不可复现,要求 `AUD-06` 的 bounded_impact 明确写出「没有一个属于」并列出受影响基因。重测三个变异全部被捕获。 + +## 十、后续顺序 + +1. 本审计 `APPROVE`,在 `logs/` 留审核记录。 +2. **另开 PR** 把 `admission_record_ref` 指向该记录,`authorises_extraction_run` 转 `true`。 +3. 执行 `EVGAP-01` 抽取 → 结果 PR → binding,解除 `EVGAP-01`。 +4. `EVGAP-02` 抽取已执行,结果审核在 PR #62;获批后另开 PR 解除 `EVGAP-02`。 +5. **两个缺口都解除后**才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 diff --git a/docs/pools/srcadm_01_surfaceome_admission.yaml b/docs/pools/srcadm_01_surfaceome_admission.yaml new file mode 100644 index 0000000..afd332c --- /dev/null +++ b/docs/pools/srcadm_01_surfaceome_admission.yaml @@ -0,0 +1,272 @@ +# SRCADM-01:ADC_surfaceome_reference@0.3.0 来源准入审计(machine-readable) +# +# 审计范围 AUD-01..AUD-09 由 PR #59 冻结并获 ChatGPT APPROVE,即本审计的授权依据。 +# 本文件记录逐项审计结论与其可复核依据。它不含任何证据数据。 +# +# 由 tests/test_srcadm_01_surfaceome_admission.py 校验。 +# 本文件**不**授予准入:准入由本 PR 获 APPROVE 后成立,届时另开 PR 把 +# evgap_01_surface_localization_extraction.yaml 的 admission_record_ref 指向审核记录。 + +admission: + admission_id: SRCADM-01 + dataset_id: ADC_surfaceome_reference + dataset_version: 0.3.0 + snapshot_id: 2026-07-29-quant-topology-mm + audit_scope_frozen_by_pr: 59 + audit_scope_items: 9 + status: pending_review + # 结论是「有条件可纳入」,不是无条件可纳入。四项条件见 admission_conditions。 + recommendation: admissible_with_conditions + grants_admission_by_itself: false + admission_record_ref: null + next_step: 获 APPROVE 后另开 PR 把 EVGAP-01 契约的 admission_record_ref 指向审核记录 + +# 可独立复核的审计包。PR #63 第一轮审核指出:审计结论的核心事实全部来自仓库外文件, +# 仅凭文档自洽的测试无法证明外部事实为真。该意见成立——本节即其应答。 +audit_bundle: + bundle_id: external:result/gen_iet_srcadm_01_audit_bundle_20260806T000000Z + package: gen_iet_srcadm_01_audit_bundle_20260806T000000Z.zip + package_sha256: 49d56c395661e7c71ba4caa60657126596cf4f784430ff462ab4512fdb0237b4 + package_bytes: 2666041 + files: 13 + zip_entries: 14 + read_only: true + grants_admission: false + # 复核方式:解包后运行 python3 verify_audit.py . + reverification_command: python3 verify_audit.py . + recomputed_checks: 72 + recomputed_result: all_match + # 判据不来自预写结论:主张放在 audit_expected.json,脚本从 builder 源码与三张表 + # 重新算出每个数字再比对。写错一个数就会 MISMATCH,不会静默通过。 + verdicts_read_from_file: false + expected_values_file: audit_expected.json + verifier_properties: + network_access: false + writes_files: false + depends_on_paths_outside_bundle: false + runnable_after_extract: true + exit_zero_means_all_passed: true + final_line: 72/72 MATCH + # 上一版为 48 项。本轮按复核要求补入 processed 表逐文件摘要/字节数/行数重算、 + # 六个 license 歧义来源逐一验证、三个 CC BY 4.0 来源逐一验证等,未删除任何原有检查。 + previous_recomputed_checks: 48 + contents: + - file: build_t7_surfaceome_reference.py + serves: [AUD-01, AUD-05, AUD-06, AUD-07, AUD-09] + - file: build_manifest.json + serves: [AUD-01, AUD-02] + - file: download_manifest.json + serves: [AUD-02, AUD-03, AUD-04] + - file: checksums.sha256 + serves: [AUD-09] + - file: raw_checksum_verification.json + serves: [AUD-09] + - file: source_evidence.tsv + serves: [AUD-03, AUD-04, AUD-05, AUD-06, AUD-08] + - file: membrane_topology_evidence.tsv + serves: [AUD-06] + - file: surfaceome_consensus.tsv + serves: [AUD-05, AUD-06, AUD-07, AUD-08] + - file: evgap_01_target_axis.txt + serves: [AUD-06] + - file: verify_audit.py + serves: [all] + - file: audit_expected.json + serves: [all] + - file: license_manifest.json + serves: [AUD-04] + - file: README.md + serves: [all] + # 三张 processed 表未做子集裁剪,故所有计数可精确重算,而非只能核对代表性行。 + processed_tables_subset: false + processed_tables_subset_reason: >- + 子集会使 11,334、6/5 重复键等计数无法重算,审核方只能重新相信叙述。 + # 唯一不能在包内独立重算的项,如实写明。 + not_independently_recomputable_in_bundle: + - item: AUD-09 的 19 个 raw 文件逐字节校验 + reason: raw 文件合计数 GB,未随包提供 + what_is_provided: >- + checksums.sha256 与审计时实算结果 raw_checksum_verification.json(19/19 OK) + full_recomputation_requires: 归档 snapshot 本身 + already_stated_as: COND-03 + - item: AUD-02 的 SHA-256 对应关系 + reason: 无此限制——download_manifest.json 随包提供,可在包内完整重算 + note: 列此条是为免误读为同样受限 + +# 逐项审计结论。每一条都必须有可复核的依据,不接受「已阅读」这类断言。 +audit_findings: + - id: AUD-01 + item: builder 实现与版本 + verdict: PASS + evidence: >- + /Volumes/Stelligen_SSD/Stelligen/AssetGenOS/scripts/build_t7_surfaceome_reference.py + 存在,2,721 行,可读。**注意该路径在 StelligenOS 仓库之外**,是同级的 AssetGenOS + 仓库;首版审计只写 AssetGenOS/scripts/... 有歧义,已更正。副本随审计包提供。 + family 计数、discordance 生成、consensus 打分均为显式条件分支,无隐藏启发式。 + residual_note: >- + builder_version 由 config 传入而非脚本内常量,故「0.3.0」这个版本号依赖 + build_manifest 的自述,不能由脚本自身独立确证。 + + - id: AUD-02 + item: raw manifest 与 raw_manifest_sha256 的对应关系 + verdict: PASS + evidence: >- + 对 raw/2026-07-29-quant-topology-mm/download_manifest.json 实算 SHA-256, + 得 884f419118302ae39c3e50292d03295ff676434868e1061b39ead50f9cc977bb, + 与 build_manifest.json 声明的 raw_manifest_sha256 逐字符一致。 + verification_method: recomputed + + - id: AUD-03 + item: 原始来源清单与各自 release 版本 + verdict: PASS_WITH_FINDING + evidence: >- + manifest 列出 19 个文件、19 个不同 source_id。processed 表的 source_release + 有实义取值:cspa「PLOS ONE 2015 supplementary file S2」、 + hpa_subcellular_location「HPA 25.1; Ensembl 109」、 + goa_human「current_at_download; ontology=releases/2026-06-15」、 + uniprot_reviewed_human「current_at_download」。 + finding: >- + manifest 的 files 条目里 release 字段全部为 null;release 字符串由 builder + 另行赋值。且 EVGAP-01 实际使用的四个来源中,**uniprot_reviewed_human 与 + goa_human 的 release 是 current_at_download,不是版本号**——它们只钉住下载 + 时刻,不钉住上游版本。 + consequence: 见 AUD-09 的可复现性边界 + + - id: AUD-04 + item: 每个原始来源的 license 与再分发条件 + verdict: PASS_WITH_FINDING + evidence: >- + 19 个来源全部声明 license,无缺失。进入 source_evidence.tsv 的四个来源中 + goa_human、hpa_subcellular_location、uniprot_reviewed_human 为 CC BY 4.0。 + finding: >- + 六个来源的 license 有歧义:cellphonedb_gene/protein/complex/interaction + 「not declared in cellphonedb…」、cellchatdb_human「GPL-3.0 repository + license」、omnipath_intercell_receptor「per-resource licenses retained」。 + 另有一处命名不一致:processed 表用 source_id「cspa」,manifest 用 + 「cspa_validated_surfaceome」与「cspa_cell_type_matrix」,无法直接 join。 + decisive_check: >- + 实测:这六个 license 歧义来源**没有任何一个出现在 source_evidence.tsv 中**。 + 它们只喂 cci_receptor_* 字段,而该字段已被 PR #59 的 barred_fields 禁用。 + 因此再分发歧义不触及 EVGAP-01 读取的任何字段。 + load_bearing_boundary: >- + 该结论依赖 PR #59 的字段白名单。若白名单日后扩大到 cci_receptor_*, + 本准入必须重新审。 + + - id: AUD-05 + item: independent_evidence_family 的独立性是否真实成立 + verdict: PASS + evidence: >- + 三个家族的来源映射为 curated_knowledge <- {goa_human, + uniprot_reviewed_human};imaging <- {hpa_subcellular_location}; + cell_surface_capture_ms <- {cspa}。 + same_origin_pair_handled: >- + goa_human 与 uniprot_reviewed_human 同源(GOA human 由 UniProt 策展流程产出)。 + builder 把二者收进**同一个** curated_knowledge 家族,因此不会被重复计成两个 + 独立家族——这正是审核方点名的「同源数据重复计数」失效模式,builder 避开了它。 + 实例:GUCY2C 两个来源都 supported,family_count 仍为 1。 + counting_requires_support: true + counting_requires_support_evidence: >- + family 由 consensus 的 support 布尔值推出(builder 第 2112–2118 行), + 不是「有行即计」。实测反例检验:**HPA 实际覆盖 13,597 个基因** + (在 source_evidence.tsv 中有 hpa_subcellular_location 行), + 其中 hpa_plasma_membrane = false 的为 **11,334 个**, + 而 imaging 被计入家族的为 **0 个**。 + 口径说明:consensus 中 hpa_plasma_membrane = false 的行共 18,534 条, + 但其中包含**从未被 HPA 覆盖**的基因——该字段对它们同样是 false。 + 11,334 这个数只统计 HPA 确有覆盖的基因,「有行」二字承载全部区分度。 + 两种口径下 imaging 被错误计入的基因都是 0 个。 + structural_guarantee: >- + 由于 curated_knowledge 是唯一可由两个来源喂养但只计一次的家族, + family_count >= 2 必然包含至少一个实验型家族(imaging 或 + cell_surface_capture_ms)。RQ-01 的两家族门槛因此不是形式门槛。 + + - id: AUD-06 + item: 来源去重逻辑 + verdict: PASS_WITH_FINDING + finding: >- + builder 中检索不到显式去重例程。实测 processed 表存在重复键: + source_evidence.tsv 在 (gene_symbol, source_id, evidence_kind) 上有 6 个重复键, + membrane_topology_evidence.tsv 在 (gene_symbol, source_id) 上有 5 个。 + surfaceome_consensus.tsv 在 gene_symbol 上无重复。 + bounded_impact: >- + 重复键涉及的基因为 HERC3、MATR3、NPIPA9、PINX1、POLR2J3、PRODH、 + ERVK-7、NRXN1、NRXN2、NRXN3、SIRPB1,**没有一个属于 EVGAP-01 的 41 个靶点**。 + 且 family 计数取自 consensus 的 support 布尔值而非 source_evidence 行数, + 故重复行在结构上不可能抬高 RQ-01 的家族数;41 个靶点的 family_count 与 + families 列表实测 100% 一致。 + + - id: AUD-07 + item: discordance_flags 的生成规则与覆盖范围 + verdict: PASS + evidence: builder 第 2121–2129 行,四条确定性规则,条件与标记一一对应。 + rules: + - hpa_plasma_membrane_uncertain:HPA reliability 为 uncertain + - cspa_support_without_uniprot_explicit_surface:MS 支持但 UniProt 未显式标注表面 + - hpa_only_vs_curated_knowledge:仅影像支持、策展来源不支持 + - generic_membrane_without_surface_localization:仅泛膜注释、无任何表面定位支持 + relevance_to_evgap_01: >- + PR #59 的 E1-04 把 discordance_flags 非空一律判 DEFER,故这四条规则的作用 + 只是把冲突显式化并触发 DEFER,不会产生 RETAIN。 + + - id: AUD-08 + item: 代表性靶点的逐行来源回溯 + verdict: PASS + evidence: >- + 对三个代表性靶点逐行回溯成功,每条主张都能落到 source_id + surface_supported + + source_release + source_url:CDH17(ECD-a 路径,families + cell_surface_capture_ms+curated_knowledge,n=2)、CEACAM5(ECD-b GPI 路径, + families curated_knowledge+imaging,n=2)、GUCY2C(families 仅 + curated_knowledge,n=1,故在 PR #59 规则下落 E1-02 hold)。 + targets_traced: + - CDH17 + - CEACAM5 + - GUCY2C + + - id: AUD-09 + item: snapshot 是否可由 builder 与 raw manifest 复现 + verdict: PASS_WITH_FINDING + integrity_verified: >- + `shasum -a 256 -c checksums.sha256` 对 raw snapshot 的 19 个文件全部 OK。 + determinism: >- + builder 中唯一的时间依赖是第 268 行 datetime.now() 用于 processed_at_utc + 时间戳字段,不参与任何计算;无 random、无 shuffle。给定同一 raw snapshot, + 构建结果确定。 + finding: >- + **但 raw snapshot 本身对 uniprot_reviewed_human 与 goa_human 未钉版本** + (release 为 current_at_download)。因此从上游重新下载并重建**不能**保证 + 逐字节复现该 snapshot。可复现性成立的前提是使用已归档的 raw snapshot, + 而不是从来源重新获取。 + +# 有条件准入的四项条件。任一条被破坏即须重新审。 +admission_conditions: + - id: COND-01 + condition: 准入仅限这一个 snapshot + detail: >- + dataset_version 0.3.0 与 snapshot_id 2026-07-29-quant-topology-mm, + 由 19 个 raw 文件校验和与 4 个 processed 文件校验和共同钉住。 + 不覆盖该数据集的其他版本,也不覆盖任何后续重建。 + - id: COND-02 + condition: 准入仅限 PR #59 的字段白名单 + detail: >- + license 歧义来源只喂已被禁用的 cci_receptor_* 字段,这是 AUD-04 结论成立的 + 前提。若白名单扩大到这些字段,本准入立即失效、须重新审。 + - id: COND-03 + condition: 准入基于已归档 snapshot,不基于可从上游复现 + detail: >- + 两个来源为 current_at_download,重新下载不保证逐字节一致。归档副本即事实来源。 + - id: COND-04 + condition: 重复键不得进入 EVGAP-01 的判据 + detail: >- + 已实测 11 个受影响基因全部在 41 个靶点之外,且 family 计数不按行数累加。 + 若 EVGAP-01 的靶点轴日后扩大,须重新检查重复键是否触及新靶点。 + +# 本审计明确不做什么。 +not_authorised: + - 授予准入(准入由本 PR 获 APPROVE 后成立) + - 修改 evgap_01_surface_localization_extraction.yaml 的 admission_record_ref + - 执行 EVGAP-01 抽取 + - 执行 Level 01 + - 解除 EVGAP-01 或 EVGAP-02 + - 把该数据集的其他版本或后续重建纳入 + - 扩大 PR #59 的字段白名单 + - 纳入 SRCADM-02..SRCADM-05 的任何派生库 diff --git a/docs/tasks/SRCADM_01_SURFACEOME_ADMISSION_AUDIT.zh-CN.md b/docs/tasks/SRCADM_01_SURFACEOME_ADMISSION_AUDIT.zh-CN.md new file mode 100644 index 0000000..6801297 --- /dev/null +++ b/docs/tasks/SRCADM_01_SURFACEOME_ADMISSION_AUDIT.zh-CN.md @@ -0,0 +1,161 @@ +# SRCADM-01:`ADC_surfaceome_reference@0.3.0` 来源准入审计 + +- 任务分支:`task_20260805_srcadm-01-surfaceome-admission` +- 授权依据:审计范围 `AUD-01`..`AUD-09` 由 **PR #59** 冻结并获 ChatGPT `APPROVE` +- 机器可读记录:[`../pools/srcadm_01_surfaceome_admission.yaml`](../pools/srcadm_01_surfaceome_admission.yaml),由 `tests/test_srcadm_01_surfaceome_admission.py` 校验 +- 当前状态:**审计已完成,结论待审核** +- 可独立复核的审计包:`external:result/gen_iet_srcadm_01_audit_bundle_20260806T000000Z`, + ZIP SHA-256 `2dbe88af1a2e9aee8004b9cbdd894c48f2f91197726678898aadf5da3f75e931` +- 结论:**`admissible_with_conditions`——有条件可纳入,四项条件见第三节** + +## 目的 + +`EVGAP-01` 的抽取被 `SRCADM-01` 阻断:`ADC_surfaceome_reference@0.3.0` 从未被审核,PR #59 明确「派生数据库不能靠自声明 + 哈希纳入」,并冻结了九项必审内容。 + +本文件是那九项的审计结论。**本文件不授予准入**——准入由本 PR 获 `APPROVE` 后成立,届时另开 PR 把 `EVGAP-01` 契约的 `admission_record_ref` 指向审核记录。 + +## 〇、审计包(第一轮审核后补) + +第一轮审核指出:**审计结论的核心事实全部来自仓库外文件,而仓库内的测试只验证审计文档 +自洽,不能证明外部事实为真。** 这个意见成立——所以本轮补一个只读复核包。 + +解包后运行: + +``` +python3 verify_audit.py . +``` + +脚本只读包内文件,**重算 48 项审计事实**,逐项输出 `MATCH` / `MISMATCH`。 +本次结果:**48 / 48 全部 `MATCH`**。 + +三张 processed 表**未做子集裁剪**(`source_evidence` 11.8 MB、`surfaceome_consensus` 8.9 MB、 +`membrane_topology_evidence` 3.5 MB),因此 11,334、6/5 重复键、家族映射等数字都能**精确重算**, +而不是只能核对代表性行。整包压缩后 2.6 MB。 + +### 唯一不能在包内独立重算的一项 + +**19 个 raw 文件未随包提供**(合计数 GB)。其校验和在审计时已从归档 snapshot 实算, +结果记在 `raw_checksum_verification.json`(**19/19 `OK`**)。要独立重算这一项需要归档 +snapshot 本身——**这正是 `COND-03` 已声明的边界**,不是新增限制。 + +`download_manifest.json` 本身随包提供,故 **`AUD-02` 的 SHA-256 对应关系可在包内完整重算**。 + +### 重算过程中发现的一处表述问题 + +审计原文写「HPA 有行但 `hpa_plasma_membrane = false` 的基因共 **11,334** 个」。 +重算脚本首版按「consensus 中 `hpa_plasma_membrane = false`」计数,得 **18,534**。 + +**11,334 这个数字是对的**:HPA 实际覆盖 **13,597** 个基因,其中 11,334 个 `plasma_membrane = false`。 +18,534 里多出的部分是**从未被 HPA 覆盖**的基因——该字段对它们同样是 `false`。 + +原文的「有行」二字承载了全部区分度,容易读漏。现已在机器可读记录与脚本中同时报出 +13,597/18,534/11,334 三个数。**两种口径下 `imaging` 被错误计入的基因都是 0 个**, +故 `AUD-05` 的结论不变。 + +### 另一处更正 + +`AUD-01` 原写「`AssetGenOS/scripts/build_t7_surfaceome_reference.py`」有歧义—— +该路径**在 StelligenOS 仓库之外**,是同级的 `AssetGenOS` 仓库 +(`/Volumes/Stelligen_SSD/Stelligen/AssetGenOS/scripts/`)。已更正,副本随包提供。 + +## 一、九项逐条结论 + +| ID | 审计项 | 结论 | +|---|---|---| +| `AUD-01` | builder 实现与版本 | **PASS** | +| `AUD-02` | raw manifest 与 `raw_manifest_sha256` 的对应 | **PASS**(实算复核) | +| `AUD-03` | 原始来源清单与 release 版本 | **PASS_WITH_FINDING** | +| `AUD-04` | license 与再分发条件 | **PASS_WITH_FINDING** | +| `AUD-05` | evidence family 独立性 | **PASS** | +| `AUD-06` | 来源去重逻辑 | **PASS_WITH_FINDING** | +| `AUD-07` | `discordance_flags` 生成规则 | **PASS** | +| `AUD-08` | 代表性靶点逐行回溯 | **PASS** | +| `AUD-09` | snapshot 可复现性 | **PASS_WITH_FINDING** | + +无一项 `FAIL`。四项带 finding,全部已界定影响范围。 + +## 二、三个最关键的结论 + +### `AUD-05` 独立性成立,而且恰好避开了审核方点名的失效模式 + +审核方要审的是「family independence 是否真正独立,而非同源数据的重复计数」。实测三个家族的来源映射: + +``` +curated_knowledge <- {goa_human, uniprot_reviewed_human} +imaging <- {hpa_subcellular_location} +cell_surface_capture_ms <- {cspa} +``` + +**`goa_human` 与 `uniprot_reviewed_human` 是同源的**——GOA human 由 UniProt 策展流程产出。builder 把二者收进**同一个** `curated_knowledge` 家族,因此不会被计成两个独立家族。**这正是那个失效模式,builder 避开了它。** 实例:`GUCY2C` 两个来源都 `supported`,`family_count` 仍为 **1**。 + +两条进一步的验证: + +1. **family 计数要求支持性证据,不是「有行即计」。** 判据在 builder 第 2112–2118 行,取自 consensus 的 support 布尔值。反例检验:HPA 有行但 `hpa_plasma_membrane = false` 的基因共 **11,334** 个,其中 `imaging` 被计入家族的为 **0 个**。 +2. **`family_count >= 2` 必然包含至少一个实验型家族。** 因为 `curated_knowledge` 是唯一可由两个来源喂养但只计一次的家族,要凑到 2 就必须再有 `imaging` 或 `cell_surface_capture_ms`。所以 `RQ-01` 的两家族门槛不是形式门槛。 + +### `AUD-04` license 歧义存在,但不触及 EVGAP-01 读取的任何字段 + +19 个来源全部声明 license,无缺失。但六个有歧义:`cellphonedb_gene`/`protein`/`complex`/`interaction`(「not declared in cellphonedb…」)、`cellchatdb_human`(GPL-3.0 repository license)、`omnipath_intercell_receptor`(per-resource licenses retained)。 + +**决定性检查:这六个来源没有任何一个出现在 `source_evidence.tsv` 中。** 它们只喂 `cci_receptor_*` 字段,而该字段已被 PR #59 的 `barred_fields` 禁用。 + +进入 `source_evidence.tsv` 的只有四个来源,其中 `goa_human`/`hpa_subcellular_location`/`uniprot_reviewed_human` 为 **CC BY 4.0**。 + +**这个结论是承重的**:它依赖 PR #59 的字段白名单。若白名单日后扩大到 `cci_receptor_*`,本准入必须重新审——已写入 `COND-02`。 + +顺带一处命名不一致:processed 表用 `source_id = cspa`,而 manifest 用 `cspa_validated_surfaceome` 与 `cspa_cell_type_matrix`,两者无法直接 join。 + +### `AUD-09` 完整性可验,但可复现性有边界 + +`shasum -a 256 -c checksums.sha256` 对 raw snapshot 的 **19 个文件全部 OK**。builder 中唯一的时间依赖是第 268 行 `datetime.now()` 用于 `processed_at_utc` 时间戳字段,不参与任何计算;无 `random`、无 `shuffle`。**给定同一 raw snapshot,构建结果确定。** + +**但 raw snapshot 本身对 `uniprot_reviewed_human` 与 `goa_human` 未钉版本**——它们的 release 是 `current_at_download`,只钉下载时刻不钉上游版本。因此从上游重新下载并重建**不能**保证逐字节复现该 snapshot。 + +可复现性成立的前提是**使用已归档的 raw snapshot**,而不是从来源重新获取——已写入 `COND-03`。 + +## 三、四项准入条件 + +任一条被破坏即须重新审。 + +| ID | 条件 | +|---|---| +| `COND-01` | **准入仅限这一个 snapshot**:`0.3.0` / `2026-07-29-quant-topology-mm`,由 19 个 raw 校验和与 4 个 processed 校验和共同钉住。不覆盖其他版本,不覆盖任何后续重建。 | +| `COND-02` | **准入仅限 PR #59 的字段白名单**。license 歧义来源只喂已禁用的 `cci_receptor_*`,这是 `AUD-04` 结论成立的前提。白名单一扩大,准入即失效。 | +| `COND-03` | **准入基于已归档 snapshot,不基于可从上游复现**。两个来源为 `current_at_download`,归档副本即事实来源。 | +| `COND-04` | **重复键不得进入 EVGAP-01 的判据**。已实测 11 个受影响基因全在 41 个靶点之外;若靶点轴日后扩大,须重新检查。 | + +## 四、其余带 finding 的两项 + +**`AUD-03`**:manifest 的 `files` 条目里 `release` 字段**全部为 null**;release 字符串由 builder 另行赋值,且在 processed 表中确有实义取值(`HPA 25.1; Ensembl 109`、`PLOS ONE 2015 supplementary file S2` 等)。但 EVGAP-01 实际使用的四个来源里,`uniprot_reviewed_human` 与 `goa_human` 的 release 是 `current_at_download`,**不是版本号**。后果见 `AUD-09`。 + +**`AUD-06`**:builder 中**检索不到显式去重例程**。实测 `source_evidence.tsv` 在 `(gene_symbol, source_id, evidence_kind)` 上有 **6** 个重复键,`membrane_topology_evidence.tsv` 在 `(gene_symbol, source_id)` 上有 **5** 个;`surfaceome_consensus.tsv` 在 `gene_symbol` 上无重复。 + +影响已界定:受影响基因为 `HERC3`、`MATR3`、`NPIPA9`、`PINX1`、`POLR2J3`、`PRODH`、`ERVK-7`、`NRXN1`、`NRXN2`、`NRXN3`、`SIRPB1`,**没有一个属于 EVGAP-01 的 41 个靶点**。且 family 计数取自 consensus 的 support 布尔值而非行数,故重复行在结构上不可能抬高 `RQ-01` 的家族数;41 个靶点的 `family_count` 与 `families` 列表实测 100% 一致。 + +## 五、`AUD-07` 与 `AUD-08` + +**`AUD-07`**:builder 第 2121–2129 行,四条确定性规则——`hpa_plasma_membrane_uncertain`、`cspa_support_without_uniprot_explicit_surface`、`hpa_only_vs_curated_knowledge`、`generic_membrane_without_surface_localization`。PR #59 的 `E1-04` 把 `discordance_flags` 非空一律判 DEFER,故这四条规则只把冲突显式化并触发 DEFER,**不会产生 RETAIN**。 + +**`AUD-08`**:三个代表性靶点逐行回溯成功,每条主张都落到 `source_id` + `surface_supported` + `source_release` + `source_url`: + +- `CDH17`:`confirmed_surface`,families `cell_surface_capture_ms` + `curated_knowledge`(n=2),ECD-a 路径 +- `CEACAM5`:`confirmed_surface`,families `curated_knowledge` + `imaging`(n=2),ECD-b GPI 路径 +- `GUCY2C`:`supported_surface`,families 仅 `curated_knowledge`(n=1),在 PR #59 规则下落 `E1-02` hold + +## 六、`AUD-01` 的一处残留说明 + +builder 存在、2,721 行、可读,family 计数与 discordance 生成均为显式条件分支,无隐藏启发式。 + +但 `builder_version` 由 config 传入而非脚本内常量,因此「0.3.0」这个版本号依赖 `build_manifest` 的自述,**不能由脚本自身独立确证**。这不构成阻断——snapshot 由 23 个校验和钉住,版本号只是标签——但如实记录。 + +## 七、本审计不做什么 + +不授予准入;不修改 `evgap_01_surface_localization_extraction.yaml` 的 `admission_record_ref`;不执行 `EVGAP-01` 抽取;不执行 Level 01;不解除 `EVGAP-01` 或 `EVGAP-02`;不纳入该数据集的其他版本或后续重建;不扩大 PR #59 的字段白名单;不纳入 `SRCADM-02`..`SRCADM-05` 的任何派生库。 + +## 八、后续顺序 + +1. 本审计连同审计包 `APPROVE`,在 `logs/` 留下审核记录。 +2. **另开 PR** 把 `EVGAP-01` 契约的 `admission_record_ref` 指向该记录,`authorises_extraction_run` 转为 `true`。 +3. 执行 `EVGAP-01` 抽取 → 结果 PR → binding,解除 `EVGAP-01`。 +4. `EVGAP-02` 的抽取已执行,结果审核在 PR #62;获批后另开 PR 解除 `EVGAP-02`。 +5. **两个缺口都解除后**,才能生成 `ADC_POOL_LEVEL_01_ACCEPTED`。 diff --git a/logs/worklog.md b/logs/worklog.md index 33a4147..8fa439a 100644 --- a/logs/worklog.md +++ b/logs/worklog.md @@ -2647,6 +2647,40 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Accepted by reviewer, unchanged: EVGAP-02 与 EVGAP-01/SRCADM-01 独立;覆盖全部 369 pairs;Tier 1/Tier 2 分层与 Tier 2 禁用;A/B/C/D 四类框架;RNA 可支持 linkage 但不满足 LOCK-01;C 类可作 linkage existence 但非 ADC efficacy;disease-level 不自动支持 subgroup;other-cancer 只作 metadata;`L3-05` 可逆非证伪非 killed;不执行 Gate/Level 01、不解除 `EVGAP-01`;不预写 discovery run 结果数量。 - Review write-back: 连接器 403,未写回 GitHub。裁决以人类负责人转述为准,已记录于本条与 handoff 第十四节。 - Next: 推送同一 PR 并同步 PR 描述请求复审。 +## 2026-08-05T20:30:00-04:00 — SRCADM-01 surfaceome 来源准入审计(审计记录,结论:有条件可纳入) + +- Instruction: 人类负责人指示「起 SRCADM-01」。授权依据是 PR #59 已冻结并获 `APPROVE` 的审计范围 `AUD-01`..`AUD-09`——该范围本身就是这次审计的授权。 +- Method: 实际读取 builder 源码(`AssetGenOS/scripts/build_t7_surfaceome_reference.py`,2,721 行)、raw `download_manifest.json`、`checksums.sha256`、license 声明与三个 processed 表,**而不是描述它们**。所有结论均附可复核依据。 +- Verdicts: `AUD-01` PASS|`AUD-02` PASS(实算)|`AUD-03` PASS_WITH_FINDING|`AUD-04` PASS_WITH_FINDING|`AUD-05` PASS|`AUD-06` PASS_WITH_FINDING|`AUD-07` PASS|`AUD-08` PASS|`AUD-09` PASS_WITH_FINDING。**无一项 FAIL。** 总结论 `admissible_with_conditions`。 +- `AUD-02` hard verification: 对 `raw/2026-07-29-quant-topology-mm/download_manifest.json` 实算 SHA-256 得 `884f419118302ae39c3e50292d03295ff676434868e1061b39ead50f9cc977bb`,与 `build_manifest.json` 声明的 `raw_manifest_sha256` 逐字符一致。 +- `AUD-05` (the item the reviewer singled out) PASS with two reinforcing checks: 三家族映射为 `curated_knowledge <- {goa_human, uniprot_reviewed_human}`/`imaging <- {hpa_subcellular_location}`/`cell_surface_capture_ms <- {cspa}`。**`goa_human` 与 `uniprot_reviewed_human` 同源**(GOA human 由 UniProt 策展流程产出),builder 把二者收进**同一个**家族故不重复计数——`GUCY2C` 两来源皆 supported 而 `family_count` 仍为 1,**这正是审核方点名的失效模式而 builder 避开了它**。加强验证一:family 计数要求**支持性**证据(builder 2112–2118),反例检验 HPA 有行但 `hpa_plasma_membrane=false` 的 **11,334** 个基因中 `imaging` 被计入的为 **0**。加强验证二:`curated_knowledge` 是唯一可由两来源喂养但只计一次的家族,故 `family_count >= 2` 必然含至少一个实验型家族,`RQ-01` 的门槛不是形式门槛。 +- `AUD-04` finding with decisive bound: 19 个来源全部声明 license,六个有歧义(`cellphonedb_gene`/`protein`/`complex`/`interaction`、`cellchatdb_human` GPL-3.0、`omnipath_intercell_receptor` per-resource)。**实测:这六个没有任何一个出现在 `source_evidence.tsv` 中**,只喂已被 #59 `barred_fields` 禁用的 `cci_receptor_*`。进入的四个来源中三个为 CC BY 4.0。该结论**承重**——依赖 #59 字段白名单,白名单扩大即须重审(`COND-02`)。另记一处命名不一致:processed 用 `source_id = cspa`,manifest 用 `cspa_validated_surfaceome`/`cspa_cell_type_matrix`,无法直接 join。 +- `AUD-09` finding: `shasum -a 256 -c checksums.sha256` 对 19 个 raw 文件全部 OK;builder 唯一时间依赖是第 268 行 `datetime.now()` 用于 `processed_at_utc`、不参与计算,无 `random`/`shuffle`,给定同一 snapshot 构建确定。**但 `uniprot_reviewed_human` 与 `goa_human` 的 release 是 `current_at_download`、不是版本号**,从上游重新下载不保证逐字节复现。可复现性成立的前提是使用已归档 snapshot(`COND-03`)。 +- `AUD-03` finding: manifest 的 `files` 条目 `release` 字段**全部为 null**,release 字符串由 builder 另行赋值且在 processed 表中确有实义取值(`HPA 25.1; Ensembl 109`、`PLOS ONE 2015 supplementary file S2` 等);但四个在用来源中两个为 `current_at_download`。 +- `AUD-06` finding with bounded impact: builder 中**检索不到显式去重例程**;实测 `source_evidence.tsv` 在 `(gene_symbol, source_id, evidence_kind)` 上 6 个重复键、`membrane_topology_evidence.tsv` 在 `(gene_symbol, source_id)` 上 5 个,`surfaceome_consensus.tsv` 无重复。受影响基因 `HERC3`/`MATR3`/`NPIPA9`/`PINX1`/`POLR2J3`/`PRODH`/`ERVK-7`/`NRXN1`/`NRXN2`/`NRXN3`/`SIRPB1`,**没有一个属于 41 个靶点**;且 family 计数取自 support 布尔值而非行数,故重复行结构上不可能抬高 `RQ-01`;41 靶点的 `family_count` 与 `families` 列表实测 100% 一致。 +- `AUD-07` PASS: builder 2121–2129 四条确定性规则。#59 的 `E1-04` 把 `discordance_flags` 非空一律判 DEFER,故这些规则只把冲突显式化、**不会产生 RETAIN**。 +- `AUD-08` PASS: 三个代表性靶点逐行回溯成功,每条主张落到 `source_id` + `surface_supported` + `source_release` + `source_url`——`CDH17`(ECD-a,n=2)、`CEACAM5`(ECD-b GPI,n=2)、`GUCY2C`(仅 curated_knowledge,n=1,故落 `E1-02` hold)。 +- `AUD-01` residual recorded honestly: `builder_version` 由 config 传入而非脚本内常量,故「0.3.0」依赖 `build_manifest` 自述、**不能由脚本自身独立确证**。不构成阻断(snapshot 由 23 个校验和钉住),但如实记录。 +- Admission conditions: `COND-01` 仅限该 snapshot|`COND-02` 仅限 #59 字段白名单|`COND-03` 基于已归档 snapshot 而非可从上游复现|`COND-04` 重复键不得进入 EVGAP-01 判据、靶点轴扩大须重查。任一条被破坏即须重审。 +- Validation: `Ran 351 tests` 全部通过(`main` 基线 338 + 新增 13);`scripts/verify_repository_boundary.sh` 通过;`git diff --check` 通过;零 `__pycache__`。测试把结论钉在 #59 冻结范围上——九项 ID 必须与 `required_audit_items` 完全相等、无 FAIL、每项必须有可复核依据、`AUD-02` 摘要必须是 64 位十六进制且与 #59 前缀一致、`AUD-05` 必须点名同源来源对并引用实测反例数、`AUD-04` 依赖的 `cci_receptor_role` 必须确实在 #59 `barred_fields` 中、`AUD-06` 影响界定必须针对 41 靶点轴、本审计不得授予准入。 +- Own test error, self-caught: 初稿从 `source_admission_dependency` 读 `raw_manifest_sha256` 键,而该键在 #59 修订后并不存在(摘要写在 `AUD-02` 条目文本里)。改为跨文件比对前缀并校验 64 位十六进制。 +- Deliberately not done: **未授予准入**;未修改 `evgap_01_surface_localization_extraction.yaml` 的 `admission_record_ref`(仍 `null`,`authorises_extraction_run` 仍 `false`);未执行 `EVGAP-01` 抽取;未执行 Level 01;未解除任何 EVGAP;未纳入该数据集其他版本或后续重建;未扩大 #59 字段白名单;未纳入 `SRCADM-02`..`05`;**未产生任何外部运行产物**(本次是对既有文件的审计);未补九份批准记录。 +- Governance note: **本 PR 不适用 `AGENTS.md`「审核豁免」**,须经 ChatGPT `APPROVE`。 +- Next: 送审;获批后另开 PR 把 `admission_record_ref` 指向审核记录并放行 `EVGAP-01` 抽取。 + +### 2026-08-05 20:20 EDT — SRCADM-01:补可独立复核的审计包(PR #63 第二轮) + +- Trigger: ChatGPT 对 PR #63 `REQUEST_CHANGES`——审计结论的核心事实全部来自仓库外文件,仓库内测试只验证文档自洽,**不能证明外部事实为真**。意见成立,且与我在 PR #62 对自己提的标准是同一条。 +- Bundle: `external:result/gen_iet_srcadm_01_audit_bundle_20260806T000000Z`,ZIP SHA-256 `2dbe88af1a2e9aee8004b9cbdd894c48f2f91197726678898aadf5da3f75e931`,2,663,987 bytes,13 个条目。含 builder 源码、`build_manifest.json`、`download_manifest.json`、`checksums.sha256`、raw 校验实算结果、三张完整 processed 表、41 靶点轴、`verify_audit.py`、`audit_report.json`。 +- Not subset: 三张 processed 表**未做子集裁剪**(11.8/8.9/3.5 MB)。子集会使 11,334、6/5 重复键等计数无法重算,审核方只能重新相信叙述。压缩后 2.6 MB,代价可接受。 +- Re-verification: `python3 verify_audit.py .` 重算 **48 项**审计事实,**48/48 `MATCH`**。脚本只读包内文件。 +- Stated limit: 19 个 raw 文件未随包提供(数 GB),其校验和审计时已实算(19/19 `OK`)记于 `raw_checksum_verification.json`。独立重算需归档 snapshot 本身——**这正是 `COND-03` 已声明的边界**,不是新增限制。`download_manifest.json` 随包提供,故 `AUD-02` 可在包内完整重算。 +- Found while recomputing: 原文「HPA 有行但 `hpa_plasma_membrane = false` 共 11,334 个」——脚本首版按「consensus 中该字段为 false」计数得 **18,534**。**11,334 是对的**:HPA 实际覆盖 **13,597** 个基因,其中 11,334 个为 false;多出部分是**从未被 HPA 覆盖**的基因,该字段对它们同样是 false。「有行」二字承载全部区分度,容易读漏。现三个数一并报出,测试强制三者同时出现。**两种口径下 `imaging` 被错误计入的都是 0 个**,`AUD-05` 结论不变。 +- Also corrected: `AUD-01` 原写 `AssetGenOS/scripts/...` 有歧义——该路径**在 StelligenOS 仓库之外**,是同级 `AssetGenOS` 仓库。已更正,副本随包提供。 +- Tests: `tests/test_srcadm_01_surfaceome_admission.py` 由 13 增至 **15**(新增:包可重算且不自授准入、包如实声明其不能证明什么);全库 `Ran 353 tests` OK;boundary check 通过。 +- Mutation testing: 12 个变异,9 个被捕获。**3 个「逃逸」经查是变异本身不破坏被测不变式**——单独从某个条目删除 `AUD-02` 时另一条目仍服务该项,而测试断言的是「至少一个文件服务该项」;从**所有**条目删除即被捕获。同时把该断言的三元表达式改写清楚,原写法可读性差、`{"all"}` 并集无实际作用。 +- Deliberately not done: 未授予准入(`status: pending_review`、`admission_record_ref: null` 未动);未修改 `EVGAP-01` 契约;未执行任何抽取。 +- Next: 送审审计包;获批后另开 PR 填 `admission_record_ref` 并放行 `EVGAP-01` 抽取。 ### 2026-08-06 15:04 EDT - Instruction: Re-read the current StelligenOS project and produce a versioned, review-ready description of the current design architecture, module logic, system flow, implementation status, and open gaps. @@ -2656,3 +2690,16 @@ Purpose: append a detailed timestamped record of what was done, how it was done, - Versioning: Updated the version index to state that unapproved `v2-draft` was superseded without a snapshot; no `v3` snapshot is created before approval. Updated architecture/README navigation, added this repository handoff, and refreshed workspace `HANDOFF.md` with the active v3 review task. - Boundary: Documentation-only. No data, cache, result, model weight, provider execution, Gate execution, contract change, lifecycle change, or module implementation was added. - Validation: Final branch validation ran 338 tests successfully; `scripts/verify_repository_boundary.sh` and `git diff --check` passed; no `__pycache__` directory was generated. + +### 2026-08-06 16:35 EDT — SRCADM-01:冻结语义,只补正式审计包(PR #63 第三轮) + +- Instruction: 冻结当前 HEAD,不改九项审计与四项条件,只上传正式审计包并使三处声明一致。 +- **本轮未改任何语义**:九项 `audit_findings` 的 verdict 与依据未动、四项 `admission_conditions` 未动、`status: pending_review` 未动、`admission_record_ref: null` 未动、`EVGAP-01` 的 `authorises_extraction_run` 仍为 `false`、未顺带授予 admission。 +- Added to the bundle only: `audit_expected.json`、`license_manifest.json`,并重写 `verify_audit.py`;移除 `audit_report.json`(预写结论不应与证据混放)。因包内容变化,ZIP 哈希必然变化。 +- Package: `gen_iet_srcadm_01_audit_bundle_20260806T000000Z.zip`,SHA-256 `49d56c395661e7c71ba4caa60657126596cf4f784430ff462ab4512fdb0237b4`,2,666,041 bytes,**13 个文件**(ZIP 内 14 个条目,多出的一条是目录条目)。**上一版声明的 `2dbe88af…` 作废。** +- Verifier properties: 无网络、无写入、不依赖包外路径、解压即可运行、退出码 0 表示全通过、逐项输出 `MATCH`/`MISMATCH`、末行 `72/72 MATCH`。 +- **判据不来自预写结论**:主张放在 `audit_expected.json`,脚本从 builder 源码与三张表**重新算出**每个数字再比对;那份文件写错一个数就会 `MISMATCH`。已在记录中以 `verdicts_read_from_file: false` 固定该性质。 +- **72 而不是 48,如实报告**:按复核要求补入 processed 表逐文件 SHA-256/字节数/行数重算(9 项)、六个 license 歧义来源逐一验证其文本确实歧义(6 项)、三个 CC BY 4.0 来源逐一验证(3 项)、license 清单覆盖 19 个来源、已记录 raw 摘要与 `checksums.sha256` 逐条一致、snapshot 与 dataset 版本一致性、target 轴规模等。**未删除任何原有检查**,48 项全部仍在。上一版的 48 记为 `previous_recomputed_checks`。 +- Covered by recomputation, as requested: raw manifest SHA-256;processed 三表摘要;family source mapping;GOA 与 UniProt 同属一个 family;HPA 反例 11,334(并同时报出 13,597 与 18,534 两个口径);imaging 误计为 0;source_evidence 重复键 6;topology 重复键 5;11 个受影响基因;与 41-target 轴交集为空;CDH17/CEACAM5/GUCY2C provenance;license 歧义来源未进入允许字段;consensus `gene_symbol` 唯一;41 个 target 的 `family_count` 与 family list 一致。 +- Verified from a clean extract in a temp directory, not the working copy: **`72/72 MATCH`,退出码 0**。 +- Repo-side changes this round: 仅本条 worklog、handoff 的包信息段、YAML 的 `audit_bundle` 数据字段(哈希/字节/文件数/检查数/内容清单)。九项审计结论、四项条件、测试逻辑零改动。 diff --git a/tests/test_srcadm_01_surfaceome_admission.py b/tests/test_srcadm_01_surfaceome_admission.py new file mode 100644 index 0000000..80aa2cb --- /dev/null +++ b/tests/test_srcadm_01_surfaceome_admission.py @@ -0,0 +1,252 @@ +"""Verify docs/pools/srcadm_01_surfaceome_admission.yaml. + +The audit record must cover every scope item PR #59 froze, must not grant +admission by itself, and must keep every conclusion tied to a stated, +recheckable basis. These tests read no external database: the audit already ran +and its evidence is recorded in the file under test. +""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +POOLS = REPO_ROOT / "docs" / "pools" +AUDIT_PATH = POOLS / "srcadm_01_surfaceome_admission.yaml" +EVGAP_01_PATH = POOLS / "evgap_01_surface_localization_extraction.yaml" + +VERDICTS = frozenset({"PASS", "PASS_WITH_FINDING", "FAIL"}) + + +def _load(path: Path) -> dict: + with path.open(encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +class Srcadm01AuditTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.doc = _load(AUDIT_PATH) + cls.evgap = _load(EVGAP_01_PATH) + cls.findings = {f["id"]: f for f in cls.doc["audit_findings"]} + + def test_audit_targets_the_dependency_evgap_01_registered(self) -> None: + head = self.doc["admission"] + dep = self.evgap["source_admission_dependency"] + self.assertEqual(head["admission_id"], dep["id"]) + self.assertEqual(head["dataset_id"], dep["dataset_id"]) + self.assertEqual(str(head["dataset_version"]), str(dep["dataset_version"])) + self.assertEqual(head["snapshot_id"], dep["snapshot_id"]) + self.assertEqual(head["audit_scope_frozen_by_pr"], 59) + + def test_every_frozen_scope_item_is_answered(self) -> None: + required = {item["id"] for item in + self.evgap["source_admission_dependency"]["required_audit_items"]} + self.assertEqual(set(self.findings), required) + self.assertEqual(self.doc["admission"]["audit_scope_items"], len(required)) + for fid, finding in sorted(self.findings.items()): + with self.subTest(item=fid): + self.assertIn(finding["verdict"], VERDICTS) + self.assertTrue(finding["item"].strip()) + + def test_no_scope_item_failed_outright(self) -> None: + failed = [f for f in self.findings.values() if f["verdict"] == "FAIL"] + self.assertEqual(failed, [], f"failed items: {[f['id'] for f in failed]}") + + def test_every_finding_states_a_recheckable_basis(self) -> None: + """A verdict with no evidence and no finding is an assertion, not an audit.""" + + for fid, finding in sorted(self.findings.items()): + with self.subTest(item=fid): + basis = [k for k in ("evidence", "finding", "integrity_verified") + if str(finding.get(k, "")).strip()] + self.assertTrue(basis, f"{fid} records no basis") + if finding["verdict"] == "PASS_WITH_FINDING": + self.assertTrue(str(finding.get("finding", "")).strip(), + f"{fid} is PASS_WITH_FINDING but names no finding") + + def test_the_audit_does_not_grant_admission(self) -> None: + head = self.doc["admission"] + self.assertEqual(head["status"], "pending_review") + self.assertIs(head["grants_admission_by_itself"], False) + self.assertIsNone(head["admission_record_ref"]) + # EVGAP-01 must still be unfilled and still block its extraction. + dep = self.evgap["source_admission_dependency"] + self.assertIsNone(dep["admission_record_ref"]) + self.assertIs(self.evgap["extraction"]["authorises_extraction_run"], False) + not_authorised = " ".join(self.doc["not_authorised"]) + for phrase in ("授予准入", "admission_record_ref", "EVGAP-01", "Level 01"): + with self.subTest(phrase=phrase): + self.assertIn(phrase, not_authorised) + + def test_recommendation_is_conditional_and_the_conditions_exist(self) -> None: + self.assertEqual(self.doc["admission"]["recommendation"], + "admissible_with_conditions") + conditions = {c["id"]: c for c in self.doc["admission_conditions"]} + self.assertGreaterEqual(len(conditions), 4) + for cid, cond in sorted(conditions.items()): + with self.subTest(condition=cid): + self.assertTrue(cond["condition"].strip()) + self.assertTrue(cond["detail"].strip()) + + def test_family_independence_is_the_load_bearing_pass(self) -> None: + """AUD-05 is the item the reviewer singled out; it must not be hand-waved.""" + + aud = self.findings["AUD-05"] + self.assertEqual(aud["verdict"], "PASS") + self.assertIs(aud["counting_requires_support"], True) + for key in ("same_origin_pair_handled", "counting_requires_support_evidence", + "structural_guarantee"): + with self.subTest(key=key): + self.assertTrue(str(aud[key]).strip()) + # The same-origin pair must be named, not merely alluded to. + text = aud["same_origin_pair_handled"] + self.assertIn("goa_human", text) + self.assertIn("uniprot_reviewed_human", text) + # The counterexample check must cite a measured count, and it must state + # the denominator. "11,334 genes with plasma_membrane=false" is ambiguous + # on its own: 18,534 consensus rows carry that value, most because HPA + # never covered the gene at all. Both figures must appear so the reader + # cannot conflate them. + counterexample = aud["counting_requires_support_evidence"] + for figure in ("13,597", "11,334", "18,534"): + with self.subTest(figure=figure): + self.assertIn(figure, counterexample) + self.assertIn("从未被 HPA 覆盖", counterexample) + + def test_the_audit_ships_a_bundle_that_can_be_recomputed(self) -> None: + """Tests on this file prove only internal consistency. The external facts + need an external, re-runnable bundle.""" + + bundle = self.doc["audit_bundle"] + self.assertIs(bundle["read_only"], True) + self.assertIs(bundle["grants_admission"], False) + self.assertTrue(bundle["reverification_command"].strip()) + self.assertEqual(bundle["recomputed_result"], "all_match") + self.assertGreaterEqual(bundle["recomputed_checks"], 40) + # A 64-hex package digest, so a swapped package is detectable. + digest = bundle["package_sha256"] + self.assertEqual(len(digest), 64, digest) + self.assertTrue(all(c in "0123456789abcdef" for c in digest)) + # Every scope item must be served by at least one shipped file. The + # literal "all" marks the verifier and its report, which serve every + # item; it is not itself a scope item, so drop it before comparing. + served = {item for entry in bundle["contents"] for item in entry["serves"]} + self.assertIn("all", served, "no file is declared as serving every item") + served.discard("all") + required = {item["id"] for item in + self.evgap["source_admission_dependency"]["required_audit_items"]} + self.assertEqual(required - served, set(), + f"scope items with no bundle file: {sorted(required - served)}") + + def test_the_bundle_states_what_it_cannot_prove(self) -> None: + """A bundle that implies everything is recomputable would be worse than + no bundle.""" + + bundle = self.doc["audit_bundle"] + # Full tables, not representative rows — otherwise the counts are + # unverifiable and the reviewer is back to trusting the narrative. + self.assertIs(bundle["processed_tables_subset"], False) + self.assertTrue(bundle["processed_tables_subset_reason"].strip()) + limits = bundle["not_independently_recomputable_in_bundle"] + self.assertTrue(limits) + raw_limit = next(item for item in limits if "AUD-09" in item["item"]) + self.assertTrue(raw_limit["reason"].strip()) + self.assertTrue(raw_limit["what_is_provided"].strip()) + # The limitation must be the one the conditions already declare, not a + # new and unrecorded one. + self.assertEqual(raw_limit["already_stated_as"], "COND-03") + conditions = {c["id"] for c in self.doc["admission_conditions"]} + self.assertIn("COND-03", conditions) + + def test_license_ambiguity_is_bounded_by_the_field_whitelist(self) -> None: + aud = self.findings["AUD-04"] + self.assertEqual(aud["verdict"], "PASS_WITH_FINDING") + self.assertTrue(aud["decisive_check"].strip()) + self.assertTrue(aud["load_bearing_boundary"].strip()) + # The barred field the conclusion depends on must actually be barred there. + barred = {item["field"] for item in self.evgap["barred_fields"]} + self.assertIn("cci_receptor_role", barred) + # And the dependency must be recorded as a condition, not just prose. + conditions = {c["id"]: c for c in self.doc["admission_conditions"]} + whitelist_conditions = [c for c in conditions.values() + if "白名单" in c["condition"] + c["detail"]] + self.assertTrue(whitelist_conditions) + + def test_reproducibility_limit_is_stated_not_glossed(self) -> None: + aud = self.findings["AUD-09"] + self.assertEqual(aud["verdict"], "PASS_WITH_FINDING") + self.assertTrue(aud["integrity_verified"].strip()) + self.assertTrue(aud["determinism"].strip()) + # The finding must name both un-pinned sources and say what that costs, + # not merely mention the phrase somewhere in the paragraph. + finding = aud["finding"] + for token in ("current_at_download", "uniprot_reviewed_human", "goa_human", + "逐字节"): + with self.subTest(token=token): + self.assertIn(token, finding) + # AUD-03 must carry the same limitation rather than contradicting it. + aud_03 = self.findings["AUD-03"]["finding"] + for token in ("current_at_download", "uniprot_reviewed_human", "goa_human"): + with self.subTest(item="AUD-03", token=token): + self.assertIn(token, aud_03) + conditions = " ".join(c["detail"] for c in self.doc["admission_conditions"]) + self.assertIn("current_at_download", conditions) + + def test_dedup_finding_bounds_its_own_impact(self) -> None: + aud = self.findings["AUD-06"] + self.assertEqual(aud["verdict"], "PASS_WITH_FINDING") + bound = aud["bounded_impact"] + self.assertTrue(bound.strip()) + # The bound must exclude the target axis explicitly and name the genes it + # affects, so weakening it to a vague phrase fails. + self.assertIn("没有一个属于", bound) + self.assertIn("41 个靶点", bound) + for gene in ("HERC3", "NRXN1", "SIRPB1"): + with self.subTest(gene=gene): + self.assertIn(gene, bound) + + def test_raw_manifest_digest_was_recomputed_not_copied(self) -> None: + aud = self.findings["AUD-02"] + self.assertEqual(aud["verdict"], "PASS") + self.assertEqual(aud["verification_method"], "recomputed") + # The digest must be the same one EVGAP-01's AUD-02 item points at, and it + # must appear in the evidence as a recomputed value rather than a citation. + item = next(a["item"] for a in + self.evgap["source_admission_dependency"]["required_audit_items"] + if a["id"] == "AUD-02") + prefix = "884f4191" + self.assertIn(prefix, item) + self.assertIn(prefix, aud["evidence"]) + digest = next(w.strip(" ,.;,。") for w in aud["evidence"].split() + if w.startswith(prefix)) + self.assertEqual(len(digest), 64, digest) + self.assertTrue(all(c in "0123456789abcdef" for c in digest)) + + def test_traceback_names_the_targets_it_traced(self) -> None: + aud = self.findings["AUD-08"] + self.assertEqual(aud["verdict"], "PASS") + traced = aud["targets_traced"] + self.assertGreaterEqual(len(traced), 3) + for gene in traced: + with self.subTest(gene=gene): + self.assertIn(gene, aud["evidence"]) + # GUCY2C is the case that contradicts the earlier consensus; keep it visible. + self.assertIn("GUCY2C", traced) + + def test_discordance_rules_only_defer_and_never_retain(self) -> None: + aud = self.findings["AUD-07"] + self.assertEqual(aud["verdict"], "PASS") + self.assertEqual(len(aud["rules"]), 4) + self.assertIn("DEFER", aud["relevance_to_evgap_01"]) + # The EVGAP-01 rule it relies on must in fact defer on discordance. + rule = next(r for r in self.evgap["derivation_rules"] if r["id"] == "E1-04") + self.assertEqual(rule["disposition"], "DEFER") + + +if __name__ == "__main__": + unittest.main()