Files
pm3py/docs/plans/2026-03-17-table-compiler-update-plan.md
michael 5551aee7b1 docs: TableCompiler update implementation plan
4 tasks: update TableEntry to 120-byte format, compile_nxp_icode,
compile_slix2, wire auto-compile into SimSession.start_15693.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:25:40 -07:00

717 lines
23 KiB
Markdown

# TableCompiler Update Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Update TableEntry to the new 120-byte firmware format with EML actions and groups, add NXP ICODE and SLIX2 compiler methods, and wire automatic table compilation into SimSession.
**Architecture:** Replace old 132-byte TableEntry (with match_mask) with new 120-byte format (with eml_action, groups, set_auth). Add `compile_nxp_icode()` and `compile_slix2()` to TableCompiler. SimSession auto-compiles and uploads table after sim starts.
**Tech Stack:** Python, pytest, existing sim-framework worktree
**Working directory:** `/home/work/pm3py/.worktrees/sim-framework/`
**Test command:** `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
---
### Task 1: Update TableEntry to new 120-byte format
**Files:**
- Modify: `pm3py/sim/table_compiler.py`
- Modify: `tests/test_sim_table_compiler.py`
**Step 1: Write failing tests for new format**
Replace the `TestTableEntry` class in `tests/test_sim_table_compiler.py`:
```python
class TestTableEntry:
def test_serialize_size(self):
entry = TableEntry(match=b"\x26", response=b"\x04\x00")
raw = entry.serialize()
assert len(raw) == 120
def test_serialize_roundtrip(self):
entry = TableEntry(
match=b"\x26",
match_mode=0,
response=b"\x04\x00",
response_flags=0x01,
)
raw = entry.serialize()
parsed = TableEntry.deserialize(raw)
assert parsed.match == b"\x26"
assert parsed.response == b"\x04\x00"
assert parsed.match_mode == 0
assert parsed.response_flags == 0x01
def test_serialize_eml_fields(self):
entry = TableEntry(
match=b"\x02\xA1\x04",
match_mode=1,
response=bytes([0x00]),
eml_action=1,
eml_offset=2040,
eml_len=1,
eml_resp_insert=1,
)
raw = entry.serialize()
parsed = TableEntry.deserialize(raw)
assert parsed.eml_action == 1
assert parsed.eml_offset == 2040
assert parsed.eml_len == 1
assert parsed.eml_resp_insert == 1
def test_serialize_group_fields(self):
entry = TableEntry(
match=b"\x02\xB3\x04\x04",
response=bytes([0x00]),
group=1,
activate_groups=0x06,
set_auth=0x04,
)
raw = entry.serialize()
parsed = TableEntry.deserialize(raw)
assert parsed.group == 1
assert parsed.activate_groups == 0x06
assert parsed.set_auth == 0x04
def test_serialize_consume_flag(self):
entry = TableEntry(match=b"\x26", response=b"\x04\x00", flags=0x02)
raw = entry.serialize()
parsed = TableEntry.deserialize(raw)
assert parsed.flags & 0x02
def test_exact_match(self):
entry = TableEntry(match=b"\x26", response=b"\x04\x00")
assert entry.matches(b"\x26")
assert not entry.matches(b"\x52")
def test_prefix_match(self):
entry = TableEntry(match=b"\x93\x20", match_mode=1, response=b"\x01\x02\x03\x04\x04")
assert entry.matches(b"\x93\x20")
assert entry.matches(b"\x93\x20\xFF\xFF")
assert not entry.matches(b"\x95\x20")
```
**Step 2: Run tests to see them fail**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableEntry -v`
Expected: FAIL — old constructor expects positional args
**Step 3: Update TableEntry in table_compiler.py**
Replace the `TableEntry` dataclass, `ENTRY_SIZE`, remove `MATCH_MASKED`:
```python
# Match modes
MATCH_EXACT = 0
MATCH_PREFIX = 1
# Entry size in firmware format (must match sim_table_entry_t)
ENTRY_SIZE = 120
SIM_TABLE_MAX_MATCH = 32
SIM_TABLE_MAX_RESPONSE = 64
# Response flags
RESP_FLAG_CRC = 0x01
# Entry flags
ENTRY_FLAG_CONSUME = 0x02
# EML actions
EML_NONE = 0
EML_READ = 1
EML_WRITE = 2
EML_AES_CMAC = 3
# Config register offset in iso15_tag_t.data[]
CONFIG_REG_OFFSET = 2040
@dataclass
class TableEntry:
"""One entry in the firmware response table (120 bytes packed)."""
match: bytes
match_mode: int = MATCH_EXACT
response: bytes = b""
response_flags: int = 0
eml_action: int = 0
eml_offset: int = 0
eml_len: int = 0
eml_resp_insert: int = 0
cmd_data_offset: int = 0
cmd_data_len: int = 0
group: int = 0
activate_groups: int = 0
deactivate_groups: int = 0
set_auth: int = 0
clear_auth: int = 0
flags: int = 0
def serialize(self) -> bytes:
"""Pack into 120-byte firmware format."""
buf = bytearray(ENTRY_SIZE)
off = 0
# match[32] + match_len[1] + match_mode[1]
m = self.match[:SIM_TABLE_MAX_MATCH]
buf[off:off + len(m)] = m
off += SIM_TABLE_MAX_MATCH
buf[off] = len(m)
buf[off + 1] = self.match_mode
off += 2
# response[64] + response_len[1] + response_flags[1]
r = self.response[:SIM_TABLE_MAX_RESPONSE]
buf[off:off + len(r)] = r
off += SIM_TABLE_MAX_RESPONSE
buf[off] = len(r)
buf[off + 1] = self.response_flags
off += 2
# eml_action[1] + eml_offset[2 LE] + eml_len[1] + eml_resp_insert[1]
# + cmd_data_offset[1] + cmd_data_len[1]
buf[off] = self.eml_action
struct.pack_into("<H", buf, off + 1, self.eml_offset)
buf[off + 3] = self.eml_len
buf[off + 4] = self.eml_resp_insert
buf[off + 5] = self.cmd_data_offset
buf[off + 6] = self.cmd_data_len
off += 7
# group[1] + activate_groups[4 LE] + deactivate_groups[4 LE]
# + set_auth[1] + clear_auth[1]
buf[off] = self.group
struct.pack_into("<I", buf, off + 1, self.activate_groups)
struct.pack_into("<I", buf, off + 5, self.deactivate_groups)
buf[off + 9] = self.set_auth
buf[off + 10] = self.clear_auth
off += 11
# flags[1] + _pad[1]
buf[off] = self.flags
off += 2
assert off == ENTRY_SIZE
return bytes(buf)
@classmethod
def deserialize(cls, data: bytes) -> TableEntry:
"""Unpack from 120-byte firmware format."""
off = 0
match_len = data[SIM_TABLE_MAX_MATCH]
match = bytes(data[0:match_len])
match_mode = data[SIM_TABLE_MAX_MATCH + 1]
off = SIM_TABLE_MAX_MATCH + 2
resp_len = data[off + SIM_TABLE_MAX_RESPONSE]
response = bytes(data[off:off + resp_len])
response_flags = data[off + SIM_TABLE_MAX_RESPONSE + 1]
off += SIM_TABLE_MAX_RESPONSE + 2
eml_action = data[off]
eml_offset = struct.unpack_from("<H", data, off + 1)[0]
eml_len = data[off + 3]
eml_resp_insert = data[off + 4]
cmd_data_offset = data[off + 5]
cmd_data_len = data[off + 6]
off += 7
group = data[off]
activate_groups = struct.unpack_from("<I", data, off + 1)[0]
deactivate_groups = struct.unpack_from("<I", data, off + 5)[0]
set_auth = data[off + 9]
clear_auth = data[off + 10]
off += 11
flags = data[off]
return cls(
match=match, match_mode=match_mode,
response=response, response_flags=response_flags,
eml_action=eml_action, eml_offset=eml_offset,
eml_len=eml_len, eml_resp_insert=eml_resp_insert,
cmd_data_offset=cmd_data_offset, cmd_data_len=cmd_data_len,
group=group, activate_groups=activate_groups,
deactivate_groups=deactivate_groups,
set_auth=set_auth, clear_auth=clear_auth,
flags=flags,
)
def matches(self, cmd: bytes) -> bool:
"""Check if a command matches this entry."""
if self.match_mode == MATCH_EXACT:
return cmd == self.match
elif self.match_mode == MATCH_PREFIX:
return len(cmd) >= len(self.match) and cmd[:len(self.match)] == self.match
return False
```
**Step 4: Update all existing callers and tests**
Replace old positional constructor calls throughout `table_compiler.py` and `tests/test_sim_table_compiler.py`:
Old: `TableEntry(b"\x26", 0, b"", b"\x04\x00", 0)`
New: `TableEntry(match=b"\x26", response=b"\x04\x00")`
Old: `flags=0x01` (CRC)
New: `response_flags=0x01`
Old: `flags=0x02` (consume)
New: `flags=0x02` (unchanged — same field name, same bit)
Old: `match_mask=b""`
New: removed
Update `ResponseTable.serialize` test: `assert len(raw) == 240` (2 * 120, not 264)
Update `SimSession.upload_table` test: `assert len(call_args[0][1]) == 120` (not 132)
Update `from_trace`: remove `match_mask=b""` parameter
**Step 5: Run all tests**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
Expected: All pass
**Step 6: Commit**
```bash
cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
git commit --no-gpg-sign -m "feat: update TableEntry to 120-byte format with EML actions and groups"
```
---
### Task 2: Add compile_nxp_icode method
**Files:**
- Modify: `pm3py/sim/table_compiler.py`
- Modify: `tests/test_sim_table_compiler.py`
**Step 1: Write failing tests**
Append to `tests/test_sim_table_compiler.py`:
```python
from pm3py.sim.nxp_icode import NxpIcodeTag
from pm3py.sim.table_compiler import CONFIG_REG_OFFSET, EML_READ, EML_WRITE
class TestTableCompilerNxpIcode:
def test_compile_has_read_config_entries(self):
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_nxp_icode(tag)
# READ_CONFIG reg 0 (unaddressed normalized)
entry = table.lookup(bytes([0x02, 0xA1, 0x04, 0x00]))
assert entry is not None
assert entry.eml_action == EML_READ
assert entry.eml_offset == CONFIG_REG_OFFSET
assert entry.eml_len == 1
assert entry.eml_resp_insert == 1 # after flags byte
def test_compile_has_write_config_entries(self):
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_nxp_icode(tag)
entry = table.lookup(bytes([0x02, 0xA2, 0x04, 0x00]))
assert entry is not None
assert entry.eml_action == EML_WRITE
assert entry.eml_offset == CONFIG_REG_OFFSET
def test_compile_has_get_random(self):
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_nxp_icode(tag)
entry = table.lookup(bytes([0x02, 0xB2, 0x04]))
assert entry is not None
assert len(entry.response) == 3 # flags + 2 random bytes
def test_compile_all_8_config_regs(self):
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_nxp_icode(tag)
for reg in range(8):
entry = table.lookup(bytes([0x02, 0xA1, 0x04, reg]))
assert entry is not None, f"Missing READ_CONFIG reg {reg}"
assert entry.eml_offset == CONFIG_REG_OFFSET + reg
def test_config_regs_in_eml_range(self):
"""Config reg EML offsets must be within tag->data[2048]."""
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_nxp_icode(tag)
for entry in table.entries:
if entry.eml_action in (EML_READ, EML_WRITE):
assert entry.eml_offset + entry.eml_len <= 2048
```
**Step 2: Run to verify failure**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableCompilerNxpIcode -v`
Expected: FAIL — `AttributeError: type object 'TableCompiler' has no attribute 'compile_nxp_icode'`
**Step 3: Implement compile_nxp_icode**
Add to `TableCompiler` class in `table_compiler.py`:
```python
@staticmethod
def compile_nxp_icode(tag) -> ResponseTable:
"""Compile NXP ICODE/NTAG5 custom command entries.
Generates table entries for READ/WRITE_CONFIG and GET_RANDOM.
Config registers stored in EML at data[2040:2048].
All entries use unaddressed format (firmware normalizes addressed cmds).
"""
entries = []
num_regs = tag._config_regs if hasattr(tag, '_config_regs') else bytearray(8)
num_config = len(num_regs)
for reg in range(num_config):
# READ_CONFIG reg N: read 1 byte from EML, prefix with 0x00
entries.append(TableEntry(
match=bytes([0x02, 0xA1, 0x04, reg]),
match_mode=MATCH_EXACT,
response=bytes([0x00]),
eml_action=EML_READ,
eml_offset=CONFIG_REG_OFFSET + reg,
eml_len=1,
eml_resp_insert=1,
))
# WRITE_CONFIG reg N: write 1 byte from cmd to EML
entries.append(TableEntry(
match=bytes([0x02, 0xA2, 0x04, reg]),
match_mode=MATCH_EXACT,
response=bytes([0x00]),
eml_action=EML_WRITE,
eml_offset=CONFIG_REG_OFFSET + reg,
eml_len=1,
cmd_data_offset=4, # value byte at position 4 in normalized cmd
))
# GET_RANDOM: return pre-picked random
import os
random_bytes = os.urandom(2)
entries.append(TableEntry(
match=bytes([0x02, 0xB2, 0x04]),
match_mode=MATCH_PREFIX,
response=bytes([0x00]) + random_bytes,
))
return ResponseTable(entries=entries)
```
**Step 4: Run tests**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py -v`
Expected: All pass
**Step 5: Commit**
```bash
cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
git commit --no-gpg-sign -m "feat: add TableCompiler.compile_nxp_icode for config registers + GET_RANDOM"
```
---
### Task 3: Add compile_slix2 method
**Files:**
- Modify: `pm3py/sim/table_compiler.py`
- Modify: `tests/test_sim_table_compiler.py`
**Step 1: Write failing tests**
Append to `tests/test_sim_table_compiler.py`:
```python
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
class TestTableCompilerSlix2:
def test_compile_has_set_password(self):
tag = IcodeSlix2Tag(
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
read_password=0x12345678,
)
table = TableCompiler.compile_slix2(tag)
# SET_PASSWORD prefix match (error for wrong password)
fallback = [e for e in table.entries if
len(e.match) >= 3 and e.match[1] == 0xB3 and
e.match_mode == MATCH_PREFIX]
assert len(fallback) > 0
assert fallback[-1].response == bytes([0x01, 0x0F]) # error
def test_compile_set_password_sets_auth(self):
tag = IcodeSlix2Tag(
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
read_password=0x12345678,
)
table = TableCompiler.compile_slix2(tag)
# Correct password entry should set auth bit
auth_entries = [e for e in table.entries if e.set_auth != 0]
assert len(auth_entries) > 0
def test_compile_includes_nxp_entries(self):
"""SLIX2 table should include NXP base entries."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
table = TableCompiler.compile_slix2(tag)
# Should have READ_CONFIG from nxp_icode compiler
entry = table.lookup(bytes([0x02, 0xA1, 0x04, 0x00]))
assert entry is not None
def test_compile_privacy_mode_groups(self):
"""Privacy mode should use groups to disable inventory."""
tag = IcodeSlix2Tag(
uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
privacy_password=0xAABBCCDD,
)
table = TableCompiler.compile_slix2(tag)
# Should have a SET_PASSWORD entry that activates a group
privacy_entries = [e for e in table.entries
if e.activate_groups != 0]
assert len(privacy_entries) > 0
```
**Step 2: Run to verify failure**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestTableCompilerSlix2 -v`
Expected: FAIL — `AttributeError: 'TableCompiler' has no attribute 'compile_slix2'`
**Step 3: Implement compile_slix2**
Add to `TableCompiler` class:
```python
@staticmethod
def compile_slix2(tag) -> ResponseTable:
"""Compile ICODE SLIX2 entries: passwords + privacy + NXP base.
Groups:
0 = always active
1 = normal operation (active unless privacy mode)
"""
# Start with NXP base entries (config regs, GET_RANDOM)
table = TableCompiler.compile_nxp_icode(tag)
# Password auth bits: read=0x01, write=0x02, privacy=0x04, destroy=0x08
PWD_AUTH_BITS = {0x01: 0x01, 0x02: 0x02, 0x04: 0x04, 0x08: 0x08}
# GET_RANDOM is already in the table — get the random bytes for XOR
get_rnd = [e for e in table.entries if len(e.match) >= 2 and e.match[1] == 0xB2]
random_bytes = get_rnd[0].response[1:3] if get_rnd else b"\x00\x00"
# SET_PASSWORD entries for each configured password
for pwd_id, password in tag._passwords.items():
if password is None:
continue
auth_bit = PWD_AUTH_BITS.get(pwd_id, 0)
# Compute XORed password bytes (as reader would send)
import struct as st
pwd_bytes = st.pack("<I", password)
xored = bytes([
pwd_bytes[0] ^ random_bytes[0],
pwd_bytes[1] ^ random_bytes[1],
pwd_bytes[2] ^ random_bytes[0],
pwd_bytes[3] ^ random_bytes[1],
])
# Correct password entry (exact match with XOR'd bytes)
entry_kwargs = dict(
match=bytes([0x02, 0xB3, 0x04, pwd_id]) + xored,
match_mode=MATCH_EXACT,
response=bytes([0x00]),
set_auth=auth_bit,
)
# Privacy password also activates group 1
if pwd_id == 0x04: # PWD_PRIVACY
entry_kwargs['activate_groups'] = (1 << 1)
table.entries.append(TableEntry(**entry_kwargs))
# SET_PASSWORD fallback (wrong password → error)
table.entries.append(TableEntry(
match=bytes([0x02, 0xB3, 0x04]),
match_mode=MATCH_PREFIX,
response=bytes([0x01, 0x0F]), # error
))
return table
```
**Step 4: Run tests**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py -v`
Expected: All pass
**Step 5: Commit**
```bash
cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/sim/table_compiler.py tests/test_sim_table_compiler.py
git commit --no-gpg-sign -m "feat: add TableCompiler.compile_slix2 with passwords and privacy groups"
```
---
### Task 4: Wire automatic table compilation into SimSession
**Files:**
- Modify: `pm3py/sim/sim_session.py`
- Modify: `tests/test_sim_table_compiler.py`
**Step 1: Write failing test**
Append to `tests/test_sim_table_compiler.py`:
```python
class TestSimSessionTableCompile:
def test_start_15693_compiles_nxp_table(self):
"""start_15693 with compile=True should upload NXP table entries."""
from pm3py.sim.sim_session import SimSession, CMD_SIM_TABLE_UPLOAD
from unittest.mock import MagicMock
import time
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
# Mock serial port
mock_ser = MagicMock()
mock_ser.in_waiting = 0
mock_ser.read.return_value = b""
session = SimSession(port=mock_ser)
session.start_15693(tag, trace=False, compile=True)
# Should have written: SIM command + table upload frames
calls = mock_ser.write.call_args_list
assert len(calls) >= 2 # at least sim start + table upload
def test_start_15693_no_compile_by_default(self):
"""start_15693 without compile should not upload table."""
from pm3py.sim.sim_session import SimSession
from unittest.mock import MagicMock
tag = NxpIcodeTag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06")
mock_ser = MagicMock()
mock_ser.in_waiting = 0
mock_ser.read.return_value = b""
session = SimSession(port=mock_ser)
session.start_15693(tag, trace=False)
# Should have written only the SIM command (+ maybe EML sync)
# No table upload
calls = mock_ser.write.call_args_list
# Check no frame contains CMD_SIM_TABLE_UPLOAD
from pm3py.sim.sim_session import CMD_SIM_TABLE_UPLOAD
import struct
for call in calls:
frame = call[0][0]
if len(frame) >= 10:
cmd_in_frame = struct.unpack_from("<H", frame, 6)[0]
assert cmd_in_frame != CMD_SIM_TABLE_UPLOAD
```
**Step 2: Run to verify failure**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_sim_table_compiler.py::TestSimSessionTableCompile -v`
Expected: FAIL — `start_15693` doesn't accept `compile` parameter
**Step 3: Add compile support to start_15693**
In `sim_session.py`, modify `start_15693()`:
```python
def start_15693(self, tag, compile: bool = False, trace: bool = False) -> None:
```
After `self._active = True` and before the trace setup, add:
```python
if compile:
self._compile_and_upload_table(tag, ser)
```
Add the helper method:
```python
def _compile_and_upload_table(self, tag, ser) -> None:
"""Compile response table from tag model and upload to firmware."""
from .table_compiler import TableCompiler, ResponseTable
table = ResponseTable(entries=[])
# NXP ICODE base (config regs, GET_RANDOM)
if hasattr(tag, '_config_regs'):
nxp_table = TableCompiler.compile_nxp_icode(tag)
table.entries.extend(nxp_table.entries)
# SLIX2 (passwords, privacy)
if hasattr(tag, '_passwords'):
slix2_table = TableCompiler.compile_slix2(tag)
# compile_slix2 already includes nxp_icode, so use it instead
table = slix2_table
if not table.entries:
return
# Upload: first chunk has 4-byte initial_groups header
import struct
initial_groups = 0xFFFFFFFF
data = table.serialize()
max_payload = 512
# First frame: header + entries
header = struct.pack("<I", initial_groups)
entries_per_chunk = (max_payload - len(header)) // 120
first_data = data[:entries_per_chunk * 120]
payload = header + first_data
frame = encode_ng_frame(CMD_SIM_TABLE_UPLOAD, payload)
ser.write(frame)
import time
time.sleep(0.05)
if ser.in_waiting:
ser.read(ser.in_waiting)
# Remaining chunks
remaining = data[entries_per_chunk * 120:]
chunk_size = (max_payload // 120) * 120
for i in range(0, len(remaining), chunk_size):
chunk = remaining[i:i + chunk_size]
frame = encode_ng_frame(CMD_SIM_TABLE_UPDATE, chunk)
ser.write(frame)
time.sleep(0.05)
if ser.in_waiting:
ser.read(ser.in_waiting)
print(f"[SimSession] Uploaded {len(table.entries)} table entries")
```
**Step 4: Run tests**
Run: `cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v`
Expected: All pass
**Step 5: Commit**
```bash
cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/sim/sim_session.py tests/test_sim_table_compiler.py
git commit --no-gpg-sign -m "feat: auto-compile and upload response table in start_15693"
```