268 lines
10 KiB
Python
268 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Prepare a field-ready install packet for LAB-003 truck battery disconnect work."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
CANDIDATE_STORES = [
|
|
"AutoZone — Newport or Claremont",
|
|
"Advance Auto Parts — Newport or Claremont",
|
|
"O'Reilly Auto Parts — Newport or Claremont",
|
|
]
|
|
|
|
REQUIRED_ITEMS = [
|
|
"battery terminal disconnect switch",
|
|
"terminal shim/post riser if needed",
|
|
]
|
|
|
|
SELECTION_CRITERIA = [
|
|
"Fits the truck battery post without forcing the clamp",
|
|
"Mounts on the negative battery terminal",
|
|
"Physically secure once tightened",
|
|
"no special tools required to operate",
|
|
]
|
|
|
|
INSTALL_CHECKLIST = [
|
|
"Verify the truck is off and keys are removed before touching the battery",
|
|
"Confirm the disconnect fits the negative battery terminal before final tightening",
|
|
"Install the disconnect on the negative battery terminal",
|
|
"Tighten until physically secure with no terminal wobble",
|
|
"Verify the disconnect can be opened and closed by hand",
|
|
]
|
|
|
|
VALIDATION_CHECKLIST = [
|
|
"Leave the truck parked with the disconnect opened for at least 24 hours",
|
|
"Reconnect the switch by hand the next day",
|
|
"Truck starts reliably after sitting 24+ hours with switch disconnected",
|
|
"Receipt or photo of installed switch uploaded to this issue",
|
|
]
|
|
|
|
BATTERY_REPLACEMENT_FOLLOWUP = (
|
|
"If the truck still fails the overnight test after the disconnect install, "
|
|
"replace battery and re-run the 24-hour validation."
|
|
)
|
|
|
|
|
|
def _as_bool(value: Any) -> bool | None:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bool):
|
|
return value
|
|
text = str(value).strip().lower()
|
|
if text in {"1", "true", "yes", "y"}:
|
|
return True
|
|
if text in {"0", "false", "no", "n"}:
|
|
return False
|
|
return None
|
|
|
|
|
|
def build_packet(details: dict[str, Any]) -> dict[str, Any]:
|
|
store_selected = (details.get("store_selected") or "").strip()
|
|
part_name = (details.get("part_name") or "").strip()
|
|
receipt_or_photo_path = (details.get("receipt_or_photo_path") or "").strip()
|
|
install_completed = _as_bool(details.get("install_completed"))
|
|
physically_secure = _as_bool(details.get("physically_secure"))
|
|
truck_started = _as_bool(details.get("truck_started_after_disconnect"))
|
|
replacement_needed = _as_bool(details.get("replacement_battery_needed"))
|
|
overnight_test_hours = details.get("overnight_test_hours")
|
|
part_cost_usd = details.get("part_cost_usd")
|
|
|
|
try:
|
|
overnight_test_hours = int(overnight_test_hours) if overnight_test_hours is not None else None
|
|
except (TypeError, ValueError):
|
|
overnight_test_hours = None
|
|
|
|
try:
|
|
part_cost_usd = float(part_cost_usd) if part_cost_usd is not None else None
|
|
except (TypeError, ValueError):
|
|
part_cost_usd = None
|
|
|
|
missing_fields: list[str] = []
|
|
if not store_selected:
|
|
missing_fields.append("store_selected")
|
|
if not part_name:
|
|
missing_fields.append("part_name")
|
|
if install_completed is not True:
|
|
missing_fields.append("install_completed")
|
|
if physically_secure is not True:
|
|
missing_fields.append("physically_secure")
|
|
if overnight_test_hours is None:
|
|
missing_fields.append("overnight_test_hours")
|
|
if truck_started is None:
|
|
missing_fields.append("truck_started_after_disconnect")
|
|
if not receipt_or_photo_path:
|
|
missing_fields.append("receipt_or_photo_path")
|
|
|
|
ready_to_operate_without_tools = True
|
|
|
|
if replacement_needed is True or truck_started is False:
|
|
status = "battery_replace_candidate"
|
|
elif not store_selected or not part_name:
|
|
status = "pending_parts_run"
|
|
elif install_completed is not True:
|
|
status = "pending_install"
|
|
elif physically_secure is not True or overnight_test_hours is None or truck_started is None or not receipt_or_photo_path:
|
|
status = "overnight_validation"
|
|
elif overnight_test_hours >= 24 and truck_started is True:
|
|
status = "verified"
|
|
else:
|
|
status = "overnight_validation"
|
|
|
|
return {
|
|
"candidate_stores": list(CANDIDATE_STORES),
|
|
"required_items": list(REQUIRED_ITEMS),
|
|
"selection_criteria": list(SELECTION_CRITERIA),
|
|
"install_target": "negative battery terminal",
|
|
"install_checklist": list(INSTALL_CHECKLIST),
|
|
"validation_checklist": list(VALIDATION_CHECKLIST),
|
|
"store_selected": store_selected,
|
|
"part_name": part_name,
|
|
"part_cost_usd": part_cost_usd,
|
|
"install_completed": install_completed,
|
|
"physically_secure": physically_secure,
|
|
"overnight_test_hours": overnight_test_hours,
|
|
"truck_started_after_disconnect": truck_started,
|
|
"receipt_or_photo_path": receipt_or_photo_path,
|
|
"ready_to_operate_without_tools": ready_to_operate_without_tools,
|
|
"missing_fields": missing_fields,
|
|
"battery_replacement_followup": BATTERY_REPLACEMENT_FOLLOWUP,
|
|
"status": status,
|
|
}
|
|
|
|
|
|
def render_markdown(packet: dict[str, Any]) -> str:
|
|
part_cost = packet["part_cost_usd"]
|
|
cost_line = f"${part_cost:.2f}" if isinstance(part_cost, (int, float)) else "pending purchase"
|
|
overnight = packet["overnight_test_hours"]
|
|
overnight_line = f"{overnight} hours" if overnight is not None else "pending"
|
|
started = packet["truck_started_after_disconnect"]
|
|
if started is True:
|
|
started_line = "yes"
|
|
elif started is False:
|
|
started_line = "no"
|
|
else:
|
|
started_line = "pending"
|
|
|
|
lines = [
|
|
"# LAB-003 — Truck Battery Disconnect Install Packet",
|
|
"",
|
|
"No battery disconnect switch has been purchased or installed yet.",
|
|
"This packet turns the issue into a field-ready purchase / install / validation checklist while preserving what still requires live work.",
|
|
"",
|
|
"## Candidate Store Run",
|
|
"",
|
|
]
|
|
lines.extend(f"- {store}" for store in packet["candidate_stores"])
|
|
lines.extend([
|
|
"",
|
|
"## Required Items",
|
|
"",
|
|
])
|
|
lines.extend(f"- {item}" for item in packet["required_items"])
|
|
lines.extend([
|
|
"",
|
|
"## Selection Criteria",
|
|
"",
|
|
])
|
|
lines.extend(f"- {item}" for item in packet["selection_criteria"])
|
|
lines.extend([
|
|
"",
|
|
"## Live Purchase State",
|
|
"",
|
|
f"- Store selected: {packet['store_selected'] or 'pending'}",
|
|
f"- Part selected: {packet['part_name'] or 'pending'}",
|
|
f"- Part cost: {cost_line}",
|
|
"",
|
|
"## Installation Target",
|
|
"",
|
|
f"- Install location: {packet['install_target']}",
|
|
f"- Ready to operate without tools: {'yes' if packet['ready_to_operate_without_tools'] else 'no'}",
|
|
"",
|
|
"## Install Checklist",
|
|
"",
|
|
])
|
|
lines.extend(f"- [ ] {item}" for item in packet["install_checklist"])
|
|
lines.extend([
|
|
"",
|
|
"## Validation Checklist",
|
|
"",
|
|
])
|
|
lines.extend(f"- [ ] {item}" for item in packet["validation_checklist"])
|
|
lines.extend([
|
|
"",
|
|
"## Overnight Verification Log",
|
|
"",
|
|
f"- Install completed: {packet['install_completed'] if packet['install_completed'] is not None else 'pending'}",
|
|
f"- Physically secure: {packet['physically_secure'] if packet['physically_secure'] is not None else 'pending'}",
|
|
f"- Overnight disconnect duration: {overnight_line}",
|
|
f"- Truck started after disconnect: {started_line}",
|
|
f"- Receipt / photo path: {packet['receipt_or_photo_path'] or 'pending'}",
|
|
"",
|
|
"## Battery Replacement Fallback",
|
|
"",
|
|
packet['battery_replacement_followup'],
|
|
"",
|
|
"## Missing Live Fields",
|
|
"",
|
|
])
|
|
if packet["missing_fields"]:
|
|
lines.extend(f"- {field}" for field in packet["missing_fields"])
|
|
else:
|
|
lines.append("- none")
|
|
lines.extend([
|
|
"",
|
|
"## Honest next step",
|
|
"",
|
|
"Buy the disconnect switch, install it on the negative battery terminal, leave the truck disconnected for 24+ hours, and only close the issue after receipt/photo evidence and the overnight start result are attached.",
|
|
"",
|
|
])
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Prepare the LAB-003 battery disconnect install packet")
|
|
parser.add_argument("--store-selected", default="")
|
|
parser.add_argument("--part-name", default="")
|
|
parser.add_argument("--part-cost-usd", type=float, default=None)
|
|
parser.add_argument("--install-completed", action="store_true")
|
|
parser.add_argument("--physically-secure", action="store_true")
|
|
parser.add_argument("--overnight-test-hours", type=int, default=None)
|
|
parser.add_argument("--truck-started-after-disconnect", choices=["yes", "no"], default=None)
|
|
parser.add_argument("--receipt-or-photo-path", default="")
|
|
parser.add_argument("--replacement-battery-needed", action="store_true")
|
|
parser.add_argument("--output", default=None)
|
|
parser.add_argument("--json", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
packet = build_packet(
|
|
{
|
|
"store_selected": args.store_selected,
|
|
"part_name": args.part_name,
|
|
"part_cost_usd": args.part_cost_usd,
|
|
"install_completed": args.install_completed,
|
|
"physically_secure": args.physically_secure,
|
|
"overnight_test_hours": args.overnight_test_hours,
|
|
"truck_started_after_disconnect": args.truck_started_after_disconnect,
|
|
"receipt_or_photo_path": args.receipt_or_photo_path,
|
|
"replacement_battery_needed": args.replacement_battery_needed,
|
|
}
|
|
)
|
|
rendered = json.dumps(packet, indent=2) if args.json else render_markdown(packet)
|
|
|
|
if args.output:
|
|
output_path = Path(args.output).expanduser()
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
output_path.write_text(rendered, encoding="utf-8")
|
|
print(f"Battery disconnect packet written to {output_path}")
|
|
else:
|
|
print(rendered)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|