Private
Public Access
0
0

chore(artifacts): keep throwaway expansion helpers in scripts/tier2/artifacts/ (per Tier 2 convention)

This commit is contained in:
2026-07-03 00:05:08 -04:00
parent 465433e067
commit 8ef66e0287
7 changed files with 251 additions and 0 deletions
@@ -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<name>[^\n]+?)\s*[—–-]\s*v1\s*\n\n"
r"(?P<why>\*\*Why this iteration:\*\*[^\n]*(?:\n(?!\n)[^\n]*)*)\n\n"
r"\*\*Source:\*\*[ \t]+(?P<source>[^\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()
@@ -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/<file>.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=\"<auto>\")\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()
@@ -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] + "'")
@@ -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")
@@ -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))
@@ -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}")
@@ -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))