--- name: migrate-guide description: Migrate a standalone guide or learning path to the Pathfinder package format by generating manifest.json (and path-level content.json for LJs). Reads content.json, index.json, recommender rules, and optionally website markdown to derive all manifest fields. Use when the user wants to migrate a guide directory to the package format, or asks to create a manifest.json for a guide. --- # Migrate Guide to Package Format Migrate a single guide directory or a learning path (`*-lj`) to the Pathfinder two-file package model (`content.json` + `manifest.json`). The skill is invoked on one directory at a time and is safe to run in parallel across different directories. **Read these reference documents on demand** for field-level detail: - `docs/manifest-reference.md` — authoritative derivation rules, templates, naming conventions, fallbacks - `.cursor/authoring-guide.mdc` — guide content conventions (for path-level content.json authoring) Keep this skill focused on workflow/orchestration. Do not duplicate field derivation tables from `docs/manifest-reference.md`. --- ## Safety Invariants These rules are **inviolable** — the skill must never break them: 1. **Never modify an existing `content.json`.** The skill may *create* a new `content.json` (path-level cover page) but must never modify one that already exists. 2. **Never modify `index.json`.** Read it as a data source; never write to it. 3. **Never modify recommender files.** The `grafana-recommender` repo is a read-only data source; never write to it. 4. **Verify after writing.** Before any writes, snapshot every in-scope pre-existing `content.json` as raw bytes (or SHA-256). After writes, confirm each snapshot is byte-identical. --- ## Batch Mode When this skill is invoked as a sub-agent by an orchestrator (i.e., the agent was not started interactively by a human), it operates in **batch mode**. In batch mode: - **Never block waiting for user input.** Any situation that would normally require asking the user is resolved by writing a `TODO` item in the migration notes instead. - **Mark the migration as incomplete** when a TODO item is written. Include `status: incomplete` in the migration-notes frontmatter and add a `## TODO` section listing every unresolved item. - **Continue past blockers.** Generate as much of the output as possible. A partially-completed manifest with TODO items is preferable to no output at all. **TODO item format** (use consistently throughout migration notes): ``` - [ ] TODO(): ``` Categories: `description`, `conflict`, `review`, `fallback`. Example: `- [ ] TODO(description): step "configure-alloy" has no description source — reviewer must supply a one-line catalog description` --- ## Mode Detection Determine the mode from the target directory: | Condition | Mode | |-----------|------| | Directory name ends with `-lj` OR a directory contains other nested directories with `content.json` | **Mode 2: Learning path** | | Otherwise | **Mode 1: Standalone guide** | --- ## Data Sources ### index.json (read-only) Location: `index.json` (repo root). **index.json is at the repository root only; there is no per-guide index.json.** Each rule has: `title`, `url`, `description`, `type`, `match`. Match a rule to a guide by: 1. Strip any trailing `/content.json` from the rule's `url` 2. Extract the last path segment (e.g., `alerting-101`) 3. Compare against the guide's directory name or `content.json` `id` If no rule matches, the guide has no targeting — omit the `targeting` field. ### Website learning path markdown (read-only, optional) Location: `/website/content/docs/learning-paths/` If the hardcoded path does not exist (e.g., on another machine or in CI), the agent may shallow-clone the `grafana/website` repo into a temporary directory and use that path instead; the structure under `content/docs/learning-paths/` is the same as in a local checkout. Map `*-lj` directory names to website paths by stripping the `-lj` suffix (e.g., `prometheus-lj` → `prometheus`). Step directory names are identical in both repos. The website markdown `pathfinder_data` frontmatter provides the authoritative mapping when present, but most steps lack it — fall back to directory name matching (see step 3 canonical mapping rules). If the website repo is unavailable, apply fallback rules from `docs/manifest-reference.md` under "When website markdown is unavailable" and flag affected fields for manual review. ### journeys.yaml (read-only, optional) Location: `/website/content/docs/learning-paths/journeys.yaml` Provides inter-journey category and relationship data. ### Recommender rules (read-only) Location: `grafana-recommender/internal/configs/state_recommendations/*.json` This is the **primary source of targeting/match rules for learning journeys**. `index.json` contains only `"type": "interactive"` entries — it never has learning-journey routing rules. All learning-journey match rules live in the recommender repo. **Locating the recommender repo (in priority order):** 1. Check for a local checkout at common paths: `~/hax/grafana-recommender`, `~/Documents/repositories/grafana-recommender`, or a sibling directory to the interactive-tutorials repo 2. If not found locally, shallow-clone `git@github.com:grafana/grafana-recommender.git` to `/tmp/grafana-recommender` 3. If a `/tmp/grafana-recommender` clone already exists, run `git -C /tmp/grafana-recommender pull --ff-only` to ensure it has the latest rules **Freshness:** Always ensure the recommender checkout is up-to-date before extracting rules. If using a `/tmp` clone, pull at the start of each migration run. Stale rules can lead to incorrect or missing targeting. **Matching rules to a learning path:** Each `*.json` file in `state_recommendations/` has a `rules` array. Filter to entries where `"type": "learning-journey"`. Match by extracting the slug from the `url` field: 1. Parse the `url` (e.g., `https://grafana.com/docs/learning-journeys/prometheus/`) 2. Extract the path name — the segment after `/learning-journeys/` or `/learning-paths/` (e.g., `prometheus`). The recommender uses both URL prefixes interchangeably; handle both. 3. Compare against the directory name with `-lj` stripped (e.g., `prometheus-lj` → `prometheus`) **Multiple rules per learning path:** A single learning path may have **multiple rules** across different recommender files (different URL contexts, different tags, different platform targets). Collect **all** matching rules. When a learning path has multiple rules: - **`targeting.match`**: If all rules share the same match expression, use it directly. If there are multiple distinct match expressions, wrap them in a top-level `{"or": [...]}` to preserve all routing contexts. Record which recommender files contributed which rules in the migration notes. - **`startingLocation`**: Traverse all collected match expressions, collect all URL-bearing leaves (`urlPrefix` values and `urlPrefixIn` entries), pick the first. - **`testEnvironment`**: Apply the standard tier inference rules across all collected matches. If any match contains `"targetPlatform": "cloud"`, the tier is `"cloud"`. If rules span both cloud and oss, use `"cloud"` (the more common deployment) and note the dual-platform targeting in migration notes. - **`description`**: If a recommender rule has a non-empty `description`, it is a valid source for the manifest description (see [Description Conventions](#description-conventions)). If the recommender repo is unavailable (clone fails, no network, no SSH key), fall back to index.json-only behavior and flag the absence in migration notes. --- ## Description Conventions The `description` field is a **compact, one-line summary** suitable for a course catalog listing. It is not introductory prose — that belongs in the `content.json` markdown blocks. **Priority for resolving `description`:** 1. **`index.json` rule or recommender rule** — if the guide has a matching rule in either source, use its `description` verbatim. These are already written in catalog style. For learning journeys, the recommender is the primary source since `index.json` does not contain learning-journey entries. Skip entries with empty `description` values. 2. **Summarize available sources** — if no rule with a non-empty description exists, collect all available description sources (website markdown frontmatter `description`, `content.json` title, path-level metadata) and boil them down into a single sentence. Write it in the style of the rule descriptions (e.g., "Hands-on guide: Learn how to..."). 3. **No sources at all** — if no sources exist at all, do not invent a description. - **Interactive mode:** Stop and ask the user for a description. - **Batch mode:** Write `- [ ] TODO(description): has no description source — reviewer must supply a one-line catalog description` in the migration notes, leave the `description` field as `"TODO: manual description required"`, and mark the migration incomplete. Continue generating all other fields. --- ## Author Conventions The `author` field has a `team` value that depends on content type: | Content type | `author.team` | |-------------|---------------| | Learning path (`type: "path"`) | `"Grafana Documentation"` | | Step within a learning path (inside a `*-lj` directory) | `"Grafana Documentation"` | | Standalone guide (not inside a `*-lj` directory) | `"interactive-learning"` | `"interactive-learning"` is the fallback default when content type is unknown. If you know the content is a learning path or a step within one, always use `"Grafana Documentation"`. The `author.name` field is optional. To derive it: 1. **`.github/CODEOWNERS` (preferred when guide-specific):** Read `.github/CODEOWNERS` from the interactive-tutorials repo root. If a **directory-scoped** rule applies to the package being migrated, use the listed GitHub handle(s) for `author.name` (strip the leading `@`; comma-separate multiple handles). - **Matching:** Normalize the migrated directory to a repo-relative path (e.g. standalone `alerting-101`, path `prometheus-lj`, step `prometheus-lj/add-data-source`). Find a line whose pattern is a path prefix for that directory, e.g. `/alerting-101/` or `/prometheus-lj/` (GitHub CODEOWNERS uses last matching rule wins). If a step directory has no own line, try the parent `*-lj` directory (e.g. `/prometheus-lj/` for any step under it). - **Do not** use generic review patterns as the author source: `*`, `**/content.json`, `**/manifest.json`, `**/assets/*`, or other repo-wide globs — those list many reviewers and are not primary author attribution. If only those patterns match, skip to git history. - Team owners (e.g. `@grafana/slo-squad`) may appear; use the handle as `name` (without `@`) when that is the listed owner. 2. **Git history** — if step 1 did not yield `author.name`, look at all git revisions since the `content.json` file was created (use `git log --follow` to track renames) 3. Prefer GitHub handles; extract commit author names/handles 4. Exclude any obvious automation or bot authors (e.g., `dependabot`, `renovate`, `github-actions`, `bot`, etc.) 5. If multiple authors remain, comma-separate them 6. If only full names (not handles) appear in git history, use full names — some data is better than none. If no authors remain after filtering bots, or if unsure, omit `name` entirely Record in migration notes when `author.name` came from CODEOWNERS vs git history. --- ## Mode 1: Standalone Guide Invoked on a guide directory (e.g., `alerting-101/`). ### Steps #### 1. Read content.json Read `{dir}/content.json` and extract the `id` and `title` fields. **Do not modify this file.** #### 2. Look up matching index.json rule Read `index.json` from the repo root. Find the rule whose `url` path segment matches the directory name or the `content.json` `id`. Record: - `description` from the rule - `match` object from the rule - `startingLocation`: traverse the `match` expression recursively, collect all URL-bearing leaves (`urlPrefix` values and entries from `urlPrefixIn` arrays), then pick the first one. If no URL can be extracted, omit `startingLocation` entirely — a missing value is preferable to a wrong one. If no rule matches, record that targeting is absent. #### 3. Check for website markdown (if step is inside a `*-lj`) If this guide directory is a direct child of a `*-lj` directory, look up the parent path's website markdown `_index.md` and extract `journey.group` for the `category` field. Otherwise, `category` defaults to `"general"`. #### 4. Derive testEnvironment **`testEnvironment` must NEVER be omitted.** Every manifest must include it. Apply these rules in order: **IF match expression exists (and is not empty):** - **IF** `match` contains `source` at any depth → evaluate the source value: - **If the source value is a regex matching all `*.grafana.net` hosts** (e.g., `".*\\.grafana\\.net"`) → `{ "tier": "cloud" }` — this means "any Grafana Cloud instance", so no specific `instance` is set - **If the source value is a concrete hostname** (e.g., `"play.grafana.org"`) → `{ "tier": "cloud", "instance": "" }` - **If the source value is any other regex or pattern** → `{ "tier": "cloud" }` and flag for manual review — do not copy a regex into `instance` - **ELSE IF** `match` contains `"targetPlatform": "cloud"` → `{ "tier": "cloud" }` - **ELSE** → `{ "tier": "local" }` **ELSE (no match expression or match expression is empty):** - → `{ "tier": "cloud" }` (minimum default) Note: An empty match expression (`match: {}`) is treated the same as no match expression — both default to `"cloud"`. #### 5. Generate manifest.json Write `{dir}/manifest.json` with the derived fields: ```json { "id": "", "type": "guide", "description": "", "category": "", "author": { "team": "" }, "startingLocation": "", "targeting": { "match": { "" } }, "testEnvironment": { "tier": "", "instance": "" }, "depends": [], "recommends": [], "suggests": [], "provides": [] } ``` Field omission rules: - Omit `repository` (schema default `"interactive-tutorials"` applies) - Omit `language` (schema default `"en"` applies) - Include `author.name` when derived per [Author Conventions](#author-conventions); omit otherwise - Omit `targeting` entirely if no targeting rule was found (index.json for standalone guides, recommender for learning journeys) - Omit `startingLocation` if no URL can be derived from the match expression — do not fall back to `"/"`. A missing value is preferable to a wrong one. - **Never omit `testEnvironment`.** Minimum is `{ "tier": "cloud" }`. Omit `instance` when no instance value is available. - Always include `depends`, `recommends`, `suggests`, and `provides` — even when empty (`[]`). This makes the fields visible to authors so they know they can fill them out later. Never invent values; use `[]` when no information is available. #### 6. Validate - Confirm `id` matches between `content.json` and `manifest.json` - Confirm the generated JSON is syntactically valid - Confirm no existing `content.json` was modified by byte-level comparison against the pre-write snapshot (raw bytes or SHA-256) #### 7. Run package validation (required) Run from the pathfinder-app checkout root, or use the full path to the CLI; pass the full path to the guide directory (e.g. `.../interactive-tutorials/alerting-101`) so it works from any cwd: ```bash node dist/cli/cli/index.js validate --package ``` This validation attempt is required for Phase 1 migration. If the command cannot run (CLI missing/unbuilt), treat this as an incomplete migration and explicitly report the blocker. If the CLI warns that `startingLocation` defaulted to `'/'`, that is expected when no index rule exists; the manifest correctly omitted it. #### 8. Write migration notes Write `{dir}/assets/migration-notes.md` following the [migration notes convention](#migration-notes). Include: - Which manifest was created and when - Which fields were derived from which sources - Result of `validate --package` - Any fields that need manual review (e.g., no index.json rule found, fallback used) - Any dangling references - Any surprises or unexpected situations #### 9. Report Tell the user: - Which manifest was created - Which fields were derived from which sources - Result of `validate --package` - Any fields that need manual review - Summary of migration notes written --- ## Mode 2: Learning Path Invoked on a `*-lj` directory (e.g., `prometheus-lj/`). ### Steps #### 1. Locate website markdown Map the directory name to the website path by stripping `-lj` (e.g., `prometheus-lj` → `prometheus`). Check for `/website/content/docs/learning-paths//`. If not found, apply fallback rules and flag for manual review. #### 2. Read path-level metadata Read `_index.md` from the website path. Extract from frontmatter: - `title` — for path-level content.json and manifest - `description` — a source for the manifest description (apply [Description Conventions](#description-conventions): if there is a recommender or `index.json` rule for this path with a non-empty description, prefer it; otherwise condense the `_index.md` description into a compact one-line catalog summary) - `journey.group` — for `category` - `journey.skill` — note but defer (not in current schema) - `journey.links.to` — for `recommends` - `related_journeys.items` — for `suggests` (default) or `depends` (only if unambiguously prerequisite). **Relationship strength heuristic:** when the `related_journeys.heading` text says "before" or "prerequisite" but the body content qualifies the relationship (e.g., "while not required"), use `suggests`. The body-level qualification takes precedence over the heading-level framing. Only use `depends` when both the heading *and* the body unambiguously describe a hard prerequisite with no "optional" or "recommended" qualifier. Example: heading says "Before you begin" but body says "while not required" → use `suggests`. Extract from body content: - All prose, learning objectives, prerequisites — for path-level content.json blocks #### 3. Read step metadata Build a canonical step map from website markdown. For each `/index.md` in the website path, extract: - `weight` — for ordering within the `milestones` array - `step` — step number (redundant with weight ordering, use as cross-check) - `pathfinder_data` — authoritative mapping to the interactive-tutorials directory (e.g., `prometheus-lj/add-data-source`) - `description` — for step manifest - `side_journeys` — for step `suggests` (see step 3a below for URL-to-ID resolution) Canonical mapping rules: - When present, treat `pathfinder_data` as authoritative for mapping website steps to interactive-tutorials step directories. Validate each target exists under the `*-lj` directory and has `content.json`. - **When `pathfinder_data` is absent** (common — most steps lack it), fall back to directory name matching: website step directory names are identical to interactive-tutorials step directory names within the same path. Confirm the match by verifying the directory exists and has `content.json`. Note which steps used name-matching fallback in the migration notes. - Build the path manifest `milestones` array from this map, ordered by `weight`. - Do not derive step order from local directory listing. #### 3a. Resolve side_journeys URLs to package IDs For each step's `side_journeys.items`, check whether any link matches the pattern `/docs/learning-paths//`. If so, resolve `` to `-lj` and check whether that directory exists in this repo. If the directory exists, add its ID to the step's `suggests` array. If the directory does not exist, the reference is still included (it may point to a not-yet-migrated path) — note it as a dangling reference in the migration notes. Links that do not match the learning path URL pattern (external docs, YouTube URLs, etc.) are not mappable to package IDs and should be ignored. #### 4. Migrate each step (Mode 1) For each mapped step in canonical `weight` order: 1. Read the step's `content.json` to get `id` and `title` (**do not modify**) 2. Check if the step has its own `index.json` entry (most steps don't — targeting lives at the path level) 3. Resolve description following the [Description Conventions](#description-conventions): 1. Matching step-level `index.json` rule `description` (first priority — already catalog-style) 2. Website step `index.md` `description` — if multi-sentence or verbose, condense to one line 3. Summarize from step `content.json` title + any other available context into a single catalog-style sentence 4. If no sources exist at all: do not guess. - **Interactive mode:** Stop and request a manual description for that step. - **Batch mode:** Write `- [ ] TODO(description): step "" has no description source — reviewer must supply a one-line catalog description` in the migration notes, set `"description": "TODO: manual description required"` in the step manifest, and mark the migration incomplete. Continue generating all remaining steps and the path manifest. 4. Generate `{step-dir}/manifest.json`: ```json { "id": "", "type": "guide", "description": "", "category": "", "author": { "team": "Grafana Documentation" }, "testEnvironment": { "tier": "" }, "depends": [""], "recommends": [""], "suggests": [], "provides": [] } ``` Include `author.name` when derived per [Author Conventions](#author-conventions) (CODEOWNERS often applies at the parent `*-lj` path for all steps); omit `name` when unknown. Step dependency rules: - Use each step's `content.json` `id` (not the directory name) in `depends`/`recommends` and in the path `milestones` array. - First step: omit `depends` - Step N+1: `depends` on step N's `id` - Last step: omit `recommends` (no next step) - Step N: `recommends` step N+1's `id` - If the step has `side_journeys`, map them to `suggests` Omit `targeting` unless the step has its own `index.json` entry. Steps within a learning path inherit targeting from the path level; step-level recommender rules are not expected and should not be searched for. #### 5. Check for metadata conflicts Compare metadata across sources (website markdown frontmatter, recommender rules, `index.json` rule if present, `journeys.yaml`). A conflict exists when the same field has different string values in two sources. **Flag conflicts** — do not silently pick one. - **Interactive mode:** Present both values and ask the user which to use. - **Batch mode:** Pick the higher-priority source (recommender > `_index.md` > `index.json` > `journeys.yaml`), write `- [ ] TODO(conflict): field "" has conflicting values — "" (from ) vs "" (from ); used ` in the migration notes, and mark the migration incomplete. #### 5a. Cross-validate journey.links.to against journeys.yaml Read `journeys.yaml` and find the entry whose `id` maps to the current learning path (e.g., `prom-data-source` for `prometheus-lj`). Compare the `links.to` values from `journeys.yaml` against the `journey.links.to` values from the `_index.md` frontmatter. If the IDs differ (e.g., `metrics-drilldown` in journeys.yaml vs `drilldown-metrics` in `_index.md`), flag the mismatch as a data quality issue in the migration notes. Use the `_index.md` value as authoritative (it maps to actual directory names in this repo) but record both values so the website team can reconcile the inconsistency. #### 5b. Duplicate description sanity check After resolving descriptions for all steps, compare them pairwise. If two or more sibling steps within the same path have identical `description` values, use the identical string for every step that has the same source description. Do not invent a variant. Record the duplicate in the migration notes and recommend an upstream fix. #### 6. Look up targeting rules Check **both** data sources for targeting rules, in this order: 1. **Recommender rules** (primary for learning journeys): Scan all `*.json` files in the recommender's `state_recommendations/` directory for entries with `"type": "learning-journey"` whose URL slug matches this path (see [Data Sources > Recommender rules](#recommender-rules-read-only) for matching logic). Collect **all** matching rules across all files. 2. **index.json** (fallback): Check if the `*-lj` directory name has an entry in `index.json`. In practice `index.json` does not contain learning-journey entries, but check anyway for future-proofing. Use the collected rules to derive `targeting`, `startingLocation`, and `testEnvironment` using the standard derivation rules (see Recommender rules data source for multi-rule handling). If rules were found in the recommender, record the source file(s) and the match expressions in the migration notes. If no rules are found in either source, the path has no targeting — omit the `targeting` field. #### 7. Generate path-level manifest.json Write `{lj-dir}/manifest.json`: ```json { "id": "", "type": "path", "description": "", "category": "", "author": { "team": "Grafana Documentation" }, "startingLocation": "", "targeting": { "match": { "" } }, "testEnvironment": { "tier": "" }, "milestones": [ "", "", "..." ], "depends": [], "recommends": [""], "suggests": [""], "provides": [] } ``` Include `author.name` on the path manifest when derived per [Author Conventions](#author-conventions); omit when unknown. Omit `targeting` if no targeting rule exists for the path (neither recommender nor index.json). Omit `startingLocation` if no URL can be derived — do not fall back to `"/"`. **Never omit `testEnvironment`.** Minimum is `{ "tier": "cloud" }`. Always include `depends`, `recommends`, `suggests`, and `provides` — use `[]` when no data is available. #### 8. Create path-level content.json **Only if `{lj-dir}/content.json` does not already exist.** If it exists, do not touch it. Derive from `_index.md` body content: ```json { "id": "", "title": "", "blocks": [ { "type": "markdown", "content": "" } ] } ``` Content transformation rules: - Strip Hugo shortcode tags (`{{< ... >}}`, `{{< /... >}}`) - For wrapping shortcodes (e.g., `{{< admonition >}}...{{< /admonition >}}`), strip tags but preserve inner content - For non-wrapping shortcodes with a `heading` attribute (e.g., `{{< docs/icon-heading heading="## Here's what to expect" >}}`), extract and preserve the heading value as a markdown header in the output - Convert remaining markdown into one or more `markdown` blocks - Preserve learning objectives, prerequisites, and descriptive prose - **Remove image links that use website-relative paths** — markdown images like `![alt text](/media/docs/...)` reference paths that only resolve on the Grafana website and will not function in Pathfinder. Strip these image references entirely (including their alt text and surrounding syntax). Retain any surrounding prose but clean up orphaned whitespace or empty paragraphs left behind. - **Remove "Grafana Cloud account" prerequisites** — any prerequisite or requirement bullet point that says the user needs a Grafana Cloud account (e.g., "A Grafana Cloud account. To create an account, refer to...") is redundant for Pathfinder users, who are already in Grafana. Remove these bullet points entirely. - **Record all removed content in migration notes** — for every image link or prerequisite removed by the above rules, record the exact text that was removed in the migration notes under a `## Content Removed During Migration` section. This provides a clear audit trail of what was stripped from the original website content. - Do NOT add a markdown title (`## Title`) — the `title` field handles that #### 9. Validate - Confirm `id` consistency: path manifest `id` matches the directory name, step manifest `id` matches step `content.json` `id` - Confirm `milestones` array in path manifest references valid step IDs that exist in step content.json files - Confirm step ordering matches website `weight` ordering - Confirm no pre-existing `content.json` in scope was modified by byte-level comparison against pre-write snapshots (including existing path-level `content.json`, if present) - Confirm all generated JSON is syntactically valid #### 10. Run package validation (required) Run from the pathfinder-app checkout root, or use the full path to the CLI; pass the full path to the guide directory (e.g. `.../interactive-tutorials/prometheus-lj`) so it works from any cwd: ```bash node dist/cli/cli/index.js validate --package ``` If you created step-level manifests, also run `validate --package ` for each created/updated step package. This validation attempt is required for Phase 1 migration. If the command cannot run (CLI missing/unbuilt), treat this as an incomplete migration and explicitly report the blocker. If the CLI warns that `startingLocation` defaulted to `'/'`, that is expected when no index rule exists; the manifest correctly omitted it. #### 11. Write migration notes Write `{lj-dir}/assets/migration-notes.md` following the [migration notes convention](#migration-notes). Include: - Path-level manifest and content.json created - N step manifests created (list them) - Fields derived from each source - Results of all `validate --package` commands - Recommender rules found (list source files, URLs, and match expressions) or note that none were found / repo was unavailable - Any metadata conflicts flagged (including journeys.yaml cross-validation mismatches, recommender vs other source conflicts) - Any duplicate descriptions detected - Any dangling references in `recommends`, `suggests`, or `depends` (these are expected and preferred — the CLI catches them) - Any side_journeys URLs resolved (or not resolved) to package IDs - Any fields that need manual review - Any fallbacks used due to missing website markdown or unavailable recommender repo - Any surprises or unexpected situations #### 12. Report Tell the user: - Path-level manifest and content.json created - N step manifests created (list them) - Fields derived from each source (including which recommender files contributed targeting rules) - Results of all `validate --package` commands - Any conflicts flagged - Any fields that need manual review - Any fallbacks used due to missing website markdown or unavailable recommender repo - Summary of migration notes written --- ## Reference-First Derivation For all field derivation logic and fallback rules, use `docs/manifest-reference.md` as the authoritative source: - `startingLocation` extraction (traverse recursively, collect all URL-bearing leaves, pick the first) - `testEnvironment` tier inference (IF/ELSE logic: source → cloud, targetPlatform: cloud → cloud, else → local; no match/empty match → cloud) - website-markdown fallback behavior Only include migration-specific orchestration logic in this skill. If this skill and `docs/manifest-reference.md` disagree, follow `docs/manifest-reference.md` and report the mismatch. --- ## Post-Migration Validation After generating all files, run this checklist: - [ ] Every `manifest.json` has a matching `content.json` in the same directory - [ ] `id` matches between each `manifest.json` and `content.json` pair - [ ] No pre-existing `content.json` was modified (byte-level check against pre-write snapshots) - [ ] `index.json` was not modified - [ ] Recommender repo files were not modified - [ ] Path manifests have `type: "path"` and a `milestones` array - [ ] Step manifests have `type: "guide"` - [ ] Step `depends`/`recommends` chains are consistent (no broken references within the current migration scope) - [ ] Dangling references (IDs in `depends`/`recommends`/`suggests` that point to directories not yet in the repo) are acceptable and **preferred** — always include them when the underlying data supports the reference. The Pathfinder CLI (`validate --package`) knows how to detect and report dangling references, so they will be caught during validation. It is better to produce more dangling references (which the CLI catches) than to silently drop relationships that exist in the source data. Record each dangling reference in the migration notes. - [ ] JSON is syntactically valid in all generated files - [ ] `node dist/cli/cli/index.js validate --package ` was run for each generated package (or a blocker was explicitly reported) --- ## Error Handling ### No targeting rule found For standalone guides, this means no `index.json` rule matched. For learning journeys, this means no recommender rule matched **and** no `index.json` rule matched. Generate the manifest without `targeting`. Omit `startingLocation` (do not default to `"/"`). `testEnvironment` defaults to `{ "tier": "cloud" }` (the minimum acceptable value — this applies when no match expression exists or when the match expression is empty). Flag for user review — the guide may be path-only (reachable via learning path, not contextual recommendation). If the CLI warns that `startingLocation` defaulted to `'/'`, that is expected; the manifest correctly omitted it. ### Recommender repo unavailable If the recommender repo cannot be found locally and the shallow clone fails (e.g., no network, no SSH key), fall back to index.json-only behavior. Flag all LJ targeting fields as "recommender unavailable — needs manual review" in the migration notes. This is a degraded but functional migration; for learning journeys the result will almost certainly lack targeting since `index.json` does not contain learning-journey entries. ### Website markdown not found Apply fallback rules. Clearly state which fields used fallback values and need manual review. ### Missing required step description during LJ fallback Follow the [Description Conventions](#description-conventions) priority: 1. Step-level `index.json` rule `description` (first priority — already catalog-style) 2. Website step `index.md` `description` — condense to one line if verbose 3. Summarize from step `content.json` title and any available context into a single catalog-style sentence 4. If no sources exist at all: apply the batch-mode rule from [## Batch Mode](#batch-mode) — in interactive mode, stop and ask; in batch mode, write a `TODO(description)` item and continue. Do not invent a description. ### content.json missing in a step directory This is unexpected. Report the missing file and skip that step. Do not create a content.json for a step — that is the content author's responsibility, not the migration skill's. ### Metadata conflict between sources Do not guess. Apply the batch-mode rule from [## Batch Mode](#batch-mode): - **Interactive mode:** Present both values, state the source of each, and ask the user to choose. - **Batch mode:** Pick the higher-priority source, record the conflict as a `TODO(conflict)` item in the migration notes, and mark the migration incomplete. --- ## Migration Notes Every migration produces a leave-behind document recording findings, decisions, surprises, and TODO items specific to that guide or path. This follows the `assets/` directory convention from `.cursor/skills/skill-memory.md`. ### Location - Standalone guide: `{dir}/assets/migration-notes.md` - Learning path: `{lj-dir}/assets/migration-notes.md` (one file for the entire path, covering path-level and all steps) ### Format ```markdown --- disclaimer: Auto-generated by migrate-guide. Do not edit manually. notice: To regenerate, re-run the migration skill on this directory. migrated_at: "" status: complete # set to "incomplete" when any TODO items are present --- # Migration Notes: ## Files Created - `manifest.json` — - (for paths) `content.json` — path-level cover page - (for paths) `/manifest.json` — one per step ## Field Derivation Summary | Field | Source | Value | |-------|--------|-------| | ... | ... | ... | ## Flags for Manual Review - - ## Content Removed During Migration - - Example: `Removed image: ![Example Logs Drilldown user interface](/media/docs/learning-journey/logs-drilldown/logs-drilldown.png)` — website-relative image path - Example: `Removed prerequisite: "A Grafana Cloud account. To create an account, refer to [Grafana Cloud](https://grafana.com/signup/cloud/connect-account)."` — redundant for Pathfinder users ## Dangling References - - (Dangling references are expected and preferred — the Pathfinder CLI catches them during validation) ## Recommender Rules | Source File | Rule URL | Match Expression | |-------------|----------|------------------| | | | | (If no recommender rules were found, note "No recommender rules found for this path" and explain whether this is expected or needs investigation. If the recommender repo was unavailable, note that here.) ## Data Quality Issues - - - - If any step lacked `pathfinder_data` and was mapped by directory name only, list those steps here. ## Surprises / Notes - ## TODO - [ ] ``` Omit any section that has no entries (e.g., if there are no dangling references, omit that section entirely). The goal is a concise, scannable document — not a verbose log. Path migration (Mode 2) produces significantly more complex notes than standalone guides (Mode 1) because of the variety of special circumstances that can arise: metadata conflicts across sources, step ordering nuances, shortcode stripping edge cases, relationship mapping ambiguities, and cross-repo data inconsistencies. The migration notes capture these per-path specifics so they are not lost. --- ## Example Invocations ### Standalone guide > "Migrate `alerting-101/` to the package format" The skill reads `alerting-101/content.json` (id: `alerting-101`), finds the matching `index.json` rule, and generates `alerting-101/manifest.json`. ### Learning path > "Migrate `prometheus-lj/` to the package format" The skill reads all 9 step `content.json` files, the website markdown at `learning-paths/prometheus/`, and searches both `index.json` and the recommender `state_recommendations/` files for targeting rules. It finds the recommender rule in `connections-cloud.json` matching `learning-journeys/prometheus/` and uses its match expression for targeting. It generates: - `prometheus-lj/manifest.json` (type: path, 9 milestones, targeting from recommender) - `prometheus-lj/content.json` (path-level cover page from website markdown) - 9 step-level `manifest.json` files