Skip to content

feat(cli): add meta types vocabulary search and whenToUse guidance#74

Merged
dmealing merged 2 commits into
mainfrom
feat/meta-types-vocab-discovery
Jun 25, 2026
Merged

feat(cli): add meta types vocabulary search and whenToUse guidance#74
dmealing merged 2 commits into
mainfrom
feat/meta-types-vocab-discovery

Conversation

@dmealing

Copy link
Copy Markdown
Member

Intent

Teach an AI coding agent to FIND and USE the full MetaObjects metadata vocabulary instead of defaulting to 'generate CRUD, hand-write the rest'. Diagnosed from a benchmark where the agent hand-wrote joins + an AVG() instead of declaring a relationship + an origin.aggregate projection. Two-part fix, deliberately NOT adding any metaobjects feature to 'win' the benchmark: (1) a new 'meta types [QUERY]' CLI command — apropos + kubectl-explain over the live registry (designed from research): substring name match, --desc/--all to search descriptions, --kind/--type filters, terse names-first default capped 20 with attr hints + 'N of M' footer, --detail drill-in, --json; a reader over buildRegistryManifest, wired into dispatch + meta --help, help-snapshot updated. (2) Populate the canonical whenToUse slot (was 0/73) for the data-modeling constructs in spec/metamodel/*.json — the 'reach for this when… so it's generated, not hand-written' guidance surfaced by meta types. Flows to all five ports: regenerated TS embedded definitions, synced Python spec_metamodel/ + C# SpecMetamodel/ committed copies (Java reads canonical), regenerated fixtures/registry-conformance/expected-registry.json (the cross-port gate carrying whenToUse). Broadened the metaobjects-authoring skill with general 'run meta types; model it, don't hand-write it' guidance (no test-specific examples). Targeted spec-JSON edits add only whenToUse lines (+ a comma where description was last key). Verified locally: TS registry-conformance + coverage 10/10, cli suite green, meta types end-to-end. CI must validate Python/Java/C# emit the same manifest as the regenerated fixture.

What Changed

  • Add a new meta types [QUERY] CLI command (server/typescript/packages/cli/src/commands/types.ts, wired into dispatch + meta --help): an apropos / kubectl explain-style reader over buildRegistryManifest with substring name matching, --desc/--all description search, --kind/--type filters, a terse names-first default capped at 20 with an "N of M" footer, --detail drill-in, and --json output. Help snapshot, CLI README, and docs/features/cli.md updated.
  • Populate the canonical whenToUse slot for data-modeling constructs across spec/metamodel/*.json (field, identity, object, origin, relationship, source, template, validator) — the "reach for this when… so it's generated, not hand-written" guidance surfaced by meta types.
  • Propagate the vocabulary across all five ports: regenerated TS embedded *-definition.embedded.ts files, synced Python spec_metamodel/ and C# SpecMetamodel/ committed copies, and regenerated fixtures/registry-conformance/expected-registry.json (now carrying 37 whenToUse entries) so the cross-port gate validates byte-identical manifests. Broadened the metaobjects-authoring skill with general "run meta types; model it, don't hand-write it" guidance.

Risk Assessment

✅ Low: A read-only CLI command plus data-only whenToUse additions whose cross-port consistency I verified (counts and strings match across all five ports); only cosmetic/edge-case nits remain.

Testing

Ran the baseline metadata conformance/coverage and CLI snapshot suites (all green), then exercised the new meta types command end-to-end across every documented mode and captured the terminal transcript as the user-facing artifact — it shows an agent finding relationship/origin.aggregate constructs and their whenToUse "declare it, don't hand-write it" guidance, the exact fix for the benchmarked behavior. The change is CLI/text only, so the captured terminal transcript (not a screenshot) is the appropriate end-user surface. I also statically verified the cross-port inputs are in sync: the 8 canonical spec JSON files are byte-identical to the Python and C# committed copies, and the regenerated fixture carries the 37 whenToUse entries, so the Java/Python/C# CI conformance gates (which need Maven/dotnet/pytest, unavailable here) will emit the same manifest. No product or setup issues found; worktree left clean.

Evidence: meta types end-to-end CLI transcript (all modes incl. whenToUse drill-in + JSON)
############ meta types --help ############
$ meta types --help
meta types [QUERY] — search the metadata vocabulary without loading it all into context.

  meta types relationship                 # subtypes/attrs whose name matches "relationship"
  meta types --all money                   # search names AND descriptions ("find by what it does")
  meta types --type field --kind subtype   # all field subtypes (terse)
  meta types field.enum --detail           # one construct: description + when-to-use + valid @attrs
  meta types --type origin --json          # machine-readable subtree

QUERY is a case-insensitive substring, matched on the name (type.subType / @attr).
Add --desc (or --all) to also match descriptions + when-to-use guidance.

  --desc            also search descriptions + whenToUse (not just names)
  --all             alias for: match name OR description
  --kind <k>        filter by category: type | subtype | attr (comma-list ok)
  --type <name>     scope to one top-level type (e.g. --type field)
  --detail          drill in: full description, when-to-use, and valid @attrs
  --json            emit the matching registry subtree verbatim (stable-sorted)
  --limit <N>       cap results (default 20; 0 = unlimited)
  --no-headers      omit headers (parse-friendly)

Default output is one terse line per match. Reach for metaobjects metadata
(declare it, regenerate) instead of hand-writing data logic — this finds the
construct.

############ meta types relationship  (substring name match, terse) ############
$ meta types relationship
relationship.aggregation     A shared/independent containment — the parent groups the target but does not own its lif…  (@cardinality, @objectRef, @onDelete, @onUpdate, @sourceRefField, @symmetric, …)
relationship.aggregation @cardinality Cardinality of the relationship target (e.g. 'one', 'many', 'many-to-one').
relationship.aggregation @objectRef Name or fully-qualified name of the target object the relationship points to (e.g. 'Week…
relationship.aggregation @onDelete Referential action on parent delete. Default derives from subtype (composition→cascade, …
relationship.aggregation @onUpdate Referential action on key update. Default cascade.
relationship.aggregation @sourceRefField Directed self-join disambiguator: names the source-side FK field on the junction (the ot…
relationship.aggregation @symmetric Undirected self-join flag (union-on-read). Valid only when @objectRef == the declaring e…
relationship.aggregation @through Junction (through) entity name for M:N relationships — a third entity declaring two iden…
relationship.association     A plain reference to another entity — no ownership; the target has an independent lifecy…  (@cardinality, @objectRef, @onDelete, @onUpdate, @sourceRefField, @symmetric, …)
relationship.association @cardinality Cardinality of the relationship target (e.g. 'one', 'many', 'many-to-one').
relationship.association @objectRef Name or fully-qualified name of the target object the relationship points to (e.g. 'Week…
relationship.association @onDelete Referential action on parent delete. Default derives from subtype (composition→cascade, …
relationship.association @onUpdate Referential action on key update. Default cascade.
relationship.association @sourceRefField Directed self-join disambiguator: names the source-side FK field on the junction (the ot…
relationship.association @symmetric Undirected self-join flag (union-on-read). Valid only when @objectRef == the declaring e…
relationship.association @through Junction (through) entity name for M:N relationships — a third entity declaring two iden…
relationship.base            Abstract relationship base — shared shape for the concrete association/aggregation/compo…  (@cardinality, @objectRef, @onDelete, @onUpdate, @sourceRefField, @symmetric, …)
relationship.base @cardinality Cardinality of the relationship target (e.g. 'one', 'many', 'many-to-one').
relationship.base @objectRef Name or fully-qualified name of the target object the relationship points to (e.g. 'Week…
relationship.base @onDelete  Referential action on parent delete. Default derives from subtype (composition→cascade, …

20 of 32 shown — narrow with QUERY/--type/--kind or raise --limit.

############ meta types --all money  (search descriptions/whenToUse) ############
$ meta types --all money
field.currency               Stores money as integer minor units (cents). Binds to long; the client formats via @curr…  (@currency, @default, @readOnly, @required, @unique)
field.decimal                Precision-exact decimal (use @precision/@scale). Native TS binding is string (lossless);…  (@default, @precision, @readOnly, @required, @scale, @unique)
field.double                 Double-precision (64-bit) IEEE-754 floating point. Binds to the native double/number typ…  (@default, @readOnly, @required, @unique)
field.float                  Single-precision floating point. Binds to the native double/number type (TS has no disti…  (@default, @readOnly, @required, @unique)

4 matches.

############ meta types --type origin  (scope to one type) ############
$ meta types --type origin
origin.aggregate             A count/sum/avg/min/max (@agg) computed over a column (@of) reached along a relationship…  (@agg, @of, @via)
origin.aggregate @agg        Aggregate function applied over the relationship path: count, sum, avg, min, or max.
origin.aggregate @of         Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durati…
origin.aggregate @via        Dotted relationship path from the base entity to the aggregated rows (e.g. 'Program.week…
origin.base                  Abstract base origin — the shared root subtype for field-level provenance. A field carry…
origin.collection            A relationship-derived array of nested view-objects: walks @via to produce the collectio…  (@via)
origin.collection @via       Dotted relationship path the collection walks to produce an array of nested view-objects…
origin.passthrough           A cross-entity field reference: this projection field passes a source entity's value str…  (@from, @via)
origin.passthrough @from     Dotted Entity.field reference identifying the source value this projection field passes …
origin.passthrough @via      Optional dotted relationship path used to reach the source entity (e.g. 'Program.weeks').

10 matches.

############ meta types origin.aggregate --detail  (drill-in: desc + when-to-use + attrs) ############
$ meta types origin.aggregate --detail

origin.aggregate  (subtype)
  A count/sum/avg/min/max (@agg) computed over a column (@of) reached along a relationship path (@via) from the base entity.
  → reach for it when: A projection needs a derived count/sum/avg/min/max over related rows. Declare it instead of hand-writing the aggregate query — it stays consistent and regenerates.
  @attrs:
    @agg  string  REQUIRED  Aggregate function applied over the relationship path: count, sum, avg, min, or max.
    @of  string  REQUIRED  Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes').
    @via  string  Dotted relationship path from the base entity to the aggregated rows (e.g. 'Program.weeks' or 'Program.weeks.workouts'). May be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029).

origin.aggregate @agg  (attr)
  Aggregate function applied over the relationship path: count, sum, avg, min, or max.

origin.aggregate @of  (attr)
  Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes').

origin.aggregate @via  (attr)
  Dotted relationship path from the base entity to the aggregated rows (e.g. 'Program.weeks' or 'Program.weeks.workouts'). May be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029).

4 matches.

############ meta types --type field --kind subtype  (terse, capped 20, N of M footer) ############
$ meta types --type field --kind subtype
field.base                   Abstract base field — the shared root subtype that concrete field subtypes specialize. C…  (@default, @readOnly, @required, @unique)
field.boolean                True/false flag. Binds to the native boolean type; DB column is BOOLEAN.  (@default, @readOnly, @required, @unique)
field.currency               Stores money as integer minor units (cents). Binds to long; the client formats via @curr…  (@currency, @default, @readOnly, @required, @unique)
field.date                   Calendar date (no time-of-day). Binds to the native date/temporal type; DB column is DATE.  (@default, @readOnly, @required, @unique)
field.decimal                Precision-exact decimal (use @precision/@scale). Native TS binding is string (lossless);…  (@default, @precision, @readOnly, @required, @scale, @unique)
field.double                 Double-precision (64-bit) IEEE-754 floating point. Binds to the native double/number typ…  (@default, @readOnly, @required, @unique)
field.enum                   String-backed enumeration constrained to a closed set of member symbols (@values). Each …  (@default, @provided, @readOnly, @required, @unique, @values)
field.float                  Single-precision floating point. Binds to the native double/number type (TS has no disti…  (@default, @readOnly, @required, @unique)
field.int                    32-bit signed integer. Binds to the native int type; DB column is INTEGER.  (@default, @readOnly, @required, @unique)
field.long                   64-bit signed integer. Binds to the native long/bigint type; DB column is BIGINT.  (@default, @readOnly, @required, @unique)
field.map                    An open-keyed map (Record<string,V> / dict[str,V]) stored in a single jsonb column. Keys…  (@default, @objectRef, @readOnly, @required, @unique, @valueType)
field.object                 A nested structured value (set @objectRef to the target object). Storage is governed by …  (@default, @objectRef, @readOnly, @required, @unique)
field.string                 Variable-length text. Binds to the native string type; DB column is VARCHAR/TEXT (use @m…  (@default, @maxLength, @readOnly, @required, @unique)
field.time                   Time-of-day (no calendar date). Binds to the native date/temporal type; DB column is TIME.  (@default, @readOnly, @required, @unique)
field.timestamp              Date + time-of-day instant (optionally with timezone). Binds to the native date/temporal…  (@default, @readOnly, @required, @unique)
field.uuid                   Logical UUID identity scalar. A bare scalar (no required attrs, no loader value-validati…  (@default, @readOnly, @required, @unique)

16 matches.

############ meta types --type origin --json  (machine-readable) ############
$ meta types --type origin --json
[
  {
    "type": "origin",
    "subType": "aggregate",
    "description": "A count/sum/avg/min/max (@agg) computed over a column (@of) reached along a relationship path (@via) from the base entity.",
    "rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (single-hop-unique inference; FR-024, ADR-0029). Multi-hop paths must always be stated explicitly.",
    "whenToUse": "A projection needs a derived count/sum/avg/min/max over related rows. Declare it instead of hand-writing the aggregate query — it stays consistent and regenerates.",
    "attrs": [
      {
        "name": "agg",
        "valueType": "string",
        "isArray": false,
        "required": true,
        "description": "Aggregate function applied over the relationship path: count, sum, avg, min, or max."
      },
      {
        "name": "of",
        "valueType": "string",
        "isArray": false,
        "required": true,
        "description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes')."
      },
      {
        "name": "via",
        "valueType": "string",
        "isArray": false,
        "required": false,
        "description": "Dotted relationship path from the base entity to the aggregated rows (e.g. 'Program.weeks' or 'Program.weeks.workouts'). May be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029)."
      }
    ],
    "children": []
  },
  {
    "name": "agg",
    "valueType": "string",
    "isArray": false,
    "required": true,
    "description": "Aggregate function applied over the relationship path: count, sum, avg, min, or max."
  },
  {
    "name": "of",
    "valueType": "string",
    "isArray": false,
    "required": true,
    "description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes')."
  },
  {
    "name": "via",
    "valueType": "string",
    "isArray": false,
    "required": false,
    "description": "Dotted relationship path from the base entity to the aggregated rows (e.g. 'Program.weeks' or 'Program.weeks.workouts'). May be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (FR-024, ADR-0029)."
  },
  {
    "type": "origin",
    "subType": "base",
    "description": "Abstract base origin — the shared root subtype for field-level provenance. A field carrying any origin.* is derived ⇒ read-only wherever it lives. The base carries no attrs of its own; concrete subtypes add their provenance attrs.",
    "attrs": [],
    "children": []
  },
  {
    "type": "origin",
    "subType": "collection",
    "description": "A relationship-derived array of nested view-objects: walks @via to produce the collection (e.g. 'Author.posts'), or a wildcard selector for a package-spanning collection.",
    "whenToUse": "A projection needs an array of nested child view-objects (a parent with its children inline). Declare it instead of hand-assembling the nested query + mapping.",
    "attrs": [
      {
        "name": "via",
        "valueType": "string",
        "isArray": false,
        "required": true,
        "description": "Dotted relationship path the collection walks to produce an array of nested view-objects (e.g. 'Author.posts'), or a wildcard selector for a package-spanning collection (e.g. '*.User')."
      }
    ],
    "children": []
  },
  {
    "name": "via",
    "valueType": "string",
    "isArray": false,
    "required": true,
    "description": "Dotted relationship path the collection walks to produce an array of nested view-objects (e.g. 'Author.posts'), or a wildcard selector for a package-spanning collection (e.g. '*.User')."
  },
  {
    "type": "origin",
    "subType": "passthrough",
    "description": "A cross-entity field reference: this projection field passes a source entity's value straight through (@from), optionally reached via a relationship path (@via).",
    "whenToUse": "A projection field just surfaces a field from a related entity. Declare the cross-entity passthrough instead of re-joining and re-selecting it by hand.",
    "attrs": [
      {
        "name": "from",
        "valueType": "string",
        "isArray": false,
        "required": true,
        "description": "Dotted Entity.field reference identifying the source value this projection field passes through (e.g. 'Program.title')."
      },
      {
        "name": "via",
        "valueType": "string",
        "isArray": false,
        "required": false,
        "description": "Optional dotted relationship path used to reach the source entity (e.g. 'Program.weeks')."
      }
    ],
    "children": []
  },
  {
    "name": "from",
    "valueType": "string",
    "isArray": false,
    "required": true,
    "description": "Dotted Entity.field reference identifying the source value this projection field passes through (e.g. 'Program.title')."
  },
  {
    "name": "via",
    "valueType": "string",
    "isArray": false,
    "required": false,
    "description": "Optional dotted relationship path used to reach the source entity (e.g. 'Program.weeks')."
  }
]

############ meta types zzzznomatch  (no-match guidance) ############
$ meta types zzzznomatch
No vocabulary matches "zzzznomatch". Try --all to search descriptions, or drop --kind/--type filters.
Evidence: origin.aggregate --detail (the key benchmark-fixing output)
$ meta types origin.aggregate --detail

origin.aggregate (subtype)
A count/sum/avg/min/max (@agg) computed over a column (@of) reached along a relationship path (@via) from the base entity.
→ reach for it when: A projection needs a derived count/sum/avg/min/max over related rows. Declare it instead of hand-writing the aggregate query — it stays consistent and regenerates.
@attrs:
@agg string REQUIRED Aggregate function applied over the relationship path: count, sum, avg, min, or max.
@of string REQUIRED Dotted Entity.field reference identifying the column being aggregated.
@via string Dotted relationship path from the base entity to the aggregated rows.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ℹ️ server/typescript/packages/cli/src/commands/types.ts:152 - const head = e.kind === &#34;subtype&#34; ? e.name : e.name; — both branches are identical, so the ternary is dead. Replace with const head = e.name;.
  • ℹ️ server/typescript/packages/cli/src/commands/types.ts:61 - --limit parsing does Math.max(0, Number(args[++i] ?? &#34;20&#34;) || 0). A non-numeric value (e.g. --limit foo) yields NaN → || 0 → 0, which means unlimited and dumps the entire vocabulary — the opposite of capping. Invalid input would be safer falling back to the default 20 rather than unlimited.
  • ℹ️ server/typescript/packages/cli/src/commands/types.ts:120 - --kind type is indistinguishable from --kind subtype: the manifest is flattened to subtype/attr entries with no standalone top-level-type node, so wantKind() treats type and subtype identically. The help advertises three categories (type | subtype | attr) but only two distinct behaviors exist, so --kind type may mislead a user expecting a top-level-type-only list. Acceptable given the manifest model; noting the help/behavior mismatch.
✅ **Test** - passed

✅ No issues found.

  • bun run bin/meta.ts types --help / relationship / --all money / --type origin / origin.aggregate --detail / --type field --kind subtype / --type origin --json / zzzznomatch (full end-to-end CLI transcript)
  • cd server/typescript && bun test packages/cli/test/cli.test.ts packages/cli/test/help-and-exit.test.ts (help snapshot incl. new types line)
  • cd server/typescript && bun test packages/metadata/test/registry-conformance.test.ts packages/metadata/test/registry-coverage.test.ts packages/metadata/test/provider-data.test.ts
  • jq-normalized diff of all 8 spec/metamodel/*.json against server/python/.../spec_metamodel and server/csharp/.../SpecMetamodel (byte-identical)
  • Confirmed fixtures/registry-conformance/expected-registry.json carries 37 whenToUse entries
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 2 commits June 25, 2026 07:53
Agents under-use MetaObjects because they don't know the full vocabulary and
default to "generate CRUD, hand-write the rest" — a benchmark showed an agent
hand-writing joins + an AVG() instead of declaring a relationship + an
origin.aggregate projection.

Two-part fix (no metaobjects feature added to "win" — teach the agent to use
what already exists):

1. `meta types [QUERY]` — apropos + `kubectl explain` over the live registry.
   Case-insensitive substring on name; --desc/--all searches descriptions ("find
   by what it does"); --kind/--type filters; terse names-first default (capped 20,
   attr-name hints, "N of M" footer); --detail drill-in (description + when-to-use
   + @attrs); --json. Token-economy-first for an agent consumer. Reader over
   buildRegistryManifest; in `meta --help`.

2. Populate the canonical `whenToUse` slot (was 0/73) for the modeling constructs
   in spec/metamodel/*.json — the decisional "reach for this when… so it's
   generated, not hand-written" guidance, surfaced by `meta types`. Flows to all
   five ports (synced Python/C# spec copies; Java reads canonical; regenerated TS
   embedded + expected-registry.json).

Plus: broaden the authoring skill to drive "search the types, model it, don't
hand-write it" (general, not test-specific).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuZWKnWzYGVnESijL7uuky
@dmealing
dmealing merged commit 07c10c3 into main Jun 25, 2026
29 checks passed
@dmealing
dmealing deleted the feat/meta-types-vocab-discovery branch June 25, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant