From c8b8f0d5609904c4d915edb6c23317a257b74cdb Mon Sep 17 00:00:00 2001 From: Pablo Ontiveros Date: Wed, 14 Oct 2026 00:35:46 -0600 Subject: [PATCH] docs(skill): correct schemaless command guidance --- .claude/skills/rootline/SKILL.md | 24 +++++++-- .claude/skills/rootline/ref-query.md | 12 ++++- CHANGELOG.md | 1 + cmd/rootline/skill_documentation_test.go | 65 ++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/.claude/skills/rootline/SKILL.md b/.claude/skills/rootline/SKILL.md index 91391711..9cc94642 100644 --- a/.claude/skills/rootline/SKILL.md +++ b/.claude/skills/rootline/SKILL.md @@ -29,16 +29,32 @@ Use `rootline skill status --source ` before remediat ## Governance Boundary (root marker) -Schema discovery walks up from the target collecting `.stem` files and stops at a `.stem` that declares `root: true`, or at the filesystem root. `.git` is not a boundary. A project must declare where it starts with a `root: true` marker in its top-level `.stem`. - -- Governed commands (`validate`, `fix`, `query`, `tree`, `graph`, `describe`, `explain`, `set`, `stats`) fail on a tree with no `.stem` or an unparseable one. Without a terminal they exit non-zero; the error names the exact one-line fix. -- If a governed command reports "Schema discovery reached the filesystem root without finding a declared boundary", the fix is to add `root: true` to the project's top-level `.stem` (one line). Then retry. Do **not** use `init --force` to migrate an existing project — it re-infers and overwrites the schema. +Schema discovery walks up from the target collecting `.stem` files and stops at a `.stem` that declares `root: true`, or at the filesystem root. `.git` is not a boundary. A schema-governed project must declare where it starts with a `root: true` marker in its top-level `.stem`. + +- **Schemaless exploration**: `query` without `--sort`, `tree`, `graph`, and `graph --check` work when no `.stem` files are discovered. They read Markdown records, frontmatter, and links without creating a schema; schema-derived and aggregated fields are unavailable. +- **Schema-required commands**: `validate`, `fix`, `describe`, `explain`, `new`, `set`, and `stats` require a `.stem` schema and fail or report diagnostics when none exists. `stats` is not a schemaless counting alternative. +- **Query sorting**: `query --sort` requires a `.stem` schema, even when sorting a built-in field such as `path`. Omit `--sort` for schemaless queries. +- **Existing schema chains**: for non-exempt commands, any discovered `.stem` chain must be parseable and terminate at `root: true`. A malformed `.stem` always fails, including for exploration commands; it is not schemaless mode. +- **Missing declared boundary**: an existing chain without `root: true` is not schemaless mode. Without a terminal (non-TTY), the command exits non-zero. On a terminal (TTY), Rootline offers to add the marker; accepting writes `root: true` to the proposed project root's `.stem` and continues. Do not accept that prompt or write the marker without explicit user authorization. +- If a command reports "Schema discovery reached the filesystem root without finding a declared boundary", the fix is to add `root: true` to the project's top-level `.stem` (one line), with explicit user authorization, then retry; accepting the authorized TTY prompt performs that repair and continues automatically. Do **not** use `init --force` to migrate an existing project — it re-infers and overwrites the schema. - `rootline init` writes `root: true` for new projects, so they never hit this. - `init`, template installation, and write-producing `migrate` modes replace each generated file atomically. This is a per-file guarantee, not rollback for an entire multi-file run. - Bootstrap commands (`schema propose`, `analyze`) work without any `.stem` — they derive one. `init` and `migrate` also run without a marker. +For a small schemaless example, use a temporary directory whose ancestors have no `.stem` files: + +```bash +dir="$(mktemp -d)" +printf '%s\n' '# First' '[[second]]' > "$dir/first.md" +printf '%s\n' '# Second' > "$dir/second.md" +rootline query "$dir" --where "type == 'markdown'" --select path -o json +rootline tree "$dir" -o json +rootline graph "$dir" -o json +rootline graph "$dir" --check +``` + ## Inheritance: a child never removes A child `.stem` adds fields or narrows what a parent declared. It never removes diff --git a/.claude/skills/rootline/ref-query.md b/.claude/skills/rootline/ref-query.md index 6516455a..8a8f28bb 100644 --- a/.claude/skills/rootline/ref-query.md +++ b/.claude/skills/rootline/ref-query.md @@ -18,6 +18,8 @@ Supported operators: `==`, `!=`, `in`, `not in`, `contains`, `&&`, `||`. Use `fi Use `query` to return records matching frontmatter, derived, aggregated, body, or built-in fields. +Without any discovered `.stem`, `query` works on Markdown records, frontmatter, and built-in fields. Schema-derived and aggregated fields are unavailable. `query --sort` requires a `.stem` schema, even for built-in fields such as `path`; omit `--sort` for schemaless queries. Any discovered schema chain must be parseable and terminate at `root: true` — a broken or undeclared chain is not schemaless mode. + ### Usage ```bash @@ -173,6 +175,8 @@ Rules: Use `tree` to show hierarchy and recursive record totals. +`tree` works without any discovered `.stem`; any discovered schema chain must be parseable and terminate at `root: true`. + ```bash rootline tree -o table rootline tree --where "estado != 'Completed'" -o table @@ -183,19 +187,23 @@ Use table output for humans and JSON for programmatic handling. Table format (`- ## stats -Use `stats` for aggregate counts. +Use `stats` for the total record count after any `--where` filters. + +`stats` requires a `.stem` schema with a declared `root: true` boundary; it does not support schemaless counting. Use `rootline query --count -o json` to count records without a schema. ```bash rootline stats -o json rootline stats --where "tipo == 'task'" -o json ``` -JSON contains counts grouped by `estado`, `tipo`, and total records. +JSON emits the version 2 contract `{version, kind, total}` with kind `rootline/stats`; it does not group counts by `estado` or `tipo`. ## explain Use `explain` when the user asks why a field has a value, where a schema-backed field is defined, or how derived/aggregate values are computed. +`explain` requires a `.stem` schema with a declared `root: true` boundary. + ```bash rootline explain -o json ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a3e507..da9ed330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Rel ### Fixed +- Corrected the canonical Rootline skill's governance guidance: `query` without `--sort`, `tree`, `graph`, and `graph --check` support schemaless records; `query --sort` and schema-dependent commands, including `stats`, require a schema. Documented the distinction between no `.stem` and an invalid or undeclared schema boundary, added runnable schemaless examples, and pinned the guidance with documentation contract assertions. Command behavior and installed skill symlinks are unchanged. Closes #295. - `query --count -o table` now renders one `Count` column and one decimal row instead of silently emitting JSON; `-o json` preserves the version-1 `rootline/count` envelope. Query rejects count with `jsonl`/`csv`, row formats without `--select`, and `--count` with `--select` before schema discovery or scanning, while preserving JSON-only `--field` validation precedence. Closes #294. - `concat` now renders strings unchanged, integers as base-10 text, and booleans as `true`/`false` instead of reflection debug output. Nil remains empty text; unsupported types, including lists, maps, and floats, return an explicit expression evaluation error. Closes #293. - Bare `rootline repair` now shows grouping-command help instead of executing apply logic and failing with `--report is required`. Help and the README identify version 1 `rootline/proposals` reports generated by `rootline fix --all --dry-run -o json`; `repair apply` behavior and flags are unchanged. Closes #292. diff --git a/cmd/rootline/skill_documentation_test.go b/cmd/rootline/skill_documentation_test.go index 28b74afc..f0876c05 100644 --- a/cmd/rootline/skill_documentation_test.go +++ b/cmd/rootline/skill_documentation_test.go @@ -29,3 +29,68 @@ func TestSkillDistributionDocumentationContract(t *testing.T) { } } } + +func TestSkillGovernanceBoundaryDocumentationContract(t *testing.T) { + repo := documentationContractRepoRoot(t) + path := ".claude/skills/rootline/SKILL.md" + text := string(documentationContractRead(t, repo, path)) + boundary := sectionFrom(text, "## Governance Boundary (root marker)") + assertScopeContainsAll(t, path+" governance boundary", boundary, []string{ + "`.git` is not a boundary", + "`query` without `--sort`, `tree`, `graph`, and `graph --check` work when no `.stem` files are discovered", + "`validate`, `fix`, `describe`, `explain`, `new`, `set`, and `stats` require a `.stem` schema", + "`stats` is not a schemaless counting alternative", + "`query --sort` requires a `.stem` schema, even when sorting a built-in field such as `path`", + "any discovered `.stem` chain must be parseable and terminate at `root: true`", + "A malformed `.stem` always fails, including for exploration commands", + "an existing chain without `root: true` is not schemaless mode", + "Without a terminal (non-TTY), the command exits non-zero", + "On a terminal (TTY), Rootline offers to add the marker", + "accepting writes `root: true` to the proposed project root's `.stem` and continues", + "Do not accept that prompt or write the marker without explicit user authorization", + "Do **not** use `init --force`", + "Bootstrap commands (`schema propose`, `analyze`) work without any `.stem`", + "`init` and `migrate` also run without a marker", + "temporary directory whose ancestors have no `.stem` files", + "dir=\"$(mktemp -d)\"", + "printf '%s\\n' '# First' '[[second]]' > \"$dir/first.md\"", + "printf '%s\\n' '# Second' > \"$dir/second.md\"", + "rootline query \"$dir\" --where \"type == 'markdown'\" --select path -o json", + "rootline tree \"$dir\" -o json", + "rootline graph \"$dir\" -o json", + "rootline graph \"$dir\" --check", + }) + for _, stale := range []string{ + "Governed commands (`validate`, `fix`, `query`, `tree`, `graph`, `describe`, `explain`, `set`, `stats`) fail on a tree with no `.stem`", + "fail on a tree with no `.stem` or an unparseable one", + } { + if strings.Contains(text, stale) { + t.Errorf("%s contains stale blanket schema requirement %q", path, stale) + } + } + + path = ".claude/skills/rootline/ref-query.md" + text = string(documentationContractRead(t, repo, path)) + assertScopeContainsAll(t, path+" query", sectionFrom(text, "## query"), []string{ + "Without any discovered `.stem`, `query` works", + "`query --sort` requires a `.stem` schema, even for built-in fields such as `path`", + "Any discovered schema chain must be parseable and terminate at `root: true`", + }) + assertScopeContainsAll(t, path+" tree", sectionFrom(text, "## tree"), []string{ + "`tree` works without any discovered `.stem`", + "any discovered schema chain must be parseable and terminate at `root: true`", + }) + assertScopeContainsAll(t, path+" stats", sectionFrom(text, "## stats"), []string{ + "`stats` requires a `.stem` schema with a declared `root: true` boundary", + "it does not support schemaless counting", + "rootline query --count -o json", + "version 2 contract `{version, kind, total}` with kind `rootline/stats`", + "it does not group counts by `estado` or `tipo`", + }) + if strings.Contains(text, "JSON contains counts grouped by `estado`, `tipo`, and total records") { + t.Errorf("%s contains stale grouped stats output contract", path) + } + assertScopeContainsAll(t, path+" explain", sectionFrom(text, "## explain"), []string{ + "`explain` requires a `.stem` schema with a declared `root: true` boundary", + }) +}