@@ -106,7 +106,7 @@ jobs:
106106 import hashlib
107107 import json
108108 import re
109- from collections import Counter, defaultdict
109+ from collections import Counter
110110 from pathlib import Path
111111 from typing import Any
112112
@@ -193,11 +193,11 @@ jobs:
193193 warnings.append("boost clock below base clock")
194194 return [f"{category}: {rel}: {warning}" for warning in warnings]
195195
196- lines : list[str] = []
197- lines .append("## Data summary")
198- lines .append("")
199- lines .append("| Category | Total | Verified | Unverified | Missing verified | Verified % |")
200- lines .append("| --- | ---: | ---: | ---: | ---: | ---: |")
196+ stats_lines : list[str] = []
197+ stats_lines .append("## Data summary")
198+ stats_lines .append("")
199+ stats_lines .append("| Category | Total | Verified | Unverified | Missing verified | Verified % |")
200+ stats_lines .append("| --- | ---: | ---: | ---: | ---: | ---: |")
201201
202202 total_all = verified_all = unverified_all = missing_verified_all = 0
203203 by_category: dict[str, dict[str, int]] = {}
@@ -225,23 +225,24 @@ jobs:
225225 verified_all += verified
226226 unverified_all += unverified
227227 missing_verified_all += missing_verified
228- lines .append(
228+ stats_lines .append(
229229 f"| {category} | {total} | {verified} | {unverified} | {missing_verified} | {pct} |"
230230 )
231231 tracked_all = verified_all + unverified_all
232232 pct_all = f"{(verified_all / tracked_all * 100):.1f}%" if tracked_all else "n/a"
233- lines .append(
233+ stats_lines .append(
234234 f"| **all** | **{total_all}** | **{verified_all}** | **{unverified_all}** | "
235235 f"**{missing_verified_all}** | **{pct_all}** |"
236236 )
237237
238- lines.append("")
239- lines .append("## PR data delta ")
240- lines .append("")
241- lines .append("| Category | Added | Modified | Deleted | Added verified | Added unverified | Added Kaggle-sourced |")
242- lines .append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |")
238+ change_lines: list[str] = []
239+ change_lines .append("## Changed data")
240+ change_lines .append("")
241+ change_lines .append("| Category | Added | Modified | Deleted | Added verified | Added unverified | Added Kaggle-sourced |")
242+ change_lines .append("| --- | ---: | ---: | ---: | ---: | ---: | ---: |")
243243
244244 all_added: list[tuple[str, str, Path]] = []
245+ changed_by_category: dict[str, dict[str, list[str]]] = {}
245246 for category in CATEGORIES:
246247 head = rel_jsons(HEAD, category)
247248 base = rel_jsons(BASE, category)
@@ -250,6 +251,11 @@ jobs:
250251 modified_keys = sorted(
251252 key for key in set(head) & set(base) if digest(head[key]) != digest(base[key])
252253 )
254+ changed_by_category[category] = {
255+ "added": added_keys,
256+ "modified": modified_keys,
257+ "deleted": deleted_keys,
258+ }
253259 added_verified = added_unverified = added_kaggle = 0
254260 for key in added_keys:
255261 record = load_json(head[key])
@@ -260,14 +266,43 @@ jobs:
260266 added_unverified += 1
261267 if has_kaggle_source(record):
262268 added_kaggle += 1
263- lines .append(
269+ change_lines .append(
264270 f"| {category} | {len(added_keys)} | {len(modified_keys)} | {len(deleted_keys)} | "
265271 f"{added_verified} | {added_unverified} | {added_kaggle} |"
266272 )
267273
268- lines.append("")
269- lines.append("## Heuristic review")
270- lines.append("")
274+ def display_record(root: Path, rel: str) -> str:
275+ record = load_json(root / rel)
276+ name = record.get("name")
277+ label = name if isinstance(name, str) and name else rel
278+ return f"`{rel}` - {label}"
279+
280+ def append_examples(title: str, root: Path, keys: list[str], limit: int = 15) -> None:
281+ if not keys:
282+ return
283+ change_lines.append("")
284+ change_lines.append(f"### {title}")
285+ for rel in keys[:limit]:
286+ change_lines.append(f"- {display_record(root, rel)}")
287+ if len(keys) > limit:
288+ change_lines.append(f"- ... {len(keys) - limit} more")
289+
290+ change_lines.append("")
291+ change_lines.append("## Changed record examples")
292+ for category, changes in changed_by_category.items():
293+ append_examples(f"{category} added", HEAD, changes["added"])
294+ append_examples(f"{category} modified", HEAD, changes["modified"])
295+ append_examples(f"{category} deleted", BASE, changes["deleted"])
296+ if not any(
297+ changes["added"] or changes["modified"] or changes["deleted"]
298+ for changes in changed_by_category.values()
299+ ):
300+ change_lines.append("")
301+ change_lines.append("- No data file changes detected.")
302+
303+ change_lines.append("")
304+ change_lines.append("## Heuristic review")
305+ change_lines.append("")
271306 warnings: list[str] = []
272307 manufacturer_counter: Counter[str] = Counter()
273308 source_counter: Counter[str] = Counter()
@@ -285,20 +320,21 @@ jobs:
285320
286321 if manufacturer_counter:
287322 top = ", ".join(f"{name}: {count}" for name, count in manufacturer_counter.most_common(8))
288- lines .append(f"- Added records by manufacturer/brand: {top}")
323+ change_lines .append(f"- Added records by manufacturer/brand: {top}")
289324 if source_counter:
290325 top = ", ".join(f"{name}: {count}" for name, count in source_counter.most_common())
291- lines .append(f"- Added records by source class: {top}")
326+ change_lines .append(f"- Added records by source class: {top}")
292327
293328 if warnings:
294- lines .append(f"- Heuristic warnings: {len(warnings)} total; showing first {min(MAX_WARNINGS, len(warnings))}.")
295- lines .append("")
329+ change_lines .append(f"- Heuristic warnings: {len(warnings)} total; showing first {min(MAX_WARNINGS, len(warnings))}.")
330+ change_lines .append("")
296331 for warning in warnings[:MAX_WARNINGS]:
297- lines .append(f" - {warning}")
332+ change_lines .append(f" - {warning}")
298333 else:
299- lines .append("- Heuristic warnings: none found.")
334+ change_lines .append("- Heuristic warnings: none found.")
300335
301- Path("quality-summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
336+ Path("change-review.md").write_text("\n".join(change_lines) + "\n", encoding="utf-8")
337+ Path("data-stats.md").write_text("\n".join(stats_lines) + "\n", encoding="utf-8")
302338 PY
303339
304340 - name : Build PR comment
@@ -381,7 +417,7 @@ jobs:
381417
382418 {
383419 echo "<!-- techengine-pr-validation -->"
384- echo "## TechEngine validation : ${result}"
420+ echo "## TechEngine change review : ${result}"
385421 echo
386422 echo "- PR: #${TECHAPI_PR_NUMBER}"
387423 echo "- Ref: \`${TECHAPI_HEAD_REF:-detached}\`"
@@ -394,10 +430,22 @@ jobs:
394430 echo "| \`python -m app.validate\` | $([ "${{ steps.validate.outputs.app_status }}" = "0" ] && echo PASS || echo FAIL) |"
395431 echo "| \`python integrity_check.py TechAPI/data --strict\` | $([ "${{ steps.validate.outputs.integrity_status }}" = "0" ] && echo PASS || echo FAIL) |"
396432 echo
397- cat quality-summary.md
433+ cat change-review.md
434+ } > change-comment.md
435+
436+ {
437+ echo "<!-- techengine-pr-validation-stats -->"
438+ echo "## TechEngine validation stats: ${result}"
439+ echo
440+ echo "- PR: #${TECHAPI_PR_NUMBER}"
441+ echo "- Ref: \`${TECHAPI_HEAD_REF:-detached}\`"
442+ echo "- Commit: \`${short_sha}\`"
443+ echo "- Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
444+ echo
445+ cat data-stats.md
398446 echo
399447 cat validation-notes.md
400- } > comment.md
448+ } > stats- comment.md
401449
402450 - name : Comment on TechAPI PR
403451 if : env.TECHAPI_COMMENT_TOKEN != ''
@@ -406,19 +454,27 @@ jobs:
406454 shell : bash
407455 run : |
408456 set -euo pipefail
409- marker="<!-- techengine-pr-validation -->"
410- comment_id="$(gh api "repos/GetTechAPI/TechAPI/issues/${TECHAPI_PR_NUMBER}/comments" --paginate \
411- --jq ".[] | select(.body | contains(\"${marker}\")) | .id" | tail -n 1)"
412- jq -n --rawfile body comment.md '{body: $body}' > comment.json
413- if [ -n "$comment_id" ]; then
414- gh api "repos/GetTechAPI/TechAPI/issues/comments/${comment_id}" \
415- --method PATCH \
416- --input comment.json
417- else
418- gh api "repos/GetTechAPI/TechAPI/issues/${TECHAPI_PR_NUMBER}/comments" \
419- --method POST \
420- --input comment.json
421- fi
457+ upsert_comment() {
458+ local marker="$1"
459+ local body_file="$2"
460+ local payload_file="$3"
461+ local comment_id
462+ comment_id="$(gh api "repos/GetTechAPI/TechAPI/issues/${TECHAPI_PR_NUMBER}/comments" --paginate \
463+ --jq ".[] | select(.body | contains(\"${marker}\")) | .id" | tail -n 1)"
464+ jq -n --rawfile body "$body_file" '{body: $body}' > "$payload_file"
465+ if [ -n "$comment_id" ]; then
466+ gh api "repos/GetTechAPI/TechAPI/issues/comments/${comment_id}" \
467+ --method PATCH \
468+ --input "$payload_file"
469+ else
470+ gh api "repos/GetTechAPI/TechAPI/issues/${TECHAPI_PR_NUMBER}/comments" \
471+ --method POST \
472+ --input "$payload_file"
473+ fi
474+ }
475+
476+ upsert_comment "<!-- techengine-pr-validation -->" change-comment.md change-comment.json
477+ upsert_comment "<!-- techengine-pr-validation-stats -->" stats-comment.md stats-comment.json
422478
423479 - name : Warn when comment token is unset
424480 if : env.TECHAPI_COMMENT_TOKEN == ''
0 commit comments