From eb8ee5ff21207a4cb0cd4c46d47eac1cb419e645 Mon Sep 17 00:00:00 2001 From: Sheng Kun Chang Date: Wed, 6 May 2026 07:09:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(scaffold):=20match=20SDK=20v0.2.0=20?= =?UTF-8?q?=E2=80=94=20Slug,=20v0.1.0-dev,=20Need=20callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the module scaffold template so freshly-created modules pick up the v0.2.0 SDK contract end-to-end: - main.go.tmpl: add Slug: "__MS_SLUG__" between ID and Name. The token was already in scaffold.rs and substituted in go.mod / response bodies; just wasn't wired into ms.Init's Config. - main.go.tmpl: bump default Versions key from "v0.1.0" to "v0.1.0-dev". The -dev prerelease tag distinguishes "iterating locally" from "real release"; mirrorstack publish prompts to promote it before shipping. See docs/module-identity-and-storage-prefix.md. - main.go.tmpl: replace stale ms.DependsOn("oauth-core") doc-comment example with the v0.2.0 shape — @/@ spec plus the optional Need callback declaring n.Table / n.Event. - go.mod.tmpl: pin app-module-sdk to v0.2.0 so scaffolded modules resolve Need / OptionalDependOn / Config.Slug. - scaffold tests: assert each substitution lands and stale shapes are gone (Slug present, v0.1.0-dev replaces v0.1.0, DependsOn example matches v0.2.0, go.mod pins v0.2.0). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/commands/module/scaffold.rs | 55 +++++++++++++++++++++++++++++++++ templates/module/go.mod.tmpl | 2 +- templates/module/main.go.tmpl | 18 +++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/commands/module/scaffold.rs b/src/commands/module/scaffold.rs index d55e188..49d1aea 100644 --- a/src/commands/module/scaffold.rs +++ b/src/commands/module/scaffold.rs @@ -138,6 +138,50 @@ mod tests { assert!(!out.contains("__MS_MODULE_ID__")); } + #[test] + fn render_substitutes_slug_into_config_slug() { + // Slug field is the SDK v0.2.0 catalog handle. The scaffolded + // module needs it set so the manifest carries it from day one; + // missing-slug behavior (dev-only mode) is for hand-written + // pre-publish modules, not the CLI scaffold. + let out = render(MAIN_GO, &ins("oauth", "OAuth")); + assert!( + out.contains(r#"Slug: "oauth""#), + "rendered main.go missing Config.Slug substitution" + ); + } + + #[test] + fn render_versions_default_is_dev_prerelease() { + // Pre-1.0 dev builds use `v0.1.0-dev` so `mirrorstack publish` can + // refuse `-dev` versions in prod by convention. Promotion + // happens at publish time. See docs/module-identity-and-storage-prefix.md. + let out = render(MAIN_GO, &ins("media", "Media")); + assert!( + out.contains(r#""v0.1.0-dev": {App: "0001"}"#), + "expected v0.1.0-dev as scaffold default version" + ); + assert!(!out.contains(r#""v0.1.0": {App: "0001"}"#)); + } + + #[test] + fn render_dependson_example_matches_v0_2_shape() { + // The doc-comment example must reflect the v0.2.0 contract: + // owner-prefixed id with version constraint, plus the Need + // callback declaring tables/events. The pre-v0.2.0 bare-id + // example (`ms.DependsOn("oauth-core")`) misled scaffolded + // modules into a shape the catalog won't accept anymore. + let out = render(MAIN_GO, &ins("media", "Media")); + assert!( + out.contains(r#"ms.DependsOn("@anna/oauth@^1.0.0", func(n *ms.Need)"#), + "DependsOn example must use @/@ + Need callback" + ); + assert!( + !out.contains(r#"ms.DependsOn("oauth-core")"#), + "stale bare-id DependsOn example still present" + ); + } + #[test] fn render_substitutes_in_go_mod() { let out = render(GO_MOD, &ins("media", "Media")); @@ -145,6 +189,17 @@ mod tests { assert!(!out.contains("__MS_SLUG__")); } + #[test] + fn render_go_mod_pins_sdk_v0_2() { + // Scaffolded modules must link against SDK v0.2.0+ to get Need / + // OptionalDependOn — the template's main.go uses both. + let out = render(GO_MOD, &ins("media", "Media")); + assert!( + out.contains("github.com/mirrorstack-ai/app-module-sdk v0.2.0"), + "go.mod must pin SDK v0.2.0 to match scaffolded API surface" + ); + } + #[test] fn sanitize_module_id_strips_hyphens_and_prepends_letter() { assert_eq!(sanitize_module_id(SAMPLE_UUID), SAMPLE_SANITIZED); diff --git a/templates/module/go.mod.tmpl b/templates/module/go.mod.tmpl index 5d9890e..3c0a471 100644 --- a/templates/module/go.mod.tmpl +++ b/templates/module/go.mod.tmpl @@ -4,5 +4,5 @@ go 1.26 require ( github.com/go-chi/chi/v5 v5.2.5 - github.com/mirrorstack-ai/app-module-sdk v0.1.1 + github.com/mirrorstack-ai/app-module-sdk v0.2.0 ) diff --git a/templates/module/main.go.tmpl b/templates/module/main.go.tmpl index 74d65a8..1296674 100644 --- a/templates/module/main.go.tmpl +++ b/templates/module/main.go.tmpl @@ -29,11 +29,18 @@ func main() { // changing this. Renaming this WOULD require migrating every schema // and table whose name is derived from it. ID: "__MS_MODULE_ID__", + // Slug is the catalog handle (e.g. "oauth"). Mutable via catalog UI; + // the storage prefix at install time is __. Renames + // force a new published version with auto-generated rename migrations. + Slug: "__MS_SLUG__", Name: "__MS_NAME__", Icon: "extension", SQL: sqlFS, Versions: map[string]system.MigrationVersions{ - "v0.1.0": {App: "0001"}, + // `-dev` prerelease tag distinguishes "iterating locally" from + // "real release". `mirrorstack publish` prompts to promote it + // before shipping; CI / prod refuses `-dev` versions. + "v0.1.0-dev": {App: "0001"}, }, }); err != nil { log.Fatalf("mirrorstack: init failed: %v", err) @@ -42,8 +49,15 @@ func main() { ms.Describe("Replace this description with your module's purpose.") // Required deps go here — these become install-time preconditions. + // Use @/@ for the spec; the optional callback + // declares which relations and events this module reads from the dep. + // The catalog validates names at install time and surfaces them for + // app-owner approval before issuing GRANT SELECT. // - // ms.DependsOn("oauth-core") + // ms.DependsOn("@anna/oauth@^1.0.0", func(n *ms.Need) { + // n.Table("oauth_users") + // n.Event("user.signed_in") + // }) for _, hook := range postInitHooks { hook()