Skip to content

Commit c8c210b

Browse files
jedudenclaude
andauthored
Plan 148: Named field-type shortcuts for inline schemas (#279)
* Add named field-type shortcuts for inline schemas (plan 148) Inline schemas can now reference a small library of named CUE patterns by short name (`created: date`, `homepage: url`). The library lives at cue/types/types.cue and ships embedded in the binary; resolution happens in the shared frontmatterExpr loader, so both inline `schema:` blocks and proto.md frontmatter pick up the shortcuts. CUE built-ins (string, int, bool, …) keep passing through so existing schemas with bare-name builtins are unaffected. Bare scalars that match neither a shortcut nor a builtin error at config-load time, naming the field and the unknown name — typos like `iso-date` no longer slide through to deep CUE errors. * Docs: add docs/reference/schema-types.md and cross-link plan 148 Documents every registered shortcut (date, datetime, time, email, url, filename, nonEmpty) with its canonical CUE expression and an inline + proto.md walk-through. The MDS020 README, schemas guide, and file-kinds guide each link the new page so a reader landing on any of them can find the vocabulary. Marks plan 148 ✅, settles the on-disk and CUE import paths, and folds the implementation evidence (test names) into each acceptance-criterion bullet so a reviewer can audit the claim against the code. The package-name nolint in cue/types/types.go keeps the Go-side identifier aligned with the documented CUE import path `github.com/jeduden/mdsmith/types` despite revive's "meaningless package names" check. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8d37e44 commit c8c210b

14 files changed

Lines changed: 736 additions & 58 deletions

File tree

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ row: "- [{summary}](../{filename})"
7777
- [Print the mdsmith build version and exit.](../docs/reference/cli/version.md)
7878
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](../docs/reference/conventions.md)
7979
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](../docs/reference/globs.md)
80+
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](../docs/reference/schema-types.md)
8081
<?/catalog?>
8182

8283
### Development Workflow

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ row: "- [{summary}]({filename})"
8383
- [Print the mdsmith build version and exit.](docs/reference/cli/version.md)
8484
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
8585
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
86+
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
8687
<?/catalog?>
8788

8889
## Development Workflow

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ row: "- [{summary}]({filename})"
6969
- [Print the mdsmith build version and exit.](docs/reference/cli/version.md)
7070
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
7171
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
72+
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
7273
<?/catalog?>
7374

7475
## Development Workflow

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ footer: |
7676
| 145 | 🔲 | opus | [Publish mdsmith via asdf and mise registry submissions](plan/145_asdf-mise-registry-submissions.md) |
7777
| 146 || opus | [Schema engine — sources, scope tree, per-scope rules](plan/146_inline-schema-in-kinds.md) |
7878
| 147 | 🔲 | opus | [Actionable schema diagnostics for MDS020](plan/147_actionable-schema-diagnostics.md) |
79-
| 148 | 🔲 | sonnet | [Named field-type shortcuts for inline schemas](plan/148_named-field-type-shortcuts.md) |
79+
| 148 | | sonnet | [Named field-type shortcuts for inline schemas](plan/148_named-field-type-shortcuts.md) |
8080
| 149 | 🔲 | opus | [Section content schema for non-heading AST nodes](plan/149_section-content-schema.md) |
8181
| 151 || opus | [LSP rename for headings and link-reference labels](plan/151_lsp-rename.md) |
8282
| 152 | 🔲 | sonnet | [Claude Code plugin extensions — skills, agents, hooks](plan/152_claude-code-skills-agents-hooks.md) |

cue/types/types.cue

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Package types is the canonical vocabulary of named
2+
// field-type shortcuts that inline schemas (plan 146)
3+
// and proto.md frontmatter values reference by short
4+
// name (e.g. `created: date`).
5+
//
6+
// Each definition resolves to a plain CUE expression.
7+
// mdsmith embeds this file via go:embed and reads the
8+
// definitions to build the runtime registry used by
9+
// schema parsing. See plan/148_named-field-type-shortcuts.md.
10+
//
11+
// The user-visible contract is the import path
12+
// `github.com/jeduden/mdsmith/types` and the symbol
13+
// names. The literal CUE `import` syntax is reserved
14+
// for a future plan; today's surface is the bare-name
15+
// YAML scalar (`created: date`).
16+
package types
17+
18+
// #date matches an ISO-8601 calendar date in YYYY-MM-DD
19+
// form. The check is shape-only; February 30th still
20+
// matches.
21+
#date: =~"^\\d{4}-\\d{2}-\\d{2}$"
22+
23+
// #datetime matches an ISO-8601 timestamp with seconds,
24+
// either `Z` or a `±HH:MM` offset. Fractional seconds
25+
// and basic-format strings are rejected.
26+
#datetime: =~"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})?$"
27+
28+
// #time matches `HH:MM` with optional `:SS` seconds.
29+
// No timezone suffix.
30+
#time: =~"^\\d{2}:\\d{2}(:\\d{2})?$"
31+
32+
// #email matches `local@domain.tld` where neither side
33+
// contains `@` or whitespace and the domain has at
34+
// least one `.`. Internationalised addresses and
35+
// quoted local parts are rejected.
36+
#email: =~"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"
37+
38+
// #url matches any string starting with `http://` or
39+
// `https://`. The remainder is not validated.
40+
#url: =~"^https?://"
41+
42+
// #filename matches a Markdown filename of safe
43+
// characters: ASCII letters, digits, `.`, `_`, `-`,
44+
// and a trailing `.md`. Path separators are rejected.
45+
#filename: =~"^[A-Za-z0-9._-]+\\.md$"
46+
47+
// #nonEmpty is a non-empty string. Useful for fields
48+
// that must carry content (titles, summaries).
49+
#nonEmpty: string & !=""

cue/types/types.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Package types embeds the canonical mdsmith
2+
// field-type-shortcut library so schema parsing can
3+
// resolve `created: date` and friends without
4+
// touching the network. The library source itself
5+
// lives in `types.cue` next to this file.
6+
//
7+
// The intended import path for external CUE consumers
8+
// is `github.com/jeduden/mdsmith/types`. The literal
9+
// CUE import syntax is not yet implemented; today's
10+
// surface is the bare-name YAML scalar handled by
11+
// internal/schema's frontmatterExpr. The Go package
12+
// name matches the directory so the Go and CUE
13+
// import paths line up for future module wiring.
14+
//
15+
//nolint:revive // "types" mirrors the CUE-side import path
16+
package types
17+
18+
import _ "embed"
19+
20+
//go:embed types.cue
21+
var source string
22+
23+
// Source returns the embedded `types.cue` contents
24+
// verbatim. internal/schema reads this once to seed
25+
// its runtime registry and to drive the drift test
26+
// that pins registry entries to the documented CUE.
27+
func Source() string { return source }

docs/guides/file-kinds.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,8 @@ For a quick primer on the same model from the CLI, run
329329
- [Placeholder grammar](../background/concepts/placeholder-grammar.md)
330330
— opt-in tokens that let kinds keep template files
331331
green under the same rules used for content.
332+
- [Schema field types](../reference/schema-types.md)
333+
— named shortcuts (`date`, `email`, `url`, …) for
334+
schema frontmatter values, so a kind's `schema:`
335+
block does not have to re-derive the same CUE regex
336+
every project lands on.

docs/guides/schemas.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ kinds:
6060
The `frontmatter:` mapping reuses CUE expressions per
6161
key: regex, disjunction, list, and any other CUE form
6262
is accepted. Trailing `?` on a key marks it optional.
63+
Named shortcuts —
64+
`date`, `datetime`, `time`, `email`, `url`, `filename`,
65+
`nonEmpty` — substitute for their canonical CUE so a
66+
schema can write `created: date` instead of repeating
67+
the ISO regex; see
68+
[Schema field types](../reference/schema-types.md)
69+
for the registered names and how they are matched.
6370

6471
`require.filename:` is a glob the document basename
6572
must match.

docs/reference/schema-types.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
summary: >-
3+
Named field-type shortcuts for inline schema
4+
frontmatter values — the registered names, the
5+
canonical CUE each one resolves to, and example
6+
usage.
7+
---
8+
# Schema field types
9+
10+
A **field-type shortcut** is a short name a schema
11+
frontmatter value can use instead of spelling out a CUE
12+
expression. `created: date` is shorter and harder to
13+
typo than `created: '=~"^\\d{4}-\\d{2}-\\d{2}$"'`, and
14+
every project using the shortcut gets the same regex —
15+
no three projects land on three slightly different ISO
16+
patterns.
17+
18+
Shortcuts work the same way for inline schemas
19+
(`kinds.<name>.schema:`) and for proto.md frontmatter.
20+
The library ships embedded in the mdsmith binary; no
21+
network access is needed to resolve a shortcut.
22+
23+
## How a value is interpreted
24+
25+
A frontmatter value (right-hand side of `key: value` in
26+
the schema's `frontmatter:` block) is classified by
27+
shape:
28+
29+
| Shape | Treatment |
30+
|---------------------------------------------|------------------------------------------------------------------------|
31+
| Bare identifier in the shortcut registry | Substituted with the canonical CUE expression |
32+
| Bare identifier that is a CUE built-in type | Passes through verbatim (`string`, `int`, `bool`, `float`, `bytes`, …) |
33+
| Bare identifier that is neither | **Config error** naming the field and the unknown name |
34+
| Anything else (operators, quotes, …) | Passes through verbatim as raw CUE |
35+
36+
A "bare identifier" is a YAML scalar matching
37+
`[a-zA-Z_][a-zA-Z0-9_-]*` — letters, digits,
38+
underscores, and hyphens, starting with a letter or
39+
underscore. Anything containing whitespace, operators,
40+
quotes, brackets, or other punctuation is treated as
41+
raw CUE.
42+
43+
The strict third row catches typos at config-load
44+
time. A schema that writes `created: iso-date` errors
45+
with `unknown shortcut "iso-date"` instead of sliding
46+
through to an undefined-reference error deep in CUE
47+
evaluation.
48+
49+
## Registered shortcuts
50+
51+
The following names are accepted in a schema
52+
frontmatter value:
53+
54+
| Name | Canonical CUE | Accepts | Rejects |
55+
|------------|---------------------------------------------------------------------------|------------------------|-----------------------|
56+
| `date` | `=~"^\\d{4}-\\d{2}-\\d{2}$"` | `2024-05-01` | `2024-5-1` |
57+
| `datetime` | `=~"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z\|[+-]\\d{2}:\\d{2})?$" ` | `2024-05-01T12:30:00Z` | `2024-05-01 12:30:00` |
58+
| `time` | `=~"^\\d{2}:\\d{2}(:\\d{2})?$"` | `12:30` | `12:3` |
59+
| `email` | `=~"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"` | `user@example.com` | `user@@example` |
60+
| `url` | `=~"^https?://"` | `https://example.com` | `ftp://example.com` |
61+
| `filename` | `=~"^[A-Za-z0-9._-]+\\.md$"` | `notes.md` | `notes.txt` |
62+
| `nonEmpty` | `string & !=""` | `hello` | `""` |
63+
64+
The regex bodies are shape-only. `date` accepts
65+
`2024-02-30` even though February never has 30 days;
66+
semantic validation is out of scope.
67+
68+
## Inline schema example
69+
70+
```yaml
71+
kinds:
72+
rfc:
73+
schema:
74+
frontmatter:
75+
created: date
76+
modified: datetime
77+
contact: email
78+
homepage: url
79+
require:
80+
filename: "RFC-[0-9][0-9][0-9][0-9].md"
81+
```
82+
83+
`mdsmith check` validates every file in the `rfc` kind:
84+
`created` must match the ISO-date regex, `contact` must
85+
look like an email, and so on.
86+
87+
## proto.md example
88+
89+
The same shortcuts work in a proto.md schema's YAML
90+
frontmatter:
91+
92+
```markdown
93+
---
94+
created: date
95+
homepage: url
96+
---
97+
# ?
98+
99+
## Goal
100+
101+
## Tasks
102+
```
103+
104+
Substitution happens at schema-load time. After that
105+
the validator treats the canonical CUE the same way
106+
whether a user typed it out or the loader expanded
107+
the shortcut.
108+
109+
## Composing a shortcut with extra constraints
110+
111+
A value that needs both a shortcut and an extra
112+
constraint is **not** a bare name. Write the canonical
113+
CUE directly:
114+
115+
```yaml
116+
frontmatter:
117+
created: '=~"^\\d{4}-\\d{2}-\\d{2}$" & >="2020-01-01"'
118+
```
119+
120+
The shortcut form is only triggered when the value is
121+
exactly the bare name. Anything more complex passes
122+
through as raw CUE.
123+
124+
## Adding a shortcut
125+
126+
The library lives at `cue/types/types.cue` in this
127+
repository. The runtime registry is in
128+
`internal/schema/shortcuts.go`; a drift test pins the
129+
two to each other. User-defined shortcuts are not yet
130+
a configuration surface — a project that needs a
131+
custom shortcut should write the canonical CUE in a
132+
shared proto.md (or wait for a follow-up plan if the
133+
need recurs).
134+
135+
## See also
136+
137+
- [Schemas](../guides/schemas.md) — the broader story
138+
on inline schemas, proto.md, and per-scope rule
139+
overrides.
140+
- [MDS020](../../internal/rules/MDS020-required-structure/README.md)
141+
— the rule that surfaces schema diagnostics.
142+
- [File kinds](../guides/file-kinds.md) — how kinds
143+
attach schemas (and other rule config) to file
144+
groups.

internal/rules/MDS020-required-structure/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,12 @@ Describe the goal here.
345345
- **Status**: ready
346346
- **Default**: enabled
347347
- **Fixable**: index side-output only (when `schema.index:` is set)
348-
- **Implementation**:
349-
[source](./)
348+
- **Implementation**: [source](./)
350349
- **Guide**:
351350
[directive guide](../../../docs/guides/directives/enforcing-structure.md)
352351
- **Category**: meta
353352

354353
## See also
355354

356355
- [Placeholder grammar](../../../docs/background/concepts/placeholder-grammar.md)
356+
- [Schema field types](../../../docs/reference/schema-types.md)

0 commit comments

Comments
 (0)