fix(A5): 体积凭证指纹改取 git 索引 blob——跨 checkout 稳定,并拦未 staged 改动
现象(道C-3 开工第一步就撞上,属于上一提交 e25e51c 引入的缺陷):
_wt/P1-10-test-fw 里 check_size_gate --local 报"不同码",而两棵树 git status 全干净、
HEAD 同为 e1967cf。定位:主工作树的受管源码里 **1046/1388 个文件落成 CRLF**,新建
工作树同一提交落成 **LF**(实测 Drivers/BSP/dbg_cfg.h:1293 B/CRLF vs 1247 B/LF),
但两者的**索引 blob 完全相同**(都是 c39395f7…)。根因是本仓 core.autocrlf=true 与
.gitattributes 的 eol=lf 并存——所以"工作树原始字节"根本不是跨 checkout 的稳定身份。
后果比红一次更糟:主树盖的凭证在**每一条道的平行工作树**里都会误红,runner 上同样
(checkout 出来的行尾取决于那台机器的配置)→ 门禁自己造 false positive。
修法(AGENTS「行尾不依赖 core.autocrlf」的机器化版本):
- 指纹 = git **索引** blob:`git ls-files -s` 的 path+blob 逐行聚合哈希(v2),
跨 checkout / 跨机器一致;凭证 schema 从 1 抬到 2,旧凭证直接判"不符、重新盖戳",
不做静默兼容(v1 凭证在别的树里必然误红,留着只会污染判读)。
- 新增第四道前置:**范围内有未 staged 改动/未跟踪文件时拒绝盖戳**——未 staged 的部分
不在索引里,凭证会声称覆盖一份并不存在的代码。已 git add 的不算脏:索引已含其内容,
正是"将要提交并被测量"的那一份。--local/--ci 同样把这类脏单独报出来。
- 新增守卫的机理性说明写在 index_snapshot() 上方与 docstring 的"指纹范围"节(唯一正文)。
- scripts/size_attestation.json:按 v2 重新盖戳(本树产品态实测 Code=214104 /
RW=1204 / ZI=131852 / RW_IRAM1 75,632 B / RW_CCM 57,376 B,与主干 2026-10-25
台账行同源)。
验证:
- 跨树一致性:_wt/P1-10-test-fw 与主工作树分别独立算索引指纹,结果都是
bf6ec94a761e…(1388 文件)→ CROSS-TREE STABLE。
- --stamp / --local / --ci 三态均 rc=0;凭证与 tsv 末行互证同源(Code=214,104 B)。
- 负向:在 test/board/ 放一个未跟踪探针文件 → --local 与 --stamp 双双 rc=1 并点名该
文件;删除后 --local 回到 rc=0(本树已确认干净,探针不留痕)。
- py_compile 通过;门禁实跑:no-mojibake / trailing-whitespace / end-of-file-fixer /
check-added-large-files / id-check 全部 0。
RAM 增量:0 B(凭证与门禁脚本,不进固件)。gate:全过,未用 --no-verify。
⚠ 需要 lead 做一步:合入后主树跑一次 python scripts/check_size_gate.py --stamp
(或 ram_baseline.py --update)把 v1 凭证换成 v2,否则主树的 L0c 会报 schema 不符。
This commit is contained in:
+115
-38
@@ -47,8 +47,13 @@
|
|||||||
|
|
||||||
指纹范围(= 会影响固件体积的全部受管源码)
|
指纹范围(= 会影响固件体积的全部受管源码)
|
||||||
App/ Drivers/ Src/ Inc/ Lib/ Middlewares/ test/board/ 以及 MDK-ARM 下的 .sct/.s
|
App/ Drivers/ Src/ Inc/ Lib/ Middlewares/ test/board/ 以及 MDK-ARM 下的 .sct/.s
|
||||||
列表取 `git ls-files`(与 layer-deps / id-check 同口径),逐文件 sha256(路径+内容) 再
|
身份取 **git 索引 blob**(`git ls-files -s` 的 path+blob 逐行聚合哈希),不是工作树
|
||||||
聚合;未跟踪文件与构建产物天然不在内。
|
原始字节——本仓 core.autocrlf=true 而 .gitattributes 写死 eol=lf,实测主树 1046/1388
|
||||||
|
个受管源码落成 CRLF、新建工作树同一提交落成 LF,而索引 blob 完全一致;按原始字节算
|
||||||
|
会让同一份代码在不同 checkout 得到不同指纹(主树盖的戳在别人工作树与 runner 上误红)
|
||||||
|
→ 索引 blob 才是跨 checkout 稳定的身份,也是 AGENTS「不依赖 core.autocrlf」的机器化。
|
||||||
|
推论:**未 staged 的改动不在索引里**,凭证覆盖不到 → --stamp 拒绝在有脏时盖章,
|
||||||
|
--local/--ci 也把它单独报出来(已 git add 的不算脏:索引已含其内容,正是要测的那份)。
|
||||||
|
|
||||||
用法(仓库根):
|
用法(仓库根):
|
||||||
python scripts/check_size_gate.py --stamp
|
python scripts/check_size_gate.py --stamp
|
||||||
@@ -76,7 +81,9 @@ import ram_report # noqa: E402
|
|||||||
|
|
||||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
ATTESTATION = REPO_ROOT / "scripts" / "size_attestation.json"
|
ATTESTATION = REPO_ROOT / "scripts" / "size_attestation.json"
|
||||||
SCHEMA = 1
|
# v1 = 工作树原始字节指纹(跨 checkout 不稳定,见 index_snapshot 的实测说明)→ 作废;
|
||||||
|
# v2 = git 索引 blob 指纹。schema 不符即拒绝复用,逼一次重新盖戳,不静默误判。
|
||||||
|
SCHEMA = 2
|
||||||
|
|
||||||
SIZE_SCOPE_DIRS = ("App/", "Drivers/", "Src/", "Inc/", "Lib/", "Middlewares/",
|
SIZE_SCOPE_DIRS = ("App/", "Drivers/", "Src/", "Inc/", "Lib/", "Middlewares/",
|
||||||
"test/board/")
|
"test/board/")
|
||||||
@@ -93,46 +100,89 @@ def in_scope(rel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def tracked_files():
|
def _git(args):
|
||||||
"""受管文件列表(git 不可用时退回目录遍历,同 check_layer_deps.py)。"""
|
"""跑 git 并返回 stdout 文本;git 不可用/非仓库 → 抛 RuntimeError(调用方按环境错误)。"""
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(["git", "ls-files", "-z"], cwd=str(REPO_ROOT),
|
out = subprocess.run(["git"] + args, cwd=str(REPO_ROOT),
|
||||||
capture_output=True, check=True)
|
capture_output=True, check=True)
|
||||||
except Exception: # noqa: BLE001 git 缺失/不在仓库内
|
except subprocess.CalledProcessError as exc:
|
||||||
files = []
|
raise RuntimeError("git %s 失败:%s" % (" ".join(args), exc)) from exc
|
||||||
for root, dirs, names in os.walk(REPO_ROOT):
|
except (OSError, subprocess.SubprocessError) as exc:
|
||||||
dirs[:] = [d for d in dirs if d not in (".git", ".venv", "build")]
|
raise RuntimeError("无法调用 git:%s" % exc) from exc
|
||||||
for n in names:
|
return out.stdout
|
||||||
rel = os.path.relpath(os.path.join(root, n), REPO_ROOT)
|
|
||||||
files.append(rel.replace(os.sep, "/"))
|
|
||||||
return sorted(files)
|
|
||||||
return sorted(p.decode("utf-8", "replace") for p in out.stdout.split(b"\0") if p)
|
|
||||||
|
|
||||||
|
|
||||||
def scoped_files():
|
def index_snapshot():
|
||||||
return [f for f in tracked_files() if in_scope(f)]
|
"""受管且**在指纹范围内**的 (路径, 索引 blob id) 有序表。
|
||||||
|
|
||||||
|
⚠ 为什么取**索引 blob**而不是工作树原始字节(2026-10-25 实测发现,别改回去):
|
||||||
|
本仓 core.autocrlf=true,而 .gitattributes 写死 eol=lf —— 主工作树的受管源码
|
||||||
|
里有 **1046/1388 个文件落成 CRLF**,新建工作树同一提交落成 LF,两者**索引 blob
|
||||||
|
完全相同**(实测 dbg_cfg.h:1247 B/LF vs 1293 B/CRLF,blob 都是 c39395f7)。
|
||||||
|
拿原始字节算指纹 = 同一份代码在不同 checkout 得到不同指纹:主树盖的戳在道C
|
||||||
|
新工作树里判"不同码",runner 上同样会误红——**门禁自己造 false positive**。
|
||||||
|
索引 blob 是 git 归一化后的身份,跨 checkout 稳定,也正是 AGENTS「行尾由
|
||||||
|
.gitattributes 写死、不依赖 core.autocrlf」的机器化说法。
|
||||||
|
"""
|
||||||
|
raw = _git(["ls-files", "-s", "-z"])
|
||||||
|
rows = []
|
||||||
|
for item in raw.split(b"\0"):
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
meta, _, path = item.partition(b"\t") # "<mode> <sha> <stage>"
|
||||||
|
parts = meta.decode("ascii", "replace").split(" ")
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
rel = path.decode("utf-8", "surrogateescape").replace(os.sep, "/")
|
||||||
|
if in_scope(rel):
|
||||||
|
rows.append((rel, parts[1]))
|
||||||
|
return sorted(rows)
|
||||||
|
|
||||||
|
|
||||||
def tree_fingerprint():
|
def tree_fingerprint():
|
||||||
"""(指纹, 文件数, 缺失数)。受管但已删除的文件以哨兵入指纹,保证增删也变指纹。"""
|
"""(指纹, 文件数)。指纹 = sha256(每行 "path\\tblob" 拼接),取自 git 索引。"""
|
||||||
|
rows = index_snapshot()
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
n = miss = 0
|
for rel, blob in rows:
|
||||||
for rel in scoped_files():
|
|
||||||
n += 1
|
|
||||||
try:
|
|
||||||
blob = (REPO_ROOT / rel).read_bytes()
|
|
||||||
except OSError:
|
|
||||||
miss += 1
|
|
||||||
blob = b"<deleted>"
|
|
||||||
h.update(rel.encode("utf-8", "surrogateescape"))
|
h.update(rel.encode("utf-8", "surrogateescape"))
|
||||||
h.update(b"\0")
|
h.update(b"\t")
|
||||||
h.update(hashlib.sha256(blob).digest())
|
h.update(blob.encode("ascii", "replace"))
|
||||||
return h.hexdigest(), n, miss
|
h.update(b"\n")
|
||||||
|
return h.hexdigest(), len(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def unmeasured_dirt(limit=8):
|
||||||
|
"""范围内**未 staged** 的改动(含未跟踪文件)——它们不在索引里,指纹覆盖不到。
|
||||||
|
|
||||||
|
返回 (条数, 样例列表)。判据用 porcelain v1 的第二列(工作树列):非空格且非
|
||||||
|
未跟踪标记即"改了没 staged";'??' = 未跟踪新文件。⚠ 第一列(索引列)有改动是
|
||||||
|
**允许**的——那正是"已 git add、即将提交"的那份内容,索引已经把它算进指纹。
|
||||||
|
"""
|
||||||
|
raw = _git(["status", "--porcelain=v1", "-z", "--untracked-files=all"])
|
||||||
|
dirty = []
|
||||||
|
for rec in raw.split(b"\0"):
|
||||||
|
if len(rec) < 4:
|
||||||
|
continue
|
||||||
|
xy = rec[:2].decode("ascii", "replace")
|
||||||
|
rel = rec[3:].decode("utf-8", "surrogateescape").replace(os.sep, "/")
|
||||||
|
if not in_scope(rel):
|
||||||
|
continue
|
||||||
|
if xy == "??":
|
||||||
|
dirty.append(rel)
|
||||||
|
elif xy[1] != " ":
|
||||||
|
dirty.append(rel)
|
||||||
|
return len(dirty), dirty[:limit]
|
||||||
|
|
||||||
|
|
||||||
def newest_source_mtime():
|
def newest_source_mtime():
|
||||||
|
"""范围内**工作树文件**的最新 mtime(只用于 .map 新鲜度守卫,不进指纹)。
|
||||||
|
|
||||||
|
注意这里必须看工作树:守卫要抓的是"盖戳前又改了文件却没重编",而那正是索引
|
||||||
|
之外(未 staged)或已 staged 但磁盘更新的那份内容。文件已删除则跳过(增删本身
|
||||||
|
由指纹变化表达)。
|
||||||
|
"""
|
||||||
newest = 0.0
|
newest = 0.0
|
||||||
for rel in scoped_files():
|
for rel, _blob in index_snapshot():
|
||||||
try:
|
try:
|
||||||
newest = max(newest, (REPO_ROOT / rel).stat().st_mtime)
|
newest = max(newest, (REPO_ROOT / rel).stat().st_mtime)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -227,12 +277,28 @@ def cmd_stamp(args):
|
|||||||
for b in bad:
|
for b in bad:
|
||||||
print(" - " + b, file=sys.stderr)
|
print(" - " + b, file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
fp, n, miss = tree_fingerprint()
|
# 第四道前置:范围内**有未 staged 的改动就拒绝盖戳**——索引(= 即将提交的那份)
|
||||||
|
# 里没有它们,凭证会"声称覆盖"一份并不存在的代码。已 git add 的改动**不算脏**
|
||||||
|
# (索引已含其内容,正是凭证该描述的东西)。
|
||||||
|
dirt, samples = unmeasured_dirt()
|
||||||
|
if dirt:
|
||||||
|
print("[FAIL] 指纹范围内有 %d 处未 staged 的改动/未跟踪文件,拒绝盖戳:" % dirt,
|
||||||
|
file=sys.stderr)
|
||||||
|
for s in samples:
|
||||||
|
print(" - " + s, file=sys.stderr)
|
||||||
|
print(" 原因:凭证按 git 索引内容生成,未 staged 的部分不在索引里、覆盖不到。",
|
||||||
|
file=sys.stderr)
|
||||||
|
print(" 修复:先 git add <这些文件>(凭证描述的就是你正要提交的那份),再盖戳;",
|
||||||
|
file=sys.stderr)
|
||||||
|
print(" 或把它们还原。@build 与盖戳都应在同一份内容上做。")
|
||||||
|
return 1
|
||||||
|
fp, n = tree_fingerprint()
|
||||||
write_attestation({
|
write_attestation({
|
||||||
"schema": SCHEMA,
|
"schema": SCHEMA,
|
||||||
"fingerprint": fp,
|
"fingerprint": fp,
|
||||||
|
"fingerprint_source": "git-index",
|
||||||
"fingerprint_files": n,
|
"fingerprint_files": n,
|
||||||
"fingerprint_missing": miss,
|
"worktree_dirt_at_stamp": dirt,
|
||||||
"sizes": m["sizes"],
|
"sizes": m["sizes"],
|
||||||
"regions": m["regions"],
|
"regions": m["regions"],
|
||||||
"pct": m["pct"],
|
"pct": m["pct"],
|
||||||
@@ -248,7 +314,7 @@ def cmd_stamp(args):
|
|||||||
})
|
})
|
||||||
ir = m["regions"].get("RW_IRAM1", {}).get("size", 0)
|
ir = m["regions"].get("RW_IRAM1", {}).get("size", 0)
|
||||||
cc = m["regions"].get("RW_CCM", {}).get("size", 0)
|
cc = m["regions"].get("RW_CCM", {}).get("size", 0)
|
||||||
print("[OK] 体积同码凭证已写出:%s(指纹 %s…,%d 个文件)" % (
|
print("[OK] 体积同码凭证已写出:%s(索引指纹 %s…,%d 个文件)" % (
|
||||||
ATTESTATION.relative_to(REPO_ROOT), fp[:12], n))
|
ATTESTATION.relative_to(REPO_ROOT), fp[:12], n))
|
||||||
print(" Code=%s B / RW_IRAM1=%s B / RW_CCM=%s B" % (
|
print(" Code=%s B / RW_IRAM1=%s B / RW_CCM=%s B" % (
|
||||||
format(m["sizes"]["code"], ","), format(ir, ","), format(cc, ",")))
|
format(m["sizes"]["code"], ","), format(ir, ","), format(cc, ",")))
|
||||||
@@ -276,17 +342,25 @@ def _cmd_parity(args, ci_mode):
|
|||||||
" 修复:本地 @build 之后跑 python scripts/ram_baseline.py --update",
|
" 修复:本地 @build 之后跑 python scripts/ram_baseline.py --update",
|
||||||
" (或单独跑 python scripts/check_size_gate.py --stamp)",
|
" (或单独跑 python scripts/check_size_gate.py --stamp)",
|
||||||
])
|
])
|
||||||
fp, n, miss = tree_fingerprint()
|
fp, n = tree_fingerprint()
|
||||||
problems = []
|
problems = []
|
||||||
|
dirt, samples = unmeasured_dirt()
|
||||||
|
if dirt:
|
||||||
|
problems += [
|
||||||
|
"指纹范围内有 %d 处**未 staged** 的改动/未跟踪文件,凭证覆盖不到它们" % dirt,
|
||||||
|
" " + "、".join(samples) + ("…" if dirt > len(samples) else ""),
|
||||||
|
" 含义:这些内容既没进索引(凭证按索引生成),也就没被那次测量算进去",
|
||||||
|
" 修复:git add 之后 @build,再跑 python scripts/ram_baseline.py --update",
|
||||||
|
]
|
||||||
if fp != doc["fingerprint"]:
|
if fp != doc["fingerprint"]:
|
||||||
problems += [
|
problems += [
|
||||||
"本次代码(指纹 %s…,%d 文件)与体积测量(指纹 %s…,%s)不同码" % (
|
"本次代码(指纹 %s…,%d 文件)与体积测量(指纹 %s…,%s)不同码" % (
|
||||||
fp[:12], n, str(doc["fingerprint"])[:12], doc.get("built_at", "?")),
|
fp[:12], n, str(doc["fingerprint"])[:12], doc.get("built_at", "?")),
|
||||||
" 含义:这份源码没有被真实链接测量过(或测量完又改了代码/合入了别的改动)",
|
" 含义:这份源码没有被真实链接测量过(或测量完又改了代码/合入了别的改动)",
|
||||||
" 修复:@build 0/0 之后跑 python scripts/ram_baseline.py --update 再提交",
|
" 修复:@build 0/0 之后跑 python scripts/ram_baseline.py --update 再提交",
|
||||||
|
" (指纹取 git 索引 blob:同一份内容在任何工作树/runner 上算法一致,"
|
||||||
|
"不受 CRLF/LF 检出差异影响)",
|
||||||
]
|
]
|
||||||
if miss:
|
|
||||||
problems.append(" (注:当前树缺 %d 个受管文件,可能 checkout 不完整)" % miss)
|
|
||||||
if ci_mode:
|
if ci_mode:
|
||||||
thr = doc.get("threshold_pct", ram_baseline.THRESHOLD_PCT)
|
thr = doc.get("threshold_pct", ram_baseline.THRESHOLD_PCT)
|
||||||
bad = []
|
bad = []
|
||||||
@@ -338,8 +412,11 @@ def _tsv_consistency(doc):
|
|||||||
|
|
||||||
|
|
||||||
def cmd_show(_args):
|
def cmd_show(_args):
|
||||||
fp, n, miss = tree_fingerprint()
|
fp, n = tree_fingerprint()
|
||||||
print("当前受管源码指纹:%s(%d 文件,缺失 %d)" % (fp, n, miss))
|
dirt, samples = unmeasured_dirt()
|
||||||
|
print("当前受管源码索引指纹:%s(%d 文件;未 staged %d 处)" % (fp, n, dirt))
|
||||||
|
for s in samples:
|
||||||
|
print(" dirty: " + s)
|
||||||
doc, err = read_attestation()
|
doc, err = read_attestation()
|
||||||
if doc is None:
|
if doc is None:
|
||||||
print("凭证:%s" % err)
|
print("凭证:%s" % err)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"built_at": "2026-10-25T03:58:34+08:00",
|
"built_at": "2026-10-25T04:15:33+08:00",
|
||||||
"fingerprint": "b18aee6e56c7388f103939646190742b22bb6573f6667202e4ae9f8213485579",
|
"fingerprint": "bf6ec94a761eacd8e3bcbc590e8780c2293462d384b9b33094e5d0fd6411f295",
|
||||||
"fingerprint_files": 1388,
|
"fingerprint_files": 1388,
|
||||||
"fingerprint_missing": 0,
|
"fingerprint_source": "git-index",
|
||||||
"gated_regions": [
|
"gated_regions": [
|
||||||
"RW_IRAM1",
|
"RW_IRAM1",
|
||||||
"RW_CCM"
|
"RW_CCM"
|
||||||
],
|
],
|
||||||
"git_head": "ba9450c50b932bd2085809fb3c1acc8d72bafbb9",
|
"git_head": "e1967cf2eac70b794c920774715c6fe78dc30ac2",
|
||||||
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
|
||||||
"map_mtime": 1792870197,
|
"map_mtime": 1792872260,
|
||||||
"pct": {
|
"pct": {
|
||||||
"ER_IROM1": 22.36,
|
"ER_IROM1": 22.36,
|
||||||
"RW_CCM": 87.89,
|
"RW_CCM": 87.89,
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"size": 48
|
"size": 48
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"schema": 2,
|
||||||
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
|
"scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring",
|
||||||
"sizes": {
|
"sizes": {
|
||||||
"code": 214104,
|
"code": 214104,
|
||||||
@@ -43,5 +43,6 @@
|
|||||||
"zi_data": 131852
|
"zi_data": 131852
|
||||||
},
|
},
|
||||||
"threshold_pct": 92.0,
|
"threshold_pct": 92.0,
|
||||||
|
"worktree_dirt_at_stamp": 0,
|
||||||
"written_by": "scripts/check_size_gate.py --stamp"
|
"written_by": "scripts/check_size_gate.py --stamp"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user