Acceptance Criteria
Unconditional scan
- Plan plugin's
register hook performs an unconditional scan of plan.dir after processing site-loaded pages - All plan entities (spec, work, bug, decision, milestone) found in
plan.dir are registered into the EntityRegistry - Registrations include
sourceFile (project-root-relative path) and extract (function returning the top-level plan rune AST or null) - Registrations include the standard
data fields (title, status, tags, source, created, modified) - When a plan file is both in
plan.dir and part of a site's content tree, the site-load registration wins (with real sourceUrl); the unconditional scan skips to avoid duplicate registration - When
plan.dir doesn't exist or is empty, the scan is a silent no-op (no error) - Duplicate IDs across different plan files fail content load with both file paths named
Filename convention as hint, not filter
- Files in
plan.dir whose filenames don't match the auto-ID or milestone-semver convention are still parsed; if a valid top-level plan rune (with id=) is present, the entity is registered - Files in
plan.dir that contain no parseable top-level plan rune are skipped with a debug-level warning
File-roots opt-in
- Plan plugin declares
fileRoots: { plan: "../../plan" } (path relative to the plugin package directory) so partials and snippet can resolve plan:filename.md references
Downstream
- xref for plan IDs in non-plan-publishing sites finds the entity in the registry; falls through to SPEC-065 patterns when
sourceUrl is undefined - expand (SPEC-066) for plan IDs in non-plan-publishing sites finds the entity in the registry and substitutes its content
- Existing register-hook tests for site-loaded plan content continue to pass; new tests cover unconditional scan, duplicate detection across paths, missing-directory silence
Approach
Extend the existing register pipeline hook in plugins/plan/src/pipeline.ts to also scan plan.dir (resolved via _planDir) for .md files. For each parseable plan rune found, register the entity with sourceFile and a closure-captured extract function returning the top-level rune AST node. Path comparison against already-processed pages skips duplicates that come in through both site-load and unconditional-scan paths.
Filename-convention check is informational only — the rune's id= attribute determines the entity's identity. Files with no plan rune at all are skipped (covers READMEs and other auxiliary content).
Plugin fileRoots opt-in is added to the plan plugin's export.
Dependencies
- WORK-250 —
Plugin.fileRoots interface (needed for the plan: namespace opt-in)
References
- SPEC-064 — plan-registration spec (full)
- SPEC-066 — expand rune (primary consumer of
sourceFile + extract) - SPEC-065 — xref resolution (pattern fallback when
sourceUrl is undefined) plugins/plan/src/pipeline.ts:156 — _planDir plumbingplugins/plan/src/pipeline.ts:208 — existing register hook to extendplugins/plan/src/scanner.ts — existing file enumeration logic to share