Files
pad/schema/cmdhelp.schema.json
T
xarmian 1e3991c865 feat(schema): publish cmdhelp.schema.json v0.1 (TASK-932) (#325)
* feat(schema): publish cmdhelp.schema.json v0.1 (TASK-932)

Adds the formal JSON Schema (draft 2020-12) describing cmdhelp v0.1's
`--format json` wire format, plus a schema/README.md documenting intended
use for CLI authors and consumer wrappers.

Per IDEA-927 the schema enforces the v0.1 contracts:
- Required top-level: cmdhelp_version, binary, commands.
- cmdhelp_version pattern is MAJOR.MINOR (no PATCH); '0.1.0' invalid.
- Argument types: closed set {string,int,float,bool,enum,path,url,
  duration,date,datetime,json,ref} plus x-* extension namespace.
- Boolean flag arity (§5.3): negate_flag only valid when type=bool.
- exit_codes union (§5.2): each entry is string OR object{when,recovery,
  message_template}; object form requires `when`. Codes must be numeric.
- Dynamic enums (§7): enum_source pattern is `^dynamic:.+$`.
- additionalProperties: true at extension points for forward-compat (§9).

Validated locally with python jsonschema: 8 tests (1 valid sample,
5 negative cases, 2 x-*/dynamic positive cases) all pass.

Unblocks TASK-934 (JSON emitter), TASK-938 (test suite validates against
this schema), and TASK-940 (publish on getpad.dev).

Parent: PLAN-930.

* fix(schema): enforce flag-name pattern + clarify test-suite wording per Codex review (round 1)

Two findings from Codex round 1 on PR #325:

1. flagMap accepted invalid keys ('--verbose', empty string, digit-leading)
   despite README documenting "without leading --". Added propertyNames
   pattern `^[a-zA-Z][a-zA-Z0-9_-]*$` so producers can't ship malformed
   flag names that consumers would parse incorrectly.

2. schema/README.md described `internal/cmdhelp/` as if it existed and
   already validated in `go test ./...`. The package ships in TASK-934
   (and the validation test in TASK-938). Reworded as future/planned.

Verification:
- Schema syntax: still valid Draft 2020-12.
- Original 8 tests still pass.
- 3 new negative tests for the propertyNames pattern: '--verbose',
  empty string, and digit-leading keys all correctly rejected.
- Single-letter short-flag keys like `h` still accepted.
- `make check` clean.

Note for next review round: Codex's first run reported a `httptest`
panic in `cmd/pad` tests; that's a sandbox networking constraint
(read-only mode can't bind sockets), not a regression — verified
locally with `go test ./cmd/pad/ -run TestEnsureWorkspaceSlugAttachExisting -count=1` (passes).
2026-05-01 00:30:53 -04:00

272 lines
11 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://getpad.dev/cmdhelp.schema.json",
"title": "cmdhelp v0.1",
"description": "JSON Schema for output of `<cmd> help --format json` per the cmdhelp v0.1 spec. Tools advertising cmdhelp/0.1 via their capability bit emit documents matching this schema. Consumers (LLM harnesses, MCP servers, IDE plugins, wrappers) validate against this schema before parsing. See IDEA-927 for the full spec.",
"type": "object",
"required": ["cmdhelp_version", "binary", "commands"],
"additionalProperties": true,
"properties": {
"cmdhelp_version": {
"description": "Wire format version. MAJOR.MINOR only — never includes a PATCH component. See spec §9.",
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+$"
},
"binary": {
"description": "The CLI's binary name as invoked on the command line (e.g. \"pad\", \"git\", \"kubectl\").",
"type": "string",
"minLength": 1
},
"version": {
"description": "The implementation's own software version (independent from cmdhelp_version). Free-form; semver recommended.",
"type": "string"
},
"summary": {
"description": "One-line description of the binary, suitable for an LLM context blurb.",
"type": "string"
},
"homepage": {
"description": "Optional canonical URL for the project.",
"type": "string",
"format": "uri"
},
"global_flags": {
"description": "Flags that apply to every command in the tree (e.g. --workspace, --format). Same shape as a per-command `flags` object.",
"$ref": "#/$defs/flagMap"
},
"commands": {
"description": "Map of command paths (space-joined, e.g. \"item create\") to their definitions.",
"type": "object",
"additionalProperties": { "$ref": "#/$defs/command" },
"minProperties": 0
},
"schemas": {
"description": "Reusable JSON Schema fragments referenced by per-command stdout definitions via `json_schema_ref` (e.g. \"#/schemas/Item\").",
"type": "object",
"additionalProperties": true
},
"context": {
"description": "Optional dynamic context spliced into help output by CLIs with session state. See spec §7.",
"type": "object",
"additionalProperties": true,
"properties": {
"workspace": { "type": "string" },
"profile": { "type": "string" },
"auth": { "type": "string" }
}
}
},
"$defs": {
"command": {
"description": "Definition of a single command in the CLI.",
"type": "object",
"required": ["summary"],
"additionalProperties": true,
"properties": {
"summary": {
"description": "One-line description of the command.",
"type": "string",
"minLength": 1
},
"description": {
"description": "Optional longer prose explanation, may include markdown.",
"type": "string"
},
"args": {
"description": "Positional arguments in order.",
"type": "array",
"items": { "$ref": "#/$defs/arg" }
},
"flags": {
"description": "Map of flag names (without leading dashes) to flag definitions.",
"$ref": "#/$defs/flagMap"
},
"stdin": {
"description": "Whether the command accepts stdin and the expected format.",
"type": "object",
"additionalProperties": false,
"properties": {
"accepted": { "type": "boolean" },
"format": { "type": "string", "description": "MIME-style hint such as text/markdown or application/json." }
},
"required": ["accepted"]
},
"stdout": {
"description": "What the command writes to stdout on success.",
"type": "object",
"additionalProperties": true,
"properties": {
"text_template": { "type": "string", "description": "Template string for the human-readable line; may use {placeholder} substitutions." },
"json_schema_ref": { "type": "string", "description": "JSON Pointer (typically into the top-level `schemas` map, e.g. \"#/schemas/Item\") describing the JSON shape printed under --format json." }
}
},
"exit_codes": {
"description": "Map of exit codes (as decimal strings) to their meaning. Each entry MAY be a terse string OR a rich object — consumers MUST handle both forms (spec §5.2).",
"type": "object",
"patternProperties": {
"^[0-9]+$": {
"oneOf": [
{ "type": "string", "minLength": 1 },
{ "$ref": "#/$defs/exitCodeRich" }
]
}
},
"additionalProperties": false
},
"examples": {
"description": "Canonical examples — single source of truth, rendered to both --format json and --format md (spec §6).",
"type": "array",
"items": { "$ref": "#/$defs/example" }
},
"see_also": {
"description": "Other command paths a reader might find relevant (e.g. [\"item update\", \"item delete\"]).",
"type": "array",
"items": { "type": "string" }
},
"since": {
"description": "Implementation version in which the command first appeared (independent of cmdhelp_version).",
"type": "string"
},
"stability": {
"description": "Stability promise. Free-form; common values: experimental, beta, stable, deprecated.",
"type": "string"
}
}
},
"arg": {
"description": "A positional argument.",
"type": "object",
"required": ["name", "type"],
"additionalProperties": true,
"properties": {
"name": { "type": "string", "minLength": 1 },
"type": { "$ref": "#/$defs/typeName" },
"required": { "type": "boolean" },
"description": { "type": "string" },
"default": { "description": "Default value if argument is omitted (when not required)." },
"format": { "type": "string", "description": "Free-form sub-type hint (e.g. \"key=value\")." },
"repeatable": { "type": "boolean" },
"enum": { "$ref": "#/$defs/enumValues" },
"enum_source": { "$ref": "#/$defs/enumSource" }
},
"allOf": [
{ "$ref": "#/$defs/enumConsistency" }
]
},
"flagMap": {
"description": "Map of flag names (without leading --) to flag definitions. Keys MUST match the flag-name pattern: start with a letter, followed by letters, digits, hyphens, or underscores. Long-form (e.g. `format`) and short-form (e.g. `h`) names are both valid; consumers add the leading `-` or `--` themselves at invocation time.",
"type": "object",
"propertyNames": {
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$"
},
"additionalProperties": { "$ref": "#/$defs/flag" }
},
"flag": {
"description": "A flag (option) on a command.",
"type": "object",
"required": ["type"],
"additionalProperties": true,
"properties": {
"type": { "$ref": "#/$defs/typeName" },
"required": { "type": "boolean" },
"description": { "type": "string" },
"default": { "description": "Default value if flag is omitted." },
"format": { "type": "string", "description": "Free-form sub-type hint (e.g. \"key=value\")." },
"repeatable": { "type": "boolean", "description": "True if the flag may appear more than once on a single invocation." },
"enum": { "$ref": "#/$defs/enumValues" },
"enum_source": { "$ref": "#/$defs/enumSource" },
"negate_flag": {
"description": "Optional negation form for a bool flag (e.g. \"--no-cache\"). Only meaningful when type=bool. See spec §5.3.",
"type": "string",
"pattern": "^--[a-zA-Z0-9][a-zA-Z0-9-]*$"
}
},
"allOf": [
{ "$ref": "#/$defs/enumConsistency" },
{
"description": "negate_flag only applies to bool-typed flags.",
"if": { "required": ["negate_flag"] },
"then": { "properties": { "type": { "const": "bool" } } }
}
]
},
"typeName": {
"description": "Argument/flag type. Closed set per spec §5.1, plus x-* extension namespace for tool-specific types.",
"anyOf": [
{
"type": "string",
"enum": [
"string", "int", "float", "bool", "enum",
"path", "url", "duration", "date", "datetime", "json", "ref"
]
},
{
"type": "string",
"pattern": "^x-[a-zA-Z0-9][a-zA-Z0-9_-]*$",
"description": "Tool-specific extension type. Wrappers MAY treat unknown x-* types as `string`."
}
]
},
"enumValues": {
"description": "Concrete list of allowed values when type=enum (or as a snapshot for dynamic enums).",
"type": "array",
"items": {
"type": ["string", "number", "boolean"]
},
"minItems": 0
},
"enumSource": {
"description": "Reference to a command that resolves the enum dynamically at help-emission time (e.g. \"dynamic:pad collection list\"). The literal `enum` array, when present alongside, is a snapshot.",
"type": "string",
"pattern": "^dynamic:.+$"
},
"enumConsistency": {
"description": "If `enum` or `enum_source` is set, the type SHOULD be `enum`. Encoded as a soft constraint via if/then so this remains permissive for tools that prefer to type the slot more specifically.",
"if": {
"anyOf": [
{ "required": ["enum"] },
{ "required": ["enum_source"] }
]
},
"then": {
"properties": {
"type": { "type": "string" }
}
}
},
"exitCodeRich": {
"description": "Rich object form of an exit-code entry. The string form is also valid (see exit_codes patternProperties).",
"type": "object",
"required": ["when"],
"additionalProperties": true,
"properties": {
"when": { "type": "string", "minLength": 1, "description": "Concise description of the condition that produces this exit code." },
"recovery": { "type": "string", "description": "Suggested user/agent action to recover from this exit code." },
"message_template": { "type": "string", "description": "Template for the stderr message printed alongside the exit; may use {placeholder} substitutions." }
}
},
"example": {
"description": "A canonical example invocation. Same source for --format json and --format md (spec §6).",
"type": "object",
"required": ["cmd"],
"additionalProperties": true,
"properties": {
"cmd": { "type": "string", "minLength": 1, "description": "The runnable invocation. MUST resolve against the live command tree (validated in test suite per spec §6)." },
"note": { "type": "string", "description": "Short prose accompanying the invocation." }
}
}
}
}