@haiyangbg/buildbeat 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +24 -301
  2. package/README.en.md +6 -19
  3. package/README.md +6 -19
  4. package/SKILL.md +168 -219
  5. package/bin/buildbeat.js +14 -2
  6. package/docs/CAPABILITY-MATRIX.md +13 -55
  7. package/docs/README.md +14 -14
  8. package/docs/RELEASING.md +8 -9
  9. package/docs/v2/RFC-0001-product-definition.md +2 -0
  10. package/docs/v2/RFC-0003-workflow-policy.md +2 -0
  11. package/docs/v2/guide/00-how-to-talk.md +3 -3
  12. package/docs/v2/guide/01-quickstart.md +12 -12
  13. package/docs/v2/guide/03-policy-guide.md +1 -1
  14. package/docs/v2/guide/06-evidence-guide.md +3 -3
  15. package/docs/v2/guide/07-approval-guide.md +10 -10
  16. package/docs/v2/guide/10-recovery.md +6 -6
  17. package/docs/v2/guide/11-session-handoff.en.md +2 -2
  18. package/docs/v2/guide/11-session-handoff.md +2 -2
  19. package/docs/v2/guide/README.md +0 -6
  20. package/lessons.md +52 -71
  21. package/package.json +16 -8
  22. package/src/v2/cli/run.js +33 -23
  23. package/src/v2/engine/risk-preset.js +1 -1
  24. package/src/v2/runtime/notify.js +5 -5
  25. package/src/v2/runtime/overview.js +7 -7
  26. package/templates/ARCHITECTURE.md +1 -1
  27. package/templates/contracts/PROTOCOL.md +2 -10
  28. package/templates/gitignore.template +0 -3
  29. package/templates/pm/adr/README.md +1 -1
  30. package/templates/pm/decisions.md +4 -5
  31. package/templates/standards/CODE.md +1 -1
  32. package/templates/standards/DESIGN.md +1 -1
  33. package/templates/standards/REVIEW.md +2 -2
  34. package/templates/standards/STACK.md +2 -8
  35. package/templates/v2/AGENTS.md +18 -18
  36. package/templates/v2/BUILDBEAT.md +2 -3
  37. package/templates/v2/CLAUDE.md +1 -1
  38. package/templates/v2/run-config.example.yaml +1 -1
  39. package/templates/v2//346/214/207/346/214/245/345/217/260.md +6 -6
  40. package/bin/buildbeat-v2.js +0 -18
  41. package/bin/solobaton.js +0 -6
  42. package/docs/BuildBeat v2/357/274/232AI /345/216/237/347/224/237/350/275/257/344/273/266/344/272/244/344/273/230/346/216/247/345/210/266/345/271/263/351/235/242.md" +0 -2053
  43. package/docs/CHECKS.md +0 -326
  44. package/docs/CLI-PILOT-2026-08-23.md +0 -25
  45. package/docs/CLI-STRATEGY-2026-08.md +0 -55
  46. package/docs/CLI.md +0 -244
  47. package/docs/EXECUTION-PLAN.md +0 -487
  48. package/docs/LEGACY-V1.16-MIGRATION.md +0 -54
  49. package/docs/PHASE1-PILOT-2026-08-24.md +0 -32
  50. package/docs/PHASE2-BUILDBEAT-PILOT-2026-08-25.md +0 -75
  51. package/docs/PHASE2-PILOT-2026-08-25.md +0 -88
  52. package/docs/PHASE2-PILOT-PREFLIGHT-2026-08-25.md +0 -42
  53. package/docs/PHASE4-STABILITY-AUDIT-2026-08-25.md +0 -35
  54. package/docs/PHASE4-V1.20-PILOT-2026-08-25.md +0 -56
  55. package/docs/ROADMAP.md +0 -875
  56. package/docs/V1.21-RELEASE-EVIDENCE-2026-08-25.md +0 -55
  57. package/docs/V2-D2-DECISION-CARD.md +0 -37
  58. package/docs/V2-DECISIONS.md +0 -11
  59. package/docs/V2-ITERATION-01.md +0 -60
  60. package/docs/V2-ITERATION-02.md +0 -32
  61. package/docs/V2-ITERATION-03.md +0 -30
  62. package/docs/V2-ITERATION-04.md +0 -29
  63. package/docs/V2-ITERATION-05.md +0 -20
  64. package/docs/V2-ITERATION-06.md +0 -18
  65. package/docs/V2-ITERATION-07.md +0 -36
  66. package/docs/V2-ITERATION-08.md +0 -62
  67. package/docs/V2-PLAN.md +0 -335
  68. package/docs/V2-PROPOSAL.md +0 -319
  69. package/docs/V2.0.0-BETA.1-RELEASE-EVIDENCE-2026-08-28.md +0 -41
  70. package/docs/V2.0.0-BETA.2-RELEASE-EVIDENCE-2026-08-28.md +0 -8
  71. package/docs/V2.0.0-BETA.3-RELEASE-EVIDENCE-2026-09-01.md +0 -8
  72. package/docs/V2.0.0-BETA.4-RELEASE-EVIDENCE-2026-09-03.md +0 -9
  73. package/docs/V2.0.0-BETA.5-RELEASE-EVIDENCE-2026-09-05.md +0 -10
  74. package/docs/V2.0.0-RELEASE-EVIDENCE-2026-09-05.md +0 -11
  75. package/docs/WP4.3-RELEASE-EVIDENCE-2026-08-25.md +0 -73
  76. package/docs/v2/M1-ACCEPTANCE-2026-08-28.md +0 -38
  77. package/docs/v2/M2-DOD-2026-08-28.md +0 -34
  78. package/docs/v2/M4-EXTERNAL-PILOT-2026-08-28.md +0 -46
  79. package/docs/v2/M4-PILOT-APP-2026-08-28.md +0 -44
  80. package/docs/v2/M4-SELFHOST-2026-08-28.md +0 -53
  81. package/docs/v2/guide/08-migration-v1.md +0 -72
  82. package/example/.buildbeat/manifest.json +0 -45
  83. package/example/AGENTS.md +0 -19
  84. package/example/ARCHITECTURE.md +0 -39
  85. package/example/BUILDBEAT.md +0 -17
  86. package/example/CLAUDE.md +0 -7
  87. package/example/README.md +0 -75
  88. package/example/contracts/PROTOCOL.md +0 -38
  89. package/example/pm/NOW.md +0 -22
  90. package/example/pm/adr/ADR-0001-local-first-sqlite.md +0 -25
  91. package/example/pm/adr/README.md +0 -7
  92. package/example/pm/archive//344/270/200/346/234/237/evidence/gate1.md +0 -5
  93. package/example/pm/archive//344/270/200/346/234/237/evidence/gate2.md +0 -5
  94. package/example/pm/archive//344/270/200/346/234/237/evidence/gate3.md +0 -5
  95. package/example/pm/archive//344/270/200/346/234/237/evidence/gate4.md +0 -5
  96. package/example/pm/archive//344/270/200/346/234/237/evidence/implementation.md +0 -5
  97. package/example/pm/decisions.md +0 -20
  98. package/example/pm/status//344/272/247/345/223/201.md +0 -20
  99. package/example/pm/status//345/205/250/346/240/210.md +0 -15
  100. package/example/pm/status//346/265/213/350/257/225.md +0 -15
  101. package/example/pm//344/270/200/346/234/237-/347/234/213/346/235/277.md +0 -97
  102. package/example/standards/CODE.md +0 -18
  103. package/example/standards/DESIGN.md +0 -34
  104. package/example/standards/REVIEW.md +0 -16
  105. package/example/standards/STACK.md +0 -31
  106. package/src/cli.js +0 -323
  107. package/src/constants.js +0 -202
  108. package/src/doctor.js +0 -267
  109. package/src/planner.js +0 -251
  110. package/src/project.js +0 -844
  111. package/src/upgrader.js +0 -1249
  112. package/src/v2/presets/risk/legacy-four-gates.yaml +0 -44
  113. package/src/writer.js +0 -534
  114. package/templates/.claude/agents/reviewer.md +0 -62
  115. package/templates/AGENTS.md +0 -85
  116. package/templates/BUILDBEAT.md +0 -13
  117. package/templates/CLAUDE.md +0 -7
  118. package/templates/pm/NOW.md +0 -26
  119. package/templates/pm/changes/README.md +0 -44
  120. package/templates/pm/status/README.md +0 -32
  121. package/templates/pm//345/275/223/346/234/237/347/234/213/346/235/277.md +0 -62
  122. package/templates/scripts/bus-check.sh +0 -1875
  123. package/templates/scripts/design-preview.sh +0 -44
  124. package/templates/scripts/drift-check.sh +0 -112
  125. package/templates/scripts/pre-commit.sh +0 -74
  126. package/templates/scripts/verify-status.sh +0 -105
  127. package/templates//346/214/207/346/214/245/345/217/260.md +0 -58
@@ -1,1875 +0,0 @@
1
- #!/usr/bin/env bash
2
- # bus-check.sh —— 协作总线 · 开工同步护栏(治信息差)。
3
- # 任意域会话「开工先跑这个」;部署 / 改契约 / 跑 migration 等不可逆动作前**再跑一次**。
4
- # 只读、不改任何东西;打印 当前期 / 协调层腐烂检测 / Gate与证据 / 可选 standards与ADR / 契约 / 最近拍板 / 各域状态 / 幽灵hash核验 / 在途提案 / 子仓同步 / 线上实况。
5
- # 规则⑨:「线上什么版本」以本脚本打的实况为准,文档不写。
6
- # --format=json:只在 stdout 输出 schema 1 JSON;不混入人类仪表盘。
7
- # --strict:机器闸模式 —— finding level 为 conflict / error 时 exit 1(挂 pre-commit/CI 用,见 pre-commit.sh)。
8
- # warning / unverified 保持可见但不拦。不带 --strict 时即使有 findings 仍 exit 0,只当仪表盘。
9
- #
10
- # 项目接入点(可选;下列同伴脚本一律**放在本脚本同一目录**,本脚本按 SDIR 找它们):
11
- # 1) SUBREPOS 数组留空 = 自动发现协调层根下一/二级目录里的独立 git 子仓;也可手工列死。
12
- # 2) 线上实况:提供 live-status.sh(自行调用部署平台 CLI,每行输出「名称<TAB或空格>版本」),
13
- # 本脚本存在即调用、不存在则提示。跳过:BUS_CHECK_NO_LIVE=1
14
- # 离线/弱网:BUS_CHECK_NO_FETCH=1 跳过 meta 仓与子仓的 git fetch(只看本地已知状态)
15
- # 3) 生产漂移检测:提供 drift-check.sh + live-config.sh(见模板),
16
- # 本脚本存在即调用;`--update-baseline` 会透传给它(部署/改 env 后刷基线)。
17
- # 4) 工程层验证能力:改 verify-status.sh(模板含参考实现)的 SUITES 接入测试命令,
18
- # 本脚本存在即调用 —— 证据分级 L3(自动化测试)靠它兜底;`--run` 真跑并记「上次全绿」。
19
- # 5) 可选 standards/STACK.md 结构合法且 Confirmed 时,比对 v1 基线与 Node/lockfile/Docker 普通文件事实;
20
- # 只报 drift/unverified,不改任何文件。默认最多扫 200 个相关候选,可用 BUS_STACK_MAX 调整。
21
- set -uo pipefail
22
- # 协调层根 = 向上最近的含 pm/NOW.md 的目录;SDIR = 本脚本目录(相对根),脚本间互调都走它。
23
- # 故本脚本放 <根>/scripts/ 或 <根>/pm/scripts/ 均可(紧凑布局见 SKILL §3),不假设固定深度。
24
- _sd="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
- ROOT="$_sd"; for _ in 1 2 3 4; do [ -f "$ROOT/pm/NOW.md" ] && break; ROOT="$(dirname "$ROOT")"; done
26
- [ -f "$ROOT/pm/NOW.md" ] || ROOT="$(cd "$_sd/.." && pwd)" # 探测不到(骨架还没建) → 退回旧假设:脚本在 <根>/scripts/
27
- SDIR="${_sd#"$ROOT"/}"; [ "$SDIR" = "$_sd" ] && SDIR="."
28
- cd "$ROOT" || { echo "bus-check: cannot enter coordination root" >&2; exit 2; }
29
- ROOT_PHYS="$(pwd -P)"
30
- DRIFT_MODE=""; STRICT=0; FORMAT="human"
31
- for arg in "$@"; do
32
- case "$arg" in
33
- --update-baseline) DRIFT_MODE="--update-baseline" ;; # 透传给漂移检测
34
- --strict) STRICT=1 ;;
35
- --format=json) FORMAT="json" ;;
36
- *) echo "bus-check: unknown argument: $arg" >&2; exit 2 ;;
37
- esac
38
- done
39
-
40
- BUS_TMP="$(mktemp -d "${TMPDIR:-/tmp}/buildbeat-bus-check.XXXXXX")" || {
41
- echo "bus-check: cannot create temporary report directory" >&2
42
- exit 2
43
- }
44
- FINDINGS_RAW="$BUS_TMP/findings.raw"
45
- FINDINGS_SORTED="$BUS_TMP/findings.sorted"
46
- HUMAN_OUT="$BUS_TMP/human.out"
47
- : > "$FINDINGS_RAW"
48
- : > "$FINDINGS_SORTED"
49
- : > "$HUMAN_OUT"
50
- # shellcheck disable=SC2317,SC2329 # invoked indirectly by trap
51
- cleanup_bus_check() { rm -rf -- "$BUS_TMP"; }
52
- trap cleanup_bus_check EXIT
53
-
54
- # Keep the existing dashboard byte-compatible enough for people while JSON gets
55
- # stdout to itself. All blocking decisions are derived from FINDINGS_RAW.
56
- exec 3>&1
57
- if [ "$FORMAT" = "json" ]; then exec > "$HUMAN_OUT"; fi
58
-
59
- finding_rank() {
60
- case "$1" in
61
- sync.now_bloated) echo 10 ;;
62
- sync.ghost_hash) echo 20 ;;
63
- sync.production_drift) echo 30 ;;
64
- sync.multirepo_drift) echo 35 ;;
65
- sync.l3_stale) echo 40 ;;
66
- sync.l3_unconfigured) echo 50 ;;
67
- sync.scan_truncated) echo 60 ;;
68
- sync.unverified) echo 70 ;;
69
- gate.line_missing) echo 100 ;;
70
- gate.invalid) echo 105 ;;
71
- gate.na_without_reason) echo 110 ;;
72
- gate.na_inconsistent) echo 115 ;;
73
- gate.pass_untraceable) echo 120 ;;
74
- evidence.missing) echo 200 ;;
75
- evidence.outside_archive) echo 210 ;;
76
- ref.broken) echo 300 ;;
77
- standards.invalid) echo 400 ;;
78
- standards.unconfirmed) echo 410 ;;
79
- stack.drift) echo 420 ;;
80
- stack.unverified) echo 430 ;;
81
- adr.status_invalid) echo 500 ;;
82
- adr.superseded_broken) echo 510 ;;
83
- *) echo 900 ;;
84
- esac
85
- }
86
-
87
- add_finding() {
88
- code="$1"; level="$2"; message="$3"; path="${4:-}"
89
- rank="$(finding_rank "$code")"
90
- message="$(printf '%s' "$message" | tr '\t\r\n' ' ')"
91
- path="$(printf '%s' "$path" | tr '\t\r\n' ' ')"
92
- printf '%s\t%s\t%s\t%s\t%s\n' "$rank" "$code" "$level" "$message" "$path" >> "$FINDINGS_RAW"
93
- }
94
-
95
- # Mechanical coverage boundaries share one non-blocking finding. The reason
96
- # token is intentionally small and stable; details never include raw OS errors
97
- # or file contents, and the path remains coordination-root relative.
98
- add_scan_boundary() {
99
- boundary_reason="$1"; boundary_path="$2"; boundary_detail="$3"
100
- add_finding "sync.scan_truncated" "unverified" \
101
- "Scan coverage is incomplete: reason=$boundary_reason; $boundary_detail." \
102
- "$boundary_path"
103
- }
104
-
105
- path_uses_symlink_component() {
106
- boundary_probe="$1"
107
- case "$boundary_probe" in
108
- "$ROOT_PHYS"/*) ;;
109
- *) return 1 ;;
110
- esac
111
- while [ "$boundary_probe" != "$ROOT_PHYS" ]; do
112
- [ -L "$boundary_probe" ] && return 0
113
- boundary_parent="$(dirname "$boundary_probe")"
114
- [ "$boundary_parent" != "$boundary_probe" ] || break
115
- boundary_probe="$boundary_parent"
116
- done
117
- return 1
118
- }
119
-
120
- resolve_hash() {
121
- candidate_hash="$1"
122
- for hash_repo in . "${SUBREPOS[@]:-}"; do
123
- [ -n "$hash_repo" ] || continue
124
- [ -d "$hash_repo" ] || continue
125
- if [ "$hash_repo" != "." ]; then
126
- path_uses_symlink_component "$ROOT_PHYS/${hash_repo#./}" && continue
127
- if [ ! -r "$hash_repo" ] || [ ! -x "$hash_repo" ]; then continue; fi
128
- fi
129
- git -C "$hash_repo" cat-file -t "$candidate_hash" >/dev/null 2>&1 && return 0
130
- done
131
- return 1
132
- }
133
-
134
- # Return 0=local/hash valid, 1=broken/unsafe local/hash, 2=remote (traceable
135
- # but unverified), 3=not a machine-reference token, 4=in-root symlink boundary,
136
- # 5=permission boundary. Callers keep 4/5 unverified rather than calling them
137
- # missing, valid, or contradictory.
138
- validate_reference() {
139
- ref="$1"; source_path="$2"; allow_source_relative="${3:-0}"
140
- case "$ref" in
141
- http://*|https://*) return 2 ;;
142
- esac
143
- if printf '%s' "$ref" | grep -Eq '^[0-9a-f]{7,40}$' \
144
- && printf '%s' "$ref" | grep -q '[a-f]' \
145
- && printf '%s' "$ref" | grep -q '[0-9]'; then
146
- resolve_hash "$ref" && return 0
147
- return 1
148
- fi
149
- ref="${ref%%#*}"; ref="${ref%%\?*}"
150
- ref="$(printf '%s' "$ref" | sed -E 's/:[0-9]+$//')"
151
- case "$ref" in
152
- ""|*$'\n'*|*$'\r'*|*$'\t'*|*"<"*|*"\\"*|*"*"*|*"?"*|*"["*|*"]"*|*"|"*) return 3 ;;
153
- /*) return 1 ;;
154
- ../*|*/../*|*/..|./*|*/./*)
155
- [ "$allow_source_relative" = "1" ] || return 1
156
- ;;
157
- esac
158
- [ "${#ref}" -le 240 ] || return 3
159
- case "$ref" in
160
- *.md|*.json|*.html|*.png|*.jpg|*.jpeg|*.svg|*.txt|*.log|*/*) ;;
161
- *) return 3 ;;
162
- esac
163
- reference_candidates=()
164
- case "$ref" in
165
- pm/*|contracts/*|design/*|scripts/*|tests/*|docs/*|example/*)
166
- reference_candidates+=("$ROOT_PHYS/$ref")
167
- ;;
168
- *)
169
- source_dir="$(dirname "$source_path")"
170
- [ "$source_dir" = "." ] && source_dir=""
171
- reference_candidates+=("$ROOT_PHYS/${source_dir:+$source_dir/}$ref")
172
- reference_candidates+=("$ROOT_PHYS/$ref")
173
- case "$ref" in */*) ;; *) reference_candidates+=("$ROOT_PHYS/contracts/$ref") ;; esac
174
- ;;
175
- esac
176
- for candidate in "${reference_candidates[@]}"; do
177
- [ -f "$candidate" ] || continue
178
- resolved="$(realpath "$candidate" 2>/dev/null)" || return 1
179
- case "$resolved" in
180
- "$ROOT_PHYS"/*) ;;
181
- *) return 1 ;;
182
- esac
183
- path_uses_symlink_component "$candidate" && return 4
184
- [ -r "$candidate" ] || return 5
185
- return 0
186
- done
187
- return 1
188
- }
189
-
190
- reference_display_path() {
191
- display_ref="$1"
192
- display_ref="${display_ref%%#*}"
193
- display_ref="${display_ref%%\?*}"
194
- display_ref="$(printf '%s' "$display_ref" | sed -E 's/:[0-9]+$//')"
195
- printf '%s\n' "$display_ref"
196
- }
197
-
198
- extract_backtick_tokens() {
199
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
200
- printf '%s\n' "$1" | grep -oE '`[^`]+`' 2>/dev/null | sed 's/^`//; s/`$//' || true
201
- }
202
-
203
- # Return 0 when a decision reference names one existing dated table row in the
204
- # canonical ledger. The explicit line keeps a passed Gate tied to one decision,
205
- # not merely to the existence of decisions.md.
206
- validate_decision_reference() {
207
- decision_ref="$1"
208
- if ! printf '%s\n' "$decision_ref" | grep -Eq '^pm/decisions\.md:[1-9][0-9]{0,6}$'; then
209
- return 1
210
- fi
211
- decision_line_n="${decision_ref##*:}"
212
- [ -f pm/decisions.md ] && [ ! -L pm/decisions.md ] && [ -r pm/decisions.md ] || return 1
213
- path_uses_symlink_component "$ROOT_PHYS/pm/decisions.md" && return 1
214
- decision_line="$(sed -n "${decision_line_n}p" pm/decisions.md 2>/dev/null || true)"
215
- printf '%s\n' "$decision_line" \
216
- | grep -Eq '^[[:space:]]*\|[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]*\|'
217
- }
218
-
219
- # Call only after validate_reference succeeds. Return 0=canonical local
220
- # evidence path, 1=local path outside pm/archive/<期>/evidence/, 2=hash/other.
221
- evidence_location_status() {
222
- evidence_ref="$1"
223
- case "$evidence_ref" in
224
- http://*|https://*) return 2 ;;
225
- esac
226
- if printf '%s' "$evidence_ref" | grep -Eq '^[0-9a-f]{7,40}$'; then
227
- return 2
228
- fi
229
- evidence_ref="${evidence_ref%%#*}"
230
- evidence_ref="${evidence_ref%%\?*}"
231
- evidence_ref="$(printf '%s' "$evidence_ref" | sed -E 's/:[0-9]+$//')"
232
- case "$evidence_ref" in
233
- *.md|*.json|*.html|*.png|*.jpg|*.jpeg|*.svg|*.txt|*.log|*/*) ;;
234
- *) return 2 ;;
235
- esac
236
- if printf '%s\n' "$evidence_ref" \
237
- | grep -Eq '^pm/archive/[^/]+/evidence/[^/].*$'; then
238
- return 0
239
- fi
240
- return 1
241
- }
242
-
243
- # Best-effort positive UI detection only. A miss is never proof that the
244
- # project has no UI; it simply means gate.na_inconsistent is not emitted.
245
- detect_ui_signal() {
246
- if [ -f standards/DESIGN.md ] && [ ! -L standards/DESIGN.md ]; then
247
- return 0
248
- fi
249
- ui_files="$BUS_TMP/ui-candidates"
250
- if ! find -P . \
251
- \( -type d \( -name .claude -o -name .codex -o -name .git -o -name .buildbeat -o -name .solobaton \
252
- -o -name .next -o -name .nuxt -o -name .output -o -name build -o -name coverage \
253
- -o -name dist -o -name node_modules -o -name target -o -name vendor \) -prune \) -o \
254
- \( -type f \( -name index.html -o -name package.json -o -name manifest.json \) \) -print0 \
255
- > "$ui_files" 2>/dev/null; then
256
- return 1
257
- fi
258
- ui_count=0
259
- while IFS= read -r -d '' ui_file; do
260
- ui_count=$((ui_count + 1))
261
- [ "$ui_count" -le 5000 ] || return 1
262
- case "${ui_file##*/}" in
263
- index.html) return 0 ;;
264
- package.json)
265
- if grep -Eq '"(@angular/core|@sveltejs/kit|next|nuxt|react|svelte|vite|vue)"[[:space:]]*:' "$ui_file" 2>/dev/null; then
266
- return 0
267
- fi
268
- ;;
269
- manifest.json)
270
- if grep -Eq '"manifest_version"[[:space:]]*:[[:space:]]*[23]' "$ui_file" 2>/dev/null \
271
- && grep -Eq '"(action|browser_action|content_scripts|options_page|options_ui|page_action|side_panel)"[[:space:]]*:' "$ui_file" 2>/dev/null; then
272
- return 0
273
- fi
274
- ;;
275
- esac
276
- done < "$ui_files"
277
- return 1
278
- }
279
-
280
- is_release_version_token() {
281
- case "$1" in
282
- *$'\n'*|*$'\r'*|*$'\t'*) return 1 ;;
283
- esac
284
- [ "${#1}" -le 100 ] || return 1
285
- printf '%s\n' "$1" \
286
- | grep -Eq '^[vV]?[0-9]+(\.[0-9]+){1,2}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
287
- }
288
-
289
- normalize_release_version() {
290
- normalized_version="$1"
291
- case "$normalized_version" in
292
- v*|V*) normalized_version="${normalized_version#?}" ;;
293
- esac
294
- printf '%s\n' "$normalized_version"
295
- }
296
-
297
- # The first non-Unreleased H2 heading is the only CHANGELOG version source.
298
- # Free-form production notes remain visible to people but are not guessed into
299
- # a version; projects can adopt a canonical release heading to close coverage.
300
- read_changelog_head_version() {
301
- changelog_path="$1"
302
- changelog_heading="$(awk '
303
- /^##[[:space:]]+/ {
304
- lower=tolower($0)
305
- if (lower !~ /unreleased/) { print; exit }
306
- }
307
- ' "$changelog_path" 2>/dev/null || true)"
308
- [ -n "$changelog_heading" ] || return 1
309
- changelog_version="$(printf '%s\n' "$changelog_heading" \
310
- | sed -E 's/^##[[:space:]]+//; s/[[:space:]].*$//; s/^\[//; s/\]$//')"
311
- is_release_version_token "$changelog_version" || return 1
312
- printf '%s\n' "$changelog_version"
313
- }
314
-
315
- # A mapped contract file must expose exactly one canonical snapshot line and a
316
- # single backticked release token. This validates linkage, not contract truth.
317
- read_contract_snapshot_version() {
318
- contract_path="$1"
319
- contract_lines="$(grep -E '契约快照对应版本' "$contract_path" 2>/dev/null || true)"
320
- contract_count="$(printf '%s\n' "$contract_lines" | awk 'NF { n++ } END { print n+0 }')"
321
- [ "$contract_count" -eq 1 ] || return 1
322
- contract_tokens="$(extract_backtick_tokens "$contract_lines")"
323
- contract_token_count="$(printf '%s\n' "$contract_tokens" | awk 'NF { n++ } END { print n+0 }')"
324
- [ "$contract_token_count" -eq 1 ] || return 1
325
- contract_version="$contract_tokens"
326
- [ -n "$contract_version" ] || return 1
327
- is_release_version_token "$contract_version" || return 1
328
- printf '%s\n' "$contract_version"
329
- }
330
-
331
- multirepo_map_value_safe() {
332
- map_value="$1"; map_value_max="$2"
333
- case "$map_value" in
334
- *$'\n'*|*$'\r'*|*$'\t'*) return 1 ;;
335
- esac
336
- [ -n "$map_value" ] && [ "${#map_value}" -le "$map_value_max" ] \
337
- && ! printf '%s' "$map_value" | LC_ALL=C grep -q '[[:cntrl:]]' \
338
- && ! printf '%s\n' "$map_value" | grep -Eq '[|`"\\]|<[^>]*>'
339
- }
340
-
341
- multirepo_repo_path_safe() {
342
- multirepo_map_value_safe "$1" 200 || return 1
343
- ! printf '%s\n' "$1" \
344
- | grep -Eq '(^/|/$|(^|/)\.\.?(/|$)|//)'
345
- }
346
-
347
- # Cross-check only explicitly joined sources. contracts/PROTOCOL.md owns the
348
- # expected repo inventory and source map; implicit directory/prose inference
349
- # would turn unrelated version domains into false drift.
350
- check_multirepo_drift() {
351
- multirepo_map_path="contracts/PROTOCOL.md"
352
- multirepo_map_raw="$BUS_TMP/multirepo.map.raw"
353
- multirepo_records="$BUS_TMP/multirepo.records"
354
- multirepo_expected="$BUS_TMP/multirepo.expected"
355
- multirepo_discovered="$BUS_TMP/multirepo.discovered"
356
- : > "$multirepo_map_raw"
357
- : > "$multirepo_records"
358
- : > "$multirepo_expected"
359
- : > "$multirepo_discovered"
360
-
361
- for multirepo_item in "${SUBREPOS[@]:-}"; do
362
- [ -n "$multirepo_item" ] || continue
363
- printf '%s\n' "${multirepo_item#./}" >> "$multirepo_discovered"
364
- done
365
- LC_ALL=C sort -u "$multirepo_discovered" -o "$multirepo_discovered"
366
-
367
- multirepo_map_real="$(realpath "$multirepo_map_path" 2>/dev/null || true)"
368
- multirepo_map_unsafe=0
369
- multirepo_map_boundary=""
370
- if [ -L "$multirepo_map_path" ] \
371
- || { [ -e "$multirepo_map_path" ] \
372
- && path_uses_symlink_component "$ROOT_PHYS/$multirepo_map_path"; }; then
373
- multirepo_map_unsafe=1
374
- multirepo_map_boundary="symlink"
375
- elif [ -e "$multirepo_map_path" ] && [ ! -r "$multirepo_map_path" ]; then
376
- multirepo_map_unsafe=1
377
- multirepo_map_boundary="permission"
378
- elif [ ! -f "$multirepo_map_path" ] || [ -z "$multirepo_map_real" ]; then
379
- multirepo_map_unsafe=1
380
- else
381
- case "$multirepo_map_real" in
382
- "$ROOT_PHYS"/*) ;;
383
- *) multirepo_map_unsafe=1; multirepo_map_boundary="symlink" ;;
384
- esac
385
- fi
386
- if [ "$multirepo_map_unsafe" -eq 1 ]; then
387
- if [ -n "$multirepo_map_boundary" ]; then
388
- echo " ⚠️ $multirepo_map_path 因 $multirepo_map_boundary 边界未读取"
389
- add_scan_boundary "$multirepo_map_boundary" "$multirepo_map_path" \
390
- "the multi-repository map was not read"
391
- fi
392
- if [ ! -s "$multirepo_discovered" ]; then
393
- echo " · 未发现子仓;多仓漂移检查不适用"
394
- return
395
- fi
396
- while IFS= read -r multirepo_repo; do
397
- echo " ⚠️ $multirepo_repo 未在可读契约 map 中登记 —— 多仓版本关系未核验"
398
- add_finding "sync.unverified" "unverified" "Discovered repo=$multirepo_repo has no readable buildbeat-multirepo-map:v1 source." "$multirepo_repo"
399
- done < "$multirepo_discovered"
400
- return
401
- fi
402
-
403
- multirepo_marker_count="$(grep -c '^<!-- buildbeat-multirepo-map:v1$' "$multirepo_map_path" 2>/dev/null || true)"
404
- if [ "$multirepo_marker_count" -eq 0 ]; then
405
- if [ ! -s "$multirepo_discovered" ]; then
406
- echo " · 未发现子仓;未配置多仓 map 不产生 finding"
407
- return
408
- fi
409
- while IFS= read -r multirepo_repo; do
410
- echo " ⚠️ $multirepo_repo 未在 buildbeat-multirepo-map:v1 登记"
411
- add_finding "sync.unverified" "unverified" "Discovered repo=$multirepo_repo is absent from buildbeat-multirepo-map:v1." "$multirepo_repo"
412
- done < "$multirepo_discovered"
413
- return
414
- fi
415
-
416
- multirepo_map_invalid=0
417
- if ! awk '
418
- $0 == "<!-- buildbeat-multirepo-map:v1" {
419
- starts++
420
- if (inside) bad=1
421
- inside=1
422
- next
423
- }
424
- inside && $0 == "-->" {
425
- closes++
426
- inside=0
427
- next
428
- }
429
- inside { print }
430
- END {
431
- if (starts != 1 || closes != 1 || inside || bad) exit 2
432
- }
433
- ' "$multirepo_map_path" > "$multirepo_map_raw"; then
434
- multirepo_map_invalid=1
435
- fi
436
-
437
- while IFS= read -r multirepo_line || [ -n "$multirepo_line" ]; do
438
- multirepo_line="${multirepo_line%$'\r'}"
439
- [ -n "$multirepo_line" ] || continue
440
- if ! printf '%s\n' "$multirepo_line" \
441
- | grep -Eq '^repo=[^|]+\|contract=[^|]+\|deployment=[^|]+(\|changelog=[^|]+)?$'; then
442
- multirepo_map_invalid=1
443
- continue
444
- fi
445
- IFS='|' read -r multirepo_repo_field multirepo_contract_field multirepo_deployment_field multirepo_changelog_field <<< "$multirepo_line"
446
- multirepo_repo="${multirepo_repo_field#repo=}"
447
- multirepo_contract="${multirepo_contract_field#contract=}"
448
- multirepo_deployment="${multirepo_deployment_field#deployment=}"
449
- # 可选第 4 字段 changelog=<repo 内的 CHANGELOG 路径>:多模块仓没有根 CHANGELOG 时,
450
- # 由 map 显式指定承载契约版本的模块 CHANGELOG;缺省仍为 <repo>/CHANGELOG.md。
451
- multirepo_changelog_override="${multirepo_changelog_field#changelog=}"
452
- [ -n "$multirepo_changelog_field" ] || multirepo_changelog_override=""
453
- multirepo_repo_trimmed="$(printf '%s' "$multirepo_repo" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
454
- multirepo_contract_trimmed="$(printf '%s' "$multirepo_contract" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
455
- multirepo_deployment_trimmed="$(printf '%s' "$multirepo_deployment" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
456
- if [ "$multirepo_repo" != "$multirepo_repo_trimmed" ] \
457
- || [ "$multirepo_contract" != "$multirepo_contract_trimmed" ] \
458
- || [ "$multirepo_deployment" != "$multirepo_deployment_trimmed" ] \
459
- || ! multirepo_repo_path_safe "$multirepo_repo" \
460
- || ! multirepo_map_value_safe "$multirepo_contract" 240 \
461
- || ! { [ "$multirepo_contract" = "n/a" ] \
462
- || printf '%s\n' "$multirepo_contract" | grep -Eq '^contracts/[^/].*\.md$'; } \
463
- || ! multirepo_map_value_safe "$multirepo_deployment" 100; then
464
- multirepo_map_invalid=1
465
- continue
466
- fi
467
- # contract=n/a 表示该仓没有契约版本域(如只读存量前端、npm 包 semver 与契约版本不同域),
468
- # 只登记入 inventory、不做版本核对;不得用它掩盖真实存在的契约版本关系。
469
- if [ -n "$multirepo_changelog_override" ]; then
470
- if ! multirepo_repo_path_safe "$multirepo_changelog_override" \
471
- || [ "${#multirepo_changelog_override}" -gt 240 ] \
472
- || ! printf '%s\n' "$multirepo_changelog_override" | grep -Eq "^$(printf '%s' "$multirepo_repo" | sed 's/[.[\*^$]/\\&/g')/.+/CHANGELOG\.md$"; then
473
- multirepo_map_invalid=1
474
- continue
475
- fi
476
- fi
477
- printf '%s\t%s\t%s\t%s\n' "$multirepo_repo" "$multirepo_contract" "$multirepo_deployment" "$multirepo_changelog_override" >> "$multirepo_records"
478
- printf '%s\n' "$multirepo_repo" >> "$multirepo_expected"
479
- done < "$multirepo_map_raw"
480
-
481
- multirepo_record_count="$(awk 'NF { n++ } END { print n+0 }' "$multirepo_records")"
482
- multirepo_duplicate="$(LC_ALL=C sort "$multirepo_expected" | uniq -d | head -1)"
483
- if [ "$multirepo_record_count" -eq 0 ] || [ -n "$multirepo_duplicate" ]; then
484
- multirepo_map_invalid=1
485
- fi
486
- LC_ALL=C sort -u "$multirepo_expected" -o "$multirepo_expected"
487
-
488
- if [ "$multirepo_map_invalid" -eq 1 ]; then
489
- echo " ⚠️ $multirepo_map_path 的 buildbeat-multirepo-map:v1 缺失、重复或格式无效"
490
- add_finding "sync.unverified" "unverified" "The buildbeat-multirepo-map:v1 source is missing, duplicated, empty, or malformed." "$multirepo_map_path"
491
- while IFS= read -r multirepo_repo; do
492
- add_finding "sync.unverified" "unverified" "Discovered repo=$multirepo_repo could not be joined because the multi-repo map is invalid." "$multirepo_repo"
493
- done < "$multirepo_discovered"
494
- return
495
- fi
496
-
497
- while IFS= read -r multirepo_repo; do
498
- grep -Fxq "$multirepo_repo" "$multirepo_expected" && continue
499
- echo " ⚠️ $multirepo_repo 已发现但未在 buildbeat-multirepo-map:v1 登记"
500
- add_finding "sync.unverified" "unverified" "Discovered repo=$multirepo_repo is absent from buildbeat-multirepo-map:v1." "$multirepo_repo"
501
- done < "$multirepo_discovered"
502
-
503
- while IFS=$'\t' read -r multirepo_repo multirepo_contract multirepo_deployment multirepo_changelog_override; do
504
- [ -n "$multirepo_repo" ] || continue
505
- multirepo_changelog="${multirepo_changelog_override:-$multirepo_repo/CHANGELOG.md}"
506
- multirepo_issue=0
507
- multirepo_drift=0
508
- multirepo_changelog_ok=0
509
- multirepo_contract_ok=0
510
- multirepo_deployment_ok=0
511
- multirepo_changelog_version=""
512
- multirepo_contract_version=""
513
- multirepo_deployment_version=""
514
-
515
- if ! grep -Fxq "$multirepo_repo" "$multirepo_discovered"; then
516
- if [ -L "$multirepo_repo" ] \
517
- || { [ -e "$multirepo_repo" ] \
518
- && path_uses_symlink_component "$ROOT_PHYS/$multirepo_repo"; }; then
519
- echo " ⚠️ $multirepo_repo 在 map 中登记,但经 symlink 到达"
520
- add_scan_boundary "symlink" "$multirepo_repo" \
521
- "the discovered repository was not traversed"
522
- elif [ -d "$multirepo_repo" ] \
523
- && { [ ! -r "$multirepo_repo" ] || [ ! -x "$multirepo_repo" ]; }; then
524
- echo " ⚠️ $multirepo_repo 在 map 中登记,但当前权限不足"
525
- add_scan_boundary "permission" "$multirepo_repo" \
526
- "the discovered repository was not readable and searchable"
527
- else
528
- echo " ⚠️ $multirepo_repo 在 map 中登记,但未被既有 SUBREPOS 深度发现"
529
- add_finding "sync.unverified" "unverified" "Expected repo=$multirepo_repo was not observed by the bounded SUBREPOS discovery." "$multirepo_repo"
530
- fi
531
- continue
532
- fi
533
-
534
- multirepo_repo_real="$(realpath "$multirepo_repo" 2>/dev/null || true)"
535
- if [ -L "$multirepo_repo" ] \
536
- || path_uses_symlink_component "$ROOT_PHYS/$multirepo_repo"; then
537
- echo " ⚠️ $multirepo_repo 经 symlink 到达,未跟随扫描"
538
- add_scan_boundary "symlink" "$multirepo_repo" \
539
- "the discovered repository was not traversed"
540
- continue
541
- fi
542
- if [ -z "$multirepo_repo_real" ]; then
543
- echo " ⚠️ $multirepo_repo 不是可安全扫描的 regular 子仓目录"
544
- add_finding "sync.unverified" "unverified" "Expected repo=$multirepo_repo cannot be resolved safely." "$multirepo_repo"
545
- continue
546
- fi
547
- if [ ! -r "$multirepo_repo" ] || [ ! -x "$multirepo_repo" ]; then
548
- echo " ⚠️ $multirepo_repo 当前权限不足,未遍历"
549
- add_scan_boundary "permission" "$multirepo_repo" \
550
- "the discovered repository was not readable and searchable"
551
- continue
552
- fi
553
- case "$multirepo_repo_real" in
554
- "$ROOT_PHYS"/*) ;;
555
- *)
556
- echo " ⚠️ $multirepo_repo 解析到协调根之外"
557
- add_finding "sync.unverified" "unverified" "Expected repo=$multirepo_repo resolves outside the coordination root." "$multirepo_repo"
558
- continue
559
- ;;
560
- esac
561
-
562
- if [ "$multirepo_contract" = "n/a" ] && [ "$multirepo_deployment" = "n/a" ]; then
563
- echo " · $multirepo_repo 已登记,无契约/部署版本域(contract=n/a, deployment=n/a),不做版本核对"
564
- continue
565
- fi
566
- if [ -L "$multirepo_changelog" ] \
567
- || { [ -e "$multirepo_changelog" ] \
568
- && path_uses_symlink_component "$ROOT_PHYS/$multirepo_changelog"; }; then
569
- echo " ⚠️ $multirepo_changelog 经 symlink 到达,未读取"
570
- add_scan_boundary "symlink" "$multirepo_changelog" \
571
- "the mapped CHANGELOG version source was not read"
572
- multirepo_issue=1
573
- elif [ -e "$multirepo_changelog" ] && [ ! -r "$multirepo_changelog" ]; then
574
- echo " ⚠️ $multirepo_changelog 当前权限不足,未读取"
575
- add_scan_boundary "permission" "$multirepo_changelog" \
576
- "the mapped CHANGELOG version source was not readable"
577
- multirepo_issue=1
578
- elif [ ! -f "$multirepo_changelog" ]; then
579
- echo " ⚠️ $multirepo_repo 缺少可读 regular $multirepo_changelog"
580
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo has no readable regular CHANGELOG version source: $multirepo_changelog." "$multirepo_changelog"
581
- multirepo_issue=1
582
- else
583
- multirepo_changelog_version="$(read_changelog_head_version "$multirepo_changelog" || true)"
584
- if [ -z "$multirepo_changelog_version" ]; then
585
- echo " ⚠️ $multirepo_changelog 的首个已发布 H2 不是可核对版本"
586
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo CHANGELOG head has no parseable released version token." "$multirepo_changelog"
587
- multirepo_issue=1
588
- else
589
- multirepo_changelog_ok=1
590
- fi
591
- fi
592
-
593
- multirepo_contract_rc=0
594
- if [ "$multirepo_contract" = "n/a" ]; then
595
- multirepo_contract_rc=0
596
- else
597
- validate_reference "$multirepo_contract" "$multirepo_map_path" || multirepo_contract_rc=$?
598
- fi
599
- if [ "$multirepo_contract" = "n/a" ]; then
600
- :
601
- elif [ "$multirepo_contract_rc" -ne 0 ]; then
602
- echo " ⚠️ $multirepo_repo 的契约版本来源不可读:$multirepo_contract"
603
- if [ "$multirepo_contract_rc" -eq 4 ]; then
604
- add_scan_boundary "symlink" "$multirepo_contract" \
605
- "the mapped contract version source was not read"
606
- elif [ "$multirepo_contract_rc" -eq 5 ]; then
607
- add_scan_boundary "permission" "$multirepo_contract" \
608
- "the mapped contract version source was not readable"
609
- else
610
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo contract version source is not a readable in-root file: $multirepo_contract." "$multirepo_contract"
611
- fi
612
- multirepo_issue=1
613
- else
614
- multirepo_contract_version="$(read_contract_snapshot_version "$multirepo_contract" || true)"
615
- if [ -z "$multirepo_contract_version" ]; then
616
- echo " ⚠️ $multirepo_contract 缺少唯一可解析的契约快照版本"
617
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo contract source has no unique parseable snapshot version." "$multirepo_contract"
618
- multirepo_issue=1
619
- else
620
- multirepo_contract_ok=1
621
- fi
622
- fi
623
-
624
- multirepo_baseline="$SDIR/bus-baseline.json"
625
- if [ "$multirepo_deployment" != "n/a" ]; then
626
- multirepo_baseline_real="$(realpath "$multirepo_baseline" 2>/dev/null || true)"
627
- if ! command -v jq >/dev/null 2>&1; then
628
- echo " ⚠️ $multirepo_repo 的部署基线需 jq 才能核对"
629
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo deployment source requires jq: $multirepo_baseline#apps.$multirepo_deployment.imageTag." "$multirepo_baseline"
630
- multirepo_issue=1
631
- elif [ -L "$multirepo_baseline" ] \
632
- || { [ -e "$multirepo_baseline" ] \
633
- && path_uses_symlink_component "$ROOT_PHYS/$multirepo_baseline"; }; then
634
- echo " ⚠️ $multirepo_repo 的部署基线经 symlink 到达,未读取"
635
- add_scan_boundary "symlink" "$multirepo_baseline" \
636
- "the deployment baseline was not read"
637
- multirepo_issue=1
638
- elif [ -e "$multirepo_baseline" ] && [ ! -r "$multirepo_baseline" ]; then
639
- echo " ⚠️ $multirepo_repo 的部署基线当前权限不足,未读取"
640
- add_scan_boundary "permission" "$multirepo_baseline" \
641
- "the deployment baseline was not readable"
642
- multirepo_issue=1
643
- elif [ ! -f "$multirepo_baseline" ] || [ -z "$multirepo_baseline_real" ]; then
644
- echo " ⚠️ $multirepo_repo 的部署基线不可读:$multirepo_baseline"
645
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo deployment baseline is missing or not a regular file: $multirepo_baseline." "$multirepo_baseline"
646
- multirepo_issue=1
647
- else
648
- case "$multirepo_baseline_real" in
649
- "$ROOT_PHYS"/*)
650
- multirepo_deployment_version="$(jq -er --arg app "$multirepo_deployment" \
651
- '.apps[$app].imageTag | select(type == "string" and length > 0)' \
652
- "$multirepo_baseline" 2>/dev/null || true)"
653
- if [ -z "$multirepo_deployment_version" ] || ! is_release_version_token "$multirepo_deployment_version"; then
654
- echo " ⚠️ $multirepo_baseline#apps.$multirepo_deployment.imageTag 缺少可核对版本"
655
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo deployment baseline has no parseable imageTag at apps.$multirepo_deployment." "$multirepo_baseline"
656
- multirepo_issue=1
657
- else
658
- multirepo_deployment_ok=1
659
- fi
660
- ;;
661
- *)
662
- echo " ⚠️ $multirepo_repo 的部署基线解析到协调根之外"
663
- add_finding "sync.unverified" "unverified" "Repo=$multirepo_repo deployment baseline resolves outside the coordination root." "$multirepo_baseline"
664
- multirepo_issue=1
665
- ;;
666
- esac
667
- fi
668
- fi
669
-
670
- if [ "$multirepo_changelog_ok" -eq 1 ] && [ "$multirepo_contract_ok" -eq 1 ]; then
671
- multirepo_changelog_normalized="$(normalize_release_version "$multirepo_changelog_version")"
672
- multirepo_contract_normalized="$(normalize_release_version "$multirepo_contract_version")"
673
- [ "$multirepo_changelog_normalized" = "$multirepo_contract_normalized" ] || multirepo_drift=1
674
- fi
675
- if [ "$multirepo_deployment" != "n/a" ] \
676
- && [ "$multirepo_deployment_ok" -eq 1 ] \
677
- && [ "$multirepo_changelog_ok" -eq 1 ]; then
678
- multirepo_deployment_normalized="$(normalize_release_version "$multirepo_deployment_version")"
679
- multirepo_changelog_normalized="$(normalize_release_version "$multirepo_changelog_version")"
680
- [ "$multirepo_deployment_normalized" = "$multirepo_changelog_normalized" ] || multirepo_drift=1
681
- fi
682
- if [ "$multirepo_deployment" != "n/a" ] \
683
- && [ "$multirepo_deployment_ok" -eq 1 ] \
684
- && [ "$multirepo_contract_ok" -eq 1 ]; then
685
- multirepo_deployment_normalized="$(normalize_release_version "$multirepo_deployment_version")"
686
- multirepo_contract_normalized="$(normalize_release_version "$multirepo_contract_version")"
687
- [ "$multirepo_deployment_normalized" = "$multirepo_contract_normalized" ] || multirepo_drift=1
688
- fi
689
-
690
- if [ "$multirepo_drift" -eq 1 ]; then
691
- multirepo_changelog_fact="${multirepo_changelog_version:-unverified}"
692
- multirepo_contract_fact="${multirepo_contract_version:-unverified}"
693
- multirepo_deployment_fact="n/a"
694
- [ "$multirepo_deployment" = "n/a" ] \
695
- || multirepo_deployment_fact="$multirepo_baseline#apps.$multirepo_deployment.imageTag=${multirepo_deployment_version:-unverified}"
696
- echo " ⚠️ $multirepo_repo 版本漂移: $multirepo_changelog=$multirepo_changelog_fact ↔ $multirepo_contract=$multirepo_contract_fact ↔ $multirepo_deployment_fact"
697
- add_finding "sync.multirepo_drift" "conflict" "Version sources disagree for repo=$multirepo_repo: $multirepo_changelog=$multirepo_changelog_fact; $multirepo_contract=$multirepo_contract_fact; $multirepo_deployment_fact." "$multirepo_changelog"
698
- elif [ "$multirepo_issue" -eq 0 ]; then
699
- if [ "$multirepo_deployment" = "n/a" ]; then
700
- echo " ✅ $multirepo_repo 多仓版本一致: $multirepo_changelog ↔ $multirepo_contract (deployment=n/a)"
701
- else
702
- echo " ✅ $multirepo_repo 多仓版本一致: $multirepo_changelog ↔ $multirepo_contract ↔ $multirepo_baseline#apps.$multirepo_deployment.imageTag"
703
- fi
704
- fi
705
- done < "$multirepo_records"
706
- }
707
-
708
- check_stack_drift() {
709
- stack_path="standards/STACK.md"
710
- stack_baseline_raw="$BUS_TMP/stack.baseline.raw"
711
- stack_expected_node="$BUS_TMP/stack.expected.node"
712
- stack_expected_lockfile="$BUS_TMP/stack.expected.lockfile"
713
- stack_expected_docker="$BUS_TMP/stack.expected.docker"
714
- stack_actual_node="$BUS_TMP/stack.actual.node"
715
- stack_actual_lockfile="$BUS_TMP/stack.actual.lockfile"
716
- stack_actual_docker="$BUS_TMP/stack.actual.docker"
717
- stack_files="$BUS_TMP/stack.files"
718
- stack_find_errors="$BUS_TMP/stack.find-errors"
719
- : > "$stack_expected_node"
720
- : > "$stack_expected_lockfile"
721
- : > "$stack_expected_docker"
722
- : > "$stack_actual_node"
723
- : > "$stack_actual_lockfile"
724
- : > "$stack_actual_docker"
725
- : > "$stack_files"
726
- : > "$stack_find_errors"
727
-
728
- # The baseline is deliberately a tiny line protocol rather than inferred
729
- # prose. Repeated keys form sets; n/a is valid only as the sole value.
730
- if ! awk '
731
- $0 == "<!-- buildbeat-stack-baseline:v1" || $0 == "<!-- solobaton-stack-baseline:v1" {
732
- starts++
733
- if (inside) bad=1
734
- inside=1
735
- next
736
- }
737
- inside && $0 == "-->" {
738
- closes++
739
- inside=0
740
- next
741
- }
742
- inside { print }
743
- END {
744
- if (starts != 1 || closes != 1 || inside || bad) exit 2
745
- }
746
- ' "$stack_path" > "$stack_baseline_raw"; then
747
- echo " ⚠️ $stack_path 缺少唯一可解析的 buildbeat-stack-baseline:v1 基线块(兼容旧 solobaton 标记)"
748
- add_finding "stack.unverified" "unverified" "Confirmed STACK.md has no unique parseable v1 observable baseline." "$stack_path"
749
- return
750
- fi
751
-
752
- stack_baseline_invalid=0
753
- while IFS= read -r stack_line || [ -n "$stack_line" ]; do
754
- stack_line="${stack_line%$'\r'}"
755
- [ -n "$stack_line" ] || continue
756
- case "$stack_line" in
757
- nodeConstraint=*) stack_value="${stack_line#nodeConstraint=}"; stack_value_file="$stack_expected_node" ;;
758
- lockfileKind=*) stack_value="${stack_line#lockfileKind=}"; stack_value_file="$stack_expected_lockfile" ;;
759
- dockerFromImage=*) stack_value="${stack_line#dockerFromImage=}"; stack_value_file="$stack_expected_docker" ;;
760
- *) stack_baseline_invalid=1; continue ;;
761
- esac
762
- stack_trimmed="$(printf '%s' "$stack_value" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
763
- case "$stack_value" in *$'\t'*) stack_baseline_invalid=1; continue ;; esac
764
- if [ -z "$stack_value" ] || [ "$stack_value" != "$stack_trimmed" ] \
765
- || [ "${#stack_value}" -gt 200 ] \
766
- || printf '%s' "$stack_value" | grep -Eq '<[^>]+>'; then
767
- stack_baseline_invalid=1
768
- continue
769
- fi
770
- printf '%s\n' "$stack_value" >> "$stack_value_file"
771
- done < "$stack_baseline_raw"
772
-
773
- for stack_expected_file in "$stack_expected_node" "$stack_expected_lockfile" "$stack_expected_docker"; do
774
- LC_ALL=C sort -u "$stack_expected_file" -o "$stack_expected_file"
775
- stack_expected_count="$(awk 'NF { n++ } END { print n+0 }' "$stack_expected_file")"
776
- if [ "$stack_expected_count" -eq 0 ]; then
777
- stack_baseline_invalid=1
778
- elif grep -Fxq 'n/a' "$stack_expected_file" && [ "$stack_expected_count" -ne 1 ]; then
779
- stack_baseline_invalid=1
780
- fi
781
- done
782
- if [ "$stack_baseline_invalid" -eq 1 ]; then
783
- echo " ⚠️ $stack_path 的 v1 可核对基线键值不完整或不唯一"
784
- add_finding "stack.unverified" "unverified" "Confirmed STACK.md has an incomplete or ambiguous v1 observable baseline." "$stack_path"
785
- return
786
- fi
787
-
788
- stack_global_issue=0
789
- stack_node_issue=0
790
- stack_lockfile_issue=0
791
- stack_docker_issue=0
792
- stack_find_failed=0
793
- stack_limit_reached=0
794
- stack_limit="${BUS_STACK_MAX:-200}"
795
- case "$stack_limit" in
796
- ''|*[!0-9]*) stack_limit=200; stack_global_issue=1 ;;
797
- *) [ "$stack_limit" -ge 1 ] || { stack_limit=200; stack_global_issue=1; } ;;
798
- esac
799
-
800
- if ! find -P . \
801
- \( -type d \( -name .claude -o -name .codex -o -name .git -o -name .buildbeat -o -name .solobaton \
802
- -o -name .next -o -name .nuxt -o -name .output -o -name build -o -name coverage \
803
- -o -name dist -o -name node_modules -o -name target -o -name vendor \) -prune \) -o \
804
- \( -type l -o \( -type f \( -name .nvmrc -o -name package.json \
805
- -o -name package-lock.json -o -name npm-shrinkwrap.json -o -name pnpm-lock.yaml \
806
- -o -name yarn.lock -o -name bun.lock -o -name bun.lockb \
807
- -o -name Dockerfile -o -name 'Dockerfile.*' \) \) \) -print0 \
808
- > "$stack_files" 2> "$stack_find_errors"; then
809
- stack_global_issue=1
810
- stack_find_failed=1
811
- fi
812
- if [ -s "$stack_find_errors" ]; then
813
- stack_global_issue=1
814
- stack_find_failed=1
815
- fi
816
-
817
- stack_scan_count=0
818
- while IFS= read -r -d '' stack_file; do
819
- stack_scan_count=$((stack_scan_count + 1))
820
- if [ "$stack_scan_count" -gt "$stack_limit" ]; then
821
- stack_global_issue=1
822
- stack_limit_reached=1
823
- break
824
- fi
825
- stack_base="${stack_file##*/}"
826
-
827
- if [ -L "$stack_file" ]; then
828
- case "$stack_base" in
829
- .claude|.codex|.git|.buildbeat|.solobaton|.next|.nuxt|.output|build|coverage|dist|node_modules|target|vendor)
830
- continue
831
- ;;
832
- esac
833
- if [ -d "$stack_file" ]; then
834
- stack_global_issue=1
835
- add_scan_boundary "symlink" "${stack_file#./}" \
836
- "a directory in the STACK observation scope was not traversed"
837
- else
838
- case "$stack_base" in
839
- .nvmrc|package.json)
840
- stack_node_issue=1
841
- add_scan_boundary "symlink" "${stack_file#./}" \
842
- "a Node version source was not read"
843
- ;;
844
- package-lock.json|npm-shrinkwrap.json|pnpm-lock.yaml|yarn.lock|bun.lock|bun.lockb)
845
- stack_lockfile_issue=1
846
- add_scan_boundary "symlink" "${stack_file#./}" \
847
- "a lockfile source was not read"
848
- ;;
849
- Dockerfile|Dockerfile.*)
850
- stack_docker_issue=1
851
- add_scan_boundary "symlink" "${stack_file#./}" \
852
- "a Docker FROM source was not read"
853
- ;;
854
- esac
855
- fi
856
- continue
857
- fi
858
-
859
- case "$stack_base" in
860
- .nvmrc)
861
- if [ ! -r "$stack_file" ]; then
862
- stack_node_issue=1
863
- add_scan_boundary "permission" "${stack_file#./}" \
864
- "a Node version source was not readable"
865
- continue
866
- fi
867
- stack_nvmrc_count="$(awk 'NF { n++ } END { print n+0 }' "$stack_file" 2>/dev/null || echo 0)"
868
- if [ "$stack_nvmrc_count" -ne 1 ]; then
869
- stack_node_issue=1
870
- continue
871
- fi
872
- stack_value="$(awk 'NF { sub(/\r$/, ""); sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, ""); print; exit }' "$stack_file" 2>/dev/null || true)"
873
- if [ -n "$stack_value" ]; then printf '%s\n' "$stack_value" >> "$stack_actual_node"
874
- else stack_node_issue=1
875
- fi
876
- ;;
877
- package.json)
878
- if [ ! -r "$stack_file" ] || ! command -v python3 >/dev/null 2>&1; then
879
- stack_node_issue=1
880
- if [ ! -r "$stack_file" ]; then
881
- add_scan_boundary "permission" "${stack_file#./}" \
882
- "a package manifest was not readable"
883
- fi
884
- continue
885
- fi
886
- stack_package_status=0
887
- stack_value="$(python3 - "$stack_file" <<'PY'
888
- import json
889
- import sys
890
-
891
- try:
892
- with open(sys.argv[1], encoding="utf-8") as handle:
893
- package = json.load(handle)
894
- except (OSError, UnicodeError, json.JSONDecodeError):
895
- raise SystemExit(2)
896
-
897
- engines = package.get("engines")
898
- if engines is None:
899
- raise SystemExit(0)
900
- if not isinstance(engines, dict):
901
- raise SystemExit(2)
902
- value = engines.get("node")
903
- if value is None:
904
- raise SystemExit(0)
905
- if not isinstance(value, str) or not value.strip() or "\n" in value or "\r" in value:
906
- raise SystemExit(2)
907
- print(value.strip())
908
- PY
909
- )" || stack_package_status=$?
910
- if [ "$stack_package_status" -ne 0 ]; then
911
- stack_node_issue=1
912
- elif [ -n "$stack_value" ]; then
913
- printf '%s\n' "$stack_value" >> "$stack_actual_node"
914
- fi
915
- ;;
916
- package-lock.json|npm-shrinkwrap.json|pnpm-lock.yaml|yarn.lock|bun.lock|bun.lockb)
917
- if [ -r "$stack_file" ]; then printf '%s\n' "$stack_base" >> "$stack_actual_lockfile"
918
- else
919
- stack_lockfile_issue=1
920
- add_scan_boundary "permission" "${stack_file#./}" \
921
- "a lockfile source was not readable"
922
- fi
923
- ;;
924
- Dockerfile|Dockerfile.*)
925
- if [ ! -r "$stack_file" ]; then
926
- stack_docker_issue=1
927
- add_scan_boundary "permission" "${stack_file#./}" \
928
- "a Docker FROM source was not readable"
929
- continue
930
- fi
931
- stack_from_values="$(awk '
932
- /^[[:space:]]*[Ff][Rr][Oo][Mm][[:space:]]+/ {
933
- line=$0
934
- sub(/^[[:space:]]*[Ff][Rr][Oo][Mm][[:space:]]+/, "", line)
935
- sub(/^--platform=[^[:space:]]+[[:space:]]+/, "", line)
936
- split(line, fields, /[[:space:]]+/)
937
- print fields[1]
938
- }
939
- ' "$stack_file" 2>/dev/null || true)"
940
- if [ -z "$stack_from_values" ]; then
941
- stack_docker_issue=1
942
- continue
943
- fi
944
- while IFS= read -r stack_value; do
945
- case "$stack_value" in
946
- ''|*'$'*) stack_docker_issue=1 ;;
947
- *)
948
- if printf '%s' "$stack_value" | grep -Fq "\\"; then stack_docker_issue=1
949
- else printf '%s\n' "$stack_value" >> "$stack_actual_docker"
950
- fi
951
- ;;
952
- esac
953
- done <<< "$stack_from_values"
954
- ;;
955
- esac
956
- done < "$stack_files"
957
-
958
- if [ "$stack_find_failed" -eq 1 ]; then
959
- add_scan_boundary "permission" "." \
960
- "filesystem traversal for STACK observations returned an unreadable or I/O boundary"
961
- fi
962
- if [ "$stack_limit_reached" -eq 1 ]; then
963
- add_scan_boundary "limit" "." \
964
- "the STACK observation scan stopped at BUS_STACK_MAX=$stack_limit"
965
- fi
966
-
967
- LC_ALL=C sort -u "$stack_actual_node" -o "$stack_actual_node"
968
- LC_ALL=C sort -u "$stack_actual_lockfile" -o "$stack_actual_lockfile"
969
- LC_ALL=C sort -u "$stack_actual_docker" -o "$stack_actual_docker"
970
-
971
- stack_drift_dims=""
972
- stack_unverified_dims=""
973
- for stack_dimension in node lockfile docker; do
974
- case "$stack_dimension" in
975
- node) stack_label="Node constraints"; stack_expected="$stack_expected_node"; stack_actual="$stack_actual_node"; stack_issue="$stack_node_issue" ;;
976
- lockfile) stack_label="lockfile kinds"; stack_expected="$stack_expected_lockfile"; stack_actual="$stack_actual_lockfile"; stack_issue="$stack_lockfile_issue" ;;
977
- docker) stack_label="Docker FROM images"; stack_expected="$stack_expected_docker"; stack_actual="$stack_actual_docker"; stack_issue="$stack_docker_issue" ;;
978
- esac
979
- [ "$stack_global_issue" -eq 0 ] || stack_issue=1
980
-
981
- stack_dimension_drift=0
982
- stack_dimension_unverified=0
983
- if [ -s "$stack_actual" ]; then
984
- cmp -s "$stack_expected" "$stack_actual" || stack_dimension_drift=1
985
- [ "$stack_issue" -eq 0 ] || stack_dimension_unverified=1
986
- elif grep -Fxq 'n/a' "$stack_expected"; then
987
- [ "$stack_issue" -eq 0 ] || stack_dimension_unverified=1
988
- else
989
- stack_dimension_unverified=1
990
- fi
991
-
992
- if [ "$stack_dimension_drift" -eq 1 ]; then
993
- [ -z "$stack_drift_dims" ] || stack_drift_dims="$stack_drift_dims, "
994
- stack_drift_dims="$stack_drift_dims$stack_label"
995
- fi
996
- if [ "$stack_dimension_unverified" -eq 1 ]; then
997
- [ -z "$stack_unverified_dims" ] || stack_unverified_dims="$stack_unverified_dims, "
998
- stack_unverified_dims="$stack_unverified_dims$stack_label"
999
- fi
1000
- done
1001
-
1002
- if [ -n "$stack_drift_dims" ]; then
1003
- echo " ❌ $stack_path 与已扫描仓库事实冲突(${stack_drift_dims})"
1004
- add_finding "stack.drift" "conflict" "Declared STACK baseline conflicts with observed repository facts for: $stack_drift_dims." "$stack_path"
1005
- fi
1006
- if [ -n "$stack_unverified_dims" ]; then
1007
- echo " ⚠️ $stack_path 的漂移检查未完整覆盖(${stack_unverified_dims})"
1008
- add_finding "stack.unverified" "unverified" "STACK drift coverage is incomplete for: $stack_unverified_dims." "$stack_path"
1009
- fi
1010
- if [ -z "$stack_drift_dims" ] && [ -z "$stack_unverified_dims" ]; then
1011
- echo " ✅ $stack_path 可核对基线与已扫描仓库事实一致"
1012
- fi
1013
- }
1014
-
1015
- finalize_findings() {
1016
- tab="$(printf '\t')"
1017
- LC_ALL=C sort -t "$tab" -k1,1n -k5,5 -k2,2 "$FINDINGS_RAW" \
1018
- | awk '!seen[$0]++' > "$FINDINGS_SORTED"
1019
- }
1020
-
1021
- render_json_report() {
1022
- awk -F '\t' -v strict="$STRICT" '
1023
- function esc(s) {
1024
- gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s)
1025
- gsub(/\r/, "\\r", s); gsub(/\t/, "\\t", s)
1026
- return s
1027
- }
1028
- {
1029
- n++; code[n]=$2; level[n]=$3; message[n]=$4; path[n]=$5
1030
- count[$3]++
1031
- if ($3=="conflict" || $3=="error") blocked=1
1032
- if ($3=="unverified" && !reason_seen[$2]++) reason[++reason_n]=$2
1033
- }
1034
- END {
1035
- ok_value = blocked ? "false" : "true"
1036
- coverage_value = reason_n ? "false" : "true"
1037
- strict_enabled = strict==1 ? "true" : "false"
1038
- strict_blocked = (strict==1 && blocked) ? "true" : "false"
1039
- print "{"
1040
- print " \"schemaVersion\": 1,"
1041
- print " \"command\": \"bus-check\","
1042
- printf " \"ok\": %s,\n", ok_value
1043
- print " \"target\": \".\","
1044
- print " \"findings\": ["
1045
- for (i=1; i<=n; i++) {
1046
- printf " {\"code\":\"%s\",\"level\":\"%s\",\"message\":\"%s\"", esc(code[i]), esc(level[i]), esc(message[i])
1047
- if (path[i] != "") printf ",\"path\":\"%s\"", esc(path[i])
1048
- suffix = ""; if (i<n) suffix = ","
1049
- printf "}%s\n", suffix
1050
- }
1051
- print " ],"
1052
- printf " \"summary\": {\"confirmed\":%d,\"warning\":%d,\"unverified\":%d,\"conflict\":%d,\"error\":%d},\n", count["confirmed"]+0, count["warning"]+0, count["unverified"]+0, count["conflict"]+0, count["error"]+0
1053
- printf " \"coverage\": {\"complete\":%s,\"reasons\":[", coverage_value
1054
- for (i=1; i<=reason_n; i++) {
1055
- suffix = ""; if (i<reason_n) suffix = ","
1056
- printf "\"%s\"%s", esc(reason[i]), suffix
1057
- }
1058
- print "]},"
1059
- printf " \"strict\": {\"enabled\":%s,\"blocked\":%s}\n", strict_enabled, strict_blocked
1060
- print "}"
1061
- }
1062
- ' "$FINDINGS_SORTED"
1063
- }
1064
-
1065
- SUBREPOS=() # 留空自动发现;或写死:SUBREPOS=("仓1" "目录/仓2")
1066
-
1067
- echo "════════ 协作总线 开工同步 (bus-check) ════════"
1068
- echo "▸ 工作区: $ROOT (协调层脚本: $SDIR/)" # 打印解析结果:根认错了(嵌套项目/骨架没建)一眼能看出,不静默
1069
- echo ""
1070
-
1071
- # 1) meta 仓是否落后远端
1072
- if [ "${BUS_CHECK_NO_FETCH:-0}" = "1" ]; then
1073
- add_finding "sync.unverified" "unverified" "Remote synchronization checks were skipped by BUS_CHECK_NO_FETCH=1." "."
1074
- elif ! git fetch --quiet 2>/dev/null; then
1075
- add_finding "sync.unverified" "unverified" "The meta-repository fetch failed; remote synchronization is not verified." "."
1076
- fi
1077
- LOCAL=$(git rev-parse @ 2>/dev/null || echo "?")
1078
- REMOTE=$(git rev-parse '@{u}' 2>/dev/null || echo "")
1079
- if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then
1080
- m_behind=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo "?")
1081
- m_ahead=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || echo "?")
1082
- if [ "$m_behind" != "0" ]; then
1083
- echo "⚠️ meta 仓落后远端 $m_behind 个提交 —— 开工前先 git pull !"
1084
- else
1085
- echo "⚠️ meta 仓领先远端 $m_ahead 个提交(本地未推送)—— 记得 git push"
1086
- fi
1087
- else
1088
- echo "✅ meta 仓与远端同步(或无上游)"
1089
- fi
1090
-
1091
- # 1.5) 子仓是否落后/领先远端
1092
- if [ ${#SUBREPOS[@]} -eq 0 ]; then
1093
- for g in ./*/.git ./*/*/.git; do
1094
- [ -d "$g" ] || continue
1095
- SUBREPOS+=("${g%/.git}")
1096
- done
1097
- fi
1098
- echo ""
1099
- echo "── 子仓 ⇄ 远端 ──"
1100
- for r in "${SUBREPOS[@]:-}"; do
1101
- if [ -z "$r" ] || [ ! -d "$r/.git" ]; then
1102
- continue
1103
- fi
1104
- subrepo_display="${r#./}"
1105
- if path_uses_symlink_component "$ROOT_PHYS/$subrepo_display"; then
1106
- echo " ⚠️ $subrepo_display 经 symlink 到达,远端同步未检查"
1107
- add_scan_boundary "symlink" "$subrepo_display" \
1108
- "the discovered repository was not traversed"
1109
- continue
1110
- fi
1111
- if [ ! -r "$r" ] || [ ! -x "$r" ]; then
1112
- echo " ⚠️ $subrepo_display 当前权限不足,远端同步未检查"
1113
- add_scan_boundary "permission" "$subrepo_display" \
1114
- "the discovered repository was not readable and searchable"
1115
- continue
1116
- fi
1117
- if [ "${BUS_CHECK_NO_FETCH:-0}" != "1" ] && ! git -C "$r" fetch --quiet 2>/dev/null; then
1118
- add_finding "sync.unverified" "unverified" "A sub-repository fetch failed; remote synchronization is not verified." "$subrepo_display"
1119
- fi
1120
- if git -C "$r" rev-parse '@{u}' >/dev/null 2>&1; then
1121
- ahead=$(git -C "$r" rev-list --count '@{u}..HEAD' 2>/dev/null || echo "?")
1122
- behind=$(git -C "$r" rev-list --count 'HEAD..@{u}' 2>/dev/null || echo "?")
1123
- head_h=$(git -C "$r" rev-parse --short HEAD 2>/dev/null || echo "?")
1124
- flag="✅"; { [ "$behind" != "0" ] || [ "$ahead" != "0" ]; } && flag="⚠️ "
1125
- printf " %s %-32s HEAD %s 领先 %s / 落后 %s\n" "$flag" "$subrepo_display" "$head_h" "$ahead" "$behind"
1126
- else
1127
- printf " · %-32s (无上游)\n" "$subrepo_display"
1128
- fi
1129
- done
1130
- echo ""
1131
-
1132
- # 1.6) Multi-repo version/contract/deployment joins. Only explicit map rows can
1133
- # establish equality; every missing/unmapped source remains visible as unverified.
1134
- echo "── 多仓版本 / 契约 / 部署基线 ──"
1135
- check_multirepo_drift
1136
- echo ""
1137
-
1138
- # 1.7) 机器闸自检(一道闸的强度不超过守闸规则的强度 —— 用在跑的闸守新闸;.git/hooks 不进版本控制,克隆后闸不存在)
1139
- echo "── 机器闸自检 (pre-commit) ──"
1140
- gate_missing=""; gate_checked=0
1141
- for r in . "${SUBREPOS[@]:-}"; do
1142
- [ -n "$r" ] || continue; [ -e "$r/.git" ] || continue
1143
- if [ "$r" != "." ]; then
1144
- path_uses_symlink_component "$ROOT_PHYS/${r#./}" && continue
1145
- if [ ! -r "$r" ] || [ ! -x "$r" ]; then continue; fi
1146
- fi
1147
- gate_checked=1
1148
- installed=0
1149
- [ -f "$r/.git/hooks/pre-commit" ] && installed=1
1150
- hpath=$(git -C "$r" config core.hooksPath 2>/dev/null || true)
1151
- if [ -n "$hpath" ]; then
1152
- { [ -f "$hpath/pre-commit" ] || [ -f "$r/$hpath/pre-commit" ]; } && installed=1
1153
- fi
1154
- [ "$installed" = 1 ] || gate_missing="$gate_missing ${r#./}"
1155
- done
1156
- if [ "$gate_checked" = 0 ]; then
1157
- echo " (没有可检查的 git 仓 —— 无法判定)"
1158
- elif [ -n "$gate_missing" ]; then
1159
- echo " ⚠️ 未装 pre-commit 闸:${gate_missing# } —— 每仓装一次(装法见 $SDIR/pre-commit.sh 头部注释)"
1160
- else
1161
- echo " ✅ meta 仓与全部子仓均已装 pre-commit 闸"
1162
- fi
1163
- echo ""
1164
-
1165
- # 2) 当前期 + 看板指针 (pm/NOW.md)
1166
- echo "── 当前期 / 协调看板 (pm/NOW.md) ──"
1167
- now_scan_ok=0
1168
- if [ -f pm/NOW.md ]; then
1169
- if [ -L pm/NOW.md ] \
1170
- || path_uses_symlink_component "$ROOT_PHYS/pm/NOW.md"; then
1171
- echo " (pm/NOW.md 经 symlink 到达,未读取)"
1172
- add_scan_boundary "symlink" "pm/NOW.md" \
1173
- "the live coordination pointer was not read"
1174
- elif [ ! -r pm/NOW.md ]; then
1175
- echo " (pm/NOW.md 当前权限不足,未读取)"
1176
- add_scan_boundary "permission" "pm/NOW.md" \
1177
- "the live coordination pointer was not readable"
1178
- else
1179
- now_scan_ok=1
1180
- fi
1181
- fi
1182
- if [ "$now_scan_ok" -eq 1 ]; then
1183
- grep -m1 "^\*\*当前期" pm/NOW.md 2>/dev/null || echo " (NOW.md 无「当前期」行)"
1184
- grep -m1 "本期轨道" pm/NOW.md 2>/dev/null || true
1185
- grep -m1 "当期看板" pm/NOW.md 2>/dev/null || true
1186
- fi
1187
- echo ""
1188
-
1189
- # 2.5) 协调层腐烂检测(仪式没有护栏 = 没有仪式;阈值可用 env 调:BUS_NOW_MAX / BUS_STATUS_MAX)
1190
- echo "── 协调层腐烂检测 ──"
1191
- cur_board=""
1192
- board_scan_ok=0
1193
- if [ ! -f pm/NOW.md ]; then
1194
- echo " (pm/NOW.md 不存在 —— 无法判定,先按模板建骨架)"
1195
- add_finding "ref.broken" "conflict" "pm/NOW.md is missing." "pm/NOW.md"
1196
- elif [ "$now_scan_ok" -eq 0 ]; then
1197
- echo " (pm/NOW.md 未读取;协调层腐烂检测保持 unverified)"
1198
- else
1199
- rot=0; board_note=""
1200
- NOW_MAX="${BUS_NOW_MAX:-40}"; ST_MAX="${BUS_STATUS_MAX:-60}"
1201
- # a) NOW 薄指针长肥(BuildBeat lessons 第 1 条:NOW 长肥 = 腐烂开端)
1202
- now_lines=$(wc -l < pm/NOW.md | tr -d ' ')
1203
- if [ "$now_lines" -gt "$NOW_MAX" ]; then
1204
- echo " ⚠️ pm/NOW.md 已 $now_lines 行(>$NOW_MAX)—— 薄指针长肥,跑换期压缩仪式(NOW 底部 checklist)"; rot=1
1205
- add_finding "sync.now_bloated" "conflict" "pm/NOW.md exceeds the configured live-file limit ($now_lines > $NOW_MAX)." "pm/NOW.md"
1206
- fi
1207
- # b) 当期看板存在性 + 非当期看板滞留 pm/(该 git mv 进 archive/<期>/)
1208
- # shellcheck disable=SC2016 # single quotes intentionally protect the backticks in the sed pattern
1209
- cur_board=$(grep -m1 "当期看板" pm/NOW.md 2>/dev/null | sed -n 's/.*`\([^`]*看板[^`]*\.md\)`.*/\1/p')
1210
- cur_board=$(basename "${cur_board:-}" 2>/dev/null)
1211
- case "$cur_board" in *"<"*) cur_board="";; esac # NOW 还是占位符 → 判不了
1212
- if [ -z "$cur_board" ]; then
1213
- board_note="(NOW 未填当期看板,看板检查跳过)"
1214
- add_finding "sync.unverified" "unverified" "The current-board pointer cannot be determined from pm/NOW.md." "pm/NOW.md"
1215
- else
1216
- if [ ! -f "pm/$cur_board" ]; then
1217
- echo " ⚠️ NOW 指向的当期看板 pm/$cur_board 不存在 —— 坏指针,先修 NOW"
1218
- add_finding "ref.broken" "conflict" "The current-board pointer does not resolve." "pm/$cur_board"
1219
- rot=1
1220
- elif [ -L "pm/$cur_board" ] \
1221
- || path_uses_symlink_component "$ROOT_PHYS/pm/$cur_board"; then
1222
- echo " ⚠️ pm/$cur_board 经 symlink 到达,未读取"
1223
- add_scan_boundary "symlink" "pm/$cur_board" \
1224
- "the current board was not read"
1225
- rot=1
1226
- elif [ ! -r "pm/$cur_board" ]; then
1227
- echo " ⚠️ pm/$cur_board 当前权限不足,未读取"
1228
- add_scan_boundary "permission" "pm/$cur_board" \
1229
- "the current board was not readable"
1230
- rot=1
1231
- else
1232
- board_scan_ok=1
1233
- fi
1234
- for b in pm/*看板*.md; do
1235
- { [ -e "$b" ] || [ -L "$b" ]; } || continue
1236
- base="$(basename "$b")"
1237
- [ "$base" = "$cur_board" ] || {
1238
- echo " ⚠️ $base 不是当期看板还留在 pm/ —— 归档进 pm/archive/<期>/"
1239
- add_finding "sync.now_bloated" "conflict" "A non-current board remains in the live pm directory." "pm/$base"
1240
- rot=1
1241
- }
1242
- done
1243
- fi
1244
- # c) status 文件超长(该截断:全文快照进 archive,live 只留基线+最近一条)
1245
- for f in pm/status/*.md; do
1246
- { [ -e "$f" ] || [ -L "$f" ]; } || continue
1247
- base="$(basename "$f")"; [ "$base" = "README.md" ] && continue
1248
- if [ -L "$f" ] || path_uses_symlink_component "$ROOT_PHYS/$f"; then
1249
- echo " ⚠️ pm/status/$base 经 symlink 到达,未读取"
1250
- add_scan_boundary "symlink" "pm/status/$base" \
1251
- "a live status file was not read"
1252
- rot=1
1253
- continue
1254
- fi
1255
- if [ ! -r "$f" ]; then
1256
- echo " ⚠️ pm/status/$base 当前权限不足,未读取"
1257
- add_scan_boundary "permission" "pm/status/$base" \
1258
- "a live status file was not readable"
1259
- rot=1
1260
- continue
1261
- fi
1262
- n=$(wc -l < "$f" | tr -d ' ')
1263
- [ "$n" -le "$ST_MAX" ] || {
1264
- echo " ⚠️ pm/status/$base 已 $n 行(>$ST_MAX)—— 换期压缩仪式该截断了"
1265
- add_finding "sync.now_bloated" "conflict" "A live status file exceeds the configured limit ($n > $ST_MAX)." "pm/status/$base"
1266
- rot=1
1267
- }
1268
- done
1269
- if [ "$rot" = 0 ]; then
1270
- if [ -n "$board_note" ]; then echo " ✅ NOW 薄、status 克制 $board_note"
1271
- else echo " ✅ NOW 薄、看板归位、status 克制"; fi
1272
- fi
1273
- fi
1274
- echo ""
1275
-
1276
- # 2.6) canonical Gate / completed-WP evidence / scoped local references
1277
- echo "── Gate / 完成证据 / 作用域引用 ──"
1278
- board_path=""
1279
- if [ "$board_scan_ok" -eq 1 ]; then
1280
- board_path="pm/$cur_board"
1281
- fi
1282
-
1283
- if [ -n "$board_path" ]; then
1284
- for gate_n in 1 2 3 4; do
1285
- gate_lines="$(grep -nE "^[[:space:]]*- Gate${gate_n}:" "$board_path" 2>/dev/null || true)"
1286
- gate_count="$(printf '%s\n' "$gate_lines" | awk 'NF { n++ } END { print n+0 }')"
1287
- if [ "$gate_count" -eq 0 ]; then
1288
- echo " ⚠️ Gate${gate_n} 缺少 canonical 状态行(legacy 看板可继续读,但不可机器判定)"
1289
- add_finding "gate.line_missing" "warning" "Gate${gate_n} canonical state line is missing." "$board_path"
1290
- continue
1291
- fi
1292
- if [ "$gate_count" -ne 1 ]; then
1293
- echo " ❌ Gate${gate_n} 状态行出现 $gate_count 次 —— protocol 无法唯一判定"
1294
- add_finding "gate.invalid" "error" "Gate${gate_n} must appear exactly once; found $gate_count lines." "$board_path"
1295
- continue
1296
- fi
1297
-
1298
- gate_line="${gate_lines#*:}"
1299
- if ! printf '%s\n' "$gate_line" | grep -Eq "^[[:space:]]*- Gate${gate_n}: (pending|passed|blocked|n/a)([[:space:]]*\|.*)?[[:space:]]*$"; then
1300
- echo " ❌ Gate${gate_n} 状态行格式或 state 非法"
1301
- add_finding "gate.invalid" "error" "Gate${gate_n} has a malformed canonical state line." "$board_path"
1302
- continue
1303
- fi
1304
- gate_state="$(printf '%s\n' "$gate_line" | sed -E "s/^[[:space:]]*- Gate${gate_n}: (pending|passed|blocked|n\/a).*/\1/")"
1305
-
1306
- if [ "$gate_state" = "n/a" ]; then
1307
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1308
- gate_reason="$(printf '%s\n' "$gate_line" | sed -nE 's/.*理由:[[:space:]]*`([^`]*)`.*/\1/p')"
1309
- if [ -z "$gate_reason" ] || printf '%s\n' "$gate_reason" | grep -qE '<[^>]*>'; then
1310
- echo " ⚠️ Gate${gate_n}=n/a 但缺少同一行的非占位理由"
1311
- add_finding "gate.na_without_reason" "conflict" "Gate${gate_n} is n/a without a non-placeholder reason." "$board_path"
1312
- fi
1313
- if [ "$gate_n" -eq 2 ] && detect_ui_signal; then
1314
- echo " ⚠️ Gate2=n/a 但仓库存在明确 UI 信号 —— 复核是否应进入 Gate2"
1315
- add_finding "gate.na_inconsistent" "warning" "Gate2 is n/a even though a positive UI signal was detected." "$board_path"
1316
- fi
1317
- fi
1318
-
1319
- if [ "$gate_state" = "passed" ]; then
1320
- gate_ref_seen=0; gate_ref_valid=0; gate_decision_issue=0
1321
- if printf '%s\n' "$gate_line" | grep -Eq '决策:'; then
1322
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1323
- gate_decision_ref="$(printf '%s\n' "$gate_line" | sed -nE 's/.*决策:[[:space:]]*`([^`]*)`.*/\1/p')"
1324
- if [ -z "$gate_decision_ref" ] || ! validate_decision_reference "$gate_decision_ref"; then
1325
- gate_decision_issue=1
1326
- fi
1327
- fi
1328
- if printf '%s\n' "$gate_line" | grep -Eq '(决策|证据):[[:space:]]*`'; then
1329
- while IFS= read -r gate_ref; do
1330
- [ -n "$gate_ref" ] || continue
1331
- gate_ref_seen=1
1332
- if validate_reference "$gate_ref" "$board_path"; then
1333
- gate_ref_valid=1
1334
- else
1335
- ref_rc=$?
1336
- if [ "$ref_rc" -eq 2 ]; then
1337
- add_finding "sync.unverified" "unverified" "Gate${gate_n} uses a remote reference that was not checked." "$board_path"
1338
- elif [ "$ref_rc" -eq 4 ]; then
1339
- echo " ⚠️ Gate${gate_n} 引用经 symlink 到达,未读取:$gate_ref"
1340
- add_scan_boundary "symlink" "$(reference_display_path "$gate_ref")" \
1341
- "a local reference was not followed"
1342
- elif [ "$ref_rc" -eq 5 ]; then
1343
- echo " ⚠️ Gate${gate_n} 引用当前权限不足,未读取:$gate_ref"
1344
- add_scan_boundary "permission" "$(reference_display_path "$gate_ref")" \
1345
- "a local reference was not readable"
1346
- elif [ "$ref_rc" -eq 1 ]; then
1347
- echo " ⚠️ Gate${gate_n} 引用无法解析:$gate_ref"
1348
- add_finding "ref.broken" "conflict" "Gate${gate_n} reference does not resolve: $gate_ref" "$board_path"
1349
- fi
1350
- fi
1351
- done < <(extract_backtick_tokens "$gate_line")
1352
- fi
1353
- if printf '%s\n' "$gate_line" | grep -Eq '证据:'; then
1354
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1355
- gate_evidence_ref="$(printf '%s\n' "$gate_line" | sed -nE 's/.*证据:[[:space:]]*`([^`]*)`.*/\1/p')"
1356
- if [ -n "$gate_evidence_ref" ] && validate_reference "$gate_evidence_ref" "$board_path"; then
1357
- evidence_location_status "$gate_evidence_ref"
1358
- evidence_location_rc=$?
1359
- if [ "$evidence_location_rc" -eq 1 ]; then
1360
- echo " ⚠️ Gate${gate_n} 的本地证据未落在 pm/archive/<期>/evidence/"
1361
- add_finding "evidence.outside_archive" "warning" "Gate${gate_n} uses a local evidence path outside pm/archive/<period>/evidence/." "$board_path"
1362
- fi
1363
- fi
1364
- fi
1365
- if [ "$gate_decision_issue" -eq 1 ]; then
1366
- echo " ⚠️ Gate${gate_n}=passed 的决策引用未指向 pm/decisions.md 有效决策行"
1367
- add_finding "gate.pass_untraceable" "warning" "Gate${gate_n} has a decision reference that does not identify an existing decisions.md row." "$board_path"
1368
- elif [ "$gate_ref_seen" -eq 0 ] || [ "$gate_ref_valid" -eq 0 ]; then
1369
- echo " ⚠️ Gate${gate_n}=passed 但没有可追溯的决策/证据引用"
1370
- add_finding "gate.pass_untraceable" "warning" "Gate${gate_n} is passed without a traceable decision or evidence reference." "$board_path"
1371
- fi
1372
- fi
1373
- done
1374
-
1375
- while IFS=$'\t' read -r wp_title evidence_count evidence_line; do
1376
- [ -n "$wp_title" ] || continue
1377
- if [ "$evidence_count" -ne 1 ]; then
1378
- echo " ⚠️ $wp_title 已完成但 **证据** 行数量为 $evidence_count(要求恰好 1)"
1379
- add_finding "evidence.missing" "conflict" "$wp_title is complete but does not have exactly one canonical evidence line." "$board_path"
1380
- continue
1381
- fi
1382
- if [ -z "$evidence_line" ] || printf '%s\n' "$evidence_line" | grep -qE '<[^>]*>|待补|TBD'; then
1383
- echo " ⚠️ $wp_title 已完成但证据仍为空或占位符"
1384
- add_finding "evidence.missing" "conflict" "$wp_title is complete but its evidence line is empty or placeholder-only." "$board_path"
1385
- continue
1386
- fi
1387
-
1388
- evidence_ref_seen=0; evidence_ref_valid=0; evidence_ref_unverified=0
1389
- while IFS= read -r evidence_ref; do
1390
- [ -n "$evidence_ref" ] || continue
1391
- if validate_reference "$evidence_ref" "$board_path"; then
1392
- evidence_ref_seen=1; evidence_ref_valid=1
1393
- evidence_location_status "$evidence_ref"
1394
- evidence_location_rc=$?
1395
- if [ "$evidence_location_rc" -eq 1 ]; then
1396
- echo " ⚠️ $wp_title 的本地证据未落在 pm/archive/<期>/evidence/"
1397
- add_finding "evidence.outside_archive" "warning" "$wp_title uses a local evidence path outside pm/archive/<period>/evidence/." "$board_path"
1398
- fi
1399
- else
1400
- ref_rc=$?
1401
- if [ "$ref_rc" -eq 2 ]; then
1402
- evidence_ref_seen=1; evidence_ref_unverified=1
1403
- add_finding "sync.unverified" "unverified" "$wp_title uses remote-only evidence that was not checked." "$board_path"
1404
- elif [ "$ref_rc" -eq 4 ]; then
1405
- evidence_ref_seen=1; evidence_ref_unverified=1
1406
- echo " ⚠️ $wp_title 的证据引用经 symlink 到达,未读取:$evidence_ref"
1407
- add_scan_boundary "symlink" "$(reference_display_path "$evidence_ref")" \
1408
- "a local reference was not followed"
1409
- elif [ "$ref_rc" -eq 5 ]; then
1410
- evidence_ref_seen=1; evidence_ref_unverified=1
1411
- echo " ⚠️ $wp_title 的证据引用当前权限不足,未读取:$evidence_ref"
1412
- add_scan_boundary "permission" "$(reference_display_path "$evidence_ref")" \
1413
- "a local reference was not readable"
1414
- elif [ "$ref_rc" -eq 1 ]; then
1415
- evidence_ref_seen=1
1416
- echo " ⚠️ $wp_title 的证据引用无法解析:$evidence_ref"
1417
- add_finding "ref.broken" "conflict" "$wp_title evidence reference does not resolve: $evidence_ref" "$board_path"
1418
- fi
1419
- fi
1420
- done < <(extract_backtick_tokens "$evidence_line")
1421
- if [ "$evidence_ref_seen" -eq 0 ] \
1422
- || { [ "$evidence_ref_valid" -eq 0 ] && [ "$evidence_ref_unverified" -eq 0 ]; }; then
1423
- echo " ⚠️ $wp_title 已完成但证据行没有可机器核验的路径/hash/URL"
1424
- add_finding "evidence.missing" "conflict" "$wp_title has no machine-verifiable evidence reference." "$board_path"
1425
- fi
1426
- done < <(awk '
1427
- function emit() {
1428
- if (in_wp && done) {
1429
- gsub(/\t/, " ", title); gsub(/\t/, " ", evidence)
1430
- printf "%s\t%d\t%s\n", title, evidence_count, evidence
1431
- }
1432
- }
1433
- /^### WP-/ { emit(); in_wp=1; done=0; evidence_count=0; evidence=""; title=$0; next }
1434
- in_wp && /^#{1,3}[[:space:]]/ { emit(); in_wp=0; next }
1435
- in_wp && /\*\*状态\*\*:/ && /✅完成/ { done=1 }
1436
- in_wp && /\*\*证据\*\*:/ {
1437
- evidence_count++
1438
- evidence = evidence (evidence ? " " : "") $0
1439
- }
1440
- END { emit() }
1441
- ' "$board_path")
1442
- else
1443
- echo " (当期看板不可读;Gate 与完成证据检查跳过)"
1444
- fi
1445
-
1446
- ref_limit="${BUS_REF_MAX:-200}"
1447
- case "$ref_limit" in ''|*[!0-9]*) ref_limit=200 ;; esac
1448
- ref_scanned=0; ref_truncated=0
1449
- for ref_source in pm/NOW.md "$board_path" pm/decisions.md; do
1450
- if [ -z "$ref_source" ] || [ ! -f "$ref_source" ]; then continue; fi
1451
- if [ -L "$ref_source" ] \
1452
- || path_uses_symlink_component "$ROOT_PHYS/$ref_source"; then
1453
- echo " ⚠️ $ref_source 经 symlink 到达,作用域引用未扫描"
1454
- add_scan_boundary "symlink" "$ref_source" \
1455
- "the scoped-reference source was not read"
1456
- continue
1457
- fi
1458
- if [ ! -r "$ref_source" ]; then
1459
- echo " ⚠️ $ref_source 当前权限不足,作用域引用未扫描"
1460
- add_scan_boundary "permission" "$ref_source" \
1461
- "the scoped-reference source was not readable"
1462
- continue
1463
- fi
1464
- ref_scan_input="$ref_source"
1465
- if [ "$ref_source" = "pm/decisions.md" ]; then
1466
- ref_scan_input="$BUS_TMP/decisions.scope"
1467
- awk '/^\| 20[0-9][0-9]-/ { print; found++; if (found == 3) exit }' "$ref_source" > "$ref_scan_input"
1468
- fi
1469
- while IFS= read -r scoped_ref; do
1470
- [ -n "$scoped_ref" ] || continue
1471
- ref_rc=0
1472
- validate_reference "$scoped_ref" "$ref_source" 1 || ref_rc=$?
1473
- [ "$ref_rc" -ne 3 ] || continue
1474
- ref_scanned=$((ref_scanned + 1))
1475
- if [ "$ref_scanned" -gt "$ref_limit" ]; then
1476
- ref_truncated=1
1477
- break
1478
- fi
1479
- if [ "$ref_rc" -eq 0 ]; then
1480
- :
1481
- elif [ "$ref_rc" -eq 2 ]; then
1482
- add_finding "sync.unverified" "unverified" "A scoped remote reference was not checked." "$ref_source"
1483
- elif [ "$ref_rc" -eq 4 ]; then
1484
- echo " ⚠️ $ref_source 中的作用域引用经 symlink 到达,未读取:$scoped_ref"
1485
- add_scan_boundary "symlink" "$(reference_display_path "$scoped_ref")" \
1486
- "a local reference was not followed"
1487
- elif [ "$ref_rc" -eq 5 ]; then
1488
- echo " ⚠️ $ref_source 中的作用域引用当前权限不足,未读取:$scoped_ref"
1489
- add_scan_boundary "permission" "$(reference_display_path "$scoped_ref")" \
1490
- "a local reference was not readable"
1491
- elif [ "$ref_rc" -eq 1 ]; then
1492
- echo " ⚠️ $ref_source 中的作用域引用无法解析:$scoped_ref"
1493
- add_finding "ref.broken" "conflict" "Scoped reference does not resolve: $scoped_ref" "$ref_source"
1494
- fi
1495
- done < <(
1496
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1497
- grep -oE '\]\([^)]*\)|`[^`]*\.md([^`]*)?`' "$ref_scan_input" 2>/dev/null \
1498
- | sed -E 's/^\]\(//; s/\)$//; s/^`//; s/`$//' \
1499
- | sed -E 's/[[:space:]]+"[^"]*"$//' || true
1500
- )
1501
- [ "$ref_truncated" -eq 0 ] || break
1502
- done
1503
- if [ "$ref_truncated" -eq 1 ]; then
1504
- echo " ⚠️ 作用域引用扫描达到 BUS_REF_MAX=$ref_limit,剩余范围未核验"
1505
- add_scan_boundary "limit" "pm/" \
1506
- "the scoped reference scan stopped at BUS_REF_MAX=$ref_limit"
1507
- fi
1508
- [ -n "$board_path" ] && echo " · 已检查 canonical Gate、完成证据与 $ref_scanned 条作用域引用"
1509
- echo ""
1510
-
1511
- # 2.7) 工程层验证能力(证据分级 L3「自动化测试」的前提是项目有能跑的测试;测试跑不动是比 NOW 长肥更重的腐烂)
1512
- echo "── 工程层验证能力 ──"
1513
- if [ -f "$SDIR/verify-status.sh" ]; then
1514
- if ! bash "$SDIR/verify-status.sh" 2>/dev/null | sed 's/^/ /'; then
1515
- echo " (verify-status.sh 执行失败)"
1516
- add_finding "sync.unverified" "unverified" "verify-status human report failed." "$SDIR/verify-status.sh"
1517
- fi
1518
- verify_machine="$BUS_TMP/verify.machine"
1519
- if bash "$SDIR/verify-status.sh" --format=machine > "$verify_machine" 2>/dev/null; then
1520
- verify_machine_unknown=0
1521
- while IFS=$'\t' read -r record code level message path; do
1522
- if [ "$record" != "FINDING" ]; then
1523
- [ -z "$record$code$level$message$path" ] || verify_machine_unknown=1
1524
- continue
1525
- fi
1526
- case "$code:$level" in
1527
- sync.l3_stale:warning|sync.l3_unconfigured:unverified)
1528
- add_finding "$code" "$level" "$message" "$path"
1529
- ;;
1530
- *)
1531
- add_finding "sync.unverified" "unverified" "verify-status returned an unknown machine finding." "$SDIR/verify-status.sh"
1532
- ;;
1533
- esac
1534
- done < "$verify_machine"
1535
- if [ "$verify_machine_unknown" -eq 1 ]; then
1536
- add_finding "sync.unverified" "unverified" "verify-status did not return the expected machine protocol." "$SDIR/verify-status.sh"
1537
- fi
1538
- else
1539
- add_finding "sync.unverified" "unverified" "verify-status machine report failed." "$SDIR/verify-status.sh"
1540
- fi
1541
- else
1542
- echo " ⚠️ 未配置 $SDIR/verify-status.sh —— 项目验证能力未知,L3 级证据无从谈起(接入:每行输出「套件名 测试命令 上次全绿时间」)"
1543
- add_finding "sync.l3_unconfigured" "unverified" "No verify-status adapter is installed." "$SDIR/verify-status.sh"
1544
- fi
1545
- echo ""
1546
-
1547
- # 3) 契约快照版本
1548
- echo "── 契约 (contracts/PROTOCOL.md) ──"
1549
- if [ "${multirepo_map_unsafe:-1}" -eq 0 ]; then
1550
- grep -m1 -E "契约快照对应版本" contracts/PROTOCOL.md 2>/dev/null || echo " (无)"
1551
- else
1552
- echo " (契约入口缺失或未安全读取)"
1553
- fi
1554
- echo ""
1555
-
1556
- # 3.5) 可选 standards / ADR。缺失即跳过;存在才检查结构与明确状态。
1557
- echo "── 可选规范 / ADR ──"
1558
- standard_count=0
1559
- stack_standard_status=""
1560
- stack_standard_valid=0
1561
- for standard_path in standards/STACK.md standards/CODE.md standards/REVIEW.md standards/DESIGN.md; do
1562
- [ -f "$standard_path" ] || continue
1563
- standard_count=$((standard_count + 1))
1564
- if [ -L "$standard_path" ] \
1565
- || path_uses_symlink_component "$ROOT_PHYS/$standard_path"; then
1566
- echo " ⚠️ $standard_path 经 symlink 到达,可选规范未读取"
1567
- add_scan_boundary "symlink" "$standard_path" \
1568
- "an optional standard was not read"
1569
- continue
1570
- fi
1571
- if [ ! -r "$standard_path" ]; then
1572
- echo " ⚠️ $standard_path 当前权限不足,可选规范未读取"
1573
- add_scan_boundary "permission" "$standard_path" \
1574
- "an optional standard was not readable"
1575
- continue
1576
- fi
1577
- standard_name="$(basename "$standard_path" .md)"
1578
- standard_invalid=0
1579
-
1580
- optional_count="$(grep -cE '^> \*\*Optional\*\*:' "$standard_path" 2>/dev/null || true)"
1581
- boundary_count="$(grep -cE '^> \*\*AI write boundary\*\*:' "$standard_path" 2>/dev/null || true)"
1582
- status_lines="$(grep -E '^> \*\*Status\*\*:' "$standard_path" 2>/dev/null || true)"
1583
- status_count="$(printf '%s\n' "$status_lines" | awk 'NF { n++ } END { print n+0 }')"
1584
- standard_status=""
1585
- if [ "$optional_count" -ne 1 ] || [ "$boundary_count" -ne 1 ] || [ "$status_count" -ne 1 ]; then
1586
- standard_invalid=1
1587
- elif ! printf '%s\n' "$status_lines" | grep -Eq '^> \*\*Status\*\*: (Draft|Confirmed)[[:space:]]*$'; then
1588
- standard_invalid=1
1589
- else
1590
- standard_status="$(printf '%s\n' "$status_lines" | sed -E 's/^> \*\*Status\*\*: (Draft|Confirmed).*/\1/')"
1591
- fi
1592
-
1593
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1594
- rule_tokens="$(grep -E '^[[:space:]]*- `[A-Z]+-(MUST|SHOULD|MAY)-' "$standard_path" 2>/dev/null \
1595
- | grep -oE '`[A-Z]+-(MUST|SHOULD|MAY)-[^`]+`' | tr -d '`' || true)"
1596
- valid_rules=0
1597
- invalid_rule=0
1598
- while IFS= read -r rule_id; do
1599
- [ -n "$rule_id" ] || continue
1600
- if printf '%s\n' "$rule_id" | grep -Eq "^${standard_name}-(MUST|SHOULD|MAY)-[0-9]{3}$"; then
1601
- valid_rules=$((valid_rules + 1))
1602
- else
1603
- invalid_rule=1
1604
- fi
1605
- done <<< "$rule_tokens"
1606
- [ "$valid_rules" -gt 0 ] || invalid_rule=1
1607
- duplicate_rule="$(printf '%s\n' "$rule_tokens" | awk 'NF' | sort | uniq -d | head -1)"
1608
- [ -z "$duplicate_rule" ] || invalid_rule=1
1609
- [ "$invalid_rule" -eq 0 ] || standard_invalid=1
1610
-
1611
- if [ "$standard_status" = "Confirmed" ] && grep -Eq '<[^>]+>' "$standard_path"; then
1612
- standard_invalid=1
1613
- fi
1614
-
1615
- while IFS= read -r standard_ref; do
1616
- [ -n "$standard_ref" ] || continue
1617
- standard_ref_rc=0
1618
- validate_reference "$standard_ref" "$standard_path" || standard_ref_rc=$?
1619
- [ "$standard_ref_rc" -ne 1 ] || standard_invalid=1
1620
- if [ "$standard_ref_rc" -eq 4 ]; then
1621
- add_scan_boundary "symlink" "$(reference_display_path "$standard_ref")" \
1622
- "a local reference was not followed"
1623
- elif [ "$standard_ref_rc" -eq 5 ]; then
1624
- add_scan_boundary "permission" "$(reference_display_path "$standard_ref")" \
1625
- "a local reference was not readable"
1626
- fi
1627
- done < <(
1628
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1629
- grep -hoE '`[^`]+`' "$standard_path" 2>/dev/null | sed 's/^`//; s/`$//' || true
1630
- )
1631
-
1632
- if [ "$standard_invalid" -eq 1 ]; then
1633
- echo " ❌ $standard_path 结构、Rule ID、已确认占位符或本地引用无效"
1634
- add_finding "standards.invalid" "error" "Optional standard structure, Rule IDs, confirmed placeholders, or local references are invalid." "$standard_path"
1635
- elif [ "$standard_status" = "Draft" ]; then
1636
- echo " ⚠️ $standard_path 仍是 Draft(待确认)"
1637
- add_finding "standards.unconfirmed" "unverified" "Optional standard is present but still marked Draft." "$standard_path"
1638
- else
1639
- echo " ✅ $standard_path 结构与 Confirmed 状态可解析"
1640
- fi
1641
-
1642
- if [ "$standard_name" = "STACK" ]; then
1643
- stack_standard_status="$standard_status"
1644
- [ "$standard_invalid" -eq 1 ] || stack_standard_valid=1
1645
- fi
1646
- done
1647
- [ "$standard_count" -gt 0 ] || echo " · standards 未启用(缺失即跳过,零 finding)"
1648
- [ "$stack_standard_valid" -eq 0 ] || [ "$stack_standard_status" != "Confirmed" ] || check_stack_drift
1649
-
1650
- adr_count=0
1651
- for adr_path in pm/adr/ADR-[0-9][0-9][0-9][0-9]-*.md; do
1652
- [ -f "$adr_path" ] || continue
1653
- adr_count=$((adr_count + 1))
1654
- if [ -L "$adr_path" ] \
1655
- || path_uses_symlink_component "$ROOT_PHYS/$adr_path"; then
1656
- echo " ⚠️ $adr_path 经 symlink 到达,ADR 未读取"
1657
- add_scan_boundary "symlink" "$adr_path" "an ADR was not read"
1658
- continue
1659
- fi
1660
- if [ ! -r "$adr_path" ]; then
1661
- echo " ⚠️ $adr_path 当前权限不足,ADR 未读取"
1662
- add_scan_boundary "permission" "$adr_path" "an ADR was not readable"
1663
- continue
1664
- fi
1665
- adr_status_lines="$(grep -E '^- Status:' "$adr_path" 2>/dev/null || true)"
1666
- adr_status_count="$(printf '%s\n' "$adr_status_lines" | awk 'NF { n++ } END { print n+0 }')"
1667
- if [ "$adr_status_count" -ne 1 ] || ! printf '%s\n' "$adr_status_lines" | grep -Eq '^- Status: (Proposed|Accepted|Rejected|Superseded)[[:space:]]*$'; then
1668
- echo " ❌ $adr_path 缺少唯一合法 Status"
1669
- add_finding "adr.status_invalid" "error" "ADR must contain exactly one legal Status line." "$adr_path"
1670
- fi
1671
- done
1672
-
1673
- for adr_path in pm/adr/ADR-[0-9][0-9][0-9][0-9]-*.md; do
1674
- [ -f "$adr_path" ] || continue
1675
- [ ! -L "$adr_path" ] || continue
1676
- path_uses_symlink_component "$ROOT_PHYS/$adr_path" && continue
1677
- [ -r "$adr_path" ] || continue
1678
- initial_status_lines="$(grep -E '^- Status:' "$adr_path" 2>/dev/null || true)"
1679
- initial_status_count="$(printf '%s\n' "$initial_status_lines" | awk 'NF { n++ } END { print n+0 }')"
1680
- if [ "$initial_status_count" -ne 1 ] || ! printf '%s\n' "$initial_status_lines" | grep -Eq '^- Status: (Proposed|Accepted|Rejected|Superseded)[[:space:]]*$'; then
1681
- continue
1682
- fi
1683
- chain_current="$adr_path"
1684
- chain_seen="|$adr_path|"
1685
- chain_steps=0
1686
- chain_broken=0
1687
- while [ "$chain_steps" -lt 100 ]; do
1688
- chain_status_lines="$(grep -E '^- Status:' "$chain_current" 2>/dev/null || true)"
1689
- chain_status_count="$(printf '%s\n' "$chain_status_lines" | awk 'NF { n++ } END { print n+0 }')"
1690
- if [ "$chain_status_count" -ne 1 ] || ! printf '%s\n' "$chain_status_lines" | grep -Eq '^- Status: (Proposed|Accepted|Rejected|Superseded)[[:space:]]*$'; then
1691
- chain_broken=1
1692
- break
1693
- fi
1694
- chain_status="$(printf '%s\n' "$chain_status_lines" | sed -E 's/^- Status: ([A-Za-z]+).*/\1/')"
1695
- [ "$chain_status" = "Superseded" ] || break
1696
-
1697
- chain_target_lines="$(grep -E '^- Superseded by:' "$chain_current" 2>/dev/null || true)"
1698
- chain_target_count="$(printf '%s\n' "$chain_target_lines" | awk 'NF { n++ } END { print n+0 }')"
1699
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1700
- if [ "$chain_target_count" -ne 1 ] || ! printf '%s\n' "$chain_target_lines" | grep -Eq '^- Superseded by: `pm/adr/ADR-[0-9]{4}-[^`]+\.md`[[:space:]]*$'; then
1701
- chain_broken=1
1702
- break
1703
- fi
1704
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters
1705
- chain_target="$(printf '%s\n' "$chain_target_lines" | sed -E 's/^- Superseded by: `([^`]*)`.*/\1/')"
1706
- case "$chain_seen" in
1707
- *"|$chain_target|"*) chain_broken=1; break ;;
1708
- esac
1709
- if [ ! -f "$chain_target" ]; then
1710
- chain_broken=1
1711
- break
1712
- fi
1713
- chain_seen="${chain_seen}${chain_target}|"
1714
- chain_current="$chain_target"
1715
- chain_steps=$((chain_steps + 1))
1716
- done
1717
- [ "$chain_steps" -lt 100 ] || chain_broken=1
1718
- if [ "$chain_broken" -eq 1 ]; then
1719
- echo " ❌ $adr_path 的 Superseded 链缺失、成环或指向非法 ADR"
1720
- add_finding "adr.superseded_broken" "conflict" "ADR supersession chain is missing, cyclic, self-referential, or targets an invalid ADR." "$adr_path"
1721
- fi
1722
- done
1723
- [ "$adr_count" -gt 0 ] || echo " · ADR 未启用(缺失即跳过,零 finding)"
1724
- echo ""
1725
-
1726
- # 4) 最近拍板 (pm/decisions.md, 规则⑨ 决策单点)
1727
- echo "── 最近拍板 (pm/decisions.md, 最新 3 条) ──"
1728
- if [ -f pm/decisions.md ] && [ ! -L pm/decisions.md ] && [ -r pm/decisions.md ] \
1729
- && ! path_uses_symlink_component "$ROOT_PHYS/pm/decisions.md"; then
1730
- if command -v perl >/dev/null 2>&1; then
1731
- # perl -CSD -Mutf8 按「字符」截断,避免按字节截断把中文/省略号切成乱码
1732
- grep -E '^\| 20[0-9]{2}-' pm/decisions.md | head -3 | perl -CSD -Mutf8 -ne 'chomp; $_ = substr($_,0,110)."…" if length() > 110; print " $_\n"'
1733
- else
1734
- grep -E '^\| 20[0-9]{2}-' pm/decisions.md | head -3 | sed 's/^/ /' # 无 perl:降级为不截断(截字节会把中文切成乱码)
1735
- fi
1736
- else
1737
- echo " (pm/decisions.md 缺失或未安全读取)"
1738
- fi
1739
- echo ""
1740
-
1741
- # 5) 各域状态文件最近更新
1742
- echo "── 各域状态 (pm/status/) ──"
1743
- if [ -d pm/status ]; then
1744
- for f in pm/status/*.md; do
1745
- { [ -e "$f" ] || [ -L "$f" ]; } || continue
1746
- base="$(basename "$f")"; [ "$base" = "README.md" ] && continue
1747
- [ ! -L "$f" ] || continue
1748
- path_uses_symlink_component "$ROOT_PHYS/$f" && continue
1749
- [ -r "$f" ] || continue
1750
- line=$(git log -1 --format="%h %ad %s" --date=short -- "$f" 2>/dev/null)
1751
- printf " %-10s %s\n" "${base%.md}" "${line:-(未提交/未跟踪)}"
1752
- done
1753
- else
1754
- echo " (无 pm/status/)"
1755
- fi
1756
- echo ""
1757
-
1758
- # 5.5) 幽灵 hash 核验(lessons 第 11 条:status 声明的 commit 必须真实存在;「写了状态」≠「已提交」)
1759
- echo "── 幽灵 hash 核验 (pm/status/) ──"
1760
- if [ -d pm/status ]; then
1761
- # 只认**反引号内**的 token(status 模板约定 hash 写成 `hash`,把约定变成解析规则,治误报):
1762
- # 先从反引号串里**抠掉** URL 与 sha256: digest(不整段丢弃——同段混有链接和真 hash 时保住 hash);
1763
- # token 须同含字母与数字(干掉 defaced 这类纯字母英文词)。
1764
- # 代价:纯字母/纯数字的 7 位真 hash(各约 0.1%/3.7%)会被静默跳过——良性漏检,换不误拦。
1765
- HASHES=$(for f in pm/status/*.md; do
1766
- { [ -e "$f" ] || [ -L "$f" ]; } || continue
1767
- [ "$(basename "$f")" = "README.md" ] && continue
1768
- [ ! -L "$f" ] || continue
1769
- path_uses_symlink_component "$ROOT_PHYS/$f" && continue
1770
- [ -r "$f" ] || continue
1771
- # shellcheck disable=SC2016 # backticks are literal Markdown delimiters, not shell expansion
1772
- grep -hoE '`[^`]*`' "$f" 2>/dev/null
1773
- done | sed -E 's#[a-zA-Z][a-zA-Z0-9+.-]*://[^` ]*##g; s#sha256:[0-9a-fA-F]*##g' \
1774
- | grep -hoE '\b[0-9a-f]{7,40}\b' | grep '[a-f]' | grep '[0-9]' | sort -u)
1775
- ghost=0; total=0
1776
- for h in $HASHES; do
1777
- total=$((total+1)); found=0
1778
- for r in . "${SUBREPOS[@]:-}"; do
1779
- if [ -z "$r" ] || [ ! -d "$r" ]; then
1780
- continue
1781
- fi
1782
- if [ "$r" != "." ]; then
1783
- path_uses_symlink_component "$ROOT_PHYS/${r#./}" && continue
1784
- if [ ! -r "$r" ] || [ ! -x "$r" ]; then continue; fi
1785
- fi
1786
- git -C "$r" cat-file -t "$h" >/dev/null 2>&1 && { found=1; break; }
1787
- done
1788
- [ "$found" = 1 ] || {
1789
- echo " ⚠️ $h —— meta 仓与全部子仓查无此号(幽灵 hash:臆造/被重置/躺工作树没提交;若刚换机器,先 git pull 复核)"
1790
- add_finding "sync.ghost_hash" "conflict" "Status references an unresolved Git hash: $h" "pm/status/"
1791
- ghost=1
1792
- }
1793
- done
1794
- if [ "$ghost" = 0 ] && [ "$total" = 0 ]; then echo " (status 里暂无 hash)"
1795
- elif [ "$ghost" = 0 ]; then echo " ✅ $total 个 hash 全部可解析"
1796
- fi
1797
- else
1798
- echo " (无 pm/status/)"
1799
- fi
1800
- echo ""
1801
-
1802
- # 6) 在途变更提案
1803
- echo "── 在途变更提案 (pm/changes/) ──"
1804
- found=0
1805
- for f in pm/changes/*.md; do
1806
- [ -e "$f" ] || continue
1807
- base="$(basename "$f")"; [ "$base" = "README.md" ] && continue
1808
- echo " - $base"; found=1
1809
- done
1810
- [ "$found" = 0 ] && echo " (无)"
1811
- echo ""
1812
-
1813
- # 7) 线上实况(规则⑨:线上版本唯一查询口;文档里的版本号一律不作准)
1814
- echo "── 线上实况 ──"
1815
- if [ "${BUS_CHECK_NO_LIVE:-0}" = "1" ]; then
1816
- echo " (BUS_CHECK_NO_LIVE=1 跳过;线上版本以重跑本脚本为准)"
1817
- add_finding "sync.unverified" "unverified" "Live status was skipped by BUS_CHECK_NO_LIVE=1." "$SDIR/live-status.sh"
1818
- elif [ -f "$SDIR/live-status.sh" ]; then
1819
- if ! bash "$SDIR/live-status.sh" 2>/dev/null | sed 's/^/ /'; then
1820
- echo " (live-status.sh 执行失败 —— 检查部署平台 CLI 凭据/网络)"
1821
- add_finding "sync.unverified" "unverified" "Live-status adapter failed; production state is not verified." "$SDIR/live-status.sh"
1822
- fi
1823
- else
1824
- echo " (未配置 $SDIR/live-status.sh —— 接入部署平台 CLI 后,每行输出「服务名 版本」即可;在此之前别引用任何文档里的\"当前版本\")"
1825
- add_finding "sync.unverified" "unverified" "No live-status adapter is configured." "$SDIR/live-status.sh"
1826
- fi
1827
- echo ""
1828
-
1829
- # 7.5) 生产漂移检测(平台侧 env/secret 指纹 + 镜像tag↔git vs scripts/bus-baseline.json)
1830
- echo "── 生产漂移检测 ──"
1831
- if [ -f "$SDIR/drift-check.sh" ]; then
1832
- drift_output="$(bash "$SDIR/drift-check.sh" $DRIFT_MODE 2>&1)"; drc=$?
1833
- printf '%s\n' "$drift_output"
1834
- if [ "$drc" -eq 2 ]; then
1835
- add_finding "sync.production_drift" "conflict" "The configured drift adapter confirmed production drift." "$SDIR/drift-check.sh"
1836
- elif [ "$drc" -ne 0 ]; then
1837
- add_finding "sync.unverified" "unverified" "The configured drift adapter failed; production drift is not verified." "$SDIR/drift-check.sh"
1838
- elif printf '%s\n' "$drift_output" | grep -Eq '未配置|跳过|无法|无基线|不可判定'; then
1839
- add_finding "sync.unverified" "unverified" "The drift adapter reported incomplete coverage." "$SDIR/drift-check.sh"
1840
- fi
1841
- else
1842
- echo " (未配置 $SDIR/drift-check.sh —— 拷模板 + 接 live-config.sh 后,可检出「配置被改没部署 / 线上镜像 git 里找不到」类漂移)"
1843
- add_finding "sync.unverified" "unverified" "No production-drift adapter is configured." "$SDIR/drift-check.sh"
1844
- fi
1845
- echo ""
1846
-
1847
- # 8) 最近 5 个 meta-repo commit
1848
- echo "── 最近提交 (meta) ──"
1849
- git log --oneline -5 2>/dev/null
1850
- echo ""
1851
- echo "▸ 开工四步: ① git pull(含子仓) ② 读 NOW → 当期看板 → 契约 → 最近拍板 ③ 确认你域要动的不 stale ④ 不可逆动作(部署/契约/migration)前重跑本脚本"
1852
- finalize_findings
1853
- blocked="$(awk -F '\t' '$3=="conflict" || $3=="error" { blocked=1 } END { print blocked+0 }' "$FINDINGS_SORTED")"
1854
- echo ""
1855
- echo "── 结构化检查摘要 ──"
1856
- if [ ! -s "$FINDINGS_SORTED" ]; then
1857
- echo " (无 findings;仅代表已覆盖的仓库事实)"
1858
- else
1859
- while IFS=$'\t' read -r _rank code level message path; do
1860
- printf ' [%s] %s%s — %s\n' "$level" "$code" "${path:+ @ $path}" "$message"
1861
- done < "$FINDINGS_SORTED"
1862
- fi
1863
- [ "$STRICT" = 0 ] || [ "$blocked" = 0 ] || echo "⛔ strict 未过:存在 conflict / error finding"
1864
- echo "════════════════════════════════════════════"
1865
-
1866
- if [ "$FORMAT" = "json" ]; then
1867
- if ! render_json_report >&3; then
1868
- echo "bus-check: failed to render JSON report" >&2
1869
- exit 2
1870
- fi
1871
- fi
1872
- if [ "$STRICT" = 1 ] && [ "$blocked" -eq 1 ]; then
1873
- exit 1
1874
- fi
1875
- exit 0