Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ body:
- cpu-variance.sh
- thermal-load.sh
- display-test.sh
- Runbook / agent flow
- run-shakedown.sh (orchestrator)
- Runbook
- Target preset loading
- Report rendering (JSON/MD)
- Other
Expand Down
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@

## Type of change

- [ ] **Calibration report submission** (`Reports/submissions/*.json` — see below)
- [ ] Target preset (`targets/*.json`)
- [ ] Generation calibration (`examples/<gen>-<year>/`)
- [ ] Script improvement (`Verification/scripts/`)
- [ ] Methodology change (runbook / pass-fail thresholds)
- [ ] Docs
- [ ] OSS hygiene (CI, templates, lint, etc.)

## Calibration report submission (skip if not applicable)

- [ ] I generated this via `./Verification/scripts/run-shakedown.sh --target <preset>` (not hand-rolled)
- [ ] I reviewed the submission JSON and confirmed no plaintext PII (serial, SSID, store name)
- [ ] Chip / RAM / chassis class: <!-- e.g. M5 Max / 64 GB / active-cooled-pro -->
- [ ] Overall verdict: <!-- PASS / FAIL -->

## Validation

If this changes a script or threshold, paste the JSON output of running the affected script on your Mac, plus your chip / RAM:
Expand Down
91 changes: 91 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,97 @@ jobs:
python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" "$py"
done

- name: Audit Reports/submissions/*.json for PII and schema shape
# Walks Reports/submissions/*.json. Each file must:
# - have a filename matching {YYYY-MM-DD}-{preset}-{hash4}.json
# - contain all SCHEMA.md top-level fields (schema_version, target, unit, phases, result, …)
# - have submission_safe: true
# - contain NO _raw_* fields anywhere (would leak SSIDs / paired BT IDs / USB serials)
# - contain NO raw_log_path (thermal-load.sh's tempfile pointer)
# - contain NO unit.serial_number (only serial_hash is allowed)
# Skips silently if no submission files exist (the directory is empty for non-submission PRs).
run: |
python3 <<'PYEOF'
import glob
import json
import os
import re
import sys

REQUIRED_TOP = ["schema_version", "shakedown_version", "timestamp",
"target", "unit", "phases", "result", "result_reason",
"submission_safe"]
REQUIRED_PHASES = ["0_preflight", "1_inventory", "2_battery", "3_sensors",
"4_cpu_variance", "5_thermal_load", "6_display",
"7_physical", "8_apple_diagnostics", "9_idle_drain"]
FILENAME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}-[a-z0-9-]+-[0-9a-f]{4}\.json$")

def find_keys(obj, predicate, path=""):
hits = []
if isinstance(obj, dict):
for k, v in obj.items():
sub = f"{path}.{k}" if path else k
if predicate(k):
hits.append(sub)
hits.extend(find_keys(v, predicate, sub))
elif isinstance(obj, list):
for i, v in enumerate(obj):
hits.extend(find_keys(v, predicate, f"{path}[{i}]"))
return hits

errors = []
files = sorted(glob.glob("Reports/submissions/*.json"))
if not files:
print("no submission files; nothing to audit")
sys.exit(0)

for path in files:
name = os.path.basename(path)
if not FILENAME_RE.match(name):
errors.append(f"{path}: filename does not match {{YYYY-MM-DD}}-{{preset}}-{{hash4}}.json")
continue
try:
with open(path) as f:
report = json.load(f)
except json.JSONDecodeError as e:
errors.append(f"{path}: invalid JSON: {e}")
continue

for field in REQUIRED_TOP:
if field not in report:
errors.append(f"{path}: missing required top-level field '{field}'")

phases = report.get("phases", {})
for p in REQUIRED_PHASES:
if p not in phases:
errors.append(f"{path}: missing required phase '{p}'")

if report.get("submission_safe") is not True:
errors.append(f"{path}: submission_safe must be true (got {report.get('submission_safe')!r})")

raw_keys = find_keys(report, lambda k: k.startswith("_raw_"))
for k in raw_keys:
errors.append(f"{path}: forbidden _raw_* field at '{k}' (may contain SSIDs/paired BT IDs)")

raw_log = find_keys(report, lambda k: k == "raw_log_path")
for k in raw_log:
errors.append(f"{path}: forbidden 'raw_log_path' field at '{k}'")

unit = report.get("unit", {})
if "serial_number" in unit:
errors.append(f"{path}: forbidden unit.serial_number (only serial_hash is allowed)")

if report.get("result") not in ("PASS", "FAIL"):
errors.append(f"{path}: result must be 'PASS' or 'FAIL' (got {report.get('result')!r})")

if errors:
print("Submission audit failed:", file=sys.stderr)
for e in errors:
print(f" - {e}", file=sys.stderr)
sys.exit(1)
print(f"audited {len(files)} submission file(s) — all clean")
PYEOF

- name: Verify markdown links resolve to existing files
# Walk every in-repo .md, parse ](relative/path) links, confirm each
# resolves to a file on disk. Skips http(s)://, mailto:, and
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
.claude/settings.local.json
.claude/worktrees/

# Run outputs (may contain serials, system info)
# Run outputs (may contain serials, system info).
# The orchestrator splits each run into:
# Reports/local/ — full output with _raw_* fields (gitignored)
# Reports/submissions/ — sanitized for PR submission (committable)
Reports/*
!Reports/.gitkeep
!Reports/SCHEMA.md
!Reports/submissions/
Reports/local/

# Local scratch
.macqa/
Expand Down
85 changes: 0 additions & 85 deletions AGENTS.md

This file was deleted.

17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to this project are documented here. Format follows [Keep a

## [Unreleased]

### Removed

- **`AGENTS.md` and `CLAUDE.md`** — the orchestrator (`./run`) handles all the automated phases end-to-end without an LLM, sudo and the y/N confirmation prompt mean an agent can't actually drive `./run` anyway, and the manual phases (display test, physical inspection, Apple Diagnostics, idle drain) are checklist work that a runbook covers directly. The cross-tool agent convention added churn without enabling anything. All agent framing dropped from README, Runbook, CONTRIBUTING, SECURITY, Reports/SCHEMA, targets/README, examples/m5-2026/README, Shakedown Brain, bug-report template, and cpu-variance.sh comments.

### Added — calibration submission workflow

- **`run-shakedown.sh` orchestrator** — wraps the four auto-runnable phases (inventory, battery, CPU variance, thermal load) end-to-end, prompts sudo upfront, aggregates into a SCHEMA-compliant JSON, and emits a sanitized submission copy alongside the full local copy. One command instead of five-plus, with the predictable `{YYYY-MM-DD}-{preset}-{hash4}.json` filename convention.
- **`--target` is optional** — running without a preset auto-detects chassis class from `system_profiler` (override with `CHASSIS_CLASS` env var), skips inventory asserts, and softens the battery "factory-fresh" checks (cycle count, ≥99% capacity) since those only make sense on a new-purchase verification. Real-degradation checks still apply. Filename becomes `{YYYY-MM-DD}-{chassis}-{memory}gb-{hash4}.json`.
- **`./run` convenience entry** — thin shim at the repo root, execs the orchestrator with all args forwarded. Means the public command is `./run` after `cd ~/mac-shakedown`, not the longer `./Verification/scripts/run-shakedown.sh`.
- **`--no-sudo` (alias `--skip-thermal`)** — skips Phase 5, which is the only phase that needs sudo. Phase 4 (variance) still runs and is the headline test. Half the runtime, no password.
- **Sudo keep-alive** — background loop refreshes `sudo -n true` every 60 s while the orchestrator runs. macOS default sudo timestamp is 5 min and Phase 4 on Intel takes ~8 min, so the user would otherwise hit a second password prompt mid-run.
- **Flame banner** — colored ASCII banner on startup, suppressed when stderr isn't a TTY (CI / piped output stays clean).
- **`Reports/local/` vs `Reports/submissions/` split** — full output with `_raw_*` debug fields stays gitignored; sanitized copy is committable as a PR. Plaintext serials never reach the submission copy, even with `INCLUDE_PLAINTEXT_SERIAL=1`.
- **CI submission audit** — new step in `.github/workflows/lint.yml` triggered on PRs touching `Reports/submissions/**`. Rejects any `_raw_*` field, `raw_log_path`, plaintext `serial_number`, off-pattern filename, missing SCHEMA fields, or `submission_safe != true`.
- **README "Submit a calibration report" section** + CONTRIBUTING.md submission flow + PR-template checkbox for calibration submissions. Until a hosted aggregator exists, PRs are how the calibration corpus grows.
- **Soft inventory assert for `model_must_include`** — Apple Silicon's `machine_model` (e.g. `Mac17,1`) doesn't reliably contain the screen-size substring, so a mismatch now warns rather than fails. Chip and memory remain hard asserts (those are reliable from `system_profiler`).

### Added — methodology hardening

- **Phase 4 cold burst measurement** — first 5 s of parallel SHA-256 captured before warmup heats the chassis. Burst figure recorded as `burst_throughput_mb_per_s` for diagnostic comparison against the steady-state mean (advisory; doesn't drive verdict without a calibration baseline).
Expand Down
5 changes: 0 additions & 5 deletions CLAUDE.md

This file was deleted.

Loading