diff --git a/.github/workflows/schema-publish.yml b/.github/workflows/schema-publish.yml new file mode 100644 index 000000000..7446010b8 --- /dev/null +++ b/.github/workflows/schema-publish.yml @@ -0,0 +1,151 @@ +name: Publish schema + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + release_tag: + description: Existing stable release tag (vX.Y.Z) + required: true + type: string + pull_request: + paths: + - 'packages/schema/**' + - 'packages/sdk/src/**' + - 'packages/sdk/package-lock.json' + - 'scripts/generate-json-schema.mjs' + - 'scripts/schema-*.mjs' + - '.github/workflows/schema-publish.yml' + +permissions: + contents: read + +concurrency: + group: schema-${{ github.event_name == 'pull_request' && github.ref || 'publish' }} + cancel-in-progress: false + +jobs: + validate: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_tag || github.ref }} + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.4.0' + - name: Install generator and test dependencies + run: npm ci --prefix packages/sdk --ignore-scripts + - name: Regenerate and check committed schema + run: | + node scripts/generate-json-schema.mjs + git diff --exit-code -- packages/schema/flows.schema.json + cp packages/schema/flows.schema.json /tmp/flows.schema.first.json + node scripts/generate-json-schema.mjs + diff -q /tmp/flows.schema.first.json packages/schema/flows.schema.json + - name: Schema parity and smoke + working-directory: packages/schema + run: bun run test + - name: Version package from stable release tag + if: github.event_name != 'pull_request' + id: version + env: + RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} + run: | + if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo 'Schema publication requires a stable vX.Y.Z tag.' >&2 + exit 1 + fi + test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")" + version="${RELEASE_TAG#v}" + npm version --prefix packages/schema "$version" --no-git-tag-version --allow-same-version + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Pack data-only artifact + if: github.event_name != 'pull_request' + run: | + mkdir -p dist/schema + npm pack ./packages/schema --pack-destination dist/schema --ignore-scripts + cp packages/schema/flows.schema.json dist/schema/ + - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' + with: + name: schema-release + path: dist/schema/ + if-no-files-found: error + + npm: + if: github.event_name != 'pull_request' + needs: validate + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: https://registry.npmjs.org + - uses: actions/download-artifact@v4 + with: + name: schema-release + path: release + - name: Publish data package + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: npm publish "release/relayflows-schema-${RELEASE_VERSION}.tgz" --access public --ignore-scripts + + pages: + if: github.event_name != 'pull_request' + needs: [validate, npm] + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: schema-release + path: release + - name: Assemble versioned schema paths + env: + RELEASE_VERSION: ${{ needs.validate.outputs.version }} + run: | + node --input-type=module <<'NODE' + import { readFileSync, mkdirSync, copyFileSync } from 'node:fs'; + const schema = JSON.parse(readFileSync('release/flows.schema.json', 'utf8')); + const dialect = new URL(schema.$id).pathname.split('/')[1]; + if (!/^v\d+\.\d+$/.test(dialect)) throw new Error('Unexpected schema dialect path'); + for (const version of [dialect, `v${process.env.RELEASE_VERSION}`]) { + mkdirSync(`public/${version}`, { recursive: true }); + copyFileSync('release/flows.schema.json', `public/${version}/flows.schema.json`); + } + NODE + # Retain old versions in gh-pages; deploy the complete retained tree with + # the official Pages API (a GITHUB_TOKEN push alone need not build Pages). + - uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: public + keep_files: true + - uses: actions/checkout@v4 + with: + ref: gh-pages + path: retained-pages + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: retained-pages + - uses: actions/deploy-pages@v4 + id: deployment diff --git a/docs/EDITOR.md b/docs/EDITOR.md index 62d833ae8..0685eaa61 100644 --- a/docs/EDITOR.md +++ b/docs/EDITOR.md @@ -98,3 +98,129 @@ families remain follow-up work and are not advertised as shipped: Those rules need their own SDK parity fixtures and source spans. YAML, runtime probes, quick fixes, and a full language server remain outside this package's scope. This slice changes no SDK, surface, or kernel semantics. +# YAML and JSON editor validation + +Register the published draft 2020-12 schema for the canonical declarative +`FlowSpec` dialect (`version`, `steps`, and explicit `type` fields). The schema +provides completion, hover documentation, and structural diagnostics using your +editor's YAML/JSON support; no Relayflows extension, server, or npm install is +needed. VS Code and Cursor need their usual YAML language support (Red Hat YAML +if it is not already installed). JetBrains includes schema-backed YAML support. + +The versioned schema URL is reserved for publication: +`https://schema.relayflows.dev/v0.1/flows.schema.json`. Until that hosting is +configured, use the committed `packages/schema/flows.schema.json` locally. +The npm package also ships this exact file, with zero runtime dependencies. + +## VS Code / Cursor + +Add to `.vscode/settings.json` (or user settings): + +```json +{ + "yaml.validate": true, + "yaml.schemas": { + "https://schema.relayflows.dev/v0.1/flows.schema.json": ["**/*.flow.yaml", "**/*.flow.yml"] + }, + "json.schemas": [{ + "fileMatch": ["**/*.flow.json"], + "url": "https://schema.relayflows.dev/v0.1/flows.schema.json" + }] +} +``` + +For offline use, replace the URL with `./packages/schema/flows.schema.json` +(relative to the workspace) or `./node_modules/@relayflows/schema/flows.schema.json` +if installed with `npm install --save-dev @relayflows/schema`. +The modeline and mapping behavior is documented by +[yaml-language-server](https://github.com/redhat-developer/yaml-language-server#language-server-settings). + +## JetBrains + +1. Open Settings / Preferences → Languages & Frameworks → Schemas and DTDs → + JSON Schema Mappings. +2. Add a mapping named **Relayflows**, select JSON Schema version **2020-12**, + and choose the URL above or the local `flows.schema.json` file. +3. Add file path patterns `*.flow.yaml`, `*.flow.yml`, and `*.flow.json`. +4. Open a flow and verify that the status-bar schema selector says **Relayflows**. + +See JetBrains' [YAML schema support](https://www.jetbrains.com/help/idea/yaml.html) +and [JSON schema mappings](https://www.jetbrains.com/help/idea/json.html). + +## Neovim + +With `yaml-language-server` on PATH, Neovim 0.11+ can start it directly: + +```lua +vim.lsp.config('relayflows_yaml', { + cmd = { 'yaml-language-server', '--stdio' }, + filetypes = { 'yaml' }, + root_markers = { '.git', 'flows.json' }, + settings = { + yaml = { + validate = true, + schemas = { + ['https://schema.relayflows.dev/v0.1/flows.schema.json'] = { + '**/*.flow.yaml', '**/*.flow.yml', + }, + }, + }, + }, +}) +vim.lsp.enable('relayflows_yaml') +``` + +If you already use `nvim-lspconfig`'s `yamlls`, merge the `settings.yaml` block +into that configuration instead of starting a second server. Local absolute +schema paths also work. + +## Per-file fallback + +Put this on the first line of a YAML flow: + +```yaml +# yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +version: '0.1.0' +steps: + - id: greet + type: deterministic + command: echo hello +``` + +A relative local URL is resolved from the YAML file: for the repository's +`testdata/hello-deterministic.flow.yaml`, use +`# yaml-language-server: $schema=../packages/schema/flows.schema.json`. +The language server modeline takes precedence over settings. `flows check` +emits `editor_schema_missing` as a warning when a `.flow.yaml` file lacks this +first-line comment, including when a settings mapping is already configured. +It never changes the file or refuses a valid flow because of that warning. + +## Validation scope + +The schema follows `packages/sdk/src/spec.ts`, with structural constraints from +runtime validation: closed objects, discriminated step types, required fields, +value bounds, named-agent declaration shapes, input selectors, and output +schema keyword shapes for SDK-supported drafts. The generated file bundles its +meta-schemas and needs no network after the file itself has loaded. + +`flows check` remains necessary for unique step/trigger IDs, dependency cycles, +named-agent and input-source lookup, declared output-path lookup, JSON Schema +reference resolution/termination and regex compilation, model allowlists, CLI +authentication, and executor readiness. JSON Schema cannot express comparisons +against arbitrary values elsewhere in the flow. Embedded schemas may use +unknown annotation keywords, just as the SDK permits. Future surface shorthand +such as `run:` and headers such as `identity:` are not canonical `FlowSpec` +fields today; both `identity:` and its typo `identitty:` are rejected here. +Compiled snake_case kernel JSON is a separate dialect and is not the editor +schema's entry point, even though `flows check` can also ingest it. + +## Editor smoke procedure + +1. Register the local schema and open `testdata/hello-deterministic.flow.yaml`. +2. Add `identitty: chief` at the root, save, and observe an unknown-property + squiggle. Remove the whole added line, save, and confirm it clears. +3. Change a deterministic step's `command` to `commmand`; confirm the unknown + field and missing required `command` diagnostics. Undo and save. +4. Hover `timeoutMs` and inspect completion after `type: agent`. + +Do not correct `identitty` to `identity` in this dialect: neither is supported. diff --git a/ops/reviews/20260911-spec-L1-schema.md b/ops/reviews/20260911-spec-L1-schema.md new file mode 100644 index 000000000..eddc7d4ec --- /dev/null +++ b/ops/reviews/20260911-spec-L1-schema.md @@ -0,0 +1,314 @@ +# L1 schema acceptance evidence — 2026-09-11 + +Suggested PR title: `feat(schema): publish @relayflows/schema for YAML editor validation` +The lead supplies the tracking issue number when opening the push-only PR. + +JSON Schema gives existing YAML/JSON editors completion, hover documentation, +and structural validation with no Relayflows-specific extension or process. +The package is data-only. A small TypeScript AST walker reads all exported +interfaces and aliases in SDK spec.ts; runtime value refinements are isolated +from generated field shapes. The SDK's existing TypeScript/Ajv dependencies are +build/test tooling only. Output meta-schemas are bundled locally and attributed. + +The editor entry point is FlowSpec. Kernel definitions are included for type +coverage but do not broaden the entry point. No spec.ts or kernel schema-bound +changes were needed. The existing SDK bounded-reference checker accepts the +emitted schema. Runtime graph/reference/environment checks remain explicitly +outside JSON Schema's structural scope. + +The check hint is warning-only and preserves file bytes and refusal behavior. +The RunReport diagnostic type follows CheckReport so check warnings survive +flows run. The existing exact CLI report expectation now pins the added warning. + +Release CI regenerates and checks drift before publication, packs only data and +documentation, then publishes npm and deploys retained versioned Pages files. +No publication was run. NPM_TOKEN, GitHub Actions Pages, custom domain and DNS +must be configured by repository maintainers. GITHUB_TOKEN-created release tags +need the documented explicit schema workflow dispatch. npm package versions +follow stable release tags; the schema URL tracks the authoring dialect v0.1. + +## Dependency setup + +All commands run from this worktree unless a cwd is shown. Node 25.8.1, Bun +1.4.2, VS Code's installed app, Red Hat YAML 1.24.0. The SDK's registry surface +2.0.8 lacks current source exports, so use the locally built surface, as the +repository release workflow does: + +```sh +npm ci --prefix packages/sdk --ignore-scripts --no-audit --no-fund +npm ci --prefix packages/surface --ignore-scripts --no-audit --no-fund +npm run --prefix packages/surface build +# cwd packages/surface +npm pack --pack-destination /tmp --ignore-scripts +# cwd packages/sdk +npm install --no-save --package-lock=false --ignore-scripts /tmp/relayflows-surface-2.0.8.tgz +``` + +## Schema parity and packaging smoke + +Command: `PATH="/Users/khaliqgant/.bun/bin:$PATH" bun run --cwd packages/schema test` + +Literal output: + +```text +$ bun test tests +bun test v1.4.2 (744846f84) + +tests/smoke.test.ts: +(pass) all exported spec type nodes have documented definitions [20.21ms] +(pass) regeneration is byte-stable and committed schema has not drifted [309.45ms] +(pass) npm tarball contains only data and documentation with no runtime dependencies [366.96ms] + +tests/parity.test.ts: +(pass) flows check fixture parity: backlog-picker.flow.yaml [15.60ms] +(pass) flows check fixture parity: dir-watcher.flow.yaml [213.32ms] +(pass) flows check fixture parity: hello-agent.flow.yaml [11.60ms] +(pass) flows check fixture parity: hello-deterministic.flow.yaml [4.01ms] +(pass) flows check fixture parity: hello-ladder.flow.yaml [23.62ms] +(pass) flows check fixture parity: hello-llm.flow.yaml [21.34ms] +(pass) flows check fixture parity: hn-monitor.flow.yaml [131.31ms] +(pass) flows check fixture parity: json-schema-invalid.flow.yaml [6.38ms] +(pass) flows check fixture parity: step-memory.flow.yaml [130.77ms] +(pass) flows check fixture parity: step-placement.flow.yaml [2.65ms] +(pass) flows check fixture parity: tick-heartbeat.flow.yaml [36.51ms] +(pass) structural parity: unknown root key [0.52ms] +(pass) structural parity: unsupported version [0.06ms] +(pass) structural parity: no steps [0.26ms] +(pass) structural parity: step typo [0.10ms] +(pass) structural parity: empty command [0.02ms] +(pass) structural parity: positive timeout [0.04ms] +(pass) structural parity: fractional retry [0.04ms] +(pass) structural parity: wrong step field [0.06ms] +(pass) structural parity: nonzero exit gate [0.06ms] +(pass) structural parity: legacy zero exit gate [0.04ms] +(pass) structural parity: boolean schema [0.47ms] +(pass) structural parity: nested invalid schema [4.12ms] +(pass) structural parity: bad memory budget [0.12ms] +(pass) structural parity: unsafe memory budget [0.05ms] +(pass) structural parity: empty memory query [0.03ms] +(pass) structural parity: unsafe duration [0.13ms] +(pass) structural parity: bad money [0.04ms] +(pass) structural parity: bad input index [0.13ms] +(pass) structural parity: blank input name [0.09ms] +(pass) structural parity: trigger silence budget [0.04ms] +(pass) structural parity: llm: output object [7.19ms] +(pass) structural parity: llm: boolean output [0.12ms] +(pass) structural parity: llm: output and verification [5.83ms] +(pass) structural parity: llm: exit gate [0.14ms] +(pass) structural parity: llm: trimmed model [0.07ms] +(pass) structural parity: llm: control in model [0.03ms] +(pass) structural parity: agent: output object [7.51ms] +(pass) structural parity: agent: boolean output [0.11ms] +(pass) structural parity: agent: output and verification [3.65ms] +(pass) structural parity: agent: exit gate [0.10ms] +(pass) structural parity: agent: trimmed model [0.04ms] +(pass) structural parity: agent: control in model [0.03ms] +(pass) step examples compile and validate [0.68ms] +(pass) generated schema satisfies the existing bounded-reference rule [2.65ms] +(pass) semantic checks remain explicit runtime responsibilities [0.81ms] +(pass) embedded dialect http://json-schema.org/draft-04/schema# [6.26ms] +(pass) embedded dialect http://json-schema.org/draft-06/schema# [5.35ms] +(pass) embedded dialect http://json-schema.org/draft-07/schema# [7.79ms] +(pass) embedded dialect https://json-schema.org/draft/2019-09/schema [10.29ms] +(pass) embedded dialect https://json-schema.org/draft/2020-12/schema [8.63ms] +(pass) header hint is warning-only, first-line aware, and never edits input [15.67ms] +(pass) canonical surface parity: "repo" [0.13ms] +(pass) canonical surface parity: "/repo/src" [0.04ms] +(pass) canonical surface parity: "pr://github/example" [0.02ms] +(pass) canonical surface parity: "/" [0.02ms] +(pass) canonical surface parity: "pr://" [0.02ms] +(pass) canonical surface parity: "" [0.04ms] +(pass) canonical surface parity: " repo" [0.03ms] +(pass) canonical surface parity: "repo " [0.02ms] +(pass) canonical surface parity: "repo//src" [0.02ms] +(pass) canonical surface parity: "repo/../src" [0.02ms] +(pass) canonical surface parity: "repo/." [0.01ms] +(pass) canonical surface parity: ":/bad//path" [0.02ms] +(pass) named declarations and selected input paths use authoring shapes [25.84ms] + + 68 pass + 0 fail + 3408 expect() calls +Ran 68 tests across 2 files. [1.70s] +``` + +The 11 top-level fixture files are copied byte-for-byte into a temporary project +with local executable CLI probes and the fixture model registry. This runs the +real checkFlow implementation with no ignored environment refusals and no live +provider calls. The negative anchor asserts the actual embedded `/type` node. +Additional cases compare structural accept/refuse decisions against compileSpec; +explicit scope tests show the DAG and embedded ref-cycle checks still require +runtime validation. + +## SDK regression + +Command: `npm run --prefix packages/sdk typecheck` + +Literal output: + +```text + +> @relayflows/sdk@2.0.8 typecheck +> tsc --noEmit && tsc -p tsconfig.type-tests.json + +``` + +Command: `npm run --prefix packages/sdk typecheck:tests` + +Literal output: + +```text +> @relayflows/sdk@2.0.8 typecheck:tests +> tsc -p tsconfig.tests.json +``` + +Command (cwd `packages/sdk`): +`./node_modules/.bin/vitest run tests/preflight.test.ts tests/cli.test.ts` + +Literal output: + +```text + + RUN v2.1.9 /Users/khaliqgant/flows-spec-L1-schema/packages/sdk + + ✓ tests/preflight.test.ts (27 tests) 23ms + ✓ tests/cli.test.ts (63 tests) 11015ms + ✓ flows check CLI > refuses a typo model before probing or contacting relayflowd 671ms + ✓ flows check CLI > maps every input refusal path to its declared kind without raw exceptions 1849ms + ✓ flows run/resume CLI over the journal protocol > parses run options, submits the kernel dialect, and exits 0 on success 644ms + ✓ flows run/resume CLI over the journal protocol > exits 1 and emits the declared completionReason for a failed run 576ms + ✓ flows run/resume CLI over the journal protocol > exits 3 and names the parked llm step 623ms + ✓ flows run/resume CLI over the journal protocol > reports a needs_human agent step as parked for human recovery 610ms + ✓ flows run/resume CLI over the journal protocol > classifies a typed hello refusal as a protocol error, not an unreachable daemon 563ms + ✓ flows run/resume CLI over the journal protocol > follows a dispatched worker step instead of reporting a protocol error 659ms + ✓ flows run/resume CLI over the journal protocol > bounds a worker wait by its lease and reports what it is waiting for 640ms + ✓ flows run/resume CLI over the journal protocol > resumes a parked run from snapshot step types without reading journal sequence one 569ms + ✓ flows run/resume CLI over the journal protocol > maps only run_not_found resumes to exit 2 1724ms + + Test Files 2 passed (2) + Tests 90 passed (90) + Start at 11:30:15 + Duration 11.48s (transform 213ms, setup 0ms, collect 498ms, tests 11.04s, environment 0ms, prepare 62ms) + +``` + +## Repeatability and workflow syntax + +Commands: + +```sh +node scripts/generate-json-schema.mjs /tmp/relayflows-schema-evidence/first.json +node scripts/generate-json-schema.mjs /tmp/relayflows-schema-evidence/second.json +diff -q /tmp/relayflows-schema-evidence/first.json /tmp/relayflows-schema-evidence/second.json +diff -q /tmp/relayflows-schema-evidence/first.json packages/schema/flows.schema.json +actionlint .github/workflows/schema-publish.yml +git diff --check +``` + +Literal output (exit 0; diff/actionlint/git diff produced no output): + +```text +Generated packages/schema/flows.schema.json (58 definitions) +Generated packages/schema/flows.schema.json (58 definitions) +``` + +## Actual VS Code smoke + +Executed through VS Code's extension-test host (automated edits in the real +editor, not a mock validator). A disposable user profile and workspace mapped +the local schema; the fixture was restored byte-for-byte after saving each edit. +No human manual inspection is claimed. + +1. Open `testdata/hello-deterministic.flow.yaml` with the local schema registered. +2. Insert `identitty: chief` on line one and save. Wait for a diagnostic naming it. +3. Restore the original content and save. Wait for zero error diagnostics. + +The brief suggests correcting `identitty` to `identity`; neither is accepted by +canonical FlowSpec today, so the correct repair is to remove the added line. + +Commands: + +```sh +'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' --extensions-dir /tmp/relayflows-schema-editor-extensions --install-extension redhat.vscode-yaml +'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' /tmp/relayflows-schema-vscode-harness/smoke.code-workspace --user-data-dir /tmp/relayflows-schema-editor-profile --extensions-dir /tmp/relayflows-schema-editor-extensions --extensionDevelopmentPath=/tmp/relayflows-schema-vscode-harness --extensionTestsPath=/tmp/relayflows-schema-vscode-harness/test.cjs --disable-workspace-trust --skip-welcome --skip-release-notes --wait +cat /tmp/relayflows-schema-editor-result.json +``` + +Literal install output: + +```text +Installing extensions... +Installing extension 'redhat.vscode-yaml'... +Extension 'redhat.vscode-yaml' v1.24.0 was successfully installed. +``` + +The editor command exited 0 with no stdout. Literal result: + +```json +{ + "ok": true, + "events": [ + { + "phase": "baseline", + "errors": [] + }, + { + "phase": "typo-saved", + "errors": [ + "Property identitty is not allowed." + ] + }, + { + "phase": "restored-saved", + "errors": [] + } + ] +} +``` + +The test host source used for that command is captured below. Its temporary +package.json names it as an extension with `main: ./extension.cjs`; that module +exports `activate = () => {}`. The temporary code-workspace points at this +checkout and sets `security.workspace.trust.enabled: false`. + +```js +const vscode = require('vscode'); +const fs = require('node:fs'); +exports.run = async () => { + const events = []; + const output = '/tmp/relayflows-schema-editor-result.json'; + try { + const extension = vscode.extensions.getExtension('redhat.vscode-yaml'); + if (!extension) throw new Error('YAML extension unavailable'); + await extension.activate(); + const uri = vscode.Uri.file('/Users/khaliqgant/flows-spec-L1-schema/testdata/hello-deterministic.flow.yaml'); + const config = vscode.workspace.getConfiguration('yaml'); + const previousSchemas = config.inspect('schemas').workspaceValue; + // Test host uses a disposable workspace file, not the checkout's settings. + await config.update('schemas', { '/Users/khaliqgant/flows-spec-L1-schema/packages/schema/flows.schema.json': ['**/*.flow.yaml'] }, vscode.ConfigurationTarget.Workspace); + const document = await vscode.workspace.openTextDocument(uri); + await vscode.window.showTextDocument(document); + const original = document.getText(); + const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + const diagnostics = () => vscode.languages.getDiagnostics(uri).filter(d => d.severity === vscode.DiagnosticSeverity.Error); + const wait = async predicate => { for (let i=0;i<100;i++) { await delay(100); if(predicate()) return; } throw new Error('Timed out waiting for editor diagnostics: '+JSON.stringify(diagnostics())); }; + try { + // Give language server activation/configuration time to settle. + await delay(2000); + events.push({phase:'baseline',errors:diagnostics().map(d=>d.message)}); + const edit = new vscode.WorkspaceEdit(); edit.insert(uri,new vscode.Position(0,0),'identitty: chief\n'); + await vscode.workspace.applyEdit(edit); await document.save(); + await wait(()=>diagnostics().some(d=>d.message.includes('identitty'))); + events.push({phase:'typo-saved',errors:diagnostics().map(d=>d.message)}); + const undo = new vscode.WorkspaceEdit(); undo.replace(uri,new vscode.Range(document.positionAt(0),document.positionAt(document.getText().length)),original); + await vscode.workspace.applyEdit(undo); await document.save(); + await wait(()=>diagnostics().length===0); + events.push({phase:'restored-saved',errors:diagnostics().map(d=>d.message)}); + } finally { + fs.writeFileSync(uri.fsPath, original); + await config.update('schemas', previousSchemas, vscode.ConfigurationTarget.Workspace); + } + fs.writeFileSync(output,JSON.stringify({ok:true,events},null,2)+'\n'); + } catch(error) { fs.writeFileSync(output,JSON.stringify({ok:false,error:String(error),events},null,2)+'\n'); throw error; } +}; +``` diff --git a/packages/schema/LICENSE b/packages/schema/LICENSE new file mode 100644 index 000000000..4b1147b33 --- /dev/null +++ b/packages/schema/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2025 Agent Workforce Incorporated + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/schema/README.md b/packages/schema/README.md new file mode 100644 index 000000000..7b0690a9c --- /dev/null +++ b/packages/schema/README.md @@ -0,0 +1,54 @@ +# @relayflows/schema + +JSON Schema (draft 2020-12) for Relayflows YAML/JSON authoring. The committed JSON +file is the complete package: no runtime dependencies and no consumer build. + +Use your editor's existing YAML support and register +`https://schema.relayflows.dev/v0.1/flows.schema.json`, or add this first line: + +```yaml +# yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json +``` + +The URL is reserved until the repository's Pages/custom-domain setup is complete. +For immediate offline use, download `flows.schema.json` or install +`npm install --save-dev @relayflows/schema` and map +`node_modules/@relayflows/schema/flows.schema.json` to `**/*.flow.yaml`. +Both the package root and `@relayflows/schema/flows.schema.json` export the JSON. +See [editor setup](https://github.com/AgentWorkforce/flows/blob/main/docs/EDITOR.md) +for VS Code, Cursor, JetBrains, Neovim, and local-file mappings. + +The root describes the canonical SDK `FlowSpec`; the definitions include every +exported interface/type in `spec.ts`, including kernel types for reference. +Generated documentation comes from TypeScript JSDoc, with labels for undocumented +nodes. Step examples adapt SURFACE.md's examples to the current canonical dialect. + +Maintain in the repository: + +```sh +npm ci --prefix packages/sdk --ignore-scripts +node scripts/generate-json-schema.mjs +cd packages/schema +bun run test +``` + +The AST walker uses the SDK's existing development TypeScript compiler and fails +on unsupported syntax. It never executes SDK code. Value constraints supplement +types in `scripts/schema-constraints.mjs`; bundled official Ajv meta-schemas +validate output declarations offline. Standard JSON Schema does not replace +`flows check` for cross-step references, dependency cycles, schema-reference +termination, model registries, or environment readiness. + +Release maintainers: `schema-publish.yml` runs on stable `vX.Y.Z` release tags. +It regenerates and checks the committed artifact, runs parity, publishes the npm +package at the release version, and preserves previous Pages files while adding +`vX.Y.Z/flows.schema.json` and the authoring-version alias `v0.1/flows.schema.json`. +Configure the repository's Pages source as **GitHub Actions**, the custom domain +`schema.relayflows.dev` and its DNS, and the `NPM_TOKEN` secret for publishing. +The schema `$id` tracks the authoring dialect, independent of the npm release. + +Tags made by another workflow using `GITHUB_TOKEN` do not trigger push workflows +([GitHub's rule](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)). +For the existing Publish Package workflow, dispatch **Publish schema** afterward +with its exact release tag, or have release automation dispatch it explicitly. +Publication is never performed by generation, testing, packing, or this PR. diff --git a/packages/schema/THIRD_PARTY_LICENSES b/packages/schema/THIRD_PARTY_LICENSES new file mode 100644 index 000000000..f22b3f371 --- /dev/null +++ b/packages/schema/THIRD_PARTY_LICENSES @@ -0,0 +1,51 @@ +Bundled meta-schemas are derived from the SDK dependencies below. + +ajv + +The MIT License (MIT) + +Copyright (c) 2015-2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +ajv-draft-04 + +MIT License + +Copyright (c) 2021 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/schema/flows.schema.json b/packages/schema/flows.schema.json new file mode 100644 index 000000000..5833803b3 --- /dev/null +++ b/packages/schema/flows.schema.json @@ -0,0 +1,4206 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schema.relayflows.dev/v0.1/flows.schema.json", + "title": "Relayflow", + "description": "Relayflows YAML/JSON authoring spec. Run flows check for cross-step references, dependency cycles, schema-reference termination, and environment readiness.", + "$ref": "#/$defs/FlowSpec", + "$defs": { + "StepType": { + "title": "StepType", + "description": "The three rungs of the ladder (RFC §1; AGENTS.md rule 7).", + "type": "string", + "enum": [ + "deterministic", + "llm", + "agent" + ] + }, + "McpServerConfig": { + "title": "McpServerConfig", + "description": "Project-owned MCP connections. env contains names, never secret values.", + "oneOf": [ + { + "type": "object", + "properties": { + "command": { + "title": "command", + "description": "command in the Relayflows spec.", + "type": "string" + }, + "args": { + "title": "args", + "description": "args in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "args items", + "description": "args items value." + } + }, + "env": { + "title": "env", + "description": "env in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "env items", + "description": "env items value." + } + } + }, + "required": [ + "command" + ], + "additionalProperties": false, + "title": "McpServerConfig alternative 1", + "description": "McpServerConfig alternative 1 value." + }, + { + "type": "object", + "properties": { + "url": { + "title": "url", + "description": "url in the Relayflows spec.", + "type": "string" + }, + "headers": { + "title": "headers", + "description": "headers in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "type": "string", + "title": "headers additionalProperties", + "description": "headers additionalProperties value." + } + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "title": "McpServerConfig alternative 2", + "description": "McpServerConfig alternative 2 value." + } + ] + }, + "FlowsJson": { + "title": "FlowsJson", + "description": "FlowsJson in the Relayflows spec.", + "type": "object", + "properties": { + "cli": { + "title": "cli", + "description": "cli in the Relayflows spec.", + "type": "string" + }, + "executors": { + "title": "executors", + "description": "executors in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "executors items", + "description": "executors items value." + } + }, + "models": { + "title": "models", + "description": "models in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "models items", + "description": "models items value." + } + }, + "mcp": { + "title": "mcp", + "description": "mcp in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/McpServerConfig", + "title": "mcp additionalProperties", + "description": "See McpServerConfig." + } + } + }, + "additionalProperties": false + }, + "VerificationGateType": { + "title": "VerificationGateType", + "description": "Verification is control flow, not decoration (kernel DESIGN.md §3).\nv0 gates are deterministic so verification is kernel-side and replayable.", + "type": "string", + "enum": [ + "exit_code", + "output_contains", + "json_schema" + ] + }, + "ExitCodeGate": { + "title": "ExitCodeGate", + "description": "`exit_code == 0` — the implicit gate for deterministic steps. v0 judges\nexactly zero (kernel DESIGN.md §4); it is not configurable, so this gate\ncarries no parameters. Writing it explicitly is allowed and compiles to the\nsame kernel spec as omitting it.", + "type": "object", + "properties": { + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "exit_code" + }, + "expect": { + "type": "integer", + "const": 0, + "description": "Legacy explicit success code. Only zero is supported.", + "title": "expect" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "OutputContainsGate": { + "title": "OutputContainsGate", + "description": "Step output (stdout_tail / llm value, stringified) contains `value`.", + "type": "object", + "properties": { + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "output_contains" + }, + "value": { + "title": "value", + "description": "value in the Relayflows spec.", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + }, + "JsonSchemaGate": { + "title": "JsonSchemaGate", + "description": "Step output validates against a JSON Schema. Used for `llm` structured output.", + "type": "object", + "properties": { + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "json_schema" + }, + "schema": { + "$ref": "#/$defs/OutputSchema", + "description": "JSON Schema object or boolean; references and termination are checked by flows check.", + "title": "schema" + } + }, + "required": [ + "type", + "schema" + ], + "additionalProperties": false + }, + "VerificationSpec": { + "title": "VerificationSpec", + "description": "VerificationSpec in the Relayflows spec.", + "oneOf": [ + { + "$ref": "#/$defs/ExitCodeGate", + "title": "VerificationSpec alternative 1", + "description": "See ExitCodeGate." + }, + { + "$ref": "#/$defs/OutputContainsGate", + "title": "VerificationSpec alternative 2", + "description": "See OutputContainsGate." + }, + { + "$ref": "#/$defs/JsonSchemaGate", + "title": "VerificationSpec alternative 3", + "description": "See JsonSchemaGate." + } + ] + }, + "OutputVerificationSpec": { + "title": "OutputVerificationSpec", + "description": "OutputVerificationSpec in the Relayflows spec.", + "oneOf": [ + { + "$ref": "#/$defs/OutputContainsGate", + "title": "OutputVerificationSpec alternative 1", + "description": "See OutputContainsGate." + }, + { + "$ref": "#/$defs/JsonSchemaGate", + "title": "OutputVerificationSpec alternative 2", + "description": "See JsonSchemaGate." + } + ] + }, + "RecoveryMode": { + "title": "RecoveryMode", + "description": "Agent-step recovery modes (RFC Appendix A rule 4). Default is `reset`.\nThese recovery modes apply to agent steps; deterministic steps retry their commands.", + "type": "string", + "enum": [ + "reset", + "inspect", + "manual" + ] + }, + "WorkspaceSurface": { + "title": "WorkspaceSurface", + "description": "Declared mutable surfaces for an agent step (RFC Appendix A rule 1).\nAnything undeclared is outside the contract and outside the step's\npermissions (gate 8 makes this enforceable, not advisory).", + "type": "object", + "properties": { + "surface": { + "title": "surface", + "description": "Relayfile mount path, or a named worktree.", + "type": "string", + "minLength": 1, + "pattern": "^(?!\\s)(?![\\s\\S]*\\s$)(?:/|[^/]+://)?(?:(?!\\.{1,2}(?:/|$))[^/]+(?:/(?!\\.{1,2}(?:/|$))[^/]+)*)?$" + } + }, + "required": [ + "surface" + ], + "additionalProperties": false + }, + "StreamSurface": { + "title": "StreamSurface", + "description": "StreamSurface in the Relayflows spec.", + "type": "object", + "properties": { + "stream": { + "title": "stream", + "description": "Durable channel the agent may write (kernel DESIGN.md §1.8).", + "type": "string", + "minLength": 1 + } + }, + "required": [ + "stream" + ], + "additionalProperties": false + }, + "AgentSurfaces": { + "title": "AgentSurfaces", + "description": "AgentSurfaces in the Relayflows spec.", + "type": "object", + "properties": { + "workspace": { + "title": "workspace", + "description": "workspace in the Relayflows spec.", + "type": "array", + "items": { + "$ref": "#/$defs/WorkspaceSurface", + "title": "workspace items", + "description": "See WorkspaceSurface." + } + }, + "streams": { + "title": "streams", + "description": "streams in the Relayflows spec.", + "type": "array", + "items": { + "$ref": "#/$defs/StreamSurface", + "title": "streams items", + "description": "See StreamSurface." + } + }, + "external": { + "title": "external", + "description": "Integration writeback paths — mount writes per gate 6.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "pattern": "^(?!\\s)(?![\\s\\S]*\\s$)(?:/|[^/]+://)?(?:(?!\\.{1,2}(?:/|$))[^/]+(?:/(?!\\.{1,2}(?:/|$))[^/]+)*)?$", + "title": "external items", + "description": "external items value." + } + } + }, + "additionalProperties": false + }, + "PermissionsSpec": { + "title": "PermissionsSpec", + "description": "Permission model for an agent step (gate 8). `readonly` provably cannot write.", + "type": "object", + "properties": { + "fileGlobs": { + "title": "fileGlobs", + "description": "fileGlobs in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "fileGlobs items", + "description": "fileGlobs items value." + } + }, + "networkAllowlist": { + "title": "networkAllowlist", + "description": "networkAllowlist in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "networkAllowlist items", + "description": "networkAllowlist items value." + } + }, + "accessPreset": { + "title": "accessPreset", + "description": "accessPreset in the Relayflows spec.", + "type": "string", + "enum": [ + "readonly", + "readwrite" + ] + } + }, + "additionalProperties": false + }, + "BudgetSpec": { + "title": "BudgetSpec", + "description": "Budget envelope. Per decision #10 every token has exactly one owner: an\ninjected context pack spends the consuming step's budget. Money is a decimal\nstring at the boundary (kernel DESIGN.md §1: no floats for money); tokens\nare integers.", + "type": "object", + "properties": { + "maxTokensIn": { + "title": "maxTokensIn", + "description": "maxTokensIn in the Relayflows spec.", + "type": "integer", + "minimum": 0 + }, + "maxTokensOut": { + "title": "maxTokensOut", + "description": "maxTokensOut in the Relayflows spec.", + "type": "integer", + "minimum": 0 + }, + "maxDollars": { + "title": "maxDollars", + "description": "Decimal string, e.g. \"1.50\".", + "type": "string", + "pattern": "^\\d+(\\.\\d+)?$" + } + }, + "additionalProperties": false + }, + "MemorySpec": { + "title": "MemorySpec", + "description": "Slice 1 records a fixed pack; retrieval is supplied by a future provider.", + "type": "object", + "properties": { + "scope": { + "title": "scope", + "description": "scope in the Relayflows spec.", + "type": "string", + "enum": [ + "script", + "agent" + ] + }, + "query": { + "title": "query", + "description": "query in the Relayflows spec.", + "type": "string", + "pattern": "\\S" + }, + "budget": { + "title": "budget", + "description": "budget in the Relayflows spec.", + "$ref": "#/$defs/BudgetSpec", + "type": "object", + "properties": { + "maxTokensIn": { + "type": "integer", + "maximum": 9007199254740991, + "title": "maxTokensIn", + "description": "maxTokensIn value." + }, + "maxTokensOut": { + "type": "integer", + "maximum": 9007199254740991, + "title": "maxTokensOut", + "description": "maxTokensOut value." + } + } + } + }, + "required": [ + "scope", + "query", + "budget" + ], + "additionalProperties": false + }, + "KernelBudgetSpec": { + "title": "KernelBudgetSpec", + "description": "KernelBudgetSpec in the Relayflows spec.", + "type": "object", + "properties": { + "max_tokens_in": { + "title": "max_tokens_in", + "description": "max_tokens_in in the Relayflows spec.", + "type": "number" + }, + "max_tokens_out": { + "title": "max_tokens_out", + "description": "max_tokens_out in the Relayflows spec.", + "type": "number" + }, + "max_dollars": { + "title": "max_dollars", + "description": "max_dollars in the Relayflows spec.", + "type": "string" + } + }, + "additionalProperties": false + }, + "KernelMemorySpec": { + "title": "KernelMemorySpec", + "description": "KernelMemorySpec in the Relayflows spec.", + "type": "object", + "properties": { + "scope": { + "title": "scope", + "description": "scope in the Relayflows spec.", + "type": "string", + "enum": [ + "script", + "agent" + ] + }, + "query": { + "title": "query", + "description": "query in the Relayflows spec.", + "type": "string" + }, + "budget": { + "title": "budget", + "description": "budget in the Relayflows spec.", + "$ref": "#/$defs/KernelBudgetSpec" + } + }, + "required": [ + "scope", + "query", + "budget" + ], + "additionalProperties": false + }, + "PlacementRequirements": { + "title": "PlacementRequirements", + "description": "Capability needs, never provider names or source revisions.", + "type": "object", + "properties": { + "execution": { + "title": "execution", + "description": "execution in the Relayflows spec.", + "type": "string", + "enum": [ + "batch", + "interactive" + ] + }, + "workspace": { + "title": "workspace", + "description": "Share the run tree; defaults true for deterministic steps declaring requirements.", + "type": "boolean" + }, + "network": { + "title": "network", + "description": "True requests connectivity; false does not impose a network deny policy.", + "type": "boolean" + }, + "expectedDurationMs": { + "title": "expectedDurationMs", + "description": "expectedDurationMs in the Relayflows spec.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + }, + "preference": { + "title": "preference", + "description": "preference in the Relayflows spec.", + "type": "string", + "enum": [ + "cost", + "latency", + "reliability", + "balanced" + ] + } + }, + "additionalProperties": false + }, + "KernelPlacementRequirements": { + "title": "KernelPlacementRequirements", + "description": "KernelPlacementRequirements in the Relayflows spec.", + "type": "object", + "properties": { + "execution": { + "title": "execution", + "description": "execution in the Relayflows spec.", + "type": "string", + "enum": [ + "batch", + "interactive" + ] + }, + "workspace": { + "title": "workspace", + "description": "Share the run tree; defaults true for deterministic steps declaring requirements.", + "type": "boolean" + }, + "network": { + "title": "network", + "description": "True requests connectivity; false does not impose a network deny policy.", + "type": "boolean" + }, + "preference": { + "title": "preference", + "description": "preference in the Relayflows spec.", + "type": "string", + "enum": [ + "cost", + "latency", + "reliability", + "balanced" + ] + }, + "expected_duration_ms": { + "title": "expected_duration_ms", + "description": "expected_duration_ms in the Relayflows spec.", + "type": "number" + } + }, + "additionalProperties": false + }, + "OutputBinding": { + "title": "OutputBinding", + "description": "OutputBinding in the Relayflows spec.", + "type": "object", + "properties": { + "step": { + "title": "step", + "description": "step in the Relayflows spec.", + "type": "string", + "pattern": "\\S" + }, + "path": { + "title": "path", + "description": "Object keys or array indices; omit to select the whole output.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "string", + "title": "path items alternative 1", + "description": "path items alternative 1 value." + }, + { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "title": "path items alternative 2", + "description": "path items alternative 2 value." + } + ], + "title": "path items", + "description": "path items value." + } + } + }, + "required": [ + "step" + ], + "additionalProperties": false + }, + "BaseStepSpec": { + "title": "BaseStepSpec", + "description": "Fields shared by every step on the ladder.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "Named values selected from earlier steps' declared, verified outputs.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + }, + "propertyNames": { + "type": "string", + "pattern": "\\S", + "title": "input propertyNames", + "description": "input propertyNames value." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/PlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/MemorySpec" + }, + "id": { + "title": "id", + "description": "Stable step identity; journaled as `step_id` and hashed into the idempotency key.", + "type": "string", + "minLength": 1 + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "$ref": "#/$defs/StepType" + }, + "dependsOn": { + "title": "dependsOn", + "description": "Step dependencies — a step runs only after these complete.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "dependsOn items", + "description": "dependsOn items value." + } + }, + "maxIterations": { + "title": "maxIterations", + "description": "Semantic retry bound (kernel DESIGN.md §1.2 `max_iterations`). Default 1.", + "type": "integer", + "minimum": 1 + } + }, + "required": [ + "id", + "type" + ], + "additionalProperties": false + }, + "DeterministicStepSpec": { + "title": "DeterministicStepSpec", + "description": "Rung 1 — a pure script. Executed by the `relayflowd` binary: spawn command,\ncapture stdout/exit code. Output = `{exit_code, stdout_tail}`. Gate-1\ndeterministic steps with placement requirements pin their worktree base commit.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "Named values selected from earlier steps' declared, verified outputs.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + }, + "propertyNames": { + "type": "string", + "pattern": "\\S", + "title": "input propertyNames", + "description": "input propertyNames value." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/PlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/MemorySpec" + }, + "id": { + "title": "id", + "description": "Stable step identity; journaled as `step_id` and hashed into the idempotency key.", + "type": "string", + "minLength": 1 + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "deterministic" + }, + "dependsOn": { + "title": "dependsOn", + "description": "Step dependencies — a step runs only after these complete.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "dependsOn items", + "description": "dependsOn items value." + } + }, + "maxIterations": { + "title": "maxIterations", + "description": "Semantic retry bound (kernel DESIGN.md §1.2 `max_iterations`). Default 1.", + "type": "integer", + "minimum": 1 + }, + "command": { + "title": "command", + "description": "command in the Relayflows spec.", + "type": "string", + "minLength": 1 + }, + "timeoutMs": { + "title": "timeoutMs", + "description": "Wall-clock command timeout; worker-backed verbs own their dispatch timeout.", + "type": "integer", + "minimum": 1 + }, + "verification": { + "title": "verification", + "description": "Omit to get the implicit `exit_code` gate.", + "$ref": "#/$defs/VerificationSpec" + } + }, + "required": [ + "id", + "type", + "command" + ], + "additionalProperties": false, + "examples": [ + { + "id": "diff", + "type": "deterministic", + "command": "git diff main" + } + ] + }, + "JsonOutputSchema": { + "title": "JsonOutputSchema", + "description": "Structured output schema, compiled into json_schema verification.", + "type": "object", + "allOf": [ + { + "$ref": "#/$defs/OutputSchema", + "title": "JsonOutputSchema alternative 1", + "description": "See OutputSchema." + } + ] + }, + "LlmStepSpec": { + "title": "LlmStepSpec", + "description": "Rung 2 — a bare model call. No workspace, output is a value. The kernel\nnever calls a model: it dispatches to an attached SDK worker (§5) which\nreturns `{output, usage}`; the kernel then runs the verification gate.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "Named values selected from earlier steps' declared, verified outputs.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + }, + "propertyNames": { + "type": "string", + "pattern": "\\S", + "title": "input propertyNames", + "description": "input propertyNames value." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/PlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/MemorySpec" + }, + "id": { + "title": "id", + "description": "Stable step identity; journaled as `step_id` and hashed into the idempotency key.", + "type": "string", + "minLength": 1 + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "llm" + }, + "dependsOn": { + "title": "dependsOn", + "description": "Step dependencies — a step runs only after these complete.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "dependsOn items", + "description": "dependsOn items value." + } + }, + "maxIterations": { + "title": "maxIterations", + "description": "Semantic retry bound (kernel DESIGN.md §1.2 `max_iterations`). Default 1.", + "type": "integer", + "minimum": 1 + }, + "prompt": { + "title": "prompt", + "description": "prompt in the Relayflows spec.", + "type": "string", + "minLength": 1 + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/OutputVerificationSpec" + }, + "model": { + "title": "model", + "description": "model in the Relayflows spec.", + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:[\\s\\S]*\\S)?$", + "allOf": [ + { + "pattern": "^[^\\u0000-\\u001f\\u007f]*$", + "title": "model alternative 1", + "description": "model alternative 1 value." + } + ] + }, + "cli": { + "title": "cli", + "description": "Inert preflight declaration; overrides the flow/project CLI default.", + "type": "string", + "minLength": 1 + }, + "output": { + "title": "output", + "description": "Structured-output authoring sugar. Compiles to the existing `json_schema`\nverification primitive and is removed before the kernel boundary.", + "$ref": "#/$defs/JsonOutputSchema" + } + }, + "required": [ + "id", + "type", + "prompt" + ], + "additionalProperties": false, + "not": { + "required": [ + "output", + "verification" + ], + "title": "LlmStepSpec not", + "description": "LlmStepSpec not value." + }, + "examples": [ + { + "id": "note", + "type": "llm", + "prompt": "One-line release note for the diff above." + } + ] + }, + "AgentStepSpec": { + "title": "AgentStepSpec", + "description": "Rung 3 — a harnessed agent in a workspace. Dispatched like `llm`, plus\nAppendix A in full: pins declared workspace revisions and stream offsets;\nevery writeback is a journaled `effect.recorded` deduped by\n`(step_id, idempotency_key, surface_path)`.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "Named values selected from earlier steps' declared, verified outputs.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + }, + "propertyNames": { + "type": "string", + "pattern": "\\S", + "title": "input propertyNames", + "description": "input propertyNames value." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/PlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/MemorySpec" + }, + "id": { + "title": "id", + "description": "Stable step identity; journaled as `step_id` and hashed into the idempotency key.", + "type": "string", + "minLength": 1 + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "agent" + }, + "dependsOn": { + "title": "dependsOn", + "description": "Step dependencies — a step runs only after these complete.", + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "title": "dependsOn items", + "description": "dependsOn items value." + } + }, + "maxIterations": { + "title": "maxIterations", + "description": "Semantic retry bound (kernel DESIGN.md §1.2 `max_iterations`). Default 1.", + "type": "integer", + "minimum": 1 + }, + "instruction": { + "title": "instruction", + "description": "instruction in the Relayflows spec.", + "type": "string", + "minLength": 1 + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/OutputVerificationSpec" + }, + "agent": { + "title": "agent", + "description": "Named authoring declaration selected from `FlowSpec.agents`. Compiled away.", + "type": "string", + "minLength": 1 + }, + "cli": { + "title": "cli", + "description": "Inert preflight declaration; overrides the flow/project CLI default.", + "type": "string", + "minLength": 1 + }, + "model": { + "title": "model", + "description": "Model the declared CLI must use. Raw Claude/Codex adapters receive their\nreal model flag; an identified Relayflows wrapper receives it in its\nsame-process execution request. Declared here so the choice is journaled with the step\ninstead of being ambient host state.", + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:[\\s\\S]*\\S)?$", + "allOf": [ + { + "pattern": "^[^\\u0000-\\u001f\\u007f]*$", + "title": "model alternative 1", + "description": "model alternative 1 value." + } + ] + }, + "surfaces": { + "title": "surfaces", + "description": "surfaces in the Relayflows spec.", + "$ref": "#/$defs/AgentSurfaces" + }, + "recoveryMode": { + "title": "recoveryMode", + "description": "recoveryMode in the Relayflows spec.", + "$ref": "#/$defs/RecoveryMode" + }, + "permissions": { + "title": "permissions", + "description": "permissions in the Relayflows spec.", + "$ref": "#/$defs/PermissionsSpec" + }, + "output": { + "title": "output", + "description": "Structured-output authoring sugar. A successful CLI JSON object is the parsed\nvalue; the kernel persists it only after `json_schema` verification.", + "$ref": "#/$defs/JsonOutputSchema" + } + }, + "required": [ + "id", + "type", + "instruction" + ], + "additionalProperties": false, + "not": { + "required": [ + "output", + "verification" + ], + "title": "AgentStepSpec not", + "description": "AgentStepSpec not value." + }, + "examples": [ + { + "id": "review", + "type": "agent", + "instruction": "Review this diff for security issues." + } + ] + }, + "StepSpec": { + "title": "StepSpec", + "description": "StepSpec in the Relayflows spec.", + "oneOf": [ + { + "$ref": "#/$defs/DeterministicStepSpec", + "title": "StepSpec alternative 1", + "description": "See DeterministicStepSpec." + }, + { + "$ref": "#/$defs/LlmStepSpec", + "title": "StepSpec alternative 2", + "description": "See LlmStepSpec." + }, + { + "$ref": "#/$defs/AgentStepSpec", + "title": "StepSpec alternative 3", + "description": "See AgentStepSpec." + } + ] + }, + "NamedAgentSpec": { + "title": "NamedAgentSpec", + "description": "Reusable authoring declaration for an agent CLI/model pair. Both fields are\nrequired so selecting a named agent can never inherit a host model. The\ncompiler lowers these values into the selected kernel agent step at the\njournal boundary; the kernel never receives this map or a new step field.", + "type": "object", + "properties": { + "cli": { + "title": "cli", + "description": "cli in the Relayflows spec.", + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:[\\s\\S]*\\S)?$" + }, + "model": { + "title": "model", + "description": "model in the Relayflows spec.", + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:[\\s\\S]*\\S)?$", + "allOf": [ + { + "pattern": "^[^\\u0000-\\u001f\\u007f]*$", + "title": "model alternative 1", + "description": "model alternative 1 value." + } + ] + } + }, + "required": [ + "cli", + "model" + ], + "additionalProperties": false + }, + "TriggerSpec": { + "title": "TriggerSpec", + "description": "A trigger is an entry condition, not a scheduler (RFC-0001 gate 2). It names\nthe event type that wakes the flow, the payload subset that must match, the\ntemplate that derives the dedupe key, and the silence budget after which the\nkernel's liveness sweep declares the subscription dead.\n\nThe event-subscription fields were shipped in `testdata/` long before this\ninterface described them: `hn-monitor.flow.yaml`, `dir-watcher.flow.yaml`\nand `event-triggered-flow.yaml` all carry `eventType`, `pattern` and\n`dedupeKeyTemplate`, and `validate.ts` has always accepted them. The type\nstill said \"inert gate-1 declaration\" with only `id` and `executor`, so the\nauthoring dialect disagreed with both the shipped specs and the kernel.", + "type": "object", + "properties": { + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string", + "minLength": 1 + }, + "executor": { + "title": "executor", + "description": "Executor registration required before this trigger may start a run.", + "type": "string", + "minLength": 1 + }, + "eventType": { + "title": "eventType", + "description": "Event type this trigger subscribes to. Lowers to `event_type`.", + "type": "string" + }, + "pattern": { + "title": "pattern", + "description": "Recursive-subset match against the event payload. Lowers to `pattern`.", + "type": "object", + "additionalProperties": { + "title": "pattern additionalProperties", + "description": "pattern additionalProperties value." + } + }, + "dedupeKeyTemplate": { + "title": "dedupeKeyTemplate", + "description": "Derives the dedupe key. Lowers to `dedupe_key_template`.", + "type": "string" + }, + "staleAfterMs": { + "title": "staleAfterMs", + "description": "Silence budget in milliseconds. When no matching event arrives inside it,\nthe kernel's liveness sweep journals `subscription.stale` and emits a\n`relayflowd: subscription.stale ...` line\n(`kernel/relayflowd/src/server/liveness.rs`). Omitted means the engine\ndefault (`DEFAULT_STALE_AFTER_MS`, 5 minutes) applies — which is a\ndecision the author did not make, not the absence of a budget.\n\nA flow that is never triggered is silently zero (RFC-0001 §\"Trigger\nliveness\"), so declaring this is how a schedule stops being able to die\nquietly. Lowers to `stale_after_ms`.", + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991 + } + }, + "required": [ + "id", + "executor" + ], + "additionalProperties": false + }, + "FlowSpec": { + "title": "FlowSpec", + "description": "A Relayflow spec in the authoring shape — the composable unit (RFC settled\ndecision #5). Schema-validated, diffable, signable (gate 8), and emittable\nby a step (gate 9 self-authoring). What the kernel inlines in `run.spawned`\nis this spec mapped to the kernel dialect (`toKernelSpec`).", + "type": "object", + "properties": { + "version": { + "title": "version", + "description": "Spec schema semver (RFC §7). Compilers always emit latest.", + "type": "string", + "const": "0.1.0" + }, + "name": { + "title": "name", + "description": "name in the Relayflows spec.", + "type": "string", + "minLength": 1 + }, + "description": { + "title": "description", + "description": "description in the Relayflows spec.", + "type": "string" + }, + "cli": { + "title": "cli", + "description": "Inert preflight default for llm/agent steps that do not declare a CLI.", + "type": "string", + "minLength": 1 + }, + "agents": { + "title": "agents", + "description": "Named authoring declarations. Compiled into agent steps, never journaled as a new primitive.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/NamedAgentSpec", + "title": "agents additionalProperties", + "description": "See NamedAgentSpec." + }, + "propertyNames": { + "type": "string", + "minLength": 1, + "pattern": "^\\S(?:[\\s\\S]*\\S)?$", + "title": "agents propertyNames", + "description": "agents propertyNames value." + } + }, + "triggers": { + "title": "triggers", + "description": "Declarations checked by preflight; gate 1 never dispatches them.", + "type": "array", + "items": { + "$ref": "#/$defs/TriggerSpec", + "title": "triggers items", + "description": "See TriggerSpec." + } + }, + "steps": { + "title": "steps", + "description": "steps in the Relayflows spec.", + "type": "array", + "items": { + "$ref": "#/$defs/StepSpec", + "title": "steps items", + "description": "See StepSpec." + }, + "minItems": 1 + }, + "budget": { + "title": "budget", + "description": "budget in the Relayflows spec.", + "$ref": "#/$defs/BudgetSpec" + } + }, + "required": [ + "version", + "steps" + ], + "additionalProperties": false + }, + "KernelRetryPolicy": { + "title": "KernelRetryPolicy", + "description": "KernelRetryPolicy in the Relayflows spec.", + "type": "object", + "properties": { + "initial_backoff_ms": { + "title": "initial_backoff_ms", + "description": "initial_backoff_ms in the Relayflows spec.", + "type": "number" + }, + "max_backoff_ms": { + "title": "max_backoff_ms", + "description": "max_backoff_ms in the Relayflows spec.", + "type": "number" + }, + "multiplier": { + "title": "multiplier", + "description": "multiplier in the Relayflows spec.", + "type": "number" + }, + "jitter_percent": { + "title": "jitter_percent", + "description": "jitter_percent in the Relayflows spec.", + "type": "number" + } + }, + "required": [ + "initial_backoff_ms", + "max_backoff_ms", + "multiplier", + "jitter_percent" + ], + "additionalProperties": false + }, + "KernelVerificationSpec": { + "title": "KernelVerificationSpec", + "description": "Flat v0 gates (kernel DESIGN.md §4): `exit_code == 0` is implicit for\ndeterministic steps; these two are optional and combinable. An empty object\nmeans \"implicit gates only\".", + "type": "object", + "properties": { + "output_contains": { + "title": "output_contains", + "description": "output_contains in the Relayflows spec.", + "type": "string" + }, + "json_schema": { + "title": "json_schema", + "description": "json_schema in the Relayflows spec.", + "oneOf": [ + { + "type": "boolean", + "title": "json_schema alternative 1", + "description": "json_schema alternative 1 value." + }, + { + "type": "object", + "additionalProperties": { + "title": "json_schema alternative 2 additionalProperties", + "description": "json_schema alternative 2 additionalProperties value." + }, + "title": "json_schema alternative 2", + "description": "json_schema alternative 2 value." + } + ] + } + }, + "additionalProperties": false + }, + "KernelStepCommon": { + "title": "KernelStepCommon", + "description": "KernelStepCommon in the Relayflows spec.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "input in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/KernelPlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/KernelMemorySpec" + }, + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string" + }, + "depends_on": { + "title": "depends_on", + "description": "depends_on in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "depends_on items", + "description": "depends_on items value." + } + }, + "max_iterations": { + "title": "max_iterations", + "description": "max_iterations in the Relayflows spec.", + "type": "number" + }, + "retry": { + "title": "retry", + "description": "retry in the Relayflows spec.", + "$ref": "#/$defs/KernelRetryPolicy" + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/KernelVerificationSpec" + } + }, + "required": [ + "id", + "depends_on", + "max_iterations", + "retry", + "verification" + ], + "additionalProperties": false + }, + "KernelDeterministicStep": { + "title": "KernelDeterministicStep", + "description": "KernelDeterministicStep in the Relayflows spec.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "input in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/KernelPlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/KernelMemorySpec" + }, + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string" + }, + "depends_on": { + "title": "depends_on", + "description": "depends_on in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "depends_on items", + "description": "depends_on items value." + } + }, + "max_iterations": { + "title": "max_iterations", + "description": "max_iterations in the Relayflows spec.", + "type": "number" + }, + "retry": { + "title": "retry", + "description": "retry in the Relayflows spec.", + "$ref": "#/$defs/KernelRetryPolicy" + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/KernelVerificationSpec" + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "deterministic" + }, + "command": { + "title": "command", + "description": "command in the Relayflows spec.", + "type": "string" + }, + "timeout_ms": { + "title": "timeout_ms", + "description": "timeout_ms in the Relayflows spec.", + "type": "number" + } + }, + "required": [ + "id", + "depends_on", + "max_iterations", + "retry", + "verification", + "type", + "command" + ], + "additionalProperties": false + }, + "KernelLlmStep": { + "title": "KernelLlmStep", + "description": "KernelLlmStep in the Relayflows spec.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "input in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/KernelPlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/KernelMemorySpec" + }, + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string" + }, + "depends_on": { + "title": "depends_on", + "description": "depends_on in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "depends_on items", + "description": "depends_on items value." + } + }, + "max_iterations": { + "title": "max_iterations", + "description": "max_iterations in the Relayflows spec.", + "type": "number" + }, + "retry": { + "title": "retry", + "description": "retry in the Relayflows spec.", + "$ref": "#/$defs/KernelRetryPolicy" + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/KernelVerificationSpec" + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "llm" + }, + "prompt": { + "title": "prompt", + "description": "prompt in the Relayflows spec.", + "type": "string" + }, + "model": { + "title": "model", + "description": "model in the Relayflows spec.", + "type": "string" + }, + "cli": { + "title": "cli", + "description": "cli in the Relayflows spec.", + "type": "string" + } + }, + "required": [ + "id", + "depends_on", + "max_iterations", + "retry", + "verification", + "type", + "prompt" + ], + "additionalProperties": false + }, + "KernelAgentSurfaces": { + "title": "KernelAgentSurfaces", + "description": "KernelAgentSurfaces in the Relayflows spec.", + "type": "object", + "properties": { + "workspace": { + "title": "workspace", + "description": "workspace in the Relayflows spec.", + "type": "array", + "items": { + "type": "object", + "properties": { + "surface": { + "title": "surface", + "description": "surface in the Relayflows spec.", + "type": "string" + } + }, + "required": [ + "surface" + ], + "additionalProperties": false, + "title": "workspace items", + "description": "workspace items value." + } + }, + "streams": { + "title": "streams", + "description": "streams in the Relayflows spec.", + "type": "array", + "items": { + "type": "object", + "properties": { + "stream": { + "title": "stream", + "description": "stream in the Relayflows spec.", + "type": "string" + } + }, + "required": [ + "stream" + ], + "additionalProperties": false, + "title": "streams items", + "description": "streams items value." + } + }, + "external": { + "title": "external", + "description": "external in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "external items", + "description": "external items value." + } + } + }, + "additionalProperties": false + }, + "KernelPermissionsSpec": { + "title": "KernelPermissionsSpec", + "description": "KernelPermissionsSpec in the Relayflows spec.", + "type": "object", + "properties": { + "file_globs": { + "title": "file_globs", + "description": "file_globs in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "file_globs items", + "description": "file_globs items value." + } + }, + "network_allowlist": { + "title": "network_allowlist", + "description": "network_allowlist in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "network_allowlist items", + "description": "network_allowlist items value." + } + }, + "access_preset": { + "title": "access_preset", + "description": "access_preset in the Relayflows spec.", + "type": "string", + "enum": [ + "readonly", + "readwrite" + ] + } + }, + "additionalProperties": false + }, + "KernelAgentStep": { + "title": "KernelAgentStep", + "description": "KernelAgentStep in the Relayflows spec.", + "type": "object", + "properties": { + "input": { + "title": "input", + "description": "input in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/OutputBinding", + "title": "input additionalProperties", + "description": "See OutputBinding." + } + }, + "requirements": { + "title": "requirements", + "description": "requirements in the Relayflows spec.", + "$ref": "#/$defs/KernelPlacementRequirements" + }, + "memory": { + "title": "memory", + "description": "memory in the Relayflows spec.", + "$ref": "#/$defs/KernelMemorySpec" + }, + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string" + }, + "depends_on": { + "title": "depends_on", + "description": "depends_on in the Relayflows spec.", + "type": "array", + "items": { + "type": "string", + "title": "depends_on items", + "description": "depends_on items value." + } + }, + "max_iterations": { + "title": "max_iterations", + "description": "max_iterations in the Relayflows spec.", + "type": "number" + }, + "retry": { + "title": "retry", + "description": "retry in the Relayflows spec.", + "$ref": "#/$defs/KernelRetryPolicy" + }, + "verification": { + "title": "verification", + "description": "verification in the Relayflows spec.", + "$ref": "#/$defs/KernelVerificationSpec" + }, + "type": { + "title": "type", + "description": "type in the Relayflows spec.", + "type": "string", + "const": "agent" + }, + "instruction": { + "title": "instruction", + "description": "instruction in the Relayflows spec.", + "type": "string" + }, + "cli": { + "title": "cli", + "description": "cli in the Relayflows spec.", + "type": "string" + }, + "model": { + "title": "model", + "description": "model in the Relayflows spec.", + "type": "string" + }, + "recovery_mode": { + "title": "recovery_mode", + "description": "recovery_mode in the Relayflows spec.", + "$ref": "#/$defs/RecoveryMode" + }, + "surfaces": { + "title": "surfaces", + "description": "surfaces in the Relayflows spec.", + "$ref": "#/$defs/KernelAgentSurfaces" + }, + "permissions": { + "title": "permissions", + "description": "permissions in the Relayflows spec.", + "$ref": "#/$defs/KernelPermissionsSpec" + } + }, + "required": [ + "id", + "depends_on", + "max_iterations", + "retry", + "verification", + "type", + "instruction", + "recovery_mode" + ], + "additionalProperties": false + }, + "KernelStepSpec": { + "title": "KernelStepSpec", + "description": "KernelStepSpec in the Relayflows spec.", + "oneOf": [ + { + "$ref": "#/$defs/KernelDeterministicStep", + "title": "KernelStepSpec alternative 1", + "description": "See KernelDeterministicStep." + }, + { + "$ref": "#/$defs/KernelLlmStep", + "title": "KernelStepSpec alternative 2", + "description": "See KernelLlmStep." + }, + { + "$ref": "#/$defs/KernelAgentStep", + "title": "KernelStepSpec alternative 3", + "description": "See KernelAgentStep." + } + ] + }, + "KernelTriggerSpec": { + "title": "KernelTriggerSpec", + "description": "KernelTriggerSpec in the Relayflows spec.", + "type": "object", + "properties": { + "id": { + "title": "id", + "description": "id in the Relayflows spec.", + "type": "string" + }, + "executor": { + "title": "executor", + "description": "executor in the Relayflows spec.", + "type": "string" + }, + "event_type": { + "title": "event_type", + "description": "event_type in the Relayflows spec.", + "type": "string" + }, + "pattern": { + "title": "pattern", + "description": "pattern in the Relayflows spec.", + "type": "object", + "additionalProperties": { + "title": "pattern additionalProperties", + "description": "pattern additionalProperties value." + } + }, + "dedupe_key_template": { + "title": "dedupe_key_template", + "description": "dedupe_key_template in the Relayflows spec.", + "type": "string" + }, + "stale_after_ms": { + "title": "stale_after_ms", + "description": "stale_after_ms in the Relayflows spec.", + "type": "number" + } + }, + "required": [ + "id", + "executor" + ], + "additionalProperties": false + }, + "KernelRunSpec": { + "title": "KernelRunSpec", + "description": "The compiled spec as the kernel parses, journals, and hashes it.", + "type": "object", + "properties": { + "version": { + "title": "version", + "description": "version in the Relayflows spec.", + "type": "string" + }, + "name": { + "title": "name", + "description": "name in the Relayflows spec.", + "type": "string" + }, + "description": { + "title": "description", + "description": "description in the Relayflows spec.", + "type": "string" + }, + "cli": { + "title": "cli", + "description": "cli in the Relayflows spec.", + "type": "string" + }, + "triggers": { + "title": "triggers", + "description": "triggers in the Relayflows spec.", + "type": "array", + "items": { + "$ref": "#/$defs/KernelTriggerSpec", + "title": "triggers items", + "description": "See KernelTriggerSpec." + } + }, + "steps": { + "title": "steps", + "description": "steps in the Relayflows spec.", + "type": "array", + "items": { + "$ref": "#/$defs/KernelStepSpec", + "title": "steps items", + "description": "See KernelStepSpec." + } + }, + "budget": { + "title": "budget", + "description": "budget in the Relayflows spec.", + "$ref": "#/$defs/KernelBudgetSpec" + } + }, + "required": [ + "version", + "steps" + ], + "additionalProperties": false + }, + "Meta04": { + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/Meta04", + "title": "schemaArray items", + "description": "See Meta04." + }, + "title": "schemaArray", + "description": "schemaArray value." + }, + "positiveInteger": { + "type": "integer", + "minimum": 0, + "title": "positiveInteger", + "description": "positiveInteger value." + }, + "positiveIntegerDefault0": { + "allOf": [ + { + "$ref": "#/$defs/Meta04/definitions/positiveInteger", + "title": "positiveIntegerDefault0 alternative 1", + "description": "See positiveInteger." + }, + { + "default": 0, + "title": "positiveIntegerDefault0 alternative 2", + "description": "positiveIntegerDefault0 alternative 2 value." + } + ], + "title": "positiveIntegerDefault0", + "description": "positiveIntegerDefault0 value." + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ], + "title": "simpleTypes", + "description": "simpleTypes value." + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "title": "stringArray items", + "description": "stringArray items value." + }, + "minItems": 1, + "uniqueItems": true, + "title": "stringArray", + "description": "stringArray value." + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "id", + "description": "id value." + }, + "$schema": { + "type": "string", + "title": "$schema", + "description": "$schema value." + }, + "title": { + "type": "string", + "title": "title", + "description": "title value." + }, + "description": { + "type": "string", + "title": "description", + "description": "description value." + }, + "default": { + "title": "default", + "description": "default value." + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0, + "title": "multipleOf", + "description": "multipleOf value." + }, + "maximum": { + "type": "number", + "title": "maximum", + "description": "maximum value." + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false, + "title": "exclusiveMaximum", + "description": "exclusiveMaximum value." + }, + "minimum": { + "type": "number", + "title": "minimum", + "description": "minimum value." + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false, + "title": "exclusiveMinimum", + "description": "exclusiveMinimum value." + }, + "maxLength": { + "$ref": "#/$defs/Meta04/definitions/positiveInteger", + "title": "maxLength", + "description": "See positiveInteger." + }, + "minLength": { + "$ref": "#/$defs/Meta04/definitions/positiveIntegerDefault0", + "title": "minLength", + "description": "See positiveIntegerDefault0." + }, + "pattern": { + "type": "string", + "title": "pattern", + "description": "pattern value." + }, + "additionalItems": { + "anyOf": [ + { + "type": "boolean", + "title": "additionalItems alternative 1", + "description": "additionalItems alternative 1 value." + }, + { + "$ref": "#/$defs/Meta04", + "title": "additionalItems alternative 2", + "description": "See Meta04." + } + ], + "default": {}, + "title": "additionalItems", + "description": "additionalItems value." + }, + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Meta04", + "title": "items alternative 1", + "description": "See Meta04." + }, + { + "$ref": "#/$defs/Meta04/definitions/schemaArray", + "title": "items alternative 2", + "description": "See schemaArray." + } + ], + "default": {}, + "title": "items", + "description": "items value." + }, + "maxItems": { + "$ref": "#/$defs/Meta04/definitions/positiveInteger", + "title": "maxItems", + "description": "See positiveInteger." + }, + "minItems": { + "$ref": "#/$defs/Meta04/definitions/positiveIntegerDefault0", + "title": "minItems", + "description": "See positiveIntegerDefault0." + }, + "uniqueItems": { + "type": "boolean", + "default": false, + "title": "uniqueItems", + "description": "uniqueItems value." + }, + "maxProperties": { + "$ref": "#/$defs/Meta04/definitions/positiveInteger", + "title": "maxProperties", + "description": "See positiveInteger." + }, + "minProperties": { + "$ref": "#/$defs/Meta04/definitions/positiveIntegerDefault0", + "title": "minProperties", + "description": "See positiveIntegerDefault0." + }, + "required": { + "$ref": "#/$defs/Meta04/definitions/stringArray", + "title": "required", + "description": "See stringArray." + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean", + "title": "additionalProperties alternative 1", + "description": "additionalProperties alternative 1 value." + }, + { + "$ref": "#/$defs/Meta04", + "title": "additionalProperties alternative 2", + "description": "See Meta04." + } + ], + "default": {}, + "title": "additionalProperties", + "description": "additionalProperties value." + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta04", + "title": "definitions additionalProperties", + "description": "See Meta04." + }, + "default": {}, + "title": "definitions", + "description": "definitions value." + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta04", + "title": "properties additionalProperties", + "description": "See Meta04." + }, + "default": {}, + "title": "properties", + "description": "properties value." + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta04", + "title": "patternProperties additionalProperties", + "description": "See Meta04." + }, + "default": {}, + "title": "patternProperties", + "description": "patternProperties value." + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Meta04", + "title": "dependencies additionalProperties alternative 1", + "description": "See Meta04." + }, + { + "$ref": "#/$defs/Meta04/definitions/stringArray", + "title": "dependencies additionalProperties alternative 2", + "description": "See stringArray." + } + ], + "title": "dependencies additionalProperties", + "description": "dependencies additionalProperties value." + }, + "title": "dependencies", + "description": "dependencies value." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "title": "enum", + "description": "enum value." + }, + "type": { + "anyOf": [ + { + "$ref": "#/$defs/Meta04/definitions/simpleTypes", + "title": "type alternative 1", + "description": "See simpleTypes." + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/Meta04/definitions/simpleTypes", + "title": "type alternative 2 items", + "description": "See simpleTypes." + }, + "minItems": 1, + "uniqueItems": true, + "title": "type alternative 2", + "description": "type alternative 2 value." + } + ], + "title": "type", + "description": "type value." + }, + "allOf": { + "$ref": "#/$defs/Meta04/definitions/schemaArray", + "title": "allOf", + "description": "See schemaArray." + }, + "anyOf": { + "$ref": "#/$defs/Meta04/definitions/schemaArray", + "title": "anyOf", + "description": "See schemaArray." + }, + "oneOf": { + "$ref": "#/$defs/Meta04/definitions/schemaArray", + "title": "oneOf", + "description": "See schemaArray." + }, + "not": { + "$ref": "#/$defs/Meta04", + "title": "not", + "description": "See Meta04." + } + }, + "dependentRequired": { + "exclusiveMaximum": [ + "maximum" + ], + "exclusiveMinimum": [ + "minimum" + ] + }, + "default": {}, + "title": "Meta04" + }, + "Meta06": { + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/Meta06", + "title": "schemaArray items", + "description": "See Meta06." + }, + "title": "schemaArray", + "description": "schemaArray value." + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0, + "title": "nonNegativeInteger", + "description": "nonNegativeInteger value." + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/$defs/Meta06/definitions/nonNegativeInteger", + "title": "nonNegativeIntegerDefault0 alternative 1", + "description": "See nonNegativeInteger." + }, + { + "default": 0, + "title": "nonNegativeIntegerDefault0 alternative 2", + "description": "nonNegativeIntegerDefault0 alternative 2 value." + } + ], + "title": "nonNegativeIntegerDefault0", + "description": "nonNegativeIntegerDefault0 value." + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ], + "title": "simpleTypes", + "description": "simpleTypes value." + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "title": "stringArray items", + "description": "stringArray items value." + }, + "uniqueItems": true, + "default": [], + "title": "stringArray", + "description": "stringArray value." + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "title": "$id", + "description": "$id value." + }, + "$schema": { + "type": "string", + "title": "$schema", + "description": "$schema value." + }, + "$ref": { + "type": "string", + "title": "$ref", + "description": "$ref value." + }, + "title": { + "type": "string", + "title": "title", + "description": "title value." + }, + "description": { + "type": "string", + "title": "description", + "description": "description value." + }, + "default": { + "title": "default", + "description": "default value." + }, + "examples": { + "type": "array", + "items": { + "title": "examples items", + "description": "examples items value." + }, + "title": "examples", + "description": "examples value." + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0, + "title": "multipleOf", + "description": "multipleOf value." + }, + "maximum": { + "type": "number", + "title": "maximum", + "description": "maximum value." + }, + "exclusiveMaximum": { + "type": "number", + "title": "exclusiveMaximum", + "description": "exclusiveMaximum value." + }, + "minimum": { + "type": "number", + "title": "minimum", + "description": "minimum value." + }, + "exclusiveMinimum": { + "type": "number", + "title": "exclusiveMinimum", + "description": "exclusiveMinimum value." + }, + "maxLength": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeInteger", + "title": "maxLength", + "description": "See nonNegativeInteger." + }, + "minLength": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeIntegerDefault0", + "title": "minLength", + "description": "See nonNegativeIntegerDefault0." + }, + "pattern": { + "type": "string", + "title": "pattern", + "description": "pattern value." + }, + "additionalItems": { + "$ref": "#/$defs/Meta06", + "title": "additionalItems", + "description": "See Meta06." + }, + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Meta06", + "title": "items alternative 1", + "description": "See Meta06." + }, + { + "$ref": "#/$defs/Meta06/definitions/schemaArray", + "title": "items alternative 2", + "description": "See schemaArray." + } + ], + "default": {}, + "title": "items", + "description": "items value." + }, + "maxItems": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeInteger", + "title": "maxItems", + "description": "See nonNegativeInteger." + }, + "minItems": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeIntegerDefault0", + "title": "minItems", + "description": "See nonNegativeIntegerDefault0." + }, + "uniqueItems": { + "type": "boolean", + "default": false, + "title": "uniqueItems", + "description": "uniqueItems value." + }, + "contains": { + "$ref": "#/$defs/Meta06", + "title": "contains", + "description": "See Meta06." + }, + "maxProperties": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeInteger", + "title": "maxProperties", + "description": "See nonNegativeInteger." + }, + "minProperties": { + "$ref": "#/$defs/Meta06/definitions/nonNegativeIntegerDefault0", + "title": "minProperties", + "description": "See nonNegativeIntegerDefault0." + }, + "required": { + "$ref": "#/$defs/Meta06/definitions/stringArray", + "title": "required", + "description": "See stringArray." + }, + "additionalProperties": { + "$ref": "#/$defs/Meta06", + "title": "additionalProperties", + "description": "See Meta06." + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta06", + "title": "definitions additionalProperties", + "description": "See Meta06." + }, + "default": {}, + "title": "definitions", + "description": "definitions value." + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta06", + "title": "properties additionalProperties", + "description": "See Meta06." + }, + "default": {}, + "title": "properties", + "description": "properties value." + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta06", + "title": "patternProperties additionalProperties", + "description": "See Meta06." + }, + "default": {}, + "title": "patternProperties", + "description": "patternProperties value." + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Meta06", + "title": "dependencies additionalProperties alternative 1", + "description": "See Meta06." + }, + { + "$ref": "#/$defs/Meta06/definitions/stringArray", + "title": "dependencies additionalProperties alternative 2", + "description": "See stringArray." + } + ], + "title": "dependencies additionalProperties", + "description": "dependencies additionalProperties value." + }, + "title": "dependencies", + "description": "dependencies value." + }, + "propertyNames": { + "$ref": "#/$defs/Meta06", + "title": "propertyNames", + "description": "See Meta06." + }, + "const": { + "title": "const", + "description": "const value." + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "title": "enum", + "description": "enum value." + }, + "type": { + "anyOf": [ + { + "$ref": "#/$defs/Meta06/definitions/simpleTypes", + "title": "type alternative 1", + "description": "See simpleTypes." + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/Meta06/definitions/simpleTypes", + "title": "type alternative 2 items", + "description": "See simpleTypes." + }, + "minItems": 1, + "uniqueItems": true, + "title": "type alternative 2", + "description": "type alternative 2 value." + } + ], + "title": "type", + "description": "type value." + }, + "format": { + "type": "string", + "title": "format", + "description": "format value." + }, + "allOf": { + "$ref": "#/$defs/Meta06/definitions/schemaArray", + "title": "allOf", + "description": "See schemaArray." + }, + "anyOf": { + "$ref": "#/$defs/Meta06/definitions/schemaArray", + "title": "anyOf", + "description": "See schemaArray." + }, + "oneOf": { + "$ref": "#/$defs/Meta06/definitions/schemaArray", + "title": "oneOf", + "description": "See schemaArray." + }, + "not": { + "$ref": "#/$defs/Meta06", + "title": "not", + "description": "See Meta06." + } + }, + "default": {}, + "description": "Meta06 value." + }, + "Meta07": { + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/Meta07", + "title": "schemaArray items", + "description": "See Meta07." + }, + "title": "schemaArray", + "description": "schemaArray value." + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0, + "title": "nonNegativeInteger", + "description": "nonNegativeInteger value." + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/$defs/Meta07/definitions/nonNegativeInteger", + "title": "nonNegativeIntegerDefault0 alternative 1", + "description": "See nonNegativeInteger." + }, + { + "default": 0, + "title": "nonNegativeIntegerDefault0 alternative 2", + "description": "nonNegativeIntegerDefault0 alternative 2 value." + } + ], + "title": "nonNegativeIntegerDefault0", + "description": "nonNegativeIntegerDefault0 value." + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ], + "title": "simpleTypes", + "description": "simpleTypes value." + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "title": "stringArray items", + "description": "stringArray items value." + }, + "uniqueItems": true, + "default": [], + "title": "stringArray", + "description": "stringArray value." + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "title": "$id", + "description": "$id value." + }, + "$schema": { + "type": "string", + "title": "$schema", + "description": "$schema value." + }, + "$ref": { + "type": "string", + "title": "$ref", + "description": "$ref value." + }, + "$comment": { + "type": "string", + "title": "$comment", + "description": "$comment value." + }, + "title": { + "type": "string", + "title": "title", + "description": "title value." + }, + "description": { + "type": "string", + "title": "description", + "description": "description value." + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false, + "title": "readOnly", + "description": "readOnly value." + }, + "examples": { + "type": "array", + "items": true, + "title": "examples", + "description": "examples value." + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0, + "title": "multipleOf", + "description": "multipleOf value." + }, + "maximum": { + "type": "number", + "title": "maximum", + "description": "maximum value." + }, + "exclusiveMaximum": { + "type": "number", + "title": "exclusiveMaximum", + "description": "exclusiveMaximum value." + }, + "minimum": { + "type": "number", + "title": "minimum", + "description": "minimum value." + }, + "exclusiveMinimum": { + "type": "number", + "title": "exclusiveMinimum", + "description": "exclusiveMinimum value." + }, + "maxLength": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeInteger", + "title": "maxLength", + "description": "See nonNegativeInteger." + }, + "minLength": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeIntegerDefault0", + "title": "minLength", + "description": "See nonNegativeIntegerDefault0." + }, + "pattern": { + "type": "string", + "title": "pattern", + "description": "pattern value." + }, + "additionalItems": { + "$ref": "#/$defs/Meta07", + "title": "additionalItems", + "description": "See Meta07." + }, + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Meta07", + "title": "items alternative 1", + "description": "See Meta07." + }, + { + "$ref": "#/$defs/Meta07/definitions/schemaArray", + "title": "items alternative 2", + "description": "See schemaArray." + } + ], + "default": true, + "title": "items", + "description": "items value." + }, + "maxItems": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeInteger", + "title": "maxItems", + "description": "See nonNegativeInteger." + }, + "minItems": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeIntegerDefault0", + "title": "minItems", + "description": "See nonNegativeIntegerDefault0." + }, + "uniqueItems": { + "type": "boolean", + "default": false, + "title": "uniqueItems", + "description": "uniqueItems value." + }, + "contains": { + "$ref": "#/$defs/Meta07", + "title": "contains", + "description": "See Meta07." + }, + "maxProperties": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeInteger", + "title": "maxProperties", + "description": "See nonNegativeInteger." + }, + "minProperties": { + "$ref": "#/$defs/Meta07/definitions/nonNegativeIntegerDefault0", + "title": "minProperties", + "description": "See nonNegativeIntegerDefault0." + }, + "required": { + "$ref": "#/$defs/Meta07/definitions/stringArray", + "title": "required", + "description": "See stringArray." + }, + "additionalProperties": { + "$ref": "#/$defs/Meta07", + "title": "additionalProperties", + "description": "See Meta07." + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta07", + "title": "definitions additionalProperties", + "description": "See Meta07." + }, + "default": {}, + "title": "definitions", + "description": "definitions value." + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta07", + "title": "properties additionalProperties", + "description": "See Meta07." + }, + "default": {}, + "title": "properties", + "description": "properties value." + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta07", + "title": "patternProperties additionalProperties", + "description": "See Meta07." + }, + "propertyNames": { + "title": "patternProperties propertyNames", + "description": "patternProperties propertyNames value." + }, + "default": {}, + "title": "patternProperties", + "description": "patternProperties value." + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Meta07", + "title": "dependencies additionalProperties alternative 1", + "description": "See Meta07." + }, + { + "$ref": "#/$defs/Meta07/definitions/stringArray", + "title": "dependencies additionalProperties alternative 2", + "description": "See stringArray." + } + ], + "title": "dependencies additionalProperties", + "description": "dependencies additionalProperties value." + }, + "title": "dependencies", + "description": "dependencies value." + }, + "propertyNames": { + "$ref": "#/$defs/Meta07", + "title": "propertyNames", + "description": "See Meta07." + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true, + "title": "enum", + "description": "enum value." + }, + "type": { + "anyOf": [ + { + "$ref": "#/$defs/Meta07/definitions/simpleTypes", + "title": "type alternative 1", + "description": "See simpleTypes." + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/Meta07/definitions/simpleTypes", + "title": "type alternative 2 items", + "description": "See simpleTypes." + }, + "minItems": 1, + "uniqueItems": true, + "title": "type alternative 2", + "description": "type alternative 2 value." + } + ], + "title": "type", + "description": "type value." + }, + "format": { + "type": "string", + "title": "format", + "description": "format value." + }, + "contentMediaType": { + "type": "string", + "title": "contentMediaType", + "description": "contentMediaType value." + }, + "contentEncoding": { + "type": "string", + "title": "contentEncoding", + "description": "contentEncoding value." + }, + "if": { + "$ref": "#/$defs/Meta07", + "title": "if", + "description": "See Meta07." + }, + "then": { + "$ref": "#/$defs/Meta07", + "title": "then", + "description": "See Meta07." + }, + "else": { + "$ref": "#/$defs/Meta07", + "title": "else", + "description": "See Meta07." + }, + "allOf": { + "$ref": "#/$defs/Meta07/definitions/schemaArray", + "title": "allOf", + "description": "See schemaArray." + }, + "anyOf": { + "$ref": "#/$defs/Meta07/definitions/schemaArray", + "title": "anyOf", + "description": "See schemaArray." + }, + "oneOf": { + "$ref": "#/$defs/Meta07/definitions/schemaArray", + "title": "oneOf", + "description": "See schemaArray." + }, + "not": { + "$ref": "#/$defs/Meta07", + "title": "not", + "description": "See Meta07." + } + }, + "default": true, + "description": "Meta07 value." + }, + "Meta2019": { + "title": "Core and Validation specifications meta-schema", + "allOf": [ + { + "$ref": "#/$defs/Meta2019_core", + "title": "Meta2019 alternative 1", + "description": "See Meta2019_core." + }, + { + "$ref": "#/$defs/Meta2019_applicator", + "title": "Meta2019 alternative 2", + "description": "See Meta2019_applicator." + }, + { + "$ref": "#/$defs/Meta2019_validation", + "title": "Meta2019 alternative 3", + "description": "See Meta2019_validation." + }, + { + "$ref": "#/$defs/Meta2019_meta-data", + "title": "Meta2019 alternative 4", + "description": "See Meta2019_meta-data." + }, + { + "$ref": "#/$defs/Meta2019_format", + "title": "Meta2019 alternative 5", + "description": "See Meta2019_format." + }, + { + "$ref": "#/$defs/Meta2019_content", + "title": "Meta2019 alternative 6", + "description": "See Meta2019_content." + } + ], + "type": [ + "object", + "boolean" + ], + "properties": { + "definitions": { + "$comment": "While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "definitions additionalProperties", + "description": "See Meta2019." + }, + "default": {}, + "title": "definitions", + "description": "definitions value." + }, + "dependencies": { + "$comment": "\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Meta2019", + "title": "dependencies additionalProperties alternative 1", + "description": "See Meta2019." + }, + { + "$ref": "#/$defs/Meta2019_validation/$defs/stringArray", + "title": "dependencies additionalProperties alternative 2", + "description": "See stringArray." + } + ], + "title": "dependencies additionalProperties", + "description": "dependencies additionalProperties value." + }, + "title": "dependencies", + "description": "dependencies value." + } + }, + "description": "Meta2019 value." + }, + "Meta2019_core": { + "title": "Core vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$", + "title": "$id", + "description": "$id value." + }, + "$schema": { + "type": "string", + "title": "$schema", + "description": "$schema value." + }, + "$anchor": { + "type": "string", + "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$", + "title": "$anchor", + "description": "$anchor value." + }, + "$ref": { + "type": "string", + "title": "$ref", + "description": "$ref value." + }, + "$recursiveRef": { + "type": "string", + "title": "$recursiveRef", + "description": "$recursiveRef value." + }, + "$recursiveAnchor": { + "type": "boolean", + "default": false, + "title": "$recursiveAnchor", + "description": "$recursiveAnchor value." + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "type": "string", + "title": "$vocabulary propertyNames", + "description": "$vocabulary propertyNames value." + }, + "additionalProperties": { + "type": "boolean", + "title": "$vocabulary additionalProperties", + "description": "$vocabulary additionalProperties value." + }, + "title": "$vocabulary", + "description": "$vocabulary value." + }, + "$comment": { + "type": "string", + "title": "$comment", + "description": "$comment value." + }, + "$defs": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "$defs additionalProperties", + "description": "See Meta2019." + }, + "default": {}, + "title": "$defs", + "description": "$defs value." + } + }, + "description": "Meta2019_core value." + }, + "Meta2019_applicator": { + "title": "Applicator vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "additionalItems": { + "$ref": "#/$defs/Meta2019", + "title": "additionalItems", + "description": "See Meta2019." + }, + "unevaluatedItems": { + "$ref": "#/$defs/Meta2019", + "title": "unevaluatedItems", + "description": "See Meta2019." + }, + "items": { + "anyOf": [ + { + "$ref": "#/$defs/Meta2019", + "title": "items alternative 1", + "description": "See Meta2019." + }, + { + "$ref": "#/$defs/Meta2019_applicator/$defs/schemaArray", + "title": "items alternative 2", + "description": "See schemaArray." + } + ], + "title": "items", + "description": "items value." + }, + "contains": { + "$ref": "#/$defs/Meta2019", + "title": "contains", + "description": "See Meta2019." + }, + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "additionalProperties", + "description": "See Meta2019." + }, + "unevaluatedProperties": { + "$ref": "#/$defs/Meta2019", + "title": "unevaluatedProperties", + "description": "See Meta2019." + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "properties additionalProperties", + "description": "See Meta2019." + }, + "default": {}, + "title": "properties", + "description": "properties value." + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "patternProperties additionalProperties", + "description": "See Meta2019." + }, + "propertyNames": { + "title": "patternProperties propertyNames", + "description": "patternProperties propertyNames value." + }, + "default": {}, + "title": "patternProperties", + "description": "patternProperties value." + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019", + "title": "dependentSchemas additionalProperties", + "description": "See Meta2019." + }, + "title": "dependentSchemas", + "description": "dependentSchemas value." + }, + "propertyNames": { + "$ref": "#/$defs/Meta2019", + "title": "propertyNames", + "description": "See Meta2019." + }, + "if": { + "$ref": "#/$defs/Meta2019", + "title": "if", + "description": "See Meta2019." + }, + "then": { + "$ref": "#/$defs/Meta2019", + "title": "then", + "description": "See Meta2019." + }, + "else": { + "$ref": "#/$defs/Meta2019", + "title": "else", + "description": "See Meta2019." + }, + "allOf": { + "$ref": "#/$defs/Meta2019_applicator/$defs/schemaArray", + "title": "allOf", + "description": "See schemaArray." + }, + "anyOf": { + "$ref": "#/$defs/Meta2019_applicator/$defs/schemaArray", + "title": "anyOf", + "description": "See schemaArray." + }, + "oneOf": { + "$ref": "#/$defs/Meta2019_applicator/$defs/schemaArray", + "title": "oneOf", + "description": "See schemaArray." + }, + "not": { + "$ref": "#/$defs/Meta2019", + "title": "not", + "description": "See Meta2019." + } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/Meta2019", + "title": "schemaArray items", + "description": "See Meta2019." + }, + "title": "schemaArray", + "description": "schemaArray value." + } + }, + "description": "Meta2019_applicator value." + }, + "Meta2019_validation": { + "title": "Validation vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0, + "title": "multipleOf", + "description": "multipleOf value." + }, + "maximum": { + "type": "number", + "title": "maximum", + "description": "maximum value." + }, + "exclusiveMaximum": { + "type": "number", + "title": "exclusiveMaximum", + "description": "exclusiveMaximum value." + }, + "minimum": { + "type": "number", + "title": "minimum", + "description": "minimum value." + }, + "exclusiveMinimum": { + "type": "number", + "title": "exclusiveMinimum", + "description": "exclusiveMinimum value." + }, + "maxLength": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "title": "maxLength", + "description": "See nonNegativeInteger." + }, + "minLength": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeIntegerDefault0", + "title": "minLength", + "description": "See nonNegativeIntegerDefault0." + }, + "pattern": { + "type": "string", + "title": "pattern", + "description": "pattern value." + }, + "maxItems": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "title": "maxItems", + "description": "See nonNegativeInteger." + }, + "minItems": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeIntegerDefault0", + "title": "minItems", + "description": "See nonNegativeIntegerDefault0." + }, + "uniqueItems": { + "type": "boolean", + "default": false, + "title": "uniqueItems", + "description": "uniqueItems value." + }, + "maxContains": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "title": "maxContains", + "description": "See nonNegativeInteger." + }, + "minContains": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "default": 1, + "title": "minContains", + "description": "See nonNegativeInteger." + }, + "maxProperties": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "title": "maxProperties", + "description": "See nonNegativeInteger." + }, + "minProperties": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeIntegerDefault0", + "title": "minProperties", + "description": "See nonNegativeIntegerDefault0." + }, + "required": { + "$ref": "#/$defs/Meta2019_validation/$defs/stringArray", + "title": "required", + "description": "See stringArray." + }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2019_validation/$defs/stringArray", + "title": "dependentRequired additionalProperties", + "description": "See stringArray." + }, + "title": "dependentRequired", + "description": "dependentRequired value." + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "title": "enum", + "description": "enum value." + }, + "type": { + "anyOf": [ + { + "$ref": "#/$defs/Meta2019_validation/$defs/simpleTypes", + "title": "type alternative 1", + "description": "See simpleTypes." + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/Meta2019_validation/$defs/simpleTypes", + "title": "type alternative 2 items", + "description": "See simpleTypes." + }, + "minItems": 1, + "uniqueItems": true, + "title": "type alternative 2", + "description": "type alternative 2 value." + } + ], + "title": "type", + "description": "type value." + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0, + "title": "nonNegativeInteger", + "description": "nonNegativeInteger value." + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/Meta2019_validation/$defs/nonNegativeInteger", + "default": 0, + "title": "nonNegativeIntegerDefault0", + "description": "See nonNegativeInteger." + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ], + "title": "simpleTypes", + "description": "simpleTypes value." + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "title": "stringArray items", + "description": "stringArray items value." + }, + "uniqueItems": true, + "default": [], + "title": "stringArray", + "description": "stringArray value." + } + }, + "description": "Meta2019_validation value." + }, + "Meta2019_meta-data": { + "title": "Meta-data vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "title": { + "type": "string", + "title": "title", + "description": "title value." + }, + "description": { + "type": "string", + "title": "description", + "description": "description value." + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false, + "title": "deprecated", + "description": "deprecated value." + }, + "readOnly": { + "type": "boolean", + "default": false, + "title": "readOnly", + "description": "readOnly value." + }, + "writeOnly": { + "type": "boolean", + "default": false, + "title": "writeOnly", + "description": "writeOnly value." + }, + "examples": { + "type": "array", + "items": true, + "title": "examples", + "description": "examples value." + } + }, + "description": "Meta2019_meta-data value." + }, + "Meta2019_format": { + "title": "Format vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "format": { + "type": "string", + "title": "format", + "description": "format value." + } + }, + "description": "Meta2019_format value." + }, + "Meta2019_content": { + "title": "Content vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "contentMediaType": { + "type": "string", + "title": "contentMediaType", + "description": "contentMediaType value." + }, + "contentEncoding": { + "type": "string", + "title": "contentEncoding", + "description": "contentEncoding value." + }, + "contentSchema": { + "$ref": "#/$defs/Meta2019", + "title": "contentSchema", + "description": "See Meta2019." + } + }, + "description": "Meta2019_content value." + }, + "Meta2020": { + "title": "Core and Validation specifications meta-schema", + "allOf": [ + { + "$ref": "#/$defs/Meta2020_core", + "title": "Meta2020 alternative 1", + "description": "See Meta2020_core." + }, + { + "$ref": "#/$defs/Meta2020_applicator", + "title": "Meta2020 alternative 2", + "description": "See Meta2020_applicator." + }, + { + "$ref": "#/$defs/Meta2020_unevaluated", + "title": "Meta2020 alternative 3", + "description": "See Meta2020_unevaluated." + }, + { + "$ref": "#/$defs/Meta2020_validation", + "title": "Meta2020 alternative 4", + "description": "See Meta2020_validation." + }, + { + "$ref": "#/$defs/Meta2020_meta-data", + "title": "Meta2020 alternative 5", + "description": "See Meta2020_meta-data." + }, + { + "$ref": "#/$defs/Meta2020_format-annotation", + "title": "Meta2020 alternative 6", + "description": "See Meta2020_format-annotation." + }, + { + "$ref": "#/$defs/Meta2020_content", + "title": "Meta2020 alternative 7", + "description": "See Meta2020_content." + } + ], + "type": [ + "object", + "boolean" + ], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "definitions additionalProperties", + "description": "See Meta2020." + }, + "deprecated": true, + "default": {}, + "title": "definitions", + "description": "definitions value." + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/Meta2020", + "title": "dependencies additionalProperties alternative 1", + "description": "See Meta2020." + }, + { + "$ref": "#/$defs/Meta2020_validation/$defs/stringArray", + "title": "dependencies additionalProperties alternative 2", + "description": "See stringArray." + } + ], + "title": "dependencies additionalProperties", + "description": "dependencies additionalProperties value." + }, + "deprecated": true, + "default": {}, + "title": "dependencies", + "description": "dependencies value." + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "#/$defs/Meta2020_core/$defs/anchorString", + "deprecated": true, + "title": "$recursiveAnchor", + "description": "See anchorString." + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "#/$defs/Meta2020_core/$defs/uriReferenceString", + "deprecated": true, + "title": "$recursiveRef", + "description": "See uriReferenceString." + } + }, + "description": "Meta2020 value." + }, + "Meta2020_core": { + "title": "Core vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "$ref": "#/$defs/Meta2020_core/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$", + "title": "$id", + "description": "See uriReferenceString." + }, + "$schema": { + "$ref": "#/$defs/Meta2020_core/$defs/uriString", + "title": "$schema", + "description": "See uriString." + }, + "$ref": { + "$ref": "#/$defs/Meta2020_core/$defs/uriReferenceString", + "title": "$ref", + "description": "See uriReferenceString." + }, + "$anchor": { + "$ref": "#/$defs/Meta2020_core/$defs/anchorString", + "title": "$anchor", + "description": "See anchorString." + }, + "$dynamicRef": { + "$ref": "#/$defs/Meta2020_core/$defs/uriReferenceString", + "title": "$dynamicRef", + "description": "See uriReferenceString." + }, + "$dynamicAnchor": { + "$ref": "#/$defs/Meta2020_core/$defs/anchorString", + "title": "$dynamicAnchor", + "description": "See anchorString." + }, + "$vocabulary": { + "type": "object", + "propertyNames": { + "$ref": "#/$defs/Meta2020_core/$defs/uriString", + "title": "$vocabulary propertyNames", + "description": "See uriString." + }, + "additionalProperties": { + "type": "boolean", + "title": "$vocabulary additionalProperties", + "description": "$vocabulary additionalProperties value." + }, + "title": "$vocabulary", + "description": "$vocabulary value." + }, + "$comment": { + "type": "string", + "title": "$comment", + "description": "$comment value." + }, + "$defs": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "$defs additionalProperties", + "description": "See Meta2020." + }, + "title": "$defs", + "description": "$defs value." + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$", + "title": "anchorString", + "description": "anchorString value." + }, + "uriString": { + "type": "string", + "title": "uriString", + "description": "uriString value." + }, + "uriReferenceString": { + "type": "string", + "title": "uriReferenceString", + "description": "uriReferenceString value." + } + }, + "description": "Meta2020_core value." + }, + "Meta2020_applicator": { + "title": "Applicator vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "prefixItems": { + "$ref": "#/$defs/Meta2020_applicator/$defs/schemaArray", + "title": "prefixItems", + "description": "See schemaArray." + }, + "items": { + "$ref": "#/$defs/Meta2020", + "title": "items", + "description": "See Meta2020." + }, + "contains": { + "$ref": "#/$defs/Meta2020", + "title": "contains", + "description": "See Meta2020." + }, + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "additionalProperties", + "description": "See Meta2020." + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "properties additionalProperties", + "description": "See Meta2020." + }, + "default": {}, + "title": "properties", + "description": "properties value." + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "patternProperties additionalProperties", + "description": "See Meta2020." + }, + "propertyNames": { + "title": "patternProperties propertyNames", + "description": "patternProperties propertyNames value." + }, + "default": {}, + "title": "patternProperties", + "description": "patternProperties value." + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020", + "title": "dependentSchemas additionalProperties", + "description": "See Meta2020." + }, + "default": {}, + "title": "dependentSchemas", + "description": "dependentSchemas value." + }, + "propertyNames": { + "$ref": "#/$defs/Meta2020", + "title": "propertyNames", + "description": "See Meta2020." + }, + "if": { + "$ref": "#/$defs/Meta2020", + "title": "if", + "description": "See Meta2020." + }, + "then": { + "$ref": "#/$defs/Meta2020", + "title": "then", + "description": "See Meta2020." + }, + "else": { + "$ref": "#/$defs/Meta2020", + "title": "else", + "description": "See Meta2020." + }, + "allOf": { + "$ref": "#/$defs/Meta2020_applicator/$defs/schemaArray", + "title": "allOf", + "description": "See schemaArray." + }, + "anyOf": { + "$ref": "#/$defs/Meta2020_applicator/$defs/schemaArray", + "title": "anyOf", + "description": "See schemaArray." + }, + "oneOf": { + "$ref": "#/$defs/Meta2020_applicator/$defs/schemaArray", + "title": "oneOf", + "description": "See schemaArray." + }, + "not": { + "$ref": "#/$defs/Meta2020", + "title": "not", + "description": "See Meta2020." + } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/Meta2020", + "title": "schemaArray items", + "description": "See Meta2020." + }, + "title": "schemaArray", + "description": "schemaArray value." + } + }, + "description": "Meta2020_applicator value." + }, + "Meta2020_unevaluated": { + "title": "Unevaluated applicator vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "unevaluatedItems": { + "$ref": "#/$defs/Meta2020", + "title": "unevaluatedItems", + "description": "See Meta2020." + }, + "unevaluatedProperties": { + "$ref": "#/$defs/Meta2020", + "title": "unevaluatedProperties", + "description": "See Meta2020." + } + }, + "description": "Meta2020_unevaluated value." + }, + "Meta2020_validation": { + "title": "Validation vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "type": { + "anyOf": [ + { + "$ref": "#/$defs/Meta2020_validation/$defs/simpleTypes", + "title": "type alternative 1", + "description": "See simpleTypes." + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/Meta2020_validation/$defs/simpleTypes", + "title": "type alternative 2 items", + "description": "See simpleTypes." + }, + "minItems": 1, + "uniqueItems": true, + "title": "type alternative 2", + "description": "type alternative 2 value." + } + ], + "title": "type", + "description": "type value." + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "title": "enum", + "description": "enum value." + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0, + "title": "multipleOf", + "description": "multipleOf value." + }, + "maximum": { + "type": "number", + "title": "maximum", + "description": "maximum value." + }, + "exclusiveMaximum": { + "type": "number", + "title": "exclusiveMaximum", + "description": "exclusiveMaximum value." + }, + "minimum": { + "type": "number", + "title": "minimum", + "description": "minimum value." + }, + "exclusiveMinimum": { + "type": "number", + "title": "exclusiveMinimum", + "description": "exclusiveMinimum value." + }, + "maxLength": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "title": "maxLength", + "description": "See nonNegativeInteger." + }, + "minLength": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeIntegerDefault0", + "title": "minLength", + "description": "See nonNegativeIntegerDefault0." + }, + "pattern": { + "type": "string", + "title": "pattern", + "description": "pattern value." + }, + "maxItems": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "title": "maxItems", + "description": "See nonNegativeInteger." + }, + "minItems": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeIntegerDefault0", + "title": "minItems", + "description": "See nonNegativeIntegerDefault0." + }, + "uniqueItems": { + "type": "boolean", + "default": false, + "title": "uniqueItems", + "description": "uniqueItems value." + }, + "maxContains": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "title": "maxContains", + "description": "See nonNegativeInteger." + }, + "minContains": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "default": 1, + "title": "minContains", + "description": "See nonNegativeInteger." + }, + "maxProperties": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "title": "maxProperties", + "description": "See nonNegativeInteger." + }, + "minProperties": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeIntegerDefault0", + "title": "minProperties", + "description": "See nonNegativeIntegerDefault0." + }, + "required": { + "$ref": "#/$defs/Meta2020_validation/$defs/stringArray", + "title": "required", + "description": "See stringArray." + }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Meta2020_validation/$defs/stringArray", + "title": "dependentRequired additionalProperties", + "description": "See stringArray." + }, + "title": "dependentRequired", + "description": "dependentRequired value." + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0, + "title": "nonNegativeInteger", + "description": "nonNegativeInteger value." + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/Meta2020_validation/$defs/nonNegativeInteger", + "default": 0, + "title": "nonNegativeIntegerDefault0", + "description": "See nonNegativeInteger." + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ], + "title": "simpleTypes", + "description": "simpleTypes value." + }, + "stringArray": { + "type": "array", + "items": { + "type": "string", + "title": "stringArray items", + "description": "stringArray items value." + }, + "uniqueItems": true, + "default": [], + "title": "stringArray", + "description": "stringArray value." + } + }, + "description": "Meta2020_validation value." + }, + "Meta2020_meta-data": { + "title": "Meta-data vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "title": { + "type": "string", + "title": "title", + "description": "title value." + }, + "description": { + "type": "string", + "title": "description", + "description": "description value." + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false, + "title": "deprecated", + "description": "deprecated value." + }, + "readOnly": { + "type": "boolean", + "default": false, + "title": "readOnly", + "description": "readOnly value." + }, + "writeOnly": { + "type": "boolean", + "default": false, + "title": "writeOnly", + "description": "writeOnly value." + }, + "examples": { + "type": "array", + "items": true, + "title": "examples", + "description": "examples value." + } + }, + "description": "Meta2020_meta-data value." + }, + "Meta2020_format-annotation": { + "title": "Format vocabulary meta-schema for annotation results", + "type": [ + "object", + "boolean" + ], + "properties": { + "format": { + "type": "string", + "title": "format", + "description": "format value." + } + }, + "description": "Meta2020_format-annotation value." + }, + "Meta2020_content": { + "title": "Content vocabulary meta-schema", + "type": [ + "object", + "boolean" + ], + "properties": { + "contentEncoding": { + "type": "string", + "title": "contentEncoding", + "description": "contentEncoding value." + }, + "contentMediaType": { + "type": "string", + "title": "contentMediaType", + "description": "contentMediaType value." + }, + "contentSchema": { + "$ref": "#/$defs/Meta2020", + "title": "contentSchema", + "description": "See Meta2020." + } + }, + "description": "Meta2020_content value." + }, + "OutputSchema": { + "title": "Output JSON Schema", + "description": "Schema declaration using SDK-supported drafts 04, 06, 07, 2019-09 or 2020-12 (default). Run flows check to resolve references and prove termination.", + "type": [ + "object", + "boolean" + ], + "properties": { + "$schema": { + "type": "string", + "enum": [ + "http://json-schema.org/draft-04/schema", + "http://json-schema.org/draft-04/schema#", + "http://json-schema.org/draft-06/schema", + "http://json-schema.org/draft-06/schema#", + "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07/schema#", + "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema#", + "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "allOf": [ + { + "if": { + "type": "object", + "properties": { + "$schema": { + "enum": [ + "http://json-schema.org/draft-04/schema", + "http://json-schema.org/draft-04/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 1 if", + "description": "OutputSchema alternative 1 if value." + }, + "then": { + "$ref": "#/$defs/Meta04", + "title": "OutputSchema alternative 1 then", + "description": "See Meta04." + }, + "title": "OutputSchema alternative 1", + "description": "OutputSchema alternative 1 value." + }, + { + "if": { + "type": "object", + "properties": { + "$schema": { + "enum": [ + "http://json-schema.org/draft-06/schema", + "http://json-schema.org/draft-06/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 2 if", + "description": "OutputSchema alternative 2 if value." + }, + "then": { + "$ref": "#/$defs/Meta06", + "title": "OutputSchema alternative 2 then", + "description": "See Meta06." + }, + "title": "OutputSchema alternative 2", + "description": "OutputSchema alternative 2 value." + }, + { + "if": { + "type": "object", + "properties": { + "$schema": { + "enum": [ + "http://json-schema.org/draft-07/schema", + "http://json-schema.org/draft-07/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 3 if", + "description": "OutputSchema alternative 3 if value." + }, + "then": { + "$ref": "#/$defs/Meta07", + "title": "OutputSchema alternative 3 then", + "description": "See Meta07." + }, + "title": "OutputSchema alternative 3", + "description": "OutputSchema alternative 3 value." + }, + { + "if": { + "type": "object", + "properties": { + "$schema": { + "enum": [ + "https://json-schema.org/draft/2019-09/schema", + "https://json-schema.org/draft/2019-09/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 4 if", + "description": "OutputSchema alternative 4 if value." + }, + "then": { + "$ref": "#/$defs/Meta2019", + "title": "OutputSchema alternative 4 then", + "description": "See Meta2019." + }, + "title": "OutputSchema alternative 4", + "description": "OutputSchema alternative 4 value." + }, + { + "if": { + "anyOf": [ + { + "type": "object", + "properties": { + "$schema": { + "enum": [ + "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema#" + ], + "title": "$schema", + "description": "$schema value." + } + }, + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 5 if alternative 1", + "description": "OutputSchema alternative 5 if alternative 1 value." + }, + { + "not": { + "type": "object", + "required": [ + "$schema" + ], + "title": "OutputSchema alternative 5 if alternative 2 not", + "description": "OutputSchema alternative 5 if alternative 2 not value." + }, + "title": "OutputSchema alternative 5 if alternative 2", + "description": "OutputSchema alternative 5 if alternative 2 value." + } + ], + "title": "OutputSchema alternative 5 if", + "description": "OutputSchema alternative 5 if value." + }, + "then": { + "$ref": "#/$defs/Meta2020", + "title": "OutputSchema alternative 5 then", + "description": "See Meta2020." + }, + "title": "OutputSchema alternative 5", + "description": "OutputSchema alternative 5 value." + } + ] + } + } +} diff --git a/packages/schema/package.json b/packages/schema/package.json new file mode 100644 index 000000000..529b0293b --- /dev/null +++ b/packages/schema/package.json @@ -0,0 +1,29 @@ +{ + "name": "@relayflows/schema", + "version": "0.1.0", + "description": "JSON Schema for Relayflows YAML and JSON editor validation", + "license": "Apache-2.0", + "main": "./flows.schema.json", + "exports": { + ".": "./flows.schema.json", + "./flows.schema.json": "./flows.schema.json" + }, + "files": [ + "flows.schema.json", + "README.md", + "LICENSE", + "THIRD_PARTY_LICENSES" + ], + "scripts": { + "generate": "node ../../scripts/generate-json-schema.mjs", + "test": "bun test tests" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AgentWorkforce/flows.git", + "directory": "packages/schema" + } +} diff --git a/packages/schema/tests/parity.test.ts b/packages/schema/tests/parity.test.ts new file mode 100644 index 000000000..07af18012 --- /dev/null +++ b/packages/schema/tests/parity.test.ts @@ -0,0 +1,158 @@ +import { afterAll, expect, test } from 'bun:test'; +import { mkdtempSync, readFileSync, readdirSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createRequire } from 'node:module'; +import { checkFlow } from '../../sdk/src/cli/check.ts'; +import { compileSpec, CompileError } from '../../sdk/src/compile.ts'; +import { jsonSchemaBoundError } from '../../sdk/src/json-schema-bound.ts'; + +const require = createRequire(new URL('../../sdk/package.json', import.meta.url)); +const Ajv = require('ajv/dist/2020.js').default; +const { parse } = require('yaml'); +const schema = JSON.parse(readFileSync(new URL('../flows.schema.json', import.meta.url), 'utf8')); +const validate = new Ajv({ strict: false, allErrors: true, validateFormats: false }).compile(schema); +const root = mkdtempSync(join(tmpdir(), 'relayflows-schema-')); +mkdirSync(join(root, 'preflight')); +mkdirSync(join(root, 'bin')); +// Real checkFlow, isolated executable probes. Fixture YAML is copied unchanged; +// every check must agree, including environment readiness (no ignored refusals). +const wrapper = '#!/bin/sh\nif [ "$1" = "--relayflows-adapter-v1" ]; then echo relayflows-agent-cli-v1; fi\nexit 0\n'; +for (const file of ['preflight/authenticated-cli', 'preflight/analyze-story-claude-cli', 'bin/claude']) writeFileSync(join(root, file), wrapper, { mode: 0o755 }); +writeFileSync(join(root, 'flows.json'), readFileSync(new URL('../../../testdata/flows.json', import.meta.url))); +const oldPath = process.env.PATH; +process.env.PATH = `${join(root, 'bin')}:${oldPath ?? ''}`; +afterAll(() => { process.env.PATH = oldPath; rmSync(root, { recursive: true, force: true }); }); +function checkSource(name: string, source: string) { + const file = join(root, name); + writeFileSync(file, source); + return checkFlow(file).report; +} +const fixtures = readdirSync(new URL('../../../testdata/', import.meta.url)).filter(name => name.endsWith('.flow.yaml')).sort(); +for (const name of fixtures) test(`flows check fixture parity: ${name}`, () => { + const source = readFileSync(new URL(`../../../testdata/${name}`, import.meta.url), 'utf8'); + const accepted = validate(parse(source)); + const errors = structuredClone(validate.errors); + const report = checkSource(name, source); + expect(accepted, JSON.stringify({ errors, report })).toBe(report.ok); + if (!accepted) { + expect(name).toBe('json-schema-invalid.flow.yaml'); + expect(errors.some((error: { instancePath: string }) => error.instancePath === '/steps/0/verification/schema/type')).toBe(true); + const refusal = report.diagnostics.find(d => d.kind === 'invalid_spec'); + expect(JSON.stringify(refusal)).toContain('spec.steps[0].verification.schema'); + expect(JSON.stringify(refusal)).toContain('type'); + } +}); + +const flow = (step: Record) => ({ version: '0.1.0', steps: [{ id: 'one', type: 'deterministic', command: 'echo hello', ...step }] }); +const cases: Array<[string, unknown, boolean]> = [ + ['unknown root key', { ...flow({}), identitty: 'chief' }, false], + ['unsupported version', { ...flow({}), version: '0.2.0' }, false], + ['no steps', { version: '0.1.0', steps: [] }, false], + ['step typo', flow({ timeotMs: 10 }), false], + ['empty command', flow({ command: '' }), false], + ['positive timeout', flow({ timeoutMs: 0 }), false], + ['fractional retry', flow({ maxIterations: 1.5 }), false], + ['wrong step field', flow({ prompt: 'hello' }), false], + ['nonzero exit gate', flow({ verification: { type: 'exit_code', expect: 1 } }), false], + ['legacy zero exit gate', flow({ verification: { type: 'exit_code', expect: 0 } }), true], + ['boolean schema', flow({ verification: { type: 'json_schema', schema: false } }), true], + ['nested invalid schema', flow({ verification: { type: 'json_schema', schema: { properties: { a: { type: 'typo' } } } } }), false], + ['bad memory budget', flow({ memory: { scope: 'script', query: 'lessons', budget: { maxTokensIn: -1 } } }), false], + ['unsafe memory budget', flow({ memory: { scope: 'script', query: 'lessons', budget: { maxTokensIn: 9007199254740992 } } }), false], + ['empty memory query', flow({ memory: { scope: 'script', query: ' ', budget: {} } }), false], + ['unsafe duration', flow({ requirements: { expectedDurationMs: 9007199254740992 } }), false], + ['bad money', { ...flow({}), budget: { maxDollars: 1.5 } }, false], + ['bad input index', flow({ input: { a: { step: 'one', path: [-1] } } }), false], + ['blank input name', flow({ input: { ' ': { step: 'one' } } }), false], + ['trigger silence budget', { ...flow({}), triggers: [{ id: 'tick', executor: 'worker', staleAfterMs: 0 }] }, false], +]; +for (const type of ['llm', 'agent']) { + const step = { id: 'one', type, [type === 'llm' ? 'prompt' : 'instruction']: 'hello' }; + for (const [label, fields, ok] of [ + ['output object', { output: { type: 'object' } }, true], + ['boolean output', { output: true }, false], + ['output and verification', { output: {}, verification: { type: 'output_contains', value: 'hello' } }, false], + ['exit gate', { verification: { type: 'exit_code' } }, false], + ['trimmed model', { model: ' model ' }, false], + ['control in model', { model: 'a\nb' }, false], + ] as const) cases.push([`${type}: ${label}`, { version: '0.1.0', steps: [{ ...step, ...fields }] }, ok]); +} +for (const [name, value, expected] of cases) test(`structural parity: ${name}`, () => { + expect(validate(value), JSON.stringify(validate.errors)).toBe(expected); + let errors: string[] = []; + try { compileSpec(value); } catch (error) { + if (!(error instanceof CompileError)) throw error; + errors = error.errors; + } + expect(errors.length === 0, errors.join('\n')).toBe(expected); +}); + +test('step examples compile and validate', () => { + for (const name of ['DeterministicStepSpec', 'LlmStepSpec', 'AgentStepSpec']) for (const step of schema.$defs[name].examples) { + const value = { version: '0.1.0', steps: [step] }; + expect(validate(value), JSON.stringify(validate.errors)).toBe(true); + expect(() => compileSpec(value)).not.toThrow(); + } +}); +test('generated schema satisfies the existing bounded-reference rule', () => { + expect(jsonSchemaBoundError(schema)).toBeUndefined(); +}); +test('semantic checks remain explicit runtime responsibilities', () => { + for (const value of [flow({ dependsOn: ['missing'] }), flow({ verification: { type: 'json_schema', schema: { $ref: '#' } } })]) { + expect(validate(value)).toBe(true); + expect(() => compileSpec(value)).toThrow(CompileError); + } +}); +for (const draft of ['http://json-schema.org/draft-04/schema#', 'http://json-schema.org/draft-06/schema#', 'http://json-schema.org/draft-07/schema#', 'https://json-schema.org/draft/2019-09/schema', 'https://json-schema.org/draft/2020-12/schema']) test(`embedded dialect ${draft}`, () => { + for (const type of ['string', 'typo']) { + const value = flow({ verification: { type: 'json_schema', schema: { $schema: draft, properties: { result: { type } } } } }); + expect(validate(value)).toBe(type === 'string'); + if (type === 'string') expect(() => compileSpec(value)).not.toThrow(); + else expect(() => compileSpec(value)).toThrow(CompileError); + } +}); +test('header hint is warning-only, first-line aware, and never edits input', () => { + const source = JSON.stringify(flow({})); + const header = '# yaml-language-server: $schema=./flows.schema.json\n'; + for (const [name, input, warning] of [ + ['no-header.flow.yaml', source, true], ['header.flow.yaml', header + source, false], + ['bom.flow.yaml', '\uFEFF' + header + source, false], ['crlf.flow.yaml', header.replace('\n', '\r\n') + source, false], + ['later.flow.yaml', '# other comment\n' + header + source, true], ['json.flow.json', source, false], + ] as const) { + const report = checkSource(name, input); + expect(report.ok).toBe(true); + const hints = report.diagnostics.filter(d => d.kind === 'editor_schema_missing'); + expect(hints.length).toBe(warning ? 1 : 0); + if (warning) { + expect(hints[0].severity).toBe('warning'); + expect(hints[0].message).toContain(schema.$id); + } + expect(readFileSync(join(root, name), 'utf8')).toBe(input); + } + expect(checkSource('invalid.flow.yaml', 'steps: [').diagnostics.some(d => d.kind === 'editor_schema_missing')).toBe(true); +}); + +for (const [path, accepted] of [ + ['repo', true], ['/repo/src', true], ['pr://github/example', true], ['/', true], ['pr://', true], + ['', false], [' repo', false], ['repo ', false], ['repo//src', false], ['repo/../src', false], ['repo/.', false], [':/bad//path', false], +] as const) test(`canonical surface parity: ${JSON.stringify(path)}`, () => { + const value = { version: '0.1.0', steps: [{ id: 'one', type: 'agent', instruction: 'edit', surfaces: { workspace: [{ surface: path }], external: [path] } }] }; + expect(validate(value)).toBe(accepted); + if (accepted) expect(() => compileSpec(value)).not.toThrow(); + else expect(() => compileSpec(value)).toThrow(CompileError); +}); + +test('named declarations and selected input paths use authoring shapes', () => { + const value = { version: '0.1.0', agents: { reviewer: { cli: 'claude', model: 'review-model' } }, steps: [ + { id: 'source', type: 'llm', prompt: 'Return results', output: { type: 'object', properties: { rows: { type: 'array', items: { type: 'string' } } } } }, + { id: 'review', type: 'agent', agent: 'reviewer', instruction: 'Review the row', input: { row: { step: 'source', path: ['rows', 0] } } }, + ] }; + expect(validate(value)).toBe(true); + expect(() => compileSpec(value)).not.toThrow(); + for (const declaration of [{ cli: 'claude' }, { cli: 'claude', model: 'ok', persona: 'extra' }, { cli: ' claude', model: 'ok' }]) { + const invalid = { ...value, agents: { reviewer: declaration } }; + expect(validate(invalid)).toBe(false); + expect(() => compileSpec(invalid)).toThrow(CompileError); + } +}); diff --git a/packages/schema/tests/smoke.test.ts b/packages/schema/tests/smoke.test.ts new file mode 100644 index 000000000..ff6fcc657 --- /dev/null +++ b/packages/schema/tests/smoke.test.ts @@ -0,0 +1,63 @@ +import { expect, test } from 'bun:test'; +import { readFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const packageDirectory = fileURLToPath(new URL('..', import.meta.url)); +const schemaText = readFileSync(new URL('../flows.schema.json', import.meta.url), 'utf8'); +const schema = JSON.parse(schemaText); +const require = createRequire(new URL('../../sdk/package.json', import.meta.url)); +const ts = require('typescript'); + +test('all exported spec type nodes have documented definitions', () => { + const source = ts.createSourceFile('spec.ts', readFileSync(new URL('../../sdk/src/spec.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true); + for (const node of source.statements) { + if (!ts.isInterfaceDeclaration(node) && !ts.isTypeAliasDeclaration(node)) continue; + expect(schema.$defs[node.name.text], node.name.text).toBeDefined(); + } + function walk(node: any) { + if (!node || typeof node !== 'object') return; + expect(typeof node.title).toBe('string'); + expect(node.title.length).toBeGreaterThan(0); + expect(typeof node.description).toBe('string'); + expect(node.description.length).toBeGreaterThan(0); + if (node.$ref) { + expect(node.$ref.startsWith('#/')).toBe(true); + let value: any = schema; + for (const key of node.$ref.slice(2).split('/')) value = value?.[key.replace(/~1/g, '/').replace(/~0/g, '~')]; + expect(value, node.$ref).toBeDefined(); + } + for (const key of ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas']) Object.values(node[key] ?? {}).forEach(walk); + for (const key of ['items', 'additionalProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else']) walk(node[key]); + for (const key of ['oneOf', 'anyOf', 'allOf', 'prefixItems']) (node[key] ?? []).forEach(walk); + } + walk(schema); + expect(schema.$defs.StepSpec.oneOf.map((arm: any) => schema.$defs[arm.$ref.split('/').at(-1)].properties.type.const)).toEqual(['deterministic', 'llm', 'agent']); +}); + +test('regeneration is byte-stable and committed schema has not drifted', () => { + const directory = mkdtempSync(join(tmpdir(), 'relayflows-schema-regen-')); + try { + const first = join(directory, 'first.json'); + const second = join(directory, 'second.json'); + const script = fileURLToPath(new URL('../../../scripts/generate-json-schema.mjs', import.meta.url)); + execFileSync('node', [script, first]); + execFileSync('node', [script, second]); + expect(readFileSync(first, 'utf8')).toBe(readFileSync(second, 'utf8')); + expect(readFileSync(first, 'utf8')).toBe(schemaText); + } finally { rmSync(directory, { recursive: true, force: true }); } +}); + +test('npm tarball contains only data and documentation with no runtime dependencies', () => { + const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + expect(Object.keys(pkg.dependencies ?? {})).toEqual([]); + expect(Object.keys(pkg.optionalDependencies ?? {})).toEqual([]); + expect(Object.keys(pkg.peerDependencies ?? {})).toEqual([]); + expect(pkg.main).toBe('./flows.schema.json'); + expect(pkg.exports['.']).toBe('./flows.schema.json'); + const packed = JSON.parse(execFileSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: packageDirectory, encoding: 'utf8' })); + expect(packed[0].files.map((file: any) => file.path).sort()).toEqual(['LICENSE', 'README.md', 'THIRD_PARTY_LICENSES', 'flows.schema.json', 'package.json'].sort()); +}); diff --git a/packages/sdk/src/cli/check.ts b/packages/sdk/src/cli/check.ts index 0838146c9..eb4cc8934 100644 --- a/packages/sdk/src/cli/check.ts +++ b/packages/sdk/src/cli/check.ts @@ -17,7 +17,7 @@ import type { FlowSpec } from '../spec.js'; import type { McpServerConfig } from '../spec.js'; import { parseMcpConfig } from '../mcp-config.js'; import type { StepGateInspection } from '../gate-contract.js'; -import type { CheckFailureKind } from '../failure-kinds.js'; +import type { CheckFailureKind, CheckWarningKind } from '../failure-kinds.js'; import { preflight, CliProbeError, @@ -43,7 +43,13 @@ export interface CheckReport { projectConfigPath?: string; gates: StepGateInspection[]; resolutions: CliResolution[]; - diagnostics: Array; + diagnostics: Array; +} + +export interface CheckWarningDiagnostic { + severity: 'warning'; + kind: CheckWarningKind; + message: string; } export interface CheckInputDiagnostic { @@ -67,8 +73,25 @@ class CheckFailure extends Error { export function checkFlow(path: string): CheckExecution { const absolutePath = resolve(path); try { - const flow = readFlow(absolutePath); - return checkAuthoredFlow(flow, path); + const source = readFlowSource(absolutePath); + const hint = path.endsWith('.flow.yaml') && !/^\uFEFF?[ \t]*# yaml-language-server:/.test(source) + ? [{ severity: 'warning' as const, kind: 'editor_schema_missing' as const, + message: 'For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json' }] + : []; + let execution: CheckExecution; + try { + execution = checkAuthoredFlow(readFlow(source, absolutePath), path); + } catch (error) { + if (!(error instanceof CheckFailure)) throw error; + execution = { report: inputFailureReport(error, path) }; + } + // The editor-schema hint is a documentation nudge, emitted for every + // .flow.yaml without a first-line yaml-language-server comment. + // Firing it even when a refusal is present is deliberate: an editor + // showing squiggles on this file should still tell the author how to + // wire the schema, so the next edit gets real-time feedback. + execution.report.diagnostics.push(...hint); + return execution; } catch (error) { const failure = error instanceof CheckFailure ? error @@ -131,14 +154,15 @@ export function inputFailureReport( }; } -function readFlow(path: string): FlowSpec { - let source: string; +function readFlowSource(path: string): string { try { - source = readFileSync(path, 'utf8'); + return readFileSync(path, 'utf8'); } catch { throw new CheckFailure('input_unreadable', `Flow "${path}" is not readable.`); } +} +function readFlow(source: string, path: string): FlowSpec { let parsed: unknown; try { parsed = parseYaml(source); diff --git a/packages/sdk/src/cli/run.ts b/packages/sdk/src/cli/run.ts index e37276988..7e2c28e19 100644 --- a/packages/sdk/src/cli/run.ts +++ b/packages/sdk/src/cli/run.ts @@ -11,7 +11,6 @@ import { daemonRefusal } from './daemon-refusal.js'; import type { RunFailureKind, RunWarningKind } from '../failure-kinds.js'; import { JournalClient, JournalProtocolError } from '../journal-client.js'; import { attachLocalAgent } from '../local-agent.js'; -import type { PreflightDiagnostic } from '../preflight.js'; import type { RunCompletionReason, RunOutcome, @@ -20,7 +19,6 @@ import type { import type { StepType } from '../spec.js'; import { checkFlow, - type CheckInputDiagnostic, type CheckReport, } from './check.js'; @@ -51,7 +49,7 @@ export interface RunReport { parkedStep?: ParkedStep; projectConfigPath?: string; resolutions: CheckReport['resolutions']; - diagnostics: Array; + diagnostics: Array; } export interface RunExecution { diff --git a/packages/sdk/src/failure-kinds.ts b/packages/sdk/src/failure-kinds.ts index b16a99930..4cdafa202 100644 --- a/packages/sdk/src/failure-kinds.ts +++ b/packages/sdk/src/failure-kinds.ts @@ -90,6 +90,10 @@ export const RUN_WARNING_KINDS = [ 'connection_file_stale', ] as const; +/** File-level editor hints emitted by flows check, outside pure preflight. */ +export const CHECK_WARNING_KINDS = ['editor_schema_missing'] as const; +export type CheckWarningKind = (typeof CHECK_WARNING_KINDS)[number]; + export type PreflightFailureKind = (typeof PREFLIGHT_FAILURE_KINDS)[number]; export type CheckFailureKind = (typeof CHECK_FAILURE_KINDS)[number]; export type PreflightWarningKind = (typeof PREFLIGHT_WARNING_KINDS)[number]; diff --git a/packages/sdk/tests/cli.test.ts b/packages/sdk/tests/cli.test.ts index 52ba43cad..87389b040 100644 --- a/packages/sdk/tests/cli.test.ts +++ b/packages/sdk/tests/cli.test.ts @@ -465,6 +465,11 @@ steps: }); it('pins the complete JSON report for a pass and a refusal', async () => { + const editorHint = { + severity: 'warning', + kind: 'editor_schema_missing', + message: 'For editor validation, add this first line: # yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json', + }; const configPath = join(PREFLIGHT, 'flows.json'); const passPath = join(PREFLIGHT, 'cli-declared.flow.yaml'); const pass = await run(passPath, true); @@ -482,7 +487,7 @@ steps: replayable: true, }], resolutions: [{ stepId: 'answer', cli: './authenticated-cli', source: 'step' }], - diagnostics: [], + diagnostics: [editorHint], }); const refusalPath = join(PREFLIGHT, 'cli-missing.flow.yaml'); @@ -502,15 +507,19 @@ steps: replayable: true, }], resolutions: [{ stepId: 'answer', cli: './missing-cli', source: 'step' }], + // The editor-schema hint fires unconditionally on a `.flow.yaml` + // that lacks the first-line yaml-language-server comment. Emitting + // it alongside a refusal is deliberate — an editor showing squiggles + // on this file should still tell the author how to wire the schema. diagnostics: [{ severity: 'refusal', kind: 'cli_missing', stepId: 'answer', cli: './missing-cli', message: 'Step "answer" declares CLI "./missing-cli", but it does not resolve as an executable.', - }], + }, editorHint], }); - expect(report.diagnostics.every((entry) => isCheckFailureKind(entry.kind))).toBe(true); + expect(report.diagnostics.filter((entry) => entry.severity === 'refusal').every((entry) => isCheckFailureKind(entry.kind))).toBe(true); }); it('checks the compiled kernel-dialect canonical spec as well as YAML', async () => { diff --git a/scripts/generate-json-schema.mjs b/scripts/generate-json-schema.mjs new file mode 100644 index 000000000..2d99062d6 --- /dev/null +++ b/scripts/generate-json-schema.mjs @@ -0,0 +1,123 @@ +#!/usr/bin/env node +// Build-time only: use the SDK's existing TypeScript compiler, never load SDK code. +import { createRequire } from 'node:module'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { applyConstraints } from './schema-constraints.mjs'; +import { outputMetaschemas } from './schema-metaschemas.mjs'; + +const sdkRequire = createRequire(new URL('../packages/sdk/package.json', import.meta.url)); +const ts = sdkRequire('typescript'); +const source = ts.createSourceFile('spec.ts', readFileSync(new URL('../packages/sdk/src/spec.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true); +const declarations = new Map(source.statements + .filter(node => ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) + .map(node => [node.name.text, node])); +const outputSource = ts.createSourceFile('output-schema.ts', readFileSync(new URL('../packages/sdk/src/output-schema.ts', import.meta.url), 'utf8'), ts.ScriptTarget.Latest, true); +for (const node of outputSource.statements) { + if (ts.isTypeAliasDeclaration(node) && node.name.text === 'JsonOutputSchema') declarations.set(node.name.text, node); +} +const defs = {}; +const ref = name => ({ $ref: `#/$defs/${name}` }); +const docs = node => (node.jsDoc ?? []).map(doc => typeof doc.comment === 'string' ? doc.comment : '').filter(Boolean).join('\n\n'); +const describe = (schema, title, description) => ({ title, description: description || `${title} in the Relayflows spec.`, ...schema }); + +function object(members, base = { properties: {}, required: [] }) { + const properties = { ...base.properties }; + const required = new Set(base.required); + for (const member of members) { + if (!ts.isPropertySignature(member) || !member.type) throw new Error(`Unsupported member: ${member.getText()}`); + const name = member.name.text; + properties[name] = describe(type(member.type), name, docs(member)); + if (!member.questionToken) required.add(name); + } + return { type: 'object', properties, ...(required.size ? { required: [...required] } : {}), additionalProperties: false }; +} + +function definition(name) { + if (defs[name]) return defs[name]; + const node = declarations.get(name); + if (!node) throw new Error(`Unknown type ${name}`); + let schema; + if (ts.isInterfaceDeclaration(node)) { + let base = { properties: {}, required: [] }; + for (const clause of node.heritageClauses ?? []) { + for (const parent of clause.types) { + let inherited; + if (parent.expression.getText() === 'Omit') { + inherited = structuredClone(definition(parent.typeArguments[0].getText())); + const keys = parent.typeArguments[1]; + for (const key of ts.isUnionTypeNode(keys) ? keys.types : [keys]) { + delete inherited.properties[key.literal.text]; + inherited.required = (inherited.required ?? []).filter(name => name !== key.literal.text); + } + } else inherited = definition(parent.expression.getText()); + base = { properties: { ...base.properties, ...inherited.properties }, required: [...new Set([...base.required, ...(inherited.required ?? [])])] }; + } + } + schema = object(node.members, base); + } else schema = type(node.type); + defs[name] = describe(schema, name, docs(node)); + return defs[name]; +} + +function type(node) { + if (ts.isParenthesizedTypeNode(node)) return type(node.type); + if (ts.isArrayTypeNode(node)) return { type: 'array', items: type(node.elementType) }; + if (ts.isTypeLiteralNode(node)) return object(node.members); + if (ts.isLiteralTypeNode(node)) { + if (ts.isStringLiteral(node.literal)) return { type: 'string', const: node.literal.text }; + if (ts.isNumericLiteral(node.literal)) return { type: 'number', const: Number(node.literal.text) }; + throw new Error(`Unsupported literal ${node.getText()}`); + } + if (ts.isUnionTypeNode(node)) { + const variants = node.types.map(type); + if (variants.every(value => value.type === 'string' && 'const' in value)) return { type: 'string', enum: variants.map(value => value.const) }; + return { oneOf: variants }; + } + if (ts.isTypeReferenceNode(node)) { + const name = node.typeName.getText(); + if (name === 'Array') return { type: 'array', items: type(node.typeArguments[0]) }; + if (name === 'Record') { + if (node.typeArguments[0].kind !== ts.SyntaxKind.StringKeyword) throw new Error('Only string-keyed maps are supported'); + return { type: 'object', additionalProperties: type(node.typeArguments[1]) }; + } + definition(name); + return ref(name); + } + const primitives = { [ts.SyntaxKind.StringKeyword]: 'string', [ts.SyntaxKind.NumberKeyword]: 'number', [ts.SyntaxKind.BooleanKeyword]: 'boolean' }; + if (primitives[node.kind]) return { type: primitives[node.kind] }; + if (node.kind === ts.SyntaxKind.UnknownKeyword) return {}; + throw new Error(`Unsupported type ${node.getText()}`); +} + +for (const name of declarations.keys()) definition(name); +const versionNode = source.statements.filter(ts.isVariableStatement).flatMap(node => [...node.declarationList.declarations]).find(node => node.name.getText() === 'SPEC_SCHEMA_VERSION'); +if (!versionNode || !ts.isStringLiteral(versionNode.initializer)) throw new Error('Missing literal SPEC_SCHEMA_VERSION'); +const version = versionNode.initializer.text; +applyConstraints(defs, version); +Object.assign(defs, outputMetaschemas(sdkRequire)); +const schema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: `https://schema.relayflows.dev/v${version.split('.').slice(0, 2).join('.')}/flows.schema.json`, + title: 'Relayflow', + description: 'Relayflows YAML/JSON authoring spec. Run flows check for cross-step references, dependency cycles, schema-reference termination, and environment readiness.', + $ref: '#/$defs/FlowSpec', + $defs: defs, +}; + +// Annotate every schema node, including items, union arms and open maps. Do not +// walk examples/defaults as schemas: their values are author data. +function annotate(node, title) { + if (typeof node !== 'object' || node === null) return; + node.title ??= title; + node.description ??= node.$ref ? `See ${node.$ref.split('/').at(-1)}.` : `${title} value.`; + for (const key of ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas']) { + for (const [name, child] of Object.entries(node[key] ?? {})) annotate(child, name); + } + for (const key of ['items', 'contains', 'additionalProperties', 'propertyNames', 'not', 'if', 'then', 'else']) annotate(node[key], `${title} ${key}`); + for (const key of ['oneOf', 'anyOf', 'allOf', 'prefixItems']) (node[key] ?? []).forEach((child, index) => annotate(child, `${title} alternative ${index + 1}`)); +} +annotate(schema, 'Relayflow'); +const destination = process.argv[2] ?? fileURLToPath(new URL('../packages/schema/flows.schema.json', import.meta.url)); +writeFileSync(destination, `${JSON.stringify(schema, null, 2)}\n`); +console.log(`Generated packages/schema/flows.schema.json (${Object.keys(defs).length} definitions)`); diff --git a/scripts/schema-constraints.mjs b/scripts/schema-constraints.mjs new file mode 100644 index 000000000..7c4417566 --- /dev/null +++ b/scripts/schema-constraints.mjs @@ -0,0 +1,60 @@ +// Structural refinements enforced by sdk/src/validate.ts and input-binding.ts. +// Field sets and requiredness come only from the AST; these are value rules. +export function applyConstraints(defs, version) { + const property = (type, field, rule) => Object.assign(defs[type].properties[field], rule); + const nonempty = { minLength: 1 }; + const trimmed = { minLength: 1, pattern: '^\\S(?:[\\s\\S]*\\S)?$' }; + const positive = { type: 'integer', minimum: 1 }; + const safePositive = { ...positive, maximum: Number.MAX_SAFE_INTEGER }; + const integer = { type: 'integer', minimum: 0 }; + const decimal = { pattern: '^\\d+(\\.\\d+)?$' }; + const canonicalPath = { + minLength: 1, + // Optional / or nonempty scheme:// prefix; every component is nonempty, + // distinct from . and .., and the complete path is trimmed. + pattern: '^(?!\\s)(?![\\s\\S]*\\s$)(?:/|[^/]+://)?(?:(?!\\.{1,2}(?:/|$))[^/]+(?:/(?!\\.{1,2}(?:/|$))[^/]+)*)?$', + }; + property('FlowSpec', 'version', { const: version }); + property('FlowSpec', 'steps', { minItems: 1 }); + for (const type of ['FlowSpec', 'BaseStepSpec', 'DeterministicStepSpec', 'LlmStepSpec', 'AgentStepSpec', 'TriggerSpec', 'StreamSurface']) { + for (const field of ['name', 'cli', 'id', 'command', 'prompt', 'instruction', 'executor', 'stream', 'agent']) { + if (defs[type].properties[field]) property(type, field, nonempty); + } + } + for (const type of ['BaseStepSpec', 'DeterministicStepSpec', 'LlmStepSpec', 'AgentStepSpec']) { + property(type, 'maxIterations', positive); + Object.assign(defs[type].properties.dependsOn.items, nonempty); + property(type, 'input', { propertyNames: { type: 'string', pattern: '\\S' } }); + } + property('DeterministicStepSpec', 'timeoutMs', positive); + for (const field of ['maxTokensIn', 'maxTokensOut']) property('BudgetSpec', field, integer); + property('BudgetSpec', 'maxDollars', decimal); + property('MemorySpec', 'query', { pattern: '\\S' }); + // Memory adds safe-integer bounds to the otherwise shared budget shape. + property('MemorySpec', 'budget', { + type: 'object', properties: Object.fromEntries(['maxTokensIn', 'maxTokensOut'].map(key => [key, { type: 'integer', maximum: Number.MAX_SAFE_INTEGER }])), + }); + property('PlacementRequirements', 'expectedDurationMs', safePositive); + property('TriggerSpec', 'staleAfterMs', safePositive); + property('FlowSpec', 'agents', { propertyNames: { type: 'string', ...trimmed } }); + property('NamedAgentSpec', 'cli', trimmed); + for (const type of ['NamedAgentSpec', 'LlmStepSpec', 'AgentStepSpec']) { + property(type, 'model', { ...trimmed, allOf: [{ pattern: '^[^\\u0000-\\u001f\\u007f]*$' }] }); + } + property('OutputBinding', 'step', { pattern: '\\S' }); + Object.assign(defs.OutputBinding.properties.path.items.oneOf[1], { ...integer, maximum: Number.MAX_SAFE_INTEGER }); + property('WorkspaceSurface', 'surface', canonicalPath); + Object.assign(defs.AgentSurfaces.properties.external.items, canonicalPath); + for (const key of ['fileGlobs', 'networkAllowlist']) Object.assign(defs.PermissionsSpec.properties[key].items, nonempty); + property('OutputContainsGate', 'value', nonempty); + // Runtime accepts this legacy spelling although ExitCodeGate omits it. + defs.ExitCodeGate.properties.expect = { type: 'integer', const: 0, description: 'Legacy explicit success code. Only zero is supported.' }; + defs.JsonSchemaGate.properties.schema = { $ref: '#/$defs/OutputSchema', description: 'JSON Schema object or boolean; references and termination are checked by flows check.' }; + defs.JsonOutputSchema = { title: 'JsonOutputSchema', description: 'Structured output schema, compiled into json_schema verification.', type: 'object', allOf: [{ $ref: '#/$defs/OutputSchema' }] }; + for (const name of ['LlmStepSpec', 'AgentStepSpec']) defs[name].not = { required: ['output', 'verification'] }; + // SURFACE.md examples expressed in the current declarative dialect (the + // document's run/llm shorthand and identity header are future surface sugar). + defs.DeterministicStepSpec.examples = [{ id: 'diff', type: 'deterministic', command: 'git diff main' }]; + defs.LlmStepSpec.examples = [{ id: 'note', type: 'llm', prompt: 'One-line release note for the diff above.' }]; + defs.AgentStepSpec.examples = [{ id: 'review', type: 'agent', instruction: 'Review this diff for security issues.' }]; +} diff --git a/scripts/schema-metaschemas.mjs b/scripts/schema-metaschemas.mjs new file mode 100644 index 000000000..504c44cd1 --- /dev/null +++ b/scripts/schema-metaschemas.mjs @@ -0,0 +1,75 @@ +// Bundle the SDK's official Ajv meta-schemas as local 2020-12 definitions. +// This validates embedded schema keyword shapes offline. Reference resolution +// and termination of author-provided schemas remain the SDK/kernel's job. +export function outputMetaschemas(require) { + const drafts = [ + ['04', 'ajv-draft-04/dist/refs/json-schema-draft-04.json'], + ['06', 'ajv/dist/refs/json-schema-draft-06.json'], + ['07', 'ajv/dist/refs/json-schema-draft-07.json'], + ['2019', 'ajv/dist/refs/json-schema-2019-09/schema.json'], + ['2020', 'ajv/dist/refs/json-schema-2020-12/schema.json'], + ]; + const documents = new Map(); + const roots = new Map(); + const ids = new Map(); + for (const [draft, path] of drafts) { + const root = require(path); + const key = `Meta${draft}`; + const uri = (root.$id ?? root.id).replace(/#$/, ''); + documents.set(key, { root, base: uri, draft }); + roots.set(draft, key); + ids.set(uri, key); + if (draft === '2019' || draft === '2020') { + const vocabularies = Object.keys(root.$vocabulary).map(uri => uri.split('/').at(-1)); + for (const vocabulary of vocabularies) { + const meta = require(path.replace('schema.json', `meta/${vocabulary}.json`)); + const name = `${key}_${vocabulary}`; + documents.set(name, { root: meta, base: meta.$id, draft }); + ids.set(meta.$id, name); + } + } + } + function convert(value, base, draft) { + if (Array.isArray(value)) return value.map(child => convert(child, base, draft)); + if (value === null || typeof value !== 'object') return value; + const result = {}; + for (const [key, child] of Object.entries(value)) { + if (['$schema', '$id', 'id', '$vocabulary', '$anchor', '$dynamicAnchor', '$recursiveAnchor', 'format'].includes(key)) continue; + if (key === '$dynamicRef' || key === '$recursiveRef') { + result.$ref = `#/$defs/${roots.get(draft)}`; + } else if (key === '$ref') { + const target = new URL(child, base); + const fragment = target.hash; + target.hash = ''; + const name = ids.get(target.href); + if (!name) throw new Error(`Unbundled meta-schema reference ${target.href}`); + result.$ref = `#/$defs/${name}${fragment.startsWith('#/') ? fragment.slice(1) : ''}`; + } else if (key === 'dependencies') { + result.dependentRequired = child; + } else if (['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'].includes(key)) { + result[key] = Object.fromEntries(Object.entries(child).map(([name, schema]) => [name, convert(schema, base, draft)])); + } else if (['items', 'additionalItems', 'additionalProperties', 'contains', 'propertyNames', 'not', 'if', 'then', 'else', 'allOf', 'anyOf', 'oneOf', 'prefixItems'].includes(key)) { + result[key] = convert(child, base, draft); + } else result[key] = child; + } + // Draft-04's meta-schema itself uses boolean exclusiveMinimum. + if (result.exclusiveMinimum === true) { + result.exclusiveMinimum = result.minimum; + delete result.minimum; + } + return result; + } + const defs = Object.fromEntries([...documents].map(([key, { root, base, draft }]) => [key, convert(root, base, draft)])); + defs.OutputSchema = { + title: 'Output JSON Schema', + description: 'Schema declaration using SDK-supported drafts 04, 06, 07, 2019-09 or 2020-12 (default). Run flows check to resolve references and prove termination.', + type: ['object', 'boolean'], + properties: { $schema: { type: 'string', enum: drafts.flatMap(([draft]) => { const uri = documents.get(roots.get(draft)).base; return [uri, `${uri}#`]; }) } }, + allOf: drafts.map(([draft]) => { + const root = documents.get(roots.get(draft)).base; + const condition = { type: 'object', properties: { $schema: { enum: [root, `${root}#`] } }, required: ['$schema'] }; + return { if: draft === '2020' ? { anyOf: [condition, { not: { type: 'object', required: ['$schema'] } }] } : condition, then: { $ref: `#/$defs/${roots.get(draft)}` } }; + }), + }; + return defs; +} diff --git a/testdata/json-schema-invalid.flow.yaml b/testdata/json-schema-invalid.flow.yaml index 795bb92e4..b8d436d3a 100644 --- a/testdata/json-schema-invalid.flow.yaml +++ b/testdata/json-schema-invalid.flow.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json version: '0.1.0' steps: - id: schema