T-6ZNY-headless-bases-syntax-parser-for-eval
Status: closed/done · Impact: low · Complexity: medium
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to open/ready before picking up.
AC-4 of T-781K-add-obsidian-bases-setup-flag requires opening
a generated .base file in Obsidian to confirm it renders as a table
without “invalid base” errors. Obsidian is not headless, so this AC
is structurally deferred-user: every future change to Bases
generation needs a human in the loop to re-verify. A minimal Bases-
syntax parser (or shelling out to one) would let the AC run as auto
on CI, closing the loop. The post-mortem flagged this explicitly as a
“heavier follow-up.”
- Bases is an Obsidian-only renderer; the
.basefile format is documented at https://help.obsidian.md/bases but there is no first- party CLI parser. - The originating task’s AC-4 reads: “Opening one generated
.basefile in Obsidian renders the corresponding planning artifacts as a table without ‘invalid base’ errors (manual verification, noted in PR).” - The post-mortem observed: “AC-4 is structurally a deferred-user
check because Obsidian isn’t headless; an eval pattern that loads a
.basethrough a minimal Bases-syntax parser would close the loop, but that’s a heavier follow-up.” - No tooling in this repo parses
.basefiles beyondyaml.safe_load, which catches gross YAML errors but not Bases-specific semantic errors (e.g. unknown view type, malformedfile.inFolder(...)expression).
Proposed
Section titled “Proposed”A small Python module (or shell out to an external tool) that loads a
.base file and validates it against the Bases schema enough to catch
the same class of errors Obsidian would surface as “invalid base”:
- Top-level keys are recognized (
views,filters,formulas, …). - Each
viewsentry has a recognizedtypeand a valid shape for that type. - Property references in
order,groupBy,sort, andfilterparse as eithernote.<field>,file.<field>, orformula.<name>. - Filter expressions like
file.inFolder("...")andnote.status == "..."parse without error.
A new test (or a /project-check extension) loads each generated
.base (or each plugin/entities/<type>/base.yaml, since the bytes
are copied verbatim) through this parser and fails on any error.
Approach
Section titled “Approach”- Implement a minimal Python parser at
plugin/validators/validate_base.py(mirrors the location ofvalidate_frontmatter.py). Scope: features we actually emit (table view, simplefile.inFolderfilter,groupBy.property: note.<field>). Reject the shell-out alternative — no stable community CLI exists for the.baseformat and shelling out adds a runtime dependency we can avoid. - CLI:
validate_base.py <path-to-.base>(one or more). Exits 0 on pass; non-zero with a per-file, per-key diagnostic on fail. - Wire the validator into
.claude/skills/project-check/as a new check script (check_entity_bases.py) that iterates everyplugin/entities/<type>/base.yamland shells out to the validator. project-check is project-local (not inplugin/), which is the right home for SDLC-corpus-specific checks. - Update AC-4 of T-781K-add-obsidian-bases-setup-flag
from
deferred-usertoauto, citing the new validator.
Files to touch
Section titled “Files to touch”plugin/validators/validate_base.py(new) — the headless validator CLI..claude/skills/project-check/check_entity_bases.py(new, project-local) — iteratesplugin/entities/*/base.yamland shells out to the validator..claude/skills/project-check/SKILL.md— list the new check in the “What this skill checks today” section.docs/planning/tasks/2026-05-19-add-obsidian-bases-setup-flag.md— reclassify AC-4 fromdeferred-usertoauto.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A new CLI accepts a
.basefile path and exits zero on every shippedplugin/entities/<type>/base.yaml. - AC-2: Hand-corrupting one of the shipped
base.yamlfiles (e.g. changingtype: tabletotype: bogus, or rewritingfile.inFolder(".")tofile.inFolder() causes the CLI to exit non-zero with a diagnostic that names the offending file and key. - AC-3:
/project-checkinvokes the validator and fails on any invalid.basetemplate. - AC-4: AC-4 of T-781K-add-obsidian-bases-setup-flag is
reclassified from
deferred-usertoauto(either by editing that task’s post-mortem coverage line or by superseding it in a follow-up PR).
Out of scope
Section titled “Out of scope”- A full reimplementation of Bases rendering — only enough syntax validation to catch what Obsidian would flag as “invalid base.”
- Validating community plugins that extend Bases beyond the upstream schema.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-781K-add-obsidian-bases-setup-flag on 2026-05-20.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
plugin/validators/validate_base.pyexited 0 against all four shippedplugin/entities/<type>/base.yamlfiles (4/4 file(s) passed). - AC-2: auto — fixtures
bogus-view-type(changestype: tabletotype: bogus) andbroken-filter(rewritesfile.inFolder(".")tofile.inFolder() both exited 1 with diagnostics naming the offending file and key (views[0].type: 'bogus' not in ['cards', 'table'],filters.and[0]: unrecognized filter expression: 'file.inFolder('). Covered byrun_base_evals.py. - AC-3: auto —
.claude/skills/project-check/check_entity_bases.pyshells out to the validator, propagates its exit code, and printsFAIL <path>+ diagnostics on corruption (verified with a synthetic--entities-dir /tmp/fake-entitiesrun). - AC-4: auto — AC-4 of T-781K-add-obsidian-bases-setup-flag reclassified from
deferred-usertoautoin the same commit, citing the new validator. The post-mortem friction bullet that originally flagged this gap was also updated to reference the resolution.
What worked
Section titled “What worked”- Mirroring
validate_frontmatter.py/validate_sdlc_yaml.py’s CLI conventions (PEP-723 inline deps,EXIT_OK/EXIT_FAILED/EXIT_CONFIG,--quiet,N/N file(s) passedsummary) made the validator drop-in compatible with the existing project-check pattern —check_entity_bases.pyis a 60-line subprocess wrapper. - The “scope = what we actually emit” constraint (table view only, simple
file.inFolderfilters,note.<field>references) kept the regex set small enough to reason about and matched 4/4 shipped templates on the first run. - The fixture-based eval pattern from
check_entities.py/run_evals.pyported directly — one tuple per drift case, substring assertions on stdout, exit-code check. Seven fixtures cover the disqualifier surface without ballooning.
Friction and automation gaps
Section titled “Friction and automation gaps”sdlc.yamlat the project root carries apr_check:block that the bundled schema (plugin/schemas/sdlc-yaml.schema.json) rejects withAdditional properties are not allowed ('pr_check' was unexpected). This meansplugin/scripts/run_quality_checks.py --config sdlc.yamlfails at the schema gate before running any verb — both forworktree_init(Step 4) andquality_checks(Step 7). Worked around by running each declared verb directly. Root fix: either promotepr_check:into the bundled schema, or document it as a recognized extension the validator should accept. → T-DN76-pr-check-schema-promote- No sub-agent dispatch tool was available in this harness, so Step 6 ran the implementation directly (no subagent delegation). The skill assumes sub-agent dispatch is always available; degrading gracefully when it isn’t (or detecting and noting the absence) would make the prose more honest about the actual execution mode. (Skipped: harness-level limitation, not a plugin gap.)
- Initial insertion of the new
### 1c.heading inSKILL.mdbetween1aand1b(out of order) required a second edit to move it after1b. A pre-edit “where does this section belong in the existing numbering” check would have caught it once. Minor churn; not worth dedicated tooling. (Skipped: per-bullet judgment, no automation hook.)
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-DN76-pr-check-schema-promote — created; adds
pr_checkto the bundledsdlc-yaml.schema.jsonso the validator stops rejecting this project’s config.