Skip to content

feat(cli): add maven and nuget dependency transports to meta deps - #541

Merged
dmealing merged 5 commits into
mainfrom
fm/mo-deps-p2-transports
Nov 6, 2026
Merged

dmealing merged 5 commits into
mainfrom
fm/mo-deps-p2-transports

Conversation

@dmealing

@dmealing dmealing commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Design decision: declaration field names

The design doc does not settle them. Chosen, following the path/npm/python shape:

  • { "name": "x", "maven": "groupId:artifactId", "version": "1.0.0", "dir"?: "..." }
  • { "name": "x", "nuget": "Id", "version": "1.0.0", "dir"?: "..." }
  • version is required for these two transports and illegal beside the others. dir is allowed beside every transport except path; the default for maven and nuget is metaobjects.
  • The lock's resolvedFrom carries the declaration minus name (maven/nuget, version, optional dir), so check detects a version change as well as a content change.
  • The Java, C# and Python config and lock readers are strict about unknown keys, so they accept the new keys too. Their check reports such a dependency as unresolved and points at the Node meta deps check; per-port sync is out of scope.
  • The publishers write only the two files into an output directory. The adopter's build packs that directory at metaobjects/ in the jar or nupkg.

Gate runs

One no-mistakes run (with review, test and document fix rounds inside it). Two ask-user gates were escalated to the supervising orchestrator and answered there: the test-step agent timeout (the unvalidated help-text commit was then validated with the ts-unit lane) and the two release-tagged docs/llms mirrors, which are left for the release cut. Local lanes ts-fast, ts-unit and gates were green before the run.

What Changed

  • meta deps sync and meta deps check accept { "name", "maven": "groupId:artifactId", "version" } and { "name", "nuget": "Id", "version" }. They read the artifact and manifest from the package's metaobjects/ directory (dir overrides it) in the local Maven repository ($MAVEN_REPO_LOCAL, -Dmaven.repo.local, settings.xml, then $HOME/.m2/repository) or the NuGet global packages folder ($NUGET_PACKAGES, else $HOME/.nuget/packages). Neither downloads. A package missing from the cache fails naming the GAV or package id and the fetch command.
  • Adds lib/zip-read.ts, a minimal read-only ZIP reader (stored and deflate; refuses ZIP64, encryption and zip-slip entries) used to read jars and nupkgs.
  • Java, Kotlin, Python and C# config and lock readers accept the new keys. Their check reports such a dependency unresolved and points to the Node meta deps check. Per-port sync is not changed.
  • Shared dependency-conformance corpus gains 2 resolution cases and 18 transport cases. Node tests build a jar and a nupkg with the same layout as the publishers' output and cover current, drift and missing-package states.
  • Docs: transports rows in docs/features/metadata-dependencies.md and docs/features/cli.md; CHANGELOG entry under Unreleased.
Validation

Risk Assessment

✅ Low: The three round-1 fixes check out against the code: zip-slip guard now refuses \ and :, packagedDir normalizes and refuses escaping paths, and meta deps --help lists maven and nuget. The JDK-built jar fixture is an independent oracle for the reader. The Java, C# and Python reader changes match the Node declaration shape. I did not run tests, per the review rules.

Testing

Validated 88fe5a4 with the ts-unit lane only, as the operator directed. Live: drove meta deps --help through the real CLI entry. Help text interpolates PACKAGED_MODEL_DIR, so the default dir reads "metaobjects" with maven/nuget rows and cache sources. That scenario passed live. The ts-unit suite result is recorded as a regression check, not a live scenario, and is marked untested. The full ci-local run and the complete regression suite were not run because the operator narrowed scope to ts-unit; this is a gap the operator chose.

  • Live validation: ✅ go - 1 of 2 scenarios driven live against the product
Scenario Result Live Evidence
meta deps --help shows the packaged default dir from the PACKAGED_MODEL_DIR constant, with maven and nuget rows ✅ pass live run evidence (meta-deps-help.txt)
ts-unit suites pass with the no-hardcoded-metadata-dir guard green ⏸️ untested no Unit suite run is a regression check, not a live product scenario. The prior payload did not establish a live result for it, so it is untested here.
Evidence: meta deps --help live output
meta deps — sync a declared metadata dependency's committed snapshot

USAGE:
  meta deps sync [<name>...]   Resolve each declared dependency (path, npm, python, maven
                                or nuget transport), validate its manifest + artifact, copy it into .metaobjects/deps/<name>/,
                                and pin it in .metaobjects/deps.lock.json. Naming one or more
                                <name>s syncs only those; bare 'sync' syncs everything
                                declared. A dependency removed from config is PRUNED from
                                the lock and its snapshot dir deleted, every run.
  meta deps list                One line per locked dependency: name, version, hash8,
                                node count, packages.
  meta deps check                Re-resolve each declared dependency (any transport, same
                                as sync steps 1-2) and compare its INSTALLED artifact's hash
                                against .metaobjects/deps.lock.json. Read-only — never
                                touches the lock or the snapshot. Exits 1 if any dependency
                                has drifted or cannot be resolved (ERR_DEPENDENCY_UPSTREAM_DRIFT).

FLAGS:
  --dry-run              Plan and report; write nothing (sync only)
  --format <toon|json|text>   Output format (global flag; default toon off-TTY)
  --help, -h              Print this help

Transports: "path" is a directory relative to the config's directory; "npm" is an installed
npm package, resolved from the config's directory the way Node resolves it; "python" is an
installed Python package, found with importlib.util.find_spec by the project's interpreter
($METAOBJECTS_PYTHON, else $VIRTUAL_ENV, else ./.venv, else python3 — the interpreter used is
printed); "maven" is a "groupId:artifactId" jar read from the local Maven repository
($MAVEN_REPO_LOCAL, else -Dmaven.repo.local in $MAVEN_ARGS / $MAVEN_OPTS, else
<localRepository> in $HOME/.m2/settings.xml, else $HOME/.m2/repository); "nuget" is a package id
read from the NuGet global packages folder ($NUGET_PACKAGES, else $HOME/.nuget/packages).
"maven" and "nuget" REQUIRE "version" (illegal beside the other transports) and are never
downloaded: fetch first (mvn dependency:get, dotnet restore). "dir" names the shared-model
directory inside the package (default: the package root for npm and python, "metaobjects"
for maven and nuget). Install the package with your package manager first; sync copies its
shared model into the committed snapshot, so nothing else needs it installed.

The lock is the ONLY thing 'meta deps sync' writes to besides the snapshot directory —
sync never touches your own metadata files. 'meta deps check' compares the lock against
what is installed right now (the same check 'meta verify --deps' runs); 'meta deps sync'
only ever compares against what config DECLARES.
  • Outcome: 🔧 2 issues found → auto-fixed → no changes applied ✅ across 3 runs (3h9m39s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed ✅
  • ⚠️ server/typescript/packages/cli/src/lib/zip-read.ts:65 - The zip-slip guard splits entry names on '/' only. The extractor then writes each entry with join(target, ...name.split(&#39;/&#39;)) (dependency-transports.ts:333). On Windows, path.join also treats '&feat(codegen-csharp): non-TPH entity inheritance chain via DirectMappedParent #39; as a separator, and Windows is a supported platform: the docs describe python on Windows. Example: a jar or nupkg in the local cache holds the entry metaobjects/..\..\..\Users\me\x.json. That name passes the prefix check and the includes(&#39;..&#39;) check, because no '/'-separated segment equals '..'. writeFileSync then writes outside the temp directory. The intent requires that the reader refuse zip-slip, so this case breaks that requirement. Fix: in readZipEntries, also refuse a name that contains '&feat(codegen-csharp): non-TPH entity inheritance chain via DirectMappedParent #39; or a drive-letter colon. Better, add a final check in unpackZipDir that relative(target, dest) does not start with '..' and is not absolute, so every caller is covered.
  • ⚠️ server/typescript/packages/cli/src/index.ts:306 - The meta deps help text still says three transports exist: "path", "npm" and "python". It also says "dir" names the directory inside "an npm or python package (default: the package root)". The change does not touch this text. A user who runs meta deps --help does not see maven or nuget, the required version, the metaobjects default dir, or the fact that sync never downloads. The intent lists the CLI doc in scope, and docs/features/cli.md was updated, but this built-in help text was not. Add the maven and nuget rows to the help text, and also say where each one reads its cache from.
  • ℹ️ server/typescript/packages/cli/src/lib/dependency-transports.ts:319 - The comment on packagedDir says it applies "the same rules as applyDir's dir", but the rules differ. applyDir (npm and python) resolves the path, so &#34;dir&#34;: &#34;./shared&#34; and &#34;dir&#34;: &#34;a/../shared&#34; both work there. packagedDir only strips trailing slashes. For &#34;./shared&#34;, maven looks for the jar prefix ./shared/ and finds nothing, so it reports a misleading "has no &test(conformance): combined multi-package extends + overlay + cross-package references fixture #34;./shared/&test(conformance): combined multi-package extends + overlay + cross-package references fixture #34; directory" error. nuget behaves differently for the same value: its extracted-folder branch works through join, but its .nupkg fallback fails. a/../shared is refused outright. Fix: normalize with path.posix.normalize, strip a leading './', and refuse only a result that is '..', starts with '../' or is absolute.
  • ℹ️ server/typescript/packages/cli/test/deps-packaged-transports.test.ts:223 - Every jar and nupkg in the tests and in transport-cases.json comes from the test-only writeZip helper, written next to the reader. That helper always writes real sizes in the local header, never sets the data-descriptor bit (0x08), and writes no extra fields. So no test proves the reader handles archives that real tools produce. The JDK jar tool and many archivers use data descriptors with zeroed local sizes, and JarOutputStream puts a 0xCAFE extra field in the first local header. The reader looks correct for these cases, because it takes sizes from the central directory and reads the local extra length on its own. But a shared misreading of the spec would pass both helper and reader. Add one small committed fixture built by a real tool (jar/zip -r or dotnet pack) as an independent oracle. It would reject a reader that trusted the local-header sizes.

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Test** - 2 issues found → auto-fixed → no changes applied ✅
  • Resolved: the ts-unit lane failed once, on the guard against hard-coded metadata directory names, and passed after the fix below.
  • Resolved: meta deps --help hard-coded the packaged directory name, which the metadata-directory guard rejected. It now interpolates PACKAGED_MODEL_DIR, and the guard is green.
  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Configured command ts-unit lane passes after fix ⏸️ untested no Prior payload recorded a test-suite run (scripts/ci-local.sh --only ts-unit), not a live product interaction. Pass not supported as live; not counted as a live scenario.
meta deps --help documents maven and nuget transports, version requirement, default dir ✅ pass live bun server/typescript/packages/cli/bin/meta.ts deps --help
maven: declared GAV synced from local repo into lock and snapshot ✅ pass live meta deps sync in temp project with MAVEN_REPO_LOCAL pointing at jar-tool-built jar; lock records maven transport, version 1.0.0, integrity sha256-10fbf886...
maven: check reports current when jar matches lock ✅ pass live meta deps check -> acme-common,current,1.0.0
maven: republished jar with changed model reports drift ✅ pass live meta deps check -> drifted with ERR_DEPENDENCY_UPSTREAM_DRIFT
maven: missing jar fails clearly naming GAV and fetch command, never downloads ✅ pass live meta deps sync with empty MAVEN_REPO_LOCAL exits 2 with GAV, expected path, mvn dependency:get command, 'does not download'
nuget: package read from NuGet global packages cache (NUGET_PACKAGES) and checked ⏸️ untested no Not driven live: a nupkg was not built with a real tool here (dotnet pack/restore not attempted). Covered only by unit tests in test/deps-packaged-transports.test.ts with the test-only writeZip helper…
adversarial zip-slip: entry with backslash or drive colon refused ⏸️ untested no Not driven live: crafting a malicious jar needs a tool that writes backslash entry names, which the JDK jar tool does not allow. Code (cli/src/lib/zip-read.ts:66 guard) and unit-test coverage read onl…
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-unit --strict-toolchains (failed before fix: no-hardcoded-metadata-dir guard; passes after fix, LOCAL CI PASSED)
  • bun test test/no-hardcoded-metadata-dir.test.ts (sdk) - 5 pass
  • bun test test/help-and-exit.test.ts test/help-lists-every-flag.test.ts test/deps-packaged-transports.test.ts (cli) - 42 pass
  • bun test (server/typescript/packages/cli, full suite) - 1998 tests, 0 fail
  • bun server/typescript/packages/cli/bin/meta.ts deps --help - live: help lists maven and nuget transports, version requirement, default dir
  • Live maven scenario in disposable /tmp project with JDK jar-built jar at isolated MAVEN_REPO_LOCAL and HOME: meta deps sync writes lock (transport+version+integrity) and snapshot; meta deps check reports current
  • Live drift scenario: republished jar with widened model, meta deps check reports drifted and ERR_DEPENDENCY_UPSTREAM_DRIFT
  • Live missing-package scenario: empty MAVEN_REPO_LOCAL, meta deps sync exits 2 naming GAV com.acme:acme-common-model:1.0.0, expected jar path, and mvn dependency:get fetch command; states it does not download
  • Not run: full ts-fast lane (includes 30-min mutation gate). Another ci-local run (pid 3393490) was already active in this worktree; I did not start or kill it.

🔧 Fix applied.
3 issues were open at that point; all are resolved, as noted:

  • Resolved: the Test agent hit its time budget during the help-text fix round. The work it left (commit 88fe5a4) was then validated in a follow-up round with the ts-unit lane, which passed.
  • Resolved: approval was held back until that one unvalidated commit had been validated. It has been, and the run continued.
  • Resolved: meta deps --help hard-coded the packaged directory name, which the metadata-directory guard rejected. It now interpolates PACKAGED_MODEL_DIR, and the guard is green.
  • Live validation: ✅ go - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Configured command ts-unit lane passes after fix ⏸️ untested no Prior payload recorded a test-suite run (scripts/ci-local.sh --only ts-unit), not a live product interaction. Pass not supported as live; not counted as a live scenario.
meta deps --help documents maven and nuget transports, version requirement, default dir ✅ pass live bun server/typescript/packages/cli/bin/meta.ts deps --help
maven: declared GAV synced from local repo into lock and snapshot ✅ pass live meta deps sync in temp project with MAVEN_REPO_LOCAL pointing at jar-tool-built jar; lock records maven transport, version 1.0.0, integrity sha256-10fbf886...
maven: check reports current when jar matches lock ✅ pass live meta deps check -> acme-common,current,1.0.0
maven: republished jar with changed model reports drift ✅ pass live meta deps check -> drifted with ERR_DEPENDENCY_UPSTREAM_DRIFT
maven: missing jar fails clearly naming GAV and fetch command, never downloads ✅ pass live meta deps sync with empty MAVEN_REPO_LOCAL exits 2 with GAV, expected path, mvn dependency:get command, 'does not download'
nuget: package read from NuGet global packages cache (NUGET_PACKAGES) and checked ⏸️ untested no Not driven live: a nupkg was not built with a real tool here (dotnet pack/restore not attempted). Covered only by unit tests in test/deps-packaged-transports.test.ts with the test-only writeZip helper…
adversarial zip-slip: entry with backslash or drive colon refused ⏸️ untested no Not driven live: crafting a malicious jar needs a tool that writes backslash entry names, which the JDK jar tool does not allow. Code (cli/src/lib/zip-read.ts:66 guard) and unit-test coverage read onl…
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-unit --strict-toolchains (failed before fix: no-hardcoded-metadata-dir guard; passes after fix, LOCAL CI PASSED)
  • bun test test/no-hardcoded-metadata-dir.test.ts (sdk) - 5 pass
  • bun test test/help-and-exit.test.ts test/help-lists-every-flag.test.ts test/deps-packaged-transports.test.ts (cli) - 42 pass
  • bun test (server/typescript/packages/cli, full suite) - 1998 tests, 0 fail
  • bun server/typescript/packages/cli/bin/meta.ts deps --help - live: help lists maven and nuget transports, version requirement, default dir
  • Live maven scenario in disposable /tmp project with JDK jar-built jar at isolated MAVEN_REPO_LOCAL and HOME: meta deps sync writes lock (transport+version+integrity) and snapshot; meta deps check reports current
  • Live drift scenario: republished jar with widened model, meta deps check reports drifted and ERR_DEPENDENCY_UPSTREAM_DRIFT
  • Live missing-package scenario: empty MAVEN_REPO_LOCAL, meta deps sync exits 2 naming GAV com.acme:acme-common-model:1.0.0, expected jar path, and mvn dependency:get fetch command; states it does not download
  • Not run: full ts-fast lane (includes 30-min mutation gate). Another ci-local run (pid 3393490) was already active in this worktree; I did not start or kill it.

🔧 No changes applied.
✅ Re-checked - no issues remain.

  • Live validation: ✅ go - 1 of 2 scenarios driven live against the product
Scenario Result Live Evidence
meta deps --help shows the packaged default dir from the PACKAGED_MODEL_DIR constant, with maven and nuget rows ✅ pass live run evidence (meta-deps-help.txt)
ts-unit suites pass with the no-hardcoded-metadata-dir guard green ⏸️ untested no Unit suite run is a regression check, not a live product scenario. The prior payload did not establish a live result for it, so it is untested here.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • cd server/typescript/packages/cli && bun ./bin/meta.ts deps --help (live CLI run; output shows maven and nuget transports, dir default "metaobjects" for maven/nuget, and the $MAVEN_REPO_LOCAL / $NUGET_PACKAGES cache paths)
  • scripts/ci-local.sh --only ts-unit (run on commit 88fe5a46c, per operator guidance; reported LOCAL CI PASSED; not a live product scenario)
  • git status after testing: clean tree, no transient artifacts left in the worktree
🔧 **Document** - 2 issues found → auto-fixed ✅
  • ℹ️ docs/llms/llms.txt:66 - llms.txt and llms-full.txt still say Maven and NuGet transports are design only; they are release-tagged mirrors, so left for the release cut.
  • ℹ️ docs/llms/llms-full.txt:120 - Same stale 'design only' sentence in llms-full.txt; left with llms.txt for the release cut.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…(FR-023)

A shared model published as a jar or nupkg is now a declared dependency:
{ name, maven: groupId:artifactId, version, dir? } and { name, nuget, version, dir? }.
Resolution reads the local Maven repository (honouring MAVEN_REPO_LOCAL,
maven.repo.local and settings.xml) or the NuGet global packages folder
(NUGET_PACKAGES), never downloads, and names the GAV or package id plus the
fetch command when the package is missing. Java, C# and Python config and lock
readers accept the new keys.
…admap FR-023 line; llms mirrors left for release cut
…roadmap.md FR-023 line; README, CLI doc, metadata-dependencies and CHANGELOG already match the change.
@dmealing
dmealing merged commit 7c41344 into main Nov 6, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-deps-p2-transports branch November 6, 2026 10:07
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