@gotgenes/pi-permission-system 30.2.0 → 31.0.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [31.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v30.2.0...pi-permission-system-v31.0.0) (2026-09-02)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **pi-permission-system:** **breaking:** gate a path named as a for or select loop operand ([6189d81](https://github.com/gotgenes/pi-packages/commit/6189d81dc48a26d572bacb91a1b2e7c062634ad9)), closes [#839](https://github.com/gotgenes/pi-packages/issues/839)
14
+ * **pi-permission-system:** **breaking:** gate a path named as a case subject ([dfd2f8d](https://github.com/gotgenes/pi-packages/commit/dfd2f8d682c39dcfe6a903cdeadde18b08a7cc60)), closes [#839](https://github.com/gotgenes/pi-packages/issues/839)
15
+
16
+ ### Documentation
17
+
18
+ * **pi-permission-system:** commit the instrument behind the statement-operand measurement ([6a708b7](https://github.com/gotgenes/pi-packages/commit/6a708b716f038bf192875a0166ae6a6ab012c3d8)), closes [#839](https://github.com/gotgenes/pi-packages/issues/839)
19
+ * **pi-permission-system:** register the statement-operand measurement script ([e3e8799](https://github.com/gotgenes/pi-packages/commit/e3e879935c6f395d4b2387a6e30e0791bb8fb765)), closes [#839](https://github.com/gotgenes/pi-packages/issues/839)
20
+ * **pi-permission-system:** correct the drift note in the operand measurement ([e300934](https://github.com/gotgenes/pi-packages/commit/e300934c0aad34b3bfd127695219250ca6f4613f)), closes [#839](https://github.com/gotgenes/pi-packages/issues/839)
21
+
8
22
  ## [30.2.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v30.1.0...pi-permission-system-v30.2.0) (2026-09-02)
9
23
 
10
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "30.2.0",
3
+ "version": "31.0.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -91,6 +91,7 @@
91
91
  "verify:public-types": "bash scripts/verify-public-types.sh",
92
92
  "measure:core-coverage": "node scripts/measure-core-coverage.mjs",
93
93
  "measure:statement-descent": "node scripts/measure-statement-descent.mjs",
94
+ "measure:statement-operands": "node scripts/measure-statement-operands.mjs",
94
95
  "measure:wrapper-transparency": "node scripts/measure-wrapper-transparency.mjs",
95
96
  "lint:md": "rumdl check *.md docs/**/*.md",
96
97
  "lint": "biome check . && eslint . && pnpm run lint:md"
@@ -11,7 +11,7 @@ import {
11
11
  } from "#src/access-intent/bash/node-text";
12
12
  import type { TSNode } from "#src/access-intent/bash/parser";
13
13
  import { redirectEffectForDestination } from "#src/access-intent/bash/redirect-analysis";
14
- import type { TokenEffect } from "#src/access-intent/effect";
14
+ import { type TokenEffect, UNPROVEN_EFFECT } from "#src/access-intent/effect";
15
15
 
16
16
  /**
17
17
  * A collected path-candidate token paired with the effect its position proved.
@@ -29,7 +29,8 @@ export interface PathToken {
29
29
 
30
30
  /**
31
31
  * Recursively visit the AST and collect resolved text of nodes that
32
- * represent command arguments or redirect destinations.
32
+ * represent command arguments, redirect destinations, or a statement's own
33
+ * path operands.
33
34
  *
34
35
  * Reads no text from `heredoc_body`, `heredoc_end`, or `comment` subtrees, but
35
36
  * still descends an execution host for the commands it hosts — an interpolating
@@ -46,6 +47,12 @@ export interface PathToken {
46
47
  export function collectPathCandidateTokens(node: TSNode): PathToken[] {
47
48
  if (node.type === "command") return collectCommandTokens(node);
48
49
  if (node.type === "file_redirect") return collectRedirectTokens(node);
50
+ if (node.type === "for_statement") {
51
+ return collectStatementOperandTokens(node, "after-in");
52
+ }
53
+ if (node.type === "case_statement") {
54
+ return collectStatementOperandTokens(node, "before-in");
55
+ }
49
56
  if (EXECUTION_HOST_TYPES.has(node.type)) {
50
57
  return collectHostedExecutionTokens(node);
51
58
  }
@@ -139,6 +146,70 @@ function collectHostedExecutionTokens(node: TSNode): PathToken[] {
139
146
  return tokens;
140
147
  }
141
148
 
149
+ /**
150
+ * Which side of a statement's `in` keyword carries its path operands.
151
+ *
152
+ * A `for`/`select` word list follows `in`; a `case` subject precedes it.
153
+ */
154
+ type OperandSide = "before-in" | "after-in";
155
+
156
+ /**
157
+ * Collect the tokens of a statement that names its own path operands, rather
158
+ * than reaching them through a command.
159
+ *
160
+ * A path in a `for`/`select` word list or a `case` subject is a child of the
161
+ * statement node, so the command and redirect collectors never see it and the
162
+ * loop body cannot recover it — `for f in /etc/shadow; do cat $f; done` carries
163
+ * the literal only here, and ADR 0009 declines to resolve the body's `$f`
164
+ * (#839).
165
+ *
166
+ * The two statements ask one question with one parameter — which side of the
167
+ * anonymous `in` keyword is the operand side — so the walk is named here once
168
+ * rather than spelled twice, as `COMMAND_PREFIX_TYPES` is for the two command
169
+ * walkers.
170
+ *
171
+ * Three properties carry the design:
172
+ *
173
+ * 1. A non-operand child falls through to the ordinary recursion, not to
174
+ * nothing. That is what keeps the `do_group` reaching the loop body's
175
+ * commands; searching it for hosted executions alone would silently drop
176
+ * every ordinary body command.
177
+ * 2. An operand-side child outside {@link ARG_NODE_TYPES} falls through the
178
+ * same way, so a bare substitution in the word list is descended for its
179
+ * command as before and its operands keep that command's own attribution
180
+ * (#807) instead of the statement's.
181
+ * 3. An operand-side argument node is read *and* searched for hosted
182
+ * executions, since a `concatenation` can be both — the pairing
183
+ * {@link collectRedirectTokens} already performs on a destination.
184
+ *
185
+ * The token carries {@link UNPROVEN_EFFECT}: no command word owns it and no
186
+ * redirect operator names it, so neither proof source can speak and the gates
187
+ * consult both directional surfaces.
188
+ */
189
+ function collectStatementOperandTokens(
190
+ node: TSNode,
191
+ operandSide: OperandSide,
192
+ ): PathToken[] {
193
+ const tokens: PathToken[] = [];
194
+ let seenIn = false;
195
+ for (let i = 0; i < node.childCount; i++) {
196
+ const child = node.child(i);
197
+ if (!child) continue;
198
+ if (!child.isNamed) {
199
+ if (child.type === "in") seenIn = true;
200
+ continue;
201
+ }
202
+ const side: OperandSide = seenIn ? "after-in" : "before-in";
203
+ if (side !== operandSide || !ARG_NODE_TYPES.has(child.type)) {
204
+ tokens.push(...collectPathCandidateTokens(child));
205
+ continue;
206
+ }
207
+ tokens.push({ token: resolveNodeText(child), effect: UNPROVEN_EFFECT });
208
+ tokens.push(...collectHostedExecutionTokens(child));
209
+ }
210
+ return tokens;
211
+ }
212
+
142
213
  /**
143
214
  * Extract the command name from a `command` node.
144
215
  * Returns the basename (e.g. `/usr/bin/sed` → `sed`), or undefined