@sema-agent/core 7.46.0 → 7.48.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/dist/core/allow-entry.d.ts +1 -1
  3. package/dist/core/allow-entry.js +4 -5
  4. package/dist/core/ask-origin.d.ts +35 -12
  5. package/dist/core/ask-origin.js +23 -6
  6. package/dist/core/checkpoint-store.d.ts +1 -1
  7. package/dist/core/engine-notice.d.ts +7 -4
  8. package/dist/core/gate-fold.js +2 -2
  9. package/dist/core/gate-lanes.js +2 -1
  10. package/dist/core/gate-outcome.d.ts +4 -2
  11. package/dist/core/gate-outcome.js +2 -2
  12. package/dist/core/mcp-server-spec.d.ts +1 -1
  13. package/dist/core/permission-rule-model.d.ts +12 -6
  14. package/dist/core/permission-rule-model.js +10 -2
  15. package/dist/core/permission-rules.d.ts +4 -5
  16. package/dist/core/permission-rules.js +28 -15
  17. package/dist/core/runner/gate-exit.d.ts +4 -1
  18. package/dist/core/runner/gate-exit.js +5 -1
  19. package/dist/core/runner/park-commit.d.ts +1 -1
  20. package/dist/core/runner/prepare-hands-readface.js +5 -5
  21. package/dist/core/runner/prepare-policy-chain.js +2 -1
  22. package/dist/core/safety-axis-vocab.d.ts +1 -1
  23. package/dist/core/safety-merge-corpus.d.ts +1 -1
  24. package/dist/core/session-policy-store.d.ts +6 -3
  25. package/dist/core/session-policy-store.js +28 -10
  26. package/dist/core/shell-wrapper-projection.js +1 -1
  27. package/dist/core/shell-wrapper-table.d.ts +8 -8
  28. package/dist/core/shell-wrapper-table.js +3 -3
  29. package/dist/core/tool-catalog-entries.js +3 -2
  30. package/dist/core/tool-face.d.ts +10 -1
  31. package/dist/core/tool-name-aliases.d.ts +9 -8
  32. package/dist/core/tool-name-aliases.js +2 -0
  33. package/dist/core/tool-name-readings.d.ts +8 -0
  34. package/dist/core/tool-name-readings.js +2 -0
  35. package/dist/core/tool-policy.d.ts +10 -9
  36. package/dist/core/tool-policy.js +6 -7
  37. package/dist/core/tool-registry.d.ts +9 -0
  38. package/dist/core/tool-registry.js +10 -0
  39. package/dist/core/write-protect.d.ts +10 -3
  40. package/dist/core/write-protect.js +11 -1
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/orchestration/workflow.js +2 -2
  44. package/dist/prompt-assembly/types.d.ts +1 -1
  45. package/dist/shell/lex.d.ts +15 -0
  46. package/dist/shell/lex.js +86 -15
  47. package/dist/tools/fs/bash-program-position.d.ts +16 -6
  48. package/dist/tools/fs/bash-program-position.js +18 -12
  49. package/dist/tools/fs/index.d.ts +3 -2
  50. package/dist/tools/fs/index.js +2 -2
  51. package/dist/tools/fs/program-read-model.d.ts +84 -12
  52. package/dist/tools/fs/program-read-model.js +105 -34
  53. package/dist/tools/fs/program-read-model.rows.js +26 -7
  54. package/dist/tools/fs/read-deny-home.d.ts +20 -1
  55. package/dist/tools/fs/read-deny-home.js +28 -7
  56. package/dist/tools/fs/read-deny.d.ts +13 -6
  57. package/dist/tools/fs/read-deny.js +12 -3
  58. package/dist/tools/fs/removal-targets.js +24 -5
  59. package/dist/tools/fs/shell-read-argv.d.ts +6 -8
  60. package/dist/tools/fs/shell-read-argv.js +5 -5
  61. package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
  62. package/dist/tools/fs/shell-read-bindings.js +190 -75
  63. package/dist/tools/fs/shell-read-establish.js +6 -7
  64. package/dist/tools/fs/shell-read-plan.d.ts +3 -15
  65. package/dist/tools/fs/shell-read-plan.js +126 -129
  66. package/dist/tools/fs/shell-read-start.d.ts +57 -0
  67. package/dist/tools/fs/shell-read-start.js +92 -0
  68. package/dist/tools/fs/shell-read-word.d.ts +68 -8
  69. package/dist/tools/fs/shell-read-word.js +106 -12
  70. package/dist/tools/fs/shell-read-write-targets.js +4 -3
  71. package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
  72. package/dist/tools/fs/shell-reentry-text.js +122 -4
  73. package/dist/tools/monitor.d.ts +3 -3
  74. package/package.json +1 -1
  75. package/test/export-surface.snapshot.json +9 -3
  76. package/wire-changes/core@7.47.0.json +412 -0
  77. package/wire-changes/core@7.48.0.json +252 -0
@@ -3,7 +3,7 @@ import { canonicalizeTarget, isAbsolutePathForm, joinForFamily, normalizeAbsPath
3
3
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "../../core/gate-bounded-await.js";
4
4
  import { thrownText } from "../../core/thrown-value.js";
5
5
  import { agentConfigDirTail } from "../../core/agent-config-dir.js";
6
- import { readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
6
+ import { isRootedReadDenyPattern, readDenyPatternRun, readDenyRunHead } from "./read-deny.js";
7
7
  export const AUXILIARY_HOME_PROBE_MS = 2_000;
8
8
  const said = new Set();
9
9
  function sayOnce(kind, line) {
@@ -111,7 +111,7 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
111
111
  const { homeAnchored, run } = readDenyPatternRun(pattern);
112
112
  if (!homeAnchored)
113
113
  continue;
114
- const head = run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/");
114
+ const head = literalHeadOf(run);
115
115
  if (head.length === 0)
116
116
  continue;
117
117
  const spelled = homes.map((home) => normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: home }), home, head)));
@@ -119,6 +119,23 @@ export async function readDenyHomeLinksOf(env, entries, homes, signal, deadline)
119
119
  }
120
120
  return await linkedLocationsOf(env, rows, signal, deadline);
121
121
  }
122
+ const ROOT = "/";
123
+ export async function readDenyRootLinksOf(env, entries, signal, deadline) {
124
+ const rows = [];
125
+ for (const { pattern, caseSensitive } of entries) {
126
+ if (!isRootedReadDenyPattern(pattern))
127
+ continue;
128
+ const head = literalHeadOf(pattern);
129
+ if (head.length === 0)
130
+ continue;
131
+ const spelled = [normalizeAbsPathLexically(joinForFamily(pathFamilyOf({ root: ROOT }), ROOT, head))];
132
+ rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: spelled });
133
+ }
134
+ return await linkedLocationsOf(env, rows, signal, deadline);
135
+ }
136
+ function literalHeadOf(run) {
137
+ return run.split("/").filter(Boolean).slice(0, readDenyRunHead(run)).join("/");
138
+ }
122
139
  export async function readDenyConfigRootLinksOf(env, entries, roots, signal, deadline) {
123
140
  return await linkedLocationsOf(env, configRootRowsOf(entries, roots), signal, deadline);
124
141
  }
@@ -129,7 +146,7 @@ function configRootRowsOf(entries, roots) {
129
146
  const tail = homeAnchored ? agentConfigDirTail(run, caseSensitive === true) : undefined;
130
147
  if (tail === undefined)
131
148
  continue;
132
- const head = tail.split("/").filter(Boolean).slice(0, readDenyRunHead(tail)).join("/");
149
+ const head = literalHeadOf(tail);
133
150
  const spelled = roots.map((root) => normalizeAbsPathLexically(head.length === 0 ? root : joinForFamily(pathFamilyOf({ root }), root, head)));
134
151
  rows.push({ pattern, caseSensitive: caseSensitive === true, spelled, anchored: [] });
135
152
  }
@@ -363,8 +380,12 @@ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoo
363
380
  const entries = compile([]).entries;
364
381
  const declared = declaredHomeOf(env);
365
382
  const relocated = configRoot === undefined ? undefined : readAuxiliaryHome(env, configRoot, { entries, configRoot: true }, signal);
383
+ const rooted = readDenyRootLinksOf(env, entries, signal);
366
384
  const heldAnywhere = async (whole) => {
367
- const linked = (await relocated)?.linked ?? new Map();
385
+ const linked = new Map();
386
+ for (const map of [(await relocated)?.linked, (await rooted).linked])
387
+ for (const [pattern, locations] of map ?? [])
388
+ linked.set(pattern, locations);
368
389
  return placementOf(carryHomeViews(linked.size === 0 ? compile([]) : compile([], linked), homeViewSourceOver(env, relocated === undefined ? [] : [relocated])), [], linked, whole);
369
390
  };
370
391
  const listed = homes === undefined ? declaredReadDenyHomesAnswered(env) : { homes, whole: true };
@@ -379,15 +400,15 @@ export async function placeReadDenyAtHome(env, compile, homes, signal, configRoo
379
400
  declaredAt = [...new Set([...own, ...ownSpellings.spellings])];
380
401
  }
381
402
  const auxiliary = readAuxiliaryHomes(env, listed.homes, { entries }, signal);
382
- const [heads, readings, root] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated]);
403
+ const [heads, readings, root, rootedHeads] = await Promise.all([readDenyHomeLinksOf(env, entries, declaredAt, signal), Promise.all(auxiliary), relocated, rooted]);
383
404
  const at = [...new Set([...declaredAt, ...readings.flatMap((r) => r.place)])];
384
405
  const linked = new Map();
385
- for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked])]) {
406
+ for (const map of [heads.linked, ...readings.map((r) => r.linked), ...(root === undefined ? [] : [root.linked]), rootedHeads.linked]) {
386
407
  for (const [pattern, locations] of map)
387
408
  linked.set(pattern, [...new Set([...(linked.get(pattern) ?? []), ...locations])]);
388
409
  }
389
410
  const matcher = linked.size === 0 ? compile(at) : compile(at, linked);
390
- return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole);
411
+ return placementOf(carryHomeViews(matcher, homeViewSourceOver(env, relocated === undefined ? auxiliary : [...auxiliary, relocated])), at, linked, heads.whole && rootedHeads.whole);
391
412
  }
392
413
  export function readDenyPlacedLater(unplaced, place) {
393
414
  let current = unplaced;
@@ -5,7 +5,10 @@
5
5
  * unless the pattern opens with the HOME word ({@link READ_DENY_HOME_WORD}): `~/<run>` matches only where
6
6
  * the run starts right below the execution environment's home directory (`~/.sema/settings.json` is the
7
7
  * home's file, never a project's `.sema/settings.json`). Only a FIRST segment that is exactly `~` is the
8
- * word; `~user` and a `~` further along are literal segments.
8
+ * word; `~user` and a `~` further along are literal segments. A pattern that opens with `/` (and is not home-anchored)
9
+ * is ROOTED, the same kind of anchor with the filesystem root as its place: `/app/config-lkg.json` names the file
10
+ * at exactly that location, never a project's `app/config-lkg.json` — the run must start at the first segment of an
11
+ * absolute path (a path that is not absolute is not placed, and the row does not judge it).
9
12
  * String shorthand ≡ `{ pattern }`. Matching is ASCII-case-insensitive by default (a case-insensitive
10
13
  * filesystem serves `.SSH` and `.ssh` as the same directory); `caseSensitive: true` opts a single
11
14
  * entry out. Unicode normalization (NFC/NFD) and volume-level case semantics are NOT modeled (v1
@@ -384,8 +387,9 @@ export interface CompiledSegmentPattern {
384
387
  /** The segment lists of the directories the run is ANCHORED at (folded as the pattern folds): the run must start
385
388
  * right after one of them. Absent = the run matches anywhere. */
386
389
  anchors?: readonly (readonly string[])[];
387
- /** Judged in a ROOTED view only: an extra location of a home-anchored row (where its literal head leads). A view that
388
- * is not rooted cannot be placed and is covered by the row's own run, held anywhere. */
390
+ /** Judged in a ROOTED view only: an extra location of a home-anchored row (where its literal head leads), or a row that is
391
+ * itself rooted (anchored at the filesystem root, {@link ReadDenyEntry}). A view that is not rooted cannot be placed: a
392
+ * home-anchored row's own run is held anywhere in it (the row cannot be placed, so it holds wider); these two judge it not. */
389
393
  rootedOnly?: true;
390
394
  }
391
395
  /**
@@ -408,6 +412,9 @@ export declare function compileSegmentPattern(raw: string, fold: "none" | "ascii
408
412
  export declare function matchSegmentPatterns(path: string, compiled: readonly CompiledSegmentPattern[], opts?: {
409
413
  readonly aliasResolved?: boolean;
410
414
  }): string | null;
415
+ /** Is a canonical pattern ROOTED — placed at the filesystem root ({@link ReadDenyEntry})? Only {@link canonicalReadDenyPattern}
416
+ * puts a leading `/` on a canonical pattern, so the text is the whole answer. */
417
+ export declare function isRootedReadDenyPattern(canonical: string): boolean;
411
418
  /**
412
419
  * the PURE validation/canonicality predicate over a PERSISTED deny entry (checkpoint v9
413
420
  * face section), for the resume pre-CAS ladder: it must refuse BEFORE the approval CAS everything
@@ -437,9 +444,9 @@ export declare function checkedReadDenyHomes(homes: unknown, seat?: string): rea
437
444
  * spellings only — any other shape refuses loudly, naming `readDenyHomes`. EMPTY = the environment declares no
438
445
  * home: a home-anchored entry then matches its run anywhere — a row that cannot be placed holds wider, never narrower.
439
446
  *
440
- * `linked` — where a home-anchored row's LITERAL HEAD ({@link readDenyRunHead}) really leads, keyed by the row's
441
- * pattern: the canonical locations a mount resolved (`readDenyHomeLinksOf`, read-deny-home.ts) where one of the
442
- * head's segments is a link (`~/.sema` linked into a dotfiles directory). The row is placed at each location too —
447
+ * `linked` — where a home-anchored or a rooted row's LITERAL HEAD ({@link readDenyRunHead}) really leads, keyed by the row's
448
+ * pattern: the canonical locations a mount resolved (`readDenyHomeLinksOf` / `readDenyRootLinksOf`, read-deny-home.ts) where
449
+ * one of the head's segments is a link (`~/.sema` linked into a dotfiles directory; `/app` a link to `/data/app`). The row is placed at each location too —
443
450
  * the same row, the same name in every refusal — with the rest of its run matched below it. The read faces judge
444
451
  * canonical keys, so every spelling that reaches the linked file meets the row there. Absolute locations only.
445
452
  */
@@ -418,7 +418,14 @@ function normalizeEntry(entry, layer) {
418
418
  throw new Error(`readDenyPatterns (${layer}): pattern ${JSON.stringify(shape.pattern)} contains no path segments${homeAnchored ? " below the home word" : ""} and would ${homeAnchored ? "name the whole home directory" : "deny nothing"}. ` +
419
419
  `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".config/gcloud", "id_rsa*", "~/.sema/settings.json"); remove the entry or spell the segments.`);
420
420
  }
421
- return { pattern: segments.join("/"), caseSensitive: shape.caseSensitive === true };
421
+ return { pattern: canonicalReadDenyPattern(shape.pattern), caseSensitive: shape.caseSensitive === true };
422
+ }
423
+ function canonicalReadDenyPattern(pattern) {
424
+ const joined = pattern.split("/").filter(Boolean).join("/");
425
+ return pattern.startsWith("/") && !readDenyPatternRun(joined).homeAnchored ? `/${joined}` : joined;
426
+ }
427
+ export function isRootedReadDenyPattern(canonical) {
428
+ return canonical.startsWith("/");
422
429
  }
423
430
  export function persistedReadDenyEntryProblem(e) {
424
431
  if (typeof e !== "object" || e === null)
@@ -434,7 +441,7 @@ export function persistedReadDenyEntryProblem(e) {
434
441
  const segments = pattern.split("/").filter(Boolean);
435
442
  if (readDenyPatternRun(segments.join("/")).run.length === 0)
436
443
  return "pattern has no path segments";
437
- if (segments.join("/") !== pattern)
444
+ if (canonicalReadDenyPattern(pattern) !== pattern)
438
445
  return "pattern is not in canonical a/b form";
439
446
  return null;
440
447
  }
@@ -473,6 +480,8 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
473
480
  const c = compileSegmentPattern(run, fold);
474
481
  if (c === null)
475
482
  throw new Error(`readDenyPatterns: pattern ${JSON.stringify(e.pattern)} compiled to zero segments.`);
483
+ if (isRootedReadDenyPattern(e.pattern))
484
+ return { ...c, raw: e.pattern, anchors: [[]], rootedOnly: true };
476
485
  if (!homeAnchored || homeSegments.length === 0)
477
486
  return { ...c, raw: e.pattern };
478
487
  return { ...c, raw: e.pattern, anchors: homeSegments.map((segs) => (fold === "ascii" ? segs.map(asciiLower) : segs)) };
@@ -483,7 +492,7 @@ export function compileReadDeny(additions = [], layer = "additions", builtin, ru
483
492
  normalized.forEach((e, i) => {
484
493
  const locations = linked.get(e.pattern);
485
494
  const { homeAnchored, run } = readDenyPatternRun(e.pattern);
486
- if (locations === undefined || !homeAnchored)
495
+ if (locations === undefined || !(homeAnchored || isRootedReadDenyPattern(e.pattern)))
487
496
  return;
488
497
  const c = compiled[i];
489
498
  const rest = c.segments.slice(readDenyRunHead(run));
@@ -2,7 +2,8 @@ import { planShellReads } from "./shell-read-plan.js";
2
2
  import { literalTextOf } from "../../shell/lex.js";
3
3
  import { createShellReadEstablisher, establishShellReadsLexically } from "./shell-read-establish.js";
4
4
  import { isAbsolutePathForm, isWinFormPath, normalizeAbsPathLexically } from "./safety.js";
5
- import { reentryText } from "./shell-reentry-text.js";
5
+ import { launchedCommands, launchedReadings, reentryText, scriptFileAt, spellLaunched } from "./shell-reentry-text.js";
6
+ import { isShellSyntaxWord, LOOP_LIST_HEADS, readProgramPosition } from "./bash-program-position.js";
6
7
  import { bashClassifyDefaultAllow } from "./bash-readonly-classifier.js";
7
8
  import { programArgs } from "./shell-read-write-targets.js";
8
9
  import { implementationsOf, programRow, runsShellText, selectedOptions, writeReadings } from "./program-read-model.js";
@@ -67,7 +68,7 @@ function bindingsOf(plan) {
67
68
  if (w.assignment !== undefined)
68
69
  bind(w.assignment.name, w.assignment.settlesWholeName ? literalTextOf(w.assignment.value.pieces) : undefined);
69
70
  const [head, name] = segment.argv;
70
- if ((head?.folded === "for" || head?.folded === "select") && name !== undefined && NAME.test(name.folded)) {
71
+ if (isShellSyntaxWord(head, LOOP_LIST_HEADS) && name !== undefined && NAME.test(name.folded)) {
71
72
  const at = segment.argv.findIndex((w, i) => i > 1 && w.folded === "in");
72
73
  const words = at < 0 ? [undefined] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces));
73
74
  for (const v of words.length > 0 ? words : [undefined])
@@ -167,10 +168,28 @@ function runtimeScriptOf(segment) {
167
168
  const program = segment.verb;
168
169
  if (segment.reentry !== "shell" || program === undefined || !runsShellText(program.basename))
169
170
  return undefined;
170
- const at = segment.argv.findIndex((w) => w.folded === program.name);
171
171
  const MARK = "\u0000";
172
- const words = at < 0 ? [] : segment.argv.slice(at + 1).map((w) => literalTextOf(w.pieces) ?? `${MARK}${w.raw}`);
173
- const texts = [reentryText(program.basename, words), ...(segment.stdin ?? []).map((s) => reentryText(program.basename, words, s.literal ?? `${MARK}${s.from}`))];
172
+ const spell = (k, from = 0) => {
173
+ const w = segment.argv[k];
174
+ const literal = literalTextOf(w.pieces);
175
+ return literal !== undefined ? literal.slice(from) : `${MARK}${w.raw.slice(from)}`;
176
+ };
177
+ const pos = readProgramPosition(segment.argv);
178
+ if (pos.program !== undefined && pos.refusal === undefined) {
179
+ const argv = segment.argv.slice(pos.program.index + 1);
180
+ const file = scriptFileAt(program.basename, argv.map((w) => w.folded));
181
+ const source = file === undefined ? undefined : argv[file];
182
+ if (source !== undefined && source.pieces.length === 1 && source.pieces[0].expansion?.kind === "process")
183
+ return source.raw;
184
+ if (pos.argvFromStdin)
185
+ return segment.text.trim();
186
+ }
187
+ const named = pos.program !== undefined && pos.refusal === undefined ? [{ shell: program.basename, words: segment.argv.slice(pos.program.index + 1).map((_, k) => spell(pos.program.index + 1 + k)) }] : [];
188
+ const launched = launchedCommands(segment.argv, pos).flatMap((c) => {
189
+ const shell = launchedReadings(segment.argv, c).shell;
190
+ return shell === undefined ? [] : spellLaunched(segment.argv, c.args, { word: spell }).map((words) => ({ shell, words }));
191
+ });
192
+ const texts = [...named, ...launched].flatMap(({ shell, words }) => [reentryText(shell, words), ...(segment.stdin ?? []).map((s) => reentryText(shell, words, s.literal ?? `${MARK}${s.from}`))]);
174
193
  const text = texts.find((t) => t?.includes(MARK) === true);
175
194
  return text === undefined ? undefined : text.split(MARK).join("");
176
195
  }
@@ -7,14 +7,13 @@
7
7
  */
8
8
  import { type LexedWord } from "../../shell/lex.js";
9
9
  import { type Read } from "./shell-read-word.js";
10
- import { type BindingTable, type BoundValue } from "./shell-read-bindings.js";
10
+ import { type BindingTable, type FloorValue } from "./shell-read-bindings.js";
11
11
  import { subcommandOf, type ArgRole, type ProgramRow } from "./program-read-model.js";
12
12
  import type { ReadCandidate, ReadCandidateProvenance } from "./shell-read-plan.js";
13
- /** A candidate before the threading fills its bases. `alternatives`: the other spellings its word may expand to (a name
14
- * bound to several values) — one candidate each once the segment's reads are read. */
13
+ /** A candidate before the threading fills its bases. `floors`: the spellings its word's names left for the deny judge
14
+ * alone (a word whose names are unknown here). */
15
15
  export type Bare = Omit<ReadCandidate, "bases"> & {
16
- readonly alternatives?: readonly string[];
17
- readonly floors?: readonly BoundValue[];
16
+ readonly floors?: readonly FloorValue[];
18
17
  };
19
18
  /** What reading a segment's words found beside its candidates. */
20
19
  export interface WordContext {
@@ -35,9 +34,8 @@ export declare function pathSubcommandOf(row: ProgramRow | undefined, args: read
35
34
  /** The sentence of inline program text the station does not read. */
36
35
  export declare const programTextSentence: (language: string) => string;
37
36
  /** One candidate without its bases (filled by the threading pass): the word's spelling — or `spelled`, a caller's
38
- * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. A word whose
39
- * names may hold several values offers each as its whole spelling; a PART of such a word is not followed through them
40
- * (it is unresolvable). */
37
+ * reading of PART of it (an option payload, a `key=value` value) — with the word's own expansion facts. The word is read
38
+ * in one reading of the segment's names (`segmentReadingsOf`): its names hold one value each, or are unknown. */
41
39
  export declare function bare(r: Read, provenance: ReadCandidateProvenance, spelled?: string): Bare;
42
40
  /** The reads one value of a declared role hands over: a candidate per file it names, or — a value the program expands
43
41
  * past the listed bound — the word itself, unresolvable. A mount source is a read the daemon makes of the whole tree
@@ -1,5 +1,5 @@
1
1
  import { literalTildeSpelling } from "../../shell/lex.js";
2
- import { expandsOf, readOfText, rewritten } from "./shell-read-word.js";
2
+ import { expandsOf, partPattern, readOfText, rewritten } from "./shell-read-word.js";
3
3
  import { arithmeticSubstitutions } from "./shell-read-bindings.js";
4
4
  import { argvProgramOf, attachedOptionPayloads, isPathShapedToken } from "./bash-readonly-classifier.js";
5
5
  import { assignmentArgumentsAreNotPaths, evaluatesOperands, messageWords, moduleTailOf, objectPathsOf, opensNoOperand, programRow, programTextMayOpen, programTextOpensOf, readArgs, selectsContentsTraversal, slotValueCandidates, subcommandOf, valuesAreLiteral, walksCwdWithoutOperand, wordsReadBySomeImplementations, } from "./program-read-model.js";
@@ -10,14 +10,13 @@ export function pathSubcommandOf(row, args) {
10
10
  export const programTextSentence = (language) => `runs inline ${language} program text, which may open files this station does not read`;
11
11
  export function bare(r, provenance, spelled = r.folded) {
12
12
  const whole = spelled === r.folded;
13
- if (!whole && r.values !== undefined)
14
- return { spelled, raw: r.raw, provenance, expands: "unresolvable" };
13
+ const pattern = whole ? r.pattern : partPattern(r, spelled);
15
14
  return {
16
15
  spelled,
17
16
  raw: r.raw,
18
17
  provenance,
19
18
  expands: r.expands,
20
- ...(whole && r.values !== undefined ? { alternatives: r.values.slice(1) } : {}),
19
+ ...(pattern !== undefined ? { pattern } : {}),
21
20
  ...(whole && r.homeFloor !== undefined ? { floorSpelled: r.homeFloor } : {}),
22
21
  ...(whole && r.floorValues !== undefined ? { floors: r.floorValues } : {}),
23
22
  };
@@ -62,8 +61,9 @@ export function pushAssignment(reads, w, provenance) {
62
61
  if (!settled && unsettled !== undefined)
63
62
  reads.push(unsettled);
64
63
  const substitutes = a.value.expansions.some((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic");
64
+ const pattern = named === undefined ? undefined : partPattern({ folded: w.folded, ...(w.judged !== undefined ? { pattern: w.judged } : {}) }, named);
65
65
  if (named !== undefined && !substitutes)
66
- reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w) });
66
+ reads.push({ spelled: named, raw: w.raw, provenance, expands: expandsOf(w), ...(pattern !== undefined ? { pattern } : {}) });
67
67
  if (a.value.folded.includes(":"))
68
68
  for (const m of a.value.folded.split(":"))
69
69
  if (m !== "") {
@@ -26,12 +26,46 @@
26
26
  */
27
27
  import { type LexedRedirect, type LexedSegment, type LexedToken, type LexedWord } from "../../shell/lex.js";
28
28
  import { type DeclaredAttribute } from "./program-read-model.js";
29
- /** A value a name may hold: the text it holds, and whether that text is a PATTERN whose matches are the values (a `for`
30
- * list word the shell globs before the loop binds it). */
31
- export interface BoundValue {
29
+ /** A value a name may hold — the text the program receives ({@link spelled}: what a shell re-parsing it reads), literal
30
+ * throughout, or a PATTERN whose matches are the values (a `for` list word the shell globs before the loop binds it;
31
+ * `spelled` is then the pattern as the program receives it where it matches nothing): `glob`, with its `parts` — the
32
+ * value in runs: a literal run (quoted or literal text, a settled value) and a pattern run (glob pieces as written,
33
+ * quoting kept, and as a reader judges them — {@link WordExpansion.pattern}). The one producer is the piece walk
34
+ * ({@link valueOf}). */
35
+ export type BoundValue = {
36
+ readonly spelled: string;
37
+ readonly glob?: never;
38
+ readonly parts?: never;
39
+ readonly fields?: readonly string[];
40
+ } | {
41
+ readonly spelled: string;
42
+ readonly glob: true;
43
+ readonly parts: readonly ValuePart[];
44
+ readonly fields?: never;
45
+ };
46
+ /** One run of a pattern {@link BoundValue}: literal text, or a pattern — the shell expands its characters — `text` as
47
+ * written (what a shell re-reading it matches by), `judged` as a reader judges the names it matches. */
48
+ export type ValuePart = {
49
+ readonly text: string;
50
+ readonly pattern: false;
51
+ } | {
52
+ readonly text: string;
53
+ readonly pattern: true;
54
+ readonly judged: string;
55
+ };
56
+ /** A spelling the deny judge alone reads ({@link Binding.floor}, {@link wordFloorValues}): a value, or a word as written
57
+ * where its names are unknown — nothing re-reads it, so a pattern says no more than that it is one. */
58
+ export interface FloorValue {
32
59
  readonly spelled: string;
33
60
  readonly glob?: true;
61
+ /** A pattern's spelling as a reader judges the names it matches, where it is not `spelled` ({@link judgedSpellingOf}). */
62
+ readonly pattern?: string;
34
63
  }
64
+ /** A pattern value's names as a reader judges them, where that is not its spelling: its runs, a class the judge widens
65
+ * as `?` (the lexer's `WordExpansion.pattern.judged`), a leading `~` the literal tilde it is. */
66
+ export declare function judgedSpellingOf(v: BoundValue): string | undefined;
67
+ /** A value as a deny-floor spelling, its judged spelling with it. */
68
+ export declare function floorOf(v: BoundValue): FloorValue;
35
69
  /** What a name holds at one point of a text: its possible values, or `"unknown"`. */
36
70
  type NameState = readonly BoundValue[] | "unknown";
37
71
  /**
@@ -58,7 +92,7 @@ export interface Binding {
58
92
  readonly scope?: "local" | "unread";
59
93
  /** Spellings the name may hold that the deny judge still reads where the name is otherwise unknown (an array's
60
94
  * elements: what `${A[k]}` expands to is one of them). */
61
- readonly floor?: readonly BoundValue[];
95
+ readonly floor?: readonly FloorValue[];
62
96
  }
63
97
  /** An attribute a declaration gives a name — or every name (`name` absent: the declaration computed it): a later
64
98
  * assignment to it is evaluated as arithmetic (`integer`), stores a case change or an array element (`transformed`), or
@@ -104,27 +138,40 @@ export declare function projectSegment(segment: LexedSegment): ProjectedSegment;
104
138
  * token, a head whose body opens in a later segment). */
105
139
  export declare function dropDefinitionHeads(tokens: LexedToken[]): readonly number[];
106
140
  /**
107
- * What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`):
108
- * · `undefined` — it refers to no name (it reads as written);
109
- * · its values — every combination of the values its names may hold here (a piece the shell globs keeps its written
110
- * pattern, and the value is then a pattern);
141
+ * What a word holds through the names it refers to — `$NAME`, `${NAME}`, a leading `~` (the value of `HOME`): `undefined`
142
+ * where it refers to no name (it reads as written), else {@link valueOf}. `table.stateOf` is read after `overlay` (the
143
+ * names the same segment bound earlier).
144
+ */
145
+ export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): readonly BoundValue[] | "unknown" | undefined;
146
+ /** The names a word's expansions read as values — a bare `$NAME` / `${NAME}`, and one written inside a bracket the shell
147
+ * forms its class from after expanding it ({@link valueOf}) — in the order they stand. */
148
+ export declare function namesIn(word: Pick<LexedWord, "pieces">): string[];
149
+ /**
150
+ * What a VALUE word holds (an assignment's value, a `for` list word, a word handed to a program) — THE piece walk, the one
151
+ * producer of {@link BoundValue}s: every combination of the values its pieces take, each field's pattern formed once the
152
+ * whole field has expanded ({@link formedField}) — a literal piece its text, a glob piece its pattern as written (a bracket
153
+ * holding an expansion its brackets and its body, the body's names substituted in place), `~` and `$NAME` the values the
154
+ * table gives:
155
+ * · its values — a field the shell globs makes the value a pattern;
111
156
  * · `"unknown"` — a name it refers to is not settled here, it carries an expansion no name answers (a modifier, a
112
- * substitution, a brace list, `~user`), an UNQUOTED expansion's value is not one word or `IFS` may have changed
113
- * (`quoted` = the whole word is read as one value: an assignment's value, which is never split), or it would list
114
- * more than {@link WORD_VALUES_MAX} values.
115
- * `table.stateOf` is read after `overlay` (the names the same segment bound earlier).
157
+ * substitution, a brace list, `~user`), an UNQUOTED expansion's value is no {@link READABLE_VALUE} (or holds
158
+ * whitespace outside `split`), `IFS` may have changed (`quoted` = the whole word is read as one value: an assignment's
159
+ * value, which is never split), a field is not formed or a split one forms a pattern, or it would list more than
160
+ * {@link WORD_VALUES_MAX} values.
161
+ * `split` = the word is read as the ARGUMENTS it becomes: an unquoted value is split into its words on the default `IFS`
162
+ * (a value then carries its {@link BoundValue} `fields`).
116
163
  */
117
- export declare function wordValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): readonly BoundValue[] | "unknown" | undefined;
164
+ export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>, split?: boolean): NameState;
118
165
  /**
119
166
  * The spellings a word the table leaves unknown may still take through the deny floors of the names it refers to
120
167
  * ({@link BindingTable.floorOf}) — `${A[k]}`, `${A[@]}`, `$A` reading an array's elements — or through their values where
121
168
  * known: for the deny judge alone. Empty where some name it refers to leaves neither.
122
169
  */
123
- export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly BoundValue[];
124
- /** What a VALUE word holds (an assignment's value, a `for` list word): {@link wordValues}, or — a word referring to no
125
- * name — its own text (a word the shell globs is a pattern; one carrying any other expansion holds what the text does
126
- * not say). */
127
- export declare function valueOf(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined, quoted: boolean, overlay?: ReadonlyMap<string, NameState>): NameState;
170
+ export declare function wordFloorValues(word: Pick<LexedWord, "pieces">, table: BindingTable | undefined): readonly FloorValue[];
171
+ /** Where a segment's argv heads a `for` / `select` loop: the index of that bare word, at the segment's start or past the
172
+ * bare control words in front of it (`do for x in …`, `then for …` — a loop nested in a body or a branch); `undefined`
173
+ * for any other segment. */
174
+ export declare function loopListHeadAt(argv: readonly LexedWord[]): number | undefined;
128
175
  /**
129
176
  * What ONE segment does to the names (see the module header). `argv` = the segment's argv projection (structure
130
177
  * dropped); `readValue` folds a value word against the table (a reader that answers `unknown` gives the names alone).
@@ -153,6 +200,9 @@ export type NameReader = (name: string) => NameState;
153
200
  export interface InheritedNames {
154
201
  readonly attributes: readonly NameAttribute[];
155
202
  readonly references: boolean;
203
+ /** The deny floors a name starts the text with ({@link BindingTable.floorOf}): what the words handing it this text as
204
+ * written may name there — or, for a substitution body, the floors its segment reads. */
205
+ readonly floors?: ReadonlyMap<string, readonly FloorValue[]>;
156
206
  }
157
207
  /** What the thread answers for one reader: what a name may hold, and whether a name carries the integer attribute. */
158
208
  export interface BindingTable {
@@ -164,7 +214,7 @@ export interface BindingTable {
164
214
  /** `HOME` may hold something other than the declared home here. */
165
215
  readonly homeTouched: boolean;
166
216
  /** Every spelling any binding of this name before here left for the deny judge alone ({@link Binding.floor}). */
167
- floorOf(name: string): readonly BoundValue[];
217
+ floorOf(name: string): readonly FloorValue[];
168
218
  /** What a substitution body here starts with. */
169
219
  readonly inherited: InheritedNames;
170
220
  }
@@ -208,6 +258,8 @@ export declare class BindingThread {
208
258
  private readonly blocks;
209
259
  /** Segments that are a pipeline member or run in the background. */
210
260
  private readonly detached;
261
+ /** The floors the names start the text with ({@link InheritedNames.floors}). */
262
+ private readonly startFloors;
211
263
  constructor(segments: readonly LexedSegment[],
212
264
  /** What each segment binds, its names alone (a value is the reader's to read). */
213
265
  named: readonly BindingEffect[],