From 2c6d12ad09e2a3d1e7af61d58da7456e8eed1dd2 Mon Sep 17 00:00:00 2001 From: Travis Tidwell Date: Tue, 26 May 2026 16:08:13 -0500 Subject: [PATCH 1/3] Merging the formio-form skill with the formio-schema, and introduce submissions and projects to the schema. --- .../.openspec.yaml | 2 + .../design.md | 135 ++++ .../proposal.md | 37 ++ .../specs/claude-plugin-packaging/spec.md | 31 + .../specs/form-create/spec.md | 12 + .../specs/form-update/spec.md | 12 + .../specs/formio-schema-skill/spec.md | 78 +++ .../tasks.md | 104 ++++ .../.openspec.yaml | 2 + .../design.md | 120 ++++ .../proposal.md | 44 ++ .../specs/formio-schema-skill/spec.md | 124 ++++ .../tasks.md | 52 ++ .../.openspec.yaml | 2 + .../design.md | 113 ++++ .../proposal.md | 36 ++ .../specs/formio-schema-skill/spec.md | 115 ++++ .../tasks.md | 53 ++ .../specs/claude-plugin-packaging/spec.md | 13 +- openspec/specs/form-create/spec.md | 5 +- openspec/specs/form-update/spec.md | 5 +- openspec/specs/formio-schema-skill/spec.md | 202 ++++++ .../src/__tests__/form_create.test.ts | 3 +- .../src/__tests__/form_update.test.ts | 3 +- .../__tests__/formio-schema-layout.test.ts | 400 ++++++++++++ .../src/__tests__/plugin-build.test.ts | 10 +- packages/mcp-server/src/tools/form_create.ts | 2 +- packages/mcp-server/src/tools/form_update.ts | 2 +- plugin/README.md | 3 +- plugin/skills/formio-api/SKILL.md | 2 +- .../formio-api/references/project-forms.md | 2 +- plugin/skills/formio-form/SKILL.md | 583 ------------------ .../skills/formio-resource-planner/SKILL.md | 4 +- plugin/skills/formio-schema/SKILL.md | 64 +- .../references/{ => form}/base-component.md | 0 .../references/{ => form}/data-components.md | 0 .../references/{ => form}/form-definition.md | 0 .../references/{ => form}/input-components.md | 0 .../{ => form}/layout-components.md | 0 .../references/project/project-access.md | 71 +++ .../references/project/project-definition.md | 76 +++ .../references/project/project-settings.md | 56 ++ .../project/project-type-and-framework.md | 76 +++ .../submission/submission-access.md | 55 ++ .../references/submission/submission-data.md | 113 ++++ .../submission/submission-definition.md | 62 ++ .../submission/submission-metadata.md | 53 ++ .../references/submission/submission-state.md | 49 ++ scripts/test-plugin.ts | 2 +- 49 files changed, 2363 insertions(+), 625 deletions(-) create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/.openspec.yaml create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/design.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/proposal.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/claude-plugin-packaging/spec.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-create/spec.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-update/spec.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/formio-schema-skill/spec.md create mode 100644 openspec/changes/archive/2026-05-26-merge-form-schema-skills/tasks.md create mode 100644 openspec/changes/archive/2026-05-26-project-schema-reference/.openspec.yaml create mode 100644 openspec/changes/archive/2026-05-26-project-schema-reference/design.md create mode 100644 openspec/changes/archive/2026-05-26-project-schema-reference/proposal.md create mode 100644 openspec/changes/archive/2026-05-26-project-schema-reference/specs/formio-schema-skill/spec.md create mode 100644 openspec/changes/archive/2026-05-26-project-schema-reference/tasks.md create mode 100644 openspec/changes/archive/2026-05-26-submission-schema-reference/.openspec.yaml create mode 100644 openspec/changes/archive/2026-05-26-submission-schema-reference/design.md create mode 100644 openspec/changes/archive/2026-05-26-submission-schema-reference/proposal.md create mode 100644 openspec/changes/archive/2026-05-26-submission-schema-reference/specs/formio-schema-skill/spec.md create mode 100644 openspec/changes/archive/2026-05-26-submission-schema-reference/tasks.md create mode 100644 openspec/specs/formio-schema-skill/spec.md create mode 100644 packages/mcp-server/src/__tests__/formio-schema-layout.test.ts delete mode 100644 plugin/skills/formio-form/SKILL.md rename plugin/skills/formio-schema/references/{ => form}/base-component.md (100%) rename plugin/skills/formio-schema/references/{ => form}/data-components.md (100%) rename plugin/skills/formio-schema/references/{ => form}/form-definition.md (100%) rename plugin/skills/formio-schema/references/{ => form}/input-components.md (100%) rename plugin/skills/formio-schema/references/{ => form}/layout-components.md (100%) create mode 100644 plugin/skills/formio-schema/references/project/project-access.md create mode 100644 plugin/skills/formio-schema/references/project/project-definition.md create mode 100644 plugin/skills/formio-schema/references/project/project-settings.md create mode 100644 plugin/skills/formio-schema/references/project/project-type-and-framework.md create mode 100644 plugin/skills/formio-schema/references/submission/submission-access.md create mode 100644 plugin/skills/formio-schema/references/submission/submission-data.md create mode 100644 plugin/skills/formio-schema/references/submission/submission-definition.md create mode 100644 plugin/skills/formio-schema/references/submission/submission-metadata.md create mode 100644 plugin/skills/formio-schema/references/submission/submission-state.md diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/.openspec.yaml b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/.openspec.yaml new file mode 100644 index 0000000..45b6a2c --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-tdd +created: 2026-05-26 diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/design.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/design.md new file mode 100644 index 0000000..5b35cb0 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/design.md @@ -0,0 +1,135 @@ +## Context + +The skills library currently ships two near-overlapping skills covering the Form.io form JSON schema: + +- `plugin/skills/formio-form/SKILL.md` — a single 584-line monolithic skill documenting form, base component, input components, layout components, and data components in one file. +- `plugin/skills/formio-schema/SKILL.md` + `plugin/skills/formio-schema/references/*.md` — a router skill that points at five reference files: `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, `data-components.md`. Its content is functionally a re-cut of `formio-form` into smaller files. + +Both skills' frontmatter `description` fires on the same triggers ("Form.io form JSON", `components`, etc.), so Claude routinely activates the wrong one or both. Whichever fires, the user sees roughly the same information. Maintaining both means every schema fix lands twice (or, more often, once and silently drifts). + +Other parts of the codebase reference `formio-form` by name in tool descriptions, validator tests, plugin packaging, and other skills: + +- MCP tools: `packages/mcp-server/src/tools/form_create.ts`, `packages/mcp-server/src/tools/form_update.ts`. +- Tests: `packages/mcp-server/src/__tests__/form_create.test.ts`, `form_update.test.ts`, `plugin-build.test.ts`. +- Smoke test: `scripts/test-plugin.ts`. +- Skills: `plugin/skills/formio-api/SKILL.md` (negative-trigger clause), `plugin/skills/formio-api/references/project-forms.md`, `plugin/skills/formio-resource-planner/SKILL.md` (two cross-references). +- Plugin docs: `plugin/README.md` skill table. +- Existing specs: `openspec/specs/form-create/spec.md`, `openspec/specs/form-update/spec.md`, `openspec/specs/claude-plugin-packaging/spec.md`. + +Separately, the user wants `formio-schema` to grow to cover other Form.io JSON schemas — submissions and projects — not just form definitions. Action JSON is explicitly out of scope here because the dedicated `formio-actions` skill already owns it; role JSON is also out of scope because the role object is shallow enough that the `formio-api` `project-roles` reference is sufficient. The current flat `references/` layout puts form-specific files at the top level (`form-definition.md`, `base-component.md`, etc.), which would collide as soon as we add, say, a `submission-definition.md` or a separate `base-component.md` for a different domain. + +## Goals / Non-Goals + +**Goals:** + +- Eliminate the duplicate `formio-form` skill. One skill, `formio-schema`, owns every Form.io JSON schema reference. +- Lay out `formio-schema/references/` so each schema domain owned by the skill (form, submission, project) lives in its own subdirectory. Action JSON and role JSON are explicitly out of scope and have no subdirectory under `references/` — `formio-actions` covers actions and `formio-api`'s `project-roles` covers roles. Adding a new domain in the future is an additive change to the directory tree and the SKILL.md router table. +- Preserve all current technical content. No reference property tables are dropped during the merge — the existing five `formio-schema/references/*.md` files are the canonical content, and any net-new property docs that exist in `formio-form/SKILL.md` but not in `formio-schema/references/*.md` get merged in (verified file-by-file during implementation). +- Update every cross-reference in the repo so nothing still points at `formio-form`. +- Keep the change purely structural and documentary. No MCP server behavior changes. + +**Non-Goals:** + +- Authoring the actual submission / action / project / role schema reference content. This change creates the *directory structure* and placeholder README files only; filling those references is follow-up work. +- Adding schema-skill validation to `packages/mcp-server/src/skills-validator.ts`. The validator currently inspects only `formio-api`; extending it to enforce the new `formio-schema` layout is out of scope and would be its own proposal. +- Changing MCP tool inputs/outputs. Only the tool *description strings* change (skill name swap). +- Backwards-compatibility aliases. There is no shim mapping old `formio-form` references to `formio-schema`; the change is a hard rename per CLAUDE.md ("No backward compatibility unless explicitly requested"). + +## Decisions + +### Decision: Partition `references/` by schema domain, one subdirectory per domain + +`plugin/skills/formio-schema/references/` becomes: + +``` +references/ + form/ + form-definition.md + base-component.md + input-components.md + layout-components.md + data-components.md + submission/ + README.md # placeholder describing future content + project/ + README.md # placeholder describing future content +``` + +Action JSON is intentionally absent — `formio-actions` already owns the action schema. Role JSON is also intentionally absent — the role object is shallow (`title`, `description`, `admin`, `default`, `machineName`) and the `formio-api` `project-roles` reference is sufficient on its own. + +**Why subdirectories, not filename prefixes:** A prefixed flat layout (`form-base-component.md`, `submission-shape.md`) leaks the domain into every filename, makes the directory listing hard to scan, and gets ugly fast if any domain needs more than three or four references. Subdirectories give each domain its own namespace, so `base-component.md` can mean different things in `form/` vs. (hypothetically) some other domain. + +**Why ship placeholder `README.md`s for the empty domains:** The user explicitly asked the restructure to "leave room for other schema definitions." An empty directory git-ignores away; a `README.md` placeholder documents intent, is discoverable when someone greps the skill, and tells Claude where to start when those domains are actually authored. Each placeholder includes a "Not yet authored — refer to the `formio-api` skill in the meantime" note so a user who navigates there gets actionable guidance. + +**Alternatives considered:** + +- **Flat layout with `domain-` prefix.** Rejected — see above. +- **Keep current flat layout, just rename `form-definition.md` → `submission-definition.md` later.** Rejected — assumes every future domain has only one file. Form already has five. +- **One subdirectory per file group, no placeholders.** Rejected — the user explicitly wants visible "room" for future schemas, and empty directories don't survive git. + +### Decision: Merge `formio-form/SKILL.md` content into `formio-schema/references/form/*.md` before deletion + +The current `formio-schema/references/*.md` files are the merge target. During implementation, walk through `formio-form/SKILL.md` section by section against the corresponding `formio-schema/references/*.md` file. If a property table row, validation rule, or component sub-section exists in `formio-form` but not in `formio-schema`, port it over. The two skills *should* be near-identical content-wise, but the merge step is the safety net against silently dropping a property. + +**Alternative considered:** Delete `formio-form` outright and trust `formio-schema` as-is. Rejected — even one missing property would be a regression. The merge step costs one careful read-through and a diff; the cost of a silent drop is open-ended. + +### Decision: Broaden `formio-schema/SKILL.md` description to cover project / form / submission JSON + +Today the description fires on form-building concepts. After the change, the description must fire on project, form, and submission JSON shapes — but not on actions (owned by `formio-actions`) and not on roles (handled by `formio-api`'s `project-roles`). The new description follows the three-clause template used by `formio-api`: + +1. **Capability statement** — "Form.io JSON schema reference covering the document shapes for projects, forms (and resources), and submissions." +2. **Trigger clause** — "Use when the user asks to construct, edit, or interpret any Form.io project / form / submission JSON — form components, submission payloads, or project templates." +3. **Negative-trigger clause** — "Not for: calling Form.io REST endpoints (see `formio-api`); configuring server-side actions on a form (see `formio-actions`); planning a new app's resource model (see `formio-resource-planner`); orchestrating an entire app build (see `formio-application`)." + +The negative-trigger clause stops mentioning `formio-form` because that skill is gone, and adds `formio-actions` so action-related prompts route there instead. + +### Decision: SKILL.md router table becomes two-column "domain → reference" + +Replace the current "Working on… / Load" single-column table with a domain-grouped layout: + +```markdown +### Form definitions + +| Working on… | Load | +| ---------------------------------------------------------- | -------------------------------------- | +| Top-level form properties (`title`, `path`, `display`, …) | `references/form/form-definition.md` | +| Properties shared by all components | `references/form/base-component.md` | +| A specific input field (textfield, number, select, …) | `references/form/input-components.md` | +| Visual layout containers (panel, columns, tabs, …) | `references/form/layout-components.md` | +| Nested or repeatable data (container, datagrid, …) | `references/form/data-components.md` | + +### Submissions and Projects + +| Domain | Status | Load | +| ---------- | ----------------------------------------------------------------------------- | --------------------------------- | +| Submission | Not yet authored — use `formio-api`'s `runtime-submissions` reference for now | `references/submission/README.md` | +| Project | Not yet authored — use `formio-api`'s `platform-projects` reference for now | `references/project/README.md` | +``` + +A short prose paragraph beneath the second table tells the reader that action configs live in the dedicated `formio-actions` skill and that role objects are handled by `formio-api`'s `project-roles` reference — so neither is a domain of this skill. + +This makes the "leave room" intent visible at the top of the skill and gives Claude a clear fallback for unauthored domains. + +### Decision: Tool description strings rename `formio-form` → `formio-schema` verbatim + +`form_create.ts` and `form_update.ts` currently say "use the formio-form skill". They will say "use the formio-schema skill". No semantic change beyond the name swap. The specs for those tools (`form-create`, `form-update`) are updated under `## MODIFIED Requirements` with the full requirement text re-pasted with the new skill name. + +### Decision: Plugin packaging contract drops `formio-form` and keeps `formio-schema` + +`claude-plugin-packaging` spec lists both today. The MODIFIED delta replaces both occurrences with `formio-schema` only. `scripts/test-plugin.ts` asserts on `formio-schema` (it currently asserts on `formio-form`). + +## Risks / Trade-offs + +- **Risk:** A property documented only in `formio-form/SKILL.md` is dropped during the merge. → **Mitigation:** Walk both files side by side during implementation; verify each component type's property table from `formio-form` exists in the corresponding `formio-schema/references/*.md`. The implementation task list calls this out as an explicit check before deleting `formio-form/`. +- **Risk:** A consumer outside this repo (downstream docs, blog posts, internal tickets) references `formio-form` by name and breaks. → **Mitigation:** This is a documentation skill, not a public API. The plugin ships as `@formio/ai`; consumers interact via the MCP server and Claude's skill activation, which both use the new name once we update the description strings. We accept the rename per the "no backwards compatibility" rule in CLAUDE.md. +- **Risk:** Claude activates `formio-schema` less reliably for plain "build me a form" prompts because the description now spans more domains. → **Mitigation:** The trigger clause keeps the existing form-builder phrases ("components", "wizard", "textfield", etc.) alongside the new domain triggers. If activation precision drops, follow-up work can tune the description; this proposal does not commit to specific precision targets. +- **Risk:** Placeholder `README.md` files in `submission/`, `action/`, `project/`, `role/` get ignored and never filled. → **Mitigation:** This is acceptable. The placeholders' job is to signal intent and route the user to `formio-api` in the meantime. Authoring those references is explicitly out of scope for this change. +- **Trade-off:** This change touches both the skill content and the tool descriptions in one proposal. Splitting it would mean two sequential merges where one suffices, at the cost of one larger review. + +## Migration Plan + +1. Implement the directory restructure and content merge on a feature branch. +2. Update all cross-references in tools, tests, scripts, and other skills in the same commit. +3. Update the OpenSpec change archive when the work merges; existing specs adopt the MODIFIED deltas. +4. No runtime migration is required — there is no persisted state. The next time a Claude Code user pulls the plugin update, they get the new skills layout. +5. Rollback: revert the merge commit. The previous `formio-form` directory and `formio-schema` flat layout reappear unchanged. diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/proposal.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/proposal.md new file mode 100644 index 0000000..f8de486 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/proposal.md @@ -0,0 +1,37 @@ +## Why + +The `formio-form` and `formio-schema` skills duplicate the same Form.io form JSON reference content — `formio-form` ships it as one monolithic `SKILL.md`, while `formio-schema` ships a near-identical router plus split reference files under `references/`. Two skills with overlapping triggers means Claude routinely activates the wrong one (or both), and any future update has to be mirrored in both places. At the same time, Form.io has more JSON schemas worth documenting (submissions, actions, projects, roles), and the current `formio-schema` layout assumes "form definition" is the only domain — there is no room to add other schemas without further muddling the structure. + +## What Changes + +- **BREAKING** Delete the `formio-form` skill entirely. All of its content folds into `formio-schema`. +- Restructure `plugin/skills/formio-schema/references/` from a flat list keyed to form-only concepts into a domain-partitioned tree, with one subdirectory per schema domain the skill will own: + - `references/form/` — current `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, `data-components.md`. + - `references/submission/`, `references/project/` — created as placeholders (each containing a single `README.md` describing what the domain will document and an explicit "Not yet authored — refer to the `formio-api` skill in the meantime" note). + - Action configs are deliberately out of scope — they belong to the dedicated `formio-actions` skill, so no `references/action/` subdirectory is created. Role schemas are similarly out of scope — the role object is shallow enough that `formio-api`'s `project-roles` reference is sufficient on its own. +- Update `plugin/skills/formio-schema/SKILL.md`: + - Broaden the `description` so triggers fire for project, form, and submission JSON shapes — not only form-builder concepts. + - Replace the "When to load which reference" table with a two-level guide: pick a domain first, then pick a reference inside that domain. + - Update the negative-trigger / overlap clause to reflect that `formio-form` no longer exists and to route action work to `formio-actions`. +- Update MCP tool descriptions (`form_create`, `form_update`) and dependent skill references (`formio-api`, `formio-resource-planner`, plugin README) to point at `formio-schema` instead of `formio-form`. +- Update the plugin packaging contract so `formio-form` is no longer a required bundled skill, and so the `test-plugin.ts` smoke test stops looking for it. + +## Capabilities + +### New Capabilities + +- `formio-schema-skill`: Defines the consolidated, domain-partitioned Form.io JSON schema reference skill — its router `SKILL.md`, its `references//` layout (project, form, submission), and the rule that adding a new schema domain MUST be additive (create a new subdirectory under `references/`, do not flatten). Action JSON is explicitly NOT a domain of this skill — the `formio-actions` skill owns it. + +### Modified Capabilities + +- `form-create`: Tool description references the `formio-schema` skill instead of `formio-form`. +- `form-update`: Tool description references the `formio-schema` skill instead of `formio-form`. +- `claude-plugin-packaging`: Bundled-skill list and `test-plugin.ts` skill-presence check no longer require `formio-form`; `formio-schema` is the only schema-authoring skill that must ship. + +## Impact + +- **Skills**: `plugin/skills/formio-form/` deleted. `plugin/skills/formio-schema/` restructured (references move into `references/form/`, new placeholder `submission/` and `project/` domain dirs added, `SKILL.md` rewritten). No `action/` or `role/` subdirectories — action JSON is owned by the dedicated `formio-actions` skill and role objects are handled by `formio-api`'s `project-roles` reference. +- **MCP tools**: `packages/mcp-server/src/tools/form_create.ts` and `form_update.ts` tool-description strings change. Existing tests that assert on those strings (`form_create.test.ts`, `form_update.test.ts`) update with them. +- **Plugin packaging**: `scripts/test-plugin.ts` no longer asserts `formio-form` is present; asserts on `formio-schema` instead. `plugin/README.md` skill table loses the `formio-form` row. +- **Other skills**: `formio-api/SKILL.md`, `formio-api/references/project-forms.md`, and `formio-resource-planner/SKILL.md` swap `formio-form` references for `formio-schema`. +- **Tests**: `pnpm test` (Vitest), `pnpm lint` (typecheck), and `pnpm format` must all pass before the change is done. No validator changes are required by this proposal — `skills-validator.ts` does not currently inspect `formio-schema` or `formio-form`. diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/claude-plugin-packaging/spec.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/claude-plugin-packaging/spec.md new file mode 100644 index 0000000..3a7de39 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/claude-plugin-packaging/spec.md @@ -0,0 +1,31 @@ +## MODIFIED Requirements + +### Requirement: Plugin bundles the skills library + +The plugin source tree SHALL include, and the build SHALL copy to `dist/plugin/skills/`, the full `formio-api` router skill, every `formio-api-` capability-group skill, and the `formio-schema` and `formio-resource-planner` skills. The bundled set SHALL NOT include `formio-form`. + +#### Scenario: Installed plugin exposes all skills + +- **WHEN** a user installs `@formio/ai` in a Claude Code project +- **THEN** Claude Code discovers every `formio-api`, `formio-schema`, and `formio-resource-planner` skill from the plugin's `skills/` directory +- **AND** no skill named `formio-form` is present in the bundled `skills/` directory + +### Requirement: Smoke test validates the built plugin over stdio + +A `scripts/test-plugin.ts` script SHALL validate that `dist/plugin/` exists, that `plugin.json` contains required fields (`name`, `version`, `description`, at least one `mcpServers` entry), that required skill directories (at minimum `formio-api` and `formio-schema`) are present, and that spawning the bundled server and sending a JSON-RPC `tools/list` request returns a well-formed response. The smoke test SHALL NOT require `formio-form` to be present. + +#### Scenario: Smoke test fails when build is missing + +- **WHEN** the smoke test runs without `dist/plugin/` present +- **THEN** it exits non-zero with a message instructing the user to run `pnpm build:plugin` + +#### Scenario: Smoke test verifies live MCP server + +- **WHEN** the smoke test spawns `dist/plugin/server/stdio.mjs` and sends a `tools/list` JSON-RPC request +- **THEN** the server responds with a `result.tools` array and the test exits zero + +#### Scenario: Smoke test asserts on the consolidated schema skill + +- **WHEN** the smoke test inspects the bundled `skills/` directory +- **THEN** it SHALL assert `formio-schema` is present +- **AND** it SHALL NOT assert `formio-form` is present diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-create/spec.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-create/spec.md new file mode 100644 index 0000000..8753b0d --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-create/spec.md @@ -0,0 +1,12 @@ +## MODIFIED Requirements + +### Requirement: form_create tool is registered with skill-referencing description + +The `form_create` tool SHALL be registered on the MCP server with a description that instructs the LLM to use the `formio-schema` skill to construct the form JSON definition before calling this tool. The description SHALL reference the skill by name so the LLM knows to invoke it for schema guidance. The description SHALL NOT reference `formio-form`. + +#### Scenario: Tool appears in tool listing with skill reference + +- **WHEN** the MCP server is initialized with valid configuration +- **THEN** the `form_create` tool is available with a required `form` parameter accepting a JSON object +- **AND** the tool description instructs the LLM to use the `formio-schema` skill to build the form JSON +- **AND** the tool description does not contain the string `formio-form` diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-update/spec.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-update/spec.md new file mode 100644 index 0000000..3fc07e8 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/form-update/spec.md @@ -0,0 +1,12 @@ +## MODIFIED Requirements + +### Requirement: form_update tool is registered with workflow guidance + +The `form_update` tool SHALL be registered on the MCP server with a description that instructs the LLM to: (1) fetch the current form via `form_get`, (2) use the `formio-schema` skill to apply the requested modifications, and (3) call `form_update` with the complete updated form JSON. The description SHALL NOT reference `formio-form`. + +#### Scenario: Tool appears in tool listing with workflow guidance + +- **WHEN** the MCP server is initialized with valid configuration +- **THEN** the `form_update` tool is available with required `formId` and `form` parameters +- **AND** the tool description references the `form_get` tool and `formio-schema` skill +- **AND** the tool description does not contain the string `formio-form` diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/formio-schema-skill/spec.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/formio-schema-skill/spec.md new file mode 100644 index 0000000..e5d4262 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/specs/formio-schema-skill/spec.md @@ -0,0 +1,78 @@ +## ADDED Requirements + +### Requirement: Consolidated Form.io JSON schema skill + +The `formio-schema` skill SHALL be the single Form.io JSON schema reference skill in the library. The repository SHALL NOT contain a separate `formio-form` skill; `plugin/skills/formio-form/` SHALL not exist. + +`plugin/skills/formio-schema/SKILL.md` SHALL be the router entry point, carrying YAML frontmatter with at least `name: formio-schema` and a `description` that: + +1. States the skill covers Form.io JSON schemas for project, form (and resource), and submission documents. It SHALL NOT claim to cover action or role JSON — action JSON is owned by the dedicated `formio-actions` skill, and role JSON is handled by `formio-api`'s `project-roles` reference. +2. Includes a "Use when…" trigger clause listing the form-builder phrases that previously activated `formio-form` (e.g., `components`, `wizard`, `textfield`, `datagrid`) so existing form-authoring prompts still activate the skill. +3. Includes a "Not for:" negative-trigger clause disambiguating from `formio-api`, `formio-actions`, `formio-resource-planner`, and `formio-application`. The clause SHALL NOT mention `formio-form`. + +#### Scenario: formio-form skill is removed + +- **WHEN** the repository is inspected +- **THEN** `plugin/skills/formio-form/` SHALL NOT exist +- **AND** no file under `plugin/skills/` SHALL reference `formio-form` by name + +#### Scenario: formio-schema router has multi-domain description + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its frontmatter `name` SHALL equal `formio-schema` +- **AND** its frontmatter `description` SHALL mention the domains "submission" and "project" alongside "form" +- **AND** its frontmatter `description` SHALL include a "Not for:" clause that names `formio-api`, `formio-actions`, `formio-resource-planner`, and `formio-application` +- **AND** its frontmatter `description` SHALL NOT contain the string `formio-form` + +### Requirement: Domain-partitioned references directory + +The skill's reference documents SHALL live under `plugin/skills/formio-schema/references//`, with one subdirectory per Form.io schema domain. The directory layout SHALL be additive: adding a new schema domain creates a new subdirectory under `references/` rather than placing files at the top level. + +The set of domains the skill owns SHALL be exactly `form`, `submission`, and `project`. Action JSON and role JSON SHALL NOT be domains of this skill — there SHALL be no `references/action/` or `references/role/` subdirectory. + +`plugin/skills/formio-schema/references/form/` SHALL contain the form-domain references previously located at `plugin/skills/formio-schema/references/`: + +- `form-definition.md` +- `base-component.md` +- `input-components.md` +- `layout-components.md` +- `data-components.md` + +`plugin/skills/formio-schema/references/` SHALL NOT contain any `.md` file directly (only subdirectories). + +The placeholder schema domains `submission` and `project` SHALL each be present as a subdirectory containing at minimum a `README.md` that describes what that domain will document and directs the user to the corresponding `formio-api` reference until the domain is authored. + +#### Scenario: References live under domain subdirectories + +- **WHEN** `plugin/skills/formio-schema/references/` is listed +- **THEN** its subdirectories SHALL be exactly `form/`, `submission/`, and `project/` +- **AND** it SHALL NOT contain `action/` or `role/` subdirectories +- **AND** it SHALL NOT contain any `.md` files at its top level + +#### Scenario: Form domain has the full reference set + +- **WHEN** `plugin/skills/formio-schema/references/form/` is listed +- **THEN** it SHALL contain `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, and `data-components.md` +- **AND** every file in `references/form/` SHALL be non-empty + +#### Scenario: Placeholder domains route to formio-api + +- **WHEN** `plugin/skills/formio-schema/references//README.md` is read for any `` in `{submission, project}` +- **THEN** the README SHALL be non-empty +- **AND** the README SHALL state that the domain is not yet authored +- **AND** the README SHALL name the corresponding `formio-api` reference document (`runtime-submissions` for submission, `platform-projects` for project) as the interim source of truth + +### Requirement: Router SKILL.md indexes every domain + +`plugin/skills/formio-schema/SKILL.md` SHALL contain a table (or equivalent structured list) that maps each domain to its references. The form domain SHALL list all five form references with their relative paths under `references/form/`. The submission and project domains SHALL each be listed with a "not yet authored" status and a pointer to the placeholder `README.md` plus the relevant `formio-api` reference. The router body SHALL NOT list `action` or `role` as domains owned by this skill. + +#### Scenario: Router enumerates all five form-domain references + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/form/form-definition.md`, `references/form/base-component.md`, `references/form/input-components.md`, `references/form/layout-components.md`, and `references/form/data-components.md` + +#### Scenario: Router enumerates the placeholder domains and excludes action/role + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/submission/README.md` and `references/project/README.md` +- **AND** its body SHALL NOT reference `references/action/` or `references/role/` diff --git a/openspec/changes/archive/2026-05-26-merge-form-schema-skills/tasks.md b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/tasks.md new file mode 100644 index 0000000..84cd34d --- /dev/null +++ b/openspec/changes/archive/2026-05-26-merge-form-schema-skills/tasks.md @@ -0,0 +1,104 @@ +## 1. Restructure `formio-schema` references into domain subdirectories + + +### Red + +- [x] 1.1 Add failing Vitest test under `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts` asserting `plugin/skills/formio-schema/references/` contains exactly the subdirectories `form/`, `submission/`, `project/` (no `action/` or `role/`) and no `.md` files at its top level +- [x] 1.2 Add failing test in the same file asserting `plugin/skills/formio-schema/references/form/` contains `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, `data-components.md` and each is non-empty +- [x] 1.3 Add failing test asserting each of `references/submission/README.md` and `references/project/README.md` exists, is non-empty, contains the phrase "not yet authored", and names the corresponding `formio-api` reference (`runtime-submissions`, `platform-projects` respectively) + +### Green + +- [x] 1.4 Create `plugin/skills/formio-schema/references/form/` and move the existing five reference files into it (`git mv` so history is preserved) +- [x] 1.5 Create `plugin/skills/formio-schema/references/{submission,project}/README.md` placeholders that each describe the domain, state it is "not yet authored", and link to the corresponding `formio-api` reference. No `action/` or `role/` placeholders are created — those domains are out of scope for this skill + +### Refactor + +- [x] 1.6 Review implementation and refactor as needed + +## 2. Update `formio-schema` SKILL.md to route across domains + + +### Red + +- [x] 2.1 Add failing test asserting `plugin/skills/formio-schema/SKILL.md` frontmatter `name` equals `formio-schema` and `description` mentions the words "submission" and "project" alongside "form" +- [x] 2.2 Add failing test asserting `formio-schema/SKILL.md` description contains a "Not for:" clause naming `formio-api`, `formio-actions`, `formio-resource-planner`, and `formio-application`, and contains zero occurrences of the string `formio-form` +- [x] 2.3 Add failing test asserting `formio-schema/SKILL.md` body references all five `references/form/*.md` paths and the two `references//README.md` paths (submission, project) + +### Green + +- [x] 2.4 Rewrite `plugin/skills/formio-schema/SKILL.md`: broaden the frontmatter `description` per `design.md`, replace the single "Working on… / Load" table with a domain-grouped layout, and ensure every reference path in the body is under `references//` + +### Refactor + +- [x] 2.5 Review implementation and refactor as needed + +## 3. Merge `formio-form` content into `formio-schema/references/form/` and delete the skill + + +### Red + +- [x] 3.1 Add failing test asserting `plugin/skills/formio-form/` does not exist +- [x] 3.2 Add failing test asserting no file under `plugin/skills/` (excluding archive paths under `openspec/`) contains the substring `formio-form` + +### Green + +- [x] 3.3 Diff `plugin/skills/formio-form/SKILL.md` against the merged `plugin/skills/formio-schema/references/form/*.md` files. For every property table row, component sub-section, or callout that exists in `formio-form` but is missing from the corresponding `formio-schema` reference, port the missing content into the appropriate `references/form/*.md` file +- [x] 3.4 Delete `plugin/skills/formio-form/` entirely (`git rm -r`) +- [x] 3.5 Update `plugin/skills/formio-api/SKILL.md` negative-trigger clause and `plugin/skills/formio-api/references/project-forms.md` to reference `formio-schema` instead of `formio-form` +- [x] 3.6 Update `plugin/skills/formio-resource-planner/SKILL.md` to reference `formio-schema` (both occurrences) instead of `formio-form` +- [x] 3.7 Update `plugin/README.md` skill table: remove the `formio-form` row and keep only the `formio-schema` row, updating its description to reflect the broadened scope + +### Refactor + +- [x] 3.8 Review implementation and refactor as needed + +## 4. Update MCP tool descriptions for `form_create` and `form_update` + + +### Red + +- [x] 4.1 Update `packages/mcp-server/src/__tests__/form_create.test.ts` line 22 to assert `tool!.description` contains `formio-schema` and does NOT contain `formio-form` — this test should now fail against the current tool description +- [x] 4.2 Update `packages/mcp-server/src/__tests__/form_update.test.ts` line 27 to the same assertion — should fail against the current tool description + +### Green + +- [x] 4.3 Update `packages/mcp-server/src/tools/form_create.ts` tool description: replace `formio-form` with `formio-schema` +- [x] 4.4 Update `packages/mcp-server/src/tools/form_update.ts` tool description: replace `formio-form` with `formio-schema` + +### Refactor + +- [x] 4.5 Review implementation and refactor as needed + +## 5. Update plugin packaging tests and smoke script + + +### Red + +- [x] 5.1 Update `packages/mcp-server/src/__tests__/plugin-build.test.ts` test `1.3` to assert the bundled `skills/` directory contains `formio-schema` and does NOT contain `formio-form`; rename the test title accordingly +- [x] 5.2 Update `plugin-build.test.ts` test `3.3` smoke-test assertion to match `formio-schema` instead of `formio-form`; rename the test title accordingly + +### Green + +- [x] 5.3 Update `scripts/test-plugin.ts` constant `REQUIRED_SKILL_DIRS` from `['formio-api', 'formio-form']` to `['formio-api', 'formio-schema']` + +### Refactor + +- [x] 5.4 Review implementation and refactor as needed + +## 6. Verify Definition of Done + + +### Red + +- [x] 6.1 (No new tests — verification step only) + +### Green + +- [x] 6.2 Run `pnpm test` and confirm all suites pass +- [x] 6.3 Run `pnpm lint` (typecheck) and confirm zero errors +- [x] 6.4 Run `pnpm format` and confirm the working tree stays clean + +### Refactor + +- [x] 6.5 Review implementation and refactor as needed diff --git a/openspec/changes/archive/2026-05-26-project-schema-reference/.openspec.yaml b/openspec/changes/archive/2026-05-26-project-schema-reference/.openspec.yaml new file mode 100644 index 0000000..45b6a2c --- /dev/null +++ b/openspec/changes/archive/2026-05-26-project-schema-reference/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-tdd +created: 2026-05-26 diff --git a/openspec/changes/archive/2026-05-26-project-schema-reference/design.md b/openspec/changes/archive/2026-05-26-project-schema-reference/design.md new file mode 100644 index 0000000..890474a --- /dev/null +++ b/openspec/changes/archive/2026-05-26-project-schema-reference/design.md @@ -0,0 +1,120 @@ +## Context + +The `formio-schema` skill's domain partitioning is now established: `form/` is fully authored (five files), `submission/` is fully authored (five files, as of the `submission-schema-reference` change archived at `openspec/changes/archive/2026-05-26-submission-schema-reference/`), and `project/` is the last remaining placeholder — currently a single `README.md` redirecting to `formio-api`'s `platform-projects` endpoint reference. + +The Form.io project envelope is the largest of the three top-level Form.io documents and the only one with multiple discriminators (`type`, `plan`, `framework`), encrypted-at-rest sub-objects (`settings`), nested authentication configuration (`oauth`, `ldap`, `saml`), integration sub-objects (email, captcha, esign, google drive, kickbox, SQL connector, file storage), parent/child relationships (stages and tenants reference a primary project), and read-only server-managed fields (`apiCalls`, `billing`, `lastDeploy`, `trial`). It cannot be documented in a single file without bloating past the size of the existing form/submission references. + +Two authoritative sources cover the envelope: + +- `~/Documents/formio/modules/nirvana/packages/core/src/types/project/Project.ts` — the canonical TypeScript declaration of `Project` plus every supporting type. +- `~/Documents/formio/modules/nirvana/apps/formio-server/src/models/Project.js` — the Mongoose schema used by the API server, carrying authoritative `description` strings, length and regex constraints, default values, the `'archived'` plan extension, the `externalOwner` sub-document for OIDC, and the encryption-at-rest contract for `settings`. + +The two sources diverge in a few small but important ways. The TS file declares `_id` as required and `deleted` as `Date | string`; the server stores `deleted` as a `Number`. The TS enum `ProjectPlan` does not include `'archived'`; the server enum does. The server adds an `externalOwner` field with `sub`/`iss`/`customIdClaim` shape that is not in the TS declaration. The reference will document the server's contract (it's the runtime truth) and call out divergences where they matter. + +`ProjectSettings` and its `authorization/` and `integrations/` sub-types live in `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/`. The reference will document `ProjectSettings` keys at one level of indirection — listing each integration / authorization block with a one-line role and pointer to the relevant `formio-api` reference for endpoint-specific behavior — rather than re-documenting every nested provider option (e.g., the OAuth client-secret rotation flow). The schema skill's job is "what's in the JSON," not "how does the integration work end-to-end." + +## Goals / Non-Goals + +**Goals:** + +- Four focused project-domain references under `references/project/`, each readable on its own, each under ~250 lines. Same pattern as the submission domain (which itself mirrors the form domain). Billing and usage statistics are intentionally out of scope as authored content — they are operator/SaaS concerns, not schema-authoring concerns. The relevant fields appear as one-line rows in `project-definition.md`'s property table and nothing more. +- Every property declared on `Project` in the TS file has a row in a property table with type / required / description / constraint columns. Where the server model adds constraint detail (length limits, regex, validator messages, default values), the reference uses that detail. Where the server adds fields not in the TS file (notably `externalOwner`), the reference adds them with a footnote that the upstream TS declaration is currently narrower. +- The `settings` reference enumerates every documented `ProjectSettings` key but treats each integration / authorization block as a one-line entry pointing at where to look next (the relevant `formio-api` endpoint reference or the upstream type definition). It explicitly documents that `settings` is encrypted at rest via the server's `EncryptedProperty` plugin and that downstream consumers see decrypted JSON only when the server returns the project to an authorized caller. +- The router `SKILL.md` lists every project reference with a one-line "Working on…" cue. +- Tests assert structural presence (file existence + non-empty + no frontmatter + body mentions every enumerated value), not exact property-table formatting. + +**Non-Goals:** + +- Authoring a billing-and-usage reference. The `billing`, `apiCalls`, `trial`, and `lastDeploy` fields are server-managed read-only stats consumed by the Form.io plan dashboard, not values an integrator would hand-author or hand-interpret. They get one-line rows in `project-definition.md`'s property table; there is no separate file and no `ProjectUsage` counter deep-dive. +- Documenting the `plan` discriminator in depth. The primary use case for these agentic skills is self-hosted / on-prem Form.io deployments, where `plan` is always `'commercial'`. The SaaS plan tiers (`basic`, `independent`, `team`, `trial`) and the server-only `'archived'` value are out of scope. `plan` appears as a one-line row in `project-definition.md`'s property table stating "for deployed projects this is always `'commercial'`" and nothing more. +- Documenting the project REST endpoints (verb, path, query params, status codes). That belongs to `formio-api`'s `platform-projects` reference and stays there. +- Documenting the full integration provider config matrix (every OAuth provider's exact field set, every file-storage provider's bucket-config block). The reference points at the upstream sub-types; deep-diving each provider would belong in the relevant integration's own documentation page. +- Documenting the project-template envelope (`Template.ts`). That's a separate artifact — used by `project_import` / `project_export` — and is large enough to deserve its own promotion from this domain in a follow-up change. The reference will mention the template envelope's existence and route to `formio-api`'s `platform-projects` for endpoint details. +- Fixing the TS / server-model divergences upstream. The reference documents both contracts and notes the gaps; resolving them would require PRs against `nirvana` and is out of scope. +- Generating the references from the TS source at build time. Hand-authored Markdown stays the authoring contract. + +## Decisions + +### Decision: Four reference files, one per logical concern + +| File | Covers | +| ------------------------------- | ------------------------------------------------------------------------------------- | +| `project-definition.md` | Top-level `Project` envelope and every declared property | +| `project-type-and-framework.md` | `type` / `framework` discriminators and the stage / tenant parent-child relationship | +| `project-settings.md` | `ProjectSettings` shape, every documented key, encryption-at-rest contract | +| `project-access.md` | Project-level `access` array, `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo` | + +**Why four not three or one:** Project is the largest envelope. Bundling `settings` into `project-definition.md` would push the definition file past 400 lines just from the settings tables. Splitting `type`/`framework` into their own file lets consumers who only want to know "what does `'tenant'` mean" load a one-screen reference. + +**Why no billing/usage file:** The `billing`, `apiCalls`, `trial`, and `lastDeploy` fields are server-managed read-only stats — operator/SaaS concerns, not schema-authoring concerns. They appear as one-line rows in `project-definition.md` (so the envelope is documented in full) but the skill does NOT teach how to read `ProjectUsage` counters, the `billing` exceeds-flag semantics, or trial-period mechanics. A reader who needs that should check their Form.io plan dashboard or the platform billing API, not a schema reference. + +**Why no `plan` section:** Same logic. The primary audience for this skill is self-hosted / on-prem Form.io deployments where every project's `plan` is `'commercial'`. The SaaS tier ladder (`basic` → `independent` → `team` → `trial`, plus the server-only `'archived'`) is a billing concern the agent never needs to reason about. `plan` gets one row in `project-definition.md` saying "for deployed projects this is always `'commercial'`; SaaS tier values exist but are not covered by this skill." + +### Decision: Document the Stage and Tenant creation patterns as explicit copy-paste snippets + +`type` alone is not enough to make a working stage or tenant — a Stage also requires `project` to point at its parent project (typically the portal/primary project). The `project-type-and-framework.md` reference includes two side-by-side JSON snippets: + +```json +{ "type": "stage", "project": "" } +``` + +```json +{ "type": "tenant" } +``` + +These are the minimum required shapes. The reference also notes that the parent of a Stage is typically the portal project (the primary project the stage's environment branches from), and that Tenants do not require a parent reference in `project` because their multi-tenancy model is keyed differently. + +**Why explicit snippets, not just prose:** Stages and tenants are the most-asked-about derived project types and the easiest to misconstruct (forgetting the `project` ObjectId on a stage is the classic mistake). A copy-paste-ready snippet is the fastest path to "right on first try." + +### Decision: Document the server-extended `plan` enum and `externalOwner` field even though they aren't in the TS declaration + +The Mongoose model is the runtime truth — the database accepts `'archived'` as a plan value and stores `externalOwner` on OIDC-owned projects. A reference that mirrors only the TS file would teach readers to reject valid production data. The reference includes both, marked with a one-line footnote: "Server-only — not in the upstream TypeScript declaration." + +### Decision: Treat each integration / authorization block as a one-line entry with a pointer, not a full table + +`ProjectSettings` has eight integration blocks and three authorization blocks. Each one has its own sub-type with its own field set (OAuth provider, LDAP server, SAML IdP metadata, etc.). Fully expanding all eleven would push `project-settings.md` past 400 lines and duplicate documentation that already lives in the upstream type files. Instead each block gets one row in a top-level `ProjectSettings` table — "what it's for, where to look next" — and the rest is left to either the upstream types (which the reference cites) or to the relevant `formio-api` endpoint reference. + +**Alternative considered:** Author one sub-file per integration (`project-settings-oauth.md`, `project-settings-email.md`, …). Rejected — over-partitioning. The router table would balloon to twenty rows, and most readers want a single page with the keys, not a per-integration deep dive. + +### Decision: Document `settings` as encrypted-at-rest with a one-paragraph callout + +The Mongoose model installs the `EncryptedProperty` plugin against `settings`. Consumers who write the project JSON via the API send plaintext settings, and the server encrypts them. Consumers who read via the API receive plaintext settings (decrypted by the server, subject to authorization). Direct database access sees ciphertext. This is the kind of behavior a schema reference must document because it changes how the field round-trips. The `project-settings.md` reference contains a top-of-file paragraph stating exactly this. + +### Decision: Cross-link to upstream type files using stable repo paths + +Cross-links from project references to the relevant TS source (e.g., `oauth.ts`, `email.ts`) use the repo-relative path under `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/`. The skill is consumed inside a Claude session where the user is working with that repo on their machine; the stable absolute path lets the user (or Claude) jump straight to the source. We accept that the path is machine-specific — every other reference cross-link in the skill is intra-repo, so this is the first time the schema skill points at an external path. This is documented in the file itself with a one-line note. + +**Alternative considered:** Link to a public GitHub URL for the `nirvana` repo. Rejected — `nirvana` is the closed-source platform monorepo. A public link would 404 for external readers. + +### Decision: Tests assert structural presence, not exact property lists + +Same test contract as the submission domain. The tests will assert: + +- Each project reference file exists, is non-empty, carries no YAML frontmatter. +- `project/README.md` no longer exists. +- `project-definition.md` mentions every TS-declared property name plus `externalOwner`. +- `project-type-and-framework.md` mentions every `ProjectType` and every `ProjectFramework` value. No `ProjectPlan` enumeration is required — that discriminator is intentionally out of scope. +- `project-definition.md` mentions `commercial` in the body, capturing the "deployed projects are always commercial" claim made in the `plan` row. +- `project-settings.md` mentions every documented `ProjectSettings` key and the word "encrypted" (covering the at-rest callout). +- `project-access.md` mentions `ProjectRole`, `ProjectFormAccess`, and `ProjectAccessInfo`. +- The router SKILL.md body references all four project reference paths and does NOT reference `project/README.md`. + +### Decision: Drop the entire "placeholder domains" test once project is authored + +After this change, no domains under `references/` remain placeholders. The existing layout test's `PLACEHOLDER_DOMAINS` constant becomes empty, and the corresponding `it('each placeholder domain README states not-yet-authored…')` block becomes meaningless. Removing them in this change keeps the test suite from carrying dead-code constants. The router body assertion for placeholder paths (`references//README.md`) similarly disappears. + +## Risks / Trade-offs + +- **Risk:** Project envelope evolves and the reference drifts. → **Mitigation:** Same as form/submission domains — author in user-experience voice, not a 1:1 type mirror. Structural tests catch deletions but not additions; additions are caught by the standard skill-iteration practice. +- **Risk:** Documenting `'archived'` plan and `externalOwner` field diverges from the upstream TS. → **Mitigation:** Each reference contains an explicit footnote naming the divergence. A reviewer trying to delete them on the basis of "TS doesn't have these" will hit the footnote. +- **Risk:** `settings` encryption-at-rest claim becomes wrong if the platform changes its persistence model. → **Mitigation:** The claim is sourced from `Project.js` line ~240 (`model.schema.plugin(EncryptedProperty, { … plainName: 'settings' })`). If the line moves, the reference moves with it. The structural test asserts the word "encrypted" appears, but does not lock the exact phrasing — so a wording update doesn't require a code change. +- **Trade-off:** Five files is more navigation than one. We accept that — same pattern as form and submission domains. +- **Trade-off:** Treating integrations as one-line entries means a reader who wants OAuth provider config still has to load `oauth.ts` directly. The router cross-links to it; we accept the indirection in exchange for keeping `project-settings.md` under 250 lines. + +## Migration Plan + +1. Read every `ProjectSettings` sub-file under `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/integrations/` and `~/.../authorization/` during apply phase so descriptions are sourced, not invented. +2. Implement the five reference files, the router SKILL.md update, and the test updates on a feature branch. +3. Delete `plugin/skills/formio-schema/references/project/README.md`. +4. Run `pnpm test`, `pnpm lint`, `pnpm format` — Definition of Done. +5. Rollback: revert the merge commit; the placeholder `README.md` returns. diff --git a/openspec/changes/archive/2026-05-26-project-schema-reference/proposal.md b/openspec/changes/archive/2026-05-26-project-schema-reference/proposal.md new file mode 100644 index 0000000..5a8072b --- /dev/null +++ b/openspec/changes/archive/2026-05-26-project-schema-reference/proposal.md @@ -0,0 +1,44 @@ +## Why + +The `formio-schema` skill now ships authored references for the `form` and `submission` domains, but `references/project/` is still a one-file `README.md` placeholder pointing back at `formio-api`'s `platform-projects` endpoint reference. That placeholder is useful for someone calling the project endpoint, but it gives Claude nothing when a user asks "what's in this project JSON?" — how to read `settings`, what `plan`/`type`/`framework` accept, how `access` differs from form/submission access, what `remote` means for stages, how `formDefaults` propagates, what `externalOwner` encodes for OIDC, what's encrypted at rest. The project envelope is the largest of the three top-level Form.io documents and is the one most often misread. + +Two authoritative sources exist on the user's machine: + +- `~/Documents/formio/modules/nirvana/packages/core/src/types/project/Project.ts` — the TypeScript declaration of `Project`, `ProjectType`, `ProjectPlan`, `ProjectFramework`, `ProjectUsage`, `ProjectBilling`, `ProjectApiCalls`, `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo`, plus the imported `ProjectSettings` shape under `project/settings/`. +- `~/Documents/formio/modules/nirvana/apps/formio-server/src/models/Project.js` — the Mongoose schema used by the API server, which carries authoritative `description` strings on most fields, validation constraints (length limits, name regex, reserved-subdomain check), default values, the runtime-extended `plan` enum (adds `'archived'`), the `externalOwner` OIDC sub-document, and the `settings` encryption-at-rest contract. + +Pairing the two is enough to author a real project-domain reference set now. + +## What Changes + +- Promote the project domain from "not yet authored" placeholder to a fully authored reference set under `plugin/skills/formio-schema/references/project/`. Delete the placeholder `README.md`. +- Author four reference files mirroring the form and submission domain pattern (each non-empty, no YAML frontmatter). Billing and usage statistics are deliberately out of scope — `billing`, `apiCalls`, `trial`, and the full `ProjectUsage` counter set get one-line rows in `project-definition.md`'s property table and nothing more; they are operator/SaaS concerns, not schema-authoring concerns, and the skill does not need to teach how to read them: + - `project-definition.md` — top-level `Project` envelope: every TS-declared property plus the server-only `externalOwner` field, with type / required / description / constraint columns sourced from the Mongoose schema. Includes a worked example. Billing/usage fields appear as table rows only (`billing` and `apiCalls` get a one-line "server-managed billing/usage data — see your Form.io plan dashboard, not documented as schema" entry). + - `project-type-and-framework.md` — two of the three discriminators: `type` (`project`/`stage`/`tenant`) and `framework` (the nine `ProjectFramework` values). Explicitly documents the two derived project patterns: a **Stage** sets `type: 'stage'` plus `project: ` (the parent is typically the portal/primary project the stage belongs to); a **Tenant** sets `type: 'tenant'`. Includes a small JSON snippet for each pattern so an integrator can copy-paste the minimum required shape. The `plan` discriminator is intentionally NOT documented in depth — for self-hosted and on-prem deployments (the primary use case for these agentic skills) it is always `'commercial'`, and the SaaS plan tiers (`basic`/`independent`/`team`/`trial`) are out of scope. + - `project-settings.md` — the `ProjectSettings` shape: `appOrigin`, `keys`, `cors`, `csp`, `secret`, PDF settings (`pdfserver`, `filetoken`), public config flags (`allowConfig`, `allowConfigToForms`), `custom` CSS/JS, `formModule`, every integration block (`email`, `captcha`, `recaptcha`, `esign`, `google`, `kickbox`, `sqlconnector`, `storage`), every authorization block (`tokenParse`, `oauth`, `ldap`, `saml`), and the encryption-at-rest contract (the entire `settings` blob is stored encrypted in MongoDB via the `EncryptedProperty` plugin). + - `project-access.md` — project-level `access` array and the supporting `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo` types. Explains how project-level `access` differs from form-level / submission-level access (project access governs who can see / modify the project itself; form-level governs form definitions; submission-level governs submission records). +- Update `plugin/skills/formio-schema/SKILL.md`: + - Promote the project row from the still-placeholder section into its own multi-row reference table mirroring the existing submission table. + - Update the trigger clause so project-specific prompts (project settings, stages, tenants, OAuth/LDAP/SAML config, file storage, project plan, project access) clearly activate the skill. + - Drop the "Projects" placeholder section (no domains remain placeholders). +- Update `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts`: + - Remove the project entry from the `PLACEHOLDER_DOMAINS` constant — no placeholders remain. + - Drop the entire "each placeholder domain README states not-yet-authored…" test (no placeholders left). + - Drop the "body references each `references//README.md` placeholder" router-body assertion (no placeholder paths left). + - Add a project-domain `describe` block mirroring the submission-domain block: assert each new `project/*.md` file exists, is non-empty, carries no YAML frontmatter, and that `project/README.md` does not exist. + - Add body-content assertions for the four project references — `project-definition.md` mentions every Project property name from the TS interface plus `externalOwner` and mentions that for deployed projects `plan` is always `'commercial'`; `project-type-and-framework.md` mentions every `ProjectType` and every `ProjectFramework` value (no `ProjectPlan` enumeration); `project-settings.md` mentions every documented `ProjectSettings` key plus a callout that `settings` is encrypted at rest; `project-access.md` mentions `ProjectRole`, `ProjectFormAccess`, and `ProjectAccessInfo`. + - Add a router-body assertion that all four `references/project/project-*.md` paths appear and that `references/project/README.md` is absent. + +## Capabilities + +### Modified Capabilities + +- `formio-schema-skill`: Adds requirements that the project domain has authored reference files (no longer a placeholder), enumerates the required project reference files, and constrains the router SKILL.md to enumerate every project reference. Removes the "placeholder domains" requirement since no domains remain unauthored. + +## Impact + +- **Skill content**: `plugin/skills/formio-schema/references/project/` gains four authored reference files; the placeholder `README.md` is deleted. +- **Skill router**: `plugin/skills/formio-schema/SKILL.md` gains a project-domain reference table, drops its "Projects" placeholder section, and broadens the trigger clause. +- **Tests**: `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts` updated — placeholder-domain test removed, project authored-reference assertions added. +- **No code changes**: No MCP tool, server, or build script changes are needed. +- **External research**: `Project.ts` + `Project.js` (server Mongoose model) + `ProjectSettings.ts` and its `authorization/` and `integrations/` sub-types are the only inputs. The change does NOT depend on any public help.form.io page. diff --git a/openspec/changes/archive/2026-05-26-project-schema-reference/specs/formio-schema-skill/spec.md b/openspec/changes/archive/2026-05-26-project-schema-reference/specs/formio-schema-skill/spec.md new file mode 100644 index 0000000..142573c --- /dev/null +++ b/openspec/changes/archive/2026-05-26-project-schema-reference/specs/formio-schema-skill/spec.md @@ -0,0 +1,124 @@ +## ADDED Requirements + +### Requirement: Authored project-domain references + +`plugin/skills/formio-schema/references/project/` SHALL contain exactly the following authored reference files, each non-empty and each carrying NO YAML frontmatter: + +- `project-definition.md` +- `project-type-and-framework.md` +- `project-settings.md` +- `project-access.md` + +Billing and usage statistics SHALL NOT be a separate authored reference. The `billing`, `apiCalls`, `trial`, and `lastDeploy` fields appear as one-line rows in `project-definition.md`'s property table and SHALL NOT receive a dedicated reference file — they are operator/SaaS concerns, not schema-authoring concerns. + +`plugin/skills/formio-schema/references/project/project-billing-and-usage.md` SHALL NOT exist. + +`plugin/skills/formio-schema/references/project/README.md` SHALL NOT exist — the router `SKILL.md` is the authoritative project index. + +#### Scenario: Every project reference file exists and is non-empty + +- **WHEN** `plugin/skills/formio-schema/references/project/` is listed +- **THEN** it SHALL contain `project-definition.md`, `project-type-and-framework.md`, `project-settings.md`, and `project-access.md` +- **AND** each file SHALL be non-empty +- **AND** none of those files SHALL begin with `---` +- **AND** `project/README.md` SHALL NOT exist +- **AND** `project/project-billing-and-usage.md` SHALL NOT exist + +#### Scenario: project-definition.md documents every top-level property + +- **WHEN** `plugin/skills/formio-schema/references/project/project-definition.md` is read +- **THEN** its body SHALL mention each of these property names: `_id`, `title`, `name`, `type`, `description`, `tag`, `owner`, `externalOwner`, `project`, `remote`, `plan`, `billing`, `apiCalls`, `steps`, `framework`, `primary`, `access`, `trial`, `lastDeploy`, `stageTitle`, `machineName`, `config`, `protect`, `settings`, `remoteSecret`, `builderConfig`, `formDefaults`, `public`, `created`, `modified`, `deleted` + +#### Scenario: project-type-and-framework.md enumerates every type and framework value + +- **WHEN** `plugin/skills/formio-schema/references/project/project-type-and-framework.md` is read +- **THEN** its body SHALL mention each ProjectType value: `project`, `stage`, `tenant` +- **AND** its body SHALL mention each ProjectFramework value: `angular`, `angular2`, `react`, `vue`, `html5`, `simple`, `custom`, `aurelia`, `javascript` + +#### Scenario: project-type-and-framework.md documents the Stage and Tenant creation patterns + +- **WHEN** `plugin/skills/formio-schema/references/project/project-type-and-framework.md` is read +- **THEN** its body SHALL contain the literal string `"type": "stage"` +- **AND** its body SHALL contain the literal string `"type": "tenant"` +- **AND** its body SHALL state that a Stage's `project` field is set to the parent project's ObjectId +- **AND** its body SHALL state that the parent is typically the portal (or primary) project + +#### Scenario: project-definition.md notes deployed projects use plan "commercial" + +- **WHEN** `plugin/skills/formio-schema/references/project/project-definition.md` is read +- **THEN** its body SHALL mention the string value `commercial` in the context of the `plan` field + +#### Scenario: project-settings.md documents every ProjectSettings key and the encryption contract + +- **WHEN** `plugin/skills/formio-schema/references/project/project-settings.md` is read +- **THEN** its body SHALL mention each of these keys: `appOrigin`, `keys`, `cors`, `csp`, `secret`, `pdfserver`, `filetoken`, `allowConfig`, `allowConfigToForms`, `custom`, `formModule`, `email`, `captcha`, `recaptcha`, `esign`, `google`, `kickbox`, `sqlconnector`, `storage`, `tokenParse`, `oauth`, `ldap`, `saml` +- **AND** its body SHALL state that the `settings` object is encrypted at rest + +#### Scenario: project-access.md documents the project access types + +- **WHEN** `plugin/skills/formio-schema/references/project/project-access.md` is read +- **THEN** its body SHALL mention `ProjectRole`, `ProjectFormAccess`, and `ProjectAccessInfo` +- **AND** its body SHALL distinguish project-level access from form-level and submission-level access + +### Requirement: Router SKILL.md indexes the project domain + +`plugin/skills/formio-schema/SKILL.md` SHALL list the project domain as an authored domain — not as a "not yet authored" placeholder — with its own multi-row reference table (or equivalent structured list) enumerating all four authored project references. The project row SHALL NOT appear in any "not yet authored" table or paragraph. The router body SHALL NOT reference a billing-and-usage file. + +#### Scenario: Router enumerates every project reference + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/project/project-definition.md`, `references/project/project-type-and-framework.md`, `references/project/project-settings.md`, and `references/project/project-access.md` +- **AND** its body SHALL NOT reference the path `references/project/README.md` +- **AND** its body SHALL NOT reference any `references/project/project-billing-and-usage.md` path + +## MODIFIED Requirements + +### Requirement: Domain-partitioned references directory + +The skill's reference documents SHALL live under `plugin/skills/formio-schema/references//`, with one subdirectory per Form.io schema domain. The directory layout SHALL be additive: adding a new schema domain creates a new subdirectory under `references/` rather than placing files at the top level. + +The set of domains the skill owns SHALL be exactly `form`, `submission`, and `project`. Action JSON and role JSON SHALL NOT be domains of this skill — there SHALL be no `references/action/` or `references/role/` subdirectory. + +`plugin/skills/formio-schema/references/form/` SHALL contain the form-domain references: + +- `form-definition.md` +- `base-component.md` +- `input-components.md` +- `layout-components.md` +- `data-components.md` + +`plugin/skills/formio-schema/references/submission/` SHALL contain authored reference files (`submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, `submission-data.md`) — NOT a placeholder `README.md`. + +`plugin/skills/formio-schema/references/project/` SHALL contain authored reference files (`project-definition.md`, `project-type-and-framework.md`, `project-settings.md`, `project-access.md`) — NOT a placeholder `README.md`. The project domain SHALL NOT include a billing-and-usage reference file. + +`plugin/skills/formio-schema/references/` SHALL NOT contain any `.md` file directly (only subdirectories). + +No domain owned by this skill SHALL be a placeholder — every domain subdirectory SHALL contain authored reference files. + +#### Scenario: References live under domain subdirectories + +- **WHEN** `plugin/skills/formio-schema/references/` is listed +- **THEN** its subdirectories SHALL be exactly `form/`, `submission/`, and `project/` +- **AND** it SHALL NOT contain `action/` or `role/` subdirectories +- **AND** it SHALL NOT contain any `.md` files at its top level + +#### Scenario: Form domain has the full reference set + +- **WHEN** `plugin/skills/formio-schema/references/form/` is listed +- **THEN** it SHALL contain `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, and `data-components.md` +- **AND** every file in `references/form/` SHALL be non-empty + +### Requirement: Router SKILL.md indexes every domain + +`plugin/skills/formio-schema/SKILL.md` SHALL contain a table (or equivalent structured list) that maps each domain to its references. The form domain SHALL list all five form references with their relative paths under `references/form/`. The submission domain SHALL list all five authored submission references with their relative paths under `references/submission/`. The project domain SHALL list all four authored project references with their relative paths under `references/project/`. The router body SHALL NOT list `action` or `role` as domains owned by this skill. The router body SHALL NOT reference any `/README.md` placeholder path. + +#### Scenario: Router enumerates all five form-domain references + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/form/form-definition.md`, `references/form/base-component.md`, `references/form/input-components.md`, `references/form/layout-components.md`, and `references/form/data-components.md` + +#### Scenario: Router excludes action/role/role and placeholder README paths + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL NOT reference `references/action/` or `references/role/` +- **AND** its body SHALL NOT reference `references/submission/README.md` or `references/project/README.md` diff --git a/openspec/changes/archive/2026-05-26-project-schema-reference/tasks.md b/openspec/changes/archive/2026-05-26-project-schema-reference/tasks.md new file mode 100644 index 0000000..5379fb2 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-project-schema-reference/tasks.md @@ -0,0 +1,52 @@ +## 1. Author project reference files + + +### Red + +- [x] 1.1 Update `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts`: remove the `PLACEHOLDER_DOMAINS` constant (no placeholders remain), delete the `it('each placeholder domain README states not-yet-authored…')` test, and delete the `it('body references each references//README.md placeholder')` test. These removals SHOULD remain green after the change — they only change in lock-step with the project-domain promotion +- [x] 1.2 Add a new `describe('formio-schema project-domain references', …)` block asserting `plugin/skills/formio-schema/references/project/` contains exactly `project-definition.md`, `project-type-and-framework.md`, `project-settings.md`, `project-access.md`, that none of them carry YAML frontmatter, that `project/README.md` does not exist, and that `project/project-billing-and-usage.md` does not exist either — these assertions should fail against the current placeholder layout +- [x] 1.3 Extend the new project `describe` block with body assertions: `project-definition.md` mentions every Project property name (`_id`, `title`, `name`, `type`, `description`, `tag`, `owner`, `externalOwner`, `project`, `remote`, `plan`, `billing`, `apiCalls`, `steps`, `framework`, `primary`, `access`, `trial`, `lastDeploy`, `stageTitle`, `machineName`, `config`, `protect`, `settings`, `remoteSecret`, `builderConfig`, `formDefaults`, `public`, `created`, `modified`, `deleted`) AND mentions the string `commercial` (for the "deployed projects always have plan `'commercial'`" note); `project-type-and-framework.md` mentions every ProjectType (`project`, `stage`, `tenant`) and every ProjectFramework value (`angular`, `angular2`, `react`, `vue`, `html5`, `simple`, `custom`, `aurelia`, `javascript`) — NO ProjectPlan enumeration — AND contains the literal strings `"type": "stage"` and `"type": "tenant"`, AND states that a Stage's `project` field holds the parent project's ObjectId (typically the portal/primary project); `project-settings.md` mentions every documented ProjectSettings key (`appOrigin`, `keys`, `cors`, `csp`, `secret`, `pdfserver`, `filetoken`, `allowConfig`, `allowConfigToForms`, `custom`, `formModule`, `email`, `captcha`, `recaptcha`, `esign`, `google`, `kickbox`, `sqlconnector`, `storage`, `tokenParse`, `oauth`, `ldap`, `saml`) and the word `encrypted`; `project-access.md` mentions `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo`, plus distinguishes project-level from form-level and submission-level access. No ProjectUsage counter assertions — usage stats are out of scope + +### Green + +- [x] 1.4 Delete `plugin/skills/formio-schema/references/project/README.md` +- [x] 1.5 Author `project-definition.md` +- [x] 1.6 Author `project-type-and-framework.md` with Stage + Tenant patterns +- [x] 1.7 Author `project-settings.md` with encryption-at-rest callout +- [x] 1.8 Author `project-access.md` with project/form/submission layer comparison + +### Refactor + +- [x] 1.9 Review implementation and refactor as needed + +## 2. Update router SKILL.md to index the project domain + + +### Red + +- [x] 2.1 Extend the layout test's existing "body indexes every domain" `describe` block: add an assertion that the router body references all four `references/project/project-*.md` paths, an assertion that the router body does NOT reference `references/project/README.md`, an assertion that the router body does NOT reference `references/project/project-billing-and-usage.md` + +### Green + +- [x] 2.2 Edit `plugin/skills/formio-schema/SKILL.md`: replace the "Projects" placeholder subsection with a "Projects" subsection containing a multi-row table listing every project reference (path + one-line "Working on…" cue); update the trigger clause so project-specific phrases (project settings, stages, tenants, OAuth/LDAP/SAML, file storage, project plan, project access, billing) clearly activate the skill + +### Refactor + +- [x] 2.3 Review implementation and refactor as needed + +## 3. Verify Definition of Done + + +### Red + +- [x] 3.1 (No new tests — verification step only) + +### Green + +- [x] 3.2 Run `pnpm test` and confirm all suites pass +- [x] 3.3 Run `pnpm lint` (typecheck) and confirm zero errors +- [x] 3.4 Run `pnpm format` and confirm the working tree stays clean + +### Refactor + +- [x] 3.5 Review implementation and refactor as needed diff --git a/openspec/changes/archive/2026-05-26-submission-schema-reference/.openspec.yaml b/openspec/changes/archive/2026-05-26-submission-schema-reference/.openspec.yaml new file mode 100644 index 0000000..45b6a2c --- /dev/null +++ b/openspec/changes/archive/2026-05-26-submission-schema-reference/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-tdd +created: 2026-05-26 diff --git a/openspec/changes/archive/2026-05-26-submission-schema-reference/design.md b/openspec/changes/archive/2026-05-26-submission-schema-reference/design.md new file mode 100644 index 0000000..dc55160 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-submission-schema-reference/design.md @@ -0,0 +1,113 @@ +## Context + +The `formio-schema` skill ships a domain-partitioned `references/` tree as of the `merge-form-schema-skills` change (archived at `openspec/changes/archive/2026-05-26-merge-form-schema-skills/`). Today three domain subdirectories exist: + +- `references/form/` — five fully authored references (form-definition, base-component, input-components, layout-components, data-components). +- `references/submission/` — single placeholder `README.md` that routes to `formio-api`'s `runtime-submissions` reference. +- `references/project/` — single placeholder `README.md` that routes to `formio-api`'s `platform-projects` reference. + +The form domain is the model for what an "authored" domain looks like: small, focused files with property tables, examples, and cross-links — no YAML frontmatter on the reference files themselves; only the router `SKILL.md` carries frontmatter. + +The user wants the submission domain promoted to that same authoring level, using the in-monorepo TypeScript source as the type spine and the public user guide for human-language descriptions. The source is at `~/Documents/formio/modules/nirvana/packages/core/src/types/Submission.ts` (outside this repo but on the user's machine), and the user guide is at `https://help.form.io/userguide/submissions`. + +Reading both during exploration produced these inputs: + +- The TypeScript `Submission` interface declares: `_id`, `_fvid`, `form`, `owner`, `roles`, `metadata`, `data`, `project`, `state`, `access`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted`. +- The TS `SubmissionState` only declares `'submitted'`. The user guide explicitly documents both `"draft"` and `"submitted"` as valid state values produced and consumed by the platform. Production data has both. The reference will treat both as the canonical set and call out that the TS type is currently narrower than the runtime contract. +- The TS `SubmissionMetadata` declares twelve documented keys plus an open-ended index signature `[key: string]: any` — the reference will list every documented key and explicitly call the metadata bag extensible. +- The TS `Access` type imported from `'types'` is `{ type: AccessType, roles: RoleId[], resources?: string[] }` with thirteen `AccessType` literal values. The reference will list all thirteen, and note the difference between form-level `access` / `submissionAccess` (the form definition's access controls) and row-level `access` (this same shape, applied per-submission). The row-level version overrides per submission. +- The TS `DataObject` is `{ [key: string]: unknown }` plus an `AddressComponentDataObject` discriminated union for the `address` component (`autocomplete` vs `manual` mode). The reference will document the data envelope and use the address shape as the worked example of how component types influence the stored value. +- The user guide identifies advanced features (submission revisions via `_fvid`; per-form custom MongoDB collections) — the reference will mention these but not re-document the API endpoints, which live in `formio-api`'s `runtime-submissions` and `project-forms` references. + +## Goals / Non-Goals + +**Goals:** + +- Five focused reference files under `references/submission/`, each readable on its own, none larger than ~250 lines. The form-domain references are the size and style template. +- Every property declared in the TypeScript source has a row in a property table with: name, type, required/optional, one-sentence description grounded in either the type definition or the user guide. Where the user guide adds nuance (e.g., draft state, `data` interpretation), that nuance is captured. +- The submission `data` object reference cross-links to the form-domain references for component-by-component data shapes (e.g., "for what each component type stores in `data[key]`, see `references/form/input-components.md` and `references/form/data-components.md`"). It does not duplicate per-component value shapes — that work already lives in the form references. +- The router `SKILL.md` lists every submission reference with a one-line "Working on…" cue so Claude can pick the right file without loading everything. +- Tests assert the file layout, file non-emptiness, and presence of the key headings each reference must carry. They do NOT diff exact property tables — that brittle assertion would create test churn whenever the platform adds a documented field. + +**Non-Goals:** + +- Documenting the submission REST endpoints (verb, path, query params, status codes). That belongs to `formio-api`'s `runtime-submissions` reference and stays there. +- Documenting per-component value shapes (e.g., how `datagrid` rows store an array of nested objects in `data`). That already lives in `references/form/data-components.md`; the submission reference cross-links rather than duplicates. +- Generating the references from the TypeScript source at build time. The references are hand-authored Markdown — same authoring contract as every other file under `plugin/skills/formio-schema/references/`. Codegen would be a separate proposal. +- Fixing the TypeScript source. The discrepancy where `SubmissionState` declares only `'submitted'` but runtime includes `'draft'` is documented in the reference and noted here, but resolving it would require a PR against `nirvana`, which is out of scope for this proposal. +- Authoring the project domain. That stays as a placeholder for now; only the submission domain is being promoted. + +## Decisions + +### Decision: Replace `submission/README.md` with five focused files; do not keep README as an index + +The form domain has no top-level `README.md` — its index lives entirely in the router `SKILL.md`. The submission domain follows the same pattern. The placeholder `README.md` is deleted (not kept as a redirect), because: + +1. The router `SKILL.md`'s submission table becomes the authoritative index, exactly as it is for the form domain. +2. Keeping a `README.md` would create two places to discover submission references — a drift hazard the form domain deliberately avoids. +3. The `formio-schema-skill` spec's "Placeholder domains route to formio-api" scenario is dropped for `submission` (kept for `project`) — that scenario was for *unauthored* domains, which submission no longer is. + +**Alternative considered:** Keep `submission/README.md` as a one-screen TOC. Rejected — duplicates the router and rots on every file rename. + +### Decision: Five reference files, one per logical concern + +| File | Covers | +| ---------------------------- | ---------------------------------------------------------------------- | +| `submission-definition.md` | The top-level `Submission` envelope and every property on it | +| `submission-state.md` | `state` lifecycle: `draft` vs `submitted`, when each is set | +| `submission-metadata.md` | The `metadata` bag — documented keys plus extension contract | +| `submission-access.md` | Row-level `access` array, all `AccessType` values, layering with form | +| `submission-data.md` | The `data` envelope, key paths, nesting, cross-links to form refs | + +**Why five not three or one:** The form domain has five files because each one is small enough to load on its own. Bundling `state` + `metadata` into `submission-definition.md` would push that file past the 200-line line. Splitting `data` out is non-negotiable because it's the largest concept and the only one that cross-references the form domain. + +**Why not one per `SubmissionState` value or one per `AccessType`:** Over-partitioning. A page per state value would be three lines each. + +### Decision: Document `"draft"` as a valid `state` value despite the TypeScript narrowing it to `"submitted"` + +The user guide explicitly identifies `"draft"` as the value for unsaved submissions, and the platform writes that value to the database. The reference is for consumers reading and writing submission JSON — it must match what the runtime actually produces, not what one TypeScript declaration happens to narrow it to. The reference will state both values, then add a one-line note: "Note: the upstream TypeScript type currently declares only `'submitted'`; treat that as a known narrowing bug, not an authoritative restriction." + +**Alternative considered:** Match the TypeScript exactly. Rejected — would teach users that `state: "draft"` is invalid when they have it in their database. + +### Decision: Cross-link `submission-data.md` to the form domain rather than duplicate component value shapes + +The form references already document, per component type, what gets stored in submission data (e.g., `signature` stores a base64 PNG data URL; `file` stores `[{ storage, name, url, size, type, originalName, hash }]`; `datagrid` stores an array of row objects). Reproducing that in `submission-data.md` would mean two places must update when, say, a new component type ships. Instead `submission-data.md` documents the envelope (key paths, nesting model, address discriminated union as the worked example) and cross-links to the form references for per-component value shapes. + +### Decision: Tests assert structural presence, not exact property lists + +The layout test today asserts file existence + non-emptiness + a couple of substring expectations (e.g., "not yet authored"). For the authored submission references, the test will assert: + +- Each of the five `submission/*.md` files exists. +- Each is non-empty. +- Each carries the canonical top-level `# ` heading expected for it (e.g., `submission-definition.md` starts with `# Submission Definition Reference`). +- `submission-definition.md` mentions every declared TS property name (`_id`, `_fvid`, `form`, `owner`, `roles`, `metadata`, `data`, `project`, `state`, `access`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted`) somewhere in its body. This is a regression net for the case where a property table row is accidentally deleted, without coupling to the exact format of the table. +- `submission-state.md` mentions both `draft` and `submitted`. +- `submission-access.md` mentions every `AccessType` value. +- `submission-metadata.md` mentions every documented `SubmissionMetadata` key. +- The router SKILL.md body links to all five new reference paths. + +This is the same depth of structural test as the existing form-domain assertions in the layout file. + +### Decision: Keep the project placeholder unchanged + +The proposal explicitly scopes to the submission domain. The `references/project/README.md` placeholder stays. The existing layout test's "Placeholder domains route to formio-api" assertion narrows from "submission and project" to "project only" — submission is no longer a placeholder. + +### Decision: No frontmatter on submission reference files + +Same rule as the form domain. Only `SKILL.md` carries YAML frontmatter; reference files are pure Markdown. The layout test will gain a per-file assertion that none of the five new files starts with `---`. + +## Risks / Trade-offs + +- **Risk:** The TypeScript source at `~/Documents/formio/modules/nirvana/packages/core/src/types/Submission.ts` evolves and the reference drifts. → **Mitigation:** Author the references in the user-guide voice (what a developer experiences), not as a 1:1 type mirror. The structural test catches deletions but not additions; additions are caught by the standard skill-iteration practice (re-read the type when revisiting the skill). Codegen is explicitly out of scope. +- **Risk:** Documenting `"draft"` as a state value diverges from the upstream TypeScript and could be reverted by someone "fixing" the docs to match the type. → **Mitigation:** The reference contains an explicit one-line callout that the TS narrowing is a known gap, not authoritative. A reviewer who tries to delete `draft` will hit that callout. +- **Risk:** The cross-links from `submission-data.md` to `references/form/*` files break if the form references are renamed. → **Mitigation:** Cross-links use relative paths within the same skill. A rename of any form reference file would already break the router SKILL.md table and be caught by the existing layout test that asserts every `references/form/*.md` path in the router body. +- **Trade-off:** Five files is more navigation than one. We accept that — load-only-what-you-need is the form domain's pattern and the schema skill is designed to be selectively loaded. +- **Trade-off:** We are not authoring the project domain in this change. The proposal scope is intentionally narrow so review is small. Authoring `project/` is a follow-up. + +## Migration Plan + +1. Implement the five reference files, the router SKILL.md update, and the test updates on a feature branch. +2. Delete `plugin/skills/formio-schema/references/submission/README.md`. +3. Run `pnpm test`, `pnpm lint`, `pnpm format` — Definition of Done. +4. Merge. Next plugin build picks up the new references; no runtime migration required. +5. Rollback: revert the merge commit; the placeholder `README.md` returns and the router table reverts to the single-row placeholder. diff --git a/openspec/changes/archive/2026-05-26-submission-schema-reference/proposal.md b/openspec/changes/archive/2026-05-26-submission-schema-reference/proposal.md new file mode 100644 index 0000000..500fa02 --- /dev/null +++ b/openspec/changes/archive/2026-05-26-submission-schema-reference/proposal.md @@ -0,0 +1,36 @@ +## Why + +The `formio-schema` skill already partitions its `references/` tree by domain and ships a placeholder `references/submission/README.md` that punts to `formio-api`'s `runtime-submissions` endpoint reference. That placeholder is good enough for someone who wants to call the submission endpoint, but it does nothing for someone who wants to *interpret* or *construct* a Form.io submission JSON document — what `data` looks like for each component type, what `state` values are valid, what `metadata` carries, when `roles`/`owner` are populated, what `access` overrides at the row level, what `_fvid` ties to. Today Claude has no first-party answer when a user asks "what's in this submission JSON?" beyond a hand-wave back to the API endpoint. + +The Form.io platform's own TypeScript source of truth for the submission shape lives at `~/Documents/formio/modules/nirvana/packages/core/src/types/Submission.ts`. Pairing that file against the user-facing user guide at `https://help.form.io/userguide/submissions` is enough to author a real reference now — no further research needed. + +## What Changes + +- Promote the submission domain from "not yet authored" placeholder to a fully authored reference set. Replace `plugin/skills/formio-schema/references/submission/README.md` with one or more proper reference files structured to mirror the form domain (one file per logical sub-topic, no YAML frontmatter, property tables for each documented type). +- Author the following submission reference files under `plugin/skills/formio-schema/references/submission/`: + - `submission-definition.md` — top-level `Submission` envelope: `_id`, `_fvid`, `form`, `project`, `owner`, `roles`, `state`, `access`, `metadata`, `data`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted`. + - `submission-metadata.md` — `SubmissionMetadata` shape, including documented properties (`timezone`, `offset`, `origin`, `referrer`, `browserName`, `userAgent`, `pathName`, `onLine`, `language`, `headers`, `ssoteam`, `memberCount`, `selectData`) and the open-ended `[key: string]: any` extension contract. + - `submission-state.md` — `SubmissionState` lifecycle: `"draft"` (unsaved, server-persisted while user is editing) vs `"submitted"` (completed). Note the discrepancy that the TypeScript source declares only `"submitted"` but the user guide and production data also recognize `"draft"`; document both as valid string values a consumer may encounter. + - `submission-access.md` — row-level `access` array of `{ type, roles, resources? }` entries, listing all `AccessType` values (`self`, `create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`, `team_read`, `team_write`, `team_admin`, `team_access`). Document how this layers on top of the form's `submissionAccess` (form-level) — row-level overrides per-submission. + - `submission-data.md` — the `data` object: how component `key`s map to data paths, how container / datagrid / editgrid / datamap / form components nest, how addresses store either autocomplete or manual mode, how `select` resource references store an embedded submission vs. an ID, how `file` components store array-of-upload-record shapes, and the relationship to `form.components` for interpreting any given `data` blob. +- Update `plugin/skills/formio-schema/SKILL.md`: + - Promote the submission row in the "Submissions and Projects" table from a single "not yet authored" placeholder to a domain heading with its own multi-row reference table (mirroring the existing form-domain layout). + - Update the trigger clause so submission-specific prompts (interpreting a submission, understanding `state`/`metadata`/`data` shape, decoding row-level access) clearly activate the skill. +- Update existing tests in `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts`: + - The `references/` directory layout test still asserts the subdirectories exactly `form/`, `submission/`, `project/`. + - The submission placeholder test is replaced by an authored-reference test asserting each new `submission/*.md` file exists, is non-empty, and contains the headings expected by the spec. + - Project placeholder remains as-is. + +## Capabilities + +### Modified Capabilities + +- `formio-schema-skill`: Adds requirements that the submission domain has authored reference files (not a placeholder), enumerates the required submission reference files, and constrains the router SKILL.md to enumerate every submission reference (not a single placeholder row). + +## Impact + +- **Skill content**: `plugin/skills/formio-schema/references/submission/` gains five authored reference files. `README.md` is removed (or kept only as a brief index pointing at the five files — the spec will pick one; the design doc records the decision). +- **Skill router**: `plugin/skills/formio-schema/SKILL.md` gains a submission-domain reference table and updated trigger clause. +- **Tests**: `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts` updated — submission placeholder assertion replaced by submission authored-reference assertions; project placeholder assertion unchanged. +- **No code changes**: No MCP tool, server, or build script changes are needed for this proposal. +- **External research**: The TypeScript type at `~/Documents/formio/modules/nirvana/packages/core/src/types/Submission.ts` (and its imports: `Access`, `DataObject`, `FormId`, `RoleId`, `ProjectId`) and the user guide at `https://help.form.io/userguide/submissions` are the only inputs needed. diff --git a/openspec/changes/archive/2026-05-26-submission-schema-reference/specs/formio-schema-skill/spec.md b/openspec/changes/archive/2026-05-26-submission-schema-reference/specs/formio-schema-skill/spec.md new file mode 100644 index 0000000..34989ed --- /dev/null +++ b/openspec/changes/archive/2026-05-26-submission-schema-reference/specs/formio-schema-skill/spec.md @@ -0,0 +1,115 @@ +## ADDED Requirements + +### Requirement: Authored submission-domain references + +`plugin/skills/formio-schema/references/submission/` SHALL contain exactly the following authored reference files, each non-empty and each carrying NO YAML frontmatter: + +- `submission-definition.md` +- `submission-state.md` +- `submission-metadata.md` +- `submission-access.md` +- `submission-data.md` + +`plugin/skills/formio-schema/references/submission/README.md` SHALL NOT exist — the router `SKILL.md` is the authoritative submission index. + +#### Scenario: Every submission reference file exists and is non-empty + +- **WHEN** `plugin/skills/formio-schema/references/submission/` is listed +- **THEN** it SHALL contain `submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, and `submission-data.md` +- **AND** each file SHALL be non-empty +- **AND** none of those files SHALL begin with `---` +- **AND** `submission/README.md` SHALL NOT exist + +#### Scenario: submission-definition.md documents every top-level property + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-definition.md` is read +- **THEN** its body SHALL mention each of these property names: `_id`, `_fvid`, `form`, `project`, `owner`, `roles`, `state`, `access`, `metadata`, `data`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted` + +#### Scenario: submission-state.md documents both lifecycle values + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-state.md` is read +- **THEN** its body SHALL mention the string value `draft` and the string value `submitted` + +#### Scenario: submission-metadata.md documents every documented metadata key + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-metadata.md` is read +- **THEN** its body SHALL mention each of these keys: `timezone`, `offset`, `origin`, `referrer`, `browserName`, `userAgent`, `pathName`, `onLine`, `language`, `headers`, `ssoteam`, `memberCount`, `selectData` +- **AND** the body SHALL state that the metadata object is extensible (open-ended) + +#### Scenario: submission-access.md documents every AccessType value + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-access.md` is read +- **THEN** its body SHALL mention each of these access type values: `self`, `create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`, `team_read`, `team_write`, `team_admin`, `team_access` + +#### Scenario: submission-data.md cross-links to the form references for per-component value shapes + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-data.md` is read +- **THEN** its body SHALL reference at least one path under `references/form/` (e.g., `references/form/input-components.md` or `references/form/data-components.md`) + +### Requirement: Router SKILL.md indexes the submission domain + +`plugin/skills/formio-schema/SKILL.md` SHALL list the submission domain as an authored domain — not as a "not yet authored" placeholder — with its own multi-row reference table (or equivalent structured list) enumerating all five authored submission references. The submission row SHALL NOT appear in any "not yet authored" table or paragraph. + +#### Scenario: Router enumerates every submission reference + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/submission/submission-definition.md`, `references/submission/submission-state.md`, `references/submission/submission-metadata.md`, `references/submission/submission-access.md`, and `references/submission/submission-data.md` +- **AND** its body SHALL NOT reference the path `references/submission/README.md` + +## MODIFIED Requirements + +### Requirement: Domain-partitioned references directory + +The skill's reference documents SHALL live under `plugin/skills/formio-schema/references/<domain>/`, with one subdirectory per Form.io schema domain. The directory layout SHALL be additive: adding a new schema domain creates a new subdirectory under `references/` rather than placing files at the top level. + +The set of domains the skill owns SHALL be exactly `form`, `submission`, and `project`. Action JSON and role JSON SHALL NOT be domains of this skill — there SHALL be no `references/action/` or `references/role/` subdirectory. + +`plugin/skills/formio-schema/references/form/` SHALL contain the form-domain references previously located at `plugin/skills/formio-schema/references/`: + +- `form-definition.md` +- `base-component.md` +- `input-components.md` +- `layout-components.md` +- `data-components.md` + +`plugin/skills/formio-schema/references/submission/` SHALL contain authored reference files (`submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, `submission-data.md`) — NOT a placeholder `README.md`. + +`plugin/skills/formio-schema/references/` SHALL NOT contain any `.md` file directly (only subdirectories). + +The placeholder schema domain `project` SHALL be present as a subdirectory containing at minimum a `README.md` that describes what that domain will document and directs the user to the corresponding `formio-api` reference until the domain is authored. The `submission` domain is no longer a placeholder. + +#### Scenario: References live under domain subdirectories + +- **WHEN** `plugin/skills/formio-schema/references/` is listed +- **THEN** its subdirectories SHALL be exactly `form/`, `submission/`, and `project/` +- **AND** it SHALL NOT contain `action/` or `role/` subdirectories +- **AND** it SHALL NOT contain any `.md` files at its top level + +#### Scenario: Form domain has the full reference set + +- **WHEN** `plugin/skills/formio-schema/references/form/` is listed +- **THEN** it SHALL contain `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, and `data-components.md` +- **AND** every file in `references/form/` SHALL be non-empty + +#### Scenario: Project placeholder routes to formio-api + +- **WHEN** `plugin/skills/formio-schema/references/project/README.md` is read +- **THEN** the README SHALL be non-empty +- **AND** the README SHALL state that the domain is not yet authored +- **AND** the README SHALL name the `formio-api` reference `platform-projects` as the interim source of truth + +### Requirement: Router SKILL.md indexes every domain + +`plugin/skills/formio-schema/SKILL.md` SHALL contain a table (or equivalent structured list) that maps each domain to its references. The form domain SHALL list all five form references with their relative paths under `references/form/`. The submission domain SHALL list all five authored submission references with their relative paths under `references/submission/`. The project domain SHALL be listed with a "not yet authored" status and a pointer to the placeholder `README.md` plus the `formio-api` `platform-projects` reference. The router body SHALL NOT list `action` or `role` as domains owned by this skill. + +#### Scenario: Router enumerates all five form-domain references + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/form/form-definition.md`, `references/form/base-component.md`, `references/form/input-components.md`, `references/form/layout-components.md`, and `references/form/data-components.md` + +#### Scenario: Router enumerates the project placeholder and excludes action/role + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the path `references/project/README.md` +- **AND** its body SHALL NOT reference `references/action/` or `references/role/` +- **AND** its body SHALL NOT reference `references/submission/README.md` diff --git a/openspec/changes/archive/2026-05-26-submission-schema-reference/tasks.md b/openspec/changes/archive/2026-05-26-submission-schema-reference/tasks.md new file mode 100644 index 0000000..6bf293c --- /dev/null +++ b/openspec/changes/archive/2026-05-26-submission-schema-reference/tasks.md @@ -0,0 +1,53 @@ +## 1. Author submission reference files +<!-- depends_on: none --> + +### Red + +- [x] 1.1 Update `packages/mcp-server/src/__tests__/formio-schema-layout.test.ts` so the "placeholder domains" loop only covers `project`, and add a new `describe` block asserting `plugin/skills/formio-schema/references/submission/` contains exactly `submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, `submission-data.md`, that none of them carry YAML frontmatter, and that `submission/README.md` does not exist — these assertions should fail against the current placeholder layout +- [x] 1.2 Extend the new submission `describe` block with body assertions: `submission-definition.md` mentions every top-level Submission property name (`_id`, `_fvid`, `form`, `project`, `owner`, `roles`, `state`, `access`, `metadata`, `data`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted`); `submission-state.md` mentions both `draft` and `submitted`; `submission-metadata.md` mentions every documented metadata key (`timezone`, `offset`, `origin`, `referrer`, `browserName`, `userAgent`, `pathName`, `onLine`, `language`, `headers`, `ssoteam`, `memberCount`, `selectData`) plus the word "extensible" (or equivalent); `submission-access.md` mentions every AccessType value (`self`, `create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`, `team_read`, `team_write`, `team_admin`, `team_access`); `submission-data.md` references at least one path under `references/form/` + +### Green + +- [x] 1.3 Delete `plugin/skills/formio-schema/references/submission/README.md` +- [x] 1.4 Author `plugin/skills/formio-schema/references/submission/submission-definition.md` with a top-level heading, an intro paragraph defining what a submission is (per the user guide), a property table covering every field declared on the TypeScript `Submission` interface (`_id`, `_fvid`, `form`, `project`, `owner`, `roles`, `state`, `access`, `metadata`, `data`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted`) with type / required / description columns, and cross-links to `submission-state.md`, `submission-metadata.md`, `submission-access.md`, and `submission-data.md` +- [x] 1.5 Author `plugin/skills/formio-schema/references/submission/submission-state.md` documenting both `"draft"` (unsaved/in-progress) and `"submitted"` (completed) state values, when each is written, the user-guide note about draft state, and the one-line callout that the upstream TypeScript currently declares only `"submitted"` as a known narrowing gap +- [x] 1.6 Author `plugin/skills/formio-schema/references/submission/submission-metadata.md` with the `SubmissionMetadata` shape, a property table covering every documented key (`ssoteam`, `memberCount`, `selectData`, `timezone`, `offset`, `origin`, `referrer`, `browserName`, `userAgent`, `pathName`, `onLine`, `language`, `headers`), and a callout that the object is extensible via an open `[key: string]: any` index signature +- [x] 1.7 Author `plugin/skills/formio-schema/references/submission/submission-access.md` documenting the row-level `access` array shape (`{ type, roles, resources? }`), enumerating every AccessType value with a short description of each, and explaining how the row-level `access` array overrides the form-level `submissionAccess` for that specific submission +- [x] 1.8 Author `plugin/skills/formio-schema/references/submission/submission-data.md` documenting the `data` envelope: key-to-path mapping, nesting under container / datagrid / editgrid / datamap / nested form / address components, the address discriminated union (autocomplete vs manual mode) as the worked example, and cross-links to `references/form/input-components.md` and `references/form/data-components.md` for per-component value shapes + +### Refactor + +- [x] 1.9 Review implementation and refactor as needed + +## 2. Update router SKILL.md to index the submission domain +<!-- depends_on: 1 --> + +### Red + +- [x] 2.1 Extend the layout test's existing "body indexes every domain" `describe` block: add an assertion that the router body references all five `references/submission/submission-*.md` paths, and an assertion that the router body does NOT reference `references/submission/README.md` +- [x] 2.2 Update the existing "Router enumerates the placeholder domains" assertion to expect only the project placeholder (`references/project/README.md`); the submission domain is no longer a placeholder + +### Green + +- [x] 2.3 Edit `plugin/skills/formio-schema/SKILL.md`: split the "Submissions and Projects" section into a "Submissions" subsection with a multi-row table listing every submission reference (path + one-line "Working on…" cue) and a separate "Projects" subsection containing the still-placeholder project row; update the trigger clause so submission-specific phrases (interpreting a submission, decoding submission `data`, row-level access, draft state, submission metadata) clearly activate the skill + +### Refactor + +- [x] 2.4 Review implementation and refactor as needed + +## 3. Verify Definition of Done +<!-- depends_on: 1, 2 --> + +### Red + +- [x] 3.1 (No new tests — verification step only) + +### Green + +- [x] 3.2 Run `pnpm test` and confirm all suites pass +- [x] 3.3 Run `pnpm lint` (typecheck) and confirm zero errors +- [x] 3.4 Run `pnpm format` and confirm the working tree stays clean + +### Refactor + +- [x] 3.5 Review implementation and refactor as needed diff --git a/openspec/specs/claude-plugin-packaging/spec.md b/openspec/specs/claude-plugin-packaging/spec.md index 91e967e..b2f63b0 100644 --- a/openspec/specs/claude-plugin-packaging/spec.md +++ b/openspec/specs/claude-plugin-packaging/spec.md @@ -29,12 +29,13 @@ The `plugin/package.json` SHALL set `name` to `@formio/ai`, `publishConfig.acces ### Requirement: Plugin bundles the skills library -The plugin source tree SHALL include, and the build SHALL copy to `dist/plugin/skills/`, the full `formio-api` router skill, every `formio-api-<group>` capability-group skill, and the `formio-form`, `formio-schema`, and `formio-resource-planner` skills. +The plugin source tree SHALL include, and the build SHALL copy to `dist/plugin/skills/`, the full `formio-api` router skill, every `formio-api-<group>` capability-group skill, and the `formio-schema` and `formio-resource-planner` skills. The bundled set SHALL NOT include `formio-form`. #### Scenario: Installed plugin exposes all skills - **WHEN** a user installs `@formio/ai` in a Claude Code project -- **THEN** Claude Code discovers every `formio-api`, `formio-form`, `formio-schema`, and `formio-resource-planner` skill from the plugin's `skills/` directory +- **THEN** Claude Code discovers every `formio-api`, `formio-schema`, and `formio-resource-planner` skill from the plugin's `skills/` directory +- **AND** no skill named `formio-form` is present in the bundled `skills/` directory ### Requirement: Build script produces a self-contained plugin tree @@ -52,7 +53,7 @@ A `scripts/build-plugin.ts` script SHALL clean `dist/plugin/`, copy the `plugin/ ### Requirement: Smoke test validates the built plugin over stdio -A `scripts/test-plugin.ts` script SHALL validate that `dist/plugin/` exists, that `plugin.json` contains required fields (`name`, `version`, `description`, at least one `mcpServers` entry), that required skill directories (at minimum `formio-api` and `formio-form`) are present, and that spawning the bundled server and sending a JSON-RPC `tools/list` request returns a well-formed response. +A `scripts/test-plugin.ts` script SHALL validate that `dist/plugin/` exists, that `plugin.json` contains required fields (`name`, `version`, `description`, at least one `mcpServers` entry), that required skill directories (at minimum `formio-api` and `formio-schema`) are present, and that spawning the bundled server and sending a JSON-RPC `tools/list` request returns a well-formed response. The smoke test SHALL NOT require `formio-form` to be present. #### Scenario: Smoke test fails when build is missing @@ -64,6 +65,12 @@ A `scripts/test-plugin.ts` script SHALL validate that `dist/plugin/` exists, tha - **WHEN** the smoke test spawns `dist/plugin/server/stdio.mjs` and sends a `tools/list` JSON-RPC request - **THEN** the server responds with a `result.tools` array and the test exits zero +#### Scenario: Smoke test asserts on the consolidated schema skill + +- **WHEN** the smoke test inspects the bundled `skills/` directory +- **THEN** it SHALL assert `formio-schema` is present +- **AND** it SHALL NOT assert `formio-form` is present + ### Requirement: Plugin ships a README documenting environment variables The plugin source tree SHALL include `plugin/README.md` — copied by the build into `dist/plugin/README.md` and therefore published with `@formio/ai` — that documents every environment variable the bundled MCP server reads, marking each as required or optional and stating its default. At minimum the README SHALL list `FORMIO_PROJECT_URL` (required, no default), `FORMIO_API_KEY` (optional, default `undefined`), and `FORMIO_LOGIN_FORM` (optional, default `${FORMIO_PROJECT_URL}/user/login`), and SHALL describe the API-key vs. JWT authentication modes selected by presence/absence of `FORMIO_API_KEY`. diff --git a/openspec/specs/form-create/spec.md b/openspec/specs/form-create/spec.md index e31ac00..c87c267 100644 --- a/openspec/specs/form-create/spec.md +++ b/openspec/specs/form-create/spec.md @@ -2,13 +2,14 @@ ### Requirement: form_create tool is registered with skill-referencing description -The `form_create` tool SHALL be registered on the MCP server with a description that instructs the LLM to use the `formio-form` skill to construct the form JSON definition before calling this tool. The description SHALL reference the skill by name so the LLM knows to invoke it for schema guidance. +The `form_create` tool SHALL be registered on the MCP server with a description that instructs the LLM to use the `formio-schema` skill to construct the form JSON definition before calling this tool. The description SHALL reference the skill by name so the LLM knows to invoke it for schema guidance. The description SHALL NOT reference `formio-form`. #### Scenario: Tool appears in tool listing with skill reference - **WHEN** the MCP server is initialized with valid configuration - **THEN** the `form_create` tool is available with a required `form` parameter accepting a JSON object -- **AND** the tool description instructs the LLM to use the `formio-form` skill to build the form JSON +- **AND** the tool description instructs the LLM to use the `formio-schema` skill to build the form JSON +- **AND** the tool description does not contain the string `formio-form` ### Requirement: form_create accepts a form definition with required fields diff --git a/openspec/specs/form-update/spec.md b/openspec/specs/form-update/spec.md index 84f4369..bffb872 100644 --- a/openspec/specs/form-update/spec.md +++ b/openspec/specs/form-update/spec.md @@ -2,13 +2,14 @@ ### Requirement: form_update tool is registered with workflow guidance -The `form_update` tool SHALL be registered on the MCP server with a description that instructs the LLM to: (1) fetch the current form via `form_get`, (2) use the `formio-form` skill to apply the requested modifications, and (3) call `form_update` with the complete updated form JSON. +The `form_update` tool SHALL be registered on the MCP server with a description that instructs the LLM to: (1) fetch the current form via `form_get`, (2) use the `formio-schema` skill to apply the requested modifications, and (3) call `form_update` with the complete updated form JSON. The description SHALL NOT reference `formio-form`. #### Scenario: Tool appears in tool listing with workflow guidance - **WHEN** the MCP server is initialized with valid configuration - **THEN** the `form_update` tool is available with required `formId` and `form` parameters -- **AND** the tool description references the `form_get` tool and `formio-form` skill +- **AND** the tool description references the `form_get` tool and `formio-schema` skill +- **AND** the tool description does not contain the string `formio-form` ### Requirement: form_update accepts a form ID and updated form definition diff --git a/openspec/specs/formio-schema-skill/spec.md b/openspec/specs/formio-schema-skill/spec.md new file mode 100644 index 0000000..8743a70 --- /dev/null +++ b/openspec/specs/formio-schema-skill/spec.md @@ -0,0 +1,202 @@ +## ADDED Requirements + +### Requirement: Consolidated Form.io JSON schema skill + +The `formio-schema` skill SHALL be the single Form.io JSON schema reference skill in the library. The repository SHALL NOT contain a separate `formio-form` skill; `plugin/skills/formio-form/` SHALL not exist. + +`plugin/skills/formio-schema/SKILL.md` SHALL be the router entry point, carrying YAML frontmatter with at least `name: formio-schema` and a `description` that: + +1. States the skill covers Form.io JSON schemas for project, form (and resource), and submission documents. It SHALL NOT claim to cover action or role JSON — action JSON is owned by the dedicated `formio-actions` skill, and role JSON is handled by `formio-api`'s `project-roles` reference. +2. Includes a "Use when…" trigger clause listing the form-builder phrases that previously activated `formio-form` (e.g., `components`, `wizard`, `textfield`, `datagrid`) so existing form-authoring prompts still activate the skill. +3. Includes a "Not for:" negative-trigger clause disambiguating from `formio-api`, `formio-actions`, `formio-resource-planner`, and `formio-application`. The clause SHALL NOT mention `formio-form`. + +#### Scenario: formio-form skill is removed + +- **WHEN** the repository is inspected +- **THEN** `plugin/skills/formio-form/` SHALL NOT exist +- **AND** no file under `plugin/skills/` SHALL reference `formio-form` by name + +#### Scenario: formio-schema router has multi-domain description + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its frontmatter `name` SHALL equal `formio-schema` +- **AND** its frontmatter `description` SHALL mention the domains "submission" and "project" alongside "form" +- **AND** its frontmatter `description` SHALL include a "Not for:" clause that names `formio-api`, `formio-actions`, `formio-resource-planner`, and `formio-application` +- **AND** its frontmatter `description` SHALL NOT contain the string `formio-form` + +### Requirement: Domain-partitioned references directory + +The skill's reference documents SHALL live under `plugin/skills/formio-schema/references/<domain>/`, with one subdirectory per Form.io schema domain. The directory layout SHALL be additive: adding a new schema domain creates a new subdirectory under `references/` rather than placing files at the top level. + +The set of domains the skill owns SHALL be exactly `form`, `submission`, and `project`. Action JSON and role JSON SHALL NOT be domains of this skill — there SHALL be no `references/action/` or `references/role/` subdirectory. + +`plugin/skills/formio-schema/references/form/` SHALL contain the form-domain references previously located at `plugin/skills/formio-schema/references/`: + +- `form-definition.md` +- `base-component.md` +- `input-components.md` +- `layout-components.md` +- `data-components.md` + +`plugin/skills/formio-schema/references/submission/` SHALL contain authored reference files (`submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, `submission-data.md`) — NOT a placeholder `README.md`. + +`plugin/skills/formio-schema/references/project/` SHALL contain authored reference files (`project-definition.md`, `project-type-and-framework.md`, `project-settings.md`, `project-access.md`) — NOT a placeholder `README.md`. The project domain SHALL NOT include a billing-and-usage reference file. + +`plugin/skills/formio-schema/references/` SHALL NOT contain any `.md` file directly (only subdirectories). + +No domain owned by this skill SHALL be a placeholder — every domain subdirectory SHALL contain authored reference files. + +#### Scenario: References live under domain subdirectories + +- **WHEN** `plugin/skills/formio-schema/references/` is listed +- **THEN** its subdirectories SHALL be exactly `form/`, `submission/`, and `project/` +- **AND** it SHALL NOT contain `action/` or `role/` subdirectories +- **AND** it SHALL NOT contain any `.md` files at its top level + +#### Scenario: Form domain has the full reference set + +- **WHEN** `plugin/skills/formio-schema/references/form/` is listed +- **THEN** it SHALL contain `form-definition.md`, `base-component.md`, `input-components.md`, `layout-components.md`, and `data-components.md` +- **AND** every file in `references/form/` SHALL be non-empty + +### Requirement: Router SKILL.md indexes every domain + +`plugin/skills/formio-schema/SKILL.md` SHALL contain a table (or equivalent structured list) that maps each domain to its references. The form domain SHALL list all five form references with their relative paths under `references/form/`. The submission domain SHALL list all five authored submission references with their relative paths under `references/submission/`. The project domain SHALL list all four authored project references with their relative paths under `references/project/`. The router body SHALL NOT list `action` or `role` as domains owned by this skill. The router body SHALL NOT reference any `<domain>/README.md` placeholder path. + +#### Scenario: Router enumerates all five form-domain references + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/form/form-definition.md`, `references/form/base-component.md`, `references/form/input-components.md`, `references/form/layout-components.md`, and `references/form/data-components.md` + +#### Scenario: Router excludes action/role and placeholder README paths + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL NOT reference `references/action/` or `references/role/` +- **AND** its body SHALL NOT reference `references/submission/README.md` or `references/project/README.md` + +### Requirement: Authored submission-domain references + +`plugin/skills/formio-schema/references/submission/` SHALL contain exactly the following authored reference files, each non-empty and each carrying NO YAML frontmatter: + +- `submission-definition.md` +- `submission-state.md` +- `submission-metadata.md` +- `submission-access.md` +- `submission-data.md` + +`plugin/skills/formio-schema/references/submission/README.md` SHALL NOT exist — the router `SKILL.md` is the authoritative submission index. + +#### Scenario: Every submission reference file exists and is non-empty + +- **WHEN** `plugin/skills/formio-schema/references/submission/` is listed +- **THEN** it SHALL contain `submission-definition.md`, `submission-state.md`, `submission-metadata.md`, `submission-access.md`, and `submission-data.md` +- **AND** each file SHALL be non-empty +- **AND** none of those files SHALL begin with `---` +- **AND** `submission/README.md` SHALL NOT exist + +#### Scenario: submission-definition.md documents every top-level property + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-definition.md` is read +- **THEN** its body SHALL mention each of these property names: `_id`, `_fvid`, `form`, `project`, `owner`, `roles`, `state`, `access`, `metadata`, `data`, `externalIds`, `externalTokens`, `permission`, `created`, `modified`, `deleted` + +#### Scenario: submission-state.md documents both lifecycle values + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-state.md` is read +- **THEN** its body SHALL mention the string value `draft` and the string value `submitted` + +#### Scenario: submission-metadata.md documents every documented metadata key + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-metadata.md` is read +- **THEN** its body SHALL mention each of these keys: `timezone`, `offset`, `origin`, `referrer`, `browserName`, `userAgent`, `pathName`, `onLine`, `language`, `headers`, `ssoteam`, `memberCount`, `selectData` +- **AND** the body SHALL state that the metadata object is extensible (open-ended) + +#### Scenario: submission-access.md documents every AccessType value + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-access.md` is read +- **THEN** its body SHALL mention each of these access type values: `self`, `create_own`, `create_all`, `read_own`, `read_all`, `update_own`, `update_all`, `delete_own`, `delete_all`, `team_read`, `team_write`, `team_admin`, `team_access` + +#### Scenario: submission-data.md cross-links to the form references for per-component value shapes + +- **WHEN** `plugin/skills/formio-schema/references/submission/submission-data.md` is read +- **THEN** its body SHALL reference at least one path under `references/form/` (e.g., `references/form/input-components.md` or `references/form/data-components.md`) + +### Requirement: Router SKILL.md indexes the submission domain + +`plugin/skills/formio-schema/SKILL.md` SHALL list the submission domain as an authored domain — not as a "not yet authored" placeholder — with its own multi-row reference table (or equivalent structured list) enumerating all five authored submission references. The submission row SHALL NOT appear in any "not yet authored" table or paragraph. + +#### Scenario: Router enumerates every submission reference + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/submission/submission-definition.md`, `references/submission/submission-state.md`, `references/submission/submission-metadata.md`, `references/submission/submission-access.md`, and `references/submission/submission-data.md` +- **AND** its body SHALL NOT reference the path `references/submission/README.md` + +### Requirement: Authored project-domain references + +`plugin/skills/formio-schema/references/project/` SHALL contain exactly the following authored reference files, each non-empty and each carrying NO YAML frontmatter: + +- `project-definition.md` +- `project-type-and-framework.md` +- `project-settings.md` +- `project-access.md` + +Billing and usage statistics SHALL NOT be a separate authored reference. The `billing`, `apiCalls`, `trial`, and `lastDeploy` fields appear as one-line rows in `project-definition.md`'s property table and SHALL NOT receive a dedicated reference file — they are operator/SaaS concerns, not schema-authoring concerns. + +`plugin/skills/formio-schema/references/project/project-billing-and-usage.md` SHALL NOT exist. + +`plugin/skills/formio-schema/references/project/README.md` SHALL NOT exist — the router `SKILL.md` is the authoritative project index. + +#### Scenario: Every project reference file exists and is non-empty + +- **WHEN** `plugin/skills/formio-schema/references/project/` is listed +- **THEN** it SHALL contain `project-definition.md`, `project-type-and-framework.md`, `project-settings.md`, and `project-access.md` +- **AND** each file SHALL be non-empty +- **AND** none of those files SHALL begin with `---` +- **AND** `project/README.md` SHALL NOT exist +- **AND** `project/project-billing-and-usage.md` SHALL NOT exist + +#### Scenario: project-definition.md documents every top-level property + +- **WHEN** `plugin/skills/formio-schema/references/project/project-definition.md` is read +- **THEN** its body SHALL mention each of these property names: `_id`, `title`, `name`, `type`, `description`, `tag`, `owner`, `externalOwner`, `project`, `remote`, `plan`, `billing`, `apiCalls`, `steps`, `framework`, `primary`, `access`, `trial`, `lastDeploy`, `stageTitle`, `machineName`, `config`, `protect`, `settings`, `remoteSecret`, `builderConfig`, `formDefaults`, `public`, `created`, `modified`, `deleted` + +#### Scenario: project-type-and-framework.md enumerates every type and framework value + +- **WHEN** `plugin/skills/formio-schema/references/project/project-type-and-framework.md` is read +- **THEN** its body SHALL mention each ProjectType value: `project`, `stage`, `tenant` +- **AND** its body SHALL mention each ProjectFramework value: `angular`, `angular2`, `react`, `vue`, `html5`, `simple`, `custom`, `aurelia`, `javascript` + +#### Scenario: project-type-and-framework.md documents the Stage and Tenant creation patterns + +- **WHEN** `plugin/skills/formio-schema/references/project/project-type-and-framework.md` is read +- **THEN** its body SHALL contain the literal string `"type": "stage"` +- **AND** its body SHALL contain the literal string `"type": "tenant"` +- **AND** its body SHALL state that a Stage's `project` field is set to the parent project's ObjectId +- **AND** its body SHALL state that the parent is typically the portal (or primary) project + +#### Scenario: project-definition.md notes deployed projects use plan "commercial" + +- **WHEN** `plugin/skills/formio-schema/references/project/project-definition.md` is read +- **THEN** its body SHALL mention the string value `commercial` in the context of the `plan` field + +#### Scenario: project-settings.md documents every ProjectSettings key and the encryption contract + +- **WHEN** `plugin/skills/formio-schema/references/project/project-settings.md` is read +- **THEN** its body SHALL mention each of these keys: `appOrigin`, `keys`, `cors`, `csp`, `secret`, `pdfserver`, `filetoken`, `allowConfig`, `allowConfigToForms`, `custom`, `formModule`, `email`, `captcha`, `recaptcha`, `esign`, `google`, `kickbox`, `sqlconnector`, `storage`, `tokenParse`, `oauth`, `ldap`, `saml` +- **AND** its body SHALL state that the `settings` object is encrypted at rest + +#### Scenario: project-access.md documents the project access types + +- **WHEN** `plugin/skills/formio-schema/references/project/project-access.md` is read +- **THEN** its body SHALL mention `ProjectRole`, `ProjectFormAccess`, and `ProjectAccessInfo` +- **AND** its body SHALL distinguish project-level access from form-level and submission-level access + +### Requirement: Router SKILL.md indexes the project domain + +`plugin/skills/formio-schema/SKILL.md` SHALL list the project domain as an authored domain — not as a "not yet authored" placeholder — with its own multi-row reference table (or equivalent structured list) enumerating all four authored project references. The project row SHALL NOT appear in any "not yet authored" table or paragraph. The router body SHALL NOT reference a billing-and-usage file. + +#### Scenario: Router enumerates every project reference + +- **WHEN** `plugin/skills/formio-schema/SKILL.md` is parsed +- **THEN** its body SHALL reference the paths `references/project/project-definition.md`, `references/project/project-type-and-framework.md`, `references/project/project-settings.md`, and `references/project/project-access.md` +- **AND** its body SHALL NOT reference the path `references/project/README.md` +- **AND** its body SHALL NOT reference any `references/project/project-billing-and-usage.md` path diff --git a/packages/mcp-server/src/__tests__/form_create.test.ts b/packages/mcp-server/src/__tests__/form_create.test.ts index 2a4c28f..511c7fe 100644 --- a/packages/mcp-server/src/__tests__/form_create.test.ts +++ b/packages/mcp-server/src/__tests__/form_create.test.ts @@ -19,7 +19,8 @@ describe('form_create tool', () => { const { tools } = await client.listTools(); const tool = tools.find((t) => t.name === 'form_create'); expect(tool).toBeDefined(); - expect(tool!.description).toContain('formio-form'); + expect(tool!.description).toContain('formio-schema'); + expect(tool!.description).not.toContain('formio-form'); }); it('sends form definition via POST to /form', async () => { diff --git a/packages/mcp-server/src/__tests__/form_update.test.ts b/packages/mcp-server/src/__tests__/form_update.test.ts index 2ce1fa0..ba2a156 100644 --- a/packages/mcp-server/src/__tests__/form_update.test.ts +++ b/packages/mcp-server/src/__tests__/form_update.test.ts @@ -24,7 +24,8 @@ describe('form_update tool', () => { const tool = tools.find((t) => t.name === 'form_update'); expect(tool).toBeDefined(); expect(tool!.description).toContain('form_get'); - expect(tool!.description).toContain('formio-form'); + expect(tool!.description).toContain('formio-schema'); + expect(tool!.description).not.toContain('formio-form'); }); it('sends PUT to /form/{formId} with form body', async () => { diff --git a/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts b/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts new file mode 100644 index 0000000..fa389bf --- /dev/null +++ b/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts @@ -0,0 +1,400 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import matter from 'gray-matter'; +import { describe, expect, it } from 'vitest'; + +const REPO_ROOT = path.resolve(__dirname, '../../../..'); +const SKILLS_DIR = path.join(REPO_ROOT, 'plugin/skills'); + +const SCHEMA_DIR = path.join(SKILLS_DIR, 'formio-schema'); +const SCHEMA_SKILL = path.join(SCHEMA_DIR, 'SKILL.md'); +const REFERENCES_DIR = path.join(SCHEMA_DIR, 'references'); +const FORM_REFS_DIR = path.join(REFERENCES_DIR, 'form'); + +const FORM_REFERENCE_FILES = [ + 'form-definition.md', + 'base-component.md', + 'input-components.md', + 'layout-components.md', + 'data-components.md', +] as const; + +const SUBMISSION_REFS_DIR = path.join(REFERENCES_DIR, 'submission'); +const PROJECT_REFS_DIR = path.join(REFERENCES_DIR, 'project'); + +const PROJECT_REFERENCE_FILES = [ + 'project-definition.md', + 'project-type-and-framework.md', + 'project-settings.md', + 'project-access.md', +] as const; + +const PROJECT_DEFINITION_PROPERTIES = [ + '_id', + 'title', + 'name', + 'type', + 'description', + 'tag', + 'owner', + 'externalOwner', + 'project', + 'remote', + 'plan', + 'billing', + 'apiCalls', + 'steps', + 'framework', + 'primary', + 'access', + 'trial', + 'lastDeploy', + 'stageTitle', + 'machineName', + 'config', + 'protect', + 'settings', + 'remoteSecret', + 'builderConfig', + 'formDefaults', + 'public', + 'created', + 'modified', + 'deleted', +] as const; + +const PROJECT_TYPE_VALUES = ['project', 'stage', 'tenant'] as const; + +const PROJECT_FRAMEWORK_VALUES = [ + 'angular', + 'angular2', + 'react', + 'vue', + 'html5', + 'simple', + 'custom', + 'aurelia', + 'javascript', +] as const; + +const PROJECT_SETTINGS_KEYS = [ + 'appOrigin', + 'keys', + 'cors', + 'csp', + 'secret', + 'pdfserver', + 'filetoken', + 'allowConfig', + 'allowConfigToForms', + 'custom', + 'formModule', + 'email', + 'captcha', + 'recaptcha', + 'esign', + 'google', + 'kickbox', + 'sqlconnector', + 'storage', + 'tokenParse', + 'oauth', + 'ldap', + 'saml', +] as const; + +const SUBMISSION_REFERENCE_FILES = [ + 'submission-definition.md', + 'submission-state.md', + 'submission-metadata.md', + 'submission-access.md', + 'submission-data.md', +] as const; + +const SUBMISSION_DEFINITION_PROPERTIES = [ + '_id', + '_fvid', + 'form', + 'project', + 'owner', + 'roles', + 'state', + 'access', + 'metadata', + 'data', + 'externalIds', + 'externalTokens', + 'permission', + 'created', + 'modified', + 'deleted', +] as const; + +const SUBMISSION_METADATA_KEYS = [ + 'timezone', + 'offset', + 'origin', + 'referrer', + 'browserName', + 'userAgent', + 'pathName', + 'onLine', + 'language', + 'headers', + 'ssoteam', + 'memberCount', + 'selectData', +] as const; + +const SUBMISSION_ACCESS_TYPES = [ + 'self', + 'create_own', + 'create_all', + 'read_own', + 'read_all', + 'update_own', + 'update_all', + 'delete_own', + 'delete_all', + 'team_read', + 'team_write', + 'team_admin', + 'team_access', +] as const; + +function exists(p: string): boolean { + try { + fs.lstatSync(p); + return true; + } catch { + return false; + } +} + +function readFrontmatter(file: string): Record<string, unknown> { + const raw = fs.readFileSync(file, 'utf8'); + return matter(raw).data as Record<string, unknown>; +} + +function readBody(file: string): string { + const raw = fs.readFileSync(file, 'utf8'); + return matter(raw).content; +} + +describe('formio-schema references directory layout', () => { + it('references/ contains every domain subdirectory and no top-level .md files', () => { + expect(exists(REFERENCES_DIR)).toBe(true); + const entries = fs.readdirSync(REFERENCES_DIR, { withFileTypes: true }); + const dirNames = entries + .filter((e) => e.isDirectory()) + .map((e) => e.name) + .sort(); + expect(dirNames).toEqual(['form', 'project', 'submission']); + const topLevelMd = entries.filter((e) => e.isFile() && e.name.endsWith('.md')); + expect(topLevelMd).toEqual([]); + }); + + it('references/form/ contains all five form-domain references and each is non-empty', () => { + expect(exists(FORM_REFS_DIR)).toBe(true); + for (const file of FORM_REFERENCE_FILES) { + const full = path.join(FORM_REFS_DIR, file); + expect(exists(full), `${file} must exist`).toBe(true); + const raw = fs.readFileSync(full, 'utf8'); + expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); + } + }); +}); + +describe('formio-schema SKILL.md frontmatter', () => { + it('name is formio-schema', () => { + const fm = readFrontmatter(SCHEMA_SKILL); + expect(fm.name).toBe('formio-schema'); + }); + + it('description spans the form, submission, and project domains', () => { + const fm = readFrontmatter(SCHEMA_SKILL); + const description = String(fm.description ?? '').toLowerCase(); + expect(description).toContain('form'); + expect(description).toContain('submission'); + expect(description).toContain('project'); + }); + + it('description has Not-for clause naming peer skills and contains no formio-form reference', () => { + const fm = readFrontmatter(SCHEMA_SKILL); + const description = String(fm.description ?? ''); + expect(description).toMatch(/not for:/i); + expect(description).toContain('formio-api'); + expect(description).toContain('formio-actions'); + expect(description).toContain('formio-resource-planner'); + expect(description).toContain('formio-application'); + expect(description).not.toContain('formio-form'); + }); +}); + +describe('formio-schema SKILL.md body indexes every domain', () => { + it('body references all five references/form/*.md paths', () => { + const body = readBody(SCHEMA_SKILL); + for (const file of FORM_REFERENCE_FILES) { + expect(body).toContain(`references/form/${file}`); + } + }); + + it('body references every references/submission/submission-*.md path and excludes submission/README.md', () => { + const body = readBody(SCHEMA_SKILL); + for (const file of SUBMISSION_REFERENCE_FILES) { + expect(body).toContain(`references/submission/${file}`); + } + expect(body).not.toContain('references/submission/README.md'); + }); + + it('body references every references/project/project-*.md path and excludes project README + billing', () => { + const body = readBody(SCHEMA_SKILL); + for (const file of PROJECT_REFERENCE_FILES) { + expect(body).toContain(`references/project/${file}`); + } + expect(body).not.toContain('references/project/README.md'); + expect(body).not.toContain('references/project/project-billing-and-usage.md'); + }); +}); + +describe('formio-schema submission-domain references', () => { + it('references/submission/ contains every authored file, none carry frontmatter, and README.md is absent', () => { + expect(exists(SUBMISSION_REFS_DIR)).toBe(true); + for (const file of SUBMISSION_REFERENCE_FILES) { + const full = path.join(SUBMISSION_REFS_DIR, file); + expect(exists(full), `${file} must exist`).toBe(true); + const raw = fs.readFileSync(full, 'utf8'); + expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); + expect(raw.startsWith('---'), `${file} must not start with YAML frontmatter`).toBe(false); + } + expect(exists(path.join(SUBMISSION_REFS_DIR, 'README.md'))).toBe(false); + }); + + it('submission-definition.md mentions every top-level Submission property name', () => { + const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-definition.md'), 'utf8'); + for (const prop of SUBMISSION_DEFINITION_PROPERTIES) { + expect(raw, `submission-definition.md must mention ${prop}`).toContain(prop); + } + }); + + it('submission-state.md documents both draft and submitted', () => { + const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-state.md'), 'utf8'); + expect(raw).toContain('draft'); + expect(raw).toContain('submitted'); + }); + + it('submission-metadata.md mentions every documented key and notes extensibility', () => { + const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-metadata.md'), 'utf8'); + for (const key of SUBMISSION_METADATA_KEYS) { + expect(raw, `submission-metadata.md must mention ${key}`).toContain(key); + } + expect(raw.toLowerCase()).toMatch(/extensible|open-ended|extension|arbitrary/); + }); + + it('submission-access.md mentions every AccessType value', () => { + const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-access.md'), 'utf8'); + for (const type of SUBMISSION_ACCESS_TYPES) { + expect(raw, `submission-access.md must mention ${type}`).toContain(type); + } + }); + + it('submission-data.md cross-links to the form references', () => { + const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-data.md'), 'utf8'); + expect(raw).toMatch(/references\/form\//); + }); +}); + +describe('formio-schema project-domain references', () => { + it('references/project/ contains every authored file, none carry frontmatter, README + billing files absent', () => { + expect(exists(PROJECT_REFS_DIR)).toBe(true); + for (const file of PROJECT_REFERENCE_FILES) { + const full = path.join(PROJECT_REFS_DIR, file); + expect(exists(full), `${file} must exist`).toBe(true); + const raw = fs.readFileSync(full, 'utf8'); + expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); + expect(raw.startsWith('---'), `${file} must not start with YAML frontmatter`).toBe(false); + } + expect(exists(path.join(PROJECT_REFS_DIR, 'README.md'))).toBe(false); + expect(exists(path.join(PROJECT_REFS_DIR, 'project-billing-and-usage.md'))).toBe(false); + }); + + it('project-definition.md mentions every Project property name', () => { + const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-definition.md'), 'utf8'); + for (const prop of PROJECT_DEFINITION_PROPERTIES) { + expect(raw, `project-definition.md must mention ${prop}`).toContain(prop); + } + }); + + it('project-definition.md notes deployed projects use plan commercial', () => { + const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-definition.md'), 'utf8'); + expect(raw).toContain('commercial'); + }); + + it('project-type-and-framework.md enumerates every type and framework value', () => { + const raw = fs.readFileSync( + path.join(PROJECT_REFS_DIR, 'project-type-and-framework.md'), + 'utf8' + ); + for (const value of PROJECT_TYPE_VALUES) { + expect(raw, `must mention ProjectType ${value}`).toContain(value); + } + for (const value of PROJECT_FRAMEWORK_VALUES) { + expect(raw, `must mention ProjectFramework ${value}`).toContain(value); + } + }); + + it('project-type-and-framework.md documents Stage and Tenant creation patterns', () => { + const raw = fs.readFileSync( + path.join(PROJECT_REFS_DIR, 'project-type-and-framework.md'), + 'utf8' + ); + expect(raw).toContain('"type": "stage"'); + expect(raw).toContain('"type": "tenant"'); + expect(raw.toLowerCase()).toMatch(/parent project|portal|primary project/); + expect(raw.toLowerCase()).toMatch(/objectid/); + }); + + it('project-settings.md mentions every ProjectSettings key and the encryption contract', () => { + const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-settings.md'), 'utf8'); + for (const key of PROJECT_SETTINGS_KEYS) { + expect(raw, `must mention setting ${key}`).toContain(key); + } + expect(raw.toLowerCase()).toContain('encrypted'); + }); + + it('project-access.md documents project-level access shapes', () => { + const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-access.md'), 'utf8'); + expect(raw).toContain('ProjectRole'); + expect(raw).toContain('ProjectFormAccess'); + expect(raw).toContain('ProjectAccessInfo'); + expect(raw.toLowerCase()).toMatch(/project-level/); + expect(raw.toLowerCase()).toMatch(/form-level|form definition/); + expect(raw.toLowerCase()).toMatch(/submission-level|submission record/); + }); +}); + +describe('formio-form skill removal', () => { + it('plugin/skills/formio-form/ does not exist', () => { + expect(exists(path.join(SKILLS_DIR, 'formio-form'))).toBe(false); + }); + + it('no file under plugin/skills/ mentions formio-form', () => { + const offenders: string[] = []; + const stack = [SKILLS_DIR]; + while (stack.length > 0) { + const current = stack.pop()!; + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const full = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(full); + continue; + } + if (!entry.isFile()) continue; + if (!/\.(md|json|ts|py|txt)$/.test(entry.name)) continue; + const raw = fs.readFileSync(full, 'utf8'); + if (raw.includes('formio-form')) offenders.push(full); + } + } + expect(offenders, `files still mention formio-form: ${offenders.join(', ')}`).toEqual([]); + }); +}); diff --git a/packages/mcp-server/src/__tests__/plugin-build.test.ts b/packages/mcp-server/src/__tests__/plugin-build.test.ts index 4f3f656..704ae38 100644 --- a/packages/mcp-server/src/__tests__/plugin-build.test.ts +++ b/packages/mcp-server/src/__tests__/plugin-build.test.ts @@ -64,11 +64,12 @@ describe('pnpm build:plugin', () => { ).not.toThrow(); }); - it('1.3 includes formio-api/ and formio-form/ but excludes openspec-* and tdd-* skills', () => { + it('1.3 includes formio-api/ and formio-schema/ but excludes formio-form/ and openspec-* and tdd-* skills', () => { expect(fs.existsSync(SKILLS_DIR)).toBe(true); const entries = fs.readdirSync(SKILLS_DIR); expect(entries).toContain('formio-api'); - expect(entries).toContain('formio-form'); + expect(entries).toContain('formio-schema'); + expect(entries).not.toContain('formio-form'); for (const entry of entries) { expect( entry.startsWith('openspec-'), @@ -120,11 +121,12 @@ describe('pnpm test:plugin — smoke test', () => { expect(stdout).toMatch(/tools\/list|tools-list|tools list/i); }, 60_000); - it('3.3 confirms formio-api/ and formio-form/ skills are present', () => { + it('3.3 confirms formio-api/ and formio-schema/ skills are present and formio-form/ is absent', () => { const { status, stdout } = runSmokeTest(); expect(status).toBe(0); expect(stdout).toMatch(/formio-api/); - expect(stdout).toMatch(/formio-form/); + expect(stdout).toMatch(/formio-schema/); + expect(stdout).not.toMatch(/formio-form/); }, 60_000); }); diff --git a/packages/mcp-server/src/tools/form_create.ts b/packages/mcp-server/src/tools/form_create.ts index c88a560..c7be201 100644 --- a/packages/mcp-server/src/tools/form_create.ts +++ b/packages/mcp-server/src/tools/form_create.ts @@ -8,7 +8,7 @@ import { cwdSchema, resolveProjectConfig } from '../project-resolver.js'; export function registerFormCreateTool(server: McpServer, config: FormioConfig) { server.tool( 'form_create', - "Create a new form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, use the formio-form skill to construct a properly structured Form.io form JSON definition based on the user's requirements. The skill documents all component types, validation options, layout patterns, and conditional logic available in Form.io.", + "Create a new form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, use the formio-schema skill to construct a properly structured Form.io form JSON definition based on the user's requirements. The skill documents all component types, validation options, layout patterns, and conditional logic available in Form.io.", { cwd: cwdSchema, form: z diff --git a/packages/mcp-server/src/tools/form_update.ts b/packages/mcp-server/src/tools/form_update.ts index 95d521d..808f9ff 100644 --- a/packages/mcp-server/src/tools/form_update.ts +++ b/packages/mcp-server/src/tools/form_update.ts @@ -8,7 +8,7 @@ import { cwdSchema, resolveProjectConfig } from '../project-resolver.js'; export function registerFormUpdateTool(server: McpServer, config: FormioConfig) { server.tool( 'form_update', - "Update an existing form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, first use form_get to fetch the current form definition, then use the formio-form skill to apply the requested modifications (add, remove, or modify fields and settings), and finally call this tool with the complete updated form JSON.", + "Update an existing form in the Form.io project mapped to the user's current working directory. IMPORTANT: Before calling this tool, first use form_get to fetch the current form definition, then use the formio-schema skill to apply the requested modifications (add, remove, or modify fields and settings), and finally call this tool with the complete updated form JSON.", { cwd: cwdSchema, formId: z diff --git a/plugin/README.md b/plugin/README.md index bd66306..8741f06 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -38,8 +38,7 @@ Every step has an approval gate before any file is written or any MCP call hits | `formio-application` | Default "build me an app" orchestrator. Six-step pipeline (Intent → Plan → Deployment → MCP Config → Import → Framework). | | `formio-resource-planner` | Plans resources, fields, roles, actions, access — emits paired `template.md` + `template.json`. | | `formio-angular` | Angular framework implementor. Five-phase scaffold flow over `@formio/angular`. | -| `formio-form` | Authoring guide for new Form.io form JSON definitions. | -| `formio-schema` | Comprehensive Form.io form JSON schema reference. | +| `formio-schema` | Comprehensive Form.io JSON schema reference — form definitions today, with placeholder slots for submissions, actions, projects, and roles. | | `formio-actions` | Configuration reference for Form.io server-side actions. | | `formio-api` | Router into the full Form.io REST API surface (platform, project, runtime, PDF). | diff --git a/plugin/skills/formio-api/SKILL.md b/plugin/skills/formio-api/SKILL.md index 8fe895e..ca12dcb 100644 --- a/plugin/skills/formio-api/SKILL.md +++ b/plugin/skills/formio-api/SKILL.md @@ -1,7 +1,7 @@ --- name: formio-api description: >- - Comprehensive Form.io API reference — covers every endpoint in the Form.io API Postman collection across platform admin, project admin, runtime/end-user, and PDF scopes. Use when the user asks to call, script, inspect, or document any Form.io REST endpoint: authenticating as platform or project admin, managing projects, stages, tenants, teams; creating, listing, updating, importing, or exporting forms, resources, or form revisions; attaching actions; managing roles; registering or logging in end users (built-in or custom user resources); submitting, querying, patching, or deleting submissions; running aggregation reports; uploading PDF templates or downloading submissions as PDFs; checking platform health/status. Also use when the user asks about x-jwt-token auth, the Form.io base URL vs project URL distinction, or wants an endpoint reference by method/path. Not for: building a whole application around Form.io (see formio-application); planning a data model (see formio-resource-planner); authoring form JSON definitions (see formio-form / formio-schema). + Comprehensive Form.io API reference — covers every endpoint in the Form.io API Postman collection across platform admin, project admin, runtime/end-user, and PDF scopes. Use when the user asks to call, script, inspect, or document any Form.io REST endpoint: authenticating as platform or project admin, managing projects, stages, tenants, teams; creating, listing, updating, importing, or exporting forms, resources, or form revisions; attaching actions; managing roles; registering or logging in end users (built-in or custom user resources); submitting, querying, patching, or deleting submissions; running aggregation reports; uploading PDF templates or downloading submissions as PDFs; checking platform health/status. Also use when the user asks about x-jwt-token auth, the Form.io base URL vs project URL distinction, or wants an endpoint reference by method/path. Not for: building a whole application around Form.io (see formio-application); planning a data model (see formio-resource-planner); authoring Form.io JSON schemas — form definitions, submissions, actions, projects, roles — (see formio-schema). --- # Form.io API Skills diff --git a/plugin/skills/formio-api/references/project-forms.md b/plugin/skills/formio-api/references/project-forms.md index c3307ef..0a975ff 100644 --- a/plugin/skills/formio-api/references/project-forms.md +++ b/plugin/skills/formio-api/references/project-forms.md @@ -91,7 +91,7 @@ Request body (JSON): } ``` -Required fields: `title`, `type` (`form` or `resource`), `name`, `path`, `components`. Optional fields: `tags`, `display`, `settings`, `access`, `submissionAccess`. See the `formio-form` skill for the full component schema. +Required fields: `title`, `type` (`form` or `resource`), `name`, `path`, `components`. Optional fields: `tags`, `display`, `settings`, `access`, `submissionAccess`. See the `formio-schema` skill for the full component schema. Response: the created form document with server-assigned `_id`, `machineName`, `created`, and `modified` fields. diff --git a/plugin/skills/formio-form/SKILL.md b/plugin/skills/formio-form/SKILL.md deleted file mode 100644 index 9ed2289..0000000 --- a/plugin/skills/formio-form/SKILL.md +++ /dev/null @@ -1,583 +0,0 @@ ---- -name: formio-form -description: This skill provides a deep understanding on how to create a new form JSON schema definition for Form.io forms. It covers the structure of form definitions, component types, and how to use this schema when working with the MCP server tools that interact with Form.io. It should be used when creating a new Form.io form definition or when interpreting the form JSON returned by the MCP server. -license: MIT ---- - -# Form.io Form Schema Definitions - -This document describes the Form.io form JSON schema used by the Form.io platform. Use this as a reference when working with form definitions returned by the MCP server tools (`form_list`, `form_get`) or when constructing forms programmatically. - ---- - -## Form - -The top-level object representing a form or resource in Form.io. - -| Property | Type | Required | Description | -| --------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------- | -| `_id` | `string` | No | MongoDB ObjectId. Assigned by the server. | -| `_vid` | `number` | No | Version ID for form revisions. | -| `title` | `string` | No | Human-readable form title (e.g., "User Registration"). | -| `name` | `string` | No | Machine name used for API references (e.g., "userRegister"). | -| `path` | `string` | No | URL path segment for the form (e.g., "user/register"). | -| `type` | `FormType` | No | Either `"form"` or `"resource"`. Resources are reusable data models; forms collect submissions. | -| `display` | `FormDisplay` | No | Rendering mode: `"form"` (single page), `"wizard"` (multi-step), or `"pdf"`. | -| `action` | `string` | No | URL to submit the form to. Defaults to the Form.io API. | -| `tags` | `string[]` | No | Arbitrary tags for categorization and filtering. | -| `access` | `Access[]` | No | Form-level access permissions (who can read/write the form definition). | -| `submissionAccess` | `Access[]` | No | Submission-level access permissions (who can create/read/update/delete submissions). | -| `fieldMatchAccess` | `object` | No | Field-level access control rules. | -| `owner` | `string` | No | Submission ID of the form owner. | -| `machineName` | `string` | No | Globally unique machine name across projects. | -| `components` | `Component[]` | **Yes** | Array of form components defining the form's fields and layout. | -| `settings` | `FormSettings` | No | Form-level display and behavior settings. | -| `properties` | `Record<string, string>` | No | Custom key-value properties attached to the form. | -| `project` | `string` | No | Project ID this form belongs to. | -| `revisions` | `string` | No | Revision mode: `"current"`, `"original"`, or `""`. | -| `submissionRevisions` | `string` | No | Whether submission revisions are enabled: `"true"` or `""`. | -| `controller` | `string` | No | Custom controller logic (server-side JavaScript). | -| `builder` | `boolean` | No | Whether to show this form in the form builder. | -| `page` | `number` | No | Current wizard page index. | -| `created` | `string` | No | ISO date when the form was created. | -| `modified` | `string` | No | ISO date when the form was last modified. | -| `deleted` | `string` | No | ISO date when the form was soft-deleted (null if active). | - -### FormType - -- `"form"` — A standard form that collects submissions. -- `"resource"` — A reusable data model (like a database table) that other forms can reference. - -### FormDisplay - -- `"form"` — Renders all components on a single page. -- `"wizard"` — Multi-step form with navigation between pages (panels become steps). -- `"pdf"` — PDF-based form rendering. - -### FormSettings - -| Property | Type | Description | -| ------------------------ | --------- | --------------------------------------------------------- | -| `collection` | `string` | Custom MongoDB collection name for submissions. | -| `condensedMode` | `boolean` | Render in condensed/compact mode. | -| `disableAutocomplete` | `boolean` | Disable browser autocomplete on all fields. | -| `fontSize` | `number` | Base font size for PDF rendering. | -| `hideTitle` | `boolean` | Hide the form title when rendered. | -| `layout` | `string` | Layout template name. | -| `margins` | `string` | Page margins for PDF rendering. | -| `showCheckboxBackground` | `boolean` | Show background color on checkbox components. | -| `theme` | `string` | CSS theme name to apply. | -| `viewAsHtml` | `boolean` | Render submissions as static HTML instead of form fields. | -| `viewer` | `string` | Viewer type for rendering. | -| `wizardHeaderType` | `string` | Wizard header style (e.g., breadcrumb). | -| `pdf` | `object` | PDF source configuration: `{ src: string, id: string }`. | - -### Access - -Each access entry defines a permission role mapping. - -| Property | Type | Description | -| -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `string` | Access type: `"read_all"`, `"create_own"`, `"create_all"`, `"update_own"`, `"update_all"`, `"delete_own"`, `"delete_all"`, `"self"`, `"team_read"`, `"team_write"`, `"team_admin"`. | -| `roles` | `string[]` | Array of role IDs that have this access type. | - ---- - -## Components - -Components are the building blocks of a form. Every component extends `BaseComponent` with type-specific properties. - -### BaseComponent - -All components share these base properties. - -| Property | Type | Required | Description | -| ------------------------ | ------------------------ | -------- | ---------------------------------------------------------------------------------------------------- | -| `type` | `string` | **Yes** | Component type identifier (e.g., `"textfield"`, `"number"`, `"select"`). | -| `key` | `string` | **Yes** | Unique data key within the form. Maps to the submission data path. | -| `input` | `boolean` | **Yes** | Whether this component accepts user input (layout components set this to `false`). | -| `label` | `string` | No | Human-readable label displayed above the field. | -| `placeholder` | `string` | No | Placeholder text shown when the field is empty. | -| `description` | `string` | No | Help text displayed below the field. | -| `tooltip` | `string` | No | Tooltip text shown on hover. | -| `prefix` | `string` | No | Text or icon displayed before the input. | -| `suffix` | `string` | No | Text or icon displayed after the input. | -| `customClass` | `string` | No | CSS class(es) added to the component wrapper. | -| `hidden` | `boolean` | No | Whether the component is initially hidden. | -| `hideLabel` | `boolean` | No | Hide the label from display. | -| `disabled` | `boolean` | No | Whether the field is read-only. | -| `autofocus` | `boolean` | No | Auto-focus this field on form load. | -| `tabindex` | `string` | No | Tab order index for keyboard navigation. | -| `tableView` | `boolean` | No | Show this field in submission table/grid views. | -| `multiple` | `boolean` | No | Allow multiple values (renders as an array). | -| `protected` | `boolean` | No | Exclude this field's value from API responses. | -| `unique` | `boolean` | No | Enforce unique values across all submissions. | -| `persistent` | `boolean \| string` | No | Whether to save to the database. `false` = ephemeral. | -| `clearOnHide` | `boolean` | No | Clear the field value when conditionally hidden. | -| `refreshOn` | `string` | No | Component key to watch — refresh this component's data when that key changes. | -| `redrawOn` | `string` | No | Component key to watch — redraw this component when that key changes. | -| `modalEdit` | `boolean` | No | Edit this component in a modal dialog. | -| `labelPosition` | `string` | No | Label position: `"top"`, `"left-left"`, `"left-right"`, `"right-left"`, `"right-right"`, `"bottom"`. | -| `dataGridLabel` | `boolean` | No | Show label when inside a DataGrid. | -| `errorLabel` | `string` | No | Custom label used in validation error messages. | -| `defaultValue` | `any` | No | Default value when no submission data exists. | -| `customDefaultValue` | `string` | No | JavaScript or JSON Logic to compute a default value. | -| `calculateValue` | `string` | No | JavaScript or JSON Logic to compute the field value dynamically. | -| `calculateServer` | `boolean` | No | Run `calculateValue` on the server side. | -| `allowCalculateOverride` | `boolean` | No | Allow manual edits to override a calculated value. | -| `validateOn` | `string` | No | When to trigger validation: `"change"`, `"blur"`, or `"submit"`. | -| `validateWhenHidden` | `boolean` | No | Run validation even when the component is hidden. | -| `encrypted` | `boolean` | No | Encrypt this field's value at rest. | -| `showCharCount` | `boolean` | No | Display a character counter below the field. | -| `showWordCount` | `boolean` | No | Display a word counter below the field. | -| `properties` | `Record<string, string>` | No | Custom key-value metadata on the component. | -| `attributes` | `Record<string, string>` | No | Custom HTML attributes added to the input element. | -| `widget` | `object \| string` | No | Widget override configuration (e.g., calendar picker). | -| `dbIndex` | `boolean` | No | Create a database index on this field. | -| `logic` | `AdvancedLogic[]` | No | Advanced logic rules (event-driven actions). | -| `conditional` | `object` | No | Conditional display logic (simple, JSON Logic, or legacy). | -| `customConditional` | `string` | No | Custom JavaScript for conditional visibility. | -| `overlay` | `object` | No | PDF overlay positioning: `{ style, left, top, width, height }`. | -| `submissionAccess` | `Access[]` | No | Field-level submission access rules. | -| `errors` | `Record<string, string>` | No | Custom error messages keyed by validation rule. | - -#### Validation (`validate` object) - -| Property | Type | Description | -| ---------------------- | --------- | -------------------------------------------- | -| `required` | `boolean` | Field must have a value. | -| `custom` | `string` | Custom JavaScript validation. | -| `customPrivate` | `boolean` | Run custom validation server-side only. | -| `customMessage` | `string` | Custom error message for failed validation. | -| `strictDateValidation` | `boolean` | Enforce strict date parsing. | -| `multiple` | `boolean` | Validate each value when `multiple` is true. | -| `unique` | `boolean` | Value must be unique across submissions. | -| `json` | `object` | JSON Logic validation rule. | - -#### Conditional Display - -**Simple conditional:** - -```json -{ - "show": true, - "conjunction": "all", - "conditions": [{ "component": "fieldKey", "operator": "isEqual", "value": "yes" }] -} -``` - -**JSON Logic conditional:** - -```json -{ - "json": { "===": [{ "var": "data.fieldKey" }, "yes"] } -} -``` - -**Legacy conditional:** - -```json -{ - "show": true, - "when": "fieldKey", - "eq": "yes" -} -``` - ---- - -### Component Types - -#### TextField (`type: "textfield"`) - -Single-line text input. - -| Property | Type | Description | -| ------------------------- | --------- | ---------------------------------------------- | -| `inputType` | `string` | HTML input type override. | -| `inputFormat` | `string` | Input format (e.g., `"plain"`, `"html"`). | -| `inputMask` | `string` | Input mask pattern (e.g., `"(999) 999-9999"`). | -| `inputMasks` | `array` | Multiple mask options: `[{ label, mask }]`. | -| `displayMask` | `string` | Display-only mask (different from input mask). | -| `spellcheck` | `boolean` | Enable browser spellcheck. | -| `truncateMultipleSpaces` | `boolean` | Collapse multiple spaces to one. | -| `validate.minLength` | `number` | Minimum character length. | -| `validate.maxLength` | `number` | Maximum character length. | -| `validate.minWords` | `number` | Minimum word count. | -| `validate.maxWords` | `number` | Maximum word count. | -| `validate.pattern` | `string` | Regex pattern the value must match. | -| `validate.patternMessage` | `string` | Custom message when pattern fails. | - -#### TextArea (`type: "textarea"`) - -Multi-line text input. Extends TextField. - -| Property | Type | Description | -| ----------- | --------- | --------------------------------------------------------- | -| `rows` | `number` | Number of visible text rows. | -| `wysiwyg` | `boolean` | Enable WYSIWYG rich text editor. | -| `editor` | `string` | Editor type: `"ckeditor"`, `"quill"`, `"ace"`. | -| `fixedSize` | `boolean` | Prevent resizing. | -| `as` | `string` | Render as alternative element (e.g., `"json"`, `"html"`). | - -#### Number (`type: "number"`) - -Numeric input. - -| Property | Type | Description | -| ---------------- | --------- | -------------------------------------------- | -| `validate.min` | `number` | Minimum allowed value. | -| `validate.max` | `number` | Maximum allowed value. | -| `validate.step` | `string` | Step increment (`"any"` for no restriction). | -| `delimiter` | `boolean` | Show thousands separator. | -| `requireDecimal` | `boolean` | Always show decimal point. | -| `inputFormat` | `string` | Number format. | - -#### Password (`type: "password"`) - -Password input. Same properties as TextField with masked display. - -#### Email (`type: "email"`) - -Email input. Extends TextField. - -| Property | Type | Description | -| ----------------- | --------- | ---------------------------------- | -| `kickbox.enabled` | `boolean` | Enable Kickbox email verification. | - -#### PhoneNumber (`type: "phoneNumber"`) - -Phone number input. Same properties as TextField. - -#### Url (`type: "url"`) - -URL input. Same properties as TextField. - -#### DateTime (`type: "datetime"`) - -Date and/or time picker. - -| Property | Type | Description | -| ------------------- | --------- | ------------------------------------------------------------------- | -| `format` | `string` | Display format (e.g., `"yyyy-MM-dd HH:mm"`). | -| `enableDate` | `boolean` | Enable date selection. | -| `enableTime` | `boolean` | Enable time selection. | -| `defaultDate` | `string` | Default date value. | -| `displayInTimezone` | `string` | Timezone for display: `"viewer"`, `"submission"`, `"utc"`. | -| `timezone` | `string` | Specific timezone identifier. | -| `datePicker` | `object` | Date picker configuration (min/max dates, disabled weekends, etc.). | -| `timePicker` | `object` | Time picker configuration (hour/minute step, meridian). | - -#### Day (`type: "day"`) - -Separate day/month/year inputs. - -| Property | Type | Description | -| ----------------- | --------- | -------------------------------------------------------------------------------------------- | -| `fields` | `object` | Configuration for `day`, `month`, and `year` sub-fields (type, placeholder, required, hide). | -| `dayFirst` | `boolean` | Show day before month. | -| `hideInputLabels` | `boolean` | Hide the sub-field labels. | -| `minDate` | `string` | Minimum allowed date. | -| `maxDate` | `string` | Maximum allowed date. | - -#### Time (`type: "time"`) - -Time-only input. Extends TextField. - -| Property | Type | Description | -| ------------ | -------- | --------------------------------- | -| `format` | `string` | Display format (e.g., `"HH:mm"`). | -| `dataFormat` | `string` | Storage format. | - -#### Checkbox (`type: "checkbox"`) - -Single boolean checkbox. - -| Property | Type | Description | -| -------- | -------- | ----------------------- | -| `value` | `string` | The value when checked. | -| `name` | `string` | Input name attribute. | - -#### Radio (`type: "radio"`) - -Radio button group. - -| Property | Type | Description | -| ---------------------- | --------- | ----------------------------------------------------- | -| `values` | `array` | Options: `[{ label, value, shortcut? }]`. | -| `dataSrc` | `string` | Data source: `"values"` (static) or `"url"` (remote). | -| `data.url` | `string` | URL for remote options (when `dataSrc: "url"`). | -| `inline` | `boolean` | Render options horizontally. | -| `optionsLabelPosition` | `string` | Label position relative to radio button. | - -#### SelectBoxes (`type: "selectboxes"`) - -Multiple checkbox group. Extends Radio. - -| Property | Type | Description | -| --------------------------- | ------------------------- | ---------------------------------- | -| `defaultValue` | `Record<string, boolean>` | Default selected state per option. | -| `validate.minSelectedCount` | `number` | Minimum selections required. | -| `validate.maxSelectedCount` | `number` | Maximum selections allowed. | - -#### Select (`type: "select"`) - -Dropdown selection. - -| Property | Type | Description | -| ---------------- | ----------------- | --------------------------------------------------------------------- | -| `dataSrc` | `string` | Data source: `"values"`, `"json"`, `"url"`, `"resource"`, `"custom"`. | -| `data.values` | `array` | Static options: `[{ label, value }]`. | -| `data.url` | `string` | URL for remote options. | -| `data.resource` | `string` | Resource ID for resource-based options. | -| `data.json` | `array \| string` | JSON data source. | -| `data.custom` | `string` | Custom JavaScript returning options. | -| `valueProperty` | `string` | Property to use as the stored value. | -| `searchEnabled` | `boolean` | Enable type-ahead search. | -| `searchField` | `string` | Field to search against in remote data. | -| `searchDebounce` | `number` | Debounce delay for search requests (ms). | -| `minSearch` | `number` | Minimum characters before triggering search. | -| `lazyLoad` | `boolean` | Load options on first open instead of on form load. | -| `filter` | `string` | Query filter for remote data. | -| `limit` | `number` | Max options to load per request. | -| `selectFields` | `string` | Fields to select from remote data. | -| `sort` | `string` | Sort order for remote data. | -| `clearOnRefresh` | `boolean` | Clear value when dependent field changes. | -| `uniqueOptions` | `boolean` | Remove duplicate options. | - -#### Resource (`type: "resource"`) - -Select from a Form.io resource. Extends Select. - -| Property | Type | Description | -| ---------- | -------- | ----------------- | -| `resource` | `string` | Resource form ID. | -| `project` | `string` | Project ID. | - -#### Hidden (`type: "hidden"`) - -Hidden input. Stores data without UI. Base properties only. - -#### Button (`type: "button"`) - -Action button. - -| Property | Type | Description | -| ------------------ | --------- | ----------------------------------------------------------------------------------------- | -| `action` | `string` | Button action: `"submit"`, `"reset"`, `"event"`, `"oauth"`, `"url"`, `"saveState"`. | -| `theme` | `string` | Button style: `"primary"`, `"secondary"`, `"info"`, `"success"`, `"danger"`, `"warning"`. | -| `size` | `string` | Size: `"sm"`, `"md"`, `"lg"`, `"xl"`, `"xxl"`. | -| `block` | `boolean` | Full-width button. | -| `leftIcon` | `string` | Icon class for left icon. | -| `rightIcon` | `string` | Icon class for right icon. | -| `disableOnInvalid` | `boolean` | Disable button when form is invalid. | -| `event` | `string` | Custom event name (when `action: "event"`). | - -#### Signature (`type: "signature"`) - -Signature pad. - -| Property | Type | Description | -| ----------------- | -------- | -------------------------- | -| `footer` | `string` | Footer text below the pad. | -| `width` | `string` | Pad width. | -| `height` | `string` | Pad height. | -| `penColor` | `string` | Signature pen color. | -| `backgroundColor` | `string` | Pad background color. | - -#### File (`type: "file"`) - -File upload. - -| Property | Type | Description | -| ----------------- | --------- | ---------------------------------------------- | -| `image` | `boolean` | Restrict to image files. | -| `privateDownload` | `boolean` | Require authentication to download. | -| `imageSize` | `string` | Max image dimensions. | -| `filePattern` | `string` | Allowed file extensions (e.g., `".pdf,.doc"`). | -| `fileMinSize` | `string` | Minimum file size (e.g., `"1KB"`). | -| `fileMaxSize` | `string` | Maximum file size (e.g., `"10MB"`). | -| `uploadOnly` | `boolean` | Hide download links after upload. | - -#### Tags (`type: "tags"`) - -Tag input. - -| Property | Type | Description | -| ----------- | -------- | ------------------------------------------ | -| `delimeter` | `string` | Character separating tags (default `","`). | -| `storeas` | `string` | Storage format: `"string"` or `"array"`. | -| `maxTags` | `number` | Maximum number of tags. | - -#### Survey (`type: "survey"`) - -Survey/matrix question grid. - -| Property | Type | Description | -| ----------- | ------- | ---------------------------------------------- | -| `questions` | `array` | Row questions: `[{ label, value, tooltip }]`. | -| `values` | `array` | Column answers: `[{ label, value, tooltip }]`. | - ---- - -### Layout Components - -Layout components organize fields visually. They set `input: false` and contain child components. - -#### Panel (`type: "panel"`) - -Collapsible section with a header. In wizard forms, each panel becomes a page/step. - -| Property | Type | Description | -| ------------ | ------------- | ----------------------------------- | -| `components` | `Component[]` | Child components inside the panel. | -| `theme` | `string` | Panel header color theme. | -| `breadcrumb` | `string` | Breadcrumb display mode in wizards. | - -#### Columns (`type: "columns"`) - -Multi-column layout. - -| Property | Type | Description | -| ------------ | --------- | ------------------------------------------------------------------------ | -| `columns` | `array` | Column definitions: `[{ components, width, offset, push, pull, size }]`. | -| `autoAdjust` | `boolean` | Auto-adjust column widths. | - -#### Table (`type: "table"`) - -HTML table layout. - -| Property | Type | Description | -| ----------- | --------------- | -------------------------------- | -| `rows` | `Component[][]` | 2D array of components in cells. | -| `numRows` | `number` | Number of rows. | -| `numCols` | `number` | Number of columns. | -| `striped` | `boolean` | Striped row styling. | -| `bordered` | `boolean` | Cell borders. | -| `hover` | `boolean` | Row hover highlighting. | -| `condensed` | `boolean` | Compact row height. | - -#### Tabs (`type: "tabs"`) - -Tabbed sections. - -| Property | Type | Description | -| ---------------- | --------- | ------------------------------------------------ | -| `components` | `array` | Tab definitions: `[{ label, key, components }]`. | -| `verticalLayout` | `boolean` | Render tabs vertically. | - -#### FieldSet (`type: "fieldset"`) - -Fieldset grouping with a legend. - -| Property | Type | Description | -| ------------ | ------------- | ----------------- | -| `components` | `Component[]` | Child components. | - -#### Well (`type: "well"`) - -Visual container with a background. - -| Property | Type | Description | -| ------------ | ------------- | ----------------- | -| `components` | `Component[]` | Child components. | - -#### Content (`type: "content"`) - -Static HTML content block. - -| Property | Type | Description | -| -------- | -------- | ----------------------- | -| `html` | `string` | HTML content to render. | - -#### HTML Element (`type: "htmlelement"`) - -Custom HTML element. - -| Property | Type | Description | -| --------- | -------- | --------------------------------------------- | -| `tag` | `string` | HTML tag name (e.g., `"div"`, `"p"`, `"h3"`). | -| `attrs` | `array` | HTML attributes: `[{ attr, value }]`. | -| `content` | `string` | Inner HTML content. | - ---- - -### Data Components - -Data components manage repeatable or nested data structures. - -#### Container (`type: "container"`) - -Groups fields under a single data key as a nested object. - -| Property | Type | Description | -| ------------ | ------------- | ------------------------------------------------------------------------------ | -| `components` | `Component[]` | Child components. Data is stored as `{ [key]: { child1: ..., child2: ... } }`. | - -#### DataGrid (`type: "datagrid"`) - -Repeatable row-based table. Each row contains the same set of fields. - -| Property | Type | Description | -| --------------------------- | ------------- | --------------------------------- | -| `components` | `Component[]` | Components in each row (columns). | -| `disableAddingRemovingRows` | `boolean` | Lock the number of rows. | - -#### EditGrid (`type: "editgrid"`) - -Repeatable list with inline or modal editing. - -| Property | Type | Description | -| --------------- | ------------- | -------------------------------------------------------- | -| `components` | `Component[]` | Components in each entry. | -| `modal` | `boolean` | Edit entries in a modal dialog. | -| `inlineEdit` | `boolean` | Edit entries inline without saving row by row. | -| `openWhenEmpty` | `boolean` | Auto-open editor when no entries exist. | -| `removeRow` | `string` | Custom remove button text. | -| `templates` | `object` | Custom Handlebars templates for header, row, and footer. | - -#### DataMap (`type: "datamap"`) - -Key-value pair editor. Extends DataGrid. - -| Property | Type | Description | -| ---------------- | --------------- | ------------------------------------------ | -| `valueComponent` | `BaseComponent` | Component definition for the value column. | -| `keyBeforeValue` | `boolean` | Show key column before value column. | - -#### Form (`type: "form"`) - -Embeds another form. - -| Property | Type | Description | -| ----------- | --------- | -------------------------------------------------------------- | -| `src` | `string` | URL of the embedded form. | -| `form` | `string` | Form ID to embed. | -| `path` | `string` | Form path to embed. | -| `reference` | `boolean` | Store as a reference instead of embedding the submission data. | - -#### Address (`type: "address"`) - -Address autocomplete with manual mode fallback. Extends Container. - -| Property | Type | Description | -| ------------------------- | --------- | --------------------------------------------------- | -| `provider` | `string` | Address provider (e.g., `"google"`, `"nominatim"`). | -| `providerOptions` | `object` | Provider-specific configuration. | -| `enableManualMode` | `boolean` | Allow switching to manual address entry. | -| `switchToManualModeLabel` | `string` | Label for the manual mode toggle. | - -#### DataSource (`type: "datasource"`) - -Fetches external data (no UI input). - -| Property | Type | Description | -| -------------------- | --------- | ------------------------------------ | -| `fetch.url` | `string` | URL to fetch data from. | -| `fetch.method` | `string` | HTTP method. | -| `fetch.headers` | `array` | Request headers: `[{ key, value }]`. | -| `fetch.authenticate` | `boolean` | Include Form.io auth token. | - -#### Recaptcha (`type: "recaptcha"`) - -Google reCAPTCHA verification. Base properties only. diff --git a/plugin/skills/formio-resource-planner/SKILL.md b/plugin/skills/formio-resource-planner/SKILL.md index d03d939..4429931 100644 --- a/plugin/skills/formio-resource-planner/SKILL.md +++ b/plugin/skills/formio-resource-planner/SKILL.md @@ -143,7 +143,7 @@ When the user describes access ("reps only see their company's deals"), they alm | File attachment | `file` | Requires a storage provider | | Email (for user login) | `email` | Always on the `user` resource | -Full component reference: see the `formio-form` skill when you need exact JSON shapes. This cheat sheet is for planning, not generation. +Full component reference: see the `formio-schema` skill when you need exact JSON shapes. This cheat sheet is for planning, not generation. ### Action cheat sheet @@ -461,5 +461,5 @@ Consult these when the user's requirements touch an edge case this skill doesn't - **Does not skip the approval gate.** Even if the user's prompt sounds decisive, always emit the map first, ask for approval, and only then produce the `template.md` + `template.json` pair. - **Does not emit one artifact without the other.** Phase B ALWAYS writes both `template.md` and `template.json`. If something prevents both, stop and explain. - **Does not look up endpoints.** The `formio-api` skill handle endpoint reference. -- **Does not deep-dive a single form's component schema.** For exhaustive component options (conditional logic, calculated values, custom validation), see `formio-form`. This skill's template.json uses the minimum viable component shape for each field. +- **Does not deep-dive a single form's component schema.** For exhaustive component options (conditional logic, calculated values, custom validation), see `formio-schema`. This skill's template.json uses the minimum viable component shape for each field. - **Does not make the plan "complete" beyond what the user described.** If they didn't mention reporting, don't add a report resource. diff --git a/plugin/skills/formio-schema/SKILL.md b/plugin/skills/formio-schema/SKILL.md index 806f0a9..cb4a5ae 100644 --- a/plugin/skills/formio-schema/SKILL.md +++ b/plugin/skills/formio-schema/SKILL.md @@ -1,30 +1,54 @@ --- name: formio-schema -description: Form.io form JSON schema reference. Use this skill whenever constructing, editing, or interpreting a Form.io form definition — including forms returned by MCP tools like `form_list`, `form_get`, `form_create`, and `form_update`, or when the user mentions form components, wizards, resources, submissions, conditional logic, validation rules, or any `type: "..."` component (textfield, select, datagrid, panel, etc.). Trigger even when the user does not explicitly say "Form.io" if the context involves JSON form schemas with `components` arrays, `key`/`input`/`type` fields, or form builder concepts. +description: >- + Form.io JSON schema reference covering the document shapes for projects, forms (and resources), and submissions. Use this skill whenever constructing, editing, or interpreting any Form.io project / form / submission JSON — forms returned by MCP tools like `form_list`, `form_get`, `form_create`, `form_update`; submission bodies returned by `/submission` endpoints (including decoding `data`, reading row-level `access`, distinguishing draft vs submitted state, or inspecting submission `metadata`); or project documents returned by `/project/{projectId}` (including project-level settings, integrations such as email / captcha / SQL connector / file storage, authorization providers such as OAuth / LDAP / SAML, project-level access, the Stage and Tenant creation patterns, and project template envelopes used by `project_import` / `project_export`). Trigger whenever the user mentions form components, wizards, resources, submissions, draft submissions, submission metadata, submission data, project templates, project settings, stages, tenants, OAuth/LDAP/SAML, file storage, project access, conditional logic, validation rules, or any `type` component (textfield, select, datagrid, panel, etc.) — even when the user does not explicitly say "Form.io" if the context involves Form.io JSON schemas. Not for: calling Form.io REST endpoints (see `formio-api`); configuring server-side actions on a form (see `formio-actions`); planning a new app's resource model from scratch (see `formio-resource-planner`); orchestrating an entire app build (see `formio-application`). license: MIT --- -# Form.io Form Schema +# Form.io JSON Schema -This skill describes the JSON schema used by [Form.io](https://form.io) forms. Use it to construct new form definitions, interpret existing ones, or modify forms via the MCP server tools. +This skill describes the JSON schemas for the three Form.io document types whose shape is non-trivial — projects, forms (and resources), and submissions. Action configs live in the `formio-actions` skill; role objects are simple enough to use directly from the `formio-api` reference. Use this skill to construct new JSON payloads, interpret existing ones, or modify them via the MCP server tools. -Detail is split across reference files. Read only the ones you need for the task at hand — the overview below is usually enough to orient yourself; load a reference file when you need a specific property list. +Detail is split across reference files under `references/<domain>/`. Read only the files you need for the task at hand — the overview below is usually enough to orient yourself; load a reference file when you need a specific property list. ## When to load which reference -Load the reference file that matches what you're working on: +References are partitioned by schema domain. Pick a domain first, then pick a reference inside that domain. Adding a new schema domain is purely additive — new subdirectory under `references/`, new row in the appropriate table below. -| Working on… | Load | -| ---------------------------------------------------------------------------------------------- | --------------------------------- | -| Top-level form properties (`title`, `path`, `display`, `access`, `settings`, etc.) | `references/form-definition.md` | -| Properties shared by all components (`key`, `label`, `validate`, `conditional`, `logic`, etc.) | `references/base-component.md` | -| A specific input field (textfield, number, select, checkbox, file, signature, button, …) | `references/input-components.md` | -| Visual layout containers (panel, columns, tabs, table, fieldset, well, content) | `references/layout-components.md` | -| Nested or repeatable data (container, datagrid, editgrid, datamap, nested form, address) | `references/data-components.md` | +### Form definitions -You can load multiple references in parallel if a task spans categories (e.g., a wizard with data grids and a signature field touches all the component references plus the form definition). +| Working on… | Load | +| ---------------------------------------------------------------------------------------------- | -------------------------------------- | +| Top-level form properties (`title`, `path`, `display`, `access`, `settings`, etc.) | `references/form/form-definition.md` | +| Properties shared by all components (`key`, `label`, `validate`, `conditional`, `logic`, etc.) | `references/form/base-component.md` | +| A specific input field (textfield, number, select, checkbox, file, signature, button, …) | `references/form/input-components.md` | +| Visual layout containers (panel, columns, tabs, table, fieldset, well, content) | `references/form/layout-components.md` | +| Nested or repeatable data (container, datagrid, editgrid, datamap, nested form, address) | `references/form/data-components.md` | -## Top-level shape +### Submissions + +| Working on… | Load | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------- | +| Top-level submission envelope (`_id`, `form`, `owner`, `roles`, `state`, `metadata`, etc.) | `references/submission/submission-definition.md` | +| Lifecycle state — `draft` vs `submitted`, when each is written | `references/submission/submission-state.md` | +| The `metadata` bag (timezone, browser, headers, extension keys) | `references/submission/submission-metadata.md` | +| Row-level `access` overrides and every `AccessType` value | `references/submission/submission-access.md` | +| Decoding the `data` envelope — key paths, nesting, address discriminated union | `references/submission/submission-data.md` | + +### Projects + +| Working on… | Load | +| ------------------------------------------------------------------------------------------------ | --------------------------------------------------- | +| Top-level project envelope (`title`, `name`, `owner`, `access`, settings, etc.) | `references/project/project-definition.md` | +| `type` (`project`/`stage`/`tenant`) and `framework` discriminators; Stage / Tenant patterns | `references/project/project-type-and-framework.md` | +| `ProjectSettings` keys, integrations, authorization providers, encryption-at-rest contract | `references/project/project-settings.md` | +| Project-level `access` array, `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo` | `references/project/project-access.md` | + +For action configs, see the dedicated `formio-actions` skill. For role objects, see `formio-api`'s `project-roles` reference directly — role JSON is shallow enough that a separate domain is not warranted. + +You can load multiple references in parallel if a task spans categories (e.g., a wizard with data grids and a signature field touches every form reference). + +## Top-level shape (form domain) A form is a JSON object. The only required property is `components`; everything else is optional but commonly set: @@ -45,7 +69,7 @@ A form is a JSON object. The only required property is `components`; everything - `display`: `"form"` (single page), `"wizard"` (each top-level `panel` becomes a page/step), or `"pdf"`. - `components`: ordered array of component objects — the body of the form. -For the full list of form-level properties including `access`, `submissionAccess`, `settings`, `revisions`, and `controller`, see `references/form-definition.md`. +For the full list of form-level properties including `access`, `submissionAccess`, `settings`, `revisions`, and `controller`, see `references/form/form-definition.md`. ## Components at a glance @@ -60,13 +84,13 @@ Every component has at minimum: - `input` — `true` for data-collecting fields, `false` for layout-only components. - `label` — displayed above the field. -All other shared properties (validation, conditional display, calculated values, access, logic, etc.) live in `references/base-component.md`. +All other shared properties (validation, conditional display, calculated values, access, logic, etc.) live in `references/form/base-component.md`. ### Component catalog Components fall into three categories. Pick a category, load its reference for full property tables. -**Input components** (`references/input-components.md`) — collect user data: +**Input components** (`references/form/input-components.md`) — collect user data: | `type` | Purpose | | ------------- | --------------------------------------------------------------------------------------------------------- | @@ -92,7 +116,7 @@ Components fall into three categories. Pick a category, load its reference for f | `tags` | Tag input | | `survey` | Matrix-style survey grid | -**Layout components** (`references/layout-components.md`) — structure the form visually, set `input: false`: +**Layout components** (`references/form/layout-components.md`) — structure the form visually, set `input: false`: | `type` | Purpose | | ------------- | -------------------------------------------- | @@ -105,7 +129,7 @@ Components fall into three categories. Pick a category, load its reference for f | `content` | Static HTML block | | `htmlelement` | Custom HTML tag | -**Data components** (`references/data-components.md`) — manage nested or repeatable data: +**Data components** (`references/form/data-components.md`) — manage nested or repeatable data: | `type` | Purpose | | ------------ | ------------------------------------------------------------------ | @@ -123,5 +147,5 @@ Components fall into three categories. Pick a category, load its reference for f - **Keys must be unique within a form.** Nested components (inside a `container`, `datagrid`, etc.) namespace their keys under the parent. - **Wizards are built from panels.** Set `display: "wizard"` on the form; each top-level `panel` component becomes one step. - **Resources vs forms.** Use `type: "resource"` for reusable data objects that can be referenced by `select` components with `dataSrc: "resource"`. Use `type: "form"` for anything that collects submissions. -- **Conditional visibility.** Three formats exist: simple, JSON Logic, and legacy. An advanced conditional can also be written with custom JS. See `references/base-component.md` for syntax. +- **Conditional visibility.** Three formats exist: simple, JSON Logic, and legacy. An advanced conditional can also be written with custom JS. See `references/form/base-component.md` for syntax. - **Avoid `calculateValue` without `allowCalculateOverride`** unless the field should truly be read-only-by-computation — users cannot edit a calculated field by default. diff --git a/plugin/skills/formio-schema/references/base-component.md b/plugin/skills/formio-schema/references/form/base-component.md similarity index 100% rename from plugin/skills/formio-schema/references/base-component.md rename to plugin/skills/formio-schema/references/form/base-component.md diff --git a/plugin/skills/formio-schema/references/data-components.md b/plugin/skills/formio-schema/references/form/data-components.md similarity index 100% rename from plugin/skills/formio-schema/references/data-components.md rename to plugin/skills/formio-schema/references/form/data-components.md diff --git a/plugin/skills/formio-schema/references/form-definition.md b/plugin/skills/formio-schema/references/form/form-definition.md similarity index 100% rename from plugin/skills/formio-schema/references/form-definition.md rename to plugin/skills/formio-schema/references/form/form-definition.md diff --git a/plugin/skills/formio-schema/references/input-components.md b/plugin/skills/formio-schema/references/form/input-components.md similarity index 100% rename from plugin/skills/formio-schema/references/input-components.md rename to plugin/skills/formio-schema/references/form/input-components.md diff --git a/plugin/skills/formio-schema/references/layout-components.md b/plugin/skills/formio-schema/references/form/layout-components.md similarity index 100% rename from plugin/skills/formio-schema/references/layout-components.md rename to plugin/skills/formio-schema/references/form/layout-components.md diff --git a/plugin/skills/formio-schema/references/project/project-access.md b/plugin/skills/formio-schema/references/project/project-access.md new file mode 100644 index 0000000..63806f0 --- /dev/null +++ b/plugin/skills/formio-schema/references/project/project-access.md @@ -0,0 +1,71 @@ +# Project Access Reference + +Project-level access controls who can see and modify the project document itself — its settings, its forms, its roles. This is a separate concern from form-level access (who can read / edit the form definition) and submission-level access (who can read / edit submission records). All three layers use the same `Access` entry shape (`{ type, roles, resources? }`), but they live on different documents and govern different operations. + +## Access layers — quick comparison + +| Layer | Where it lives | What it gates | Reference | +| ----------- | ----------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| Project | `Project.access` (top-level on the project doc) | Who can see/modify the project itself, settings, role list | This file | +| Form | `Form.access` (on the form definition) | Who can see/modify the form definition (form-level) | `references/form/form-definition.md` | +| Submission | `Form.submissionAccess` + `Submission.access` | Who can create/read/update/delete submission records | `references/form/form-definition.md`, `submission/submission-access.md` | + +A consumer can hold project-level access without form-level access (e.g., a developer who can see the project's portal but cannot edit a specific locked-down form). The three layers are checked independently. + +## Project access entries + +The `Project.access` field is an array of `Access` entries. Each entry maps an access type to a set of role IDs. See `submission/submission-access.md` for the full `AccessType` enumeration and per-value semantics — the access types themselves are the same set used at every layer. + +```json +{ + "access": [ + { "type": "create_all", "roles": ["5f8d0c4e9b1e8a0017a10001"] }, + { "type": "read_all", "roles": ["5f8d0c4e9b1e8a0017a10002"] }, + { "type": "team_admin", "roles": ["5f8d0c4e9b1e8a0017a10003"] } + ] +} +``` + +## Supporting types + +The `Project` envelope ships with a small family of types that describe project access in aggregate (used by import/export and admin endpoints). + +### ProjectRole + +| Property | Type | Description | +| ------------- | --------- | -------------------------------------------------------------------------------------------------------- | +| `_id` | `string` | Role ID (MongoDB ObjectId). | +| `title` | `string` | Display title of the role. | +| `description` | `string` | Free-text description. | +| `admin` | `boolean` | `true` if the role grants administrative privileges within the project. | +| `default` | `boolean` | `true` if this role is assigned by default to new users created in this project. | + +`ProjectRole` is the same role document shipped by the project-roles API; it is reproduced here as a convenience type for the `ProjectAccessInfo.roles` map. + +### ProjectFormAccess + +| Property | Type | Description | +| ------------------ | ----------- | ------------------------------------------------------------------------------------------------------ | +| `_id` | `string` | Form ID. | +| `title` | `string` | Form title. | +| `name` | `string` | Form machine name. | +| `path` | `string` | Form URL path. | +| `access` | `Access[]` | Form-level access entries (who can see/modify the form definition). | +| `submissionAccess` | `Access[]` | Submission-level access entries (who can create/read/update/delete submissions against the form). | + +`ProjectFormAccess` is the per-form access summary the platform uses when reporting "what does the access matrix look like across this project's forms" — used by admin tooling. + +### ProjectAccessInfo + +| Property | Type | Description | +| -------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| `roles` | `Record<string, ProjectRole>` | Map of role ID → `ProjectRole` for every role defined in the project. | +| `forms` | `Record<string, ProjectFormAccess>` | Map of form ID → `ProjectFormAccess` for every form whose access matrix is being reported. | + +`ProjectAccessInfo` is the project-wide access snapshot — `roles` lists every role available to be granted, and `forms` lists each form's access posture. Useful when implementing project-admin UIs or when verifying an import/export round-trip. + +## See also + +- `project-definition.md` — where `access` sits on the Project envelope. +- `submission/submission-access.md` — full `AccessType` value enumeration and per-value semantics. +- `references/form/form-definition.md` — form-level `access` and `submissionAccess` arrays. diff --git a/plugin/skills/formio-schema/references/project/project-definition.md b/plugin/skills/formio-schema/references/project/project-definition.md new file mode 100644 index 0000000..660f5b2 --- /dev/null +++ b/plugin/skills/formio-schema/references/project/project-definition.md @@ -0,0 +1,76 @@ +# Project Definition Reference + +Top-level shape of a Form.io project — the JSON object the platform creates when a user provisions a new project, stage, or tenant and the object the platform-projects endpoint returns. Load this file when interpreting a project payload returned by `/project/{projectId}`, when constructing one to `POST` / `PUT`, or when importing/exporting a project template envelope. + +A project is the top-level container for forms, resources, roles, submissions, and actions. Every form belongs to exactly one project. Stages and tenants are themselves project documents — see `project-type-and-framework.md` for the discriminator details. + +## Project object + +| Property | Type | Required | Description | +| ---------------- | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `_id` | `string` | **Yes** | MongoDB ObjectId assigned by the server. | +| `title` | `string` (≤63 chars) | **Yes** | Human-readable project title. Indexed. | +| `name` | `string` (≤63 chars) | **Yes** | Machine name / subdomain segment. Must match `^[0-9a-zA-Z-]+$` (letters, digits, hyphens), cannot start or end with `-`, cannot be a reserved subdomain. Must be unique. | +| `type` | `'project' \| 'stage' \| 'tenant'` | No | Project kind. See `project-type-and-framework.md` for the discriminator and the Stage/Tenant creation patterns. Default `'project'`. | +| `description` | `string` (≤512 chars) | No | Free-text description of the project. | +| `tag` | `string` (≤32 chars) | No | Last-deployed tag of the project. Default `"0.0.0"`. | +| `owner` | `string` (submission ID) | No | User-submission ID of the project owner. Server stores as ObjectId; API returns as string. | +| `externalOwner` | `{ sub, iss, customIdClaim? }` | No | OIDC SSO external owner — `sub` (subject) and `iss` (issuer) of the external identity, with optional `customIdClaim: { key, value }` for the `idPath` resolution. Server-only — not in the upstream TypeScript declaration. | +| `project` | `string` (project ObjectId) | No | Parent project ID. Set on Stages to reference the parent (typically portal) project. Indexed. | +| `remote` | `object` | No | Remote project definition for stage-to-remote-environment connections. Server stores only `{ url, project: { _id, name, title } }`. | +| `plan` | `string` | No | Project plan. **For deployed projects this is always `'commercial'`.** SaaS tier values exist but are not covered by this skill — see your Form.io plan dashboard for SaaS billing. | +| `billing` | `object` | No | Server-managed billing data. See your Form.io plan dashboard, not documented as schema. | +| `apiCalls` | `object` | No | Server-managed API-call counter snapshot (limit / used / reset). See your Form.io plan dashboard, not documented as schema. | +| `steps` | `string[]` | No | Ordered list of onboarding / setup step identifiers the project has completed. | +| `framework` | `string` | No | Target client framework. Default `'angular'`. See `project-type-and-framework.md` for the enumeration. | +| `primary` | `boolean` | No | `true` if this project is the primary (portal) project of the deployment. Default `false`. | +| `access` | `Access[]` | No | Project-level access-control entries — who can see / modify the project itself. See `project-access.md` for the entry shape and how it layers with form-level / submission-level access. | +| `trial` | `Date \| string` | No | Start date of the trial period. Server-managed; `__readonly` on the server. See your Form.io plan dashboard, not documented as schema. | +| `lastDeploy` | `Date \| string` | No | Timestamp of the last deploy. Server-managed; `__readonly`. See your Form.io plan dashboard, not documented as schema. | +| `stageTitle` | `string` (≤63 chars) | No | Display title for a Stage project (e.g., "Dev", "Staging"). Used by Stages only. | +| `machineName` | `string` | No | Globally unique machine-readable name. Derived from `name` by the server's machine-name plugin. | +| `config` | `Record<string, string>` | No | Public configuration key-value pairs. Surfaced to forms when `settings.allowConfigToForms` is set. See `project-settings.md`. | +| `protect` | `boolean` | No | `true` to prevent destructive operations on the project. Default `false`. | +| `settings` | `ProjectSettings` | No | All project-level settings (API keys, CORS, integrations, authorization, custom JS/CSS, etc.). See `project-settings.md`. **Encrypted at rest.** | +| `remoteSecret` | `string` | No | Secret used when this project is connected as a remote stage from another project. | +| `builderConfig` | `object` | No | Form-builder UI configuration for the project. | +| `formDefaults` | `{ revisions?: 'current' \| 'original' }` | No | Default behavior applied to forms created in this project — currently just the default revision mode. | +| `public` | `{ custom?: { css?, js? }, formModule? }` | No | Public-facing custom CSS / JS / form-module bundle exposed to unauthenticated form renderers. | +| `created` | `Date \| string` | No | Server-assigned creation timestamp. | +| `modified` | `Date \| string` | No | Server-assigned timestamp of the most recent update. | +| `deleted` | `Date \| string \| number \| null` | No | Soft-delete timestamp. Non-null indicates the project has been deleted but is still recoverable; `null` indicates active. (Server stores as `Number`; upstream TypeScript declares as `Date \| string`.) | + +## Worked example + +```json +{ + "_id": "5f8d0c4e9b1e8a0017a10000", + "title": "Acme Portal", + "name": "acme-portal", + "type": "project", + "tag": "1.4.2", + "owner": "5f8d0c4e9b1e8a0017a1aaaa", + "plan": "commercial", + "framework": "angular", + "primary": true, + "access": [ + { "type": "create_all", "roles": ["5f8d0c4e9b1e8a0017a10001"] }, + { "type": "read_all", "roles": ["5f8d0c4e9b1e8a0017a10002"] } + ], + "settings": { + "appOrigin": "https://acme.example.com", + "cors": "*" + }, + "config": { "supportEmail": "support@acme.example.com" }, + "formDefaults": { "revisions": "current" }, + "machineName": "acme-portal", + "created": "2026-04-01T12:00:00.000Z", + "modified": "2026-05-26T18:04:11.000Z" +} +``` + +## Related references + +- `project-type-and-framework.md` — `type` and `framework` discriminators plus the Stage/Tenant creation patterns. +- `project-settings.md` — every `ProjectSettings` key and the encryption-at-rest contract. +- `project-access.md` — project-level `access` array, `ProjectRole`, `ProjectFormAccess`, `ProjectAccessInfo`. diff --git a/plugin/skills/formio-schema/references/project/project-settings.md b/plugin/skills/formio-schema/references/project/project-settings.md new file mode 100644 index 0000000..bb48688 --- /dev/null +++ b/plugin/skills/formio-schema/references/project/project-settings.md @@ -0,0 +1,56 @@ +# Project Settings Reference + +`settings` is the bag of project-level configuration covering API exposure (keys, CORS, CSP), public configuration, custom JS/CSS, third-party integrations (email, captcha, e-sign, file storage, SQL connector, Google Drive, Kickbox), and authorization providers (OAuth, LDAP, SAML). + +## Encryption at rest + +The entire `settings` object is **encrypted at rest** in MongoDB. The server's Mongoose schema installs the `EncryptedProperty` plugin against `settings` (`plugins/EncryptedProperty`, `plainName: 'settings'`). The round-trip works like this: + +1. Consumers send **plaintext** `settings` JSON over the API on create / update. +2. The server **encrypts** the bag before persisting it to MongoDB. +3. On read, the server **decrypts** the bag and returns plaintext to authorized callers. +4. Direct database access (mongosh, backup files, replica reads) sees **ciphertext** only. + +This matters when you're reading project documents out-of-band (e.g., from a database backup) — the settings will be opaque without the server's encryption key. It also means that anything you put in `settings` is treated as sensitive by the platform — use it for secrets (SMTP credentials, OAuth client secrets, API keys), not for non-sensitive configuration that consumers should see directly. + +## ProjectSettings keys + +| Key | Type | Role | +| -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `appOrigin` | `string` | The project's canonical application origin (`https://app.example.com`). | +| `keys` | `Array<{ key: string }>` | API key entries used for server-to-server / API-key authentication mode. | +| `cors` | `string` | CORS allowed-origins value the server returns. `"*"` for permissive; comma-separated for explicit allowlist. | +| `csp` | `string` | Content-Security-Policy header value the server attaches to project responses. | +| `secret` | `string` | Project-level shared secret used by signing flows. | +| `pdfserver` | `string` | URL of the PDF server this project routes PDF operations to. | +| `filetoken` | `string` | Signing token used by the file upload / download flow. | +| `allowConfig` | `boolean` | Surface `config` to portal UI callers. | +| `allowConfigToForms` | `boolean` | Surface `config` to form renderers (forms can read public config values). | +| `custom` | `{ css?, js? }` | Custom global CSS / JS injected into project-rendered forms. | +| `formModule` | `string` | URL or module identifier for a custom form-module bundle to load. | +| `email` | `ProjectEmailConfig` | Email provider config — one of `smtp`, `sendgrid`, `mailgun`. See `integrations/email.ts` upstream. | +| `captcha` | `ProjectCaptchaConfig` | reCAPTCHA / hCaptcha config — `{ siteKey, secretKey }`. | +| `recaptcha` | `ProjectCaptchaConfig` | Legacy reCAPTCHA configuration alongside `captcha`. Same shape. | +| `esign` | `ProjectESignConfig` | Box e-sign configuration including enterprise ID and Box app credentials. See `integrations/eSign.ts`. | +| `google` | `ProjectGoogleDriveConfig` | Google Drive integration — `{ clientId, cskey, refreshtoken }`. See `integrations/dataConnections.ts`. | +| `kickbox` | `ProjectKickboxConfig` | Kickbox email-verification API key — `{ apikey }`. | +| `sqlconnector` | `ProjectSQLConnectorConfig`| SQL Connector config — `{ host, password, type: 'mysql'\|'mssql'\|'postgres', user }`. | +| `storage` | `ProjectFileStorageConfig` | File-storage provider config — one of `azure`, `s3` (or MinIO), `dropbox`, `google`. See `integrations/fileStorage.ts`. | +| `tokenParse` | `string` | Custom JWT-claim parser expression for resolving the authenticated user. | +| `oauth` | `ProjectOauthConfig` | OAuth providers — one or more of `openid`, `github`, `google`. See `authorization/oauth.ts` for per-provider shape. | +| `ldap` | `ProjectLdapConfig` | LDAP bind / search config — `{ bindDn, bindCredentials, searchBase, searchFilter, url }`. See `authorization/ldap.ts`. | +| `saml` | `ProjectSamlConfig` | SAML IdP / SP configuration including `idp`, `issuer`, `callbackUrl`, roles mapping. See `authorization/saml.ts`. | + +## Integration and authorization details + +Each integration / authorization block is its own sub-type with its own field set. Documenting every provider's full shape here would duplicate the upstream type files and balloon this reference. Instead: + +- For per-provider integration shapes, see the upstream TypeScript files under `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/integrations/`. +- For per-provider authorization shapes, see `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/authorization/`. +- For runtime behavior of these providers (how the server authenticates against them, what endpoints they expose), see the `formio-api` skill's `runtime-auth`, `platform-auth`, and `project-auth` references. + +## See also + +- `project-definition.md` — where `settings` sits on the Project envelope. +- `project-access.md` — for project-level access entries (separate from settings). +- The `formio-api` skill's `platform-projects` reference for project endpoint contract. diff --git a/plugin/skills/formio-schema/references/project/project-type-and-framework.md b/plugin/skills/formio-schema/references/project/project-type-and-framework.md new file mode 100644 index 0000000..c87bfc0 --- /dev/null +++ b/plugin/skills/formio-schema/references/project/project-type-and-framework.md @@ -0,0 +1,76 @@ +# Project Type & Framework Reference + +Two discriminators sit on the `Project` envelope: `type` distinguishes a regular project from a stage or tenant, and `framework` declares which client framework the project targets. The third discriminator (`plan`) is intentionally not documented in depth — for deployed projects it is always `'commercial'`, and the SaaS tier values are out of scope for this skill. + +## ProjectType + +| Value | Meaning | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `'project'` | A regular standalone project. Default value. Used for the portal/primary project and for any project that isn't a derived environment. | +| `'stage'` | A Stage of a parent project — a deployment environment (Dev / Staging / etc.) that branches from a primary project. Requires `project` to be set to the parent project's ObjectId. | +| `'tenant'` | A tenant of the deployment — a multi-tenant child project that inherits its parent's plan at runtime. Tenants are server-keyed differently from stages. | + +### Stage creation pattern + +A Stage is a project document with `type: 'stage'` and a `project` field pointing at its parent. The parent is typically the portal (primary) project the stage's environment branches from. + +```json +{ + "type": "stage", + "project": "<parent project ObjectId>", + "title": "Staging", + "name": "acme-portal-staging", + "stageTitle": "Staging" +} +``` + +Required minimum for a Stage: + +- `type: 'stage'` +- `project: <parent project ObjectId>` — the `_id` of the parent project (typically the portal / primary project) +- `title` and `name` (inherited from the standard Project envelope) + +`stageTitle` is the human-readable label shown in the Stage selector UI. + +### Tenant creation pattern + +A Tenant is a project document with `type: 'tenant'`. Tenants do not require a `project` parent reference because the multi-tenant model is keyed differently from stages — the server walks tenants on `findOne` to inherit the parent's `plan` automatically. + +```json +{ + "type": "tenant", + "title": "Acme Customer A", + "name": "acme-tenant-a" +} +``` + +Required minimum for a Tenant: + +- `type: 'tenant'` +- `title` and `name` (inherited from the standard Project envelope) + +A tenant inherits the `plan` of its parent project at read time — the server's `findOne` hook on the project model replaces the tenant's `plan` with the parent's `plan` whenever a tenant document is fetched. This is invisible to the consumer; just be aware that the `plan` value returned for a tenant reflects its parent's billing tier, not anything you set on the tenant itself. + +## ProjectFramework + +Declares the target client framework for the project — used by the portal UI to pick the right SDK / starter template suggestions. + +| Value | Meaning | +| ------------- | ------------------------------------------------------------------------------------------------------ | +| `'angular'` | Modern Angular (the default). | +| `'angular2'` | Angular 2.x — historical alias retained for backward compatibility with older project documents. | +| `'react'` | React. | +| `'vue'` | Vue. | +| `'html5'` | Plain HTML5 / vanilla JS. | +| `'simple'` | "Simple" template — minimal UI, suitable for embedded or non-SPA use cases. | +| `'custom'` | Custom integration — the framework is unspecified or out-of-band. | +| `'aurelia'` | Aurelia. | +| `'javascript'`| Generic JavaScript SDK without a specific framework wrapper. | + +The server's Mongoose schema enforces this enum and defaults to `'angular'`. + +## See also + +- `project-definition.md` — full Project envelope. +- `project-settings.md` — project-level settings. +- `project-access.md` — project-level access control. diff --git a/plugin/skills/formio-schema/references/submission/submission-access.md b/plugin/skills/formio-schema/references/submission/submission-access.md new file mode 100644 index 0000000..8f71900 --- /dev/null +++ b/plugin/skills/formio-schema/references/submission/submission-access.md @@ -0,0 +1,55 @@ +# Submission Access Reference + +`access` on a submission is a row-level access-control override applied to that specific submission. It uses the same `Access` entry shape as the form's `submissionAccess`, but it lives on the individual record rather than the form definition, so it overrides the form-level rule for that one submission. + +## Access entry shape + +| Property | Type | Required | Description | +| ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------- | +| `type` | `string` | **Yes** | One of the AccessType values listed below. | +| `roles` | `string[]` | **Yes** | Role IDs granted this access type for the submission. | +| `resources` | `string[]` | No | Resource (form) IDs limiting the scope of the grant — used by team and self-referential access types. | + +The `access` field on the Submission envelope is an array of these entries. Each entry maps one access type to the set of roles that get it. + +## AccessType values + +| Value | Meaning | +| -------------- | -------------------------------------------------------------------------------------------------------------------- | +| `self` | Grants access to the user whose user-resource submission is referenced by `owner`. Used to express "the submitter can read/update their own record." | +| `create_own` | Grants creation rights to roles, but only for records whose `owner` is the requester's own user submission. | +| `create_all` | Grants unrestricted creation rights to roles. | +| `read_own` | Grants read rights to roles, but only for records whose `owner` matches the requester. | +| `read_all` | Grants unrestricted read rights to roles. | +| `update_own` | Grants update rights to roles, but only for records the requester owns. | +| `update_all` | Grants unrestricted update rights to roles. | +| `delete_own` | Grants delete rights to roles, but only for records the requester owns. | +| `delete_all` | Grants unrestricted delete rights to roles. | +| `team_read` | Grants read rights to members of the listed teams (Team plan / Enterprise). | +| `team_write` | Grants read + update rights to team members. | +| `team_admin` | Grants full read + update + delete rights to team members. | +| `team_access` | Grants access to a specific resource (form) the team has been granted access to — used in combination with `resources`. | + +## Layering with form-level submissionAccess + +The form definition can carry its own `submissionAccess` array (form-level rule that applies to every submission against that form — see `references/form/form-definition.md`). The submission's own `access` array (this reference) is checked on top of the form-level rule. Where both define the same `type`, the submission's row-level entry wins for that specific record. Where only one defines a `type`, that single source applies. This lets a form's default be "owners read their own" while a specific record opens itself up to a broader audience without changing the form definition. + +## Worked example + +A submission that overrides the form-level access to additionally grant read access to a manager role for this specific record: + +```json +{ + "_id": "5f8d0c4e9b1e8a0017a1b2c3", + "owner": "5f8d0c4e9b1e8a0017a1aaaa", + "access": [ + { "type": "read_all", "roles": ["5f8d0c4e9b1e8a0017a1man1"] }, + { "type": "update_own", "roles": ["5f8d0c4e9b1e8a0017a1emp1"] } + ] +} +``` + +## See also + +- `references/form/form-definition.md` for the form-level `access` and `submissionAccess` arrays. +- The `formio-api` skill's `runtime-access-control` reference for how the server resolves these entries on a request. diff --git a/plugin/skills/formio-schema/references/submission/submission-data.md b/plugin/skills/formio-schema/references/submission/submission-data.md new file mode 100644 index 0000000..eadf34a --- /dev/null +++ b/plugin/skills/formio-schema/references/submission/submission-data.md @@ -0,0 +1,113 @@ +# Submission Data Reference + +`data` is the actual collected values of a submission. It is a plain JSON object whose top-level keys are the `key` properties of input components on the parent form, and whose values are the per-component stored shapes. To interpret any given `data` blob you need the parent form's `components` array (load `references/form/form-definition.md` for the form envelope and `references/form/input-components.md` / `references/form/data-components.md` for per-component value shapes). + +This file documents the `data` envelope itself — how keys map to paths, how nesting works, and how the address discriminated union illustrates the general "value shape varies by component type" rule. It does NOT redocument every component's value shape; that lives in the form-domain references. + +## Key-to-path mapping + +Every input component on a form has a unique `key` within its enclosing scope. The submission stores the value at `data[key]` for top-level components. For example a form with three top-level inputs (`firstName`, `lastName`, `email`) produces: + +```json +{ + "data": { + "firstName": "Ada", + "lastName": "Lovelace", + "email": "ada@example.com" + } +} +``` + +Layout components (`panel`, `columns`, `tabs`, `fieldset`, `well`, `table`, `content`, `htmlelement`) have `input: false` and contribute no `data` key — their `components` flatten into the enclosing scope. + +## Nesting rules + +Data components (the ones documented in `references/form/data-components.md`) introduce nested data shapes under their own `key`: + +| Component | Shape under `data[key]` | +| ----------- | ---------------------------------------------------------------------------------------------------------------------- | +| `container` | A nested object: `data.containerKey.childKey = value` — child components are namespaced under the container's key. | +| `datagrid` | An array of row objects: `data.gridKey = [ { childKey: value, ... }, ... ]` — each row mirrors the component schema. | +| `editgrid` | Same array-of-rows shape as `datagrid`. | +| `datamap` | A key-value object where each key is user-defined: `data.mapKey = { userKey1: value1, userKey2: value2 }`. | +| `form` | Either the nested form's full submission object (when `reference: false`) or a `{ _id }` reference (when `reference: true`). | +| `address` | A discriminated union — see "Address mode" below. | + +For exact per-component value shapes (e.g., what a `file` component stores, what a `signature` stores, what a `select` stores when `multiple: true`), load `references/form/input-components.md` and `references/form/data-components.md`. + +## Address mode (worked example) + +The `address` component stores either an autocomplete result or a manually entered address, distinguished by a `mode` discriminator. This is the canonical example of "component type dictates value shape" — useful as a model for reading any other component's nested data. + +```json +{ + "data": { + "shippingAddress": { + "mode": "autocomplete", + "address": { "place_id": "...", "formatted_address": "..." } + } + } +} +``` + +```json +{ + "data": { + "shippingAddress": { + "mode": "manual", + "address": { + "address1": "123 Main St", + "address2": "", + "city": "Chicago", + "state": "IL", + "country": "US", + "zip": "60601" + } + } + } +} +``` + +## Worked example: a multi-component submission + +```json +{ + "data": { + "firstName": "Ada", + "billingAddress": { + "mode": "manual", + "address": { + "address1": "1 Analytical Way", + "city": "London", + "country": "GB", + "zip": "EC1A" + } + }, + "lineItems": [ + { "sku": "A1", "quantity": 2 }, + { "sku": "B2", "quantity": 1 } + ], + "preferences": { + "newsletter": true, + "categories": ["analytical", "engines"] + } + } +} +``` + +In this example `firstName` is a flat textfield, `billingAddress` is an `address` component, `lineItems` is a `datagrid` of `{ sku, quantity }` rows, and `preferences` is a `container` wrapping a `checkbox` and a `selectboxes`. + +## Conditional values, calculated values, persistence + +- A component with `persistent: false` does NOT appear under `data` even if the user entered a value. +- A component cleared by `clearOnHide` when its conditional is false has its value removed from `data` (rather than being stored as `null`). +- A component with `calculateValue` set has its `data[key]` populated by the calculation result on submit — the stored value may not match anything the user typed directly. +- Encrypted fields (`encrypted: true`) are stored encrypted at rest and returned in encrypted form unless the request decrypts them; the storage shape in `data` is still keyed by the component `key`. + +## See also + +- `references/form/form-definition.md` — the form envelope that defines the `components` array shaping `data`. +- `references/form/input-components.md` — per-input-component value shapes. +- `references/form/data-components.md` — per-data-component nesting shapes. +- `references/form/base-component.md` — `persistent`, `clearOnHide`, `calculateValue`, `encrypted` semantics that affect what lands in `data`. +- `submission-definition.md` — where `data` sits on the Submission envelope. diff --git a/plugin/skills/formio-schema/references/submission/submission-definition.md b/plugin/skills/formio-schema/references/submission/submission-definition.md new file mode 100644 index 0000000..c29a61c --- /dev/null +++ b/plugin/skills/formio-schema/references/submission/submission-definition.md @@ -0,0 +1,62 @@ +# Submission Definition Reference + +Top-level shape of a Form.io submission — the JSON object the platform creates when a user submits a form (or saves an in-progress draft) and the object the runtime submission endpoints return. Load this file when interpreting a submission payload returned by `/{projectName}/{formPath}/submission`, when constructing one to `POST` or `PUT`, or when modeling submissions in a downstream system. + +A submission is a discrete record of values submitted to a single form or resource. Form definitions describe what fields exist; submissions are the data those fields collected. One form has many submissions; each submission belongs to exactly one form within exactly one project. + +## Submission object + +| Property | Type | Required | Description | +| ---------------- | --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `_id` | `string` | No | MongoDB ObjectId assigned by the server. Absent on a new submission being created; populated on every read. | +| `_fvid` | `number` | No | Form-revision ID this submission was captured against. Tracks which form-definition revision the `data` shape was validated under. | +| `form` | `string` | No | Form ID this submission belongs to. Server-populated from the request path. | +| `project` | `string` | No | Project ID containing the parent form. Server-populated. | +| `owner` | `string` | No | Submission ID of the user that created this submission — the foreign key to a user-resource submission in the same project. | +| `roles` | `string[]` | No | Role IDs assigned to this submission when the submission represents a user (i.e., when the parent form is a user resource). See `submission-access.md`. | +| `state` | `'draft' \| 'submitted'` | No | Lifecycle state of the submission. See `submission-state.md` for when each value is written. | +| `access` | `Access[]` | No | Row-level access overrides applied to this specific submission. See `submission-access.md` for the entry shape and the layering rules with form-level `submissionAccess`. | +| `metadata` | `SubmissionMetadata` | No | Bag of contextual values captured at submit time — timezone, browser, headers, etc. See `submission-metadata.md`. | +| `data` | `object` | No | The actual submitted values, keyed by each input component's `key`. See `submission-data.md` for how the form definition shapes this object. | +| `externalIds` | `unknown[]` | No | Identifiers from external systems (e.g., OAuth provider IDs) attached to this submission by integrations or actions. | +| `externalTokens` | `unknown[]` | No | Bearer tokens or refresh tokens from external systems associated with this submission. Typically populated by OAuth-style login actions. | +| `permission` | `string` | No | Effective permission string the caller has on this submission, computed by the server and returned on read. | +| `created` | `string` (ISO date) | No | Server-assigned creation timestamp. | +| `modified` | `string` (ISO date) | No | Server-assigned timestamp of the most recent update. | +| `deleted` | `string` (ISO date) | No | Soft-delete timestamp. Non-null indicates the submission has been deleted but is still recoverable; `null` or absent indicates active. | + +## Worked example + +```json +{ + "_id": "5f8d0c4e9b1e8a0017a1b2c3", + "_fvid": 7, + "form": "5f8d0c4e9b1e8a0017a1b200", + "project": "5f8d0c4e9b1e8a0017a10000", + "owner": "5f8d0c4e9b1e8a0017a1aaaa", + "roles": ["5f8d0c4e9b1e8a0017a10001"], + "state": "submitted", + "access": [{ "type": "read_all", "roles": ["5f8d0c4e9b1e8a0017a10002"] }], + "metadata": { + "timezone": "America/Chicago", + "browserName": "Chrome", + "origin": "https://app.example.com" + }, + "data": { + "firstName": "Ada", + "lastName": "Lovelace", + "email": "ada@example.com" + }, + "externalIds": [], + "externalTokens": [], + "created": "2026-05-26T18:04:11.000Z", + "modified": "2026-05-26T18:04:11.000Z" +} +``` + +## Related references + +- `submission-state.md` — what `state` values mean and when each is written. +- `submission-metadata.md` — documented `metadata` keys and the extension contract. +- `submission-access.md` — row-level `access` array, all `AccessType` values, layering with form-level `submissionAccess`. +- `submission-data.md` — how the `data` object is shaped by the parent form's `components`, with cross-links to the form-domain references for per-component value shapes. diff --git a/plugin/skills/formio-schema/references/submission/submission-metadata.md b/plugin/skills/formio-schema/references/submission/submission-metadata.md new file mode 100644 index 0000000..7b0fdca --- /dev/null +++ b/plugin/skills/formio-schema/references/submission/submission-metadata.md @@ -0,0 +1,53 @@ +# Submission Metadata Reference + +`metadata` is a bag of contextual values captured at submit time that don't belong in `data` (because they're about the submission environment, not the form's fields). The platform writes a number of well-known keys; the object is extensible, so integrations and custom actions can attach additional keys without changes to the form definition. + +## SubmissionMetadata object + +| Property | Type | Description | +| ------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `timezone` | `string` | IANA timezone identifier captured from the submitter's browser at submit time (e.g., `"America/Chicago"`). | +| `offset` | `number` | UTC offset in minutes corresponding to `timezone` at submit time. | +| `origin` | `string` | The `window.location.origin` of the page that submitted the form (`https://app.example.com`). | +| `referrer` | `string` | The HTTP `Referer` header value at submit time, identifying the page that linked to the form. | +| `browserName` | `string` | User-agent-parsed browser name (e.g., `"Chrome"`, `"Safari"`). | +| `userAgent` | `string` | Raw `User-Agent` request header string. | +| `pathName` | `string` | The `window.location.pathname` of the page that submitted the form. | +| `onLine` | `boolean` | `navigator.onLine` value at submit time — `true` if the browser believed it had network connectivity. | +| `language` | `string` | Browser-preferred language tag (e.g., `"en-US"`). | +| `headers` | `Record<string, string>` | Subset of HTTP headers the server captured from the submit request. | +| `ssoteam` | `boolean` | `true` when the submitter authenticated via SSO and is a member of a Form.io team. Populated by SSO login actions. | +| `memberCount` | `number` | Membership count snapshot for team-aware submissions — typically the number of team members the submitter belongs to at submit time. | +| `selectData` | `unknown` | Snapshot of the full option objects (label + value + any extra fields) for any `select` components whose `dataSrc` is `"resource"` or `"url"`. Lets consumers render submissions without re-fetching the source list. | + +## Extension contract + +`metadata` carries an open-ended index signature: any additional string key with any value is permitted (`[key: string]: unknown`). Integrations, custom actions, and downstream systems MAY add arbitrary keys. Consumers reading `metadata` SHOULD treat unrecognized keys as opaque and pass them through unchanged. + +## Worked example + +```json +{ + "metadata": { + "timezone": "America/Chicago", + "offset": -300, + "origin": "https://app.example.com", + "referrer": "https://app.example.com/dashboard", + "browserName": "Chrome", + "userAgent": "Mozilla/5.0 (...) Chrome/124.0", + "pathName": "/forms/contact", + "onLine": true, + "language": "en-US", + "headers": { "x-request-id": "req_abc123" }, + "ssoteam": false, + "selectData": { "country": { "label": "United States", "value": "US" } }, + "customCampaignId": "spring-2026" + } +} +``` + +The trailing `customCampaignId` key shows the extension contract in practice — it isn't part of the platform-defined set, but it round-trips through the API unchanged. + +## See also + +- `submission-definition.md` for where `metadata` sits on the Submission envelope. diff --git a/plugin/skills/formio-schema/references/submission/submission-state.md b/plugin/skills/formio-schema/references/submission/submission-state.md new file mode 100644 index 0000000..67339df --- /dev/null +++ b/plugin/skills/formio-schema/references/submission/submission-state.md @@ -0,0 +1,49 @@ +# Submission State Reference + +`state` records where a submission sits in its lifecycle. Two values appear in production data and in the Form.io user guide: + +| Value | When it's written | Meaning | +| ------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `"draft"` | A user opens a form, fills in some fields, and the platform persists the in-progress data without finalizing it (Save Draft action / SaveState button / form-level draft setting). | The submission is incomplete and can still be edited by the user before final submit. Validation rules that require values are NOT enforced on a draft. | +| `"submitted"` | The user completes the form and submits. The server runs all validations, executes Submit-time actions, and writes the final record. | The submission is finalized. Subsequent edits go through `PUT /submission/{_id}` (with permission) but the state stays `"submitted"`. | + +A submission record may transition `draft` → `submitted` exactly once during its lifetime. There is no reverse transition; reopening a submitted record for editing does NOT move it back to `draft`. + +## Note on the upstream TypeScript type + +The TypeScript declaration in the Form.io platform source currently narrows `SubmissionState` to `'submitted'` only. This is a known narrowing gap — the runtime, the user guide, and production data all treat `'draft'` as a valid state value. Consumers reading submission JSON SHOULD accept both `'draft'` and `'submitted'` as valid `state` values; consumers writing submission JSON SHOULD use `'submitted'` for finalized records and `'draft'` for partial saves. + +## Worked examples + +A draft submission produced by a Save-Draft action: + +```json +{ + "_id": "5f8d0c4e9b1e8a0017a1b2c3", + "form": "5f8d0c4e9b1e8a0017a1b200", + "state": "draft", + "data": { + "firstName": "Ada" + } +} +``` + +The same submission once the user finalizes it: + +```json +{ + "_id": "5f8d0c4e9b1e8a0017a1b2c3", + "form": "5f8d0c4e9b1e8a0017a1b200", + "state": "submitted", + "data": { + "firstName": "Ada", + "lastName": "Lovelace", + "email": "ada@example.com" + } +} +``` + +## See also + +- `submission-definition.md` — full Submission envelope including `state`. +- The `formio-api` skill's `runtime-submissions` reference for how `state` interacts with the `/submission` endpoints (filtering by state, retrieving drafts, etc.). diff --git a/scripts/test-plugin.ts b/scripts/test-plugin.ts index 8322d92..4a5b8b3 100644 --- a/scripts/test-plugin.ts +++ b/scripts/test-plugin.ts @@ -9,7 +9,7 @@ const DIST_PLUGIN = path.join(REPO_ROOT, 'dist/plugin'); const PLUGIN_JSON = path.join(DIST_PLUGIN, '.claude-plugin/plugin.json'); const SERVER_BUNDLE = path.join(DIST_PLUGIN, 'server/stdio.mjs'); const SKILLS_DIR = path.join(DIST_PLUGIN, 'skills'); -const REQUIRED_SKILL_DIRS = ['formio-api', 'formio-form']; +const REQUIRED_SKILL_DIRS = ['formio-api', 'formio-schema']; const REQUIRED_MANIFEST_FIELDS = ['name', 'version', 'description'] as const; type Manifest = { From a74605eb95d64f1d54ac5abfdd81e84ac45cf228 Mon Sep 17 00:00:00 2001 From: Travis Tidwell <travis@form.io> Date: Thu, 28 May 2026 13:58:26 -0500 Subject: [PATCH 2/3] Updates from code reviews. --- .../__tests__/formio-schema-layout.test.ts | 400 ------------------ plugin/README.md | 2 +- .../references/project/project-access.md | 2 +- .../references/project/project-definition.md | 1 - .../references/project/project-settings.md | 3 +- .../submission/submission-access.md | 71 ++-- .../references/submission/submission-data.md | 2 +- .../submission/submission-definition.md | 1 + 8 files changed, 50 insertions(+), 432 deletions(-) delete mode 100644 packages/mcp-server/src/__tests__/formio-schema-layout.test.ts diff --git a/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts b/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts deleted file mode 100644 index fa389bf..0000000 --- a/packages/mcp-server/src/__tests__/formio-schema-layout.test.ts +++ /dev/null @@ -1,400 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import matter from 'gray-matter'; -import { describe, expect, it } from 'vitest'; - -const REPO_ROOT = path.resolve(__dirname, '../../../..'); -const SKILLS_DIR = path.join(REPO_ROOT, 'plugin/skills'); - -const SCHEMA_DIR = path.join(SKILLS_DIR, 'formio-schema'); -const SCHEMA_SKILL = path.join(SCHEMA_DIR, 'SKILL.md'); -const REFERENCES_DIR = path.join(SCHEMA_DIR, 'references'); -const FORM_REFS_DIR = path.join(REFERENCES_DIR, 'form'); - -const FORM_REFERENCE_FILES = [ - 'form-definition.md', - 'base-component.md', - 'input-components.md', - 'layout-components.md', - 'data-components.md', -] as const; - -const SUBMISSION_REFS_DIR = path.join(REFERENCES_DIR, 'submission'); -const PROJECT_REFS_DIR = path.join(REFERENCES_DIR, 'project'); - -const PROJECT_REFERENCE_FILES = [ - 'project-definition.md', - 'project-type-and-framework.md', - 'project-settings.md', - 'project-access.md', -] as const; - -const PROJECT_DEFINITION_PROPERTIES = [ - '_id', - 'title', - 'name', - 'type', - 'description', - 'tag', - 'owner', - 'externalOwner', - 'project', - 'remote', - 'plan', - 'billing', - 'apiCalls', - 'steps', - 'framework', - 'primary', - 'access', - 'trial', - 'lastDeploy', - 'stageTitle', - 'machineName', - 'config', - 'protect', - 'settings', - 'remoteSecret', - 'builderConfig', - 'formDefaults', - 'public', - 'created', - 'modified', - 'deleted', -] as const; - -const PROJECT_TYPE_VALUES = ['project', 'stage', 'tenant'] as const; - -const PROJECT_FRAMEWORK_VALUES = [ - 'angular', - 'angular2', - 'react', - 'vue', - 'html5', - 'simple', - 'custom', - 'aurelia', - 'javascript', -] as const; - -const PROJECT_SETTINGS_KEYS = [ - 'appOrigin', - 'keys', - 'cors', - 'csp', - 'secret', - 'pdfserver', - 'filetoken', - 'allowConfig', - 'allowConfigToForms', - 'custom', - 'formModule', - 'email', - 'captcha', - 'recaptcha', - 'esign', - 'google', - 'kickbox', - 'sqlconnector', - 'storage', - 'tokenParse', - 'oauth', - 'ldap', - 'saml', -] as const; - -const SUBMISSION_REFERENCE_FILES = [ - 'submission-definition.md', - 'submission-state.md', - 'submission-metadata.md', - 'submission-access.md', - 'submission-data.md', -] as const; - -const SUBMISSION_DEFINITION_PROPERTIES = [ - '_id', - '_fvid', - 'form', - 'project', - 'owner', - 'roles', - 'state', - 'access', - 'metadata', - 'data', - 'externalIds', - 'externalTokens', - 'permission', - 'created', - 'modified', - 'deleted', -] as const; - -const SUBMISSION_METADATA_KEYS = [ - 'timezone', - 'offset', - 'origin', - 'referrer', - 'browserName', - 'userAgent', - 'pathName', - 'onLine', - 'language', - 'headers', - 'ssoteam', - 'memberCount', - 'selectData', -] as const; - -const SUBMISSION_ACCESS_TYPES = [ - 'self', - 'create_own', - 'create_all', - 'read_own', - 'read_all', - 'update_own', - 'update_all', - 'delete_own', - 'delete_all', - 'team_read', - 'team_write', - 'team_admin', - 'team_access', -] as const; - -function exists(p: string): boolean { - try { - fs.lstatSync(p); - return true; - } catch { - return false; - } -} - -function readFrontmatter(file: string): Record<string, unknown> { - const raw = fs.readFileSync(file, 'utf8'); - return matter(raw).data as Record<string, unknown>; -} - -function readBody(file: string): string { - const raw = fs.readFileSync(file, 'utf8'); - return matter(raw).content; -} - -describe('formio-schema references directory layout', () => { - it('references/ contains every domain subdirectory and no top-level .md files', () => { - expect(exists(REFERENCES_DIR)).toBe(true); - const entries = fs.readdirSync(REFERENCES_DIR, { withFileTypes: true }); - const dirNames = entries - .filter((e) => e.isDirectory()) - .map((e) => e.name) - .sort(); - expect(dirNames).toEqual(['form', 'project', 'submission']); - const topLevelMd = entries.filter((e) => e.isFile() && e.name.endsWith('.md')); - expect(topLevelMd).toEqual([]); - }); - - it('references/form/ contains all five form-domain references and each is non-empty', () => { - expect(exists(FORM_REFS_DIR)).toBe(true); - for (const file of FORM_REFERENCE_FILES) { - const full = path.join(FORM_REFS_DIR, file); - expect(exists(full), `${file} must exist`).toBe(true); - const raw = fs.readFileSync(full, 'utf8'); - expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); - } - }); -}); - -describe('formio-schema SKILL.md frontmatter', () => { - it('name is formio-schema', () => { - const fm = readFrontmatter(SCHEMA_SKILL); - expect(fm.name).toBe('formio-schema'); - }); - - it('description spans the form, submission, and project domains', () => { - const fm = readFrontmatter(SCHEMA_SKILL); - const description = String(fm.description ?? '').toLowerCase(); - expect(description).toContain('form'); - expect(description).toContain('submission'); - expect(description).toContain('project'); - }); - - it('description has Not-for clause naming peer skills and contains no formio-form reference', () => { - const fm = readFrontmatter(SCHEMA_SKILL); - const description = String(fm.description ?? ''); - expect(description).toMatch(/not for:/i); - expect(description).toContain('formio-api'); - expect(description).toContain('formio-actions'); - expect(description).toContain('formio-resource-planner'); - expect(description).toContain('formio-application'); - expect(description).not.toContain('formio-form'); - }); -}); - -describe('formio-schema SKILL.md body indexes every domain', () => { - it('body references all five references/form/*.md paths', () => { - const body = readBody(SCHEMA_SKILL); - for (const file of FORM_REFERENCE_FILES) { - expect(body).toContain(`references/form/${file}`); - } - }); - - it('body references every references/submission/submission-*.md path and excludes submission/README.md', () => { - const body = readBody(SCHEMA_SKILL); - for (const file of SUBMISSION_REFERENCE_FILES) { - expect(body).toContain(`references/submission/${file}`); - } - expect(body).not.toContain('references/submission/README.md'); - }); - - it('body references every references/project/project-*.md path and excludes project README + billing', () => { - const body = readBody(SCHEMA_SKILL); - for (const file of PROJECT_REFERENCE_FILES) { - expect(body).toContain(`references/project/${file}`); - } - expect(body).not.toContain('references/project/README.md'); - expect(body).not.toContain('references/project/project-billing-and-usage.md'); - }); -}); - -describe('formio-schema submission-domain references', () => { - it('references/submission/ contains every authored file, none carry frontmatter, and README.md is absent', () => { - expect(exists(SUBMISSION_REFS_DIR)).toBe(true); - for (const file of SUBMISSION_REFERENCE_FILES) { - const full = path.join(SUBMISSION_REFS_DIR, file); - expect(exists(full), `${file} must exist`).toBe(true); - const raw = fs.readFileSync(full, 'utf8'); - expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); - expect(raw.startsWith('---'), `${file} must not start with YAML frontmatter`).toBe(false); - } - expect(exists(path.join(SUBMISSION_REFS_DIR, 'README.md'))).toBe(false); - }); - - it('submission-definition.md mentions every top-level Submission property name', () => { - const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-definition.md'), 'utf8'); - for (const prop of SUBMISSION_DEFINITION_PROPERTIES) { - expect(raw, `submission-definition.md must mention ${prop}`).toContain(prop); - } - }); - - it('submission-state.md documents both draft and submitted', () => { - const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-state.md'), 'utf8'); - expect(raw).toContain('draft'); - expect(raw).toContain('submitted'); - }); - - it('submission-metadata.md mentions every documented key and notes extensibility', () => { - const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-metadata.md'), 'utf8'); - for (const key of SUBMISSION_METADATA_KEYS) { - expect(raw, `submission-metadata.md must mention ${key}`).toContain(key); - } - expect(raw.toLowerCase()).toMatch(/extensible|open-ended|extension|arbitrary/); - }); - - it('submission-access.md mentions every AccessType value', () => { - const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-access.md'), 'utf8'); - for (const type of SUBMISSION_ACCESS_TYPES) { - expect(raw, `submission-access.md must mention ${type}`).toContain(type); - } - }); - - it('submission-data.md cross-links to the form references', () => { - const raw = fs.readFileSync(path.join(SUBMISSION_REFS_DIR, 'submission-data.md'), 'utf8'); - expect(raw).toMatch(/references\/form\//); - }); -}); - -describe('formio-schema project-domain references', () => { - it('references/project/ contains every authored file, none carry frontmatter, README + billing files absent', () => { - expect(exists(PROJECT_REFS_DIR)).toBe(true); - for (const file of PROJECT_REFERENCE_FILES) { - const full = path.join(PROJECT_REFS_DIR, file); - expect(exists(full), `${file} must exist`).toBe(true); - const raw = fs.readFileSync(full, 'utf8'); - expect(raw.trim().length, `${file} must be non-empty`).toBeGreaterThan(0); - expect(raw.startsWith('---'), `${file} must not start with YAML frontmatter`).toBe(false); - } - expect(exists(path.join(PROJECT_REFS_DIR, 'README.md'))).toBe(false); - expect(exists(path.join(PROJECT_REFS_DIR, 'project-billing-and-usage.md'))).toBe(false); - }); - - it('project-definition.md mentions every Project property name', () => { - const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-definition.md'), 'utf8'); - for (const prop of PROJECT_DEFINITION_PROPERTIES) { - expect(raw, `project-definition.md must mention ${prop}`).toContain(prop); - } - }); - - it('project-definition.md notes deployed projects use plan commercial', () => { - const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-definition.md'), 'utf8'); - expect(raw).toContain('commercial'); - }); - - it('project-type-and-framework.md enumerates every type and framework value', () => { - const raw = fs.readFileSync( - path.join(PROJECT_REFS_DIR, 'project-type-and-framework.md'), - 'utf8' - ); - for (const value of PROJECT_TYPE_VALUES) { - expect(raw, `must mention ProjectType ${value}`).toContain(value); - } - for (const value of PROJECT_FRAMEWORK_VALUES) { - expect(raw, `must mention ProjectFramework ${value}`).toContain(value); - } - }); - - it('project-type-and-framework.md documents Stage and Tenant creation patterns', () => { - const raw = fs.readFileSync( - path.join(PROJECT_REFS_DIR, 'project-type-and-framework.md'), - 'utf8' - ); - expect(raw).toContain('"type": "stage"'); - expect(raw).toContain('"type": "tenant"'); - expect(raw.toLowerCase()).toMatch(/parent project|portal|primary project/); - expect(raw.toLowerCase()).toMatch(/objectid/); - }); - - it('project-settings.md mentions every ProjectSettings key and the encryption contract', () => { - const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-settings.md'), 'utf8'); - for (const key of PROJECT_SETTINGS_KEYS) { - expect(raw, `must mention setting ${key}`).toContain(key); - } - expect(raw.toLowerCase()).toContain('encrypted'); - }); - - it('project-access.md documents project-level access shapes', () => { - const raw = fs.readFileSync(path.join(PROJECT_REFS_DIR, 'project-access.md'), 'utf8'); - expect(raw).toContain('ProjectRole'); - expect(raw).toContain('ProjectFormAccess'); - expect(raw).toContain('ProjectAccessInfo'); - expect(raw.toLowerCase()).toMatch(/project-level/); - expect(raw.toLowerCase()).toMatch(/form-level|form definition/); - expect(raw.toLowerCase()).toMatch(/submission-level|submission record/); - }); -}); - -describe('formio-form skill removal', () => { - it('plugin/skills/formio-form/ does not exist', () => { - expect(exists(path.join(SKILLS_DIR, 'formio-form'))).toBe(false); - }); - - it('no file under plugin/skills/ mentions formio-form', () => { - const offenders: string[] = []; - const stack = [SKILLS_DIR]; - while (stack.length > 0) { - const current = stack.pop()!; - for (const entry of fs.readdirSync(current, { withFileTypes: true })) { - const full = path.join(current, entry.name); - if (entry.isDirectory()) { - stack.push(full); - continue; - } - if (!entry.isFile()) continue; - if (!/\.(md|json|ts|py|txt)$/.test(entry.name)) continue; - const raw = fs.readFileSync(full, 'utf8'); - if (raw.includes('formio-form')) offenders.push(full); - } - } - expect(offenders, `files still mention formio-form: ${offenders.join(', ')}`).toEqual([]); - }); -}); diff --git a/plugin/README.md b/plugin/README.md index 8741f06..d92128a 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -38,7 +38,7 @@ Every step has an approval gate before any file is written or any MCP call hits | `formio-application` | Default "build me an app" orchestrator. Six-step pipeline (Intent → Plan → Deployment → MCP Config → Import → Framework). | | `formio-resource-planner` | Plans resources, fields, roles, actions, access — emits paired `template.md` + `template.json`. | | `formio-angular` | Angular framework implementor. Five-phase scaffold flow over `@formio/angular`. | -| `formio-schema` | Comprehensive Form.io JSON schema reference — form definitions today, with placeholder slots for submissions, actions, projects, and roles. | +| `formio-schema` | Comprehensive Form.io JSON schema reference — form, submission and project definitions today, with placeholder slots for actions and roles. | | `formio-actions` | Configuration reference for Form.io server-side actions. | | `formio-api` | Router into the full Form.io REST API surface (platform, project, runtime, PDF). | diff --git a/plugin/skills/formio-schema/references/project/project-access.md b/plugin/skills/formio-schema/references/project/project-access.md index 63806f0..2720185 100644 --- a/plugin/skills/formio-schema/references/project/project-access.md +++ b/plugin/skills/formio-schema/references/project/project-access.md @@ -48,7 +48,7 @@ The `Project` envelope ships with a small family of types that describe project | ------------------ | ----------- | ------------------------------------------------------------------------------------------------------ | | `_id` | `string` | Form ID. | | `title` | `string` | Form title. | -| `name` | `string` | Form machine name. | +| `name` | `string` | Form name. | | `path` | `string` | Form URL path. | | `access` | `Access[]` | Form-level access entries (who can see/modify the form definition). | | `submissionAccess` | `Access[]` | Submission-level access entries (who can create/read/update/delete submissions against the form). | diff --git a/plugin/skills/formio-schema/references/project/project-definition.md b/plugin/skills/formio-schema/references/project/project-definition.md index 660f5b2..792ebb9 100644 --- a/plugin/skills/formio-schema/references/project/project-definition.md +++ b/plugin/skills/formio-schema/references/project/project-definition.md @@ -32,7 +32,6 @@ A project is the top-level container for forms, resources, roles, submissions, a | `config` | `Record<string, string>` | No | Public configuration key-value pairs. Surfaced to forms when `settings.allowConfigToForms` is set. See `project-settings.md`. | | `protect` | `boolean` | No | `true` to prevent destructive operations on the project. Default `false`. | | `settings` | `ProjectSettings` | No | All project-level settings (API keys, CORS, integrations, authorization, custom JS/CSS, etc.). See `project-settings.md`. **Encrypted at rest.** | -| `remoteSecret` | `string` | No | Secret used when this project is connected as a remote stage from another project. | | `builderConfig` | `object` | No | Form-builder UI configuration for the project. | | `formDefaults` | `{ revisions?: 'current' \| 'original' }` | No | Default behavior applied to forms created in this project — currently just the default revision mode. | | `public` | `{ custom?: { css?, js? }, formModule? }` | No | Public-facing custom CSS / JS / form-module bundle exposed to unauthenticated form renderers. | diff --git a/plugin/skills/formio-schema/references/project/project-settings.md b/plugin/skills/formio-schema/references/project/project-settings.md index bb48688..aca1cf1 100644 --- a/plugin/skills/formio-schema/references/project/project-settings.md +++ b/plugin/skills/formio-schema/references/project/project-settings.md @@ -22,6 +22,7 @@ This matters when you're reading project documents out-of-band (e.g., from a dat | `cors` | `string` | CORS allowed-origins value the server returns. `"*"` for permissive; comma-separated for explicit allowlist. | | `csp` | `string` | Content-Security-Policy header value the server attaches to project responses. | | `secret` | `string` | Project-level shared secret used by signing flows. | +| `remoteSecret` | `string` | Secret set after this project is connected to a remote stage. It is the PORTAL_SECRET of the remove env. | | `pdfserver` | `string` | URL of the PDF server this project routes PDF operations to. | | `filetoken` | `string` | Signing token used by the file upload / download flow. | | `allowConfig` | `boolean` | Surface `config` to portal UI callers. | @@ -29,7 +30,7 @@ This matters when you're reading project documents out-of-band (e.g., from a dat | `custom` | `{ css?, js? }` | Custom global CSS / JS injected into project-rendered forms. | | `formModule` | `string` | URL or module identifier for a custom form-module bundle to load. | | `email` | `ProjectEmailConfig` | Email provider config — one of `smtp`, `sendgrid`, `mailgun`. See `integrations/email.ts` upstream. | -| `captcha` | `ProjectCaptchaConfig` | reCAPTCHA / hCaptcha config — `{ siteKey, secretKey }`. | +| `captcha` | `ProjectCaptchaConfig` | reCAPTCHA / Cloudflare Turnstile config — `{ siteKey, secretKey }`. | | `recaptcha` | `ProjectCaptchaConfig` | Legacy reCAPTCHA configuration alongside `captcha`. Same shape. | | `esign` | `ProjectESignConfig` | Box e-sign configuration including enterprise ID and Box app credentials. See `integrations/eSign.ts`. | | `google` | `ProjectGoogleDriveConfig` | Google Drive integration — `{ clientId, cskey, refreshtoken }`. See `integrations/dataConnections.ts`. | diff --git a/plugin/skills/formio-schema/references/submission/submission-access.md b/plugin/skills/formio-schema/references/submission/submission-access.md index 8f71900..7dbb9b9 100644 --- a/plugin/skills/formio-schema/references/submission/submission-access.md +++ b/plugin/skills/formio-schema/references/submission/submission-access.md @@ -1,55 +1,72 @@ # Submission Access Reference -`access` on a submission is a row-level access-control override applied to that specific submission. It uses the same `Access` entry shape as the form's `submissionAccess`, but it lives on the individual record rather than the form definition, so it overrides the form-level rule for that one submission. +`access` on a submission is a row-level access-control override stored on that one record. It grants permission on the submission to a set of **resource/user IDs** — not to roles. Use it to open a single submission to specific users (or groups of users) beyond whatever the form's `submissionAccess` already allows. + +> **Important:** the submission-level `access` array is **not** role-based. Each entry maps a permission `type` to a list of `resources` (IDs). There is no `roles` key on a submission access entry — role-based grants live on the form definition's `submissionAccess` array (see `references/form/form-definition.md`). The server silently ignores any entry that lacks a `resources` key. ## Access entry shape -| Property | Type | Required | Description | -| ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------- | -| `type` | `string` | **Yes** | One of the AccessType values listed below. | -| `roles` | `string[]` | **Yes** | Role IDs granted this access type for the submission. | -| `resources` | `string[]` | No | Resource (form) IDs limiting the scope of the grant — used by team and self-referential access types. | +| Property | Type | Required | Description | +| ----------- | ---------- | -------- | --------------------------------------------------------------------------------------------- | +| `type` | `string` | **Yes** | One of the AccessType values below. | +| `resources` | `string[]` | **Yes** | IDs granted this permission on the submission. Each ID is a user submission ID or a group submission ID. An entry without `resources` is skipped. | -The `access` field on the Submission envelope is an array of these entries. Each entry maps one access type to the set of roles that get it. +The `access` field on the Submission envelope is an array of these entries. Each entry maps one permission `type` to the set of IDs that get it. ## AccessType values -| Value | Meaning | -| -------------- | -------------------------------------------------------------------------------------------------------------------- | -| `self` | Grants access to the user whose user-resource submission is referenced by `owner`. Used to express "the submitter can read/update their own record." | -| `create_own` | Grants creation rights to roles, but only for records whose `owner` is the requester's own user submission. | -| `create_all` | Grants unrestricted creation rights to roles. | -| `read_own` | Grants read rights to roles, but only for records whose `owner` matches the requester. | -| `read_all` | Grants unrestricted read rights to roles. | -| `update_own` | Grants update rights to roles, but only for records the requester owns. | -| `update_all` | Grants unrestricted update rights to roles. | -| `delete_own` | Grants delete rights to roles, but only for records the requester owns. | -| `delete_all` | Grants unrestricted delete rights to roles. | -| `team_read` | Grants read rights to members of the listed teams (Team plan / Enterprise). | -| `team_write` | Grants read + update rights to team members. | -| `team_admin` | Grants full read + update + delete rights to team members. | -| `team_access` | Grants access to a specific resource (form) the team has been granted access to — used in combination with `resources`. | +| Value | Grants | +| -------- | -------------------------------------------- | +| `read` | read | +| `create` | create | +| `update` | update | +| `delete` | delete | +| `write` | read + create + update | +| `admin` | read + create + update + delete | + +Each ID in `resources` is added to the submission's effective access set for the granted permission(s). A request gains access when the requesting user's own ID — or an ID the user is a member of — appears in that set. `read`, `create`, `update`, and `write` grants are explicitly flagged so they do **not** confer implicit admin (delete) rights on the record; only `admin` confers full delete. + +## What an ID in `resources` resolves to + +The IDs in `resources` are matched against the requester's identity set, which lets the same mechanism cover two distinct use cases: + +### 1. Field-based resource access + +Grants access based on a **Resource being selected in the submission** — the selected user's own submission ID is written into `resources`. Access follows the field value. + +**Example:** A patient record form has a "Primary Physician" select component backed by the Physician resource. When a physician is chosen, that physician's user ID is added to the record's `access` with `type: "read"`, so that physician — and only that physician — can read the patient record. Reassign the field, and access moves with it. + +### 2. Group-based resource access + +Grants access based on a user's **membership in a Group** that is assigned to the submission — the group's submission ID is written into `resources`, and every user who belongs to that group inherits the permission. Here the "resource" is the group, not an individual user. + +**Example:** A physician is the group. When that physician is assigned to a patient, the physician group's ID is added to the `access` of **all** of that patient's documents (form submissions). Every user belonging to the physician group can then read all of that patient's records, without listing each user individually. ## Layering with form-level submissionAccess -The form definition can carry its own `submissionAccess` array (form-level rule that applies to every submission against that form — see `references/form/form-definition.md`). The submission's own `access` array (this reference) is checked on top of the form-level rule. Where both define the same `type`, the submission's row-level entry wins for that specific record. Where only one defines a `type`, that single source applies. This lets a form's default be "owners read their own" while a specific record opens itself up to a broader audience without changing the form definition. +The form definition can carry its own `submissionAccess` array — a **role-based** rule that applies to every submission against that form (see `references/form/form-definition.md`). The submission's own `access` array (this reference) is checked on top of that form-level rule: + +- Form-level `submissionAccess` grants by **role** (e.g. "Authenticated users can read_own"). +- Submission-level `access` grants by **resource/user ID** on a single record. + +The two are additive: a request is allowed if either source grants it. This lets a form's default stay restrictive while individual records open themselves to specific physicians or groups without editing the form definition. ## Worked example -A submission that overrides the form-level access to additionally grant read access to a manager role for this specific record: +A patient record granting read access to the selected primary physician (field-based) and full read access to the assigned physician group (group-based): ```json { "_id": "5f8d0c4e9b1e8a0017a1b2c3", "owner": "5f8d0c4e9b1e8a0017a1aaaa", "access": [ - { "type": "read_all", "roles": ["5f8d0c4e9b1e8a0017a1man1"] }, - { "type": "update_own", "roles": ["5f8d0c4e9b1e8a0017a1emp1"] } + { "type": "read", "resources": ["6a188a52bb04c38a9102dff4"] }, + { "type": "write", "resources": ["6a188a52bb04c38a9102e001"] } ] } ``` ## See also -- `references/form/form-definition.md` for the form-level `access` and `submissionAccess` arrays. +- `references/form/form-definition.md` for the form-level `access` and role-based `submissionAccess` arrays. - The `formio-api` skill's `runtime-access-control` reference for how the server resolves these entries on a request. diff --git a/plugin/skills/formio-schema/references/submission/submission-data.md b/plugin/skills/formio-schema/references/submission/submission-data.md index eadf34a..27a5f39 100644 --- a/plugin/skills/formio-schema/references/submission/submission-data.md +++ b/plugin/skills/formio-schema/references/submission/submission-data.md @@ -37,7 +37,7 @@ For exact per-component value shapes (e.g., what a `file` component stores, what ## Address mode (worked example) -The `address` component stores either an autocomplete result or a manually entered address, distinguished by a `mode` discriminator. This is the canonical example of "component type dictates value shape" — useful as a model for reading any other component's nested data. +The `address` component stores either an autocomplete result or a manually entered address, distinguished by a `mode` discriminator. If the address uses an autocomplete provider, then the value of the address component is the value provided by the provider (Google, Open Street Maps, etc). This is the canonical example of "component type dictates value shape" — useful as a model for reading any other component's nested data. ```json { diff --git a/plugin/skills/formio-schema/references/submission/submission-definition.md b/plugin/skills/formio-schema/references/submission/submission-definition.md index c29a61c..02757f8 100644 --- a/plugin/skills/formio-schema/references/submission/submission-definition.md +++ b/plugin/skills/formio-schema/references/submission/submission-definition.md @@ -13,6 +13,7 @@ A submission is a discrete record of values submitted to a single form or resour | `form` | `string` | No | Form ID this submission belongs to. Server-populated from the request path. | | `project` | `string` | No | Project ID containing the parent form. Server-populated. | | `owner` | `string` | No | Submission ID of the user that created this submission — the foreign key to a user-resource submission in the same project. | +| `externalOwner` | `{ sub, iss, customIdClaim? }` | No | OIDC SSO external owner — `sub` (subject) and `iss` (issuer) of the external identity, with optional `customIdClaim: { key, value }` for the `idPath` resolution. Server-only — not in the upstream TypeScript declaration. | | `roles` | `string[]` | No | Role IDs assigned to this submission when the submission represents a user (i.e., when the parent form is a user resource). See `submission-access.md`. | | `state` | `'draft' \| 'submitted'` | No | Lifecycle state of the submission. See `submission-state.md` for when each value is written. | | `access` | `Access[]` | No | Row-level access overrides applied to this specific submission. See `submission-access.md` for the entry shape and the layering rules with form-level `submissionAccess`. | From 1636dcfc0b8b3288b38f503b3eb3a0491a9aa608 Mon Sep 17 00:00:00 2001 From: blakekrammes <blakekrammes@protonmail.com> Date: Thu, 28 May 2026 17:10:38 -0500 Subject: [PATCH 3/3] Add small changes - Remove local file references from project-settings skill reference - Add externalOwner to form-definition reference to match project and submission references - Remove language referring to portal base/primary project in project type reference --- .../formio-schema/references/form/form-definition.md | 1 + .../formio-schema/references/project/project-settings.md | 2 -- .../references/project/project-type-and-framework.md | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/skills/formio-schema/references/form/form-definition.md b/plugin/skills/formio-schema/references/form/form-definition.md index ec37e8e..852c6a6 100644 --- a/plugin/skills/formio-schema/references/form/form-definition.md +++ b/plugin/skills/formio-schema/references/form/form-definition.md @@ -21,6 +21,7 @@ The top-level object representing a form or resource. Only `components` is requi | `submissionAccess` | `Access[]` | No | Submission-level access permissions (who can create/read/update/delete submissions). | | `fieldMatchAccess` | `object` | No | Field-level access control rules. | | `owner` | `string` | No | Submission ID of the form owner. | +| `externalOwner` | `{ sub, iss, customIdClaim? }` | No | OIDC SSO external owner — `sub` (subject) and `iss` (issuer) of the external identity, with optional `customIdClaim: { key, value }` for the `idPath` resolution. Server-only — not in the upstream TypeScript declaration. | | `machineName` | `string` | No | Globally unique machine name across projects. | | `components` | `Component[]` | **Yes** | Array of form components defining the form's fields and layout. | | `settings` | `FormSettings` | No | Form-level display and behavior settings. | diff --git a/plugin/skills/formio-schema/references/project/project-settings.md b/plugin/skills/formio-schema/references/project/project-settings.md index aca1cf1..8938b81 100644 --- a/plugin/skills/formio-schema/references/project/project-settings.md +++ b/plugin/skills/formio-schema/references/project/project-settings.md @@ -46,8 +46,6 @@ This matters when you're reading project documents out-of-band (e.g., from a dat Each integration / authorization block is its own sub-type with its own field set. Documenting every provider's full shape here would duplicate the upstream type files and balloon this reference. Instead: -- For per-provider integration shapes, see the upstream TypeScript files under `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/integrations/`. -- For per-provider authorization shapes, see `~/Documents/formio/modules/nirvana/packages/core/src/types/project/settings/authorization/`. - For runtime behavior of these providers (how the server authenticates against them, what endpoints they expose), see the `formio-api` skill's `runtime-auth`, `platform-auth`, and `project-auth` references. ## See also diff --git a/plugin/skills/formio-schema/references/project/project-type-and-framework.md b/plugin/skills/formio-schema/references/project/project-type-and-framework.md index c87bfc0..126bf33 100644 --- a/plugin/skills/formio-schema/references/project/project-type-and-framework.md +++ b/plugin/skills/formio-schema/references/project/project-type-and-framework.md @@ -6,13 +6,13 @@ Two discriminators sit on the `Project` envelope: `type` distinguishes a regular | Value | Meaning | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `'project'` | A regular standalone project. Default value. Used for the portal/primary project and for any project that isn't a derived environment. | -| `'stage'` | A Stage of a parent project — a deployment environment (Dev / Staging / etc.) that branches from a primary project. Requires `project` to be set to the parent project's ObjectId. | +| `'project'` | A regular standalone project. Default value. Used for any project that isn't a derived environment. | +| `'stage'` | A Stage of a parent project — a deployment environment (Dev / Staging / etc.) that branches from the parent project. Requires `project` to be set to the parent project's ObjectId. | | `'tenant'` | A tenant of the deployment — a multi-tenant child project that inherits its parent's plan at runtime. Tenants are server-keyed differently from stages. | ### Stage creation pattern -A Stage is a project document with `type: 'stage'` and a `project` field pointing at its parent. The parent is typically the portal (primary) project the stage's environment branches from. +A Stage is a project document with `type: 'stage'` and a `project` field pointing at its parent. `project` refers to the parent project the stage was created in. ```json { @@ -27,7 +27,7 @@ A Stage is a project document with `type: 'stage'` and a `project` field pointin Required minimum for a Stage: - `type: 'stage'` -- `project: <parent project ObjectId>` — the `_id` of the parent project (typically the portal / primary project) +- `project: <parent project ObjectId>` — the `_id` of the parent project - `title` and `name` (inherited from the standard Project envelope) `stageTitle` is the human-readable label shown in the Stage selector UI.