From 3f5a26589c243d0d9844035d0a2c2787c2b50a66 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Wed, 1 Jul 2026 23:11:29 +0800 Subject: [PATCH 01/11] Remove mention of legacy setup --- agents/artifact-clerk.md | 41 ++-------------------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/agents/artifact-clerk.md b/agents/artifact-clerk.md index 45de19a..0e3ca09 100644 --- a/agents/artifact-clerk.md +++ b/agents/artifact-clerk.md @@ -51,9 +51,6 @@ The `Project:` field is optional. When provided, use it as the canonical project - `../capstone/capstone.md` (capstone project spec, if it exists — lives in `capstone/` sibling to `learning/`) - `cross-refs/INDEX.md` (cross-project topic registry index — look for the `cross-refs/` directory by walking up from the learning path to the repo root. Search up to 4 parent directories from the specified path.) - From INDEX.md, find the current project's row and load `cross-refs/.md` plus each file listed in the "Overlaps With" column. From overlapping project files, extract only rows where the current project appears in "Also Covered In." - - **Legacy fallback:** If `cross-refs/` directory does not exist but `cross-references.md` does, read the monolithic file instead. - - **Legacy fallback:** If `journal/index.md` does not exist but `journal.md` does, this is a legacy layout. Read `journal.md` instead and note that migration is needed. 2. Run SRS engine commands (if `cards.srs.json` exists): ```bash @@ -238,8 +235,6 @@ Plan Updates: ### Step 1: Read current state Read artifact files to understand current state (card numbering, weak spot numbering, knowledge-map rows, etc.). Read `journal/index.md` to determine the current session count and the next session number. -**Legacy check:** If `journal/index.md` does not exist but `journal.md` does, run the migration procedure (see **Migration** section below) before proceeding. - **First session:** If neither `journal/index.md` nor `journal.md` exists, create the `journal/` directory and a new `journal/index.md` with this header: ```markdown @@ -326,7 +321,7 @@ Metadata block rules: - `cards_reviewed` and `avg_grade` come from the coach's "Review Stats" in session notes. If not provided, use `0` and `null`. ### Step 2b: Update `journal/index.md` -- Do NOT write to `journal/index.md` directly. Use the `journal_writer.py` script which guarantees canonical 8-column format and handles legacy migration automatically. +- Do NOT write to `journal/index.md` directly. Use the `journal_writer.py` script which guarantees canonical 8-column format. - Build a JSON object from the session data and pipe it to the script: ```bash SAGE_ROOT=$(cat /tmp/.sage-plugin-root) @@ -387,13 +382,11 @@ Where `` is: } ``` - Only updates `Status`, `Last Tested`, and `Notes` — the tool preserves `Introduced` automatically -- The tool handles both legacy 4-column tables and the current 5-column format - **Status validation:** Every status value written to the knowledge map MUST be one of the canonical values: `not started`, `introduced`, `developing`, `solid`, `mastered`, or `prior (from [project])`. If the coach sends a non-canonical status (e.g., "familiar", "shaky", "recalled", "practicing", "exposed"), map it to the closest canonical equivalent and WARN. Mapping guide: - `new`, `exposed` → `introduced` - `familiar`, `shaky`, `practicing`, `in-progress`, `developing` → `developing` - `understood`, `recalled`, `demonstrated`, `acquired`, `reinforced`, `developed` → `solid` - No aliases for `mastered` — only use when the coach explicitly says `mastered` -- **Legacy status migration:** When reading `knowledge-map.md`, scan all existing rows for non-canonical statuses. If any are found, normalize them using the mapping guide above and WARN with a summary (e.g., "Migrated 8 statuses: familiar→developing (3), recalled→solid (2), shaky→developing (2), exposed→introduced (1)"). Also replace the status legend section with the canonical one. This runs on every checkpoint but only produces changes once per project — subsequent checkpoints will find only canonical statuses. - **First session (knowledge-map is being created):** Check `plan.md` for concepts marked "Prior Knowledge (from [project])" in the skill tree. Only use `prior (from [project])` for concepts that are `solid` or `mastered` in the sibling project — this status means "no need to teach this." For concepts that are `developing` or lower in the sibling project, use `developing` with a note like "Also covered in [project]" — the learner still needs work on these. - **Status Changelog:** Do NOT write changelog rows directly. Use the `kmap_writer.py` script: ```bash @@ -593,8 +586,7 @@ If no new entries of a given kind, skip that write. - If the concept already exists in this project's file: update its status and notes. - If the concept already exists in another project's file (because that project owns it as primary): update that file's row — add the current project to "Also Covered In" if not already listed, and update notes. - If a new overlap is created (current project appears in another project's file for the first time): update `cross-refs/INDEX.md` — add the current project to the other project's "Overlaps With" column, and add the other project to the current project's row (create the row if needed). -- **Legacy fallback:** If `cross-refs/` does not exist but `cross-references.md` does, update the monolithic file instead. -- If neither `cross-refs/` nor `cross-references.md` was found, skip this step and WARN. +- If `cross-refs/` was not found, skip this step and WARN. ### Step 9: Validate cross-artifact consistency Run these checks and collect results: @@ -738,35 +730,6 @@ Metrics file: /tmp/session-metrics-.txt --- -## Migration: Legacy `journal.md` to `journal/` Directory - -When you detect a legacy `journal.md` file (no `journal/` directory exists), migrate it automatically before proceeding with the current operation. - -**Migration steps:** - -1. Read `journal.md` and parse it into individual session entries. Each session starts with a `## Session` heading. -2. Create the `journal/` directory. -3. Write each session entry to its own file: `journal/session-NN.md` (zero-padded two digits based on session number). -4. Build `journal/index.md` from the parsed sessions: - ```markdown - # Session Index - - | # | Date | Focus | File | - |---|------|-------|------| - | 1 | YYYY-MM-DD | [focus from entry] | session-01.md | - | 2 | YYYY-MM-DD | [focus from entry] | session-02.md | - ``` -5. After confirming all session files were written successfully, delete the legacy `journal.md` file. -6. Report the migration in your output: "Migrated journal.md → journal/ directory ([N] sessions)". - -**Ordering:** Write session files in the order they appear in the original file. The index table rows should be sorted by session number, regardless of the order they appeared in `journal.md`. - -**Edge cases:** -- If a legacy session has a non-numeric identifier (e.g., "3b"), preserve it as-is during migration: `session-03b.md`. New sessions always use integer-only IDs. -- If the file has no parseable session entries, create an empty index and WARN. - ---- - ## What You Do NOT Do - Make pedagogical decisions (what to teach, how to assess, when to advance) From c0bd78517d69e7f3db19cfd0d1b342a2a053be82 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 7 Jul 2026 09:42:21 +0800 Subject: [PATCH 02/11] Remove junk columns from table --- docs/ref-subagents.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/ref-subagents.md b/docs/ref-subagents.md index ef2a6b3..dbcff9a 100644 --- a/docs/ref-subagents.md +++ b/docs/ref-subagents.md @@ -8,23 +8,23 @@ You delegate to several subagents via the Task tool. Each agent has its own spec **SRS engine path:** `$SAGE_ROOT/tools/srs/srs_engine.py` — used for live grading during reviews. See `docs/ref-srs.md` for full command reference. -| Agent | Operation | When | Call Pattern | -|-------|-----------|------|-------------| -| artifact-clerk | `brief` | Session start (resume) | `Operation: brief\nPath: /\nProject: ` | -| artifact-clerk | `checkpoint` | Session end | `Operation: checkpoint\nPath: /\nProject: \n\n[session notes]` | -| assessment-agent | `select-and-prepare` | Session start (warm-up), post-material checks | `Operation: select-and-prepare\nPath: /\n\nSession context: [...]\nCount: 3\nMin mastery: developing` | -| assessment-agent | `generate` | After covering new material | `Operation: generate\nPath: /\n\nTarget:\n- Concept: [...]\n- Difficulty: [1-5]\n- Question type: [free_recall|conceptual|application|analysis|transfer|reverse]` | -| assessment-agent | `evaluate` | After learner answers assessment | `Operation: evaluate\nPath: /\n\nQuestion ID: q-N\nQuestion text: [...]\nExpected answer: [...]\nLearner response: [...]\nSession: [N]` | -| verification-gate | `verify-claims` | Session start (batch) + topic-section gate at each topic transition + message-counter fallback (5+ messages without a gate) + ad-hoc fallback for unplanned claims | `Operation: verify-claims\nTopic: [...]\n\nClaims:\n1. [...]` | -| verification-gate | `verify-code` | Before presenting code examples | `Operation: verify-code\nLanguage: [...]\nExpected behavior: [...]\n\nCode:\n[...]` | -| verification-gate | `verify-cards` | Before checkpoint (new cards only) | `Operation: verify-cards\nTopic: [...]\n\nCards:\n[card definitions]` | -| reference-clerk | `generate` | Learner requests, concept deeply explored, or after misconception | `Operation: generate\nPath: /\nConcept: \nContext: [...]\n\nSource material:\n[...]` | -| reference-clerk | `update` | Corrections or additions to existing ref doc | `Operation: update\nPath: /\nConcept: \nUpdates:\n- [...]` | -| reference-clerk | `audit` | Check coverage gaps | `Operation: audit\nPath: /` | -| demo-generator | `generate` | Learner confirms demo after `visual_demo` plateau mode | `Operation: generate\nPath: /\nConcept: \nMisconception: M[N] — [desc]\nCollision point: [...]\nLearner's wrong model: [...]\nCorrect model: [...]` | -| demo-generator | `update` | Demo needs adjustment after feedback | `Operation: update\nPath: /\nMisconception: M[N]\nUpdates:\n- [...]` | -| capstone-architect | `propose` | Learner requests capstone, `/capstone` command, or coach judges mastery is sufficient | `Operation: propose\nPath: /\nAudience: ` | -| capstone-architect | `specify` | After learner selects a project from proposals | `Operation: specify\nPath: /\nSelected: \nAudience: \n\nProject details:\n` | +| Agent | Operation | When | Call Pattern | +| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| artifact-clerk | `brief` | Session start (resume) | `Operation: brief\nPath: /\nProject: ` | +| artifact-clerk | `checkpoint` | Session end | `Operation: checkpoint\nPath: /\nProject: \n\n[session notes]` | +| assessment-agent | `select-and-prepare` | Session start (warm-up), post-material checks | `Operation: select-and-prepare\nPath: /\n\nSession context: [...]\nCount: 3\nMin mastery: developing` | +| assessment-agent | `generate` | After covering new material | `Operation: generate\nPath: /\n\nTarget:\n- Concept: [...]\n- Difficulty: [1-5]\n- Question type: [free_recall \| conceptual \| application \| analysis \| transfer \| reverse]` | +| assessment-agent | `evaluate` | After learner answers assessment | `Operation: evaluate\nPath: /\n\nQuestion ID: q-N\nQuestion text: [...]\nExpected answer: [...]\nLearner response: [...]\nSession: [N]` | +| verification-gate | `verify-claims` | Session start (batch) + topic-section gate at each topic transition + message-counter fallback (5+ messages without a gate) + ad-hoc fallback for unplanned claims | `Operation: verify-claims\nTopic: [...]\n\nClaims:\n1. [...]` | +| verification-gate | `verify-code` | Before presenting code examples | `Operation: verify-code\nLanguage: [...]\nExpected behavior: [...]\n\nCode:\n[...]` | +| verification-gate | `verify-cards` | Before checkpoint (new cards only) | `Operation: verify-cards\nTopic: [...]\n\nCards:\n[card definitions]` | +| reference-clerk | `generate` | Learner requests, concept deeply explored, or after misconception | `Operation: generate\nPath: /\nConcept: \nContext: [...]\n\nSource material:\n[...]` | +| reference-clerk | `update` | Corrections or additions to existing ref doc | `Operation: update\nPath: /\nConcept: \nUpdates:\n- [...]` | +| reference-clerk | `audit` | Check coverage gaps | `Operation: audit\nPath: /` | +| demo-generator | `generate` | Learner confirms demo after `visual_demo` plateau mode | `Operation: generate\nPath: /\nConcept: \nMisconception: M[N] — [desc]\nCollision point: [...]\nLearner's wrong model: [...]\nCorrect model: [...]` | +| demo-generator | `update` | Demo needs adjustment after feedback | `Operation: update\nPath: /\nMisconception: M[N]\nUpdates:\n- [...]` | +| capstone-architect | `propose` | Learner requests capstone, `/capstone` command, or coach judges mastery is sufficient | `Operation: propose\nPath: /\nAudience: ` | +| capstone-architect | `specify` | After learner selects a project from proposals | `Operation: specify\nPath: /\nSelected: \nAudience: \n\nProject details:\n` | ## Key Integration Notes From 48b4542f656c640cf9df747a5678b39a6a54eb86 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 7 Jul 2026 09:45:37 +0800 Subject: [PATCH 03/11] Remove migration from legacy reference doc format --- agents/reference-clerk.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/agents/reference-clerk.md b/agents/reference-clerk.md index 9b86dda..58d197a 100644 --- a/agents/reference-clerk.md +++ b/agents/reference-clerk.md @@ -14,11 +14,13 @@ You do NOT teach. You do NOT interact with the learner. You produce reference do Reference documents are standalone deep-dive explanations of a single concept or mechanism. They sit alongside the 5 core learning artifacts (plan, journal, knowledge-map, cards, weak-spots) as supplementary material. Unlike flashcards (retrieval cues) or journal entries (session logs), reference docs are **explanatory** — they exist to make a mechanism legible through concrete examples, visuals, and walkthroughs. **Reference docs are NOT:** + - Textbook chapters (too long, too broad) - Flashcard expansions (flashcards test recall; references explain mechanisms) - Session notes (those go in the journal) **Reference docs ARE:** + - Single-concept deep dives with concrete, worked examples - Verified against official documentation and/or code execution - Cross-referenced to the learner's cards and knowledge map @@ -33,11 +35,13 @@ All reference documents live in the `docs/references/` subdirectory under the to ``` **Slug rules:** + - Lowercase, hyphen-separated - 2-4 words maximum - Descriptive of the mechanism, not the session or card **Examples:** + - `docs/references/ref-btree-fanout.md` - `docs/references/ref-streaming-replication.md` - `docs/references/ref-composite-index-keys.md` @@ -55,6 +59,7 @@ You support three operations, determined by the `Operation:` field in your promp **Purpose:** Create a new reference document for a concept. **Input format:** + ``` Operation: generate Path: / @@ -68,9 +73,11 @@ Source material (optional): **What you do:** ### Step 1: Check for existing reference + Look for an existing `docs/references/ref-.md`. If it exists, switch to the `update` operation internally and merge new material rather than overwriting. ### Step 2: Research and verify + Before writing anything, gather authoritative information: 1. **Check official documentation** — Use Context7, MCP doc tools, or web search to verify core claims about the concept @@ -182,6 +189,7 @@ Append an entry to `docs/references/index.md` (create the file if it doesn't exi **Purpose:** Update an existing reference document with new information (e.g., after a session reveals new nuances, or a weak spot is resolved). **Input format:** + ``` Operation: update Path: / @@ -209,6 +217,7 @@ Updates: **Purpose:** Compare knowledge-map concepts against existing reference docs and identify coverage gaps. **Input format:** + ``` Operation: audit Path: / @@ -254,26 +263,12 @@ Path: / --- -## Migration: Existing Ad Hoc Reference Docs - -When you encounter reference documents that predate the `docs/references/` directory structure (e.g., files named `reference-*.md` or `ref-*.md` in the root of the learning path, or in a legacy `refs/` directory), migrate them: - -1. Read each legacy reference file -2. Rewrite it to match the standard template (add missing sections, add cross-reference header, verify facts) -3. Move it to `docs/references/ref-.md` with the standardized naming -4. Add it to `docs/references/index.md` -5. Delete the legacy file from the root -6. Report the migration: "Migrated [old filename] → docs/references/[new filename]" - -**Important:** Preserve all content from the original document. The migration adds structure and cross-references; it does not delete information. If the original document has content that doesn't fit the template, include it in the most appropriate section or add it as a "Notes" section at the bottom. - ---- - ## How You Are Invoked ### Primary path: Learner requests via the coach The learner says something like: + - "Can we create a reference doc for cache-aside?" - "I want a reference document on XFetch" - "Generate a ref doc for this concept" @@ -298,6 +293,7 @@ Task(subagent_type="reference-clerk", prompt="Operation: generate\nPath: scaling ### Coach-initiated (no learner request) The coach may also invoke you proactively: + - **After a session** where a concept was deeply explored and deserves a reference doc - **After an audit** reveals coverage gaps - **After a weak spot** reveals the learner needs a clearer explanation of a mechanism @@ -317,14 +313,18 @@ The Sage skill includes the following in its "Tools Available to You" section, a Delegation format: ``` + Task(subagent_type="reference-clerk", prompt="Operation: generate\nPath: /\nConcept: \nContext: \n\nSource material:\n") + ``` After the clerk returns, tell the learner what was generated and where the file lives. You can also run an audit to find coverage gaps: ``` + Task(subagent_type="reference-clerk", prompt="Operation: audit\nPath: /") + ``` ``` From 0e6ca481f1a91ff5460fb90dbd921e6d6628fa77 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Wed, 8 Jul 2026 21:59:49 +0800 Subject: [PATCH 04/11] Remove reference to old cross-ref file --- hooks/scripts/enforce-cross-refs.sh | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/hooks/scripts/enforce-cross-refs.sh b/hooks/scripts/enforce-cross-refs.sh index 33546eb..9f15a24 100755 --- a/hooks/scripts/enforce-cross-refs.sh +++ b/hooks/scripts/enforce-cross-refs.sh @@ -2,7 +2,6 @@ # Stop hook: blocks session end if knowledge maps were modified # but cross-reference registry wasn't updated. # Only fires when cwd is the sage repo. -# Supports both sharded cross-refs/ directory and legacy cross-references.md. set -euo pipefail @@ -32,7 +31,6 @@ if [[ "$CWD" != "$SAGE_DIR"* ]]; then fi CROSS_REFS_DIR="${SAGE_DIR}/cross-refs" -CR_FILE="${SAGE_DIR}/cross-references.md" NOW=$(date +%s) @@ -65,7 +63,7 @@ fi # Knowledge map modified — check if cross-refs were updated too CR_UPDATED=false -# Check sharded cross-refs/ directory (preferred) +# Check sharded cross-refs/ directory if [ -d "$CROSS_REFS_DIR" ]; then while IFS= read -r cr; do CR_MTIME=$(stat -c %Y "$cr" 2>/dev/null || echo 0) @@ -75,13 +73,6 @@ if [ -d "$CROSS_REFS_DIR" ]; then break fi done < <(find "$CROSS_REFS_DIR" -name "*.md" 2>/dev/null) -# Fall back to legacy monolithic file -elif [ -f "$CR_FILE" ]; then - CR_MTIME=$(stat -c %Y "$CR_FILE" 2>/dev/null || echo 0) - CR_AGE=$((NOW - CR_MTIME)) - if [ "$CR_AGE" -lt "$THRESHOLD" ]; then - CR_UPDATED=true - fi fi # Knowledge map modified but cross-references weren't — block From b320ccd26bc3bcd6ca6c9f65064d903384d493dd Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Wed, 8 Jul 2026 22:00:01 +0800 Subject: [PATCH 05/11] Remove reference to legacy journal format --- tools/session_router.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/session_router.py b/tools/session_router.py index d82f819..70956a0 100644 --- a/tools/session_router.py +++ b/tools/session_router.py @@ -73,9 +73,6 @@ def find_journal(topic_path): current = os.path.join(topic_path, "journal", "index.md") if os.path.isfile(current): return current - legacy = os.path.join(topic_path, "journal.md") - if os.path.isfile(legacy): - return legacy return None From 1f1d6527079ca780551dfa33163b841f81c52f80 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Wed, 8 Jul 2026 22:00:31 +0800 Subject: [PATCH 06/11] Remove reference to old misconception format --- tests/test_demo_index_writer.py | 26 -------------------------- tools/demo/demo_index_writer.py | 12 +++--------- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/tests/test_demo_index_writer.py b/tests/test_demo_index_writer.py index d446211..ea1947d 100644 --- a/tests/test_demo_index_writer.py +++ b/tests/test_demo_index_writer.py @@ -155,32 +155,6 @@ def test_entries_sorted_chronologically(self): self.assertLess(pos_second, pos_third) -class TestAppendLegacyFieldNames(unittest.TestCase): - """append — legacy field names (misconception_id -> weak_spot_id).""" - - def test_legacy_misconception_id_accepted(self): - with tempfile.TemporaryDirectory() as demos_dir: - demos = Path(demos_dir) - (demos / "legacy-demo.html").write_text("") - - legacy_entry = { - "misconception_id": "WS-10", - "misconception_description": "legacy description", - "demo_title": "Legacy Demo", - "demo_filename": "legacy-demo.html", - "related_reference": "ref-legacy.md", - "created_date": "2026-01-15", - } - result = _run_append(demos_dir, legacy_entry) - - self.assertEqual(result.returncode, 0) - self.assertIn("Appended", result.stdout) - - html = (demos / "index.html").read_text() - self.assertIn("WS-10", html) - self.assertIn("legacy description", html) - - class TestAppendMissingReference(unittest.TestCase): """append — missing related_reference shows 'No reference doc yet'.""" diff --git a/tools/demo/demo_index_writer.py b/tools/demo/demo_index_writer.py index 96f882a..af92b5e 100755 --- a/tools/demo/demo_index_writer.py +++ b/tools/demo/demo_index_writer.py @@ -84,7 +84,7 @@ # Regex to extract existing rows from the tbody ROW_RE = re.compile( r"\s*" - r"((?:WS-|M)\d+):\s*(.*?)\s*" + r"(WS-\d+):\s*(.*?)\s*" r"(.*?)\s*" r"(.*?)\s*" r"(\d{4}-\d{2}-\d{2})\s*" @@ -158,12 +158,6 @@ def build_index(rows: List[Dict[str, str]]) -> str: def cmd_append(demos_dir: Path, entry: Dict[str, Any]) -> None: """Append a new demo entry to the index.""" - # Accept legacy field names as fallback - if "misconception_id" in entry and "weak_spot_id" not in entry: - entry["weak_spot_id"] = entry.pop("misconception_id") - if "misconception_description" in entry and "weak_spot_description" not in entry: - entry["weak_spot_description"] = entry.pop("misconception_description") - # Validate required fields required = ["weak_spot_id", "weak_spot_description", "demo_title", "demo_filename", "created_date"] @@ -172,8 +166,8 @@ def cmd_append(demos_dir: Path, entry: Dict[str, Any]) -> None: print(f"Error: missing required fields: {', '.join(missing)}", file=sys.stderr) sys.exit(1) - # Validate weak_spot_id format (WS-N or legacy M-N) - if not re.match(r"^(WS-|M)\d+$", entry["weak_spot_id"]): + # Validate weak_spot_id format + if not re.match(r"^WS-\d+$", entry["weak_spot_id"]): print(f"Error: weak_spot_id must match WS-, got: {entry['weak_spot_id']}", file=sys.stderr) sys.exit(1) From 7789f08a3b9260863a13fba7e150f8801bec1d2a Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 14 Jul 2026 21:31:58 +0800 Subject: [PATCH 07/11] Trim card writer --- tests/test_card_writer.py | 15 --------------- tools/srs/card_writer.py | 15 +-------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/tests/test_card_writer.py b/tests/test_card_writer.py index 141725b..8d1d93d 100644 --- a/tests/test_card_writer.py +++ b/tests/test_card_writer.py @@ -61,7 +61,6 @@ ### Card 2 **Q** : Another bad line **A**: Answer here -**ID**: legacy-id-123 **Tags**: type:fact --- @@ -328,18 +327,6 @@ def test_validate_reports_bad_q_format(self): self.assertNotEqual(result.returncode, 0) self.assertIn("bad_q_format", result.stdout) - def test_validate_reports_legacy_id_line(self): - with tempfile.TemporaryDirectory() as tmpdir: - cards_md = Path(tmpdir) / "cards.md" - cards_md.write_text( - "### Card 1\n**Q:** Question\n**A:** Answer\n**ID**: legacy-123\n**Tags:** type:fact\n---\n" - ) - - result = _run(["validate", str(cards_md)]) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("legacy_id_line", result.stdout) - class TestFixRewritesFormat(unittest.TestCase): """fix — rewrites non-canonical format in-place.""" @@ -359,8 +346,6 @@ def test_fix_corrects_colon_placement(self): self.assertNotIn("**Q**:", content) self.assertNotIn("**A**:", content) self.assertNotIn("**Tags**:", content) - # Legacy ID lines should be removed - self.assertNotIn("**ID**:", content) def test_fix_then_validate_passes(self): """After fix, validate should pass.""" diff --git a/tools/srs/card_writer.py b/tools/srs/card_writer.py index 3942070..4f356bb 100644 --- a/tools/srs/card_writer.py +++ b/tools/srs/card_writer.py @@ -77,8 +77,6 @@ Q_BAD_RE = re.compile(r"\*\*Q\*\*:") # colon outside bold A_BAD_RE = re.compile(r"\*\*A\*\*:") TAGS_BAD_RE = re.compile(r"\*\*Tags\*\*:") -ID_LINE_RE = re.compile(r"^\*\*ID\*\*:\s*", re.IGNORECASE) - # --------------------------------------------------------------------------- # Core functions @@ -137,7 +135,7 @@ def build_existing_question_index(text: str) -> Dict[str, int]: continue # First-write wins: if two non-retired cards already share a normalized # form, keep the lower-numbered one as the canonical match. This case - # shouldn't happen once dedup is in force, but handles legacy decks. + # shouldn't happen once dedup is in force. index.setdefault(normalized, card["number"]) return index @@ -207,13 +205,6 @@ def validate_cards_md(text: str) -> List[Dict[str, Any]]: "text": stripped, "fix": stripped.replace("**Tags**:", "**Tags:**", 1), }) - if ID_LINE_RE.match(stripped): - issues.append({ - "line": i, - "type": "legacy_id_line", - "text": stripped, - "fix": "(remove line — IDs are derived from card number)", - }) return issues @@ -224,10 +215,6 @@ def fix_cards_md(text: str) -> Tuple[str, int]: result = [] for line in lines: stripped = line.strip() - # Skip legacy ID lines - if ID_LINE_RE.match(stripped): - fixes += 1 - continue # Fix colon placement if Q_BAD_RE.match(stripped): line = line.replace("**Q**:", "**Q:**", 1) From dc5ad34bb933814995461e5724e907f4791bcf0c Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 14 Jul 2026 21:48:35 +0800 Subject: [PATCH 08/11] Remove legacy kmap writer mentions --- tests/test_kmap_writer.py | 33 ------------------------------ tools/srs/kmap_writer.py | 43 +++++++++++---------------------------- 2 files changed, 12 insertions(+), 64 deletions(-) diff --git a/tests/test_kmap_writer.py b/tests/test_kmap_writer.py index ccb7cd8..0cb3550 100644 --- a/tests/test_kmap_writer.py +++ b/tests/test_kmap_writer.py @@ -3,13 +3,11 @@ Covers: - add-concept appends to last table in 5-col format -- add-concept appends to last table in 4-col (legacy) format - add-concept validates introduced format (S or prior) - add-concept rejects duplicate concept names - add-concept rejects missing required fields - update-status finds concept by name and updates status/last_tested/notes - update-status preserves Introduced in 5-col tables -- update-status works on 4-col (legacy) tables - update-status rejects unknown concept names - concept search is case-insensitive """ @@ -37,15 +35,6 @@ | Beta | Developing | S2 | S4 | Needs work | """ -KMAP_4COL = """\ -# Knowledge Map — Legacy - -| Concept | Status | Last Tested | Notes | -|---------|--------|-------------|-------| -| Gamma | Solid | S3 | Fine | -| Delta | Introduced | S1 | New | -""" - KMAP_MULTI_TABLE = """\ # Knowledge Map @@ -114,18 +103,6 @@ def test_add_concept_5col(self) -> None: body = (self.tmp / "knowledge-map.md").read_text() self.assertIn("| NewConcept | Introduced | S7 | S7 |", body) - def test_add_concept_4col_legacy(self) -> None: - self._write_kmap(KMAP_4COL) - result = run_writer( - "add-concept", str(self.tmp), "--stdin", - stdin=make_add_entry("Epsilon", introduced="S5", last_tested="S5"), - ) - self.assertEqual(result.returncode, 0, result.stderr) - body = (self.tmp / "knowledge-map.md").read_text() - # 4-col table: Introduced column not written - self.assertIn("| Epsilon | Introduced | S5 |", body) - self.assertNotIn("S5 | S5", body) - def test_add_concept_appends_to_last_table(self) -> None: self._write_kmap(KMAP_MULTI_TABLE) result = run_writer( @@ -226,16 +203,6 @@ def test_update_preserves_introduced(self) -> None: # Alpha was introduced at S1 — must still be S1 self.assertIn("| Alpha | Mastered | S1 | S20 |", body) - def test_update_status_4col_legacy(self) -> None: - self._write_kmap(KMAP_4COL) - result = run_writer( - "update-status", str(self.tmp), "--stdin", - stdin=make_update_entry("Gamma", status="Mastered", last_tested="S10"), - ) - self.assertEqual(result.returncode, 0, result.stderr) - body = (self.tmp / "knowledge-map.md").read_text() - self.assertIn("| Gamma | Mastered | S10 |", body) - def test_update_status_case_insensitive_lookup(self) -> None: self._write_kmap(KMAP_5COL) result = run_writer( diff --git a/tools/srs/kmap_writer.py b/tools/srs/kmap_writer.py index b9e5193..1da8b7d 100644 --- a/tools/srs/kmap_writer.py +++ b/tools/srs/kmap_writer.py @@ -116,13 +116,6 @@ def _is_concept_table_header(line: str) -> bool: cells = _parse_table_row(line) return len(cells) >= 4 and cells[0].lower() == "concept" and cells[1].lower() == "status" - -def _table_has_introduced(header_line: str) -> bool: - """Check if a concept table header includes the Introduced column.""" - cells = _parse_table_row(header_line) - return len(cells) >= 5 and cells[2].lower() == "introduced" - - def _find_all_concept_tables(lines: List[str]) -> List[Tuple[int, int, int]]: """Find all concept tables. Returns list of (header_idx, sep_idx, last_data_idx).""" tables: List[Tuple[int, int, int]] = [] @@ -144,18 +137,17 @@ def _find_all_concept_tables(lines: List[str]) -> List[Tuple[int, int, int]]: return tables -def _find_concept_row(lines: List[str], concept_name: str) -> Optional[Tuple[int, int, bool]]: +def _find_concept_row(lines: List[str], concept_name: str) -> Optional[Tuple[int, int]]: """Find a concept by name across all tables. - Returns (row_idx, header_idx, has_introduced) or None. + Returns (row_idx, header_idx) or None. """ tables = _find_all_concept_tables(lines) for header_idx, sep_idx, last_data in tables: - has_intro = _table_has_introduced(lines[header_idx]) for row_idx in range(sep_idx + 1, last_data + 1): cells = _parse_table_row(lines[row_idx]) if cells and cells[0].lower() == concept_name.lower(): - return (row_idx, header_idx, has_intro) + return (row_idx, header_idx) return None @@ -347,13 +339,9 @@ def cmd_add_concept(path: Path, entry: Dict[str, Any]) -> None: print("Error: no concept table found in knowledge-map", file=sys.stderr) sys.exit(1) - header_idx, sep_idx, last_data = tables[-1] - has_intro = _table_has_introduced(lines[header_idx]) + _header_idx, _sep_idx, last_data = tables[-1] - if has_intro: - row = f"| {concept} | {status} | {introduced} | {last_tested} | {notes} |" - else: - row = f"| {concept} | {status} | {last_tested} | {notes} |" + row = f"| {concept} | {status} | {introduced} | {last_tested} | {notes} |" lines.insert(last_data + 1, row) path.write_text("\n".join(lines), encoding="utf-8") @@ -379,22 +367,15 @@ def cmd_update_status(path: Path, entry: Dict[str, Any]) -> None: print(f"Error: concept '{concept}' not found", file=sys.stderr) sys.exit(1) - row_idx, header_idx, has_intro = result + row_idx, _header_idx = result cells = _parse_table_row(lines[row_idx]) - if has_intro: - # 5-col: Concept | Status | Introduced | Last Tested | Notes - current_introduced = cells[2] if len(cells) > 2 else "" - new_status = entry.get("status", cells[1] if len(cells) > 1 else "").strip() - new_last_tested = entry.get("last_tested", cells[3] if len(cells) > 3 else "").strip() - new_notes = entry.get("notes", cells[4] if len(cells) > 4 else "").strip() - lines[row_idx] = f"| {cells[0]} | {new_status} | {current_introduced} | {new_last_tested} | {new_notes} |" - else: - # 4-col: Concept | Status | Last Tested | Notes - new_status = entry.get("status", cells[1] if len(cells) > 1 else "").strip() - new_last_tested = entry.get("last_tested", cells[2] if len(cells) > 2 else "").strip() - new_notes = entry.get("notes", cells[3] if len(cells) > 3 else "").strip() - lines[row_idx] = f"| {cells[0]} | {new_status} | {new_last_tested} | {new_notes} |" + # 5-col: Concept | Status | Introduced | Last Tested | Notes + current_introduced = cells[2] if len(cells) > 2 else "" + new_status = entry.get("status", cells[1] if len(cells) > 1 else "").strip() + new_last_tested = entry.get("last_tested", cells[3] if len(cells) > 3 else "").strip() + new_notes = entry.get("notes", cells[4] if len(cells) > 4 else "").strip() + lines[row_idx] = f"| {cells[0]} | {new_status} | {current_introduced} | {new_last_tested} | {new_notes} |" path.write_text("\n".join(lines), encoding="utf-8") print(f"Updated concept '{concept}' in {path}") From 77090595ad9caa3975d3498f43d86141a612212e Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 14 Jul 2026 22:10:11 +0800 Subject: [PATCH 09/11] Remove legacy mentions from journal writer --- tests/test_journal_writer.py | 87 ++-------------------------- tools/srs/journal_writer.py | 106 +++++------------------------------ 2 files changed, 17 insertions(+), 176 deletions(-) diff --git a/tests/test_journal_writer.py b/tests/test_journal_writer.py index ee599a9..024b6ff 100644 --- a/tests/test_journal_writer.py +++ b/tests/test_journal_writer.py @@ -31,8 +31,8 @@ | 2 | 2026-06-02 | review | Closures | 3 | 3.67 | Review session | session-02.md | """ -# A legacy 5-column file (missing Reviews, Avg Grade, Summary) -LEGACY_FILE = """\ +# A 5-column file missing Reviews, Avg Grade, Summary — validate must reject it +FEWER_COLUMNS_FILE = """\ # Session Index | # | Date | Type | Focus | File | @@ -268,8 +268,8 @@ def test_reports_column_count_mismatch(self): self.assertNotEqual(result.returncode, 0) self.assertIn("issue", result.stdout.lower()) - def test_reports_missing_columns_in_legacy_format(self): - Path(self.index_path).write_text(LEGACY_FILE) + def test_reports_missing_columns(self): + Path(self.index_path).write_text(FEWER_COLUMNS_FILE) result = _run(["validate", self.index_path]) self.assertNotEqual(result.returncode, 0) @@ -282,78 +282,6 @@ def test_missing_file_exits_nonzero(self): self.assertIn("does not exist", result.stderr) -class TestFixNormalizesLegacyFormat(unittest.TestCase): - """fix — normalizes legacy format (fewer columns) to 8-column.""" - - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - self.index_path = os.path.join(self.tmpdir, "index.md") - Path(self.index_path).write_text(LEGACY_FILE) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def test_migrates_to_canonical_format(self): - result = _run(["fix", self.index_path]) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Fixed", result.stdout) - self.assertIn("2 rows", result.stdout) - - content = Path(self.index_path).read_text() - self.assertIn(CANONICAL_HEADER, content) - - # Original data preserved - self.assertIn("React Hooks", content) - self.assertIn("Closures", content) - self.assertIn("session-01.md", content) - - def test_adds_missing_columns_with_dashes(self): - _run(["fix", self.index_path]) - - content = Path(self.index_path).read_text() - # After fix, all rows should have 8 columns - data_lines = [ - l for l in content.split("\n") - if l.strip().startswith("|") - and not l.strip().startswith("| #") - and "---" not in l - and l.strip() - ] - for line in data_lines: - cells = [c.strip() for c in line.strip("|").split("|")] - self.assertEqual(len(cells), 8, f"Expected 8 columns, got {len(cells)}: {line}") - - def test_validate_passes_after_fix(self): - _run(["fix", self.index_path]) - result = _run(["validate", self.index_path]) - self.assertEqual(result.returncode, 0, f"Validate failed after fix: {result.stdout}") - - -class TestFixNoOpOnCanonical(unittest.TestCase): - """fix — no-op on already-canonical file.""" - - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - self.index_path = os.path.join(self.tmpdir, "index.md") - Path(self.index_path).write_text(WELL_FORMATTED_FILE) - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def test_reports_no_fixes_needed(self): - result = _run(["fix", self.index_path]) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertIn("Already canonical", result.stdout) - - def test_file_unchanged(self): - original = Path(self.index_path).read_text() - _run(["fix", self.index_path]) - after = Path(self.index_path).read_text() - self.assertEqual(original, after) - - class TestErrorCases(unittest.TestCase): """Error cases: invalid JSON, missing file, missing args.""" @@ -384,13 +312,6 @@ def test_append_without_json_or_stdin_exits_nonzero(self): self.assertNotEqual(result.returncode, 0) self.assertIn("--json", result.stderr) - def test_fix_missing_file_exits_nonzero(self): - path = os.path.join(self.tmpdir, "nonexistent.md") - result = _run(["fix", path]) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("does not exist", result.stderr) - def test_validate_missing_file_exits_nonzero(self): path = os.path.join(self.tmpdir, "nonexistent.md") result = _run(["validate", path]) diff --git a/tools/srs/journal_writer.py b/tools/srs/journal_writer.py index 8893cde..58cab86 100644 --- a/tools/srs/journal_writer.py +++ b/tools/srs/journal_writer.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 """Deterministic journal index writer for journal/index.md. -Appends rows and normalizes journal index tables to the canonical 8-column -format. The LLM produces content; this script enforces formatting. +Appends rows to journal index tables in the canonical 8-column format. +The LLM produces content; this script enforces formatting. Commands: append --json '' Append a new row from JSON append --stdin Read row JSON from stdin validate Check for format violations - fix Normalize to canonical 8-column format -Canonical format (8-column superset): +Canonical format (8-column): | # | Date | Type | Focus | Reviews | Avg Grade | Summary | File | All columns except #, Date, Focus are optional (default to —). @@ -110,31 +109,6 @@ def _map_headers(source_headers: List[str]) -> List[Optional[str]]: return result -def _remap_row(row: List[str], source_headers: List[str], canonical_map: List[Optional[str]]) -> Dict[str, str]: - """Convert a row from source format to a dict keyed by canonical column names. - - Handles short rows (fewer cells than headers) by mapping positionally, - then using heuristics to fix misplacements: - - If a value looks like a filename (session-*.md) but isn't mapped to "File", - move it to "File" and clear the wrong slot. - """ - d: Dict[str, str] = {} - for i, cell in enumerate(row): - if i < len(canonical_map) and canonical_map[i]: - d[canonical_map[i]] = cell.strip() - - # Heuristic: detect filename in wrong column - file_re = re.compile(r"^session-\S+\.md$") - if "File" not in d or not d.get("File", "").strip() or d.get("File", "—") == "—": - for col_name, value in list(d.items()): - if col_name != "File" and file_re.match(value.strip()): - d["File"] = value.strip() - d[col_name] = "—" - break - - return d - - def _format_row(data: Dict[str, str]) -> str: """Format a row dict into a canonical table row.""" cells = [] @@ -143,14 +117,6 @@ def _format_row(data: Dict[str, str]) -> str: return "| " + " | ".join(cells) + " |" -def _build_table(rows: List[Dict[str, str]]) -> str: - """Build a complete canonical table from row dicts.""" - lines = [HEADER_LINE, SEPARATOR_LINE] - for row in rows: - lines.append(_format_row(row)) - return "\n".join(lines) - - # --------------------------------------------------------------------------- # Commands # --------------------------------------------------------------------------- @@ -201,15 +167,16 @@ def cmd_append(path: Path, row_json: Dict[str, Any]) -> None: # Already canonical — just append content = text.rstrip() + "\n" + _format_row(new_row) + "\n" else: - # Non-canonical — migrate existing rows + append - migrated_rows = [] - for row in rows: - migrated_rows.append(_remap_row(row, headers, canonical_map)) - migrated_rows.append(new_row) - table = _build_table(migrated_rows) - content = pre.rstrip() + "\n\n" + table + "\n" - if post.strip(): - content += "\n" + post.lstrip("\n") + # Not canonical — refuse rather than silently rewriting the table. + mapped = {m for m in canonical_map if m} + missing = set(CANONICAL_HEADERS) - mapped + print( + f"Error: {path} is not in canonical 8-column format " + f"(missing columns: {', '.join(sorted(missing))}). " + f"Expected: {' | '.join(CANONICAL_HEADERS)}", + file=sys.stderr, + ) + sys.exit(1) path.write_text(content, encoding="utf-8") print(f"Appended session {session_num} to {path}") @@ -265,47 +232,6 @@ def cmd_validate(path: Path) -> None: sys.exit(1) -def cmd_fix(path: Path) -> None: - """Normalize journal/index.md to canonical 8-column format.""" - if not path.exists(): - print(f"Error: {path} does not exist", file=sys.stderr) - sys.exit(1) - - text = path.read_text(encoding="utf-8") - headers, rows, pre, post = _parse_table(text) - - if headers is None: - print(f"No table found in {path} — nothing to fix") - return - - canonical_map = _map_headers(headers) - mapped = {m for m in canonical_map if m} - - if mapped == set(CANONICAL_HEADERS): - # Check row widths for mismatches - needs_fix = False - for row in rows: - if len(row) != len(headers): - needs_fix = True - break - if not needs_fix: - print(f"Already canonical — no fixes needed in {path}") - return - - # Migrate all rows - migrated_rows = [] - for row in rows: - migrated_rows.append(_remap_row(row, headers, canonical_map)) - table = _build_table(migrated_rows) - - content = pre.rstrip() + "\n\n" + table + "\n" - if post.strip(): - content += "\n" + post.lstrip("\n") - - path.write_text(content, encoding="utf-8") - print(f"Fixed {path} — migrated {len(migrated_rows)} rows to canonical 8-column format") - - # --------------------------------------------------------------------------- # CLI # --------------------------------------------------------------------------- @@ -333,9 +259,6 @@ def main() -> None: p_validate = subparsers.add_parser("validate", help="Check for format violations") p_validate.add_argument("path", help="Path to journal/index.md or learning directory") - p_fix = subparsers.add_parser("fix", help="Normalize to canonical 8-column format") - p_fix.add_argument("path", help="Path to journal/index.md or learning directory") - args = parser.parse_args() path = _resolve_path(args.path) @@ -363,9 +286,6 @@ def main() -> None: elif args.command == "validate": cmd_validate(path) - elif args.command == "fix": - cmd_fix(path) - if __name__ == "__main__": main() From 5386ac2ab1fd7634da7ad7234d4fcc9dbf4978f9 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 14 Jul 2026 22:11:32 +0800 Subject: [PATCH 10/11] remove legacy mentions from coach metrics --- tools/coach/coach_metrics.py | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/tools/coach/coach_metrics.py b/tools/coach/coach_metrics.py index e5a7b7c..62c8746 100644 --- a/tools/coach/coach_metrics.py +++ b/tools/coach/coach_metrics.py @@ -130,24 +130,15 @@ def parse_changelog_solid_sessions(path: Path) -> Dict[str, int]: continue cells = [c.strip() for c in line.strip().split("|")[1:-1]] - if len(cells) < 4: + if len(cells) < 5: continue if cells[0].lower() == "date" or "---" in cells[0]: continue - # Handle both formats: - # 5-col: Date | Concept | From | To | Session - # 4-col: Date | Concept | From → To | Session + # 5-col: Date | Concept | From | To | Session concept = cells[1].strip().lower() session_str = cells[-1].strip() - - if len(cells) >= 5: - to_status = cells[3].strip().lower() - else: - # 4-col: parse "From → To" cell - arrow_cell = cells[2] - parts = re.split(r"\s*→\s*", arrow_cell) - to_status = parts[-1].strip().lower() if parts else "" + to_status = cells[3].strip().lower() if to_status != "solid": continue @@ -188,22 +179,16 @@ def parse_changelog_regressions(path: Path) -> List[Dict[str, Any]]: continue cells = [c.strip() for c in line.strip().split("|")[1:-1]] - if len(cells) < 4: + if len(cells) < 5: continue if cells[0].lower() == "date" or "---" in cells[0]: continue + # 5-col: Date | Concept | From | To | Session concept = cells[1].strip() session_str = cells[-1].strip() - - if len(cells) >= 5: - from_status = cells[2].strip().lower() - to_status = cells[3].strip().lower() - else: - arrow_cell = cells[2] - parts = re.split(r"\s*→\s*", arrow_cell) - from_status = parts[0].strip().lower() if len(parts) >= 2 else "" - to_status = parts[-1].strip().lower() if parts else "" + from_status = cells[2].strip().lower() + to_status = cells[3].strip().lower() if from_status != "solid": continue From 5714488120c014267b4af916dde83f45b1a4ebe2 Mon Sep 17 00:00:00 2001 From: 0-BSCode Date: Tue, 14 Jul 2026 22:11:57 +0800 Subject: [PATCH 11/11] Remove legacy tests --- tests/test_demo_index_writer.py | 2 +- tests/test_enforce_cross_refs.py | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/tests/test_demo_index_writer.py b/tests/test_demo_index_writer.py index ea1947d..6058015 100644 --- a/tests/test_demo_index_writer.py +++ b/tests/test_demo_index_writer.py @@ -2,7 +2,7 @@ """Regression-safety tests for demo_index_writer.py. Tests the CLI contract via subprocess — no internal imports. -Verifies append (create, dedup, sort, legacy fields), validate, +Verifies append (create, dedup, sort), validate, and error handling (invalid JSON, missing fields, bad WS format). """ diff --git a/tests/test_enforce_cross_refs.py b/tests/test_enforce_cross_refs.py index b5b98e5..9ceb245 100644 --- a/tests/test_enforce_cross_refs.py +++ b/tests/test_enforce_cross_refs.py @@ -118,21 +118,6 @@ def test_kmap_modified_crossrefs_stale_blocks(self): self.assertEqual(result.returncode, 0) self.assertIn("block", result.stdout) - # ------------------------------------------------------------------ - # Legacy cross-references.md fallback works - # ------------------------------------------------------------------ - def test_legacy_crossrefs_fallback(self): - now = time.time() - kmap = os.path.join(self.tmpdir, "knowledge-map.md") - self._touch(kmap, mtime=now) - - legacy_file = os.path.join(self.tmpdir, "cross-references.md") - self._touch(legacy_file, mtime=now) - - inp = self._base_input(self.tmpdir) - result = self._run(inp) - self.assertEqual(result.returncode, 0) - self.assertNotIn("block", result.stdout) # ------------------------------------------------------------------ # No knowledge-map.md exists -- exits 0