From d2bb652c80302e666dc23c24f3dd0655cdbc6faf Mon Sep 17 00:00:00 2001 From: "warp-factories[bot]" <243557089+warp-factories[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:36:53 +0000 Subject: [PATCH 1/2] spec: define list command behavior for issue 377 --- specs/GH377/product.md | 95 ++++++++++++++++++++++++++++++++ specs/GH377/tech.md | 120 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 specs/GH377/product.md create mode 100644 specs/GH377/tech.md diff --git a/specs/GH377/product.md b/specs/GH377/product.md new file mode 100644 index 00000000..872cf95a --- /dev/null +++ b/specs/GH377/product.md @@ -0,0 +1,95 @@ +# Product specification: `list` command with graceful empty-input handling + +## Background +Issue [#377](https://github.com/warpdotdev/command-signatures/issues/377) reported that `command-signatures list --file /tmp/empty.json` panicked on an empty file. That panic cannot be reproduced on this repository's `main` branch because the described command does not exist: this workspace provides the `warp-command-signatures` and `warp-completion-metadata` libraries, and its only binary is the PowerShell spec generator in `command-signatures/src/bin/autogenerate_powershell.rs`. + +The maintainer approved treating the report as an enhancement request. This specification defines the `list` capability the reporter expected, rather than a fix for an existing panic. + +## Motivation +Maintainers and tooling need a deterministic way to inspect the signatures available in this repository without writing a Rust consumer. The same capability should support validating and inspecting an external signatures document while treating an empty input as a valid, empty collection. User-controlled input must always return a deliberate result or error, never panic. + +## Goals +- Add a stable `command-signatures list` CLI surface. +- Use the repository's embedded completion specs by default. +- Allow `--file ` to list an external document that uses the existing Fig-compatible command schema. +- Define non-panicking behavior and exit codes for empty, missing, and malformed external input. +- Provide deterministic human-readable and machine-readable output. +- Keep source loading and summary generation reusable from Rust. + +## Non-goals +- Changing completion behavior or the existing JSON completion specs. +- Editing, generating, validating, or repairing signature files. +- Recursively listing every nested subcommand as its own row. +- Executing generators or shell commands referenced by a signature. +- Merging an external file with embedded assets; `--file` replaces the default source. +- Supporting YAML, TOML, JavaScript Fig specs, directories, URLs, or standard input. +- Replacing `autogenerate_powershell` or changing the PowerShell generation workflow. +- Publishing a versioned standalone package or defining distribution outside normal Cargo binary builds. + +## Proposed design + +### CLI and library surface +Add a binary target named `command-signatures` to the `warp-command-signatures` crate. Its first subcommand is: + +`command-signatures list [--file ] [--json]` + +The binary is intentionally thin. Source loading, conversion, sorting, and summary construction belong in a public library API so Rust callers and CLI tests exercise the same behavior. A subcommand-oriented CLI is preferred over a one-off `list-signatures` binary because it gives future repository utilities one coherent entry point. + +Use `clap` with its derive API. Although this adds a dependency to a workspace that currently has no argument parser, it provides conventional help, stable usage errors, and an extensible subcommand model without maintaining a custom parser. `clap`-reported usage errors exit with code 2. + +### Signature sources +Without `--file`, `list` summarizes the signatures returned by the existing embedded-assets path exposed through `commands()` in `command-signatures/src/lib.rs`. + +With `--file`, the file replaces the embedded source. The accepted UTF-8 JSON document forms are: +- One non-empty object matching `warp_completion_metadata::fig_types::Command`, the same schema used by each file under `command-signatures/json/`. +- An array of zero or more objects matching that schema, for callers that need one portable collection. +- The exact empty object `{}`, treated as an empty collection for graceful compatibility with common empty JSON documents. +- An empty or whitespace-only file, treated as an empty collection. + +Each command object's `name` field may retain its existing one-or-many representation. Conversion therefore may produce more than one listing row from one object. A non-empty object that does not satisfy the existing `Command` schema is malformed; the empty-object exception must not weaken validation of other objects. JSON `null`, scalar JSON values, and arrays containing non-command values are malformed. + +### Listing rows +One row represents one top-level `Signature` produced by the existing Fig `Command` conversion. It contains: +- `name`: the command name. +- `description`: the top-level description, or no value when absent. +- `subcommand_count`: the number of immediate subcommands. + +Rows are sorted case-insensitively by `name`, with the original name as a tie-breaker, so output is deterministic even though embedded loading is parallel. Nested subcommands are counted but not emitted as separate rows. Descriptions in text output replace tabs, carriage returns, and newlines with spaces so each signature occupies exactly one line. + +Default text output starts with the tab-separated header `NAME SUBCOMMANDS DESCRIPTION`, followed by one tab-separated row per signature. Missing descriptions render as an empty final field. + +`--json` writes a JSON array to standard output. Each element has exactly `name` (string), `description` (string or `null`), and `subcommand_count` (non-negative integer). JSON uses the same ordering as text output. No headings or status prose are mixed into JSON output. + +### No-results messaging +An empty result is successful. In text mode, standard output contains exactly `No signatures found.` followed by a newline. In JSON mode, standard output contains exactly `[]` followed by a newline. Standard error is empty in both cases. + +### Behavior matrix +| Input | Standard output | Standard error | Exit code | +| --- | --- | --- | --- | +| No `--file`; embedded signatures available | Sorted text table, or JSON array with `--json` | Empty | 0 | +| Empty or whitespace-only file | `No signatures found.` or `[]` | Empty | 0 | +| File containing `[]` | `No signatures found.` or `[]` | Empty | 0 | +| File containing `{}` | `No signatures found.` or `[]` | Empty | 0 | +| Valid command object or command array that converts to zero signatures, such as `{"name":[]}` | `No signatures found.` or `[]` | Empty | 0 | +| Valid command object or non-empty command array | Sorted text table, or JSON array with `--json` | Empty | 0 | +| Malformed JSON or schema-invalid non-empty JSON | Empty | `error: failed to parse signatures file '': ` plus newline | 1 | +| Missing path, directory path, permission failure, or other read error | Empty | `error: failed to read signatures file '': ` plus newline | 1 | +| Invalid CLI arguments or missing option value | Empty apart from any `clap` usage output | `clap` diagnostic and usage | 2 | + +`` should preserve the useful parser or operating-system explanation without echoing file contents. `` is the user-provided display path. + +## User-visible acceptance criteria +1. Running `command-signatures list` emits a deterministic list sourced from embedded assets and exits 0. +2. Every text row includes a command name, immediate subcommand count, and description field. +3. `command-signatures list --json` emits only a valid JSON array using the documented fields and ordering. +4. `--file` accepts one existing Fig-compatible command object or an array of such objects and does not merge them with embedded assets. +5. Empty bytes, whitespace-only bytes, `[]`, `{}`, and a valid document that converts to no names all produce the documented no-results output and exit 0. +6. A malformed or schema-invalid file writes a clear parse error to standard error, emits no standard output, and exits 1. +7. A nonexistent or unreadable path writes a clear read error to standard error, emits no standard output, and exits 1. +8. No external-file case panics or prints a Rust panic diagnostic. +9. Listing external data never runs referenced generators or other shell commands. +10. Existing embedded-signature invariants and the PowerShell generator continue to pass unchanged. + +## Open questions +- **External collection syntax:** This specification recommends accepting both a single existing `Command` object and an array of `Command` objects. Maintainers may choose to restrict the first release to one object per file, but doing so would make `[]` an error rather than the useful empty collection requested here. +- **Empty object compatibility:** This specification recommends treating only the exact empty object `{}` as no signatures found. Maintainers may instead classify it as schema-invalid for stricter consistency, but that would make two common representations of an empty JSON collection behave differently. diff --git a/specs/GH377/tech.md b/specs/GH377/tech.md new file mode 100644 index 00000000..281e86da --- /dev/null +++ b/specs/GH377/tech.md @@ -0,0 +1,120 @@ +# Technical specification: `list` command with graceful empty-input handling + +## Current architecture +- `command-signatures/src/lib.rs` exposes `signature_by_name()` and `commands()` when the default `embed-signatures` feature is enabled. +- `command-signatures/src/assets.rs` uses `rust-embed` to flatten hand-written and autogenerated JSON assets into one logical namespace. +- `completion-metadata/src/fig_types.rs` defines the deserializable Fig-compatible `Command` schema and converts one command into zero or more `Signature` values. +- `completion-metadata/src/signature.rs` defines `Signature`, including optional descriptions and accessors that return empty slices for absent subcommands. +- `command-signatures/src/bin/autogenerate_powershell.rs` is currently the workspace's only binary and is unrelated to interactive argument parsing. +- `command-signatures/Cargo.toml` already depends on `serde` and `serde_json`, but has no CLI framework. + +The existing `commands()` implementation parses embedded assets in parallel, silently skips an asset that cannot be read or deserialized, and returns an unspecified order. Existing tests compensate for embedded-data validity by asserting that every asset deserializes. + +## Implementation approach + +### Reusable listing module +Add a library module in the `warp-command-signatures` crate, such as `command-signatures/src/listing.rs`, and re-export its public API from `command-signatures/src/lib.rs`. + +The API should expose: +- A `SignatureSource` enum with `Embedded` and `File(PathBuf)` variants. +- A serializable `SignatureSummary` with `name: String`, `description: Option`, and `subcommand_count: usize`. +- A `list_signatures(source: SignatureSource) -> Result, ListSignaturesError>` function. +- Structured read and parse error variants that retain the display path and underlying error for the CLI without requiring callers to inspect strings. + +`SignatureSummary` construction uses `Signature::name()`, the public `description` field, and `Signature::subcommands().len()`. It must not call `dynamic_command_signature_data()` and must not execute any generator. + +All sources flow through one summary-and-sort function. Compare lowercase names first and original names second. Text-only control-character normalization belongs in the renderer rather than mutating the reusable summary. + +### Embedded source +For `SignatureSource::Embedded`, call `commands()` and summarize its returned `Signature` values. The API and binary require the `embed-signatures` feature. If maintainers need no-default-feature builds to compile the binary, declare the binary with `required-features = ["embed-signatures"]` rather than making an empty non-embedded list look successful. + +This feature gate is important because the non-embedded `signature_by_name()` behavior returns `None` and there is currently no non-embedded `commands()` implementation. + +### External source parser +Read the selected path without indexing into the resulting byte or value collection. + +1. If the bytes are empty or all JSON whitespace, return an empty vector. +2. Deserialize to `serde_json::Value` to distinguish document shapes. +3. Treat an object with no keys or an array with no elements as an empty vector. +4. Deserialize any other object as one `fig_types::Command`. +5. Deserialize any non-empty array as `Vec`. +6. Reject null, boolean, number, and string roots with a structured parse error. +7. Convert every `Command` with the existing `Vec::::from(command)` implementation, flatten the results, then summarize and sort. + +Parsing through the existing `Command` type preserves the repository's accepted field names and one-or-many handling. Do not deserialize external input directly into `Signature`; that internal representation is not the source JSON schema stored under `command-signatures/json/`. + +No code path should call `first().unwrap()`, index element zero, or otherwise assume at least one byte, JSON element, command name, converted signature, or summary. + +### Thin CLI +Add `clap` with the `derive` feature to `command-signatures/Cargo.toml` and add `command-signatures/src/bin/command-signatures.rs`. Model the root parser with a subcommand enum containing `List`; model `List` arguments with mutually compatible optional `--file ` and boolean `--json`. + +The CLI should: +- Select `Embedded` when `--file` is absent and `File` when present. +- Call the library API exactly once. +- Render summaries to standard output in text or JSON form. +- Render structured library errors to standard error with the exact prefixes in `product.md`. +- Return `ExitCode::SUCCESS` for populated and empty results, and exit code 1 for read/parse failures. +- Delegate syntax and usage failures to `clap`, which returns exit code 2. + +Serialize JSON output with `serde_json`; do not construct JSON manually. Write output through fallible I/O rather than `println!` chains where practical. A closed output pipe should terminate cleanly without a panic diagnostic. + +## Edge cases and trust boundaries +- External paths are untrusted. The command reads one local file only and performs no path traversal beyond normal operating-system path resolution. +- External descriptions may contain control characters. JSON escaping handles machine output; the text renderer replaces tabs and line breaks with spaces to preserve row boundaries. +- Duplicate names are retained as separate rows. Listing reflects the source and does not invent deduplication semantics. +- Multiple names on one Fig command become multiple rows because that is the existing `Command`-to-`Signature` conversion. +- Counts cover immediate subcommands only and use the post-conversion `Signature`, so inherited or transformed schema behavior is reflected consistently. +- Empty names in a non-empty document follow the existing schema conversion and may produce an empty result; they must not be indexed. +- `--file` is read-only. The implementation must not modify the selected file or load adjacent files. + +## Testing strategy + +### Library unit tests +Add focused tests beside the listing module for: +- Empty bytes and whitespace-only bytes returning an empty vector. +- `{}`, `[]`, and `{"name":[]}` returning an empty vector. +- A valid single command object producing the expected description and immediate subcommand count. +- A valid command array flattening all entries and names. +- Deterministic case-insensitive ordering with an original-name tie-breaker. +- Malformed JSON, scalar roots, schema-invalid non-empty objects, and invalid array members returning parse errors. +- Text normalization preserving one row per signature. +- JSON summaries serializing with exactly the documented field names and null handling. + +### CLI integration tests +Add process-level tests using Cargo's built binary and isolated temporary files. For each case, assert exit code, complete standard output, complete standard error, and that standard error does not contain `panicked at` or `index out of bounds`. + +Required fixtures and assertions: +- Empty file: text no-results message, empty standard error, exit 0. +- Whitespace-only file: same result. +- `[]`: text no-results message and JSON `[]` under `--json`, exit 0. +- `{}`: same result. +- `{"name":[]}`: same result. +- Malformed JSON: empty standard output, parse prefix on standard error, exit 1. +- Non-empty object without `name`: empty standard output, parse prefix, exit 1. +- Nonexistent path: empty standard output, read prefix on standard error, exit 1. +- Valid single object and valid array: exact sorted text rows and exact decoded JSON summaries, exit 0. +- Default embedded source: exit 0, a stable header in text mode, and a non-empty valid array in JSON mode. +- Unknown subcommand and missing `--file` value: `clap` usage error and exit 2. + +Tests should avoid asserting platform-specific operating-system error wording after the stable error prefix. + +### Existing regression suite +Run the full workspace tests. In particular, retain the `command-signatures/src/lib.rs` invariants that: +- Every referenced generator name resolves. +- Every embedded JSON spec deserializes. +- Generator commands contain no unsafe unquoted newlines. + +Also run formatting and lint checks for the workspace. No completion JSON fixture should change as part of implementation. + +## Expected implementation footprint +- `command-signatures/Cargo.toml`: add `clap`, binary metadata if a required feature is used, and any test-only temporary-file dependency. +- `command-signatures/src/lib.rs`: expose the listing API. +- `command-signatures/src/listing.rs`: source loading, parsing, summarization, sorting, errors, and unit tests. +- `command-signatures/src/bin/command-signatures.rs`: CLI parsing and rendering. +- A CLI integration test under `command-signatures/tests/`. + +No files under `command-signatures/json/`, `completion-metadata/`, or the PowerShell generator need functional changes. + +## Open questions +- Should the public source enum own a `PathBuf`, as recommended for a simple stable API, or borrow `&Path` to avoid a small allocation? This does not change CLI behavior. +- Should the implementation add a test-only crate such as `tempfile`/`assert_cmd`, or use only `std::process::Command` and uniquely named files under the process temporary directory? The recommendation is `tempfile` plus standard process APIs to keep isolation reliable while minimizing new dependencies. From 6bfa046da2a26c37039724702f58ab8d8aa07d10 Mon Sep 17 00:00:00 2001 From: "warp-factories[bot]" <243557089+warp-factories[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:45:41 +0000 Subject: [PATCH 2/2] spec: bound list input parsing --- specs/GH377/product.md | 23 ++++++++++++++++++----- specs/GH377/tech.md | 36 ++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/specs/GH377/product.md b/specs/GH377/product.md index 872cf95a..2715464e 100644 --- a/specs/GH377/product.md +++ b/specs/GH377/product.md @@ -46,8 +46,18 @@ With `--file`, the file replaces the embedded source. The accepted UTF-8 JSON do - The exact empty object `{}`, treated as an empty collection for graceful compatibility with common empty JSON documents. - An empty or whitespace-only file, treated as an empty collection. +This object-or-array grammar is the binding contract for the first release. Supporting one existing command object preserves compatibility with repository specs, while supporting arrays gives the `list` operation an explicit collection format and makes `[]` a natural empty input. The exact `{}` exception is also binding: it accommodates the empty object named in the enhancement without weakening validation for any non-empty object. + Each command object's `name` field may retain its existing one-or-many representation. Conversion therefore may produce more than one listing row from one object. A non-empty object that does not satisfy the existing `Command` schema is malformed; the empty-object exception must not weaken validation of other objects. JSON `null`, scalar JSON values, and arrays containing non-command values are malformed. +### External-input resource limits +External input is bounded before full JSON materialization: +- Maximum file size: 10 MiB (10,485,760 bytes). +- Maximum JSON container nesting depth: 64, where the root object or array has depth 1 and each nested object or array adds 1. +- Maximum number of command objects in a top-level array: 10,000. + +These limits are binding and apply before signature conversion. They are high enough for individual repository-format specs and intentionally bounded so a user-selected file cannot cause unbounded memory or stack consumption. A file that exceeds a limit is an input error: standard output is empty, the stable diagnostic below is written to standard error, and the process exits 1. + ### Listing rows One row represents one top-level `Signature` produced by the existing Fig `Command` conversion. It contains: - `name`: the command name. @@ -73,6 +83,9 @@ An empty result is successful. In text mode, standard output contains exactly `N | Valid command object or command array that converts to zero signatures, such as `{"name":[]}` | `No signatures found.` or `[]` | Empty | 0 | | Valid command object or non-empty command array | Sorted text table, or JSON array with `--json` | Empty | 0 | | Malformed JSON or schema-invalid non-empty JSON | Empty | `error: failed to parse signatures file '': ` plus newline | 1 | +| More than 10,485,760 input bytes | Empty | `error: signatures file '' exceeds maximum size of 10485760 bytes` plus newline | 1 | +| JSON container nesting exceeds depth 64 | Empty | `error: signatures file '' exceeds maximum JSON nesting depth of 64` plus newline | 1 | +| Top-level array contains more than 10,000 command objects | Empty | `error: signatures file '' contains more than 10000 commands` plus newline | 1 | | Missing path, directory path, permission failure, or other read error | Empty | `error: failed to read signatures file '': ` plus newline | 1 | | Invalid CLI arguments or missing option value | Empty apart from any `clap` usage output | `clap` diagnostic and usage | 2 | @@ -86,10 +99,10 @@ An empty result is successful. In text mode, standard output contains exactly `N 5. Empty bytes, whitespace-only bytes, `[]`, `{}`, and a valid document that converts to no names all produce the documented no-results output and exit 0. 6. A malformed or schema-invalid file writes a clear parse error to standard error, emits no standard output, and exits 1. 7. A nonexistent or unreadable path writes a clear read error to standard error, emits no standard output, and exits 1. -8. No external-file case panics or prints a Rust panic diagnostic. -9. Listing external data never runs referenced generators or other shell commands. -10. Existing embedded-signature invariants and the PowerShell generator continue to pass unchanged. +8. Files larger than 10 MiB, JSON deeper than 64 containers, and arrays containing more than 10,000 commands each emit their documented limit diagnostic, emit no standard output, and exit 1. +9. No external-file case, including a resource-limit violation, panics or prints a Rust panic diagnostic. +10. Listing external data never runs referenced generators or other shell commands. +11. Existing embedded-signature invariants and the PowerShell generator continue to pass unchanged. ## Open questions -- **External collection syntax:** This specification recommends accepting both a single existing `Command` object and an array of `Command` objects. Maintainers may choose to restrict the first release to one object per file, but doing so would make `[]` an error rather than the useful empty collection requested here. -- **Empty object compatibility:** This specification recommends treating only the exact empty object `{}` as no signatures found. Maintainers may instead classify it as schema-invalid for stricter consistency, but that would make two common representations of an empty JSON collection behave differently. +There are no unresolved user-visible contract questions. The object-or-array grammar, empty `{}` compatibility, resource limits, output, and exit behavior above are normative. Maintainers can override those decisions during spec review, but implementation should not begin against an ambiguous fork. diff --git a/specs/GH377/tech.md b/specs/GH377/tech.md index 281e86da..0cc9a85a 100644 --- a/specs/GH377/tech.md +++ b/specs/GH377/tech.md @@ -19,7 +19,7 @@ The API should expose: - A `SignatureSource` enum with `Embedded` and `File(PathBuf)` variants. - A serializable `SignatureSummary` with `name: String`, `description: Option`, and `subcommand_count: usize`. - A `list_signatures(source: SignatureSource) -> Result, ListSignaturesError>` function. -- Structured read and parse error variants that retain the display path and underlying error for the CLI without requiring callers to inspect strings. +- Structured read, parse, input-too-large, nesting-too-deep, and too-many-commands error variants that retain the display path and any underlying error for the CLI without requiring callers to inspect strings. `SignatureSummary` construction uses `Signature::name()`, the public `description` field, and `Signature::subcommands().len()`. It must not call `dynamic_command_signature_data()` and must not execute any generator. @@ -31,20 +31,25 @@ For `SignatureSource::Embedded`, call `commands()` and summarize its returned `S This feature gate is important because the non-embedded `signature_by_name()` behavior returns `None` and there is currently no non-embedded `commands()` implementation. ### External source parser -Read the selected path without indexing into the resulting byte or value collection. - -1. If the bytes are empty or all JSON whitespace, return an empty vector. -2. Deserialize to `serde_json::Value` to distinguish document shapes. -3. Treat an object with no keys or an array with no elements as an empty vector. -4. Deserialize any other object as one `fig_types::Command`. -5. Deserialize any non-empty array as `Vec`. -6. Reject null, boolean, number, and string roots with a structured parse error. -7. Convert every `Command` with the existing `Vec::::from(command)` implementation, flatten the results, then summarize and sort. +Define `MAX_EXTERNAL_FILE_BYTES` as 10,485,760, `MAX_JSON_NESTING_DEPTH` as 64, and `MAX_EXTERNAL_COMMANDS` as 10,000. Read and parse the selected path without indexing into the resulting byte or value collection. + +1. Open the path, then read through a bounded reader capped at `MAX_EXTERNAL_FILE_BYTES + 1`. File metadata may reject a known-oversized regular file early, but it must not be the only check because the file can change between metadata and read. +2. If more than `MAX_EXTERNAL_FILE_BYTES` bytes are observed, stop and return `InputTooLarge`. Never allocate based on untrusted file metadata. +3. If the bounded bytes are empty or all JSON whitespace, return an empty vector. +4. Scan the bounded bytes once with a string- and escape-aware structural scanner. Increment depth for `{` and `[` outside strings, decrement only a positive depth for `}` and `]`, and return `NestingTooDeep` as soon as depth would exceed `MAX_JSON_NESTING_DEPTH`. This preflight bounds nesting before recursive JSON materialization; normal JSON parsing remains responsible for mismatched or otherwise malformed structures. +5. Inspect the first non-whitespace byte to select the binding root grammar without first materializing a generic `serde_json::Value`. +6. For an object root, recognize an object containing only JSON whitespace as empty; otherwise deserialize directly to one `fig_types::Command`. +7. For an array root, deserialize through a custom Serde sequence visitor. Accumulate at most `MAX_EXTERNAL_COMMANDS` commands, probe once for an additional element, and immediately return `TooManyCommands` if a 10,001st element exists. Do not deserialize the entire array and count afterward. +8. Reject null, boolean, number, and string roots with a structured parse error. +9. Require the parser to consume the complete document so trailing non-whitespace bytes are malformed. +10. Convert every accepted `Command` with the existing `Vec::::from(command)` implementation, flatten the results, then summarize and sort. Parsing through the existing `Command` type preserves the repository's accepted field names and one-or-many handling. Do not deserialize external input directly into `Signature`; that internal representation is not the source JSON schema stored under `command-signatures/json/`. No code path should call `first().unwrap()`, index element zero, or otherwise assume at least one byte, JSON element, command name, converted signature, or summary. +The CLI maps `InputTooLarge`, `NestingTooDeep`, and `TooManyCommands` to the exact diagnostics in `product.md`, with empty standard output and exit code 1. These are deliberate input failures, not usage errors, and therefore use the same exit status as malformed input. The byte, depth, and command-count constants should be shared by parser logic, error formatting, and tests to prevent drift. + ### Thin CLI Add `clap` with the `derive` feature to `command-signatures/Cargo.toml` and add `command-signatures/src/bin/command-signatures.rs`. Model the root parser with a subcommand enum containing `List`; model `List` arguments with mutually compatible optional `--file ` and boolean `--json`. @@ -66,6 +71,9 @@ Serialize JSON output with `serde_json`; do not construct JSON manually. Write o - Counts cover immediate subcommands only and use the post-conversion `Signature`, so inherited or transformed schema behavior is reflected consistently. - Empty names in a non-empty document follow the existing schema conversion and may produce an empty result; they must not be indexed. - `--file` is read-only. The implementation must not modify the selected file or load adjacent files. +- Oversized whitespace-only or otherwise valid JSON is still rejected by the byte limit before empty-input or schema handling. +- A depth-limit violation discovered before a later syntax error reports the depth-limit diagnostic. Inputs within depth 64 continue to receive normal schema or syntax diagnostics. +- JSON delimiters inside strings, including escaped quotes and backslashes, do not contribute to the nesting count. ## Testing strategy @@ -77,6 +85,9 @@ Add focused tests beside the listing module for: - A valid command array flattening all entries and names. - Deterministic case-insensitive ordering with an original-name tie-breaker. - Malformed JSON, scalar roots, schema-invalid non-empty objects, and invalid array members returning parse errors. +- Exactly 10,485,760 bytes being accepted for parsing and 10,485,761 bytes returning `InputTooLarge` without reading or retaining additional data. +- JSON at exactly depth 64 reaching normal parsing and depth 65 returning `NestingTooDeep`, including fixtures with delimiter characters inside escaped strings to verify the scanner does not over-count. +- A top-level array of exactly 10,000 valid commands being accepted and 10,001 entries returning `TooManyCommands`. - Text normalization preserving one row per signature. - JSON summaries serializing with exactly the documented field names and null handling. @@ -92,6 +103,9 @@ Required fixtures and assertions: - Malformed JSON: empty standard output, parse prefix on standard error, exit 1. - Non-empty object without `name`: empty standard output, parse prefix, exit 1. - Nonexistent path: empty standard output, read prefix on standard error, exit 1. +- Oversized file: empty standard output, exact maximum-size diagnostic, exit 1, and no panic text. +- Depth-65 JSON: empty standard output, exact maximum-depth diagnostic, exit 1, and no panic text. +- Array with 10,001 command objects: empty standard output, exact maximum-command-count diagnostic, exit 1, and no panic text. - Valid single object and valid array: exact sorted text rows and exact decoded JSON summaries, exit 0. - Default embedded source: exit 0, a stable header in text mode, and a non-empty valid array in JSON mode. - Unknown subcommand and missing `--file` value: `clap` usage error and exit 2. @@ -118,3 +132,5 @@ No files under `command-signatures/json/`, `completion-metadata/`, or the PowerS ## Open questions - Should the public source enum own a `PathBuf`, as recommended for a simple stable API, or borrow `&Path` to avoid a small allocation? This does not change CLI behavior. - Should the implementation add a test-only crate such as `tempfile`/`assert_cmd`, or use only `std::process::Command` and uniquely named files under the process temporary directory? The recommendation is `tempfile` plus standard process APIs to keep isolation reliable while minimizing new dependencies. + +Neither question changes the normative external-file grammar, resource limits, output, or exit behavior in `product.md`.