|
| 1 | +# AI Agent Instructions for Sysdiagnose Analysis Framework |
| 2 | + |
| 3 | +This file provides context for AI coding assistants working on this codebase. |
| 4 | +Full developer documentation: `docs/developer_guidelines.md` |
| 5 | + |
| 6 | +## Project Structure |
| 7 | + |
| 8 | +``` |
| 9 | +src/sysdiagnose/ |
| 10 | +├── parsers/ # Parse raw sysdiagnose files into structured data |
| 11 | +├── analysers/ # Process parsed data to produce insights |
| 12 | +└── utils/ |
| 13 | + ├── base.py # BaseParserInterface, BaseAnalyserInterface, BaseInterface |
| 14 | + └── summary.py # ResultSummary, ResultSummaryExecutionHandler, ResultSummaryFactory |
| 15 | +tests/ # Unit tests (one per parser/analyser) |
| 16 | +docs/ # Documentation |
| 17 | +``` |
| 18 | + |
| 19 | +## Architecture |
| 20 | + |
| 21 | +- Parsers extend `BaseParserInterface`, analysers extend `BaseAnalyserInterface` |
| 22 | +- Constructor signature: `def __init__(self, config: SysdiagnoseConfig, case: dict)` |
| 23 | + - `case` is the full case metadata dict (contains `case_id`, `ios_version`, `model`, etc.) |
| 24 | + - `self.case_id` is derived automatically from `case.get("case_id")` |
| 25 | +- Only override `execute()` and `get_log_files()` (parsers) — the base class handles I/O, caching, and summary tracking |
| 26 | +- Override `_write_result()` only for custom output formats (CSV, GPX, KML) |
| 27 | +- Override `_load_output()` only for multi-file parsers |
| 28 | + |
| 29 | +## iOS Version Compatibility |
| 30 | + |
| 31 | +- Declare `ios_version = ">=17.0"` (PEP 440 specifier) on a parser/analyser class to restrict it to specific iOS versions |
| 32 | +- Default is `ios_version = "*"` (all versions) |
| 33 | +- `_execute_and_write()` automatically skips incompatible versions with `ExecutionStatus.SKIPPED` |
| 34 | +- Use `self.is_compatible()` to check programmatically |
| 35 | +- The `test_parsers_filestructure` and `test_analysers_filestructure` tests validate that all `ios_version` values are valid PEP 440 specifiers |
| 36 | + |
| 37 | +## Key Rules |
| 38 | + |
| 39 | +- Parsers: `execute()` must guard against missing files: check `get_log_files()` first, `logger.warning()` + return empty if none found |
| 40 | +- Parsers: never let `IndexError` propagate from `get_log_files()[0]` |
| 41 | +- Never raise exceptions for expected conditions (missing files, empty data, unsupported iOS versions) |
| 42 | +- Use `logger` (not `print()`) — warnings/errors are captured by `ResultSummaryExecutionHandler` |
| 43 | +- `print()` is enforced by ruff rule `T201` — only CLI entry points are exempt |
| 44 | +- Return type must match `format`: json→dict/list, jsonl→list[dict] or Generator[dict], custom→str |
| 45 | +- For jsonl, use `Event` dataclass and return `event.to_dict()` |
| 46 | +- For large datasets (jsonl only), return a Generator to enable lazy streaming |
| 47 | +- Analysers instantiate parsers using `ParserClass(self.config, self.case)` to pass through case metadata |
| 48 | + |
| 49 | +## Tests |
| 50 | + |
| 51 | +- Use `self.subTest(case_id=case_id, ios_version=_case.get('ios_version'))` when iterating over multiple cases so each case is reported independently |
| 52 | +- Check compatibility first: `if not p.is_compatible(): self.skipTest(...)` |
| 53 | +- Parsers: then check for log files: `if not files: self.fail(...)` |
| 54 | +- Use `self.assert_has_required_fields_jsonl(item)` for jsonl validation |
| 55 | +- Use `self.assert_result_summary_consistent(instance, result)` to validate summary matches output and **fail on execution errors** |
| 56 | +- Use `self.assert_result_summary_consistent(instance, result, allow_errors=True)` for parsers with known upstream issues, but that is not recommended as it will spam the user when using the CLI |
| 57 | +- Call `save_result(force=True)` to ensure fresh execution |
| 58 | +- Test execution order: parser tests run before analyser tests (configured in `tests/conftest.py`) so analysers benefit from cached parsed data |
| 59 | + |
| 60 | +## Method Lifecycle |
| 61 | + |
| 62 | +``` |
| 63 | +save_result() → _execute_and_write() → [is_compatible() check] → execute() + _write_result() + ResultSummaryExecutionHandler |
| 64 | +``` |
| 65 | + |
| 66 | +If incompatible: `_execute_and_write()` returns empty result with `ExecutionStatus.SKIPPED`. |
| 67 | + |
| 68 | +Outliers (custom I/O) use: `execute_with_result_summary()` → execute() + summary (no file write) |
| 69 | + |
| 70 | +The persisted summary file (`<module>.summary.json`) has two purposes: |
| 71 | + |
| 72 | +1. **Trust signal for cached data.** When loading from cache (CLI or API), the summary provides a glimpse of data quality (event count, errors, warnings, timing) without re-executing. |
| 73 | +2. **Staleness detection (future).** The `start_time` field enables identifying outdated cached results — relevant for analysers depending on previously parsed data, since CLI logs only capture the current run. |
| 74 | + |
| 75 | +## Do NOT |
| 76 | + |
| 77 | +- Override `save_result()` unless absolutely necessary |
| 78 | +- Catch broad `Exception` or `IndexError` to mask missing files |
| 79 | +- Return `{"error": "..."}` for missing files — use `logger.warning()` + empty return instead |
| 80 | +- Use `print()` for diagnostics — ruff `T201` will reject it |
| 81 | +- Hold large datasets in memory when a Generator suffices |
| 82 | +- Use `case_id` directly in constructors — always pass the full `case` dict |
0 commit comments