feat: new manifest format#261
Conversation
There was a problem hiding this comment.
Pull request overview
This PR upgrades the AppKit plugin manifest ecosystem to a v2.0 template manifest format to support smarter databricks apps init scaffolding, including field discovery metadata, post-scaffold user instructions, computed field origins, and semantic (cross-field) validation during plugin validate.
Changes:
- Bump template plugin manifest version to
2.0and add a top-levelscaffoldingdescriptor. - Extend plugin/template schemas with
discovery(CLI command-based value discovery) andpostScaffoldsteps; generate/propagate computedorigininto template manifests during sync. - Add semantic validation (dependsOn cycles/dangling refs,
<PROFILE>placeholder, discovery/origin coherence, postScaffold structure) and associated tests/docs updates.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| template/appkit.plugins.json | Updates template manifest to v2.0 and annotates built-in plugins with discovery/origin/postScaffold plus scaffolding descriptor. |
| packages/shared/src/schemas/template-plugins.schema.json | Expands template manifest schema to support v2.0 and scaffolding descriptor; inlines field/requirement defs for origin. |
| packages/shared/src/schemas/plugin-manifest.schema.json | Adds discovery to resource fields and postScaffold to plugin manifests. |
| packages/shared/src/schemas/plugin-manifest.generated.ts | Updates generated TS types to include discovery/postScaffold types. |
| packages/shared/src/plugin.ts | Re-exports new generated types (DiscoveryDescriptor, PostScaffoldStep). |
| packages/shared/src/cli/commands/plugin/validate/validate.ts | Runs semantic validation and formats semantic errors/warnings in CLI output. |
| packages/shared/src/cli/commands/plugin/validate/validate-manifest.ts | Implements semantic validation rules and issue formatting. |
| packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts | Adds test coverage for new semantic validation behavior. |
| packages/shared/src/cli/commands/plugin/sync/sync.ts | Computes/injects origin, bumps template manifest to v2.0, and adds scaffolding descriptor on write. |
| packages/shared/src/cli/commands/plugin/sync/sync.test.ts | Adds unit tests for origin computation. |
| packages/shared/src/cli/commands/plugin/manifest-types.ts | Adds scaffolding descriptor types and exports new manifest-related types. |
| packages/appkit/src/plugins/lakebase/manifest.json | Adds discovery descriptors and postScaffold steps to the lakebase built-in plugin manifest. |
| packages/appkit/src/plugins/genie/manifest.json | Adds schema reference, discovery descriptor, and postScaffold steps to genie plugin manifest. |
| packages/appkit/src/plugins/files/manifest.json | Adds discovery descriptor and postScaffold steps to files plugin manifest. |
| packages/appkit/src/plugins/analytics/manifest.json | Adds discovery descriptor and postScaffold steps to analytics plugin manifest. |
| docs/static/schemas/template-plugins.schema.json | Publishes updated template plugins schema for docs site. |
| docs/static/schemas/plugin-manifest.schema.json | Publishes updated plugin manifest schema for docs site. |
| docs/static/appkit-ui/styles.gen.css | Updates generated UI styles (tailwind output) used by docs UI. |
| docs/docs/api/appkit/Interface.ResourceFieldEntry.md | Documents the new discovery field on ResourceFieldEntry. |
| docs/docs/api/appkit/Interface.PluginManifest.md | Documents the new postScaffold field on PluginManifest. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@atilafassina this one looks ok and evolution, however little heavy code-wise (let me post comments from isaac about these) and mixing concepts little bit. What do you think of this: Alternatives worth weighing Zod as single source of truth (my default for a TS-first ecosystem):
TS-authored manifests compiled to JSON:
Split concerns into two files:
|
keugenek
left a comment
There was a problem hiding this comment.
Overall direction looks right — v2.0 versioning via if/then is correct and co-located manifest.json per plugin is the right locality. A few shape-level concerns worth addressing before or soon after this lands. Happy to chat on any of them.
f27eed4 to
a1c30e3
Compare
…tScaffold, and scaffolding Xavier loop: iteration 1 — Phase 1 (Schema Definitions & Type Generation) Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
…te manifest emission Xavier loop: iteration 2 — Phase 2 (Origin Computation & Sync Enrichment) Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
…, and postScaffold Xavier loop: iteration 3 — Phase 3 (Semantic Validation) Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
…ostScaffold steps Xavier loop: iteration 4 — Phase 4 (Core Plugin Manifest Annotations) Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
…t for origin support JSON Schema Draft-07 additionalProperties:false blocks allOf composition. Inlined both defs in template schema so origin validates correctly. Xavier loop: iteration 5 — Phase 5 (Integration & Backpressure) Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Add Zod schemas mirroring the existing plugin and template manifest JSON Schemas, the @standard-schema/spec dep that consumer code will use in phase 2, and a Zod→JSON Schema generator wired into build:package and generate:types. AJV continues to run in validate-manifest.ts; this is purely additive groundwork. Parity test uses fixture equivalence (Strategy B) — Zod 4's toJSONSchema emits per-type permission constraints as oneOf-of-discriminated-variants while the hand-written schema uses allOf+if/then over $defs/$ref, so byte parity is structurally infeasible. The test asserts AJV-with-legacy and Zod-with-new return matching accept/reject verdicts on the four core plugin manifests plus 5 synthetic plugin and 3 synthetic template fixtures (12 cases total). Build-pipeline byproducts of running pnpm build && pnpm docs:build cleanly are also captured: docs/static/schemas/plugin-manifest.schema.json loses a description field that copy-schemas.ts overwrote from the package-internal source (where the description was never present), and template/appkit.plugins.json gains origin enrichment on jobs.id and serving.name fields the parent PRD's enrichFieldsWithOrigin pass missed. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Replace AJV with Zod-via-Standard-Schema in validate-manifest.ts. The CLI validator now calls `~standard.validate` against the Zod schemas authored in phase 1; consumer code never imports zod directly. Cycle detection, dangling-reference checks, and the <PROFILE> placeholder constraint move from the standalone runSemanticValidation pass into Zod refinements co-located with the shape: - resourceRequirementSchema gains a superRefine running DFS over discovery.dependsOn — dangling refs emit at fields.<name>.discovery.dependsOn, cycles emit at the resource root with the existing 'a → b → c → a' chain. - discoveryDescriptorSchema.refine() enforces the <PROFILE> placeholder on cliCommand. - postScaffoldStepSchema.instruction tightens to z.string().min(1). origin-drift detection (validateDiscoveryOrigin) is dropped — origin becomes a transform in phase 3, eliminating the desync surface entirely. validate-manifest.ts shrinks from 498 to 177 lines: loadSchema, getPluginValidator, getTemplateValidator, the AJV compile cache, the JSON-pointer humanizer, the AJV error formatter, runSemanticValidation, validateDependsOn, validateDiscoveryProfile, validateDiscoveryOrigin, validatePostScaffold, and formatSemanticIssues all delete. Tests rewrite to drive validateManifest end-to-end and assert on the resulting SemanticIssue shape. validateManifest returns the original input object as `manifest` rather than result.output — Zod parsing is used purely as a verifier here so property order is preserved for round-trip writers like add-resource. Phase 3 will introduce the first real transform (origin), at which point output-vs-input distinction becomes intentional. ajv and ajv-formats remain in packages/shared/package.json for the phase-1 parity test (deletes in phase 5). Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
templateFieldEntrySchema gains a .transform() that computes origin from localOnly/value/resolve and emits it on every parse. origin is accepted as optional input but always overwritten — drift-by-construction is now structurally impossible. The new sync.test.ts case verifies this: a field with value: "5432" and stale input origin: "user" parses to "static". enrichFieldsWithOrigin and its mutation pass delete from sync.ts. The template manifest is now built by parsing each field through templateFieldEntrySchema before serialization. Per-field parse (rather than whole-manifest parse) is chosen because Zod 4's strict-object parse reorders keys aggressively, churning resource and plugin entries; per-field parse leaves the surrounding structure in input order. Sync output is byte-identical to phase 2 (md5 verified). computeOrigin and Origin type delete from manifest-types.ts and sync.ts. The replacement, computeOriginFromField, is private to manifest.ts and invoked only by the transform — nothing else in the codebase needs origin computation now that validateDiscoveryOrigin (deleted in phase 2) is gone. generate-json-schema.ts passes io: "input" to z.toJSONSchema so the transform doesn't break schema emission. The published JSON Schema describes what plugin authors write (no origin slot), not the transformed output — exactly the right semantic for IDE intellisense and external validators. template/appkit.plugins.json regenerated by build pipeline (pre-existing drift, not introduced here). Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Replace the free-form discoveryDescriptorSchema with a discriminated
union on `type`:
- kind variant: { type: "kind", resourceKind, select?, display?, dependsOn?, shortcut? }
resourceKind enum is the closed set of databricks resources AppKit owns
command templates for: warehouse, genie_space, postgres_branch,
postgres_database, volume.
- cli variant: { type: "cli", cliCommand, selectField, displayField?, dependsOn?, shortcut? }
preserves the existing free-form shape as an escape hatch for bespoke
resources not yet in the kind enum.
The <PROFILE> placeholder .refine() moves from the top-level descriptor
to the cli variant only — kind has no cliCommand to validate. The cycle
and dangling-reference DFS on resourceRequirementSchema reads
field.discovery?.dependsOn generically and continues to work across both
variants.
A typed RESOURCE_KIND_COMMANDS map ships next to the schema. Each entry
declares the CLI command template (with <PROFILE> placeholder + optional
{<fieldName>} placeholders for dependsOn substitution) and an optional
unwrap path for wrapped responses. Volume's catalog/schema parent
context is documented in a code comment as a phase 6 MUST-rule concern,
not a schema construct.
The four core plugin manifests migrate to the kind variant:
- analytics: { type: "kind", resourceKind: "warehouse" }
- genie: { type: "kind", resourceKind: "genie_space" }
- lakebase: branch → postgres_branch, database → postgres_database (dependsOn: "branch")
- files: { type: "kind", resourceKind: "volume", select: "full_name" }
Lakebase carries select: "name" (non-default for postgres_branch and
postgres_database); files carries select: "full_name". Defaults are kind-
specific identifiers and live in the command map / runner (out of scope).
The Zod-derived ResourceRequirement is a discriminated union (per-type
permission tightness baked in). Two consumer interface declarations in
packages/shared/src/plugin.ts and packages/appkit/src/registry/types.ts
previously did `interface ResourceRequirement extends GeneratedResourceRequirement` —
TS interfaces cannot extend union types. Both flatten to structural
interfaces with `permission: string`. This matches the previous
consumer-facing shape (legacy generated permission was already loose
string); per-variant tightness is enforced at schema parse time, where
it belongs. add-resource.ts's literal entry construction casts to
ResourceRequirement for the same reason.
manifest-types.ts re-export source switches from the legacy
plugin-manifest.generated to the canonical Zod schemas/manifest. The
generated.ts file is now orphaned (no source imports it) and deletes
in phase 5.
The phase 1 parity test (json-schema-parity.test.ts) deletes — it
asserted AJV-with-legacy-schema and Zod-with-new-schema return matching
verdicts on the four core plugin manifests, but those manifests now use
type: "kind" which the legacy schema doesn't understand. The test was
the phase-1 transition gate; once the contract intentionally diverges
it loses meaning.
template/appkit.plugins.json and docs/docs/api/appkit/* re-emitted by
the build pipeline.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Now that Zod is canonical and the validation runtime calls ~standard.validate, the legacy artifacts have nothing left to do. This phase deletes them and the build steps that produced them. Deleted: - packages/shared/src/schemas/plugin-manifest.generated.ts (orphaned since phase 4 switched manifest-types.ts re-exports to Zod). - packages/shared/src/schemas/plugin-manifest.schema.json, template-plugins.schema.json (legacy hand-written JSON Schema; Zod is now the source, JSON Schema is generated into docs/static/schemas by tools/generate-json-schema.ts). - tools/generate-schema-types.ts (JSON Schema → TS codegen, replaced by tools/generate-json-schema.ts going the other way). - docs/scripts/copy-schemas.ts (copied the legacy schemas to docs/static, now no-op). Dependencies removed from packages/shared: - ajv, ajv-formats — runtime validator gone in phase 2. - json-schema-to-typescript — codegen tool gone above. Build pipeline updated: - root package.json generate:types and packages/shared build:package scripts drop generate-schema-types.ts. - packages/shared/tsdown.config.ts drops the copy: block (the .json files no longer exist). - docs/package.json drops the copy-schemas script and removes it from the gen chain. - knip.json drops json-schema-to-typescript from ignoreDependencies. - .github/workflows/ci.yml `Check generated types are up to date` step drops the deleted plugin-manifest.generated.ts and adds docs/static/schemas/*.schema.json (now owned by generate-json-schema.ts). Source migrations to Zod: - schema-resources.ts: was reading plugin-manifest.schema.json at runtime to derive resource type options and per-type permissions. Now imports the per-type permission schemas and resourceTypeSchema from the Zod module and reads .options. No filesystem reads, no caching, no defensive null branches — values are module-level constants now. Public API preserved. - tools/generate-registry-types.ts: hidden consumer that also read the legacy JSON schema. Same migration. - packages/shared/src/cli/commands/plugin/manifest-types.ts: shrunk to a thin re-export shim of z.infer types and StandardSchemaV1. Type-level fix: - TemplatePlugin / TemplateResourceRequirement / TemplateFieldEntry / TemplatePluginsManifest type aliases switched to z.input instead of z.infer/z.output. The field-level origin transform makes origin REQUIRED on z.output, but consumer code (sync.ts) constructs template plugins without origin before writeManifest runs the transform at write time. z.input gives the pre-transform shape, matching the runtime invariant. Stale JSDoc references to GeneratedPluginManifest and plugin-manifest.generated.ts updated. The published JSON Schema URL is unchanged. Plugin authors' VSCode intellisense continues to work; the docs/static/schemas/*.json files are now byte-stable across runs (generated solely by the Zod-fed generate-json-schema.ts) and contain the new discriminated-union discovery shape. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Final phase of the manifest zod refactor. Three small, targeted changes to the scaffolding descriptor: 1. Rule items are constrained to ≤ 120 chars via z.string().max(120) on both rules.never[] and rules.must[]. The schema can't validate prose content but it can stop a directive from growing into a paragraph — enforces the "short directive" intent at the only boundary the schema can express. 2. TEMPLATE_SCAFFOLDING moves from sync.ts into the schema module. The constant lives next to the schemas it conforms to, with a `satisfies z.infer<typeof scaffoldingDescriptorSchema>` clause for compile-time validation against the input shape. sync.ts imports it. 3. New MUST rule directive describing volume parent-context handling: "When discovering volume resources, prompt the user for catalog and schema before listing volumes." The kind variant for `volume` doesn't model catalog/schema parents in the schema (per PRD design decision #7 — hierarchical context as MUST rule, not schema structure); this directive carries the requirement to LLM scaffolding agents instead. Tests added under "scaffolding rule item maxLength (Phase 6)": - never[]/must[] items exceeding 120 chars produce errors with the right path and message. - 120 chars exactly is accepted (≤ semantics). - A mixed-length array flags only the offending entry. - TEMPLATE_SCAFFOLDING parses cleanly against scaffoldingDescriptorSchema. - The synced template manifest carries the new volume MUST rule string. template/appkit.plugins.json regenerated by sync:template — the new rule string is now in scaffolding.rules.must. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
CI's `Check generated types are up to date` step was failing because two zod versions live in the workspace: - 4.1.13 hoisted at root (transitive via clean-app's eslint-plugin-react-hooks → zod-validation-error peer) - 4.3.6 in packages/shared (explicit dep) `tools/generate-json-schema.ts` imports zod from its own location, which resolves to root's 4.1.13. `packages/shared/src/schemas/manifest.ts` imports zod, resolving to shared's 4.3.6. The two runtimes operate on each other's schema objects, and the older zod's `toJSONSchema` doesn't extract all the constraints (pattern, minLength, propertyNames) that the newer zod baked into the schemas. CI's pnpm install resolves them consistently and emits the richer output, which then drifts from what's committed. Adding zod@4.3.6 as a root devDependency makes pnpm hoist the matching version to the top-level node_modules. The generator now resolves the same zod runtime as the schema module, and the JSON Schema output is byte-stable across local and CI. Regenerated docs/static/schemas/*.schema.json carry the now-emitted constraints (~135 minLength/pattern entries on plugin-manifest, similar on template-plugins). The constraints were always in the Zod schemas since phase 1 — they just weren't surviving the cross-version emit. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
… strict config Three real bugs flagged by the multi-model review of PR #261, fixed in this iteration. (CRITICAL cliCommand RCE hardening + HIGH z.lazy() perf deferred to follow-up PRs.) 1. RESOURCE_KIND_COMMANDS strings now match the real Databricks CLI (verified against v0.299.0 --help output): - `genie list` → `genie list-spaces` - `volumes list <catalog>.<schema>` → `volumes list {catalog} {schema}` (two separate positionals, prompted via the volume MUST rule) - `postgres list-branches` → `postgres list-branches {project}` with a project parent (covered by Fix 2 below) 2. Lakebase branch discovery is now actually runnable: - resourceKindSchema gains `postgres_project`. RESOURCE_KIND_COMMANDS gains the corresponding `databricks postgres list-projects` entry. - lakebase/manifest.json gains a new `project` field with `discovery: { type: "kind", resourceKind: "postgres_project", select: "name" }`. - The existing `branch` field's discovery adds `dependsOn: "project"`, so the parent project name flows into the branch listing command. 3. configSchemaPropertySchema and configSchemaSchema gain `.strict()`, so plugin config-schema typos no longer pass validation silently. `additionalProperties` (a standard JSON Schema keyword used by three core plugins — serving, vector-search, genie — inside nested property entries) is added explicitly as `z.union([z.boolean(), configSchemaPropertySchema]).optional()` so those manifests keep validating; this is a deliberate canonical addition, not a loosening of strict mode. Auto-regenerated by the build pipeline: - docs/static/schemas/{plugin-manifest,template-plugins}.schema.json - template/appkit.plugins.json Backpressure: typecheck=0, test=0 (108 files / 2136 tests), build=0, docs:build=0, knip=0, check:fix=0. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
GitHub collapses these in the diff view by default and excludes them from language stats. The files are emitted by the build pipeline and should not need reviewer attention. - docs/static/schemas/*.schema.json — emitted by tools/generate-json-schema.ts - template/appkit.plugins.json — emitted by pnpm sync:template - packages/appkit/src/registry/types.generated.ts — generate-registry-types.ts - packages/appkit/src/plugins/*-exports.generated.ts — generate-plugin-entries.ts - docs/docs/api/** — typedoc API reference - pnpm-lock.yaml — pnpm Reduces perceived PR size on this branch by ~10k lines (two regenerated JSON Schema files alone account for ~91% of insertions on PR #261). Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
The `cli` variant of discoveryDescriptor accepts a free-form Databricks
CLI command supplied by the plugin author. With no further constraint
beyond the existing `<PROFILE>` placeholder check, the shape is open to
two unrelated foot-guns reviewers flagged:
- output-shape brittleness: a plugin writes `selectField: ".id"` but the
CLI returns a wrapped object (e.g. `{warehouses: [...]}`); jq fails
silently at scaffold time
- shell-injection-if-executed: when an executor lands and passes the
string to a shell, statement separators / pipes / command substitution
/ redirects all become attack surface
The `kind` variant addresses both for first-party plugins (AppKit owns
the command and unwrap rules). The `cli` variant is the escape hatch for
third-party plugins that need bespoke commands. Tighten it cheaply now,
before anyone ships against the loose shape:
- new SHELL_METACHAR_RE blocks `;`, `|`, `&`, backtick, `$`, newlines on
both `cliCommand` and `shortcut`. Angle brackets are still permitted
so `<PROFILE>` (and future `<…>` placeholders) work.
- describes on cliCommand and the variant overall direct authors to use
`kind` for first-party resources and call out that the cli shape is
intentionally minimal and may tighten further.
Not a security boundary on its own — executors must still spawn(argv)
not shell-exec the string. argv-array form, denylist of shell operators
in argv, and an output-shape contract are all separate decisions tied
to the executor PR.
Two new test cases cover the two refinements (cliCommand + shortcut).
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
postScaffoldStepSchema.instruction was bounded below (.min(1)) but had no upper bound, while scaffolding.rules.must[] / never[] are capped at 120 chars per phase 6. Same intent applies to postScaffold instructions — they are checklist items, not prose — so add .max(200) with a parse-time error message. 200 (vs 120 for rules.must/never) allows short imperative sentences with placeholders; the longest existing core-plugin instruction is ~120 chars, so all current instructions fit with headroom. Regenerated docs/static/schemas/*.schema.json carry the new maxLength entry on the postScaffold step's instruction field. Two boundary tests added next to the existing "rejects empty postScaffold instruction" test (rejects 201, accepts exactly 200). Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
# Conflicts: # .github/workflows/ci.yml # docs/docs/api/appkit/index.md # docs/docs/api/appkit/typedoc-sidebar.ts # docs/package.json # knip.json # packages/appkit/src/plugins/genie/manifest.json # template/appkit.plugins.json
Delete postScaffold from canonical Zod schema and add plugin-level
scaffolding.rules ({must?, should?, never?}, ≤120 chars each) gated by the
substitutability principle. Adds parity should[] on template-level
scaffoldingRules. Migrates analytics, files, genie, lakebase manifests off
postScaffold per the A5 mapping table.
xavier loop iteration 1 — phase 1 of plugin-manifest-refactor amendment.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Apply the A4 audit from the 2026-05-18 manifest amendment. All 4 prior must[] entries are substitutable (skill content, derivable from requiredByTemplate, derivable from field.env, or belongs on volume discovery descriptor — A6 candidate for a later phase). Net result: must=[], should=[] (parity), never keeps the 3 cross-cutting guardrails. Regression test pins the shape. xavier loop iteration 2 — phase 2 of plugin-manifest-refactor amendment. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Extend RESOURCE_KIND_COMMANDS value shape with parents?: readonly string[].
parents declares free-text prompts the runner must collect before invoking
a kind's listing command — each entry substitutes the matching {name}
placeholder in the command template. Unlike dependsOn (which references a
sibling field), parents covers transient inputs not persisted as fields.
Applies to volume.parents = ['catalog', 'schema'], replacing the prose rule
deleted in the A4 audit. Regression tests pin volume's parents shape and
confirm no other kind currently uses parents.
A6 decisions (b) and (c) accept-as-prose — see ~/.xavier/tasks/plugin-manifest-refactor.md.
xavier loop iteration 3 — phase 3 of plugin-manifest-refactor amendment.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…ecks audit-core-plugin: Step 3.6 — substitutability-gate patterns (permission duplication, existence tautology, inactionable --set, enum-or, length cap); discovery descriptor completeness; RESOURCE_KIND_COMMANDS.parents vs dependsOn consistency. All findings feed Category 1 (Manifest Design). review-core-plugin: Step 5.6 — same gate/discovery checks scoped to changed manifest files only. create-core-plugin: Step 4e.1 — post-scaffold enrichment pass that adds discovery descriptors to user-supplied fields and gates scaffolding.rules against the substitutability principle with 5 reject patterns + 3 legitimate categories. No changes to plugin-best-practices.md or plugin-review-guidance.md reference docs — additions are procedural workflow steps, not narrative rules. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
…tion The original three never entries surfaced false-positive conflicts with plugin setup instructions during integration testing against the databricks-apps skill (Track B PR databricks/databricks-agent-skills#79): - 'Modify files inside the template directory' caught every plugin must rule that edits scaffolded files (genie spaces config, lakebase migrations, analytics queries). The rule was either unreachable (if read as the SDK source-of-truth) or contradictory (if read as the scaffolded output, which the user owns post-init). Deleted. - 'Hardcode workspace-specific values in template files' conflated workspace IDs (which are correctly committed to bundle config) with credentials (which must not be). Replaced with a must/never pair that names the legitimate destinations (app.yaml, databricks.yml, .env) and the leak path (client bundle). - 'Skip resource configuration prompts' conflicted with the --set non-interactive path. Replaced with a should/never pair covering the actual decision-time failures: ask the user when uncertain; never guess when discovery returns zero or multiple options. Net: 0 must + 0 should + 3 never -> 1 must + 1 should + 2 never. All entries describe agent behaviors at decision points (substitutability gate passes), each under 120 chars, and the merged set is precedence- and phase-clean per the skill PR's protocol. Regression test in validate-manifest.test.ts updated to pin the new contents. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
| "scaffolding": { | ||
| "rules": { | ||
| "must": [ | ||
| "Before init, ensure the SQL Warehouse passed via --set analytics.sql-warehouse.id is running" |
There was a problem hiding this comment.
how does the agent know how to do this? should we add some command?
There was a problem hiding this comment.
It has just used the CLI (databricks apps manifest) so it's right in their context to find the command. In my xp it's capable of that.
I'm torn between adding a CLI command in the manifest to make it more effectivebecause such a contract is not enforced anywhere. I'm assuming that CLI commands are sufficiently self discoverable.
| "scaffolding": { | ||
| "rules": { | ||
| "must": [ | ||
| "Before init, verify your Unity Catalog volume exists and you have WRITE_VOLUME permission" |
There was a problem hiding this comment.
same as the comment before, how does it know how to do it?
There was a problem hiding this comment.
It has just used the CLI (databricks apps manifest) so it's right in their context to find the command. In my xp it's capable of that.
I'm torn between adding a CLI command in the manifest to make it more effectivebecause such a contract is not enforced anywhere. I'm assuming that CLI commands are sufficiently self discoverable.
| $schema: | ||
| "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", | ||
| version: "1.1", | ||
| version: "2.0", |
There was a problem hiding this comment.
do we need to bump this to 2.0? is just incremental no? no breaking changes
There was a problem hiding this comment.
you're right if we only look at the schema, but the validation treats the some of the new props as required, so version 1.1 will fail 2.0 validation.
If we bump to 1.2 the strict way would be to make the validation looser.
| "--template-dir": { | ||
| "description": "Path to the template directory containing the app scaffold", | ||
| "required": true | ||
| }, | ||
| "--config-dir": { | ||
| "description": "Path to the output directory for the initialized app", | ||
| "required": true |
There was a problem hiding this comment.
these 2 flags are not required as far as I know no?
| { | ||
| "$schema": "https://databricks.github.io/appkit/schemas/template-plugins.schema.json", | ||
| "version": "1.1", | ||
| "version": "2.0", |
There was a problem hiding this comment.
same as above #261 (comment)
we can, but validation would need to be less strict.
| optional stability: "beta" | "ga"; | ||
| ``` | ||
|
|
||
| Plugin stability level. Beta plugins may have breaking API changes between minor releases but are on a path to GA. GA (general availability) plugins follow semver strictly. |
There was a problem hiding this comment.
why is this one removed?
There was a problem hiding this comment.
I think this was an accident when I split docs PR from this one. Let me revert it.
| "scaffolding": { | ||
| "rules": { | ||
| "must": [ | ||
| "After init, run database migrations via 'pnpm drizzle:migrate' before first request" |
There was a problem hiding this comment.
I don't think the template has this command in the scripts in package.json no? also it doesn't use pnpm
There was a problem hiding this comment.
replacing this wiht a package and ORM agnostic rule.
"run migrations". I'll also downgrade it to a should
- tools/generate-json-schema.ts, tools/generate-registry-types.ts: drop .ts extensions on relative imports — editors with TS LS configured without allowImportingTsExtensions flagged them as errors (per MarioCadenas on PR #261). tsx resolver accepts both forms; the rest of tools/ already imports without extensions. - lakebase/manifest.json: reword the migrations must rule. Previous text ('pnpm drizzle:migrate') referenced a script that doesn't exist in template/package.json AND a package manager (pnpm) the template doesn't use (template scripts use npm throughout). New text is ORM-agnostic: 'After init, run any database migrations for your chosen ORM before first request'. Preserves the don't-forget-migrations directive without presuming a tool the template doesn't ship. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Phase 5 deleted plugin-manifest.generated.ts (the json-schema-to-typescript output that carried JSDoc converted from Zod .describe() strings). The Omit<GeneratedPluginManifest, ...>-based PluginManifest interface in packages/shared/src/plugin.ts then inherited fields from z.infer<typeof pluginManifestSchema> — a computed type with no JSDoc to render. TypeDoc walks the TS surface (not the JSON Schema), so 10 inherited field descriptions silently vanished from docs/docs/api/appkit/Interface.PluginManifest.md. Redeclare the affected fields locally on PluginManifest with JSDoc copied verbatim from the Zod .describe() text. Restores the rendered descriptions in TypeDoc output. Drift risk acknowledged: if Zod .describe() changes, the JSDoc here stays stale until manually synced. A proper follow-up would either (a) re-emit JSDoc'd .d.ts from the regenerated JSON Schema as a codegen artifact pointed at by TypeDoc, or (b) adopt a Zod-aware TypeDoc plugin. Both out of scope for this PR. Addresses PR #261 review comment from MarioCadenas on docs/docs/api/appkit/Interface.PluginManifest.md. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Multi-line JSDoc comments produced multi-line description text in the TypeDoc-rendered markdown, which git diffed against the original single-line descriptions in main. Functionally identical; just whitespace. Collapsing onSetupMessage, hidden, and stability JSDoc to single-line matches the original markdown formatting verbatim. Net change vs main on docs/docs/api/appkit/Interface.PluginManifest.md is now just the See link swap (deleted plugin-manifest.generated.ts -> Zod source of truth pointer). Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
CI sync:template diff check caught two source/template drifts on 4ae1741: 1. lakebase manifest had been moved must -> should in the source, but the committed template still carried the rule under must. Resyncing now that sync:template is invoked as part of pnpm build. 2. TEMPLATE_SCAFFOLDING declared --template-dir and --config-dir as required: true, but the committed template had them as required: false (per MarioCadenas on PR #261 #6 — these flags aren't actually required by databricks apps init). Flipping the source constant to match; resync now produces consistent output. After this commit, source and template are in lockstep; CI sync:template diff check should pass. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Code comments should describe implementation details, not project-management state. Stripped references to phase numbers (Phase 1-6), the substitutability gate amendment, A4/A5/A6 audit candidates, and Track B PR cross-links from: - packages/shared/src/schemas/manifest.ts (header comment) - packages/shared/src/plugin.ts (re-export comment) - packages/shared/src/cli/commands/plugin/manifest-types.ts (shim header) - packages/shared/src/cli/commands/plugin/schema-resources.ts (header) - packages/shared/src/cli/commands/plugin/validate/validate-manifest.test.ts (describe/it names + inline comments) - tools/generate-json-schema.ts (header — dropped the phase narrative entirely) - tools/generate-registry-types.ts (header) Where the original prose carried an implementation fact worth keeping (transform overwrites input, parents replaces dependsOn for volume, strict object rejects postScaffold, etc.), the fact is preserved phase-free. Co-authored-by: Isaac Signed-off-by: Atila Fassina <atila@fassina.eu>
Summary
Evolves the AppKit plugin manifest contract from v1.0 to v2.0 and swaps the canonical authoring surface from hand-written JSON Schema to Zod via the Standard Schema interface. AJV, the
json-schema-to-typescriptcodegen pipeline, and the standalone semantic-validator delete; refinement and.transform()on Zod schemas replace them.Mid-review (2026-05-18) the design picked up a substitutability-gate amendment that deleted
postScaffoldentirely in favor of a unified plugin-levelscaffolding.rulesblock, and audited every prose rule against the gate. See "2026-05-18 amendment" below.Important
~91% of insertions are auto-regenerated artifacts — two JSON Schema files (
docs/static/schemas/{plugin-manifest,template-plugins}.schema.json) account for+10,400lines..gitattributesmarks all generated artifactslinguist-generated=true. GitHub collapses them in the file list — substantive review surface is ~32 files / ~2,300 insertions.What changes vs
mainContract
discoveryDescriptoris a discriminated union:{ type: "kind", resourceKind, ... } | { type: "cli", cliCommand, ... }. Six known kinds:warehouse,genie_space,volume,postgres_project,postgres_branch,postgres_database.clivariant rejects shell metacharacters (;,|,&, backtick,$, newlines) oncliCommandandshortcut..describe()notes direct authors tokindfor first-party resources and flag theclishape as minimal-and-may-tighten.RESOURCE_KIND_COMMANDSmap next to the schema; commands verified against Databricks CLI v0.299.0.volumekind carriesparents: ["catalog", "schema"]— declares free-text prompts the runner must collect before invoking discovery (replaces the prior prose rule).originon template fields is a.transform()output (drift-impossible).rules.must/rules.should/rules.neveritems capped atmaxLength: 120. Per-bucket and cross-bucket dedup enforced via.superRefine().projectfield.Authoring surface
packages/shared/src/schemas/manifest.ts(Zod schemas +RESOURCE_KIND_COMMANDS+TEMPLATE_SCAFFOLDING).tools/generate-json-schema.ts(Zod → draft-07 JSON Schema, emits todocs/static/schemas/).packages/shared:@standard-schema/spec.Deletions
ajv,ajv-formats,json-schema-to-typescript.packages/shared/src/schemas/{plugin-manifest.schema.json,template-plugins.schema.json,plugin-manifest.generated.ts}.tools/generate-schema-types.ts,docs/scripts/copy-schemas.ts.validate-manifest.ts(498 → 177 lines): AJV plumbing +runSemanticValidation+ helpers.enrichFieldsWithOrigin,validateDiscoveryOrigin.postScaffoldStepSchemaandpostScaffoldfield (post-amendment — see below).Migrated to Zod-direct
schema-resources.tsandtools/generate-registry-types.ts(no more runtime JSON-schema reads).manifest-types.tsis now a re-export shim (z.infertypes +StandardSchemaV1).Plugin Rules (per core plugin,
scaffolding.rules)--set analytics.sql-warehouse.idis runningconfig/queries/has at least one.sqlfile before runningnpm run typegenWRITE_VOLUMEpermission'spaces'map in plugin config with alias-to-Space-ID mappings'pnpm drizzle:migrate'before first request'psql $PGHOST -c "select 1"'Template-level scaffolding.rules
{ "must": [ "Keep all secrets and credentials only in app.yaml, databricks.yml, and/or .env" ], "should": [ "ask user when in doubt of resource to use for plugin" ], "never": [ "guess resources when multiple or no options are available", "embed secrets in files that will go to the client-bundle" ] }Each entry passes the substitutability gate (describes a cross-cutting agent behavior at decision time, not derivable from structured manifest data). Each ≤120 chars. Conflict-detection-safe against the current plugin rule corpus (no
must↔neveroverlaps).2026-05-18 amendment — substitutability gate
The original v2.0 plan shipped
postScaffoldarrays ({ instruction, required }) at the plugin level. During v2.0 implementation we found the format couldn't participate in thedatabricks-appsagent skill's command-construction flow — prose doesn't compose across plugins, duplicates work the skill drives from structured resources, and{ instruction, required }adds zero parseable signal.The amendment introduces one design principle and applies it uniformly:
Changes that landed under the amendment:
postScaffolddeleted.postScaffoldStepSchemaand thepostScaffoldfield removed from bothpluginManifestSchemaand the template plugin schema. All four core plugins migrated to plugin-levelscaffolding.rules.scaffolding.ruleswithmust/should/neverarrays (≤120 chars each, no duplicates within a bucket, no overlap across buckets — all enforced via.superRefine()).scaffoldingRulesSchemagainsshould[]?for parity with plugin-level.mustentries pruned (every one was substitutable: skill content, derivable fromrequiredByTemplate, derivable fromfield.env, or a discovery-descriptor concern). Original 3neverentries replaced with 2 sharper rules covering credentials hygiene and resource-selection decision points; one entry deleted as unreachable. See "Template-level scaffolding.rules" above.RESOURCE_KIND_COMMANDS.volume.parents = ["catalog", "schema"]. Models transient query inputs as a kind-level property without conflating withdependsOn(which references sibling fields).--setderivation for genie spaces → ACCEPT AS PROSE pending Track B confirmation of the skill's consumption model..claude/commands/skill extensionsOutside the
packages/tree but relevant to reviewers — the in-repo plugin commands gained procedural v2.0 semantic checks:audit-core-plugin.md— Step 3.6 (3.6a substitutability-gate patterns, 3.6b discovery descriptor completeness, 3.6cparentsvsdependsOnconsistency). All findings feed Category 1 (Manifest Design).review-core-plugin.md— Step 5.6 with the same checks scoped to changed manifest files only.create-core-plugin.md— Step 4e.1 post-scaffold enrichment that walks each user-supplied field to add adiscoveryblock and gates anyscaffolding.rulesagainst 5 explicit reject patterns.No changes to
plugin-best-practices.mdorplugin-review-guidance.md— the additions are procedural workflow steps, not narrative rules.Track B — consumer skill (cross-repo)
The
databricks-appsskill indatabricks/databricks-agent-skills#79("docs(apps): update AppKit scaffolding setup for Manifest 2.0") adds a new "Scaffolding Rules Protocol" subsection toskills/databricks-apps/SKILL.md(skill version bump0.1.1 → 0.1.2). It consumes the rules emitted by this PR:--featuresplugins + every plugin withrequiredByTemplate: true+ template-levelBefore init/After initprefixesmust↔ templateneverstops the flowSequencing: Track A (this PR) ships first. Track B is opened, not blocking merge — plugin-level rules are advisory metadata in the transition state. Failure mode if Track B lands later: degraded (third-party plugin guardrails don't fire), not broken (resources still drive the build).
End-to-end integration runbook lives in
~/.xavier/research/manifest-2-skill-integration-test.md(vault note, not in this repo).Deferred to follow-up PRs
defineManifest({ ... }))manifest-docs)dependsOnchecksclivariant full hardeningdatabricks-agent-skills#79— opens for skill author review; not required to merge before Track ANote
Plugin authors continue to write
manifest.json. See the docs PR (#370) for the authoring guide.Note on the generated files diff
z.toJSONSchema()has its own key ordering, and that's what now writes the file.$ref: "#/$defs/resourceRequirement"got expanded into giantoneOf: [...]blocks per resource type — the schema's actual shape changed, not just its layout.Biome's JSON formatter only normalizes whitespace; it doesn't reorder keys. So the noise you're seeing is the cost of moving from hand-authored JSON Schema → Zod-generated JSON.