Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ Tests should always pass on `main`. Run `make test` before sending a PR.

```
cmd/ cobra commands (root, init, check, fix, inspect, collection, item, schema, rules)
internal/project project domain layer: the .katalyst/ loader (loader.go: schemas + storage instances, which embed their collections), the whole workspace, selectors, item enumeration
internal/storage backend-kind registry: StorageType, Known, Granularity, Reference
internal/project project domain layer: the .katalyst/ loader (loader.go: schemas + bases, which embed their collections), the whole workspace, selectors, item enumeration
internal/storage backend-kind registry: BaseType, Known, Scope, Reference
internal/storage/collection the read stack: CollectionDefinition + the thin Item
internal/storage/collection/listing item list filter/grep/sort/skip/limit pipeline
internal/storage/collection/predicate metadata predicate grammar (item list --filter, collection variants)
Expand Down Expand Up @@ -64,8 +64,8 @@ reconstruction), implemented per backend under `storage/collection/<backend>`
(filesystem today). Don't inline filesystem assumptions (globbing, stem-as-id,
path joins) elsewhere, a second backend (SQLite) attaches by implementing that
interface. The `internal/project` loader (`loader.go`) owns the `.katalyst/`
*vocabulary*: it reads the workspace, resolves schemas, and assembles storage
instances. Each object type owns the parse of its own config — the storage
*vocabulary*: it reads the workspace, resolves schemas, and assembles bases.
Each object type owns the parse of its own config — the storage
registry validates a declared `type` (`storage.Known`), and a collection parses
its own block, including variant predicates, in `storage/collection` (which
imports the sibling `predicate` grammar intra-subtree). The loader depends on
Expand Down Expand Up @@ -159,7 +159,7 @@ you add a fixture.
descriptor is malformed, so a new check type ships with a complete descriptor. The `json:` tags on `Descriptor`/`Field` are the published wire
contract for `katalyst check-types list --json`; keep them stable.
- A check type's **family** groups it by source-data kind, and is orthogonal to
its granularity: a collection-scoped check is filed by the data it reads
its scope: a collection-scoped check is filed by the data it reads
(`unique_field` → `structuredObject`, `unique_filename` → `fileSystem`). The
`kind` id is the wire contract and never changes, even when the family does.
- A **CheckLibrary** (`internal/checks`, `CheckLibrary`/`SchemaLibrary`) is the
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ As your content evolves, Katalyst gives you tools to navigate change.

- *Add or change checks*
- *Change the structure of your content*
- *Change your storage layer*
- *Change your base*

## Design principles

Expand Down
2 changes: 1 addition & 1 deletion cmd/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ When adding a top-level command, decide which family it joins:
selectors (`check`, `fix`). `init` and `inspect` are verbs too, they take
flags or a path rather than a selector. `inspect` infers its inspector
**layer** from the single argument: a configured collection name runs the
collection layer; anything else is a filesystem path for the raw-source layer
collection layer; anything else is a filesystem path for the raw base layer
(with no project, always raw). Layer selection is by argument, deliberately
not a flag, to keep the onboarding case (`inspect ./wiki`) flag-free.
- **Resource noun:** `katalyst <noun> <verb> <selector>`, a group whose
Expand Down
4 changes: 2 additions & 2 deletions cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func newCheckCmd() *cobra.Command {
Use: "check [selector ...]",
Short: "Run configured checks against the selected items",
Long: `check parses each selected item's frontmatter (YAML, TOML, or JSON)
and runs the checks configured for its collection under .katalyst/storage/.
and runs the checks configured for its collection under .katalyst/bases/.

Selectors (see docs/content/deep-dives/domain-model.md):
Selectors (see docs/content/deep-dives/domain-model/_index.md):

(none) the whole project (every collection)
<collection> one collection (all its items)
Expand Down
16 changes: 8 additions & 8 deletions cmd/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ func setupNotesRepo(t *testing.T, notesCollection string) string {
t.Helper()
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"config.yaml": schemaFormatJSON,
"schemas/book.json": bookSchemaFixture,
"storage/local.yaml": storageLocal(map[string]string{"notes": notesCollection}),
"config.yaml": schemaFormatJSON,
"schemas/book.json": bookSchemaFixture,
"bases/local.yaml": baseLocal(map[string]string{"notes": notesCollection}),
})
chdir(t, dir)
return dir
Expand Down Expand Up @@ -167,7 +167,7 @@ func setupVariantRepo(t *testing.T, pagesBody string) string {
"schemas/page.yaml": "type: object\nrequired: [title]\nproperties:\n title: {type: string}\n",
"schemas/section.yaml": "type: object\n",
"schemas/content.yaml": "type: object\nrequired: [weight]\nproperties:\n weight: {type: integer}\n",
"storage/local.yaml": storageLocal(map[string]string{"pages": pagesBody}),
"bases/local.yaml": baseLocal(map[string]string{"pages": pagesBody}),
})
chdir(t, dir)
return dir
Expand Down Expand Up @@ -335,7 +335,7 @@ func TestCheck_inlineSchemaKeyTakesPrecedence(t *testing.T) {
"config.yaml": schemaFormatJSON,
"schemas/book.json": bookSchemaFixture,
"schemas/strict-book.json": strictBookSchemaFixture,
"storage/local.yaml": storageLocal(map[string]string{"notes": "path: notes\nschema: book\n"}),
"bases/local.yaml": baseLocal(map[string]string{"notes": "path: notes\nschema: book\n"}),
})
chdir(t, dir)

Expand All @@ -356,7 +356,7 @@ func TestCheck_inlineSchemaKeyTakesPrecedence(t *testing.T) {
func TestCheck_markdownAndFilesystemChecks(t *testing.T) {
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"storage/local.yaml": storageLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: markdown_title_matches_h1\n field: title\n"}),
"bases/local.yaml": baseLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: markdown_title_matches_h1\n field: title\n"}),
})
chdir(t, dir)
mustWrite(t, filepath.Join(dir, "notes/dune.md"), "---\ntitle: Dune\n---\n# Children of Dune\n")
Expand All @@ -376,7 +376,7 @@ func TestCheck_markdownAndFilesystemChecks(t *testing.T) {
func TestCheck_collectionScoped_rescanFullCollectionForSingleItemSelector(t *testing.T) {
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"storage/local.yaml": storageLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: filesystem_unique_field\n field: slug\n"}),
"bases/local.yaml": baseLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: filesystem_unique_field\n field: slug\n"}),
})
chdir(t, dir)
mustWrite(t, filepath.Join(dir, "notes/a.md"), "---\nslug: dune\n---\n# A\n")
Expand All @@ -395,7 +395,7 @@ func TestCheck_collectionScoped_rescanFullCollectionForSingleItemSelector(t *tes
func TestCheck_writingTells_warnButPass(t *testing.T) {
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"storage/local.yaml": storageLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: markdown_writing_tells\n"}),
"bases/local.yaml": baseLocal(map[string]string{"notes": "path: notes\nchecks:\n - kind: markdown_writing_tells\n"}),
})
chdir(t, dir)
mustWrite(t, filepath.Join(dir, "notes/x.md"),
Expand Down
8 changes: 4 additions & 4 deletions cmd/check_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ func TestCheckTypes_listsEveryTypeGroupedByFamily(t *testing.T) {
// Family titles appear in Families() order.
last := -1
for _, title := range []string{
"Structured object check types",
"Markdown body text check types",
"File system check types",
"Plain text check types",
"Structured object",
"Markdown body text",
"File system",
"Plain text",
} {
i := strings.Index(stdout, title)
if i < 0 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func newCollectionCmd() *cobra.Command {
c := &cobra.Command{
Use: "collection",
Short: "Inspect collections declared by storage instances under .katalyst/storage/",
Short: "Inspect collections declared by bases under .katalyst/bases/",
}
c.AddCommand(newCollectionListCmd(), newCollectionGetCmd())
return c
Expand Down
2 changes: 1 addition & 1 deletion cmd/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ top-level keys sorted alphabetically, yaml.v3 default block style, and
exactly one trailing newline. The body is preserved verbatim.

fix never invents semantic values: it will not inject placeholders for
missing required keys. See docs/content/deep-dives/formatting.md for why.
missing required keys. See docs/content/deep-dives/domain-model/fix.md for why.

Selectors follow the same grammar as 'check'. With no selector, every
item in the project is considered.
Expand Down
4 changes: 2 additions & 2 deletions cmd/fix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func setupFixRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"storage/local.yaml": storageLocal(map[string]string{"notes": fixNotesConfig}),
"bases/local.yaml": baseLocal(map[string]string{"notes": fixNotesConfig}),
})
chdir(t, dir)
return dir
Expand All @@ -27,7 +27,7 @@ func setupFixRepoWith(t *testing.T, notesConfig string) string {
t.Helper()
dir := t.TempDir()
writeProject(t, dir, map[string]string{
"storage/local.yaml": storageLocal(map[string]string{"notes": notesConfig}),
"bases/local.yaml": baseLocal(map[string]string{"notes": notesConfig}),
})
chdir(t, dir)
return dir
Expand Down
2 changes: 1 addition & 1 deletion cmd/gendocs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func inspectorsIndex(layers []inspect.Layer, byLayer map[string][]inspect.Descri
fmt.Fprint(&b, "distributions, never recommendations. They are the descriptive dual of ")
fmt.Fprint(&b, "[check types]({{< relref \"../check-types/_index.md\" >}}) and drive the ")
fmt.Fprint(&b, "[`inspect`]({{< relref \"../cli.md\" >}}) command. They come in two layers: ")
fmt.Fprint(&b, "raw-source inspectors profile a store before configuration, collection ")
fmt.Fprint(&b, "raw base inspectors profile a base before configuration, collection ")
fmt.Fprint(&b, "inspectors profile a configured collection. These pages are generated from the ")
fmt.Fprint(&b, "inspector registry, so they always match the shipped binary.\n")
for _, layer := range layers {
Expand Down
14 changes: 7 additions & 7 deletions cmd/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ func chdir(t *testing.T, dir string) {
const schemaFormatJSON = "schemas:\n format: json\n"

// writeProject scaffolds a .katalyst/ tree. Keys are paths relative to the
// .katalyst/ directory (e.g. "schemas/book.json", "storage/local.yaml",
// .katalyst/ directory (e.g. "schemas/book.json", "bases/local.yaml",
// "config.yaml"); values are file contents.
func writeProject(t *testing.T, dir string, files map[string]string) {
t.Helper()
projecttest.WriteProject(t, dir, files)
}

// storageLocal builds a .katalyst/storage/local.yaml body: a filesystem
// instance rooted at the project, declaring the given collections. Each value
// baseLocal builds a .katalyst/bases/local.yaml body: a filesystem base rooted
// at the project, declaring the given collections. Each value
// is the collection's YAML body, re-indented under its name. Collections now
// live inside their storage instance, so tests scaffold them this way instead
// live inside their base, so tests scaffold them this way instead
// of one file per collection.
func storageLocal(collections map[string]string) string {
return projecttest.LocalStorage(collections)
func baseLocal(collections map[string]string) string {
return projecttest.LocalBase(collections)
}

// writeConfigDir writes the two-schema book-and-person project (book and
Expand All @@ -87,7 +87,7 @@ func writeConfigDir(t *testing.T) string {
"config.yaml": schemaFormatJSON,
"schemas/book.json": bookSchemaFixture,
"schemas/person.json": personSchemaFixture,
"storage/local.yaml": storageLocal(map[string]string{
"bases/local.yaml": baseLocal(map[string]string{
"books": "path: notes/books\nschema: book\n",
"people": "path: notes/people\nschema: person\n",
}),
Expand Down
32 changes: 16 additions & 16 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,24 @@ import (
// the available knobs.
const scaffoldConfig = `# katalyst project configuration.
#
# Schemas live in .katalyst/schemas/<name>.yaml. Storage instances live in
# .katalyst/storage/<name>.yaml, and each instance declares the collections it
# maps. The settings below are optional and shown at their defaults; uncomment
# to change them.
# Schemas live in .katalyst/schemas/<name>.yaml. Bases live in
# .katalyst/bases/<name>.yaml, and each base declares the collections it maps.
# The settings below are optional and shown at their defaults; uncomment to
# change them.
#
# schemas:
# discovery: convention # convention | explicit
# format: yaml # yaml | json | both
# storage:
# bases:
# discovery: convention
# format: yaml
`

// scaffoldLocalStorage is the default storage instance written by init: the
// local filesystem rooted at the project. There is no implicit instance,
// this file is what makes the default explicit. Collections are declared
// inline here (or split into .katalyst/storage/local/<name>.yaml).
const scaffoldLocalStorage = `# The default storage instance: the local filesystem, rooted at the project.
// scaffoldLocalBase is the default base written by init: the local filesystem
// rooted at the project. There is no implicit base, this file is what makes the
// default explicit. Collections are declared inline here (or split into
// .katalyst/bases/local/<name>.yaml).
const scaffoldLocalBase = `# The default base: the local filesystem, rooted at the project.
# Declare collections under "collections:", e.g.
#
# collections:
Expand Down Expand Up @@ -68,21 +68,21 @@ func newInitCmd() *cobra.Command {
return usageErr(fmt.Sprintf("%s already exists; refusing to overwrite", katalystDir))
}

for _, sub := range []string{"schemas", "storage"} {
for _, sub := range []string{"schemas", "bases"} {
rel := filepath.Join(project.Dir, sub)
if err := os.MkdirAll(filepath.Join(target, rel), 0o755); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "created %s/\n", rel)
}

// Write the default storage instance explicitly; katalyst never
// synthesizes one at runtime.
storageRel := filepath.Join(project.Dir, "storage", "local.yaml")
if err := os.WriteFile(filepath.Join(target, storageRel), []byte(scaffoldLocalStorage), 0o644); err != nil {
// Write the default base explicitly; katalyst never synthesizes one
// at runtime.
baseRel := filepath.Join(project.Dir, "bases", "local.yaml")
if err := os.WriteFile(filepath.Join(target, baseRel), []byte(scaffoldLocalBase), 0o644); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "created %s\n", storageRel)
fmt.Fprintf(cmd.OutOrStdout(), "created %s\n", baseRel)

cfgRel := filepath.Join(project.Dir, "config.yaml")
if err := os.WriteFile(filepath.Join(target, cfgRel), []byte(scaffoldConfig), 0o644); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ func TestInit_preparesKatalystDir(t *testing.T) {
for _, want := range []string{
".katalyst",
".katalyst/schemas",
".katalyst/storage",
".katalyst/storage/local.yaml",
".katalyst/bases",
".katalyst/bases/local.yaml",
".katalyst/config.yaml",
} {
if _, err := os.Stat(filepath.Join(dir, want)); err != nil {
Expand All @@ -39,7 +39,7 @@ func TestInit_writesNoExampleContent(t *testing.T) {
"schemas",
"notes",
".katalyst/schemas/book.yaml",
".katalyst/storage/local/notes.yaml",
".katalyst/bases/local/notes.yaml",
} {
if _, err := os.Stat(filepath.Join(dir, unwanted)); err == nil {
t.Errorf("did not expect %s to exist", unwanted)
Expand Down
6 changes: 3 additions & 3 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ finds as evidence: counts and distributions, never recommendations.
The layer is inferred from the argument. Inside a katalyst project, a
configured collection name (e.g. notes) runs the collection inspectors over
that collection's items. Otherwise the argument is a filesystem path and the
raw-source inspectors profile the tree (the onboarding case: "what's here?").
raw base inspectors profile the tree (the onboarding case: "what's here?").

Inspectors describe; they never recommend. inspect writes no schema and mutates
nothing. Output is Markdown by default; --json emits the same evidence as JSON.`,
Expand All @@ -37,7 +37,7 @@ nothing. Output is Markdown by default; --json emits the same evidence as JSON.`
params := inspect.Params{}
if selectExpr != "" {
if len(inspectors) != 1 || inspectors[0] != "file_content_shape" {
return usageErr("--select requires exactly one source inspector: --inspector file_content_shape")
return usageErr("--select requires exactly one raw base inspector: --inspector file_content_shape")
}
params = params.WithSelection(inspect.ParseSelection(selectExpr))
}
Expand Down Expand Up @@ -82,7 +82,7 @@ nothing. Output is Markdown by default; --json emits the same evidence as JSON.`

// runInspect selects the layer from the argument and runs its inspectors. A
// configured collection name runs the collection layer; anything else is a
// filesystem path for the raw-source layer.
// filesystem path for the raw base layer.
func runInspect(arg string, names []string, params inspect.Params) ([]inspect.Evidence, error) {
if proj, c, ok := resolveCollection(arg); ok {
return runCollectionLayer(proj, c, names, params)
Expand Down
2 changes: 1 addition & 1 deletion cmd/inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestInspect_rawPathRunsSourceLayer(t *testing.T) {

func TestInspect_collectionLayerWhenConfigured(t *testing.T) {
dir := t.TempDir()
writeFile(t, dir, ".katalyst/storage/local.yaml", `type: filesystem
writeFile(t, dir, ".katalyst/bases/local.yaml", `type: filesystem
root: .
collections:
notes:
Expand Down
2 changes: 1 addition & 1 deletion cmd/inspectors.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func newInspectorsCmd() *cobra.Command {
Short: "Inspect the inspectors katalyst can run, grouped by layer",
Long: `inspectors is a read-only view of katalyst's inspector registry, the same
catalog cmd/gendocs renders and that the inspect command runs. List every
inspector grouped by layer (raw-source, collection), or show one inspector's
inspector grouped by layer (raw base, collection), or show one inspector's
docs-style readout. It reads no project, so it runs in any directory.`,
}
c.AddCommand(newInspectorsListCmd(), newInspectorsShowCmd())
Expand Down
2 changes: 1 addition & 1 deletion cmd/inspectors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestInspectors_listsEveryInspectorGroupedByLayer(t *testing.T) {
}

last := -1
for _, title := range []string{"Raw-source inspectors", "Collection inspectors"} {
for _, title := range []string{"Raw base inspectors", "Collection inspectors"} {
i := strings.Index(stdout, title)
if i < 0 {
t.Errorf("expected layer title %q in output", title)
Expand Down
2 changes: 1 addition & 1 deletion cmd/item_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func setupItemRepo(t *testing.T) string {
"config.yaml": schemaFormatJSON,
"schemas/book.json": bookSchemaFixture,
"schemas/strict-book.json": strictBookSchemaFixture,
"storage/local.yaml": storageLocal(map[string]string{"notes": objectNotesConfig}),
"bases/local.yaml": baseLocal(map[string]string{"notes": objectNotesConfig}),
})
chdir(t, dir)
return dir
Expand Down
4 changes: 2 additions & 2 deletions cmd/testdata/snapshots/check-types/list-family-markdown.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Markdown body text check types (7)
----------------------------------
Markdown body text (7)
----------------------
- markdown_code_fence_language_required
purpose: Require that opening fenced code blocks include a language tag.
required: -
Expand Down
Loading