Skip to content

Commit 7908a04

Browse files
authored
Merge pull request #311 from metaobjectsdev/worktree-metadata-source-resolution-phase1
feat(sdk): add metadata source resolution and scope filtering
2 parents 79bedda + 232a1af commit 7908a04

80 files changed

Lines changed: 9160 additions & 428 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ import { EntityFetcherProvider, EntityGrid } from "@metaobjectsdev/tanstack";
200200
- **Codegen substrate**: ts-poet for greenfield emit, ts-morph for in-place edits, Biome for format pass, `git merge-file --diff3` for hand-edit-preserving regen.
201201
- **Runtime substrate**: Kysely for TS (user-provided connection, async-only).
202202
- **Migration substrate**: Postgres + SQLite for TS v0.3.
203+
- **Metadata location**: resolved via `resolveCollection()` (`@metaobjectsdev/sdk`) — the single authority. `metaobjects/` is the **default value of `sources`** and nothing else: no other module, command or user-facing message may assert that a directory of that name exists or is where metadata lives. Exactly six sites may name it — `sdk/src/metadata-files.ts` (`DEFAULT_METADATA_DIR`, its single definition), `sdk/src/sources.ts` (`DEFAULT_SOURCES`, **the** default), `sdk/src/collection.ts` (inside `resolveCollection`, *applying* that default), `sdk/src/index.ts` (the barrel re-export of the constant, no use), `cli/src/commands/init.ts` (the scaffolder **writing** the layout), and `sdk/src/agent-docs/body.ts` (the agent-docs prose `meta init` scaffolds beside that layout). Enforced by `sdk/test/no-hardcoded-metadata-dir.test.ts`, whose allowlist demands a written reason per entry. See [docs/features/metadata-sources.md](docs/features/metadata-sources.md).
203204

204205
## Explicitly out of scope
205206

@@ -224,6 +225,8 @@ import { EntityFetcherProvider, EntityGrid } from "@metaobjectsdev/tanstack";
224225

225226
**Default convention**: one file per domain concept under `metaobjects/`. Multiple objects per file when they share a domain. Projections (`source.dbView`) live inline with their base entity.
226227

228+
`metaobjects/` is the **default value** of `sources` in `.metaobjects/config.json` — never a requirement. A project declaring `sources` explicitly can point anywhere (and need not have such a directory at all); `"sources": []`, which is what `meta init` scaffolds, takes the default.
229+
227230
```
228231
project-root/
229232
├── metaobjects/ # VISIBLE — entity declarations

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,59 @@ the local release path published `docs-site` ahead of `metadata` and `render`, t
9191
packages it depends on. The tier is declared now, and an undeclared one is an error
9292
instead of an accidental position.
9393

94+
### Metadata source resolution — adopter-visible changes
95+
96+
`.metaobjects/config.json` gains `sources`, `scope` and `migrate.scope`, and every
97+
command resolves where metadata lives through one authority instead of reading a
98+
hardcoded `metaobjects/` directory. A project with one config at its root, no
99+
`sources` and no `scope` resolves the same files, generates the same code and emits
100+
the same migrations. Three changes are visible even to that project. Adopter guide:
101+
[`docs/features/metadata-sources.md`](docs/features/metadata-sources.md#upgrading).
102+
103+
- **The workspace `extends:` walk is retired.** `loadMemory` used to have a second,
104+
undocumented way of finding metadata: a `package.meta.json` declaring `extends:`
105+
dependencies, inside a discoverable workspace (`pnpm-workspace.yaml` or
106+
`package.json` `workspaces`), pulled in each peer package's `metaobjects/`
107+
directory first, in topological order. Every CLI read path now resolves through
108+
`sources`, which does no such walk. It fails LOUDLY — `ERR_UNRESOLVED_SUPER`
109+
naming the target it cannot find, never a half-resolved model — and the
110+
replacement is an explicit `{ "path": "../shared-model/metaobjects" }` source,
111+
which works in any layout and needs no topological ordering.
112+
- **`.metaobjects/config.json` rejects unknown keys.** `ConfigSchema` is `.strict()`
113+
at every level, so a key that was previously stripped in silence is now a load
114+
error naming the key. Silently dropping a key means the setting you wrote does not
115+
exist: `{ "migrate": { "scopee": [...] } }` used to mean *unscoped*, governing
116+
every table in a database you were trying to share.
117+
- **`ExpectedView.fqn` is required.** On the public `@metaobjectsdev/codegen-ts`
118+
export, the declaring object's fully-qualified name is no longer optional —
119+
`migrate.scope` decides ownership on that name, and a view arriving without one
120+
cannot be scoped at all. `buildProjectionViews` already supplies it; only
121+
hand-built `ExpectedView` values need the field added.
122+
- **`meta export` output order changed, and `_pending/` is excluded.** `export` now
123+
serializes the file set `resolveCollection` resolved rather than scanning a
124+
directory through `DirectorySource`, so siblings emit files-before-subdirectories
125+
(the overlay-safe order the loader has always been given) instead of a flat
126+
basename sort, and staged `_pending/` files — skipped by every other read path —
127+
are no longer exported. The canonical JSON content is unchanged; a committed
128+
export diffed against a fresh one shows a reordering.
129+
- **The migrations directory follows the project root.** `.metaobjects/migrations`
130+
and the schema snapshot resolve from the directory whose `.metaobjects/config.json`
131+
governs the run, found by walking up from the working directory. `meta migrate
132+
apply-pending` and `--rollback` load no metadata and previously used the working
133+
directory unconditionally, so a subdirectory holding a ledger but no config of its
134+
own now replays the project root's history. `migrate` says so out loud when the
135+
resolved directory differs from `<cwd>/.metaobjects/migrations` and that local
136+
directory exists; `--out-dir` overrides, and giving the subdirectory its own
137+
`.metaobjects/config.json` makes it a project root.
138+
- **A project boundary is a `.metaobjects/config.json` — a bare `metaobjects/`
139+
directory is not one.** Discovery walks up for a config and stops at nothing
140+
else short of the `.git` boundary, so a command run inside a nested directory
141+
that holds metadata but declares no config of its own resolves the nearest
142+
ancestor config — adopting its `sources` and `outDir`. `metaobjects/` is the
143+
default *value* of `sources`, so a directory of that name says nothing about
144+
whether a project lives there. If a subdirectory should own its metadata, give
145+
it a config: `meta init` writes one, and a `"sources": []` config is enough to
146+
claim the directory and take the default.
94147

95148
## [0.23.2] — npm `0.23.2` · PyPI `0.23.2` · NuGet `0.23.2` · Maven `7.23.2`
96149

docs/CONFORMANCE.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Conformance coverage
22

3-
The MetaObjects standard ships **19 shared conformance corpora** under
3+
The MetaObjects standard ships **20 shared conformance corpora** under
44
[`fixtures/`](../fixtures/). Every port runs every corpus that is *applicable to
55
it* and asserts the same expected behaviour against the same fixtures. **This page
66
is the inverse index**: fixture → feature doc + per-port pass status, and it is the
@@ -42,6 +42,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l`.
4242
| [`fixtures/template-output-render-conformance/`](../fixtures/template-output-render-conformance/) | 5 ||||||
4343
| [`fixtures/generator-registry-conformance/`](../fixtures/generator-registry-conformance/) | 1 canonical manifest ||||||
4444
| [`fixtures/provider-composition-conformance/`](../fixtures/provider-composition-conformance/) | 9 (5 error-shape + 4 compose-load) ||| — (JVM registry via Java) |||
45+
| [`fixtures/scope-conformance/`](../fixtures/scope-conformance/) | 10 cases | ✓ (reference implementation) |||||
4546
| [`fixtures/agent-context-conformance/`](../fixtures/agent-context-conformance/) | 4 | ✓ (the emitter is TS-owned) |||||
4647
| [`fixtures/metamodel-docs/`](../fixtures/metamodel-docs/) | 1 | ✓ (docs emit is TS-owned) |||||
4748

@@ -138,10 +139,33 @@ inheritance), `m2m/` (3), `jsonb/` (2, typed value-object columns) and
138139
Kotlin, C#, Python — run it in BOTH lanes: a hand-rolled reference server and
139140
the port's own GENERATED API artifact booted over HTTP.
140141

142+
### `fixtures/scope-conformance/` (10 cases)
143+
144+
All 10 cases → [features/metadata-sources.md](features/metadata-sources.md) (the
145+
`scope` pattern grammar). The corpus is file-shaped: one committed `cases.json`,
146+
read directly by every port's runner, with no per-port fixture and no ledger.
147+
148+
It pins the semantics of a consumer's `include`/`exclude` scope over
149+
fully-qualified names — **`*` matches within one `::` segment and never crosses
150+
it; a segment that is exactly `**` matches one or more whole segments (so
151+
`acme::**` does not match the bare `acme`); every other character is literal,
152+
regex metacharacters included; an absent or empty `include` means everything;
153+
multiple `include` patterns are a union and `exclude` is applied after it; and
154+
matching is case-sensitive.** These are exactly the rules four independent
155+
implementations would otherwise each get slightly wrong — the failure mode that
156+
produced the cross-port `LIKE`/`ILIKE` divergence fixed in 0.21.6.
157+
158+
**TypeScript is the only port with a runner today.** The reference implementation is
159+
[`server/typescript/packages/sdk/src/scope.ts`](../server/typescript/packages/sdk/src/scope.ts)
160+
(`compilePattern` / `compileScope` / `matchesScope`), and the corpus was authored
161+
against it. Java, Kotlin, C# and Python have no runner yet; when each gains one, this corpus is
162+
what it implements against — it exists now precisely so those four land on one
163+
grammar rather than four.
164+
141165
## Orphaned fixtures (tested but not yet documented)
142166

143-
The fixtures in the six corpora mapped above (metamodel 255 + yaml 15 + verify 31
144-
+ render 15 + persistence 33 + api-contract 41) each map to a feature doc. None
167+
The fixtures in the seven corpora mapped above (metamodel 255 + yaml 15 + verify 31
168+
+ render 15 + persistence 33 + api-contract 41 + scope 10) each map to a feature doc. None
145169
are orphaned today. The remaining corpora in the totals table gate tooling
146170
contracts (registry manifests, provider composition, agent context, docs emit)
147171
rather than user-facing metamodel behaviour, so they have no feature-doc row.

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ docs/
3737
│ ├── downstream-metadata-decisions.md # guidance for adopters extending the metamodel
3838
│ ├── generated-mutations.md # generated POST/PATCH mutation surface
3939
│ ├── image-upload.md # view.image form control (TS-web)
40+
│ ├── metadata-sources.md # where metadata comes from: sources, scope, discovery
4041
│ └── own-your-codegen.md # scaffold-and-own generator ownership (ADR-0034)
4142
└── ports/ # one file per language/framework port
4243
├── typescript.md
@@ -58,6 +59,7 @@ this tree is documentation, not the source of truth.
5859
| Understand what `object.entity`, `source.rdb`, `template.prompt` mean | [`features/`](features/) |
5960
| Compare what TS vs Java vs Kotlin vs C# vs Python emit for the same metadata | any [`features/*.md`](features/) — every feature shows all five ports side-by-side |
6061
| Author metadata in YAML instead of JSON | [`features/yaml-authoring.md`](features/yaml-authoring.md) |
62+
| Point the toolchain at metadata that lives somewhere other than `metaobjects/`, or scope what a project generates and migrates | [`features/metadata-sources.md`](features/metadata-sources.md) |
6163
| Record what the system is supposed to do, and stop agents reviving retired features | [`features/requirements.md`](features/requirements.md) |
6264
| Wire prompt construction (FR-004) | [`features/templates-and-payloads.md`](features/templates-and-payloads.md) |
6365
| Share a metadata shape across multiple instances (abstracts, `extends:`) | [`features/abstracts-and-inheritance.md`](features/abstracts-and-inheritance.md) |

0 commit comments

Comments
 (0)