-
Notifications
You must be signed in to change notification settings - Fork 0
feat(dashboard): 采集层纯函数区+提取式自测(W5-C4 .github#227,ADR-0073) #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4aeb0bb
907ca4e
25f9c17
f4ec90d
89232c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -196,6 +196,165 @@ def sli_stuck(repos): | |
| return stuck | ||
|
|
||
|
|
||
| # @w5c4-pure-begin —— 纯函数区(governance/tests/test-metrics-wiring.sh 按标记对 | ||
| # 提取本块离线单测——不复制实现,防"测试测影子";标记对缺失=测试红) | ||
| DRILL_MARKS = ("演练", "演习", "[drill]") # 演习数据约定标记(sli-report"演练"+ADR-0069"演习"双词兼容) | ||
|
|
||
|
|
||
| def _ts(s): | ||
| """ISO→datetime(失败 None);本块自包含(不依赖模块级 _iso——提取测试可独立运行)。""" | ||
| try: | ||
| return _dt.datetime.fromisoformat(str(s or "").replace("Z", "+00:00")) | ||
| except ValueError: | ||
| return None | ||
|
|
||
|
|
||
| def _is_drill_text(*texts): | ||
| joined = " ".join(str(t or "") for t in texts) | ||
| return any(m in joined for m in DRILL_MARKS) | ||
|
|
||
|
|
||
| def partition_escapes(prs, p0s, now): | ||
| """双窗逃逸事件:current=[now-7d,now),previous=[now-14d,now-7d)。 | ||
|
|
||
| prs=merged PR 节点([auto-revert] 标题约定);p0s=post-merge 冒烟 P0 issue。 | ||
| 演习数据(title/body 含约定标记)从分子排除且 drills_excluded 计数可见—— | ||
| 过滤不可见=作弊通道(sli-report 先例)。reverts_current 供回滚率护栏(分子 | ||
| 只算 revert,不含 P0)。 | ||
| """ | ||
| w = _dt.timedelta(days=7) | ||
| cur = prev = drills = reverts_cur = 0 | ||
| for p in prs or []: | ||
| if "[auto-revert]" not in str(p.get("title") or ""): | ||
| continue | ||
| if _is_drill_text(p.get("title"), p.get("body")): | ||
| drills += 1 | ||
| continue | ||
| ts = _ts(p.get("mergedAt")) | ||
| if ts and now - 2 * w < ts <= now: | ||
| if ts > now - w: | ||
|
Comment on lines
+234
to
+235
|
||
| cur += 1 | ||
|
Comment on lines
+234
to
+236
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Escape window boundary bug partition_escapes()’s comparisons don’t match its stated windows: it excludes exactly now-14d, includes exactly now, and assigns exactly now-7d to the previous window. This mis-buckets events on boundary timestamps and will skew escape metrics once wired. Agent Prompt
|
||
| reverts_cur += 1 | ||
| else: | ||
| prev += 1 | ||
|
Comment on lines
+234
to
+239
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 使窗口边界符合文档定义。 文档定义 建议修复- if ts and now - 2 * w < ts <= now:
- if ts > now - w:
+ if ts and now - 2 * w <= ts < now:
+ if ts >= now - w:请增加 🤖 Prompt for AI Agents |
||
| for i in p0s or []: | ||
| if _is_drill_text(i.get("title"), i.get("body")): | ||
| drills += 1 | ||
| continue | ||
| ts = _ts(i.get("created_at")) | ||
| if ts and now - 2 * w < ts <= now: | ||
| if ts > now - w: | ||
| cur += 1 | ||
| else: | ||
| prev += 1 | ||
| return {"current": cur, "previous": prev, "reverts_current": reverts_cur, | ||
| "drills_excluded": drills} | ||
|
|
||
|
|
||
| def drill_redrate_lines(lines): | ||
| """drill history.jsonl 行→红率输入(kind=seed-drill,red/denom=red+green—— | ||
| 与 drill.py redrate 同口径,no-surface 与 failclose 演习不入分母;畸形行计入 bad 可见)。""" | ||
| red = denom = bad = 0 | ||
| for ln in lines or []: | ||
| ln = ln.strip() | ||
| if not ln or ln.startswith("#"): | ||
| continue | ||
| try: | ||
| rec = json.loads(ln) | ||
| except ValueError: | ||
| bad += 1 | ||
| continue | ||
| if rec.get("kind") != "seed-drill": | ||
| continue | ||
| v = rec.get("verdict") | ||
|
Comment on lines
+262
to
+269
|
||
| if v in ("red", "green"): | ||
| denom += 1 | ||
| if v == "red": | ||
| red += 1 | ||
| return {"red": red, "denom": denom, "bad_lines": bad} | ||
|
|
||
|
|
||
| def false_decision_parse(text, now, window_days): | ||
| """arbiter 误放行台账文本→(窗内 false-allow 数, 窗内 false-deny 数, 全部行列表)。 | ||
| `#` 注释行跳过(台账文件头约定);date 出窗不计。""" | ||
| allow = deny = 0 | ||
| lines = [] | ||
| for ln in str(text or "").splitlines(): | ||
| ln = ln.strip() | ||
| if not ln or ln.startswith("#"): | ||
| continue | ||
| try: | ||
| rec = json.loads(ln) | ||
| except ValueError: | ||
| continue | ||
| lines.append(rec) | ||
| ts = _ts(rec.get("date")) | ||
| if ts and (now - ts).days <= window_days: | ||
| if rec.get("kind") == "false-allow": | ||
|
Comment on lines
+291
to
+293
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Future ledger counted false_decision_parse() counts entries with future date values because it only checks `(now - ts).days <= window_days, which is also true when ts > now` (negative days). This inflates false-allow/false-deny counts if the ledger contains clock-skewed or malformed future timestamps. Agent Prompt
|
||
| allow += 1 | ||
| elif rec.get("kind") == "false-deny": | ||
| deny += 1 | ||
|
Comment on lines
+291
to
+296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 排除未来记录并使用精确窗口。
建议修复- if ts and (now - ts).days <= window_days:
+ if ts and now - _dt.timedelta(days=window_days) <= ts <= now:请添加未来记录和 🤖 Prompt for AI Agents |
||
| return allow, deny, lines | ||
|
|
||
|
|
||
| def sign_durations(timelines): | ||
| """type:intent issue 的 timeline 事件列表→(签署耗时秒列表, 在途 draft 数)。 | ||
|
|
||
| 耗时=首个 labeled state:ir-signed 时刻 − 首个 labeled state:ir-draft 时刻 | ||
| (宪法 §7:签署耗时如实计入判断预算)。无 draft 事件的已签 IR 不可算→跳过 | ||
| (不造 0);有 draft 无 signed→在途。""" | ||
| durations, in_flight = [], 0 | ||
| for events in timelines or []: | ||
| draft = signed = None | ||
| for ev in events or []: | ||
| if ev.get("event") != "labeled": | ||
| continue | ||
| name = (ev.get("label") or {}).get("name") | ||
| ts = _ts(ev.get("created_at")) | ||
| if not ts: | ||
| continue | ||
| if name == "state:ir-draft" and draft is None: | ||
| draft = ts | ||
| elif name == "state:ir-signed" and signed is None and draft is not None: | ||
| signed = ts | ||
| if draft and signed and signed > draft: | ||
| durations.append(round((signed - draft).total_seconds())) | ||
|
Comment on lines
+320
to
+321
|
||
| elif draft and not signed: | ||
| in_flight += 1 | ||
| return durations, in_flight | ||
|
|
||
|
|
||
| def dwell_hours(events, now, label="state:needs-human"): | ||
| """timeline 事件→进入 label 态至今停留小时数(取最近一次 labeled 时刻—— | ||
| 反复进出取当前段)。无该事件→None。""" | ||
| latest = None | ||
| for ev in events or []: | ||
| if ev.get("event") != "labeled": | ||
| continue | ||
| if (ev.get("label") or {}).get("name") != label: | ||
| continue | ||
|
Comment on lines
+331
to
+335
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Needs-human dwell overcounts dwell_hours() ignores unlabeled events, so a card that entered state:needs-human and later exited (unlabeled) will still be treated as continuously “dwelling” since the last labeled event. This breaks the stated requirement “反复进出取当前段”. Agent Prompt
|
||
| ts = _ts(ev.get("created_at")) | ||
| if ts and (latest is None or ts > latest): | ||
| latest = ts | ||
|
Comment on lines
+334
to
+338
|
||
| if latest is None or latest > now: | ||
| return None | ||
| return round((now - latest).total_seconds() / 3600, 2) | ||
|
Comment on lines
+331
to
+341
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target function and nearby helpers ---'
sed -n '285,350p' governance/dashboard-update.py
printf '%s\n' '--- related tests and references ---'
rg -n -C 3 'dwell_hours|unlabeled|needs-human|dashboard-update' governance tests .github 2>/dev/null || true
printf '%s\n' '--- repository governance references ---'
rg -n -C 2 'ADR-[0-9]{4}|agent-registry|owner' governance .github CODEOWNERS 2>/dev/null || trueRepository: Cloudbird-Software/.github Length of output: 50383 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
p = Path("governance/dashboard-update.py")
text = p.read_text()
start = text.index("def dwell_hours")
end = text.find("\ndef ", start + 1)
if end == -1:
end = len(text)
print(text[start:end])
PYRepository: Cloudbird-Software/.github Length of output: 725 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- exact existing dwell assertions ---'
sed -n '108,128p' governance/tests/test-metrics-wiring.sh
printf '%s\n' '--- label transition/removal handling ---'
rg -n -C 3 'unlabel|remove.*label|label.*remove|state:needs-human' governance .github --glob '*.py' --glob '*.sh' --glob '*.yaml' --glob '*.yml' | head -160
printf '%s\n' '--- source-derived behavioral probe ---'
python3 - <<'PY'
from datetime import datetime, timezone
def _ts(v):
if not v:
return None
try:
s = str(v).replace("Z", "+00:00")
return datetime.fromisoformat(s)
except (TypeError, ValueError):
return None
# This is a standalone probe of the reviewed function's exact logic.
def dwell_hours(events, now, label="state:needs-human"):
latest = None
for ev in events or []:
if ev.get("event") != "labeled":
continue
if (ev.get("label") or {}).get("name") != label:
continue
ts = _ts(ev.get("created_at"))
if ts and (latest is None or ts > latest):
latest = ts
if latest is None or latest > now:
return None
return round((now - latest).total_seconds() / 3600, 2)
now = datetime(2026, 8, 22, tzinfo=timezone.utc)
events = [
{"event": "labeled", "label": {"name": "state:needs-human"},
"created_at": "2026-08-20T00:00:00Z"},
{"event": "unlabeled", "label": {"name": "state:needs-human"},
"created_at": "2026-08-21T00:00:00Z"},
]
print({"labeled_then_unlabeled_result": dwell_hours(events, now)})
print({"expected_if_current_state_is_removed": None})
assert dwell_hours(events, now) == 48.0
assert dwell_hours(events, now) is not None
PYRepository: Cloudbird-Software/.github Length of output: 4174 🌐 Web query:
💡 Result: In the GitHub REST API, the unlabeled event signifies that a label has been removed from an issue or pull request [1][2]. This event type is part of the Issue Events API [3][4]. The object returned for an unlabeled event includes the following core properties: id: The unique identifier of the event [1][3]. node_id: The Global Node ID of the event [1][3]. url: The REST API URL to fetch the event [1][3]. actor: The user who triggered the event [1][3]. event: Identifies the event type (in this case, unlabeled) [1][3]. created_at: The timestamp of the event [1][3]. label: An object containing the name and color of the label that was removed [1][3]. commit_id and commit_url: Information about the commit, if applicable [1][3]. performed_via_github_app: Information regarding whether the event was performed by a GitHub app [3][4]. Additionally, the unlabeled action exists within the payload of the IssuesEvent type in the GitHub Events API (used for general activity streams), where it appears as an action within the payload object [5]. It is also available as an action type in webhooks [6]. For authoritative technical specifications, see the official GitHub documentation on Issue Event Types and Issue Events endpoints [1][4]. Citations:
在移除 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def user_metric_from(content_text): | ||
| """产品仓 user-result.yaml 文本→指标 dict(缺 metric_key/value=不完整→None)。 | ||
| 本地 import yaml——提取测试独立运行不依赖模块级导入。""" | ||
| import yaml | ||
| try: | ||
| d = yaml.safe_load(content_text) | ||
| except Exception: | ||
| return None | ||
| if isinstance(d, dict) and d.get("metric_key") and "value" in d: | ||
| return d | ||
|
Comment on lines
+352
to
+353
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Null metric value accepted user_metric_from() returns a dict whenever the value key exists, even if value is null/None, contradicting “缺 metric_key/value=不完整→None”. This can cause “pending 不造数” to be bypassed and propagate null metrics as if valid. Agent Prompt
|
||
| return None | ||
| # @w5c4-pure-end | ||
|
|
||
|
|
||
| def build_payload(repos, cards, purl=""): | ||
| rate, denom = sli_automerge(repos) | ||
| sli = {"automerge_rate": rate, "human_touch_per_pr": None, "escape_rate": None, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,148 @@ | ||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||
| # test-metrics-wiring.sh —— dashboard 采集层纯函数自测(W5-C4 .github#227,ADR-0073) | ||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||
| # 从 dashboard-update.py 按 @w5c4-pure 标记对提取纯函数区(不复制实现——防"测试 | ||||||||||||||||||||||||||||||||
| # 测影子",test-ir0002.sh 同模式;标记对缺失=fail-closed 红),fixture 断言: | ||||||||||||||||||||||||||||||||
| # 逃逸双窗分割(sustained 无状态化)· 演习过滤可见 · 演习红率分母口径 | ||||||||||||||||||||||||||||||||
| # 误放行台账窗过滤 · 签署耗时 timeline 差(无 draft 不造 0)· needs-human 停留 | ||||||||||||||||||||||||||||||||
| # 产品仓用户结果指标读取位(缺失/畸形=pending 不造数) | ||||||||||||||||||||||||||||||||
| # 用法:bash governance/tests/test-metrics-wiring.sh(零网络零真实 gh) | ||||||||||||||||||||||||||||||||
| set -uo pipefail | ||||||||||||||||||||||||||||||||
| HERE="$(cd "$(dirname "$0")" && pwd)" | ||||||||||||||||||||||||||||||||
| GOV="$(cd "$HERE/.." && pwd)" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| PASS=0; FAIL=0 | ||||||||||||||||||||||||||||||||
| pass() { PASS=$((PASS+1)); echo "PASS $1"; } | ||||||||||||||||||||||||||||||||
| fail() { FAIL=$((FAIL+1)); echo "FAIL $1"; } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| PY="" | ||||||||||||||||||||||||||||||||
| for c in "${PYTHON:-}" python3 python py -3; do | ||||||||||||||||||||||||||||||||
| [[ -n "$c" ]] || continue | ||||||||||||||||||||||||||||||||
| "$c" -c 'import sys, yaml; print("ok")' >/dev/null 2>&1 || continue | ||||||||||||||||||||||||||||||||
| PY="$c"; break | ||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 5. Python picker unreliable test-metrics-wiring.sh can select a non-Python-3 interpreter (e.g., python/py pointing to Python 2) because it only checks import yaml, and it never actually tries py -3 due to word-splitting. This can make the new test fail spuriously or run under the wrong interpreter. Agent Prompt
|
||||||||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||||||||
| [[ -n "$PY" ]] || { echo "::error::无可用 python(含 pyyaml)"; exit 2; } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT | ||||||||||||||||||||||||||||||||
| SRC="$GOV/dashboard-update.py" | ||||||||||||||||||||||||||||||||
| [[ -f "$SRC" ]] || { echo "FATAL: dashboard-update.py 不存在"; exit 2; } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # --- 提取被测纯函数区(标记对缺失=fail-closed) --- | ||||||||||||||||||||||||||||||||
| awk '/@w5c4-pure-begin/{f=1} f{print} /@w5c4-pure-end/{exit}' "$SRC" >"$TMP/pure_body.py" | ||||||||||||||||||||||||||||||||
| if ! grep -q '^def partition_escapes(' "$TMP/pure_body.py"; then | ||||||||||||||||||||||||||||||||
| echo "FATAL: 标记对内未找到纯函数定义(提取失效——实现与测试脱钩)"; exit 2 | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
| # 垫片头:提取块只依赖 stdlib(datetime/json)+局部 yaml——补导入即可独立运行 | ||||||||||||||||||||||||||||||||
| { echo 'import datetime as _dt'; echo 'import json'; cat "$TMP/pure_body.py"; } >"$TMP/pure.py" | ||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 验证结束标记,保持 fail-closed 行为。 Line 31 在缺少 建议修复 awk '/@w5c4-pure-begin/{f=1} f{print} /@w5c4-pure-end/{exit}' "$SRC" >"$TMP/pure_body.py"
-if ! grep -q '^def partition_escapes(' "$TMP/pure_body.py"; then
+if ! grep -q '^# `@w5c4-pure-end`$' "$TMP/pure_body.py" ||
+ ! grep -q '^def partition_escapes(' "$TMP/pure_body.py"; then
echo "FATAL: 标记对内未找到纯函数定义(提取失效——实现与测试脱钩)"; exit 2
fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| cat >>"$TMP/pure.py" <<'PYEOF' | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # ==== 驱动断言(now 固定——离线可复现,owner 复算同款) ==== | ||||||||||||||||||||||||||||||||
| NOW = _dt.datetime(2026, 8, 22, 0, 0, tzinfo=_dt.timezone.utc) | ||||||||||||||||||||||||||||||||
| W7 = _dt.timedelta(days=7) | ||||||||||||||||||||||||||||||||
| results = [] | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def check(name, cond): | ||||||||||||||||||||||||||||||||
| results.append((name, bool(cond))) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 1) 逃逸双窗分割:current=[now-7d,now) previous=[now-14d,now-7d);演习排除且可见 | ||||||||||||||||||||||||||||||||
| prs = [ | ||||||||||||||||||||||||||||||||
| {"title": "[auto-revert] #11 bad fix", "body": "x", | ||||||||||||||||||||||||||||||||
| "mergedAt": (NOW - _dt.timedelta(days=1)).isoformat()}, # 本窗 revert | ||||||||||||||||||||||||||||||||
| {"title": "[auto-revert] #10 older", "body": "x", | ||||||||||||||||||||||||||||||||
| "mergedAt": (NOW - _dt.timedelta(days=10)).isoformat()}, # 上一窗 revert | ||||||||||||||||||||||||||||||||
| {"title": "[auto-revert] drill tail", "body": "演习收尾", | ||||||||||||||||||||||||||||||||
| "mergedAt": (NOW - _dt.timedelta(days=2)).isoformat()}, # 演习排除 | ||||||||||||||||||||||||||||||||
| {"title": "feat: normal", "body": "x", | ||||||||||||||||||||||||||||||||
| "mergedAt": (NOW - _dt.timedelta(days=2)).isoformat()}, # 非 revert | ||||||||||||||||||||||||||||||||
| {"title": "[auto-revert] ancient", "body": "x", | ||||||||||||||||||||||||||||||||
| "mergedAt": (NOW - _dt.timedelta(days=30)).isoformat()}, # 出 14d 窗 | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| p0s = [ | ||||||||||||||||||||||||||||||||
| {"title": "post-merge 冒烟失败 #91", "created_at": (NOW - _dt.timedelta(days=3)).isoformat()}, | ||||||||||||||||||||||||||||||||
| {"title": "post-merge 冒烟失败 #80", "created_at": (NOW - _dt.timedelta(days=9)).isoformat()}, | ||||||||||||||||||||||||||||||||
| {"title": "post-merge 冒烟失败 drill", "body": "[drill]", "created_at": (NOW - _dt.timedelta(days=1)).isoformat()}, | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| esc = partition_escapes(prs, p0s, NOW) | ||||||||||||||||||||||||||||||||
| check("逃逸双窗 current=2(1 revert+1 P0)", esc["current"] == 2) | ||||||||||||||||||||||||||||||||
| check("逃逸双窗 previous=2(1 revert+1 P0)", esc["previous"] == 2) | ||||||||||||||||||||||||||||||||
| check("回滚率分子 reverts_current=1(不含 P0)", esc["reverts_current"] == 1) | ||||||||||||||||||||||||||||||||
| check("演习排除计数可见=2(过滤不可见=作弊通道)", esc["drills_excluded"] == 2) | ||||||||||||||||||||||||||||||||
| check("空输入零窗", partition_escapes([], [], NOW) == {"current": 0, "previous": 0, "reverts_current": 0, "drills_excluded": 0}) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 2) 演习红率:kind=seed-drill + no-surface 不入分母(drill.py redrate 同口径);畸形行计数可见 | ||||||||||||||||||||||||||||||||
| lines = [ | ||||||||||||||||||||||||||||||||
| '# 台账头注释', | ||||||||||||||||||||||||||||||||
| '{"kind":"seed-drill","verdict":"red"}', | ||||||||||||||||||||||||||||||||
| '{"kind":"seed-drill","verdict":"green"}', | ||||||||||||||||||||||||||||||||
| '{"kind":"seed-drill","verdict":"no-surface"}', | ||||||||||||||||||||||||||||||||
| '{"kind":"failclose-drill","outcome":"pass"}', # 非种子演习——不入红率分母 | ||||||||||||||||||||||||||||||||
| 'not-json-line', | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| agg = drill_redrate_lines(lines) | ||||||||||||||||||||||||||||||||
| check("演习红率 red=1 denom=2(no-surface 与 failclose 出分母)", agg["red"] == 1 and agg["denom"] == 2) | ||||||||||||||||||||||||||||||||
| check("畸形行 bad_lines=1 可见", agg["bad_lines"] == 1) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 3) 误放行台账:注释跳过 · 窗过滤 · infra 不算误拒(ADR-0054 §7) | ||||||||||||||||||||||||||||||||
| ledger = '\n'.join([ | ||||||||||||||||||||||||||||||||
| '# arbiter 误放行/误拒台账(false decision ledger,ADR-0054 §7)', | ||||||||||||||||||||||||||||||||
| '{"date": "2026-08-15T00:00:00Z", "kind": "false-allow"}', | ||||||||||||||||||||||||||||||||
| '{"date": "2026-08-20T00:00:00Z", "kind": "false-deny"}', | ||||||||||||||||||||||||||||||||
| '{"date": "2026-05-01T00:00:00Z", "kind": "false-allow"}', | ||||||||||||||||||||||||||||||||
| '{"date": "2026-08-21T00:00:00Z", "kind": "infra"}', | ||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||
| allow, deny, fd_lines = false_decision_parse(ledger, NOW, 30) | ||||||||||||||||||||||||||||||||
| check("误放行窗内=1(窗外不计)", allow == 1) | ||||||||||||||||||||||||||||||||
| check("误拒窗内=1(infra 不计)", deny == 1) | ||||||||||||||||||||||||||||||||
| check("行列表=4(注释行剔除)", len(fd_lines) == 4) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 4) 签署耗时:draft→signed timeline 差;在途计数;无 draft 不造 0 | ||||||||||||||||||||||||||||||||
| t_ok = [ | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:ir-draft"}, "created_at": "2026-08-20T10:00:00Z"}, | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:ir-signed"}, "created_at": "2026-08-20T11:30:00Z"}, | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| t_fast = [ | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:ir-draft"}, "created_at": "2026-08-21T10:00:00Z"}, | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:ir-signed"}, "created_at": "2026-08-21T10:00:30Z"}, | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| t_inflight = [{"event": "labeled", "label": {"name": "state:ir-draft"}, "created_at": "2026-08-18T00:00:00Z"}] | ||||||||||||||||||||||||||||||||
| t_nodraft = [{"event": "labeled", "label": {"name": "state:ir-signed"}, "created_at": "2026-08-19T00:00:00Z"}] | ||||||||||||||||||||||||||||||||
| durs, inflight = sign_durations([t_ok, t_fast, t_inflight, t_nodraft]) | ||||||||||||||||||||||||||||||||
| check("签署耗时=[5400, 30](90min 与 30s 快签)", durs == [5400, 30]) | ||||||||||||||||||||||||||||||||
| check("在途 draft=1", inflight == 1) | ||||||||||||||||||||||||||||||||
| check("signed 无 draft 不入统计(不造 0)", len(durs) == 2) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 5) needs-human 停留:取最近一次 labeled 时刻(反复进出取当前段) | ||||||||||||||||||||||||||||||||
| ev = [ | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:needs-human"}, "created_at": "2026-08-10T00:00:00Z"}, | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:in-progress"}, "created_at": "2026-08-15T00:00:00Z"}, | ||||||||||||||||||||||||||||||||
| {"event": "labeled", "label": {"name": "state:needs-human"}, "created_at": "2026-08-21T00:00:00Z"}, | ||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||
| h = dwell_hours(ev, NOW) | ||||||||||||||||||||||||||||||||
| check("停留=24h(最近一次 needs-human)", h == 24.0) | ||||||||||||||||||||||||||||||||
| check("无事件→None", dwell_hours([], NOW) is None) | ||||||||||||||||||||||||||||||||
| check("未来时戳→None(不造负数)", dwell_hours( | ||||||||||||||||||||||||||||||||
| [{"event": "labeled", "label": {"name": "state:needs-human"}, "created_at": "2026-08-23T00:00:00Z"}], NOW) is None) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # 6) 用户结果读取位:完整→ok;缺 value→None;非 yaml→None(pending 不造数) | ||||||||||||||||||||||||||||||||
| check("完整声明→dict", user_metric_from("metric_key: dau\nvalue: 42\nunit: 人\n") == {"metric_key": "dau", "value": 42, "unit": "人"}) | ||||||||||||||||||||||||||||||||
| check("缺 metric_key→None", user_metric_from("value: 42\n") is None) | ||||||||||||||||||||||||||||||||
| check("畸形 yaml→None", user_metric_from(":::not yaml[") is None) | ||||||||||||||||||||||||||||||||
| check("空文本→None", user_metric_from("") is None) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| bad = [n for n, ok in results if not ok] | ||||||||||||||||||||||||||||||||
| for n, ok in results: | ||||||||||||||||||||||||||||||||
| print(("PASS " if ok else "FAIL ") + n) | ||||||||||||||||||||||||||||||||
| raise SystemExit(1 if bad else 0) | ||||||||||||||||||||||||||||||||
| PYEOF | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if "$PY" "$TMP/pure.py" >"$TMP/run.txt" 2>"$TMP/err.txt"; then | ||||||||||||||||||||||||||||||||
| sed 's/^/ /' "$TMP/run.txt" | ||||||||||||||||||||||||||||||||
| pass "dashboard 采集层纯函数 fixture 全过($(grep -c '^PASS' "$TMP/run.txt") 项)" | ||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||
| sed 's/^/ /' "$TMP/run.txt" "$TMP/err.txt" 2>/dev/null | ||||||||||||||||||||||||||||||||
| fail "采集层纯函数断言失败(详见上行)" | ||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| echo "== test-metrics-wiring: pass=$PASS fail=$FAIL ==" | ||||||||||||||||||||||||||||||||
| [[ $FAIL -eq 0 ]] | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
拒绝无时区时间并统一为 UTC。
datetime.fromisoformat会接受无时区时间。后续函数将该值与带 UTC 时区的now比较时会抛出TypeError。例如,partition_escapes处理"2026-08-20T00:00:00"会在 Line 234 失败。该问题也影响false_decision_parse和dwell_hours。建议修复
def _ts(s): """ISO→datetime(失败 None);本块自包含(不依赖模块级 _iso——提取测试可独立运行)。""" try: - return _dt.datetime.fromisoformat(str(s or "").replace("Z", "+00:00")) + value = _dt.datetime.fromisoformat(str(s or "").replace("Z", "+00:00")) + if value.tzinfo is None: + return None + return value.astimezone(_dt.timezone.utc) except ValueError: return None同时在
governance/tests/test-metrics-wiring.sh添加无时区时间被忽略的回归断言。📝 Committable suggestion
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 205-205: Docstring contains ambiguous
((FULLWIDTH LEFT PARENTHESIS). Did you mean((LEFT PARENTHESIS)?(RUF002)
[warning] 205-205: Docstring contains ambiguous
)(FULLWIDTH RIGHT PARENTHESIS). Did you mean)(RIGHT PARENTHESIS)?(RUF002)
[warning] 205-205: Docstring contains ambiguous
;(FULLWIDTH SEMICOLON). Did you mean;(SEMICOLON)?(RUF002)
[warning] 205-205: Docstring contains ambiguous
((FULLWIDTH LEFT PARENTHESIS). Did you mean((LEFT PARENTHESIS)?(RUF002)
[warning] 205-205: Docstring contains ambiguous
)(FULLWIDTH RIGHT PARENTHESIS). Did you mean)(RIGHT PARENTHESIS)?(RUF002)
🤖 Prompt for AI Agents