dsh-hashline-edit 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -45
- package/cordis.patch.yml +3 -3
- package/lib/index.js +3175 -3943
- package/lib/types/dsh/contracts.d.ts +12 -3
- package/lib/types/dsh/fs.d.ts +3 -0
- package/lib/types/dsh/guidance.d.ts +5 -5
- package/lib/types/dsh/hide.d.ts +79 -0
- package/lib/types/dsh/host-compat.d.ts +7 -78
- package/lib/types/dsh/index.d.ts +21 -9
- package/lib/types/dsh/metrics/events.d.ts +1 -1
- package/lib/types/dsh/metrics/observer.d.ts +1 -1
- package/lib/types/dsh/sandbox.d.ts +2 -2
- package/lib/types/dsh/shell-hook.d.ts +1 -1
- package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
- package/lib/types/dsh/tools/read.d.ts +3 -3
- package/lib/types/src/apply-patch/commit.d.ts +49 -0
- package/lib/types/src/apply-patch/grammar.d.ts +29 -0
- package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
- package/lib/types/src/apply-patch/plan.d.ts +50 -0
- package/lib/types/src/apply-patch/render.d.ts +51 -0
- package/lib/types/src/apply-patch/resolve.d.ts +129 -0
- package/lib/types/src/apply-patch/tool.d.ts +54 -0
- package/lib/types/src/apply-patch/types.d.ts +362 -0
- package/lib/types/src/domain-errors.d.ts +16 -2
- package/lib/types/src/edit-undo.d.ts +11 -2
- package/lib/types/src/file-content/index.d.ts +1 -1
- package/lib/types/src/file-content/preview.d.ts +1 -1
- package/lib/types/src/fs-lifecycle.d.ts +44 -0
- package/lib/types/src/hashline/alphabet.d.ts +4 -3
- package/lib/types/src/hashline/hash-identity.d.ts +12 -7
- package/lib/types/src/host-utils.d.ts +63 -0
- package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
- package/lib/types/src/mutation-engine/pipeline.d.ts +8 -1
- package/lib/types/src/mutation-engine/types.d.ts +40 -50
- package/lib/types/src/served-session/serve-render.d.ts +6 -0
- package/lib/types/src/undo-store.d.ts +17 -1
- package/package.json +18 -21
- package/prompts/apply-patch-guidelines.md +10 -0
- package/prompts/apply-patch.md +1 -0
- package/prompts/read-guidelines.md +4 -4
- package/prompts/read.md +1 -1
- package/prompts/undo-last-edit-guidelines.md +2 -2
- package/prompts/undo-last-edit-snippet.md +1 -1
- package/prompts/undo-last-edit.md +1 -1
- package/src/apply-patch/commit.ts +190 -0
- package/src/apply-patch/grammar.ts +620 -0
- package/src/apply-patch/index.ts +17 -0
- package/src/apply-patch/lifecycle.ts +195 -0
- package/src/apply-patch/plan.ts +800 -0
- package/src/apply-patch/render.ts +137 -0
- package/src/apply-patch/resolve.ts +470 -0
- package/src/apply-patch/tool.ts +257 -0
- package/src/apply-patch/types.ts +387 -0
- package/src/cut-repair.ts +77 -11
- package/src/domain-errors.ts +35 -3
- package/src/edit-undo.ts +379 -25
- package/src/file-content/index.ts +1 -1
- package/src/file-content/preview.ts +1 -1
- package/src/file-content/served-header.ts +1 -1
- package/src/fs-lifecycle.ts +201 -0
- package/src/hash-store.ts +18 -1
- package/src/hashline/alphabet.ts +7 -7
- package/src/hashline/hash-identity.ts +12 -7
- package/src/host-utils.ts +224 -0
- package/src/mutation-engine/edit-request.ts +39 -0
- package/src/mutation-engine/pipeline.ts +198 -27
- package/src/mutation-engine/types.ts +39 -57
- package/src/mutation-queue.ts +1 -1
- package/src/prompts.ts +3 -30
- package/src/served-session/serve-render.ts +2 -2
- package/src/undo-store.ts +74 -5
- package/lib/types/dsh/tools/edit.d.ts +0 -10
- package/lib/types/src/edit-tool.d.ts +0 -41
- package/lib/types/src/mutation-engine/engine.d.ts +0 -31
- package/lib/types/src/payload-contract.d.ts +0 -135
- package/prompts/edit-guidelines.md +0 -10
- package/prompts/edit-snippet.md +0 -1
- package/prompts/edit.md +0 -1
- package/src/bash-classifier.ts +0 -1626
- package/src/bash-conversion/decide.ts +0 -812
- package/src/bash-display-transform.ts +0 -50
- package/src/bash-multifile-config.ts +0 -239
- package/src/bash-view-config.ts +0 -158
- package/src/edit-normalize.ts +0 -8
- package/src/edit-render.ts +0 -161
- package/src/edit-tool.ts +0 -118
- package/src/edit.ts +0 -136
- package/src/integrations/pi-lens/command.ts +0 -712
- package/src/integrations/pi-lens/config.ts +0 -148
- package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
- package/src/integrations/pi-lens/io-bridge.ts +0 -112
- package/src/integrations/pi-lens/settings-overlay.ts +0 -272
- package/src/lifecycle-hooks/index.ts +0 -1992
- package/src/lifecycle-hooks/types.ts +0 -64
- package/src/mutated-files.ts +0 -75
- package/src/mutation-engine/engine.ts +0 -239
- package/src/mutation-engine/index.ts +0 -17
- package/src/payload-contract.ts +0 -685
- package/src/preview-controller.ts +0 -84
- package/src/read.ts +0 -865
- package/src/search-hook.ts +0 -41
- package/src/store-config.ts +0 -73
- package/src/store-janitor-warning.ts +0 -169
- package/src/store-janitor.ts +0 -539
- package/src/store-kernel/settings.ts +0 -233
- package/src/store.ts +0 -154
- package/src/tui-presenter.ts +0 -134
- package/src/write-hook.ts +0 -130
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bash conversion as data (ADR: #151): the `classifyBash` guard chain, transcribed rule by rule
|
|
3
|
-
* into one ordered table instead of an inline `if` ladder.
|
|
4
|
-
*
|
|
5
|
-
* WHY a table: array order is precedence — a new rule is inserted at its altitude and every guard
|
|
6
|
-
* it can shadow is visible above it, which is exactly what the inline chain made expensive
|
|
7
|
-
* (the #135 gate and the declined compounds both turned on shadowing). Each rule cites the source
|
|
8
|
-
* line it was transcribed from (pre-extraction `src/bash-classifier.ts`) and the ADR it implements.
|
|
9
|
-
*
|
|
10
|
-
* WHY the phases: the chain has loop-carried state (statements feed segments feed blocks), so a
|
|
11
|
-
* flat pass cannot express it. `decide` runs each phase's entries in array order — pre, then one
|
|
12
|
-
* statement walk (guards plus the structural segment push), then the chain ceiling, then one
|
|
13
|
-
* segment walk, then the conversion tail. Inside a walk, `run` returns `ADVANCE` to move to the
|
|
14
|
-
* next statement/segment (the transcribed `continue`) or a `Decision` to stop; `undefined` from
|
|
15
|
-
* `fireRule` means no rule fired. Accumulation (glue admission, `cd` recording, block recording)
|
|
16
|
-
* lives inside the entries because the source interleaves it with the guards.
|
|
17
|
-
*
|
|
18
|
-
* Scope note: `Decision` mirrors the established `BashClass` union (including the `passThrough`
|
|
19
|
-
* kind spelling) rather than inventing new block types — S1 is zero behaviour change, and the
|
|
20
|
-
* serve seams already switch on those kinds.
|
|
21
|
-
*/
|
|
22
|
-
import { parse } from "unbash";
|
|
23
|
-
import type { AndOr, Command, ParsedScript, Pipeline, Statement } from "unbash";
|
|
24
|
-
import {
|
|
25
|
-
BASH_VIEW_MAX_CHAIN_SEGMENTS,
|
|
26
|
-
classifySegment,
|
|
27
|
-
isRedundantCd,
|
|
28
|
-
isSemicolonStatementChain,
|
|
29
|
-
parseGlueSegment,
|
|
30
|
-
parseInertTail,
|
|
31
|
-
} from "../bash-classifier.js";
|
|
32
|
-
import type {
|
|
33
|
-
BashClass,
|
|
34
|
-
BashMultiView,
|
|
35
|
-
CompoundSegment,
|
|
36
|
-
SearchFamily,
|
|
37
|
-
SearchSeg,
|
|
38
|
-
SearchProgram,
|
|
39
|
-
Segment,
|
|
40
|
-
ViewSeg,
|
|
41
|
-
} from "../bash-classifier.js";
|
|
42
|
-
|
|
43
|
-
/** Box for the line-number injection offsets (ADR-0033 D5): the terminal search records its program token end while the chain walk runs, so the rewriter splices without re-deriving. */
|
|
44
|
-
export interface LocatedBox {
|
|
45
|
-
searchNameEnds?: Array<{ end: number; program: SearchProgram; lineNumbered: boolean }>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Closed decline vocabulary: every user-visible reason string, byte-identical to the chain. */
|
|
49
|
-
export type DeclineReason =
|
|
50
|
-
| "empty"
|
|
51
|
-
| "parse-error"
|
|
52
|
-
| "multi-statement"
|
|
53
|
-
| "background"
|
|
54
|
-
| "non-and-chain"
|
|
55
|
-
| "compound-top"
|
|
56
|
-
| "compound-andor-operand"
|
|
57
|
-
| "chain-too-long"
|
|
58
|
-
| "compound-segment"
|
|
59
|
-
| "non-literal-command"
|
|
60
|
-
| `unsupported-command:${string}`
|
|
61
|
-
| "unsupported-pipeline"
|
|
62
|
-
| "post-block-cd"
|
|
63
|
-
| "post-view-cd"
|
|
64
|
-
| "multi-cd"
|
|
65
|
-
| "multi-view"
|
|
66
|
-
| "no-view";
|
|
67
|
-
|
|
68
|
-
/** The conversion verdict: the `BashClass` shapes with the reason vocabulary closed. */
|
|
69
|
-
export type Decision =
|
|
70
|
-
| { kind: "passThrough"; reason: DeclineReason }
|
|
71
|
-
| Extract<BashClass, { kind: "pureView" }>
|
|
72
|
-
| Extract<BashClass, { kind: "pureMultiView" }>
|
|
73
|
-
| Extract<BashClass, { kind: "pureSearch" }>
|
|
74
|
-
| Extract<BashClass, { kind: "pureMultiSearch" }>
|
|
75
|
-
| Extract<BashClass, { kind: "pureRecursiveSearch" }>
|
|
76
|
-
| Extract<BashClass, { kind: "pureFilesList" }>
|
|
77
|
-
| Extract<BashClass, { kind: "pureCompound" }>;
|
|
78
|
-
|
|
79
|
-
/** `run` returns this to stop rule evaluation for the current statement/segment and advance the walk (the transcribed `continue`). */
|
|
80
|
-
const ADVANCE = "advance" as const;
|
|
81
|
-
|
|
82
|
-
export type RulePhase = "pre" | "statement" | "presegment" | "segment" | "post";
|
|
83
|
-
|
|
84
|
-
export interface ConversionRule {
|
|
85
|
-
/** Stable tag; the table test pins uniqueness and the documented order. */
|
|
86
|
-
id: string;
|
|
87
|
-
/** Authority: ADR citation where one exists, else the structural source line. */
|
|
88
|
-
why: string;
|
|
89
|
-
phase: RulePhase;
|
|
90
|
-
when(ctx: RuleContext): boolean;
|
|
91
|
-
/** A `Decision` stops the walk; `ADVANCE` moves to the next statement/segment. */
|
|
92
|
-
run(ctx: RuleContext): Decision | typeof ADVANCE;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface SegmentNode {
|
|
96
|
-
node: Command | Pipeline;
|
|
97
|
-
prefixPosition: boolean;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface RuleContext {
|
|
101
|
-
readonly command: string;
|
|
102
|
-
parseFailed: boolean;
|
|
103
|
-
script: ParsedScript | undefined;
|
|
104
|
-
statements: readonly Statement[];
|
|
105
|
-
segments: SegmentNode[];
|
|
106
|
-
hasGlue: boolean;
|
|
107
|
-
located: LocatedBox;
|
|
108
|
-
stmtIndex: number;
|
|
109
|
-
segIndex: number;
|
|
110
|
-
/** The current segment's classification, computed once per walk step (source: one call). */
|
|
111
|
-
current: Segment | undefined;
|
|
112
|
-
view: ViewSeg | undefined;
|
|
113
|
-
/** ADR-0049 M1: the standalone multi-file view, recorded only when the chain carries nothing else. */
|
|
114
|
-
multiView: BashMultiView | undefined;
|
|
115
|
-
searchSeg: SearchFamily | undefined;
|
|
116
|
-
baseDir: string | undefined;
|
|
117
|
-
effCwd: string | undefined;
|
|
118
|
-
sawGlue: boolean;
|
|
119
|
-
compoundSegs: CompoundSegment[];
|
|
120
|
-
/** Relaxation A (ADR-0039 D10): set by the head rule once the leading block is admitted — only inert tails may follow. */
|
|
121
|
-
tailMode: boolean;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function statementOf(ctx: RuleContext): Statement {
|
|
125
|
-
const statement = ctx.statements[ctx.stmtIndex];
|
|
126
|
-
if (statement === undefined)
|
|
127
|
-
throw new Error(`[151] unreachable: statement ${ctx.stmtIndex} missing`);
|
|
128
|
-
return statement;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function segmentOf(ctx: RuleContext): SegmentNode {
|
|
132
|
-
const segment = ctx.segments[ctx.segIndex];
|
|
133
|
-
if (segment === undefined) throw new Error(`[151] unreachable: segment ${ctx.segIndex} missing`);
|
|
134
|
-
return segment;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function currentOf(ctx: RuleContext): Segment {
|
|
138
|
-
if (ctx.current === undefined) throw new Error("[151] unreachable: segment not classified");
|
|
139
|
-
return ctx.current;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Trailing-else semantics (source: the view arm is the fallthrough past unsafe/silent/search). */
|
|
143
|
-
function isViewTerminal(segment: Segment): boolean {
|
|
144
|
-
return segment.kind !== "unsafe" && segment.kind !== "silent" && segment.kind !== "search";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* WHY: (item 10) one place applies the chain's single pre-view `cd` to whichever search family
|
|
149
|
-
* WHY: shape won — moved verbatim with the chain so the resolution base cannot be remembered for
|
|
150
|
-
* WHY: one shape and forgotten for another.
|
|
151
|
-
*/
|
|
152
|
-
function applyBaseDir(family: SearchFamily, baseDir: string | undefined): Decision {
|
|
153
|
-
if (baseDir === undefined) return family;
|
|
154
|
-
switch (family.kind) {
|
|
155
|
-
case "pureSearch":
|
|
156
|
-
return { kind: "pureSearch", search: { ...family.search, baseDir } };
|
|
157
|
-
case "pureMultiSearch":
|
|
158
|
-
return { kind: "pureMultiSearch", search: { ...family.search, baseDir } };
|
|
159
|
-
case "pureRecursiveSearch":
|
|
160
|
-
return { kind: "pureRecursiveSearch", search: { ...family.search, baseDir } };
|
|
161
|
-
case "pureFilesList":
|
|
162
|
-
return { kind: "pureFilesList", search: { ...family.search, baseDir } };
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* WHY: (ADR-0049 M1) a multi-file view keeps the chain's single pre-view `cd` for the same reason a
|
|
168
|
-
* single-file view does — bash resolved the operands under it — so the base rides the same field.
|
|
169
|
-
*/
|
|
170
|
-
function applyMultiViewBaseDir(view: BashMultiView, baseDir: string | undefined): Decision {
|
|
171
|
-
return { kind: "pureMultiView", view: baseDir === undefined ? view : { ...view, baseDir } };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export const RULES: readonly ConversionRule[] = [
|
|
175
|
-
{
|
|
176
|
-
id: "empty",
|
|
177
|
-
why: "structural input guard (source src/bash-classifier.ts:1091 pre-extraction; no ADR — vacuously safe)",
|
|
178
|
-
phase: "pre",
|
|
179
|
-
when: (ctx) => ctx.command.trim() === "",
|
|
180
|
-
run: () => ({ kind: "passThrough", reason: "empty" }),
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
id: "parse-error",
|
|
184
|
-
why: "structural input guard (source src/bash-classifier.ts:1096 throw and :1100 errors array pre-extraction; no ADR — unparseable means unprovable)",
|
|
185
|
-
phase: "pre",
|
|
186
|
-
when: (ctx) =>
|
|
187
|
-
ctx.parseFailed || (ctx.script?.errors !== undefined && ctx.script.errors.length > 0),
|
|
188
|
-
run: () => ({ kind: "passThrough", reason: "parse-error" }),
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
id: "multi-statement",
|
|
192
|
-
// WHY: D1/D9, amended 2026-10-09 (commit 7b78282): the `;`-chain gate lives here; every other
|
|
193
|
-
// WHY: multi-statement script is the compound ceiling and stays pass-through (ADR-0033 D1).
|
|
194
|
-
why: "ADR-0033 D1/D9 as amended (source src/bash-classifier.ts:1105-1110 pre-extraction)",
|
|
195
|
-
phase: "pre",
|
|
196
|
-
when: (ctx) =>
|
|
197
|
-
ctx.statements.length !== 1 && !isSemicolonStatementChain(ctx.command, ctx.statements),
|
|
198
|
-
run: () => ({ kind: "passThrough", reason: "multi-statement" }),
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
id: "background",
|
|
202
|
-
why: "ADR-0033 D1 (source src/bash-classifier.ts:1116 pre-extraction; a backgrounded statement is not the deterministic foreground shape)",
|
|
203
|
-
phase: "statement",
|
|
204
|
-
when: (ctx) => {
|
|
205
|
-
const statement = statementOf(ctx);
|
|
206
|
-
return statement.type !== "Statement" || statement.background === true;
|
|
207
|
-
},
|
|
208
|
-
run: () => ({ kind: "passThrough", reason: "background" }),
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
id: "non-and-chain",
|
|
212
|
-
why: "ADR-0033 D1 span-kind chain rule (source src/bash-classifier.ts:1125 pre-extraction; only `&&` sequences)",
|
|
213
|
-
phase: "statement",
|
|
214
|
-
when: (ctx) => {
|
|
215
|
-
const top = statementOf(ctx).command;
|
|
216
|
-
return top.type === "AndOr" && (top as AndOr).operators.some((op) => op !== "&&");
|
|
217
|
-
},
|
|
218
|
-
run: () => ({ kind: "passThrough", reason: "non-and-chain" }),
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
id: "compound-top",
|
|
222
|
-
why: "ADR-0033 D1 span-kind chain rule (source src/bash-classifier.ts:1133 pre-extraction; top must be AndOr/Command/Pipeline)",
|
|
223
|
-
phase: "statement",
|
|
224
|
-
when: (ctx) => {
|
|
225
|
-
const type = statementOf(ctx).command.type;
|
|
226
|
-
return type !== "AndOr" && type !== "Command" && type !== "Pipeline";
|
|
227
|
-
},
|
|
228
|
-
run: () => ({ kind: "passThrough", reason: "compound-top" }),
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
id: "compound-andor-operand",
|
|
232
|
-
// WHY: ADR-0033 D1 span-kind chain rule; `unbash` types AndOr.commands as Command[]
|
|
233
|
-
// WHY: but the parser puts ANY statement kind there (For/If/While/TimeClause/Block/
|
|
234
|
-
// WHY: Subshell/Case/FuncDecl/TestClause), so `cd x && for …; do …; done` pushed a
|
|
235
|
-
// WHY: node with no `operators` into the walk and crashed parseViewSegment.
|
|
236
|
-
why: "ADR-0033 D1 span-kind chain rule (structural: AndOr operands must be Command/Pipeline)",
|
|
237
|
-
phase: "statement",
|
|
238
|
-
when: (ctx) => {
|
|
239
|
-
const top = statementOf(ctx).command;
|
|
240
|
-
if (top.type !== "AndOr") return false;
|
|
241
|
-
return (top as AndOr).commands.some(
|
|
242
|
-
(node) => node.type !== "Command" && node.type !== "Pipeline",
|
|
243
|
-
);
|
|
244
|
-
},
|
|
245
|
-
run: () => ({ kind: "passThrough", reason: "compound-andor-operand" }),
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
id: "chain-too-long",
|
|
249
|
-
why: "structural ceiling BASH_VIEW_MAX_CHAIN_SEGMENTS (source src/bash-classifier.ts:1137 pre-extraction; no ADR — bounded walk)",
|
|
250
|
-
phase: "presegment",
|
|
251
|
-
when: (ctx) => ctx.segments.length > BASH_VIEW_MAX_CHAIN_SEGMENTS,
|
|
252
|
-
run: () => ({ kind: "passThrough", reason: "chain-too-long" }),
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
id: "compound-segment",
|
|
256
|
-
why: "structural type gate (source src/bash-classifier.ts:1153 pre-extraction; no ADR — defensive: the walker only pushes Command/Pipeline, so this is unreachable via parse)",
|
|
257
|
-
phase: "segment",
|
|
258
|
-
when: (ctx) => {
|
|
259
|
-
const type = segmentOf(ctx).node.type;
|
|
260
|
-
return type !== "Command" && type !== "Pipeline";
|
|
261
|
-
},
|
|
262
|
-
run: () => ({ kind: "passThrough", reason: "compound-segment" }),
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
id: "admit-glue",
|
|
266
|
-
// WHY: (ADR-0039 D6) static glue parses here so a compound can carry it — but only inside a
|
|
267
|
-
// WHY: multi-segment chain and only beside an admitted block. Accumulation, not a verdict.
|
|
268
|
-
why: "ADR-0039 D6 (source src/bash-classifier.ts:1165-1174 pre-extraction; glue admission)",
|
|
269
|
-
phase: "segment",
|
|
270
|
-
when: (ctx) => {
|
|
271
|
-
const current = currentOf(ctx);
|
|
272
|
-
if (current.kind !== "unsafe") return false;
|
|
273
|
-
const { node } = segmentOf(ctx);
|
|
274
|
-
return (
|
|
275
|
-
ctx.segments.length > 1 && node.type === "Command" && parseGlueSegment(node) !== undefined
|
|
276
|
-
);
|
|
277
|
-
},
|
|
278
|
-
run: (ctx) => {
|
|
279
|
-
const { node } = segmentOf(ctx);
|
|
280
|
-
const glue = node.type === "Command" ? parseGlueSegment(node) : undefined;
|
|
281
|
-
if (glue === undefined) throw new Error("[151] unreachable: glue re-parse diverged");
|
|
282
|
-
ctx.sawGlue = true;
|
|
283
|
-
ctx.compoundSegs.push({ kind: "glue", glue });
|
|
284
|
-
return ADVANCE;
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
id: "inert-tail",
|
|
289
|
-
// WHY: (ADR-0039 D10) an inert tail command contributes only its already-observed bytes —
|
|
290
|
-
// WHY: no block, no anchors, no leases. It fires only inside tail mode (set by the head
|
|
291
|
-
// WHY: rule), so a lone `wc` still declines via `unsafe` with its exact legacy reason.
|
|
292
|
-
why: "ADR-0039 D10 (relaxation A; inert tail accumulation)",
|
|
293
|
-
phase: "segment",
|
|
294
|
-
when: (ctx) => ctx.tailMode && isInertTailNode(segmentOf(ctx).node),
|
|
295
|
-
run: pushRawTail,
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
id: "inert-count-tail",
|
|
299
|
-
// WHY: (ADR-0048 D4) relaxation A is glue-free and single-block, so a glued chain's count tail
|
|
300
|
-
// WHY: used to reach `unsafe`. D4 admits it after the LAST verified block of a multi-block
|
|
301
|
-
// WHY: glued chain: at least two blocks are already recorded and every remaining segment is a
|
|
302
|
-
// WHY: count tail, so no block can follow and no later block competes for attribution (ADR-0039
|
|
303
|
-
// WHY: D10's second-block guard stays closed — this rule never admits a block). The tail is a
|
|
304
|
-
// WHY: `raw` segment: its already-observed bytes ride the peel's `rest`, never a block, anchor,
|
|
305
|
-
// WHY: or lease.
|
|
306
|
-
why: "ADR-0048 D4 (inert count tail after the last verified block)",
|
|
307
|
-
phase: "segment",
|
|
308
|
-
when: (ctx) => {
|
|
309
|
-
if (!ctx.hasGlue || admittedBlockCount(ctx) < 2) return false;
|
|
310
|
-
return ctx.segments
|
|
311
|
-
.slice(ctx.segIndex)
|
|
312
|
-
.every((segment) => isInertCountTailNode(segment.node));
|
|
313
|
-
},
|
|
314
|
-
run: pushRawTail,
|
|
315
|
-
},
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
id: "unsafe",
|
|
319
|
-
// WHY: (ADR-0039 D6) a lone print keeps its own decline (F2: glue locates blocks; with no block
|
|
320
|
-
// WHY: there is nothing to locate). The reason string flows through byte-identically.
|
|
321
|
-
why: "ADR-0039 D6/F2 (source src/bash-classifier.ts:1162 pre-extraction; dynamic segment reason)",
|
|
322
|
-
phase: "segment",
|
|
323
|
-
when: (ctx) => currentOf(ctx).kind === "unsafe",
|
|
324
|
-
run: (ctx) => {
|
|
325
|
-
const current = currentOf(ctx);
|
|
326
|
-
if (current.kind !== "unsafe") throw new Error("[151] unreachable: kind changed");
|
|
327
|
-
return { kind: "passThrough", reason: current.reason as DeclineReason };
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
id: "skip-redundant-cd",
|
|
332
|
-
// WHY: (ADR-0039 D7/D9) a redundant `cd` is inert — skipped, never counted toward the single
|
|
333
|
-
// WHY: `cd` limit. Only base-independent dot forms and exact known-absolute repeats qualify.
|
|
334
|
-
why: "ADR-0039 D7/D9 (source src/bash-classifier.ts:1173 pre-extraction; redundancy skip)",
|
|
335
|
-
phase: "segment",
|
|
336
|
-
when: (ctx) => {
|
|
337
|
-
const current = currentOf(ctx);
|
|
338
|
-
return (
|
|
339
|
-
current.kind === "silent" &&
|
|
340
|
-
current.cdDir !== undefined &&
|
|
341
|
-
isRedundantCd(current.cdDir, ctx.effCwd)
|
|
342
|
-
);
|
|
343
|
-
},
|
|
344
|
-
run: () => ADVANCE,
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
id: "post-block-cd",
|
|
348
|
-
// WHY: (commit 4) a non-redundant `cd` after an admitted block would misresolve that block
|
|
349
|
-
// WHY: (`baseDir` attaches to later blocks only) — fail closed. Gated on glue presence so
|
|
350
|
-
// WHY: single-block chains keep their exact current reasons.
|
|
351
|
-
why: "commit 4 post-block gate (source src/bash-classifier.ts:1184 pre-extraction)",
|
|
352
|
-
phase: "segment",
|
|
353
|
-
when: (ctx) => {
|
|
354
|
-
const current = currentOf(ctx);
|
|
355
|
-
return (
|
|
356
|
-
current.kind === "silent" &&
|
|
357
|
-
current.cdDir !== undefined &&
|
|
358
|
-
ctx.hasGlue &&
|
|
359
|
-
(ctx.view !== undefined || ctx.searchSeg !== undefined)
|
|
360
|
-
);
|
|
361
|
-
},
|
|
362
|
-
run: () => ({ kind: "passThrough", reason: "post-block-cd" }),
|
|
363
|
-
},
|
|
364
|
-
{
|
|
365
|
-
id: "post-view-cd",
|
|
366
|
-
// WHY: bash resolves each `cd` against the directory in effect at that point, so only a single
|
|
367
|
-
// WHY: pre-view `cd` applied to `ctx.cwd` is sound (ADR-0033 D1). A `cd` after the view ran too
|
|
368
|
-
// WHY: late to affect it and fails closed.
|
|
369
|
-
why: "ADR-0033 D1 (source src/bash-classifier.ts:1186 pre-extraction)",
|
|
370
|
-
phase: "segment",
|
|
371
|
-
when: (ctx) => {
|
|
372
|
-
const current = currentOf(ctx);
|
|
373
|
-
// WHY: (ADR-0049 M1) a multi-file view is a view for this rule too: a `cd` after it ran is just
|
|
374
|
-
// WHY: as late, so the base would misresolve every operand.
|
|
375
|
-
return (
|
|
376
|
-
current.kind === "silent" &&
|
|
377
|
-
current.cdDir !== undefined &&
|
|
378
|
-
(ctx.view !== undefined || ctx.multiView !== undefined)
|
|
379
|
-
);
|
|
380
|
-
},
|
|
381
|
-
run: () => ({ kind: "passThrough", reason: "post-view-cd" }),
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
id: "multi-cd",
|
|
385
|
-
// WHY: chained `cd`s compose (`cd a && cd b` lands in `a/b`, not `b`) — applying only one out
|
|
386
|
-
// WHY: of order would lease a path never viewed (ADR-0033 D1).
|
|
387
|
-
why: "ADR-0033 D1 (source src/bash-classifier.ts:1187 pre-extraction)",
|
|
388
|
-
phase: "segment",
|
|
389
|
-
when: (ctx) => {
|
|
390
|
-
const current = currentOf(ctx);
|
|
391
|
-
return current.kind === "silent" && current.cdDir !== undefined && ctx.baseDir !== undefined;
|
|
392
|
-
},
|
|
393
|
-
run: () => ({ kind: "passThrough", reason: "multi-cd" }),
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
id: "record-cd",
|
|
397
|
-
why: "ADR-0033 D1 single pre-view `cd` (source src/bash-classifier.ts:1188-1191 pre-extraction; accumulation: the resolution base for the winning shape)",
|
|
398
|
-
phase: "segment",
|
|
399
|
-
when: (ctx) => {
|
|
400
|
-
const current = currentOf(ctx);
|
|
401
|
-
return current.kind === "silent" && current.cdDir !== undefined;
|
|
402
|
-
},
|
|
403
|
-
run: (ctx) => {
|
|
404
|
-
const current = currentOf(ctx);
|
|
405
|
-
const dir = current.kind === "silent" ? current.cdDir : undefined;
|
|
406
|
-
if (dir === undefined) throw new Error("[151] unreachable: cd vanished");
|
|
407
|
-
ctx.baseDir = dir;
|
|
408
|
-
if (dir.startsWith("/")) ctx.effCwd = dir;
|
|
409
|
-
return ADVANCE;
|
|
410
|
-
},
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
id: "silent-skip",
|
|
414
|
-
why: "structural (source src/bash-classifier.ts:1192 pre-extraction; `:`/`true`/prefix `pwd` carry no geometry — absorbed)",
|
|
415
|
-
phase: "segment",
|
|
416
|
-
when: (ctx) => currentOf(ctx).kind === "silent",
|
|
417
|
-
run: () => ADVANCE,
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
id: "inert-tail-head",
|
|
421
|
-
// WHY: (ADR-0039 D10) relaxation A admits a compound with NO glue iff exactly one block
|
|
422
|
-
// WHY: exists, it is the first segment, and every later segment is inert (checked by
|
|
423
|
-
// WHY: classifying the remainder up front — pure functions, no walk state touched). It
|
|
424
|
-
// WHY: preempts the prefix/second declines for that head only; anything else in the tail
|
|
425
|
-
// WHY: falls through to the original guards with their original reasons. Glued chains
|
|
426
|
-
// WHY: keep their exact legacy behavior (`!ctx.hasGlue`).
|
|
427
|
-
why: "ADR-0039 D10 (relaxation A; single leading block with inert tail)",
|
|
428
|
-
phase: "segment",
|
|
429
|
-
when: (ctx) => {
|
|
430
|
-
if (ctx.segIndex !== 0 || ctx.hasGlue) return false;
|
|
431
|
-
if (ctx.view !== undefined || ctx.searchSeg !== undefined) return false;
|
|
432
|
-
const current = currentOf(ctx);
|
|
433
|
-
// WHY: (ADR-0049 M1) a multi-file view is standalone: it takes no inert tail, because the
|
|
434
|
-
// WHY: tail's `raw` segment would be a second block whose attribution this shape never proves.
|
|
435
|
-
if (current.kind === "multiView") return false;
|
|
436
|
-
if (current.kind !== "search" && !isViewTerminal(current)) return false;
|
|
437
|
-
const tail = ctx.segments.slice(1);
|
|
438
|
-
return tail.length > 0 && tail.every((segment) => isInertTailNode(segment.node));
|
|
439
|
-
},
|
|
440
|
-
run: (ctx) => {
|
|
441
|
-
const current = currentOf(ctx);
|
|
442
|
-
if (current.kind === "search") recordSearchBlock(ctx, current);
|
|
443
|
-
else if (current.kind === "view") recordViewBlock(ctx, current);
|
|
444
|
-
else throw new Error("[152] unreachable: head is not a block");
|
|
445
|
-
ctx.tailMode = true;
|
|
446
|
-
return ADVANCE;
|
|
447
|
-
},
|
|
448
|
-
},
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
id: "search-prefix",
|
|
452
|
-
// WHY: the search obeys the view's chain rule verbatim (ADR-0033 D1 as amended): only the
|
|
453
|
-
// WHY: terminal statement may carry it — a prefix `grep` would be a compound chain the model
|
|
454
|
-
// WHY: did not ask about, so it fails closed.
|
|
455
|
-
why: "ADR-0033 D1 as amended (source src/bash-classifier.ts:1197 pre-extraction)",
|
|
456
|
-
phase: "segment",
|
|
457
|
-
when: (ctx) => {
|
|
458
|
-
const current = currentOf(ctx);
|
|
459
|
-
return current.kind === "search" && segmentOf(ctx).prefixPosition && !ctx.hasGlue;
|
|
460
|
-
},
|
|
461
|
-
run: () => ({ kind: "passThrough", reason: "multi-statement" }),
|
|
462
|
-
},
|
|
463
|
-
{
|
|
464
|
-
id: "search-second",
|
|
465
|
-
why: "ADR-0033 D1 one view-or-search per command (source src/bash-classifier.ts:1198 pre-extraction)",
|
|
466
|
-
phase: "segment",
|
|
467
|
-
when: (ctx) => {
|
|
468
|
-
const current = currentOf(ctx);
|
|
469
|
-
return (
|
|
470
|
-
current.kind === "search" &&
|
|
471
|
-
(ctx.view !== undefined || ctx.searchSeg !== undefined) &&
|
|
472
|
-
!ctx.hasGlue
|
|
473
|
-
);
|
|
474
|
-
},
|
|
475
|
-
run: () => ({ kind: "passThrough", reason: "multi-view" }),
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
id: "record-search",
|
|
479
|
-
why: "structural accumulation (source src/bash-classifier.ts:1201-1216 pre-extraction; records the family, the compound slot, and the line-number injection offsets)",
|
|
480
|
-
phase: "segment",
|
|
481
|
-
when: (ctx) => currentOf(ctx).kind === "search",
|
|
482
|
-
run: (ctx) => {
|
|
483
|
-
const current = currentOf(ctx);
|
|
484
|
-
if (current.kind !== "search") throw new Error("[151] unreachable: kind changed");
|
|
485
|
-
recordSearchBlock(ctx, current);
|
|
486
|
-
return ADVANCE;
|
|
487
|
-
},
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
id: "view-prefix",
|
|
491
|
-
// WHY: only the terminal statement may carry the view (ADR-0033 D1 as amended) — a view in a
|
|
492
|
-
// WHY: prefix statement means the script is a compound chain the model did not ask about.
|
|
493
|
-
why: "ADR-0033 D1 as amended (source src/bash-classifier.ts:1223 pre-extraction)",
|
|
494
|
-
phase: "segment",
|
|
495
|
-
when: (ctx) => isViewTerminal(currentOf(ctx)) && segmentOf(ctx).prefixPosition && !ctx.hasGlue,
|
|
496
|
-
run: () => ({ kind: "passThrough", reason: "multi-statement" }),
|
|
497
|
-
},
|
|
498
|
-
{
|
|
499
|
-
id: "view-second",
|
|
500
|
-
why: "ADR-0033 D1 one view-or-search per command (source src/bash-classifier.ts:1225 pre-extraction)",
|
|
501
|
-
phase: "segment",
|
|
502
|
-
when: (ctx) =>
|
|
503
|
-
isViewTerminal(currentOf(ctx)) &&
|
|
504
|
-
(ctx.view !== undefined || ctx.searchSeg !== undefined) &&
|
|
505
|
-
!ctx.hasGlue,
|
|
506
|
-
run: () => ({ kind: "passThrough", reason: "multi-view" }),
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
id: "record-view",
|
|
510
|
-
why: "structural accumulation (source src/bash-classifier.ts:1227-1228 pre-extraction; the trailing else — every non-unsafe/silent/search segment is a view)",
|
|
511
|
-
phase: "segment",
|
|
512
|
-
when: () => true,
|
|
513
|
-
run: (ctx) => {
|
|
514
|
-
const current = currentOf(ctx);
|
|
515
|
-
// WHY: (ADR-0049 M1) the multi-file view is the one non-`view` block the trailing else
|
|
516
|
-
// WHY: records — into its own slot only. It is deliberately NOT pushed into `compoundSegs`, which
|
|
517
|
-
// WHY: is the compound envelope's segment list: a multi-file view never rides in one.
|
|
518
|
-
if (current.kind === "multiView") {
|
|
519
|
-
ctx.multiView = current.view;
|
|
520
|
-
return ADVANCE;
|
|
521
|
-
}
|
|
522
|
-
if (!isViewTerminal(current) || current.kind !== "view") {
|
|
523
|
-
throw new Error("[151] unreachable: non-view reached the view arm");
|
|
524
|
-
}
|
|
525
|
-
recordViewBlock(ctx, current);
|
|
526
|
-
return ADVANCE;
|
|
527
|
-
},
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
id: "multi-view-chain",
|
|
531
|
-
// WHY: (ADR-0049 M1) the multi-file shape is a STANDALONE view: this ADR does not compose it as a
|
|
532
|
-
// WHY: block inside a chain. Any glue, second block or inert tail means the observed stream carries
|
|
533
|
-
// WHY: more than the N pages this seam proves, so the whole command passes through with the existing
|
|
534
|
-
// WHY: `multi-view` reason (ADR-0049 adds no reason to the closed vocabulary).
|
|
535
|
-
why: "ADR-0049 M1 (standalone only; a chained multi-file view has no admitted envelope)",
|
|
536
|
-
phase: "post",
|
|
537
|
-
when: (ctx) => ctx.multiView !== undefined && (ctx.sawGlue || ctx.compoundSegs.length > 0),
|
|
538
|
-
run: () => ({ kind: "passThrough", reason: "multi-view" }),
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
id: "multi-view",
|
|
542
|
-
why: "ADR-0049 M1/M2 (single-base application for the standalone multi-file view)",
|
|
543
|
-
phase: "post",
|
|
544
|
-
when: (ctx) => ctx.multiView !== undefined,
|
|
545
|
-
run: (ctx) => {
|
|
546
|
-
if (ctx.multiView === undefined) throw new Error("[151] unreachable: multi view vanished");
|
|
547
|
-
return applyMultiViewBaseDir(ctx.multiView, ctx.baseDir);
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
|
|
551
|
-
{
|
|
552
|
-
id: "display-compound",
|
|
553
|
-
why: "ADR-0048 D2 (display tails are standalone; a compound envelope has no transform attribution)",
|
|
554
|
-
phase: "post",
|
|
555
|
-
when: (ctx) => ctx.view?.display !== undefined && (ctx.sawGlue || ctx.tailMode),
|
|
556
|
-
run: () => ({ kind: "passThrough", reason: "unsupported-pipeline" }),
|
|
557
|
-
},
|
|
558
|
-
|
|
559
|
-
{
|
|
560
|
-
id: "compound",
|
|
561
|
-
// WHY: F2-as-restated (ADR-0039): glue alone admits nothing — zero admitted BLOCKS means the
|
|
562
|
-
// WHY: compound declines even if glue parsed. With a block, the admitted segments envelope.
|
|
563
|
-
why: "ADR-0039 D1/D2/D4 + F2 (source src/bash-classifier.ts:1233-1239 pre-extraction)",
|
|
564
|
-
phase: "post",
|
|
565
|
-
when: (ctx) => ctx.sawGlue && (ctx.view !== undefined || ctx.searchSeg !== undefined),
|
|
566
|
-
run: (ctx) => ({
|
|
567
|
-
kind: "pureCompound",
|
|
568
|
-
segments: ctx.compoundSegs,
|
|
569
|
-
...(ctx.baseDir === undefined ? {} : { baseDir: ctx.baseDir }),
|
|
570
|
-
}),
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
id: "inert-compound",
|
|
574
|
-
// WHY: (ADR-0039 D10) the relaxation-A envelope: the leading block plus its raw tail.
|
|
575
|
-
// WHY: No `baseDir` spread — the head fires only at segment zero, before any `cd` records.
|
|
576
|
-
why: "ADR-0039 D10 (relaxation A; envelope for the leading block plus raw tail)",
|
|
577
|
-
phase: "post",
|
|
578
|
-
when: (ctx) => ctx.tailMode && ctx.compoundSegs.length > 0,
|
|
579
|
-
run: (ctx) => ({
|
|
580
|
-
kind: "pureCompound",
|
|
581
|
-
segments: ctx.compoundSegs,
|
|
582
|
-
}),
|
|
583
|
-
},
|
|
584
|
-
|
|
585
|
-
{
|
|
586
|
-
id: "search",
|
|
587
|
-
why: "item 10 single-base application (source src/bash-classifier.ts:1240 pre-extraction)",
|
|
588
|
-
phase: "post",
|
|
589
|
-
when: (ctx) => ctx.searchSeg !== undefined,
|
|
590
|
-
run: (ctx) => {
|
|
591
|
-
if (ctx.searchSeg === undefined) throw new Error("[151] unreachable: search vanished");
|
|
592
|
-
return applyBaseDir(ctx.searchSeg, ctx.baseDir);
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
id: "no-view",
|
|
597
|
-
why: "ADR-0039 F2-as-restated (source src/bash-classifier.ts:1241 pre-extraction; no admitted block — nothing to serve)",
|
|
598
|
-
phase: "post",
|
|
599
|
-
when: (ctx) => ctx.view === undefined,
|
|
600
|
-
run: () => ({ kind: "passThrough", reason: "no-view" }),
|
|
601
|
-
},
|
|
602
|
-
{
|
|
603
|
-
id: "view",
|
|
604
|
-
why: "structural terminal conversion (source src/bash-classifier.ts:1243-1257 pre-extraction; the fallthrough that serves the admitted view)",
|
|
605
|
-
phase: "post",
|
|
606
|
-
when: () => true,
|
|
607
|
-
run: (ctx) => {
|
|
608
|
-
const view = ctx.view;
|
|
609
|
-
if (view === undefined) throw new Error("[151] unreachable: view vanished");
|
|
610
|
-
return {
|
|
611
|
-
kind: "pureView",
|
|
612
|
-
view:
|
|
613
|
-
ctx.baseDir === undefined
|
|
614
|
-
? {
|
|
615
|
-
filePath: view.filePath,
|
|
616
|
-
ops: view.ops,
|
|
617
|
-
...(view.lineNumbered === true ? { lineNumbered: true } : {}),
|
|
618
|
-
...(view.display === undefined ? {} : { display: view.display }),
|
|
619
|
-
}
|
|
620
|
-
: {
|
|
621
|
-
filePath: view.filePath,
|
|
622
|
-
baseDir: ctx.baseDir,
|
|
623
|
-
ops: view.ops,
|
|
624
|
-
...(view.lineNumbered === true ? { lineNumbered: true } : {}),
|
|
625
|
-
...(view.display === undefined ? {} : { display: view.display }),
|
|
626
|
-
},
|
|
627
|
-
} satisfies Extract<BashClass, { kind: "pureView" }>;
|
|
628
|
-
},
|
|
629
|
-
},
|
|
630
|
-
];
|
|
631
|
-
|
|
632
|
-
function fireRule(rule: ConversionRule, ctx: RuleContext): Decision | typeof ADVANCE | undefined {
|
|
633
|
-
if (!rule.when(ctx)) return undefined;
|
|
634
|
-
return rule.run(ctx);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* WHY: (ADR-0033 D5) the line-number injection needs the search program token's source offset.
|
|
639
|
-
* Shared by the plain and relaxation-A recording paths so the splice sees identical offsets.
|
|
640
|
-
*/
|
|
641
|
-
function recordSearchBlock(ctx: RuleContext, current: SearchSeg): void {
|
|
642
|
-
ctx.searchSeg = current.family;
|
|
643
|
-
ctx.compoundSegs.push(current);
|
|
644
|
-
// WHY: the line-number injection must splice at the search program token's source offset, and
|
|
645
|
-
// WHY: that offset is only known while the chain walk runs (transcribed with the walk).
|
|
646
|
-
const { node } = segmentOf(ctx);
|
|
647
|
-
if (node.type === "Command" && node.name?.end !== undefined) {
|
|
648
|
-
const family = current.family;
|
|
649
|
-
if (
|
|
650
|
-
family.kind === "pureSearch" ||
|
|
651
|
-
family.kind === "pureMultiSearch" ||
|
|
652
|
-
family.kind === "pureRecursiveSearch"
|
|
653
|
-
) {
|
|
654
|
-
if (ctx.located.searchNameEnds === undefined) ctx.located.searchNameEnds = [];
|
|
655
|
-
ctx.located.searchNameEnds.push({
|
|
656
|
-
end: node.name.end,
|
|
657
|
-
program: family.search.program,
|
|
658
|
-
lineNumbered: family.search.lineNumbered,
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function recordViewBlock(ctx: RuleContext, current: ViewSeg): void {
|
|
665
|
-
ctx.view = current;
|
|
666
|
-
ctx.compoundSegs.push(current);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/** A tail command is inert when it is exactly the D10 shape: `wc` with counting flags and file operands. */
|
|
670
|
-
function isInertTailNode(node: Command | Pipeline): boolean {
|
|
671
|
-
return node.type === "Command" && parseInertTail(node) !== undefined;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/**
|
|
675
|
-
* WHY: (ADR-0048 D4) the count-tail closed set is narrower than D10's inert set: the ADR names
|
|
676
|
-
* `wc -l`/`-c` only, so `-w`/`-m` stay out on this seam. Every other exclusion (stdin, `--`
|
|
677
|
-
* escapes, dash-led operands, redirects, prefixes) is `parseInertTail`'s, shared unchanged.
|
|
678
|
-
* Declining a shape is fail-closed; widening this set is a follow-up with its own safety case —
|
|
679
|
-
* add a flag here, not a new rule.
|
|
680
|
-
*/
|
|
681
|
-
function isInertCountTailNode(node: Command | Pipeline): boolean {
|
|
682
|
-
if (node.type !== "Command") return false;
|
|
683
|
-
const tail = parseInertTail(node);
|
|
684
|
-
if (tail === undefined) return false;
|
|
685
|
-
const flags = tail.args.filter((arg) => arg.startsWith("-"));
|
|
686
|
-
return flags.length > 0 && flags.every((flag) => /^-[lc]+$/.test(flag));
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/** Blocks recorded so far — the D4 tail is admitted only after a second one. */
|
|
690
|
-
function admittedBlockCount(ctx: RuleContext): number {
|
|
691
|
-
return ctx.compoundSegs.filter((segment) => segment.kind === "view" || segment.kind === "search")
|
|
692
|
-
.length;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* WHY: both inert-tail rules accumulate identically — the tail's own source bytes become a `raw`
|
|
697
|
-
* segment, which the peel passes through as already-observed output (never a block, anchor, or lease).
|
|
698
|
-
*/
|
|
699
|
-
function pushRawTail(ctx: RuleContext): typeof ADVANCE {
|
|
700
|
-
const { node } = segmentOf(ctx);
|
|
701
|
-
ctx.compoundSegs.push({ kind: "raw", command: ctx.command.slice(node.pos, node.end) });
|
|
702
|
-
return ADVANCE;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function failUndecided(where: string): never {
|
|
706
|
-
throw new Error(`[151] unreachable: no rule fired ${where} — RULES must end in a total rule`);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* WHY the out-param box: callers that splice line numbers (`withSearchLineNumbers`) need the
|
|
711
|
-
* terminal search's program offset, which only exists mid-walk. Same contract as the chain.
|
|
712
|
-
*/
|
|
713
|
-
export function decide(command: string, located: LocatedBox = {}): Decision {
|
|
714
|
-
let script: ParsedScript | undefined;
|
|
715
|
-
let parseFailed = false;
|
|
716
|
-
try {
|
|
717
|
-
script = parse(command);
|
|
718
|
-
} catch {
|
|
719
|
-
parseFailed = true;
|
|
720
|
-
}
|
|
721
|
-
const ctx: RuleContext = {
|
|
722
|
-
command,
|
|
723
|
-
parseFailed,
|
|
724
|
-
script,
|
|
725
|
-
statements: parseFailed || script === undefined ? [] : (script.commands as Statement[]),
|
|
726
|
-
segments: [],
|
|
727
|
-
hasGlue: false,
|
|
728
|
-
located,
|
|
729
|
-
stmtIndex: -1,
|
|
730
|
-
segIndex: -1,
|
|
731
|
-
current: undefined,
|
|
732
|
-
view: undefined,
|
|
733
|
-
multiView: undefined,
|
|
734
|
-
searchSeg: undefined,
|
|
735
|
-
baseDir: undefined,
|
|
736
|
-
effCwd: undefined,
|
|
737
|
-
sawGlue: false,
|
|
738
|
-
compoundSegs: [],
|
|
739
|
-
tailMode: false,
|
|
740
|
-
};
|
|
741
|
-
for (const rule of RULES) {
|
|
742
|
-
if (rule.phase !== "pre") continue;
|
|
743
|
-
const out = fireRule(rule, ctx);
|
|
744
|
-
if (out !== undefined) {
|
|
745
|
-
if (out === ADVANCE) failUndecided("in pre phase");
|
|
746
|
-
return out;
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
for (let index = 0; index < ctx.statements.length; index++) {
|
|
750
|
-
ctx.stmtIndex = index;
|
|
751
|
-
for (const rule of RULES) {
|
|
752
|
-
if (rule.phase !== "statement") continue;
|
|
753
|
-
const out = fireRule(rule, ctx);
|
|
754
|
-
if (out !== undefined) {
|
|
755
|
-
if (out === ADVANCE) failUndecided("in statement phase");
|
|
756
|
-
return out;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
// WHY: structural segment push (source src/bash-classifier.ts:1112-1135 pre-extraction) —
|
|
760
|
-
// WHY: the statement guards above already declined anything unpushable, so the else arm is
|
|
761
|
-
// WHY: unreachable here (the `compound-top` rule fired for it).
|
|
762
|
-
const statement = statementOf(ctx);
|
|
763
|
-
const prefixPosition = index < ctx.statements.length - 1;
|
|
764
|
-
const top = statement.command;
|
|
765
|
-
if (top.type === "AndOr") {
|
|
766
|
-
for (const node of (top as AndOr).commands as Array<Command | Pipeline>) {
|
|
767
|
-
ctx.segments.push({ node, prefixPosition });
|
|
768
|
-
}
|
|
769
|
-
} else if (top.type === "Command" || top.type === "Pipeline") {
|
|
770
|
-
ctx.segments.push({ node: top, prefixPosition });
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
for (const rule of RULES) {
|
|
774
|
-
if (rule.phase !== "presegment") continue;
|
|
775
|
-
const out = fireRule(rule, ctx);
|
|
776
|
-
if (out !== undefined) {
|
|
777
|
-
if (out === ADVANCE) failUndecided("in presegment phase");
|
|
778
|
-
return out;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
// WHY: (commit 4) compound-aware relaxations apply only when glue is present anywhere in the
|
|
782
|
-
// WHY: chain; computed once up front exactly as the chain did (source :1144 pre-extraction).
|
|
783
|
-
ctx.hasGlue = ctx.segments.some(
|
|
784
|
-
(segment) => segment.node.type === "Command" && parseGlueSegment(segment.node) !== undefined,
|
|
785
|
-
);
|
|
786
|
-
for (let index = 0; index < ctx.segments.length; index++) {
|
|
787
|
-
ctx.segIndex = index;
|
|
788
|
-
// WHY: one classification per segment (source src/bash-classifier.ts:1155 pre-extraction).
|
|
789
|
-
ctx.current = classifySegment(segmentOf(ctx).node, segmentOf(ctx).prefixPosition);
|
|
790
|
-
let advanced = false;
|
|
791
|
-
for (const rule of RULES) {
|
|
792
|
-
if (rule.phase !== "segment") continue;
|
|
793
|
-
const out = fireRule(rule, ctx);
|
|
794
|
-
if (out === undefined) continue;
|
|
795
|
-
if (out === ADVANCE) {
|
|
796
|
-
advanced = true;
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
return out;
|
|
800
|
-
}
|
|
801
|
-
if (!advanced) failUndecided("for a walked segment");
|
|
802
|
-
}
|
|
803
|
-
for (const rule of RULES) {
|
|
804
|
-
if (rule.phase !== "post") continue;
|
|
805
|
-
const out = fireRule(rule, ctx);
|
|
806
|
-
if (out !== undefined) {
|
|
807
|
-
if (out === ADVANCE) failUndecided("in post phase");
|
|
808
|
-
return out;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return failUndecided("at the conversion tail");
|
|
812
|
-
}
|