deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-22 20:56:31 +08:00
parent b3ad3f40cf
commit 28eb4ebc59
2 changed files with 227 additions and 30 deletions

View File

@ -185,6 +185,34 @@ python3 scripts/load_path.py --check
python3 scripts/selfcheck_m5a.py
```
### 收口自证(git 收口前必跑,`scripts/verify_m5a_closure.py`)
`selfcheck_m5a.py` 验的是「模块该长什么样」;`verify_m5a_closure.py` 验的是
「**磁盘上的代码 == 门禁所声称的那份代码**」+ 工作空间卫生 + 文档/代码条数一致,
专防 QC 退回过的三类问题:① 交付件声称的新增门禁函数其实没落盘;② 以工作空间根的
一次性补丁脚本(`tmp_patch_*.py`)充当交付件本体;③ README 条数与代码 CHECKS 漂移。
> 一条命令,7 项全通过输出 `CLOSURE VERIFY ALL PASS (7/7)` 且 exit 0:
>
> ```bash
> cd modules/pbl_evidence && python3 scripts/verify_m5a_closure.py
> ```
>
> | 项 | 验什么 |
> |---|---|
> | V1 | `selfcheck_m5a.py` 的 4 个新增门禁函数(`check_harness_static` / `check_no_hardcoded_credentials` / `check_evidence_logs` / `check_harness_matches_model`)已落盘**且装配进 `CHECKS`**(防「定义了但不可达」死代码) |
> | V2 | `tests/m5a_live_db_harness.py` 崩溃版特征(`test123` / `args or {}` / `params or {}` / `dict(params or {})`)0 命中 + `ast.parse` 通过 |
> | V3 | `projects/{项目}/env/test.json` 的 `db.sandbox` 段齐备(`scope=sandbox_only` + `sandbox_schema` + host/port/user/password)——凭据唯一事实源在 env,代码内零明文 |
> | V4 | `docs/02-develop` 三份运行证据日志齐备,harness 日志含 C-3.1~C-3.4 全 `[PASS]` 与 IDM.6 的 `1062 Duplicate entry ... uk_ev_dedup` 真防重原文,且无裸 `Traceback` |
> | V5 | 工作空间根一次性产物卫生:`tmp_*.py` / `_scratch_*.py` / `*_patch.py` / `*_out.txt` / `*_ev<N>.txt` / `r<N>.txt` / `*.bak` / 根级临时目录全类扫描;M5a 自身转储必须已删,他链产物必须在 `WS_KEPT` 白名单里**逐项写明引用它的文档路径**,否则 FAIL |
> | V6 | README 声称的 selfcheck 门禁条数 == 代码 `CHECKS` 条数(12) |
> | V7 | README 本小节声称的收口条数 == 本脚本 `CHECKS` 条数(7),且脚本路径磁盘命中 |
>
> **复跑范围**:V1/V2/V6/V7 是模块内自证,任何宿主可复跑;V3/V4/V5 读工作空间级兄弟产物
> (`projects/{项目}/...` 与工作空间根),仅在 pbls 项目工作空间内、或显式设置
> `SDLC_WS_ROOT`(配合可选 `SDLC_PROJECT` 覆盖项目名,默认 `pbls`)时生效;定位不到工作空间
> 根时这三项返回 `SKIPPED`(不计 FAIL),避免别的宿主挂载本模块时把「路径不存在」误判成质量回归。
## Integration(宿主挂载方式)
模块只依赖:基础包(sqlor、ahserver ServerEnv、appPublic 工具)、`pbl_common`(租户上下文/

View File

@ -19,8 +19,25 @@
# 全通过 → 逐条 [PASS] + `CLOSURE VERIFY ALL PASS (N/N)`,exit 0
# 任一失败 → [FAIL] + 原因,exit 1
幂等契约:只读(不连库、不写文件、不取时间戳/随机数/绝对路径),所有明细 sorted()
后输出,重复执行逐字节一致。
复跑范围与路径解析(QC #3)
--------------------------
本脚本的 V3/V4/V5 三项要读**工作空间级**的兄弟产物(`projects/{项目}/env/test.json`、
`projects/{项目}/docs/02-develop/*.log`、工作空间根的一次性产物),这些不在模块仓库
内。解析优先级(**不再无条件硬推层级**):
1. 环境变量 `SDLC_WS_ROOT` —— 显式指定机构工作空间根(跨宿主复跑用这个)
2. 环境变量 `SDLC_PROJECT` —— 指定项目名(默认 `pbls`),只影响 `projects/{p}/` 段
3. 缺省:按 `modules/{模块}/scripts/ → up 3 级` 推导,并**校验推导结果确实是工作空间
根**(同时存在 `projects/` 与 `modules/` 两个子目录)。推不出来时 V3/V4/V5 返回
明确的 SKIP 语义(`SKIPPED: 非 pbls 工作空间/未设 SDLC_WS_ROOT`)而不是伪报 FAIL,
避免别的宿主挂载本模块时把「路径不存在」误判成质量回归。
结论:**本脚本的 V1/V2/V6/V7 是模块内自证(任何宿主可复跑);V3/V4/V5 是工作空间级
取证,仅在 pbls 项目工作空间内或显式设置 `SDLC_WS_ROOT` 时生效**。
幂等契约:只读(不连库、不写文件、不取时间戳/随机数),所有明细 sorted() 后输出,
同一工作状态下重复执行逐字节一致(`os.path.abspath` 仅用于定位,不进入输出文本;
输出里一律用相对工作空间根的 posix 路径)。
"""
import ast
@ -34,10 +51,35 @@ SELFCHK = os.path.join(ROOT, 'scripts', 'selfcheck_m5a.py')
HARNESS = os.path.join(ROOT, 'tests', 'm5a_live_db_harness.py')
README = os.path.join(ROOT, 'README.md')
# 机构工作空间根:modules/{模块}/scripts/ → up 3 级
WS_ROOT = os.path.abspath(os.path.join(ROOT, os.pardir, os.pardir))
DOCS = os.path.join(WS_ROOT, 'projects', 'pbls', 'docs', '02-develop')
ENV_TEST = os.path.join(WS_ROOT, 'projects', 'pbls', 'env', 'test.json')
# 项目名可由环境变量覆盖(QC #3:不把 pbls 写死进常量)
PROJECT = os.environ.get('SDLC_PROJECT', 'pbls')
def _looks_like_ws_root(path):
"""工作空间根的机械判据:同时含 projects/ 与 modules/ 两个子目录。"""
return (os.path.isdir(os.path.join(path, 'projects'))
and os.path.isdir(os.path.join(path, 'modules')))
def resolve_ws_root():
"""返回 (ws_root 或 None, 解析来源说明)。环境变量优先,其次层级推导+校验。"""
env_root = os.environ.get('SDLC_WS_ROOT')
if env_root:
cand = os.path.abspath(env_root)
if _looks_like_ws_root(cand):
return cand, 'SDLC_WS_ROOT 显式指定'
return None, ('SDLC_WS_ROOT=%s 不像机构工作空间根(缺 projects/ 或 modules/)'
% env_root)
cand = os.path.abspath(os.path.join(ROOT, os.pardir, os.pardir))
if _looks_like_ws_root(cand):
return cand, '按 modules/{模块}/scripts → up 3 级推导并校验 projects/+modules/ 命中'
return None, ('无法定位机构工作空间根(推导结果 %s 缺 projects/ 或 modules/);'
'跨宿主复跑请设 SDLC_WS_ROOT' % cand)
WS_ROOT, WS_ROOT_SRC = resolve_ws_root()
DOCS = os.path.join(WS_ROOT, 'projects', PROJECT, 'docs', '02-develop') if WS_ROOT else None
ENV_TEST = os.path.join(WS_ROOT, 'projects', PROJECT, 'env', 'test.json') if WS_ROOT else None
# ① selfcheck 必须定义的 4 个新增门禁函数(QC #1 点名的改法)
GATE_FUNCS = (
@ -51,11 +93,62 @@ HARNESS_FORBIDDEN = ('test123', 'args or {}', 'params or {}', 'dict(params or {}
# ③ 三份运行证据日志(与 selfcheck EVIDENCE_LOGS 同名单)
EVIDENCE_LOGS = ('m5a-live-db-harness.log', 'm5a-selfcheck.log', 'm5a-pytest-offline.log')
RC_MARKER = '# RC=0'
# ④ 一次性补丁脚本清理名单(QC #8 点名 + 本批 s1/s2 期间新产生的)
TMP_PATCHES = ('tmp_patch_u7rb.py',)
# 工作空间根允许保留的历史取证脚本(M11b-1c-B 文档明确「正式唯一取证脚本」,
# 且被 projects/pbls/docs/M11b-1c-*.md 逐条引用作复跑入口,删除会破坏历史验收可复现性)
TMP_KEPT = ('tmp_m11b1c_diff_check.py',)
# ④ 工作空间根卫生门禁(QC #2 退回项:口径从「只匹配 tmp_*.py」扩到全类一次性产物)
#
# 一次性产物的**机械判据**(命中任一模式即视为需处置,不再只看 tmp_ 前缀):
TMP_PATTERNS = (
r'^tmp_.*\.py$', # 一次性补丁/取证脚本
r'^_scratch_.*\.py$', # 一次性补丁脚本(下划线前缀变体)
r'^.*_patch\.py$', # 名字里带 patch 的脚本
r'^.*_out\.txt$', # 运行转储
r'^.*_after\.txt$', # 改造前后对照转储
r'^.*_ev\d+\.txt$', # 取证转储分片
r'^.*evidence.*\.txt$', # 取证转储
r'^r\d+\.txt$', # run #N 复跑转储
r'^closure_out\.txt$', # 收口转储
r'^.*\.bak$', # 编辑备份件
)
# 根级临时**目录**判据:以 `.` 开头且不是 VCS/工具配置白名单,或名字含 scratch/tmp/work
WS_DIR_ALLOW = ('.git', '.pylibs', '.pytest_cache', '.cache', '.ssh', '.config',
'.venv', '.local', '.viminfo')
TMP_DIR_PATTERNS = (r'^\.[dqtms_]', r'^_scratch$', r'^tmp_tools$', r'.*scratch.*',
r'.*tmp.*', r'.*work.*')
# **白名单**:确需保留的历史取证产物,逐项写明「谁引用它」(文档路径),
# 未列入本表(且不属于他链保留表)的根级一次性产物一律判 FAIL。
# 保留理由:这些文件被 projects/pbls 仓内的验收文档逐条引用作「复跑入口」或「留证原件」,
# 删除会破坏**其他任务链**(M11b-1c / M11b-2d / M11b-4 / M9 / M1b)的历史验收可复现性;
# 本任务(M5a 收口)只删 M5a 链路自身产生的转储,不代处置他链产物。
WS_KEPT = {
'tmp_m11b1c_diff_check.py': 'M11b-1c 正式取证脚本,被 projects/pbls/docs/M11b-1c-A_revert_generator_overreach.md、projects/pbls/docs/02-develop/dev-notes-m11b1c-b.md、dev-notes-m11b1-pbl_runtime_event.md 引用为复跑入口',
'_scratch_m11b2d_a2_patch.py': 'M11b-2d-A2 补丁原件,被 projects/pbls/deliverables/agent.develop/OLdcikgd7ELN0ked7f33d_agent.develop_fallback.md 引用留证',
'_scratch/': 'M11b-2d 补丁脚本目录,同上(_scratch_m11b2d_a2_patch.py 的目录形态副本)',
'_m11b2d_a2_after.txt': 'M11b-2d-A2 改造后对照转储,被 projects/pbls/deliverables/agent.develop/OLdcikgd7ELN0ked7f33d_agent.develop_fallback.md 引用',
'm11b4_out.txt': 'M11b-4 自测转储,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 登记为「他链产物·保留原样」',
'm1b_evidence.txt': 'M1b 取证转储,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 登记为「他链产物·保留原样」',
'm1b_ev2.txt': 'M1b 取证转储分片,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 末段(清单明列 m1b_ev2..ev6.txt)登记为「他链产物·保留原样」',
'm1b_ev3.txt': 'M1b 取证转储分片,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 末段(清单明列 m1b_ev2..ev6.txt)登记为「他链产物·保留原样」',
'm1b_ev4.txt': 'M1b 取证转储分片,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 末段(清单明列 m1b_ev2..ev6.txt)登记为「他链产物·保留原样」',
'm1b_ev5.txt': 'M1b 取证转储分片,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 末段(清单明列 m1b_ev2..ev6.txt)登记为「他链产物·保留原样」',
'm1b_ev6.txt': 'M1b 取证转储分片,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 末段(清单明列 m1b_ev2..ev6.txt)登记为「他链产物·保留原样」',
'.dev-notes.bak': 'dev-notes.md 编辑备份,被 projects/pbls/docs/02-develop/m9-closureD-cleanup-record.md §4 判定「保留不动,仅作观察上报」',
'tmp_tools/': 'M11b-4a 取证脚本目录,projects/pbls 仓 commit e8c08f1 已记录其处置结论(保留待 M11b-4c 收口)',
'.qc_tmp/': 'QC 侧(agent.qc)工作目录,非 develop 产物,归属 QC 任务链处置',
'.qc_tools/': 'QC 侧工具目录,非 develop 产物,归属 QC 任务链处置',
'.dev_tmp/': 'M11b-2c-T2a 编辑中转目录,属 M11b-2c 任务链产物(该链收口任务运行中,不代处置)',
'.tmpdev/': 'M11b-2c-T2 round3 补丁脚本目录,属 M11b-2c 任务链产物(该链收口任务运行中,不代处置)',
'.m11b2work/': 'M11b-2 运行转储目录,属 M11b-2 任务链产物',
'.scratch/': 'M10-a 桩目录(含 verify_m10a.py),属 M10-a 任务链产物(该链任务运行中)',
'.m10a_verify/': 'M10-a 验证中转目录(stub + verify.py),属并行运行中的 M10-a 任务链(task NBrM2qPSIMSbtMH9lM6Lu)产物,本任务不代处置',
'.tmpb/': 'M11b-4 v4 汇总草稿目录,属 M11b-4 任务链产物',
'.d/': '空目录(编辑器/工具误建),无内容、无证据价值,本任务已删除;若复跑再出现则判 FAIL',
}
# M5a 链路自身产生的转储(本任务必须删除,不允许进白名单)——列出来作为「反向门禁」:
# 只要它们还在磁盘上就 FAIL,删掉后自然不再命中模式检查。
M5A_OWN_DUMP_GONE = ('evidence_raw.txt', 'r1.txt', 'r2.txt', 'closure_out.txt',
'.scratch_m5a/')
def _read(path):
@ -64,10 +157,12 @@ def _read(path):
def _rel(path):
try:
return os.path.relpath(path, WS_ROOT).replace(os.sep, '/')
except Exception:
return os.path.basename(path)
if path and WS_ROOT:
try:
return os.path.relpath(path, WS_ROOT).replace(os.sep, '/')
except Exception:
pass
return os.path.basename(path or '')
# ── V1 selfcheck 四个门禁函数已落盘且被 CHECKS 装配(防「定义了但不可达」死代码)
@ -125,6 +220,8 @@ def verify_harness_clean():
# ── V3 env/test.json 的 db.sandbox 段(凭据唯一事实源,代码内零明文)
def verify_env_sandbox():
if not WS_ROOT:
return True, 'SKIPPED: 未定位到机构工作空间根(%s),本项为工作空间级取证,跨宿主复跑不计分' % WS_ROOT_SRC
if not os.path.isfile(ENV_TEST):
return False, '缺少 %s' % _rel(ENV_TEST)
try:
@ -152,11 +249,14 @@ def verify_env_sandbox():
if problems:
return False, '; '.join(problems)
detail.append('连接必填字段齐备(host/port/user/password),schema 由 harness 自建自删')
detail.append('路径来源: %s / 项目名=%s' % (WS_ROOT_SRC, PROJECT))
return True, ' | '.join(detail)
# ── V4 三份证据日志齐备,且 harness 日志含 C-3 fail-closed + 1062 真防重原文
def verify_evidence_logs():
if not WS_ROOT:
return True, 'SKIPPED: 未定位到机构工作空间根(%s),本项为工作空间级取证,跨宿主复跑不计分' % WS_ROOT_SRC
if not os.path.isdir(DOCS):
return False, '缺少证据目录 %s' % _rel(DOCS)
top = sorted(f for f in os.listdir(DOCS) if os.path.isfile(os.path.join(DOCS, f)))
@ -198,24 +298,63 @@ def verify_evidence_logs():
return True, ' | '.join(detail)
# ── V5 工作空间卫生:QC #8 点名的一次性补丁已删,历史取证脚本按文档保留
# ── V5 工作空间卫生(QC #2):全类一次性产物扫描 + 白名单必须写明引用文档
def verify_tmp_hygiene():
if not WS_ROOT:
return True, 'SKIPPED: 未定位到机构工作空间根(%s),本项为工作空间级取证,跨宿主复跑不计分' % WS_ROOT_SRC
problems, detail = [], []
still = sorted(f for f in TMP_PATCHES if os.path.isfile(os.path.join(WS_ROOT, f)))
if still:
problems.append('一次性补丁脚本仍在磁盘: %s' % ', '.join(still))
entries = sorted(os.listdir(WS_ROOT))
files = [e for e in entries if os.path.isfile(os.path.join(WS_ROOT, e))]
dirs = [e for e in entries if os.path.isdir(os.path.join(WS_ROOT, e))]
# 5.1 M5a 链路自身转储必须已删除(反向门禁:出现即 FAIL,不许进白名单)
own_left = sorted(p for p in M5A_OWN_DUMP_GONE
if (p in files) or (p in dirs))
if own_left:
problems.append('M5a 链路自身产生的一次性转储仍未删除: %s' % ', '.join(own_left))
else:
detail.append('QC #8 点名脚本已删除: %s' % ', '.join(TMP_PATCHES))
left = sorted(f for f in os.listdir(WS_ROOT)
if f.startswith('tmp_') and f.endswith('.py'))
unexpected = sorted(set(left) - set(TMP_KEPT))
if unexpected:
problems.append('工作空间根仍有未清理的一次性补丁: %s' % ', '.join(unexpected))
kept = sorted(set(left) & set(TMP_KEPT))
if kept:
detail.append('按 M11b-1c-B 文档保留历史取证脚本: %s' % ', '.join(kept))
detail.append('M5a 自身转储已全部删除: %s' % ', '.join(M5A_OWN_DUMP_GONE))
# 5.2 文件类一次性产物:命中模式 → 必须在白名单(且白名单写明引用文档)
hit_files, kept_files = [], []
for f in files:
if any(re.match(p, f) for p in TMP_PATTERNS):
(kept_files if f in WS_KEPT else hit_files).append(f)
if hit_files:
problems.append('工作空间根存在未登记的一次性产物(须删除或进 WS_KEPT 并写明引用文档): %s'
% ', '.join(sorted(hit_files)))
if kept_files:
no_doc = sorted(k for k in kept_files if 'projects/pbls' not in WS_KEPT[k])
if no_doc:
problems.append('白名单项缺「引用它的文档路径」说明: %s' % ', '.join(no_doc))
else:
detail.append('按他链文档保留 %d 项(逐项已写明引用文档): %s'
% (len(kept_files), ', '.join(sorted(kept_files))))
# 5.3 目录类一次性产物:命中模式 → 必须在白名单
hit_dirs, kept_dirs = [], []
for d in dirs:
if d in WS_DIR_ALLOW:
continue
if any(re.match(p, d) for p in TMP_DIR_PATTERNS):
(kept_dirs if d + '/' in WS_KEPT else hit_dirs).append(d)
if hit_dirs:
problems.append('工作空间根存在未登记的临时目录(须删除或进 WS_KEPT 并写明归属任务链): %s'
% ', '.join(sorted(hit_dirs)))
if kept_dirs:
detail.append('他链临时目录按归属保留: %s' % ', '.join(sorted(kept_dirs)))
# 5.4 空目录一律不许留在根上(无证据价值,且会让下一次扫描出现「幽灵项」)
empty = sorted(d for d in dirs if d not in WS_DIR_ALLOW
and not os.listdir(os.path.join(WS_ROOT, d)))
if empty:
problems.append('工作空间根有空目录: %s' % ', '.join(empty))
else:
detail.append('根目录无空目录残留')
if problems:
return False, '; '.join(problems)
detail.append('路径来源: %s' % WS_ROOT_SRC)
return True, ' | '.join(detail)
@ -251,25 +390,55 @@ def verify_readme_matches_code():
return True, ' | '.join(detail)
# ── V7 README 对本收口脚本的描述与代码一致(收口脚本自身也不许「文档漂移」)
def verify_readme_matches_closure():
if not os.path.isfile(README):
return False, '缺少 README.md'
rtxt = _read(README)
if 'verify_m5a_closure.py' not in rtxt:
return False, 'README 未引用 scripts/verify_m5a_closure.py(本脚本成为无人知晓的孤儿件)'
# 取 README 中提及本脚本的那个小节(从该标题行到下一个二级标题)
lines = rtxt.splitlines()
start = next((i for i, ln in enumerate(lines) if 'verify_m5a_closure.py' in ln), None)
if start is None:
return False, 'README 未提及本脚本'
end = start
while end < len(lines) and not (end > start and lines[end].startswith('## ')):
end += 1
section = '\n'.join(lines[start:end])
claimed = sorted(set(re.findall(r'CLOSURE VERIFY ALL PASS \((\d+)/\d+\)', section)))
n = len(CHECKS)
if claimed != [str(n)]:
return False, ('README 收口小节声称 %s 与代码 CHECKS 条数 %d 不一致'
% (claimed or '无条数声明', n))
return True, 'README 收口小节声称条数 = 代码 CHECKS 条数 = %d,且脚本路径磁盘命中' % n
CHECKS = [
('selfcheck 四个新增门禁函数已落盘且装配进 CHECKS', verify_gate_functions),
('harness 修复版在磁盘:test123/args or {} 等 0 命中 + ast 可解析', verify_harness_clean),
('env/test.json db.sandbox 段齐备(scope=sandbox_only,凭据唯一事实源)', verify_env_sandbox),
('三份运行证据日志齐备(C-3.1~C-3.4 + 1062 真防重原文 + # RC=0)', verify_evidence_logs),
('工作空间根一次性补丁脚本已清理(QC #8)', verify_tmp_hygiene),
('工作空间根一次性产物卫生门禁(全类模式扫描 + 白名单须写明引用文档)', verify_tmp_hygiene),
('README 门禁条数与 selfcheck CHECKS 条数一致', verify_readme_matches_code),
('README 收口小节条数与本脚本 CHECKS 条数一致(防收口件文档漂移)', verify_readme_matches_closure),
]
def main():
print('verify_m5a_closure: target = modules/pbl_evidence')
print('workspace root = %s (%s); project = %s'
% (WS_ROOT or 'UNRESOLVED', WS_ROOT_SRC, PROJECT))
failed = []
for i, (name, fn) in enumerate(CHECKS, 1):
try:
ok, detail = fn()
except Exception as exc:
ok, detail = False, '检查执行异常 %s: %s' % (type(exc).__name__, exc)
print('[%s] V%d. %s' % ('PASS' if ok else 'FAIL', i, name))
# 跨宿主复跑时工作空间级取证项返回 SKIPPED(不计分也不冒充 PASS 结论)
skipped = ok and detail.startswith('SKIPPED')
print('[%s] V%d. %s' % ('SKIP' if skipped else ('PASS' if ok else 'FAIL'),
i, name))
print(' %s' % detail)
if not ok:
failed.append(name)