|
@@ -25,7 +25,7 @@ r"""**反向**呼应审计:自输出 → 功能与算法 → 输入(用户
|
|
|
✓ 呼应成立 生成端在位 ∧ 输入在位 ∧ 判据通过(跨度 ⊆ 输入 / 键集 ⊆ 输入 / 计数一致 / 行数 > 0)
|
|
✓ 呼应成立 生成端在位 ∧ 输入在位 ∧ 判据通过(跨度 ⊆ 输入 / 键集 ⊆ 输入 / 计数一致 / 行数 > 0)
|
|
|
~ 输入不在位 生成端知道,但 `data/raw/<场>/<类>` 不在 ⇒ **现在无法验证**(放数据后复跑本器)
|
|
~ 输入不在位 生成端知道,但 `data/raw/<场>/<类>` 不在 ⇒ **现在无法验证**(放数据后复跑本器)
|
|
|
✗ 无生成端 全库 0 处写入方 ⇒ **输出↔输入的呼应在原理上不成立**:这件产物无法由输入推导出来,
|
|
✗ 无生成端 全库 0 处写入方 ⇒ **输出↔输入的呼应在原理上不成立**:这件产物无法由输入推导出来,
|
|
|
- 只能从"含产物的交付包"补齐(products_restore_missing.py --stash <包.zip>)
|
|
|
|
|
|
|
+ **无法由重算生出来**;按用户令 2026-09-17 #1 运行期也不从交付包补齐 ⇒ 只能由研发补生成端(本器 --feasibility 给出逐族可逆性)
|
|
|
? 未归类 既不在族表里、台账里也没有 —— 需要人工认领(本器把它当缺口报出来)
|
|
? 未归类 既不在族表里、台账里也没有 —— 需要人工认领(本器把它当缺口报出来)
|
|
|
|
|
|
|
|
## 用法
|
|
## 用法
|
|
@@ -246,6 +246,87 @@ def _npz_ok(p: pathlib.Path) -> bool:
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# ── 逆向工程可行性 (用户令 2026-09-17 #2) ────────────────────────────────────────────
|
|
|
|
|
+# 问的是: "这批没有生成端的产物, 能不能从原始件/在包生成端**推**出来?"
|
|
|
|
|
+# 判定不靠印象, 靠两条机器证据:
|
|
|
|
|
+# ① 生成端在不在包内(在 → 只是没接上, 跑一次就有);
|
|
|
|
|
+# ② 产物正文里有没有**人工判断**的痕迹("裁决/审核/校准/评审/经验"这类字段)——
|
|
|
|
|
+# 含人工判断的件不是任何输入的纯函数, 逆向工程推不出来, 只能把那一步人工工作重做。
|
|
|
|
|
+import re as _re # noqa: E402
|
|
|
|
|
+JUDGE_PAT = _re.compile(r'人工|裁决|审核|复核|校准|评审|经验值|专家|judg|review|manual|calibrat|verdict|sign[_ ]?off',
|
|
|
|
|
+ _re.I)
|
|
|
|
|
+# ★ 只认"**字段**叫这个名字"(`"裁决": …` / `裁决,`),不认正文里顺嘴提一句 ——
|
|
|
|
|
+# 2026-09-17 第一版拿整篇子串匹配, 结果把**页面**里的展示标签(`index.html` 里的"评审/人工")当成
|
|
|
|
|
+# 人工判断, 于是把"页面"整族误判成不可逆。这属于"判据太糙 → 结论假"。
|
|
|
|
|
+JUDGE_KEY_PAT = _re.compile(r'["\']?[^"\',:]{0,24}(人工|裁决|审核|复核|校准|评审|经验值|专家|'
|
|
|
|
|
+ r'judg|review|manual|calibrat|verdict|sign[_ ]?off)[^"\',:]{0,24}["\']?\s*[:=]',
|
|
|
|
|
+ _re.I)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def judgement_evidence(farm: str, rels: list[str], cap: int = 8) -> tuple[int, list[str], int]:
|
|
|
|
|
+ """→ (含人工判断字段的抽样比例%, 样例, 被检查件数)。
|
|
|
|
|
+
|
|
|
|
|
+ 只查**数据/文本件**(.json/.csv/.md/.txt); **页面(.html/.js)不算** —— 页面是产物的渲染,
|
|
|
|
|
+ 里面出现"评审/人工"是展示标签, 不代表这份件内含判断。
|
|
|
|
|
+ """
|
|
|
|
|
+ hit, samples, checked, pages = 0, [], 0, 0
|
|
|
|
|
+ for rel in rels[:cap]:
|
|
|
|
|
+ p = P.out_root(farm) / rel
|
|
|
|
|
+ suf = p.suffix.lower()
|
|
|
|
|
+ if suf in ('.html', '.js', '.css', '.htm'):
|
|
|
|
|
+ pages += 1
|
|
|
|
|
+ continue
|
|
|
|
|
+ try:
|
|
|
|
|
+ if suf == '.json':
|
|
|
|
|
+ txt = p.read_text(encoding='utf-8', errors='replace')[:200000]
|
|
|
|
|
+ pat = JUDGE_KEY_PAT
|
|
|
|
|
+ elif suf in ('.csv', '.txt'):
|
|
|
|
|
+ txt = p.read_text(encoding='utf-8', errors='replace')[:50000]
|
|
|
|
|
+ pat = JUDGE_KEY_PAT
|
|
|
|
|
+ elif suf == '.md':
|
|
|
|
|
+ txt = p.read_text(encoding='utf-8', errors='replace')[:50000]
|
|
|
|
|
+ pat = JUDGE_PAT
|
|
|
|
|
+ else:
|
|
|
|
|
+ continue
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ continue
|
|
|
|
|
+ checked += 1
|
|
|
|
|
+ if pat.search(txt):
|
|
|
|
|
+ hit += 1
|
|
|
|
|
+ samples.append(rel)
|
|
|
|
|
+ return (100 * hit // checked if checked else 0), samples, pages
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def feasibility(fam: dict, rels: list[str], gen_ok: bool, in_ok: bool,
|
|
|
|
|
+ judge_pct: int, judge_samples: list[str], pages: int = 0) -> tuple[str, str]:
|
|
|
|
|
+ """→ (可逆性判定, 依据)。四类: 可逆(直接) / 可逆(需反推口径) / 不可逆(人工判断) / 应移出产物仓。"""
|
|
|
|
|
+ if fam['kind'] == 'not-product':
|
|
|
|
|
+ return '应移出产物仓', '它本就不是产物(工具脚本/交证件/测试输出) ⇒ 该从产物仓移走, 而不是"补生成端"'
|
|
|
|
|
+ if gen_ok and in_ok:
|
|
|
|
|
+ return '可逆(直接)', f"生成端在包内({fam['gen']})且输入在位 ⇒ 接上/重跑即可"
|
|
|
|
|
+ if pages and pages >= max(3, len(rels[:8]) // 2):
|
|
|
|
|
+ return '可逆(页面可再生)', ('这一族主要是**页面**(.html): 页面是产物的渲染, 不是独立数据 ⇒ '
|
|
|
|
|
+ '接上在包的页面生成端重出即可(未必逐字节同, 能力与数据一致)')
|
|
|
|
|
+ m_n = sum(1 for r in rels if r.endswith(('.parquet', '.csv', '.npz')))
|
|
|
|
|
+ t_n = len(rels) - m_n
|
|
|
|
|
+ split = f'({m_n} 件测量形态 / {t_n} 件文本·判断件)' if (m_n and t_n) else ''
|
|
|
|
|
+ # 两类都占相当比重时, 结论必须是"混合"而不是挑一类代表全族 —— 否则一句判定就把另一半骗过去了
|
|
|
|
|
+ if m_n and t_n and min(m_n, t_n) * 4 >= len(rels):
|
|
|
|
|
+ return '混合(测量件可反推 / 文本件需人定)', split + \
|
|
|
|
|
+ '测量形态的件可由 data/raw 反推口径 + 逐值对拍; 文本/判断件(评审、裁决、回复)推不出来'
|
|
|
|
|
+ if judge_pct >= 50 and judge_samples:
|
|
|
|
|
+ return '不可逆(人工判断)', (f"抽样 {len(judge_samples)} 件里都写着人工判断字段(如 {judge_samples[0]}) "
|
|
|
|
|
+ f"⇒ 不是输入的纯函数, 逆向工程推不出来")
|
|
|
|
|
+ if m_n and m_n * 2 >= len(rels):
|
|
|
|
|
+ return '可逆(需反推口径)', split + '测量形态的件是测量数据的函数 ⇒ 可从 data/raw 反推口径 + 逐值对拍' \
|
|
|
|
|
+ '(做法同 temp_monthly: 反推 → 逐值一致才敢用)'
|
|
|
|
|
+ if gen_ok:
|
|
|
|
|
+ return '半可逆(生成端在包, 输入不足)', '生成端在包内, 缺的是上游输入 ⇒ 上游补齐后即可重出'
|
|
|
|
|
+ if judge_pct > 0:
|
|
|
|
|
+ return '半可逆(需人工裁定)', f'抽样里 {judge_pct}% 的件含人工判断字段 ⇒ 机器部分可推、判断部分要人定'
|
|
|
|
|
+ return '需人工裁定', '既无生成端、形态也不是测量函数 ⇒ 需研发给口径或领域正本'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _turbines_of(p: pathlib.Path, col: str = 'turbine', cap: int = 200000):
|
|
def _turbines_of(p: pathlib.Path, col: str = 'turbine', cap: int = 200000):
|
|
|
import pandas as pd
|
|
import pandas as pd
|
|
|
try:
|
|
try:
|
|
@@ -294,14 +375,24 @@ def audit(farm: str | None = None, verbose: bool = True):
|
|
|
if fam['kind'] == 'not-product':
|
|
if fam['kind'] == 'not-product':
|
|
|
# ★ 不是产物, 却躺在产物仓里: 反向审计对它的结论是"它压根不该按产物管" ——
|
|
# ★ 不是产物, 却躺在产物仓里: 反向审计对它的结论是"它压根不该按产物管" ——
|
|
|
# 既不需要生成端, 也谈不上与输入呼应(这一类最该被人看见, 故单独一类, 不当失败计)。
|
|
# 既不需要生成端, 也谈不上与输入呼应(这一类最该被人看见, 故单独一类, 不当失败计)。
|
|
|
|
|
+ jp, js, pg = judgement_evidence(farm, rels)
|
|
|
|
|
+ fv, fw = feasibility(fam, rels, False, False, jp, js, pg)
|
|
|
row['verdict'] = '✗ 非产物'
|
|
row['verdict'] = '✗ 非产物'
|
|
|
row['note'] = fam.get('why', '')
|
|
row['note'] = fam.get('why', '')
|
|
|
|
|
+ row['rev'], row['rev_why'] = fv, fw
|
|
|
for r in rels:
|
|
for r in rels:
|
|
|
verdict[r] = dict(fam=fam['id'], verdict='✗',
|
|
verdict[r] = dict(fam=fam['id'], verdict='✗',
|
|
|
why='非产物(过程留痕/工具脚本躺在产物仓里): ' + fam.get('why', ''))
|
|
why='非产物(过程留痕/工具脚本躺在产物仓里): ' + fam.get('why', ''))
|
|
|
elif fam['kind'] == 'shipped':
|
|
elif fam['kind'] == 'shipped':
|
|
|
|
|
+ jp, js, pg = judgement_evidence(farm, rels)
|
|
|
|
|
+ gen_file = ROOT / fam['gen'] if fam.get('gen') else None
|
|
|
|
|
+ gen_ok0 = bool(gen_file and gen_file.is_file())
|
|
|
|
|
+ fv, fw = feasibility(fam, rels, gen_ok0, False, jp, js, pg)
|
|
|
row['verdict'] = '✗ 无生成端'
|
|
row['verdict'] = '✗ 无生成端'
|
|
|
row['note'] = fam.get('why', '')
|
|
row['note'] = fam.get('why', '')
|
|
|
|
|
+ row['rev'], row['rev_why'] = fv, fw
|
|
|
|
|
+ if jp:
|
|
|
|
|
+ row['rev_why'] += f';人工判断字段抽样命中 {jp}%' + (f'(如 {js[0]})' if js else '')
|
|
|
for r in rels:
|
|
for r in rels:
|
|
|
verdict[r] = dict(fam=fam['id'], verdict='✗', why='无生成端(全库 0 处写入方): ' + fam['why'])
|
|
verdict[r] = dict(fam=fam['id'], verdict='✗', why='无生成端(全库 0 处写入方): ' + fam['why'])
|
|
|
else:
|
|
else:
|
|
@@ -377,6 +468,7 @@ def audit(farm: str | None = None, verbose: bool = True):
|
|
|
else:
|
|
else:
|
|
|
row['verdict'] = '✓ 呼应成立'
|
|
row['verdict'] = '✓ 呼应成立'
|
|
|
row['note'] = ';'.join(notes + passed)
|
|
row['note'] = ';'.join(notes + passed)
|
|
|
|
|
+ row['rev'], row['rev_why'] = '已在重算链上', '生成端在位、输入在位、判据通过 —— 无需逆向工程'
|
|
|
for r in rels:
|
|
for r in rels:
|
|
|
verdict[r] = dict(fam=fam['id'], verdict=row['verdict'][0],
|
|
verdict[r] = dict(fam=fam['id'], verdict=row['verdict'][0],
|
|
|
why=row['note'] if row['verdict'][0] != '✓' else row['note'][:120])
|
|
why=row['note'] if row['verdict'][0] != '✓' else row['note'][:120])
|
|
@@ -415,6 +507,19 @@ def audit(farm: str | None = None, verbose: bool = True):
|
|
|
ok = counts.get('✓', 0)
|
|
ok = counts.get('✓', 0)
|
|
|
print(f' 结论: 输出↔输入呼应**成立** {ok} 件 / **不成立(无生成端)** {counts.get("✗", 0)} 件 / '
|
|
print(f' 结论: 输出↔输入呼应**成立** {ok} 件 / **不成立(无生成端)** {counts.get("✗", 0)} 件 / '
|
|
|
f'无法验证 {counts.get("~", 0)} 件 / 未归类 {counts.get("?", 0)} 件')
|
|
f'无法验证 {counts.get("~", 0)} 件 / 未归类 {counts.get("?", 0)} 件')
|
|
|
|
|
+ # ── 逆向工程可行性 (用户令 #2): 没有呼应关系的那些, 能不能推出来 ──
|
|
|
|
|
+ import collections as _c
|
|
|
|
|
+ by_rev = _c.Counter()
|
|
|
|
|
+ for r in fam_rows:
|
|
|
|
|
+ by_rev[r.get('rev', '?')] += r['n']
|
|
|
|
|
+ print()
|
|
|
|
|
+ print('== 反向「可逆性」矩阵(用户令 2026-09-17 #2: 无生成端的件能否从原件推出来)==')
|
|
|
|
|
+ print(f'{"族":26s} {"件数":>6s} {"可逆性":22s} 依据')
|
|
|
|
|
+ for r in fam_rows:
|
|
|
|
|
+ if r['verdict'][0] == '✓':
|
|
|
|
|
+ continue
|
|
|
|
|
+ print(f' {r["id"]:24s} {r["n"]:6d} {r.get("rev", "?"):22s} {r.get("rev_why", "")[:96]}')
|
|
|
|
|
+ print(' 按件数汇总: ' + ' · '.join(f'{k}={v}' for k, v in by_rev.most_common()))
|
|
|
rc = 5 if (fails or unclassified) else 0
|
|
rc = 5 if (fails or unclassified) else 0
|
|
|
return fam_rows, verdict, counts, fails, unclassified, rc
|
|
return fam_rows, verdict, counts, fails, unclassified, rc
|
|
|
|
|
|
|
@@ -434,10 +539,23 @@ def doc_block(farm: str) -> str:
|
|
|
f'{r["input"] or "—(无生成端)"} | {r["pred"]} | {r["note"][:160]} |')
|
|
f'{r["input"] or "—(无生成端)"} | {r["pred"]} | {r["note"][:160]} |')
|
|
|
if unclassified:
|
|
if unclassified:
|
|
|
L += ['', f'**未归类 {len(unclassified)} 件**(需人工认领):' + '、'.join(f'`{x}`' for x in unclassified[:12])]
|
|
L += ['', f'**未归类 {len(unclassified)} 件**(需人工认领):' + '、'.join(f'`{x}`' for x in unclassified[:12])]
|
|
|
- L += ['', '> 生成方式:`python scripts/products_reverse_audit.py --write-doc`。',
|
|
|
|
|
- '> `✓` 与 `✗` 的区别就是"这件产物的输出能不能由输入推导出来":`✗` 的那些**只能**用 '
|
|
|
|
|
- '`scripts/products_restore_missing.py --stash <含产物的交付包.zip>` 补齐 —— 重算永远生不出它们。',
|
|
|
|
|
- DOC_END]
|
|
|
|
|
|
|
+ L += ['', '#### 反向「可逆性」:没有呼应关系的那批,能否从原始件推出来(用户令 2026-09-17 #2)', '',
|
|
|
|
|
+ '判据两条机器证据:① 生成端在不在包内 ② 产物正文里有没有**人工判断**字段'
|
|
|
|
|
+ '(`人工/裁决/审核/校准/评审/经验/judge/review/calibrat/verdict`,抽样读件统计命中率)。'
|
|
|
|
|
+ '含人工判断的件不是任何输入的纯函数 ⇒ 逆向工程推不出来,只能把那一步人工工作重做。', '',
|
|
|
|
|
+ '| 输出族 | 件数 | 可逆性 | 依据 |', '|---|---:|---|---|']
|
|
|
|
|
+ for r in fam_rows:
|
|
|
|
|
+ if r['verdict'][0] == '✓':
|
|
|
|
|
+ continue
|
|
|
|
|
+ L.append(f'| `{r["id"]}` | {r["n"]} | {r.get("rev", "?")} | {r.get("rev_why", "")} |')
|
|
|
|
|
+ import collections as _c
|
|
|
|
|
+ by_rev = _c.Counter()
|
|
|
|
|
+ for r in fam_rows:
|
|
|
|
|
+ by_rev[r.get('rev', '?')] += r['n']
|
|
|
|
|
+ L += ['', '**按件数汇总**:' + ' · '.join(f'{k} = {v} 件' for k, v in by_rev.most_common()), '',
|
|
|
|
|
+ '> 结论口径:`可逆(直接)` = 包内已有生成端,接上即可;`可逆(需反推口径)` = 内容是测量数据的函数,'
|
|
|
|
|
+ '照 `temp_monthly` 的办法反推 + 逐值对拍;`不可逆(人工判断)` = 逆向工程推不出来,'
|
|
|
|
|
+ '要么由研发补生成端、要么承认它是人工件(不该按产物管)。', DOC_END]
|
|
|
return '\n'.join(L)
|
|
return '\n'.join(L)
|
|
|
|
|
|
|
|
|
|
|