From dbe93fa4a042d43b49364cdd126322604c8da5e2 Mon Sep 17 00:00:00 2001 From: Abe Gong Date: Thu, 25 Jun 2026 13:55:34 -0600 Subject: [PATCH 1/6] Document release install and agent bootstrap --- docs/content/getting-started.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md index fe011a3..49ffcfe 100644 --- a/docs/content/getting-started.md +++ b/docs/content/getting-started.md @@ -59,13 +59,13 @@ katalyst check - `.katalyst/config.yaml`, commented project settings - `.katalyst/schemas/`, one schema per file (empty to start) -- `.katalyst/bases/local.yaml`, the default base (the local +- `.katalyst/storage/local.yaml`, the default storage instance (the local filesystem), where you declare collections It writes no example content. Add a schema under `.katalyst/schemas/` and -declare a collection inside `.katalyst/bases/local.yaml`, then run +declare a collection inside `.katalyst/storage/local.yaml`, then run `katalyst check`. Next: -- [Configs]({{< relref "reference/configs/_index.md" >}}) +- [Configuration]({{< relref "reference/configuration.md" >}}) - [Check types reference]({{< relref "reference/check-types/_index.md" >}}) From 8e5a175d6f691cb3c4db1f043fce624f55106ffc Mon Sep 17 00:00:00 2001 From: Abe Gong Date: Mon, 29 Jun 2026 13:29:11 -0600 Subject: [PATCH 2/6] Add filesystem checks specification --- product/specs/filesystem-checks-spec.md | 476 ++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 product/specs/filesystem-checks-spec.md diff --git a/product/specs/filesystem-checks-spec.md b/product/specs/filesystem-checks-spec.md new file mode 100644 index 0000000..bf452a3 --- /dev/null +++ b/product/specs/filesystem-checks-spec.md @@ -0,0 +1,476 @@ +# Spec - filesystem checks and collection checks + +> **Status: planning.** Adds filesystem-attached checks that run before +> collections exist, while keeping collection-attached checks and sharing check +> implementations across both attachment points. + +## Overview + +Katalyst checks are attached to collections today. That makes schemas, variants, +and item-level rules coherent, but it prevents basic filesystem policy from +running until the user has defined collections. Introduce a second attachment +point, **FilesystemChecks**, for path-selected files under filesystem storage +instances. Keep **CollectionChecks** as the checks attached to collection +definitions. + +## Value + +Users can enforce early, concrete rules while they are still profiling a +project: + +- Markdown filenames use kebab-case. +- Markdown content does not live deeper than N directories. +- A subtree contains only allowed extensions. +- Every populated directory has an index file. +- Asset references point at existing files. + +The same check type works under a collection and under a filesystem scope when +both contexts provide the data it needs. A rule does not get a duplicate +implementation or a duplicate docs page just because it has two attachment +points. + +## Current State + +`cmd/check.go` resolves selectors through `project.Resolve`. Selectors name +collections or collection items. With no selectors, Katalyst expands every +configured collection. A freshly initialized project has no collections, so +`katalyst check` succeeds because there is nothing to validate. + +`internal/storage/collection/parse.go` gives collections one check attachment +site: `schema:` plus `checks:`. The parser folds `schema:` into a leading +`object` check and parses each configured check through the registry. It rejects +collections with no schema, checks, or variants. + +`cmd/engine.go` builds runnable per-item checks from a collection's configured +checks and variants. It also builds collection-scoped checks in a second pass. +The runtime distinction already exists: + +- `checks.Check` runs once per item with `checks.Context`. +- `checks.CollectionCheck` runs once per collection with + `checks.CollectionContext`. + +`internal/checks/registry.go` documents check types with one `Descriptor`. The +descriptor has `Family`, which says what data the check reads, and `Scope`, +which currently says whether the check is collection-scoped. It does not say +where the check is attachable. + +The current terms collide. A "filesystem check" can mean either: + +- a check type in the `fileSystem` family, such as + `filesystem_name_case`; or +- a future check instance attached to the filesystem instead of a collection. + +This spec separates those axes. + +## Design + +### Attachment Targets + +Add an attachment-target axis to check descriptors: + +```go +type Descriptor struct { + // existing fields... + Targets []string // "collection", "filesystem" +} +``` + +`Family` keeps its current meaning: the source data the check reads. +`Targets` says where a check instance may be configured. + +During migration, an empty `Targets` means `["collection"]`. That keeps every +existing check valid until each descriptor opts into filesystem attachment. + +Use these concepts in product language: + +- **CollectionCheck:** a check instance attached to a collection definition. +- **FilesystemCheck:** a check instance attached to a filesystem scope. +- **FileCheck:** a runtime check that runs once per file. +- **FileSetCheck:** a runtime check that runs once per selected file set. + +The last two replace the overloaded internal names over time. Today's +`checks.Check` is a FileCheck. Today's `checks.CollectionCheck` is a +FileSetCheck. + +### Config Shape + +Keep collection checks where they are: + +```yaml +collections: + posts: + path: content/posts + pattern: "*.md" + checks: + - kind: filesystem_name_case + style: kebab + - kind: markdown_requires_h1 +``` + +Do not rename collection `checks:` to `collectionChecks:`. Inside a collection, +the attachment target is already clear. A rename adds migration cost without +making the config easier to write. + +Add `filesystemChecks` to filesystem storage instances: + +```yaml +# .katalyst/storage/local.yaml +type: filesystem +root: . + +filesystemChecks: + - name: docs + path: docs/content + include: ["**/*.md"] + exclude: ["**/_generated/**"] + checks: + - kind: filesystem_name_case + style: kebab + - kind: filesystem_path_depth + max: 4 + - kind: filesystem_index_file_required + name: _index.md + +collections: + pages: + path: docs/content + pattern: "**/*.md" + schema: page +``` + +Each `filesystemChecks` entry defines a filesystem scope: + +| Key | Required | Default | Meaning | +|---|---|---|---| +| `name` | no | `path` | Diagnostic label and future selector handle. | +| `path` | no | `.` | Scope root, relative to the storage instance root. | +| `include` | yes | - | Glob patterns relative to `path`. | +| `exclude` | no | `[]` | Glob patterns removed from the included set. | +| `parseFailures` | no | `error` | Severity for document parse failures: `error` or `warning`. | +| `checks` | yes | - | Check instances to run over selected files. | + +Names are optional. A named scope gives diagnostics and future selective +execution a stable handle. An unnamed scope uses its `path` as its diagnostic +label. + +`include` is required in the first cut. Filesystem scopes serve both path-only +checks and document-aware checks, and a default would surprise one of those +cases. A later release can add a default after real configs show whether +filesystem scopes are mostly broad path scans or Markdown-focused checks. + +`parseFailures` applies only when at least one check in the scope needs +document data. The default is `error`, matching collection checks and CI +expectations. Set it to `warning` for onboarding a messy tree while still +surfacing skipped document-aware checks. + +The same check type can appear in both attachment sites when its descriptor +supports both targets: + +```yaml +type: filesystem +root: . + +filesystemChecks: + - name: allMarkdown + path: . + include: ["**/*.md"] + checks: + - kind: filesystem_name_case + style: kebab + +collections: + posts: + path: content/posts + pattern: "*.md" + checks: + - kind: filesystem_name_case + style: kebab + - kind: markdown_requires_h1 +``` + +`filesystemChecks` is valid only on `type: filesystem` storage instances. +SQLite storage instances reject it. + +### Why Storage-Level Config + +Filesystem checks attach to filesystem storage because storage owns the root +path. This avoids a second project-level root and keeps path resolution beside +`collections:`. + +The alternative is project-level `filesystemChecks` in `.katalyst/config.yaml`: + +```yaml +filesystemChecks: + - name: docs + root: . + path: docs/content + include: ["**/*.md"] + checks: + - kind: filesystem_name_case + style: kebab +``` + +That shape is visible, but it duplicates storage root semantics. It also raises +unclear questions for SQLite-backed projects. Storage-level config fits the +existing model: a filesystem storage instance declares both its domain mapping +and its raw filesystem policy. + +### Shared Runtime Contexts + +Avoid duplicate implementations by normalizing both attachment points into the +same contexts. + +Rename or adapt `checks.Context` into a file-oriented context: + +```go +type FileContext struct { + FilePath string + Root string + Doc *markdownbodytext.Document + Meta map[string]any +} +``` + +For collection checks: + +- `FilePath` is the item path. +- `Root` is the collection directory. +- `Doc` and `Meta` come from `Project.ReadItem`. + +For filesystem checks: + +- `FilePath` is the selected file path. +- `Root` is the filesystem check scope root. +- `Doc` and `Meta` are populated only when a check declares that it needs + document data. + +Unify collection-scoped checks and filesystem-scoped checks around a file-set +context: + +```go +type FileSetContext struct { + Root string + Files []FileContext + Unmatched []string + Include []string + Exclude []string +} +``` + +Then existing set-level checks become target-independent: + +- `filesystem_unique_filename` groups `ctx.Files` by basename. +- `filesystem_index_file_required` groups `ctx.Files` by directory. +- `filesystem_unique_field` groups `ctx.Files` by metadata field. + +This runtime shape also fixes the naming collision. Internal +`CollectionCheck` means "set-level check" today. Product language needs +"CollectionCheck" to mean "attached to a collection." Rename the internal +interface after the filesystem runner lands. + +`Files` contains the files selected by the scope after `include` and `exclude`. +`Unmatched` contains regular files under the scope root that match neither +`include` nor `exclude`. Most checks ignore `Unmatched`; the +`filesystem_unmatched_files` check uses it to enforce raw subtree coverage. + +### Document Access + +FilesystemChecks are document-aware, but document parsing is lazy. + +Path-only checks do not open files. Checks that need metadata or markdown body +content declare that need in the registry. The filesystem runner parses only +when at least one check in the scope needs document data. + +Document parse failures use the filesystem scope's `parseFailures` severity. +With the default: + +```yaml +filesystemChecks: + - name: posts + path: content/posts + include: ["**/*.md"] + checks: + - kind: filesystem_name_matches_field + field: slug +``` + +an invalid document fails the run: + +```text +filesystem posts: content/posts/broken.md: /: cannot parse document: invalid frontmatter +``` + +With `parseFailures: warning`: + +```yaml +filesystemChecks: + - name: posts + path: content/posts + include: ["**/*.md"] + parseFailures: warning + checks: + - kind: filesystem_name_matches_field + field: slug +``` + +the same parse failure is advisory: + +```text +filesystem posts: content/posts/broken.md: warning: /: cannot parse document: invalid frontmatter; skipped document-aware checks +``` + +The runner always reports parse failures. A passing run proves that every +selected file needed by a document-aware check was inspected. + +This lets almost every current filesystem-related check run under both targets: + +| Check type | CollectionChecks | FilesystemChecks | +|---|---:|---:| +| `filesystem_extension_in` | yes | yes | +| `filesystem_parent_dir_in` | yes | yes | +| `filesystem_name_case` | yes | yes | +| `filesystem_name_affix` | yes | yes | +| `filesystem_path_charset` | yes | yes | +| `filesystem_name_regex` | yes | yes | +| `filesystem_name_length` | yes | yes | +| `filesystem_path_depth` | yes | yes | +| `filesystem_unique_filename` | yes | yes | +| `filesystem_index_file_required` | yes | yes | +| `filesystem_name_matches_field` | yes | yes, with metadata | +| `filesystem_parent_dir_matches_field` | yes | yes, with metadata | +| `filesystem_referenced_files_exist` | yes | yes, with metadata | +| `filesystem_unique_field` | yes | yes, with metadata | + +`filesystem_unique_field` is in the `structuredObject` family today despite its +historical `filesystem_` prefix. Keep the kind for compatibility. A future +rename can add an alias. + +### Filesystem Unmatched Files + +Add a new filesystem-only FileSetCheck: + +```yaml +filesystemChecks: + - name: docsCoverage + path: docs/content + include: ["**/*.md"] + exclude: ["assets/**", "_generated/**"] + checks: + - kind: filesystem_unmatched_files +``` + +`filesystem_unmatched_files` enforces scope coverage. Every regular file under +the scope root must either match `include` and enter the checked file set, or +match `exclude` and be intentionally ignored. A file that matches neither is an +unmatched file. + +Example diagnostic: + +```text +filesystem docsCoverage: docs/content/raw.txt: unmatched file (does not match include ["**/*.md"] or exclude ["assets/**", "_generated/**"]) +``` + +This check is separate from collection unmatched-file detection. Collection +unmatched detection remains the existing invariant: files inside a collection +directory must match the collection's `pattern`. The new check applies before +collections exist and only runs when the user configures it. + +### Command Behavior + +`katalyst check` with no selectors runs: + +1. all configured FilesystemChecks +2. all configured CollectionChecks + +Existing collection selectors stay collection-only: + +```text +katalyst check pages +katalyst check pages/home +``` + +Those commands do not run unrelated filesystem scopes. Selective filesystem +execution can come later through named scopes: + +```text +katalyst check --filesystem docs +``` + +Do not add filesystem selectors in the first implementation unless the CLI work +needs them. Running all filesystem scopes under the empty selector covers the +main onboarding and CI cases. + +### Diagnostics + +Diagnostics name the attachment target: + +```text +filesystem docs: docs/content/Old Note.md: /path: filename is not kebab-case +``` + +When a scope has no `name`, use its path: + +```text +filesystem docs/content: docs/content/Old Note.md: /path: filename is not kebab-case +``` + +Collection diagnostics keep the existing item path format unless the new target +label is needed to disambiguate output when both attachment types run. + +### Migration + +Existing configs keep working: + +- Collection `checks:` keeps its meaning. +- Check type `kind` names keep their meaning. +- Existing collection-scoped runtime interfaces can remain during the first + implementation. + +Implementation is additive: + +1. Add descriptor target metadata, defaulting to `collection`. +2. Add file and file-set runtime contexts without changing behavior. +3. Add `filesystemChecks` parsing to filesystem storage instances. +4. Run path-only FilesystemChecks. +5. Add lazy document parsing and opt metadata-aware check types into the + filesystem target. +6. Rename internal set-level interfaces away from `CollectionCheck`. + +## Open Questions + +_None._ + +## Documentation Updates + +- `docs/content/deep-dives/checks.md`: split data family, library, + attachment target, and runtime granularity. +- `docs/content/reference/configuration.md`: document storage-level + `filesystemChecks`. +- `docs/content/reference/glossary.md`: add CollectionCheck, FilesystemCheck, + FileCheck, FileSetCheck, and attachment target. Update Check instance and + Collection-scoped check. +- `docs/content/how-to/configure-rules.md`: add a pre-collection filesystem + check workflow. +- Generated check-type reference: render supported targets and file vs. + file-set granularity from descriptors. Run `make docs-gen`. +- `internal/checks/AGENTS.md`: document descriptor target metadata and the + runtime naming distinction. +- `internal/storage/collection/AGENTS.md`: document that collection `checks:` + remain collection-attached and filesystem checks live on filesystem storage + instances. +- `.cursor/skills/add-katalyst-check-type/SKILL.md`: update the checklist so a + new check type declares supported attachment targets. + +## Rejected Alternatives + +- **Rename collection `checks:` to `collectionChecks:`.** Rejected because the + collection block already supplies the target. The rename creates migration + work without changing behavior. +- **Put `filesystemChecks` in `.katalyst/config.yaml`.** Rejected because it + duplicates storage roots and makes filesystem policy independent of the + storage instance that owns path resolution. +- **Create separate check kinds for filesystem and collection attachment.** + Rejected because it splits docs and implementations for the same rule. +- **Infer collections to run filesystem rules.** Rejected because a filesystem + scope has no item identity, schema, variants, or selector namespace. It is not + a collection. From da2d1c8e331b10f7d2076af5c791988d5cb4439b Mon Sep 17 00:00:00 2001 From: Abe Gong Date: Mon, 29 Jun 2026 13:34:46 -0600 Subject: [PATCH 3/6] Add filesystem checks implementation plan --- product/specs/filesystem-checks-plan.md | 353 ++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 product/specs/filesystem-checks-plan.md diff --git a/product/specs/filesystem-checks-plan.md b/product/specs/filesystem-checks-plan.md new file mode 100644 index 0000000..aeeb661 --- /dev/null +++ b/product/specs/filesystem-checks-plan.md @@ -0,0 +1,353 @@ +# Plan - filesystem checks and collection checks +> Spec: [Filesystem checks and collection checks](./filesystem-checks-spec.md) +> **Status: planning.** + +## Current State + +- `cmd/check.go` runs collection-attached checks only. With no selectors it + resolves every collection through `project.Resolve`, runs per-item checks, + scans wholesale-selected collections for unmatched files, then runs + collection-scoped checks. +- `cmd/engine.go` builds checks from a `project.Collection`. It owns schema + compilation, library availability checks, variant routing, per-item builders, + and collection-scoped builders. +- `internal/project/loader.go` loads `.katalyst/bases/.yaml` files into + `BaseInstance` values. Legacy `.katalyst/storage/` remains readable, but the + current code and tests use bases. +- `internal/storage/collection/parse.go` parses collection config. It owns + `RawCheck`, folds `schema:` into an `object` check, parses check args through + `checks.Parse`, and rejects SQLite collections that configure file-system + family checks. +- `internal/checks/registry.go` records each check type's `Descriptor`, parser, + per-item builder, and collection-scoped builder. `Descriptor.Scope` names + collection-scoped runtime behavior. It has no attachment-target metadata. +- `internal/checks/checks.go` defines per-item `Context` and `Check`. + `internal/checks/collection.go` defines `CollectionContext` and + `CollectionCheck`. Those names mix runtime granularity with the future + product term CollectionCheck. +- `internal/storage/collection/filesystem/collection.go` already contains the + filesystem traversal pieces needed by filesystem scopes: doublestar matching, + sorted item discovery, and unmatched-file walking. +- `cmd/check_types.go` and `cmd/gendocs/main.go` render check descriptors for + CLI and generated docs. They currently render family, scope, severity, fields, + and config examples. +- `cmd/check_test.go`, `internal/project/loader_test.go`, + `internal/checks/registry_test.go`, and check-family tests are the main test + homes for this change. + +## Sequencing + +| Phase | Focus | Scope | +|---|---|---| +| 1 | Failing contracts | loader tests, check CLI tests, registry tests, snapshots | +| 2 | Shared check metadata and config parsing | descriptor targets, document needs, reusable raw check parsing | +| 3 | Filesystem scope config and expansion | base-level `filesystemChecks`, include/exclude matching, unmatched set | +| 4 | File and file-set runtime contexts | shared per-file context, set-level interface, collection compatibility | +| 5 | Filesystem check execution | no-selector execution, lazy parsing, parse-failure severity, diagnostics | +| 6 | Dual-target check types | target metadata, document-aware file-system checks, `filesystem_unmatched_files` | +| 7 | Documentation and verification | user docs, generated reference, developer docs, focused test suite | + +The order keeps the suite honest. First pin the behavior, then add registry and +config shape, then build the filesystem runner and opt check types into it. + +## Phases + +### Phase 1 - Failing contracts + +**Goal:** tests describe filesystem-attached checks before production code +exists. + +1. **File:** `internal/project/loader_test.go`. + Add load tests for `filesystemChecks` under a filesystem base: + optional `name`, required `include`, default `path: .`, default + `parseFailures: error`, explicit `parseFailures: warning`, and parsed + nested `checks`. +2. **File:** `internal/project/loader_test.go`. + Add rejection tests for missing `include`, unknown `parseFailures`, unknown + check kind, a check kind that does not support the `filesystem` target, and + `filesystemChecks` on a SQLite base. +3. **File:** `cmd/check_test.go`. + Add a no-selector CLI test where a project has no collections, a filesystem + scope includes `**/*.md`, and `filesystem_name_case` reports a bad Markdown + filename. +4. **File:** `cmd/check_test.go`. + Add a selector test proving `katalyst check notes` runs collection checks + only and does not run unrelated filesystem scopes. +5. **File:** `cmd/check_test.go`. + Add parse-failure tests for `filesystem_name_matches_field`: default + `parseFailures: error` exits 1, while `parseFailures: warning` reports a + warning and does not fail by itself. +6. **File:** `cmd/check_test.go`. + Add a CLI test for `filesystem_unmatched_files`: a file under the scope root + matching neither `include` nor `exclude` produces an unmatched-file + diagnostic. +7. **File:** `cmd/testdata/snapshots/check/`. + Add snapshots for filesystem diagnostics: path-rule failure, parse warning, + and filesystem unmatched file. +8. **File:** `internal/checks/registry_test.go`. + Add descriptor tests for supported targets and document-needs metadata. + +### Phase 2 - Shared check metadata and config parsing + +**Goal:** the registry describes where a check attaches and one parser serves +collection and filesystem config. + +1. **File:** `internal/checks/registry.go`. + Add target constants, `Descriptor.Targets []string`, and helpers such as + `SupportsTarget(kind, target)` and `DescriptorTargets(d)`. Treat an empty + target list as `collection` during migration. +2. **File:** `internal/checks/registry.go`. + Add document-needs metadata to `Descriptor`, for example + `NeedsDocument bool`, plus `NeedsDocument(kind)`. Filesystem scopes use this + to decide whether to parse selected files. +3. **File:** `internal/checks/config.go` (new). + Move the reusable raw check shape out of + `internal/storage/collection/parse.go`. Define `RawCheck`, key validation, + and a `BuildConfigured` helper that folds optional object schema shorthands + and calls `checks.Parse`. +4. **File:** `internal/storage/collection/parse.go`. + Replace `RawCheck` and `buildChecks` with the shared checks config helper. + Keep collection-specific schema shorthand and variant wiring behavior + byte-for-byte compatible. +5. **File:** `internal/storage/collection/parse.go`. + Keep SQLite collection rejection based on descriptor family or target support + so existing behavior stays stable. +6. **File:** `cmd/check_types.go`. + Include supported targets in `check-types show` and JSON output through the + descriptor. Keep existing scope and severity output. +7. **File:** `cmd/gendocs/main.go`. + Render supported targets on generated check-type pages. Keep generated docs + deterministic. + +### Phase 3 - Filesystem scope config and expansion + +**Goal:** filesystem bases load named scopes and expand them into deterministic +file sets. + +1. **File:** `internal/storage/filesystemcheck/scope.go` (new). + Add `RawScope` and `Scope` types with `Name`, `Path`, resolved `Root`, + `Include`, `Exclude`, `ParseFailures`, and parsed `Checks`. +2. **File:** `internal/storage/filesystemcheck/scope.go` (new). + Add `Build` to validate scope config: `include` required, + `parseFailures` is `error` or `warning`, `checks` required, and every check + supports the `filesystem` target. +3. **File:** `internal/storage/filesystemcheck/scope.go` (new). + Add deterministic expansion over `os.DirFS(scope.Root)` using doublestar: + selected files match at least one include and no exclude; unmatched files + are regular files that match neither include nor exclude. +4. **File:** `internal/storage/filesystemcheck/scope_test.go` (new). + Test include/exclude matching, sorted selected files, sorted unmatched files, + missing directories, invalid globs, and default labels for unnamed scopes. +5. **File:** `internal/project/loader.go`. + Add `FilesystemChecks []filesystemcheck.RawScope` to `rawBaseInstance`. + Build scopes only for `type: filesystem`, resolve paths against the base + root, and store them on `BaseInstance`. +6. **File:** `internal/project/loader.go`. + Reject `filesystemChecks` on non-filesystem bases with a load-time error. + Preserve legacy `.katalyst/storage/` readability by parsing the same field + there when that legacy directory is used. +7. **File:** `internal/project/project.go`. + Add a `FilesystemCheckScopes()` accessor or expose the loaded scopes through + `Config.Bases` in a way `cmd/check.go` can use without knowing raw config. +8. **File:** `internal/project/projecttest/projecttest.go`. + Add a helper for filesystem scope config only if it removes repeated YAML + from loader and CLI tests. + +### Phase 4 - File and file-set runtime contexts + +**Goal:** collection-attached and filesystem-attached checks share runtime +contexts without breaking existing collection checks. + +1. **File:** `internal/checks/checks.go`. + Add `FileContext` as the canonical per-file context. Keep `Context` as an + alias or compatibility wrapper during the migration. +2. **File:** `internal/checks/collection.go`. + Add `FileSetContext` with `Root`, `Files`, `Unmatched`, `Include`, and + `Exclude`. Include enough metadata for existing set-level checks and the new + unmatched-files check. +3. **File:** `internal/checks/collection.go`. + Add `FileSetCheck` and `RunFileSetAll`. Keep `CollectionCheck` and + `RunCollectionAll` as compatibility wrappers until collection callers move. +4. **File:** `internal/checks/filesystem/unique_filename.go`. + Convert `UniqueFilename` to the file-set context, or add a compatibility + adapter if full conversion waits until Phase 6. +5. **File:** `internal/checks/filesystem/index_file_required.go`. + Convert `IndexFileRequired` to the file-set context, preserving diagnostics. +6. **File:** `internal/checks/structuredobject/unique_field.go`. + Convert `UniqueField` to the file-set context with metadata read from each + file context. +7. **File:** `cmd/check.go`. + Update collection-scoped execution to build the new `FileSetContext` while + preserving existing output and selector behavior. + +### Phase 5 - Filesystem check execution + +**Goal:** `katalyst check` with no selectors runs filesystem scopes before +collection checks. + +1. **File:** `cmd/filesystem_check.go` (new). + Add the filesystem check runner: expand each scope, build runnable file and + file-set checks, run file checks per selected file, then run file-set checks. +2. **File:** `cmd/engine.go`. + Add helpers that build checks from an arbitrary list of + `checks.ConfiguredCheck`, separate from collection variant routing. Reuse + library availability checks and non-object builders. +3. **File:** `cmd/filesystem_check.go` (new). + Parse selected files lazily only when a configured check needs document data. + Strip the `schema` directive from metadata the same way `checkItem` does. +4. **File:** `cmd/filesystem_check.go` (new). + Implement `parseFailures`: default error-severity violations fail the run; + `warning` emits advisory diagnostics and does not fail by itself. +5. **File:** `cmd/filesystem_check.go` (new). + Format filesystem diagnostics as + `filesystem