Skip to content

Commit 313cfaf

Browse files
committed
test: add v0.3 shell analysis design corpus
1 parent 57f3767 commit 313cfaf

13 files changed

Lines changed: 1334 additions & 40 deletions

File tree

IMPLEMENTATION_PLAN.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ priorities.
133133
as the v0.3 roadmap and cross-link issue #71 control flow and issue #69
134134
shared native argument-fragment classification without merging their
135135
scopes.
136+
- [x] Add a versioned pre-implementation design corpus with 15 Bash and 15
137+
PowerShell representative, boundary, and adversarial cases. The validator
138+
rejects schema drift, checks references and command ordering, confirms
139+
every `current` expectation against the v0.2 parsers, and includes the
140+
files in the PII audit. Corpus review established that command role must
141+
be immediate while ancestry remains compositional, occurrence
142+
completeness is independent of value precision, and PowerShell authored
143+
parameter classification must remain distinct from effective values.
136144
- [ ] Complete OpenSpec task group 1: lock the additive public type names,
137145
compatibility projection, fixed analysis bounds, and supported-construct
138146
matrix before production implementation.

TOOLING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ directory, routes each entry to the matching parser (`bash/` → `BashParser`,
3030
gate — it feeds every PowerShell corpus input to real `pwsh` and enforces
3131
the oracle matrix.
3232

33+
`tests/ShellSyntaxTree.Tests/DesignCorpus/v0.3/` is a separate,
34+
pre-implementation contract corpus. Its focused validator rejects unknown JSON
35+
members, checks syntax/occurrence references and security invariants, and
36+
compares each recorded `current` result with the real v0.2 parser. Design cases
37+
move into the executable `Corpus/<shell>/` only when the corresponding v0.3 API
38+
and parser slice exists. The PII audit scans both corpus trees.
39+
3340
### PwshCorpusTool
3441

3542
`tools/PwshCorpusTool` is the PowerShell corpus authoring aid

openspec/changes/v0-3-structured-shell-analysis/design.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,23 @@ than inventing offsets into escaped or encoded outer text.
123123

124124
`Commands` contains one entry per authored simple command that may execute,
125125
not one entry per predicted runtime iteration. Each occurrence carries its
126-
`Clause`, structural role, ancestry suitable for diagnostics, and an explicit
127-
completeness fact. Roles include at least ordinary, pipeline stage, condition,
128-
iterator, loop body, branch, and substitution; the final names are locked with
129-
the public API review.
126+
`Clause`, immediate structural role, compositional ancestry suitable for
127+
analysis and diagnostics, and an explicit completeness fact. Immediate roles
128+
include at least ordinary, pipeline stage, condition, iterator, loop body,
129+
branch, and substitution; ancestry frames retain every outer role, such as a
130+
pipeline stage nested inside a loop body. The final names are locked with the
131+
public API review.
130132

131133
Condition and iterator commands are never omitted. Mutually exclusive branch
132134
commands all appear because the collection is a may-execute set. Runtime loop
133135
counts do not duplicate occurrences; bounded variable domains describe the
134136
possible effective values at the occurrence.
135137

138+
Completeness and value precision are independent. A structurally complete
139+
occurrence may conservatively contain an `Unknown` value domain when the
140+
command and its ancestry are fully discovered but a runtime value cannot be
141+
proved.
142+
136143
If any executable region cannot be discovered completely, the containing
137144
`ParsedCommand` remains `IsUnparseable=true`. Partial syntax and occurrences
138145
may be returned for diagnostics but MUST NOT be used to authorize execution.
@@ -171,10 +178,13 @@ shell rules prove the resulting argument boundary. Unquoted Bash expansion,
171178
PowerShell object-valued pipelines, indirect expansion, mutation, and
172179
cross-product explosion remain unknown until separately specified.
173180

174-
Effective values are shell facts, not executable semantics. A consumer must
175-
re-run its executable-aware option grammar for every exact or finite candidate;
176-
for example, a loop value beginning with `-` may inject an option even if the
177-
authored `$variable` token was not option-shaped.
181+
Effective values are shell facts, not executable semantics. The analysis must
182+
preserve both the authored shell classification and each proved effective
183+
value. PowerShell does not retroactively turn a string value such as `-Force`
184+
into a cmdlet parameter token, while the same value passed to a native
185+
executable may participate in that executable's option grammar. A consumer
186+
must therefore apply the relevant shell binding rules and re-run its complete
187+
executable-aware grammar for every exact or finite candidate.
178188

179189
### Join state rather than selecting a path
180190

@@ -393,7 +403,7 @@ remain shell-specific regardless of the eventual public choice.
393403
public sealed record CommandOccurrence
394404
{
395405
public Clause Clause { get; init; } = new();
396-
public CommandOccurrenceRole Role { get; init; }
406+
public CommandOccurrenceRole ImmediateRole { get; init; }
397407
public IReadOnlyList<CommandAncestryFrame> Ancestry { get; init; } = [];
398408
public IReadOnlyList<EffectiveArgument> EffectiveArguments { get; init; } = [];
399409
public ShellValueDomain WorkingDirectory { get; init; } = ShellValueDomain.Unknown;
@@ -444,6 +454,8 @@ the compatibility leaf and provides a place for one authored token to have
444454
multiple possible effective values. Contract review must still account for
445455
redirect operands, inline option bindings, shell expansions that create more
446456
than one argument, and occurrences that do not have an exact outer source span.
457+
An `Unknown` value at one of those coordinates does not by itself make the
458+
occurrence structurally incomplete.
447459

448460
### Explicit redirect facts
449461

openspec/changes/v0-3-structured-shell-analysis/specs/bounded-shell-analysis/spec.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ shell's quoting and expansion rules prove the resulting argument boundaries.
4848
- **WHEN** a PowerShell `foreach` variable may hold objects emitted by a pipeline
4949
- **THEN** its effective string or path value is unknown
5050

51-
### Requirement: Executable semantics are reapplied after substitution
52-
ShellSyntaxTree SHALL preserve effective candidate values without claiming
53-
whether they are options, operands, subcommands, revisions, or paths for a
54-
particular executable. Consumers SHALL interpret every candidate through a
51+
### Requirement: Shell and executable semantics are reapplied after substitution
52+
ShellSyntaxTree SHALL preserve the authored shell classification together with
53+
effective candidate values without claiming whether native-command candidates
54+
are options, operands, subcommands, revisions, or paths. Consumers SHALL apply
55+
the shell's binding rules and interpret every native candidate through a
5556
complete executable-aware grammar before reusing authorization.
5657

5758
#### Scenario: Finite value injects an rm option
@@ -64,6 +65,16 @@ complete executable-aware grammar before reusing authorization.
6465
- **THEN** the authored `--` remains visible before the effective candidate
6566
- **THEN** the consumer may account for it using rm semantics
6667

68+
#### Scenario: PowerShell cmdlet parameter-like value
69+
- **WHEN** PowerShell parses `foreach ($value in '-Force') { Write-Output $value }`
70+
- **THEN** the authored variable argument remains a positional expression
71+
- **THEN** the effective string `-Force` is not retroactively classified as a cmdlet parameter token
72+
73+
#### Scenario: PowerShell native option-like value
74+
- **WHEN** PowerShell parses `foreach ($value in '--force') { git clean $value }`
75+
- **THEN** the authored variable argument remains distinct from its effective value
76+
- **THEN** the consumer applies the native executable grammar to `--force`
77+
6778
### Requirement: Control-flow state joins conservatively
6879
Working-directory and supported variable state SHALL be propagated through
6980
sequential regions and joined across branches and loop exits. Disagreement

openspec/changes/v0-3-structured-shell-analysis/specs/executable-command-projection/spec.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ of whether the command is top-level or nested.
1616
- **THEN** its possible effective values are represented by analysis facts rather than three duplicated occurrences
1717

1818
### Requirement: Occurrences identify structural execution roles
19-
Each command occurrence SHALL identify the structural role by which it may
20-
execute and SHALL retain enough ancestry for diagnostics and UI grouping.
19+
Each command occurrence SHALL identify its immediate structural execution role
20+
and SHALL retain compositional ancestry for analysis, diagnostics, and UI
21+
grouping.
2122

2223
#### Scenario: While condition and body roles
2324
- **WHEN** Bash parses `while curl URL; do sleep 1; done`
@@ -29,6 +30,11 @@ execute and SHALL retain enough ancestry for diagnostics and UI grouping.
2930
- **THEN** `Get-ChildItem` is identified as an iterator occurrence
3031
- **THEN** `Remove-Item` is identified as a loop-body occurrence
3132

33+
#### Scenario: Pipeline stage nested in a loop body
34+
- **WHEN** Bash parses `for f in a b; do printf '%s\n' "$f" | sort; done`
35+
- **THEN** `printf` and `sort` have the immediate role pipeline stage
36+
- **THEN** their ancestry also identifies the enclosing loop body
37+
3238
### Requirement: Iterator and substitution commands remain visible
3339
The parser SHALL include every inner command from a supported executable
3440
iterator, substitution, or nested command in the occurrence collection even
@@ -58,6 +64,11 @@ be sufficient authorization evidence.
5864
- **THEN** the occurrence is structurally complete
5965
- **THEN** completeness does not imply that `echo` is authorized
6066

67+
#### Scenario: Complete occurrence with unknown value
68+
- **WHEN** PowerShell parses `foreach ($item in Get-ChildItem) { Write-Output $item }`
69+
- **THEN** the `Write-Output` occurrence may be structurally complete
70+
- **THEN** its effective `$item` value remains unknown because pipeline objects are not evaluated
71+
6172
### Requirement: Source order is deterministic
6273
The occurrence collection SHALL be ordered by authored command occurrence,
6374
including commands nested in headers and bodies, with a documented tie-breaker

openspec/changes/v0-3-structured-shell-analysis/specs/explicit-redirect-semantics/spec.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ executable may interpret that data as code.
8484
- **THEN** the body is preserved as non-expanding authored data
8585
- **THEN** the parser does not execute or reinterpret the receiving command
8686

87+
#### Scenario: Quoted delimiter adjacent to the operator
88+
- **WHEN** Bash parses `cat <<'EOF'` followed by a body and the `EOF` delimiter
89+
- **THEN** the quoted delimiter is recognized without requiring whitespace after `<<`
90+
- **THEN** the result is not reported as missing a delimiter
91+
8792
#### Scenario: Executable substitution in an expanding body
8893
- **WHEN** a supported expanding heredoc body contains command substitution
8994
- **THEN** the inner command is exposed or the result is unparseable

openspec/changes/v0-3-structured-shell-analysis/tasks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- [ ] 1.6 Synchronize the accepted public API and shared requirements into `SPEC.md`.
99
- [ ] 1.7 Synchronize PowerShell grammar and analysis deltas into `SPEC.POWERSHELL.md`.
1010
- [ ] 1.8 Update `PROJECT_CONTEXT.md` and `IMPLEMENTATION_PLAN.md` with the accepted v0.3 scope and delivery slices.
11+
- [x] 1.9 Add a paired Bash and PowerShell design corpus that records current behavior, desired structure, command occurrences, bounded values, redirect facts, compatibility projections, and security invariants.
12+
- [ ] 1.10 Promote each design case into the executable corpus as its production parser slice lands.
1113

1214
## 2. Behavior-Preserving Shared Preparation
1315

@@ -90,7 +92,7 @@
9092

9193
## 10. Separately Gated Syntax Concerns
9294

93-
- [ ] 10.1 Specify heredoc delimiter quoting, expansion mode, body provenance, substitutions, and completeness before enabling heredoc bodies.
95+
- [ ] 10.1 Specify heredoc delimiter adjacency and quoting, expansion mode, body provenance, substitutions, and completeness before enabling heredoc bodies.
9496
- [ ] 10.2 Specify process-substitution command discovery and the unknown produced descriptor/path value before enabling it.
9597
- [ ] 10.3 Specify background-list concurrency, ordering, and shell-state boundaries before enabling single `&`.
9698
- [ ] 10.4 Specify C-style loop and arithmetic hidden-execution behavior before enabling either construct.

tests/ShellSyntaxTree.Tests/Corpus/PiiAuditTests.cs

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ namespace ShellSyntaxTree.Tests.Corpus;
1616

1717
/// <summary>
1818
/// PII audit gate per SPEC §14 / SPEC.POWERSHELL.md §14. Scans every JSON
19-
/// corpus entry under every <c>tests/ShellSyntaxTree.Tests/Corpus/&lt;shell&gt;/</c>
20-
/// directory for the forbidden patterns listed in the sanitization table.
21-
/// The audit reads from the build-output copy of the corpus (the same
22-
/// location the runner pulls from) so CI runs against the same bytes a
19+
/// entry under the executable corpus and the pre-implementation design corpus
20+
/// for the forbidden patterns listed in the sanitization table. The audit
21+
/// reads from the build-output copies so CI runs against the same bytes a
2322
/// developer's local <c>dotnet test</c> would.
2423
/// </summary>
2524
/// <remarks>
@@ -101,35 +100,29 @@ public class PiiAuditTests
101100
[Fact]
102101
public void Corpus_contains_no_pii_per_spec_section_14()
103102
{
104-
var root = Path.Combine(AppContext.BaseDirectory, "Corpus");
105-
if (!Directory.Exists(root))
106-
{
107-
// The audit is vacuous when there's no corpus to audit; the
108-
// separate CorpusRunnerTests asserts the corpus is present.
109-
return;
110-
}
111-
112103
var hits = new List<string>();
113-
foreach (var shellDir in Directory.GetDirectories(root).OrderBy(d => d))
104+
var roots = new[] { "Corpus", "DesignCorpus" };
105+
foreach (var relativeRoot in roots)
114106
{
115-
var shell = Path.GetFileName(shellDir);
116-
foreach (var file in Directory.GetFiles(shellDir, "*.json").OrderBy(f => f))
107+
var root = Path.Combine(AppContext.BaseDirectory, relativeRoot);
108+
if (!Directory.Exists(root))
109+
{
110+
continue;
111+
}
112+
113+
foreach (var file in Directory.GetFiles(
114+
root, "*.json", SearchOption.AllDirectories).OrderBy(f => f))
117115
{
118-
var name = $"{shell}/{Path.GetFileName(file)}";
119-
JsonDocument doc;
116+
var name = Path.GetRelativePath(AppContext.BaseDirectory, file)
117+
.Replace('\\', '/');
120118
try
121119
{
122-
doc = JsonDocument.Parse(File.ReadAllText(file));
120+
using var doc = JsonDocument.Parse(File.ReadAllText(file));
121+
Walk(doc.RootElement, name, fieldPath: string.Empty, hits);
123122
}
124123
catch (JsonException ex)
125124
{
126125
hits.Add($"{name}: failed to parse JSON for PII audit: {ex.Message}");
127-
continue;
128-
}
129-
130-
using (doc)
131-
{
132-
Walk(doc.RootElement, name, fieldPath: string.Empty, hits);
133126
}
134127
}
135128
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# v0.3 Design Corpus
2+
3+
This directory is a pre-implementation contract corpus for the
4+
`v0-3-structured-shell-analysis` OpenSpec change. It does not feed the shipped
5+
v0.2 AST runner because that schema cannot represent nested syntax, command
6+
occurrences, effective values, or joined state.
7+
8+
Each case records these independent views:
9+
10+
1. `current`: the behavior the released flat parser produces today;
11+
2. `syntax`: the desired authored structural nodes and their parent slots;
12+
3. `commands`: every authored command that may execute, exactly once;
13+
4. `effectiveValues`: bounded shell facts without executable policy claims;
14+
5. `securityInvariants`: properties every future implementation and consumer
15+
must preserve.
16+
17+
The accompanying tests deserialize with unknown-member rejection, validate
18+
node and command references, compare `current` with the real v0.2 parser, and
19+
include these JSON files in the repository PII audit. A case moves into
20+
`Corpus/<shell>/` only after the corresponding public API and parser behavior
21+
exist and its full expected AST can pass the normal corpus runner.
22+
23+
The design corpus is intentionally shell-specific. Similar Bash and PowerShell
24+
cases may share structural expectations while retaining different quoting,
25+
expression, option-binding, object, and scope semantics.
26+
27+
Each command records one `immediateRole` plus its full `ancestry`. These are
28+
deliberately separate: a pipeline stage nested inside a loop body is immediately
29+
a pipeline stage and still carries the enclosing loop-body context. Likewise,
30+
`isComplete` describes command discovery and structure, not value precision; a
31+
complete occurrence may contain an `Unknown` effective value.

0 commit comments

Comments
 (0)