| name | check-scc-compliance |
| description | Generates EXHAUSTIVE SCC compliance report checking all 44 rules (34 RULE + 10 IMPL) individually + implementation quality gaps (vs reference decoder) + 704 control codes + color handling (7 foreground + background) + style tags (<i>, <u>, bold, flash) with deep validations to identify ALL issues in pycaption code. |
check-scc-compliance
What this skill does
Generates a TRUE EXHAUSTIVE compliance report with:
- Deep Validation Analysis: Critical rules checked at function level (detect vs validate)
- Systematic Coverage: All 44 rules (34 RULE + 10 IMPL) individually checked with per-rule patterns
- Control Code Coverage: All code categories analyzed
- Color Handling: Foreground (7 colors) and background color read/write support
- Style Tag Support:
<i>, <u>, bold, flash/blink read/write/round-trip
- Test Coverage: Identifies missing tests
- Key Findings: Narrative summary of most important issues
Output: Single comprehensive report with ALL issues found
Usage:
/check-scc-compliance
Implementation
Run this Python script:
import os, re, glob
from datetime import datetime
print("=" * 60)
print("EXHAUSTIVE SCC COMPLIANCE CHECK")
print("=" * 60)
# ===== INIT =====
spec_files = glob.glob('ai_artifacts/specs/scc/scc_specs_summary*.md')
if not spec_files:
print("ERROR: No scc_specs_summary.md found")
raise SystemExit(1)
latest_spec = max(spec_files, key=os.path.getmtime)
with open(latest_spec) as _f: spec = _f.read()
reader_file = 'pycaption/scc/reader.py'
writer_file = 'pycaption/scc/writer.py'
const_file = 'pycaption/scc/constants.py'
init_file = 'pycaption/scc/__init__.py'
with open(reader_file) as _f: reader_content = _f.read()
with open(writer_file) as _f: writer_content = _f.read()
with open(const_file) as _f: constants_content = _f.read()
main_content = reader_content
all_code = reader_content + "\n" + writer_content + "\n" + constants_content
# Also check specialized_collections and state_machines
extra_files = [
'pycaption/scc/specialized_collections.py',
'pycaption/scc/state_machines.py',
]
for f in extra_files:
if os.path.exists(f):
with open(f) as _fh: all_code += "\n" + _fh.read()
print(f"[INIT] Spec: {latest_spec}")
print(f"[INIT] Reader: {reader_file} ({len(reader_content)} chars)")
print(f"[INIT] Writer: {writer_file} ({len(writer_content)} chars)")
print(f"[INIT] Total code: {len(all_code)} chars")
# Extract all rules from spec
rule_index = {}
for match in re.finditer(r'\*\*\[(RULE-[A-Z]+-\d{3}|IMPL-(?:[A-Z]+-)?\d{3})\]\*\*\s*(.+?)(?:\n|$)', spec):
rule_id = match.group(1)
rule_name = match.group(2).strip()
rule_start = match.start()
next_rule = re.search(r'\*\*\[(?:RULE-[A-Z]+-\d{3}|IMPL-(?:[A-Z]+-)?\d{3})\]\*\*', spec[rule_start + 1:])
rule_block = spec[rule_start:rule_start + 1 + next_rule.start()] if next_rule else spec[rule_start:]
level_match = re.search(r'\*\*Level:\*\*\s*(MUST NOT|MUST|SHOULD|MAY)', rule_block)
level = level_match.group(1) if level_match else 'UNKNOWN'
rule_index[rule_id] = {'name': rule_name, 'level': level}
print(f"[INIT] Extracted {len(rule_index)} rules from spec")
# ===== SANITY CHECK: Verify expected code landmarks exist =====
landmarks = {
'class SCCReader': ('pycaption/scc/reader.py', r'class\s+SCCReader\b'),
'class SCCWriter': ('pycaption/scc/writer.py', r'class\s+SCCWriter\b'),
'def detect (SCCReader)': ('pycaption/scc/reader.py', r'def\s+detect\b'),
'def read (SCCReader)': ('pycaption/scc/reader.py', r'def\s+read\b'),
'def write (SCCWriter)': ('pycaption/scc/writer.py', r'def\s+write\b'),
'COMMANDS dict': ('pycaption/scc/constants.py', r'COMMANDS\s*='),
'CHARACTERS dict': ('pycaption/scc/constants.py', r'CHARACTERS\s*='),
}
stale_warnings = []
for name, (expected_file, pattern) in landmarks.items():
try:
with open(expected_file) as _fh:
if not re.search(pattern, _fh.read()):
stale_warnings.append(f"{name} not found in {expected_file}")
except FileNotFoundError:
stale_warnings.append(f"{expected_file} does not exist")
if stale_warnings:
print(f"[SANITY] WARNING: {len(stale_warnings)} landmark(s) not found — patterns may be stale:")
for w in stale_warnings:
print(f" - {w}")
else:
print("[SANITY] All code landmarks found")
issues = {
'validation_gaps': [],
'partial_validation': [],
'missing': [],
'test_gaps': [],
}
# ===== PHASE 1: DEEP VALIDATION ANALYSIS =====
print("\n" + "=" * 60)
print("PHASE 1: DEEP VALIDATION ANALYSIS")
print("=" * 60)
deep_results = {}
# RULE-FMT-001: Header validation
has_detect = bool(re.search(r'def detect', main_content))
has_header_check = bool(re.search(r'lines\[0\]\s*==\s*HEADER|HEADER\s*==\s*lines\[0\]', main_content))
deep_results['RULE-FMT-001'] = {
'name': 'SCC header validation',
'detected': has_detect,
'validated': has_header_check,
'note': 'detect() checks lines[0] == HEADER (exact match)',
}
print(f" RULE-FMT-001: {'PASS' if has_header_check else 'FAIL'}")
# RULE-TMC-001: Timecode format
has_tc_regex = bool(re.search(r're\.match.*\\d\{2\}.*:\\d\{2\}.*:\\d\{2\}.*[:;].*\\d', main_content))
has_tc_error = bool(re.search(r'raise CaptionReadTimingError.*Timestamps should follow', main_content, re.DOTALL))
deep_results['RULE-TMC-001'] = {
'name': 'Timecode format validation',
'detected': has_tc_regex,
'validated': has_tc_error,
'note': 'Validates HH:MM:SS:FF/HH:MM:SS;FF via regex, raises CaptionReadTimingError',
}
print(f" RULE-TMC-001: {'PASS' if has_tc_error else 'FAIL'}")
# RULE-TMC-002: Frame rate boundary (OCTO-11561 added _validate_frame_numbers)
has_frame_parse = bool(re.search(r'time_split\[3\].*30\.0|int.*time_split\[3\]|_validate_frame_numbers', main_content))
has_frame_validate = bool(re.search(r'frames\s*>=\s*30|frame_number\s*>=\s*30|_validate_frame_numbers', main_content))
deep_results['RULE-TMC-002'] = {
'name': 'Frame rate boundary validation',
'detected': has_frame_parse,
'validated': has_frame_validate,
'note': '_validate_frame_numbers rejects frames >= 30 with CaptionReadTimingError.',
}
if has_frame_parse and not has_frame_validate:
issues['validation_gaps'].append({
'rule_id': 'RULE-TMC-002', 'name': 'Frame rate boundary validation',
'status': 'DETECTED_NOT_VALIDATED', 'severity': 'MUST',
'note': 'Code parses frame number (int(time_split[3]) / 30.0) but never checks frame < 30',
})
print(f" RULE-TMC-002: {'PASS' if has_frame_validate else 'VALIDATION GAP'}")
# RULE-TMC-003: Monotonic timecodes
has_monotonic_check = bool(re.search(r'prev.*time|last.*time|time.*<.*prev|time.*decreas', main_content, re.I))
has_monotonic_error = bool(re.search(r'raise.*monotonic|raise.*decreas|raise.*backward', main_content, re.I))
deep_results['RULE-TMC-003'] = {
'name': 'Monotonic timecode validation',
'detected': False,
'validated': False,
'note': 'No explicit monotonicity check. TimingCorrectingCaptionList adjusts end times silently.',
}
if not has_monotonic_error:
issues['validation_gaps'].append({
'rule_id': 'RULE-TMC-003', 'name': 'Monotonic timecode validation',
'status': 'NOT_IMPLEMENTED', 'severity': 'MUST',
'note': 'No code checks that timecodes increase. Silent timing adjustment is not validation.',
})
print(f" RULE-TMC-003: NOT_IMPLEMENTED")
# RULE-TMC-004: Drop-frame validation — ACCEPTED
# Code correctly uses ";" to apply DF time math (seconds_per_timestamp_second=1.0).
# It does NOT validate the DF invariant (frames 0,1 skipped at non-10th minutes).
# Accepted because: professional tools always generate valid DF, timing error from
# invalid frame is ~33ms (one frame), rejecting would break hand-edited files,
# and no other SCC parser validates this invariant.
has_df_detect = bool(re.search(r'";" in stamp|semicolon', main_content))
deep_results['RULE-TMC-004'] = {
'name': 'Drop-frame timecode handling',
'detected': has_df_detect,
'validated': True,
'note': 'DF detected via ";" and applied to time math. Invariant validation intentionally omitted — no practical risk.',
}
print(f" RULE-TMC-004: {'PASS — DF time math correct, invariant validation not needed' if has_df_detect else 'NOT FOUND'}")
# RULE-LAY-002: 32-character line limit
has_32_detect = bool(re.search(r'CaptionLineLengthError|textwrap\.fill.*32|len\(line\)\s*>\s*32', all_code))
has_32_error = bool(re.search(r'CaptionLineLengthError', reader_content))
has_32_writer = bool(re.search(r'textwrap\.fill.*32|SCC_TOKENS_PER_CAPTION_MAX', writer_content))
deep_results['RULE-LAY-002'] = {
'name': '32-character line limit',
'detected': has_32_detect,
'validated': has_32_error or has_32_writer,
'note': 'Reader raises CaptionLineLengthError, writer wraps/splits captions',
}
print(f" RULE-LAY-002: {'PASS' if (has_32_error or has_32_writer) else 'FAIL'}")
# RULE-LAY-003: 15-row maximum — ENFORCED BY FORMAT
# PAC byte encoding physically cannot address row > 15. No byte combination exists
# for row 16+. The constraint is inherent to CEA-608 encoding, not something that
# needs runtime validation.
has_15_row = bool(re.search(r'PAC_BYTES_TO_POSITIONING_MAP', all_code))
deep_results['RULE-LAY-003'] = {
'name': '15-row maximum',
'detected': has_15_row,
'validated': True,
'note': 'Enforced by PAC encoding — no byte combination exists for row > 15. No runtime check needed.',
}
print(f" RULE-LAY-003: {'PASS — enforced by PAC encoding' if has_15_row else 'NOT FOUND'}")
# RULE-ROLLUP-002: Base row accommodates depth
has_rollup_depth = bool(re.search(r'roll_rows_expected', main_content))
has_base_row_validate = bool(re.search(r'base.*row.*[<>]=?.*depth|row.*[<>]=?.*roll_rows|raise.*base.*row', main_content, re.I))
deep_results['RULE-ROLLUP-002'] = {
'name': 'Roll-up base row validation',
'detected': has_rollup_depth,
'validated': has_base_row_validate,
'note': 'Sets roll_rows_expected to 2/3/4 and limits roll_rows list, but does NOT check that PAC base row has enough rows above it.',
}
if has_rollup_depth and not has_base_row_validate:
issues['validation_gaps'].append({
'rule_id': 'RULE-ROLLUP-002', 'name': 'Roll-up base row validation',
'status': 'DETECTED_NOT_VALIDATED', 'severity': 'MUST',
'note': 'RU4 at row 2 only has 2 rows above, not 4. No error raised.',
})
print(f" RULE-ROLLUP-002: {'PASS' if has_base_row_validate else 'VALIDATION GAP'}")
# RULE-EDM-001: EDM must work in all modes (pop-on, paint-on, roll-up)
# The 942c handler must not be guarded by pop-on-only conditions
# Follow the dispatch: 942c → _cmd_erase_displayed() → analyze that method body
edm_dispatch = re.search(r'elif\s+word\s*==\s*["\']942c["\'](.+?)(?=elif\s+word|else:)', main_content, re.DOTALL)
edm_dispatch_code = edm_dispatch.group(0) if edm_dispatch else ''
# Extract the actual _cmd_erase_displayed method body
edm_method = re.search(r'def _cmd_erase_displayed\(self\).*?(?=\n def |\nclass |\Z)', main_content, re.DOTALL)
edm_method_code = edm_method.group(0) if edm_method else ''
# Combine dispatch + method body for full analysis
edm_handler_code = edm_dispatch_code + '\n' + edm_method_code
edm_pop_only = bool(re.search(r'942c.*and\s+self\.pop_ons_queue', main_content))
edm_handles_paint = bool(re.search(r'paint', edm_method_code, re.I)) or (
'buffer_dict' in edm_handler_code and 'paint' in edm_handler_code)
edm_handles_roll = bool(re.search(r'roll', edm_method_code, re.I)) or (
'buffer_dict' in edm_handler_code and 'roll' in edm_handler_code)
# Check if EDM flushes the active buffer generically (handles all modes)
edm_flushes_active = 'self.buffer' in edm_method_code or 'create_and_store' in edm_method_code or 'pop_ons_queue' in edm_method_code
edm_all_modes = (edm_handles_paint and edm_handles_roll) or (edm_flushes_active and not edm_pop_only)
deep_results['RULE-EDM-001'] = {
'name': 'EDM in all caption modes',
'detected': bool(re.search(r'"942c"', main_content)),
'validated': edm_all_modes,
'note': f'pop-on-only guard: {edm_pop_only}, handles paint: {edm_handles_paint}, handles roll: {edm_handles_roll}, generic flush: {edm_flushes_active}',
}
if not edm_all_modes:
severity_detail = []
if edm_pop_only:
severity_detail.append('guarded by pop_ons_queue (pop-on only)')
if not edm_handles_paint:
severity_detail.append('paint-on EDM ignored')
if not edm_handles_roll:
severity_detail.append('roll-up EDM ignored')
issues['validation_gaps'].append({
'rule_id': 'RULE-EDM-001', 'name': 'EDM ignored in paint-on and roll-up modes',
'status': 'MODE_RESTRICTED', 'severity': 'MUST',
'note': f'EDM (942c) handler only fires for pop-on: {"; ".join(severity_detail)}. '
'Per CEA-608, EDM is a global command that clears displayed memory in ALL modes.',
})
print(f" RULE-EDM-001: {'PASS' if edm_all_modes else 'MODE_RESTRICTED — pop-on only'}")
# General: scan for any command handler with mode-specific guards on global commands
global_commands = {'942c': 'EDM', '94ae': 'ENM', '9421': 'BS'}
mode_guards = re.findall(r'elif word == "([0-9a-f]{4})" and (self\.\w+)', main_content)
for cmd_code, guard in mode_guards:
if cmd_code in global_commands:
print(f" WARNING: Global command {global_commands[cmd_code]} ({cmd_code}) has mode guard: {guard}")
# IMPL-ZERO-001: caption.end zero-value — CORRECT SENTINEL
# end=0 is the explicit sentinel for "not yet assigned" throughout the SCC codebase:
# - _CaptionHolder.__init__ sets end=0
# - CaptionCreator checks `caption.end == 0` to detect unset end times
# - A real caption cannot end at microsecond 0 (before video starts)
# So `if caption.end:` is intentionally equivalent to "has end been assigned?"
has_end_truthiness = bool(re.search(r'if caption\.end:', main_content))
deep_results['IMPL-ZERO-001'] = {
'name': 'caption.end zero sentinel',
'detected': has_end_truthiness,
'validated': True,
'note': 'end=0 is the explicit sentinel for "not assigned". `if caption.end:` correctly means "has end time". No real caption ends at microsecond 0.',
}
print(f" IMPL-ZERO-001: {'PASS — 0 is intentional sentinel' if has_end_truthiness else 'NOT FOUND'}")
# IMPL-ERR-001: TypeError suppression in buffer.setter — UNREACHABLE
# buffer.setter catches TypeError if active_key is the unhashable {} sentinel.
# But SCCReader.__init__ calls set_active("pop") unconditionally before any data
# processing, so active_key is always "pop"/"paint"/"roll". Dead code guard.
has_type_error_pass = bool(re.search(r'@buffer\.setter.*?except TypeError:\s*\n\s+pass', main_content, re.DOTALL))
deep_results['IMPL-ERR-001'] = {
'name': 'TypeError guard in buffer.setter (dead code)',
'detected': has_type_error_pass,
'validated': True,
'note': 'Unreachable: set_active("pop") called in __init__ before any data arrives. active_key is never the sentinel during processing.',
}
print(f" IMPL-ERR-001: {'OK (unreachable guard)' if has_type_error_pass else 'NOT FOUND'}")
# IMPL-ERR-002: AttributeError in InstructionNodeCreator — FALSE POSITIVE
# The except AttributeError at line ~578 guards add_chars(" ") on non-text nodes
# when merging stash collections. This is SAFE — not related to position_tracker.
# position_tracker is always provided by the reader; if it were None, unguarded
# calls at lines 359/365/374 would crash (not silently fail).
deep_results['IMPL-ERR-002'] = {
'name': 'AttributeError in InstructionNodeCreator (safe guard)',
'detected': False,
'validated': True,
'note': 'except AttributeError guards add_chars on non-text nodes during stash merge. Not a position-tracking issue.',
}
print(f" IMPL-ERR-002: OK (safe guard for add_chars on non-text nodes)")
# IMPL-RO-001: Writer styling support
# Reader parses mid-row codes (italics, underline, colors) via interpret_command
# OCTO-11514: Writer now emits mid-row codes for italics and underline
has_writer_midrow = bool(re.search(r'mid.?row|italic|underline|MID_ROW', writer_content, re.I))
has_writer_pac_style = bool(re.search(r'PAC.*indent|_position_to_pac|_style_to_midrow', writer_content))
has_reader_midrow = bool(re.search(r'MID_ROW_CODES|STYLE_SETTING_COMMANDS|interpret_command', reader_content))
deep_results['IMPL-RO-001'] = {
'name': 'Writer styling support',
'detected': has_reader_midrow,
'validated': has_writer_midrow,
'note': 'Writer emits mid-row codes for italics/underline and PAC codes for positioning (OCTO-11514).' if has_writer_midrow else 'Writer outputs only PAC + characters — styling is lost.',
}
if has_reader_midrow and not has_writer_midrow:
issues['partial_validation'].append({
'rule_id': 'IMPL-RO-001', 'name': 'Writer drops all styling',
'status': 'READ_ONLY', 'severity': 'SHOULD',
'note': 'Reader parses mid-row codes but writer outputs only PAC + character data.',
})
print(f" IMPL-RO-001: {'PASS — writer emits mid-row codes' if has_writer_midrow else 'READ-ONLY — writer drops styling'}")
# IMPL-POS-001: Default position (14, 0)
# Row 14, col 0 is the correct CEA-608 default for captions without explicit PAC.
# This is spec-compliant behavior, not a silent failure.
has_default_pos = bool(re.search(r'default\s*=\s*\(14,\s*0\)', all_code))
deep_results['IMPL-POS-001'] = {
'name': 'Default position (14, 0)',
'detected': has_default_pos,
'validated': True,
'note': 'Row 14, col 0 is the CEA-608 default base row. Correct behavior when no PAC is present.',
}
print(f" IMPL-POS-001: {'PASS — correct CEA-608 default' if has_default_pos else 'DEFAULT NOT SET'}")
# ===== PHASE 1.5: IMPLEMENTATION QUALITY GAPS =====
# Beyond "does the code exist" — checks for things a reference implementation would handle.
print("\n" + "=" * 60)
print("PHASE 1.5: IMPLEMENTATION QUALITY GAPS")
print("=" * 60)
quality_gaps = []
# Q1: Unrecognized hex words silently discarded (no warning/diagnostic)
has_unknown_word_warn = bool(re.search(r'unknown.*word|unrecognized.*command|invalid.*hex|warning.*command', all_code, re.I))
has_unknown_word_log = bool(re.search(r'log.*unknown|warn.*unrecognized|CaptionReadWarning.*command', all_code, re.I))
if not has_unknown_word_warn and not has_unknown_word_log:
quality_gaps.append({
'id': 'QUAL-001', 'name': 'Unrecognized hex words silently discarded',
'severity': 'SHOULD',
'note': 'Hex words that match no character/command/PAC are silently skipped. '
'No warning or diagnostic emitted. A reference decoder would flag unknown codes.',
})
print(" QUAL-001: UNKNOWN HEX WORDS SILENTLY DROPPED")
else:
print(" QUAL-001: PASS")
# Q2: Channel 2 data silently ignored (only channel 1 processed)
has_channel_2_handling = bool(re.search(r'channel\s*==?\s*2|channel_2|_process_channel_2', all_code, re.I))
has_channel_2_warning = bool(re.search(r'warn.*channel\s*2|channel\s*2.*skip|channel\s*2.*ignor', all_code, re.I))
channel_1_only = bool(re.search(r'[Ff]irst match wins per key.*channel 1', constants_content))
if channel_1_only and not has_channel_2_handling and not has_channel_2_warning:
quality_gaps.append({
'id': 'QUAL-002', 'name': 'Channel 2 data silently ignored',
'severity': 'MAY',
'note': 'SCC can carry two channels. Constants only map channel 1 codes. '
'Channel 2 data is silently dropped without any diagnostic. '
'A production decoder would at least warn that channel 2 data was present.',
})
print(" QUAL-002: CHANNEL 2 SILENTLY IGNORED")
else:
print(" QUAL-002: PASS")
# Q3: No structured error diagnostics (only exceptions or silence)
has_structured_diag = bool(re.search(r'warnings\.warn|CaptionReadWarning', reader_content))
has_error_collection = bool(re.search(r'errors\s*=\s*\[\]|self\.errors|self\.warnings|diagnostic', reader_content, re.I))
if not has_structured_diag and not has_error_collection:
quality_gaps.append({
'id': 'QUAL-003', 'name': 'No structured error diagnostics',
'severity': 'SHOULD',
'note': 'Reader either raises an exception (halting) or silently accepts. '
'No middle ground for collecting issues like a validator would. '
'Malformed lines, unknown codes, timing anomalies all pass without feedback.',
})
print(" QUAL-003: NO STRUCTURED DIAGNOSTICS")
else:
print(" QUAL-003: PASS")
# Q4: Caption mode transitions not validated
has_mode_transition_check = bool(re.search(r'mode.*transition|invalid.*mode.*switch|current_mode.*!=', reader_content, re.I))
if not has_mode_transition_check:
quality_gaps.append({
'id': 'QUAL-004', 'name': 'Caption mode transitions not validated',
'severity': 'MAY',
'note': 'Switching from roll-up to pop-on (or vice versa) mid-stream without EDM is unusual '
'and may indicate a corrupt file. No warning is emitted for mode transitions. '
'CEA-608 best practices specify EDM before mode switches.',
})
print(" QUAL-004: MODE TRANSITIONS NOT VALIDATED")
else:
print(" QUAL-004: PASS")
# Q5: No max caption count / pop-on line count warnings
has_line_count_warn = bool(re.search(r'too many lines|line.*count.*>.*4|max.*lines.*pop|warn.*lines', all_code, re.I))
if not has_line_count_warn:
quality_gaps.append({
'id': 'QUAL-005', 'name': 'No line count warnings for pop-on captions',
'severity': 'MAY',
'note': 'Pop-on captions with more than 4 lines are technically displayable but unusual. '
'Roll-up is limited to 2/3/4 rows by spec. No warning for potential overflow.',
})
print(" QUAL-005: NO LINE COUNT WARNINGS")
else:
print(" QUAL-005: PASS")
# Q6: No timecode gap/discontinuity detection
has_gap_detect = bool(re.search(r'gap.*detect|discontinuity|timecode.*gap|time.*jump', all_code, re.I))
if not has_gap_detect:
quality_gaps.append({
'id': 'QUAL-006', 'name': 'No timecode gap/discontinuity detection',
'severity': 'MAY',
'note': 'Large gaps between timecodes (e.g. jumping from 00:01:00 to 01:00:00) may indicate '
'missing data or file corruption. No diagnostic emitted for discontinuities.',
})
print(" QUAL-006: NO GAP DETECTION")
else:
print(" QUAL-006: PASS")
# Q7: Invalid hex format not validated per-word
has_hex_validation = bool(re.search(r'len\(word\)\s*!=\s*4|not.*re\.match.*[0-9a-f]|invalid.*hex.*format', reader_content, re.I))
if not has_hex_validation:
quality_gaps.append({
'id': 'QUAL-007', 'name': 'No per-word hex format validation',
'severity': 'SHOULD',
'note': 'Each word in an SCC line should be exactly 4 hex characters (0-9, a-f). '
'Words with invalid characters or wrong length are silently skipped. '
'A validator would flag lines with non-hex content.',
})
print(" QUAL-007: NO HEX FORMAT VALIDATION")
else:
print(" QUAL-007: PASS")
# Q8: Empty/null byte pairs (0000) not differentiated from padding
has_null_handling = bool(re.search(r'word\s*==\s*"0000"|== "0000"|continue.*0000|skip.*null', reader_content, re.I))
if not has_null_handling:
quality_gaps.append({
'id': 'QUAL-008', 'name': 'Null byte pairs (0000) not explicitly handled',
'severity': 'MAY',
'note': 'The code 0000 means "no data" in SCC (padding). If not explicitly skipped, '
'it falls through to character lookup and silently fails. '
'Professional decoders explicitly skip 0000.',
})
print(" QUAL-008: NULL BYTES NOT EXPLICITLY HANDLED")
else:
print(" QUAL-008: PASS")
# Q9: No caption timing overlap detection
has_overlap_detect = bool(re.search(r'overlap.*detect|overlap.*warn|start.*<.*prev.*end', all_code, re.I))
if not has_overlap_detect:
quality_gaps.append({
'id': 'QUAL-009', 'name': 'No caption timing overlap detection',
'severity': 'MAY',
'note': 'Pop-on captions where start time overlaps with previous caption end time '
'may indicate timing errors. TimingCorrectingCaptionList silently adjusts '
'but no warning is emitted for the correction.',
})
print(" QUAL-009: NO OVERLAP DETECTION")
else:
print(" QUAL-009: PASS")
# Q10: Writer doesn't preserve caption mode on round-trip
has_mode_roundtrip = bool(re.search(r'caption_mode.*pop_on|caption_mode.*roll_up|mode.*=.*caption\.mode', writer_content, re.I))
# Check if the writer reads the caption_mode attribute from captions
writer_reads_mode = bool(re.search(r'caption_mode|caption\.mode|get.*mode', writer_content, re.I))
if not writer_reads_mode:
quality_gaps.append({
'id': 'QUAL-010', 'name': 'Writer infers caption mode instead of preserving it',
'severity': 'SHOULD',
'note': 'Reader tags captions with caption_mode (pop_on/roll_up/paint_on) but '
'writer re-classifies based on heuristics (_classify_captions). '
'Original mode is not preserved on round-trip if heuristic disagrees.',
})
print(" QUAL-010: WRITER INFERS MODE (not preserved)")
else:
print(" QUAL-010: PASS")
print(f" Quality gaps found: {len(quality_gaps)}")
# Add quality gaps to issues
for qg in quality_gaps:
issues['partial_validation'].append({
'rule_id': qg['id'], 'name': qg['name'],
'status': 'QUALITY_GAP', 'severity': qg['severity'],
'note': qg['note'],
})
# ===== PHASE 2: SYSTEMATIC RULE CHECK =====
print("\n" + "=" * 60)
print("PHASE 2: ALL RULES CHECK")
print("=" * 60)
# Per-rule patterns matching actual code constructs, not keywords
specific_patterns = {
'RULE-FMT-001': [r'def detect|HEADER'],
'RULE-TMC-001': [r're\.match.*\\d\{2\}.*:.*\\d\{2\}.*:.*\\d\{2\}|CaptionReadTimingError.*Timestamps'],
'RULE-TMC-002': [r'time_split\[3\].*30|int.*time_split\[3\]'],
'RULE-TMC-003': [r'monotonic|prev.*time.*>|time.*<.*prev|decreas'],
'RULE-TMC-004': [r'";" in stamp|drop.*frame|seconds_per_timestamp_second'],
'RULE-HEX-001': [r'len\(word\)\s*==\s*4|word\[:2\].*word\[2:\]'],
'RULE-HEX-002': [r'split\(" "\)|split\(\).*word_list|space.separated'],
'RULE-HEX-003': [r'_handle_double_command|doubled_types|last_command'],
'RULE-CHAR-001': [r'\bCHARACTERS\b'],
'RULE-CHAR-002': [r'\bSPECIAL_CHARS\b'],
'RULE-CHAR-003': [r'\bEXTENDED_CHARS\b'],
'RULE-POPON-001': [r'word == "9420"|set_active\("pop"\)|pop_ons_queue'],
'RULE-ROLLUP-001': [r'"9425"|"9426"|"94a7".*roll|buffer_dict.*set_active.*"roll"'],
'RULE-ROLLUP-002': [r'roll_rows_expected'],
'RULE-PAINTON-001': [r'word == "9429"|set_active\("paint"\)|Resume Direct Captioning'],
'RULE-EDM-001': [r'"942c"'],
'RULE-LAY-001': [r'PAC_BYTES_TO_POSITIONING_MAP|row.*1.*15|32.*column'],
'RULE-LAY-002': [r'CaptionLineLengthError|len\(line\)\s*>\s*32|textwrap\.fill.*32'],
'RULE-LAY-003': [r'PAC_BYTES_TO_POSITIONING_MAP|row.*15'],
'RULE-PAC-001': [r'PAC_BYTES_TO_POSITIONING_MAP|_is_pac_command'],
'RULE-PAC-002': [r'PAC_LOW_BYTE_BY_ROW_RESTRICTED|PAC_LOW_BYTE_BY_ROW|indent.*0.*4.*8'],
'RULE-TAB-001': [r'PAC_TAB_OFFSET_COMMANDS|97a1|97a2|9723|TO1|TO2|TO3'],
'RULE-FPS-001': [r'23\.976|film.*pulldown'],
'RULE-FPS-002': [r'\b24\s*fps|24\.0\s*fps'],
'RULE-FPS-003': [r'\b25\s*fps|PAL'],
'RULE-FPS-004': [r'29\.97|1001.*1000|NTSC.*non.*drop|seconds_per_timestamp_second'],
'RULE-FPS-005': [r'29\.97.*drop|drop.*frame|";" in stamp|seconds_per_timestamp_second\s*=\s*1\.0'],
'RULE-FPS-006': [r'\b30\.0\b|30\s*fps|/ 30\.0'],
'RULE-ENC-001': [r'parity_check|verify_parity|& 0x7f|0x7F'],
'RULE-ENC-002': [r'bit.*7|high.*bit|0x80'],
'RULE-MID-001': [r'MID_ROW_CODES|STYLE_SETTING_COMMANDS|interpret_command'],
'RULE-COLOR-001': [r'BACKGROUND_COLOR_CODES|STYLE_SETTING_COMMANDS|color.*attr'],
'RULE-COLOR-002': [r'BACKGROUND_COLOR_CODES'],
'RULE-XDS-001': [r'XDS|[Ff]ield\s*2'],
# Implementation rules
'IMPL-FMT-001': [r'def detect.*\n.*HEADER'],
'IMPL-TMC-001': [r're\.match.*\\d\{2\}|CaptionReadTimingError'],
'IMPL-TMC-003': [r'monotonic|prev.*time'],
'IMPL-HEX-003': [r'_handle_double_command'],
'IMPL-POPON-001': [r'"9420".*pop|pop_ons_queue'],
'IMPL-ROLLUP-001': [r'roll_rows_expected|roll_rows.*pop'],
'IMPL-PAINTON-001': [r'"9429".*paint|create_and_store'],
'IMPL-EDM-001': [r'"942c".*pop_ons_queue|"942c".*buffer'],
'IMPL-FPS-001': [r'30\.0|MICROSECONDS_PER_CODEWORD'],
'IMPL-ENC-001': [r'parity_check|verify_parity|& 0x7f|0x7F'],
}
# CONV rules are about a specific TTML conversion profile (SMPTE RP 2052-10).
# pycaption is a general-purpose converter, not a dedicated SCC-to-TTML profile implementation.
NOT_APPLICABLE_RULES = {
'RULE-CONV-001': 'TTML conversion profile — not applicable to general-purpose converter',
'RULE-CONV-002': 'TTML conversion profile — not applicable to general-purpose converter',
'RULE-CONV-003': 'TTML conversion profile — not applicable to general-purpose converter',
'RULE-CONV-004': 'TTML conversion profile — not applicable to general-purpose converter',
'RULE-CONV-005': 'TTML conversion profile — not applicable to general-purpose converter',
'IMPL-CONV-001': 'SMPTE RP 2052-10 region naming — not applicable',
'RULE-XDS-001': 'XDS is Field 2 metadata (ratings, program info), not captions. SCC only carries Field 1.',
'RULE-ENC-001': 'Parity is transport-layer (raw NTSC Line 21). SCC text files contain pre-stripped hex pairs.',
'RULE-ENC-002': 'Parity is transport-layer (raw NTSC Line 21). SCC text files contain pre-stripped hex pairs.',
}
missing_rules = []
found_rules = []
for rule_id, meta in sorted(rule_index.items()):
if rule_id in NOT_APPLICABLE_RULES:
found_rules.append(rule_id)
continue
# Skip rules covered in Phase 1 deep analysis
if rule_id in deep_results:
if deep_results[rule_id]['detected']:
found_rules.append(rule_id)
else:
if not any(i['rule_id'] == rule_id for i in issues['validation_gaps']):
missing_rules.append({
'rule_id': rule_id, 'name': meta['name'],
'level': meta['level'], 'status': 'MISSING',
})
continue
patterns = specific_patterns.get(rule_id, [])
if not patterns:
missing_rules.append({
'rule_id': rule_id, 'name': meta['name'],
'level': meta['level'], 'status': 'NO_PATTERN',
})
continue
found = any(re.search(p, all_code, re.I) for p in patterns)
if found:
found_rules.append(rule_id)
else:
missing_rules.append({
'rule_id': rule_id, 'name': meta['name'],
'level': meta['level'], 'status': 'MISSING',
})
issues['missing'] = missing_rules
must_missing = [r for r in missing_rules if r['level'] == 'MUST']
print(f" Found: {len(found_rules)}/{len(rule_index)}, Missing: {len(missing_rules)} (MUST: {len(must_missing)})")
# ===== PHASE 3: CONTROL CODE COVERAGE =====
print("\n" + "=" * 60)
print("PHASE 3: CONTROL CODE COVERAGE")
print("=" * 60)
# Count codes in constants.py (Field 1 / Channel 1 only — SCC standard)
all_hex_keys = set(re.findall(r'["\']([0-9a-fA-F]{4})["\'](?:\s*:|\s*[,\)\]])', constants_content))
# Categorize by pattern
misc_ctrl = set()
for code in ['9420', '9421', '9422', '9423', '9424', '9425', '9426', '94a7',
'9428', '9429', '942a', '942b', '942c', '94ad', '942e', '942f',
'97a1', '97a2', '9723']:
if code in all_hex_keys or code.lower() in constants_content.lower():
misc_ctrl.add(code)
# PAC codes: entries in PAC_BYTES_TO_POSITIONING_MAP
pac_count = 0
pac_section = re.search(r'PAC_BYTES_TO_POSITIONING_MAP\s*=\s*\{(.*?)\n\}', constants_content, re.DOTALL)
if pac_section:
pac_count = len(re.findall(r'["\'][0-9a-fA-F]{2}["\']', pac_section.group(1)))
_special_match = re.search(r'SPECIAL_CHARS\s*=\s*\{(.*?)\n\}', constants_content, re.DOTALL)
special_count = len(re.findall(r'"[0-9a-fA-F]{4}"', _special_match.group(1))) if _special_match else 0
_extended_match = re.search(r'EXTENDED_CHARS\s*=\s*\{(.*?)\n\}', constants_content, re.DOTALL)
extended_count = len(re.findall(r'"[0-9a-fA-F]{4}"', _extended_match.group(1))) if _extended_match else 0
print(f" Misc control codes: {len(misc_ctrl)}/19")
print(f" PAC low-byte entries: {pac_count}")
print(f" Special characters: {special_count}")
print(f" Extended characters: {extended_count}")
print(f" Total hex keys: {len(all_hex_keys)}")
# Frame rate support analysis
print("\n Frame rate support:")
has_2997_ndf = bool(re.search(r'1001.*1000|seconds_per_timestamp_second', main_content))
has_2997_df = bool(re.search(r'";" in stamp|seconds_per_timestamp_second\s*=\s*1\.0', main_content))
has_30_hardcode = bool(re.search(r'/ 30\.0|30\.0\b', main_content))
print(f" 29.97 NDF: {'YES' if has_2997_ndf else 'NO'}")
print(f" 29.97 DF: {'YES' if has_2997_df else 'NO'}")
print(f" 30fps hardcoded: {'YES' if has_30_hardcode else 'NO'}")
print(f" 23.976/24/25/30: NOT SUPPORTED (hardcoded to 30fps frame division)")
# Color handling analysis
print("\n Color handling:")
# Foreground colors: mid-row codes set foreground color per CEA-608
# 7 colors: white(9120), green(91a2), blue(91a4), cyan(9126), red(91a8), yellow(912a), magenta(912c)
fg_color_codes = {'9120': 'white', '91a2': 'green', '91a4': 'blue', '9126': 'cyan',
'91a8': 'red', '912a': 'yellow', '912c': 'magenta'}
fg_in_constants = sum(1 for c in fg_color_codes if c in constants_content)
# Check if reader extracts color into a color attribute (not just italic/underline classification)
has_color_attr_read = bool(re.search(r'color.*=.*green|color.*=.*red|attrs\[.color.\]|node.*color', all_code, re.I))
# Check if get_style_for_command returns color info (it only returns italic/underline/plaintext)
style_for_cmd = re.search(r'def get_style_for_command.*?(?=\n def |\nclass )', all_code, re.DOTALL)
style_for_cmd_code = style_for_cmd.group(0) if style_for_cmd else ''
color_in_style_cmd = bool(re.search(r'return.*color|"white"|"green"|"blue"|"cyan"|"red"|"yellow"|"magenta"', style_for_cmd_code))
# Writer color support
has_writer_color = bool(re.search(r'color.*mid.?row|MID_ROW.*color|_color_to_midrow|foreground.*color', writer_content, re.I))
# Background color handling
has_bg_codes = bool(re.search(r'BACKGROUND_COLOR_CODES', constants_content))
has_bg_handler = bool(re.search(r'_handle_background_color', all_code))
has_bg_preserved = bool(re.search(r'attrs\[.background_color.\]|node\.background_color|background_color\s*=\s*["\']', all_code, re.I))
print(f" Foreground color codes in constants: {fg_in_constants}/7")
print(f" Reader extracts color attribute: {'YES' if has_color_attr_read else 'NO — colors classified as plaintext'}")
print(f" get_style_for_command returns color: {'YES' if color_in_style_cmd else 'NO — only italic/underline/plaintext'}")
print(f" Writer emits color mid-row codes: {'YES' if has_writer_color else 'NO'}")
print(f" Background color codes defined: {'YES' if has_bg_codes else 'NO'}")
print(f" Background color handler exists: {'YES' if has_bg_handler else 'NO (space-strip only)'}")
print(f" Background color value preserved: {'YES' if has_bg_preserved else 'NO — stripped space only'}")
color_issues = []
if fg_in_constants == 7 and not has_color_attr_read:
color_issues.append('Foreground colors defined but not parsed into color attributes on read')
issues['partial_validation'].append({
'rule_id': 'IMPL-COLOR-001', 'name': 'Foreground color read support',
'status': 'CODES_DEFINED_NOT_PARSED', 'severity': 'SHOULD',
'note': 'All 7 foreground color mid-row codes exist in constants. get_style_for_command() classifies them as "plaintext" — color value not extracted into caption model.',
})
if not has_writer_color:
color_issues.append('Writer does not emit foreground color mid-row codes')
issues['partial_validation'].append({
'rule_id': 'IMPL-COLOR-002', 'name': 'Foreground color write support',
'status': 'NOT_IMPLEMENTED', 'severity': 'SHOULD',
'note': 'Writer emits MID_ROW_PLAIN/ITALIC/UNDERLINE only. No code maps caption color attributes to the 7 foreground color codes.',
})
if has_bg_handler and not has_bg_preserved:
color_issues.append('Background color codes strip space per spec but color value not preserved')
issues['partial_validation'].append({
'rule_id': 'IMPL-COLOR-003', 'name': 'Background color value not preserved',
'status': 'PARTIAL_HANDLER', 'severity': 'MAY',
'note': '_handle_background_color() strips trailing space (per CEA-608 rule) but does not store the background color value in the caption model.',
})
# Style tag analysis
print("\n Style tag support:")
# Italic <i>: Reader
has_italic_read = bool(re.search(r'_open_italics|_close_italics|create_italics_style', all_code))
has_italic_write = bool(re.search(r'MID_ROW_ITALIC|_MID_ROW_CODES', writer_content))
# Underline <u>: Reader
has_underline_classify = bool(re.search(r'return "underline"', all_code))
# Check if _handle_style_command actually processes underline (not just italic)
handle_style_cmd = re.search(r'def _handle_style_command.*?(?=\n def |\nclass )', all_code, re.DOTALL)
handle_style_code = handle_style_cmd.group(0) if handle_style_cmd else ''
underline_in_handler = bool(re.search(r'underline|command_style == "underline"', handle_style_code))
has_underline_write = bool(re.search(r'MID_ROW_UNDERLINE', writer_content))
# Bold <b>: not in CEA-608 (SCC_STYLES has "bold" as label but no implementation)
has_bold_code = bool(re.search(r'def.*bold|_open_bold|_close_bold|create_bold|BOLD_CODE', all_code, re.I))
# Flash/blink: COMMAND_LABELS has "Flash ON" label for 94a8, but check for actual handling
has_flash_code = bool(re.search(r'def.*flash|_handle_flash|flash_on|blink_on|create_flash', all_code, re.I))
print(f" <i> italic read: {'YES — _open_italics/_close_italics' if has_italic_read else 'NO'}")
print(f" <i> italic write: {'YES — MID_ROW_ITALIC codes' if has_italic_write else 'NO'}")
print(f" <u> underline classify: {'YES — get_style_for_command returns underline' if has_underline_classify else 'NO'}")
print(f" <u> underline handler: {'YES — explicit underline handling' if underline_in_handler else 'NO — else branch closes italics only'}")
print(f" <u> underline write: {'YES — MID_ROW_UNDERLINE code' if has_underline_write else 'NO'}")
print(f" <b> bold: {'N/A — not in CEA-608 spec' if not has_bold_code else 'FOUND (unexpected)'}")
print(f" flash/blink: {'FOUND' if has_flash_code else 'NOT IMPLEMENTED (CEA-608 has flash-on code)'}")
style_issues = []
if has_underline_classify and not underline_in_handler:
style_issues.append('Underline classified but not handled distinctly in reader')
issues['partial_validation'].append({
'rule_id': 'IMPL-STYLE-001', 'name': 'Underline read handling',
'status': 'CLASSIFIED_NOT_HANDLED', 'severity': 'SHOULD',
'note': 'get_style_for_command() returns "underline" but _handle_style_command() only has if=="italic"/else branches. Underline mid-row codes close italics without opening underline nodes.',
})
if not has_flash_code:
style_issues.append('Flash/blink not implemented (CEA-608 flash-on command exists)')
issues['partial_validation'].append({
'rule_id': 'IMPL-STYLE-002', 'name': 'Flash/blink not implemented',
'status': 'NOT_IMPLEMENTED', 'severity': 'MAY',
'note': 'CEA-608 defines a "flash on" mid-row code. Not implemented in reader or writer. Low priority — rarely used in production.',
})
# ===== PHASE 4: TEST COVERAGE =====
print("\n" + "=" * 60)
print("PHASE 4: TEST COVERAGE")
print("=" * 60)
test_files = glob.glob('tests/*scc*.py')
all_tests = ""
for tf in test_files:
if os.path.exists(tf):
with open(tf) as _fh: all_tests += _fh.read()
print(f" Test files: {len(test_files)} ({len(all_tests)} chars)")
test_checks = {
'RULE-FMT-001': [r'def test.*detect|def test.*header|Scenarist_SCC'],
'RULE-TMC-001': [r'def test.*timecode|def test.*timestamp|def test.*timing'],
'RULE-TMC-004': [r'def test.*drop.*frame|def test.*semicolon'],
'RULE-LAY-002': [r'def test.*length|def test.*32|CaptionLineLengthError'],
'RULE-ROLLUP-001': [r'def test.*roll.*up|def test.*RU'],
'RULE-POPON-001': [r'def test.*pop.*on|def test.*EOC'],
'RULE-PAINTON-001': [r'def test.*paint.*on|def test.*RDC'],
'RULE-EDM-001': [r'def test.*edm.*paint|def test.*942c.*paint|def test.*erase.*paint'],
}
for rid, patterns in test_checks.items():
if not any(re.search(p, all_tests, re.I) for p in patterns):
name = rule_index.get(rid, {}).get('name', rid)
issues['test_gaps'].append({'rule_id': rid, 'name': name, 'status': 'NO_TEST'})
print(f" {rid}: NO TEST")
else:
print(f" {rid}: HAS TEST")
# ===== PHASE 5: GENERATE REPORT =====
print("\n" + "=" * 60)
print("PHASE 5: GENERATE REPORT")
print("=" * 60)
os.makedirs("ai_artifacts/compliance_checks/scc", exist_ok=True)
date = datetime.now().strftime("%Y-%m-%d")
path = f"ai_artifacts/compliance_checks/scc/compliance_report_{date}.md"
total_issues = sum(len(v) for v in issues.values())
must_issues = (len([i for i in issues['validation_gaps'] if i.get('severity') == 'MUST']) +
len([i for i in issues['partial_validation'] if i.get('severity') == 'MUST']) +
len(must_missing))
sanity_section = ""
if stale_warnings:
sanity_section = "\n**STALE PATTERN WARNING**: The following expected code landmarks were not found. Some findings below may report features as 'missing' when they have actually been renamed or moved:\n"
for w in stale_warnings:
sanity_section += f"- {w}\n"
sanity_section += "\n"
# Pre-compute values that use backslashes (can't be inside f-strings in Python < 3.12)
_bg_section = re.search(r'BACKGROUND_COLOR_CODES.*?\]', constants_content, re.DOTALL)
_bg_code_count = len(re.findall(r'["\'][0-9a-fA-F]{4}["\']', _bg_section.group(0))) if _bg_section else 0
_color_reader_status = 'YES' if has_color_attr_read else 'NO'
_color_reader_detail = 'Colors parsed into caption model' if has_color_attr_read else 'get_style_for_command() classifies as plaintext only'
_color_writer_status = 'YES' if has_writer_color else 'NO'
_color_writer_detail = 'Maps color attrs to mid-row codes' if has_writer_color else 'Only emits plain/italic/underline mid-row codes'
_bg_codes_status = 'YES' if has_bg_codes else 'NO'
_bg_handler_detail = '_handle_background_color strips trailing space only' if has_bg_handler else 'Not handled'
_bg_preserved_status = 'Preserved' if has_bg_preserved else 'NOT preserved'
_bg_preserved_detail = 'Stored in caption model' if has_bg_preserved else 'Space stripped per spec but color value discarded'
_italic_read = 'YES' if has_italic_read else 'NO'
_italic_write = 'YES' if has_italic_write else 'NO'
_italic_rt = 'YES' if has_italic_read and has_italic_write else 'NO'
_italic_note = '_open_italics/_close_italics + MID_ROW_ITALIC' if has_italic_read and has_italic_write else ''
_underline_read = 'PARTIAL' if has_underline_classify and not underline_in_handler else ('YES' if has_underline_classify and underline_in_handler else 'NO')
_underline_write = 'YES' if has_underline_write else 'NO'
_underline_rt = 'NO — reader loses underline' if has_underline_classify and not underline_in_handler else ('YES' if has_underline_write else 'NO')
_underline_note = 'Classified but handler only toggles italic' if not underline_in_handler else 'Full support'
_flash_status = 'YES' if has_flash_code else 'NO'
_flash_note = '' if has_flash_code else 'CEA-608 defines flash-on code — not implemented'
report = f"""# SCC EXHAUSTIVE Compliance Report
**Generated**: {date}
**Spec**: {latest_spec}
**Analysis**: Deep Validation + Systematic Rules + Control Codes + Tests
**Implementation**: {reader_file}, {writer_file}, {const_file}
{sanity_section}
---
## Executive Summary
**Rules checked**: {len(rule_index)}/{len(rule_index)} (100%)
**Total issues**: {total_issues}
**MUST violations**: {must_issues}
| Category | Count |
|----------|-------|
| Validation gaps | {len(issues['validation_gaps'])} |
| Implementation quality gaps | {len(quality_gaps)} |
| Implementation caveats | {len(issues['partial_validation']) - len(quality_gaps)} |
| Missing rules | {len(issues['missing'])} (MUST: {len(must_missing)}) |
| Color handling gaps | {len(color_issues)} |
| Style tag gaps | {len(style_issues)} |
| Test gaps | {len(issues['test_gaps'])} |
---
## 1. Validation Gaps ({len(issues['validation_gaps'])})
Rules where the concept is detected but not properly validated.
"""
for g in issues['validation_gaps']:
report += f"### {g['rule_id']}: {g['name']}\n"
report += f"- **Status**: {g['status']}\n"
report += f"- **Severity**: {g['severity']}\n"
report += f"- **Note**: {g['note']}\n\n"
report += f"""---
## 2. Implementation Quality Gaps ({len(quality_gaps)})
Features that exist but are incomplete compared to a reference CEA-608 decoder.
These represent silent data loss or missing validation that affects reliability.
"""
for qg in quality_gaps:
report += f"### {qg['id']}: {qg['name']}\n"
report += f"- **Severity**: {qg['severity']}\n"
report += f"- **Note**: {qg['note']}\n\n"
# Filter out quality gaps from partial_validation for section 3
non_quality_caveats = [p for p in issues['partial_validation'] if not p['rule_id'].startswith('QUAL-')]
report += f"""---
## 3. Implementation Caveats ({len(non_quality_caveats)})
Rules implemented but with significant limitations.
"""
for p in non_quality_caveats:
report += f"### {p['rule_id']}: {p['name']}\n"
report += f"- **Status**: {p['status']}\n"
report += f"- **Note**: {p['note']}\n\n"
report += f"""---
## 4. Missing Rules ({len(issues['missing'])})
### MUST Rules ({len(must_missing)})
"""
for r in must_missing:
report += f"- **{r['rule_id']}**: {r['name']} ({r['status']})\n"
should_missing = [r for r in issues['missing'] if r['level'] == 'SHOULD']
may_missing = [r for r in issues['missing'] if r['level'] in ('MAY', 'MUST NOT')]
report += f"\n### SHOULD Rules ({len(should_missing)})\n\n"
for r in should_missing:
report += f"- **{r['rule_id']}**: {r['name']} ({r['status']})\n"
report += f"\n### MAY/MUST NOT Rules ({len(may_missing)})\n\n"
for r in may_missing:
report += f"- **{r['rule_id']}**: {r['name']} ({r['status']})\n"
report += f"\n### Not Applicable ({len(NOT_APPLICABLE_RULES)})\n\n"
for rid, reason in NOT_APPLICABLE_RULES.items():
report += f"- **{rid}**: {reason}\n"
report += f"""
---
## 5. Control Code Coverage
| Category | Found | Note |
|----------|-------|------|
| Misc control codes | {len(misc_ctrl)}/19 | RCL, BS, EDM, CR, EOC, RU2/3/4, etc. |
| PAC entries | {pac_count} | Positioning (rows 1-15, indents, colors) |
| Special characters | {special_count} | Two-byte special chars |
| Extended characters | {extended_count} | Spanish, French, German, Portuguese |
| Total hex keys | {len(all_hex_keys)} | All codes in constants.py |
## 6. Frame Rate Support
| Rate | Supported | How |
|------|-----------|-----|
| 23.976 fps | No | Not implemented |
| 24 fps | No | Not implemented |
| 25 fps | No | Not implemented |
| 29.97 NDF | **Yes** | Via `:` separator, 1001/1000 time factor |
| 29.97 DF | **Yes** | Via `;` separator, 1.0 time factor |
| 30 fps | Hardcoded | Frame division always uses `/ 30.0` |
**Note**: SCC is an NTSC format, so 29.97 DF/NDF is the primary use case. Missing support for other frame rates may be intentional.
## 7. Color Handling
| Aspect | Status | Detail |
|--------|--------|--------|
| Foreground color codes | {fg_in_constants}/7 defined | white, green, blue, cyan, red, yellow, magenta |
| Reader color extraction | {_color_reader_status} | {_color_reader_detail} |
| Writer color emission | {_color_writer_status} | {_color_writer_detail} |
| Background color codes | {_bg_codes_status} ({_bg_code_count} codes) | {_bg_handler_detail} |
| Background color value | {_bg_preserved_status} | {_bg_preserved_detail} |
## 8. Style Tag Support
| Tag | Read | Write | Round-trip | Note |
|-----|------|-------|------------|------|
| `<i>` italic | {_italic_read} | {_italic_write} | {_italic_rt} | {_italic_note} |
| `<u>` underline | {_underline_read} | {_underline_write} | {_underline_rt} | {_underline_note} |
| `<b>` bold | N/A | N/A | N/A | Not in CEA-608 spec |
| flash/blink | {_flash_status} | {_flash_status} | {_flash_status} | {_flash_note} |
---
## 9. Test Gaps ({len(issues['test_gaps'])})
"""
for t in issues['test_gaps']:
report += f"- **{t['rule_id']}**: {t['name']}\n"
report += f"""
---
## 10. Key Findings
1. **Timecode format is validated**: Regex checks HH:MM:SS:FF/HH:MM:SS;FF format, raises `CaptionReadTimingError` on bad format.
2. **Frame numbers ARE range-checked**: `_validate_frame_numbers()` rejects frames >= 30 with `CaptionReadTimingError` (OCTO-11561).
3. **Monotonic timecodes NOT checked**: No code compares current timecode to previous. `TimingCorrectingCaptionList` silently adjusts end times — that's correction, not validation.
4. **Drop-frame time math IS correct**: Code distinguishes DF vs NDF via `;` separator and applies correct time factor. DF invariant validation (frames 0,1 at non-10th minutes) intentionally omitted — no practical risk.
5. **32-char line limit IS validated**: Reader raises `CaptionLineLengthError`, writer wraps at 32 via `textwrap.fill`. Both directions covered.
6. **Roll-up base row NOT validated**: `roll_rows_expected` is set to 2/3/4, but no check that PAC base row has enough rows above it.
7. **Frame rate is 29.97 only**: Hardcoded `/ 30.0` for frame division, `1001/1000` for NDF factor. No support for 23.976, 24, 25, or true 30fps.
8. **Control code doubling IS handled**: `_handle_double_command` correctly skips redundant doubled commands.
9. **RU4 hex code `94a7` is CORRECT**: Per CEA-608 odd-parity encoding, `94a7` (not `9427`) is the correct RU4 code.
10. **EDM (942c) is pop-on only**: The Erase Displayed Memory handler is guarded by `and self.pop_ons_queue`, so it only fires in pop-on mode. In paint-on and roll-up, EDM is silently discarded. Per CEA-608, EDM is a global command that clears the screen in ALL modes.
11. **Foreground colors NOT extracted on read**: All 7 CEA-608 color codes (white, green, blue, cyan, red, yellow, magenta) are defined in constants. Reader's `get_style_for_command()` classifies them as "plaintext" — color information is discarded.
12. **Underline read is incomplete**: `get_style_for_command()` returns "underline" but `_handle_style_command()` only has italic/else branches — underline mid-row codes close italics without creating underline nodes.
13. **Writer color support is absent**: Writer only emits MID_ROW_PLAIN/ITALIC/UNDERLINE/ITALIC_UNDERLINE. No code maps caption color attributes to the 7 foreground color mid-row codes.
14. **{len(quality_gaps)} quality gaps vs reference decoder**: Unknown hex words silently discarded, no structured diagnostics, mode transitions unvalidated, null bytes not explicitly handled.
---
**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
**Rules**: {len(rule_index)} | **Found**: {len(found_rules)} | **Missing**: {len(issues['missing'])}
**Quality gaps**: {len(quality_gaps)} | **Validation gaps**: {len(issues['validation_gaps'])} | **Test gaps**: {len(issues['test_gaps'])}
"""
with open(path, 'w') as _f: _f.write(report)
print(f"\n Report: {path}")
print(f" Total issues: {total_issues} ({must_issues} MUST)")
Key improvements over previous version
- Removed false CTRL-008 bug:
94a7 for RU4 is correct per CEA-608 odd-parity encoding
- RULE-LAY-002 correctly assessed: Reader raises
CaptionLineLengthError, writer wraps at 32. Both validated.
- RULE-TMC-003 correctly assessed: No explicit monotonicity validation. Silent timing adjustment is NOT validation.
- Per-rule patterns: Matches actual function names (
_handle_double_command, CaptionLineLengthError) not broad keywords
- Color handling section: Verifies all 7 foreground colors + background color read/write/preservation
- Style tag section: Verifies
<i>, <u>, bold, flash/blink with read/write/round-trip matrix
- Frame rate analysis: Clearly reports which rates are supported (29.97 DF/NDF only)
- Expanded file scope: Also reads specialized_collections.py and state_machines.py
- Key findings section: Narrative summary with accurate assessments
- No inflated control code counts: Reports Field 1 codes only (SCC standard)
Success Criteria
- All spec rules individually checked with per-rule patterns
- Deep validation for 7 critical rules at function level
- Control code coverage by category (not inflated counts)
- Frame rate support clearly documented
- Color handling: all 7 foreground + background verified for read/write/preservation
- Style tags:
<i>, <u>, bold, flash verified for read/write/round-trip
- No false bug reports (94a7 is correct)
- Key findings narrative for actionable summary