feat(fr-023): metadata dependencies — declare, sync, and build on a shared model (Phase 1a) - #366
Merged
Merged
Conversation
Twenty-three TDD tasks for TypeScript, Python, the dependency-conformance corpus and the docs/skills, executed from the committed design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…sk sections Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…s, and nine error codes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…t package (FR-023) A shared document is one canonical-JSON metadata.root with NO root package, whose top-level nodes each carry their own explicit package, so it re-loads to the same resolution keys in every port. Each node is its own-layer canonical form (extends preserved, attrs alphabetized, FR-016 rewrite applied); top-level nodes sort by resolution key, children keep their authored order, and the body key order is name, package, then the canonical rest. A node with no package is refused. - TS: serializeSharedDocument + packageOfResolutionKey, exported from the index. - Python: serialize_shared_document + package_of_resolution_key, byte-identical (both reproduce the pinned dependency-conformance artifact acme-common-v1.json). - fixtures/conformance/xpkg-node-level-package-no-root-package: the artifact as loader input and as expected canonical output; green in TS, Python, Java, Kotlin and C#. - The metamodel fixture count in docs/CONFORMANCE.md, AGENTS.md and README.md moves 313 -> 314. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…inned artifact Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
fixtures/dependency-conformance/ is a shared corpus, so the stated corpora count moves 21 -> 22 wherever it is typed (docs/CONFORMANCE.md, AGENTS.md, README.md), and CONFORMANCE.md gains its row. The row states the corpus's actual state: one case so far, a TypeScript runner that is red by design until the resolver lands, no Python runner yet, and Java/Kotlin/C# deferred to Phase 2. The llms mirrors carry the metamodel fixture count 314, and the site payload is regenerated through `bun run site:payload` (counts only: fixtures 314, corpora 22). scripts/site/counts.test.ts and the payload --check are green again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…ance)
new FileSource(path, { id? }) (TS) and FileSource(path, format=None, id=None)
(Python) accept an explicit source id, defaulting to basename(path) as before.
The parser stamps source.files with whatever id a node's FileSource carries
(ADR-0009), so a dependency artifact loaded later with
id: "dep:<name>/<artifact>" will record where it really came from.
Default behavior for every existing caller (single positional path argument)
is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… in TS Python's id property used `self._id or self._path.name`, so an explicit `id=""` fell through to the basename default — diverging from TS, where `??` (nullish coalescing) already honours `id: ""`. Switched to an explicit None-check (`self._id if self._id is not None else self._path.name`) and pinned the empty-string case in both ports' tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…ort (FR-023) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… and Python neutral reader (FR-023) Task 5 of FR-023 phase 1a. Adds the `dependencies` key (DESIGN §3.1): a declared name, exactly one transport (`path` | `npm` | `python`, the latter two with an optional `dir`), and a `mode` (`reference` | `own`, default `reference`). Strict at every level, mirroring the existing `sources`/`SourceSpec` pattern: an unknown key, a second transport, no transport, a malformed name, a bad mode, or a duplicate name all refuse to parse. - `sdk/src/dependencies.ts`: the DESIGN-pinned constants (`DEPS_DIR`, `LOCK_FILE`, `LOCAL_OVERRIDE_FILE`, `MANIFEST_FILE`, `ARTIFACT_SUFFIX`, `DEPENDENCY_SOURCE_ID_PREFIX`, `INTEGRITY_PREFIX`, `DEPENDENCY_MODES`, `DEFAULT_DEPENDENCY_MODE`), `DependencySpec`/`DependencyMode` types, and `DependencySpecSchema` (zod), exported from the package barrel. - `sdk/src/config.ts`: `ConfigSchema.dependencies` (default `[]`, unique-names refine) plus the two-direction compile-time parity guard between the schema and the hand-written `DependencySpec`, matching the existing `SourceSpec` guard. - `server/python/.../config/dependencies.py`: the same constants. - `server/python/.../config/neutral_config.py`: `NeutralConfig.dependencies` and validation matching the TS schema's refusals exactly, coded `ERR_COLLECTION_NOT_FOUND` (this reader's existing shape-error code). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…me option cut The re-cut left six rulings open. All are now settled, and the design and plan are corrected to match. - Exclusion is keyed on the lock's `packages`, not its `nodes`. The design argued `nodes` on a single empirical claim: that legacy consumers declare new top-level nodes into a dependency's package. That was checked against the estate and no instance distinguishes the two keys — the one legacy consumer declaring into a shared framework package adds no class the framework does not already declare, because it amends rather than adds, and an amendment is an overlay. The claim is false as written; §2.4 and §2.7 are corrected and §11.5 records the evidence. - The one failure mode a package-keyed rule introduces — a consumer node in a dependency package silently never generating — becomes a loud `ERR_DEPENDENCY_PACKAGE_NOT_OWNED`, raised after load. An overlay merges into a node whose FQN is in the lock's `nodes`, so it passes untouched; only a genuinely new object is refused. `nodes` is retained in the manifest and lock for artifact integrity, collision detection and that refusal. - A wildcard `include` does not opt a dependency in; only a literal package prefix does. - Task 16, the ObjectManager scope predicate, is cut: "runtime" meant what metadata gets loaded, which the collection resolver already does. The task number is kept so the progress ledger's references stay resolvable. - `verify --deps` fails when the publisher is unreachable, and stays out of the default verify run. - Python keeps applying only the import rule, not the user's full `scope`, to its own objects — confirmed against the tree, where no caller of the scope grammar exists. Error codes after Task 6 become seven. `metamodelVersion` is unaffected: the gate reads the registry manifest, and an error code is not registered vocabulary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…d error codes (FR-023 §11)
Task 6 of FR-023 phase 1a. The B+ re-cut (DESIGN §11) supersedes v2's
mode: reference | own and the D7/D6(b) machinery it depended on;
this task is the pruning pass, plus one addition, plus two carried
minors. No new feature.
- `sdk/src/dependencies.ts` + `config/dependencies.py`: remove
`DEPENDENCY_MODES`, `DEFAULT_DEPENDENCY_MODE`, `DependencyMode`,
`LOCAL_OVERRIDE_FILE`, and the `mode` member of `DependencySpec` /
the Mode zod field on every schema arm. `DependencySpec` is now
`{ name } & (path | npm+dir? | python+dir?)` with no mode — a
`mode` key of ANY value (including "reference", the old default)
is now an unrecognized-key schema error, since explicit
`scope.include` replaces it (DESIGN §11.1 item 2).
- `neutral_config.py` `_validate_dependency_spec`: drops `mode`
handling and its allowed-key entry to match.
- `fixtures/conformance/ERROR-CODES.json` + `errors.ts` + `errors.py`:
remove `ERR_DEPENDENCY_OVERLAY_IMPLICIT`, `ERR_DEPENDENCY_SCHEMA_NOT_OWNED`,
`ERR_DEPENDENCY_BREAKING_CHANGE` (superseded — §11.3); add
`ERR_DEPENDENCY_PACKAGE_NOT_OWNED` (raised by a later task — the
package-keyed exclusion rule of §11.5 needs a loud refusal when a
local top-level node lands in a dependency's package); reword
`ERR_DEPENDENCY_UNRESOLVED` to drop the retired local-override
clause. All three registries move together (`errors.test.ts` /
`test_errors.py` assert parity) — verified with both green.
- `fixtures/dependency-conformance/`: the corpus's case schema drops
`localOverrides` / `expectOverrides` / `expectForeign` /
`expectGoverned` / `classify`; adds `expectImported` /
`expectSelected` / `expectMigrateGoverned`, each the EXHAUSTIVE set
over every loaded top-level object admitted by
`collection.imported` / `collection.inScope` /
`collection.inMigrateScope` respectively (DESIGN §11.2). The TS
runner is rewritten to match; `collection.imported` doesn't exist
on `Collection` until a later task, so the corpus's one case now
fails on that TypeError — expected, left failing per the plan.
- Two carried minors from earlier reviews: Python `matches_scope`
now uses `re.fullmatch` (bare `re.match` lets `$` match just before
a trailing newline, a genuine JS/Python semantic divergence — a
literal, non-wildcarded pattern isolates it, since a wildcard
segment's `[^:]*` legitimately consumes a literal newline as
content either way); the TS corpus runner writes the lock file
under `LOCK_FILE` imported from `dependencies.ts` rather than the
inlined `"deps.lock.json"` string.
TDD: config.test.ts / test_neutral_config.py / test_scope_conformance.py
edited to fail first (mode still parsed; re.match still matched),
then made to pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… arms Two corrections from the Tasks 6-21 conflict scan, made before Task 6 ran and committed after it so its diff stayed scoped to its own files. - The ownership refusal must fire on BOTH of loadMemory's arms. As first written it covered only the caller-supplied `files` arm, so every embedder calling loadMemory(repoRoot) with no files would have silently lost it. collectionLoadOptions now returns all four fields. - The codegen runner's options type is RunGenOpts, not RunGenOptions, verified against the source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…n bug Task 6's RED-phase example was wrong twice: compile_scope takes positional sequences, not a dict (passing a dict compiles its keys, silently yielding a stub pattern), and the wildcard it used compiles to a character class that admits a newline — so match and fullmatch agreed and the assertion could never have failed against the defect it was written for. The literal pattern is the only form where the two diverge. Measured both ways before correcting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Task 7 (Phase 1a): the data contracts for `metaobjects.pkg.json` (ManifestSchema) and `.metaobjects/deps.lock.json` (LockSchema/LockEntrySchema), plus sha256 content hashing, pure and with no consumer yet — the collection resolver that folds a dependency's artifact into the loaded tree, and the package-based exclusion key, land next. - `packages`/`nodes` are both required and both validated sorted on every schema, per the 2026-09-11 ruling (§11.5): `packages` is the exclusion key, `nodes` stays for sync's artifact check, node-collision detection and the package-ownership refusal. - No `mode` field anywhere (Task 6 removed it; a resurrected `mode` key is a hard parse error via `.strict()` in TS and an unknown-key check in Python). - `writeLock` sorts dependency keys, 2-space indent, trailing newline, no timestamps or host paths — byte-identical across machines. - Python hand-validates (no schema library), raising `ParseError` with `ERR_DEPENDENCY_MANIFEST_INVALID` / `ERR_DEPENDENCY_SNAPSHOT_STALE`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…ourceId Fix round 1 on Task 7 review (Important finding): readLock/writeLock (TS) and read_lock/dependency_source_id (both ports) shipped with zero test coverage. Adds a writeLock→readLock round-trip asserting the exact on-disk bytes (sorted dependency keys, 2-space indent, trailing newline, no host paths or timestamps) against a hand-written expected literal captured from a real run, plus direct assertions for dependencySourceId/dependency_source_id and read_lock's absent-file, present-file, malformed-JSON and shape-violation cases. No production code changed — the functions were already correct; every added test passed on first run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…poses the default-exclusion scope (FR-023 §11)
`resolveCollection` now verifies a project's committed snapshot against
`.metaobjects/deps.lock.json`, leads its file list with the dependency
artifacts (name order, then the project's own files), and composes the two
predicates every downstream surface acts on:
inScope(fqn) = matchesScope(fqn, scope)
&& (!imported(fqn) || explicitlyIncludes(scope.include, pkg))
inMigrateScope(fqn) = (declaredMigrateScope?.(fqn) ?? true)
&& (!imported(fqn) || explicitlyIncludes(migrate.scope, pkg))
`imported` keys on the lock's `packages` (DESIGN §11.5), not its `nodes`.
`explicitlyIncludes` requires a pattern to NAME a package literally, so
`acme::common::**` opts in and `**` does not: a project that writes a
wildcard to mean "all of my model" must not thereby generate, and migrate,
somebody else's.
`inMigrateScope` stays `undefined` exactly when the project declares no
`migrate.scope` AND resolves no dependencies — the untouched path that leaves
migrate's expected schema alone. A project with no dependencies resolves as it
always did; `nodes` is retained for one reader, the new post-load
`ERR_DEPENDENCY_PACKAGE_NOT_OWNED` refusal, which tells an overlay of an
imported node from a new local declaration in its package.
`loadMemory` gains `fileIds` (so an artifact's nodes carry `dep:<name>/<artifact>`
provenance rather than a local-looking basename) plus the imported sets, and
runs the refusal AFTER the loader's own errors on BOTH arms — the
caller-supplied file list and the self-resolving one, so an embedder calling
`loadMemory(repoRoot)` does not silently lose it. The nine CLI load sites now
thread all four through one helper, `collectionLoadOptions`.
Corpus: 15 cases appended to fixtures/dependency-conformance (resolution,
scope/migrate-scope composition, the ownership refusal and its overlay
counter-case, five staleness arms, node collision, metamodel-major
incompatibility, and a native-source-surface layout). The corpus is fully
green; the sanctioned `collection.imported is not a function` failure ends
here.
Also fixes one pre-existing break this task's cli typecheck was the first
command to see: the sdk root barrel exports two types named `Manifest` (the
agent-context one via `export *`, FR-023's explicitly), and an explicit export
shadows a star one, so `agent-context-staleness.ts` was resolving the wrong
type once `dist/` was rebuilt. It now imports from the `/agent-context`
subpath, as `init.ts` already did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…; the requirements line's dependency count is conditional (FR-023) Two adjudicated fixes from the Task 8 review. 1. `verify`'s requirements summary appends `, N from dependencies` only when there ARE dependencies. A project that declares none prints the sentence it printed before metadata dependencies existed, ending at `counted over N metadata file(s).` — the byte-identical no-dependency guarantee covers this surface like any other. The covering assertion now includes the trailing period, so it can tell the two sentences apart; without it the test passed just as happily on the ungated output. 2. FR-023's manifest type is `DependencyManifest` / `DependencyManifestSchema`. The package barrel already published an agent-context `Manifest` through an `export *`, and an explicit export shadows a star one, so the bare name silently re-typed every consumer asking the root barrel for the agent-context manifest. FR-023's is the newcomer, and the longer name reads right beside its own siblings (`DependencySpec`, `LockEntry`, `Lock`, `ResolvedDependency`). The previous commit's subpath fix in `agent-context-staleness.ts` stands on its own and is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…t in the docs Two controller rulings from Task 8's concerns, synced into the documents that later tasks read. - The ownership refusal names the object and the package, not the dependency. The load-time option shape carries flat package and node sets with no package-to-dependency-name map, and the package is what a user acts on since scope.include takes a package. The design claimed otherwise; the code was right and the design was wrong. - The dependency manifest type is DependencyManifest / DependencyManifestSchema after the rename that ended the sdk barrel's two-Manifest collision. The plan and the design still named the old symbol, and the design is a living spec a later task reads. Prose naming the manifest FILE is deliberately unchanged, as is the unrelated readManifestDir. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…ft gate (FR-023) Appends 7 load-time failure cases to fixtures/dependency-conformance/cases.json (DESIGN §2.5's table, plus the sibling whole-object-overlay case and the malformed-artifact case from §11.1 item 4) — no source change. Every one demonstrates that when an upstream dependency's snapshot changes underneath a consumer, an existing loader error (ERR_OVERLAY_NO_TARGET, ERR_UNRESOLVED_SUPER, ERR_UNRESOLVED_OBJECT_REF, ERR_EXTENDS_TARGET_MISMATCH, ERR_MERGE_CONFLICT, ERR_MALFORMED_JSON) already fires — proven by running each case, not merely asserted. README documents the mapping table and the confirmed load-order guarantee (loader errors before the package-ownership refusal) the whole-object-overlay case depends on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Pre-flight check before the codegen-selection task: the plan's file list named only one of renderSharedEnumsFile's two call sites. The other is the reference template meta init copies eagerly into every scaffolded project, so threading the selection predicate through the library path alone would leave every consumer's own generated copy emitting enums for imported entities that the library path correctly excludes. Both call sites must honour it, and a diff updating only one is an Important finding rather than a nit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
The task's file list said six while its own step enumerates six bullets whose first carries an embedded sibling case — seven in total. The implementer caught the disagreement and followed the step, which was right; the file line would have misled the next reader the same way it misled me when I summarised it into the dispatch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…de names their package (FR-023 §11) Task 10: `meta gen`'s existing scope wiring (Task 8) already narrows per-entity output, but the shared-enums module renders once over the whole loaded root and never runs a generator's per-entity filter — so it needed its own `select` knob threaded through GenContext/RunGenOpts to honour the same default exclusion of imported metadata. Also adds a `meta gen <Name>` pre-check that refuses (exit 2) a positional naming only objects imported from a dependency and excluded from output, distinct from the existing "no entities matched" warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…call sites Pre-flight check before the schema-tooling task: its file list names four CLI sites, but only three of them call scopeExpectedSchema directly. The offline migrate pipeline reaches it through planOffline, and verify --db reaches it through computeDriftFromActual. Threading the option only at the direct sites would leave both of those performing no import exclusion at all, with every test still passing — that task's own Postgres test calls the function directly and cannot see the gap. Both option bags now carry it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…t files Its step ran three test paths that are not in the repo. The implementer trusted the source over the example and ran the real analogs, which was right. Corrected to the real paths, including the integration subdirectory a flat listing of the test folder hides. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
The codegen-selection task told an implementer to build its consumer scaffold the way a test does that is not in the repo — a pattern to copy rather than just a command to run, so it would have sent someone hunting. Now names the real gen test that scaffolds a config with the owned entity and barrel generators, verified to contain that shape before the edit was written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…nd never widen its schema scope (FR-023 §11)
A dependency's nodes are loaded so the consumer's own model can resolve against
them. Nobody governs them here, so `migrate` and `verify --db` must exclude them
on BOTH sides: out of the expected schema (no create, no alter) and suppressed on
the actual side via `unmanagedNames` (no drop). Doing only the first half is
strictly worse than doing nothing — the publisher's table exists in the shared
database, so removing it from `expected` alone turns it into a proposed DROP
TABLE against data this consumer does not own.
The correction that makes it safe is an ORDERING one. `scopeExpectedSchema` pins
`diff`'s schema scope to the unscoped model on purpose ("a scope narrows objects,
never schemas"), which is right for `migrate.scope` — the same model declared into
that schema — and wrong for an import: the consumer never declared into the
publisher's schema, so leaving it pinned makes every table the publisher did NOT
export a drop candidate. So an excluded import now leaves the expected side BEFORE
`declaredSchemas` is computed, and the value is derived from the remainder.
Everything `migrate.scope` excluded is still in that remainder, so the older rule
is untouched.
The predicate reaches all five CLI paths, including the two that call
`scopeExpectedSchema` one layer down: `PlanOfflineArgs` (the offline pipeline, the
default `meta migrate`) and `ComputeDriftOptions` (`verify --db`, both dialect
paths). Threading only the direct sites would leave those performing no import
exclusion while every test still passed.
Also: the "your migrate.scope matched nothing" refusal now reads the DECLARED
scope rather than the composed predicate. The composed one also excludes imports,
so a consumer whose only table-declaring objects are imported — and who declared
no scope at all — would have been refused, citing patterns that do not exist.
`scopeExpectedSchema` without the new option is byte-identical: same snapshot
object, no `declaredSchemas`, and the CLI supplies no predicate at all for a
project with no dependencies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…call sites Pre-flight check before the ledger task: its read-first line cited a line number off by over two hundred, and described coverableEntities as a single call site when it has two. Gating one and not the other would leave imported entities still counted, with every test green — the third time this plan has named one call site where there are several. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…sk's step Its first step named a test file that does not exist, showed a builder call with the wrong signature, and then talked itself out of its own example mid-sentence before giving the right one. A step an implementer is told to follow verbatim cannot contain the author's discarded first attempt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
The schema task's second step told an implementer to assert the note on stderr. Under the default text format it goes through the info logger, which writes to standard output; only the non-text branch uses the error stream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… axis The guarantee promised identical behaviour for any project with no dependencies, on every surface, without qualification. That holds for the dependency axis it was written about, but the design deliberately routes the requirements denominator through the same predicate that applies a project's own declared scope — so a project that declares one, with no dependencies at all, does see its denominator narrow. Intended behaviour, overreaching wording; qualified so a later reader does not read it as a contradiction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…red-model artifact and manifest (FR-023) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…upe key with the \0 escape (FR-023) A literal 0x00 control byte in the closure-check dedupe key made git treat the file as binary — diffs showed no content, and the public-repo-hygiene pre-commit grep never enumerated its added lines, silently bypassing that guard for this file. The `\0` escape produces the identical runtime string with byte-identical source. Also adds a targeted test for a top-level node whose own explicit `package` differs from its file's default: the closure check's effectivePackage()-derived referrer package agrees with the loader's own threaded reference resolution here (fileDefaultPackage is captured at parse time from the nearest PACKAGED ancestor, not the file root), so this closes the reviewer-flagged unknown rather than a live defect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Three reference defects found while resuming execution at Task 13. Each was verified against the tree before editing; the source won in every case. - Task 13's file list named `RunGenOptions.sourceFiles?`. The exported type is `RunGenOpts` (runner.ts:45) and `RunGenOptions` exists nowhere in the workspace. The plan had already diagnosed this name in Task 10's notes and left the wrong one standing here. - Tasks 6 and 21 both grep for deleted vocabulary expecting no output, using a bare `OVERLAY_IMPLICIT`. That pattern also matches `WARN_OVERLAY_IMPLICIT` — the diagnostic code Task 17 introduces, and which Task 20 then documents under docs/features and agent-context, both of which Task 21's grep covers. As written, the gate would fail on the plan's own deliverables. Both are anchored to `ERR_DEPENDENCY_OVERLAY_IMPLICIT`, which is what the deletion check means. The remaining alternatives are unambiguous: `SCHEMA_NOT_OWNED` does not match the live `ERR_DEPENDENCY_PACKAGE_NOT_OWNED`. No behaviour change; plan text only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… sha256 lock (FR-023) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…lp, dedupe hash8, cover the name filter (FR-023) Fix round 1 on Task 14: catch readLock's throw in `deps sync`/`deps list` (a corrupted committed lock previously crashed via an unhandled rejection — the same failure class already guarded for a bad config.json); add `deps` to help-lists-every-flag.test.ts's COMMANDS table so its flags are actually gated going forward; export one `hash8` helper from dependency-sync.ts (built on INTEGRITY_PREFIX) instead of three independent inlined copies; and add coverage for `meta deps sync <name>`'s carry-forward + cross- dependency collision seeding (no defect found — confirms the existing `planSync` behavior). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…ing itself (FR-023) Fix round 2 on Task 14: the round-1 name-filter test exercised real code but could not fail under a mutant that deletes filtering entirely (targets = [...specs] unconditionally) — the untargeted dependency's untouched publisher made "carried forward" and "re-resolved fresh" produce identical results. Adds a test that deletes the untargeted dependency's publisher directory outright before a filtered sync, so re-resolving it (the mutant's behavior) fails while correct filtering succeeds. Verified against the mutant by hand: fails under it, passes reverted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Task 14's file list specified `DEPS_OPTIONS = { "dry-run": boolean, format:
string }`. `--format` is a GLOBAL flag: `index.ts` extracts it from anywhere in
argv before any command parser runs, and validates it once for every command. No
command's option table declares a `format` key — the apparent counter-example,
`MIGRATE_OPTIONS`, matches only the unrelated `"migration-format"`.
`parseDepsArgs` also runs `parseArgs` with `strict: true`, so the two facts have
to hold together: listing `format` there would be dead at best, and omitting it
is only safe because the global extraction happens first. Both verified before
correcting.
Found by the Task 14 implementer against the source, not by the plan's author.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…dependency differs from the lock (FR-023) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… re-hash leans on (FR-023) Task 15 fix round 1: readManifestDir already guarantees sha256Integrity(artifactContent) === manifest.integrity before checkDependencies's own re-hash can run, so the re-hash cannot currently observe a divergence. Comment only — records the invariant and the risk if readManifestDir's check is ever relaxed, so a future reader isn't left to trace it themselves. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…as an overlay authoring finding (FR-023 §11) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…me way the real parser does Reuses parser-core.ts's expandPackageForPath (now exported) instead of using an own `package` verbatim, so a `::`-prefixed package on a root-level declaration resolves to the same key MetaData.resolutionKey() produces on the loaded side. Fixes a silent mis-grouping risk in the overlay lint (Task 17 fix round 1). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Both found while executing Task 17; both verified against the tree before editing. - Task 17's Step 2 asserted "NO existing test pins the ordering guarantee itself" for the overlay-last source partition. False: `sdk/test/order-independence.test.ts` permutes all six orderings of three real files — overlay-before-base included — through the real loader, and its own comment records that disabling the partition makes three of the six throw ERR_OVERLAY_NO_TARGET. The earlier sweep missed it by scoping the search to `metadata/test` when the test lives in `sdk/test`; the plan's own suggested discovery command would have found it at repo scope. - Task 20 told the implementer to regenerate the agent-context corpus with `node scripts/bundle-agent-context.mjs`, citing `agent-context/bundle.test.ts` as the assertion. Wrong on three counts: that path does not exist from the repo root, it is a different script that does not refresh the corpus, and the cited test is a ten-line existence check that cannot detect stale prose. The gate that pins prose is `agent-context-conformance.test.ts`, regenerated by `bun scripts/regen-agent-context-conformance.ts` from the sdk package. Plan text only; no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…gen unless scope.include names their package (FR-023 §11) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…iminating ordering test (FR-023 §11 fix round 1) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…no longer scaffolds package.meta.json (FR-023) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…--print-only forecast (FR-023 task 19 fix round 1) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
… when upstream moves (FR-023 §11) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…-any-generator claim (task 20 fix round 1) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Both asserted that every port reads `dependencies` and loads dependency snapshots. Only TypeScript and Python do, in Phase 1a — Java, Kotlin and C# don't read `dependencies` at all; that is Phase 2. Verified by grep: the dependency-resolution code exists in `sdk/src/dependencies.ts` and Python's `config/dependencies.py`, with zero occurrences under `server/java`, `server/csharp`, or any Kotlin source. This wording almost certainly caused the same overclaim to ship in the documentation, where a review caught it across nine sites — including five regenerated agent-context goldens that ship to Java, Kotlin and C# projects. A contracts document is a fact source its readers reproduce, errors included. The correction keeps the design intent and pins the phase, rather than flattening "every port" into "two ports" — dependencies are meant to be read at every rung by every port; only two implement it today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Task 10 taught the canonical reference `entityFile` to pass `select` into `renderSharedEnumsFile`, so a whole-root render excludes enums used only by an imported, out-of-scope entity. Its three owned copies were never re-synced. That is not bookkeeping: this template is what `meta init` copies into every scaffolded project's `codegen/generators/`, so the library path had the fix while every newly scaffolded project kept silently emitting shared enums the library correctly excludes. Re-synced through `bun scripts/sync-owned-template-copies.ts`, never by hand. All three copies received a byte-identical change, and the gate now reports 12 of 12 current. Found by the full `gates` lane at the end of the plan. Per-task run lists are scoped to the packages a task touches, so nothing ran this gate for the ten tasks between its introduction and here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Task 21's Step 2 grep ran a vocabulary-DELETION check over `docs/features` and `agent-context` with `--include=*.md`. That collided with Task 20's own mandated deliverable: its brief requires the new page to carry a `## Deferred` section naming the local override, the classifier and the npm/python transports, so the gate flagged prose that is correct and required. Same shape as the earlier unanchored `OVERLAY_IMPLICIT` pattern, which would have matched Task 17's own `WARN_OVERLAY_IMPLICIT`. An absence check belongs on the surface where the thing would actually live; prose legitimately names a cut feature in order to say it does not exist. Scoped to code paths, with the prose rule stated separately beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
generateSite runs its own independent loadModel over sourceDirs rather than reusing the already-loaded collection root, and collection.sourceRoots deliberately excludes dependency artifacts. A consumer that extends an imported abstract or overlay:trues an imported node — the two constructs entities.md and abstracts-and-inheritance.md recommend for this exact boundary — hit ERR_UNRESOLVED_SUPER / ERR_OVERLAY_NO_TARGET and --site exited 1. Append each resolved dependency's snapshot dir to the dirs passed to generateSite; loadModel picks it up like any other source dir since a snapshot dir holds exactly the one artifact file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Python implements only the import-exclusion conjunct of inScope (its own docstring already says so) — it has never applied matchesScope to a project's own objects. The doc presented one unqualified formula that reads as cross-port; state both, and note in the corpus README that the two scope-bearing expectSelected cases both use includes under which every own object already matches, so neither can catch this divergence today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…pendencies runOverlayLintAdvisory() is called unconditionally in verify.ts and lintOverlays() iterates collection.files, which for a zero-dependency project is simply its own files — so a plain multi-file local overlay (no dependencies at all) triggers the same advisory finding. The metaobjects-verify skill said the lint only runs "if this project declares dependencies"; correct it and regenerate the five agent-context golden copies in the same commit. Also: AGENTS.md's own "Optional layered overlay pattern" example (three files sharing one package+name) carried no overlay: true, so every project following the repo's own documented pattern would now get the advisory finding — add the flag to the example. Record the second carve-out from "no dependencies behaves identically" in the CHANGELOG: new WARN_OVERLAY_IMPLICIT advisory output on meta verify for zero-dependency projects, alongside the existing ledger-denominator carve-out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
Two of 23 cases used view.text — a TS-web-presentation-only view subtype Python does not register — purely as incidental overlay content, so Python asserted only the documented ERR_UNKNOWN_SUBTYPE load failure instead of the full imported/selected/governed assertions the corpus defines. view.text appears in neither of the three pinned artifacts, only in two inline tree strings, so swapping it for view.currency (the one concrete view.* subtype registered cross-port per expected-registry.json) has zero effect on any pinned hash. That mattered because of which cases were exempted: the positive overlay case and the primary upstream-removal (ERR_OVERLAY_NO_TARGET) case — overlaying a dependency's node is the central authoring move this feature documents, and it was asserted by nothing on Python. Remove the now-unnecessary _VIEW_TEXT_NOT_REGISTERED_IN_PYTHON allowlist; both cases run normally. Both ports' runners pass (TS 24/24, Python 24/24) with no behavioural difference surfaced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
codegen-ts can't import sdk (the dependency runs the other way), so shared-model-file.ts redefines MANIFEST_FILE, ARTIFACT_SUFFIX and INTEGRITY_PREFIX locally, guarded only by a "Keep in sync" comment — the same shape as the drift that shipped earlier in this branch (a canonical template changed; its three owned copies went stale for ten tasks, caught only by the final gate). Export the three constants from shared-model-file.ts (re-exported, aliased, from the generators barrel) and add a cli test — cli is the one package depending on both sdk and codegen-ts — asserting byte-equality against sdk's canonical copies. Verified by mutation: changing ARTIFACT_SUFFIX's local value to ".metaobjects.json.bak" turns the test red; reverting turns it green again. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
…p ledger)
1. src/reference/entity.ts's ctx.select call site — the exact file
meta init copies into every project's codegen/generators/entity.ts,
the default entity-module path — had no end-to-end test. The
underlying renderSharedEnumsFile({select}) primitive was already
unit-tested (shared-enums-imported.test.ts), but nothing proved
runGen actually threads opts.scope through to ctx.select, or that
this file's own ternary reads it. Add an end-to-end runGen test
through the literal reference/entity.ts generator: a scope
excluding an enum's only consumer suppresses enums.ts; no scope
still materializes it.
2. No test covered "scope declared, zero dependencies" — the one
behaviour change this branch deliberately records in the
CHANGELOG (Collection.inScope composes matchesScope(fqn, scope)
regardless of imports, so scope.include narrows the requirements
ledger's denominator even with no dependencies at all).
verify-requirements-imported.test.ts only covered the WITH-a-
dependency half of the predicate. Add a zero-dependency fixture
(two of the project's own entities, no dependency anywhere) that
asserts scope.include narrows the ledger denominator from 1/2 to
1/1 and drops the excluded entity out of the count entirely.
Both mutation-verified: dropping ctx.select in the reference
generator, and dropping `coverable: collection.inScope` in verify.ts,
each turn their respective new test red; reverting turns them green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
FR-023 Phase 1a — metadata dependencies. One project publishes its typed metadata as a flattened, hash-pinned artifact; another declares it, syncs a committed snapshot, and builds on it — extending, overlaying and referencing its nodes — while being protected from silent upstream drift.
TypeScript and Python only. Java, Kotlin and C# do not read
dependenciesyet; that is Phase 2.What ships:
dependenciesin.metaobjects/config.json(pathtransport;npm/pythonrefused as deferred).meta deps syncvalidates the publisher's manifest, re-hashes its artifact, copies it to a committed snapshot under.metaobjects/deps/<name>/, and pins it in a sha256deps.lock.json.meta deps listreports the lock.dep:<name>/<artifact>source ids, so bases precede the declarations that amend them. Imported packages are excluded from every action surface — codegen, migrate, and the requirements ledger — unless ascope.includenames their package. One predicate, composed once and threaded through all three.ERR_DEPENDENCY_PACKAGE_NOT_OWNEDcatches a local node declared into a dependency's package, which the package-keyed exclusion would otherwise drop from its own project's output with no error.sharedModelFile()emits the artifact plus its manifest. Registered and discoverable, deliberately not ejectable — it produces a contract whose bytes consumers verify.meta deps checkandverify --depsre-hash the installed artifact and fail on divergence. An advisory overlay lint reports an unflagged cross-file redeclaration.meta initcleanup that stops scaffolding the dead v0.3package.meta.jsonand tracks the new dependency files.No metamodel change:
metamodelVersionstays1.0andexpected-registry.jsonis untouched.Checklist
metamodelVersionunchanged at1.0;expected-registry.jsonuntouched; the gates lane'smetamodel-version bumpcheck reports no diff.fixtures/dependency-conformance/) runs on both consuming ports, 24 assertions eachany(TS)docs/features/metadata-dependencies.mdplus seven existing pages, three agent-context skills (corpus regenerated), CHANGELOG and roadmapNotes for reviewers
Gate status. All three lanes green on this exact tree:
--only ts(build+typecheck, conformance, unit, mutation gate, two real-PG suites, integration),--only python(2098 + 110),--only gates(30/30).Process. Each of the 20 tasks (one CUT) was individually reviewed for spec compliance and quality; 19 fix rounds ran. A whole-branch review then found 1 Critical and 2 Important that per-task review structurally could not see, all fixed in one wave and re-verified.
One known residual, deliberately not fixed here.
meta docs --sitecan still fail for a project that both mixes a fresh declaration and an imported-node overlay in the same file — violating the documented "overlay in its own file" convention — and names a dependency that sorts after its own source directory alphabetically.docs-site's loader re-sorts source dirs alphabetically and only rescues overlay-only files. This is strictly narrower than before (that path was wholly broken for any dependency);genandverifyare unaffected, sinceCollection.filesunconditionally leads with artifacts. Two remedies, and the choice is a design call: harden the convention into a requirement, or makedocs-siteshareCollection.files' dependency-leads ordering.Four findings surfaced that are not this branch's to fix:
scripts/ci-local.shprintsLOCAL CI FAILED — N gate(s) redand exits 0. A genuinely red gate on this branch nearly passed as green; it was caught only by reading the summary text. Any automation trusting that exit status gets a false pass.--print-onlyassertions in the CLI tests usetoContain, which can detect a missing forecast entry but never a spurious one — the blind spot is directional. One such stale entry shipped and was found by review, not by the suite.bin/meta.tshas no.catch(), so any throw from a command surfaces as an unhandled rejection rather than a diagnostic and an exit code.Worth a reviewer's attention: the two-pass
migrate-tsschema partition (declaredSchemasderived from the remainder) and thedocs --sitesnapshot-dir resolution are the subtlest pieces. Theview.textcorpus exemption that earlier deferred two Python cases has been discharged — both ports now assert the same 24.🤖 Generated with Claude Code
https://claude.ai/code/session_01ApQWqotP1jaKs85XhKVXG4