diff --git a/WATCHDOG.md b/WATCHDOG.md
index f6fde14..839100d 100644
--- a/WATCHDOG.md
+++ b/WATCHDOG.md
@@ -160,6 +160,11 @@ identical to the register.
| CONFIG-IDS | recorded | *mast_id is replaced by the column, base_plate, head_pitch and base_tilt IDs*: `supersed (superseded, supersedes)`, `replac`, `historical`, `legacy`, `no longer`
*the head_pitch baseline is hp30*: `supersed (superseded, supersedes)`, `historical`, `was` |
| NO-SUSPENSION | recorded | `3.0`, `no`, `not fitted`, `without` |
| DRIVE-TRACK | open | not scanned until the decision is taken |
+| UI-MANUAL-VELOCITY-INPUT | open | not scanned until the decision is taken |
+| STATUS-VOCABULARY | recorded | `not`, `never`, `only`, `hardwired`, `STOPPED \(SOFTWARE\)` |
+| STOP-SCOPE | recorded | *E-stop means only the hardwired buttons; the UI control is Stop (software), a functional stop*: `not`, `never`, `no`, `supersed (superseded, supersedes)`, `historical`
*Stop (software) covers base motion only today and is labelled base-only*: `target`, `planned`, `future`, `will`, `not`, `base[- ]only`
*Stop (software) is a functional stop, not a safety function*: `not`, `never`, `functional` |
+| OPERATOR-PANEL | recorded | *the operator panel is the Getac ZX80, 8 inch, 1920 x 1200, landscape*: `supersed (superseded, supersedes)`, `replac`, `historical`, `earlier`, `previous`, `was`
*the ZX80 operator panel is used in landscape*: `not`, `never` |
+| UI-GATE-A | recorded | `partial`, `A1`, `supersed (superseded, supersedes)`, `historical`, `was`, `6 Nov` |
## Approved public contacts
diff --git a/decisions.yaml b/decisions.yaml
index c2ecbf0..61d5c1c 100644
--- a/decisions.yaml
+++ b/decisions.yaml
@@ -53,12 +53,21 @@
schema_version: 1
in_force:
- source: P-03-rev19.6
- date: 2026-10-18
+ source: P-03-rev20.1
+ date: 2026-10-30
sources:
+ P-03-rev20.1:
+ title: "OpenAMRobot 2.0 Decision Addendum rev20.1, with the platform lead's decisions of 30 October 2026 (the addendum in force; carries the rev20.0 items 15, 17 and 18 head-camera values; item 24: whole chest layout, including the ZX80 operator panel correction of 28 October 2026)"
+ evidence: plan-set document, not held in any repository; provenance only, CI never reads it
+ Alex decision 2026-10-30:
+ title: Platform decision of 30 October 2026 (UI manual velocity input and stop lock, status vocabulary, stop scope, UI Gate A date); change request openAMRobot/.github#72
+ evidence: decision record, not held in any repository; provenance only, CI never reads it; recorded with P-03 rev20.1
+ E-01-rev1.5:
+ title: E-01 Safety and Power Distribution Baseline rev1.5 ("E-stop" means only the hardwired buttons)
+ evidence: plan-set document, not held in any repository; provenance only, CI never reads it
P-03-rev20.0:
- title: "OpenAMRobot 2.0 Decision Addendum rev20.0, 27 October 2026, with the 28 October 2026 corrections (items 15, 17 and 18: head-camera lens 42 mm ahead of the shoulder, baseline pitch 30 degrees, hp30; item 24: second E-stop on the static lift column)"
+ title: "OpenAMRobot 2.0 Decision Addendum rev20.0, 27 October 2026, with the 28 October 2026 corrections (earlier revision; superseded by rev20.1 as the addendum in force; items 15, 17 and 18: head-camera lens 42 mm ahead of the shoulder, baseline pitch 30 degrees, hp30; item 24: second E-stop on the static lift column)"
evidence: plan-set document, not held in any repository; provenance only, CI never reads it
Alex decision 2026-10-20:
title: Platform decision of 20 October 2026 (TF-Luna cliff pitch 10 degrees; WCM-500-24 on release 001 and 2.0; embodied-AI repository; platform-sw pin for the combined model)
@@ -83,7 +92,7 @@ sources:
title: P-03 Decision Addendum, revision 18.4 (earlier revision)
evidence: plan-set document, not held in any repository; cited by item
P-03-rev19.6:
- title: "OpenAMRobot 2.0 Decision Addendum rev19.6, 18 October 2026 (the addendum in force; item 22: Gigavac GX11CAC contactors and the item 22 controller I/O)"
+ title: "OpenAMRobot 2.0 Decision Addendum rev19.6, 18 October 2026 (earlier revision; superseded by rev20.1 as the addendum in force; item 22: Gigavac GX11CAC contactors and the item 22 controller I/O)"
evidence: plan-set document, not held in any repository; cited by item
E-01-rev1.3:
title: E-01 Safety and Power Distribution Baseline rev1.3, 18 October 2026 (detail for P-03 rev19.6 item 22)
@@ -661,7 +670,7 @@ decisions:
unit: none
date: 2026-10-28
review_by: 2026-12-09
- source: {document: P-03-rev20.0, item: "items 15 and 18 (28 Oct 2026 corrections); camera identity per P-03-rev18.7 item 15"}
+ source: {document: P-03-rev20.1, item: "items 15 and 18 (values of the P-03-rev20.0 28 Oct 2026 corrections, unchanged); camera identity per P-03-rev18.7 item 15"}
supersedes:
- {value: "lens 0.022 m ahead of and 0.223 m above the arm mount point", source: P-03-rev18.9 item 18}
- {value: "pitch 15 to 35 degrees down in 5 degree steps, baseline 25 degrees", source: P-03-rev18.7 item 15}
@@ -673,10 +682,10 @@ decisions:
message: the head camera is the Stereolabs ZED Mini, SKU ZED-121210 (P-03 rev18.7 item 15)
- pattern: '(?P\b(?:0\.022 m|22 mm) ahead\b)'
unless: 'supersed|historical|\bwas\b'
- message: the head-camera lens is 0.042 m (42 mm) ahead of the arm mount point (P-03 rev20.0 item 18)
+ message: the head-camera lens is 0.042 m (42 mm) ahead of the arm mount point (P-03 rev20.1 item 18)
- pattern: '(?P\b(?:head[- ]camera|ZED Mini)\b[^\n]{0,60}\bpitch\b[^\n]{0,40}\bbaseline:?\s*25\b(?!\s*mm))'
unless: 'supersed|historical|\bwas\b'
- message: the head-camera baseline pitch is 30 degrees down (P-03 rev20.0 item 15)
+ message: the head-camera baseline pitch is 30 degrees down (P-03 rev20.1 item 15)
verification:
machine: another ZED model (ZED 2, ZED 2i, ZED X, ZED X Mini) without a superseded or legacy label; the lens 0.022 m or 22 mm ahead; a head-camera pitch baseline of 25 degrees
human: {reviewer: platform-lead, evidence: supplier SKU on the OpenArm 2.0 set and the lift-carriage mount drawing with the pitch steps}
@@ -1270,7 +1279,7 @@ decisions:
unit: none
date: 2026-10-28
review_by: 2026-12-09
- source: {document: P-03-rev20.0, item: "item 17 (28 Oct 2026 corrections: head_pitch baseline hp30); column, base_plate and base_tilt per P-03-rev18.9 items 17 and 18"}
+ source: {document: P-03-rev20.1, item: "item 17 (head_pitch baseline hp30, value of the P-03-rev20.0 28 Oct 2026 corrections, unchanged); column, base_plate and base_tilt per P-03-rev18.9 items 17 and 18"}
supersedes:
- {value: "head_pitch hp15, hp20, hp25, hp30, hp35 (baseline hp25)", source: P-03-rev18.9 item 17}
- {value: "mast_id (mast_1300 to mast_1450)", source: P-03-rev18.2 shoulder height}
@@ -1281,7 +1290,7 @@ decisions:
message: mast_id is replaced by the column, base_plate, head_pitch and base_tilt IDs (P-03 rev18.9 item 17)
- pattern: '(?P\bhead_pitch\b[^\n]{0,60}\bbaseline:?\s*[`"]?(?:hp)?25\b|\bbaseline:?\s*[`"]?hp25\b)'
unless: 'supersed|historical|\bwas\b'
- message: the head_pitch baseline is hp30 (P-03 rev20.0 item 17)
+ message: the head_pitch baseline is hp30 (P-03 rev20.1 item 17)
verification:
machine: mast_id used as a current configuration ID; a head_pitch baseline of hp25
human: {reviewer: software-lead, evidence: configuration hash inputs and dataset metadata carrying the four IDs}
@@ -1327,3 +1336,149 @@ decisions:
machine: none while open
human: {reviewer: platform-lead, evidence: final hub-motor bracket design}
owner: platform-lead
+
+ - id: UI-MANUAL-VELOCITY-INPUT
+ title: "Proposed: UI manual velocity input and stop lock interface (source PR openamr-platform-sw #90 still open)"
+ summary: "Proposed: /ui/cmd_vel TwistStamped into twist_mux manual; /safety/stop_lock released only by stop_lock_release."
+ fix_hint: "Not scanned while Proposed; use /ui/cmd_vel (TwistStamped) and release the lock only via /safety/stop_lock_release."
+ kind: configuration
+ status: open
+ values:
+ - "Proposed until openamr-platform-sw #90 (head 710c98bc) merges and the owner accepts it; stored as status open because the schema has no Proposed status"
+ - UI manual velocity input /ui/cmd_vel, geometry_msgs/TwistStamped, into the twist_mux manual input
+ - stop lock state /safety/stop_lock, std_msgs/Bool, QoS reliable and transient local
+ - engaged by /safety/stop_lock_engage and released only by /safety/stop_lock_release, both std_srvs/Trigger
+ unit: none
+ date: 2026-10-30
+ review_by: 2026-12-11
+ source: {document: Alex decision 2026-10-30, item: "UI manual velocity input and stop lock; implementation openamr-platform-sw #90 head 710c98bc (open); P-03-rev20.1"}
+ applies_to:
+ repositories: ["openamrobot-ui", "openamr-platform-sw", "openamrobot-docs"]
+ files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.py", "**/*.ts", "**/*.tsx", "**/*.js"]
+ check:
+ - pattern: '(?P/ui/cmd_vel\b[^\n]{0,60}\bgeometry_msgs/(?:msg/)?Twist\b|\bgeometry_msgs/(?:msg/)?Twist\b[^\n]{0,60}/ui/cmd_vel\b)'
+ unless: 'supersed|historical|legacy|\bnot\b|\bwas\b'
+ message: /ui/cmd_vel carries geometry_msgs/TwistStamped, not unstamped Twist (Alex decision 2026-10-30)
+ - pattern: '(?P/safety/stop_lock_engage\b[^\n]{0,60}\breleas\w*|\breleas\w*[^\n]{0,60}/safety/stop_lock_engage\b)'
+ unless: 'stop_lock_release|\bonly\b|\bnot\b|never'
+ message: the stop lock is released only by /safety/stop_lock_release (Alex decision 2026-10-30)
+ - pattern: '(?P/safety/stop_lock\b(?!_)[^\n]{0,60}\b(?:best[ _-]?effort|volatile)\b)'
+ unless: '\bnot\b|never|supersed'
+ message: /safety/stop_lock is reliable and transient local (Alex decision 2026-10-30)
+ verification:
+ machine: none while open; when recorded, unstamped Twist on /ui/cmd_vel, the engage service described as releasing the lock, a best-effort or volatile /safety/stop_lock
+ human: {reviewer: software-lead, evidence: "merged openamr-platform-sw #90 with ros2 topic info -v showing /safety/stop_lock reliable and transient local, and the UI publishing TwistStamped on /ui/cmd_vel into the twist_mux manual input"}
+ owner: software-lead
+
+ - id: STATUS-VOCABULARY
+ title: Robot status and UI presentation state vocabulary
+ summary: "8 robot states; SAFETY_STOPPED only from the hardwired chain; UI adds DISCONNECTED, UNKNOWN, STALE, STOPPED (SOFTWARE)."
+ fix_hint: "Derive SAFETY_STOPPED only from the hardwired chain diagnostic; show a software stop as STOPPED (SOFTWARE)."
+ kind: distinction
+ status: recorded
+ values:
+ - robot states READY, EXECUTING, WAITING, BLOCKED, RECOVERING, NEEDS_ASSISTANCE, SAFETY_STOPPED, FAULTED
+ - UI presentation states DISCONNECTED, UNKNOWN, STALE, STOPPED (SOFTWARE); the UI derives them and they are not robot states
+ - SAFETY_STOPPED comes only from the hardwired safety chain's diagnostic signal, never from the software stop, the stop lock, a watchdog or a timeout
+ unit: none
+ date: 2026-10-30
+ review_by: 2026-12-11
+ source: {document: Alex decision 2026-10-30, item: "status vocabulary; P-03-rev20.1"}
+ applies_to:
+ repositories: ["openamrobot-ui", "openamr-platform-sw", "openamr-upperbody-sw", "openamrobot-docs"]
+ files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.py", "**/*.ts", "**/*.tsx", "**/*.js"]
+ check:
+ - pattern: '(?P\bSAFETY_STOPPED\b[^\n]{0,60}\b(?:stop_lock|software stop|Stop \(software\)|watchdog|timeout|heartbeat)\b|\b(?:stop_lock|software stop|Stop \(software\)|watchdog|timeout|heartbeat)\b[^\n]{0,60}\bSAFETY_STOPPED\b)'
+ unless: '\bnot\b|never|\bonly\b|hardwired|STOPPED \(SOFTWARE\)'
+ message: SAFETY_STOPPED comes only from the hardwired chain's diagnostic signal; a software stop is shown as STOPPED (SOFTWARE) (Alex decision 2026-10-30)
+ verification:
+ machine: SAFETY_STOPPED derived from the stop lock, a software stop, a watchdog, a timeout or a heartbeat
+ human: {reviewer: platform-lead, evidence: "status mapping source in platform-sw and the UI showing SAFETY_STOPPED driven only by the hardwired chain diagnostic input; the hardwired safety-chain test record, never a software PASS"}
+ owner: platform-lead
+
+ - id: STOP-SCOPE
+ title: Software stop scope and the meaning of E-stop
+ summary: "Stop (software) is a robot-side functional stop, base-only today; E-stop means only the hardwired buttons."
+ fix_hint: "Label the software stop Stop (software), base-only; keep E-stop for the hardwired buttons only."
+ kind: distinction
+ status: recorded
+ values:
+ - "\"Stop (software)\" is a robot-side functional stop, not a safety function"
+ - today it covers base motion only and is labelled base-only
+ - "target: one robot-side stop for base, arms and lift, designed by the platform lead and the safety owner (not yet designed)"
+ - "\"E-stop\" means only the hardwired buttons (E-01 rev1.5); see ESTOP-SCOPE and SAFETY-PROCUREMENT"
+ unit: none
+ date: 2026-10-30
+ review_by: 2026-12-11
+ source: {document: Alex decision 2026-10-30, item: "stop scope; E-stop meaning per E-01-rev1.5; P-03-rev20.1"}
+ applies_to:
+ repositories: ["openamrobot-ui", "openamr-platform-sw", "openamr-upperbody-sw", "openamrobot-docs"]
+ files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.ts", "**/*.tsx", "**/*.js"]
+ check:
+ - pattern: '(?P\b(?:software|soft|UI|virtual|on-?screen|panel|app)[ -]E-?stop\b)'
+ unless: '\bnot\b|never|\bno\b|supersed|historical'
+ message: E-stop means only the hardwired buttons; the UI control is Stop (software), a functional stop (E-01 rev1.5)
+ - pattern: '(?P\bStop \(software\)[^\n]{0,60}\b(?:arms?|lift|whole robot|all motion)\b)'
+ unless: 'target|planned|future|\bwill\b|\bnot\b|base[- ]only'
+ message: Stop (software) covers base motion only today and is labelled base-only (Alex decision 2026-10-30)
+ - pattern: '(?P\bStop \(software\)[^\n]{0,40}\bsafety (?:stop|function|rated|evidence)\b)'
+ unless: '\bnot\b|never|functional'
+ message: Stop (software) is a functional stop, not a safety function (Alex decision 2026-10-30)
+ verification:
+ machine: a software, UI, virtual or panel E-stop; Stop (software) claimed to stop arms or lift today; Stop (software) called a safety stop or function
+ human: {reviewer: platform-lead, evidence: "UI label showing Stop (software), base-only; platform-sw stop handling covering base motion; the hardwired safety-chain test record for the E-stop buttons"}
+ owner: platform-lead
+
+ - id: OPERATOR-PANEL
+ title: Operator panel
+ summary: "Operator panel: Getac ZX80, 8 inch, 1920 x 1200, landscape."
+ fix_hint: "Name the Getac ZX80 (8 inch, 1920 x 1200, landscape); label ZX10 material as superseded."
+ kind: value
+ status: recorded
+ value: Getac ZX80, 8 inch, 1920 x 1200, landscape
+ unit: none
+ date: 2026-10-28
+ review_by: 2026-12-09
+ source: {document: P-03-rev20.1, item: "item 24 (whole chest layout, including the ZX80 correction of 28 Oct 2026)"}
+ supersedes:
+ - {value: Getac ZX10, source: "earlier panel selection, not held in this register; replaced by P-03-rev20.1 item 24"}
+ applies_to:
+ repositories: ["openamrobot-ui", "openamrobot-docs"]
+ files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html", "**/*.json", "**/*.ts", "**/*.tsx", "**/*.css"]
+ check:
+ - pattern: '(?P\bZX10\b)'
+ unless: 'supersed|replac|historical|earlier|previous|\bwas\b'
+ message: the operator panel is the Getac ZX80, 8 inch, 1920 x 1200, landscape (P-03 rev20.1 item 24)
+ - pattern: '(?P\bZX80\b[^\n]{0,60}\bportrait\b|\bportrait\b[^\n]{0,60}\bZX80\b)'
+ unless: '\bnot\b|never'
+ message: the ZX80 operator panel is used in landscape (P-03 rev20.1 item 24)
+ verification:
+ machine: ZX10 without a superseded label; the ZX80 in portrait
+ human: {reviewer: software-lead, evidence: UI layout screenshots on the ZX80 at 1920 x 1200 landscape}
+ owner: platform-lead
+
+ - id: UI-GATE-A
+ title: UI Gate A date (a UI milestone; not the hardware Gate A, Teensy and MPU6500)
+ summary: "Full UI Gate A on 6 November 2026; 30 October 2026 was the partial A1 result."
+ fix_hint: "Give the full UI Gate A as 6 November 2026; call the 30 October result partial (A1)."
+ kind: value
+ status: recorded
+ values:
+ - full UI Gate A on 6 November 2026
+ - 30 October 2026 was the partial A1 result, not the full UI Gate A
+ - UI Gate A is an openamrobot-ui milestone; it is distinct from the hardware Gate A (Teensy, MPU6500) in BASE-CONTROLLER-GATES
+ unit: none
+ date: 2026-10-30
+ review_by: 2026-12-11
+ source: {document: Alex decision 2026-10-30, item: "UI Gate A date; P-03-rev20.1"}
+ applies_to:
+ repositories: ["openamrobot-ui", "openamrobot-docs"]
+ files: ["**/*.md", "**/*.yaml", "**/*.yml", "**/*.html"]
+ check:
+ - pattern: '(?P\bUI Gate A\b[^\n]{0,60}\b30 Oct(?:ober)?\b|\b30 Oct(?:ober)?\b[^\n]{0,60}\bUI Gate A\b)'
+ unless: 'partial|\bA1\b|supersed|historical|\bwas\b|6 Nov'
+ message: the full UI Gate A is on 6 November 2026; 30 October 2026 was the partial A1 result (Alex decision 2026-10-30)
+ verification:
+ machine: the full UI Gate A dated 30 October without a partial or A1 label
+ human: {reviewer: software-lead, evidence: UI Gate A test record dated 6 November 2026 and the A1 partial result of 30 October 2026}
+ owner: platform-lead
diff --git a/tests/test_check_decisions.py b/tests/test_check_decisions.py
index bb45abe..ab7fd58 100644
--- a/tests/test_check_decisions.py
+++ b/tests/test_check_decisions.py
@@ -7,6 +7,7 @@
import contextlib
import io
import os
+import re
import sys
import tempfile
import unittest
@@ -720,7 +721,7 @@ def test_lift_check_after_cross_repository_dry_run(self):
self.assertEqual(self.ids("docs/a.md", text, "x"), [], text)
def test_head_camera_lens_42_mm_ahead(self):
- # P-03 rev20.0 item 18 (28 Oct 2026): lens 42 mm ahead of the arm mount point, was 22 mm.
+ # P-03 rev20.1 item 18 (value of the rev20.0 28 Oct 2026 correction): lens 42 mm ahead of the arm mount point, was 22 mm.
repo = "openamrobot-docs"
for text in ("Head-camera lens 0.022 m ahead of and 0.223 m above the arm mount point.\n",
"The ZED Mini lens sits 22 mm ahead of the shoulder.\n"):
@@ -732,7 +733,7 @@ def test_head_camera_lens_42_mm_ahead(self):
self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
def test_head_camera_pitch_baseline_30(self):
- # P-03 rev20.0 items 15 and 17 (28 Oct 2026): baseline pitch 30 degrees, hp30; was 25 degrees, hp25.
+ # P-03 rev20.1 items 15 and 17 (values of the rev20.0 28 Oct 2026 corrections): baseline pitch 30 degrees, hp30; was 25 degrees, hp25.
repo = "openamr-upperbody-sw"
self.assertEqual(self.ids("docs/a.md", "Head-camera pitch 15 to 35 degrees down, baseline 25 degrees.\n", repo),
["HEAD-CAMERA-IDENTITY"])
@@ -762,6 +763,86 @@ def test_second_estop_is_not_the_openarm_kit_button(self):
"Historical: the OpenArm kit chest button.\n"):
self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+ def test_p03_rev20_1_is_in_force_and_cited(self):
+ # Owner decision 30 Oct 2026: P-03 rev20.1 is in force; head-camera values stay 30 degrees and hp30.
+ data = yaml.safe_load((ROOT / "decisions.yaml").read_text(encoding="utf-8"))
+ self.assertEqual(data["in_force"]["source"], "P-03-rev20.1")
+ self.assertIn("P-03-rev20.1", data["sources"])
+ by_id = {d["id"]: d for d in self.decisions}
+ for did in ("HEAD-CAMERA-IDENTITY", "CONFIG-IDS", "OPERATOR-PANEL"):
+ self.assertEqual(by_id[did]["source"]["document"], "P-03-rev20.1", did)
+ self.assertIn("baseline 30 degrees", " ".join(by_id["HEAD-CAMERA-IDENTITY"]["values"]))
+ self.assertIn("(baseline hp30)", " ".join(by_id["CONFIG-IDS"]["values"]))
+ self.assertTrue(by_id["OPERATOR-PANEL"]["source"]["item"].startswith("item 24"))
+ self.assertIn("ZX80", by_id["OPERATOR-PANEL"]["source"]["item"])
+
+ def test_ui_manual_velocity_input_is_proposed_and_not_scanned(self):
+ # Alex decision 30 Oct 2026; source PR openamr-platform-sw #90 open, so stored as open (Proposed).
+ by_id = {d["id"]: d for d in self.decisions}
+ entry = by_id["UI-MANUAL-VELOCITY-INPUT"]
+ self.assertEqual(entry["status"], "open")
+ self.assertTrue(entry["title"].startswith("Proposed:"))
+ bad = ("The UI publishes geometry_msgs/Twist on /ui/cmd_vel.\n",
+ "Call /safety/stop_lock_engage again to release the lock.\n",
+ "/safety/stop_lock uses best_effort QoS.\n")
+ good = ("The UI publishes geometry_msgs/TwistStamped on /ui/cmd_vel.\n",
+ "/safety/stop_lock_engage engages; /safety/stop_lock_release releases.\n",
+ "/safety/stop_lock is reliable and transient local.\n",
+ "/ui/cmd_vel is not geometry_msgs/Twist.\n")
+ for text in bad:
+ self.assertEqual(self.ids("docs/a.md", text, "openamrobot-ui"), [], text)
+ self.assertTrue(any(re.search(c["pattern"], text, re.IGNORECASE)
+ and not re.search(c.get("unless") or "$^", text, re.IGNORECASE)
+ for c in entry["check"]), text)
+ for text in good:
+ self.assertFalse(any(re.search(c["pattern"], text, re.IGNORECASE)
+ and not re.search(c.get("unless") or "$^", text, re.IGNORECASE)
+ for c in entry["check"]), text)
+
+ def test_safety_stopped_comes_only_from_the_hardwired_chain(self):
+ # Alex decision 30 Oct 2026: status vocabulary.
+ repo = "openamrobot-ui"
+ for text in ("Set SAFETY_STOPPED when /safety/stop_lock is true.\n",
+ "A heartbeat timeout raises SAFETY_STOPPED.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), ["STATUS-VOCABULARY"], text)
+ for text in ("SAFETY_STOPPED comes only from the hardwired chain diagnostic.\n",
+ "The stop lock shows STOPPED (SOFTWARE), not SAFETY_STOPPED.\n",
+ "Robot states: READY, EXECUTING, WAITING, BLOCKED, RECOVERING, NEEDS_ASSISTANCE, SAFETY_STOPPED, FAULTED.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+
+ def test_software_stop_is_base_only_and_not_an_estop(self):
+ # Alex decision 30 Oct 2026 and E-01 rev1.5: E-stop means only the hardwired buttons.
+ repo = "openamrobot-ui"
+ for text in ("Press the software E-stop on the panel.\n",
+ "The UI E-stop halts the robot.\n",
+ "Stop (software) halts the base, both arms and the lift.\n",
+ "Stop (software) is a safety function.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), ["STOP-SCOPE"], text)
+ for text in ("Stop (software), base-only: stops base motion.\n",
+ "Target: Stop (software) will cover base, arms and lift.\n",
+ "Stop (software) is a functional stop, not a safety function.\n",
+ "There is no software E-stop; E-stop means the hardwired buttons.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+
+ def test_operator_panel_is_the_getac_zx80(self):
+ # P-03 rev20.1 item 24: Getac ZX80, 8 inch, 1920 x 1200, landscape; supersedes ZX10.
+ repo = "openamrobot-ui"
+ for text in ("Operator panel: Getac ZX10.\n", "Layout for the ZX80 in portrait.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), ["OPERATOR-PANEL"], text)
+ for text in ("Operator panel: Getac ZX80, 8 inch, 1920 x 1200, landscape.\n",
+ "The ZX10 was replaced by the ZX80.\n",
+ "Getac ZX10 (superseded).\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+
+ def test_ui_gate_a_is_6_november(self):
+ # Alex decision 30 Oct 2026: full UI Gate A on 6 November 2026; 30 October was partial (A1).
+ repo = "openamrobot-docs"
+ self.assertEqual(self.ids("docs/a.md", "UI Gate A passed on 30 October 2026.\n", repo), ["UI-GATE-A"])
+ for text in ("Full UI Gate A on 6 November 2026.\n",
+ "30 October 2026: partial UI Gate A result (A1).\n",
+ "Gate A (Teensy, MPU6500) test log of 30 October 2026.\n"):
+ self.assertEqual(self.ids("docs/a.md", text, repo), [], text)
+
def test_legacy_label_exempts_compute(self):
self.assertEqual(self.ids("README.md", "Legacy build: Raspberry Pi 5.\n", "openamr-platform-hw"), [])
self.assertEqual(self.ids("README.md", "Compute: Raspberry Pi 5.\n", "openamr-platform-hw"), ["COMPUTE"])