From a55c8dc4caeb9f26f311048f779541ee5a9f075f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 19:20:09 +0000 Subject: [PATCH 01/35] sdg(liaison): update PHILOSOPHY.md --- specs/PHILOSOPHY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/PHILOSOPHY.md b/specs/PHILOSOPHY.md index 4630f00..2f437fe 100644 --- a/specs/PHILOSOPHY.md +++ b/specs/PHILOSOPHY.md @@ -14,4 +14,5 @@ IMPORTANT: This file may only be edited and interpreted by Liaison. Only Liaison - Operational and infrastructure setup work is not a patch in Developer's eyes: "this is not intended to be a patch, its just a one off set up task" (2026-07-28, correcting the npm-publishing work after triage drafted a Bug Report patch for it). Work whose substance is release/deploy/distribution machinery with zero product-behavior change routes as one-off release/devops execution under DEVOPS.md — not through the patch pipeline — and any patch artifacts created by such a misclassification are retired, not refined. Reserve the patch taxonomy for changes to specified product behavior (IP) or to the harness's ability to catch defects (Bug Report). - Developer prefers tokenless, workload-identity credentials over long-lived stored secrets for automation: asked "can we use OIDC?" (2026-07-28) immediately after receiving the NPM_TOKEN setup checklist, choosing npm Trusted Publishing over a stored automation token. General rule: when a platform offers an OIDC/trusted-publisher path for a credential the process manages, default to it — long-lived secrets are bootstrap-only fallbacks, and Developer checklists should not require creating or rotating a token that workload identity can replace. - xspec's consumption targets include coding-agent cloud environments — Developer asked how best to distribute the CLI into Claude Code web sessions (2026-07-29). Distribution is npm-only: `@modularcloud/xspec` on the public registry is the sole artifact channel; `vX.Y.Z` tags are release records and GitHub Releases carry no distribution artifacts. Consumption guidance (Liaison recommendation accepted as working default, 2026-07-29): per-repo devDependency + `npx xspec` preferred (rides the environment's normal dependency install, lockfile-pinned); `npx -y @modularcloud/xspec` for ad-hoc use; global install only where a bare `xspec` on PATH is explicitly wanted, via the environment's session-setup mechanism. A dependency-free compiled binary distributed via GitHub Releases would be new work, warranted only if a no-Node target ever matters. +- The xspec product boundary stays headless (2026-07-31): Developer plans an interactive UI on top of xspec — editing specs, visualizing requirement dependencies, seeing the nested structure inline with the MDX, jumping between references — but the UI itself is expected to live outside the xspec product ("won't necessarily be a part of the xspec spec itself"). xspec's role is to expose the foundational, machine-consumable APIs such an interface needs. When scoping UI-adjacent work: programmatic/observability surfaces belong in the product spec; rendering, editing chrome, and interaction design belong outside it. Developer routed this as a patch and asked the process to recommend the concrete changes — an open-ended seed that requests recommendations is a valid seed; the 2026-07-09 near-complete-draft pattern is Developer's habit, not a requirement. - Refinement loops that plateau are closed by valve ruling, not run to a spontaneous clean round (first applied 2026-07-10, TEST-SPEC.md at iteration 12 of the xspec initial build). Plateau markers: each fresh review yields only one or two genuine but ever-narrower findings, nothing is re-litigated or reversed, and the upstream documents are already converged. Closure shape: one final iteration whose Driver applies what is necessary and then HALTs, with escape hatches for blocking upstream problems or an indefensible late discovery; residual gaps are deliberately left to the downstream problems-file net, which finds them with implementation eyes when they actually matter. Basis: Developer's revealed preference for bounded forward progress over open-ended polishing (bare "continue" nudges, cost sensitivity shown by the 2026-07-09 credits outage, full delegation of process judgment). From e85f836de3b683c74ea17d9848b6f8147650eec1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 19:22:03 +0000 Subject: [PATCH 02/35] =?UTF-8?q?sdg(liaison):=20write=20SEED.md=20?= =?UTF-8?q?=E2=80=94=20foundational=20APIs=20for=20an=20external=20spec=20?= =?UTF-8?q?UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- specs/tmp/SEED.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 specs/tmp/SEED.md diff --git a/specs/tmp/SEED.md b/specs/tmp/SEED.md new file mode 100644 index 0000000..6a19351 --- /dev/null +++ b/specs/tmp/SEED.md @@ -0,0 +1,12 @@ +# Seed — Foundational APIs for an external spec UI + +Developer message (2026-07-31), verbatim: + +> I want to create a UI for xspec. The idea is that you can edit specs and visualize their dependencies, see the nested structure inline with the MDX and jump between references etc. This won't necessarily be a part of the xspec spec itself but xspec needs to have the foundational apis to connect to this interface. what changes do you recommend to put in a patch in order to work toward this goal? + +## Scope + +- Developer plans an interactive UI on top of xspec: editing specs, visualizing requirement dependencies, seeing the nested structure inline with the MDX, and jumping between references. +- The UI itself is expected to live outside the xspec product boundary — xspec stays headless. Building the UI is not part of this work. +- The work: xspec gains the foundational, machine-consumable surfaces that such an external interface needs to connect to it — the data behind dependency visualization, nested structure, and reference navigation, and whatever the product must expose for an external editor to work against it safely — as those needs map onto xspec's existing behavior. +- This is an open-ended, recommendation-seeking seed: Developer asks the process to determine and propose the concrete set of changes as a patch, coming back to Developer with questions wherever the right call depends on Developer intent. From 2cd687e52a11d8fee9573a29e1f47786bc760b7d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 19:33:18 +0000 Subject: [PATCH 03/35] sdg(phase-2): draft IP 0001-external-ui-apis (intent question pending) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 111 +++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 specs/patches/0001-external-ui-apis.md diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md new file mode 100644 index 0000000..7f5f7f5 --- /dev/null +++ b/specs/patches/0001-external-ui-apis.md @@ -0,0 +1,111 @@ +# 0001 — Foundational machine surfaces for an external spec UI + +- **Type:** Improvement Proposal (IP) +- **Stage:** Proposed +- **Branch:** `claude/xspec-ui-apis-4df8fa` (harness-designated for this session; stands in for `patch/external-ui-apis`) + +## Motivation + +Developer plans an interactive UI on top of xspec: editing spec documents, visualizing requirement dependencies, seeing the nested requirement structure inline with the MDX text, and jumping between references. The UI itself lives outside the xspec product boundary — xspec stays headless — but xspec must expose the machine-consumable surfaces such an external interface needs to connect to it safely. + +xspec's existing machine surface (`query`, universal `--json`, byte-deterministic output, requirement source ranges) covers set-level graph access well. It does not cover what an interactive editor additionally needs: exact source positions for every reference occurrence and for code, a single structural view of a document that maps onto its raw text, machine-readable knowledge of which files xspec owns, diagnostics precise enough to render inline, previews of identity-changing operations, and a way for an external tool to detect interface compatibility. This proposal adds those foundations. + +Archival — Developer message (2026-07-31), verbatim: + +> I want to create a UI for xspec. The idea is that you can edit specs and visualize their dependencies, see the nested structure inline with the MDX and jump between references etc. This won't necessarily be a part of the xspec spec itself but xspec needs to have the foundational apis to connect to this interface. what changes do you recommend to put in a patch in order to work toward this goal? + +## Scope + +The UI's needs map to product capabilities as follows: + +1. **Dependency visualization** — complete graph data. Largely present (`query nodes`, `query edges`, hashes, impact categories); gap: code-location endpoints are not locatable in their files. +2. **Nested structure inline with the MDX** — per-document structural data tied to exact byte positions in the source text. Partially present (per-node source ranges); gap: no single document view, and no positions for the constructs inside a node's text (imports, embeddings, dependency references). +3. **Jumping between references** — per-occurrence positions for every reference, in spec sources and TypeScript sources, navigable in both directions (occurrence → target, node → incoming occurrences). Absent: edges collapse to sets with no occurrence positions, and code locations carry no source range. +4. **Safe external editing** — the UI edits source text; xspec supplies the safety net: machine-readable validation with precise positions, previews of `rename`/`move`, and a machine-readable inventory of which files are sources, derived, or durable. Partially present. + +## Non-goals + +Confirmed with Developer at triage: + +- **No UI ships with xspec.** xspec remains headless; the complete interface remains the CLI, configuration, source syntax, generated modules, and workspace files (per GOALS). +- **No long-running service, watch, or push surface.** The connection point is the one-shot CLI: outputs are deterministic and reads are safe to run concurrently, so the UI re-invokes and re-queries as needed. A live surface, if the UI turns out to need one, is a separate future proposal (it would also touch the GOALS interface statement). +- **No structured content-mutation commands.** The UI owns text editing. xspec's only source-rewriting operations remain `rename` and `move` (extended here with previews); commands like "add dependency" or "set tags" are not added. +- **No analysis of unsaved editor content.** xspec reads the workspace as saved on disk; the UI validates on save. + +## Proposed `SPEC.md` changes + +The following describes the behaviors `SPEC.md` is to define, at the rigor `SPEC.md` requires (implementation-agnostic, blackbox-testable, deterministic, edge cases handled). Exact command and flag names, JSON field naming, and section placement are settled during spec refinement; the information contracts below are the requirement. + +### 1. Reference occurrences + +Introduce the concept of a **reference occurrence**: one textual spelling that records a dependency-kind edge — a `d` reference (each entry of a `d` array separately), an MDX `{text(...)}` embedding, a TypeScript `text(...)` call, or a TypeScript dependency marker. Each occurrence carries: the referencing file, its source range (byte offsets, per the existing range convention), its edge kind, its source graph node (requirement node or code location), and its resolved target's identity. + +- Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). +- Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, dynamic references — which are validation errors anyway). +- Occurrence data is unavailable for a file masked as unparseable, consistent with existing masking behavior. +- Ordering is deterministic: by file path (byte order), then by range start, then by range end, with a stated tiebreak for identical ranges. + +### 2. Source ranges for code + +Amend the source-range concept (currently: "code locations carry no source range"): + +- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. +- Everywhere the specification outputs a graph node with a source range, a code location now qualifies — in particular in occurrence enumeration (change 1), `query` results that return code locations, and review payloads that present code-location scopes. +- TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range. + +### 3. Whole-document structural view + +A query surface returns, for one spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: + +- the root node and the full section tree in document order — each node with identity, source range, tags, coverage attribute, and (on request) own and subtree text; +- every spec-module import declaration with its source range, binding name, and resolved target file; +- every reference occurrence in the file (change 1), positioned in document order; +- enough per-construct positional data that an external tool can resolve any byte position in the file to the innermost enclosing section and, when the position lies within a reference occurrence, to that occurrence and its target — without re-parsing the MDX. Whether position resolution is additionally offered as its own query (file + offset in, node/occurrence out) is a refinement decision; the resolution outcome itself is required. + +The view is defined for a discovered, parseable spec source; unknown files are usage errors and unparseable files report their validation errors, consistent with existing conventions. + +### 4. Workspace inventory + +A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: + +- the workspace root and the configuration file's path (workspace-relative, per existing path conventions); +- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, coverage profile names and definitions, policy rule names; +- every discovered source file with its group memberships; +- the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; +- the durable files: the journal path and existing review-session files. + +The inventory contains no environment-dependent content and no absolute paths, consistent with existing determinism and security conventions. + +### 5. Structured diagnostics + +Sharpen the validation-error contract so an external tool can render findings inline: + +- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. +- Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. +- The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. + +### 6. Refactoring previews + +`rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: + +- the complete identity mapping the operation would journal; +- every file the operation would rewrite, with the occurrences (ranges in current, pre-operation coordinates) it would rewrite in each, including import additions and removals in the move case and, in the file-move case, the file relocation itself; +- the derived files that would be regenerated as a consequence. + +A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. + +### 7. Machine-interface identification + +- A surface reports the product's version and a machine-interface version in JSON, so an external tool can detect compatibility before relying on output shapes. Output remains deterministic for a given product build. +- The specification states that the JSON document shapes of the machine-facing surfaces are part of the product's contract: shape changes are product behavior changes, not free implementation detail. + +## Existing surfaces relied on, unchanged + +Dependency visualization and change overlays already rest on: `query node`/`nodes`/`edges`/`subtree`/`ancestors`/`reachable`; `ids --tree`; `show`; the four hashes; `impact --json` (change categories, impacted code, witness paths); `coverage --json`; `review … --json` self-contained payloads; universal `--json` and exit-code conventions; write atomicity, mutating-command exclusivity, and read-time graph refresh. This proposal adds to that surface; it removes or alters none of it beyond the amendments stated above. + +## Compatibility and rigor notes + +- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths, the exit-code partition, and configuration-error precedence. +- New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. +- Range data added for code and occurrences follows the existing byte-offset range convention so consumers handle one range model everywhere. From df0171bd41435d0cb0e8adad98f8beebd58caaf2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 15:25:44 +0000 Subject: [PATCH 04/35] sdg(liaison): update PHILOSOPHY.md --- specs/PHILOSOPHY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/PHILOSOPHY.md b/specs/PHILOSOPHY.md index 2f437fe..4249021 100644 --- a/specs/PHILOSOPHY.md +++ b/specs/PHILOSOPHY.md @@ -15,4 +15,6 @@ IMPORTANT: This file may only be edited and interpreted by Liaison. Only Liaison - Developer prefers tokenless, workload-identity credentials over long-lived stored secrets for automation: asked "can we use OIDC?" (2026-07-28) immediately after receiving the NPM_TOKEN setup checklist, choosing npm Trusted Publishing over a stored automation token. General rule: when a platform offers an OIDC/trusted-publisher path for a credential the process manages, default to it — long-lived secrets are bootstrap-only fallbacks, and Developer checklists should not require creating or rotating a token that workload identity can replace. - xspec's consumption targets include coding-agent cloud environments — Developer asked how best to distribute the CLI into Claude Code web sessions (2026-07-29). Distribution is npm-only: `@modularcloud/xspec` on the public registry is the sole artifact channel; `vX.Y.Z` tags are release records and GitHub Releases carry no distribution artifacts. Consumption guidance (Liaison recommendation accepted as working default, 2026-07-29): per-repo devDependency + `npx xspec` preferred (rides the environment's normal dependency install, lockfile-pinned); `npx -y @modularcloud/xspec` for ad-hoc use; global install only where a bare `xspec` on PATH is explicitly wanted, via the environment's session-setup mechanism. A dependency-free compiled binary distributed via GitHub Releases would be new work, warranted only if a no-Node target ever matters. - The xspec product boundary stays headless (2026-07-31): Developer plans an interactive UI on top of xspec — editing specs, visualizing requirement dependencies, seeing the nested structure inline with the MDX, jumping between references — but the UI itself is expected to live outside the xspec product ("won't necessarily be a part of the xspec spec itself"). xspec's role is to expose the foundational, machine-consumable APIs such an interface needs. When scoping UI-adjacent work: programmatic/observability surfaces belong in the product spec; rendering, editing chrome, and interaction design belong outside it. Developer routed this as a patch and asked the process to recommend the concrete changes — an open-ended seed that requests recommendations is a valid seed; the 2026-07-09 near-complete-draft pattern is Developer's habit, not a requirement. +- UI-adjacent scope rulings, approved 2026-08-03 (single "That sounds great" to the grouped seven-surface proposal for the external-UI patch — the concise grouped-approval pattern again): (1) an external UI connects by invoking the `xspec` CLI per interaction; no persistent service, watch, or push surface without a fresh proposal (one would also touch GOALS' interface statement, an approval-gated edit); (2) the UI owns text editing — xspec supplies positions, structure, validation, and previews, and its only source-rewriting operations remain `rename`/`move`; structured content-mutation commands ("add dependency", "insert section") are deliberately absent; (3) xspec reads only saved files — unsaved-buffer diagnostics are at most a later addition. Treat these as standing defaults for future UI-adjacent scoping, not just this patch. +- modularcloud/cspec (the repo renamed 2026-07-27 to free the xspec name) contains Developer's earlier partial UI — the "cspec editor" — built on an outdated conception of xspec. Developer's standing filter (2026-08-03): it may be mined for individual good ideas ("see if there are any other good ideas that we should take from it") but is never authoritative and "we should not draw from this too much" — never import its architecture, data model, or naming; adopted ideas must stand on their own merits in current-xspec terms. - Refinement loops that plateau are closed by valve ruling, not run to a spontaneous clean round (first applied 2026-07-10, TEST-SPEC.md at iteration 12 of the xspec initial build). Plateau markers: each fresh review yields only one or two genuine but ever-narrower findings, nothing is re-litigated or reversed, and the upstream documents are already converged. Closure shape: one final iteration whose Driver applies what is necessary and then HALTs, with escape hatches for blocking upstream problems or an indefensible late discovery; residual gaps are deliberately left to the downstream problems-file net, which finds them with implementation eyes when they actually matter. Basis: Developer's revealed preference for bounded forward progress over open-ended polishing (bare "continue" nudges, cost sensitivity shown by the 2026-07-09 credits outage, full delegation of process judgment). From 85a9dd29866f0f82c6efc62503e2ebae5cd56b85 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 15:39:54 +0000 Subject: [PATCH 05/35] sdg(phase-2): finalize IP 0001 per Developer-confirmed scope; consume seed Triage: improvement (new machine-consumable surfaces require SPEC.md changes). Developer confirmed CLI-only connection, UI-owned text editing, and saved-files-only analysis; audit candidates folded in on their merits (parse-local availability, tag sub-ranges, multi-file document view, comment ranges, invocation-anchored root, direct position query). SEED.md consumed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 44 ++++++++++++++++++-------- specs/tmp/SEED.md | 12 ------- 2 files changed, 30 insertions(+), 26 deletions(-) delete mode 100644 specs/tmp/SEED.md diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 7f5f7f5..0b1ec48 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -19,7 +19,7 @@ Archival — Developer message (2026-07-31), verbatim: The UI's needs map to product capabilities as follows: 1. **Dependency visualization** — complete graph data. Largely present (`query nodes`, `query edges`, hashes, impact categories); gap: code-location endpoints are not locatable in their files. -2. **Nested structure inline with the MDX** — per-document structural data tied to exact byte positions in the source text. Partially present (per-node source ranges); gap: no single document view, and no positions for the constructs inside a node's text (imports, embeddings, dependency references). +2. **Nested structure inline with the MDX** — per-document structural data tied to exact byte positions in the source text. Partially present (per-node source ranges); gap: no single document view, no positions for the constructs inside a node's text (imports, embeddings, dependency references, comments), and no decomposition of a section's range into its tags. 3. **Jumping between references** — per-occurrence positions for every reference, in spec sources and TypeScript sources, navigable in both directions (occurrence → target, node → incoming occurrences). Absent: edges collapse to sets with no occurrence positions, and code locations carry no source range. 4. **Safe external editing** — the UI edits source text; xspec supplies the safety net: machine-readable validation with precise positions, previews of `rename`/`move`, and a machine-readable inventory of which files are sources, derived, or durable. Partially present. @@ -42,8 +42,8 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). -- Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, dynamic references — which are validation errors anyway). -- Occurrence data is unavailable for a file masked as unparseable, consistent with existing masking behavior. +- Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). +- Availability follows change 4: occurrence data is per-file and parse-local; only a masked (unparseable) file loses its occurrences. - Ordering is deterministic: by file path (byte order), then by range start, then by range end, with a stated tiebreak for identical ranges. ### 2. Source ranges for code @@ -56,36 +56,51 @@ Amend the source-range concept (currently: "code locations carry no source range ### 3. Whole-document structural view -A query surface returns, for one spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: +A query surface returns, for a spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: - the root node and the full section tree in document order — each node with identity, source range, tags, coverage attribute, and (on request) own and subtree text; +- for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; - every spec-module import declaration with its source range, binding name, and resolved target file; - every reference occurrence in the file (change 1), positioned in document order; -- enough per-construct positional data that an external tool can resolve any byte position in the file to the innermost enclosing section and, when the position lies within a reference occurrence, to that occurrence and its target — without re-parsing the MDX. Whether position resolution is additionally offered as its own query (file + offset in, node/occurrence out) is a refinement decision; the resolution outcome itself is required. +- every MDX comment's source range. With tags, imports, comments, and embedding occurrences located, every construct that Markdown compilation removes is positioned, so a consumer can classify each byte of the file as annotation or content without re-parsing the MDX; +- position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. -The view is defined for a discovered, parseable spec source; unknown files are usage errors and unparseable files report their validation errors, consistent with existing conventions. +The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. -### 4. Workspace inventory +### 4. Availability on imperfect workspaces + +The structural surfaces of changes 1 and 3 exist to serve an editor while a person is mid-edit — when transiently invalid states (an unknown reference target, a failing file elsewhere in the workspace) are the norm, and exactly when the existing read commands refuse to answer. Their availability is therefore defined per file, from parsing alone, not gated on workspace-wide validity: + +- Structure derived from one file's parse — the section tree, ranges and their decompositions, imports, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries resolution-level findings (unresolved references, cycle participation, and similar). +- Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. +- Data that only workspace-level resolution defines — resolved target identities, expanded own and subtree text, hashes — is reported as explicitly unavailable wherever it is undefined: deterministically, never silently omitted, never fabricated from partial resolution. +- A reference spelling that resolves to no target records no edge and therefore no occurrence; its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid. +- Findings present in the answered files are reported alongside the answer; how an answer-with-findings maps onto the existing exit-code partition is settled during spec refinement. + +Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. + +### 5. Workspace inventory A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: -- the workspace root and the configuration file's path (workspace-relative, per existing path conventions); +- how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; - the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, coverage profile names and definitions, policy rule names; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; - the durable files: the journal path and existing review-session files. -The inventory contains no environment-dependent content and no absolute paths, consistent with existing determinism and security conventions. +The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. It depends only on configuration and discovery, so it remains available whatever the sources' validity; configuration errors keep their existing precedence. -### 5. Structured diagnostics +### 6. Structured diagnostics Sharpen the validation-error contract so an external tool can render findings inline: - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. - Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. +- Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. -### 6. Refactoring previews +### 7. Refactoring previews `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: @@ -95,7 +110,7 @@ Sharpen the validation-error contract so an external tool can render findings in A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. -### 7. Machine-interface identification +### 8. Machine-interface identification - A surface reports the product's version and a machine-interface version in JSON, so an external tool can detect compatibility before relying on output shapes. Output remains deterministic for a given product build. - The specification states that the JSON document shapes of the machine-facing surfaces are part of the product's contract: shape changes are product behavior changes, not free implementation detail. @@ -106,6 +121,7 @@ Dependency visualization and change overlays already rest on: `query node`/`node ## Compatibility and rigor notes -- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths, the exit-code partition, and configuration-error precedence. +- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (change 5's invocation anchoring is the one stated exception, itself deterministic per invocation), the exit-code partition, and configuration-error precedence. +- The availability contract (change 4) is a deliberate, surface-scoped delta from the all-or-nothing read refusal of the existing commands, which keep their semantics unchanged; its refinement must stay deterministic and free of partial-resolution fabrication. - New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. -- Range data added for code and occurrences follows the existing byte-offset range convention so consumers handle one range model everywhere. +- Range data added for code, occurrences, tag decompositions, and comments follows the existing byte-offset range convention so consumers handle one range model everywhere. diff --git a/specs/tmp/SEED.md b/specs/tmp/SEED.md deleted file mode 100644 index 6a19351..0000000 --- a/specs/tmp/SEED.md +++ /dev/null @@ -1,12 +0,0 @@ -# Seed — Foundational APIs for an external spec UI - -Developer message (2026-07-31), verbatim: - -> I want to create a UI for xspec. The idea is that you can edit specs and visualize their dependencies, see the nested structure inline with the MDX and jump between references etc. This won't necessarily be a part of the xspec spec itself but xspec needs to have the foundational apis to connect to this interface. what changes do you recommend to put in a patch in order to work toward this goal? - -## Scope - -- Developer plans an interactive UI on top of xspec: editing specs, visualizing requirement dependencies, seeing the nested structure inline with the MDX, and jumping between references. -- The UI itself is expected to live outside the xspec product boundary — xspec stays headless. Building the UI is not part of this work. -- The work: xspec gains the foundational, machine-consumable surfaces that such an external interface needs to connect to it — the data behind dependency visualization, nested structure, and reference navigation, and whatever the product must expose for an external editor to work against it safely — as those needs map onto xspec's existing behavior. -- This is an open-ended, recommendation-seeking seed: Developer asks the process to determine and propose the concrete set of changes as a patch, coming back to Developer with questions wherever the right call depends on Developer intent. From 9b173b410dd5a59fffd96f14be0fd4546db6d89f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 15:59:18 +0000 Subject: [PATCH 06/35] =?UTF-8?q?sdg(phase-3):=20apply=20review=20round=20?= =?UTF-8?q?1=20to=20IP=200001=20=E2=80=94=20imperfect-workspace=20model,?= =?UTF-8?q?=20exit=20mapping,=20refresh,=20inventory=20provenance,=20previ?= =?UTF-8?q?ew=20edits,=20interface=20versioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: I1 (identity/interpreted-data model for parseable-but-invalid files; invalid imports listed with unavailable targets), I2 (unavailable-data list corrected — occurrence targets never unavailable, source identities and import targets can be; parse-local claim scoped to positions/spellings), I3 (exit mapping stated: findings or unavailable data in the answer -> 1, clean complete answer -> 0, usage/config errors keep 2; answer always emitted), I4 (new query surfaces join read-time refresh on valid workspaces, modify nothing on imperfect ones; inventory never refreshes or writes), I5 (inventory availability restated as parse-independence; content provenance split into invocation/config/discovery, recorded generation state, filesystem), I6 (preview edit classes enumerated: occurrence rewrites, id-attribute rewrites, import specifier/addition/removal edits, section-move deletion/insertion/ self-closing rewrite, file relocation; derived-file removals added), I7 (machine-interface version value stated in SPEC.md, surface reports exactly it — per-build observable; contract scope defined), O1 (per-unit ranges for multi-unit declarations), O2 (identical-range tiebreak removed as unreachable), O3 (byte-classification claim scoped; position-resolution totality and EOF/ beyond-EOF rules), O4 (policy rules reported at coverage-profile depth), O5 (cross-drive anchoring rule). Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 40 ++++++++++++++------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 0b1ec48..2ff6c75 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -43,14 +43,14 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). -- Availability follows change 4: occurrence data is per-file and parse-local; only a masked (unparseable) file loses its occurrences. -- Ordering is deterministic: by file path (byte order), then by range start, then by range end, with a stated tiebreak for identical ranges. +- Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. +- Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. ### 2. Source ranges for code Amend the source-range concept (currently: "code locations carry no source range"): -- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. +- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. - Everywhere the specification outputs a graph node with a source range, a code location now qualifies — in particular in occurrence enumeration (change 1), `query` results that return code locations, and review payloads that present code-location scopes. - TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range. @@ -58,12 +58,12 @@ Amend the source-range concept (currently: "code locations carry no source range A query surface returns, for a spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: -- the root node and the full section tree in document order — each node with identity, source range, tags, coverage attribute, and (on request) own and subtree text; +- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed, and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text; - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; -- every spec-module import declaration with its source range, binding name, and resolved target file; +- every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; -- every MDX comment's source range. With tags, imports, comments, and embedding occurrences located, every construct that Markdown compilation removes is positioned, so a consumer can classify each byte of the file as annotation or content without re-parsing the MDX; -- position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. +- every MDX comment's source range. With tags, imports, comments, and embedding occurrences located, every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; +- position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. @@ -71,25 +71,28 @@ The view is defined for discovered spec sources: one file, a set restricted by t The structural surfaces of changes 1 and 3 exist to serve an editor while a person is mid-edit — when transiently invalid states (an unknown reference target, a failing file elsewhere in the workspace) are the norm, and exactly when the existing read commands refuse to answer. Their availability is therefore defined per file, from parsing alone, not gated on workspace-wide validity: -- Structure derived from one file's parse — the section tree, ranges and their decompositions, imports, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries resolution-level findings (unresolved references, cycle participation, and similar). +- Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. -- Data that only workspace-level resolution defines — resolved target identities, expanded own and subtree text, hashes — is reported as explicitly unavailable wherever it is undefined: deterministically, never silently omitted, never fabricated from partial resolution. -- A reference spelling that resolves to no target records no edge and therefore no occurrence; its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid. -- Findings present in the answered files are reported alongside the answer; how an answer-with-findings maps onto the existing exit-code partition is settled during spec refinement. +- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source-node identity undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). +- Data these rules or workspace-level resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, expanded own and subtree text, hashes — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. +- A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid. +- Findings present in the answered files are reported alongside the answer, and the mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. -Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. +Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. Their relationship to stored graph data follows the same line: the query surfaces of changes 1 and 3 never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. ### 5. Workspace inventory A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: - how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; -- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, coverage profile names and definitions, policy rule names; +- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — the two reported at the same depth; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; - the durable files: the journal path and existing review-session files. -The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. It depends only on configuration and discovery, so it remains available whatever the sources' validity; configuration errors keep their existing precedence. +The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. + +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics @@ -105,15 +108,16 @@ Sharpen the validation-error contract so an external tool can render findings in `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: - the complete identity mapping the operation would journal; -- every file the operation would rewrite, with the occurrences (ranges in current, pre-operation coordinates) it would rewrite in each, including import additions and removals in the move case and, in the file-move case, the file relocation itself; -- the derived files that would be regenerated as a consequence. +- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion, its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself; +- the derived-file consequences, in both directions: the derived files that would be regenerated and the recorded derived files that would be removed as no longer generated — the old module path after a file move included. A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. ### 8. Machine-interface identification -- A surface reports the product's version and a machine-interface version in JSON, so an external tool can detect compatibility before relying on output shapes. Output remains deterministic for a given product build. -- The specification states that the JSON document shapes of the machine-facing surfaces are part of the product's contract: shape changes are product behavior changes, not free implementation detail. +- A surface reports the product's version and a machine-interface version in JSON. Output remains deterministic for a given product build: both values are fixed per build, and the product version identifies the build. +- The machine-interface version's current value is stated in `SPEC.md` itself, and the surface reports exactly the stated value — observable against the specification in any single build, with no cross-build comparison needed. +- The stated value names the machine-facing JSON contract `SPEC.md` defines: the JSON output of the product's commands under the existing universal-JSON and same-information conventions, the surfaces this proposal adds included. Because those contracts and the version value live in the same document, a change to the machine-facing JSON contract is by construction a specification change, and the proposal making it updates the stated value in the same change. An external tool detects incompatibility by comparing the reported value with the value its own interface knowledge was built against. ## Existing surfaces relied on, unchanged From 39007807e84d3651148c125564fe3fde4b0a1f8b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 16:17:01 +0000 Subject: [PATCH 07/35] =?UTF-8?q?sdg(phase-3):=20apply=20review=20round=20?= =?UTF-8?q?2=20to=20IP=200001=20=E2=80=94=20occurrence=20spans,=20exit-2?= =?UTF-8?q?=20JSON=20channel,=20expansion=20definedness,=20enumeration=20d?= =?UTF-8?q?omain,=20identification=20preconditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: I1, I2, I3, I4, I5, O1, O2, O3, O4, O5. Rejected: none. - I1: change 8 gains a workspace-independence bullet — consults no workspace or configuration, cannot fail for configuration reasons. - I2: resolved via the channel arm (bullet 2 of change 6 already committed the IP to machine-consumable configuration errors): exit-2 errors emit a JSON error document as the entire standard output, amending the empty-stdout rule. - I3: occurrence spans stated per kind; MDX embedding spans the full braced container, which change 3's byte-classification claim now cites. - I4: dropped "hashes" from the unavailable-data list (no surface this proposal adds reports hashes; naming one would add an unrequested capability) and added the exact expansion-definedness rule (every transitively reached embedding records an occurrence, no cycle re-entered). - I5: finding reporting defined over a consulted domain; a target-only enumeration's domain is the whole discovered set, so masked files always surface as findings with exit 1. - O1: joint-location claim scoped to parseable files. - O2: created-target-file previews report the creation as its own class with the insertion point at file start. - O3: inventory's review-session listing is name-based and content-blind, corrupt sessions included. - O4: structured-diagnostics contract extended to rename/move refusal reasons. - O5: product version phrased as informational; testable contract carried by the machine-interface version. - Compatibility note records the two convention amendments (exit-2 JSON form, change-8 precedence exception). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 2ff6c75..845c43b 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -41,7 +41,8 @@ The following describes the behaviors `SPEC.md` is to define, at the rigor `SPEC Introduce the concept of a **reference occurrence**: one textual spelling that records a dependency-kind edge — a `d` reference (each entry of a `d` array separately), an MDX `{text(...)}` embedding, a TypeScript `text(...)` call, or a TypeScript dependency marker. Each occurrence carries: the referencing file, its source range (byte offsets, per the existing range convention), its edge kind, its source graph node (requirement node or code location), and its resolved target's identity. - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. -- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). +- Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). - Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. @@ -62,7 +63,7 @@ A query surface returns, for a spec source file, everything needed to overlay st - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; - every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; -- every MDX comment's source range. With tags, imports, comments, and embedding occurrences located, every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; +- every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; - position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. @@ -74,9 +75,9 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. - What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source-node identity undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). -- Data these rules or workspace-level resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, expanded own and subtree text, hashes — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. -- A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid. -- Findings present in the answered files are reported alongside the answer, and the mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. +- Data these rules or resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. +- A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. +- Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. Their relationship to stored graph data follows the same line: the query surfaces of changes 1 and 3 never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. @@ -92,7 +93,7 @@ A query surface reports the machine-readable shape of the workspace, so an exter The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present, selected by name alone: every file directly under the review-session directory whose name is a well-formed session file name is listed, corrupt or unparseable sessions included, since the inventory reads no session content; a file there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics @@ -101,6 +102,8 @@ Sharpen the validation-error contract so an external tool can render findings in - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. - Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. +- The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns, so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. +- Machine-readable delivery is closed over the outcome classes: under the JSON output convention, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. - Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. ### 7. Refactoring previews @@ -108,14 +111,15 @@ Sharpen the validation-error contract so an external tool can render findings in `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: - the complete identity mapping the operation would journal; -- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion, its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself; +- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion, its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; - the derived-file consequences, in both directions: the derived files that would be regenerated and the recorded derived files that would be removed as no longer generated — the old module path after a file move included. A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. ### 8. Machine-interface identification -- A surface reports the product's version and a machine-interface version in JSON. Output remains deterministic for a given product build: both values are fixed per build, and the product version identifies the build. +- A surface reports the product's version and a machine-interface version in JSON. Output remains deterministic for a given product build: both values are fixed per build. The product version is informational — reported for display and support, with no requirement beyond per-build fixedness; the testable contract is carried by the machine-interface version below. +- The surface is workspace-independent: it consults no workspace and no configuration, answers identically in any working directory — no discoverable workspace, a missing configuration file, and an invalid one included — and cannot fail for workspace or configuration reasons; configuration-error precedence does not apply to it. An external tool's compatibility check is plausibly its first call, made before it can trust anything about the workspace, so nothing a workspace contains or lacks may block the answer. - The machine-interface version's current value is stated in `SPEC.md` itself, and the surface reports exactly the stated value — observable against the specification in any single build, with no cross-build comparison needed. - The stated value names the machine-facing JSON contract `SPEC.md` defines: the JSON output of the product's commands under the existing universal-JSON and same-information conventions, the surfaces this proposal adds included. Because those contracts and the version value live in the same document, a change to the machine-facing JSON contract is by construction a specification change, and the proposal making it updates the stated value in the same change. An external tool detects incompatibility by comparing the reported value with the value its own interface knowledge was built against. @@ -125,7 +129,7 @@ Dependency visualization and change overlays already rest on: `query node`/`node ## Compatibility and rigor notes -- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (change 5's invocation anchoring is the one stated exception, itself deterministic per invocation), the exit-code partition, and configuration-error precedence. +- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (change 5's invocation anchoring is the one stated exception, itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: under the JSON convention, exit-2 errors now emit a JSON error document (change 6), and configuration-error precedence does not reach the workspace-independent identification surface (change 8). - The availability contract (change 4) is a deliberate, surface-scoped delta from the all-or-nothing read refusal of the existing commands, which keep their semantics unchanged; its refinement must stay deterministic and free of partial-resolution fabrication. - New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. - Range data added for code, occurrences, tag decompositions, and comments follows the existing byte-offset range convention so consumers handle one range model everywhere. From b590c536aeeeb7db68acea3b4a08141ce9908267 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 16:37:27 +0000 Subject: [PATCH 08/35] sdg(phase-3): apply review round 3 to patch 0001 (external UI APIs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied all items; none rejected. - C1: widened change 4's view domain — when expanded text is requested, the domain gains every file the expansions transitively consult, so the finding blocking an expansion is always in-domain and the never-silent guarantee holds. - I1: change 1's target-node filter is now syntactic acceptance — a well-formed targetable identity selects (possibly zero) occurrences by resolved target; nonexistent, masked, identity-undefined, and unknown targets yield an empty answer with domain findings; only malformed spellings are usage errors; delta flagged in compatibility notes. - O1: default-export unit ranges (named construct vs. export declaration). - O2: graph-data area reported unconditionally as derived, xspec-owned. - O3: session listing selects directory entries by name whatever occupies the path. - O4: preview derived-file consequences defined as the identity-relevant delta, not the full regeneration set. - O5: preview/real-operation equivalence scoped to workspace state, not scheduling. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 845c43b..3e7135d 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -42,7 +42,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). -- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included). Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive: nothing references the identity. Only a malformed identity spelling is a usage error. This deliberately departs, for this surface, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). - Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. @@ -51,7 +51,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r Amend the source-range concept (currently: "code locations carry no source range"): -- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. +- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. - Everywhere the specification outputs a graph node with a source range, a code location now qualifies — in particular in occurrence enumeration (change 1), `query` results that return code locations, and review payloads that present code-location scopes. - TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range. @@ -77,7 +77,7 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source-node identity undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). - Data these rules or resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. -- Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. +- Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files and — when expanded text is requested — every further file those expansions consult (each embedded target's file the expansion transitively reaches), because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. Their relationship to stored graph data follows the same line: the query surfaces of changes 1 and 3 never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. @@ -89,11 +89,12 @@ A query surface reports the machine-readable shape of the workspace, so an exter - the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — the two reported at the same depth; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; +- the graph-data area: the location under which graph data is kept, reported unconditionally as a derived, xspec-owned area — the recorded derived-file map can lag or be empty, but the area is xspec's regardless, and an editor must know it before any build has run; - the durable files: the journal path and existing review-session files. The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present, selected by name alone: every file directly under the review-session directory whose name is a well-formed session file name is listed, corrupt or unparseable sessions included, since the inventory reads no session content; a file there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it (a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content); a path there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics @@ -112,9 +113,9 @@ Sharpen the validation-error contract so an external tool can render findings in - the complete identity mapping the operation would journal; - every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion, its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; -- the derived-file consequences, in both directions: the derived files that would be regenerated and the recorded derived files that would be removed as no longer generated — the old module path after a file move included. +- the derived-file consequences, in both directions, as the identity-relevant delta: the derived paths the operation would newly generate — paths where nothing is currently generated — and the recorded derived paths it would remove as no longer generated, the old module path after a file move included. The delta is the whole report: a successful operation finishes by regenerating every derived file, so the full regeneration set is workspace-constant, already named by the inventory of change 5, and carries no information about the operation — the delta is what the identity changes cause. -A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. +A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification — an equivalence over workspace state, validation and planning, not over scheduling: the refusal that meets a mutating command while another runs applies to the real operation only, never to its preview, which the concurrency rules class as non-mutating. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. ### 8. Machine-interface identification @@ -130,6 +131,6 @@ Dependency visualization and change overlays already rest on: `query node`/`node ## Compatibility and rigor notes - All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (change 5's invocation anchoring is the one stated exception, itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: under the JSON convention, exit-2 errors now emit a JSON error document (change 6), and configuration-error precedence does not reach the workspace-independent identification surface (change 8). -- The availability contract (change 4) is a deliberate, surface-scoped delta from the all-or-nothing read refusal of the existing commands, which keep their semantics unchanged; its refinement must stay deterministic and free of partial-resolution fabrication. +- The availability contract (change 4) and the syntactic target-filter acceptance of change 1 are deliberate, surface-scoped deltas from the existing commands' all-or-nothing read refusal and unknown-identity usage errors; the existing commands keep their semantics unchanged, and the contract's refinement must stay deterministic and free of partial-resolution fabrication. - New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. - Range data added for code, occurrences, tag decompositions, and comments follows the existing byte-offset range convention so consumers handle one range model everywhere. From 5bd9c08bfcfce6d64489c36a9d81a744009849bb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 16:57:07 +0000 Subject: [PATCH 09/35] =?UTF-8?q?sdg(phase-3):=20apply=20review=20round=20?= =?UTF-8?q?4=20to=20IP=200001=20=E2=80=94=20area/durable=20composition,=20?= =?UTF-8?q?corrupt-record=20outcome,=20file-filter=20semantics,=20availabi?= =?UTF-8?q?lity=20scoping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied all items; rejected none. - I1: graph-data area reported as owned with the durable paths carved out and taking precedence — never a blanket derived, deletable unit. - I2: recorded state present but unreadable (corrupt graph data) gets a defined outcome: recorded entries explicitly unavailable, corruption a reported finding, exit 1, full inventory still emitted. - I3: change 1 file filter pinned to the existing file-glob convention (set restriction, empty admission = empty exit-0 answer); the malformed-identity usage-error sentence scoped to the target filter. - I4: change 4 closing sentence scoped to changes 1 and 3, naming where changes 5/7/8 state their own availability. - O1: explicit TypeScript spans — text(...) occurrence = whole call expression; marker occurrence = bare chain, no statement terminator. - O2: "the two" replaced — profiles and rules alike fully expanded. - O3: multi-file view order pinned to byte order of workspace-relative path. - O4: journal occupancy datum added (absence = empty journal). - O5: origin-deletion class's single range covers the adjunct dropped-line bytes, contiguous by construction; no separate class. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 3e7135d..0e19ef1 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -42,7 +42,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). -- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive: nothing references the identity. Only a malformed identity spelling is a usage error. This deliberately departs, for this surface, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included: it is a set restriction over discovered files, not an existence assertion, so the enumeration's consulted domain is the discovered files it admits (change 4), a glob admitting none admits the empty set (an empty, finding-free answer, exit 0), and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive: nothing references the identity. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). - Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. @@ -53,7 +53,7 @@ Amend the source-range concept (currently: "code locations carry no source range - Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. - Everywhere the specification outputs a graph node with a source range, a code location now qualifies — in particular in occurrence enumeration (change 1), `query` results that return code locations, and review payloads that present code-location scopes. -- TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range. +- TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range — exact per form, matching the explicitness of the `d` and MDX cases: a `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included — the expression that records the edge — and a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. ### 3. Whole-document structural view @@ -66,7 +66,7 @@ A query surface returns, for a spec source file, everything needed to overlay st - every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; - position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. -The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. +The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. ### 4. Availability on imperfect workspaces @@ -79,22 +79,22 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files and — when expanded text is requested — every further file those expansions consult (each embedded target's file the expansion transitively reaches), because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. -Existing commands keep their current all-or-nothing read semantics; this availability contract governs the surfaces this proposal adds. Their relationship to stored graph data follows the same line: the query surfaces of changes 1 and 3 never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. +Existing commands keep their current all-or-nothing read semantics; this availability contract governs the structural surfaces of changes 1 and 3 — the other added surfaces state their own: the inventory of change 5 is unconditional on its own terms, the previews of change 7 refuse exactly when the real operation would, and the identification surface of change 8 is workspace-independent. The relationship of changes 1 and 3 to stored graph data follows the same line: these query surfaces never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. ### 5. Workspace inventory A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: - how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; -- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — the two reported at the same depth; +- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — profiles and rules alike fully expanded, never name-only; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; -- the graph-data area: the location under which graph data is kept, reported unconditionally as a derived, xspec-owned area — the recorded derived-file map can lag or be empty, but the area is xspec's regardless, and an editor must know it before any build has run; -- the durable files: the journal path and existing review-session files. +- the graph-data area: the location under which graph data is kept, reported unconditionally as an xspec-owned area — the recorded derived-file map can lag or be empty, but the area is xspec's regardless, and an editor must know it before any build has run. The durable files live under this same location, so the area's classification composes with theirs by carve-out, and the report states the composition in its own terms: every path under the area is xspec's; the durable paths reported below are durable and take precedence over the area's classification; only the remainder — the graph data itself — is derived, rebuild-recoverable content. The area is never presented as a deletable or wholesale-regenerable unit, precisely because the durable files inside it are neither; +- the durable files: the journal path with whether anything presently occupies it — an absent journal is an empty journal, per existing journal semantics, and this datum surfaces that — and existing review-session files. The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run; the filesystem supplies the durable entries — the journal path is fixed, and the review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it (a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content); a path there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome: the recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map, the corruption accompanies the answer as a reported finding, and the invocation exits 1 under the existing partition — the full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries — the journal path is fixed and its occupancy datum is presence alone, whatever kind of filesystem object occupies it (the inventory reads no journal content), and the review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it (a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content); a path there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics @@ -112,7 +112,7 @@ Sharpen the validation-error contract so an external tool can render findings in `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: - the complete identity mapping the operation would journal; -- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion, its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; +- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of a line the existing line-drop rule additionally drops (bytes contiguous with the construct by construction), so the adjunct drop lies inside this class's range rather than forming a class of its own — its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; - the derived-file consequences, in both directions, as the identity-relevant delta: the derived paths the operation would newly generate — paths where nothing is currently generated — and the recorded derived paths it would remove as no longer generated, the old module path after a file move included. The delta is the whole report: a successful operation finishes by regenerating every derived file, so the full regeneration set is workspace-constant, already named by the inventory of change 5, and carries no information about the operation — the delta is what the identity changes cause. A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification — an equivalence over workspace state, validation and planning, not over scheduling: the refusal that meets a mutating command while another runs applies to the real operation only, never to its preview, which the concurrency rules class as non-mutating. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. From b9136123ce31f1da8830092845f745997198f9c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 17:17:14 +0000 Subject: [PATCH 10/35] sdg(phase-3): one ownership rule for graph-area paths; scope occurrence-absence guarantee MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 5 of patch refinement for 0001-external-ui-apis (IP). Applied: - C1: replaced the graph-data area's contradictory per-occupant ownership composition with one rule stated once in the area entry: the area is a write reservation; reported durable and recorded derived paths are classified as reported; every other path under the area is unattributed — never listed, never claimed, never presented as rebuild-recoverable. The availability paragraph's review-session sentence now defers to that rule instead of asserting opposite ownership. - I1: the empty-finding-free-answer guarantee of change 1 is now scoped to the consulted domain, absolute exactly when no file filter narrows it. - O1: change 4's resolution-failure exemplars now include the unique-bearer-with-undefined-identity case change 1 cites. - O2: change 6 defines the concerned path for missing configuration with no --config: the search-origin directory (invocation working directory), invocation input like change 5's anchoring; compatibility note updated so the invocation-anchored exception covers both changes. - O3: change 5's configuration view names its shape: profiles and rules carried with complete definitions; group references stay names resolved against the view's own group list, never glob expansions. - O4: split the change-1 file-filter sentence and the change-5 provenance/availability sentences into separable statements, all clauses preserved. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 0e19ef1..2c1c78b 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -42,7 +42,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). -- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included: it is a set restriction over discovered files, not an existence assertion, so the enumeration's consulted domain is the discovered files it admits (change 4), a glob admitting none admits the empty set (an empty, finding-free answer, exit 0), and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive: nothing references the identity. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included. It is a set restriction over discovered files, not an existence assertion: the enumeration's consulted domain is the discovered files it admits (change 4); a glob admitting none admits the empty set (an empty, finding-free answer, exit 0); and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive over the consulted domain: nothing in the files the enumeration consulted references the identity. When no file filter narrows the domain, the domain is the entire discovered set and the guarantee is absolute — nothing in the workspace references the identity; under a narrowing file filter it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). - Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. @@ -76,7 +76,7 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. - What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source-node identity undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). - Data these rules or resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. -- A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target, or an ambiguous one whose candidates' own identities are undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. +- A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files and — when expanded text is requested — every further file those expansions consult (each embedded target's file the expansion transitively reaches), because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. Existing commands keep their current all-or-nothing read semantics; this availability contract governs the structural surfaces of changes 1 and 3 — the other added surfaces state their own: the inventory of change 5 is unconditional on its own terms, the previews of change 7 refuse exactly when the real operation would, and the identification surface of change 8 is workspace-independent. The relationship of changes 1 and 3 to stored graph data follows the same line: these query surfaces never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. @@ -86,22 +86,22 @@ Existing commands keep their current all-or-nothing read semantics; this availab A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: - how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; -- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — profiles and rules alike fully expanded, never name-only; +- the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — every profile and rule carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, never its glob expansion: the name resolves against the group list this same view reports, so nothing is lost when two groups share one definition; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; -- the graph-data area: the location under which graph data is kept, reported unconditionally as an xspec-owned area — the recorded derived-file map can lag or be empty, but the area is xspec's regardless, and an editor must know it before any build has run. The durable files live under this same location, so the area's classification composes with theirs by carve-out, and the report states the composition in its own terms: every path under the area is xspec's; the durable paths reported below are durable and take precedence over the area's classification; only the remainder — the graph data itself — is derived, rebuild-recoverable content. The area is never presented as a deletable or wholesale-regenerable unit, precisely because the durable files inside it are neither; +- the graph-data area: the location under which graph data is kept, reported unconditionally — the recorded derived-file map can lag or be empty, but an editor must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — per the existing derived-file rules, a derived-file write there replaces whatever occupies its path — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths reported below are durable, and a recorded derived path lying under the area is derived, like every recorded derived path. Every other path under the area falls under one rule, stated here once and holding everywhere in this inventory: it is unattributed. The graph data xspec keeps under the area is derived, rebuild-recoverable content, but its layout is deliberately not enumerated, so an unreported path may equally be xspec's graph data or foreign content: the inventory neither lists it, nor claims it for xspec, nor calls it rebuild-recoverable — rebuilding regenerates derived files and removes recorded ones no longer generated, but neither reproduces nor removes a path of which no record exists, so deleting an unattributed path is undone by nothing. For the same reasons the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and an unattributed path is not recoverable at all; - the durable files: the journal path with whether anything presently occupies it — an absent journal is an empty journal, per existing journal semantics, and this datum surfaces that — and existing review-session files. The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is: invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules); recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded, which can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome: the recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map, the corruption accompanies the answer as a reported finding, and the invocation exits 1 under the existing partition — the full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries — the journal path is fixed and its occupancy datum is presence alone, whatever kind of filesystem object occupies it (the inventory reads no journal content), and the review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it (a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content); a path there with any other name is not xspec's and is not listed. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is. Invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules). Recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome. The recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map. The corruption accompanies the answer as a reported finding, and the invocation exits 1 under the existing partition. The full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries. The journal path is fixed, and its occupancy datum is presence alone, whatever kind of filesystem object occupies it — the inventory reads no journal content. The review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content. A directory entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the one rule the graph-data-area entry states. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics Sharpen the validation-error contract so an external tool can render findings inline: - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. -- Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. +- Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory: invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns, so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. - Machine-readable delivery is closed over the outcome classes: under the JSON output convention, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. @@ -130,7 +130,7 @@ Dependency visualization and change overlays already rest on: `query node`/`node ## Compatibility and rigor notes -- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (change 5's invocation anchoring is the one stated exception, itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: under the JSON convention, exit-2 errors now emit a JSON error document (change 6), and configuration-error precedence does not reach the workspace-independent identification surface (change 8). +- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (invocation-anchored content is the stated exception — change 5's anchoring and the configuration-error concerned paths of change 6 — itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: under the JSON convention, exit-2 errors now emit a JSON error document (change 6), and configuration-error precedence does not reach the workspace-independent identification surface (change 8). - The availability contract (change 4) and the syntactic target-filter acceptance of change 1 are deliberate, surface-scoped deltas from the existing commands' all-or-nothing read refusal and unknown-identity usage errors; the existing commands keep their semantics unchanged, and the contract's refinement must stay deterministic and free of partial-resolution fabrication. - New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. - Range data added for code, occurrences, tag decompositions, and comments follows the existing byte-offset range convention so consumers handle one range model everywhere. From 1c595e48f2cb330137147aad866a28114334be8b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 17:39:44 +0000 Subject: [PATCH 11/35] sdg(phase-3): pin code-range outputs; ground unattributed-path rule in unknowability (iter 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied I1: change 2's second bullet now names the exact outputs that gain code-location ranges — occurrence records present their source graph node as one datum (identity plus that node's own construct range; changes 1 and 4 adjusted to match), review payloads generalize the present-node range rule from requirement nodes to graph nodes, and query edge endpoints explicitly remain bare identities. Applied I2: the graph-data-area bullet no longer claims an unattributed path is irrecoverable (false for the graph-data subcase per SPEC 12.1/13.3); the undeletable rule now rests on the consumer's inability to tell regenerable graph data from foreign content. Applied O1: view's glob form states the empty-match outcome (empty set, finding-free, exit 0), mirroring change 1. Applied O2: anchoring bullet's "never as absolute paths" now carries the different-Windows-drives carve-out stated at the change's end. Applied O3: a root's tags/coverage attribute are defined structural absence, not change 4 unavailability — no finding, no exit-1 consequence. Applied O4: preview edits explicitly carry no replacement text; the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Applied O5: the exit-2 JSON error document's trigger is delimited — --json among the arguments, or a JSON-only surface with no flag needed; the compatibility note now points at that delimitation. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 2c1c78b..10137b4 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -38,13 +38,13 @@ The following describes the behaviors `SPEC.md` is to define, at the rigor `SPEC ### 1. Reference occurrences -Introduce the concept of a **reference occurrence**: one textual spelling that records a dependency-kind edge — a `d` reference (each entry of a `d` array separately), an MDX `{text(...)}` embedding, a TypeScript `text(...)` call, or a TypeScript dependency marker. Each occurrence carries: the referencing file, its source range (byte offsets, per the existing range convention), its edge kind, its source graph node (requirement node or code location), and its resolved target's identity. +Introduce the concept of a **reference occurrence**: one textual spelling that records a dependency-kind edge — a `d` reference (each entry of a `d` array separately), an MDX `{text(...)}` embedding, a TypeScript `text(...)` call, or a TypeScript dependency marker. Each occurrence carries: the referencing file, its source range (byte offsets, per the existing range convention), its edge kind, its source graph node, and its resolved target's identity. The source graph node (requirement node or code location) is one datum: the node's identity together with that node's own source range — a section's construct range or a root's whole-file range per the existing convention, a code location's range per change 2. - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). - A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included. It is a set restriction over discovered files, not an existence assertion: the enumeration's consulted domain is the discovered files it admits (change 4); a glob admitting none admits the empty set (an empty, finding-free answer, exit 0); and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive over the consulted domain: nothing in the files the enumeration consulted references the identity. When no file filter narrows the domain, the domain is the entire discovered set and the guarantee is absolute — nothing in the workspace references the identity; under a narrowing file filter it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). -- Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source-node identity explicitly unavailable. +- Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source graph node explicitly unavailable — identity and range withheld together, since the datum is the node; the enclosing construct's position stays on view through change 3. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. ### 2. Source ranges for code @@ -52,21 +52,21 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r Amend the source-range concept (currently: "code locations carry no source range"): - Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. -- Everywhere the specification outputs a graph node with a source range, a code location now qualifies — in particular in occurrence enumeration (change 1), `query` results that return code locations, and review payloads that present code-location scopes. +- Exactly two outputs gain code-location ranges, named here rather than left to a general rule. Occurrence enumeration: an occurrence record presents its source graph node with that node's own range (change 1) — the surface that makes every code unit's range reachable, since a code location enters the graph's edges only as a source and every edge from it is recorded by at least one occurrence. Review payloads: the present-node rule generalizes from requirement nodes to graph nodes, so a present code-location scope carries its source range exactly as a present requirement node does; an absent node still enters as identity and presence alone. No other output changes shape: edge endpoints in `query` results — `edges` rows, `reachable` witness paths, the per-node incoming and outgoing edge lists — remain bare identities, for code locations exactly as for requirement nodes today, and every other position presenting a graph node as a bare identity keeps that form. - TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range — exact per form, matching the explicitness of the `d` and MDX cases: a `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included — the expression that records the edge — and a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. ### 3. Whole-document structural view A query surface returns, for a spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: -- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed, and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text; +- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed, and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; - every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; - every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; - position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. -The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions. +The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions; the glob form is a set restriction exactly as in change 1 — a glob admitting no discovered file admits the empty set, an empty, finding-free answer, exit 0. ### 4. Availability on imperfect workspaces @@ -74,8 +74,8 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. -- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source-node identity undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). -- Data these rules or resolution leave undefined — section and occurrence-source identities as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. +- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). +- Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files and — when expanded text is requested — every further file those expansions consult (each embedded target's file the expansion transitively reaches), because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. @@ -85,11 +85,11 @@ Existing commands keep their current all-or-nothing read semantics; this availab A query surface reports the machine-readable shape of the workspace, so an external editor never edits files xspec owns and never misses files xspec reads: -- how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; +- how the resolved workspace root anchors to the invocation: the workspace root and the configuration file are identified relative to the invocation working directory — invocation input, exactly as existing conventions already treat `--config` resolution — and, outside the one platform case stated at the end of this change, never as absolute paths. Configuration discovery thereby has one authority: a tool invoking xspec from an arbitrary directory can map the workspace-relative paths in every output to real files without re-implementing the upward search, which is an editing-safety requirement — a consumer that guesses the root wrong edits the wrong files; - the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — every profile and rule carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, never its glob expansion: the name resolves against the group list this same view reports, so nothing is lost when two groups share one definition; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; -- the graph-data area: the location under which graph data is kept, reported unconditionally — the recorded derived-file map can lag or be empty, but an editor must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — per the existing derived-file rules, a derived-file write there replaces whatever occupies its path — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths reported below are durable, and a recorded derived path lying under the area is derived, like every recorded derived path. Every other path under the area falls under one rule, stated here once and holding everywhere in this inventory: it is unattributed. The graph data xspec keeps under the area is derived, rebuild-recoverable content, but its layout is deliberately not enumerated, so an unreported path may equally be xspec's graph data or foreign content: the inventory neither lists it, nor claims it for xspec, nor calls it rebuild-recoverable — rebuilding regenerates derived files and removes recorded ones no longer generated, but neither reproduces nor removes a path of which no record exists, so deleting an unattributed path is undone by nothing. For the same reasons the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and an unattributed path is not recoverable at all; +- the graph-data area: the location under which graph data is kept, reported unconditionally — the recorded derived-file map can lag or be empty, but an editor must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — per the existing derived-file rules, a derived-file write there replaces whatever occupies its path — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths reported below are durable, and a recorded derived path lying under the area is derived, like every recorded derived path. Every other path under the area falls under one rule, stated here once and holding everywhere in this inventory: it is unattributed. The graph data xspec keeps under the area is derived, rebuild-recoverable content, but its layout is deliberately not enumerated, so an unattributed path may equally be xspec's graph data or foreign content — and the two cases diverge exactly at deletion: deleted graph data returns with the next successful build or read-time refresh, per the existing rules, while foreign content is recorded nowhere, so nothing ever reproduces it. The inventory neither lists the path, nor claims it for xspec, nor says which case holds; telling them apart is precisely what it declines to enable, and the safety rule rests on that unknowability, not on any claim of irrecoverability: an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case — the one whose deletion is undone by nothing. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory; - the durable files: the journal path with whether anything presently occupies it — an absent journal is an empty journal, per existing journal semantics, and this datum surfaces that — and existing review-session files. The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. @@ -104,7 +104,7 @@ Sharpen the validation-error contract so an external tool can render findings in - Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory: invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns, so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. -- Machine-readable delivery is closed over the outcome classes: under the JSON output convention, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. +- Machine-readable delivery is closed over the outcome classes: whenever JSON output is in effect, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. JSON output is in effect in exactly two cases: `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or the invoked surface is JSON-only, a single JSON document its only output form with or without `--json`, as the existing single-document surfaces are and added surfaces may be defined; no flag need be present there. Outside these two cases the existing empty-standard-output rule stands. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. - Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. ### 7. Refactoring previews @@ -112,7 +112,7 @@ Sharpen the validation-error contract so an external tool can render findings in `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: - the complete identity mapping the operation would journal; -- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of a line the existing line-drop rule additionally drops (bytes contiguous with the construct by construction), so the adjunct drop lies inside this class's range rather than forming a class of its own — its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; +- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script for external application, which would bypass the journaled mapping. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of a line the existing line-drop rule additionally drops (bytes contiguous with the construct by construction), so the adjunct drop lies inside this class's range rather than forming a class of its own — its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; - the derived-file consequences, in both directions, as the identity-relevant delta: the derived paths the operation would newly generate — paths where nothing is currently generated — and the recorded derived paths it would remove as no longer generated, the old module path after a file move included. The delta is the whole report: a successful operation finishes by regenerating every derived file, so the full regeneration set is workspace-constant, already named by the inventory of change 5, and carries no information about the operation — the delta is what the identity changes cause. A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification — an equivalence over workspace state, validation and planning, not over scheduling: the refusal that meets a mutating command while another runs applies to the real operation only, never to its preview, which the concurrency rules class as non-mutating. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. @@ -130,7 +130,7 @@ Dependency visualization and change overlays already rest on: `query node`/`node ## Compatibility and rigor notes -- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (invocation-anchored content is the stated exception — change 5's anchoring and the configuration-error concerned paths of change 6 — itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: under the JSON convention, exit-2 errors now emit a JSON error document (change 6), and configuration-error precedence does not reach the workspace-independent identification surface (change 8). +- All additions obey the existing global conventions: single-JSON-document output, same-information JSON, byte-determinism, byte-wise ordering and comparison, workspace-relative paths (invocation-anchored content is the stated exception — change 5's anchoring and the configuration-error concerned paths of change 6 — itself deterministic per invocation), the exit-code partition, and configuration-error precedence — with the two amendments stated above: exit-2 errors now emit a JSON error document whenever JSON output is in effect, as change 6 delimits, and configuration-error precedence does not reach the workspace-independent identification surface (change 8). - The availability contract (change 4) and the syntactic target-filter acceptance of change 1 are deliberate, surface-scoped deltas from the existing commands' all-or-nothing read refusal and unknown-identity usage errors; the existing commands keep their semantics unchanged, and the contract's refinement must stay deterministic and free of partial-resolution fabrication. - New surfaces are reads (or, for previews, validated no-op plans); none introduces new durable state, none writes through any new path, and none weakens the security posture of test seams — exposed data is workspace-local content only. - Range data added for code, occurrences, tag decompositions, and comments follows the existing byte-offset range convention so consumers handle one range model everywhere. From 3643efad39d1a81e8b1c70bdf14c62394da7fad0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 18:00:08 +0000 Subject: [PATCH 12/35] sdg(phase-3): apply iteration-7 review to patch 0001 (diagnostics cardinality, view domain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: I1, I2, O1, O2, O3, O4. - I1: change 6 states the multi-construct location rule — one finding per condition instance carrying a location for every participating construct (duplicate-ID bearers, colliding import declarations, a cycle's full path), context entities as identity data; extended to refusal reasons (cycle a refused move would create). - I2: change 3 assigns the wrong-kind usage error to a discovered code source named directly and fixes the view glob's restriction universe as the discovered spec sources. - O1: change 2 scopes the absent-node clause to the range datum alone, leaving historical-text payload rules untouched. - O2: change 1 states that file and target filters combine conjunctively in one invocation. - O3: split the heaviest sentences in change 4's consulted-domain bullet and change 5's graph-data-area bullet. - O4: change 6 states the missing-configuration concerned path's reported form (change 5's anchoring; degenerate self-reference). Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 10137b4..678e662 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -42,7 +42,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r - Edges remain sets; occurrences are the positions behind them. Duplicate references that collapse to a single edge each remain distinct occurrences. - Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression (each entry of an array separately — never the array or the prop); an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces, on which change 3's byte classification depends; a TypeScript occurrence spans the referencing expression itself (change 2). -- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included. It is a set restriction over discovered files, not an existence assertion: the enumeration's consulted domain is the discovered files it admits (change 4); a glob admitting none admits the empty set (an empty, finding-free answer, exit 0); and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive over the consulted domain: nothing in the files the enumeration consulted references the identity. When no file filter narrows the domain, the domain is the entire discovered set and the guarantee is absolute — nothing in the workspace references the identity; under a narrowing file filter it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. +- A query surface enumerates occurrences, filterable at least by source file and by target node, so "find all references to this requirement, with positions" and "list every reference this file makes" are single calls. The two filters combine conjunctively in one invocation, per the existing filter-combination convention: the file filter fixes the enumeration's consulted domain (change 4), and the target filter selects within whatever domain is in effect — "who in these files references this node" is likewise a single call. The file filter follows the existing file-glob convention — the glob rules existing file filters use, their invalid-pattern usage errors included. It is a set restriction over discovered files, not an existence assertion: the enumeration's consulted domain is the discovered files it admits (change 4); a glob admitting none admits the empty set (an empty, finding-free answer, exit 0); and no unknown-file usage error exists on it. Incoming enumeration must accept any graph-node identity a reference can target (root nodes included), and acceptance is syntactic: a well-formed identity of a targetable kind is a valid filter value whatever the workspace currently contains. The filter selects the occurrences whose resolved target it names; when it names no currently resolvable node — its file not discovered, its file masked, its bearer's identity undefined (change 4), or no such node in a parseable file — the selection is empty, since a spelling that does not resolve records no occurrence, and the consulted domain's findings (change 4) accompany the empty answer. An empty, finding-free answer (exit 0) is therefore definitive over the consulted domain: nothing in the files the enumeration consulted references the identity. When no file filter narrows the domain, the domain is the entire discovered set and the guarantee is absolute — nothing in the workspace references the identity; under a narrowing file filter it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. On the target filter, only a malformed identity spelling is a usage error. This deliberately departs, for that filter, from the existing convention classing unknown node identities in arguments as usage errors: the mid-edit question "who references this section?" must answer, not refuse, exactly while the section's file is broken. Finding reporting and exit mapping cover the enumeration's whole consulted domain, defined in change 4. - Constructs that record no edge produce no occurrence (unused import bindings, type-only bindings, shadowed identifiers, and reference spellings that are dynamic or do not resolve — the invalid ones are located by diagnostics instead, change 6). - Availability follows change 4: occurrence positions and spellings are per-file, parse-local data — only a masked (unparseable) file loses them — while occurrence existence is resolution-dependent: a spelling that does not resolve to exactly one target records no edge and no occurrence. An occurrence inside a section whose identity change 4 leaves undefined keeps its position, kind, and resolved target, with its source graph node explicitly unavailable — identity and range withheld together, since the datum is the node; the enclosing construct's position stays on view through change 3. - Ordering is deterministic: by file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and this order is total; no further tiebreak exists. @@ -52,7 +52,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r Amend the source-range concept (currently: "code locations carry no source range"): - Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. -- Exactly two outputs gain code-location ranges, named here rather than left to a general rule. Occurrence enumeration: an occurrence record presents its source graph node with that node's own range (change 1) — the surface that makes every code unit's range reachable, since a code location enters the graph's edges only as a source and every edge from it is recorded by at least one occurrence. Review payloads: the present-node rule generalizes from requirement nodes to graph nodes, so a present code-location scope carries its source range exactly as a present requirement node does; an absent node still enters as identity and presence alone. No other output changes shape: edge endpoints in `query` results — `edges` rows, `reachable` witness paths, the per-node incoming and outgoing edge lists — remain bare identities, for code locations exactly as for requirement nodes today, and every other position presenting a graph node as a bare identity keeps that form. +- Exactly two outputs gain code-location ranges, named here rather than left to a general rule. Occurrence enumeration: an occurrence record presents its source graph node with that node's own range (change 1) — the surface that makes every code unit's range reachable, since a code location enters the graph's edges only as a source and every edge from it is recorded by at least one occurrence. Review payloads: the present-node range rule generalizes from requirement nodes to graph nodes, so a present code-location scope carries its source range exactly as a present requirement node does, and an absent node of either kind carries no range. The generalization touches the range datum alone; every other payload rule stands — the historical text an absent requirement node carries, and code locations' having no text value, included. No other output changes shape: edge endpoints in `query` results — `edges` rows, `reachable` witness paths, the per-node incoming and outgoing edge lists — remain bare identities, for code locations exactly as for requirement nodes today, and every other position presenting a graph node as a bare identity keeps that form. - TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range — exact per form, matching the explicitness of the `d` and MDX cases: a `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included — the expression that records the edge — and a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. ### 3. Whole-document structural view @@ -66,7 +66,7 @@ A query surface returns, for a spec source file, everything needed to overlay st - every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; - position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. -The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. A file named outside the discovered set is a usage error, per existing conventions; the glob form is a set restriction exactly as in change 1 — a glob admitting no discovered file admits the empty set, an empty, finding-free answer, exit 0. +The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. Naming a file directly asserts membership in the view's domain, and both failures of that assertion are usage errors: a file outside the discovered set, per existing conventions, and a discovered file the view is not defined for — a code source has no structural view, so naming one directly is the existing conventions' wrong-kind usage error, exactly as a code group's name is where a spec group's is required. The glob form is a set restriction exactly as in change 1, restricting over the view's domain — the discovered spec sources, not the whole discovered set: a glob admits the discovered spec sources it matches, and one admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. ### 4. Availability on imperfect workspaces @@ -77,7 +77,7 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). - Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. -- Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is: for the view of change 3, the requested files and — when expanded text is requested — every further file those expansions consult (each embedded target's file the expansion transitively reaches), because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named; for an occurrence enumeration of change 1, every discovered file its file filter admits — and, when no file filter narrows it, the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. +- Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is defined per surface. For the view of change 3 it is the requested files plus, when expanded text is requested, every further file those expansions consult — each embedded target's file the expansion transitively reaches — because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. For an occurrence enumeration of change 1 it is every discovered file the file filter admits; when no file filter narrows it, it is the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. Existing commands keep their current all-or-nothing read semantics; this availability contract governs the structural surfaces of changes 1 and 3 — the other added surfaces state their own: the inventory of change 5 is unconditional on its own terms, the previews of change 7 refuse exactly when the real operation would, and the identification surface of change 8 is workspace-independent. The relationship of changes 1 and 3 to stored graph data follows the same line: these query surfaces never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. @@ -89,7 +89,7 @@ A query surface reports the machine-readable shape of the workspace, so an exter - the resolved configuration view: spec and code groups with their glob lists and kinds, Markdown emission state and destinations, and coverage profiles and policy rules, each name with its full definition — every profile and rule carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, never its glob expansion: the name resolves against the group list this same view reports, so nothing is lost when two groups share one definition; - every discovered source file with its group memberships; - the derived-file map: per source file, the generated module and companion paths and the Markdown emit destination (when enabled), plus any other recorded derived paths; -- the graph-data area: the location under which graph data is kept, reported unconditionally — the recorded derived-file map can lag or be empty, but an editor must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — per the existing derived-file rules, a derived-file write there replaces whatever occupies its path — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths reported below are durable, and a recorded derived path lying under the area is derived, like every recorded derived path. Every other path under the area falls under one rule, stated here once and holding everywhere in this inventory: it is unattributed. The graph data xspec keeps under the area is derived, rebuild-recoverable content, but its layout is deliberately not enumerated, so an unattributed path may equally be xspec's graph data or foreign content — and the two cases diverge exactly at deletion: deleted graph data returns with the next successful build or read-time refresh, per the existing rules, while foreign content is recorded nowhere, so nothing ever reproduces it. The inventory neither lists the path, nor claims it for xspec, nor says which case holds; telling them apart is precisely what it declines to enable, and the safety rule rests on that unknowability, not on any claim of irrecoverability: an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case — the one whose deletion is undone by nothing. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory; +- the graph-data area: the location under which graph data is kept, reported unconditionally — the recorded derived-file map can lag or be empty, but an editor must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — per the existing derived-file rules, a derived-file write there replaces whatever occupies its path — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths reported below are durable, and a recorded derived path lying under the area is derived, like every recorded derived path. Every other path under the area falls under one rule, stated here once and holding everywhere in this inventory: it is unattributed. The graph data xspec keeps under the area is derived, rebuild-recoverable content, but its layout is deliberately not enumerated, so an unattributed path may equally be xspec's graph data or foreign content. The two cases diverge exactly at deletion: deleted graph data returns with the next successful build or read-time refresh, per the existing rules, while foreign content is recorded nowhere, so nothing ever reproduces it. The inventory neither lists the path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable. The safety rule rests on that unknowability, not on any claim of irrecoverability: an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case — the one whose deletion is undone by nothing. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory; - the durable files: the journal path with whether anything presently occupies it — an absent journal is an empty journal, per existing journal semantics, and this datum surfaces that — and existing review-session files. The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. @@ -101,9 +101,9 @@ Availability is unconditional: no part of the inventory requires parsing sources Sharpen the validation-error contract so an external tool can render findings inline: - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. -- Every error that locates inside a source file carries the file and a source range (byte offsets) for the offending construct, at the precision the condition allows; conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory: invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. +- Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. -- The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns, so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. +- The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. - Machine-readable delivery is closed over the outcome classes: whenever JSON output is in effect, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. JSON output is in effect in exactly two cases: `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or the invoked surface is JSON-only, a single JSON document its only output form with or without `--json`, as the existing single-document surfaces are and added surfaces may be defined; no flag need be present there. Outside these two cases the existing empty-standard-output rule stands. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. - Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. From b24d2aa323d2ccd2ea3fb2b437d9946cf970dd72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 18:18:16 +0000 Subject: [PATCH 13/35] sdg(phase-3): decide text-value and identity-uniqueness rules on imperfect files (iter 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - I1: change 4 now states the text-value principle for files with findings — Markdown compilation's removal rules classify constructs by syntactic form, never by validity or resolution (imports removed by form, tags removed with every spelled attribute, non-inventoried constructs preserved as content); resolution enters only via text(...) replacement, already the unavailable case. - I2: identity definedness disambiguated — chain conditions (presence, well-formedness, structural validity) are inherited; uniqueness constrains the section's own spelled identity alone, so a uniquely spelled descendant of duplicate-id ancestors keeps its defined identity; defined identity does not imply defined prefixes, and occurrence resolution / the target filter turn on the referenced identity's own definedness. - O1: spread attributes appear among the view's raw attribute spellings by form; invalidity is a located finding, never a view omission. - O2: position-resolution offset domain closed — a non-non-negative-integer offset value is the same usage error as a greater offset. - O3: stable-code scoping stated as deliberate — codes cover exactly the numbered conditions plus refusal reasons; plain usage errors carry no code but still get the JSON error document when JSON output is in effect. Rejected: - O4: the Branch header's mapping is deliberate harness bookkeeping — pushes go to the designated branch and the mapping is recorded in the patch header and PR #7; stripping it mid-process would name a branch nothing pushes to. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 678e662..df47609 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -59,12 +59,12 @@ Amend the source-range concept (currently: "code locations carry no source range A query surface returns, for a spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: -- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed, and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; +- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed (attribute position is reported by form: every attribute the tag spells appears — repeated, unknown, and spread attributes included — its invalidity a located finding of change 6, never a view omission), and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; - every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; - every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; -- position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. +- position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset, like an offset value that is not a non-negative integer, is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. The view is defined for discovered spec sources: one file, a set restricted by the existing file-glob convention, or all of them — a multi-file request returns per-file views, ordered by byte order of workspace-relative path per existing conventions, in one deterministic JSON document, so a consumer can index an entire workspace in a single invocation. Naming a file directly asserts membership in the view's domain, and both failures of that assertion are usage errors: a file outside the discovered set, per existing conventions, and a discovered file the view is not defined for — a code source has no structural view, so naming one directly is the existing conventions' wrong-kind usage error, exactly as a code group's name is where a spec group's is required. The glob form is a set restriction exactly as in change 1, restricting over the view's domain — the discovered spec sources, not the whole discovered set: a glob admits the discovered spec sources it matches, and one admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. @@ -74,8 +74,8 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. -- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity — duplicate spellings leave every bearer's identity undefined; no winner is picked. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). -- Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. +- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section whose `id` is missing, malformed, or structurally invalid has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity, its spelling chain intact and its own identity unambiguous. A defined identity therefore does not imply defined prefix identities; occurrence resolution and the target filter (change 1) turn on the definedness of the referenced identity itself — a reference to the one section spelling `a.b` resolves and records an occurrence even while duplicate spellings of `a` leave every bearer of `a` undefined. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). +- Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too. On a valid file construct form and construct validity coincide, so the existing text-value definition never had to say which of the two decides the removal rules of Markdown compilation; for these surfaces the classification is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier, and target discovery notwithstanding, so the core mid-edit state, an import whose target file was deleted or renamed, perturbs no text value — a section tag is removed with every attribute it spells (unknown, repeated, or spread included), and a construct matching no removal rule's form (the stray elements, expression containers, and exports of the invalid-construct condition) is content under compilation's stated default, preserved byte-for-byte and located by its finding (change 6). Resolution reaches a text value in exactly one place — `text(...)` replacement — and an unresolved spelling is already the unavailable case above, so a defined value is a pure function of the consulted files' parses and the resolved expansions. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is defined per surface. For the view of change 3 it is the requested files plus, when expanded text is requested, every further file those expansions consult — each embedded target's file the expansion transitively reaches — because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. For an occurrence enumeration of change 1 it is every discovered file the file filter admits; when no file filter narrows it, it is the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. @@ -100,7 +100,7 @@ Availability is unconditional: no part of the inventory requires parsing sources Sharpen the validation-error contract so an external tool can render findings inline: -- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. +- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. - Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. From 9c86b59fb559b1b9e5a8f5ec2a24a364014e9f1b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 18:34:10 +0000 Subject: [PATCH 14/35] sdg(phase-3): disambiguate attribute-view datum; seal stable-code delimitation (iter 9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - I1: change 3's attribute parenthetical now reads "attribute inclusion is by form" — it governs which attributes appear in the view (every spelled one, validity notwithstanding), not per-attribute ranges. Git history confirms the intent: the parenthetical entered in iteration 8 as an inclusion statement ("spread attributes appear among the view's raw attribute spellings by form"); "position" was a wording slip. Attributes are carried as raw spellings; ranges are granted explicitly where intended (tag decomposition, change 6 findings, change 7 id-rewrite edits). - O1: the stable-code delimitation now covers review-operation refusals explicitly — findings under the existing exit-code partition, neither numbered conditions nor rename/move refusal reasons, uncoded because review flows lie outside this proposal's UI scope (relied on unchanged). - O2: change 2's disambiguated-unit sentence now says "each carry the range of their own construct", ending the collision with change 1's coined "reference occurrence" (SPEC.md 4.6 uses "occurrence" for the Nth same-named unit). Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index df47609..052b2df 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -51,7 +51,7 @@ Introduce the concept of a **reference occurrence**: one textual spelling that r Amend the source-range concept (currently: "code locations carry no source range"): -- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry their own occurrence's range. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. +- Every code location gains a source range: for a named code unit, the construct's own characters (analogous to a section's construct range); for a whole-file location, the entire file (analogous to a root node). Document-order-disambiguated units (`path#unit@N`) each carry the range of their own construct. Where one declaration derives several named units, each unit's range is the construct that binds its own name: a function- or class-valued variable declaration's range spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives share the single namespace declaration's range, which is the one construct binding them all. A default export follows the same principle: when the exported construct is named, the unit's range is that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range — the construct that binds that name. - Exactly two outputs gain code-location ranges, named here rather than left to a general rule. Occurrence enumeration: an occurrence record presents its source graph node with that node's own range (change 1) — the surface that makes every code unit's range reachable, since a code location enters the graph's edges only as a source and every edge from it is recorded by at least one occurrence. Review payloads: the present-node range rule generalizes from requirement nodes to graph nodes, so a present code-location scope carries its source range exactly as a present requirement node does, and an absent node of either kind carries no range. The generalization touches the range datum alone; every other payload rule stands — the historical text an absent requirement node carries, and code locations' having no text value, included. No other output changes shape: edge endpoints in `query` results — `edges` rows, `reachable` witness paths, the per-node incoming and outgoing edge lists — remain bare identities, for code locations exactly as for requirement nodes today, and every other position presenting a graph node as a bare identity keeps that form. - TypeScript reference occurrences (markers, `text(...)` calls) carry the range of the referencing expression itself, distinct from the enclosing unit's range — exact per form, matching the explicitness of the `d` and MDX cases: a `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included — the expression that records the edge — and a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. @@ -59,7 +59,7 @@ Amend the source-range concept (currently: "code locations carry no source range A query surface returns, for a spec source file, everything needed to overlay structure on the raw MDX bytes in a single call: -- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed (attribute position is reported by form: every attribute the tag spells appears — repeated, unknown, and spread attributes included — its invalidity a located finding of change 6, never a view omission), and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; +- the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed (attribute inclusion is by form: every attribute the tag spells appears — repeated, unknown, and spread attributes included — its invalidity a located finding of change 6, never a view omission), and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; - every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; @@ -100,7 +100,7 @@ Availability is unconditional: no part of the inventory requires parsing sources Sharpen the validation-error contract so an external tool can render findings inline: -- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. +- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. Review-operation refusals — findings under the existing exit-code partition, but neither numbered conditions nor the `rename`/`move` refusal reasons below — likewise carry no stable code: review flows lie outside this proposal's UI scope, relied on unchanged. - Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. From d6f8c6efcc292c534d04219749850739c420247b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 18:55:10 +0000 Subject: [PATCH 15/35] sdg(phase-3): pin invalid-form id-prop identity semantics (iter 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: I1, O1, O2, O3. - I1: change 4 now defines when a section spells an identity (id prop exactly once, quoted attribute form); missing and invalid-form id props (repeated — spellings agreeing or not — braced or valueless values) spell no identity: own identity undefined, descendants' chain broken, and no participation in uniqueness — an invalid-form claimant never poisons a well-formed bearer. Resolution grounded in the document's own principles (no fabrication from invalid syntax, tags/coverage analog, mid-edit containment). - O1: review-refusal aside in change 6 scoped past change 2's range generalization (removes the one self-contradiction reading). - O2: change 5's corrupt-recorded-state finding explicitly joins the numbered validation-error conditions with a stable code. - O3: an occurrence-less MDX-embedding spelling's finding range pinned to the full braced container, keeping change 3's byte classification exact on imperfect files. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 052b2df..bf99a8d 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -74,7 +74,7 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. -- What findings make undefined is interpreted data, never structure. A section's node identity is defined exactly when its own `id` and each enclosing section's `id` are present and well-formed, each satisfies the structural-ID rules, and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section whose `id` is missing, malformed, or structurally invalid has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity, its spelling chain intact and its own identity unambiguous. A defined identity therefore does not imply defined prefix identities; occurrence resolution and the target filter (change 1) turn on the definedness of the referenced identity itself — a reference to the one section spelling `a.b` resolves and records an occurrence even while duplicate spellings of `a` leave every bearer of `a` undefined. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). +- What findings make undefined is interpreted data, never structure. A section spells an identity exactly when its `id` prop occurs exactly once with its value in the quoted attribute form the source syntax requires; that value, well-formed or not, is its spelled identity. A section with no `id` prop, and equally one whose `id` prop is invalid in form — repeated, its spellings agreeing or not, or its value in any other form, braced or valueless included — spells no identity: its own identity is undefined, and it contests no other section's, for uniqueness compares spelled identities only — a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` props the file holds beside it. A section's node identity is defined exactly when it and each enclosing section spell an identity, each spelled identity is well-formed and satisfies the structural-ID rules, and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity, its spelling chain intact and its own identity unambiguous. A defined identity therefore does not imply defined prefix identities; occurrence resolution and the target filter (change 1) turn on the definedness of the referenced identity itself — a reference to the one section spelling `a.b` resolves and records an occurrence even while duplicate spellings of `a` leave every bearer of `a` undefined. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). - Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too. On a valid file construct form and construct validity coincide, so the existing text-value definition never had to say which of the two decides the removal rules of Markdown compilation; for these surfaces the classification is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier, and target discovery notwithstanding, so the core mid-edit state, an import whose target file was deleted or renamed, perturbs no text value — a section tag is removed with every attribute it spells (unknown, repeated, or spread included), and a construct matching no removal rule's form (the stray elements, expression containers, and exports of the invalid-construct condition) is content under compilation's stated default, preserved byte-for-byte and located by its finding (change 6). Resolution reaches a text value in exactly one place — `text(...)` replacement — and an unresolved spelling is already the unavailable case above, so a defined value is a pure function of the consulted files' parses and the resolved expansions. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is defined per surface. For the view of change 3 it is the requested files plus, when expanded text is requested, every further file those expansions consult — each embedded target's file the expansion transitively reaches — because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. For an occurrence enumeration of change 1 it is every discovered file the file filter admits; when no file filter narrows it, it is the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. @@ -94,18 +94,18 @@ A query surface reports the machine-readable shape of the workspace, so an exter The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is. Invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules). Recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome. The recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map. The corruption accompanies the answer as a reported finding, and the invocation exits 1 under the existing partition. The full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries. The journal path is fixed, and its occupancy datum is presence alone, whatever kind of filesystem object occupies it — the inventory reads no journal content. The review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content. A directory entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the one rule the graph-data-area entry states. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is. Invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules). Recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome. The recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map. The corruption accompanies the answer as a reported finding — a reported error condition under change 6's contract, numbered in the validation-errors section and carrying its stable code — and the invocation exits 1 under the existing partition. The full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries. The journal path is fixed, and its occupancy datum is presence alone, whatever kind of filesystem object occupies it — the inventory reads no journal content. The review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content. A directory entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the one rule the graph-data-area entry states. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics Sharpen the validation-error contract so an external tool can render findings inline: -- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. Review-operation refusals — findings under the existing exit-code partition, but neither numbered conditions nor the `rename`/`move` refusal reasons below — likewise carry no stable code: review flows lie outside this proposal's UI scope, relied on unchanged. +- Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. Review-operation refusals — findings under the existing exit-code partition, but neither numbered conditions nor the `rename`/`move` refusal reasons below — likewise carry no stable code: review flows lie outside this proposal's UI scope, relied on unchanged beyond change 2's range generalization. - Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. - Machine-readable delivery is closed over the outcome classes: whenever JSON output is in effect, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. JSON output is in effect in exactly two cases: `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or the invoked surface is JSON-only, a single JSON document its only output form with or without `--json`, as the existing single-document surfaces are and added surfaces may be defined; no flag need be present there. Outside these two cases the existing empty-standard-output rule stands. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. -- Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. +- Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. For a spelling of the MDX embedding form, that range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (change 1) — so the byte classification of change 3 stays exact on imperfect files. ### 7. Refactoring previews From 937e36d141da330d3c864e62582403117af431d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:16:17 +0000 Subject: [PATCH 16/35] sdg(phase-3): define unreadable-record outcome for preview delta; drop exclusivity claim (iter 11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: C1 — replaced change 5's false "alone among readers" claim with an accurate shared-reader statement (inventory + change 7's delta; existing check noted as covered by its staleness condition) and gave change 7's delta a defined outcome on recorded state that exists but cannot be read: the delta, both directions one datum, reported explicitly unavailable, same numbered condition and stable code as change 5's, exit 1, rest of the preview report emitted in full; stated as the succeed/refuse equivalence's one success-side exception (the real operation is not refused — corruption fails no build validation and finishing regeneration replaces it). Aligned change 4's summary pointer and change 6's refusal appositive with the new outcome. Applied: O1 — absent tags/coverage props define the existing defaults (no tags; coverage-required), closing the misreading that presence is required. Applied: O2 — the test seam tied to acquiring workspace exclusivity never engages on a preview, stated at behavior level (no flag prescription). Applied: O3 — reported edit ranges may nest: section-move re-identification rewrites locate inside the origin-deletion range in pre-operation coordinates. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index bf99a8d..3bb9f87 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -74,12 +74,12 @@ The structural surfaces of changes 1 and 3 exist to serve an editor while a pers - Structure derived from one file's parse — the positional section tree (change 3), every construct's ranges and their decompositions, raw attribute and import spellings, comments, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation, and similar) and per-file structural (sections with missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. - Only an unparseable file (or content the existing masking rules already hide) loses its structural view; masking is per file, and the surfaces still answer for every other requested file. -- What findings make undefined is interpreted data, never structure. A section spells an identity exactly when its `id` prop occurs exactly once with its value in the quoted attribute form the source syntax requires; that value, well-formed or not, is its spelled identity. A section with no `id` prop, and equally one whose `id` prop is invalid in form — repeated, its spellings agreeing or not, or its value in any other form, braced or valueless included — spells no identity: its own identity is undefined, and it contests no other section's, for uniqueness compares spelled identities only — a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` props the file holds beside it. A section's node identity is defined exactly when it and each enclosing section spell an identity, each spelled identity is well-formed and satisfies the structural-ID rules, and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity, its spelling chain intact and its own identity unambiguous. A defined identity therefore does not imply defined prefix identities; occurrence resolution and the target filter (change 1) turn on the definedness of the referenced identity itself — a reference to the one section spelling `a.b` resolves and records an occurrence even while duplicate spellings of `a` leave every bearer of `a` undefined. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously (a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined; the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). +- What findings make undefined is interpreted data, never structure. A section spells an identity exactly when its `id` prop occurs exactly once with its value in the quoted attribute form the source syntax requires; that value, well-formed or not, is its spelled identity. A section with no `id` prop, and equally one whose `id` prop is invalid in form — repeated, its spellings agreeing or not, or its value in any other form, braced or valueless included — spells no identity: its own identity is undefined, and it contests no other section's, for uniqueness compares spelled identities only — a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` props the file holds beside it. A section's node identity is defined exactly when it and each enclosing section spell an identity, each spelled identity is well-formed and satisfies the structural-ID rules, and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity, its spelling chain intact and its own identity unambiguous. A defined identity therefore does not imply defined prefix identities; occurrence resolution and the target filter (change 1) turn on the definedness of the referenced identity itself — a reference to the one section spelling `a.b` resolves and records an occurrence even while duplicate spellings of `a` leave every bearer of `a` undefined. A section's interpreted tags and coverage attribute are defined exactly when its parsed props define them unambiguously — an absent prop defines them as the existing defaults, no tags and coverage-required, so a section spelling neither prop carries both values defined — while a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined (the raw spelling is still reported). A section whose identity is undefined still occupies its tree position with its ranges and raw spellings, and the occurrences inside it keep their positions, kinds, and resolved targets, with their source graph node — one datum, identity and range (change 1) — undefined. Invalid constructs outside change 3's inventory (stray elements, expression containers, exports) get no view entry; their findings' ranges locate them (change 6). - Data these rules or resolution leave undefined — section identities and occurrence source nodes as above, an import's resolved target when specifier form or discovery defines none, and expanded own and subtree text — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. Expanded text has an exact definedness rule: a node's own (respectively subtree) text is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one inside every embedded target's subtree — records an occurrence (change 1), and the recursion re-enters no node already being expanded (an embedding cycle); one unresolved spelling or one cycle on the expansion path makes the value unavailable as a whole — partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too. On a valid file construct form and construct validity coincide, so the existing text-value definition never had to say which of the two decides the removal rules of Markdown compilation; for these surfaces the classification is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier, and target discovery notwithstanding, so the core mid-edit state, an import whose target file was deleted or renamed, perturbs no text value — a section tag is removed with every attribute it spells (unknown, repeated, or spread included), and a construct matching no removal rule's form (the stray elements, expression containers, and exports of the invalid-construct condition) is content under compilation's stated default, preserved byte-for-byte and located by its finding (change 6). Resolution reaches a text value in exactly one place — `text(...)` replacement — and an unresolved spelling is already the unavailable case above, so a defined value is a pure function of the consulted files' parses and the resolved expansions. - A reference occurrence, by contrast, never reports an unavailable target: a spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity the definedness rule above leaves undefined; or an ambiguous one, every duplicate bearer's identity undefined — records no edge and therefore no occurrence, and its position reaches consumers through the diagnostics of change 6, which carry ranges. The two surfaces jointly locate every reference spelling, valid or invalid, in every parseable file; spellings inside an unparseable file are hidden with the rest of it, pointed to only by that file's parse-failure finding. - Every answer has a consulted domain of files, and the findings of every file in that domain are reported alongside the answer — a masked file's parse-failure finding included. The domain is defined per surface. For the view of change 3 it is the requested files plus, when expanded text is requested, every further file those expansions consult — each embedded target's file the expansion transitively reaches — because the finding that blocks an expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. For an occurrence enumeration of change 1 it is every discovered file the file filter admits; when no file filter narrows it, it is the entire discovered set, spec and code sources alike, because a masked file anywhere could conceal occurrences the enumeration would otherwise return. The mapping onto the existing exit-code partition is: an invocation whose answer reports any finding or any explicitly-unavailable datum exits 1; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their existing precedence. A possibly-incomplete answer is therefore never silent: the finding always accompanies it, and the exit code says so. The full answer document is emitted in the 0 and 1 cases alike — exit 1 signals imperfection and never withholds the answer. -Existing commands keep their current all-or-nothing read semantics; this availability contract governs the structural surfaces of changes 1 and 3 — the other added surfaces state their own: the inventory of change 5 is unconditional on its own terms, the previews of change 7 refuse exactly when the real operation would, and the identification surface of change 8 is workspace-independent. The relationship of changes 1 and 3 to stored graph data follows the same line: these query surfaces never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. +Existing commands keep their current all-or-nothing read semantics; this availability contract governs the structural surfaces of changes 1 and 3 — the other added surfaces state their own: the inventory of change 5 is unconditional on its own terms, the previews of change 7 refuse exactly when the real operation would — their one record-dependent datum, the delta, carrying change 5's unreadable-record outcome — and the identification surface of change 8 is workspace-independent. The relationship of changes 1 and 3 to stored graph data follows the same line: these query surfaces never answer from stale data — on a workspace that passes build validation they participate in read-time refresh through the existing path, exactly as the existing read commands do, and on a workspace that does not, their answers reflect the current sources and they modify nothing: no graph data, no derived files — just as a failed refresh modifies nothing today. The inventory of change 5 states its own relationship to stored state. ### 5. Workspace inventory @@ -94,7 +94,7 @@ A query surface reports the machine-readable shape of the workspace, so an exter The inventory contains no absolute paths and no environment-dependent content beyond the invocation anchoring above (a function of the invocation, like `--config` resolution — not of the machine), consistent with existing determinism and security conventions. When the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives), the anchoring is reported in the platform's absolute form — the one further case of the stated exception, still a pure function of invocation input. -Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is. Invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules). Recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. Because the inventory alone among readers consults the record without refreshing it, it is the one surface that can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves — and that case has a defined outcome. The recorded entries are reported explicitly unavailable, never fabricated and never passed off as an empty map. The corruption accompanies the answer as a reported finding — a reported error condition under change 6's contract, numbered in the validation-errors section and carrying its stable code — and the invocation exits 1 under the existing partition. The full inventory, every other provenance's content unaffected, is still emitted. Finally, the filesystem supplies the durable entries. The journal path is fixed, and its occupancy datum is presence alone, whatever kind of filesystem object occupies it — the inventory reads no journal content. The review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content. A directory entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the one rule the graph-data-area entry states. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. +Availability is unconditional: no part of the inventory requires parsing sources, so it answers whatever the sources' validity; configuration errors keep their existing precedence. Its content has three provenances, each reported as what it is. Invocation, configuration, and discovery determine the anchoring, the configuration view, the discovered sources with their groups, the graph-data area, and the per-source generated-module and Markdown-emit-destination paths (the destinations exist exactly while emission is enabled, per existing rules). Recorded generation state supplies the remaining derived-file map entries — companion paths and any other recorded derived paths — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. A reader that consults the record without refreshing it can meet recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise, a state the existing derived-file rules contemplate and a rebuild resolves. This proposal adds two such readers — the inventory, which meets it in these recorded entries, and the preview delta of change 7 (existing `check` also reads the record without refreshing; its staleness condition already covers this state) — and defines one outcome for the case, stated here and adopted by change 7. The record-supplied datum is reported explicitly unavailable, never fabricated and never passed off as an empty map. The corruption accompanies the answer as a reported finding — a reported error condition under change 6's contract, numbered in the validation-errors section and carrying its stable code — and the invocation exits 1 under the existing partition. The full answer — for the inventory, every other provenance's content — is still emitted. Finally, the filesystem supplies the durable entries. The journal path is fixed, and its occupancy datum is presence alone, whatever kind of filesystem object occupies it — the inventory reads no journal content. The review-session files are those present, selected by name alone: every directory entry directly under the review-session directory whose name is a well-formed session file name is listed, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session, and corrupt or unparseable sessions are included, since the inventory reads no session content. A directory entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the one rule the graph-data-area entry states. The inventory reports recorded and durable state as it stands and never refreshes or writes anything. ### 6. Structured diagnostics @@ -103,7 +103,7 @@ Sharpen the validation-error contract so an external tool can render findings in - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. Review-operation refusals — findings under the existing exit-code partition, but neither numbered conditions nor the `rename`/`move` refusal reasons below — likewise carry no stable code: review flows lie outside this proposal's UI scope, relied on unchanged beyond change 2's range generalization. - Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. -- The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what the previews of change 7 report — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. +- The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what a refused preview of change 7 reports — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. - Machine-readable delivery is closed over the outcome classes: whenever JSON output is in effect, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. JSON output is in effect in exactly two cases: `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or the invoked surface is JSON-only, a single JSON document its only output form with or without `--json`, as the existing single-document surfaces are and added surfaces may be defined; no flag need be present there. Outside these two cases the existing empty-standard-output rule stands. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. - Diagnostics are the locating surface for constructs that record nothing in the graph: an invalid, dynamic, or unresolved reference spelling has no occurrence (changes 1, 4), so its range reaches consumers here. For a spelling of the MDX embedding form, that range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (change 1) — so the byte classification of change 3 stays exact on imperfect files. @@ -112,10 +112,10 @@ Sharpen the validation-error contract so an external tool can render findings in `rename` and `move` gain a preview mode that performs the full validation and planning of the real operation and reports, without modifying anything: - the complete identity mapping the operation would journal; -- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script for external application, which would bypass the journaled mapping. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of a line the existing line-drop rule additionally drops (bytes contiguous with the construct by construction), so the adjunct drop lies inside this class's range rather than forming a class of its own — its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates; -- the derived-file consequences, in both directions, as the identity-relevant delta: the derived paths the operation would newly generate — paths where nothing is currently generated — and the recorded derived paths it would remove as no longer generated, the old module path after a file move included. The delta is the whole report: a successful operation finishes by regenerating every derived file, so the full regeneration set is workspace-constant, already named by the inventory of change 5, and carries no information about the operation — the delta is what the identity changes cause. +- every file the operation would rewrite or relocate, with every edit the operation would make in it — each located by a range in current, pre-operation coordinates and classed by what it is. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script for external application, which would bypass the journaled mapping. The classes cover everything the operations edit, not only reference occurrences: reference-occurrence rewrites (change 1's occurrences — `d` references, `text(...)` references, TypeScript markers); `id`-attribute rewrites (rename's and the section move's re-identification); import edits — specifier rewrites, import additions, and import removals; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of a line the existing line-drop rule additionally drops (bytes contiguous with the construct by construction), so the adjunct drop lies inside this class's range rather than forming a class of its own — its target insertion point, and the self-closing-target-parent rewrite when one applies; and the file move's relocation of the file itself. A section move whose target file does not yet exist reports that file's creation as its own class, with the insertion point at the start of the new file — the one reported location without pre-operation coordinates; every range in a file that exists stays in current, pre-operation coordinates. Reported ranges may nest: the section move's re-identification `id`-attribute rewrites locate, in those same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; +- the derived-file consequences, in both directions, as the identity-relevant delta: the derived paths the operation would newly generate — paths where nothing is currently generated — and the recorded derived paths it would remove as no longer generated, the old module path after a file move included. The delta is the whole report: a successful operation finishes by regenerating every derived file, so the full regeneration set is workspace-constant, already named by the inventory of change 5, and carries no information about the operation — the delta is what the identity changes cause. Both directions consult the record — currently generated means recorded as generated: presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes it, so recorded state that exists but cannot be read as a record meets the delta exactly as it meets the inventory, with the outcome change 5 defines: the delta, both directions one datum, is reported explicitly unavailable — never fabricated, never read as empty — the corruption accompanies the report as the same reported finding, same numbered condition and stable code (changes 5, 6), the invocation exits 1 under the existing partition, and every other part of the preview report is emitted in full. -A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification — an equivalence over workspace state, validation and planning, not over scheduling: the refusal that meets a mutating command while another runs applies to the real operation only, never to its preview, which the concurrency rules class as non-mutating. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run. Preview output is byte-deterministic. +A preview succeeds exactly when the real operation would proceed and is refused exactly when — and reporting what — the real operation would refuse, with the same exit-code classification — an equivalence over workspace state, validation and planning, not over scheduling: the refusal that meets a mutating command while another runs applies to the real operation only, never to its preview, which the concurrency rules class as non-mutating. The unreadable-record outcome of the delta is the equivalence's one stated exception, and it sits on the success side: the real operation is not refused there — a corrupt record fails no build validation, and the finishing regeneration replaces corrupt graph data — so the preview is not refused either; it succeeds carrying the finding and the unavailable delta, exiting 1 under the existing partition where the operation it previews would proceed. A preview writes nothing (no sources, no journal, no derived files, no graph data) and is therefore a non-mutating command under the concurrency rules, safe to run while readers run; the existing test seam tied to acquiring workspace exclusivity is a behavior of that acquisition — a preview, acquiring nothing, never engages it. Preview output is byte-deterministic. ### 8. Machine-interface identification From a72466283da8170d7ed192f523f48e4166101e4b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:35:43 +0000 Subject: [PATCH 17/35] sdg(phase-3): apply optional clarifications O2/O3, accept patch 0001 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 12 review: no Critical or Important items — refinement converged. Applied O2: change 6 now states the invocation-anchored reporting form for every configuration-error concerned path (the found or --config-named configuration file, and the missing-configuration working-directory case), instead of leaving the non-missing cases to the compatibility note's plural. Applied O3: change 3's import entry now states the binding-name datum for a bindingless declaration is structural absence — reported as absent, never as unavailable — per the document's root tags/coverage taxonomy and change 4's closed unavailable-data list. Rejected O1: naming whether the unreadable-record finding becomes a new numbered condition or folds into the existing staleness condition is validation-section organization, which the patch's methodology preamble delegates to spec refinement; the information contract (reported finding, stable code, exit 1, full answer emitted, identical reporter set) is the same on both branches, as the review itself notes. Stage: Proposed -> Accepted. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/patches/0001-external-ui-apis.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index 3bb9f87..a847940 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -1,7 +1,7 @@ # 0001 — Foundational machine surfaces for an external spec UI - **Type:** Improvement Proposal (IP) -- **Stage:** Proposed +- **Stage:** Accepted - **Branch:** `claude/xspec-ui-apis-4df8fa` (harness-designated for this session; stands in for `patch/external-ui-apis`) ## Motivation @@ -61,7 +61,7 @@ A query surface returns, for a spec source file, everything needed to overlay st - the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — so it exists for every parseable file whatever findings the file carries (change 4). Each node carries its source range, its raw attribute spellings as parsed (attribute inclusion is by form: every attribute the tag spells appears — repeated, unknown, and spread attributes included — its invalidity a located finding of change 6, never a view omission), and — each where defined, explicitly unavailable otherwise (change 4) — its node identity, tags, coverage attribute, and (on request) own and subtree text. A root's tags and coverage attribute are a distinct, defined case: structurally absent under existing conventions (absent for roots), they are reported as absent, never as unavailable — structural absence implies no finding and, unlike change 4 unavailability, carries no exit-1 consequence; - for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range (a self-closing section has an opening-tag range only; a root node spans the whole file and has neither). An interactive consumer needs the tags separately from the content they enclose — to render a section header in place of its opening tag, hide or fold what a tag pair encloses, and land navigation on a section's tag rather than selecting its entire construct; -- every import declaration, valid or invalid, with its source range, its binding name where one is bound, and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; +- every import declaration, valid or invalid, with its source range, its binding name where one is bound (for a declaration binding none the datum is structurally absent — reported as absent, never as unavailable, exactly as a root's tags and coverage attribute are), and its resolved target file where specifier form and discovery define one — explicitly unavailable otherwise (change 4), with the invalidity itself a located finding of change 6; - every reference occurrence in the file (change 1), positioned in document order; - every MDX comment's source range. With tags, imports, comments, and embedding occurrences located (an embedding occurrence's span is its full braced container, change 1), every construct that Markdown compilation removes is positioned, so on a finding-free file a consumer can classify each byte as annotation or content without re-parsing the MDX. On an imperfect file the classification is joint with change 6: spellings and constructs that produce no occurrence or view entry (change 4) are located by their findings' ranges, and the two surfaces together still position every removable construct; - position resolution as a direct query: given the file and a byte offset, the innermost enclosing section and, when the offset lies within a reference occurrence, that occurrence and its resolved target. Resolution is by range containment and is total over the file: every within-file offset lies in the root's range, so bytes inside imports, comments, and content between sections resolve to the innermost section construct containing them — the root when none does; the offset equal to the file length (the caret position at end of file) resolves to the root; a greater offset, like an offset value that is not a non-negative integer, is a usage error, per existing conventions. The same resolution must also be derivable from the view's data alone, so both index-building consumers and lightweight ones that keep no client-side index are served. @@ -101,7 +101,7 @@ Availability is unconditional: no part of the inventory requires parsing sources Sharpen the validation-error contract so an external tool can render findings inline: - Every reported error condition carries a stable machine-readable code identifying which numbered condition of the validation-errors section it is. Stable codes deliberately cover exactly these conditions plus the refusal reasons below, and no more: a plain usage error — an unknown command or flag, an invalid flag value, and the rest of the existing usage class — describes the invocation the consuming tool itself composed, never workspace content to render inline, so it carries no stable code, while still arriving as the JSON error document of the delivery rule below whenever JSON output is in effect. Review-operation refusals — findings under the existing exit-code partition, but neither numbered conditions nor the `rename`/`move` refusal reasons below — likewise carry no stable code: review flows lie outside this proposal's UI scope, relied on unchanged beyond change 2's range generalization. -- Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. For missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — the concerned path is the directory the failed upward search started from, the invocation working directory, reported in change 5's anchoring form (identified relative to the working directory — for this path the degenerate self-reference): invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. +- Every error that locates in source carries a location — the file and a source range (byte offsets), the range at the precision the condition allows — for each offending construct. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders inline where it stands and no representative construct is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling that records a participating dependency edge or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call — is identity data on the finding, not a further range. Conditions without an in-source location (configuration errors, path-level conditions, journal and session conditions) carry the file or path they concern. Configuration-error concerned paths are all reported in change 5's anchoring form (identified relative to the invocation working directory): configuration errors precede and block the inventory that reports the anchoring, so the concerned path must be mappable from invocation input alone. Where a configuration file is concerned — the file the upward search found or the path `--config` names — the concerned path is that file; for missing configuration with no `--config` given — the one condition where no configuration file exists to be concerned — it is the directory the failed upward search started from, the invocation working directory (for this path the degenerate self-reference). Both cases are invocation input, deterministic per invocation exactly as change 5's anchoring is, so the concerned-path datum is total over these conditions. - The JSON report form presents these fields for every finding, preserving the existing requirements that all conditions are reported together and that JSON carries the same information as the human report. - The same contract covers operation refusals: each distinct reason `rename` and `move` refuse — exactly what a refused preview of change 7 reports — carries a stable machine-readable code and the file, source range, or identity it concerns — under the location-cardinality rule above when a reason involves several constructs, the cycle a refused move would create included — so a refusal renders as precisely as a finding. Refusals are findings under the existing exit-code partition, so the JSON report form above already carries them. - Machine-readable delivery is closed over the outcome classes: whenever JSON output is in effect, an invocation that fails with a usage or configuration error (exit 2) emits a single JSON document as its entire standard output reporting the error — carrying, for conditions with a defined code (configuration errors included), the stable code and the concerned file or path above — amending the existing rule that such an error leaves standard output empty. JSON output is in effect in exactly two cases: `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or the invoked surface is JSON-only, a single JSON document its only output form with or without `--json`, as the existing single-document surfaces are and added surfaces may be defined; no flag need be present there. Outside these two cases the existing empty-standard-output rule stands. Exit codes, error precedence, and human-readable standard-error text are unchanged. Without this channel, configuration errors — the one class that precedes and blocks every surface of changes 1, 3, 5, and 7 — would be the one class an external tool cannot consume. From 60409426b690eeedb881e023f1a294ba29fb98fa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 20:03:13 +0000 Subject: [PATCH 18/35] sdg(phase-4): apply IP 0001 external-UI machine surfaces to SPEC.md (iter 1) Integrates all eight accepted surfaces as timeless end-state requirements (review items C1-C9), with concrete invocations (I1): - 5.7 reference occurrences (concept, spans per kind, no-occurrence rule, total order); 11.3 `xspec occurrences [--file ] [--to ]` with syntactic --to acceptance and malformed-only usage errors. - 1.7 rewritten: code-location ranges defined; presented in exactly two outputs (occurrence records, review payloads); edge endpoints stay bare identities; 10.7 payload generalized to present graph nodes. - 11.4 `xspec view` (tree, tag decompositions, raw attributes, imports, occurrences, comments, byte-classification guarantee, domain forms) and 11.5 `xspec at ` position resolution. - 11.2 availability contract: parse-local structure, spelled/defined identity, tags/coverage and expanded-text definedness, explicit unavailability, consulted domains, exit mapping, stale-data rule; 13.3 scoped so occurrences/view/at answer mid-edit. - 11.6 `xspec inventory` (anchoring, configuration view, sources, derived map with provenances, graph-data area write reservation and unattributed-path rule, durable files, deterministic ordering). - 14: stable codes for all 23 conditions, location cardinality, concerned paths, refusal-reason codes; new condition 23 (unreadable recorded state); 12.0 JSON error-document delivery whenever JSON output is in effect. - 6.6 previews (`--preview` on rename/move): full plan report, edit classes with pinned ranges, derived delta with condition-23 outcome, refusal equivalence with 13.5 scheduling exception; --test-hold with --preview pinned as a usage error (I2). - 12.6 `xspec version`: machine-interface version 1, workspace-independent, outside configuration-error precedence (14.14 scoped). - 12.0/12.5/13.3/13.5 conventions absorbed the new outcomes (C9). I3: no IP/delta framing survives; internal cross-references only. I4 modularity assessment: no module split - every added portion is consumer-facing interface/contract that PROCESS.md requires in SPEC.md itself, and it is densely interlocked with 1.7/12.0/13.3/14; no clean seam exists in the pre-existing content either. Single file retained under the SHOULD. O1 placement suggestion followed. Rejected items: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 158 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 138 insertions(+), 20 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 6cd9a55..6acc211 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -76,7 +76,7 @@ Every requirement node has two text values, defined by the removal and replaceme * subtree text: the section construct's contribution to its file's compiled Markdown output (for the root, the entire output). Each child contributes its subtree text at the position it occupies in the source — interleaved with the node's own contribution in document order, not appended after it. * own text: the node's subtree text with every child's contribution excised: the runs that child constructs divide (its own-text runs), joined exactly at the excision points. N child constructs divide a node's contribution into exactly N + 1 runs in document order — one before the first child construct, one between each adjacent pair, one after the last. A run MAY be empty, and empty runs count, both here and in hashing (5.5). -Both text values are exact bytes; the rules of 3 leave no joining or separator choices, and `text(...)` replacement is one of them, so both values carry embedded text fully expanded. Every own or subtree text this specification outputs — documentation comments (4.2), review text payloads (10.2, 10.7), `query` (11), `show` (12.4) — is this expanded value, and `text(...)` always returns subtree text. +Both text values are exact bytes; the rules of 3 leave no joining or separator choices, and `text(...)` replacement is one of them, so both values carry embedded text fully expanded. Every own or subtree text this specification outputs — documentation comments (4.2), review text payloads (10.2, 10.7), `query` (11.1), structural views (11.4), `show` (12.4) — is this expanded value, and `text(...)` always returns subtree text. Hashing does not use the expanded values. For hashing (5.5), a node has an own content sequence, computed like its own-text runs but with `text(...)` replacement suspended: each `text(...)` expression is excised like a child construct — contributing no bytes and marking an excision point where its target node enters — rather than replaced by expanded text. For the line-drop rule of 3, the excised expression counts as remaining line content, so the empty-expansion drop never applies; all other removal rules of 3 apply unchanged. Own content thus alternates byte runs (empty runs included) with node references — the excised child at each child excision point, the target at each embedding excision point, the two kinds distinguished — and an embedded target's text is no part of the embedder's own content. This distinction drives hashing (5.5) and change categories (5.6). @@ -84,7 +84,13 @@ Source files are UTF-8: a discovered spec or code source that is not valid UTF-8 ### 1.7 Source ranges -Where this specification outputs a source range (10.7, 11, 12.4), the range locates a requirement node in its source file: a pair of byte offsets into the file's bytes, zero-based, start-inclusive and end-exclusive, spanning — for a non-root node — the section construct's own characters, from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters (1.1, 6.5), and — for a root node — the entire file. Code locations carry no source range: a code-location identity (4.6) already locates its construct. +A source range is a pair of byte offsets into a file's bytes, zero-based, start-inclusive and end-exclusive. Every source range this specification outputs — for requirement nodes (10.7, 11, 12.4), code locations, reference occurrences (5.7), findings and refusals (14), structural views (11.4), and preview edits (6.6) — uses this one convention. + +A requirement node's range spans — for a non-root node — the section construct's own characters, from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters (1.1, 6.5), and — for a root node — the entire file. + +A code location's range spans — for a whole-file location — the entire file, and — for a named code unit (4.6) — the construct that binds the unit's name. Where one declaration derives several named units, each unit's range is the construct binding its own name: a function- or class-valued variable declaration's unit spans its own name through its initializer, not the enclosing multi-declaration statement, while the nested units a dotted namespace name derives all share the single namespace declaration's range — the one construct binding them all. A default export whose exported construct is named takes that construct's own range; the unit named `default` that an anonymous exported construct derives takes the whole export declaration's range. A document-order-disambiguated unit (`path#unit@N`, 4.6) carries the range of its own occurrence's construct. + +A code location is presented with its source range in exactly two outputs: occurrence records (5.7, 11.3) — the surface that makes every code unit's range reachable, since a code location enters the graph's edges only as a source and every edge from it is recorded by at least one occurrence — and review payloads (10.7). Everywhere else a graph node appears as an edge endpoint — `edges` rows, `reachable` witness paths, and the per-node incoming and outgoing edge lists of `query` (11.1) — it is a bare identity, requirement node and code location alike. ## 2. Source Syntax @@ -235,7 +241,7 @@ The graph contains requirement nodes and code locations. * `embeds`: created by `{text(...)}` in MDX and `text(...)` in TypeScript * `references`: created by a bare TypeScript reference -`depends`, `embeds`, and `references` are the dependency edge kinds; an edge of these kinds means the source depends on the target. `contains` is structural. Edges of each kind form a set: duplicate declarations collapse to a single edge. Each feature states which kinds it interprets. +`depends`, `embeds`, and `references` are the dependency edge kinds; an edge of these kinds means the source depends on the target. `contains` is structural. Edges of each kind form a set: duplicate declarations collapse to a single edge; the textual spellings behind dependency edges are recorded as reference occurrences (5.7). Each feature states which kinds it interprets. ### 5.3 Cycles @@ -269,6 +275,16 @@ Baseline hash comparison is defined only for a node present on both sides: a nod Categories are independent flags; a node MAY carry several. The originating nodes of a change are the nodes where edits occurred — those carrying `changed` or `metadata-changed`; every category MUST be attributed to its originating nodes. For a single edit to a leaf's text: the leaf is `changed`; every ancestor is `descendant-changed` attributed to the leaf; sibling subtrees receive no category; dependents of any node on that path are `upstream-changed`, as are those dependents' ancestors — all attributed to the leaf. For an edit that only adds or removes a child C of parent P (no other text touched): C is `changed` — added or deleted; P is `changed` (its own content changed, 5.5) and `descendant-changed` attributed to C; P's ancestors are `descendant-changed` attributed to P and C; and the `upstream-changed` cascade follows as above. For an edit that only adds or removes `d` targets on a node D: D is `metadata-changed`, no node is `changed` or `descendant-changed`, and every other node whose effectiveHash changed — D's ancestors, dependents, dependents' ancestors, and so on transitively — is `upstream-changed` attributed to D. A metadata edit touching only `coverage` or `tags` changes no effectiveHash and propagates no category. +### 5.7 Reference occurrences + +A reference occurrence is one textual spelling that records a dependency-kind edge (5.2): one `d` reference — each entry of a `d` array separately, never the array or the prop (2.2) — one MDX `{text(...)}` embedding (2.3), one TypeScript `text(...)` call (4.3), or one TypeScript dependency marker (4.5). Edges are sets; occurrences are the positions behind them: duplicate references that collapse to a single edge each remain distinct occurrences. + +An occurrence carries: the referencing file; its own source range (1.7); its edge kind; its source graph node — one datum: the node's identity together with that node's own source range (1.7); and its resolved target's identity. Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression; an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces (3); a TypeScript `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included; a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. + +A construct that records no edge records no occurrence: an import declaration (its binding used or not), a binding introduced type-only, a chain rooted at a shadowing local declaration (4.5), and a reference spelling that is dynamic or does not resolve (11.2) record none — invalid and unresolved spellings are located by their findings' ranges instead (14). + +Occurrence order is total and deterministic: by referencing file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and no further tiebreak exists. + ## 6. Identity Continuity ### 6.1 The journal @@ -286,23 +302,31 @@ When a command takes a baseline git ref, the baseline graph is reconstructed fro ### 6.4 Rename ```sh -xspec rename +xspec rename [--preview] ``` -Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); every other validation failure refuses the rename (exit 1). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0). A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. +Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0). A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. ### 6.5 Move ```sh -xspec move -xspec move # # +xspec move [--preview] +xspec move # # [--preview] ``` The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4); its identifier choice and placement, like every rewrite, are deterministic — rewritten file content is byte-deterministic for a given operation and workspace state (6.1). A successful move regenerates derived files as rename does (6.4). -Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination file already exists; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. +Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination file already exists; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). -### 6.6 Manual restructuring +### 6.6 Previews + +`xspec rename … --preview` and `xspec move … --preview` perform the full validation and planning of the operation and report its consequences while modifying nothing: no sources, no journal, no derived files, no graph data. A preview is refused exactly when — reporting what, and exiting as — the real operation would be refused, and succeeds exactly when the real operation would proceed. The equivalence is over workspace state (validation and planning), never over scheduling: the mutual-exclusion refusal of 13.5 applies to the real operation only. A preview invocation is a non-mutating command under 13.5 — it acquires no workspace exclusivity and is safe to run while readers run — and does not take the acquisition-tied test seam: supplying `--test-hold` together with `--preview` is a usage error (12.0). Preview output is byte-deterministic (12.0), supports `--json` per 12.0, and reports: + +* the complete identity mapping the operation would journal; +* every file the operation would rewrite or relocate, with every edit the operation would make in it, each located by a source range (1.7) in current, pre-operation coordinates and classed as exactly one of: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal (6.5); the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file, the one reported location without pre-operation coordinates. A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; +* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14): the delta is reported explicitly unavailable — never fabricated, never read as an empty record — the finding accompanies the report with its stable code, the invocation exits 1 (12.0), and every other part of the report is emitted in full. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. + +### 6.7 Manual restructuring Renames or moves performed by editing files directly, without the commands, produce no journal entries and are treated as deletions plus additions. @@ -512,15 +536,19 @@ xspec review export --json `list` reports every session, in byte order of session name, with its name, strategy, and item counts by status — counted from stored statuses, without the read-time invalidation of 10.4 — and reports each corrupt session (14.21) by name as corrupt in place of those fields; `list` exits 1 when any session is corrupt and 0 otherwise. `status ` reports the session's items in item order — each with id, kind, scope, status, and blocked state — plus totals by status. `show ` reports the full item: every field of 10.2 plus the same self-contained text payload as `next --json`. `export ` emits the entire session as a single JSON document — its only output form, with or without `--json`: the session's name, strategy, recorded creation parameters, and recorded decompositions, plus every item in item order, each with every field of 10.2, its blocked state, and the same self-contained text payload as `next --json`, with read-time invalidation (10.4) applied. -`next` returns the first item in the session's item order (10.5, 10.6, or the coverage order below) that needs review (`unresolved` or `invalidated`, 10.3) and is unblocked. When no item qualifies, every item is resolved — with acyclic `blockedBy` (10.1) a minimal needing-review item is always unblocked, so no other case exists — and `next` exits 0 and reports the session fully resolved (a session with no items reports the same) in both human and `--json` output; the JSON payload then contains no item. With `--json`, the payload MUST be self-contained, so the item can be acted on without further reads: every scope, context, and origin node, under its current identity and presence (10.4) and — for a present requirement node — with its source range (1.7); the item's `baseline` and `current` hashes (10.2); and text per item kind. Scope text is the scope root's subtree text for `subtree-coherence`, the scope node's subtree text for `uncovered-requirement`, and the scope node's own text for `parent-consistency`, `dependency-consistency`, and `metadata-consistency`; a code location has no text value, so a `code-impact` scope enters as identity and presence alone. Context text is own text where the context is an ancestor chain (`subtree-coherence`, `uncovered-requirement`) and subtree text otherwise (`parent-consistency` branch children; `dependency-consistency`, `metadata-consistency`, and `code-impact` targets). Origin text is a before/after pair of the node's own text: before from the item's `baseline` state (10.2), after from the current graph. Every text value is the expanded value of 1.6. A present node's text is read from the current graph; an absent node's is its value in the most recent graph state that contained it, among the item's `baseline` state and the states under which mutating subcommands (13.5) derived the item with that node among its nodes (10.2, 10.5) — a node contained in none, and the absent side of a before/after pair, is presented absent, with no text. A `coverage` session's `uncovered-requirement` items are ordered by file path, then document order. +`next` returns the first item in the session's item order (10.5, 10.6, or the coverage order below) that needs review (`unresolved` or `invalidated`, 10.3) and is unblocked. When no item qualifies, every item is resolved — with acyclic `blockedBy` (10.1) a minimal needing-review item is always unblocked, so no other case exists — and `next` exits 0 and reports the session fully resolved (a session with no items reports the same) in both human and `--json` output; the JSON payload then contains no item. With `--json`, the payload MUST be self-contained, so the item can be acted on without further reads: every scope, context, and origin node, under its current identity and presence (10.4) and — for a present graph node, requirement node and code location alike — with its source range (1.7; an absent node carries none); the item's `baseline` and `current` hashes (10.2); and text per item kind. Scope text is the scope root's subtree text for `subtree-coherence`, the scope node's subtree text for `uncovered-requirement`, and the scope node's own text for `parent-consistency`, `dependency-consistency`, and `metadata-consistency`; a code location has no text value, so a `code-impact` scope enters as identity, presence, and — when present — source range, with no text. Context text is own text where the context is an ancestor chain (`subtree-coherence`, `uncovered-requirement`) and subtree text otherwise (`parent-consistency` branch children; `dependency-consistency`, `metadata-consistency`, and `code-impact` targets). Origin text is a before/after pair of the node's own text: before from the item's `baseline` state (10.2), after from the current graph. Every text value is the expanded value of 1.6. A present node's text is read from the current graph; an absent node's is its value in the most recent graph state that contained it, among the item's `baseline` state and the states under which mutating subcommands (13.5) derived the item with that node among its nodes (10.2, 10.5) — a node contained in none, and the absent side of a before/after pair, is presented absent, with no text. A `coverage` session's `uncovered-requirement` items are ordered by file path, then document order. `split` decomposes a `subtree-coherence` item whose scope root has children into one `subtree-coherence` item per child subtree — its context the child's ancestor chain, as in 10.5 and 10.6 — plus one `parent-consistency` item for the scope root's own text, whose context is the child subtrees and whose `blockedBy` is those child items. An item of the decomposition whose kind and scope node already exist in the session is not created: the existing item takes its place, keeping its `id`, status, and recorded state — so `split` in an `audit` session reuses the children's existing items. Each decomposition item's `origin` is the originating nodes (5.6) within its scope and context — empty in an `audit` session. Newly created decomposition items additionally inherit the original's `blockedBy`; every item that was blocked by the original becomes blocked by all items of the decomposition; the original item is removed from the session and its `id` is never reused. The decomposition — the original's kind and scope node, replaced by per-child `subtree-coherence` items and the scope node's `parent-consistency` item — is recorded durably in the session and governs re-derivation (10.5). `split` on an item of any other kind, or on a `subtree-coherence` item whose scope root has no children, is refused. `resolve` sets the status and records the current relevant state (10.4); it applies to any unblocked item regardless of current status, so an `invalidated` (or previously resolved) item is re-resolved the same way. `--status` accepts `updated`, `no-change`, and `skipped`; any other value is a usage error, as is an unknown session name or item ID in any `review` command's arguments (12.0). Resolving a blocked item is refused, as is `review create` with the name of an existing session. -## 11. Query +## 11. Query Surfaces + +Five commands give scripts, agents, and external tools machine access to the workspace: `query` (11.1) answers set-level graph questions; `occurrences` (11.3) enumerates reference occurrences; `view` (11.4) returns whole-document structural views; `at` (11.5) resolves byte positions; `inventory` (11.6) reports the workspace's shape. Each is JSON-only: a single JSON document is its only output form, with or without `--json` (12.0). `occurrences`, `view`, and `at` answer per file under the availability contract of 11.2; `query` reads only valid workspaces (13.3); `inventory` parses no sources and answers whatever their validity (11.6). -`xspec query` gives scripts and agents set-level, JSON-only access to the graph — a single JSON document is its only output form, with or without `--json` (12.0): +### 11.1 `xspec query` + +`xspec query` gives scripts and agents set-level access to the graph: ```sh xspec query node @@ -533,20 +561,95 @@ xspec query reachable --from --to [--kinds ] `` is a requirement-node identity: `path#id`, or a bare `path` for a file's root node (1.5). `` is any graph-node identity: a requirement node, or a code location (`path`, `path#unit`, or `path#unit@N`; 4.6); whether a bare path names a root node or a code file follows from the file's group (7), and a path in no configured group is unknown (12.0). `node` returns identity, source range (1.7), own and subtree text, all four hashes, tags, coverage attribute, and incoming and outgoing edges by kind; for a root node the coverage attribute is reported as absent (5.5), and `nodes --coverage` matches no root. `nodes` filters combine conjunctively, and its rows are requirement nodes: `--group` accepts only a configured spec group's name — a code group's name is an invalid flag value (12.0), the wrong-kind group reference of 14.14. `nodes`, `subtree`, and `ancestors` return one row per node: identity, source range, tags, and coverage attribute (absent for roots). `subtree ` returns the queried node and all its descendants, in document order; `ancestors ` returns the queried node's proper ancestors — itself excluded — nearest first, ending at the file root. `reachable` reports whether a dependency path — one or more edges; a zero-length path is not one — exists under the given kinds and, when one does, one shortest witness path (12.0); equal `--from` and `--to` therefore report that no path exists, since a nontrivial path from a node to itself would be a dependency cycle (5.3). `reachable`'s `--kinds` accepts only the three dependency edge kinds and defaults to all three — `contains` is an invalid flag value (12.0) — while `edges --kinds` filters over all four kinds and defaults to no kind filter. List-valued flags (`--kinds`) take a comma-separated list; `--file ` uses the glob rules of 7, the outside-root rule included — a `--file` pattern resolving outside the workspace root is an invalid flag value (12.0), exit 2 like its configuration-time counterpart (14.14). All results use stable, deterministic ordering. +### 11.2 Availability on imperfect files + +`occurrences` (11.3), `view` (11.4), and `at` (11.5) serve consumers — an external editor above all — while a workspace is mid-edit, when transiently invalid states are the norm. Their availability is defined per file, from parsing alone, never gated on workspace-wide validity; the read semantics of every other command (13.3) are untouched by this section. + +**Structure is parse-local.** Everything derived from one file's parse — the positional section tree (11.4), every construct's ranges and their decompositions, raw attribute and import spellings, comment ranges, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation) and per-file structural (missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. Only an unparseable file (14.20), or content the masking rules of 14 already hide, loses its structural data; masking is per file, and these surfaces still answer for every other requested file. + +**Interpreted data is defined or explicitly unavailable.** A section spells an identity exactly when exactly one `id` attribute occurs on its tag with its value in the quoted static-string form of 2.7; that value, well-formed or not, is its spelled identity. Any other case — no `id` attribute, a repeated one (its spellings agreeing or not), or a value in any other form, braced or valueless included — spells no identity, and contests no other section's: identity uniqueness compares spelled identities only, so a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` attributes the file holds beside it. A section's node identity (1.5) is defined exactly when it and each enclosing section spell an identity, each spelled identity in the chain is well-formed (1.4) and satisfies the structural rules (1.3), and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity. A defined identity therefore does not imply defined prefix identities. A section's interpreted tags and coverage attribute are defined exactly when its parsed attributes define them unambiguously: an absent prop defines the defaults — no tags, coverage-required (2.5, 2.6) — while a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined, its raw spelling still reported (11.4). + +**Resolution.** A reference spelling resolves exactly when it names exactly one target under these rules: a section whose node identity is defined and equals the named identity, or — for a module reference with no segments (2.2) — the root of a discovered, parseable spec source. Resolution turns on the definedness of the referenced identity itself: a reference to the one section spelling `a.b` resolves — and records an occurrence — even while duplicate spellings of `a` leave every bearer of `a` undefined. A spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity these rules leave undefined; an ambiguous one, every duplicate bearer undefined — records no edge and no occurrence, and never reports an unavailable target: its position reaches consumers through its finding's range (14). The two surfaces jointly locate every reference spelling in every parseable file; a spelling inside an unparseable file is hidden with the rest of it, pointed to only by that file's parse-failure finding. + +**Expanded text.** A node's own (respectively subtree) text (1.6) is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one anywhere in every embedded target's subtree — records an occurrence, and the recursion re-enters no node already being expanded (an embedding cycle). One unresolved spelling or one cycle on the expansion path makes the whole value unavailable: partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too, and the removal classification of 3 is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier validity, and target discovery notwithstanding, so an import whose target file was deleted or renamed perturbs no text value; a section tag is removed with every attribute it spells, unknown, repeated, and spread included; and a construct matching no removal rule's form (the stray elements, expression containers, and exports of 14.16) is content, preserved byte-for-byte and located by its finding. A defined value is thus a pure function of the consulted files' parses and the resolved expansions. + +**Unavailability is explicit.** A datum these rules leave undefined — a section's identity, tags, or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7), the enclosing construct's position staying on view through 11.4; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. + +**Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). A possibly-incomplete answer is therefore never silent. + +**Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes `build` validation they participate in read-time refresh exactly as the reads of 13.3 do; on one that does not, they answer from the current sources and modify nothing — no graph data, no derived files (13.3). + +### 11.3 `xspec occurrences` + +```sh +xspec occurrences [--file ] [--to ] +``` + +Enumerates reference occurrences (5.7) in occurrence order, one record per occurrence carrying every datum of 5.7 — the source graph node per 11.2 where its section's identity is undefined. The two filters combine conjunctively. + +`--file` admits the discovered source files — spec and code alike — that the glob matches, under the glob rules of 7 (a pattern resolving outside the workspace root is an invalid flag value, as in 11.1). It is a set restriction, not an existence assertion: the enumeration's consulted domain (11.2) is the discovered files it admits; a glob admitting none admits the empty set — an empty, finding-free answer, exit 0 — and no unknown-file usage error exists on this filter. Without `--file`, the consulted domain is the entire discovered set. + +`--to` selects the occurrences whose resolved target it names. It accepts any syntactically well-formed requirement-node identity — `path#id`, or a bare `path` for a root (1.5) — whatever the workspace contains: acceptance is syntactic, and only a malformed spelling is a usage error (12.0). A spelling is well-formed exactly when it contains at most one `#`, its path part — the whole spelling, or the part before the `#` — is non-empty, and, when a `#` is present, the part after it is one or more non-empty segments joined by `.`, each satisfying the segment rules of 1.4. When the named identity does not currently resolve — its file not discovered, its file masked (14.20), its bearer's identity undefined (11.2), or no such node — the selection is empty: a spelling that does not resolve records no occurrence. + +The consulted domain's findings accompany the answer (11.2), so an empty, finding-free answer (exit 0) is definitive over the domain: nothing in the consulted files references the identity. Without `--file` the guarantee is absolute — nothing in the workspace references it; under `--file` it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. + +### 11.4 `xspec view` + +```sh +xspec view [ …] [--file ] [--text] +``` + +Returns, per requested file, everything needed to overlay structure on the raw MDX bytes. The view's domain is the discovered spec sources. Naming `` operands asserts membership: a file outside the discovered set is an unknown file (12.0), and a discovered code source, which has no structural view, is an invalid argument (12.0) — the wrong-kind reference of 14.14's pattern, as a code group's name is where a spec group's is required (11.1) — each exit 2. `--file` is instead a set restriction over the domain, under the glob rules of 7 (as in 11.3): it admits the discovered spec sources it matches, and a glob admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. Combining `` operands with `--file` is a usage error; with neither, the request covers every discovered spec source. The requested files form a set; a multi-file request returns per-file views ordered by byte order of workspace-relative path, in one JSON document. The consulted domain (11.2) is the requested files plus, with `--text`, every further file the requested expansions transitively consult — where the finding that blocks an expansion can lie. An unparseable requested file contributes no view, its parse-failure finding reporting it (11.2). Each parseable requested file's view contains: + +* the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — and exists for every parseable file, whatever findings the file carries. Each node carries its construct range (1.7); its raw attribute spellings as parsed — inclusion is by form: every attribute the tag spells appears, repeated, unknown, and spread attributes included, their invalidity a located finding (14), never a view omission; and, each per 11.2 (defined, or explicitly unavailable), its node identity, interpreted tags, and coverage attribute, plus — with `--text` — its own and subtree text. A root's tags and coverage attribute are structurally absent (5.5, 11.1): reported as absent, never as unavailable — no finding, no exit-1 consequence; +* for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range — a self-closing section has an opening-tag range only, a root node neither; +* every import declaration, valid or invalid, with its source range, its binding name — structurally absent when the declaration binds none, reported as absent, never as unavailable — and its resolved target file where specifier form and discovery define one, explicitly unavailable otherwise (11.2), the invalidity itself a located finding (14); +* every reference occurrence in the file (5.7), in document order; +* every MDX comment's source range. + +With tags, imports, comments, and embedding occurrences located — an embedding occurrence's span is its full braced container (5.7) — every construct Markdown compilation removes (3) is positioned: on a finding-free file a consumer can classify each byte as annotation or content from the view alone, without re-parsing the MDX. On an imperfect file the classification is joint with the findings: constructs producing no occurrence and no view entry — the invalid constructs of 14.16 get no view entry — are located by their findings' ranges (14), and the two surfaces together still position every removable construct. + +### 11.5 `xspec at` + +```sh +xspec at +``` + +Resolves a byte position in a discovered spec source: the innermost section construct whose range (1.7) contains the offset — the root when no narrower section does — reported with its construct range and, per 11.2, its node identity; and, when the offset lies within a reference occurrence's range, that occurrence and its resolved target (5.7). `` asserts domain membership exactly as a `view` operand does (11.4). Resolution is by range containment and total over the file: every within-file offset resolves — bytes inside imports, comments, and content between sections resolve to the innermost enclosing section construct — and the offset equal to the file's byte length (the caret position at end of file) resolves to the root. A greater offset, like an offset that is not a non-negative integer, is a usage error (12.0). The same resolution is derivable from the view's data alone (11.4): `at` adds convenience, not information, serving consumers that keep no client-side index. The consulted domain (11.2) is the named file; on an unparseable file the resolution is reported explicitly unavailable, the parse-failure finding accompanying it (11.2). + +### 11.6 `xspec inventory` + +```sh +xspec inventory +``` + +Reports the machine-readable shape of the workspace, so an external tool never edits files xspec owns and never misses files xspec reads. The inventory parses no sources, so it answers whatever the sources' validity; configuration errors keep their precedence (14.14). It never refreshes or writes anything, and it reports: + +* **Anchoring.** The workspace root and the configuration file, identified relative to the invocation working directory — pure invocation input, exactly as `--config` resolution is (7, 12.0) — so a consumer can map the workspace-relative paths in every output to real files without re-implementing the upward search. Only when the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives) is the anchoring reported in the platform's absolute form — the sole absolute-path case (12.0), still a pure function of invocation input. +* **Configuration.** The resolved configuration view: the spec and code groups with their glob lists and kinds; Markdown emission state and destinations (7.3); and the coverage profiles and policy rules, each carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, resolving against the group list this same view reports. +* **Sources.** Every discovered source file with its group memberships. +* **Derived-file map.** Per source file: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run — plus, from recorded generation state (13.3), the companion paths and any other recorded derived paths, reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. +* **Graph-data area.** The location under which graph data is kept (13.3), reported unconditionally — the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case — the one whose deletion is undone by nothing. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. +* **Durable files.** The journal path (6.1), with whether anything presently occupies it — an absent journal is an empty journal (6.1), and occupancy is presence alone, whatever kind of filesystem object occupies the path: the inventory reads no journal content. And the review-session files: every directory entry directly under the review-session directory (10.1) whose name is a well-formed session file name, selected by name alone, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session (10.1), and corrupt or unparseable sessions are listed, since the inventory reads no session content. An entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the rule above. + +Inventory lists are ordered deterministically: files and paths in byte order of workspace-relative path, groups, profiles, and rules in configuration order, and session files in byte order of file name. + +Recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise — is condition 23 (14): the record-supplied datum (the recorded derived-file entries) is reported explicitly unavailable, never fabricated and never read as an empty map; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other provenance's content is emitted in full. + ## 12. Commands ### 12.0 Global conventions * Every command supports `--json`, emitting a single JSON document. Where this specification defines report content, the JSON form MUST contain the same information. -* The report — findings included: a failing `build`'s validation errors and `check` findings are reports — is standard-output content; usage and configuration error messages (exit 2) and all other diagnostic text are standard-error content. With `--json`, the single JSON document is the entire standard output; when an exit-2 error prevents emitting one, standard output is empty. +* The report — findings included: a failing `build`'s validation errors and `check` findings are reports — is standard-output content; usage and configuration error messages (exit 2) and all other diagnostic text are standard-error content. JSON output is in effect exactly when `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or when the invoked surface is JSON-only, a single JSON document its only output form with or without `--json` (10.7, 11, 12.6). When JSON output is in effect, the single JSON document is the entire standard output, and an invocation that fails with a usage or configuration error (exit 2) emits as its entire standard output a single JSON document reporting the error — carrying the stable code and concerned file or path (14) where the condition defines them. When JSON output is not in effect, an exit-2 error leaves standard output empty. The output form never changes an exit code, the error-precedence rules, or standard-error content. * Every command supports `--config ` (7). * A flag MAY be given at most once per invocation; repeating a flag is a usage error. List-valued flags (`--kinds`) take one comma-separated value (11). * Arguments that name requirement nodes, graph nodes, workspace files, or file globs (``, ``, ``, `--file`) are workspace-relative in the form of 1.5, independent of the working directory. `--config ` and `--test-hold ` are filesystem paths resolved against the working directory. * Argument values are interpreted as UTF-8; an argument value that is not valid UTF-8 is a usage error. * IDs, tags, identities, session names, and paths compare byte-wise and case-sensitively; no Unicode normalization or case folding is applied anywhere (the create-time session-name restriction of 10.1 is the sole exception). -* All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. +* All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. -* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` (6.4, 6.5), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments; invalid session names; missing or invalid configuration (14.14); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). +* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). * The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current sources also fail build validation (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. ### 12.1 `xspec build` @@ -569,10 +672,16 @@ xspec show Accepts `path#id`, or a bare `path` for a file's root node (1.5). Prints one requirement for human reading: identity, source range (1.7), own and subtree text, hashes, tags, coverage attribute (absent for a root node, 11), and edges by kind. `query node` is the machine-facing equivalent. -### 12.5 `xspec coverage`, `xspec impact`, `xspec review`, `xspec query`, `xspec rename`, `xspec move` +### 12.5 `xspec coverage`, `xspec impact`, `xspec review`, `xspec query`, `xspec occurrences`, `xspec view`, `xspec at`, `xspec inventory`, `xspec rename`, `xspec move` As specified in sections 8, 9, 10, 11, and 6. +### 12.6 `xspec version` + +Reports the product version and the machine-interface version. The surface is JSON-only: a single JSON document is its only output form, with or without `--json` (12.0). Both values are fixed per build. The product version is informational — reported for display and support, with no requirement beyond per-build fixedness. The machine-interface version is `1`, and the surface reports exactly this value. The value names the machine-facing JSON contract this specification defines — the JSON output of the product's commands under the universal-JSON and same-information conventions of 12.0 — so an external tool checks compatibility by comparing the reported value against the value its own interface knowledge targets. + +`xspec version` is workspace-independent: it consults no workspace and no configuration — `--config` is accepted (12.0) and not consulted — answers identically in any working directory, no discoverable workspace, missing configuration, and invalid configuration included, and cannot fail for workspace or configuration reasons: configuration-error precedence (14.14) does not reach it. Usage errors keep exit 2 (12.0). + ## 13. Workspace Files ### 13.1 Generated TypeScript @@ -585,7 +694,7 @@ As specified in sections 8, 9, 10, 11, and 6. ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, and `query`. Read results never come from stale data: when graph data is missing or does not match the current sources and configuration, `ids`, `show`, `coverage`, `impact`, `review`, and `query` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering. If the current sources fail `build` validation, these commands report the validation errors and exit 1 without answering and without modifying anything: a failed refresh, like a failed build (12.1), leaves every derived file and all graph data unmodified. `check` never refreshes; it reports staleness instead (14.10). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data: when graph data is missing or does not match the current sources and configuration, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering. If the current sources fail `build` validation, `ids`, `show`, `coverage`, `impact`, `review`, and `query` report the validation errors and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2; in either case nothing is modified: a failed refresh, like a failed build (12.1), leaves every derived file and all graph data unmodified. `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files @@ -600,11 +709,17 @@ Writes never traverse symbolic links. A symbolic link at a derived file's path i ### 13.5 Concurrency and isolation -All state is workspace-local; instances operating on different workspaces MUST NOT interfere with each other. Within one workspace, file writes are atomic in their observable effect: at every moment — concurrent readers and interrupted commands included — a path xspec writes holds either its prior state (the previous content, or absence) or the complete new content, never a partial write. Commands that modify sources or durable files — `rename`, `move`, and the mutating `review` subcommands (`create`, `resolve`, `split`) — are mutually exclusive per workspace: while one runs, another MUST fail promptly with a usage error (12.0) without modifying anything, so concurrency never loses a journal append or a resolution. Exclusivity ends when the holding command's process terminates, normally or abnormally; a terminated holder MUST NOT block later commands. As a deterministic test seam for this exclusion, every mutating command accepts `--test-hold `: immediately after acquiring workspace exclusivity and before modifying anything, the command creates an empty file at the given path — creation MUST fail if anything, a symbolic link included, already exists at that path — then proceeds normally only once that file has been deleted. If the hold file cannot be created, the command fails with a usage error (12.0) without modifying anything. The seam changes no other behavior and grants no access beyond the invoking user's own file permissions. All other commands may run concurrently, with last-write-wins per file; any resulting derived-file inconsistency is resolved by rerunning `xspec build`. A mutating command interrupted before completion can leave sources and durable files inconsistent; `xspec check` reports such states (14). +All state is workspace-local; instances operating on different workspaces MUST NOT interfere with each other. Within one workspace, file writes are atomic in their observable effect: at every moment — concurrent readers and interrupted commands included — a path xspec writes holds either its prior state (the previous content, or absence) or the complete new content, never a partial write. Commands that modify sources or durable files — `rename` and `move`, their `--preview` invocations excepted (6.6), and the mutating `review` subcommands (`create`, `resolve`, `split`) — are mutually exclusive per workspace: while one runs, another MUST fail promptly with a usage error (12.0) without modifying anything, so concurrency never loses a journal append or a resolution. Exclusivity ends when the holding command's process terminates, normally or abnormally; a terminated holder MUST NOT block later commands. As a deterministic test seam for this exclusion, every mutating command accepts `--test-hold `: immediately after acquiring workspace exclusivity and before modifying anything, the command creates an empty file at the given path — creation MUST fail if anything, a symbolic link included, already exists at that path — then proceeds normally only once that file has been deleted. If the hold file cannot be created, the command fails with a usage error (12.0) without modifying anything. The seam changes no other behavior and grants no access beyond the invoking user's own file permissions. All other commands may run concurrently, with last-write-wins per file; any resulting derived-file inconsistency is resolved by rerunning `xspec build`. A mutating command interrupted before completion can leave sources and durable files inconsistent; `xspec check` reports such states (14). ## 14. Validation Errors -`xspec build` and `xspec check` MUST report actionable errors that identify the file, location, and correction. When several error conditions are present, they MUST report each of them, not only the first; a condition goes unreported only where another error makes it undetectable — an unparseable file (14.20) masks the conditions inside itself, and a reference into it reports as unresolved (14.5–14.7) — and a configuration error (14.14) precedes all source analysis. The defined error conditions, each reported by `build` and `check` unless its entry states otherwise: +`xspec build` and `xspec check` MUST report actionable errors that identify the file, location, and correction. When several error conditions are present, they MUST report each of them, not only the first; a condition goes unreported only where another error makes it undetectable — an unparseable file (14.20) masks the conditions inside itself, and a reference into it reports as unresolved (14.5–14.7) — and a configuration error (14.14) precedes all source analysis. + +Every reported condition carries a stable machine-readable code identifying which numbered condition it is: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. + +Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, and session conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory. The JSON report form presents code, locations, and concerned path for every finding, all conditions reported together, with the same information as the human report (12.0). + +The defined error conditions — also the findings that accompany answers over a consulted domain (11.2, 11.6, 6.6) — each reported by `build` and `check` unless its entry states otherwise: 1. Missing ID: a non-root section without `id`. 2. Invalid structural ID: a child ID that does not equal the parent ID plus one segment, including IDs that skip levels; the error states the expected form. A top-level section is checked against the empty prefix (exactly one segment). The check needs the parent's ID: for the immediate children of a section lacking `id`, condition 1 masks this condition — their other conditions, and this condition for their own children, report normally. @@ -619,7 +734,7 @@ All state is workspace-local; instances operating on different workspaces MUST N 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. 12. Policy violation: rule name plus offending edge. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). -14. Configuration error: missing or invalid configuration — a configuration file that is not well-formed TypeScript or not in the declarative form of 7; missing required fields, unknown keys (7), or invalid profile, rule, or group shapes; group names referenced by profiles, rules, or selectors that are unknown or not of the kind the reference requires (7.4, 7.5); ambiguous kinds (7.4, 7.5); an empty `edgeKinds`, `targetTags`, rule `kinds`, or selector `tags` list; a capture violation (7.5); a glob or `markdown.outDir` resolving outside the workspace root (7, 7.3); a file matched by both a spec and a code group. Reported by every command when it loads the configuration and discovers sources, as a usage error (12.0), not a finding. +14. Configuration error: missing or invalid configuration — a configuration file that is not well-formed TypeScript or not in the declarative form of 7; missing required fields, unknown keys (7), or invalid profile, rule, or group shapes; group names referenced by profiles, rules, or selectors that are unknown or not of the kind the reference requires (7.4, 7.5); ambiguous kinds (7.4, 7.5); an empty `edgeKinds`, `targetTags`, rule `kinds`, or selector `tags` list; a capture violation (7.5); a glob or `markdown.outDir` resolving outside the workspace root (7, 7.3); a file matched by both a spec and a code group. Reported by every command that loads the configuration — every command but `version` (12.6), which loads none — when it loads the configuration and discovers sources, as a usage error (12.0), not a finding. 15. Invalid import: in an xspec source file, an import that is not a single default binding, does not designate an xspec source file belonging to a configured spec group, or binds the identifier `S`, `Spec`, or `text` (2.1); in a TypeScript file, a `.xspec` import that does not designate such a source, a spec-module binding other than the default and `text` exports, a dynamic `import()` whose static specifier ends in `.xspec`, an export declaration or an `import X = require(…)` declaration whose specifier ends in `.xspec` (4), or an import or export declaration, `import X = require(…)`, or dynamic `import()` whose relative specifier designates a derived-file path other than through a spec module import's `.xspec` specifier (4, 13.4); in either kind of file, an import binding an identifier already bound by another import in the same file, when either import is a spec module import. 16. Invalid construct: a JSX element other than ``/``, an expression container other than a `text(...)` embedding or an MDX comment (2.7), or an export statement in a source file. 17. Invalid prop: an unknown or repeated prop, or a spread attribute, on ``/`` (2.7), an `id`, `coverage`, or `tags` value that is not a quoted-form static string literal, a `d` value that is not a braced expression (2.7), or a `coverage` value other than `required` or `none`. @@ -628,6 +743,9 @@ All state is workspace-local; instances operating on different workspaces MUST N 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. 21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, by any `review` subcommand naming the session, and by `review list` (exit 1); not reported by `build`, which does not read sessions. 22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). +23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4); `check` reports the state as staleness (14.10). + +Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code. ## 15. Example From f5b312e339c191520ec27ea0507d6625002f0b9f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 20:32:16 +0000 Subject: [PATCH 19/35] sdg(phase-4): define invalid-path behavior on UI surfaces; close refresh, import-edit, and view gaps (iter 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: node identities exist only over valid source paths — every node of a 14.19-invalid file (roots, sections, code units) has an undefined identity under 11.2; such files keep parse-local structure and their condition-19 finding accompanies every answer whose domain includes them; 1.5's #-unambiguity re-grounded on this rule; occurrences --to gains the invalid-path non-resolve case; 12.0 gains a marked byte-form representation rule for non-UTF-8 workspace-relative paths in outputs. - I1: read-time refresh leaves an unreadable record (14.23) unread, unrepaired, unreplaced; refreshing reads consult no record and report no finding for it; the state persists, reported by 11.6/6.6/14.10, until build or rename/move regeneration replaces the record (13.3, 14.23). - I2: import-removal extent pinned (declaration plus line-drop adjunct, as in 3); import addition inserted as a line of its own at a grammar-permitting, deterministic implementation-latitude offset equal to the previewed offset (6.5, 6.6). - I3: view --text consulted domain defined: exactly the files of resolved targets reachable through occurrence-recording embeddings; a non-resolving spelling is the expansion's boundary; a masked file is never consulted by expansion, its finding surfacing only when requested. - I4: argument checks of 11.3-11.5 precede answering — usage errors exit 2 whatever findings the workspace or named files carry (11.2). - I5: raw attribute spelling defined per attribute, in tag order: name as spelled (absent for spread), source range, and source text (name through value; a spread attribute's entire braced construct) (11.4). - I6: verified against pre-IP baseline (a724662): query was already JSON-only ("a single JSON document is its only output form"), so section 11's JSON-only statement alters nothing pre-existing — no change needed. - O2: 11.4 code-source operand rephrased "wrong-kind operand, a usage error" to avoid conflation with stable code invalid-argument. - O3: review export synopsis aligned to [--json] per its JSON-only text. - O4: confirmed intended, no edit — multiple view operands are deliberate refinement latitude (IP: exact surface shapes settled during spec refinement), consistent with the mandated per-file multi-view response in one document; removing the form would lose harmless capability an editor plausibly wants (view several dirty files at once). Rejected: - O1 (modularization): declined. The extraction candidates (10.4-10.6, 7.4-7.5, 8) are load-bearing contract surface: strategy derivation determines export/next payloads, item identity, and invalidation, and coverage/policy evaluation is itself the command contract; PROCESS requires the full interface/contract in SPEC.md and forbids referencing module internals, so extraction would duplicate contracts rather than shrink the document. The SHOULD yields to those MUSTs here. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 6acc211..aec507f 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -67,7 +67,7 @@ A tag (2.6) follows the same rules as an ID segment, except that tags MAY contai ### 1.5 Node identity -A requirement node is identified by its source file path plus its requirement ID, written `path#id`. The root node of a file is identified by the path alone. File paths in identities, outputs, and stored data are always workspace-relative and always use `/` as the path separator, on every platform. A discovered source file whose path contains `#` is invalid (14.19), so the `#` in an identity is unambiguous. +A requirement node is identified by its source file path plus its requirement ID, written `path#id`. The root node of a file is identified by the path alone. File paths in identities, outputs, and stored data are always workspace-relative and always use `/` as the path separator, on every platform. A discovered source file whose path contains `#` is invalid (14.19), and no node of a file whose path is invalid has a defined identity (11.2) — identities are only ever formed, emitted, or resolved against over valid source paths — so the `#` in an identity is unambiguous. ### 1.6 Own text, subtree text, and own content @@ -314,7 +314,7 @@ xspec move [--preview] xspec move # # [--preview] ``` -The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4); its identifier choice and placement, like every rewrite, are deterministic — rewritten file content is byte-deterministic for a given operation and workspace state (6.1). A successful move regenerates derived files as rename does (6.4). +The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and the offset is exactly the one the operation's preview reports (6.6). A successful move regenerates derived files as rename does (6.4). Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination file already exists; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). @@ -323,7 +323,7 @@ Move validation mirrors rename validation, including the valid-workspace precond `xspec rename … --preview` and `xspec move … --preview` perform the full validation and planning of the operation and report its consequences while modifying nothing: no sources, no journal, no derived files, no graph data. A preview is refused exactly when — reporting what, and exiting as — the real operation would be refused, and succeeds exactly when the real operation would proceed. The equivalence is over workspace state (validation and planning), never over scheduling: the mutual-exclusion refusal of 13.5 applies to the real operation only. A preview invocation is a non-mutating command under 13.5 — it acquires no workspace exclusivity and is safe to run while readers run — and does not take the acquisition-tied test seam: supplying `--test-hold` together with `--preview` is a usage error (12.0). Preview output is byte-deterministic (12.0), supports `--json` per 12.0, and reports: * the complete identity mapping the operation would journal; -* every file the operation would rewrite or relocate, with every edit the operation would make in it, each located by a source range (1.7) in current, pre-operation coordinates and classed as exactly one of: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal (6.5); the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file, the one reported location without pre-operation coordinates. A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; +* every file the operation would rewrite or relocate, with every edit the operation would make in it, each located by a source range (1.7) in current, pre-operation coordinates and classed as exactly one of: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file, the one reported location without pre-operation coordinates. A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; * the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14): the delta is reported explicitly unavailable — never fabricated, never read as an empty record — the finding accompanies the report with its stable code, the invocation exits 1 (12.0), and every other part of the report is emitted in full. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. ### 6.7 Manual restructuring @@ -529,7 +529,7 @@ xspec review next [--json] xspec review show xspec review split xspec review resolve --status [--note ] -xspec review export --json +xspec review export [--json] ``` `review create` requires exactly one of `--base`, `--strategy audit`, or `--coverage`; supplying none, more than one, or any other `--strategy` value is a usage error (12.0). `create` records the session's creation parameters in the session file, fully resolved: a baseline session records the commit identity `--base` resolved to at creation, a `coverage` session records the named profile's definition — its 7.4 fields, with each group name replaced by that group's configured glob list and kind — and an audit session records none. Every later generator run (10.4, 10.5) uses the recorded parameters — the recorded commit as the baseline, the recorded globs matched against the currently discovered sources (7) — so renaming or editing refs, profiles, or groups after `create` never changes the recorded parameters the session runs with. Discovery itself still follows the current configuration: a file that no longer belongs to any configured group is out of the session's view, exactly as if deleted. A `review` command that cannot resolve or reconstruct the recorded (or, at `create`, the given) baseline fails per 6.3 as a usage error (12.0), modifying nothing. A `coverage` session contains one `uncovered-requirement` item per uncovered required node of the profile — scope: that node; context: its ancestor chain; origin and `blockedBy` empty. @@ -567,15 +567,15 @@ xspec query reachable --from --to [--kinds ] **Structure is parse-local.** Everything derived from one file's parse — the positional section tree (11.4), every construct's ranges and their decompositions, raw attribute and import spellings, comment ranges, and reference-occurrence positions — remains available while other files are invalid and while the file itself carries findings of either level: resolution-level (unresolved references, cycle participation) and per-file structural (missing, duplicate, or structurally invalid IDs; malformed segments; invalid props; invalid constructs) alike. Only an unparseable file (14.20), or content the masking rules of 14 already hide, loses its structural data; masking is per file, and these surfaces still answer for every other requested file. -**Interpreted data is defined or explicitly unavailable.** A section spells an identity exactly when exactly one `id` attribute occurs on its tag with its value in the quoted static-string form of 2.7; that value, well-formed or not, is its spelled identity. Any other case — no `id` attribute, a repeated one (its spellings agreeing or not), or a value in any other form, braced or valueless included — spells no identity, and contests no other section's: identity uniqueness compares spelled identities only, so a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` attributes the file holds beside it. A section's node identity (1.5) is defined exactly when it and each enclosing section spell an identity, each spelled identity in the chain is well-formed (1.4) and satisfies the structural rules (1.3), and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity. A defined identity therefore does not imply defined prefix identities. A section's interpreted tags and coverage attribute are defined exactly when its parsed attributes define them unambiguously: an absent prop defines the defaults — no tags, coverage-required (2.5, 2.6) — while a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined, its raw spelling still reported (11.4). +**Interpreted data is defined or explicitly unavailable.** A node identity (1.5) is formed over the file's path and requires a valid one: in a discovered file whose own path is invalid (14.19), no graph node — the root and every section of a spec source, the whole-file location and every named unit of a code source — has a defined identity, whatever the content spells. Such a file keeps its parse-local structure and positions; its condition-19 finding accompanies every answer whose consulted domain includes it (below); and no identity over an invalid path is ever emitted or resolved against (1.5). A root node's identity is defined exactly when its file's path is valid. A section spells an identity exactly when exactly one `id` attribute occurs on its tag with its value in the quoted static-string form of 2.7; that value, well-formed or not, is its spelled identity. Any other case — no `id` attribute, a repeated one (its spellings agreeing or not), or a value in any other form, braced or valueless included — spells no identity, and contests no other section's: identity uniqueness compares spelled identities only, so a bearer whose spelled identity no other section spells keeps its defined identity whatever invalid-form `id` attributes the file holds beside it. A section's node identity (1.5) is defined exactly when its file's path is valid, it and each enclosing section spell an identity, each spelled identity in the chain is well-formed (1.4) and satisfies the structural rules (1.3), and no other section of the file spells the same identity as it does. The chain conditions are inherited — a descendant of a section that spells no identity, or whose spelled identity is malformed or structurally invalid, has no defined identity — but uniqueness is not: it constrains the section's own spelled identity alone. Duplicate spellings leave every bearer of the duplicated identity undefined, no winner picked, while a uniquely spelled descendant of duplicate-`id` ancestors keeps its defined identity. A defined identity therefore does not imply defined prefix identities. A section's interpreted tags and coverage attribute are defined exactly when its parsed attributes define them unambiguously: an absent prop defines the defaults — no tags, coverage-required (2.5, 2.6) — while a repeated, malformed, or invalid-valued prop leaves the interpreted value undefined, its raw spelling still reported (11.4). -**Resolution.** A reference spelling resolves exactly when it names exactly one target under these rules: a section whose node identity is defined and equals the named identity, or — for a module reference with no segments (2.2) — the root of a discovered, parseable spec source. Resolution turns on the definedness of the referenced identity itself: a reference to the one section spelling `a.b` resolves — and records an occurrence — even while duplicate spellings of `a` leave every bearer of `a` undefined. A spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity these rules leave undefined; an ambiguous one, every duplicate bearer undefined — records no edge and no occurrence, and never reports an unavailable target: its position reaches consumers through its finding's range (14). The two surfaces jointly locate every reference spelling in every parseable file; a spelling inside an unparseable file is hidden with the rest of it, pointed to only by that file's parse-failure finding. +**Resolution.** A reference spelling resolves exactly when it names exactly one target under these rules: a section whose node identity is defined and equals the named identity, or — for a module reference with no segments (2.2) — the root, its identity defined (a valid path, above), of a discovered, parseable spec source. Resolution turns on the definedness of the referenced identity itself: a reference to the one section spelling `a.b` resolves — and records an occurrence — even while duplicate spellings of `a` leave every bearer of `a` undefined. A spelling that does not resolve to exactly one target — an unknown target; a unique bearer whose identity these rules leave undefined; an ambiguous one, every duplicate bearer undefined — records no edge and no occurrence, and never reports an unavailable target: its position reaches consumers through its finding's range (14). The two surfaces jointly locate every reference spelling in every parseable file; a spelling inside an unparseable file is hidden with the rest of it, pointed to only by that file's parse-failure finding. **Expanded text.** A node's own (respectively subtree) text (1.6) is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one anywhere in every embedded target's subtree — records an occurrence, and the recursion re-enters no node already being expanded (an embedding cycle). One unresolved spelling or one cycle on the expansion path makes the whole value unavailable: partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too, and the removal classification of 3 is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier validity, and target discovery notwithstanding, so an import whose target file was deleted or renamed perturbs no text value; a section tag is removed with every attribute it spells, unknown, repeated, and spread included; and a construct matching no removal rule's form (the stray elements, expression containers, and exports of 14.16) is content, preserved byte-for-byte and located by its finding. A defined value is thus a pure function of the consulted files' parses and the resolved expansions. -**Unavailability is explicit.** A datum these rules leave undefined — a section's identity, tags, or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7), the enclosing construct's position staying on view through 11.4; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. +**Unavailability is explicit.** A datum these rules leave undefined — a node's identity (a section's under the spelling rules above; every node's in a file whose path is invalid); a section's tags or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7), the enclosing construct's position staying on view through 11.4; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. -**Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). A possibly-incomplete answer is therefore never silent. +**Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. **Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes `build` validation they participate in read-time refresh exactly as the reads of 13.3 do; on one that does not, they answer from the current sources and modify nothing — no graph data, no derived files (13.3). @@ -585,11 +585,11 @@ xspec query reachable --from --to [--kinds ] xspec occurrences [--file ] [--to ] ``` -Enumerates reference occurrences (5.7) in occurrence order, one record per occurrence carrying every datum of 5.7 — the source graph node per 11.2 where its section's identity is undefined. The two filters combine conjunctively. +Enumerates reference occurrences (5.7) in occurrence order, one record per occurrence carrying every datum of 5.7 — the source graph node per 11.2 where its source node's identity is undefined. The two filters combine conjunctively. `--file` admits the discovered source files — spec and code alike — that the glob matches, under the glob rules of 7 (a pattern resolving outside the workspace root is an invalid flag value, as in 11.1). It is a set restriction, not an existence assertion: the enumeration's consulted domain (11.2) is the discovered files it admits; a glob admitting none admits the empty set — an empty, finding-free answer, exit 0 — and no unknown-file usage error exists on this filter. Without `--file`, the consulted domain is the entire discovered set. -`--to` selects the occurrences whose resolved target it names. It accepts any syntactically well-formed requirement-node identity — `path#id`, or a bare `path` for a root (1.5) — whatever the workspace contains: acceptance is syntactic, and only a malformed spelling is a usage error (12.0). A spelling is well-formed exactly when it contains at most one `#`, its path part — the whole spelling, or the part before the `#` — is non-empty, and, when a `#` is present, the part after it is one or more non-empty segments joined by `.`, each satisfying the segment rules of 1.4. When the named identity does not currently resolve — its file not discovered, its file masked (14.20), its bearer's identity undefined (11.2), or no such node — the selection is empty: a spelling that does not resolve records no occurrence. +`--to` selects the occurrences whose resolved target it names. It accepts any syntactically well-formed requirement-node identity — `path#id`, or a bare `path` for a root (1.5) — whatever the workspace contains: acceptance is syntactic, and only a malformed spelling is a usage error (12.0). A spelling is well-formed exactly when it contains at most one `#`, its path part — the whole spelling, or the part before the `#` — is non-empty, and, when a `#` is present, the part after it is one or more non-empty segments joined by `.`, each satisfying the segment rules of 1.4. When the named identity does not currently resolve — its file not discovered, its file masked (14.20), its file's path invalid (14.19), its bearer's identity otherwise undefined (11.2), or no such node — the selection is empty: a spelling that does not resolve records no occurrence. The consulted domain's findings accompany the answer (11.2), so an empty, finding-free answer (exit 0) is definitive over the domain: nothing in the consulted files references the identity. Without `--file` the guarantee is absolute — nothing in the workspace references it; under `--file` it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. @@ -599,9 +599,9 @@ The consulted domain's findings accompany the answer (11.2), so an empty, findin xspec view [ …] [--file ] [--text] ``` -Returns, per requested file, everything needed to overlay structure on the raw MDX bytes. The view's domain is the discovered spec sources. Naming `` operands asserts membership: a file outside the discovered set is an unknown file (12.0), and a discovered code source, which has no structural view, is an invalid argument (12.0) — the wrong-kind reference of 14.14's pattern, as a code group's name is where a spec group's is required (11.1) — each exit 2. `--file` is instead a set restriction over the domain, under the glob rules of 7 (as in 11.3): it admits the discovered spec sources it matches, and a glob admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. Combining `` operands with `--file` is a usage error; with neither, the request covers every discovered spec source. The requested files form a set; a multi-file request returns per-file views ordered by byte order of workspace-relative path, in one JSON document. The consulted domain (11.2) is the requested files plus, with `--text`, every further file the requested expansions transitively consult — where the finding that blocks an expansion can lie. An unparseable requested file contributes no view, its parse-failure finding reporting it (11.2). Each parseable requested file's view contains: +Returns, per requested file, everything needed to overlay structure on the raw MDX bytes. The view's domain is the discovered spec sources. Naming `` operands asserts membership: a file outside the discovered set is an unknown file (12.0), and a discovered code source, which has no structural view, is a wrong-kind operand, a usage error (12.0) — the wrong-kind reference of 14.14's pattern, as a code group's name is where a spec group's is required (11.1) — each exit 2. `--file` is instead a set restriction over the domain, under the glob rules of 7 (as in 11.3): it admits the discovered spec sources it matches, and a glob admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. Combining `` operands with `--file` is a usage error; with neither, the request covers every discovered spec source. The requested files form a set; a multi-file request returns per-file views ordered by byte order of workspace-relative path, in one JSON document. The consulted domain (11.2) is the requested files plus, with `--text`, every file the requested expansions transitively consult: exactly the files of the resolved targets reachable from the requested files' embeddings through resolved — occurrence-recording (5.7) — embeddings, an embedding cycle's participants included, whether or not any expansion completes. A spelling that records no occurrence is an expansion's boundary: it consults no further file — the finding blocking there is the spelling's own (11.2, 14.5–14.7), lying in a file already consulted — while the finding that blocks a deeper expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. A masked file (14.20) is never consulted by an expansion — no spelling resolves into it (11.2) — so its parse-failure finding accompanies the answer only when it is itself a requested file. An unparseable requested file contributes no view, its parse-failure finding reporting it (11.2). A requested file whose path is invalid (14.19) keeps its view — structure is parse-local (11.2) — every node identity in it explicitly unavailable, its condition-19 finding accompanying. Each parseable requested file's view contains: -* the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — and exists for every parseable file, whatever findings the file carries. Each node carries its construct range (1.7); its raw attribute spellings as parsed — inclusion is by form: every attribute the tag spells appears, repeated, unknown, and spread attributes included, their invalidity a located finding (14), never a view omission; and, each per 11.2 (defined, or explicitly unavailable), its node identity, interpreted tags, and coverage attribute, plus — with `--text` — its own and subtree text. A root's tags and coverage attribute are structurally absent (5.5, 11.1): reported as absent, never as unavailable — no finding, no exit-1 consequence; +* the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — and exists for every parseable file, whatever findings the file carries. Each node carries its construct range (1.7); its raw attribute spellings as parsed, one entry per attribute the tag spells, in tag order — each entry the attribute's name as spelled (structurally absent for a spread attribute), its source range (1.7), and its source text: the attribute's own characters, for a named attribute its name through the last character of its value or the bare name where it spells no value, for a spread attribute (2.7) its entire braced construct — inclusion is by form: every attribute the tag spells appears, repeated, unknown, and spread attributes included, their invalidity a located finding (14), never a view omission; and, each per 11.2 (defined, or explicitly unavailable), its node identity, interpreted tags, and coverage attribute, plus — with `--text` — its own and subtree text. A root's tags and coverage attribute are structurally absent (5.5, 11.1): reported as absent, never as unavailable — no finding, no exit-1 consequence; * for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range — a self-closing section has an opening-tag range only, a root node neither; * every import declaration, valid or invalid, with its source range, its binding name — structurally absent when the declaration binds none, reported as absent, never as unavailable — and its resolved target file where specifier form and discovery define one, explicitly unavailable otherwise (11.2), the invalidity itself a located finding (14); * every reference occurrence in the file (5.7), in document order; @@ -648,6 +648,7 @@ Recorded state that exists but cannot be read as a record — corrupt graph data * Argument values are interpreted as UTF-8; an argument value that is not valid UTF-8 is a usage error. * IDs, tags, identities, session names, and paths compare byte-wise and case-sensitively; no Unicode normalization or case folding is applied anywhere (the create-time session-name restriction of 10.1 is the sole exception). * All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. +* A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. * Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). * The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current sources also fail build validation (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. @@ -694,7 +695,7 @@ Reports the product version and the machine-interface version. The surface is JS ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data: when graph data is missing or does not match the current sources and configuration, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering. If the current sources fail `build` validation, `ids`, `show`, `coverage`, `impact`, `review`, and `query` report the validation errors and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2; in either case nothing is modified: a failed refresh, like a failed build (12.1), leaves every derived file and all graph data unmodified. `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data: when graph data is missing or does not match the current sources and configuration, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. If the current sources fail `build` validation, `ids`, `show`, `coverage`, `impact`, `review`, and `query` report the validation errors and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2; in either case nothing is modified: a failed refresh, like a failed build (12.1), leaves every derived file and all graph data unmodified. `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files @@ -743,7 +744,7 @@ The defined error conditions — also the findings that accompany answers over a 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. 21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, by any `review` subcommand naming the session, and by `review list` (exit 1); not reported by `build`, which does not read sessions. 22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). -23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4); `check` reports the state as staleness (14.10). +23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code. From f97917874f79c06e435e948610003e9ad2948450 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 21:03:13 +0000 Subject: [PATCH 20/35] sdg(phase-4): define machine-facing JSON forms (12.7); pin finding concerns and file-operand parsing (iter 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: C1 (new 12.7 — value forms for ranges, paths and the marked byte form, the value/absent/unavailable three-state, finding and error documents, and document forms for occurrences/view/at/inventory/ previews/version, with pinned orderings; 11.6 derived-map bullet aligned with the record-supplied datum; pointers from 6.6, 11, 12.0, 12.6, 14). I1 (condition 22 concerns the offending symlink component, one finding per component; condition 23 concerns the graph-data area; record conditions added to 14's concerned-path rule). I2 (12.0: # splits only identity-form arguments, including # operands; bare operands and --file globs are whole paths with no delimiter role for #). O1 (10.1 "directory entry" replaces "file"). O2 (5.7 occurrence existence anchored to target resolution). O3 (13.4 orphan rule extended to a missing or unreadable record). O4 (12.0 intra-exit-2 precedence: syntax-only errors before configuration loading, configuration errors before workspace-consulting argument checks). O5 (finding order and preview-edit order pinned in 12.7). Partially applied: O6 — removed the one redundant clause in 11.6's unattributed-path rule ("the one whose deletion is undone by nothing"); the remaining statements carry distinct load (the foreign case's definition, the inventory's stance, the consumer rule), so further compression risks dropping content. Rejected: O7 — no modularization: nearly all of sections 8-10 is consumer-facing interface contract that PROCESS.md requires to remain in SPEC.md itself, which the review itself concedes makes single-file defensible; extracting mechanism prose mid-IP would churn cross- references for no requirement. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 54 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index aec507f..441ab55 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -277,7 +277,7 @@ Categories are independent flags; a node MAY carry several. The originating node ### 5.7 Reference occurrences -A reference occurrence is one textual spelling that records a dependency-kind edge (5.2): one `d` reference — each entry of a `d` array separately, never the array or the prop (2.2) — one MDX `{text(...)}` embedding (2.3), one TypeScript `text(...)` call (4.3), or one TypeScript dependency marker (4.5). Edges are sets; occurrences are the positions behind them: duplicate references that collapse to a single edge each remain distinct occurrences. +A reference occurrence is one textual spelling that records a dependency-kind edge (5.2): one `d` reference — each entry of a `d` array separately, never the array or the prop (2.2) — one MDX `{text(...)}` embedding (2.3), one TypeScript `text(...)` call (4.3), or one TypeScript dependency marker (4.5). Edges are sets; occurrences are the positions behind them: duplicate references that collapse to a single edge each remain distinct occurrences. Occurrence existence turns on target resolution (11.2): a spelling that resolves records an occurrence even where 11.2 leaves its source graph node's identity undefined, the source datum then reported explicitly unavailable. An occurrence carries: the referencing file; its own source range (1.7); its edge kind; its source graph node — one datum: the node's identity together with that node's own source range (1.7); and its resolved target's identity. Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression; an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces (3); a TypeScript `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included; a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. @@ -320,7 +320,7 @@ Move validation mirrors rename validation, including the valid-workspace precond ### 6.6 Previews -`xspec rename … --preview` and `xspec move … --preview` perform the full validation and planning of the operation and report its consequences while modifying nothing: no sources, no journal, no derived files, no graph data. A preview is refused exactly when — reporting what, and exiting as — the real operation would be refused, and succeeds exactly when the real operation would proceed. The equivalence is over workspace state (validation and planning), never over scheduling: the mutual-exclusion refusal of 13.5 applies to the real operation only. A preview invocation is a non-mutating command under 13.5 — it acquires no workspace exclusivity and is safe to run while readers run — and does not take the acquisition-tied test seam: supplying `--test-hold` together with `--preview` is a usage error (12.0). Preview output is byte-deterministic (12.0), supports `--json` per 12.0, and reports: +`xspec rename … --preview` and `xspec move … --preview` perform the full validation and planning of the operation and report its consequences while modifying nothing: no sources, no journal, no derived files, no graph data. A preview is refused exactly when — reporting what, and exiting as — the real operation would be refused, and succeeds exactly when the real operation would proceed. The equivalence is over workspace state (validation and planning), never over scheduling: the mutual-exclusion refusal of 13.5 applies to the real operation only. A preview invocation is a non-mutating command under 13.5 — it acquires no workspace exclusivity and is safe to run while readers run — and does not take the acquisition-tied test seam: supplying `--test-hold` together with `--preview` is a usage error (12.0). Preview output is byte-deterministic (12.0), supports `--json` per 12.0 — the preview document form of 12.7 — and reports: * the complete identity mapping the operation would journal; * every file the operation would rewrite or relocate, with every edit the operation would make in it, each located by a source range (1.7) in current, pre-operation coordinates and classed as exactly one of: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file, the one reported location without pre-operation coordinates. A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; @@ -459,7 +459,7 @@ Review turns graph results into a staged checklist. xspec separates the review m ### 10.1 Sessions -A review session is stored at `.xspec/reviews/.json` as a plain, deterministic file. A session name MUST consist of one or more characters from `A–Z`, `a–z`, `0–9`, `.`, `_`, and `-`, and MUST NOT begin with `.`; any other name is a usage error (12.0). Session names are case-sensitive, but so that session files stay unambiguous on case-insensitive filesystems, a name that matches an existing session's name ignoring ASCII case is treated at `review create` as the name of an existing session and refused (10.7); every other subcommand matches names exactly. A session is a durable task ledger for a specific graph state (13.4), not a source of requirement identity. Only a file directly under `.xspec/reviews/` named `.json` with a valid session name is a session; any other file there is not a session and is ignored by every command, `check` included. A session file that exists but is not a plain file (13.4), cannot be parsed, or violates a session invariant — the fields of 10.2 present and well-formed, statuses drawn from 10.3, item `id`s unique within the session, `blockedBy` naming only item `id`s present in the session and containing no cycle (no item transitively blocks itself; every built-in strategy and `split` produce only acyclic blocking, so a cycle can only enter by external modification), at most one item per kind and scope node (10.5), and the recorded creation parameters and decompositions (10.7) well-formed — is corrupt (14.21): every `review` subcommand naming that session reports the corruption and exits 1, modifying nothing, and `list` reports it as corrupt (10.7). +A review session is stored at `.xspec/reviews/.json` as a plain, deterministic file. A session name MUST consist of one or more characters from `A–Z`, `a–z`, `0–9`, `.`, `_`, and `-`, and MUST NOT begin with `.`; any other name is a usage error (12.0). Session names are case-sensitive, but so that session files stay unambiguous on case-insensitive filesystems, a name that matches an existing session's name ignoring ASCII case is treated at `review create` as the name of an existing session and refused (10.7); every other subcommand matches names exactly. A session is a durable task ledger for a specific graph state (13.4), not a source of requirement identity. Only a directory entry directly under `.xspec/reviews/` named `.json` with a valid session name is a session; any other entry there is not a session and is ignored by every command, `check` included. A session file that exists but is not a plain file (13.4), cannot be parsed, or violates a session invariant — the fields of 10.2 present and well-formed, statuses drawn from 10.3, item `id`s unique within the session, `blockedBy` naming only item `id`s present in the session and containing no cycle (no item transitively blocks itself; every built-in strategy and `split` produce only acyclic blocking, so a cycle can only enter by external modification), at most one item per kind and scope node (10.5), and the recorded creation parameters and decompositions (10.7) well-formed — is corrupt (14.21): every `review` subcommand naming that session reports the corruption and exits 1, modifying nothing, and `list` reports it as corrupt (10.7). ### 10.2 Items @@ -544,7 +544,7 @@ xspec review export [--json] ## 11. Query Surfaces -Five commands give scripts, agents, and external tools machine access to the workspace: `query` (11.1) answers set-level graph questions; `occurrences` (11.3) enumerates reference occurrences; `view` (11.4) returns whole-document structural views; `at` (11.5) resolves byte positions; `inventory` (11.6) reports the workspace's shape. Each is JSON-only: a single JSON document is its only output form, with or without `--json` (12.0). `occurrences`, `view`, and `at` answer per file under the availability contract of 11.2; `query` reads only valid workspaces (13.3); `inventory` parses no sources and answers whatever their validity (11.6). +Five commands give scripts, agents, and external tools machine access to the workspace: `query` (11.1) answers set-level graph questions; `occurrences` (11.3) enumerates reference occurrences; `view` (11.4) returns whole-document structural views; `at` (11.5) resolves byte positions; `inventory` (11.6) reports the workspace's shape. Each is JSON-only: a single JSON document, in the document form of 12.7, is its only output form, with or without `--json` (12.0). `occurrences`, `view`, and `at` answer per file under the availability contract of 11.2; `query` reads only valid workspaces (13.3); `inventory` parses no sources and answers whatever their validity (11.6). ### 11.1 `xspec query` @@ -628,30 +628,30 @@ Reports the machine-readable shape of the workspace, so an external tool never e * **Anchoring.** The workspace root and the configuration file, identified relative to the invocation working directory — pure invocation input, exactly as `--config` resolution is (7, 12.0) — so a consumer can map the workspace-relative paths in every output to real files without re-implementing the upward search. Only when the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives) is the anchoring reported in the platform's absolute form — the sole absolute-path case (12.0), still a pure function of invocation input. * **Configuration.** The resolved configuration view: the spec and code groups with their glob lists and kinds; Markdown emission state and destinations (7.3); and the coverage profiles and policy rules, each carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, resolving against the group list this same view reports. * **Sources.** Every discovered source file with its group memberships. -* **Derived-file map.** Per source file: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run — plus, from recorded generation state (13.3), the companion paths and any other recorded derived paths, reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. -* **Graph-data area.** The location under which graph data is kept (13.3), reported unconditionally — the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case — the one whose deletion is undone by nothing. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. +* **Derived-file map.** Per discovered spec source: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run. Beside it, one record-supplied datum: the recorded derived-file paths (13.3) — the paths as last generated, companions included — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. +* **Graph-data area.** The location under which graph data is kept (13.3), reported unconditionally — the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. * **Durable files.** The journal path (6.1), with whether anything presently occupies it — an absent journal is an empty journal (6.1), and occupancy is presence alone, whatever kind of filesystem object occupies the path: the inventory reads no journal content. And the review-session files: every directory entry directly under the review-session directory (10.1) whose name is a well-formed session file name, selected by name alone, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session (10.1), and corrupt or unparseable sessions are listed, since the inventory reads no session content. An entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the rule above. Inventory lists are ordered deterministically: files and paths in byte order of workspace-relative path, groups, profiles, and rules in configuration order, and session files in byte order of file name. -Recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise — is condition 23 (14): the record-supplied datum (the recorded derived-file entries) is reported explicitly unavailable, never fabricated and never read as an empty map; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other provenance's content is emitted in full. +Recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise — is condition 23 (14): the record-supplied datum (the recorded derived-file paths) is reported explicitly unavailable, never fabricated and never read as an empty map; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other provenance's content is emitted in full. ## 12. Commands ### 12.0 Global conventions * Every command supports `--json`, emitting a single JSON document. Where this specification defines report content, the JSON form MUST contain the same information. -* The report — findings included: a failing `build`'s validation errors and `check` findings are reports — is standard-output content; usage and configuration error messages (exit 2) and all other diagnostic text are standard-error content. JSON output is in effect exactly when `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or when the invoked surface is JSON-only, a single JSON document its only output form with or without `--json` (10.7, 11, 12.6). When JSON output is in effect, the single JSON document is the entire standard output, and an invocation that fails with a usage or configuration error (exit 2) emits as its entire standard output a single JSON document reporting the error — carrying the stable code and concerned file or path (14) where the condition defines them. When JSON output is not in effect, an exit-2 error leaves standard output empty. The output form never changes an exit code, the error-precedence rules, or standard-error content. +* The report — findings included: a failing `build`'s validation errors and `check` findings are reports — is standard-output content; usage and configuration error messages (exit 2) and all other diagnostic text are standard-error content. JSON output is in effect exactly when `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or when the invoked surface is JSON-only, a single JSON document its only output form with or without `--json` (10.7, 11, 12.6). When JSON output is in effect, the single JSON document is the entire standard output, and an invocation that fails with a usage or configuration error (exit 2) emits as its entire standard output a single JSON document reporting the error — the error document of 12.7, carrying the stable code and concerned file or path (14) where the condition defines them. When JSON output is not in effect, an exit-2 error leaves standard output empty. The output form never changes an exit code, the error-precedence rules, or standard-error content. * Every command supports `--config ` (7). * A flag MAY be given at most once per invocation; repeating a flag is a usage error. List-valued flags (`--kinds`) take one comma-separated value (11). -* Arguments that name requirement nodes, graph nodes, workspace files, or file globs (``, ``, ``, `--file`) are workspace-relative in the form of 1.5, independent of the working directory. `--config ` and `--test-hold ` are filesystem paths resolved against the working directory. +* Arguments that name requirement nodes, graph nodes, workspace files, or file globs (``, ``, ``, `--file`) are workspace-relative, independent of the working directory. `` and `` values are identities in the form of 1.5, their `#` splitting path from id or unit; the split applies equally to an operand spelled `#` (6.5). A bare `` operand and a `--file` glob are a whole path or pattern: `#` has no delimiter role in them, so a `#`-containing spelling names the discovered file of that invalid path (14.19, 11.4), never a `path#id` pair. `--config ` and `--test-hold ` are filesystem paths resolved against the working directory. * Argument values are interpreted as UTF-8; an argument value that is not valid UTF-8 is a usage error. * IDs, tags, identities, session names, and paths compare byte-wise and case-sensitively; no Unicode normalization or case folding is applied anywhere (the create-time session-name restriction of 10.1 is the sole exception). * All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. -* A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). +* A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. * Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). -* The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current sources also fail build validation (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. +* The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current sources also fail build validation (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. ### 12.1 `xspec build` @@ -679,10 +679,32 @@ As specified in sections 8, 9, 10, 11, and 6. ### 12.6 `xspec version` -Reports the product version and the machine-interface version. The surface is JSON-only: a single JSON document is its only output form, with or without `--json` (12.0). Both values are fixed per build. The product version is informational — reported for display and support, with no requirement beyond per-build fixedness. The machine-interface version is `1`, and the surface reports exactly this value. The value names the machine-facing JSON contract this specification defines — the JSON output of the product's commands under the universal-JSON and same-information conventions of 12.0 — so an external tool checks compatibility by comparing the reported value against the value its own interface knowledge targets. +Reports the product version and the machine-interface version. The surface is JSON-only: a single JSON document, in the form of 12.7, is its only output form, with or without `--json` (12.0). Both values are fixed per build. The product version is informational — reported for display and support, with no requirement beyond per-build fixedness. The machine-interface version is `1`, and the surface reports exactly this value (12.7). The value names the machine-facing JSON contract this specification defines — the JSON output of the product's commands under the universal-JSON and same-information conventions of 12.0 and the value and document forms of 12.7 — so an external tool checks compatibility by comparing the reported value against the value its own interface knowledge targets. `xspec version` is workspace-independent: it consults no workspace and no configuration — `--config` is accepted (12.0) and not consulted — answers identically in any working directory, no discoverable workspace, missing configuration, and invalid configuration included, and cannot fail for workspace or configuration reasons: configuration-error precedence (14.14) does not reach it. Usage errors keep exit 2 (12.0). +### 12.7 JSON document forms + +The machine-interface version (12.6) names the JSON contract this specification defines. This section fixes its observable forms: the value forms every JSON output uses, and the document forms of the finding report, the error document, and the surfaces of 6.6, 11.3–11.6, and 12.6; every other command's JSON output carries its defining section's information (12.0). Consumers locate every datum by the member names fixed here. Each object carries exactly the members its form names — where a member's datum does not arise (a flag not given, a datum a section defines as structurally absent, a refused preview's plan) the member is `null`, never omitted, except where a form states conditional presence — and no object of any form other than the unavailability marker carries a member named `unavailable`. + +Value forms: + +* A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. +* A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). +* A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise, then by concerned path. A report whose defined content is findings alone — `build` and `check` reports, the validation errors of refusing reads (13.3), refused operations and their refused previews (6.4–6.6, 10.7) — is `{"findings": […]}`. +* The exit-2 error document (12.0) is `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. +* A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. + +Document forms — each a single JSON document whose top level is an object; every one below except `version`'s carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: + +* `occurrences` (11.3): `{"findings", "occurrences"}` — occurrence records in occurrence order (5.7). +* `view` (11.4): `{"findings", "views"}` — one `{"file", "root", "imports", "occurrences", "comments"}` per parseable requested file, ordered by file path bytes; an unparseable requested file contributes no entry (11.4). Each node of the section tree is `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus, exactly when `--text` is given, `"ownText"` and `"subtreeText"`: `identity`, `tags`, `coverage`, and the text members are each a plain value, `null` where 11.4 defines structural absence, or unavailable (11.2); `opening` and `closing` are the tag ranges of 11.4, `null` where none exists; `attributes` is one `{"name", "range", "text"}` per spelled attribute in tag order, `name` `null` for a spread attribute; `children` holds the child nodes in document order, and `root` the root node. `imports` is one `{"range", "name", "target"}` per import declaration in document order, `name` `null` where the declaration binds none, `target` a path or unavailable; `occurrences` holds the file's occurrence records and `comments` the comment ranges, each in document order. +* `at` (11.5): `{"findings", "resolution"}` — `resolution` is `{"section", "occurrence"}` or unavailable (11.5): `section` is `{"identity", "range"}` of the innermost enclosing section construct, its identity per 11.2; `occurrence` is the containing occurrence's record, `null` when the offset lies within none. +* `inventory` (11.6): `{"findings", "root", "config", "configuration", "sources", "derived", "recorded", "graphData", "journal", "sessions"}`. `root` and `config` are the anchoring paths (11.6). `configuration` is `{"specs", "code", "markdown", "coverage", "policy"}`, the resolved view (11.6) with every default and inferred kind explicit: `specs` and `code` one `{"name", "globs"}` per group; `markdown` `{"emit", "outDir"}`, `outDir` `null` where unset and an absent `markdown` key resolving to `{"emit": false, "outDir": null}` (7.3); `coverage` one `{"name", "target", "targetTags", "targets", "boundary", "boundaryKind", "mode", "edgeKinds"}` per profile, `targetTags` `null` where absent; `policy` one `{"name", "type", "from", "to", "kinds"}` per rule, each selector `{"group", "kind"}`, `{"files"}`, or `{"tags"}` (7.5). `sources` is one `{"path", "groups"}` per discovered file, `groups` one `{"name", "kind"}` each; `derived` one `{"source", "module", "markdown"}` per discovered spec source, `markdown` `null` while emission is disabled (7.3); `recorded` the record-supplied datum (11.6) — the recorded derived-file paths in byte order — or unavailable (14.23); `graphData` the graph-data area's path; `journal` `{"path", "occupied"}`, `occupied` a boolean; `sessions` the session file paths. List order follows 11.6. +* `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes; each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). +* `version` (12.6): `{"product", "interface"}` — the product version and the machine-interface version, both strings; the reported machine-interface value is the string form of 12.6's stated value, `"1"`. + ## 13. Workspace Files ### 13.1 Generated TypeScript @@ -701,7 +723,7 @@ xspec maintains graph data under `.xspec/`, containing requirement nodes, code l Every file xspec writes is a plain file suitable for committing, written with stable ordering and sorted keys. Files are classified: -* Derived: generated TypeScript modules and their companion files (13.1), emitted Markdown, and graph data. Derived files are fully reproducible from sources, configuration, and the journal (5.4) via `xspec build`; a conflicted, corrupted, deleted, or orphaned derived file is correctly resolved by rebuilding (12.1). Orphan removal relies on the recorded derived-file paths (13.3): a derived file orphaned while that record was itself missing is outside xspec's knowledge — xspec does not remove it, and it MAY be deleted manually. +* Derived: generated TypeScript modules and their companion files (13.1), emitted Markdown, and graph data. Derived files are fully reproducible from sources, configuration, and the journal (5.4) via `xspec build`; a conflicted, corrupted, deleted, or orphaned derived file is correctly resolved by rebuilding (12.1). Orphan removal relies on the recorded derived-file paths (13.3): a derived file orphaned while that record was itself missing or unreadable (14.23) is outside xspec's knowledge — xspec does not remove it, and it MAY be deleted manually. * Durable: the journal (6.1) and review sessions (10.1). Durable files record operations and resolutions; they are not reproducible, are never regenerated, and MUST NOT be modified except by their owning commands. They are line-oriented or stably keyed so that concurrent additions merge textually; `xspec check` validates their integrity and reports unresolvable states. Derived-file paths belong to xspec: writing a derived file replaces whatever exists at its path, whether or not xspec wrote it. Derived files are never sources: paths whose file name contains `.xspec.`, files under `.xspec/`, and files at the configured Markdown emit destinations (7.3) are excluded from every spec and code group (7). @@ -718,7 +740,7 @@ All state is workspace-local; instances operating on different workspaces MUST N Every reported condition carries a stable machine-readable code identifying which numbered condition it is: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. -Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, and session conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory. The JSON report form presents code, locations, and concerned path for every finding, all conditions reported together, with the same information as the human report (12.0). +Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory. The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). The defined error conditions — also the findings that accompany answers over a consulted domain (11.2, 11.6, 6.6) — each reported by `build` and `check` unless its entry states otherwise: @@ -743,8 +765,8 @@ The defined error conditions — also the findings that accompany answers over a 19. Invalid source path: a discovered spec or code source file whose workspace-relative path contains `#` or is not valid UTF-8 (7), or a spec-group file without the `.mdx` extension (7.1). 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. 21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, by any `review` subcommand naming the session, and by `review list` (exit 1); not reported by `build`, which does not read sessions. -22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). -23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). +22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). +23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code. From 71f18e0aca416d8350c03488e7a4667bc5ca9759 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 21:35:22 +0000 Subject: [PATCH 21/35] sdg(phase-4): seal refused-preview form, unify build-validation gate, total finding order (iter 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: C1 (refused previews leave the findings-alone list; they keep the four-member preview form with null mapping/files/delta), C2 (one gate phrase — "the validations of \`xspec build\`" — in 6.4, 11.2, 12.0, 13.3; the refusal branch of the all-or-nothing reads covers journal errors (14.13) and refused writes (14.22); refresh runs only where build would succeed and cannot fail; conditions 13/22 accompany no 11.3-11.5 answer — findings are the domain files' alone, finding-free answers stay exit 0), I1 (11 intro corrected: only occurrences/view/at/inventory have 12.7 document forms; query — JSON-only already before the IP — carries its defining section's information), I2 (finding order made total: concerned path with null-first, identities, message; identical findings collapse to one), I3 (list members are [] when empty; null only where a form states the datum absent), O1 (companion attribution via 13.1 naming stated in 11.6), O2 (duplicate unresolved-spelling clauses trimmed from 5.7 and 11.3), O4 (offset spelling pinned to ASCII decimal digits), O5 (recorded paths excluded from the staleness comparison), O6 (marked-byte-form list extended with inventory derived paths and view import targets). Rejected: O3 — no modules extracted: the candidates (§10 review mechanism, §14 condition catalog) are consumer-facing interface/contract, which PROCESS.md requires defined in SPEC.md itself, so no loosely coupled non-contract component exists to extract at the current size. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 441ab55..abae132 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -281,7 +281,7 @@ A reference occurrence is one textual spelling that records a dependency-kind ed An occurrence carries: the referencing file; its own source range (1.7); its edge kind; its source graph node — one datum: the node's identity together with that node's own source range (1.7); and its resolved target's identity. Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression; an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces (3); a TypeScript `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included; a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. -A construct that records no edge records no occurrence: an import declaration (its binding used or not), a binding introduced type-only, a chain rooted at a shadowing local declaration (4.5), and a reference spelling that is dynamic or does not resolve (11.2) record none — invalid and unresolved spellings are located by their findings' ranges instead (14). +A construct that records no edge records no occurrence: an import declaration (its binding used or not), a binding introduced type-only, a chain rooted at a shadowing local declaration (4.5), and a reference spelling that is dynamic or does not resolve (11.2) record none. Occurrence order is total and deterministic: by referencing file path (byte order), then by range start, then by range end. Distinct occurrences are distinct spellings occupying distinct spans, so identical ranges do not occur and no further tiebreak exists. @@ -544,7 +544,7 @@ xspec review export [--json] ## 11. Query Surfaces -Five commands give scripts, agents, and external tools machine access to the workspace: `query` (11.1) answers set-level graph questions; `occurrences` (11.3) enumerates reference occurrences; `view` (11.4) returns whole-document structural views; `at` (11.5) resolves byte positions; `inventory` (11.6) reports the workspace's shape. Each is JSON-only: a single JSON document, in the document form of 12.7, is its only output form, with or without `--json` (12.0). `occurrences`, `view`, and `at` answer per file under the availability contract of 11.2; `query` reads only valid workspaces (13.3); `inventory` parses no sources and answers whatever their validity (11.6). +Five commands give scripts, agents, and external tools machine access to the workspace: `query` (11.1) answers set-level graph questions; `occurrences` (11.3) enumerates reference occurrences; `view` (11.4) returns whole-document structural views; `at` (11.5) resolves byte positions; `inventory` (11.6) reports the workspace's shape. Each is JSON-only: a single JSON document is its only output form, with or without `--json` (12.0) — `occurrences`, `view`, `at`, and `inventory` in the document forms of 12.7, `query` carrying its defining section's information (12.7). `occurrences`, `view`, and `at` answer per file under the availability contract of 11.2; `query` reads only valid workspaces (13.3); `inventory` parses no sources and answers whatever their validity (11.6). ### 11.1 `xspec query` @@ -577,7 +577,7 @@ xspec query reachable --from --to [--kinds ] **Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. -**Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes `build` validation they participate in read-time refresh exactly as the reads of 13.3 do; on one that does not, they answer from the current sources and modify nothing — no graph data, no derived files (13.3). +**Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes the validations of `xspec build` (12.1) they participate in read-time refresh exactly as the reads of 13.3 do; on one that fails them — source validation errors, journal errors (14.13), and refused writes (14.22) alike (13.3) — they answer from the current sources and modify nothing: no graph data, no derived files. A gate condition that is a finding of no domain file — the journal's (14.13), a write path's (14.22) — accompanies no answer of these surfaces: on the failing side these answers consult no journal and no record and write nothing, and on the passing side no such finding exists. An answer's findings are its domain files' findings alone, and a complete, finding-free answer exits 0 (above) whatever journal or write-path state the workspace holds. ### 11.3 `xspec occurrences` @@ -589,7 +589,7 @@ Enumerates reference occurrences (5.7) in occurrence order, one record per occur `--file` admits the discovered source files — spec and code alike — that the glob matches, under the glob rules of 7 (a pattern resolving outside the workspace root is an invalid flag value, as in 11.1). It is a set restriction, not an existence assertion: the enumeration's consulted domain (11.2) is the discovered files it admits; a glob admitting none admits the empty set — an empty, finding-free answer, exit 0 — and no unknown-file usage error exists on this filter. Without `--file`, the consulted domain is the entire discovered set. -`--to` selects the occurrences whose resolved target it names. It accepts any syntactically well-formed requirement-node identity — `path#id`, or a bare `path` for a root (1.5) — whatever the workspace contains: acceptance is syntactic, and only a malformed spelling is a usage error (12.0). A spelling is well-formed exactly when it contains at most one `#`, its path part — the whole spelling, or the part before the `#` — is non-empty, and, when a `#` is present, the part after it is one or more non-empty segments joined by `.`, each satisfying the segment rules of 1.4. When the named identity does not currently resolve — its file not discovered, its file masked (14.20), its file's path invalid (14.19), its bearer's identity otherwise undefined (11.2), or no such node — the selection is empty: a spelling that does not resolve records no occurrence. +`--to` selects the occurrences whose resolved target it names. It accepts any syntactically well-formed requirement-node identity — `path#id`, or a bare `path` for a root (1.5) — whatever the workspace contains: acceptance is syntactic, and only a malformed spelling is a usage error (12.0). A spelling is well-formed exactly when it contains at most one `#`, its path part — the whole spelling, or the part before the `#` — is non-empty, and, when a `#` is present, the part after it is one or more non-empty segments joined by `.`, each satisfying the segment rules of 1.4. When the named identity does not currently resolve — its file not discovered, its file masked (14.20), its file's path invalid (14.19), its bearer's identity otherwise undefined (11.2), or no such node — the selection is empty (11.2). The consulted domain's findings accompany the answer (11.2), so an empty, finding-free answer (exit 0) is definitive over the domain: nothing in the consulted files references the identity. Without `--file` the guarantee is absolute — nothing in the workspace references it; under `--file` it is exactly domain-wide — a file outside the admitted set can still hold a resolving occurrence, which the answer neither reports nor denies. @@ -615,7 +615,7 @@ With tags, imports, comments, and embedding occurrences located — an embedding xspec at ``` -Resolves a byte position in a discovered spec source: the innermost section construct whose range (1.7) contains the offset — the root when no narrower section does — reported with its construct range and, per 11.2, its node identity; and, when the offset lies within a reference occurrence's range, that occurrence and its resolved target (5.7). `` asserts domain membership exactly as a `view` operand does (11.4). Resolution is by range containment and total over the file: every within-file offset resolves — bytes inside imports, comments, and content between sections resolve to the innermost enclosing section construct — and the offset equal to the file's byte length (the caret position at end of file) resolves to the root. A greater offset, like an offset that is not a non-negative integer, is a usage error (12.0). The same resolution is derivable from the view's data alone (11.4): `at` adds convenience, not information, serving consumers that keep no client-side index. The consulted domain (11.2) is the named file; on an unparseable file the resolution is reported explicitly unavailable, the parse-failure finding accompanying it (11.2). +Resolves a byte position in a discovered spec source: the innermost section construct whose range (1.7) contains the offset — the root when no narrower section does — reported with its construct range and, per 11.2, its node identity; and, when the offset lies within a reference occurrence's range, that occurrence and its resolved target (5.7). `` asserts domain membership exactly as a `view` operand does (11.4). Resolution is by range containment and total over the file: every within-file offset resolves — bytes inside imports, comments, and content between sections resolve to the innermost enclosing section construct — and the offset equal to the file's byte length (the caret position at end of file) resolves to the root. A greater offset is a usage error (12.0); so is an `` spelled as anything but one or more ASCII decimal digits, read in decimal — leading zeros permitted; a sign, whitespace, or any other character is not a non-negative integer's spelling. The same resolution is derivable from the view's data alone (11.4): `at` adds convenience, not information, serving consumers that keep no client-side index. The consulted domain (11.2) is the named file; on an unparseable file the resolution is reported explicitly unavailable, the parse-failure finding accompanying it (11.2). ### 11.6 `xspec inventory` @@ -628,7 +628,7 @@ Reports the machine-readable shape of the workspace, so an external tool never e * **Anchoring.** The workspace root and the configuration file, identified relative to the invocation working directory — pure invocation input, exactly as `--config` resolution is (7, 12.0) — so a consumer can map the workspace-relative paths in every output to real files without re-implementing the upward search. Only when the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives) is the anchoring reported in the platform's absolute form — the sole absolute-path case (12.0), still a pure function of invocation input. * **Configuration.** The resolved configuration view: the spec and code groups with their glob lists and kinds; Markdown emission state and destinations (7.3); and the coverage profiles and policy rules, each carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, resolving against the group list this same view reports. * **Sources.** Every discovered source file with its group memberships. -* **Derived-file map.** Per discovered spec source: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run. Beside it, one record-supplied datum: the recorded derived-file paths (13.3) — the paths as last generated, companions included — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. +* **Derived-file map.** Per discovered spec source: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run. Beside it, one record-supplied datum: the recorded derived-file paths (13.3) — the paths as last generated, companions included, each companion attributable to its source through the naming scheme of 13.1 — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. * **Graph-data area.** The location under which graph data is kept (13.3), reported unconditionally — the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. * **Durable files.** The journal path (6.1), with whether anything presently occupies it — an absent journal is an empty journal (6.1), and occupancy is presence alone, whatever kind of filesystem object occupies the path: the inventory reads no journal content. And the review-session files: every directory entry directly under the review-session directory (10.1) whose name is a well-formed session file name, selected by name alone, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session (10.1), and corrupt or unparseable sessions are listed, since the inventory reads no session content. An entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the rule above. @@ -648,10 +648,10 @@ Recorded state that exists but cannot be read as a record — corrupt graph data * Argument values are interpreted as UTF-8; an argument value that is not valid UTF-8 is a usage error. * IDs, tags, identities, session names, and paths compare byte-wise and case-sensitively; no Unicode normalization or case folding is applied anywhere (the create-time session-name restriction of 10.1 is the sole exception). * All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. -* A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). +* A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source or derived path in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file or an import's resolved target (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. * Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). -* The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current sources also fail build validation (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. +* The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. ### 12.1 `xspec build` @@ -685,14 +685,14 @@ Reports the product version and the machine-interface version. The surface is JS ### 12.7 JSON document forms -The machine-interface version (12.6) names the JSON contract this specification defines. This section fixes its observable forms: the value forms every JSON output uses, and the document forms of the finding report, the error document, and the surfaces of 6.6, 11.3–11.6, and 12.6; every other command's JSON output carries its defining section's information (12.0). Consumers locate every datum by the member names fixed here. Each object carries exactly the members its form names — where a member's datum does not arise (a flag not given, a datum a section defines as structurally absent, a refused preview's plan) the member is `null`, never omitted, except where a form states conditional presence — and no object of any form other than the unavailability marker carries a member named `unavailable`. +The machine-interface version (12.6) names the JSON contract this specification defines. This section fixes its observable forms: the value forms every JSON output uses, and the document forms of the finding report, the error document, and the surfaces of 6.6, 11.3–11.6, and 12.6; every other command's JSON output carries its defining section's information (12.0). Consumers locate every datum by the member names fixed here. Each object carries exactly the members its form names — where a member's datum does not arise (a flag not given, a datum a section defines as structurally absent, a refused preview's plan) the member is `null`, never omitted, except where a form states conditional presence — and no object of any form other than the unavailability marker carries a member named `unavailable`. A list-valued member with no elements is the empty array: `null` never encodes emptiness — it marks a datum whose absence its form or defining section states — so a root node's `attributes`, a finding-free answer's `findings`, and an empty delta direction are each `[]`, while an absent `targetTags` (11.6) and a root's interpreted `tags` (11.4) are the stated `null`. Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise, then by concerned path. A report whose defined content is findings alone — `build` and `check` reports, the validation errors of refusing reads (13.3), refused operations and their refused previews (6.4–6.6, 10.7) — is `{"findings": […]}`. +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise, then by concerned path (`null` before any path), then by identities, compared element-wise, then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). * The exit-2 error document (12.0) is `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. @@ -717,7 +717,7 @@ Document forms — each a single JSON document whose top level is an object; eve ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data: when graph data is missing or does not match the current sources and configuration, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. If the current sources fail `build` validation, `ids`, `show`, `coverage`, `impact`, `review`, and `query` report the validation errors and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2; in either case nothing is modified: a failed refresh, like a failed build (12.1), leaves every derived file and all graph data unmodified. `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files From 9cc71e83cd58b92c493a97cab8415ef0681765f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 22:02:05 +0000 Subject: [PATCH 22/35] sdg(phase-4): define .mdx-less derived map, import name datum, anchoring spelling, unreadable-record staleness (iter 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: C1 (derived-file map and emit destinations structurally absent for .mdx-less discovered spec sources — 13.1, 13.2 via 7.3, 11.6, 12.7), C2 (view import name = default binding only, absent otherwise — 11.4, 12.7), I1 (canonical anchoring spelling incl. "." self-reference and separator — 11.6, 14), I2 (check reports an unreadable record as a condition-10 finding concerning the graph-data area — 14.10, 12.2), O1 (5.7 definition by resolution), O2 (prefix-first finding order — 12.7), O3 (occurrences in 13.3 content list), O4 (.xspec spelling pinned — 11.6), O5 (exit-2 document holds one finding — 12.7), O6 (positional tree parents to innermost enclosing section — 11.4). Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index abae132..623bd79 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -277,7 +277,7 @@ Categories are independent flags; a node MAY carry several. The originating node ### 5.7 Reference occurrences -A reference occurrence is one textual spelling that records a dependency-kind edge (5.2): one `d` reference — each entry of a `d` array separately, never the array or the prop (2.2) — one MDX `{text(...)}` embedding (2.3), one TypeScript `text(...)` call (4.3), or one TypeScript dependency marker (4.5). Edges are sets; occurrences are the positions behind them: duplicate references that collapse to a single edge each remain distinct occurrences. Occurrence existence turns on target resolution (11.2): a spelling that resolves records an occurrence even where 11.2 leaves its source graph node's identity undefined, the source datum then reported explicitly unavailable. +A reference occurrence is one textual spelling of a dependency-kind reference (5.2) whose target resolves (11.2): one `d` reference — each entry of a `d` array separately, never the array or the prop (2.2) — one MDX `{text(...)}` embedding (2.3), one TypeScript `text(...)` call (4.3), or one TypeScript dependency marker (4.5). Edges are sets; occurrences are the positions behind them: duplicate references that collapse to a single edge each remain distinct occurrences. Occurrence existence turns on target resolution (11.2): a spelling that resolves records an occurrence even where 11.2 leaves its source graph node's identity undefined, the source datum then reported explicitly unavailable. An occurrence carries: the referencing file; its own source range (1.7); its edge kind; its source graph node — one datum: the node's identity together with that node's own source range (1.7); and its resolved target's identity. Occurrence spans are exact per kind: a `d` reference occurrence spans that one reference's own expression; an MDX embedding occurrence spans the entire `{text(...)}` expression container, opening brace through closing brace — the whole construct Markdown compilation replaces (3); a TypeScript `text(...)` occurrence spans the entire call expression, callee through closing parenthesis, argument included; a marker occurrence spans the bare reference chain alone, exclusive of any statement terminator. @@ -381,7 +381,7 @@ Named groups of TypeScript source files, each a list of globs. Code groups serve ### 7.3 `markdown` -The `markdown` key is optional; when it is absent, no Markdown is emitted. When present, `markdown.emit` (boolean, required) controls whether pure Markdown files are emitted, and `markdown.outDir` (optional path) redirects emitted files into a directory, preserving workspace-relative paths; the default emits next to each source file. `outDir` resolves relative to the workspace root and MUST resolve within it; a value resolving outside the workspace root is a configuration error (14.14). The configured Markdown emit destinations (4, 13.4) exist exactly while emission is enabled: with `markdown` present and `emit` `true`, they are the paths at which the discovered spec sources emit (13.2), whether or not emission has yet run; with `markdown` absent or `emit` `false`, no path is a Markdown emit destination — the exclusion of 13.4 and the import rule of 4 then have no Markdown component. +The `markdown` key is optional; when it is absent, no Markdown is emitted. When present, `markdown.emit` (boolean, required) controls whether pure Markdown files are emitted, and `markdown.outDir` (optional path) redirects emitted files into a directory, preserving workspace-relative paths; the default emits next to each source file. `outDir` resolves relative to the workspace root and MUST resolve within it; a value resolving outside the workspace root is a configuration error (14.14). The configured Markdown emit destinations (4, 13.4) exist exactly while emission is enabled: with `markdown` present and `emit` `true`, they are the paths at which the discovered spec sources emit (13.2) — a spec-group file without the `.mdx` extension emits nothing and contributes none (13.1) — whether or not emission has yet run; with `markdown` absent or `emit` `false`, no path is a Markdown emit destination — the exclusion of 13.4 and the import rule of 4 then have no Markdown component. ### 7.4 `coverage` @@ -601,9 +601,9 @@ xspec view [ …] [--file ] [--text] Returns, per requested file, everything needed to overlay structure on the raw MDX bytes. The view's domain is the discovered spec sources. Naming `` operands asserts membership: a file outside the discovered set is an unknown file (12.0), and a discovered code source, which has no structural view, is a wrong-kind operand, a usage error (12.0) — the wrong-kind reference of 14.14's pattern, as a code group's name is where a spec group's is required (11.1) — each exit 2. `--file` is instead a set restriction over the domain, under the glob rules of 7 (as in 11.3): it admits the discovered spec sources it matches, and a glob admitting none — matching no discovered file, or only code sources — admits the empty set, an empty, finding-free answer, exit 0. Combining `` operands with `--file` is a usage error; with neither, the request covers every discovered spec source. The requested files form a set; a multi-file request returns per-file views ordered by byte order of workspace-relative path, in one JSON document. The consulted domain (11.2) is the requested files plus, with `--text`, every file the requested expansions transitively consult: exactly the files of the resolved targets reachable from the requested files' embeddings through resolved — occurrence-recording (5.7) — embeddings, an embedding cycle's participants included, whether or not any expansion completes. A spelling that records no occurrence is an expansion's boundary: it consults no further file — the finding blocking there is the spelling's own (11.2, 14.5–14.7), lying in a file already consulted — while the finding that blocks a deeper expansion, an unresolved spelling or a cycle participation, can lie in a consulted file the request never named. A masked file (14.20) is never consulted by an expansion — no spelling resolves into it (11.2) — so its parse-failure finding accompanies the answer only when it is itself a requested file. An unparseable requested file contributes no view, its parse-failure finding reporting it (11.2). A requested file whose path is invalid (14.19) keeps its view — structure is parse-local (11.2) — every node identity in it explicitly unavailable, its condition-19 finding accompanying. Each parseable requested file's view contains: -* the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — and exists for every parseable file, whatever findings the file carries. Each node carries its construct range (1.7); its raw attribute spellings as parsed, one entry per attribute the tag spells, in tag order — each entry the attribute's name as spelled (structurally absent for a spread attribute), its source range (1.7), and its source text: the attribute's own characters, for a named attribute its name through the last character of its value or the bare name where it spells no value, for a spread attribute (2.7) its entire braced construct — inclusion is by form: every attribute the tag spells appears, repeated, unknown, and spread attributes included, their invalidity a located finding (14), never a view omission; and, each per 11.2 (defined, or explicitly unavailable), its node identity, interpreted tags, and coverage attribute, plus — with `--text` — its own and subtree text. A root's tags and coverage attribute are structurally absent (5.5, 11.1): reported as absent, never as unavailable — no finding, no exit-1 consequence; +* the root node and the full section tree in document order. The tree is positional — defined by construct nesting alone — and exists for every parseable file, whatever findings the file carries; its nodes are the root and every section construct of the file, wherever it stands — a section nested inside any non-section construct (an invalid element of 14.16 included) parents to the innermost enclosing section construct, the root when none encloses it: the same enclosure 11.2's chain conditions read. Each node carries its construct range (1.7); its raw attribute spellings as parsed, one entry per attribute the tag spells, in tag order — each entry the attribute's name as spelled (structurally absent for a spread attribute), its source range (1.7), and its source text: the attribute's own characters, for a named attribute its name through the last character of its value or the bare name where it spells no value, for a spread attribute (2.7) its entire braced construct — inclusion is by form: every attribute the tag spells appears, repeated, unknown, and spread attributes included, their invalidity a located finding (14), never a view omission; and, each per 11.2 (defined, or explicitly unavailable), its node identity, interpreted tags, and coverage attribute, plus — with `--text` — its own and subtree text. A root's tags and coverage attribute are structurally absent (5.5, 11.1): reported as absent, never as unavailable — no finding, no exit-1 consequence; * for each non-root node, the decomposition of its construct range: the opening tag's range and the closing tag's range — a self-closing section has an opening-tag range only, a root node neither; -* every import declaration, valid or invalid, with its source range, its binding name — structurally absent when the declaration binds none, reported as absent, never as unavailable — and its resolved target file where specifier form and discovery define one, explicitly unavailable otherwise (11.2), the invalidity itself a located finding (14); +* every import declaration, valid or invalid, with its source range, its binding name — the identifier the declaration binds as its default binding; structurally absent when it binds no default, the invalid side-effect-only, named-only, and namespace-only forms (2.1) alike, an identifier bound by any non-default clause never being this datum — reported as absent, never as unavailable — and its resolved target file where specifier form and discovery define one, explicitly unavailable otherwise (11.2), the invalidity itself a located finding (14); * every reference occurrence in the file (5.7), in document order; * every MDX comment's source range. @@ -625,11 +625,11 @@ xspec inventory Reports the machine-readable shape of the workspace, so an external tool never edits files xspec owns and never misses files xspec reads. The inventory parses no sources, so it answers whatever the sources' validity; configuration errors keep their precedence (14.14). It never refreshes or writes anything, and it reports: -* **Anchoring.** The workspace root and the configuration file, identified relative to the invocation working directory — pure invocation input, exactly as `--config` resolution is (7, 12.0) — so a consumer can map the workspace-relative paths in every output to real files without re-implementing the upward search. Only when the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives) is the anchoring reported in the platform's absolute form — the sole absolute-path case (12.0), still a pure function of invocation input. +* **Anchoring.** The workspace root and the configuration file, identified relative to the invocation working directory — pure invocation input, exactly as `--config` resolution is (7, 12.0) — so a consumer can map the workspace-relative paths in every output to real files without re-implementing the upward search. The spelling is canonical: the segments ascending from the working directory to the nearest common ancestor, each spelled `..`, then the segments descending to the identified file or directory, joined with `/` on every platform — no `.` segments, no trailing separator — and the working directory itself spelled `.`. Only when the platform admits no relative path between the working directory and the workspace root (roots on different Windows drives) is the anchoring reported in the platform's absolute form, drive-qualified in the platform's own spelling — the sole absolute-path case and the sole output spelling whose separator is the platform's (12.0), still a pure function of invocation input. * **Configuration.** The resolved configuration view: the spec and code groups with their glob lists and kinds; Markdown emission state and destinations (7.3); and the coverage profiles and policy rules, each carried with its complete definition, never as a bare name. A group reference inside a profile or rule stays the configured group name, resolving against the group list this same view reports. * **Sources.** Every discovered source file with its group memberships. -* **Derived-file map.** Per discovered spec source: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run. Beside it, one record-supplied datum: the recorded derived-file paths (13.3) — the paths as last generated, companions included, each companion attributable to its source through the naming scheme of 13.1 — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. -* **Graph-data area.** The location under which graph data is kept (13.3), reported unconditionally — the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. +* **Derived-file map.** Per discovered spec source: the generated module path (13.1) and, while emission is enabled, the Markdown emit destination (7.3) — determined by configuration and discovery, existing whether or not emission has yet run; for a spec-group file without the `.mdx` extension (14.19), which generates and emits nothing (13.1), both are structurally absent (12.7). Beside it, one record-supplied datum: the recorded derived-file paths (13.3) — the paths as last generated, companions included, each companion attributable to its source through the naming scheme of 13.1 — reported as recorded: recorded state can lag configuration until a rebuild and is empty before any generation has run. +* **Graph-data area.** The location under which graph data is kept (13.3) — the `.xspec` directory, spelled as its workspace-relative path with no trailing separator — reported unconditionally: the record can lag or be empty, but a consumer must know the area before any build has run. The area's classification is a write reservation, not per-occupant ownership: the area is reserved for xspec's writes — a derived-file write there replaces whatever occupies its path (13.4) — so an external tool must never create, edit, or keep content of its own anywhere under it. Individual paths under the area are classified exactly as the inventory reports them: the durable paths below are durable, and a recorded derived path lying under the area is derived. Every other path under the area is unattributed: it may equally be xspec's graph data — derived, rebuild-recoverable, returning with the next successful build or read-time refresh — or foreign content, recorded nowhere and reproduced by nothing. The inventory neither lists such a path, nor claims it for xspec, nor says which case holds: telling them apart is precisely what it declines to enable, and an external tool must treat every unattributed path as undeletable, because it cannot exclude the foreign case. For the same reason the area is never presented as a deletable or wholesale-regenerable unit: the durable files inside it are neither, and whether any particular unattributed path would return is unknowable from the inventory. * **Durable files.** The journal path (6.1), with whether anything presently occupies it — an absent journal is an empty journal (6.1), and occupancy is presence alone, whatever kind of filesystem object occupies the path: the inventory reads no journal content. And the review-session files: every directory entry directly under the review-session directory (10.1) whose name is a well-formed session file name, selected by name alone, whatever kind of filesystem object occupies it — a session-named path holding anything but a plain file is a corrupt session (10.1), and corrupt or unparseable sessions are listed, since the inventory reads no session content. An entry there with any other name is not a session and is never listed: it is an unattributed path under the area, governed by the rule above. Inventory lists are ordered deterministically: files and paths in byte order of workspace-relative path, groups, profiles, and rules in configuration order, and session files in byte order of file name. @@ -659,7 +659,7 @@ Parses configured sources; validates section structure, IDs, tags, and reference ### 12.2 `xspec check` -Performs all build validations without accepting stale outputs, and additionally verifies: generated files are content-identical to what the current sources and configuration generate, and no recorded derived file remains at a path no longer generated (14.10); all dependency and text references resolve and are static; all TypeScript spec references resolve; no dependency cycles and no spec import cycles exist; the journal is well-formed and replayable with no conflicting mappings; no policy violations exist; review sessions are not internally corrupt. Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. +Performs all build validations without accepting stale outputs, and additionally verifies: generated files are content-identical to what the current sources and configuration generate, no recorded derived file remains at a path no longer generated, and the recorded generation state is readable as a record (14.10, 14.23); all dependency and text references resolve and are static; all TypeScript spec references resolve; no dependency cycles and no spec import cycles exist; the journal is well-formed and replayable with no conflicting mappings; no policy violations exist; review sessions are not internally corrupt. Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. ### 12.3 `xspec ids` @@ -692,16 +692,16 @@ Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise, then by concerned path (`null` before any path), then by identities, compared element-wise, then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). -* The exit-2 error document (12.0) is `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared the same way, then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). +* The exit-2 error document (12.0) is `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. One invocation reports one error: the document holds a single finding however many defects are present — a configuration file with several distinct defects is one condition-14 finding, its message deterministic (12.0) but otherwise unpinned. * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. Document forms — each a single JSON document whose top level is an object; every one below except `version`'s carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: * `occurrences` (11.3): `{"findings", "occurrences"}` — occurrence records in occurrence order (5.7). -* `view` (11.4): `{"findings", "views"}` — one `{"file", "root", "imports", "occurrences", "comments"}` per parseable requested file, ordered by file path bytes; an unparseable requested file contributes no entry (11.4). Each node of the section tree is `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus, exactly when `--text` is given, `"ownText"` and `"subtreeText"`: `identity`, `tags`, `coverage`, and the text members are each a plain value, `null` where 11.4 defines structural absence, or unavailable (11.2); `opening` and `closing` are the tag ranges of 11.4, `null` where none exists; `attributes` is one `{"name", "range", "text"}` per spelled attribute in tag order, `name` `null` for a spread attribute; `children` holds the child nodes in document order, and `root` the root node. `imports` is one `{"range", "name", "target"}` per import declaration in document order, `name` `null` where the declaration binds none, `target` a path or unavailable; `occurrences` holds the file's occurrence records and `comments` the comment ranges, each in document order. +* `view` (11.4): `{"findings", "views"}` — one `{"file", "root", "imports", "occurrences", "comments"}` per parseable requested file, ordered by file path bytes; an unparseable requested file contributes no entry (11.4). Each node of the section tree is `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus, exactly when `--text` is given, `"ownText"` and `"subtreeText"`: `identity`, `tags`, `coverage`, and the text members are each a plain value, `null` where 11.4 defines structural absence, or unavailable (11.2); `opening` and `closing` are the tag ranges of 11.4, `null` where none exists; `attributes` is one `{"name", "range", "text"}` per spelled attribute in tag order, `name` `null` for a spread attribute; `children` holds the child nodes in document order, and `root` the root node. `imports` is one `{"range", "name", "target"}` per import declaration in document order, `name` `null` where the declaration binds no default binding (11.4), `target` a path or unavailable; `occurrences` holds the file's occurrence records and `comments` the comment ranges, each in document order. * `at` (11.5): `{"findings", "resolution"}` — `resolution` is `{"section", "occurrence"}` or unavailable (11.5): `section` is `{"identity", "range"}` of the innermost enclosing section construct, its identity per 11.2; `occurrence` is the containing occurrence's record, `null` when the offset lies within none. -* `inventory` (11.6): `{"findings", "root", "config", "configuration", "sources", "derived", "recorded", "graphData", "journal", "sessions"}`. `root` and `config` are the anchoring paths (11.6). `configuration` is `{"specs", "code", "markdown", "coverage", "policy"}`, the resolved view (11.6) with every default and inferred kind explicit: `specs` and `code` one `{"name", "globs"}` per group; `markdown` `{"emit", "outDir"}`, `outDir` `null` where unset and an absent `markdown` key resolving to `{"emit": false, "outDir": null}` (7.3); `coverage` one `{"name", "target", "targetTags", "targets", "boundary", "boundaryKind", "mode", "edgeKinds"}` per profile, `targetTags` `null` where absent; `policy` one `{"name", "type", "from", "to", "kinds"}` per rule, each selector `{"group", "kind"}`, `{"files"}`, or `{"tags"}` (7.5). `sources` is one `{"path", "groups"}` per discovered file, `groups` one `{"name", "kind"}` each; `derived` one `{"source", "module", "markdown"}` per discovered spec source, `markdown` `null` while emission is disabled (7.3); `recorded` the record-supplied datum (11.6) — the recorded derived-file paths in byte order — or unavailable (14.23); `graphData` the graph-data area's path; `journal` `{"path", "occupied"}`, `occupied` a boolean; `sessions` the session file paths. List order follows 11.6. +* `inventory` (11.6): `{"findings", "root", "config", "configuration", "sources", "derived", "recorded", "graphData", "journal", "sessions"}`. `root` and `config` are the anchoring paths (11.6). `configuration` is `{"specs", "code", "markdown", "coverage", "policy"}`, the resolved view (11.6) with every default and inferred kind explicit: `specs` and `code` one `{"name", "globs"}` per group; `markdown` `{"emit", "outDir"}`, `outDir` `null` where unset and an absent `markdown` key resolving to `{"emit": false, "outDir": null}` (7.3); `coverage` one `{"name", "target", "targetTags", "targets", "boundary", "boundaryKind", "mode", "edgeKinds"}` per profile, `targetTags` `null` where absent; `policy` one `{"name", "type", "from", "to", "kinds"}` per rule, each selector `{"group", "kind"}`, `{"files"}`, or `{"tags"}` (7.5). `sources` is one `{"path", "groups"}` per discovered file, `groups` one `{"name", "kind"}` each; `derived` one `{"source", "module", "markdown"}` per discovered spec source — `module` and `markdown` `null` for a spec-group file without the `.mdx` extension (11.6, 13.1), `markdown` `null` also while emission is disabled (7.3); `recorded` the record-supplied datum (11.6) — the recorded derived-file paths in byte order — or unavailable (14.23); `graphData` the graph-data area's path; `journal` `{"path", "occupied"}`, `occupied` a boolean; `sessions` the session file paths. List order follows 11.6. * `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes; each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). * `version` (12.6): `{"product", "interface"}` — the product version and the machine-interface version, both strings; the reported machine-interface value is the string form of 12.6's stated value, `"1"`. @@ -709,7 +709,7 @@ Document forms — each a single JSON document whose top level is an object; eve ### 13.1 Generated TypeScript -`NAME.mdx` generates, in the source file's directory, the TypeScript module `NAME.xspec.ts`, beginning with the generated-file header (4), together with whatever companion files beside it are needed so that the specifier `./NAME.xspec` resolves for consumers: type checking (4.1), hover documentation and go-to-definition into the source `.mdx` (4.2), and runtime behavior (4.3–4.5) MUST all hold under standard TypeScript tooling with no xspec runtime dependency. Every companion file is named `NAME.xspec.` plus a suffix, so the module and all companions carry `.xspec.` in their names and are derived files under the source-discovery exclusion (13.4). +`NAME.mdx` generates, in the source file's directory, the TypeScript module `NAME.xspec.ts`, beginning with the generated-file header (4), together with whatever companion files beside it are needed so that the specifier `./NAME.xspec` resolves for consumers: type checking (4.1), hover documentation and go-to-definition into the source `.mdx` (4.2), and runtime behavior (4.3–4.5) MUST all hold under standard TypeScript tooling with no xspec runtime dependency. Every companion file is named `NAME.xspec.` plus a suffix, so the module and all companions carry `.xspec.` in their names and are derived files under the source-discovery exclusion (13.4). Per-source derived paths are defined by this `NAME.mdx` name shape alone: a spec-group file without the `.mdx` extension (14.19) generates no module and emits no Markdown (13.2) — it has no generated-module path and no Markdown emit destination (7.3, 11.6). ### 13.2 Markdown output @@ -717,7 +717,7 @@ Document forms — each a single JSON document whose top level is an object; eve ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, reference occurrences (5.7), source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files @@ -740,7 +740,7 @@ All state is workspace-local; instances operating on different workspaces MUST N Every reported condition carries a stable machine-readable code identifying which numbered condition it is: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. -Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory. The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). +Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory, spelled `.` (11.6). The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). The defined error conditions — also the findings that accompany answers over a consulted domain (11.2, 11.6, 6.6) — each reported by `build` and `check` unless its entry states otherwise: @@ -753,7 +753,7 @@ The defined error conditions — also the findings that accompany answers over a 7. Unknown TypeScript reference: a marker or `text` call that does not resolve; this is also a type error against the generated module. 8. Invalid argument: a `d` or `text(...)` reference that is not static per 2.4, a non-static bare reference in TypeScript expression-statement position (4.5), a `text(...)` call without exactly one argument, or a string-form `text(...)` argument in a TypeScript file (4.3). 9. Cycle: a dependency cycle (with the full path) or a spec import cycle. -10. Stale generated output: a derived file whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate; the error names the file and instructs rebuilding. Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). +10. Stale generated output: a derived file whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding — or recorded generation state that exists but cannot be read as a record (14.23): one finding, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named; while that state holds, this form alone reports the graph data, and the recorded-file form above, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. 12. Policy violation: rule name plus offending edge. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). From 32b5bd4ea7947309b7102515821ad48bb7346719 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 22:40:17 +0000 Subject: [PATCH 23/35] sdg(phase-4): pin preview reporting for created files, finding-order comparators, JSON contract details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 6 of applying IP 0001. Applied: C1, I1, O1, O2, O3, O4, O5, O6. - C1: 6.6/6.5 — a created target file's entry reports its creation as its only edit, subsuming the insertion and the import additions composed into the file's initial content (per the IP's "one reported location without pre-operation coordinates"); the moved text's own rewrites stay reported in the origin file; 6.5's preview-offset promise scoped to files existing before the operation. - I1: 12.7 — between-findings `locations` ordering given its element comparator (file path bytes, range start, range end); identities' element rule spelled out alongside. - O1: 14/12.7 — a stable code's value pinned as the listed token string; numerals are ordinals for ordering, no part of the value. - O2: 12.7 — preview edit tiebreak pinned to class-name bytes. - O3: 11.2 — membership rule for a domain file's findings (a location in the file, or the file as concerned path); joint multi-file findings accompany whole when any participant is in the domain. - O4: 6.6 — a refused preview consults no record; no condition-23 finding accompanies a refusal. - O5: 11.5 — a non-UTF-8-path source is nameable by no argument value; its positions are reachable only through the view by glob. - O6: 12.7 — exit-2 error document moved under Document forms, lead exception amended. Rejected: O7 — modularization is a PROCESS SHOULD, not violated at the current size; the reviewer's own framing conditions extraction on future growth, and restructuring at convergence risks breakage with no requirement gain. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 623bd79..32b5ac1 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -314,7 +314,7 @@ xspec move [--preview] xspec move # # [--preview] ``` -The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and the offset is exactly the one the operation's preview reports (6.6). A successful move regenerates derived files as rename does (6.4). +The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files as rename does (6.4). Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination file already exists; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). @@ -323,8 +323,8 @@ Move validation mirrors rename validation, including the valid-workspace precond `xspec rename … --preview` and `xspec move … --preview` perform the full validation and planning of the operation and report its consequences while modifying nothing: no sources, no journal, no derived files, no graph data. A preview is refused exactly when — reporting what, and exiting as — the real operation would be refused, and succeeds exactly when the real operation would proceed. The equivalence is over workspace state (validation and planning), never over scheduling: the mutual-exclusion refusal of 13.5 applies to the real operation only. A preview invocation is a non-mutating command under 13.5 — it acquires no workspace exclusivity and is safe to run while readers run — and does not take the acquisition-tied test seam: supplying `--test-hold` together with `--preview` is a usage error (12.0). Preview output is byte-deterministic (12.0), supports `--json` per 12.0 — the preview document form of 12.7 — and reports: * the complete identity mapping the operation would journal; -* every file the operation would rewrite or relocate, with every edit the operation would make in it, each located by a source range (1.7) in current, pre-operation coordinates and classed as exactly one of: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file, the one reported location without pre-operation coordinates. A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; -* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14): the delta is reported explicitly unavailable — never fabricated, never read as an empty record — the finding accompanies the report with its stable code, the invocation exits 1 (12.0), and every other part of the report is emitted in full. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. +* every file the operation would rewrite, relocate, or create, with every edit the operation would make in it, classed as exactly one of the following and — target-file creation excepted — located by a source range (1.7) in current, pre-operation coordinates: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file: the one reported location without pre-operation coordinates, and the created file's only reported edit — creation composes the file's entire initial content, subsuming the insertion and the import additions the rewrite requires there (6.5), edits no pre-operation coordinates exist to locate, while the moved text's own rewrites are reported in the origin file, inside the origin deletion's range (below). A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; +* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14): the delta is reported explicitly unavailable — never fabricated, never read as an empty record — the finding accompanies the report with its stable code, the invocation exits 1 (12.0), and every other part of the report is emitted in full. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. A refused preview consults no record — it reports the refusal findings alone, its `mapping`, `files`, and `delta` `null` (12.7) — so no condition-23 finding ever accompanies a refusal. ### 6.7 Manual restructuring @@ -575,7 +575,7 @@ xspec query reachable --from --to [--kinds ] **Unavailability is explicit.** A datum these rules leave undefined — a node's identity (a section's under the spelling rules above; every node's in a file whose path is invalid); a section's tags or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7), the enclosing construct's position staying on view through 11.4; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. -**Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. +**Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. A finding is a domain file's exactly when one of its locations (14) lies in that file or that file is its concerned path (14.19); a condition several files jointly violate — a cross-file cycle (14.9) — accompanies the answer, whole, whenever any participating file lies in the domain. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. **Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes the validations of `xspec build` (12.1) they participate in read-time refresh exactly as the reads of 13.3 do; on one that fails them — source validation errors, journal errors (14.13), and refused writes (14.22) alike (13.3) — they answer from the current sources and modify nothing: no graph data, no derived files. A gate condition that is a finding of no domain file — the journal's (14.13), a write path's (14.22) — accompanies no answer of these surfaces: on the failing side these answers consult no journal and no record and write nothing, and on the passing side no such finding exists. An answer's findings are its domain files' findings alone, and a complete, finding-free answer exits 0 (above) whatever journal or write-path state the workspace holds. @@ -615,7 +615,7 @@ With tags, imports, comments, and embedding occurrences located — an embedding xspec at ``` -Resolves a byte position in a discovered spec source: the innermost section construct whose range (1.7) contains the offset — the root when no narrower section does — reported with its construct range and, per 11.2, its node identity; and, when the offset lies within a reference occurrence's range, that occurrence and its resolved target (5.7). `` asserts domain membership exactly as a `view` operand does (11.4). Resolution is by range containment and total over the file: every within-file offset resolves — bytes inside imports, comments, and content between sections resolve to the innermost enclosing section construct — and the offset equal to the file's byte length (the caret position at end of file) resolves to the root. A greater offset is a usage error (12.0); so is an `` spelled as anything but one or more ASCII decimal digits, read in decimal — leading zeros permitted; a sign, whitespace, or any other character is not a non-negative integer's spelling. The same resolution is derivable from the view's data alone (11.4): `at` adds convenience, not information, serving consumers that keep no client-side index. The consulted domain (11.2) is the named file; on an unparseable file the resolution is reported explicitly unavailable, the parse-failure finding accompanying it (11.2). +Resolves a byte position in a discovered spec source: the innermost section construct whose range (1.7) contains the offset — the root when no narrower section does — reported with its construct range and, per 11.2, its node identity; and, when the offset lies within a reference occurrence's range, that occurrence and its resolved target (5.7). `` asserts domain membership exactly as a `view` operand does (11.4). Resolution is by range containment and total over the file: every within-file offset resolves — bytes inside imports, comments, and content between sections resolve to the innermost enclosing section construct — and the offset equal to the file's byte length (the caret position at end of file) resolves to the root. A greater offset is a usage error (12.0); so is an `` spelled as anything but one or more ASCII decimal digits, read in decimal — leading zeros permitted; a sign, whitespace, or any other character is not a non-negative integer's spelling. The same resolution is derivable from the view's data alone (11.4): `at` adds convenience, not information, serving consumers that keep no client-side index. A discovered spec source whose path is not valid UTF-8 is nameable by no argument value (12.0), so `at` cannot address it: for such a file (14.19) the view, reached by glob (11.4), is the one route to position data. The consulted domain (11.2) is the named file; on an unparseable file the resolution is reported explicitly unavailable, the parse-failure finding accompanying it (11.2). ### 11.6 `xspec inventory` @@ -692,18 +692,18 @@ Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared the same way, then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). -* The exit-2 error document (12.0) is `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. One invocation reports one error: the document holds a single finding however many defects are present — a configuration file with several distinct defects is one condition-14 finding, its message deterministic (12.0) but otherwise unpinned. +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. -Document forms — each a single JSON document whose top level is an object; every one below except `version`'s carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: +Document forms — each a single JSON document whose top level is an object; every one below except `version`'s and the exit-2 error document carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: * `occurrences` (11.3): `{"findings", "occurrences"}` — occurrence records in occurrence order (5.7). * `view` (11.4): `{"findings", "views"}` — one `{"file", "root", "imports", "occurrences", "comments"}` per parseable requested file, ordered by file path bytes; an unparseable requested file contributes no entry (11.4). Each node of the section tree is `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus, exactly when `--text` is given, `"ownText"` and `"subtreeText"`: `identity`, `tags`, `coverage`, and the text members are each a plain value, `null` where 11.4 defines structural absence, or unavailable (11.2); `opening` and `closing` are the tag ranges of 11.4, `null` where none exists; `attributes` is one `{"name", "range", "text"}` per spelled attribute in tag order, `name` `null` for a spread attribute; `children` holds the child nodes in document order, and `root` the root node. `imports` is one `{"range", "name", "target"}` per import declaration in document order, `name` `null` where the declaration binds no default binding (11.4), `target` a path or unavailable; `occurrences` holds the file's occurrence records and `comments` the comment ranges, each in document order. * `at` (11.5): `{"findings", "resolution"}` — `resolution` is `{"section", "occurrence"}` or unavailable (11.5): `section` is `{"identity", "range"}` of the innermost enclosing section construct, its identity per 11.2; `occurrence` is the containing occurrence's record, `null` when the offset lies within none. * `inventory` (11.6): `{"findings", "root", "config", "configuration", "sources", "derived", "recorded", "graphData", "journal", "sessions"}`. `root` and `config` are the anchoring paths (11.6). `configuration` is `{"specs", "code", "markdown", "coverage", "policy"}`, the resolved view (11.6) with every default and inferred kind explicit: `specs` and `code` one `{"name", "globs"}` per group; `markdown` `{"emit", "outDir"}`, `outDir` `null` where unset and an absent `markdown` key resolving to `{"emit": false, "outDir": null}` (7.3); `coverage` one `{"name", "target", "targetTags", "targets", "boundary", "boundaryKind", "mode", "edgeKinds"}` per profile, `targetTags` `null` where absent; `policy` one `{"name", "type", "from", "to", "kinds"}` per rule, each selector `{"group", "kind"}`, `{"files"}`, or `{"tags"}` (7.5). `sources` is one `{"path", "groups"}` per discovered file, `groups` one `{"name", "kind"}` each; `derived` one `{"source", "module", "markdown"}` per discovered spec source — `module` and `markdown` `null` for a spec-group file without the `.mdx` extension (11.6, 13.1), `markdown` `null` also while emission is disabled (7.3); `recorded` the record-supplied datum (11.6) — the recorded derived-file paths in byte order — or unavailable (14.23); `graphData` the graph-data area's path; `journal` `{"path", "occupied"}`, `occupied` a boolean; `sessions` the session file paths. List order follows 11.6. -* `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes; each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). +* `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes; each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class-name bytes; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). * `version` (12.6): `{"product", "interface"}` — the product version and the machine-interface version, both strings; the reported machine-interface value is the string form of 12.6's stated value, `"1"`. +* The exit-2 error document (12.0): `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. One invocation reports one error: the document holds a single finding however many defects are present — a configuration file with several distinct defects is one condition-14 finding, its message deterministic (12.0) but otherwise unpinned. ## 13. Workspace Files @@ -738,7 +738,7 @@ All state is workspace-local; instances operating on different workspaces MUST N `xspec build` and `xspec check` MUST report actionable errors that identify the file, location, and correction. When several error conditions are present, they MUST report each of them, not only the first; a condition goes unreported only where another error makes it undetectable — an unparseable file (14.20) masks the conditions inside itself, and a reference into it reports as unresolved (14.5–14.7) — and a configuration error (14.14) precedes all source analysis. -Every reported condition carries a stable machine-readable code identifying which numbered condition it is: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. +Every reported condition carries a stable machine-readable code identifying which numbered condition it is — a code's value is its token as listed, a string (12.7), and a numeral below is the condition's ordinal, ordering findings (12.7), no part of the value: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory, spelled `.` (11.6). The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). From 1df1cf6a539a692d953f61b1bf86931b779d6ddb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 23:04:20 +0000 Subject: [PATCH 24/35] sdg(phase-4): pin policy-violation finding form and invalid-workspace refusal scope (iter 7) Applied: - I1: condition 12 carries no in-source locations and no concerned path (no file's finding per 11.2, so it accompanies no 11.3-11.5 answer, closing 7.5's check-only routing structurally); identities pinned as rule name, edge source, kind token, target; 14's cardinality paragraph and 12.7's identities description widened to match. - I2: the invalid-workspace refusal precedes the operation-specific validation, which is defined and evaluated only over a workspace passing build's validations - the refusal reports the workspace's findings alone, never mixed with refusal reasons (6.4, 14). - O2: 12.2 restated build validations under the build-validations clause so the "additionally" list holds only check-only verifications. - O3: preview `files` entries keyed by current pre-operation path, the relocated file's entry included; created target file by its new path. - O4: 13.3's recorded derived-file paths scoped to generated modules, companions, and emitted Markdown - graph data records no paths, its layout staying unenumerated. Rejected: - O1 (modularization): deferred deliberately - candidate components are bound to the core by dense numbered cross-references (5.4-5.6, 12.0, 12.7, 14) whose contracts could not move to modules without the restatement module rules forbid; the SHOULD is weighed, not dropped. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 32b5ac1..0135908 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -305,7 +305,7 @@ When a command takes a baseline git ref, the baseline graph is reconstructed fro xspec rename [--preview] ``` -Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0). A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. +Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. ### 6.5 Move @@ -659,7 +659,7 @@ Parses configured sources; validates section structure, IDs, tags, and reference ### 12.2 `xspec check` -Performs all build validations without accepting stale outputs, and additionally verifies: generated files are content-identical to what the current sources and configuration generate, no recorded derived file remains at a path no longer generated, and the recorded generation state is readable as a record (14.10, 14.23); all dependency and text references resolve and are static; all TypeScript spec references resolve; no dependency cycles and no spec import cycles exist; the journal is well-formed and replayable with no conflicting mappings; no policy violations exist; review sessions are not internally corrupt. Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. +Performs all build validations (12.1, 14) — unresolved and non-static references, dependency and spec import cycles, and journal errors included — without accepting stale outputs, and additionally verifies what `build` does not: generated files are content-identical to what the current sources and configuration generate, no recorded derived file remains at a path no longer generated, and the recorded generation state is readable as a record (14.10, 14.23); no policy violations exist (14.12); review sessions are not internally corrupt (14.21). Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. ### 12.3 `xspec ids` @@ -692,7 +692,7 @@ Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or paths the condition names as context (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or other context strings the condition names (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. Document forms — each a single JSON document whose top level is an object; every one below except `version`'s and the exit-2 error document carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: @@ -701,7 +701,7 @@ Document forms — each a single JSON document whose top level is an object; eve * `view` (11.4): `{"findings", "views"}` — one `{"file", "root", "imports", "occurrences", "comments"}` per parseable requested file, ordered by file path bytes; an unparseable requested file contributes no entry (11.4). Each node of the section tree is `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus, exactly when `--text` is given, `"ownText"` and `"subtreeText"`: `identity`, `tags`, `coverage`, and the text members are each a plain value, `null` where 11.4 defines structural absence, or unavailable (11.2); `opening` and `closing` are the tag ranges of 11.4, `null` where none exists; `attributes` is one `{"name", "range", "text"}` per spelled attribute in tag order, `name` `null` for a spread attribute; `children` holds the child nodes in document order, and `root` the root node. `imports` is one `{"range", "name", "target"}` per import declaration in document order, `name` `null` where the declaration binds no default binding (11.4), `target` a path or unavailable; `occurrences` holds the file's occurrence records and `comments` the comment ranges, each in document order. * `at` (11.5): `{"findings", "resolution"}` — `resolution` is `{"section", "occurrence"}` or unavailable (11.5): `section` is `{"identity", "range"}` of the innermost enclosing section construct, its identity per 11.2; `occurrence` is the containing occurrence's record, `null` when the offset lies within none. * `inventory` (11.6): `{"findings", "root", "config", "configuration", "sources", "derived", "recorded", "graphData", "journal", "sessions"}`. `root` and `config` are the anchoring paths (11.6). `configuration` is `{"specs", "code", "markdown", "coverage", "policy"}`, the resolved view (11.6) with every default and inferred kind explicit: `specs` and `code` one `{"name", "globs"}` per group; `markdown` `{"emit", "outDir"}`, `outDir` `null` where unset and an absent `markdown` key resolving to `{"emit": false, "outDir": null}` (7.3); `coverage` one `{"name", "target", "targetTags", "targets", "boundary", "boundaryKind", "mode", "edgeKinds"}` per profile, `targetTags` `null` where absent; `policy` one `{"name", "type", "from", "to", "kinds"}` per rule, each selector `{"group", "kind"}`, `{"files"}`, or `{"tags"}` (7.5). `sources` is one `{"path", "groups"}` per discovered file, `groups` one `{"name", "kind"}` each; `derived` one `{"source", "module", "markdown"}` per discovered spec source — `module` and `markdown` `null` for a spec-group file without the `.mdx` extension (11.6, 13.1), `markdown` `null` also while emission is disabled (7.3); `recorded` the record-supplied datum (11.6) — the recorded derived-file paths in byte order — or unavailable (14.23); `graphData` the graph-data area's path; `journal` `{"path", "occupied"}`, `occupied` a boolean; `sessions` the session file paths. List order follows 11.6. -* `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes; each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class-name bytes; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). +* `rename`/`move` previews (6.6): `{"findings", "mapping", "files", "delta"}`; on refusal `mapping`, `files`, and `delta` are `null`. `mapping` is one `{"from", "to"}` per mapped identity, ordered by `from` bytes. `files` is one `{"file", "edits"}` per file the operation would rewrite, relocate, or create, ordered by file path bytes — `file` the file's current, pre-operation path, the relocated file's entry included, and for target-file creation the path the creation would occupy (6.6); each edit is `{"class", "range"}`, edits ordered by range start, then range end, then class-name bytes; `class` names, in order, the classes of 6.6: `"reference-rewrite"`, `"id-rewrite"`, `"import-specifier-rewrite"`, `"import-addition"`, `"import-removal"`, `"origin-deletion"`, `"target-insertion"`, `"target-parent-rewrite"`, `"file-relocation"`, or `"file-creation"`. `delta` is `{"generated", "removed"}`, each direction's paths in byte order, or unavailable as one datum (6.6). * `version` (12.6): `{"product", "interface"}` — the product version and the machine-interface version, both strings; the reported machine-interface value is the string form of 12.6's stated value, `"1"`. * The exit-2 error document (12.0): `{"error": …}` holding one finding form: for a configuration error, its stable code and concerned path (14); for a plain usage error, `code` and `path` `null`. One invocation reports one error: the document holds a single finding however many defects are present — a configuration file with several distinct defects is one condition-14 finding, its message deterministic (12.0) but otherwise unpinned. @@ -717,7 +717,7 @@ Document forms — each a single JSON document whose top level is an object; eve ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, reference occurrences (5.7), source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, reference occurrences (5.7), source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4) — the generated modules with their companions (13.1) and the emitted Markdown (13.2); graph data records no paths of its own, its layout staying deliberately unenumerated (11.6). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files @@ -740,7 +740,7 @@ All state is workspace-local; instances operating on different workspaces MUST N Every reported condition carries a stable machine-readable code identifying which numbered condition it is — a code's value is its token as listed, a string (12.7), and a numeral below is the condition's ordinal, ordering findings (12.7), no part of the value: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. -Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern. A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory, spelled `.` (11.6). The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). +Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern; a policy violation, constraining an edge rather than any file's content, carries neither location nor concerned path — its context identities alone (12). A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory, spelled `.` (11.6). The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). The defined error conditions — also the findings that accompany answers over a consulted domain (11.2, 11.6, 6.6) — each reported by `build` and `check` unless its entry states otherwise: @@ -755,7 +755,7 @@ The defined error conditions — also the findings that accompany answers over a 9. Cycle: a dependency cycle (with the full path) or a spec import cycle. 10. Stale generated output: a derived file whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding — or recorded generation state that exists but cannot be read as a record (14.23): one finding, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named; while that state holds, this form alone reports the graph data, and the recorded-file form above, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. -12. Policy violation: rule name plus offending edge. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). +12. Policy violation: one finding per violation (7.5) — per rule and offending edge. The offending entity is a graph edge, not a spelling: the finding carries no in-source locations and concerns no path — `locations` empty, `path` `null` (12.7) — so it is no file's finding (11.2) and accompanies no answer of 11.3–11.5. Its `identities` are, in order, the violated rule's name and the edge's source identity, kind token (12.7), and target identity. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). 14. Configuration error: missing or invalid configuration — a configuration file that is not well-formed TypeScript or not in the declarative form of 7; missing required fields, unknown keys (7), or invalid profile, rule, or group shapes; group names referenced by profiles, rules, or selectors that are unknown or not of the kind the reference requires (7.4, 7.5); ambiguous kinds (7.4, 7.5); an empty `edgeKinds`, `targetTags`, rule `kinds`, or selector `tags` list; a capture violation (7.5); a glob or `markdown.outDir` resolving outside the workspace root (7, 7.3); a file matched by both a spec and a code group. Reported by every command that loads the configuration — every command but `version` (12.6), which loads none — when it loads the configuration and discovers sources, as a usage error (12.0), not a finding. 15. Invalid import: in an xspec source file, an import that is not a single default binding, does not designate an xspec source file belonging to a configured spec group, or binds the identifier `S`, `Spec`, or `text` (2.1); in a TypeScript file, a `.xspec` import that does not designate such a source, a spec-module binding other than the default and `text` exports, a dynamic `import()` whose static specifier ends in `.xspec`, an export declaration or an `import X = require(…)` declaration whose specifier ends in `.xspec` (4), or an import or export declaration, `import X = require(…)`, or dynamic `import()` whose relative specifier designates a derived-file path other than through a spec module import's `.xspec` specifier (4, 13.4); in either kind of file, an import binding an identifier already bound by another import in the same file, when either import is a spec module import. @@ -768,7 +768,7 @@ The defined error conditions — also the findings that accompany answers over a 22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). 23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). -Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code. +Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. ## 15. Example From b2094c4a9ddabee9ee66da994c8d044cd3644562 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 15:24:43 +0000 Subject: [PATCH 25/35] sdg(phase-4): pin old-ID existence datum, disjoint refusal scopes, destination occupancy (iter 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - I1: rename/move old-ID existence judged over spelled identities (11.2), parse-local — undefined-identity bearers still establish existence, no-identity spellings establish none, unparseable origin stays masked (6.4, 6.5). - I2: refused-invalid-id scoped to intrinsic ID form (dot-path of 1.4-valid segments); positional conformance (1.3) is refused-structural-parent's alone, evaluated only over intrinsically valid IDs — no identity reports under both (14). - I3: refused-destination-exists triggers on any occupant of the file-form destination path, whatever kind of filesystem object, symbolic links included (6.5, 14). - O2: a successful non-preview rename/move reports the applied mapping — the preview's mapping information (6.4, 6.5). - O3: finding-form identities content contractual exactly where 14 states it for the condition or reason, otherwise informational — deterministic, composition unpinned (12.7). Not applied: - O1 (modularization): standing deferral upheld. Extracting Review (10) or the query/JSON complex (11, 12.7) requires full interface restatement in SPEC.md per PROCESS module rules; the restructuring risk mid-refinement outweighs the SHOULD at current size. The deferral stays a recorded decision, not an omission. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 0135908..339ca38 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -305,7 +305,7 @@ When a command takes a baseline git ref, the baseline graph is reconstructed fro xspec rename [--preview] ``` -Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. +Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); the old ID's existence is parse-local, judged over spelled identities (11.2): it exists exactly when a section of the origin file spells it — a bearer whose node identity is undefined (duplicate spellings; an undefined ancestor chain, 11.2) still establishes existence, a section spelling no identity (its `id` attribute repeated or in invalid value form, 11.2) establishes none, and an unparseable origin file is masked (12.0). Every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. A successful rename's report is the applied mapping: the complete identity mapping the operation journaled — the information of the preview's `mapping` (6.6), carried in JSON per 12.0 (12.7). ### 6.5 Move @@ -314,9 +314,9 @@ xspec move [--preview] xspec move # # [--preview] ``` -The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files as rename does (6.4). +The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files, and reports its applied mapping, as rename does (6.4). -Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination file already exists; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). +Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification, existence judged as in 6.4, of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). ### 6.6 Previews @@ -692,7 +692,7 @@ Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or other context strings the condition names (14), empty where none. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or other context strings the condition names (14), empty where none — content contractual exactly where 14 states it for the condition or reason (14.12's enumeration, a condition's named context entity such as 14.11's foreign module, a refusal reason's concerned identity), otherwise informational: deterministic (12.0), its composition unpinned. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. Document forms — each a single JSON document whose top level is an object; every one below except `version`'s and the exit-2 error document carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: @@ -768,7 +768,7 @@ The defined error conditions — also the findings that accompany answers over a 22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). 23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). -Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, violates 1.3/1.4, concerning that identity; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination file already exists (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. +Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. ## 15. Example From ccc02b87272370227d9d38bebb88eee2282c2486 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 15:52:20 +0000 Subject: [PATCH 26/35] sdg(phase-4): pin move target occupancy, origin kind, rename collision timing (iter 9) Applied: - C1: section-form move target path occupied by anything other than a discovered spec source is refused under refused-destination-exists (6.5 body and refusal list, 14). - C2: rename/move origin operands pinned to discovered spec sources; a code-source origin is a wrong-kind usage error per 11.4's pattern (6.4, 6.5, 12.0 precedence bullet and exit partition). - I1: rename's collision check pinned post-mapping - vacated IDs are no collision, produced IDs checked too, identity-unchanged reports refused-identity-unchanged alone (6.4, 6.5, 14 refused-id-collision). - O2: 11.2 unavailability parenthetical no longer implies a structural view exists for code sources; occurrence's own range (11.3) named. - O3: unreadable-record outcome consolidated into 14.23 as its one normative home; 6.6 and 11.6 defer to it. - O4: explicit sentence in 14 that a refusal reports every applicable reason together, never only the first found. Rejected: - O1: modularization deferred again - the named candidates (Review, Coverage, validation catalogue) are consumer-facing contract that PROCESS.md requires to stay in SPEC.md itself and forbids modules to restate; extracting them mid-IP refinement would churn every section reference while yielding little movable non-contract content. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 339ca38..2e6560e 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -305,7 +305,7 @@ When a command takes a baseline git ref, the baseline graph is reconstructed fro xspec rename [--preview] ``` -Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID and collides with no existing ID; structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); the old ID's existence is parse-local, judged over spelled identities (11.2): it exists exactly when a section of the origin file spells it — a bearer whose node identity is undefined (duplicate spellings; an undefined ancestor chain, 11.2) still establishes existence, a section spelling no identity (its `id` attribute repeated or in invalid value form, 11.2) establishes none, and an unparseable origin file is masked (12.0). Every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. A successful rename's report is the applied mapping: the complete identity mapping the operation journaled — the information of the preview's `mapping` (6.6), carried in JSON per 12.0 (12.7). +Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID; the new ID, and each ID the prefix replacement produces, collides with no ID remaining in the file once the vacated IDs — the old ID and its descendants' — are removed, exactly as the section move's after-removal check reads (6.5): an identity-unchanged rename therefore collides with nothing and reports `refused-identity-unchanged` alone (14); structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); so is a `` naming a discovered source that is not a spec source — a code source bears no requirement IDs, so a code-source origin is a wrong-kind operand, the usage error of 11.4's pattern (12.0), judged like existence before any content question; the old ID's existence is parse-local, judged over spelled identities (11.2): it exists exactly when a section of the origin file spells it — a bearer whose node identity is undefined (duplicate spellings; an undefined ancestor chain, 11.2) still establishes existence, a section spelling no identity (its `id` attribute repeated or in invalid value form, 11.2) establishes none, and an unparseable origin file is masked (12.0). Every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. A successful rename's report is the applied mapping: the complete identity mapping the operation journaled — the information of the preview's `mapping` (6.6), carried in JSON per 12.0 (12.7). ### 6.5 Move @@ -314,9 +314,9 @@ xspec move [--preview] xspec move # # [--preview] ``` -The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is created if absent, empty before insertion. The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files, and reports its applied mapping, as rename does (6.4). +The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is the discovered spec source occupying the target path when one occupies it; at a target path nothing occupies, the target file is created, empty before insertion; a target path occupied by anything else is refused (below). The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files, and reports its applied mapping, as rename does (6.4). -Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification, existence judged as in 6.4, of a nonexistent origin file or ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, `` collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). +Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification — existence and kind judged as in 6.4, both forms' origin operands naming discovered spec sources — of a nonexistent or wrong-kind origin file or a nonexistent origin ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, ``, and each ID its prefix replacement produces, collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target path is occupied by anything other than a discovered spec source — a directory, a symbolic link (discovery never follows one, 7), or any other occupant discovery does not yield as a spec source: neither an insertion target nor an absent path to create; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). ### 6.6 Previews @@ -324,7 +324,7 @@ Move validation mirrors rename validation, including the valid-workspace precond * the complete identity mapping the operation would journal; * every file the operation would rewrite, relocate, or create, with every edit the operation would make in it, classed as exactly one of the following and — target-file creation excepted — located by a source range (1.7) in current, pre-operation coordinates: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file: the one reported location without pre-operation coordinates, and the created file's only reported edit — creation composes the file's entire initial content, subsuming the insertion and the import additions the rewrite requires there (6.5), edits no pre-operation coordinates exist to locate, while the moved text's own rewrites are reported in the origin file, inside the origin deletion's range (below). A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; -* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14): the delta is reported explicitly unavailable — never fabricated, never read as an empty record — the finding accompanies the report with its stable code, the invocation exits 1 (12.0), and every other part of the report is emitted in full. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. A refused preview consults no record — it reports the refusal findings alone, its `mapping`, `files`, and `delta` `null` (12.7) — so no condition-23 finding ever accompanies a refusal. +* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the delta — with the one outcome 14.23 defines. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. A refused preview consults no record — it reports the refusal findings alone, its `mapping`, `files`, and `delta` `null` (12.7) — so no condition-23 finding ever accompanies a refusal. ### 6.7 Manual restructuring @@ -573,7 +573,7 @@ xspec query reachable --from --to [--kinds ] **Expanded text.** A node's own (respectively subtree) text (1.6) is defined exactly when every embedding the expansion transitively reaches — each `text(...)` spelling in the node's own contribution (respectively anywhere in its subtree), and recursively each one anywhere in every embedded target's subtree — records an occurrence, and the recursion re-enters no node already being expanded (an embedding cycle). One unresolved spelling or one cycle on the expansion path makes the whole value unavailable: partial expansion is fabrication and never occurs. Where defined, the value is exact on imperfect files too, and the removal classification of 3 is by syntactic form, never by validity or resolution: every import declaration is removed by form — binding shape, specifier validity, and target discovery notwithstanding, so an import whose target file was deleted or renamed perturbs no text value; a section tag is removed with every attribute it spells, unknown, repeated, and spread included; and a construct matching no removal rule's form (the stray elements, expression containers, and exports of 14.16) is content, preserved byte-for-byte and located by its finding. A defined value is thus a pure function of the consulted files' parses and the resolved expansions. -**Unavailability is explicit.** A datum these rules leave undefined — a node's identity (a section's under the spelling rules above; every node's in a file whose path is invalid); a section's tags or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7), the enclosing construct's position staying on view through 11.4; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. +**Unavailability is explicit.** A datum these rules leave undefined — a node's identity (a section's under the spelling rules above; every node's in a file whose path is invalid); a section's tags or coverage value; an occurrence's source graph node, identity and range withheld together as one datum (5.7) — the occurrence's own range (11.3) and, in a spec source, the enclosing construct's position (11.4) staying on view; an import's resolved target when specifier form or discovery defines none; an own- or subtree-text value — is reported as explicitly unavailable wherever an answer would otherwise carry it: deterministically, never silently omitted, never fabricated from partial resolution. **Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. A finding is a domain file's exactly when one of its locations (14) lies in that file or that file is its concerned path (14.19); a condition several files jointly violate — a cross-file cycle (14.9) — accompanies the answer, whole, whenever any participating file lies in the domain. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. @@ -634,7 +634,7 @@ Reports the machine-readable shape of the workspace, so an external tool never e Inventory lists are ordered deterministically: files and paths in byte order of workspace-relative path, groups, profiles, and rules in configuration order, and session files in byte order of file name. -Recorded state that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise — is condition 23 (14): the record-supplied datum (the recorded derived-file paths) is reported explicitly unavailable, never fabricated and never read as an empty map; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other provenance's content is emitted in full. +Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the recorded derived-file paths — with the one outcome 14.23 defines. ## 12. Commands @@ -650,8 +650,8 @@ Recorded state that exists but cannot be read as a record — corrupt graph data * All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. * A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source or derived path in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file or an import's resolved target (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. -* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). -* The argument existence checks of `rename` and `move` (a nonexistent origin file or old ID, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. +* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); wrong-kind operands — a code source named where a spec source is required (6.4, 6.5, 11.4, 11.5); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). +* The argument checks of `rename` and `move` (a nonexistent origin file or old ID; a wrong-kind, non-spec-source origin file, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. ### 12.1 `xspec build` @@ -766,9 +766,9 @@ The defined error conditions — also the findings that accompany answers over a 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. 21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, by any `review` subcommand naming the session, and by `review list` (exit 1); not reported by `build`, which does not read sessions. 22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). -23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by `inventory` (11.6) and by `rename`/`move` previews (6.6), the surfaces that read the record without refreshing it: the record-supplied datum is reported explicitly unavailable, the rest of the answer is emitted in full, and the invocation exits 1. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). +23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by the surfaces that read the record without refreshing it — `inventory` (11.6) and `rename`/`move` previews (6.6) — with one outcome, defined here for both: the surface's record-supplied datum — the inventory's recorded derived-file paths, the preview's delta — is reported explicitly unavailable, never fabricated and never read as an empty record; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other part of the answer — every other provenance's content, every other part of the preview report — is emitted in full. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). -Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID collides with an existing ID (6.4) or with an ID remaining in the target file after the removal (6.5), locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. +Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. A refused operation or preview reports every applicable reason together, one finding per reason — never only the first found — each reason's applicability read on its own terms below. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID, or an ID the prefix replacement produces, collides with an ID remaining after the operation's removals — rename's prefix mapping (6.4), the section move's subtree removal (6.5) — locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it, or the section form's target path is occupied by anything other than a discovered spec source (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. ## 15. Example From e34bcddc6f68b2c34dfb31a2f05d2f0635be5417 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 16:32:58 +0000 Subject: [PATCH 27/35] sdg(phase-4): pin check's graph-data staleness form, classify mixed move operands (iter 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: 14.10 restructured into four forms — per-file staleness (generated modules/companions per 13.1, emitted Markdown per 13.2) and orphaned recorded files name the file; graph-data staleness (missing or mismatching under 13.3's comparison, recorded paths excluded) and the unreadable record are each one finding whose concerned path is the graph-data area, no path inside named, mirroring 14.23's shape. 12.2 now enumerates the verifications, graph data included, removing the 'generated files' ambiguity. Follows 13.3's existing commitment that check reports graph-data staleness while keeping 11.6's unattributed- path regime intact. - O1: 6.5 classifies move operands by spelling — a mixed-form invocation matches neither synopsis and is a usage error; the file form's inability to spell '#'-containing paths noted as harmless (14.19). - O2: 6.4 drops the '(12.7)' citation on the successful-rename report; 12.7 pins no form for it, so the report stays information-only per 12.0 — consistent with O4's recorded-gap stance. - O3: 7 qualifies 'Every command locates the configuration' to except version, matching 12.6 and 14.14. Rejected: - O5 (modularization): SHOULD, not MUST. The named components are not loosely coupled — 14 is cross-referenced from nearly every section, 10 and 11 interlock with 5.4-5.7, 12.0, 12.7, 13.3-13.5 — so extraction would duplicate contract text into SPEC.md or violate the no-reference-into-module rule; restructuring at convergence adds contradiction risk with no behavioral content. Consistent with prior rounds' deferrals. No-op: - O4: reviewer records the unpinned query/impact/coverage/review member names as a deliberate, IP-faithful gap for a future proposal; no change requested. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index 2e6560e..e42bd60 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -305,7 +305,7 @@ When a command takes a baseline git ref, the baseline graph is reconstructed fro xspec rename [--preview] ``` -Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID; the new ID, and each ID the prefix replacement produces, collides with no ID remaining in the file once the vacated IDs — the old ID and its descendants' — are removed, exactly as the section move's after-removal check reads (6.5): an identity-unchanged rename therefore collides with nothing and reports `refused-identity-unchanged` alone (14); structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); so is a `` naming a discovered source that is not a spec source — a code source bears no requirement IDs, so a code-source origin is a wrong-kind operand, the usage error of 11.4's pattern (12.0), judged like existence before any content question; the old ID's existence is parse-local, judged over spelled identities (11.2): it exists exactly when a section of the origin file spells it — a bearer whose node identity is undefined (duplicate spellings; an undefined ancestor chain, 11.2) still establishes existence, a section spelling no identity (its `id` attribute repeated or in invalid value form, 11.2) establishes none, and an unparseable origin file is masked (12.0). Every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. A successful rename's report is the applied mapping: the complete identity mapping the operation journaled — the information of the preview's `mapping` (6.6), carried in JSON per 12.0 (12.7). +Renames a requirement ID, rewrites descendant IDs by prefix replacement, rewrites every reference to the affected identities across all configured spec and code sources (`id` attributes, `d` references, `text(...)` references, TypeScript markers), and appends the mapping to the journal. Rewrites are minimal in-place edits, preserving each reference's quote style and access form (2.4); where a form cannot be kept — a chain segment whose new name is not a valid TypeScript identifier, or a reference converted between local and imported form (6.5) — the rewritten part uses dot access for segments that are valid TypeScript identifiers, double-quoted computed access for segments that are not, and double-quoted string literals. Type-level TypeScript references record no edges (4.5) and are not rewritten: a rename or move can leave them naming vacated identities — a consumer type error outside xspec's validations — while the workspace stays valid. Validation MUST confirm: the new ID is valid; it differs from the old ID; the new ID, and each ID the prefix replacement produces, collides with no ID remaining in the file once the vacated IDs — the old ID and its descendants' — are removed, exactly as the section move's after-removal check reads (6.5): an identity-unchanged rename therefore collides with nothing and reports `refused-identity-unchanged` alone (14); structural parent rules remain satisfied; all rewritten references resolve. A `` or old ID that does not exist is a usage error (12.0); so is a `` naming a discovered source that is not a spec source — a code source bears no requirement IDs, so a code-source origin is a wrong-kind operand, the usage error of 11.4's pattern (12.0), judged like existence before any content question; the old ID's existence is parse-local, judged over spelled identities (11.2): it exists exactly when a section of the origin file spells it — a bearer whose node identity is undefined (duplicate spellings; an undefined ancestor chain, 11.2) still establishes existence, a section spelling no identity (its `id` attribute repeated or in invalid value form, 11.2) establishes none, and an unparseable origin file is masked (12.0). Every other validation failure refuses the rename (exit 1), each distinct refusal reason carrying its stable code and location (14). With `--preview`, the operation is planned and reported but performed on nothing (6.6). Rename MUST also refuse (exit 1), before modifying anything, when the current workspace fails the validations of `xspec build` (12.1), so the operation only ever rewrites a valid workspace; the usage-error argument checks precede this refusal (12.0), and it precedes the operation-specific validation above, which is defined — and evaluated — only over a workspace passing `build`'s validations: an invalid-workspace refusal reports the workspace's findings alone (14), no refusal reason evaluated or reported beside them. A successful rename finishes by regenerating derived files exactly as `xspec build` does (12.1) — which cannot fail, per the precondition — so generated modules, Markdown output, and graph data match the rewritten sources and no stale output (14.10) remains. A successful rename's report is the applied mapping: the complete identity mapping the operation journaled — the information of the preview's `mapping` (6.6), carried in JSON per 12.0. ### 6.5 Move @@ -316,7 +316,7 @@ xspec move # # [--preview] The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is the discovered spec source occupying the target path when one occupies it; at a target path nothing occupies, the target file is created, empty before insertion; a target path occupied by anything else is refused (below). The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files, and reports its applied mapping, as rename does (6.4). -Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification — existence and kind judged as in 6.4, both forms' origin operands naming discovered spec sources — of a nonexistent or wrong-kind origin file or a nonexistent origin ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, ``, and each ID its prefix replacement produces, collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target path is occupied by anything other than a discovered spec source — a directory, a symbolic link (discovery never follows one, 7), or any other occupant discovery does not yield as a spec source: neither an insertion target nor an absent path to create; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). +A move operand is classified by spelling alone: an operand containing `#` is a `#` pair under the split of 12.0 and one without is a file — an invocation mixing the two synopses' forms therefore matches neither and is a usage error (12.0), and the file form cannot spell a `#`-containing path: a harmless limit, such paths being invalid source paths (14.19). Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification — existence and kind judged as in 6.4, both forms' origin operands naming discovered spec sources — of a nonexistent or wrong-kind origin file or a nonexistent origin ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, ``, and each ID its prefix replacement produces, collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target path is occupied by anything other than a discovered spec source — a directory, a symbolic link (discovery never follows one, 7), or any other occupant discovery does not yield as a spec source: neither an insertion target nor an absent path to create; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). ### 6.6 Previews @@ -369,7 +369,7 @@ export default defineConfig({ The configuration is declarative — data, not executed code. The file MUST consist of exactly an import of `defineConfig` from the module specifier `"xspec"` (optionally aliased) and a default export of one call to that binding, whose sole argument is statically literal: object literals with non-computed identifier or string-literal keys, array literals, static string literals (2.4), and the boolean literals `true` and `false` — no other statement or expression form, no spread, no computed value. Configuration therefore cannot carry side effects, environment-dependent values (12.0), or network access; a configuration file that is not well-formed TypeScript or does not conform is a configuration error (14.14). -Every command locates the configuration by upward search for `xspec.config.ts` from the working directory, or uses the path given by the global `--config ` option. `specs` is required; `code`, `markdown`, `coverage`, and `policy` are optional — omitting one means no code groups, no Markdown emission, no coverage profiles, or no policy rules, respectively; an empty `coverage` or `policy` list is valid and equivalent to omitting the key. Unknown keys anywhere in the `defineConfig` argument — a top-level key, or a field of `markdown`, a profile, a rule, or a selector — are a configuration error (14.14). All configured paths and globs resolve relative to the configuration file's directory, which is the workspace root. Glob matching, like every path comparison (12.0), is byte-wise: workspace-relative paths are matched as their UTF-8 bytes, and a discovered source file whose workspace-relative path is not valid UTF-8 is invalid (14.19). Globs support exactly `*` (any possibly empty run of bytes within one path segment), `?` (one byte within a segment), and `**` (any number of whole segments, including none); matching is case-sensitive; a path segment beginning with `.` is matched only by a pattern segment written with a leading `.`; a pattern that resolves outside the workspace root is a configuration error (14.14). Discovery never follows symbolic links: a symbolic link — to a file or to a directory, broken or not — is never a discovered source and is never traversed, so symlinked, cyclic, or workspace-external content never enters the discovered set. Discovery of source files is controlled exclusively by configuration; derived files are never discovered as sources (13.4); imports resolve references between files but never add files to the workspace (2.1). A group whose globs match no files is valid, as is a `specs` or `code` map with no groups: discovery simply yields fewer, possibly zero, sources. +Every command except `version` (12.6), which loads no configuration, locates the configuration by upward search for `xspec.config.ts` from the working directory, or uses the path given by the global `--config ` option. `specs` is required; `code`, `markdown`, `coverage`, and `policy` are optional — omitting one means no code groups, no Markdown emission, no coverage profiles, or no policy rules, respectively; an empty `coverage` or `policy` list is valid and equivalent to omitting the key. Unknown keys anywhere in the `defineConfig` argument — a top-level key, or a field of `markdown`, a profile, a rule, or a selector — are a configuration error (14.14). All configured paths and globs resolve relative to the configuration file's directory, which is the workspace root. Glob matching, like every path comparison (12.0), is byte-wise: workspace-relative paths are matched as their UTF-8 bytes, and a discovered source file whose workspace-relative path is not valid UTF-8 is invalid (14.19). Globs support exactly `*` (any possibly empty run of bytes within one path segment), `?` (one byte within a segment), and `**` (any number of whole segments, including none); matching is case-sensitive; a path segment beginning with `.` is matched only by a pattern segment written with a leading `.`; a pattern that resolves outside the workspace root is a configuration error (14.14). Discovery never follows symbolic links: a symbolic link — to a file or to a directory, broken or not — is never a discovered source and is never traversed, so symlinked, cyclic, or workspace-external content never enters the discovered set. Discovery of source files is controlled exclusively by configuration; derived files are never discovered as sources (13.4); imports resolve references between files but never add files to the workspace (2.1). A group whose globs match no files is valid, as is a `specs` or `code` map with no groups: discovery simply yields fewer, possibly zero, sources. ### 7.1 `specs` @@ -659,7 +659,7 @@ Parses configured sources; validates section structure, IDs, tags, and reference ### 12.2 `xspec check` -Performs all build validations (12.1, 14) — unresolved and non-static references, dependency and spec import cycles, and journal errors included — without accepting stale outputs, and additionally verifies what `build` does not: generated files are content-identical to what the current sources and configuration generate, no recorded derived file remains at a path no longer generated, and the recorded generation state is readable as a record (14.10, 14.23); no policy violations exist (14.12); review sessions are not internally corrupt (14.21). Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. +Performs all build validations (12.1, 14) — unresolved and non-static references, dependency and spec import cycles, and journal errors included — without accepting stale outputs, and additionally verifies what `build` does not: generated modules, companions (13.1), and emitted Markdown (13.2) are content-identical to what the current sources and configuration generate; graph data matches the current sources and configuration (13.3); no recorded derived file remains at a path no longer generated; and the recorded generation state is readable as a record (14.10, 14.23); no policy violations exist (14.12); review sessions are not internally corrupt (14.21). Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. ### 12.3 `xspec ids` @@ -753,7 +753,7 @@ The defined error conditions — also the findings that accompany answers over a 7. Unknown TypeScript reference: a marker or `text` call that does not resolve; this is also a type error against the generated module. 8. Invalid argument: a `d` or `text(...)` reference that is not static per 2.4, a non-static bare reference in TypeScript expression-statement position (4.5), a `text(...)` call without exactly one argument, or a string-form `text(...)` argument in a TypeScript file (4.3). 9. Cycle: a dependency cycle (with the full path) or a spec import cycle. -10. Stale generated output: a derived file whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding — or recorded generation state that exists but cannot be read as a record (14.23): one finding, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named; while that state holds, this form alone reports the graph data, and the recorded-file form above, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). +10. Stale generated output, in four forms. Per file: a generated module or companion (13.1) or emitted Markdown file (13.2) whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding. As one unit: graph data that is missing or does not match the current sources and configuration — the comparison of 13.3, the recorded derived-file paths excluded, so a lagging record alone is never staleness — or recorded generation state that exists but cannot be read as a record (14.23): one finding either way, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named. The unit forms are exclusive — an unreadable record (14.23) reports under its own form alone, never the mismatch form beside it — and while that state holds the recorded-file form, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. 12. Policy violation: one finding per violation (7.5) — per rule and offending edge. The offending entity is a graph edge, not a spelling: the finding carries no in-source locations and concerns no path — `locations` empty, `path` `null` (12.7) — so it is no file's finding (11.2) and accompanies no answer of 11.3–11.5. Its `identities` are, in order, the violated rule's name and the edge's source identity, kind token (12.7), and target identity. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). From 6a74e5be18031bd91f203f41cca0e9338f73ca08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 17:04:22 +0000 Subject: [PATCH 28/35] sdg(phase-4): pin per-file staleness occupant judgment and absent-file case (iter 11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: 14.10/12.2 — the per-file content comparison judges the path's occupant itself, never traversing a symbolic link (13.4): it matches only a plain file holding exactly the generated content, so a symlink (whatever its target holds), a directory, or any other non-plain-file occupant is stale per se. Two conforming implementations can no longer diverge on a symlinked byte-identical occupant. - I1: 14.10 — per-file form now reads "missing or does not match", covering check on a deleted generated module/companion/Markdown file, parallel to the unit form, 13.3, and 13.4's deleted-file promise; 12.2 aligned ("present as plain files content-identical"). - O2: 11.6 — stated that the condition-23 finding is the only finding an inventory answer ever carries; findings a listed file or path may bear (14.19, 14.13, 14.21) report where their conditions assign them. - O3: 12.7 — concerned-path ordering key pinned to byte-wise comparison across presentation forms: marked byte-form and plain-string paths sort in one byte order. Rejected: - O1: modularization deferred again — standing SHOULD acknowledged, but the consumer-facing contract must stay in SPEC.md (PROCESS), limiting the extractable interior, and restructuring mid-IP-refinement is not clearly right this round. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index e42bd60..a36037a 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -634,7 +634,7 @@ Reports the machine-readable shape of the workspace, so an external tool never e Inventory lists are ordered deterministically: files and paths in byte order of workspace-relative path, groups, profiles, and rules in configuration order, and session files in byte order of file name. -Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the recorded derived-file paths — with the one outcome 14.23 defines. +Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the recorded derived-file paths — with the one outcome 14.23 defines. That is the only finding an inventory answer ever carries: parsing no sources, reading no journal or session content, and writing nothing, the inventory meets no other condition, and the findings a listed file or path may bear — an invalid source path (14.19), a journal error (14.13), a corrupt session (14.21) — are reported where their conditions assign them (14), never here. ## 12. Commands @@ -659,7 +659,7 @@ Parses configured sources; validates section structure, IDs, tags, and reference ### 12.2 `xspec check` -Performs all build validations (12.1, 14) — unresolved and non-static references, dependency and spec import cycles, and journal errors included — without accepting stale outputs, and additionally verifies what `build` does not: generated modules, companions (13.1), and emitted Markdown (13.2) are content-identical to what the current sources and configuration generate; graph data matches the current sources and configuration (13.3); no recorded derived file remains at a path no longer generated; and the recorded generation state is readable as a record (14.10, 14.23); no policy violations exist (14.12); review sessions are not internally corrupt (14.21). Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. +Performs all build validations (12.1, 14) — unresolved and non-static references, dependency and spec import cycles, and journal errors included — without accepting stale outputs, and additionally verifies what `build` does not: generated modules, companions (13.1), and emitted Markdown (13.2) are present as plain files content-identical to what the current sources and configuration generate — each path's occupant judged itself, never through a symbolic link (14.10); graph data matches the current sources and configuration (13.3); no recorded derived file remains at a path no longer generated; and the recorded generation state is readable as a record (14.10, 14.23); no policy violations exist (14.12); review sessions are not internally corrupt (14.21). Exits 1 on any finding. Configuration validity is enforced at load by every command (14.14) and is a usage error, not a `check` finding. ### 12.3 `xspec ids` @@ -692,7 +692,7 @@ Value forms: * A source range (1.7) is `{"start": …, "end": …}`, both non-negative integers. * A path — workspace-relative, or in the anchoring form of 11.6 — is a string where its bytes are valid UTF-8, and otherwise the marked byte form of 12.0: `{"bytes": "…"}`, the path's exact bytes as lowercase hexadecimal, two digits per byte — an object, equal to no path string. Identities are strings (1.5); no identity carries a non-UTF-8 path (12.0). * A datum reported explicitly unavailable (11.2, 11.6, 6.6) is `{"unavailable": true}`. A plain value, `null`, and `{"unavailable": true}` are the three observable states of a datum (11.4). -* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or other context strings the condition names (14), empty where none — content contractual exactly where 14 states it for the condition or reason (14.12's enumeration, a condition's named context entity such as 14.11's foreign module, a refusal reason's concerned identity), otherwise informational: deterministic (12.0), its composition unpinned. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). +* A finding (14) is `{"code", "message", "locations", "path", "identities"}`: the stable code, the token string 14 assigns (`null` where 14 assigns none); the human-readable description; one `{"file", "range"}` per offending construct — ordered by file path bytes, then range start, then range end — empty for conditions without in-source locations; the concerned file or path (`null` for located conditions); and the identities or other context strings the condition names (14), empty where none — content contractual exactly where 14 states it for the condition or reason (14.12's enumeration, a condition's named context entity such as 14.11's foreign module, a refusal reason's concerned identity), otherwise informational: deterministic (12.0), its composition unpinned. Wherever a document carries findings they form the array member `"findings"`, ordered by code — the numbered conditions in numeric order, then the refusal reasons in the order 14 lists them, then code-less findings — then by locations, compared element-wise — each element by file path bytes, then range start, then range end; a sequence that is a proper prefix of another sorts first — then by concerned path (`null` before any path; paths compare byte-wise whatever their presentation form (12.0) — a marked byte-form path and a plain string sort in one byte order), then by identities, compared element-wise under the same prefix rule (string elements by bytes, 12.0), then by message; findings identical in every member collapse to one, so the order is total. A report whose defined content is findings alone — `build` and `check` reports, the findings of refusing reads (13.3), refused operations (6.4, 6.5, 10.7) — is `{"findings": […]}`; a refused preview instead keeps the preview document form, its `mapping`, `files`, and `delta` `null` (6.6). * A reference occurrence record (5.7) is `{"file", "range", "kind", "source", "target"}`: the referencing file; the occurrence's own range; its edge kind, `"depends"`, `"embeds"`, or `"references"` (5.2); its source graph node, `{"identity", "range"}` or unavailable (11.2); and its resolved target's identity. Document forms — each a single JSON document whose top level is an object; every one below except `version`'s and the exit-2 error document carries the consulted domain's findings (11.2, 11.6, 6.6) under `"findings"`: @@ -753,7 +753,7 @@ The defined error conditions — also the findings that accompany answers over a 7. Unknown TypeScript reference: a marker or `text` call that does not resolve; this is also a type error against the generated module. 8. Invalid argument: a `d` or `text(...)` reference that is not static per 2.4, a non-static bare reference in TypeScript expression-statement position (4.5), a `text(...)` call without exactly one argument, or a string-form `text(...)` argument in a TypeScript file (4.3). 9. Cycle: a dependency cycle (with the full path) or a spec import cycle. -10. Stale generated output, in four forms. Per file: a generated module or companion (13.1) or emitted Markdown file (13.2) whose content does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding. As one unit: graph data that is missing or does not match the current sources and configuration — the comparison of 13.3, the recorded derived-file paths excluded, so a lagging record alone is never staleness — or recorded generation state that exists but cannot be read as a record (14.23): one finding either way, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named. The unit forms are exclusive — an unreadable record (14.23) reports under its own form alone, never the mismatch form beside it — and while that state holds the recorded-file form, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). +10. Stale generated output, in four forms. Per file: a generated module or companion (13.1) or emitted Markdown file (13.2) that is missing or does not match what the current sources and configuration generate, or a recorded derived file (13.3) remaining at a path the current sources and configuration no longer generate — the error names the file and instructs rebuilding. The per-file content comparison judges the path's occupant itself, never traversing a symbolic link (13.4): it matches only a plain file holding exactly the generated content, so a symbolic link (whatever its target holds), a directory, or any other non-plain-file occupant is stale, exactly as a missing or content-differing file is. As one unit: graph data that is missing or does not match the current sources and configuration — the comparison of 13.3, the recorded derived-file paths excluded, so a lagging record alone is never staleness — or recorded generation state that exists but cannot be read as a record (14.23): one finding either way, instructing rebuilding, whose concerned path is the graph-data area (11.6) — the record's layout is unenumerated (13.3), so no path inside the area is named. The unit forms are exclusive — an unreadable record (14.23) reports under its own form alone, never the mismatch form beside it — and while that state holds the recorded-file form, consulting no readable record, is undetectable (14). Reported by `check` only: `build` cannot observe staleness because it regenerates every derived file (12.1). 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. 12. Policy violation: one finding per violation (7.5) — per rule and offending edge. The offending entity is a graph edge, not a spelling: the finding carries no in-source locations and concerns no path — `locations` empty, `path` `null` (12.7) — so it is no file's finding (11.2) and accompanies no answer of 11.3–11.5. Its `identities` are, in order, the violated rule's name and the edge's source identity, kind token (12.7), and target identity. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). From 926ce74846361144c982be220ce6708980781898 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 14:41:40 +0000 Subject: [PATCH 29/35] sdg(phase-4): pin gated-read precedence and move-destination component causes (iter 12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: C1, I1, O2, O3 - C1: one precedence statement for the 13.3-gated reads (12.0) — their argument checks precede the invalid-workspace gate, judged per 6.4's parse-local pattern with unparseable-named-file masking, so unknown names exit 2 whatever the workspace's findings; the gate precedes session reading, so a corrupt session (14.21) reports only on a passing workspace and item-ID checks are masked by the corruption (13.3, 10.1, 10.7, 14.21 aligned). - I1: a non-directory-occupied (symbolic link included) workspace-relative directory component of a move destination or of a derived path the destination would generate is one refusal cause under refused-invalid-destination (6.5, 14), disclaimed in 14.22 so refusal reports stay refusal-reasons-only and the finishing-regeneration infallibility claim holds; writes create nonexistent intermediate directories (13.4). - O2: 11.2 closing label no longer overstates — "Never stale; writing nothing on a failing workspace." - O3: duplicate profile/rule names named explicitly in 14.14. Rejected: O1 — modularization stays deferred, consistent with prior rounds; the Reviewer records it for completeness and states nothing reopens that decision. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index a36037a..af7e0a5 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -316,7 +316,7 @@ xspec move # # [--preview] The first form relocates an entire source file; IDs are unchanged and every node's identity changes only in its file part. Relocation also rewrites the moved file's own import specifiers, and the paths by which other files import the moved file's generated module, so all references continue to resolve. The second form extracts a section subtree: the section and its descendants are removed from the origin, inserted as the last child of the target parent (or at the end of the file for a top-level `new-id`), and re-identified by prefix replacement of `` with ``. The target file is the discovered spec source occupying the target path when one occupies it; at a target path nothing occupies, the target file is created, empty before insertion; a target path occupied by anything else is refused (below). The second form's text edits are exact: the moved text is the section construct's own characters — from the first character of its opening tag through the last character of its closing tag, or the self-closing tag's own characters for a self-closing section (1.1). At the origin it is deleted in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3). It is inserted immediately before the target parent's closing tag — at the end of the file for a top-level `new-id` — followed by a U+000A line terminator, and preceded by one when the insertion point is not at the start of a line. A self-closing target parent (1.1) is first rewritten to the paired form: its `/` and any whitespace immediately before or after the `/` are deleted, and the closing tag matching the opening tag's name (`` or ``) is appended immediately after the tag's terminating `>`; the insertion rule then applies before that closing tag. Beyond these edits, the identity and reference rewrites of this section, and the finishing regeneration, a move changes no bytes. In both forms, all references across the workspace are rewritten to resolve to the new identities, converting between local and imported forms and adding or removing spec module imports as the rewrite requires — an import is added when a rewritten reference needs a module binding its file lacks, and an existing spec module import is removed exactly when its binding had references and the rewrite leaves it with none (an import whose binding was already unreferenced stays, 2.1) — and the full mapping is appended to the journal. Import edits are exact. An import removal deletes the declaration's own characters in place, and lines left empty or whitespace-only purely by that deletion are dropped with their line terminators, exactly as in Markdown compilation (3): the removal's extent is the declaration plus any such adjunct drop. An added import binds fresh identifiers colliding with no binding already in the file (2.1, 4) and is inserted as a line of its own — the declaration's characters followed by a U+000A line terminator, preceded by one when the insertion point is not at the start of a line — at an offset where the file's grammar permits an import declaration; the identifier choice and the insertion offset are implementation latitude, exercised deterministically: rewritten file content is byte-deterministic for a given operation and workspace state (6.1), and in a file existing before the operation the offset is exactly the one the operation's preview reports (6.6) — an addition into a created target file is subsumed, with the rest of that file's initial content, by the preview's file-creation class (6.6), its placement pinned by byte-determinism alone. A successful move regenerates derived files, and reports its applied mapping, as rename does (6.4). -A move operand is classified by spelling alone: an operand containing `#` is a `#` pair under the split of 12.0 and one without is a file — an invocation mixing the two synopses' forms therefore matches neither and is a usage error (12.0), and the file form cannot spell a `#`-containing path: a harmless limit, such paths being invalid source paths (14.19). Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification — existence and kind judged as in 6.4, both forms' origin operands naming discovered spec sources — of a nonexistent or wrong-kind origin file or a nonexistent origin ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, ``, and each ID its prefix replacement produces, collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target path is occupied by anything other than a discovered spec source — a directory, a symbolic link (discovery never follows one, 7), or any other occupant discovery does not yield as a spec source: neither an insertion target nor an absent path to create; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace, so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). +A move operand is classified by spelling alone: an operand containing `#` is a `#` pair under the split of 12.0 and one without is a file — an invocation mixing the two synopses' forms therefore matches neither and is a usage error (12.0), and the file form cannot spell a `#`-containing path: a harmless limit, such paths being invalid source paths (14.19). Move validation mirrors rename validation, including the valid-workspace precondition (6.4) and the usage-error classification — existence and kind judged as in 6.4, both forms' origin operands naming discovered spec sources — of a nonexistent or wrong-kind origin file or a nonexistent origin ID (12.0); the mirrored checks read in identity terms: the new ID is valid, structural parent rules remain satisfied, all rewritten references resolve, the new identity differs from the old — a cross-file section move keeping its ID is therefore valid, while the exact self-move, `#` equal to `#`, is refused and appends no journal entry — and, in the section form, ``, and each ID its prefix replacement produces, collides with no ID remaining in the target file after the removal. Move additionally MUST refuse: a move that would create an import cycle among spec source files or a dependency cycle; a file-form move whose destination path is already occupied — by whatever kind of filesystem object, a symbolic link included; a section-form move whose target path is occupied by anything other than a discovered spec source — a directory, a symbolic link (discovery never follows one, 7), or any other occupant discovery does not yield as a spec source: neither an insertion target nor an absent path to create; a section-form move whose target parent — the target file's section bearing `` minus its final segment, needed whenever `` has more than one segment — is missing or lies within the moved subtree, leaving no insertion point after the removal; and a move whose destination file path (including a target file to be created) would not be a valid discovered spec source after the move, or whose destination could not be written and regenerated — a path belonging to no configured spec group (a move never takes a node out of the workspace), belonging to a code group as well (14.14), containing `#`, not valid UTF-8, or lacking the `.mdx` extension (14.19); or a workspace-relative directory component of the destination path, or of a derived path the destination would generate (13.1, 13.2, 7.3), occupied by anything other than a directory — a symbolic link included, whatever it targets: discovery never traverses one (7) and writes never traverse or replace one (13.4, 14.22); a nonexistent component is never a cause, since writes create those (13.4). These refusals keep every successful move's finishing regeneration (6.4) on a valid workspace — the destination's write paths vetted here, the rewritten sources lying under real directories (7: discovery never traverses a symbolic link), and every other regenerated path vetted by the valid-workspace precondition (14.22) — so it cannot fail. Each distinct refusal reason carries its stable code and location (14); with `--preview`, the operation is planned and reported but performed on nothing (6.6). ### 6.6 Previews @@ -459,7 +459,7 @@ Review turns graph results into a staged checklist. xspec separates the review m ### 10.1 Sessions -A review session is stored at `.xspec/reviews/.json` as a plain, deterministic file. A session name MUST consist of one or more characters from `A–Z`, `a–z`, `0–9`, `.`, `_`, and `-`, and MUST NOT begin with `.`; any other name is a usage error (12.0). Session names are case-sensitive, but so that session files stay unambiguous on case-insensitive filesystems, a name that matches an existing session's name ignoring ASCII case is treated at `review create` as the name of an existing session and refused (10.7); every other subcommand matches names exactly. A session is a durable task ledger for a specific graph state (13.4), not a source of requirement identity. Only a directory entry directly under `.xspec/reviews/` named `.json` with a valid session name is a session; any other entry there is not a session and is ignored by every command, `check` included. A session file that exists but is not a plain file (13.4), cannot be parsed, or violates a session invariant — the fields of 10.2 present and well-formed, statuses drawn from 10.3, item `id`s unique within the session, `blockedBy` naming only item `id`s present in the session and containing no cycle (no item transitively blocks itself; every built-in strategy and `split` produce only acyclic blocking, so a cycle can only enter by external modification), at most one item per kind and scope node (10.5), and the recorded creation parameters and decompositions (10.7) well-formed — is corrupt (14.21): every `review` subcommand naming that session reports the corruption and exits 1, modifying nothing, and `list` reports it as corrupt (10.7). +A review session is stored at `.xspec/reviews/.json` as a plain, deterministic file. A session name MUST consist of one or more characters from `A–Z`, `a–z`, `0–9`, `.`, `_`, and `-`, and MUST NOT begin with `.`; any other name is a usage error (12.0). Session names are case-sensitive, but so that session files stay unambiguous on case-insensitive filesystems, a name that matches an existing session's name ignoring ASCII case is treated at `review create` as the name of an existing session and refused (10.7); every other subcommand matches names exactly. A session is a durable task ledger for a specific graph state (13.4), not a source of requirement identity. Only a directory entry directly under `.xspec/reviews/` named `.json` with a valid session name is a session; any other entry there is not a session and is ignored by every command, `check` included. A session file that exists but is not a plain file (13.4), cannot be parsed, or violates a session invariant — the fields of 10.2 present and well-formed, statuses drawn from 10.3, item `id`s unique within the session, `blockedBy` naming only item `id`s present in the session and containing no cycle (no item transitively blocks itself; every built-in strategy and `split` produce only acyclic blocking, so a cycle can only enter by external modification), at most one item per kind and scope node (10.5), and the recorded creation parameters and decompositions (10.7) well-formed — is corrupt (14.21): every `review` subcommand naming that session — on a workspace passing `build`'s validations, the only state in which a `review` subcommand reads a session (13.3, 12.0) — reports the corruption and exits 1, modifying nothing, an item ID named beside the session masked by the corruption (12.0), and `list` reports it as corrupt (10.7). ### 10.2 Items @@ -534,7 +534,7 @@ xspec review export [--json] `review create` requires exactly one of `--base`, `--strategy audit`, or `--coverage`; supplying none, more than one, or any other `--strategy` value is a usage error (12.0). `create` records the session's creation parameters in the session file, fully resolved: a baseline session records the commit identity `--base` resolved to at creation, a `coverage` session records the named profile's definition — its 7.4 fields, with each group name replaced by that group's configured glob list and kind — and an audit session records none. Every later generator run (10.4, 10.5) uses the recorded parameters — the recorded commit as the baseline, the recorded globs matched against the currently discovered sources (7) — so renaming or editing refs, profiles, or groups after `create` never changes the recorded parameters the session runs with. Discovery itself still follows the current configuration: a file that no longer belongs to any configured group is out of the session's view, exactly as if deleted. A `review` command that cannot resolve or reconstruct the recorded (or, at `create`, the given) baseline fails per 6.3 as a usage error (12.0), modifying nothing. A `coverage` session contains one `uncovered-requirement` item per uncovered required node of the profile — scope: that node; context: its ancestor chain; origin and `blockedBy` empty. -`list` reports every session, in byte order of session name, with its name, strategy, and item counts by status — counted from stored statuses, without the read-time invalidation of 10.4 — and reports each corrupt session (14.21) by name as corrupt in place of those fields; `list` exits 1 when any session is corrupt and 0 otherwise. `status ` reports the session's items in item order — each with id, kind, scope, status, and blocked state — plus totals by status. `show ` reports the full item: every field of 10.2 plus the same self-contained text payload as `next --json`. `export ` emits the entire session as a single JSON document — its only output form, with or without `--json`: the session's name, strategy, recorded creation parameters, and recorded decompositions, plus every item in item order, each with every field of 10.2, its blocked state, and the same self-contained text payload as `next --json`, with read-time invalidation (10.4) applied. +`list` reports every session, in byte order of session name, with its name, strategy, and item counts by status — counted from stored statuses, without the read-time invalidation of 10.4 — and reports each corrupt session (14.21) by name as corrupt in place of those fields; `list` exits 1 when any session is corrupt and 0 otherwise — on a workspace failing `build`'s validations the gate's report replaces all of this (13.3, 12.0). `status ` reports the session's items in item order — each with id, kind, scope, status, and blocked state — plus totals by status. `show ` reports the full item: every field of 10.2 plus the same self-contained text payload as `next --json`. `export ` emits the entire session as a single JSON document — its only output form, with or without `--json`: the session's name, strategy, recorded creation parameters, and recorded decompositions, plus every item in item order, each with every field of 10.2, its blocked state, and the same self-contained text payload as `next --json`, with read-time invalidation (10.4) applied. `next` returns the first item in the session's item order (10.5, 10.6, or the coverage order below) that needs review (`unresolved` or `invalidated`, 10.3) and is unblocked. When no item qualifies, every item is resolved — with acyclic `blockedBy` (10.1) a minimal needing-review item is always unblocked, so no other case exists — and `next` exits 0 and reports the session fully resolved (a session with no items reports the same) in both human and `--json` output; the JSON payload then contains no item. With `--json`, the payload MUST be self-contained, so the item can be acted on without further reads: every scope, context, and origin node, under its current identity and presence (10.4) and — for a present graph node, requirement node and code location alike — with its source range (1.7; an absent node carries none); the item's `baseline` and `current` hashes (10.2); and text per item kind. Scope text is the scope root's subtree text for `subtree-coherence`, the scope node's subtree text for `uncovered-requirement`, and the scope node's own text for `parent-consistency`, `dependency-consistency`, and `metadata-consistency`; a code location has no text value, so a `code-impact` scope enters as identity, presence, and — when present — source range, with no text. Context text is own text where the context is an ancestor chain (`subtree-coherence`, `uncovered-requirement`) and subtree text otherwise (`parent-consistency` branch children; `dependency-consistency`, `metadata-consistency`, and `code-impact` targets). Origin text is a before/after pair of the node's own text: before from the item's `baseline` state (10.2), after from the current graph. Every text value is the expanded value of 1.6. A present node's text is read from the current graph; an absent node's is its value in the most recent graph state that contained it, among the item's `baseline` state and the states under which mutating subcommands (13.5) derived the item with that node among its nodes (10.2, 10.5) — a node contained in none, and the absent side of a before/after pair, is presented absent, with no text. A `coverage` session's `uncovered-requirement` items are ordered by file path, then document order. @@ -577,7 +577,7 @@ xspec query reachable --from --to [--kinds ] **Consulted domain, findings, exits.** Every answer of 11.3–11.5 has a consulted domain of files, defined per surface, and the findings (14) of every domain file accompany the answer — a masked file's parse-failure finding included. A finding is a domain file's exactly when one of its locations (14) lies in that file or that file is its concerned path (14.19); a condition several files jointly violate — a cross-file cycle (14.9) — accompanies the answer, whole, whenever any participating file lies in the domain. An invocation whose answer carries any finding or any explicitly-unavailable datum exits 1 with the full answer document still emitted — exit 1 signals imperfection and never withholds the answer; a complete, finding-free answer exits 0; usage and configuration errors keep exit 2 and their precedence (12.0, 14.14). The argument checks of 11.3–11.5 precede answering, as `rename`'s and `move`'s argument checks precede source validation (12.0): a malformed `--to` spelling or invalid glob pattern (11.3, 11.1), a `` operand outside the domain or of the wrong kind (11.4, 11.5), an ill-formed or out-of-range offset (11.5), and every other usage error of these surfaces exits 2, whatever findings the workspace or the named files carry. A possibly-incomplete answer is therefore never silent. -**Never stale, never writing.** These surfaces never answer from stale graph data: on a workspace that passes the validations of `xspec build` (12.1) they participate in read-time refresh exactly as the reads of 13.3 do; on one that fails them — source validation errors, journal errors (14.13), and refused writes (14.22) alike (13.3) — they answer from the current sources and modify nothing: no graph data, no derived files. A gate condition that is a finding of no domain file — the journal's (14.13), a write path's (14.22) — accompanies no answer of these surfaces: on the failing side these answers consult no journal and no record and write nothing, and on the passing side no such finding exists. An answer's findings are its domain files' findings alone, and a complete, finding-free answer exits 0 (above) whatever journal or write-path state the workspace holds. +**Never stale; writing nothing on a failing workspace.** These surfaces never answer from stale graph data: on a workspace that passes the validations of `xspec build` (12.1) they participate in read-time refresh exactly as the reads of 13.3 do; on one that fails them — source validation errors, journal errors (14.13), and refused writes (14.22) alike (13.3) — they answer from the current sources and modify nothing: no graph data, no derived files. A gate condition that is a finding of no domain file — the journal's (14.13), a write path's (14.22) — accompanies no answer of these surfaces: on the failing side these answers consult no journal and no record and write nothing, and on the passing side no such finding exists. An answer's findings are its domain files' findings alone, and a complete, finding-free answer exits 0 (above) whatever journal or write-path state the workspace holds. ### 11.3 `xspec occurrences` @@ -651,7 +651,7 @@ Recorded state that exists but cannot be read as a record is condition 23 (14), * A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source or derived path in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file or an import's resolved target (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. * Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); wrong-kind operands — a code source named where a spec source is required (6.4, 6.5, 11.4, 11.5); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). -* The argument checks of `rename` and `move` (a nonexistent origin file or old ID; a wrong-kind, non-spec-source origin file, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. +* The argument checks of `rename` and `move` (a nonexistent origin file or old ID; a wrong-kind, non-spec-source origin file, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. The reads 13.3 gates (`ids`, `show`, `coverage`, `impact`, `review`, `query`) observe the same precedence: their argument checks precede the invalid-workspace report of 13.3, so a usage-error argument — an unknown or wrong-kind name included — exits 2 whatever findings the workspace carries. Each check is judged from what it consults, identically on valid and failing workspaces: a profile or group name against the configuration (7.4, 7.5, 11.1), a session name against the session directory (10.1), and a requirement-node or graph-node identity parse-local against the named file, as 6.4 judges the old ID — a discovered path of the identity's kind (11.1), an `id` over the file's spelled identities (11.2), a code unit over the file's named units (4.6) — an unparseable named file masking the check as in 6.4, the gated report of 13.3 then exiting 1. One check runs past the gate: an item ID is judged against its session's content, which no gated command reads on a failing workspace (13.3) and a corrupt session withholds — the corruption reported in the check's place (10.1, 14.21). Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. ### 12.1 `xspec build` @@ -717,7 +717,7 @@ Document forms — each a single JSON document whose top level is an object; eve ### 13.3 Graph data -xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, reference occurrences (5.7), source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4) — the generated modules with their companions (13.1) and the emitted Markdown (13.2); graph data records no paths of its own, its layout staying deliberately unenumerated (11.6). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering, while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. +xspec maintains graph data under `.xspec/`, containing requirement nodes, code locations, edges by kind, reference occurrences (5.7), source ranges (1.7), all four hashes, coverage attributes, tags, and the paths of the derived files most recently generated (13.4) — the generated modules with their companions (13.1) and the emitted Markdown (13.2); graph data records no paths of its own, its layout staying deliberately unenumerated (11.6). Graph data serves `check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at`. Read results never come from stale data. On a workspace that passes the validations of `xspec build` (12.1), when graph data is missing or does not match the current sources and configuration (a comparison from which the recorded derived-file paths are excluded — refresh leaves them unchanged, so a lagging record alone is never staleness), `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, and `at` refresh it — writing exactly what `xspec build` would write, except that no TypeScript or Markdown is generated or removed and the recorded derived-file paths are left unchanged — before answering; running only where `build` would succeed, refresh, like the finishing regeneration of 6.4, cannot fail. The record is left unchanged in every state: recorded state that exists but cannot be read as a record (14.23) is neither read, repaired, nor replaced by a refresh — these reads never consult the record and report no finding for it — so the state persists, met by the surfaces that consult the record (11.6, 6.6) and reported as staleness by `check` (14.10), until a successful `build` (12.1) or the finishing regeneration of `rename`/`move` (6.4, 6.5) replaces the record. When the current workspace fails the validations of `xspec build` — source validation errors, journal errors (14.13), and refused writes (14.22) alike: the findings a `build` would now report — `ids`, `show`, `coverage`, `impact`, `review`, and `query` report exactly those findings and exit 1 without answering — only their argument checks precede this report (12.0), and nothing is evaluated past it: a `review` subcommand then reads no session file, so a session's corruption (14.21) is reported exactly where sessions are read, on a workspace passing `build`'s validations (10.1, 12.0) — while `occurrences`, `view`, and `at` answer from the current sources per 11.2, which states the findings accompanying their answers; in either case nothing is modified: every derived file and all graph data remain byte-for-byte as they were, as after a failed `build` (12.1). `check` never refreshes; it reports staleness instead (14.10). `inventory` neither refreshes nor writes (11.6), and a preview writes nothing (6.6). Graph data is byte-deterministic for a given workspace (12.0); its content is otherwise opaque — graph data's observable contract is its location under `.xspec/`, its classification as a derived file (13.4), and the refresh, failure, and staleness behaviors above and in 14.10. ### 13.4 Derived and durable files @@ -728,7 +728,7 @@ Every file xspec writes is a plain file suitable for committing, written with st Derived-file paths belong to xspec: writing a derived file replaces whatever exists at its path, whether or not xspec wrote it. Derived files are never sources: paths whose file name contains `.xspec.`, files under `.xspec/`, and files at the configured Markdown emit destinations (7.3) are excluded from every spec and code group (7). -Writes never traverse symbolic links. A symbolic link at a derived file's path is an occupant like any other: the write replaces the link itself, and nothing is ever written through it. A durable file's path occupied by a symbolic link — or by anything other than a plain file — is never read, appended to, or replaced: such a journal is a journal error (14.13), such a session corrupt (10.1, 14.21). A write path having a symbolic link at a workspace-relative directory component is refused (14.22); path components above the workspace root are unrestricted. +Writes never traverse symbolic links. A symbolic link at a derived file's path is an occupant like any other: the write replaces the link itself, and nothing is ever written through it. A durable file's path occupied by a symbolic link — or by anything other than a plain file — is never read, appended to, or replaced: such a journal is a journal error (14.13), such a session corrupt (10.1, 14.21). A write path having a symbolic link at a workspace-relative directory component is refused (14.22; a move's destination-side case is instead the refusal of 6.5); path components above the workspace root are unrestricted. A write brings the nonexistent workspace-relative directory components of its path into existence as directories — the file-form move's fresh destination directories, a created target file's (6.5), and a first emission under `markdown.outDir` (7.3) alike — so a missing intermediate directory never refuses or fails a write. ### 13.5 Concurrency and isolation @@ -757,18 +757,18 @@ The defined error conditions — also the findings that accompany answers over a 11. Cross-module text call: a node passed to the `text` export of a spec module other than its own; additionally a TypeScript type error and a runtime throw per 4.4. 12. Policy violation: one finding per violation (7.5) — per rule and offending edge. The offending entity is a graph edge, not a spelling: the finding carries no in-source locations and concerns no path — `locations` empty, `path` `null` (12.7) — so it is no file's finding (11.2) and accompanies no answer of 11.3–11.5. Its `identities` are, in order, the violated rule's name and the edge's source identity, kind token (12.7), and target identity. Reported by `check` only: policy constrains the workspace graph, not source validity, and `build` regenerates output regardless of policy findings (7.5, 12.1). 13. Journal error: malformed, conflicting, or unreplayable entries, naming the lines; a journal path occupied by anything other than a plain file (13.4). -14. Configuration error: missing or invalid configuration — a configuration file that is not well-formed TypeScript or not in the declarative form of 7; missing required fields, unknown keys (7), or invalid profile, rule, or group shapes; group names referenced by profiles, rules, or selectors that are unknown or not of the kind the reference requires (7.4, 7.5); ambiguous kinds (7.4, 7.5); an empty `edgeKinds`, `targetTags`, rule `kinds`, or selector `tags` list; a capture violation (7.5); a glob or `markdown.outDir` resolving outside the workspace root (7, 7.3); a file matched by both a spec and a code group. Reported by every command that loads the configuration — every command but `version` (12.6), which loads none — when it loads the configuration and discovers sources, as a usage error (12.0), not a finding. +14. Configuration error: missing or invalid configuration — a configuration file that is not well-formed TypeScript or not in the declarative form of 7; missing required fields, unknown keys (7), a duplicate profile or rule name (7.4, 7.5), or otherwise invalid profile, rule, or group shapes; group names referenced by profiles, rules, or selectors that are unknown or not of the kind the reference requires (7.4, 7.5); ambiguous kinds (7.4, 7.5); an empty `edgeKinds`, `targetTags`, rule `kinds`, or selector `tags` list; a capture violation (7.5); a glob or `markdown.outDir` resolving outside the workspace root (7, 7.3); a file matched by both a spec and a code group. Reported by every command that loads the configuration — every command but `version` (12.6), which loads none — when it loads the configuration and discovers sources, as a usage error (12.0), not a finding. 15. Invalid import: in an xspec source file, an import that is not a single default binding, does not designate an xspec source file belonging to a configured spec group, or binds the identifier `S`, `Spec`, or `text` (2.1); in a TypeScript file, a `.xspec` import that does not designate such a source, a spec-module binding other than the default and `text` exports, a dynamic `import()` whose static specifier ends in `.xspec`, an export declaration or an `import X = require(…)` declaration whose specifier ends in `.xspec` (4), or an import or export declaration, `import X = require(…)`, or dynamic `import()` whose relative specifier designates a derived-file path other than through a spec module import's `.xspec` specifier (4, 13.4); in either kind of file, an import binding an identifier already bound by another import in the same file, when either import is a spec module import. 16. Invalid construct: a JSX element other than ``/``, an expression container other than a `text(...)` embedding or an MDX comment (2.7), or an export statement in a source file. 17. Invalid prop: an unknown or repeated prop, or a spread attribute, on ``/`` (2.7), an `id`, `coverage`, or `tags` value that is not a quoted-form static string literal, a `d` value that is not a braced expression (2.7), or a `coverage` value other than `required` or `none`. 18. Unsupported node usage: a spec module binding or node used in TypeScript other than as a dependency marker, a child property access, or a direct argument to a spec module's `text` export (a cross-module `text` argument is condition 11, and a non-static bare reference in expression-statement position is condition 8, not this one; a value-level use of a binding introduced type-only falls under no condition, 4.5). 19. Invalid source path: a discovered spec or code source file whose workspace-relative path contains `#` or is not valid UTF-8 (7), or a spec-group file without the `.mdx` extension (7.1). 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. -21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, by any `review` subcommand naming the session, and by `review list` (exit 1); not reported by `build`, which does not read sessions. -22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). +21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, beside a failing workspace's other findings (14); by any `review` subcommand naming the session and by `review list` (exit 1), each only on a workspace passing `build`'s validations — on a failing one the gate's findings are reported without any session being read (13.3, 12.0); not reported by `build`, which does not read sessions. +22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). A component under a move's destination path or under a derived path the destination would generate is the move's `refused-invalid-destination` (6.5), never this condition — a refused operation reports refusal reasons alone (below). 23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by the surfaces that read the record without refreshing it — `inventory` (11.6) and `rename`/`move` previews (6.6) — with one outcome, defined here for both: the surface's record-supplied datum — the inventory's recorded derived-file paths, the preview's delta — is reported explicitly unavailable, never fabricated and never read as an empty record; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other part of the answer — every other provenance's content, every other part of the preview report — is emitted in full. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). -Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. A refused operation or preview reports every applicable reason together, one finding per reason — never only the first found — each reason's applicability read on its own terms below. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID, or an ID the prefix replacement produces, collides with an ID remaining after the operation's removals — rename's prefix mapping (6.4), the section move's subtree removal (6.5) — locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it, or the section form's target path is occupied by anything other than a discovered spec source (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source (6.5), concerning that path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. +Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. A refused operation or preview reports every applicable reason together, one finding per reason — never only the first found — each reason's applicability read on its own terms below. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID, or an ID the prefix replacement produces, collides with an ID remaining after the operation's removals — rename's prefix mapping (6.4), the section move's subtree removal (6.5) — locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it, or the section form's target path is occupied by anything other than a discovered spec source (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source, or a workspace-relative directory component of it or of a derived path it would generate is occupied by anything other than a directory (6.5), concerning the destination path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. ## 15. Example From 253e7e07e33c0e8fd4e045891a1882b06ad74d2f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 15:38:19 +0000 Subject: [PATCH 30/35] =?UTF-8?q?sdg(phase-4):=20close=20IP=200001=20?= =?UTF-8?q?=E2=80=94=20write-path=20occupancy,=20spells-no-identity=20find?= =?UTF-8?q?ings,=20multi-#=20split=20(iter=2013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: 13.4/14.22 generalized from symlink-only to any non-directory occupant of a workspace-relative write-path directory component; condition 22 retitled obstructed-write-path, own-path and 6.5 destination-side exclusivities kept. Closes the plain-file-at-.xspec/outDir gap; 12.1's taxonomy, 13.3's gate enumeration, and 6.5's cannot-fail regeneration argument now cover the state. - C2: condition 1 pinned to id-attribute absence; repeated/invalid-form id is condition 17 alone; condition 2's mask extended to every spells-no-identity parent (11.2), so finding sets like + child are derivable. - I1: 12.0 pins at most one '#' in //# spellings (mirroring 11.3); more is a malformed value, a usage error — split never ambiguous. - O1: 5.3 cycle detection attributed to validation (build and check alike). - O2: 7.5 pins captures as exactly $1-$9; every other $ is a literal byte. - O3: 12.0 wrong-kind illustration now cites the 11.1/12.4 node-kind checks. - O4: 6.6 delta rationale marked rationale-not-filter; degenerates toward the full set on an empty or lagging record. Rejected/deferred: - O5 (modularization): re-deferred — SHOULD-level; near-total contract locality keeps the single file; residual delegated to the downstream problems-file net per the standing closure ruling. Closing iteration per Liaison valve ruling: REVIEW.md deleted; patch 0001 Stage: Accepted -> Applied. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/SPEC.md | 20 ++++++++++---------- specs/patches/0001-external-ui-apis.md | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/specs/SPEC.md b/specs/SPEC.md index af7e0a5..7f707b4 100644 --- a/specs/SPEC.md +++ b/specs/SPEC.md @@ -245,7 +245,7 @@ The graph contains requirement nodes and code locations. ### 5.3 Cycles -Dependency-edge cycles are invalid. `xspec check` MUST detect and report cycles in the combined graph of `contains`, `depends`, and `embeds` edges over requirement nodes, including the full cycle path. A node that depends on or embeds itself is a dependency cycle of length one. In particular, a section MUST NOT depend on or embed its own ancestor, because text expansion and effectiveHash recurse through both children and dependency targets. +Dependency-edge cycles are invalid. Validation — `build` and `check` alike (14.9) — MUST detect and report cycles in the combined graph of `contains`, `depends`, and `embeds` edges over requirement nodes, including the full cycle path. A node that depends on or embeds itself is a dependency cycle of length one. In particular, a section MUST NOT depend on or embed its own ancestor, because text expansion and effectiveHash recurse through both children and dependency targets. ### 5.4 Reference canonicalization @@ -324,7 +324,7 @@ A move operand is classified by spelling alone: an operand containing `#` is a ` * the complete identity mapping the operation would journal; * every file the operation would rewrite, relocate, or create, with every edit the operation would make in it, classed as exactly one of the following and — target-file creation excepted — located by a source range (1.7) in current, pre-operation coordinates: a reference-occurrence rewrite (5.7 — `d` references, `text(...)` references, TypeScript markers); an `id`-attribute rewrite (rename's and the section move's re-identification); an import-specifier rewrite; an import addition; an import removal — import-edit extents and insertion offsets per 6.5; the section move's origin deletion — one range spanning every byte the origin edit removes: the construct's own characters, extended over the leftover whitespace and line terminator of each line the line-drop rule additionally drops (6.5, 3), bytes contiguous with the construct, so the adjunct drop lies inside this class's range rather than forming a class of its own; the section move's target insertion point; the self-closing-target-parent rewrite when one applies (6.5); the file move's relocation of the file itself; or target-file creation — reported, when the section form's target file does not yet exist, as its own class with the insertion point at the start of the new file: the one reported location without pre-operation coordinates, and the created file's only reported edit — creation composes the file's entire initial content, subsuming the insertion and the import additions the rewrite requires there (6.5), edits no pre-operation coordinates exist to locate, while the moved text's own rewrites are reported in the origin file, inside the origin deletion's range (below). A rewrite's range is the construct it rewrites — a reference occurrence's span (5.7), the `id` attribute's own characters, the import specifier literal's characters, the target parent's self-closing tag; a removal's range spans every byte its edit removes, as the origin deletion's does; an import addition's insertion point, the section move's target insertion point, and the created target file's start are zero-length ranges at the insertion offset; the relocation's range is the entire moved file. An edit is reported without replacement text: its class and the identity mapping state what changes, and the resulting bytes are observable only by running the operation — the preview is a safety report, not an edit script whose external application would bypass the journaled mapping. Reported ranges MAY nest — the section move's re-identification rewrites locate, in the same pre-operation coordinates, inside its origin deletion's range — each edit reported under its own class, containment being geometry, not double-reporting; -* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the delta — with the one outcome 14.23 defines. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. A refused preview consults no record — it reports the refusal findings alone, its `mapping`, `files`, and `delta` `null` (12.7) — so no condition-23 finding ever accompanies a refusal. +* the derived-file delta, both directions one datum: the derived paths the operation would newly generate — paths where nothing is currently recorded as generated — and the recorded derived paths (13.3) the operation would leave no longer generated, the pre-move module path after a file move included. The delta is the identity-relevant consequence; the full regeneration set a successful operation rewrites is workspace-constant and reported by the inventory (11.6), so the preview does not repeat it — a rationale, not a filter: both directions follow the record-based rule above, and on an empty or lagging record the newly-generated direction approaches the full regeneration set. Both directions consult the recorded derived-file paths — presence at a path cannot tell a generated occupant from a foreign one — and a preview, writing nothing, never refreshes the record. Recorded state that exists but cannot be read as a record is condition 23 (14), met here in the record-supplied datum — the delta — with the one outcome 14.23 defines. The real operation is not refused in that state — a corrupt record fails no build validation, and the finishing regeneration (6.4) replaces corrupt graph data — so the preview is not refused either: the unreadable record lies on the success side of the refusal equivalence. A refused preview consults no record — it reports the refusal findings alone, its `mapping`, `files`, and `delta` `null` (12.7) — so no condition-23 finding ever accompanies a refusal. ### 6.7 Manual restructuring @@ -407,7 +407,7 @@ Named policy rules constraining which dependency edges may exist. Each rule has: A selector matches nodes (or code locations) by exactly one of: `{ group: }`, `{ files: }`, or `{ tags: [, ...] }` (matching means carrying at least one listed tag; an empty tag list is a configuration error, 14.14). A group selector MAY include `kind: "spec" | "code"`; as with `boundaryKind` (7.4), the kind MUST be inferred when the name is unambiguous and MUST be given when the name exists as both a spec group and a code group (14.14). -In `files` selectors, the `from` pattern MAY contain capture wildcards `$1`…`$9`, each appearing at most once, and the `to` pattern MAY reference them; a `to` containing captures matches only targets whose expansion agrees with the captured values. A capture matches one or more bytes within a single path segment (never `/`). When a pattern could match a path in more than one way, the match is disambiguated across the whole pattern, left to right: each wildcard (`*`, `?`, `**`) and each capture, in pattern order, takes as few bytes as possible while a match of the remainder of the pattern still exists — so every match, and every capture value, is unique. `$1-$2.ts` against `a-b-c.ts` captures `$1 = a` and `$2 = b-c`; `*$1*` against `abc` captures `$1 = a` (the leading `*` takes the empty string). A `to` referencing a capture absent from `from` is a configuration error (14.14). +In `files` selectors, the `from` pattern MAY contain capture wildcards `$1`…`$9`, each appearing at most once, and the `to` pattern MAY reference them; a capture is exactly `$` followed by one digit `1`–`9` — every other `$`, `$0` and a trailing `$` included, is a literal byte in either pattern, never a capture or a capture violation (14.14) — and a `to` containing captures matches only targets whose expansion agrees with the captured values. A capture matches one or more bytes within a single path segment (never `/`). When a pattern could match a path in more than one way, the match is disambiguated across the whole pattern, left to right: each wildcard (`*`, `?`, `**`) and each capture, in pattern order, takes as few bytes as possible while a match of the remainder of the pattern still exists — so every match, and every capture value, is unique. `$1-$2.ts` against `a-b-c.ts` captures `$1 = a` and `$2 = b-c`; `*$1*` against `abc` captures `$1 = a` (the leading `*` takes the empty string). A `to` referencing a capture absent from `from` is a configuration error (14.14). Semantics, evaluated over dependency edges of the rule's kinds: @@ -644,13 +644,13 @@ Recorded state that exists but cannot be read as a record is condition 23 (14), * The report — findings included: a failing `build`'s validation errors and `check` findings are reports — is standard-output content; usage and configuration error messages (exit 2) and all other diagnostic text are standard-error content. JSON output is in effect exactly when `--json` appears among the invocation's arguments — governing error delivery even when the arguments are themselves the error, an unknown command or flag included — or when the invoked surface is JSON-only, a single JSON document its only output form with or without `--json` (10.7, 11, 12.6). When JSON output is in effect, the single JSON document is the entire standard output, and an invocation that fails with a usage or configuration error (exit 2) emits as its entire standard output a single JSON document reporting the error — the error document of 12.7, carrying the stable code and concerned file or path (14) where the condition defines them. When JSON output is not in effect, an exit-2 error leaves standard output empty. The output form never changes an exit code, the error-precedence rules, or standard-error content. * Every command supports `--config ` (7). * A flag MAY be given at most once per invocation; repeating a flag is a usage error. List-valued flags (`--kinds`) take one comma-separated value (11). -* Arguments that name requirement nodes, graph nodes, workspace files, or file globs (``, ``, ``, `--file`) are workspace-relative, independent of the working directory. `` and `` values are identities in the form of 1.5, their `#` splitting path from id or unit; the split applies equally to an operand spelled `#` (6.5). A bare `` operand and a `--file` glob are a whole path or pattern: `#` has no delimiter role in them, so a `#`-containing spelling names the discovered file of that invalid path (14.19, 11.4), never a `path#id` pair. `--config ` and `--test-hold ` are filesystem paths resolved against the working directory. +* Arguments that name requirement nodes, graph nodes, workspace files, or file globs (``, ``, ``, `--file`) are workspace-relative, independent of the working directory. `` and `` values are identities in the form of 1.5, their `#` splitting path from id or unit; the split applies equally to an operand spelled `#` (6.5). At most one `#` is well-formed in any such value — no identity contains one in path, id segment, or unit name (1.4, 1.5, 4.6), and 11.3 pins the same bound for `--to` — so a spelling containing more than one `#` is a malformed value, a usage error, and the split is never ambiguous. A bare `` operand and a `--file` glob are a whole path or pattern: `#` has no delimiter role in them, so a `#`-containing spelling names the discovered file of that invalid path (14.19, 11.4), never a `path#id` pair. `--config ` and `--test-hold ` are filesystem paths resolved against the working directory. * Argument values are interpreted as UTF-8; an argument value that is not valid UTF-8 is a usage error. * IDs, tags, identities, session names, and paths compare byte-wise and case-sensitively; no Unicode normalization or case folding is applied anywhere (the create-time session-name restriction of 10.1 is the sole exception). * All output, generated files, and stored data are byte-deterministic for identical input: no wall-clock values, no randomness, no absolute paths, no environment-dependent content. Invocation-anchored content is the stated exception where a section calls for it — the inventory's anchoring (11.6) and configuration-error concerned paths (14), with 11.6's no-relative-path platform case the sole absolute form — itself a pure function of invocation input, deterministic per invocation. * A workspace-relative path that is not valid UTF-8 (14.19) has no plain string form. Wherever an output carries one — a discovered source or derived path in the inventory (11.6), an occurrence's referencing file (11.3), a per-file view's file or an import's resolved target (11.4), a finding's location file or concerned path (14) — it is presented in an explicitly marked byte form (12.7) that carries the path's exact bytes and is distinguishable from every plain path string, deterministically; a valid-UTF-8 path is never presented in the marked form. No identity carries such a path — no node of such a file has a defined identity (11.2) — and no argument value names one (argument values are UTF-8, above). * Where this specification calls for one shortest path and several shortest paths qualify, the reported one is the least by element-wise byte comparison of the paths' node-identity sequences. -* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); wrong-kind operands — a code source named where a spec source is required (6.4, 6.5, 11.4, 11.5); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). +* Exit codes partition all outcomes; every defined failure belongs to exactly one class. `0` — success, including informational reports (`ids`, `show`, `impact`, `query`, the `review` read subcommands including `next` with nothing to review, `coverage` without `--check`, `version`) and complete, finding-free answers (11.2, 11.6). `1` — findings: source, workspace, and operation validation failures (`build` on invalid sources, `check` findings, `coverage --check` with uncovered requirements, refused `rename`/`move` and their refused previews (6.4–6.6), refused review operations (10.7), `review` subcommands naming a corrupt session and `review list` reporting one (14.21)), and answers carrying findings or explicitly-unavailable data — emitted in full, with exit 1 (11.2, 11.6, 6.6). `2` — usage and configuration errors: unknown commands or flags; missing required flags or arguments; invalid flag values; unknown profiles, sessions, groups, review items, node identities, or files named in arguments — except on `occurrences --to`, where only a malformed identity spelling is a usage error and an unknown or unresolving one selects nothing (11.3); wrong-kind operands — a code source named where a spec source is required (6.4, 6.5, 11.4, 11.5) or where a requirement-node identity is required (11.1, 12.4); invalid session names; missing or invalid configuration (14.14), which never reaches `version` (12.6); a baseline that cannot be read or reconstructed (6.3); a mutating command refused because another is running (13.5). * The argument checks of `rename` and `move` (a nonexistent origin file or old ID; a wrong-kind, non-spec-source origin file, 6.4, 6.5) and baseline resolution (6.3) precede source validation: these usage errors are reported, and the command exits 2, even when the current workspace also fails the validations of `xspec build` (6.4, 13.3) — as configuration errors precede all source analysis (14.14). An old ID inside an unparseable origin file (14.20) is masked (14): there the validation findings are reported and the command exits 1. The reads 13.3 gates (`ids`, `show`, `coverage`, `impact`, `review`, `query`) observe the same precedence: their argument checks precede the invalid-workspace report of 13.3, so a usage-error argument — an unknown or wrong-kind name included — exits 2 whatever findings the workspace carries. Each check is judged from what it consults, identically on valid and failing workspaces: a profile or group name against the configuration (7.4, 7.5, 11.1), a session name against the session directory (10.1), and a requirement-node or graph-node identity parse-local against the named file, as 6.4 judges the old ID — a discovered path of the identity's kind (11.1), an `id` over the file's spelled identities (11.2), a code unit over the file's named units (4.6) — an unparseable named file masking the check as in 6.4, the gated report of 13.3 then exiting 1. One check runs past the gate: an item ID is judged against its session's content, which no gated command reads on a failing workspace (13.3) and a corrupt session withholds — the corruption reported in the check's place (10.1, 14.21). Within exit class 2, an error the invocation's syntax alone determines — an unknown command or flag, a repeated flag, a missing required flag or argument, a malformed value — is reported without loading configuration; a configuration error (14.14) precedes every argument check that consults configuration, discovery, or the workspace, the unknown names and files of the usage class included. ### 12.1 `xspec build` @@ -728,7 +728,7 @@ Every file xspec writes is a plain file suitable for committing, written with st Derived-file paths belong to xspec: writing a derived file replaces whatever exists at its path, whether or not xspec wrote it. Derived files are never sources: paths whose file name contains `.xspec.`, files under `.xspec/`, and files at the configured Markdown emit destinations (7.3) are excluded from every spec and code group (7). -Writes never traverse symbolic links. A symbolic link at a derived file's path is an occupant like any other: the write replaces the link itself, and nothing is ever written through it. A durable file's path occupied by a symbolic link — or by anything other than a plain file — is never read, appended to, or replaced: such a journal is a journal error (14.13), such a session corrupt (10.1, 14.21). A write path having a symbolic link at a workspace-relative directory component is refused (14.22; a move's destination-side case is instead the refusal of 6.5); path components above the workspace root are unrestricted. A write brings the nonexistent workspace-relative directory components of its path into existence as directories — the file-form move's fresh destination directories, a created target file's (6.5), and a first emission under `markdown.outDir` (7.3) alike — so a missing intermediate directory never refuses or fails a write. +Writes never traverse symbolic links. A symbolic link at a derived file's path is an occupant like any other: the write replaces the link itself, and nothing is ever written through it. A durable file's path occupied by a symbolic link — or by anything other than a plain file — is never read, appended to, or replaced: such a journal is a journal error (14.13), such a session corrupt (10.1, 14.21). A write path having a workspace-relative directory component occupied by anything other than a directory — a plain file, a symbolic link (whatever it targets), or any other non-directory occupant — is refused (14.22; a move's destination-side case is instead the refusal of 6.5); path components above the workspace root are unrestricted. A write brings the nonexistent workspace-relative directory components of its path into existence as directories — the file-form move's fresh destination directories, a created target file's (6.5), and a first emission under `markdown.outDir` (7.3) alike — so a missing intermediate directory never refuses or fails a write. ### 13.5 Concurrency and isolation @@ -738,14 +738,14 @@ All state is workspace-local; instances operating on different workspaces MUST N `xspec build` and `xspec check` MUST report actionable errors that identify the file, location, and correction. When several error conditions are present, they MUST report each of them, not only the first; a condition goes unreported only where another error makes it undetectable — an unparseable file (14.20) masks the conditions inside itself, and a reference into it reports as unresolved (14.5–14.7) — and a configuration error (14.14) precedes all source analysis. -Every reported condition carries a stable machine-readable code identifying which numbered condition it is — a code's value is its token as listed, a string (12.7), and a numeral below is the condition's ordinal, ordering findings (12.7), no part of the value: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `symlink-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. +Every reported condition carries a stable machine-readable code identifying which numbered condition it is — a code's value is its token as listed, a string (12.7), and a numeral below is the condition's ordinal, ordering findings (12.7), no part of the value: 1 `missing-id`, 2 `invalid-structural-id`, 3 `duplicate-id`, 4 `invalid-segment-or-tag`, 5 `unknown-dependency`, 6 `unknown-text-target`, 7 `unknown-ts-reference`, 8 `invalid-argument`, 9 `cycle`, 10 `stale-output`, 11 `cross-module-text`, 12 `policy-violation`, 13 `journal-error`, 14 `configuration-error`, 15 `invalid-import`, 16 `invalid-construct`, 17 `invalid-prop`, 18 `unsupported-node-usage`, 19 `invalid-source-path`, 20 `unparseable-source`, 21 `corrupt-session`, 22 `obstructed-write-path`, 23 `unreadable-record`. Stable codes cover exactly these conditions and the refusal reasons below, and no more: a plain usage error (12.0) describes the invocation the consuming tool itself composed, never workspace content to render inline, and carries no stable code — while still arriving as the JSON error document of 12.0 whenever JSON output is in effect — and review-operation refusals (10.7) likewise carry none. Every condition that locates in source carries, for each offending construct, the containing file and a source range (1.7) at the precision the condition allows. Location cardinality follows the condition's structure: a condition that several constructs jointly violate is one finding carrying a location for every participating construct, each located in the file that contains it, so every offending spelling renders in place and no representative is chosen — duplicate identities locate every bearer; an import-binding collision locates every colliding declaration; a cycle locates its full path in source, every reference spelling recording a participating dependency edge, or each participating import declaration of a spec import cycle. An entity a condition names as context rather than as an offending construct — the foreign module of a cross-module `text` call (14.11) — is identity data on the finding, not a further range. A reference spelling that records no occurrence (5.7, 11.2) is located here: for a spelling of the MDX embedding form, its finding's range is the full braced container, opening brace through closing brace — the span its occurrence would occupy (5.7) — keeping the byte classification of 11.4 exact on imperfect files. Conditions without an in-source location — configuration, path-level, journal, session, and record conditions — carry the file or path they concern; a policy violation, constraining an edge rather than any file's content, carries neither location nor concerned path — its context identities alone (12). A configuration error's concerned path is reported in the anchoring form of 11.6, identified relative to the invocation working directory: where a configuration file is concerned — the file the upward search found, or the path `--config` names — it is that file; for missing configuration with no `--config` given, it is the directory the failed upward search started from, the invocation working directory, spelled `.` (11.6). The JSON report form presents code, locations, and concerned path for every finding — the finding form of 12.7 — all conditions reported together, with the same information as the human report (12.0). The defined error conditions — also the findings that accompany answers over a consulted domain (11.2, 11.6, 6.6) — each reported by `build` and `check` unless its entry states otherwise: -1. Missing ID: a non-root section without `id`. -2. Invalid structural ID: a child ID that does not equal the parent ID plus one segment, including IDs that skip levels; the error states the expected form. A top-level section is checked against the empty prefix (exactly one segment). The check needs the parent's ID: for the immediate children of a section lacking `id`, condition 1 masks this condition — their other conditions, and this condition for their own children, report normally. +1. Missing ID: a non-root section with no `id` attribute. A section whose `id` attribute is repeated or whose value is not in quoted static-string form (2.7) is condition 17, never this one — each case spells no identity (11.2) and masks condition 2 for its immediate children (condition 2's rule). +2. Invalid structural ID: a child ID that does not equal the parent ID plus one segment, including IDs that skip levels; the error states the expected form. A top-level section is checked against the empty prefix (exactly one segment). The check needs the parent's spelled identity (11.2): for the immediate children of a section spelling no identity — its `id` absent (condition 1), repeated, or in invalid value form (condition 17) — the parent's condition masks this one; their other conditions, and this condition for their own children, report normally. 3. Duplicate ID within a file. 4. Invalid segment or tag: violation of 1.4. 5. Unknown dependency: a `d` reference that does not resolve. @@ -765,7 +765,7 @@ The defined error conditions — also the findings that accompany answers over a 19. Invalid source path: a discovered spec or code source file whose workspace-relative path contains `#` or is not valid UTF-8 (7), or a spec-group file without the `.mdx` extension (7.1). 20. Unparseable source: a spec-group file that is not well-formed MDX, a code-group file that is not well-formed TypeScript under the grammar its file name selects (`.tsx` parses as TSX, any other name as plain TypeScript), or a discovered source file of either kind that is not valid UTF-8 or begins with a byte-order mark (1.6); the error reports the location of the parse failure. 21. Corrupt review session: a session file that is not a plain file (13.4), cannot be parsed, or violates a session invariant (10.1). Reported by `check`, beside a failing workspace's other findings (14); by any `review` subcommand naming the session and by `review list` (exit 1), each only on a workspace passing `build`'s validations — on a failing one the gate's findings are reported without any session being read (13.3, 12.0); not reported by `build`, which does not read sessions. -22. Symbolic link in a write path: a workspace-relative directory component of a path xspec writes is a symbolic link (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending symbolic-link component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. A symbolic link at a derived file's own path is not an error — writing replaces the link (13.4). A component under a move's destination path or under a derived path the destination would generate is the move's `refused-invalid-destination` (6.5), never this condition — a refused operation reports refusal reasons alone (below). +22. Obstructed write path: a workspace-relative directory component of a path xspec writes is occupied by anything other than a directory — a plain file, a symbolic link (whatever it targets), or any other non-directory occupant (13.4); a nonexistent component is never this condition, since writes create those (13.4). A command refuses the write and reports it before modifying anything; `check` reports it without writing. The concerned path is the offending component's workspace-relative path — one finding per distinct offending component, whatever write paths it refuses. An occupant at a derived file's own path is not an error — the write replaces whatever occupies the path (13.4) — and a durable file's own path holding anything but a plain file is that file's condition (14.13, 14.21), never this one. A component under a move's destination path or under a derived path the destination would generate is the move's `refused-invalid-destination` (6.5), never this condition — a refused operation reports refusal reasons alone (below). 23. Unreadable recorded state: recorded generation state (13.3) that exists but cannot be read as a record — corrupt graph data, merge-conflicted or otherwise. Reported by the surfaces that read the record without refreshing it — `inventory` (11.6) and `rename`/`move` previews (6.6) — with one outcome, defined here for both: the surface's record-supplied datum — the inventory's recorded derived-file paths, the preview's delta — is reported explicitly unavailable, never fabricated and never read as an empty record; the finding accompanies the answer with its stable code; the invocation exits 1 (12.0); and every other part of the answer — every other provenance's content, every other part of the preview report — is emitted in full. The concerned path is the graph-data area (11.6): the record's layout is deliberately unenumerated (13.3), so no path inside the area is named. Not reported by `build`, whose rebuild replaces the record (12.1, 13.4), nor by the refreshing reads of 13.3, which leave the record — unreadable state included — unchanged without consulting it (13.3); `check` reports the state as staleness (14.10). Each distinct reason `rename` and `move` refuse (6.4, 6.5) — exactly what a refused preview (6.6) reports — carries a stable code and, under the location-cardinality rule above, the file, source range, or identity it concerns, so a refusal renders as precisely as a finding; refusals are findings in the exit-code partition (12.0), and the JSON report form above carries them. A refused operation or preview reports every applicable reason together, one finding per reason — never only the first found — each reason's applicability read on its own terms below. The reasons and their codes: `refused-invalid-id` — the new ID, or an ID the prefix replacement produces, is not in intrinsic ID form (one or more segments joined by `.`, each satisfying 1.4), concerning that identity — intrinsic form only: positional conformance (1.3) is `refused-structural-parent`'s, evaluated only over intrinsically valid IDs, so no identity reports under both; `refused-identity-unchanged` — the new identity equals the old (6.4; the exact self-move of 6.5), concerning it; `refused-id-collision` — the new ID, or an ID the prefix replacement produces, collides with an ID remaining after the operation's removals — rename's prefix mapping (6.4), the section move's subtree removal (6.5) — locating every colliding bearer; `refused-structural-parent` — structural parent rules (1.3) would not remain satisfied, concerning the violated identity; `refused-unresolvable-reference` — a rewritten reference would not resolve, locating each such reference spelling; `refused-cycle` — the move would create a spec import cycle or a dependency cycle (6.5), locating the would-be cycle's full path per the cardinality rule; `refused-destination-exists` — the file form's destination path is already occupied, whatever kind of filesystem object occupies it, or the section form's target path is occupied by anything other than a discovered spec source (6.5), concerning that path; `refused-missing-target-parent` — the section form's target parent is missing or lies within the moved subtree (6.5), concerning the target-parent identity; `refused-invalid-destination` — the destination file path would not be a valid discovered spec source, or a workspace-relative directory component of it or of a derived path it would generate is occupied by anything other than a directory (6.5), concerning the destination path. The invalid-workspace refusal (6.4) reports the workspace's findings themselves, each under its own numbered condition and stable code — and those alone: the reasons above are defined and evaluated only over a workspace passing `build`'s validations (6.4, 6.5), so no report mixes refusal reasons with numbered conditions. diff --git a/specs/patches/0001-external-ui-apis.md b/specs/patches/0001-external-ui-apis.md index a847940..141ee5d 100644 --- a/specs/patches/0001-external-ui-apis.md +++ b/specs/patches/0001-external-ui-apis.md @@ -1,7 +1,7 @@ # 0001 — Foundational machine surfaces for an external spec UI - **Type:** Improvement Proposal (IP) -- **Stage:** Accepted +- **Stage:** Applied - **Branch:** `claude/xspec-ui-apis-4df8fa` (harness-designated for this session; stands in for `patch/external-ui-apis`) ## Motivation From 0fb8dcaa1eca9a76a4eb9b1fde9c14ebc2f66d53 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 16:09:07 +0000 Subject: [PATCH 31/35] sdg(phase-6): cover IP 0001 surfaces in TEST-SPEC (iter 1) Applied all review items; none rejected. Critical: C1 new 5.7 section (T5.7-1..4) plus T1.7-2 code-location ranges; C2 6.6 Previews (T6.6-2..6), manual restructuring renumbered to 6.7 (T6.6-1 retired, T6.7-1 issued), T6.1-3 reference fixed, applied-mapping reports added to T6.4-1/T6.5-1; C3 11.2 section (T11.2-1..6) and 13.3 read-list updates; C4 11.3 (T11.3-1..4); C5 11.4 (T11.4-1..6); C6 11.5 (T11.5-1..3); C7 11.6 (T11.6-1..4); C8 12.6 (T12.6-1/2) plus T12.0-9/T12.0-12 updates; C9 12.7 (T12.7-1..3); C10 T12.0-2 and H-5 flipped to the JSON error-document rule; C11 T1.7-1/T10.7-7/T10.7-12 flipped to range-in-two-outputs and bare-identity endpoints; C12 stable-code notation rule, T14-6/7/8, map rows; C13 condition-23 arms in T6.6-6, T11.6-4, T12.2-2, T13.3-2, T14-4; C14 H-3 re-scoped to form-exact 12.7 surfaces vs adapter-decoded information surfaces. Important: I1 T12.0-10 gated-read precedence and T11-6 wrong-kind arms; I2 T6.4-4/T6.5-5 operand-classification arms; I3 T12.0-13; I4 T12.0-9 class members; I5 E-6 Windows leg extended (new surfaces, drive-mismatch anchoring); I6 P-11/P-12. Optional: O1 T12.5-1 dispatch list, section 11 heading, ID retirement; O2 unified JSON-only preamble for section 11. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/TEST-SPEC.md | 139 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 29 deletions(-) diff --git a/specs/TEST-SPEC.md b/specs/TEST-SPEC.md index 6d30518..bfc9acf 100644 --- a/specs/TEST-SPEC.md +++ b/specs/TEST-SPEC.md @@ -6,7 +6,7 @@ The harness treats xspec strictly as a black box. Tests drive the product exclus Sections 1–15 of this document mirror sections 1–15 of SPEC.md one-to-one. Every normative statement in SPEC.md section *N* is covered by tests in section *N* here (cross-references are explicit where one test covers statements from several sections), so coverage can be verified requirement by requirement. SPEC.md's unnumbered document preamble also carries requirements — no network access; git read only where explicitly stated, never written — covered by this introduction, T12.0-11/12, and E-1. Sections 16–18 define property-based/fuzz testing, harness self-testing and certification, and execution/CI requirements. -Test case notation: each test has a stable ID `T
-` (e.g. `T2.4-3`), a setup (workspace content), an action (commands run or consumer code compiled/executed), and expected observations. IDs are never reused; a withdrawn test's ID is retired. Where a test asserts an error, it MUST assert the exit code class (12.0) and that the report identifies the file/location/correction information SPEC.md §14 requires — not exact wording. +Test case notation: each test has a stable ID `T
-` (e.g. `T2.4-3`), a setup (workspace content), an action (commands run or consumer code compiled/executed), and expected observations. IDs are never reused; a withdrawn test's ID is retired. Where a test asserts an error, it MUST assert the exit code class (12.0), that the report identifies the file/location/correction information SPEC.md §14 requires — not exact wording — and, where §14 assigns the condition or refusal reason a stable code, that exact code string (a code is contract, not wording; 12.7, T14-6). There are currently no spec modules under `specs/modules/`; consequently there are no test modules. If a spec module `specs/modules/.md` is added, a test module `specs/modules/TEST-.md` MUST accompany it under the same rules as this document. @@ -18,9 +18,9 @@ These requirements bind the harness implementation regardless of test framework * **H-1 Workspace isolation.** Every test constructs a fresh, self-contained workspace in a unique temporary directory: `xspec.config.ts`, source files, and (when needed) a local git repository with scripted commits. Tests share no mutable state. Two harness instances MUST be able to run concurrently on the same machine (unique temporary roots), satisfying SPEC.md 13.5 isolation from the observer side. * **H-2 Blackbox drive.** Tests invoke the `xspec` executable as a subprocess with controlled working directory, arguments, and environment, and observe: exit code, standard output, standard error, and workspace file state. Consumer-side contracts (generated modules, type errors, runtime behavior, hover/go-to-definition) are exercised by compiling and running small consumer TypeScript programs under standard TypeScript tooling with no xspec runtime dependency (SPEC.md 13.1). No other channel into the product exists — in particular, invoking the product in-process (importing product code or calling a product-internal entry function) is not a permitted channel for any test, fast paths included: SPEC.md's complete interface is the executable, an in-process entry is an implementation detail outside that interface, and subprocess invocation carries the process-level contract the suite asserts (exit codes, stream separation, working directory, environment, termination; 12.0, 13.5). -* **H-3 Output adapters.** SPEC.md fixes the information content of reports and JSON documents but not their concrete shape. Each command's assertions go through a thin decoding adapter that maps the product's actual output onto the information model this document asserts against (nodes, categories, counts, paths, findings, …). Adapters are the only place aware of concrete output shape; they may be adjusted to shape, never to values, and they MUST fail loudly (test error, not pass) when required information is absent. Human-readable reports are asserted only for required information (via robust matching), never exact wording. Where a test stages a corrupt or tampered product-written file whose concrete shape SPEC.md leaves opaque (T10.1-4), the staging transformation lives in this same adapter layer — shape-aware, value-blind, applied to a file the product itself wrote, and failing loudly when the shape does not match — never fabricating such a file from an assumed layout. +* **H-3 Output adapters and form-exact surfaces.** SPEC.md 12.7 fixes the concrete JSON shape — member names, `null`-vs-omission, `[]`-vs-`null`, the range, path, byte-form, unavailability-marker, and finding value forms, and findings order — of every findings array and findings-only report, the exit-2 error document, and the document forms of 6.6, 11.3–11.6, and 12.6. Assertions on those surfaces are form-exact: tests assert the 12.7 member names and forms literally, and no adapter may re-map, rename, or coerce them — output differing from 12.7 in shape is a conformance failure, never an adapter fixture (T12.7-1..3). Everywhere else — human-readable reports, and command JSON that carries its defining section's information (12.0: `query`, `ids`, `show`, `coverage`, `impact`, the `review` payloads, the applied-mapping reports of 6.4/6.5) — SPEC.md fixes information content, not concrete shape: those assertions go through a thin decoding adapter that maps the product's actual output onto the information model this document asserts against (nodes, categories, counts, paths, …). Adapters are the only place aware of that unpinned shape; they may be adjusted to shape, never to values, and they MUST fail loudly (test error, not pass) when required information is absent. Human-readable reports are asserted only for required information (via robust matching), never exact wording. Where a test stages a corrupt or tampered product-written file whose concrete shape SPEC.md leaves opaque (T10.1-4, T6.6-6), the staging transformation lives in this same adapter layer — shape-aware, value-blind, applied to a file the product itself wrote, and failing loudly when the shape does not match — never fabricating such a file from an assumed layout. * **H-4 Byte assertions.** Where SPEC.md requires byte determinism or exact bytes (12.0 determinism, 3 Markdown output, 6.5 move edits, 13.4 stable ordering), tests assert byte equality. Where SPEC.md declares content opaque (journal entry content 6.1, graph data content 13.3), tests assert only the stated observable contract (location, line-orientation, append-only effect, refresh/staleness behavior) and MUST NOT pin opaque bytes across product versions — except for determinism checks comparing the product to itself. -* **H-5 Exit codes and streams.** Every test asserts the exact exit code and, where relevant, the stdout/stderr separation of 12.0 (reports and findings on stdout; usage/configuration errors and diagnostics on stderr; with `--json`, stdout is exactly one JSON document or empty on exit-2). +* **H-5 Exit codes and streams.** Every test asserts the exact exit code and, where relevant, the stdout/stderr separation of 12.0: reports and findings are stdout content; usage/configuration error messages and all other diagnostics are stderr content; when JSON output is in effect — `--json` among the invocation's arguments, even when the arguments are themselves the error, or a JSON-only surface (10.7, 11, 12.6) — stdout is exactly one JSON document, on exit 2 the error document of 12.7; when it is not in effect, exit-2 stdout is empty. * **H-6 Determinism protocol.** Tests marked *determinism* run the same command twice (or rebuild the same workspace in two separate directories) and assert byte-identical outputs and written files, after normalizing nothing. Workspace-relative path rules (1.5) make this well-defined across directories. * **H-7 Traceability.** The harness maintains a machine-readable mapping from test ID to the SPEC.md passage(s) it covers. The map's keys are: SPEC.md's unnumbered document preamble (T12.0-11/12, E-1); every numbered subsection; and every numbered section's own body text outside its subsections (3, 4, 5, 7, 8, 9, 10, 11, 14, and 15 — sections 1, 2, 6, 12, and 13 carry no requirements outside their subsections and are covered through them). A harness self-check (17) fails if any key lacks at least one mapped test, or if a test maps to a nonexistent key. * **H-8 Red-green compatibility.** The full suite MUST be runnable when no product is installed (or against a deliberately empty stub): every product-facing test fails with a diagnosed assertion failure — never a harness crash, hang, or false pass. Self-tests (17) and certification MUST pass before the product exists. @@ -73,7 +73,8 @@ These requirements bind the harness implementation regardless of test framework ### 1.7 Source ranges -* **T1.7-1 Range definition.** A fixture whose exact bytes are known, with an import line and multi-byte UTF-8 content preceding the first section (so byte offsets into the source diverge from code-point offsets, UTF-16 offsets, and compiled-output offsets): the source range reported by `query node` — and equal via `show` (12.4) — is a pair of zero-based byte offsets into the file's bytes, start-inclusive and end-exclusive; for a non-root node it spans the section construct's own characters, from the first character of its opening tag through the last character of its closing tag; for a self-closing section (1.1), exactly the self-closing tag's own characters; for the root node, the entire file — start 0, end the file's byte length. Asserted against precomputed offsets, so a product emitting line/column pairs, 1-based, code-point-based, or end-inclusive ranges fails. Code locations carry no source range (1.7): asserted on the 10.7 payload, where a `code-impact` scope enters as identity and presence alone (T10.7-12). Field presence across the other surfaces is covered by T11-1/T11-2, T12.4-1, and T10.7-7. +* **T1.7-1 Range definition.** A fixture whose exact bytes are known, with an import line and multi-byte UTF-8 content preceding the first section (so byte offsets into the source diverge from code-point offsets, UTF-16 offsets, and compiled-output offsets): the source range reported by `query node` — and equal via `show` (12.4) — is a pair of zero-based byte offsets into the file's bytes, start-inclusive and end-exclusive; for a non-root node it spans the section construct's own characters, from the first character of its opening tag through the last character of its closing tag; for a self-closing section (1.1), exactly the self-closing tag's own characters; for the root node, the entire file — start 0, end the file's byte length. Asserted against precomputed offsets, so a product emitting line/column pairs, 1-based, code-point-based, or end-inclusive ranges fails. A code location is presented with its source range in exactly two outputs — occurrence records (5.7, 11.3; T1.7-2) and review payloads (10.7; T10.7-12) — and everywhere a graph node appears as an edge endpoint it is a bare identity, requirement node and code location alike (1.7): asserted on `edges` rows, on a `reachable` witness path, and on `query node`'s incoming and outgoing edge lists, each traversing a code location — the reported endpoints are identities alone, no range datum accompanying them. Field presence across the other surfaces is covered by T11-1/T11-2, T12.4-1, and T10.7-7. +* **T1.7-2 Code-location ranges.** Occurrence records are the surface making every code unit's range reachable (1.7): against precomputed byte offsets, the `source` node of a marker or TS `text(...)` occurrence carries — for a whole-file location (top-level marker) — the entire file; for a function and a class declaration, the construct binding the name; for a function- or class-valued variable declaration inside a multi-declaration statement (`const a = 1, f = () => {…}`), the unit's own name through its initializer, not the enclosing statement; for the nested units of a dotted namespace (`namespace A.B`), the single namespace declaration's range shared by the `path#A` and `path#A.B` units; for a default export of a named construct, that construct's own range, and for an anonymous one, the whole export declaration's range under unit `default`; for a document-order-disambiguated `path#unit@2` (4.6), the range of its own — second — occurrence's construct. The same ranges appear on a `code-impact` scope in the 10.7 payload (T10.7-12). ## 2. Source Syntax @@ -219,13 +220,22 @@ All category tests run `impact --base ` against a committed baseline and as * **T5.6-5 Multiple flags.** A node that is simultaneously `changed` and `upstream-changed` (own edit plus dependency-target edit) carries both categories. * **T5.6-6 Added/deleted convention.** Baseline hash comparison is defined only for nodes present on both sides (5.6). Add a subtree whose root carries `d` targets (one targeting a node also edited since the baseline), `coverage="none"`, tags, children, and an embedding: every added node is `changed` only — never `metadata-changed`, `descendant-changed`, or `upstream-changed`, whatever metadata, children, or dependency edges it carries. Delete a subtree with the same features: each deleted node reports as deleted and `changed` only. (Impacted-code evaluation is the stated exception, 9.2: T9.2-1.) +### 5.7 Reference occurrences + +Occurrences are observed through `xspec occurrences` (11.3) and per-file views (11.4), in the 12.7 record form (T12.7-1, form-exact per H-3). + +* **T5.7-1 Units and duplicates.** One workspace spelling every occurrence kind: a three-entry `d` array, a single-reference `d`, an MDX `{text(...)}`, a TS `text(...)` call, and a TS marker. `occurrences` reports one occurrence per `d` array entry — never one for the array or the prop (2.2) — and one per embedding, call, and marker, each carrying its edge kind. Duplicates: `d={[BASE.a.b, BASE.a.b]}` and a twice-spelled marker collapse to one edge each (T2.2-3, T5.2-1) yet remain two distinct occurrences each, at distinct ranges. +* **T5.7-2 Spans.** Byte-precise fixtures per kind against precomputed offsets: a `d` occurrence spans exactly that one reference's own expression — an array's middle entry alone, no brackets, commas, or surrounding whitespace; an MDX embedding occurrence spans the entire braced container `{text(...)}`, opening brace through closing brace — the whole construct compilation replaces (3); a TS call occurrence spans callee through closing parenthesis, argument included (an aliased callee `t(SPEC.a)` from its `t`); a marker occurrence spans the bare reference chain alone, exclusive of the statement's terminating `;` and surrounding trivia. +* **T5.7-3 Record data and order.** Each record carries the referencing file, its own range, its edge kind, its source graph node as one identity-plus-range datum — the containing section for MDX (construct range, 1.7), the innermost enclosing named unit or file for TS (T1.7-2) — and the resolved target's identity. Order is total and deterministic (5.7): a multi-file fixture asserts file-path byte order, then range start, then range end, byte-identical across repeated runs (H-6); no two records share a range. +* **T5.7-4 No occurrence.** Constructs that record no edge record no occurrence: an import declaration (binding used and unused, 2.1); a type-only binding's marker-shaped uses (T4-4); a chain rooted at a shadowing local declaration (T4.5-4); a dynamic reference spelling and an unresolving one (each also its finding, 14.8/14.5–14.7). `occurrences` over such a workspace reports records for exactly the resolving spellings — the unresolved spelling's position reaching consumers only through its finding's range (11.2, T14-8), never as a record with an unavailable target — the answer carrying the domain's findings, exit 1 (11.2). + ## 6. Identity Continuity ### 6.1 The journal * **T6.1-1 Lifecycle.** No journal file exists after `build` in a fresh workspace; the file appears at `.xspec/journal` with the first `rename`/`move`; each subsequent operation appends exactly one line-oriented entry and rewrites nothing above it (byte-prefix asserted); `build`, `check`, `coverage`, `impact`, `review`, `query` never modify it (byte-compare around each). * **T6.1-2 Determinism.** The same operation on the same workspace state (two identical directories) appends byte-identical entries. -* **T6.1-3 Integrity.** `check` reports a malformed journal (garbage line), naming the line, with 14.13; a journal path occupied by a directory or symbolic link is a journal error (14.13). The conflicting and well-formed-yet-unreplayable arms of 12.2/14.13 admit no discriminating fixture at `check`: entry content is opaque (6.1, H-4), so such an entry cannot be authored directly, and tampering with product-written lines (duplicating one, recombining entries) has no product-independent expected outcome — manual restructuring is never journaled (6.6), so under some conforming entry contents the tampered bytes are exactly what a legitimate history of journaled operations interleaved with manual edits would have appended (entries are byte-deterministic for a given operation and workspace state, 6.1), indistinguishable and accepted, while under others they are detectably impossible and rejected. Replay failure where outcomes are pinned is tested at baseline resolution: T6.3-4's unresolvable-mapping and prefix-violation arms (6.3). +* **T6.1-3 Integrity.** `check` reports a malformed journal (garbage line), naming the line, with 14.13; a journal path occupied by a directory or symbolic link is a journal error (14.13). The conflicting and well-formed-yet-unreplayable arms of 12.2/14.13 admit no discriminating fixture at `check`: entry content is opaque (6.1, H-4), so such an entry cannot be authored directly, and tampering with product-written lines (duplicating one, recombining entries) has no product-independent expected outcome — manual restructuring is never journaled (6.7), so under some conforming entry contents the tampered bytes are exactly what a legitimate history of journaled operations interleaved with manual edits would have appended (entries are byte-deterministic for a given operation and workspace state, 6.1), indistinguishable and accepted, while under others they are detectably impossible and rejected. Replay failure where outcomes are pinned is tested at baseline resolution: T6.3-4's unresolvable-mapping and prefix-violation arms (6.3). ### 6.2 Identity guarantee @@ -243,26 +253,36 @@ All category tests run `impact --base ` against a committed baseline and as ### 6.4 Rename -* **T6.4-1 Rewrites.** Renaming a mid-tree ID rewrites: its `id`, all descendant `id`s by prefix replacement, local string references, external chain references in other files, `text(...)` targets in MDX and TS, and TS markers — workspace builds and all edges retarget (query-asserted); mapping appended to journal. +* **T6.4-1 Rewrites.** Renaming a mid-tree ID rewrites: its `id`, all descendant `id`s by prefix replacement, local string references, external chain references in other files, `text(...)` targets in MDX and TS, and TS markers — workspace builds and all edges retarget (query-asserted); mapping appended to journal. The command's own report is the applied mapping — every identity pair the operation journaled, the information of the preview's `mapping` (6.4) — carried in JSON per 12.0 (H-3 adapter; the successful operation's report shape is unpinned). * **T6.4-2 Minimal edits.** Quote style (single vs double) and access form (dot vs computed) of untouched reference parts are preserved byte-wise; only the affected parts change. Where the form cannot be kept: a new segment that is not a TS identifier is written as double-quoted computed access; a valid-identifier segment as dot access; string literals double-quoted. * **T6.4-3 Validation refusals (exit 1).** New ID invalid (1.4); equal to old; colliding with an existing ID; violating structural parent rules. Each refusal modifies nothing (workspace byte-compare). The remaining 6.4 clause — all rewritten references resolve — admits no discriminating fixture: rename rewrites only valid workspaces (T6.4-6) and retargets every affected reference to identities that exist after the operation, so a non-resolving rewritten reference is unconstructible; the clause is exercised as the always-passing side of every successful rename (T6.4-1). -* **T6.4-4 Usage errors (exit 2).** Nonexistent ``; nonexistent old ID. Checked before source validation: same exit 2 even when the workspace also has unrelated validation errors (12.0 ordering); but an old ID inside an unparseable origin file is masked — validation findings reported, exit 1. +* **T6.4-4 Usage errors (exit 2).** Nonexistent ``; nonexistent old ID; a discovered code source as `` — a wrong-kind operand, judged like existence before any content question (6.4). Checked before source validation: same exit 2 even when the workspace also has unrelated validation errors (12.0 ordering); but an old ID inside an unparseable origin file is masked — validation findings reported, exit 1. Old-ID existence is parse-local over spelled identities (6.4, 11.2): renaming an ID two sections both spell is no usage error — the bearers establish existence, their undefined node identities notwithstanding, and the duplicate-ID finding refuses instead (exit 1, the invalid-workspace refusal, T14-7); an old ID whose only would-be bearer spells no identity (its `id` attribute repeated on the tag) is nonexistent — exit 2 even beside that file's findings. * **T6.4-5 Type-level references.** A `typeof`-level reference to the old identity is not rewritten; the workspace stays xspec-valid (the consumer type error is outside xspec's validations — `build` and `check` report no finding for it). * **T6.4-6 Valid-workspace precondition.** With a pre-existing validation error elsewhere, rename refuses (exit 1) before modifying anything. * **T6.4-7 Finishing regeneration.** After a successful rename, generated modules, Markdown output, and graph data are byte-identical to a fresh `build` of the rewritten sources; `check` immediately after reports no staleness (14.10). ### 6.5 Move -* **T6.5-1 File form.** IDs unchanged; identities change file part only; the moved file's own import specifiers and other files' imports of its generated module are rewritten so everything resolves; journal appended; finishing regeneration as T6.4-7. +* **T6.5-1 File form.** IDs unchanged; identities change file part only; the moved file's own import specifiers and other files' imports of its generated module are rewritten so everything resolves; journal appended; finishing regeneration as T6.4-7; the applied-mapping report as T6.4-1 (6.5: both forms report as rename does). * **T6.5-2 Section form text edits.** Byte-exact fixtures: moved text spans opening tag's first character through closing tag's last; origin deletion drops lines left empty/whitespace-only (rule of 3); insertion immediately before the target parent's closing tag (or end of file for top-level `new-id`), followed by U+000A and preceded by one when not at line start; target file created when absent; no other byte changes beyond these edits, the identity and reference rewrites, and the finishing regeneration (6.5). Self-closing arms (1.1, 6.5), byte-exact: moving a self-closing section moves exactly the self-closing tag's own characters; a self-closing target parent is first rewritten to paired form — its `/` and any whitespace immediately before or after the `/` deleted, the closing tag matching the opening tag's name appended immediately after the tag's terminating `>` — and the insertion rule then applies before that closing tag: a `` parent becomes `` + U+000A + the moved text + U+000A + ``. * **T6.5-3 Re-identification and reference conversion.** Subtree re-identified by prefix replacement; references convert between local and imported forms; needed spec imports are added binding fresh, non-colliding identifiers and unneeded ones removed — removal is exact: an import is removed only when its binding had references and the rewrite leaves it with none, so an import whose binding was already unreferenced before the move stays (6.5, 2.1); rewritten content is byte-deterministic (two identical fixtures produce identical bytes); the full mapping is appended to the journal (6.5: both forms); finishing regeneration as T6.4-7 — after the section-form move, generated modules, Markdown output, and graph data are byte-identical to a fresh `build` of the moved sources and `check` reports no staleness. * **T6.5-4 Refusals (exit 1, nothing modified).** A move creating a spec import cycle; creating a dependency cycle; file form whose destination exists; section form whose `` is invalid per 1.4 (the mirrored "new ID is valid" check, 6.5) — a forbidden name (`then`) and a whitespace-bearing segment, one arm each; section form whose `` collides with an ID in a distinct target file — the ordinary cross-file collision, `a.mdx#x` → `b.mdx#y` with a section `y` already present in `b.mdx` (the same-file variant and its after-the-removal qualifier: T6.5-6); section form whose target parent is missing; whose target parent lies within the moved subtree; destination path in no configured spec group; in a code group as well; containing `#`; not valid UTF-8 (Linux leg); lacking `.mdx`. Plus the valid-workspace precondition as T6.4-6. -* **T6.5-5 Usage errors (exit 2).** Nonexistent origin file or origin ID, ordering as T6.4-4. +* **T6.5-5 Usage errors (exit 2).** Nonexistent origin file or origin ID; a wrong-kind (code-source) origin in each form (6.5: both forms' origin operands name discovered spec sources); ordering, masking, and parse-local ID existence as T6.4-4. Operand classification is by spelling alone (6.5): the mixed-synopsis invocations `a.mdx b.mdx#y` and `a.mdx#x b.mdx` match neither form — exit 2 — and an operand containing `#` is always a `#` pair under the 12.0 split, so the file form cannot spell a `#`-containing path (a harmless limit, such paths being invalid source paths, 14.19; T12.0-13). * **T6.5-6 Identity terms.** The new-identity checks read in identity terms (6.5): a cross-file section move keeping its ID (`a.mdx#x` → `b.mdx#x`, no `x` in `b.mdx`) is valid; the exact self-move — `#` equal to `#` — is refused (exit 1), modifies nothing, and appends no journal entry (journal byte-compared around the attempt); a same-file move whose `` collides with an ID remaining in the target file after the removal is refused. The collision clause's after-the-removal qualifier admits no discriminating fixture: structural IDs (1.3) make the vacated set exactly the moved subtree's IDs, so a `` matching only vacated identities is always independently refused — as the exact self-move, or because its target parent is missing or lies within the moved subtree (T6.5-4). The mirrored "all rewritten references resolve" clause is likewise unstageable, for T6.4-3's reason. -### 6.6 Manual restructuring +### 6.6 Previews + +(T6.6-1 is retired: it covered manual restructuring, formerly SPEC.md 6.6, now 6.7 — see T6.7-1.) + +* **T6.6-2 Modifies nothing.** A rename `--preview` and a section-form move `--preview` on workspaces where the real operation would proceed: exit 0, findings `[]`, and every byte of the workspace identical afterward — sources, journal (an absent journal stays absent), derived files, and graph data untouched; a subsequent real run on the same state performs the previewed plan, its applied mapping (T6.4-1) equal to the preview's `mapping`. Preview output is byte-deterministic across repeated runs (H-6) and, under `--json`, the form-exact 12.7 preview document (H-3). +* **T6.6-3 Refusal and scheduling equivalence.** For each refusal of T6.4-3 and T6.5-4 — the invalid-workspace precondition included — staged identically, the `--preview` invocation reports the same findings (same stable codes, locations, identities; 14) and exits 1, its `mapping`, `files`, and `delta` `null` (12.7), modifying nothing; for the usage errors of T6.4-4/T6.5-5 the preview exits 2 identically (argument checks precede either way). The equivalence is over workspace state, never scheduling (6.6): while another mutating command is held (`--test-hold`, 13.5), a `--preview` invocation runs to completion — it takes no exclusivity and never meets the mutual-exclusion refusal of T13.5-2 — and `--test-hold` combined with `--preview` is a usage error, exit 2. +* **T6.6-4 Report content.** Byte-precise fixtures asserted against precomputed pre-operation offsets, form-exact per 12.7: (a) a rename preview reports the complete identity mapping — the renamed ID and every descendant — and, per rewritten file, `id-rewrite` edits spanning each rewritten `id` attribute's own characters and `reference-rewrite` edits spanning each affected occurrence's span (5.7) across MDX and TS; (b) a section-move preview into an existing target file reports the `origin-deletion` as one range spanning the construct's own characters extended over the leftover whitespace and line terminator of each additionally dropped line (contiguous bytes: the adjunct drop lies inside this range, no class of its own), the re-identification's `id-rewrite` edits nested inside that deletion range in the same pre-operation coordinates (containment is geometry, each edit under its own class), `target-insertion` as a zero-length range at the insertion offset, `target-parent-rewrite` spanning a self-closing target parent's tag, `import-addition` as a zero-length range at the exact offset the real operation then uses (6.5; byte-asserted by running the operation on a copy), and `import-removal` spanning the declaration plus its adjunct drops; (c) a file-form move preview reports `import-specifier-rewrite` edits spanning the specifier literals and `file-relocation` spanning the entire moved file, the relocated file's entry under its current, pre-operation path; (d) a section-move preview whose target file does not exist reports, under the path the creation would occupy, exactly one `file-creation` edit at the start of the new file — the only location without pre-operation coordinates; the insertion and any import additions there are subsumed, while the moved text's own rewrites are reported inside the origin file's deletion range. Every edit is class-plus-range only — no replacement text (12.7) — and every reported class is one of the ten 12.7 class names. +* **T6.6-5 Delta.** After a build, a file-form move preview reports the derived-file delta both directions: under `generated` the destination's module, companion, and (emission enabled) Markdown paths — nothing recorded there — and under `removed` the recorded pre-move module, companions, and Markdown the operation would leave no longer generated (6.6); a rename preview on the same workspace reports `[]` in both directions (regeneration rewrites recorded paths in place), and the created-target move of T6.6-4(d) reports the new file's derived paths under `generated`. Record-based, not presence-based: with graph data deleted (T13.3-2's operational definition) the same move preview's `generated` approaches the full regeneration set — nothing being recorded — and the preview still writes nothing: no refresh, graph data still absent afterward. +* **T6.6-6 Unreadable record.** Corrupt the product-written graph data shape-blind (truncation or garbage over T13.3-2's operational path set; H-3/H-4 staging discipline, as T10.1-4 stages sessions): a move `--preview` whose plan is otherwise valid emits the full preview — `mapping` and `files` complete — with `delta` explicitly unavailable as one datum, never read as an empty record, the condition-23 finding (`unreadable-record`, concerned path the graph-data area, no path inside it named) in `findings`, exit 1 (14.23). The real operation on the same state is not refused — it proceeds, its finishing regeneration replacing the corrupt record (`check` clean afterward, T12.2-2) — and a refused preview staged on the same corrupt-record state (an identity-unchanged rename) reports the refusal findings alone, `mapping`/`files`/`delta` `null`, never a condition-23 finding (6.6: a refused preview consults no record). + +### 6.7 Manual restructuring -* **T6.6-1** Renaming an ID by editing the file directly: no journal entry; impact reports a deletion plus an addition (not continuity); dependents referencing the old identity fail validation (14.5) until rewritten. +* **T6.7-1** Renaming an ID by editing the file directly: no journal entry; impact reports a deletion plus an addition (not continuity); dependents referencing the old identity fail validation (14.5) until rewritten. (Formerly T6.6-1, retired when SPEC.md 6.6 became Previews.) ## 7. Project Configuration @@ -358,41 +378,84 @@ All category tests run `impact --base ` against a committed baseline and as * **T10.7-4 coverage sessions.** One `uncovered-requirement` item per uncovered required node of the recorded profile — scope: the node; context: ancestor chain; origin and `blockedBy` empty; item order file path then document order, with absent-scope items after the same file's present ones by scope-node identity then item `id` (10.5 ordering rule; fixture deletes an uncovered node's section after `create`). * **T10.7-5 list.** Reports every session, in byte order of session name — the fixture creates `a2`, then `a`, then `B`, and `list` reports `B`, `a`, `a2`: creation order and ASCII-case-folded order both differ from byte order (10.7, 12.0) — with name, strategy, item counts by stored status (no read-time invalidation applied — a stale-resolved item still counts under its stored status); corrupt sessions reported by name as corrupt; exit 1 iff any corrupt session exists, else 0. * **T10.7-6 status.** Items in item order with id, kind, scope, status, blocked state, plus totals by status (read-time invalidation applied). -* **T10.7-7 next.** Returns the first needing-review unblocked item in item order; when all items are resolved (and for an empty session), exits 0 and reports fully resolved in human and `--json` forms with no item in the JSON payload; `--json` payload is self-contained: scope text, context text, origin before/after text, source ranges, baseline and current hashes. +* **T10.7-7 next.** Returns the first needing-review unblocked item in item order; when all items are resolved (and for an empty session), exits 0 and reports fully resolved in human and `--json` forms with no item in the JSON payload; `--json` payload is self-contained: scope text, context text, origin before/after text, source ranges — every present node's, requirement node and present code location alike (1.7); none for absent nodes — and baseline and current hashes. * **T10.7-8 show/export.** `show ` reports the full item (10.2 fields plus the `next --json` text payload); unknown item ID → exit 2. `export` emits one JSON document — with or without `--json` — containing name, strategy, recorded creation parameters, recorded decompositions, and every item in item order with fields, blocked state, payload, and read-time invalidation applied. * **T10.7-9 split.** Splitting a `subtree-coherence` item whose scope root has children: one `subtree-coherence` item per child subtree (context: child's ancestor chain) plus one `parent-consistency` item for the scope root (context: the child subtrees; `blockedBy`: the child items); existing kind+scope items are reused with `id`/status/state kept (audit case); newly created decomposition items enter `unresolved` (10.2) — asserted on a split of a resolved item, whose status must not propagate to them — and inherit the original's `blockedBy`; every item blocked by the original becomes blocked by all decomposition items; the original is removed and its `id` never reused (assert across subsequent re-derivations, exercised in a path-blocks session and in an audit session — the re-derivation and decomposition rules of 10.5 hold for every strategy); the decomposition is recorded durably and governs re-derivation (T10.5-5); `origin` per decomposition scope (empty in audit). Refused (exit 1): `split` on any other kind; on a childless scope root. * **T10.7-10 resolve.** Sets status and records current relevant state; works on any unblocked item regardless of status (re-resolving `invalidated` and flipping a resolved status both work); resolving a blocked item refused (exit 1); unknown session or item → exit 2; `--note` stored and reported. * **T10.7-11 Coverage re-derivation.** Resolving an `uncovered-requirement` item `updated` re-derives with the session's recorded profile against the current workspace (10.5: every strategy; 10.7): a required node made newly uncovered since `create` (its covering edge removed) gains an `uncovered-requirement` item, created `unresolved` (10.2), in coverage item order; an item whose node was meanwhile covered is no longer generated and remains in the session with its status and recorded state (10.5); a generated item matching an existing kind and scope node keeps its `id`, status, and recorded state. -* **T10.7-12 Payload text contract.** A baseline fixture generating every built-in kind (a coverage session supplying `uncovered-requirement`), texts byte-asserted in `next --json` and identically via `show` and `export` (one payload rule, 10.7), with an embedding inside one asserted text to pin expansion (1.6). Scope text by kind: the scope root's subtree text for `subtree-coherence`; the scope node's subtree text for `uncovered-requirement`; the scope node's own text — not subtree text, the fixture making them differ — for `parent-consistency`, `dependency-consistency`, and `metadata-consistency`; a `code-impact` scope enters as identity and presence alone, with no text and no source range (1.7). Context text: own text where the context is an ancestor chain (`subtree-coherence`, `uncovered-requirement`); subtree text otherwise (`parent-consistency` branch children; `dependency-consistency`, `metadata-consistency`, and `code-impact` targets). Origin text: a before/after pair of the node's own text — before from the item's `baseline`, after from the current graph (the fixture edits an originating node again after `create`, so the sides differ and neither equals the create-time value); for a node added since the baseline the before side, and for a since-deleted node the after side, is the absent side of the pair — presented absent, with no text, like a node contained in no state. Absent-node provenance: a node edited between the baseline and `create`, recorded into an item's scope or context at `create`, then deleted, presents the text of the most recent graph state containing it — the `create`-time derivation, not the baseline (the values differ) — and still does after a later `updated` resolve re-derives the session without it (a state not containing the node contributes nothing); a node deleted since the baseline and never seen by a mutating derivation with newer text presents its `baseline` value. +* **T10.7-12 Payload text contract.** A baseline fixture generating every built-in kind (a coverage session supplying `uncovered-requirement`), texts byte-asserted in `next --json` and identically via `show` and `export` (one payload rule, 10.7), with an embedding inside one asserted text to pin expansion (1.6). Scope text by kind: the scope root's subtree text for `subtree-coherence`; the scope node's subtree text for `uncovered-requirement`; the scope node's own text — not subtree text, the fixture making them differ — for `parent-consistency`, `dependency-consistency`, and `metadata-consistency`; a `code-impact` scope enters as identity, presence, and — when present — its source range, with no text (10.7; review payloads are one of the two range-presenting outputs for code locations, 1.7: the fixture's location is a named unit whose range is byte-asserted per T1.7-2, and a deleted location's entry carries none). Context text: own text where the context is an ancestor chain (`subtree-coherence`, `uncovered-requirement`); subtree text otherwise (`parent-consistency` branch children; `dependency-consistency`, `metadata-consistency`, and `code-impact` targets). Origin text: a before/after pair of the node's own text — before from the item's `baseline`, after from the current graph (the fixture edits an originating node again after `create`, so the sides differ and neither equals the create-time value); for a node added since the baseline the before side, and for a since-deleted node the after side, is the absent side of the pair — presented absent, with no text, like a node contained in no state. Absent-node provenance: a node edited between the baseline and `create`, recorded into an item's scope or context at `create`, then deleted, presents the text of the most recent graph state containing it — the `create`-time derivation, not the baseline (the values differ) — and still does after a later `updated` resolve re-derives the session without it (a state not containing the node contributes nothing); a node deleted since the baseline and never seen by a mutating derivation with newer text presents its `baseline` value. -## 11. Query +## 11. Query Surfaces -All `query` output is JSON-only: a `query` subcommand without `--json` also emits a single JSON document carrying the same information as with `--json`, compared via H-3 adapters — SPEC.md 11 fixes JSON-only output and its information content, not byte-identity between the two invocation forms. +SPEC.md 11's five commands are JSON-only: for `query`, `occurrences`, `view`, `at`, and `inventory`, a single JSON document is the only output form, with or without `--json` — `occurrences`, `view`, `at`, and `inventory` in the form-exact document forms of 12.7 (H-3, T12.7-2), `query` carrying its defining section's information through H-3 adapters. Each surface's flag-less and `--json` invocations are asserted to carry the same information — never byte-identity between the two forms — and an exit-2 error of any of them arrives as the 12.7 error document on stdout (12.0; T12.0-2, T12.7-3). + +### 11.1 `xspec query` * **T11-1 node.** Returns identity, source range, own and subtree text (expanded, 1.6), all four hashes, tags, coverage attribute (absent for roots), and incoming and outgoing edges by kind. * **T11-2 nodes.** Filters `--group`, `--file `, `--tag`, `--coverage` combine conjunctively; `--coverage` matches no root; each row carries identity, source range, tags, coverage attribute (absent for roots); a `--file` pattern resolving outside the workspace root → exit 2 (invalid flag value); `--group` naming a code group → exit 2 (invalid flag value — the wrong-kind group reference of 14.14, 11). * **T11-3 subtree/ancestors.** `subtree` returns the node plus descendants in document order (root query returns the whole file); `ancestors` returns proper ancestors nearest-first ending at the file root, excluding the queried node (empty for a root); rows carry the row fields of T11-2 — identity, source range, tags, coverage attribute (11: one row contract for `nodes`, `subtree`, and `ancestors`) — asserted on `subtree` and `ancestors` rows including a tagged `coverage="none"` node and a root (attribute absent), so a product omitting a row field from either subcommand fails. * **T11-4 edges.** `--from`/`--to` accept requirement nodes and code locations; `--kinds` filters over all four kinds and defaults to no filter (contains edges included); comma-separated list form; unknown kind value → exit 2. * **T11-5 reachable.** Reports existence of a dependency path under the given kinds (default: all three dependency kinds, never `contains`) and one shortest witness path with the 12.0 tie-break (two-equal-paths fixture); equal `--from` and `--to` (a node bearing both incoming and outgoing dependency edges) report that no path exists — a zero-length path is not a path (11); `--kinds contains` → exit 2 (invalid flag value: `reachable` accepts only the three dependency kinds, 11 — `edges` accepts all four, T11-4). -* **T11-6 Identity resolution.** Bare `path` resolves to a root node for a spec-group file and to a code location for a code-group file; `path#unit` and `path#unit@N` address code locations; a path in no configured group → exit 2 (unknown, 12.0). +* **T11-6 Identity resolution.** Bare `path` resolves to a root node for a spec-group file and to a code location for a code-group file; `path#unit` and `path#unit@N` address code locations; a path in no configured group → exit 2 (unknown, 12.0). Wrong-kind operands (12.0): `query node` and `show` (12.4) given a code-group `path` or `path#unit` — a code source named where a requirement-node identity is required — each exit 2. * **T11-7 Ordering.** Every result list is deterministic: repeated runs byte-identical; content-identical workspaces in different directories produce identical output (H-6). +### 11.2 Availability on imperfect files + +Tests here drive `occurrences`, `view`, and `at`; the per-surface contracts are 11.3–11.5's, the availability rules this section's. + +* **T11.2-1 Parse-local structure, per-file masking, no writes.** Three spec files: A parseable with findings of both levels — an unresolved `d` reference and a self-cycle (resolution-level); a duplicate-ID pair, a malformed segment, an unknown prop, an invalid construct (per-file structural) — B unparseable, C finding-free. `view` over all three: A's full positional tree, construct ranges, raw attribute spellings, comment ranges, and occurrence positions are all served — structure survives A's own findings and B's invalidity; B contributes no view, its parse-failure finding accompanying; C's view is complete. The workspace fails `build`, so the gated reads report findings without answering (T13.3-3) while these surfaces answer per file — and modify nothing: graph data and derived files byte-identical around each invocation (11.2; the passing-workspace counterpart participates in refresh, T13.3-2). +* **T11.2-2 Spelled identities and interpreted data.** One file, each node's identity datum asserted via `view`: exactly one quoted static `id` → defined; a repeated `id` (values agreeing, and disagreeing — one arm each), a braced `id={"x"}`, a valueless `id`, and no `id` → each spells none, identity explicitly unavailable; two sections both spelling `x` → both unavailable, no winner, while a uniquely spelled `x.y` beneath one of them keeps its defined identity (a defined identity without defined prefix identities); descendants of a no-identity or malformed-identity section are undefined by inheritance; a section uniquely spelling `z` stays defined beside another section's invalid-form `id` attributes (uniqueness compares spelled identities only — an invalid form contests nothing). Interpreted tags and coverage: absent props define the defaults (no tags, coverage-required); a repeated, malformed, or invalid-valued `tags`/`coverage` leaves the interpreted value unavailable, its raw spelling still listed (T11.4-3). +* **T11.2-3 Invalid paths.** (Linux leg) A discovered spec source `a#b.mdx` and a non-UTF-8-named one (14.19): every node identity in each — root included — is explicitly unavailable while tree, ranges, and attributes stay on view; the condition-19 finding accompanies every answer whose domain includes the file; no identity over the invalid path is ever emitted, the non-UTF-8 path itself presented in the marked byte form (12.0, T12.7-1). A code source with `#` in its path defines no identity for its whole-file location or any unit: its spellings still record occurrences, each record's `source` explicitly unavailable (5.7, T11.3-1). Root identity is defined exactly when the file's path is valid. +* **T11.2-4 Resolution and expanded text.** Resolution turns on the referenced identity's own definedness: with duplicate spellings of `a` and a unique `a.b` beneath one bearer, a reference to `a.b` resolves and records its occurrence while a reference to `a` records none — ambiguous, every bearer undefined — reported by its finding's range, never as a record or an unavailable target. Expanded text via `view --text`: a chain A embeds B embeds C with an unresolved embedding in C → A's and B's own/subtree text unavailable (one unresolved spelling on the expansion path, or one embedding cycle — staged separately — poisons the whole value; partial expansion never occurs), sibling nodes with resolved expansions staying defined and byte-exact; removal classification is by syntactic form — after deleting an imported file, the importing file's text values are byte-identical to before (the import removed by form, its 14.15 finding notwithstanding), and a stray element (14.16) is content, preserved byte-for-byte in the enclosing text and located by its finding. +* **T11.2-5 Domain, findings, exits.** `view` naming only C (T11.2-1's finding-free file) → finding-free, exit 0, while A and B stay invalid — the domain is the requested files; naming A → A's findings of both levels accompany, exit 1, the full answer still emitted (the document complete and parseable, H-5); a two-file cycle accompanies whole when either participant is in the domain (14.9). Any finding or explicitly-unavailable datum → exit 1 with the full answer; complete and finding-free → exit 0; argument checks precede answering — unknown ``, wrong-kind ``, invalid glob, malformed `--to`, out-of-range offset each exit 2 whatever findings the named files carry (per-surface arms in T11.3-2/3, T11.4-2, T11.5-2). +* **T11.2-6 Never stale, gate findings never attach.** On a passing workspace, `occurrences`, `view`, and `at` participate in read-time refresh exactly as 13.3's reads (T13.3-2 covers them in its sweep); on a failing one they answer from current sources and write nothing (T11.2-1). A gate condition that is no domain file's finding accompanies no answer: with a garbage journal line (14.13) staged, and separately an obstructed write path (14.22), `view` of a finding-free file answers finding-free, exit 0 — those states surface through `build`, `check`, and the gated reads (13.3), never these answers. + +### 11.3 `xspec occurrences` + +* **T11.3-1 Enumeration.** Over the T5.7-* fixtures: every occurrence in occurrence order, each record carrying every 5.7 datum in the form-exact 12.7 record form (T12.7-1); in T11.2-3's invalid-path code source, records are served with `source` unavailable while `file`, `range`, `kind`, and `target` are present. +* **T11.3-2 `--file`.** A set restriction over discovered files, spec and code alike: a glob admitting a subset restricts the consulted domain — only its findings accompany; a glob matching no discovered file admits the empty set — an empty, finding-free answer, exit 0, no unknown-file usage error on this filter (contrast T11.4-2's operands); an outside-root pattern → exit 2 (invalid flag value, as 11.1); `--file` and `--to` combine conjunctively (a fixture where each filter alone admits more than the intersection). +* **T11.3-3 `--to`.** Acceptance is syntactic: well-formed spellings — `path#id`, bare `path`, an undiscovered file's identity, a masked file's, an undefined bearer's — are accepted and select the empty set (with the domain's findings; never an error); malformed spellings exit 2: more than one `#`, an empty path part, an empty segment (`a#b..c`), a whitespace-bearing or forbidden-name segment (`a#then`), a trailing empty id part (`a.mdx#`) (11.3, 1.4). Selection is exact: a resolving identity selects the occurrences targeting it — not its descendants' — and a bare path selects module-form root references (T2.2-2). +* **T11.3-4 Definitive emptiness.** In a valid workspace with no reference to node X: `occurrences --to X` → empty, finding-free, exit 0 — proof over the domain, absolute without `--file` (the whole discovered set consulted); restricted by `--file` away from a file that does hold a resolving occurrence of X, the answer is still empty, finding-free, exit 0 — the guarantee is domain-wide only, the outside occurrence neither reported nor denied (11.3). + +### 11.4 `xspec view` + +* **T11.4-1 Views and tree.** With neither operands nor `--file`, every discovered spec source is viewed, multi-file order by path bytes, one JSON document; per parseable file: the root and the full positional section tree in document order — a section nested inside an invalid non-section element parents to the innermost enclosing section construct (the enclosure 11.2's chain conditions read), the root when none encloses it; per node, its construct range and the decomposition: opening and closing tag ranges for paired sections, opening only for self-closing, neither for the root — byte-asserted against precomputed offsets (1.7). +* **T11.4-2 Operands vs restriction.** `` operands assert membership: an undiscovered file → exit 2 (unknown); a discovered code source → exit 2 (wrong-kind operand, 12.0); `--file` restricts the domain: a glob matching nothing, or only code sources, admits the empty set — empty, finding-free answer, exit 0; combining `` operands with `--file` → exit 2; the requested files form a set (a file named twice yields one view). +* **T11.4-3 Attributes and per-node data.** Raw attribute spellings as parsed, one entry per spelled attribute in tag order — a repeated `id` (both entries), an unknown prop, a spread attribute (`name` structurally absent, its text the whole braced construct), a valueless prop (bare name) — each with range and source text; inclusion is by form, the invalidity a located finding beside the view, never an omission (14.17). Per-node `identity`, `tags`, `coverage` each plain or explicitly unavailable per T11.2-2; a root's `tags` and `coverage` are structurally absent — the stated `null`, never the unavailability marker, no finding and no exit-1 consequence: a finding-free file's view exits 0 with them `null` (11.4, 12.7). +* **T11.4-4 Imports.** Every import declaration, valid and invalid, with its range; its binding name — the default binding's identifier; structurally absent for the side-effect-only, named-only, and namespace-only forms (never "unavailable"; a named-clause identifier is not this datum) — and its resolved target where specifier form and discovery define one, explicitly unavailable otherwise (`./typo.xspec`; a bare specifier), the invalidity a located 14.15 finding beside it (11.4). +* **T11.4-5 `--text` and the expansion domain.** With `--text`, each node carries own and subtree text per T11.2-4. The consulted domain: requesting only A, whose embeddings reach B and C transitively — B's and C's findings accompany (a deep unresolved spelling's or cycle's finding lies in a consulted file never requested); a non-occurrence-recording spelling is the expansion's boundary — no further file is consulted, the blocking finding lying in a file already consulted; a masked file is never consulted by expansion (no spelling resolves into it), its parse-failure finding accompanying only when itself requested; an unparseable requested file contributes no view; an invalid-path requested file keeps its view (T11.2-3). Without `--text`, requesting A consults A alone: B's findings absent, the exit following A's own findings. +* **T11.4-6 Byte classification.** On a finding-free file with imports, sections, tags, comments, and embeddings: from the view alone — tag ranges, attribute ranges, import ranges, comment ranges, embedding-occurrence container spans (5.7) — the harness classifies every byte as annotation or content and reproduces the compiled Markdown through the rules of 3, byte-equal to the emitted output (the P-2 oracle applied to view data). On an imperfect file, jointly with the findings: an invalid construct (no view entry) and a no-occurrence embedding spelling are located by their findings' ranges — the embedding form's finding spanning its full braced container (14, T14-8) — so view plus findings again position every removable construct. + +### 11.5 `xspec at` + +* **T11.5-1 Total resolution.** A file with imports, comments, nested sections, and between-section prose: offsets inside an import, a comment, deep section content, between sections, and inside opening and closing tags each resolve to the innermost section construct whose range contains the offset — the root where none does — reported with construct range and identity per 11.2; the offset equal to the file's byte length → the root; byte length + 1 → exit 2. Derivability: for every offset of the file, `at`'s resolution equals the resolution computed from the file's `view` data alone (11.5; P-12 generalizes). +* **T11.5-2 Offset spelling and operands.** `007` is accepted as 7 (leading zeros; ASCII decimal digits only); `+7`, `-1`, `" 7"`, `"7 "`, `0x7`, and an empty value each exit 2 — not a digits-only spelling (11.5). `` membership and wrong-kind checks as T11.4-2; the argument checks precede answering: the same errors on a finding-laden file still exit 2 (T11.2-5). +* **T11.5-3 Occurrences and imperfect files.** Offsets at a `d` reference expression's start, at its end − 1, and at its end, and likewise for an embedding container: within-range offsets report the containing occurrence's record and resolved target, the end offset and other outside offsets report none (start-inclusive, end-exclusive, 1.7); an unparseable file → resolution explicitly unavailable, the parse-failure finding accompanying, exit 1; a non-UTF-8-pathed source is nameable by no argument value (12.0) — every `at` spelling for it is an unknown file, exit 2 — the glob-reached view being the one route to its positions (T11.2-3; Linux leg). + +### 11.6 `xspec inventory` + +* **T11.6-1 Anchoring.** From the workspace root, `root` is `.`; from nested `a/b`, `root` is `../..` and `config` `../../xspec.config.ts`; from a sibling directory with `--config`, ascent `..` segments then descent segments, joined with `/`, no `.` segments, no trailing separator (11.6) — asserted byte-exactly, working-directory-dependence being pure invocation input (12.0). Drive-mismatch arm, Windows leg (E-6): a working directory and workspace root on different drive letters (a substituted drive mapping suffices) → the anchoring in the platform's absolute, drive-qualified spelling — the sole absolute-path case and sole platform-separator output — deterministic per invocation; on the Linux leg no absolute form ever appears. +* **T11.6-2 Configuration, sources, derived map.** The resolved view with every default and inferred kind explicit: `markdown` absent → `{"emit": false, "outDir": null}`; a defaulted profile → `targets` `"leaves"`, `edgeKinds` all three, `boundaryKind` explicit though inferred, `targetTags` `null`; group references inside profiles and rules stay configured names resolving against the reported group list; every discovered source with its group memberships (a two-group file); the derived map per spec source — module path, and Markdown destination exactly while emission is enabled, both present before any build has run (determined by configuration and discovery); a spec-group file without `.mdx` (14.19 staged beside it) → both structurally absent; with emission disabled → `markdown` `null` for every source (7.3, 12.7). +* **T11.6-3 Record, area, durables, order.** `recorded` is empty before any generation; after a build it lists the recorded derived paths — modules, companions, Markdown — each companion attributable to its source through the 13.1 naming scheme; after a configuration change without rebuild it lags, reported as recorded, not as configured (11.6). The graph-data area is reported unconditionally — before any build — as `.xspec`, no trailing separator; a foreign file placed under `.xspec/` (neither journal, session-named, nor recorded) appears in no inventory list and is never claimed (unattributed, 11.6). `journal` reports occupancy by presence alone: absent → `false`; a plain file, a directory, and a symlink each → `true`, no content read, no 14.13 from inventory. Sessions are selected by name alone: a product-written session, a garbage-content `S.json`, and a directory named `S2.json` are all listed (content unread, no 14.21 here); `notes.txt` and `.foo.json` never (10.1). Orders: paths byte order; groups, profiles, rules configuration order; session files byte order of file name. +* **T11.6-4 No parse, no write, one finding.** On a workspace whose sources fail every validation family — an unparseable file included — plus a garbage journal line and a corrupt session: `inventory` answers in full, finding-free, exit 0, modifying nothing (byte-compare; no refresh) — it parses no sources and reads no journal or session content, those findings reported where their conditions assign them, never here. Configuration errors keep precedence: missing and invalid configuration → exit 2, the error document, no inventory. The one finding it ever carries: with the record corrupted shape-blind (T6.6-6's staging), `recorded` is explicitly unavailable — never read as empty — with the condition-23 finding (stable code, concerned path the graph-data area), exit 1, every other member emitted in full (14.23). + ## 12. Commands ### 12.0 Global conventions -* **T12.0-1 --json everywhere.** For every command and subcommand this specification covers, `--json` emits exactly one JSON document as the entire standard output, carrying the same information as the human report (adapter-verified per command in the sections above; this test sweeps that every command accepts the flag). -* **T12.0-2 Streams.** A failing `build`'s validation errors and `check`'s findings are standard-output content (exit 1); usage/configuration errors print diagnostics to standard error with empty standard output under `--json` (exit 2); non-JSON diagnostics never contaminate a `--json` stdout. +* **T12.0-1 --json everywhere.** For every command and subcommand this specification covers, `--json` emits exactly one JSON document as the entire standard output, carrying the same information as the human report (adapter-verified per command in the sections above; this test sweeps that every command accepts the flag; the JSON-only surfaces of 10.7, 11, and 12.6 emit the same single document with the flag as without). +* **T12.0-2 Streams.** A failing `build`'s validation errors and `check`'s findings are standard-output content (exit 1); usage and configuration error messages are standard-error content. With JSON output in effect — `--json` among the arguments, even when the arguments are themselves the error (an unknown command; an unknown flag), or a JSON-only surface (10.7, 11, 12.6) — an exit-2 invocation emits the 12.7 error document as its entire stdout (T12.7-3); without JSON in effect, exit-2 stdout is empty. Non-JSON diagnostics never contaminate a JSON stdout, and the output form never changes an exit code. * **T12.0-3 --config.** Every command accepts `--config `; a relative path resolves against the working directory, not the workspace root. * **T12.0-4 Flag repetition.** Repeating a flag on any command → exit 2; list-valued flags take one comma-separated value (`--kinds depends,embeds`). * **T12.0-5 Argument addressing.** ``, ``, ``, and `--file` arguments are workspace-relative with `/` separators, independent of the working directory (run each representative command from a subdirectory); `--test-hold ` resolves against the working directory (13.5). Native-separator negative: an argument spelled with `\` (`specs\A.mdx`) names no workspace file — paths compare byte-wise — and is an unknown-file usage error, exit 2; discriminating on the Windows leg (E-6), where `\` is the native separator. An argument value that is not valid UTF-8 (raw bytes in the OS argument vector, Linux leg) → usage error, exit 2 (12.0). * **T12.0-6 Case and bytes.** IDs, tags, identities, session names, and paths compare byte-wise case-sensitively: `A.mdx` vs `a.mdx` identities are distinct; `--tag Foo` does not match `foo`; no Unicode normalization (NFC vs NFD spellings of one tag are two tags). Single-casing path probe, stageable on any filesystem: in a workspace whose only source is `specs/A.mdx`, an argument `specs/a.mdx` (`show`, representative) names no workspace file — paths compare byte-wise — and is an unknown-file usage error, exit 2; rerun on the Windows leg (E-6), where a product resolving path arguments through case-insensitive filesystem lookups wrongly finds the file. Sole exception: session-name creation collision (T10.1-2). * **T12.0-7 Determinism.** Representative sweep: `build` outputs, generated files, graph data, Markdown, journal entries, session files, and every report are byte-identical across repeated runs and across content-identical workspaces at different absolute paths (no wall-clock, randomness, absolute paths, or environment leakage; run with differing irrelevant environment variables). * **T12.0-8 Shortest-path tie-break.** Where one shortest path is reported (coverage 8.2, impact 9.3, reachable 11), among equal-length candidates the element-wise byte-least node-identity sequence is reported (dedicated fixtures per command). -* **T12.0-9 Exit-code partition.** A table-driven sweep asserting one representative per class per command family: 0 (success and informational reports: `ids`, `show`, `impact` with differences, `query`, review reads including fully-resolved `next`, `coverage` without `--check`); 1 (findings: failing `build`, `check` findings, `coverage --check` uncovered, refused `rename`/`move`, refused review operations, corrupt-session reports); 2 (usage/configuration: unknown command; unknown flag; missing required flag/argument; invalid flag value; unknown profile/session/group/item/node/file; invalid session name; configuration errors; unreadable baseline; mutual-exclusion refusal). -* **T12.0-10 Check ordering.** Covered by T6.4-4/T6.5-5 (rename/move existence checks precede source validation; unparseable-file masking flips to exit 1) and T6.3-4's precedence arm (baseline resolution precedes source validation). +* **T12.0-9 Exit-code partition.** A table-driven sweep asserting one representative per class per command family: 0 (success and informational reports: `ids`, `show`, `impact` with differences, `query`, review reads including fully-resolved `next`, `coverage` without `--check`, `version`, and complete finding-free answers — `occurrences`/`view`/`at` over a clean domain, `inventory`, a successful preview; 11.2, 11.6, 6.6); 1 (findings: failing `build`, `check` findings, `coverage --check` uncovered, refused `rename`/`move` and their refused previews, refused review operations, corrupt-session reports, and answers carrying findings or explicitly-unavailable data — emitted in full; 11.2, 11.6, 6.6); 2 (usage/configuration: unknown command; unknown flag; missing required flag/argument; invalid flag value; unknown profile/session/group/item/node/file — except `occurrences --to`, where only a malformed spelling is a usage error, T11.3-3; wrong-kind operands — a code source where a spec source or a requirement-node identity is required; invalid session name; configuration errors; unreadable baseline; mutual-exclusion refusal). +* **T12.0-10 Argument-check precedence.** Rename/move and baseline arms: T6.4-4/T6.5-5 (existence, kind, and masking) and T6.3-4. Gated reads (12.0): on one workspace failing `build`'s validations, each gated read given a usage-error argument exits 2 with that error and reports no validation findings — `coverage `; `query nodes --group `; `review status `; `show #` and `query node ` (T11-6) — each check judged from what it consults (configuration; the session directory; parse-local spelled identities of the named file, 11.2), with the same names on a valid twin workspace giving the same exit-2 errors. Masking: `show #` on the failing workspace → the gated report, exit 1 (as T6.4-4). Past the gate: on a passing workspace, `review resolve --status updated` reports the corruption, exit 1 — the item ID judged only against session content, which the corruption withholds (10.1; an unknown item ID in a well-formed session stays exit 2, T10.7-10). Within class 2: an unknown command and a repeated flag are reported without loading configuration — identically with the workspace's configuration file invalid or missing — while a configuration error precedes every check that consults configuration or discovery: `coverage ` with invalid configuration reports 14.14, not the unknown profile (12.0). * **T12.0-11 Git is read-only.** SPEC.md's preamble: git data is read only where explicitly stated and never written. On a freshly built git fixture, around each git-reading invocation — `impact --base`, `review create --base`, and `review status`/`next`/`resolve` on the resulting baseline session (whose generator runs reconstruct the recorded baseline, 6.3/10.4) — everything under `.git/` is byte-identical before and after (same file set, same bytes: refs, HEAD, index, and objects untouched), and no workspace file changes except those the command's own specification writes (the session file; none for `impact`). -* **T12.0-12 Git-less operation.** The non-baseline surface — `build`, `check`, `ids`, `show`, `coverage`, `query`, `rename`, `move`, and `review` with the `audit` and `coverage` strategies through `create`/`list`/`status`/`next`/`show`/`split`/`resolve`/`export` — runs to its specified outcomes in a workspace that is not a git repository and has no enclosing repository. Only baseline-taking invocations (`impact --base`, `review create --base`, later commands on a baseline session) require git; T10.6-1's git-less audit is one instance of this sweep. +* **T12.0-12 Git-less operation.** The non-baseline surface — `build`, `check`, `ids`, `show`, `coverage`, `query`, `occurrences`, `view`, `at`, `inventory`, `version`, `rename`, `move` (their `--preview` invocations included), and `review` with the `audit` and `coverage` strategies through `create`/`list`/`status`/`next`/`show`/`split`/`resolve`/`export` — runs to its specified outcomes in a workspace that is not a git repository and has no enclosing repository. Only baseline-taking invocations (`impact --base`, `review create --base`, later commands on a baseline session) require git; T10.6-1's git-less audit is one instance of this sweep. +* **T12.0-13 `#` in operands.** More than one `#` in a ``, ``, `--to`, or move-operand value (`a#b#c`) is a malformed value — exit 2 on `show`, `query node`, `occurrences --to`, and `move` (12.0). A bare `` operand or `--file` glob is a whole path or pattern with no delimiter role for `#`: with a discovered source `specs/a#b.mdx` staged (Linux leg; condition 19), `view specs/a#b.mdx` names that discovered file — membership holds, the view served with identities unavailable, exit 1 (T11.2-3), never a `specs/a` + `b.mdx` pair (which would be exit 2, unknown file); `at specs/a#b.mdx 0` resolves the same way, and `occurrences --file 'specs/a#*'` matches it as a pattern. ### 12.1 `xspec build` @@ -404,7 +467,7 @@ All `query` output is JSON-only: a `query` subcommand without `--json` also emit ### 12.2 `xspec check` * **T12.2-1 Green path.** On a freshly built valid workspace, `check` exits 0. -* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). +* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding); the unreadable-record unit form (14.10/14.23): with graph data corrupted shape-blind (T6.6-6's staging), `check` reports one condition-10 finding under the unit form alone — concerned path the graph-data area, no path inside it named, never the mismatch form beside it, and the recorded-file form undetectable while the state holds — and a successful `build` replaces the state (`check` clean afterward; `inventory` reports `recorded` again, T11.6-4); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). * **T12.2-3 Never refreshes.** `check` on a stale workspace reports staleness and leaves graph data and derived files byte-identical (13.3). ### 12.3 `xspec ids` @@ -418,7 +481,20 @@ All `query` output is JSON-only: a `query` subcommand without `--json` also emit ### 12.5 Dispatch -* **T12.5-1** `coverage`, `impact`, `review`, `query`, `rename`, `move` behave per sections 8, 9, 10, 11, 6 (covered there); an unknown subcommand or command → exit 2. +* **T12.5-1** `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, `at`, `inventory`, `rename`, `move` behave per sections 8, 9, 10, 11, and 6 (covered there); an unknown subcommand or command → exit 2. + +### 12.6 `xspec version` + +* **T12.6-1 Surface and values.** `xspec version` emits, with and without `--json`, a single JSON document as its entire stdout in the 12.7 form: `{"product", "interface"}`, both strings, `interface` exactly `"1"` (form-exact, H-3); both values byte-identical across invocations of one build (fixed per build); usage errors keep exit 2 — an unknown flag on `version` yields the error document (T12.0-2). +* **T12.6-2 Workspace independence.** Byte-identical answers, exit 0: inside a valid workspace; in a directory with no discoverable configuration (where the other commands exit 2, T7-1); with invalid configuration present; and with `--config` naming a nonexistent and a malformed file — accepted, never consulted (12.6). Configuration-error precedence never reaches `version` (14.14): the same invalid-configuration fixture makes `build` exit 2, the discriminating pair. + +### 12.7 JSON document forms + +Assertions here — and wherever these forms appear across the suite — are form-exact (H-3): member names, `null`-vs-omission, `[]`-vs-`null`, and orderings are asserted literally, never adapted. + +* **T12.7-1 Value forms.** A source range is `{"start", "end"}`, non-negative integers, everywhere the 12.7 surfaces carry one. Paths: valid-UTF-8 paths are plain strings; a non-UTF-8 path (Linux leg) is `{"bytes": "…"}` — its exact bytes as lowercase hexadecimal, two digits per byte — asserted at each output the 12.0 rule names: an inventory source path, an occurrence's referencing file, a view's file and an import's resolved target, and a finding's location file and concerned path; a valid-UTF-8 path never takes the byte form. Unavailability is exactly `{"unavailable": true}`, and no object of any other form carries a member named `unavailable` (a structural walk over every 12.7 document the suite captures; S-5 guards the walk). A finding is `{"code", "message", "locations", "path", "identities"}`: `code` the stable token string or `null` where 14 assigns none (a review-refusal finding); `locations` one `{"file", "range"}` per offending construct, ordered by file bytes, then start, then end, `[]` for unlocated conditions; `path` `null` for located conditions, the concerned path otherwise; `identities` contractual where 14 states them — a policy finding carries the rule name, source identity, kind token, and target identity in that order with `locations` `[]` and `path` `null` (14.12), a cross-module call names the foreign module (14.11), a refusal reason its concerned identity (T14-7). +* **T12.7-2 Findings arrays and document forms.** A workspace staging several conditions, and a multi-reason refusal (T14-7): every findings array is ordered by code — numbered conditions in numeric order, then refusal reasons in 14's listed order, then code-less findings — then by locations element-wise (a proper prefix sorting first), then by concerned path (`null` first; byte-form and plain paths in one byte order), then by identities, then by message; identically-staged duplicate findings collapse to one. Document forms: `build`/`check`/gated-read/refused-operation reports are `{"findings": […]}`; a refused preview keeps `{"findings", "mapping", "files", "delta"}` with the three `null` (T6.6-3); `occurrences` is `{"findings", "occurrences"}`; `view` `{"findings", "views"}`, each node `{"identity", "range", "opening", "closing", "attributes", "tags", "coverage", "children"}` plus `ownText`/`subtreeText` exactly when `--text` is given (the stated conditional presence — absent without the flag), `attributes` entries `{"name", "range", "text"}`, imports `{"range", "name", "target"}`; `at` `{"findings", "resolution"}`, `resolution` `{"section", "occurrence"}` with `occurrence` `null` when the offset lies in none; `inventory` and previews per T11.6-* and T6.6-4/5; `version` `{"product", "interface"}`. Member presence: `null` is never omission (a refused preview still carries all four members; an unset `outDir` is `null`); empty lists are `[]`, never `null` (a finding-free `findings`, a root's `attributes`, an empty delta direction); stated `null`s vs structural absence per surface (a root's `tags`/`coverage`, T11.4-3; an absent `targetTags`, T11.6-2). +* **T12.7-3 Error document.** Exit-2 invocations with JSON in effect emit `{"error": …}` holding one finding form as the entire stdout: a configuration error → stable code `configuration-error` and concerned path in the anchoring form (the configuration file; for a failed upward search with no `--config`, `.`); a plain usage error → `code` and `path` `null`; one finding however many defects — a configuration file with several distinct defects yields a single condition-14 finding. JSON is in effect for a JSON-only surface without `--json` (`inventory` with an unknown flag) and whenever `--json` appears among the arguments, the arguments themselves erroneous included (an unknown command beside `--json`) — each the error document on stdout, diagnostics on stderr (T12.0-2). ## 13. Workspace Files @@ -433,9 +509,9 @@ All `query` output is JSON-only: a `query` subcommand without `--json` also emit ### 13.3 Graph data -* **T13.3-1 Serving reads.** After `build`, the read commands (`check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`) answer without error; graph data lives under `.xspec/`. -* **T13.3-2 Refresh.** Delete the graph data — operationally, here and in T13.4-3: every path under `.xspec/` except the durable `.xspec/journal` and `.xspec/reviews/` (13.4); graph-data content is opaque (H-4), so tests only ever remove or compare it whole — (or edit a source) and run each of `ids`, `show`, `coverage`, `impact`, `review status`, `query`: the answer reflects current sources (never stale data), graph data is rewritten as `build` would write it, but no TypeScript or Markdown is generated or removed and recorded derived-file paths are unchanged (a stale generated module stays stale — `check` still reports 14.10 afterwards). -* **T13.3-3 Failed refresh.** With invalid sources, each read command reports the validation errors, exits 1, answers nothing, and modifies nothing (derived files and graph data byte-identical). The mutating `review` subcommands observe the same rule — 13.3 binds `review` whole, and `create`, `resolve`, and `split` all consult the current graph: on a workspace holding a session, a configured coverage profile, and a resolvable commit from when its sources were valid, a source is then edited to fail validation, and `review create` under each of `--base` (the resolvable ref — baseline resolution precedes source validation, 12.0, so the refresh failure is the operative error), `--strategy audit`, and `--coverage`, plus `resolve` and `split` naming the pre-existing session and an unblocked item, each reports the validation errors, exits 1, and modifies nothing: no session file is created, and the existing session file, journal, derived files, and graph data are byte-identical (no status recorded, no decomposition). +* **T13.3-1 Serving reads.** After `build`, the read commands (`check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, `at`) answer without error; graph data lives under `.xspec/`. +* **T13.3-2 Refresh.** Delete the graph data — operationally, here and in T13.4-3: every path under `.xspec/` except the durable `.xspec/journal` and `.xspec/reviews/` (13.4); graph-data content is opaque (H-4), so tests only ever remove or compare it whole — (or edit a source) and run each of `ids`, `show`, `coverage`, `impact`, `review status`, `query`, `occurrences`, `view`, `at`: the answer reflects current sources (never stale data), graph data is rewritten as `build` would write it, but no TypeScript or Markdown is generated or removed and recorded derived-file paths are unchanged (a stale generated module stays stale — `check` still reports 14.10 afterwards). Record discipline (13.3): with the record corrupted shape-blind instead (T6.6-6's staging), each refreshing read answers finding-free — exit 0 on the otherwise clean workspace — reporting nothing for the record and leaving the corrupt state neither read, repaired, nor replaced: `inventory` afterwards still reports `recorded` unavailable (T11.6-4), until a successful `build` or a finishing regeneration (6.4) replaces the state. +* **T13.3-3 Failed refresh.** With invalid sources, each read command reports the validation errors, exits 1, answers nothing, and modifies nothing (derived files and graph data byte-identical). The mutating `review` subcommands observe the same rule — 13.3 binds `review` whole, and `create`, `resolve`, and `split` all consult the current graph: on a workspace holding a session, a configured coverage profile, and a resolvable commit from when its sources were valid, a source is then edited to fail validation, and `review create` under each of `--base` (the resolvable ref — baseline resolution precedes source validation, 12.0, so the refresh failure is the operative error), `--strategy audit`, and `--coverage`, plus `resolve` and `split` naming the pre-existing session and an unblocked item, each reports the validation errors, exits 1, and modifies nothing: no session file is created, and the existing session file, journal, derived files, and graph data are byte-identical (no status recorded, no decomposition). The never-gated contrast: on the same failing workspace `occurrences`, `view`, and `at` answer per file under 11.2 (T11.2-1) and `inventory` answers whatever the sources' validity (T11.6-4) — none of them modifying anything. * **T13.3-4 Determinism.** Graph data files are byte-deterministic across rebuilds of an identical workspace (content otherwise unasserted, H-4). ### 13.4 Derived and durable files @@ -462,13 +538,16 @@ All mutual-exclusion tests use the `--test-hold ` seam for determinism. ## 14. Validation Errors -Sections 1–13 exercise each numbered condition in its home context; this section adds the reporting-contract tests. Primary tests per condition (not exhaustive; the H-7 map is the complete record): 14.1 (T1.3-1), 14.2 (T1.3-2/3/4/6), 14.3 (T1.3-5), 14.4 (T1.4-1/4), 14.5/14.6/14.7 (T14-2), 14.8 (T2.4-2/3, T4.3-2, T4.5-3), 14.9 (T2.1-5, T5.3-1/2), 14.10 (T12.2-2), 14.11 (T4.4-1), 14.12 (T7.5-2/6), 14.13 (T6.1-3, T13.4-6), 14.14 (T7-1..T7.5-1), 14.15 (T2.1-2/3, T4-2), 14.16 (T2.7-1), 14.17 (T2.5-3, T2.7-3), 14.18 (T4.5-5), 14.19 (T1.5-2, T7.1-1), 14.20 (T1.6-5, T14-3, T14-5), 14.21 (T10.1-4), 14.22 (T13.4-6). +Sections 1–13 exercise each numbered condition in its home context; this section adds the reporting-contract tests. Primary tests per condition (not exhaustive; the H-7 map is the complete record): 14.1 (T1.3-1), 14.2 (T1.3-2/3/4/6), 14.3 (T1.3-5), 14.4 (T1.4-1/4), 14.5/14.6/14.7 (T14-2), 14.8 (T2.4-2/3, T4.3-2, T4.5-3), 14.9 (T2.1-5, T5.3-1/2), 14.10 (T12.2-2), 14.11 (T4.4-1), 14.12 (T7.5-2/6), 14.13 (T6.1-3, T13.4-6), 14.14 (T7-1..T7.5-1), 14.15 (T2.1-2/3, T4-2), 14.16 (T2.7-1), 14.17 (T2.5-3, T2.7-3), 14.18 (T4.5-5), 14.19 (T1.5-2, T7.1-1), 14.20 (T1.6-5, T14-3, T14-5), 14.21 (T10.1-4), 14.22 (T13.4-6), 14.23 (T6.6-6, T11.6-4, T12.2-2, T13.3-2); the refusal reasons and their codes (T14-7; staged at T6.4-3, T6.5-4, T6.5-6, T6.6-3); the stable-code and location-cardinality contracts (T14-6, T14-8). * **T14-1 Actionable and complete reporting.** A workspace seeded with several independent error conditions across files: `build` and `check` report each of them (not only the first), and every report identifies file and location and states a correction-oriented message (information presence, not wording). * **T14-2 Unresolved references.** A `d` reference, a `text(...)` target, and a TypeScript marker/`text` call that do not resolve → 14.5, 14.6, 14.7 respectively; the TS case is also a type error against the generated module (asserted when a prior valid generation exists). * **T14-3 Masking.** An unparseable file (14.20 — malformed MDX; malformed TS under the grammar its name selects: a TSX-only construct in a `.ts` file; invalid UTF-8; BOM) masks conditions inside itself, and every reference into it from other files reports as unresolved (14.5–14.7); the parse-failure location is reported. A configuration error suppresses all source analysis: only 14.14 is reported (exit 2) even with invalid sources present. -* **T14-4 Reporter matrix.** 14.10 and 14.12 reported by `check` only (a stale workspace `build`s successfully by regenerating; a policy-violating workspace `build`s successfully); 14.21 reported by `check`, by `review` subcommands naming the session, and by `review list` — not by `build`; every other condition reported by both `build` and `check`. +* **T14-4 Reporter matrix.** 14.10 and 14.12 reported by `check` only (a stale workspace `build`s successfully by regenerating; a policy-violating workspace `build`s successfully); 14.21 reported by `check`, by `review` subcommands naming the session, and by `review list` — not by `build`; 14.23 reported by `inventory` and `rename`/`move` previews only — `check` reports the state as 14.10's unit form, and `build` and the refreshing reads never do (the rebuild replaces the record; the reads leave it unconsulted, T13.3-2); 14.14 delivered as an exit-2 error by every command that loads configuration — never `version` (T12.6-2); every other condition reported by both `build` and `check`, and as a domain file's finding accompanying `occurrences`/`view`/`at` answers (11.2, T11.2-5). * **T14-5 Grammar selection.** A file matched by a code group and named `.tsx`, containing TSX-only syntax (not parseable as plain TypeScript — T14-3's construct) inside a named unit that also holds a dependency marker and a `text(...)` call: `build` succeeds — `.tsx` parses as TSX (14.20) — and the marker's `references` edge and the call's `embeds` edge are recorded and attributed to that unit per 4.6. The negative direction, the same TSX-only construct in a `.ts` file failing 14.20, is T14-3's; a further arm stages it in a code-group file of another name (`.mts`), failing 14.20 identically — any name but `.tsx` selects plain TypeScript (14.20), discriminating against products keying specifically on `.ts`. +* **T14-6 Stable codes.** For each of the 23 conditions, staged via its primary test's fixture and read from its stated reporter (T14-4): the finding carries the exact token 14 lists (`missing-id` … `unreadable-record`) as its `code` in the JSON report form, where 12.7 pins it — the value is the token string alone, the ordinal numeral no part of it — so a product omitting or misspelling a code fails even where exit class and located information are right. A plain usage error and a review-operation refusal carry no stable code — `code` `null` (14, T12.7-1/3). +* **T14-7 Refusal reasons.** Staged refusals asserting each stable code with its concerned file, range, or identity (14): `refused-invalid-id` (concerning the invalid identity; intrinsic form only — a structurally misplaced but intrinsically valid new ID reports `refused-structural-parent` alone, never both); `refused-identity-unchanged`, reported alone by an identity-unchanged rename — no collision reason beside it (6.4: the after-removal check collides with nothing); `refused-id-collision`, locating every colliding bearer; `refused-structural-parent` (the violated identity); `refused-cycle`, locating the would-be cycle's full path; `refused-destination-exists` (the occupied path; the section form's non-spec-source occupant included); `refused-missing-target-parent` (the target-parent identity); `refused-invalid-destination` (the destination path; the destination-side directory-component case of 6.5 reports this code, never 14.22 — a plain file staged as a destination directory component). `refused-unresolvable-reference` admits no fixture (T6.4-3/T6.5-6) and is asserted only as the always-passing side of successful operations. Every applicable reason reports together, one finding per reason: a section move staged to both collide (`` present in the target file) and create a dependency cycle reports both findings, never only the first. The invalid-workspace refusal reports the workspace's numbered findings alone — on a workspace failing validation, a rename staged to also collide reports the validation findings only, exit 1, no refusal reason evaluated or reported beside them (6.4, 14). +* **T14-8 Location cardinality.** A condition several constructs jointly violate is one finding locating every participant, each in its containing file: a triple-duplicated ID → one condition-3 finding with three locations (one per bearer, no representative chosen); an import-binding collision → one condition-15 finding locating every colliding declaration; a cross-file dependency cycle → one condition-9 finding locating its full path, every participating reference spelling, and a spec import cycle every participating import declaration; a no-occurrence MDX embedding spelling → its condition-6 finding's range the full braced container — the span its occurrence would occupy (5.7), keeping T11.4-6's byte classification exact; a policy finding → `locations` `[]`, `path` `null`, its context identities alone (T12.7-1). Location order within a finding is file bytes, then start, then end (12.7). ## 15. Example @@ -488,6 +567,8 @@ Property tests generate inputs from seeded, reproducible generators (H-10), asse * **P-8 Parser robustness.** Fuzzed byte inputs (mutated MDX/TS/config, invalid UTF-8, BOMs, giant nesting, pathological line terminators): every command terminates, never emits a partial JSON document on `--json`, and always exits 0, 1, or 2 per the 12.0 partition; `build` failures modify nothing. * **P-9 Review session invariants.** Random sequences of valid review operations (create/next/resolve/split/re-derive triggers) interleaved with workspace edits: at most one item per kind and scope node; `blockedBy` acyclic; retired `id`s never reused; `next` always returns an unblocked needing-review item or reports fully resolved; reads never change session bytes; stored sessions always re-read as non-corrupt. * **P-10 Concurrency.** Randomized schedules of concurrent readers and one mutating command (via `--test-hold` and process kills): readers observe only prior-or-complete file states (T13.5-5); mutual exclusion never loses a journal append or a resolution (post-hoc: journal lines = successful `rename`/`move` operations — the journal's only writers, 6.1; session statuses = successful resolves). +* **P-11 Availability robustness.** Fuzzed and mutated spec and code sources (P-8's generators — the availability contract is precisely an imperfect-input surface) driven through `occurrences`, `view` (with and without `--text`), and `at` at random offsets: every invocation terminates; stdout is one complete JSON document, never partial; the exit is 0 or 1 per 11.2 (2 only for staged argument errors); every datum is exactly one of plain value, `null`, or `{"unavailable": true}` (11.4, 12.7); any finding or unavailable datum implies exit 1 with the full document emitted, and exit 0 implies a finding-free document carrying none. +* **P-12 at ≡ view; occurrence order.** For random workspaces: for every file and every offset 0…byte length, `at`'s resolution — section identity, construct range, containing occurrence — equals the resolution computed from that file's `view` document alone (11.5, T11.5-1); and the workspace-wide `occurrences` enumeration equals the view-collected occurrences sorted by file bytes, range start, range end — total, duplicate-free, byte-identical across runs (5.7). ## 17. Self-Tests and Certification @@ -510,7 +591,7 @@ Confidence that the harness itself is correct comes primarily from certification * **E-3 Parallelism.** The suite runs its tests in parallel and MUST pass under parallel execution; multiple suite instances can run on one machine concurrently (H-1, T13.5-6). * **E-4 No production keys, no external services.** The suite uses no credentials and contacts no hosted services (there are none to test; git fixtures are local). * **E-5 Determinism of the suite.** Two consecutive full runs on one machine produce the same pass/fail results; flaky tests are defects. Property tests run a fixed seed set in CI (plus an optional randomized local mode reporting seeds). -* **E-6 Windows leg.** SPEC.md 1.5 requires `/`-separated workspace-relative paths in identities, outputs, and stored data on every platform, and a Linux runner cannot discriminate a product emitting native separators — `/` is the native separator there. The same by-construction masking covers case sensitivity: 12.0 (byte-wise, case-sensitive comparison, no case folding), 10.1 (every subcommand but `create` matches session names exactly), and 7 (case-sensitive glob matching) bind on every platform, but on Linux the case-sensitive filesystem enforces the distinctions on behalf of a product that resolves session names, path arguments, or glob matches through case-insensitive filesystem lookups; only a case-insensitive filesystem exposes such a product. A second GitHub CI leg on Windows runners therefore runs the platform-sensitive subset: the path and identity assertions T1.5-1, T1.5-3, and T12.0-5 (less its Linux-leg arm); the single-casing case-mismatch probes — stageable on any filesystem, each staging one casing and probing another — of T10.1-2 (`status Foo` against stored session `foo` → exit 2), T10.1-3 (`NAME.JSON` is no session: `status NAME` → exit 2), T12.0-6 (sole source `specs/A.mdx`, argument `specs/a.mdx` → exit 2), and T7-4 (glob `SPECS/*.mdx` over directory `specs/` → zero sources); plus one representative fixture exercising `build`, `check`, `query`, `coverage`, `impact`, a journaled `rename`, a journaled file-form `move`, and an `audit` review session (`review create --strategy audit`, `next --json`, a `resolve`, and `export`), whose reports, move-rewritten sources, generated files, emitted Markdown, graph data, journal, and session file are asserted byte-identical to the same fixture's results on the Linux leg (12.0: no environment-dependent content; session files and review payloads are stored data and output carrying identities and source ranges, 1.5; a product-to-itself comparison, permitted by H-4). The `move` is the subset's specifier-computation probe: it crosses directories in both rewrite directions — the moved file's own import specifiers and another file's import of its generated module are recomputed (6.5), the one operation that computes new relative specifiers between files, which a native-path-API product writes `\`-separated only on Windows — and `check` is clean after it (T6.4-7); `rename` rewrites IDs but computes no specifier paths, so it cannot stand in for this probe. Byte-identity is promised only for byte-identical input (12.0), and `impact --base` reads the git baseline: the fixture's repository is therefore scripted with pinned, platform-independent commit metadata — fixed author, committer, and timestamps over identical file bytes and messages — so both legs realize identical commit identities and every invocation, baseline-taking ones included, runs on byte-identical input; input-derived content a conforming product may echo (the resolved baseline commit in an impact report or its JSON, H-3) then compares equal too. The subset depends on no case-sensitive filesystem (each case probe stages a single casing), symlink creation, or POSIX signal semantics; the Windows leg carries only the Linux-masked classes (native separators, filesystem-mediated case distinctions, native-path specifier computation); everything else remains fully exercised on the Linux leg, and the local-only set stays empty (H-9, E-2). +* **E-6 Windows leg.** SPEC.md 1.5 requires `/`-separated workspace-relative paths in identities, outputs, and stored data on every platform, and a Linux runner cannot discriminate a product emitting native separators — `/` is the native separator there. The same by-construction masking covers case sensitivity: 12.0 (byte-wise, case-sensitive comparison, no case folding), 10.1 (every subcommand but `create` matches session names exactly), and 7 (case-sensitive glob matching) bind on every platform, but on Linux the case-sensitive filesystem enforces the distinctions on behalf of a product that resolves session names, path arguments, or glob matches through case-insensitive filesystem lookups; only a case-insensitive filesystem exposes such a product. A second GitHub CI leg on Windows runners therefore runs the platform-sensitive subset: the path and identity assertions T1.5-1, T1.5-3, and T12.0-5 (less its Linux-leg arm); the single-casing case-mismatch probes — stageable on any filesystem, each staging one casing and probing another — of T10.1-2 (`status Foo` against stored session `foo` → exit 2), T10.1-3 (`NAME.JSON` is no session: `status NAME` → exit 2), T12.0-6 (sole source `specs/A.mdx`, argument `specs/a.mdx` → exit 2), and T7-4 (glob `SPECS/*.mdx` over directory `specs/` → zero sources); plus the drive-mismatch anchoring arm of T11.6-1 — the sole platform-form output, stageable on no Linux runner — and one representative fixture exercising `build`, `check`, `query`, `coverage`, `impact`, `occurrences`, `view --text`, `at`, `inventory` (run from a nested working directory, pinning the relative `/`-joined anchoring), `version`, a `move --preview`, a journaled `rename`, a journaled file-form `move`, and an `audit` review session (`review create --strategy audit`, `next --json`, a `resolve`, and `export`), whose reports and JSON documents — the path- and range-dense occurrence, view, at, inventory, and preview documents included — move-rewritten sources, generated files, emitted Markdown, graph data, journal, and session file are asserted byte-identical to the same fixture's results on the Linux leg (12.0: no environment-dependent content; session files and review payloads are stored data and output carrying identities and source ranges, 1.5; a product-to-itself comparison, permitted by H-4). The `move` is the subset's specifier-computation probe: it crosses directories in both rewrite directions — the moved file's own import specifiers and another file's import of its generated module are recomputed (6.5), the one operation that computes new relative specifiers between files, which a native-path-API product writes `\`-separated only on Windows — and `check` is clean after it (T6.4-7); `rename` rewrites IDs but computes no specifier paths, so it cannot stand in for this probe. Byte-identity is promised only for byte-identical input (12.0), and `impact --base` reads the git baseline: the fixture's repository is therefore scripted with pinned, platform-independent commit metadata — fixed author, committer, and timestamps over identical file bytes and messages — so both legs realize identical commit identities and every invocation, baseline-taking ones included, runs on byte-identical input; input-derived content a conforming product may echo (the resolved baseline commit in an impact report or its JSON, H-3) then compares equal too. The subset depends on no case-sensitive filesystem (each case probe stages a single casing), symlink creation, or POSIX signal semantics — the drive-mismatch arm needs only a substituted drive mapping; the Windows leg carries only the Linux-masked classes (native separators, filesystem-mediated case distinctions, native-path specifier computation, the drive-mismatch anchoring form); everything else remains fully exercised on the Linux leg, and the local-only set stays empty (H-9, E-2). From 9efbd867d46b3227fe4ac7e9bd9b7f2a1bc11c39 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 16:43:24 +0000 Subject: [PATCH 32/35] sdg(phase-6): close literal-\$, read-gate, occupant, and directory-creation coverage gaps (iter 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied all review items; none rejected. - C1: T7.5-5 gains literal-$ arms ($0, trailing $, $ before non-digit in from and to: no 14.14, literal-byte matching); P-7 generators extended to the $ capture boundary. - C2: T13.3-3 gains gate arms on T11.2-6's fixtures — garbage journal line (14.13) and obstructed write path (14.22) each gate the six reads: finding reported, exit 1, nothing answered, nothing modified. - C3: T12.2-2 gains occupant-kind arms — symlink to byte-identical generated content, and a directory, each judged stale by check (14.10). - C4: new T13.4-8 — writes create missing intermediate directories for the three 13.4-named cases (file-form move, created target file, first emission under nested outDir). - I1: T1.3-6 gains repeated-id and braced-id arms — 14.17 on the bearer, no 14.1, condition-2 masked for immediate children only. - I2: T6.5-4 file-form destination-exists split into plain-file, symlink, and broken-symlink occupant arms (refused-destination-exists). - O1: T4.5-2 gains the upstream root-marker arm (effectiveHash-only change, transitively impacted). - O2: T13.4-6 gains the 14.22 finding-cardinality arm (one per distinct offending component). - O3: retirement notes at 6.6/6.7 trimmed to the bare ID-ledger fact. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/TEST-SPEC.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/specs/TEST-SPEC.md b/specs/TEST-SPEC.md index bfc9acf..e2bfb48 100644 --- a/specs/TEST-SPEC.md +++ b/specs/TEST-SPEC.md @@ -48,7 +48,7 @@ These requirements bind the harness implementation regardless of test framework * **T1.3-3 Level skipping.** A child whose ID adds two segments (`a` containing `a.b.c` with no `a.b` section) fails with 14.2. * **T1.3-4 Top-level segment count.** A top-level section with a multi-segment ID fails; a one-segment top-level ID passes (checked against the empty prefix, 14.2). * **T1.3-5 Duplicate IDs.** Two sections with the same ID in one file fail with 14.3; the same ID in two different files is valid (uniqueness is per file, identities differ by path, 1.5). -* **T1.3-6 Missing-id masking.** A section lacking `id` with children: the immediate children report no 14.2 (masked by 14.1), while their other conditions and the grandchildren's structural checks still report (14.2 masking note). +* **T1.3-6 No-identity masking.** A section lacking `id` with children: the immediate children report no 14.2 (masked by 14.1), while their other conditions and the grandchildren's structural checks still report (14.2 masking note). Invalid-form arms (14.1: a repeated `id` attribute or a non-quoted-static value is condition 17, never condition 1, and masks condition 2 for the immediate children the same way): a repeated-`id` section and a braced-`id` (`id={"x"}`) section, each with an immediate child whose ID the structural rule would otherwise judge — each bearer reports 14.17 and no 14.1, its immediate children report no 14.2, and their own children's structural checks still report (14.2). ### 1.4 ID segments and tags @@ -170,7 +170,7 @@ Consumer programs in this section are compiled and run under standard TypeScript ### 4.5 Dependency markers * **T4.5-1 Marker semantics.** A bare requirement reference as an expression statement records a `references` edge from the enclosing code location; at runtime the program behaves as if the line were absent (harmless property read) with no additional tooling installed. -* **T4.5-2 Root marker.** A bare reference to the default export records a `references` edge to the root; it grants no coverage in any profile (roots never appear in coverage paths and root-targeted edges never extend one, 8; T8-5), but the code location is impacted (9.2) by any text edit in the document — an edit changing the root's subtreeHash or effectiveHash (4.5). +* **T4.5-2 Root marker.** A bare reference to the default export records a `references` edge to the root; it grants no coverage in any profile (roots never appear in coverage paths and root-targeted edges never extend one, 8; T8-5), but the code location is impacted (9.2) by any text edit in the document — an edit changing the root's subtreeHash or effectiveHash (4.5). Upstream arm (4.5: in the document or upstream of it): with the marker's document bearing a root-sourced dependency edge into another file (a top-level `{text(...)}`, as T8-5), an edit in that file changing only the root's effectiveHash leaves the location impacted — transitively (9.2), no node of the marker's document `changed`. * **T4.5-3 Static rule in TS.** A non-static bare reference in expression-statement position (computed index by variable, optional chaining, etc.) fails with 14.8 (invalid argument, not 14.18). * **T4.5-4 Shadowing.** A local declaration shadowing the import binding: chains rooted at the local are not spec references — no edge, no error, program builds. * **T4.5-5 Sanctioned uses only.** Each fails with 14.18: aliasing a node to a variable; destructuring the module; re-exporting the binding; storing a node in an array/object; passing a node to a function other than a spec module's `text` export; using `text` as a value (passing/storing it) other than as a callee. @@ -266,13 +266,13 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T6.5-1 File form.** IDs unchanged; identities change file part only; the moved file's own import specifiers and other files' imports of its generated module are rewritten so everything resolves; journal appended; finishing regeneration as T6.4-7; the applied-mapping report as T6.4-1 (6.5: both forms report as rename does). * **T6.5-2 Section form text edits.** Byte-exact fixtures: moved text spans opening tag's first character through closing tag's last; origin deletion drops lines left empty/whitespace-only (rule of 3); insertion immediately before the target parent's closing tag (or end of file for top-level `new-id`), followed by U+000A and preceded by one when not at line start; target file created when absent; no other byte changes beyond these edits, the identity and reference rewrites, and the finishing regeneration (6.5). Self-closing arms (1.1, 6.5), byte-exact: moving a self-closing section moves exactly the self-closing tag's own characters; a self-closing target parent is first rewritten to paired form — its `/` and any whitespace immediately before or after the `/` deleted, the closing tag matching the opening tag's name appended immediately after the tag's terminating `>` — and the insertion rule then applies before that closing tag: a `` parent becomes `` + U+000A + the moved text + U+000A + ``. * **T6.5-3 Re-identification and reference conversion.** Subtree re-identified by prefix replacement; references convert between local and imported forms; needed spec imports are added binding fresh, non-colliding identifiers and unneeded ones removed — removal is exact: an import is removed only when its binding had references and the rewrite leaves it with none, so an import whose binding was already unreferenced before the move stays (6.5, 2.1); rewritten content is byte-deterministic (two identical fixtures produce identical bytes); the full mapping is appended to the journal (6.5: both forms); finishing regeneration as T6.4-7 — after the section-form move, generated modules, Markdown output, and graph data are byte-identical to a fresh `build` of the moved sources and `check` reports no staleness. -* **T6.5-4 Refusals (exit 1, nothing modified).** A move creating a spec import cycle; creating a dependency cycle; file form whose destination exists; section form whose `` is invalid per 1.4 (the mirrored "new ID is valid" check, 6.5) — a forbidden name (`then`) and a whitespace-bearing segment, one arm each; section form whose `` collides with an ID in a distinct target file — the ordinary cross-file collision, `a.mdx#x` → `b.mdx#y` with a section `y` already present in `b.mdx` (the same-file variant and its after-the-removal qualifier: T6.5-6); section form whose target parent is missing; whose target parent lies within the moved subtree; destination path in no configured spec group; in a code group as well; containing `#`; not valid UTF-8 (Linux leg); lacking `.mdx`. Plus the valid-workspace precondition as T6.4-6. +* **T6.5-4 Refusals (exit 1, nothing modified).** A move creating a spec import cycle; creating a dependency cycle; file form whose destination exists — occupied by a plain file, by a symbolic link, and by a broken symbolic link (target absent), one arm each (6.5: whatever kind of filesystem object occupies it, a symbolic link included; the broken-link arm discriminates a product probing existence through link-following stat, which sees it absent and proceeds); section form whose `` is invalid per 1.4 (the mirrored "new ID is valid" check, 6.5) — a forbidden name (`then`) and a whitespace-bearing segment, one arm each; section form whose `` collides with an ID in a distinct target file — the ordinary cross-file collision, `a.mdx#x` → `b.mdx#y` with a section `y` already present in `b.mdx` (the same-file variant and its after-the-removal qualifier: T6.5-6); section form whose target parent is missing; whose target parent lies within the moved subtree; destination path in no configured spec group; in a code group as well; containing `#`; not valid UTF-8 (Linux leg); lacking `.mdx`. Plus the valid-workspace precondition as T6.4-6. * **T6.5-5 Usage errors (exit 2).** Nonexistent origin file or origin ID; a wrong-kind (code-source) origin in each form (6.5: both forms' origin operands name discovered spec sources); ordering, masking, and parse-local ID existence as T6.4-4. Operand classification is by spelling alone (6.5): the mixed-synopsis invocations `a.mdx b.mdx#y` and `a.mdx#x b.mdx` match neither form — exit 2 — and an operand containing `#` is always a `#` pair under the 12.0 split, so the file form cannot spell a `#`-containing path (a harmless limit, such paths being invalid source paths, 14.19; T12.0-13). * **T6.5-6 Identity terms.** The new-identity checks read in identity terms (6.5): a cross-file section move keeping its ID (`a.mdx#x` → `b.mdx#x`, no `x` in `b.mdx`) is valid; the exact self-move — `#` equal to `#` — is refused (exit 1), modifies nothing, and appends no journal entry (journal byte-compared around the attempt); a same-file move whose `` collides with an ID remaining in the target file after the removal is refused. The collision clause's after-the-removal qualifier admits no discriminating fixture: structural IDs (1.3) make the vacated set exactly the moved subtree's IDs, so a `` matching only vacated identities is always independently refused — as the exact self-move, or because its target parent is missing or lies within the moved subtree (T6.5-4). The mirrored "all rewritten references resolve" clause is likewise unstageable, for T6.4-3's reason. ### 6.6 Previews -(T6.6-1 is retired: it covered manual restructuring, formerly SPEC.md 6.6, now 6.7 — see T6.7-1.) +(T6.6-1 is retired.) * **T6.6-2 Modifies nothing.** A rename `--preview` and a section-form move `--preview` on workspaces where the real operation would proceed: exit 0, findings `[]`, and every byte of the workspace identical afterward — sources, journal (an absent journal stays absent), derived files, and graph data untouched; a subsequent real run on the same state performs the previewed plan, its applied mapping (T6.4-1) equal to the preview's `mapping`. Preview output is byte-deterministic across repeated runs (H-6) and, under `--json`, the form-exact 12.7 preview document (H-3). * **T6.6-3 Refusal and scheduling equivalence.** For each refusal of T6.4-3 and T6.5-4 — the invalid-workspace precondition included — staged identically, the `--preview` invocation reports the same findings (same stable codes, locations, identities; 14) and exits 1, its `mapping`, `files`, and `delta` `null` (12.7), modifying nothing; for the usage errors of T6.4-4/T6.5-5 the preview exits 2 identically (argument checks precede either way). The equivalence is over workspace state, never scheduling (6.6): while another mutating command is held (`--test-hold`, 13.5), a `--preview` invocation runs to completion — it takes no exclusivity and never meets the mutual-exclusion refusal of T13.5-2 — and `--test-hold` combined with `--preview` is a usage error, exit 2. @@ -282,7 +282,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( ### 6.7 Manual restructuring -* **T6.7-1** Renaming an ID by editing the file directly: no journal entry; impact reports a deletion plus an addition (not continuity); dependents referencing the old identity fail validation (14.5) until rewritten. (Formerly T6.6-1, retired when SPEC.md 6.6 became Previews.) +* **T6.7-1** Renaming an ID by editing the file directly: no journal entry; impact reports a deletion plus an addition (not continuity); dependents referencing the old identity fail validation (14.5) until rewritten. ## 7. Project Configuration @@ -301,7 +301,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T7.5-2 forbidden.** An edge whose source matches `from` and target matches `to` is a finding of `check` (rule name + offending edge, exit 1); non-matching edges are not; `kinds` restricts which edges are evaluated. * **T7.5-3 allowedOnly.** Every edge from a `from`-matching source must have a `to`-matching target; each violating edge is a separate finding. * **T7.5-4 Selectors.** `group` (with `kind` where needed) matches nodes of spec groups and code locations of code groups; `files` matches by glob; `tags` matches nodes carrying at least one listed tag. -* **T7.5-5 Captures.** `$1-$2.ts` against `a-b-c.ts` captures `a` and `b-c`; `*$1*` against `abc` captures `a`; a capture never matches `/` or the empty string; a `to` with captures matches only when expansions agree (mirror-structure policy fixture passes for agreeing pairs, violates for disagreeing ones); left-to-right shortest-match disambiguation is deterministic (repeat runs identical). +* **T7.5-5 Captures.** `$1-$2.ts` against `a-b-c.ts` captures `a` and `b-c`; `*$1*` against `abc` captures `a`; a capture never matches `/` or the empty string; a `to` with captures matches only when expansions agree (mirror-structure policy fixture passes for agreeing pairs, violates for disagreeing ones); left-to-right shortest-match disambiguation is deterministic (repeat runs identical). Literal `$` forms (7.5: a capture is exactly `$` followed by one digit `1`–`9` — every other `$` is a literal byte in either pattern, never a capture or a capture violation): patterns containing `$0`, a trailing `$`, and `$` before a non-digit, staged in `from` and in `to` (one arm each), load without 14.14 — a `to` containing `$0` or ending in `$` references no absent capture — and match exactly the paths spelling those literal bytes: `a$0.ts` matches the file `a$0.ts` and never `ab.ts` (what a capture reading would match), and a trailing-`$` pattern matches only the `$`-suffixed name. * **T7.5-6 build vs check.** `build` succeeds and regenerates output on a workspace full of policy violations (12.1); only `check` reports them (14.12). ## 8. Coverage @@ -467,7 +467,7 @@ Tests here drive `occurrences`, `view`, and `at`; the per-surface contracts are ### 12.2 `xspec check` * **T12.2-1 Green path.** On a freshly built valid workspace, `check` exits 0. -* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding); the unreadable-record unit form (14.10/14.23): with graph data corrupted shape-blind (T6.6-6's staging), `check` reports one condition-10 finding under the unit form alone — concerned path the graph-data area, no path inside it named, never the mismatch form beside it, and the recorded-file form undetectable while the state holds — and a successful `build` replaces the state (`check` clean afterward; `inventory` reports `recorded` again, T11.6-4); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). +* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding; occupant-kind arms — 14.10: the per-file comparison judges the path's occupant itself, never traversing a symbolic link — a generated module's path occupied by a symbolic link whose target holds byte-identical generated content, the discriminating arm a link-following product wrongly passes, and by a directory: each stale, exactly as a missing or content-differing file); the unreadable-record unit form (14.10/14.23): with graph data corrupted shape-blind (T6.6-6's staging), `check` reports one condition-10 finding under the unit form alone — concerned path the graph-data area, no path inside it named, never the mismatch form beside it, and the recorded-file form undetectable while the state holds — and a successful `build` replaces the state (`check` clean afterward; `inventory` reports `recorded` again, T11.6-4); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). * **T12.2-3 Never refreshes.** `check` on a stale workspace reports staleness and leaves graph data and derived files byte-identical (13.3). ### 12.3 `xspec ids` @@ -511,7 +511,7 @@ Assertions here — and wherever these forms appear across the suite — are for * **T13.3-1 Serving reads.** After `build`, the read commands (`check`, `ids`, `show`, `coverage`, `impact`, `review`, `query`, `occurrences`, `view`, `at`) answer without error; graph data lives under `.xspec/`. * **T13.3-2 Refresh.** Delete the graph data — operationally, here and in T13.4-3: every path under `.xspec/` except the durable `.xspec/journal` and `.xspec/reviews/` (13.4); graph-data content is opaque (H-4), so tests only ever remove or compare it whole — (or edit a source) and run each of `ids`, `show`, `coverage`, `impact`, `review status`, `query`, `occurrences`, `view`, `at`: the answer reflects current sources (never stale data), graph data is rewritten as `build` would write it, but no TypeScript or Markdown is generated or removed and recorded derived-file paths are unchanged (a stale generated module stays stale — `check` still reports 14.10 afterwards). Record discipline (13.3): with the record corrupted shape-blind instead (T6.6-6's staging), each refreshing read answers finding-free — exit 0 on the otherwise clean workspace — reporting nothing for the record and leaving the corrupt state neither read, repaired, nor replaced: `inventory` afterwards still reports `recorded` unavailable (T11.6-4), until a successful `build` or a finishing regeneration (6.4) replaces the state. -* **T13.3-3 Failed refresh.** With invalid sources, each read command reports the validation errors, exits 1, answers nothing, and modifies nothing (derived files and graph data byte-identical). The mutating `review` subcommands observe the same rule — 13.3 binds `review` whole, and `create`, `resolve`, and `split` all consult the current graph: on a workspace holding a session, a configured coverage profile, and a resolvable commit from when its sources were valid, a source is then edited to fail validation, and `review create` under each of `--base` (the resolvable ref — baseline resolution precedes source validation, 12.0, so the refresh failure is the operative error), `--strategy audit`, and `--coverage`, plus `resolve` and `split` naming the pre-existing session and an unblocked item, each reports the validation errors, exits 1, and modifies nothing: no session file is created, and the existing session file, journal, derived files, and graph data are byte-identical (no status recorded, no decomposition). The never-gated contrast: on the same failing workspace `occurrences`, `view`, and `at` answer per file under 11.2 (T11.2-1) and `inventory` answers whatever the sources' validity (T11.6-4) — none of them modifying anything. +* **T13.3-3 Failed refresh.** With invalid sources, each read command reports the validation errors, exits 1, answers nothing, and modifies nothing (derived files and graph data byte-identical). The mutating `review` subcommands observe the same rule — 13.3 binds `review` whole, and `create`, `resolve`, and `split` all consult the current graph: on a workspace holding a session, a configured coverage profile, and a resolvable commit from when its sources were valid, a source is then edited to fail validation, and `review create` under each of `--base` (the resolvable ref — baseline resolution precedes source validation, 12.0, so the refresh failure is the operative error), `--strategy audit`, and `--coverage`, plus `resolve` and `split` naming the pre-existing session and an unblocked item, each reports the validation errors, exits 1, and modifies nothing: no session file is created, and the existing session file, journal, derived files, and graph data are byte-identical (no status recorded, no decomposition). The gate is over every finding a `build` would report, source validity or not (13.3: source validation errors, journal errors, and refused writes alike): on an otherwise-valid workspace with a garbage journal line staged, and separately with an obstructed write path staged (T11.2-6's two fixtures), each of `ids`, `show`, `coverage`, `impact`, `review status`, and `query` reports that finding — the journal error (14.13) naming the line, the refused write (14.22) its offending component — exits 1, answers nothing, and modifies nothing (journal, sessions, derived files, and graph data byte-identical), discriminating a product that gates on source validity alone and answers `query` from a broken journal with exit 0 (refresh consumes the journal for canonical identities, 5.4). The never-gated contrast: on the same failing workspaces `occurrences`, `view`, and `at` answer per file under 11.2 (T11.2-1, T11.2-6) and `inventory` answers whatever the sources' validity (T11.6-4) — none of them modifying anything. * **T13.3-4 Determinism.** Graph data files are byte-deterministic across rebuilds of an identical workspace (content otherwise unasserted, H-4). ### 13.4 Derived and durable files @@ -521,8 +521,9 @@ Assertions here — and wherever these forms appear across the suite — are for * **T13.4-3 Orphan knowledge boundary.** Build so a derived file exists and is recorded; delete all graph data (T13.3-2's operational definition), taking the recorded derived-file paths with it; change the configuration so that file is no longer generated; `build`: the orphaned file is outside xspec's knowledge and is not removed; it may be deleted manually (asserted: subsequent builds leave the stray file alone). * **T13.4-4 Derived paths belong to xspec.** A user-created file at a derived path is replaced by `build`; a symbolic link at a derived file's own path is replaced as the occupant — nothing is written through it (link target byte-identical after build, link gone, plain file present; not an error). * **T13.4-5 Durable protection.** `build` and read commands never modify or delete the journal or session files (byte-compare); durable files are never regenerated (deleting a session file: xspec does not recreate it; review naming it → exit 2 unknown session). -* **T13.4-6 Symlink write rules.** A write whose path has a symbolic link at a workspace-relative directory component is refused before anything is modified (14.22; command exits 1 with the report, workspace byte-identical); `check` reports the same without writing; a durable path occupied by a symlink or non-plain file → journal error (14.13) / corrupt session (14.21) — never read, appended, or replaced (link and target byte-identical after the attempt). Positive counterpart: path components above the workspace root are unrestricted (13.4) — a workspace whose absolute path traverses a symbolic link above its root builds, mutates, and `check`s normally. +* **T13.4-6 Symlink write rules.** A write whose path has a symbolic link at a workspace-relative directory component is refused before anything is modified (14.22; command exits 1 with the report, workspace byte-identical); `check` reports the same without writing; finding cardinality (14.22: one finding per distinct offending component, whatever write paths it refuses) — one non-directory occupant at a component under which two derived files would be written yields one finding, concerned path that component, and two distinct offending components yield two findings (asserted via `check`); a durable path occupied by a symlink or non-plain file → journal error (14.13) / corrupt session (14.21) — never read, appended, or replaced (link and target byte-identical after the attempt). Positive counterpart: path components above the workspace root are unrestricted (13.4) — a workspace whose absolute path traverses a symbolic link above its root builds, mutates, and `check`s normally. * **T13.4-7 Source exclusion.** T7-6 covers `.xspec.`/`.xspec/`/emit-destination exclusion from groups. +* **T13.4-8 Writes create missing directories.** A missing intermediate directory never refuses or fails a write: the nonexistent workspace-relative directory components of a written path come into existence as directories (13.4), each named case staged with its directories absent beforehand and asserted present as real directories afterward — a file-form move to `new/deep/b.mdx` (destination in a configured spec group, `new/` absent) succeeds, the moved file and its regenerated derived files under the fresh directories; a section-form move whose created target file (6.5) lies under an absent directory succeeds likewise; a first emission under a nested nonexistent `markdown.outDir` (7.3) writes every destination, creating the chain. ### 13.5 Concurrency and isolation @@ -563,7 +564,7 @@ Property tests generate inputs from seeded, reproducible generators (H-10), asse * **P-4 Hash laws.** For random workspaces and random single edits: subtreeHash changed iff the 5.5 condition holds; metadataHash changed iff `d`/`coverage`/`tags` changed; ownHash insensitive to embedded-target edits; effectiveHash monotone over the dependency closure (any dependency-target effectiveHash change propagates); identical workspaces hash identically. * **P-5 Rename/move purity.** Random valid workspaces, random journaled rename/file-move sequences: all hashes byte-stable, impact against any prior commit in the sequence reports no categories, and all references still resolve; random section moves: only the predicted parents gain categories. * **P-6 Baseline replay.** Random edit/rename/move/commit interleavings: impact categories against each historical baseline equal an oracle diff of the two graphs with identities mapped through the journal suffix. -* **P-7 Glob and capture matching.** Random patterns and paths over the 7/7.5 grammar, with generators including the glob metacharacters of common dialects (`[` `]` `{` `}` `!` `+` `(` `)`), which the 7 grammar treats as literals: match decisions and capture values equal a spec oracle; every match is unique under the left-to-right shortest-match rule; captures never span `/` and never match empty. +* **P-7 Glob and capture matching.** Random patterns and paths over the 7/7.5 grammar, with generators including the glob metacharacters of common dialects (`[` `]` `{` `}` `!` `+` `(` `)`), which the 7 grammar treats as literals, and `$` forms at the capture boundary (`$0`, `$` before a non-digit, trailing `$` — literals in 7.5 patterns; T7.5-5): match decisions and capture values equal a spec oracle; every match is unique under the left-to-right shortest-match rule; captures never span `/` and never match empty. * **P-8 Parser robustness.** Fuzzed byte inputs (mutated MDX/TS/config, invalid UTF-8, BOMs, giant nesting, pathological line terminators): every command terminates, never emits a partial JSON document on `--json`, and always exits 0, 1, or 2 per the 12.0 partition; `build` failures modify nothing. * **P-9 Review session invariants.** Random sequences of valid review operations (create/next/resolve/split/re-derive triggers) interleaved with workspace edits: at most one item per kind and scope node; `blockedBy` acyclic; retired `id`s never reused; `next` always returns an unblocked needing-review item or reports fully resolved; reads never change session bytes; stored sessions always re-read as non-corrupt. * **P-10 Concurrency.** Randomized schedules of concurrent readers and one mutating command (via `--test-hold` and process kills): readers observe only prior-or-complete file states (T13.5-5); mutual exclusion never loses a journal append or a resolution (post-hoc: journal lines = successful `rename`/`move` operations — the journal's only writers, 6.1; session statuses = successful resolves). From 638cfd0784d3abbfbb4a67a3a7884341e6dececa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 17:36:20 +0000 Subject: [PATCH 33/35] sdg(phase-6): close move-refusal, gate-vs-corruption, presence, and MDX-boundary gaps (iter 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied all items of the iteration-3 review of TEST-SPEC.md: - C1: derived-path arm of refused-invalid-destination staged in T6.5-4 (markdown.outDir emit-destination component obstructed — the separable fixture, module/companion paths sharing the destination's directory); T14-7 cross-ref extended. - C2: new T10.1-5 — failing workspace holding a corrupt session: gated review subcommands and list report the gate's findings alone (no 14.21, session bytes untouched) while check reports 14.21 beside them; T14-4 reporter matrix and the 14.21 primary-test list updated. - I1: named section-form target-path occupant arms in T6.5-4 (directory; symlink resolving to a discovered spec source; out-of-group .mdx file). - I2: pure context-node and origin-node presence-invalidation arms added to T10.4-2 (metadata-consistency and dependency-consistency recipes). - I3: T3-1 grammar-boundary sharpening (construct-like bytes in fences and inline code are content: no nodes, no edges, no findings, bytes preserved); P-2 generators sharpened to match. - O1: zero-/two-argument TS text(...) arity arms added to T4.3-2. - O2: embedding half of 10.5's added-target note staged in T10.5-3. - O3: new P-13 coverage-reachability property with independent oracle; S-6 extended to vet that oracle. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/TEST-SPEC.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/specs/TEST-SPEC.md b/specs/TEST-SPEC.md index e2bfb48..c4b088f 100644 --- a/specs/TEST-SPEC.md +++ b/specs/TEST-SPEC.md @@ -128,7 +128,7 @@ These requirements bind the harness implementation regardless of test framework All tests here run with `markdown: { emit: true }` and byte-assert emitted files, except T3-6 (emission scope). -* **T3-1 Removals.** Imports, ``/`` opening and closing tags with all their props, and MDX comments are removed by exact textual deletion in place; all other Markdown content and author whitespace is preserved byte-for-byte (fixture with tables, code fences, trailing spaces, blank lines). +* **T3-1 Removals.** Imports, ``/`` opening and closing tags with all their props, and MDX comments are removed by exact textual deletion in place; all other Markdown content and author whitespace is preserved byte-for-byte (fixture with tables, code fences, trailing spaces, blank lines). Grammar boundary: constructs exist only where the MDX parse yields them (2.7, 14.16, 14.20) — fenced code blocks and inline code spans are literal text — so the fixture's fences and an inline code span contain construct-like bytes (``, `
`, `import X from "./X.xspec"`, and `{text("a")}`): they create no node and no edge (`query nodes`/`query edges`), trigger no finding of any kind (`build` and `check` exit 0), and are preserved into the output byte-for-byte, discriminating a product that removes constructs by textual pattern rather than by parse. * **T3-2 Replacement.** Each `text(...)` expression is replaced by the target's compiled subtree text, fully expanded through chained embeddings (A embeds B embeds C). * **T3-3 Line-drop rule.** A line that contained non-whitespace in the source and is left empty or whitespace-only purely by removals is dropped with its terminator: covers a line holding only an import; a line holding only an opening tag; a line holding only a closing tag; a line holding only a comment; a line holding only a `text(...)` whose expansion is empty. Counter-cases: a line that was already empty in the source is kept; a line keeping any content keeps its terminator; a removal-affected line that retains other content is kept; a line holding only a `text(...)` whose expansion is whitespace-only but non-empty — target subtree text a single space, e.g. an in-line section whose sole content is one space — is kept with that expansion and its terminator: neither drop cause applies (the line is not left whitespace-only purely by removals, and the expansion is not empty), discriminating a product that drops any whitespace-only result line whose source line held non-whitespace. Class boundaries (1.4): a line left holding only U+00A0, U+0085, or U+2028 after removals is kept — those code points are neither whitespace nor line terminators — while a line left holding only U+0009 or U+0020 drops. Multi-line constructs: a construct whose own characters include a line terminator (a multi-line MDX comment) is deleted exactly, merging the surrounding lines' residues into one line — a fixture with retained non-whitespace on both sides (`foo {/* …` on one line, `… */} bar` on the next) compiles to `foo bar` on one line, byte-asserted; an own-lines multi-line comment (empty residues) leaves the merged line empty purely by removals, and it drops with its terminator. * **T3-4 Line terminators.** CRLF, lone LF, and lone CR terminators are each recognized as one terminator by the drop rule; a final line without a terminator survives compilation without gaining one (byte-asserted fixtures for each). @@ -160,7 +160,7 @@ Consumer programs in this section are compiled and run under standard TypeScript ### 4.3 text * **T4.3-1** `text(node)` returns the node's subtree text as a `string` at runtime (byte-compared to expected expansion) and records an `embeds` edge from the calling code location to the node (`query edges`). -* **T4.3-2** A string argument to `text` in a TypeScript file fails with 14.8; so does a dynamic node-form argument there — a computed index by variable and an optional-chaining chain, each as the `text` argument (2.4, 4.5). +* **T4.3-2** A string argument to `text` in a TypeScript file fails with 14.8; so does a dynamic node-form argument there — a computed index by variable and an optional-chaining chain, each as the `text` argument (2.4, 4.5); and so do a zero-argument and a two-argument `text(...)` call in a TypeScript file (14.8's arity clause holds in either language, 2.4/4.5 — the MDX arms are T2.4-3). ### 4.4 Module branding @@ -266,7 +266,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T6.5-1 File form.** IDs unchanged; identities change file part only; the moved file's own import specifiers and other files' imports of its generated module are rewritten so everything resolves; journal appended; finishing regeneration as T6.4-7; the applied-mapping report as T6.4-1 (6.5: both forms report as rename does). * **T6.5-2 Section form text edits.** Byte-exact fixtures: moved text spans opening tag's first character through closing tag's last; origin deletion drops lines left empty/whitespace-only (rule of 3); insertion immediately before the target parent's closing tag (or end of file for top-level `new-id`), followed by U+000A and preceded by one when not at line start; target file created when absent; no other byte changes beyond these edits, the identity and reference rewrites, and the finishing regeneration (6.5). Self-closing arms (1.1, 6.5), byte-exact: moving a self-closing section moves exactly the self-closing tag's own characters; a self-closing target parent is first rewritten to paired form — its `/` and any whitespace immediately before or after the `/` deleted, the closing tag matching the opening tag's name appended immediately after the tag's terminating `>` — and the insertion rule then applies before that closing tag: a `` parent becomes `` + U+000A + the moved text + U+000A + ``. * **T6.5-3 Re-identification and reference conversion.** Subtree re-identified by prefix replacement; references convert between local and imported forms; needed spec imports are added binding fresh, non-colliding identifiers and unneeded ones removed — removal is exact: an import is removed only when its binding had references and the rewrite leaves it with none, so an import whose binding was already unreferenced before the move stays (6.5, 2.1); rewritten content is byte-deterministic (two identical fixtures produce identical bytes); the full mapping is appended to the journal (6.5: both forms); finishing regeneration as T6.4-7 — after the section-form move, generated modules, Markdown output, and graph data are byte-identical to a fresh `build` of the moved sources and `check` reports no staleness. -* **T6.5-4 Refusals (exit 1, nothing modified).** A move creating a spec import cycle; creating a dependency cycle; file form whose destination exists — occupied by a plain file, by a symbolic link, and by a broken symbolic link (target absent), one arm each (6.5: whatever kind of filesystem object occupies it, a symbolic link included; the broken-link arm discriminates a product probing existence through link-following stat, which sees it absent and proceeds); section form whose `` is invalid per 1.4 (the mirrored "new ID is valid" check, 6.5) — a forbidden name (`then`) and a whitespace-bearing segment, one arm each; section form whose `` collides with an ID in a distinct target file — the ordinary cross-file collision, `a.mdx#x` → `b.mdx#y` with a section `y` already present in `b.mdx` (the same-file variant and its after-the-removal qualifier: T6.5-6); section form whose target parent is missing; whose target parent lies within the moved subtree; destination path in no configured spec group; in a code group as well; containing `#`; not valid UTF-8 (Linux leg); lacking `.mdx`. Plus the valid-workspace precondition as T6.4-6. +* **T6.5-4 Refusals (exit 1, nothing modified).** A move creating a spec import cycle; creating a dependency cycle; file form whose destination exists — occupied by a plain file, by a symbolic link, and by a broken symbolic link (target absent), one arm each (6.5: whatever kind of filesystem object occupies it, a symbolic link included; the broken-link arm discriminates a product probing existence through link-following stat, which sees it absent and proceeds); section form whose target path is occupied by anything other than a discovered spec source — neither an insertion target nor an absent path to create, refused `refused-destination-exists` (6.5, T14-7) — one arm each: a directory; a symbolic link resolving to a discovered spec source (the link-following discriminator: discovery never yields a symlink, 7 — a product resolving the target path through the filesystem finds a spec source there and inserts through the link); and an existing `.mdx` file outside every configured spec group (present, right extension, still no discovered spec source); section form whose `` is invalid per 1.4 (the mirrored "new ID is valid" check, 6.5) — a forbidden name (`then`) and a whitespace-bearing segment, one arm each; section form whose `` collides with an ID in a distinct target file — the ordinary cross-file collision, `a.mdx#x` → `b.mdx#y` with a section `y` already present in `b.mdx` (the same-file variant and its after-the-removal qualifier: T6.5-6); section form whose target parent is missing; whose target parent lies within the moved subtree; destination path in no configured spec group; in a code group as well; containing `#`; not valid UTF-8 (Linux leg); lacking `.mdx`; and the derived-path arm of `refused-invalid-destination` (6.5: a workspace-relative directory component of a derived path the destination would generate — 13.1, 13.2, 7.3 — occupied by a non-directory): with emission enabled under `markdown.outDir`, a file-form move whose destination `new/b.mdx` is otherwise valid, its own directory components unobstructed (`new/` absent — a nonexistent component is never a refusal cause, 13.4, T13.4-8), but whose emit destination `/new/b.md` has its component `/new` occupied by a plain file lying under no current source's write path (so the workspace passes `build`'s validations) — refused `refused-invalid-destination`, never 14.22 (14, T14-7), discriminating a product that vets only the destination path's own components; the module and companion paths share the destination's directory (13.1), so the `outDir` emit destination is the separable derived-path fixture. Plus the valid-workspace precondition as T6.4-6. * **T6.5-5 Usage errors (exit 2).** Nonexistent origin file or origin ID; a wrong-kind (code-source) origin in each form (6.5: both forms' origin operands name discovered spec sources); ordering, masking, and parse-local ID existence as T6.4-4. Operand classification is by spelling alone (6.5): the mixed-synopsis invocations `a.mdx b.mdx#y` and `a.mdx#x b.mdx` match neither form — exit 2 — and an operand containing `#` is always a `#` pair under the 12.0 split, so the file form cannot spell a `#`-containing path (a harmless limit, such paths being invalid source paths, 14.19; T12.0-13). * **T6.5-6 Identity terms.** The new-identity checks read in identity terms (6.5): a cross-file section move keeping its ID (`a.mdx#x` → `b.mdx#x`, no `x` in `b.mdx`) is valid; the exact self-move — `#` equal to `#` — is refused (exit 1), modifies nothing, and appends no journal entry (journal byte-compared around the attempt); a same-file move whose `` collides with an ID remaining in the target file after the removal is refused. The collision clause's after-the-removal qualifier admits no discriminating fixture: structural IDs (1.3) make the vacated set exactly the moved subtree's IDs, so a `` matching only vacated identities is always independently refused — as the exact self-move, or because its target parent is missing or lies within the moved subtree (T6.5-4). The mirrored "all rewritten references resolve" clause is likewise unstageable, for T6.4-3's reason. @@ -334,6 +334,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T10.1-2 Names.** Valid: letters, digits, `.`, `_`, `-`, not beginning with `.`. Invalid names (`/`, space, empty, leading `.`, non-ASCII) → usage error, exit 2, nothing created. Names are case-sensitive for all subcommands (`status Foo` does not find `foo` → exit 2 — a single-casing probe rerun on the Windows leg, E-6, where a case-insensitive filesystem exposes a product matching session names via filesystem lookup), but `create` refuses a name matching an existing session ignoring ASCII case (exit 1, refused operation per 10.7/12.0). * **T10.1-3 Non-session files.** A stray file `.xspec/reviews/notes.txt` and a subdirectory are ignored by `list`, `check`, and every subcommand. The valid-name qualifier of 10.1 discriminates: a garbage-content `.json` file whose stem is an invalid session name — `.foo.json` (leading `.`) and `a b.json` (whitespace) — and a wrong-case extension `NAME.JSON` (paths compare byte-wise, 12.0) are not sessions: `list` reports them neither as sessions nor as corrupt and exits 0, `check` reports no 14.21, and naming them finds no session (`status` on `.foo` or `a b` → exit 2 invalid name; on `NAME` → exit 2 unknown session — the `NAME.JSON` probe stages a single casing and reruns on the Windows leg, E-6). * **T10.1-4 Corruption.** Each corrupt state → every `review` subcommand naming the session reports corruption, exits 1, modifies nothing; `list` reports the session corrupt in place of its fields and exits 1; `check` reports 14.21: unparseable JSON; missing 10.2 field; unknown status; duplicate item `id`s; `blockedBy` naming an absent item; a `blockedBy` cycle; two items with same kind and scope node; malformed recorded creation parameters or decompositions; a session path that is a directory or symlink (13.4). Staging is blackbox: SPEC.md leaves the session file's concrete shape opaque, so every shape-dependent corrupt fixture starts from a session file the product itself wrote and is corrupted through the H-3 adapter layer (shape-aware, value-blind: duplicating an item entry, rewriting a status to an unknown value, redirecting `blockedBy` into a cycle or at an absent id, deleting a field, garbling recorded parameters); shape-independent states (unparseable bytes, truncation, a directory or symlink at the path) are staged directly. The harness never writes a session file from an assumed layout. +* **T10.1-5 Failing workspace: gate precedence over corruption.** One workspace: create a session on a valid build, corrupt it shape-independently (T10.1-4's garbage-bytes staging), then edit a source to fail `build`'s validations. Every `review` subcommand naming the session — `status`, `next`, `show`, `export`, and `resolve`/`split` with any item ID (the ID is judged only against session content, never reached here, 12.0) — and `review list` report exactly the gate's findings: the validation errors, no condition-21 finding beside them, exit 1, nothing modified, the corrupt session's bytes untouched — no session file is read on a failing workspace, so corruption is reported exactly where sessions are read (10.1, 13.3, 14.21), and for `list` the gate's report replaces the per-session report whole (10.7). `check` on the same workspace reports 14.21 together with the validation findings (14.21: beside a failing workspace's other findings) — the discriminating pair against a product that opens the session first, reports corruption from a gated `review` subcommand, or drops 14.21 from `check` on the failing side. ### 10.2 Items @@ -350,7 +351,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( ### 10.4 Relevant hashes and invalidation * **T10.4-1 Per-kind sensitivity.** For each kind, every relevant hash listed in 10.4 is exercised as an invalidating case, plus a non-invalidating control — an edit touching none of the item's relevant state and leaving its generated context set unchanged. `subtree-coherence`: a text edit inside the scope subtree (a scope node's subtreeHash); a metadata-only edit on the scope root and, separately, on a descendant (the relevant metadataHash is each scope node's — the descendant's metadata edit changes no subtreeHash and MUST still invalidate); control: an edit outside the subtree. `parent-consistency`: an own-text edit of the scope node (ownHash); a metadata edit of the scope node (metadataHash); a deep text edit under a context child (a context node's subtreeHash); control: an edit in a sibling subtree of the scope node. `dependency-consistency`: an own-text edit of the scope node (ownHash); a metadata edit of the scope node (metadataHash); a text edit under an upstream target in context (target subtreeHash); control: an edit to an unrelated node. `metadata-consistency`: a metadata edit of the scope node (metadataHash only); control: a text edit of the scope node does not invalidate. `code-impact`: a text edit of an impact-edge target (target subtreeHash); an upstream edit changing only a target's effectiveHash; control: an edit to a node that is no impact-edge target and upstream of none. `uncovered-requirement`: a text edit in the scope node's subtree (subtreeHash); a metadata edit of the scope node (metadataHash); control: an edit elsewhere. -* **T10.4-2 Presence changes.** Deleting a scope node after resolve invalidates; restoring it invalidates a resolution recorded against absence; a node already absent at resolve time does not invalidate by remaining absent (deletion review stays resolvable). +* **T10.4-2 Presence changes.** Deleting a scope node after resolve invalidates; restoring it invalidates a resolution recorded against absence; a node already absent at resolve time does not invalidate by remaining absent (deletion review stays resolvable). Non-scope recordings (10.4: presence is recorded for every scope, context, and origin node), each arm pure — no recorded relevant hash of the item and no generated context set changes, so only the named node's presence divergence can invalidate, and a product recording presence for scope nodes alone reports the item still resolved. Context arm (`metadata-consistency`): baseline `D` bearing a `d` reference to sibling `T`; one edit removes the reference and deletes `T`'s section; `review create --base` — `D`'s item's context is the removed target `T`, recorded absent; resolve it; re-author `T`: the item reads `invalidated` (`D`'s metadataHash and the context set are unchanged; only the context node's absent-to-present flip diverges). Origin arm (`dependency-consistency`): baseline `X` depends on `T`, `T` depends on `D` (a section in its own file); a `d`-list edit on `D` makes `X`'s item — scope `X`, context `{T}`, origin `{D}` (10.5); resolve it; one edit then removes `T`'s reference to `D` and deletes `D`'s section: `X`'s ownHash and metadataHash and `T`'s subtreeHash are unchanged (`d`-prop edits touch no own content, 1.6/5.5) and the context set stays `{T}` (`T`'s effectiveHash still changed against the baseline), so the item reads `invalidated` through the origin node's present-to-absent flip alone. * **T10.4-3 Context-set change.** A change that alters the item's generator-derived context set (e.g. a new changed branch under a resolved `parent-consistency` item's scope) invalidates without any recorded hash changing. * **T10.4-4 Rename immunity.** `xspec rename`/`move` on scoped or context nodes: no duplicate items, no lost statuses, nothing invalidated by the identity mapping alone; reads present recorded nodes under current identities (mapped forward), for present and absent nodes alike. Item order follows current identities (10.5): a journaled file `move` that flips which of two same-depth, same-kind items' scope file paths sorts first flips their order in `status`/`next`/`export`, with statuses and recorded state intact. Reintroduction arm (10.4: recorded nodes compare as canonical identities, 5.4 — the journal-position pairing included): in an audit session over a file with top-level leaf sections `a` and `s`, resolve `a`'s item; `xspec rename` `a`→`b`; author a new top-level leaf section `a`; resolve `s`'s item `updated` (re-derivation, 10.5/10.6). The item recorded against old-`a` keeps its `id` and resolved status, presented under scope `b`; new-`a`'s item enters as a distinct item, `unresolved` (10.2); the root item's `blockedBy` gains it. A product matching by walked-back identity string collapses the two generated items (both bearers walk to `a`) into the resolved one, losing new-`a`'s item. * **T10.4-5 Reads never write.** `status`, `next`, `show`, `export` leave the session file byte-identical, including when they compute and report invalidation; a stale resolution is reported `invalidated` on read, and the stored status is only rewritten by mutating subcommands. @@ -359,7 +360,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T10.5-1 Generation.** SPEC.md §15's worked change (leaf text edit) yields exactly the four listed items with specified scope/context/origin. Extended fixture: a `changed` node with a `changed` ancestor generates no own item (skipping rule); scope of `subtree-coherence` is the node plus all descendants; multiple changed nodes sharing an ancestor A yield one `parent-consistency` item for A against the union of branches. * **T10.5-2 Blocking chains.** A's `parent-consistency` item is blocked by, per changed branch, the child's `subtree-coherence` item (child is the changed node) or the child's `parent-consistency` item (deeper change); chains extend to the root; only those two kinds block `parent-consistency` items; `metadata-consistency`, `dependency-consistency`, and `code-impact` items have empty `blockedBy`. -* **T10.5-3 Metadata/dependency/code items.** One `metadata-consistency` per `metadata-changed` node (context: added and removed `d` targets; `coverage`/`tags` changes described in `reason`); one `dependency-consistency` per node with a dependency edge to a target present on both sides of the baseline (5.6) whose effectiveHash changed (context: those targets; origin: originating nodes) — an edge to a target added since the baseline yields no such item, the change being reviewed at its source (10.5): a fixture node whose only affected target was added since the baseline gets no `dependency-consistency` item, its new `d` edge surfacing as its own `metadata-consistency` item; one `code-impact` per impacted location (context: the impact-edge targets that make it impacted, added and deleted included). +* **T10.5-3 Metadata/dependency/code items.** One `metadata-consistency` per `metadata-changed` node (context: added and removed `d` targets; `coverage`/`tags` changes described in `reason`); one `dependency-consistency` per node with a dependency edge to a target present on both sides of the baseline (5.6) whose effectiveHash changed (context: those targets; origin: originating nodes) — an edge to a target added since the baseline yields no such item, the change being reviewed at its source — both halves of 10.5's note staged (a new `d` edge makes the source `metadata-changed`, a new embedding makes it `changed`): a fixture node whose only affected target was added since the baseline gets no `dependency-consistency` item, its new `d` edge surfacing as its own `metadata-consistency` item; a second node whose only affected target entered through a new `{text(...)}` embedding likewise gets no `dependency-consistency` item — the new embedded reference changes its own content (5.5), it is `changed`, and the change is reviewed via its own `subtree-coherence` item; one `code-impact` per impacted location (context: the impact-edge targets that make it impacted, added and deleted included). * **T10.5-4 Item order.** A fixture with items of all kinds across two files asserts the total order: requirement-scoped first by depth deepest-first (roots 0), then kind order `subtree-coherence`, `metadata-consistency`, `dependency-consistency`, `parent-consistency`, then file path bytes, then document order; `code-impact` items last by location identity; after deleting a scope node, absent-scope items order after present ones by identity then item `id` (10.5 ordering rule); `status`/`next`/`export` all present this order. * **T10.5-5 Re-derivation on updated.** Resolving an item `updated` re-derives: a matching kind+scope item keeps `id`, status, recorded state; a context-set change marks it per 10.4; items no longer generated remain with their `blockedBy` and retain their recorded context set (10.4) — `show`/`export` after the re-derivation present that context unchanged; a newly `changed` node's item appears in order, created `unresolved` (10.2 — the discriminating fixture: the triggering item resolved `updated`, whose status must not propagate to the new item); `blockedBy` is recomputed with decomposed references replaced by decompositions (after a `split`); a decomposed kind+scope is never re-added — its decomposition applies recursively; sibling subtrees enter only through re-derivation (resolving with `no-change`/`skipped` does not re-derive — a concurrent workspace edit surfaces as invalidation, not new items, until an `updated` resolve). * **T10.5-6 Baseline recording.** The session records the resolved commit identity of `--base`; later `HEAD` movement or branch renames do not change what generators run against (re-derivation still diffs against the recorded commit). @@ -539,15 +540,15 @@ All mutual-exclusion tests use the `--test-hold ` seam for determinism. ## 14. Validation Errors -Sections 1–13 exercise each numbered condition in its home context; this section adds the reporting-contract tests. Primary tests per condition (not exhaustive; the H-7 map is the complete record): 14.1 (T1.3-1), 14.2 (T1.3-2/3/4/6), 14.3 (T1.3-5), 14.4 (T1.4-1/4), 14.5/14.6/14.7 (T14-2), 14.8 (T2.4-2/3, T4.3-2, T4.5-3), 14.9 (T2.1-5, T5.3-1/2), 14.10 (T12.2-2), 14.11 (T4.4-1), 14.12 (T7.5-2/6), 14.13 (T6.1-3, T13.4-6), 14.14 (T7-1..T7.5-1), 14.15 (T2.1-2/3, T4-2), 14.16 (T2.7-1), 14.17 (T2.5-3, T2.7-3), 14.18 (T4.5-5), 14.19 (T1.5-2, T7.1-1), 14.20 (T1.6-5, T14-3, T14-5), 14.21 (T10.1-4), 14.22 (T13.4-6), 14.23 (T6.6-6, T11.6-4, T12.2-2, T13.3-2); the refusal reasons and their codes (T14-7; staged at T6.4-3, T6.5-4, T6.5-6, T6.6-3); the stable-code and location-cardinality contracts (T14-6, T14-8). +Sections 1–13 exercise each numbered condition in its home context; this section adds the reporting-contract tests. Primary tests per condition (not exhaustive; the H-7 map is the complete record): 14.1 (T1.3-1), 14.2 (T1.3-2/3/4/6), 14.3 (T1.3-5), 14.4 (T1.4-1/4), 14.5/14.6/14.7 (T14-2), 14.8 (T2.4-2/3, T4.3-2, T4.5-3), 14.9 (T2.1-5, T5.3-1/2), 14.10 (T12.2-2), 14.11 (T4.4-1), 14.12 (T7.5-2/6), 14.13 (T6.1-3, T13.4-6), 14.14 (T7-1..T7.5-1), 14.15 (T2.1-2/3, T4-2), 14.16 (T2.7-1), 14.17 (T2.5-3, T2.7-3), 14.18 (T4.5-5), 14.19 (T1.5-2, T7.1-1), 14.20 (T1.6-5, T14-3, T14-5), 14.21 (T10.1-4, T10.1-5), 14.22 (T13.4-6), 14.23 (T6.6-6, T11.6-4, T12.2-2, T13.3-2); the refusal reasons and their codes (T14-7; staged at T6.4-3, T6.5-4, T6.5-6, T6.6-3); the stable-code and location-cardinality contracts (T14-6, T14-8). * **T14-1 Actionable and complete reporting.** A workspace seeded with several independent error conditions across files: `build` and `check` report each of them (not only the first), and every report identifies file and location and states a correction-oriented message (information presence, not wording). * **T14-2 Unresolved references.** A `d` reference, a `text(...)` target, and a TypeScript marker/`text` call that do not resolve → 14.5, 14.6, 14.7 respectively; the TS case is also a type error against the generated module (asserted when a prior valid generation exists). * **T14-3 Masking.** An unparseable file (14.20 — malformed MDX; malformed TS under the grammar its name selects: a TSX-only construct in a `.ts` file; invalid UTF-8; BOM) masks conditions inside itself, and every reference into it from other files reports as unresolved (14.5–14.7); the parse-failure location is reported. A configuration error suppresses all source analysis: only 14.14 is reported (exit 2) even with invalid sources present. -* **T14-4 Reporter matrix.** 14.10 and 14.12 reported by `check` only (a stale workspace `build`s successfully by regenerating; a policy-violating workspace `build`s successfully); 14.21 reported by `check`, by `review` subcommands naming the session, and by `review list` — not by `build`; 14.23 reported by `inventory` and `rename`/`move` previews only — `check` reports the state as 14.10's unit form, and `build` and the refreshing reads never do (the rebuild replaces the record; the reads leave it unconsulted, T13.3-2); 14.14 delivered as an exit-2 error by every command that loads configuration — never `version` (T12.6-2); every other condition reported by both `build` and `check`, and as a domain file's finding accompanying `occurrences`/`view`/`at` answers (11.2, T11.2-5). +* **T14-4 Reporter matrix.** 14.10 and 14.12 reported by `check` only (a stale workspace `build`s successfully by regenerating; a policy-violating workspace `build`s successfully); 14.21 reported by `check`, by `review` subcommands naming the session, and by `review list` — not by `build`, and on a workspace failing `build`'s validations by `check` alone, beside the gate's findings (T10.1-5); 14.23 reported by `inventory` and `rename`/`move` previews only — `check` reports the state as 14.10's unit form, and `build` and the refreshing reads never do (the rebuild replaces the record; the reads leave it unconsulted, T13.3-2); 14.14 delivered as an exit-2 error by every command that loads configuration — never `version` (T12.6-2); every other condition reported by both `build` and `check`, and as a domain file's finding accompanying `occurrences`/`view`/`at` answers (11.2, T11.2-5). * **T14-5 Grammar selection.** A file matched by a code group and named `.tsx`, containing TSX-only syntax (not parseable as plain TypeScript — T14-3's construct) inside a named unit that also holds a dependency marker and a `text(...)` call: `build` succeeds — `.tsx` parses as TSX (14.20) — and the marker's `references` edge and the call's `embeds` edge are recorded and attributed to that unit per 4.6. The negative direction, the same TSX-only construct in a `.ts` file failing 14.20, is T14-3's; a further arm stages it in a code-group file of another name (`.mts`), failing 14.20 identically — any name but `.tsx` selects plain TypeScript (14.20), discriminating against products keying specifically on `.ts`. * **T14-6 Stable codes.** For each of the 23 conditions, staged via its primary test's fixture and read from its stated reporter (T14-4): the finding carries the exact token 14 lists (`missing-id` … `unreadable-record`) as its `code` in the JSON report form, where 12.7 pins it — the value is the token string alone, the ordinal numeral no part of it — so a product omitting or misspelling a code fails even where exit class and located information are right. A plain usage error and a review-operation refusal carry no stable code — `code` `null` (14, T12.7-1/3). -* **T14-7 Refusal reasons.** Staged refusals asserting each stable code with its concerned file, range, or identity (14): `refused-invalid-id` (concerning the invalid identity; intrinsic form only — a structurally misplaced but intrinsically valid new ID reports `refused-structural-parent` alone, never both); `refused-identity-unchanged`, reported alone by an identity-unchanged rename — no collision reason beside it (6.4: the after-removal check collides with nothing); `refused-id-collision`, locating every colliding bearer; `refused-structural-parent` (the violated identity); `refused-cycle`, locating the would-be cycle's full path; `refused-destination-exists` (the occupied path; the section form's non-spec-source occupant included); `refused-missing-target-parent` (the target-parent identity); `refused-invalid-destination` (the destination path; the destination-side directory-component case of 6.5 reports this code, never 14.22 — a plain file staged as a destination directory component). `refused-unresolvable-reference` admits no fixture (T6.4-3/T6.5-6) and is asserted only as the always-passing side of successful operations. Every applicable reason reports together, one finding per reason: a section move staged to both collide (`` present in the target file) and create a dependency cycle reports both findings, never only the first. The invalid-workspace refusal reports the workspace's numbered findings alone — on a workspace failing validation, a rename staged to also collide reports the validation findings only, exit 1, no refusal reason evaluated or reported beside them (6.4, 14). +* **T14-7 Refusal reasons.** Staged refusals asserting each stable code with its concerned file, range, or identity (14): `refused-invalid-id` (concerning the invalid identity; intrinsic form only — a structurally misplaced but intrinsically valid new ID reports `refused-structural-parent` alone, never both); `refused-identity-unchanged`, reported alone by an identity-unchanged rename — no collision reason beside it (6.4: the after-removal check collides with nothing); `refused-id-collision`, locating every colliding bearer; `refused-structural-parent` (the violated identity); `refused-cycle`, locating the would-be cycle's full path; `refused-destination-exists` (the occupied path; the section form's non-spec-source occupant included); `refused-missing-target-parent` (the target-parent identity); `refused-invalid-destination` (the destination path; the destination-side directory-component cases of 6.5 report this code, never 14.22 — a plain file staged as a destination directory component and, in T6.5-4's derived-path arm, as a directory component of the destination's `outDir` emit destination). `refused-unresolvable-reference` admits no fixture (T6.4-3/T6.5-6) and is asserted only as the always-passing side of successful operations. Every applicable reason reports together, one finding per reason: a section move staged to both collide (`` present in the target file) and create a dependency cycle reports both findings, never only the first. The invalid-workspace refusal reports the workspace's numbered findings alone — on a workspace failing validation, a rename staged to also collide reports the validation findings only, exit 1, no refusal reason evaluated or reported beside them (6.4, 14). * **T14-8 Location cardinality.** A condition several constructs jointly violate is one finding locating every participant, each in its containing file: a triple-duplicated ID → one condition-3 finding with three locations (one per bearer, no representative chosen); an import-binding collision → one condition-15 finding locating every colliding declaration; a cross-file dependency cycle → one condition-9 finding locating its full path, every participating reference spelling, and a spec import cycle every participating import declaration; a no-occurrence MDX embedding spelling → its condition-6 finding's range the full braced container — the span its occurrence would occupy (5.7), keeping T11.4-6's byte classification exact; a policy finding → `locations` `[]`, `path` `null`, its context identities alone (T12.7-1). Location order within a finding is file bytes, then start, then end (12.7). ## 15. Example @@ -559,7 +560,7 @@ Sections 1–13 exercise each numbered condition in its home context; this secti Property tests generate inputs from seeded, reproducible generators (H-10), assert spec-derived invariants, and shrink failures. Each property is also anchored by the deterministic fixtures of sections 1–15; properties exist to search the input space, not to replace them. * **P-1 Segment/tag validity.** Generator over code points (weighted toward boundaries: whitespace/control classes of 1.4, U+00A0/U+0085/U+2028, `.`/`#`, forbidden names, and glob metacharacters of common dialects — `[` `]` `{` `}` `!` `+` `(` `)` — which are ordinary valid segment characters): a generated segment is accepted by `build` iff it satisfies 1.4; likewise tags, with `.` allowed and whitespace never reaching tag validation — a generated value containing whitespace stages as multiple tags (2.6 splits on runs of whitespace), so the tag property asserts acceptance iff every resulting token satisfies 1.4 (zero tokens: accepted as an omitted prop, T2.6-2). -* **P-2 Markdown compilation.** Random documents composed of prose blocks, nested sections, imports, comments (single- and multi-line), and embeddings, over mixed line terminators, with content weighted toward the whitespace/non-whitespace boundary code points of 1.4 (U+00A0, U+0085, and U+2028 included): compiled output equals an independent oracle implementing the removal/replacement/line-drop rules of 3 (the oracle lives in the harness); compilation is deterministic; content bytes outside removed constructs are preserved. +* **P-2 Markdown compilation.** Random documents composed of prose blocks — fenced code blocks and inline code spans spelling tag-, import-, and expression-like bytes included (T3-1's grammar boundary: such bytes are content) — nested sections, imports, comments (single- and multi-line), and embeddings, over mixed line terminators, with content weighted toward the whitespace/non-whitespace boundary code points of 1.4 (U+00A0, U+0085, and U+2028 included): compiled output equals an independent oracle implementing the removal/replacement/line-drop rules of 3 (the oracle lives in the harness); compilation is deterministic; content bytes outside removed constructs are preserved. * **P-3 Text algebra.** For random documents: root subtree text equals compiled Markdown output; a node's subtree text equals its own-text runs interleaved with its children's subtree texts in document order (1.6); N children yield N+1 runs. * **P-4 Hash laws.** For random workspaces and random single edits: subtreeHash changed iff the 5.5 condition holds; metadataHash changed iff `d`/`coverage`/`tags` changed; ownHash insensitive to embedded-target edits; effectiveHash monotone over the dependency closure (any dependency-target effectiveHash change propagates); identical workspaces hash identically. * **P-5 Rename/move purity.** Random valid workspaces, random journaled rename/file-move sequences: all hashes byte-stable, impact against any prior commit in the sequence reports no categories, and all references still resolve; random section moves: only the predicted parents gain categories. @@ -570,6 +571,7 @@ Property tests generate inputs from seeded, reproducible generators (H-10), asse * **P-10 Concurrency.** Randomized schedules of concurrent readers and one mutating command (via `--test-hold` and process kills): readers observe only prior-or-complete file states (T13.5-5); mutual exclusion never loses a journal append or a resolution (post-hoc: journal lines = successful `rename`/`move` operations — the journal's only writers, 6.1; session statuses = successful resolves). * **P-11 Availability robustness.** Fuzzed and mutated spec and code sources (P-8's generators — the availability contract is precisely an imperfect-input surface) driven through `occurrences`, `view` (with and without `--text`), and `at` at random offsets: every invocation terminates; stdout is one complete JSON document, never partial; the exit is 0 or 1 per 11.2 (2 only for staged argument errors); every datum is exactly one of plain value, `null`, or `{"unavailable": true}` (11.4, 12.7); any finding or unavailable datum implies exit 1 with the full document emitted, and exit 0 implies a finding-free document carrying none. * **P-12 at ≡ view; occurrence order.** For random workspaces: for every file and every offset 0…byte length, `at`'s resolution — section identity, construct range, containing occurrence — equals the resolution computed from that file's `view` document alone (11.5, T11.5-1); and the workspace-wide `occurrences` enumeration equals the view-collected occurrences sorted by file bytes, range start, range end — total, duplicate-free, byte-identical across runs (5.7). +* **P-13 Coverage oracle.** Random workspaces (spec and code groups; `depends`, `embeds`, and `references` edges; tags; `coverage="none"`; root-sourced and root-targeted edges) and random profiles (`mode`, `targets`, `targetTags`, `edgeKinds`, spec and code boundaries): `xspec coverage`'s required, covered, uncovered, and ignored sets — exclusion reasons included — equal an independent oracle implementing 8.1's required set and 8's reachability (direct: one edge; transitive: one or more; only the profile's `edgeKinds`; `contains` never grants; roots never boundary, intermediate, or target), and every reported covering path is a permitted path of the profile from a boundary node to its target, shortest with the 12.0 tie-break — guarding what the deterministic T8-* matrix samples pointwise. ## 17. Self-Tests and Certification @@ -582,7 +584,7 @@ Confidence that the harness itself is correct comes primarily from certification * **S-3 Subprocess driver.** Captures exit codes and keeps stdout/stderr separated (verified against a known-behavior stand-in command); enforces per-test working directories; detects hangs via timeout and reports them as failures, not skips. * **S-4 TypeScript tooling driver.** Detects a known type error, a known definition location, and a known hover text in a hand-written non-xspec fixture project, so section 4's consumer assertions cannot pass vacuously. * **S-5 Output adapters.** Each adapter (H-3) rejects documents missing required information (fed synthetic wrong-shape documents) rather than defaulting. -* **S-6 Oracles.** The Markdown oracle (P-2) and glob/capture oracle (P-7) pass their own fixed vector suites derived from SPEC.md's examples (3, 7.5) before being trusted by property tests. +* **S-6 Oracles.** The Markdown oracle (P-2), the glob/capture oracle (P-7), and the coverage-reachability oracle (P-13) pass their own fixed vector suites derived from SPEC.md's examples (3, 7.5, 15) before being trusted by property tests. * **S-7 Red-green sweep.** Against an empty stub product (every command exits with an unexpected code and no output), every product-facing test fails with a diagnosed assertion and the suite completes without harness errors (H-8). ## 18. Execution and CI From 213b935e365bccc5a824c49bba0f19a927f91080 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 18:12:45 +0000 Subject: [PATCH 34/35] sdg(phase-6): positively assert 14.10's graph-data unit form at check (iter 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: T12.2-2 gains isolated missing- and mismatch-arm fixtures asserting check's exactly-one condition-10 unit-form finding (concerned path the graph-data area, no path inside it named); T12.2-3 now pins its never-refreshes assertion to those states plus the combined edited-source state. - O2: section 11 preamble reworded so the same-information bound reads as "byte-identity is not asserted", not as an assertion the forms differ. Rejected: - O1: wrong on the facts — SPEC.md §15 describes the worked change and lists the four items verbatim ("contains exactly: a subtree-coherence item for print.hello, ..."), so T10.5-1/T15-1's citations have an exact antecedent. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/TEST-SPEC.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/TEST-SPEC.md b/specs/TEST-SPEC.md index c4b088f..9683bdd 100644 --- a/specs/TEST-SPEC.md +++ b/specs/TEST-SPEC.md @@ -388,7 +388,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( ## 11. Query Surfaces -SPEC.md 11's five commands are JSON-only: for `query`, `occurrences`, `view`, `at`, and `inventory`, a single JSON document is the only output form, with or without `--json` — `occurrences`, `view`, `at`, and `inventory` in the form-exact document forms of 12.7 (H-3, T12.7-2), `query` carrying its defining section's information through H-3 adapters. Each surface's flag-less and `--json` invocations are asserted to carry the same information — never byte-identity between the two forms — and an exit-2 error of any of them arrives as the 12.7 error document on stdout (12.0; T12.0-2, T12.7-3). +SPEC.md 11's five commands are JSON-only: for `query`, `occurrences`, `view`, `at`, and `inventory`, a single JSON document is the only output form, with or without `--json` — `occurrences`, `view`, `at`, and `inventory` in the form-exact document forms of 12.7 (H-3, T12.7-2), `query` carrying its defining section's information through H-3 adapters. Each surface's flag-less and `--json` invocations are asserted to carry the same information — byte-identity between the two forms is not asserted (SPEC.md does not require it) — and an exit-2 error of any of them arrives as the 12.7 error document on stdout (12.0; T12.0-2, T12.7-3). ### 11.1 `xspec query` @@ -468,8 +468,8 @@ Tests here drive `occurrences`, `view`, and `at`; the per-surface contracts are ### 12.2 `xspec check` * **T12.2-1 Green path.** On a freshly built valid workspace, `check` exits 0. -* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding; occupant-kind arms — 14.10: the per-file comparison judges the path's occupant itself, never traversing a symbolic link — a generated module's path occupied by a symbolic link whose target holds byte-identical generated content, the discriminating arm a link-following product wrongly passes, and by a directory: each stale, exactly as a missing or content-differing file); the unreadable-record unit form (14.10/14.23): with graph data corrupted shape-blind (T6.6-6's staging), `check` reports one condition-10 finding under the unit form alone — concerned path the graph-data area, no path inside it named, never the mismatch form beside it, and the recorded-file form undetectable while the state holds — and a successful `build` replaces the state (`check` clean afterward; `inventory` reports `recorded` again, T11.6-4); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). -* **T12.2-3 Never refreshes.** `check` on a stale workspace reports staleness and leaves graph data and derived files byte-identical (13.3). +* **T12.2-2 Scope.** One workspace per finding family asserting `check` reports it with exit 1: all build validations (derived state from a prior valid build persists while the sources have since been edited to be invalid — `check` re-validates from the current sources rather than accepting the stale outputs); stale generated output and orphaned recorded derived file (14.10, `check`-only; asserted after hand-editing a generated file, hand-deleting one, editing a source without rebuilding, and disabling emission without rebuilding; occupant-kind arms — 14.10: the per-file comparison judges the path's occupant itself, never traversing a symbolic link — a generated module's path occupied by a symbolic link whose target holds byte-identical generated content, the discriminating arm a link-following product wrongly passes, and by a directory: each stale, exactly as a missing or content-differing file); the graph-data unit form, missing and mismatch arms each positively isolated (12.2/14.10: `check` verifies graph data against the current sources and configuration) — missing: on a freshly built, otherwise clean workspace, delete the graph data (T13.3-2's operational definition): `check` exits 1 with exactly one condition-10 finding, the unit form — concerned path the graph-data area, no path inside it named — and no per-file finding beside it (every generated file present and matching; the absent record leaves the recorded-file form nothing to report), discriminating a product that treats absent graph data as nothing to verify; mismatch: build, edit a source, run one refreshing read — graph data then reflects the edit while the generated files go stale (13.3) — and revert the edit: the generated files again match the current sources while graph data does not, and `check` exits 1 with exactly one condition-10 finding, the unit form under the same concerned-path contract, no per-file finding beside it, discriminating a product that runs the per-file and record-readability checks but never compares graph data against the current sources and configuration; the unreadable-record unit form (14.10/14.23): with graph data corrupted shape-blind (T6.6-6's staging), `check` reports one condition-10 finding under the unit form alone — concerned path the graph-data area, no path inside it named, never the mismatch form beside it, and the recorded-file form undetectable while the state holds — and a successful `build` replaces the state (`check` clean afterward; `inventory` reports `recorded` again, T11.6-4); unresolved/non-static references; cycles; journal integrity (14.13); policy (14.12, `check`-only); corrupt sessions (14.21). +* **T12.2-3 Never refreshes.** `check` reports staleness and modifies nothing, pinned per state (13.3): on T12.2-2's missing-arm state graph data stays absent — `check` never rewrites it, where every refreshing read would (T13.3-2); on its isolated mismatch state, and on an edited-source-without-rebuild state carrying per-file and unit staleness together, graph data and every derived file are byte-identical around the invocation. ### 12.3 `xspec ids` From b0984f3b3cacb185d286bd1b5ceffedcf01c22bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 18:44:34 +0000 Subject: [PATCH 35/35] sdg(phase-6): cover undefined-source occurrences and unknown code units (iter 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applied: - C1: T11.2-4 gains the spec-source source-side arm — resolving spellings inside a duplicate-id bearer and an id-less section record occurrences with source exactly the one unavailable datum, the enclosing construct still on view, findings accompanying, exit 1; T11.3-1 enumerates the same records beside the invalid-path code-source arm. - C2: T11-6 gains unknown-code-unit arms — an unspelled unit and an out-of-range #unit@N on a discovered code source exit 2 across the graph-node flags, judged parse-locally over the file's named units (4.6); T12.0-10's gated sweep and consulted-domain list extended with the code-unit check. - O1: T12.0-10's syntax-alone class gains its malformed-value member (show a#b#c against invalid configuration reports the usage error, never 14.14). - O2: T6.4-4 pins 6.4's second existence mechanism — an old-ID bearer beneath an ancestor spelling no identity establishes existence, the invalid-workspace refusal reporting exit 1, never exit 2. Rejected: none. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01TyZ5zUv2UCkvTkM1tkYUp2 --- specs/TEST-SPEC.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/TEST-SPEC.md b/specs/TEST-SPEC.md index 9683bdd..736fd5c 100644 --- a/specs/TEST-SPEC.md +++ b/specs/TEST-SPEC.md @@ -256,7 +256,7 @@ Occurrences are observed through `xspec occurrences` (11.3) and per-file views ( * **T6.4-1 Rewrites.** Renaming a mid-tree ID rewrites: its `id`, all descendant `id`s by prefix replacement, local string references, external chain references in other files, `text(...)` targets in MDX and TS, and TS markers — workspace builds and all edges retarget (query-asserted); mapping appended to journal. The command's own report is the applied mapping — every identity pair the operation journaled, the information of the preview's `mapping` (6.4) — carried in JSON per 12.0 (H-3 adapter; the successful operation's report shape is unpinned). * **T6.4-2 Minimal edits.** Quote style (single vs double) and access form (dot vs computed) of untouched reference parts are preserved byte-wise; only the affected parts change. Where the form cannot be kept: a new segment that is not a TS identifier is written as double-quoted computed access; a valid-identifier segment as dot access; string literals double-quoted. * **T6.4-3 Validation refusals (exit 1).** New ID invalid (1.4); equal to old; colliding with an existing ID; violating structural parent rules. Each refusal modifies nothing (workspace byte-compare). The remaining 6.4 clause — all rewritten references resolve — admits no discriminating fixture: rename rewrites only valid workspaces (T6.4-6) and retargets every affected reference to identities that exist after the operation, so a non-resolving rewritten reference is unconstructible; the clause is exercised as the always-passing side of every successful rename (T6.4-1). -* **T6.4-4 Usage errors (exit 2).** Nonexistent ``; nonexistent old ID; a discovered code source as `` — a wrong-kind operand, judged like existence before any content question (6.4). Checked before source validation: same exit 2 even when the workspace also has unrelated validation errors (12.0 ordering); but an old ID inside an unparseable origin file is masked — validation findings reported, exit 1. Old-ID existence is parse-local over spelled identities (6.4, 11.2): renaming an ID two sections both spell is no usage error — the bearers establish existence, their undefined node identities notwithstanding, and the duplicate-ID finding refuses instead (exit 1, the invalid-workspace refusal, T14-7); an old ID whose only would-be bearer spells no identity (its `id` attribute repeated on the tag) is nonexistent — exit 2 even beside that file's findings. +* **T6.4-4 Usage errors (exit 2).** Nonexistent ``; nonexistent old ID; a discovered code source as `` — a wrong-kind operand, judged like existence before any content question (6.4). Checked before source validation: same exit 2 even when the workspace also has unrelated validation errors (12.0 ordering); but an old ID inside an unparseable origin file is masked — validation findings reported, exit 1. Old-ID existence is parse-local over spelled identities (6.4, 11.2): renaming an ID two sections both spell is no usage error — the bearers establish existence, their undefined node identities notwithstanding, and the duplicate-ID finding refuses instead (exit 1, the invalid-workspace refusal, T14-7); so is renaming an ID whose sole bearer spells it beneath an ancestor spelling no identity (an undefined ancestor chain — 6.4's second parenthesized mechanism, 11.2): the bearer establishes existence and the ancestor's finding refuses (exit 1, never exit 2); an old ID whose only would-be bearer spells no identity (its `id` attribute repeated on the tag) is nonexistent — exit 2 even beside that file's findings. * **T6.4-5 Type-level references.** A `typeof`-level reference to the old identity is not rewritten; the workspace stays xspec-valid (the consumer type error is outside xspec's validations — `build` and `check` report no finding for it). * **T6.4-6 Valid-workspace precondition.** With a pre-existing validation error elsewhere, rename refuses (exit 1) before modifying anything. * **T6.4-7 Finishing regeneration.** After a successful rename, generated modules, Markdown output, and graph data are byte-identical to a fresh `build` of the rewritten sources; `check` immediately after reports no staleness (14.10). @@ -397,7 +397,7 @@ SPEC.md 11's five commands are JSON-only: for `query`, `occurrences`, `view`, `a * **T11-3 subtree/ancestors.** `subtree` returns the node plus descendants in document order (root query returns the whole file); `ancestors` returns proper ancestors nearest-first ending at the file root, excluding the queried node (empty for a root); rows carry the row fields of T11-2 — identity, source range, tags, coverage attribute (11: one row contract for `nodes`, `subtree`, and `ancestors`) — asserted on `subtree` and `ancestors` rows including a tagged `coverage="none"` node and a root (attribute absent), so a product omitting a row field from either subcommand fails. * **T11-4 edges.** `--from`/`--to` accept requirement nodes and code locations; `--kinds` filters over all four kinds and defaults to no filter (contains edges included); comma-separated list form; unknown kind value → exit 2. * **T11-5 reachable.** Reports existence of a dependency path under the given kinds (default: all three dependency kinds, never `contains`) and one shortest witness path with the 12.0 tie-break (two-equal-paths fixture); equal `--from` and `--to` (a node bearing both incoming and outgoing dependency edges) report that no path exists — a zero-length path is not a path (11); `--kinds contains` → exit 2 (invalid flag value: `reachable` accepts only the three dependency kinds, 11 — `edges` accepts all four, T11-4). -* **T11-6 Identity resolution.** Bare `path` resolves to a root node for a spec-group file and to a code location for a code-group file; `path#unit` and `path#unit@N` address code locations; a path in no configured group → exit 2 (unknown, 12.0). Wrong-kind operands (12.0): `query node` and `show` (12.4) given a code-group `path` or `path#unit` — a code source named where a requirement-node identity is required — each exit 2. +* **T11-6 Identity resolution.** Bare `path` resolves to a root node for a spec-group file and to a code location for a code-group file; `path#unit` and `path#unit@N` address code locations; a path in no configured group → exit 2 (unknown, 12.0). Wrong-kind operands (12.0): `query node` and `show` (12.4) given a code-group `path` or `path#unit` — a code source named where a requirement-node identity is required — each exit 2. Unknown code units (12.0: an unknown node identity named in arguments, the check judged parse-local over the named file's named units, 4.6): on a discovered code source, `query edges --from #` → exit 2 — likewise `edges --to` and `reachable --from`/`--to` given the same identity — and an out-of-range disambiguator, `#@2` where the chain occurs once in the file (4.6), is equally unknown, exit 2: never a bare edgeless graph node with an empty answer, exit 0 (the failing-workspace arm: T12.0-10). * **T11-7 Ordering.** Every result list is deterministic: repeated runs byte-identical; content-identical workspaces in different directories produce identical output (H-6). ### 11.2 Availability on imperfect files @@ -407,13 +407,13 @@ Tests here drive `occurrences`, `view`, and `at`; the per-surface contracts are * **T11.2-1 Parse-local structure, per-file masking, no writes.** Three spec files: A parseable with findings of both levels — an unresolved `d` reference and a self-cycle (resolution-level); a duplicate-ID pair, a malformed segment, an unknown prop, an invalid construct (per-file structural) — B unparseable, C finding-free. `view` over all three: A's full positional tree, construct ranges, raw attribute spellings, comment ranges, and occurrence positions are all served — structure survives A's own findings and B's invalidity; B contributes no view, its parse-failure finding accompanying; C's view is complete. The workspace fails `build`, so the gated reads report findings without answering (T13.3-3) while these surfaces answer per file — and modify nothing: graph data and derived files byte-identical around each invocation (11.2; the passing-workspace counterpart participates in refresh, T13.3-2). * **T11.2-2 Spelled identities and interpreted data.** One file, each node's identity datum asserted via `view`: exactly one quoted static `id` → defined; a repeated `id` (values agreeing, and disagreeing — one arm each), a braced `id={"x"}`, a valueless `id`, and no `id` → each spells none, identity explicitly unavailable; two sections both spelling `x` → both unavailable, no winner, while a uniquely spelled `x.y` beneath one of them keeps its defined identity (a defined identity without defined prefix identities); descendants of a no-identity or malformed-identity section are undefined by inheritance; a section uniquely spelling `z` stays defined beside another section's invalid-form `id` attributes (uniqueness compares spelled identities only — an invalid form contests nothing). Interpreted tags and coverage: absent props define the defaults (no tags, coverage-required); a repeated, malformed, or invalid-valued `tags`/`coverage` leaves the interpreted value unavailable, its raw spelling still listed (T11.4-3). * **T11.2-3 Invalid paths.** (Linux leg) A discovered spec source `a#b.mdx` and a non-UTF-8-named one (14.19): every node identity in each — root included — is explicitly unavailable while tree, ranges, and attributes stay on view; the condition-19 finding accompanies every answer whose domain includes the file; no identity over the invalid path is ever emitted, the non-UTF-8 path itself presented in the marked byte form (12.0, T12.7-1). A code source with `#` in its path defines no identity for its whole-file location or any unit: its spellings still record occurrences, each record's `source` explicitly unavailable (5.7, T11.3-1). Root identity is defined exactly when the file's path is valid. -* **T11.2-4 Resolution and expanded text.** Resolution turns on the referenced identity's own definedness: with duplicate spellings of `a` and a unique `a.b` beneath one bearer, a reference to `a.b` resolves and records its occurrence while a reference to `a` records none — ambiguous, every bearer undefined — reported by its finding's range, never as a record or an unavailable target. Expanded text via `view --text`: a chain A embeds B embeds C with an unresolved embedding in C → A's and B's own/subtree text unavailable (one unresolved spelling on the expansion path, or one embedding cycle — staged separately — poisons the whole value; partial expansion never occurs), sibling nodes with resolved expansions staying defined and byte-exact; removal classification is by syntactic form — after deleting an imported file, the importing file's text values are byte-identical to before (the import removed by form, its 14.15 finding notwithstanding), and a stray element (14.16) is content, preserved byte-for-byte in the enclosing text and located by its finding. +* **T11.2-4 Resolution and expanded text.** Resolution turns on the referenced identity's own definedness: with duplicate spellings of `a` and a unique `a.b` beneath one bearer, a reference to `a.b` resolves and records its occurrence while a reference to `a` records none — ambiguous, every bearer undefined — reported by its finding's range, never as a record or an unavailable target. Source-side unavailability (5.7, 11.2): resolving spellings themselves live in undefined-identity sections — a `d` entry naming `a.b` on the other duplicate bearer of `a`, and a `{text("a.b")}` embedding inside a section spelling no identity (`id` absent) — and each still records its occurrence: the record carries `file`, its own `range`, `kind`, and `target` (`a.b`), with `source` exactly the unavailability marker — identity and range withheld together as one datum (12.7; enumerated so in T11.3-1), never a picked bearer's identity and never a dropped record — while the view still positions each enclosing construct, its identity unavailable (11.4, T11.2-2); the file's findings — the duplicate-`id` and missing-`id` conditions among them — accompany, exit 1. Expanded text via `view --text`: a chain A embeds B embeds C with an unresolved embedding in C → A's and B's own/subtree text unavailable (one unresolved spelling on the expansion path, or one embedding cycle — staged separately — poisons the whole value; partial expansion never occurs), sibling nodes with resolved expansions staying defined and byte-exact; removal classification is by syntactic form — after deleting an imported file, the importing file's text values are byte-identical to before (the import removed by form, its 14.15 finding notwithstanding), and a stray element (14.16) is content, preserved byte-for-byte in the enclosing text and located by its finding. * **T11.2-5 Domain, findings, exits.** `view` naming only C (T11.2-1's finding-free file) → finding-free, exit 0, while A and B stay invalid — the domain is the requested files; naming A → A's findings of both levels accompany, exit 1, the full answer still emitted (the document complete and parseable, H-5); a two-file cycle accompanies whole when either participant is in the domain (14.9). Any finding or explicitly-unavailable datum → exit 1 with the full answer; complete and finding-free → exit 0; argument checks precede answering — unknown ``, wrong-kind ``, invalid glob, malformed `--to`, out-of-range offset each exit 2 whatever findings the named files carry (per-surface arms in T11.3-2/3, T11.4-2, T11.5-2). * **T11.2-6 Never stale, gate findings never attach.** On a passing workspace, `occurrences`, `view`, and `at` participate in read-time refresh exactly as 13.3's reads (T13.3-2 covers them in its sweep); on a failing one they answer from current sources and write nothing (T11.2-1). A gate condition that is no domain file's finding accompanies no answer: with a garbage journal line (14.13) staged, and separately an obstructed write path (14.22), `view` of a finding-free file answers finding-free, exit 0 — those states surface through `build`, `check`, and the gated reads (13.3), never these answers. ### 11.3 `xspec occurrences` -* **T11.3-1 Enumeration.** Over the T5.7-* fixtures: every occurrence in occurrence order, each record carrying every 5.7 datum in the form-exact 12.7 record form (T12.7-1); in T11.2-3's invalid-path code source, records are served with `source` unavailable while `file`, `range`, `kind`, and `target` are present. +* **T11.3-1 Enumeration.** Over the T5.7-* fixtures: every occurrence in occurrence order, each record carrying every 5.7 datum in the form-exact 12.7 record form (T12.7-1); in T11.2-3's invalid-path code source, and equally at T11.2-4's spec-source arm (resolving spellings inside a duplicate-`id` bearer and an id-less section), records are served with `source` unavailable while `file`, `range`, `kind`, and `target` are present. * **T11.3-2 `--file`.** A set restriction over discovered files, spec and code alike: a glob admitting a subset restricts the consulted domain — only its findings accompany; a glob matching no discovered file admits the empty set — an empty, finding-free answer, exit 0, no unknown-file usage error on this filter (contrast T11.4-2's operands); an outside-root pattern → exit 2 (invalid flag value, as 11.1); `--file` and `--to` combine conjunctively (a fixture where each filter alone admits more than the intersection). * **T11.3-3 `--to`.** Acceptance is syntactic: well-formed spellings — `path#id`, bare `path`, an undiscovered file's identity, a masked file's, an undefined bearer's — are accepted and select the empty set (with the domain's findings; never an error); malformed spellings exit 2: more than one `#`, an empty path part, an empty segment (`a#b..c`), a whitespace-bearing or forbidden-name segment (`a#then`), a trailing empty id part (`a.mdx#`) (11.3, 1.4). Selection is exact: a resolving identity selects the occurrences targeting it — not its descendants' — and a bare path selects module-form root references (T2.2-2). * **T11.3-4 Definitive emptiness.** In a valid workspace with no reference to node X: `occurrences --to X` → empty, finding-free, exit 0 — proof over the domain, absolute without `--file` (the whole discovered set consulted); restricted by `--file` away from a file that does hold a resolving occurrence of X, the answer is still empty, finding-free, exit 0 — the guarantee is domain-wide only, the outside occurrence neither reported nor denied (11.3). @@ -453,7 +453,7 @@ Tests here drive `occurrences`, `view`, and `at`; the per-surface contracts are * **T12.0-7 Determinism.** Representative sweep: `build` outputs, generated files, graph data, Markdown, journal entries, session files, and every report are byte-identical across repeated runs and across content-identical workspaces at different absolute paths (no wall-clock, randomness, absolute paths, or environment leakage; run with differing irrelevant environment variables). * **T12.0-8 Shortest-path tie-break.** Where one shortest path is reported (coverage 8.2, impact 9.3, reachable 11), among equal-length candidates the element-wise byte-least node-identity sequence is reported (dedicated fixtures per command). * **T12.0-9 Exit-code partition.** A table-driven sweep asserting one representative per class per command family: 0 (success and informational reports: `ids`, `show`, `impact` with differences, `query`, review reads including fully-resolved `next`, `coverage` without `--check`, `version`, and complete finding-free answers — `occurrences`/`view`/`at` over a clean domain, `inventory`, a successful preview; 11.2, 11.6, 6.6); 1 (findings: failing `build`, `check` findings, `coverage --check` uncovered, refused `rename`/`move` and their refused previews, refused review operations, corrupt-session reports, and answers carrying findings or explicitly-unavailable data — emitted in full; 11.2, 11.6, 6.6); 2 (usage/configuration: unknown command; unknown flag; missing required flag/argument; invalid flag value; unknown profile/session/group/item/node/file — except `occurrences --to`, where only a malformed spelling is a usage error, T11.3-3; wrong-kind operands — a code source where a spec source or a requirement-node identity is required; invalid session name; configuration errors; unreadable baseline; mutual-exclusion refusal). -* **T12.0-10 Argument-check precedence.** Rename/move and baseline arms: T6.4-4/T6.5-5 (existence, kind, and masking) and T6.3-4. Gated reads (12.0): on one workspace failing `build`'s validations, each gated read given a usage-error argument exits 2 with that error and reports no validation findings — `coverage `; `query nodes --group `; `review status `; `show #` and `query node ` (T11-6) — each check judged from what it consults (configuration; the session directory; parse-local spelled identities of the named file, 11.2), with the same names on a valid twin workspace giving the same exit-2 errors. Masking: `show #` on the failing workspace → the gated report, exit 1 (as T6.4-4). Past the gate: on a passing workspace, `review resolve --status updated` reports the corruption, exit 1 — the item ID judged only against session content, which the corruption withholds (10.1; an unknown item ID in a well-formed session stays exit 2, T10.7-10). Within class 2: an unknown command and a repeated flag are reported without loading configuration — identically with the workspace's configuration file invalid or missing — while a configuration error precedes every check that consults configuration or discovery: `coverage ` with invalid configuration reports 14.14, not the unknown profile (12.0). +* **T12.0-10 Argument-check precedence.** Rename/move and baseline arms: T6.4-4/T6.5-5 (existence, kind, and masking) and T6.3-4. Gated reads (12.0): on one workspace failing `build`'s validations, each gated read given a usage-error argument exits 2 with that error and reports no validation findings — `coverage `; `query nodes --group `; `review status `; `show #`, `query node `, and `query edges --from #` (T11-6) — each check judged from what it consults (configuration; the session directory; parse-local spelled identities or named units of the named file, 11.2, 4.6), with the same names on a valid twin workspace giving the same exit-2 errors. Masking: `show #` on the failing workspace → the gated report, exit 1 (as T6.4-4). Past the gate: on a passing workspace, `review resolve --status updated` reports the corruption, exit 1 — the item ID judged only against session content, which the corruption withholds (10.1; an unknown item ID in a well-formed session stays exit 2, T10.7-10). Within class 2: an unknown command, a repeated flag, and a malformed value (`show a#b#c`, the multi-`#` spelling of T12.0-13) are reported without loading configuration — identically with the workspace's configuration file invalid or missing — while a configuration error precedes every check that consults configuration or discovery: `coverage ` with invalid configuration reports 14.14, not the unknown profile (12.0). * **T12.0-11 Git is read-only.** SPEC.md's preamble: git data is read only where explicitly stated and never written. On a freshly built git fixture, around each git-reading invocation — `impact --base`, `review create --base`, and `review status`/`next`/`resolve` on the resulting baseline session (whose generator runs reconstruct the recorded baseline, 6.3/10.4) — everything under `.git/` is byte-identical before and after (same file set, same bytes: refs, HEAD, index, and objects untouched), and no workspace file changes except those the command's own specification writes (the session file; none for `impact`). * **T12.0-12 Git-less operation.** The non-baseline surface — `build`, `check`, `ids`, `show`, `coverage`, `query`, `occurrences`, `view`, `at`, `inventory`, `version`, `rename`, `move` (their `--preview` invocations included), and `review` with the `audit` and `coverage` strategies through `create`/`list`/`status`/`next`/`show`/`split`/`resolve`/`export` — runs to its specified outcomes in a workspace that is not a git repository and has no enclosing repository. Only baseline-taking invocations (`impact --base`, `review create --base`, later commands on a baseline session) require git; T10.6-1's git-less audit is one instance of this sweep. * **T12.0-13 `#` in operands.** More than one `#` in a ``, ``, `--to`, or move-operand value (`a#b#c`) is a malformed value — exit 2 on `show`, `query node`, `occurrences --to`, and `move` (12.0). A bare `` operand or `--file` glob is a whole path or pattern with no delimiter role for `#`: with a discovered source `specs/a#b.mdx` staged (Linux leg; condition 19), `view specs/a#b.mdx` names that discovered file — membership holds, the view served with identities unavailable, exit 1 (T11.2-3), never a `specs/a` + `b.mdx` pair (which would be exit 2, unknown file); `at specs/a#b.mdx 0` resolves the same way, and `occurrences --file 'specs/a#*'` matches it as a pattern.