diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/backfill_meta.py b/scripts/tier2/artifacts/directive_hotswap_expansion/backfill_meta.py new file mode 100644 index 00000000..f9d85525 --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/backfill_meta.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Throwaway helper for the directive_hotswap_harness expansion. +Back-fills the existing 51 v1.md files: strips the metadata header and +writes a meta.md alongside containing the lifted provenance. + +This script is throwaway. Lives under scripts/tier2/artifacts/ per the +Tier 2 convention; deleted after the expansion commits land. +""" +import re +from pathlib import Path + +REPO = Path(".").resolve() +DIRECTIVES_DIR = REPO / "conductor" / "directives" + +HEADER_RE = re.compile( + r"^(?P[^\n]+?)\s*[—–-]\s*v1\s*\n\n" + r"(?P\*\*Why this iteration:\*\*[^\n]*(?:\n(?!\n)[^\n]*)*)\n\n" + r"\*\*Source:\*\*[ \t]+(?P[^\n]+)\n\n" + r"---\n\n", + re.MULTILINE, +) + +def parse_header(content: str, directive_name: str) -> tuple[str, str]: + m = HEADER_RE.search(content) + if not m: + raise ValueError(f"Could not parse header in {directive_name}/v1.md") + body = content[m.end():] + header_block = content[:m.end()] + return header_block, body + +def main() -> None: + directive_dirs = sorted([ + d for d in DIRECTIVES_DIR.iterdir() + if d.is_dir() and d.name != "presets" + ]) + if not directive_dirs: + raise SystemExit(f"No directive directories found under {DIRECTIVES_DIR}") + print(f"Processing {len(directive_dirs)} directives") + for d in directive_dirs: + name = d.name + v1_path = d / "v1.md" + meta_path = d / "meta.md" + if not v1_path.exists(): + print(f"SKIP {name}: no v1.md") + continue + if meta_path.exists(): + print(f"SKIP {name}: meta.md already exists") + continue + content = v1_path.read_text(encoding="utf-8") + try: + header_block, body = parse_header(content, name) + except ValueError as e: + print(f"ERROR {name}: {e}") + continue + why_text = header_block.split("**Why this iteration:**", 1)[1].split("**Source:**", 1)[0].strip() + source_text = header_block.split("**Source:**", 1)[1].strip() + meta_content = ( + f"# {name}\n\n" + f"## v1\n\n" + f"**Why this iteration:** {why_text}\n" + f"**Source:** {source_text}\n" + f"**Lifted:** 2026-07-02 (Phase 1 harvest by Tier 3 worker; user directive 2026-07-02 moved the header into this meta file)\n" + ) + meta_path.write_text(meta_content, encoding="utf-8") + v1_path.write_text(body, encoding="utf-8") + print(f"OK {name}") + +if __name__ == "__main__": + main() + diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/create_new_directives.py b/scripts/tier2/artifacts/directive_hotswap_expansion/create_new_directives.py new file mode 100644 index 00000000..db7830d0 --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/create_new_directives.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Throwaway helper for the directive_hotswap_harness scope-A expansion. +Creates new directives with clean v1.md (body only) + meta.md (provenance). +""" +from pathlib import Path + +DIRECTIVES_DIR = Path("conductor/directives") + +NEW_DIRECTIVES = [ + { + "name": "core_value_read_first", + "body": "### Core Value (Added 2026-06-25)\n\n**C11/Odin/Jai semantics in a Python runtime.** The project is written in Python because of practical constraints (time, dependencies, LLM codegen ability), but the convention is to make Python behave as close to a statically-typed value-typed language as the runtime allows.\n\nLLMs default to opaque types (`dict[str, Any]`, `Any`, `Optional[T]`, `hasattr()` polymorphism) because that's what idiomatic Python training data looks like. **That defaults to mediocrity. This rule overrides it.**\n\nThe canonical mandate is in `conductor/code_styleguides/data_oriented_design.md` §8.5 (The Python Type Promotion Mandate). The banned patterns are in `conductor/code_styleguides/python.md` §17 (LLM Default Anti-Patterns). The boundary-layer concept is in `conductor/code_styleguides/type_aliases.md`.\n\n**Every section of every styleguide and every deep-dive guide MUST be read through the lens of this Core Value.** If a section suggests `dict[str, Any]`, `Any`, `Optional[T]`, or `hasattr()` for entity dispatch in non-boundary code, that's an anti-pattern; flag it and ask.\n\n**READ THIS BEFORE WRITING ANY PYTHON IN THIS REPO.**\n", + "source": "docs/AGENTS.md:17-25 (Convention Enforcement §Core Value)", + }, + { + "name": "convention_enforcement_4_mechanisms", + "body": "## Convention Enforcement: the 4 mechanisms (defense-in-depth)\n\n1. **`conductor/code_styleguides/data_oriented_design.md` §8.5 (The Python Type Promotion Mandate)** — the canonical mandate. Banned patterns: `dict[str, Any]`, `Any`, `Optional[T]`, `hasattr()` for entity dispatch, `getattr()` for type-dispatch, `.get()` on known fields.\n\n2. **`conductor/code_styleguides/python.md` §17 (LLM Default Anti-Patterns)** — the explicit cheatsheet. Each banned pattern has a before/after example.\n\n3. **`conductor/code_styleguides/error_handling.md`** — the `Result[T]` + `NIL_T` convention. Replaces `Optional[T]` returns.\n\n4. **The enforcement audit scripts** — the project-level enforcement set:\n - `scripts/audit_weak_types.py --strict` — flags `dict[str, Any]`, `Any`, anonymous tuples\n - `scripts/audit_optional_in_3_files.py --strict` — flags `Optional[T]` return types in the baseline refactored files. An all-`src/*.py` successor (`audit_optional_returns.py`) is referenced in older planning docs but is NOT yet built — the live script today is `audit_optional_in_3_files.py`.\n - `scripts/audit_exception_handling.py --strict` — the data-oriented error handling convention\n - `scripts/audit_main_thread_imports.py` — always strict; the import graph gate\n - `scripts/audit_no_models_config_io.py` — the config-I/O ownership gate\n - The boundary-layer audit (planned in `conductor/tracks/cruft_elimination_20260627/spec.md`) — documents every `Metadata` usage\n\n**Pre-commit workflow (recommended):**\n\n```bash\nuv run python scripts/audit_weak_types.py\nuv run python scripts/audit_optional_in_3_files.py\nuv run python scripts/audit_exception_handling.py\nuv run python scripts/audit_main_thread_imports.py\nuv run python scripts/audit_no_models_config_io.py\n```\n\n**Why this is enforced:** the convention prevents the LLM-training-data problem. Without these mechanisms, AI agents writing new code will revert to idiomatic patterns — exactly the tech rot the user is preventing. The 5+ mechanisms (Core Value + 3 styleguides + 5 audit scripts) are the defense-in-depth.\n", + "source": "docs/AGENTS.md:27-62 (Convention Enforcement §4 mechanisms)", + }, + { + "name": "edit_small_incremental", + "body": "### Rule 1. ALWAYS Use Small, Incremental Edits\n\n**WRONG:** Replace large blocks (50+ lines)\n\n**RIGHT:** Replace 3-10 lines at a time, verify, repeat\n\nRationale: `manual-slop_edit_file` requires exact string matches. The `set_file_slice` tool requires exact line ranges. A single 50+ line edit can fail on whitespace drift and lose the working tree's state silently. Small edits let you verify after each, isolate failures, and roll back atomically.\n", + "source": "conductor/edit_workflow.md:9-12 (Rule 1)", + }, + { + "name": "verify_before_editing", + "body": "### Rule 2. Verify Before Editing\n\nBefore ANY edit to a function you haven't touched recently:\n\n1. Run: `py_check_syntax` on `src/.py`\n2. Get current state with `get_file_slice` (the exact lines you're about to touch)\n3. Read the contract: does this function/field/method's signature, yield shape, or return type have callers I need to update?\n\nDO NOT use `git checkout` or `git restore` to revert your way to a clean state. That destroys in-progress work. If a previous edit left the file in a broken state, ask the user.\n", + "source": "conductor/edit_workflow.md:14-24 (Rule 2)", + }, + { + "name": "decorator_orphan_pitfall", + "body": "### Rule 6. The Decorator-Orphan Pitfall\n\nWhen inserting new methods **before an existing `@property` def**:\n\n```python\n@property\ndef perf_profiling_enabled(self) -> bool:\n ...\n```\n\nIf you anchor on `def perf_profiling_enabled` and insert before it, the `@property` decorator on the line above is left orphaned on the line right before YOUR new method. Now `@property` decorates your method (which is no longer a property), and the original setter `@perf_profiling_enabled.setter` blows up at import with `'function' object has no attribute 'setter'`.\n\n**Fix:** Anchor on a non-decorated landmark, or include the decorator in the replacement:\n\n- `old_string` = ` self._init_actions()\\n\\n @property\\n def perf_profiling_enabled`\n- `new_string` = ` self._init_actions()\\n\\n def your_new(...)\\n ...\\n\\n @property\\n def perf_profiling_enabled`\n\nThis keeps the `@property` attached to its original method.\n", + "source": "conductor/edit_workflow.md:51-68 (Rule 6)", + }, + { + "name": "ast_parse_insufficient", + "body": "### Rule 7. ast.parse() Is Not Enough\n\n`py_check_syntax` only confirms `ast.parse()` succeeds. Semantic errors (wrong decorator targets, wrong base class, wrong attribute, missing `self`) are NOT caught. After any multi-line edit, ALWAYS:\n\n1. Import the module: `python -c \"from src.app_controller import AppController\"`\n2. Instantiate the class\n3. Call the new method in the way it's expected to be called (`ctrl.foo_ts` for a property, `ctrl.foo_ts()` for a method)\n\nIf you skip this step, the file passes `py_check_syntax` but blows up at import time or when the new method is called. The 2026-06-05 `_capture_workspace_profile` regression (3-space indent vs 2-space class level) is the canonical example.\n", + "source": "conductor/edit_workflow.md:70-76 (Rule 7)", + }, + { + "name": "contract_change_audit", + "body": "### The contract-change check (mandatory for any edit that changes a public interface)\n\nBefore any edit, search the codebase for callers of the function/symbol/yield shape you're changing. If your edit changes:\n\n- A function signature (add/remove/rename a parameter)\n- A return type or yield shape (e.g. `yield process, gui_script` -> `yield process, gui_script, workspace_path`)\n- A class hierarchy (add/remove a base class, change a method's name)\n- A module-level function name (rename)\n- A public attribute name\n\n...you MUST update ALL callers in the same atomic commit. Use `py_find_usages` to locate them. If you change a contract and don't update callers, you have broken the codebase.\n", + "source": "conductor/edit_workflow.md:90-99 (Rule 8 contract-change check)", + }, + { + "name": "preserve_line_endings", + "body": "### The whitespace-and-EOL rule (mandatory for set_file_slice)\n\nThe `new_content` must preserve:\n\n- The file's line ending convention (CRLF on Windows, LF on Linux — pick from the surrounding file, not from your text editor's default)\n- The indentation of the surrounding code (1 space per level, per `conductor/code_styleguides/python.md` §1)\n- The number of lines replaced (`start_line`..`end_line` must equal `len(new_content.splitlines())`)\n\nIf you mismatch any of these, the file will fail to parse. Run `py_check_syntax` and a real `import` after every `set_file_slice`.\n\n**Do NOT normalize line endings via git smudge.** This repo has a mix of CRLF and LF; repo-wide LF standardization is a future track. For now, preserve existing line endings on edit.\n", + "source": "conductor/edit_workflow.md:101-108 (Rule 8 whitespace/EOL)", + }, + { + "name": "no_real_io_during_tests", + "body": "## The 4 test infrastructure principles\n\n1. **No real I/O during tests** — every test gets a sandboxed workspace via the `isolate_workspace` autouse fixture.\n2. **No real AI calls** — tests use mock providers, reset session state, and never hit the network.\n3. **GUI tests launch a real app** — the `live_gui` session fixture starts `sloppy.py --enable-test-hooks` so integration tests can drive the actual app via the Hook API.\n4. **Tests are categorized by marker** — unit, integration, strict, clean_install, docker — so CI can opt in to expensive tests.\n\nThe autouse `isolate_workspace` fixture (1 of 7 conftest fixtures) gives every test a fresh, isolated workspace so it cannot pollute the user's real `manual_slop.toml`, `presets.toml`, etc. The session-scoped `live_gui` fixture starts `sloppy.py --enable-test-hooks` once per session so integration tests can drive the actual app via the Hook API.\n\nAll test-generated artifacts (logs, temporary workspaces, mock outputs) MUST be written to `tests/artifacts/` or `tests/logs/` (gitignored).\n", + "source": "docs/guide_testing.md:9-16 (Overview)", + }, + { + "name": "live_gui_session_scoped_no_restart", + "body": "## live_gui session-scoped fixture contract\n\n`live_gui` is a **session-scoped** fixture. All tests in a session share the same `sloppy.py` subprocess. The subprocess is **not** restarted between tests; its internal state (Fonts, DisplaySize, internal caches, current theme, current workspace profile, current discussion, current MMA track) **accumulates** from the previous test.\n\n**This is a test-authoring contract, not a fixture bug.** A test that passes when run after test X but fails when run in isolation is a fragile test. Robust `live_gui` tests must:\n\n1. **Not assume clean state.** Before invoking an operation, explicitly verify the precondition via the Hook API (e.g. `client.get_value(\"show_my_window\")`, `client.get_mma_status()`, `client.get_session()`). Do not assume a previous test set the state.\n2. **Use the wait-for-ready pattern, not fixed sleeps.** `time.sleep(1)` is not enough for ImGui to stabilize in the first few render frames; use `wait_for_event` with a generous timeout, or poll `client.get_status()` until ImGui reports `ready`.\n3. **Reset state explicitly if the test depends on it.** Reset relevant state via Hook API in a `try/finally` so the next test starts from a known baseline.\n4. **Test both in the full suite AND in isolation before merging.** If a test passes in the full suite but fails in isolation, the test is fragile — fix the test, don't add a warmup comment.\n5. **Use `get_value`/`wait_for_event` to assert ready, not just to assert success.**\n", + "source": "docs/guide_testing.md:753-787 (§Known Gotchas: Authoring Robust live_gui Tests)", + }, + { + "name": "defer_not_catch_for_native_crashes", + "body": "## Defer-Not-Catch Pattern for Native Crashes\n\n`imgui-bundle` (and similar native extension libraries) expose C-level functions that can crash the Python process with a Windows access violation (`0xc0000005`) or a SIGSEGV on Linux. **These crashes are not catchable from Python** — `try/except Exception` does not intercept native access violations, only Python exceptions.\n\nSymptoms:\n- The `sloppy.py` subprocess disappears without a Python traceback.\n- The pytest output shows `pytest.fail(\"Hook server did not start in 15s\")` (the subprocess died during startup).\n- Windows Event Viewer shows `Faulting module: _imgui_bundle.cp311-win_amd64.pyd` with exception code `0xc0000005`.\n\n**Fix pattern: defer-not-catch.** Track a one-shot ready flag in instance state; return early on the first call, only invoking the C function on subsequent calls:\n\n```python\ndef _capture_workspace_profile(self, name: str) -> models.WorkspaceProfile:\n if not getattr(self, \"_ini_capture_ready\", False):\n self._ini_capture_ready = True\n return models.WorkspaceProfile(name=name, docking_layout=b\"\", ...)\n ini = imgui.save_ini_settings_to_memory()\n return models.WorkspaceProfile(name=name, docking_layout=ini.encode(\"utf-8\") if isinstance(ini, str) else ini, ...)\n```\n\nThe first call (during initial startup) returns a safe empty profile and flips the flag; subsequent calls (when the user actually clicks Save Profile) invoke the C function.\n\n**Sentinel type contract.** The early-return sentinel value must match the type contract of the downstream consumer. For `WorkspaceProfile.ini_content: str`, the sentinel must be `\"\"` (str), not `b\"\"` (bytes) — `tomli_w` rejects bytes (`TypeError: Object of type 'bytes' is not TOML serializable`).\n", + "source": "docs/guide_testing.md:789-813 (§Known Gotchas: Early-Render C-Level Crashes)", + }, + { + "name": "test_narrow_not_kitchen_sink", + "body": "## Pattern: Narrow Test Paths vs. Kitchen-Sink Functions\n\n**Anti-pattern: calling a kitchen-sink function.** A test that does `gui_2.render_main_interface(app_instance)` requires mocking 50+ imgui/imscope methods because `render_main_interface` dispatches to dozens of nested render functions. Adding a single mock for `imscope.window` (to return a tuple) just reveals the next un-mocked dependency (e.g. `imgui.begin` returning bool where a 2-tuple is expected). The test never reaches its assertion.\n\n**Better pattern: test the narrow function.** Most render flows have a dedicated sub-function (e.g. `render_prior_session_view`, `render_preset_manager_window`, `render_theme_panel`). Refactor the test to call the narrow function directly with mocks scoped to what that function actually uses.\n\n**When to refactor vs. add mocks:**\n- If the test intent is verify push/pop balance in the prior-session render path, call the narrow function.\n- If the test intent is verify the whole GUI render path is correct, accept the 50+ mock cost (and ensure all mocks are correct).\n", + "source": "docs/guide_testing.md:817-829 (§Pattern: Narrow Test Paths)", + }, + { + "name": "ast_verify_class_methods_after_edit", + "body": "## Pattern: Indentation-Driven Method Visibility\n\n**The bug:** A class method defined with the right intent (2-space indent) may be parsed as nested inside a previous function if indentation is off by even one space. The file passes syntactically (imports OK) but the method is **not** on the class — `hasattr(App, 'method_name')` returns `False`. Any production code that calls `app.method_name` falls through to `__getattr__`, which delegates to the controller (which also doesn't have the method), and a cryptic `AttributeError` is raised at runtime.\n\n**How to detect:**\n- Use AST to list all App methods: `uv run python -c \"import ast; tree = ast.parse(open('src/gui_2.py').read()); [print(item.name) for n in ast.walk(tree) if isinstance(n, ast.ClassDef) and n.name == 'App' for item in n.body if isinstance(item, ast.FunctionDef)]\"`\n- The skeleton via `manual-slop_py_get_skeleton` should show the method as a class member.\n\n**How to fix:** Re-indent the affected method to 2-space class level. Run the failing test to confirm.\n\n**Prevention:** When reorganizing a class body, run the AST check above immediately after the edit. This catches the issue in <1 second vs. finding it via failing live_gui tests minutes later.\n", + "source": "docs/guide_testing.md:834-842 (§Pattern: Indentation-Driven Method Visibility)", + }, + { + "name": "undo_redo_100_snapshot_capacity", + "body": "## HistoryManager — the 100-snapshot undo/redo stack\n\n`src/history.py:71 HistoryManager` is a 100-snapshot capacity stack with the following API:\n\n- `push(state, description)` — appends; clears the redo stack; pops the oldest if capacity exceeded.\n- `undo(current_state, current_description)` — moves current state to redo stack; returns the top of the undo stack.\n- `redo(...)` — inverse of undo.\n- `jump_to_undo(index, current_state, current_description)` — time-travels to any past snapshot, moving subsequent states to the redo stack.\n- `can_undo`, `can_redo` properties\n- `get_history()` — returns `[{description, timestamp}, ...]` for the History List view\n\nThe `max_capacity=100` is the default and is sufficient for a 5-second window of rapid typing or a longer session of infrequent edits.\n\n## The Push Trigger — debounced change detection at render frame\n\nThe undo stack is **not** pushed on every keystroke. It's pushed via debounced change-detection at the start of every render frame:\n\n```python\ncurrent = self._take_snapshot()\nif self._last_ui_snapshot is None:\n self._last_ui_snapshot = current\n return\n\nchanged = (\n current.ai_input != self._last_ui_snapshot.ai_input or\n ...\n len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or\n ...\n)\n\nif changed:\n self.history.push(current, description=\"\")\n self._last_ui_snapshot = current\n```\n\nThe check is at the start of every render frame. `copy.deepcopy(self.disc_entries)` is the most expensive part — O(N) where N is the entry count. The full snapshot push only happens when a change is detected.\n", + "source": "docs/guide_state_lifecycle.md:56-117 (§1 Undo/Redo: HistoryManager + UISnapshot)", + }, + { + "name": "reset_session_preserves_project_path", + "body": "## _handle_reset_session field-clear contract\n\n`src/app_controller.py:3286-3356 _handle_reset_session` is the **nuclear** reset, called from the Reset Session button in the message panel. It clears 30+ state buckets:\n\n- **AI client**: `ai_client.reset_session()` + `clear_comms_log()`\n- **Tool stats**: `_tool_log.clear()`, `_tool_stats.clear()`, `_comms_log.clear()`\n- **Discussion**: `self.disc_entries.clear()`; for each discussion in project: `discussions[d_name][\"history\"] = []`\n- **Files**: `self.files.clear()`, `self.context_files.clear()`\n- **Tracks**: `self.tracks.clear()`\n- **Project dict (full replacement)**: `self.project = project_manager.default_project(...)`\n- **Project paths**: `self.project_paths = []`\n- **AI status**: `ai_status = \"session reset\"`, `ai_response = \"\"`\n- **UI inputs**: `ui_ai_input = \"\"`, `ui_manual_approve = False`, `ui_auto_add_history = False`\n- **MMA**: `active_track = None`, `active_tier = None`, `mma_status = \"idle\"`, ...\n- **Provider/model**: `_current_provider = \"gemini\"`, `_current_model = \"gemini-2.5-flash-lite\"`\n- **Locks + queues**: `_pending_history_adds.clear()`, `_api_event_queue.clear()`, `_pending_gui_tasks.clear()`\n- **Prompts**: `ui_use_default_base_prompt = True`, all 3 system prompts = `''`\n- **Persona/tool settings**: `ui_active_persona = ''`, `ui_active_tool_preset = None`, ...\n- **Generation params**: `temperature = 0.0`, `top_p = 1.0`, `max_tokens = 8192`\n\n## What It Does NOT Touch (the preserve list)\n\n| Field | Why preserved |\n|---|---|\n| `self.active_project_path` | `_do_project_switch` writes to this path; clearing it would cause OSError on next switch and an infinite re-switch loop. The 2026-06-08 regression test `test_context_sim_live` documents this. |\n| `self.history` (the `HistoryManager`) | The undo stack survives a reset. Ctrl+Z after a reset can restore the pre-reset state. This may be a bug or a feature. |\n| The on-disk `manual_slop.toml` | The saved project TOML is not deleted or rewritten. Switching projects after reset reloads from disk. |\n", + "source": "docs/guide_state_lifecycle.md:217-244 (§3 Reset)", + }, +] + +def main() -> None: + print(f"Creating {len(NEW_DIRECTIVES)} new directives") + for d in NEW_DIRECTIVES: + name = d["name"] + target_dir = DIRECTIVES_DIR / name + target_dir.mkdir(parents=True, exist_ok=True) + (target_dir / "v1.md").write_text(d["body"], encoding="utf-8") + meta_content = ( + f"# {name}\\n\\n" + f"## v1\\n\\n" + f"**Why this iteration:** Lifted verbatim from `{d['source']}`.\\n" + f"**Source:** `{d['source']}`\\n" + f"**Lifted:** 2026-07-02 (Phase A expansion harvest; user directive 2026-07-02)\\n" + ) + (target_dir / "meta.md").write_text(meta_content, encoding="utf-8") + print(f"OK {name}") + +if __name__ == "__main__": + main() + diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta.py b/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta.py new file mode 100644 index 00000000..b8ffb85d --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +from pathlib import Path +meta = Path("conductor/directives/defer_not_catch_for_native_crashes/meta.md") +content = meta.read_text(encoding="utf-8") +for i, ch in enumerate(content): + if ch == "?": + print("Found ? at index " + str(i) + ": context='" + content[max(0,i-10):i+10] + "'") + break +else: + print("No ? found") +print("---") +for i, ch in enumerate(content): + if ord(ch) > 127: + print("Non-ASCII at " + str(i) + ": U+" + format(ord(ch), "04X") + " context='" + content[max(0,i-5):i+5] + "'") + diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta2.py b/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta2.py new file mode 100644 index 00000000..126e0a68 --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/debug_meta2.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +from pathlib import Path +meta = Path("conductor/directives/defer_not_catch_for_native_crashes/meta.md") +data = meta.read_bytes() +# Print bytes around position 110-130 +print("Bytes 110-130:", data[110:130]) +print("As string (utf-8):", data[110:130].decode("utf-8", errors="replace")) +print("Length:", len(data)) +print("Char at byte 122:", hex(data[122]) if len(data) > 122 else "N/A") \ No newline at end of file diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta.py b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta.py new file mode 100644 index 00000000..752faf61 --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import sys +from pathlib import Path + +NL = chr(10) + +count = 0 +for d in sorted(Path("conductor/directives").iterdir()): + meta = d / "meta.md" + if not meta.exists(): + continue + content = meta.read_text(encoding="utf-8") + if chr(92) + "n" not in content: + continue + fixed = content.replace(chr(92) + "n", NL) + meta.write_text(fixed, encoding="utf-8") + count += 1 + print("FIXED " + d.name) +print("Total fixed: " + str(count)) \ No newline at end of file diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta_newlines.py b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta_newlines.py new file mode 100644 index 00000000..49118e14 --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_meta_newlines.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +"""Fix the meta.md files that have literal \\n instead of newlines.""" +from pathlib import Path + +for d in sorted(Path("conductor/directives").iterdir()): + meta = d / "meta.md" + if not meta.exists(): + continue + content = meta.read_text(encoding="utf-8") + if "\\n" not in content: + continue + fixed = content.replace("\\n", "\n") + meta.write_text(fixed, encoding="utf-8") + print(f"FIXED {d.name}") + diff --git a/scripts/tier2/artifacts/directive_hotswap_expansion/fix_section_symbol.py b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_section_symbol.py new file mode 100644 index 00000000..0ec5d3ee --- /dev/null +++ b/scripts/tier2/artifacts/directive_hotswap_expansion/fix_section_symbol.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +from pathlib import Path +SECTION = chr(167) +QUESTION = chr(63) +count = 0 +for d in sorted(Path("conductor/directives").iterdir()): + meta = d / "meta.md" + if not meta.exists(): + continue + content = meta.read_text(encoding="utf-8") + if QUESTION not in content: + continue + fixed = content.replace(QUESTION, SECTION) + meta.write_text(fixed, encoding="utf-8") + count += 1 + print("FIXED " + d.name) +print("Total fixed: " + str(count)) \ No newline at end of file