Skip to content

Commit 881a7bd

Browse files
dmealingclaude
andcommitted
fix(cli): the stack-value guard sat at one door, and RELEASING still said 7
Both from the same independent review of the previous session's commits. **`meta agent-docs` still silently dropped an unknown `--server` value.** `assertKnownStackValues` was added to `initCommand`'s arg parse only, and `agent-docs` calls `init()` directly. Side by side: meta init --docs-only --server klingon → exit 2, names the value meta agent-docs --server klingon → exit 0, "Scaffolded … (11 files)", manifest records "servers": [] That is the worse of the two doors to miss: `index.ts` labels `agent-docs` the "canonical redirect target for all language ports", so the four non-TS ports were the ones using the unguarded one. And the failure does not degrade — a non-empty override array suppresses BOTH the prior manifest's stack and detection, so a dropped value inverts three statements in the artifact whose whole job is to orient an agent correctly. The guard now lives in `init()`, the shared function every door goes through, refusing before anything is written. `initCommand` keeps its own call so a bad flag stays a usage error (exit 2) rather than the exit 1 an init failure gets. Gated at both doors plus the programmatic one, proved by removing the shared guard. **`docs/RELEASING.md` still hardcoded Maven major 7 in three places**, one release before the cut that makes it 8. `663f67d87` consolidated the derivation into `scripts/maven-coordinate.mjs` and named the un-executed door as "the hardest of the four to catch: nothing executes it" — then left the document that instruction points at. It now states the offset as permanent, names `8.0.0` for the 1.0 cut explicitly, and says not to type a literal `7.` anywhere, because at the cut that asks Central for a version BELOW the last release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent e4dbede commit 881a7bd

3 files changed

Lines changed: 62 additions & 8 deletions

File tree

docs/RELEASING.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
> number when you do (standing policy since 0.24.5, replacing the version-parity rule of
1818
> 0.20.13):** a registry publishes only when it has a changed product file, and when it does it
1919
> adopts the **current shared `minor.patch`** — skipping the numbers it sat out. npm / PyPI /
20-
> NuGet on `0.<m>.<p>`, Maven on `7.<m>.<p>` (only the major differs, for historical continuity).
20+
> NuGet on `<M>.<m>.<p>`, Maven on `<M+7>.<m>.<p>` — only the MAJOR differs, and the offset is
21+
> permanent (`scripts/maven-coordinate.mjs` is the single derivation; every script imports it).
22+
> Concretely: npm `0.25.0` is Maven `7.25.0`, and npm `1.0.0` is Maven **`8.0.0`**. Do not type
23+
> a literal `7.` anywhere — at the 1.0 cut it asks Central for a version BELOW the last release.
2124
> Two carve-outs: the **14 npm packages still move atomically with each other** (they
2225
> cross-depend — that is intra-npm lockstep and it is unchanged), and a change to
2326
> `expected-registry.json` / `metamodelVersion` **forces all four**, because that is the
@@ -161,7 +164,8 @@ Publish in tier order so a dependent never lands before its dependency. **`forge
161164
displayed a coordinate yet.
162165

163166
So `release.mjs` pushes `main` and **stops**. After the ports are bumped, committed,
164-
pushed and tagged (`python-v*`, `csharp-v*`, `java-v7.*`), run:
167+
pushed and tagged (`python-v*`, `csharp-v*`, `java-v<npm major + 7>.*``java-v8.*` at the
168+
1.0 cut, NOT `java-v7.*`), run:
165169

166170
```bash
167171
bun scripts/finish-release.mjs <version> # gates, then tags and pushes
@@ -662,8 +666,8 @@ subsequent releases keyless.)
662666
# Releasing the Java/Kotlin modules to Maven Central
663667

664668
The 18 `com.metaobjects:*` modules ship to **Maven Central via the Sonatype Central Portal**,
665-
versioned on the `7.x` line (currently `7.25.0`) in the parent + module poms. Signed with the
666-
maintainer's GPG key.
669+
versioned on its own major line — npm major + 7, so `7.x` while npm is `0.x` and `8.x` from the
670+
1.0 cut (currently `7.25.0`) — in the parent + module poms. Signed with the maintainer's GPG key.
667671

668672
## Procedure
669673

server/typescript/packages/cli/src/commands/init.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,16 @@ async function writeConfigFile(opts: InitOptions, result: InitResult, agentDir:
603603
}
604604

605605
export async function init(opts: InitOptions): Promise<InitResult> {
606+
// Refused HERE, in the shared function, before anything is written — NOT at one CLI
607+
// entry point. It was at `initCommand`'s arg parse only, and `meta agent-docs` calls
608+
// this directly: `meta init --docs-only --server klingon` exited 2 with the message
609+
// while `meta agent-docs --server klingon` exited 0, reported "11 files", and recorded
610+
// `"servers": []`. That is the worse of the two doors to miss — `agent-docs` is the
611+
// canonical redirect target for every language port, so the ports used the unguarded
612+
// one. A non-empty override array suppresses both the prior manifest's stack AND
613+
// detection, so a dropped value does not degrade, it inverts three statements in the
614+
// generated context.
615+
assertKnownStackValues({ servers: opts.servers ?? [], clients: opts.clients ?? [] });
606616
const result: InitResult = { created: [], preserved: [], removed: [], warnings: [] };
607617
const agentDir = join(opts.cwd, DEFAULT_METAOBJECTS_DIR);
608618
const metaobjectsDir = join(opts.cwd, DEFAULT_METADATA_DIR);
@@ -1138,10 +1148,8 @@ export async function initCommand(args: string[], cwd: string): Promise<number>
11381148
let flags;
11391149
try {
11401150
flags = parseInitArgs(args);
1141-
// Refused HERE, before anything is written: a value that names nothing used to be
1142-
// dropped silently, and a non-empty override array then suppressed both the prior
1143-
// manifest's stack and detection — so the scaffolded context asserted an empty stack
1144-
// and a missing config about a project that had both. See `assertKnownStackValues`.
1151+
// Also here, so a bad value is a USAGE error (exit 2) rather than the exit 1 an
1152+
// init failure gets. `init()` refuses it too, which is what covers every other door.
11451153
assertKnownStackValues({ servers: flags.servers ?? [], clients: flags.clients ?? [] });
11461154
} catch (err) {
11471155
log.error((err as Error).message);

server/typescript/packages/cli/test/unit/stack-values.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,45 @@ describe("stack value validation", () => {
4545
expect(stack.servers).toContain("typescript");
4646
});
4747
});
48+
49+
// The guard has to sit in the SHARED function, not at one CLI entry point.
50+
//
51+
// It was at `initCommand`'s arg parse only, and `meta agent-docs` calls `init()` directly:
52+
// `meta init --docs-only --server klingon` exited 2 with the message, while
53+
// `meta agent-docs --server klingon` exited 0, reported "Scaffolded … (11 files)" and
54+
// wrote `"servers": []` into the manifest. That is the worse door to miss — index.ts
55+
// labels `agent-docs` the "canonical redirect target for all language ports", so the four
56+
// non-TS ports were the ones using the unguarded one.
57+
describe("every door refuses an unknown stack value", () => {
58+
test("init() itself refuses, so a programmatic caller cannot bypass it", async () => {
59+
const { init } = await import("../../src/commands/init.js");
60+
const { mkdtemp, rm } = await import("node:fs/promises");
61+
const { tmpdir } = await import("node:os");
62+
const { join } = await import("node:path");
63+
const dir = await mkdtemp(join(tmpdir(), "stack-guard-"));
64+
try {
65+
await expect(init({ cwd: dir, servers: ["klingon"], docsOnly: true })).rejects.toThrow(/klingon/);
66+
} finally { await rm(dir, { recursive: true, force: true }); }
67+
});
68+
69+
test("meta agent-docs refuses it too, and writes nothing", async () => {
70+
const { run } = await import("../../src/index.js");
71+
const { mkdtemp, rm, readdir } = await import("node:fs/promises");
72+
const { tmpdir } = await import("node:os");
73+
const { join } = await import("node:path");
74+
const dir = await mkdtemp(join(tmpdir(), "stack-guard-cli-"));
75+
const orig = console.error;
76+
const lines: string[] = [];
77+
console.error = (...a: unknown[]) => { lines.push(a.map(String).join(" ")); };
78+
try {
79+
const exit = await run(["agent-docs", "--server", "klingon", "--out", dir]);
80+
expect(exit).not.toBe(0);
81+
expect(lines.join("\n")).toContain("klingon");
82+
// Nothing scaffolded: the refusal lands before any write, as it does for init.
83+
expect(await readdir(dir)).toEqual([]);
84+
} finally {
85+
console.error = orig;
86+
await rm(dir, { recursive: true, force: true });
87+
}
88+
});
89+
});

0 commit comments

Comments
 (0)