Skip to content

docs: document v2.0 manifest contract#370

Merged
atilafassina merged 4 commits into
manifest-axfrom
manifest-docs
May 22, 2026
Merged

docs: document v2.0 manifest contract#370
atilafassina merged 4 commits into
manifest-axfrom
manifest-docs

Conversation

@atilafassina
Copy link
Copy Markdown
Contributor

Stacked on #261. Documents the v2.0 plugin manifest contract introduced there.

Summary

  • New: docs/docs/plugins/manifest.md — full v2.0 manifest reference: resources (per-type permissions), kind / cli discovery descriptors, dependsOn field ordering, post-scaffold steps, optional metadata.
  • Updated: docs/docs/development/templates.md — covers the synced template manifest's v2.0 additions: computed origin field (with precedence rules), scaffolding descriptor (command + flags + never / must rules), postScaffold propagation.
  • Updated: docs/docs/plugins/custom-plugins.md — recommends the import manifest from "./manifest.json" + static manifest pattern that all core plugins use, with a JSON-side example.

No code changes — docs only. Stacked-PR base will flip to main once #261 lands.

Test plan

  • pnpm docs:build — Docusaurus production build passes, no broken links.
  • pnpm check:fix — Biome clean.
  • pnpm -r typecheck — clean across all packages (docs included).
  • Spot-check rendered output once preview is live.

This pull request and its description were written by Isaac.

@atilafassina atilafassina marked this pull request as ready for review May 11, 2026 13:10
@atilafassina atilafassina requested a review from a team as a code owner May 11, 2026 13:10
@atilafassina atilafassina requested review from Copilot and pkosiec and removed request for a team May 11, 2026 13:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Documents the AppKit plugin manifest v2.0 contract and how it propagates into the synced template manifest used by databricks apps init, giving plugin authors a single reference for resources, discovery, and post-scaffold guidance.

Changes:

  • Added a new “Plugin manifest” reference page covering v2.0 fields, resource permissions, discovery descriptors, dependency ordering, and post-scaffold steps.
  • Updated the templates documentation to describe v2.0 synced-manifest additions (origin, scaffolding, postScaffold propagation).
  • Updated the custom plugins guide to recommend importing manifest.json and attaching it via static manifest (matching core plugin patterns).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
docs/docs/plugins/manifest.md New v2.0 plugin manifest contract reference (resources, discovery, dependencies, post-scaffold, optional metadata).
docs/docs/plugins/custom-plugins.md Updates basic example to use JSON-authored manifests imported into the plugin class.
docs/docs/development/templates.md Documents v2.0 additions to the synced template manifest (origin, scaffolding, and postScaffold propagation).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/docs/development/templates.md Outdated
Add docs/docs/plugins/manifest.md covering the v2.0 plugin manifest:
resources, kind/cli discovery descriptors, field dependencies,
transient prompts via `parents`, and plugin-level scaffolding rules.

Update templates.md to cover the v2.0 template manifest: computed
`origin` field, scaffolding descriptor + rules with substitutability
gate, and scaffolding.rules propagation from plugin manifests.

Update custom-plugins.md to recommend the manifest.json import pattern
matching the core plugins.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Per Copilot review on #370: the published template-plugins.schema.json
marks only `version` and `plugins` as top-level required. `scaffolding`
is enforced via Zod superRefine (conditional on version=2.0), which
JSON Schema cannot express. Reword both claims so readers validating
against the published schema alone are not misled.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Copy link
Copy Markdown
Member

@pkosiec pkosiec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread docs/docs/plugins/manifest.md
Comment thread docs/docs/development/templates.md Outdated
Comment thread docs/docs/plugins/manifest.md Outdated
Comment thread docs/docs/plugins/manifest.md Outdated
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Please take a look at the comments and address them before merging 🙏🏻

scaffolding.flags in templates.md still showed the pre-curation 3-flag
placeholder set (--template-dir, --config-dir, --profile). Replace with
three representative real flags plus a reference table covering all 10
canonical flags shipped by TEMPLATE_SCAFFOLDING.

lakebase scaffolding rules example in manifest.md still used a `must`
with Drizzle-specific wording, but the merged lakebase manifest.json
has no `must` and uses generic ORM language under `should`. Sync the
JSON example and the substitutability-gate prose example to match.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
@atilafassina atilafassina merged commit 40bb8a7 into manifest-ax May 22, 2026
@atilafassina atilafassina deleted the manifest-docs branch May 22, 2026 09:57
atilafassina added a commit that referenced this pull request May 22, 2026
* feat: extend plugin and template manifest schemas with discovery, postScaffold, and scaffolding

Xavier loop: iteration 1 — Phase 1 (Schema Definitions & Type Generation)

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* feat: add origin computation, scaffolding descriptor, and v2.0 template manifest emission

Xavier loop: iteration 2 — Phase 2 (Origin Computation & Sync Enrichment)

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* feat: add semantic validation for dependsOn cycles, discovery profile, and postScaffold

Xavier loop: iteration 3 — Phase 3 (Semantic Validation)

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* feat: annotate core plugin manifests with discovery descriptors and postScaffold steps

Xavier loop: iteration 4 — Phase 4 (Core Plugin Manifest Annotations)

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* fix: inline template schema resourceFieldEntry and resourceRequirement 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>

* chore: address code reviews

* chore: untrack .claude/scheduled_tasks.lock

Accidentally committed in a1c30e3; it's ephemeral Claude Code loop
state, not source. Flagged in PR #261 review.

Signed-off-by: Atila Fassina <atila@fassina.eu>

* feat: introduce zod-canonical manifest schema (phase 1)

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>

* refactor: switch validation runtime to standard schema (phase 2)

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>

* refactor: derive origin via zod transform (phase 3)

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>

* feat: reshape discovery as discriminated union (phase 4)

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>

* chore: delete legacy json-schema pipeline (phase 5)

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>

* feat: harden scaffolding directives (phase 6)

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&lt;typeof scaffoldingDescriptorSchema&gt;` 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>

* fix: dedupe zod resolution for json-schema generator

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>

* fix: address review findings — cli command syntax + lakebase parent + 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>

* chore: mark auto-generated artifacts as linguist-generated

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>

* fix: deny shell metacharacters on cli discovery descriptor

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>

* feat: cap postScaffold instruction at 200 chars

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>

* feat: replace postScaffold with plugin-level scaffolding.rules

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>

* feat: prune TEMPLATE_SCAFFOLDING.rules.must per substitutability gate

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>

* feat: model kind discovery parents for transient query inputs

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>

* chore(.claude): extend plugin commands with manifest v2.0 semantic checks

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>

* refactor: sharpen template scaffolding rules for skill conflict detection

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>

* chore: address review comments (#4, #5, #9)

- 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>

* fix: restore inherited-field descriptions on PluginManifest interface

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>

* fix: collapse restored JSDoc to single line for diff-clean docs regen

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>

* fix: align TEMPLATE_SCAFFOLDING flag required to false; resync template

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>

* chore: scrub PRD/phase leakage from source comments

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>

* fix: curate scaffolding.flags to match real CLI surface (#261)

Replace stale --template-dir / --config-dir entries with the canonical
databricks apps init flag set, derived from cmd/apps/init.go:

  --name (required, with kebab-case pattern), --template, --version,
  --features (with no-whitespace pattern), --set, --output-dir,
  --description, --run, --auto-approve, --profile

Excluded by design: --branch (niche GitHub-template only, mutually
exclusive with --version), --deploy (post-creation side effect to
shared workspace), --warehouse-id (deprecated), --plugins (hidden
alias for --features).

Addresses PR feedback on template/appkit.plugins.json scaffolding.flags
(thread r3282166233): --name is required by the scaffolder because
{{.projectName}} populates package.json, databricks.yml, and .env.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* docs: document v2.0 manifest contract (#370)

* docs: document v2.0 manifest contract

Add docs/docs/plugins/manifest.md covering the v2.0 plugin manifest:
resources, kind/cli discovery descriptors, field dependencies,
transient prompts via `parents`, and plugin-level scaffolding rules.

Update templates.md to cover the v2.0 template manifest: computed
`origin` field, scaffolding descriptor + rules with substitutability
gate, and scaffolding.rules propagation from plugin manifests.

Update custom-plugins.md to recommend the manifest.json import pattern
matching the core plugins.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* docs: clarify scaffolding requirement is CLI-enforced, not JSON-Schema

Per Copilot review on #370: the published template-plugins.schema.json
marks only `version` and `plugins` as top-level required. `scaffolding`
is enforced via Zod superRefine (conditional on version=2.0), which
JSON Schema cannot express. Reword both claims so readers validating
against the published schema alone are not misled.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

* docs: align manifest v2.0 docs with merged PR #261 schema

scaffolding.flags in templates.md still showed the pre-curation 3-flag
placeholder set (--template-dir, --config-dir, --profile). Replace with
three representative real flags plus a reference table covering all 10
canonical flags shipped by TEMPLATE_SCAFFOLDING.

lakebase scaffolding rules example in manifest.md still used a `must`
with Drizzle-specific wording, but the merged lakebase manifest.json
has no `must` and uses generic ORM language under `should`. Sync the
JSON example and the substitutability-gate prose example to match.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>

---------

Signed-off-by: Atila Fassina <atila@fassina.eu>

---------

Signed-off-by: Atila Fassina <atila@fassina.eu>
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.

4 participants