mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
366 lines
23 KiB
Markdown
366 lines
23 KiB
Markdown
---
|
||
issue: 478
|
||
issue_title: "pi-permission-system: narrow ScopedPermissionResolver to a single resolve(intent) (Phase 6 Step 6)"
|
||
---
|
||
|
||
# Narrow `ScopedPermissionResolver` to a single `resolve(intent)`
|
||
|
||
## Release Recommendation
|
||
|
||
**Release:** ship independently
|
||
|
||
This is Phase 6 Step 6, tagged `Release: independent` in the architecture roadmap (it is not a member of either Phase 6 batch).
|
||
It is a self-contained, behavior-preserving structural refactor of the resolver/manager surface, so it ships on its own.
|
||
|
||
## Problem Statement
|
||
|
||
The permission-resolution surface widens once per gate.
|
||
Gates today call either `resolver.resolve(surface, input, agentName)` (tool-shaped) or `resolver.resolvePathPolicy(values, agentName, surface)` (precomputed path values), and the underlying `ScopedPermissionManager` mirrors that split with `checkPermission` + `checkPathPolicy`.
|
||
Adding a gate can widen this surface again, and — because the surface is a method pair rather than a single entry — a test fixture can stub one method and forget the other.
|
||
That is the structural cause of the [#393] false-green: a stubbed-but-unrouted `checkPathPolicy` silently returned its default (`allow`), a bug invisible in the edited test file and caught only by the full suite.
|
||
|
||
The fix is to make gates *emit* a minimal access intent that one `resolve(intent)` answers.
|
||
Adding a gate then cannot widen the resolver surface, and — by collapsing the manager to a single resolution method — the false-green class becomes structurally impossible (there is no second method to forget).
|
||
|
||
## Goals
|
||
|
||
- Introduce a minimal `AccessIntent` value that each gate emits (surface + value-or-`AccessPath` + `agentName`), carrying no principal identity.
|
||
- Collapse `ScopedPermissionResolver.resolve` + `resolvePathPolicy` into one `resolve(intent)`.
|
||
- Collapse `ScopedPermissionManager.checkPermission` + `checkPathPolicy` into one `check(intent, sessionRules?)`, migrating every raw (non-gate) caller onto it.
|
||
- Preserve all current behavior — this is a structural refactor, not a behavior change.
|
||
- Let `AccessPath` flow into the resolver as a first-class intent variant (the resolver, not the gate, asks it for `matchValues()`), seeding the `AccessPath`-as-universal-representation direction ([#487]).
|
||
|
||
## Non-Goals
|
||
|
||
- No change to what either path surface matches against.
|
||
The `path` surface stays lexical-only; `external_directory` stays lexical ∪ canonical (the [#418] set).
|
||
Whether `path` should also match the canonical form is tracked separately in [#486].
|
||
- No migration of bash-path's `path-values` intent onto `AccessPath` — that depends on [#486] and is part of the broader [#487] direction.
|
||
- No principal identity on `AccessIntent`; cross-session path portability stays deferred ([#309] tracks the related advisory-path unification).
|
||
- No change to the resolver's query methods (`getToolPermission`, `getConfigIssues`) or to the resolver's raw `checkPermission` (the no-session-rules path the skill-input gate depends on via `SkillInputGateInputs`).
|
||
- No change to `configuration.md` — there is no user-facing behavior or config change.
|
||
|
||
## Background
|
||
|
||
Relevant modules and how they relate:
|
||
|
||
- `src/permission-resolver.ts` — the `ScopedPermissionResolver` interface (`resolve` + `resolvePathPolicy`) and the `PermissionResolver` class.
|
||
The class composes a `ScopedPermissionManager` with a `SessionRules` store so gates never thread the session ruleset by hand (the [#319] / [#340] seam).
|
||
The class also exposes `checkPermission` (raw, no session rules), `getToolPermission`, and `getConfigIssues` — these are **not** on the narrow interface and are out of scope.
|
||
- `src/permission-manager.ts` — `ScopedPermissionManager` interface + `PermissionManager` class.
|
||
`checkPermission(toolName, input, agentName?, sessionRules?)` runs `normalizeInput` (needs `getConfiguredMcpServerNames()` + `currentCwd`) then `buildCheckResult`.
|
||
`checkPathPolicy(values, agentName?, sessionRules?, surface="path")` skips normalization, uses precomputed values, then `buildCheckResult`.
|
||
Both already funnel through the shared private `buildCheckResult` helper ([#393]).
|
||
- `src/access-intent/access-path.ts` — the `AccessPath` value object ([#476]); `matchValues()` returns the lexical alias union ∪ canonical for the `external_directory` surface.
|
||
- Gate descriptor factories that resolve:
|
||
`src/handlers/gates/path.ts` (`resolve("path", {path})`), `bash-command.ts` (three `resolve("bash", {command})` calls), `bash-path.ts` (`resolvePathPolicy(policyValues)`), `external-directory-policy.ts` (`resolvePathPolicy(path.matchValues(), …, "external_directory")`), plus the tool-resolve site in `tool-call-gate-pipeline.ts` and the descriptor-resolve site in `runner.ts`.
|
||
- Raw (non-gate) manager callers: `src/permissions-service.ts`, `src/skill-prompt-sanitizer.ts`, `src/permission-event-rpc.ts`, and the resolver's own raw `checkPermission`.
|
||
|
||
AGENTS.md / skill constraints that apply:
|
||
|
||
- The `architecture.md` "ScopedPermissionResolver surface" health-metric row and the access-intent directory listing must be updated when this lands (the package skill's "module-move check misses narrative prose" rule).
|
||
- The package skill's testing notes about wiring new manager/resolver methods through `makeHandler`'s surface dispatcher ([#393] / [#418]) must be rewritten, because there is now a single method.
|
||
|
||
## Design Overview
|
||
|
||
### `AccessIntent` — the gate-emitted value
|
||
|
||
Three variants, modeling the three genuine ways a gate supplies "what is being accessed":
|
||
|
||
```typescript
|
||
// src/access-intent/access-intent.ts
|
||
import type { AccessPath } from "#src/access-intent/access-path";
|
||
|
||
/** Raw tool input the manager must normalize (path/bash/MCP/extension tools). */
|
||
export interface ToolAccessIntent {
|
||
kind: "tool";
|
||
/** Tool name fed to input normalization (e.g. "read", "bash", "path", an MCP name). */
|
||
surface: string;
|
||
input: unknown;
|
||
agentName?: string;
|
||
}
|
||
|
||
/** Precomputed equivalent policy values for a path-shaped surface (bash-path). */
|
||
export interface PathValuesAccessIntent {
|
||
kind: "path-values";
|
||
/** "path" or "external_directory". */
|
||
surface: string;
|
||
values: readonly string[];
|
||
agentName?: string;
|
||
}
|
||
|
||
/** An AccessPath value object for a path-shaped surface (external-directory). */
|
||
export interface AccessPathAccessIntent {
|
||
kind: "access-path";
|
||
surface: string;
|
||
path: AccessPath;
|
||
agentName?: string;
|
||
}
|
||
|
||
/** What a gate emits. */
|
||
export type AccessIntent =
|
||
| ToolAccessIntent
|
||
| PathValuesAccessIntent
|
||
| AccessPathAccessIntent;
|
||
|
||
/** What the manager consumes — access-path already unwrapped to values. */
|
||
export type ResolvedAccessIntent = ToolAccessIntent | PathValuesAccessIntent;
|
||
```
|
||
|
||
Why three variants and not two:
|
||
|
||
- The `tool` variant carries raw input only the manager can normalize (it needs `getConfiguredMcpServerNames()` + `currentCwd`).
|
||
- The `path-values` variant carries bash-path's cd-resolved lexical `string[]` for the `path` surface, which has no canonical-boundary notion.
|
||
- The `access-path` variant lets the external-directory gate hand its `AccessPath` directly, so `AccessPath` flows into the resolver (a meaningful domain boundary) rather than being flattened at the gate.
|
||
Forcing bash-path's plain `string[]` into an `AccessPath` would inject the canonical alias the `path` surface does not match today — a behavior change out of scope here.
|
||
|
||
### Where the unwrap happens
|
||
|
||
The **resolver** unwraps the `access-path` variant via `path.matchValues()` (Tell-Don't-Ask: it asks the `AccessPath` for its match set) and hands a `ResolvedAccessIntent` (string-based) to the manager.
|
||
The low-level `PermissionManager` stays string-based — it never imports `AccessPath`.
|
||
|
||
### Resolver
|
||
|
||
```typescript
|
||
export interface ScopedPermissionResolver {
|
||
resolve(intent: AccessIntent): PermissionCheckResult;
|
||
}
|
||
|
||
// PermissionResolver class
|
||
resolve(intent: AccessIntent): PermissionCheckResult {
|
||
return this.permissionManager.check(
|
||
toResolvedIntent(intent),
|
||
this.sessionRules.getRuleset(),
|
||
);
|
||
}
|
||
```
|
||
|
||
`toResolvedIntent` is a private module helper:
|
||
|
||
```typescript
|
||
function toResolvedIntent(intent: AccessIntent): ResolvedAccessIntent {
|
||
if (intent.kind === "access-path") {
|
||
return {
|
||
kind: "path-values",
|
||
surface: intent.surface,
|
||
values: intent.path.matchValues(),
|
||
agentName: intent.agentName,
|
||
};
|
||
}
|
||
return intent;
|
||
}
|
||
```
|
||
|
||
The class keeps `checkPermission` (raw, for skill-input), `getToolPermission`, and `getConfigIssues` unchanged — they remain off the narrow interface.
|
||
The raw `checkPermission` body now builds a `tool` intent and calls `manager.check(intent, sessionRules)`.
|
||
|
||
### Manager
|
||
|
||
```typescript
|
||
export interface ScopedPermissionManager {
|
||
configureForCwd(cwd: string | undefined | null): void;
|
||
check(
|
||
intent: ResolvedAccessIntent,
|
||
sessionRules?: Ruleset,
|
||
): PermissionCheckResult;
|
||
getToolPermission(toolName: string, agentName?: string): PermissionState;
|
||
getConfigIssues(agentName?: string): string[];
|
||
}
|
||
|
||
// PermissionManager class
|
||
check(intent: ResolvedAccessIntent, sessionRules?: Ruleset): PermissionCheckResult {
|
||
const { composedRules } = this.resolvePermissions(intent.agentName);
|
||
const fullRules: Ruleset = sessionRules?.length
|
||
? [...composedRules, ...sessionRules]
|
||
: composedRules;
|
||
|
||
if (intent.kind === "path-values") {
|
||
const lookupValues = intent.values.length > 0 ? [...intent.values] : ["*"];
|
||
return buildCheckResult(
|
||
intent.surface, lookupValues, {}, intent.surface, intent.surface, fullRules,
|
||
);
|
||
}
|
||
|
||
const toolName = intent.surface.trim();
|
||
const { surface, values, resultExtras } = normalizeInput(
|
||
toolName, intent.input, this.loader.getConfiguredMcpServerNames(), this.currentCwd,
|
||
);
|
||
return buildCheckResult(
|
||
surface, values, resultExtras, toolName, intent.surface, fullRules,
|
||
);
|
||
}
|
||
```
|
||
|
||
The two branches are exactly the former `checkPermission` and `checkPathPolicy` bodies, preserving the trimmed-toolName-for-source / original-toolName-for-result distinction.
|
||
|
||
### Consumer call-site sketches
|
||
|
||
External-directory policy helper (the `AccessPath` now flows into the resolver):
|
||
|
||
```typescript
|
||
// external-directory-policy.ts
|
||
export function resolveExternalDirectoryPolicy(path, resolver, agentName) {
|
||
return resolver.resolve({
|
||
kind: "access-path",
|
||
surface: "external_directory",
|
||
path,
|
||
agentName,
|
||
});
|
||
}
|
||
```
|
||
|
||
bash-path gate (precomputed values):
|
||
|
||
```typescript
|
||
// bash-path.ts
|
||
const check = resolver.resolve({
|
||
kind: "path-values",
|
||
surface: "path",
|
||
values: policyValues,
|
||
agentName: tcc.agentName ?? undefined,
|
||
});
|
||
```
|
||
|
||
Tool/path/bash-command/runner sites emit a `tool` intent:
|
||
|
||
```typescript
|
||
const check = resolver.resolve({
|
||
kind: "tool",
|
||
surface: "path", // or "bash" / tcc.toolName / descriptor.surface
|
||
input: { path: filePath }, // or { command } / tcc.input / descriptor.input
|
||
agentName: tcc.agentName ?? undefined,
|
||
});
|
||
```
|
||
|
||
### Edge cases preserved
|
||
|
||
- Empty `path-values` falls back to `["*"]` (the former `checkPathPolicy` behavior).
|
||
- The `path` vs `external_directory` surface tag still drives `evaluateAnyValue` (last-match-wins across aliases) via `PATH_SURFACES` inside `buildCheckResult` — unchanged.
|
||
- The bash-command unparseable-command fail-closed sentinel ([#452]) is in `resolveBashCommandCheck`, which now emits `tool` intents; the sentinel path is untouched.
|
||
- The `path`-surface "only the universal default fired → skip gate" guard ([#58]) lives in the gate factories, not the resolver — untouched.
|
||
|
||
## Module-Level Changes
|
||
|
||
### Added
|
||
|
||
- `src/access-intent/access-intent.ts` — `ToolAccessIntent`, `PathValuesAccessIntent`, `AccessPathAccessIntent`, `AccessIntent`, `ResolvedAccessIntent`.
|
||
|
||
### Changed — production
|
||
|
||
- `src/permission-manager.ts` — replace `checkPermission` + `checkPathPolicy` (interface + class) with `check(intent, sessionRules?)`; the two former bodies become the intent-kind branches; `buildCheckResult` unchanged.
|
||
- `src/permission-resolver.ts` — narrow `ScopedPermissionResolver` to `resolve(intent: AccessIntent)`; remove `resolvePathPolicy`; `resolve` delegates via `toResolvedIntent`; raw `checkPermission` body builds a `tool` intent; add `toResolvedIntent` private helper.
|
||
- `src/permissions-service.ts` — `checkPermission` → `check({ kind: "tool", … })`.
|
||
- `src/skill-prompt-sanitizer.ts` — `checkPermission` → `check({ kind: "tool", … })`.
|
||
- `src/permission-event-rpc.ts` — `checkPermission` → `check({ kind: "tool", … })`.
|
||
- `src/handlers/gates/path.ts` — emit a `tool` intent.
|
||
- `src/handlers/gates/bash-command.ts` — three call sites emit `tool` intents.
|
||
- `src/handlers/gates/bash-path.ts` — emit a `path-values` intent.
|
||
- `src/handlers/gates/external-directory-policy.ts` — `resolveExternalDirectoryPolicy` emits an `access-path` intent (drops the inline `path.matchValues()` call).
|
||
- `src/handlers/gates/tool-call-gate-pipeline.ts` — the tool-resolve site emits a `tool` intent.
|
||
- `src/handlers/gates/runner.ts` — the descriptor-resolve site emits a `tool` intent from `descriptor.surface` + `descriptor.input`.
|
||
|
||
### Changed — tests and fixtures
|
||
|
||
- `test/helpers/session-fixtures.ts` — `makeFakePermissionManager`: replace `checkPermission` + `checkPathPolicy` stubs with a single `check` stub.
|
||
- `test/helpers/handler-fixtures.ts` — `makeHandler`: route the `surfaceCheck` override onto the single `permissionManager.check` via an intent→(surface,input) adapter (replaces the dual `checkPermission`/`checkPathPolicy` routing); the `MockGateHandlerSession.checkPermission` override surface is unchanged.
|
||
- `test/helpers/gate-fixtures.ts` — `makeResolver`, `makeGateRunner`, `makePathDispatchResolver`: drop `resolvePathPolicy`; `makePathDispatchResolver`'s single `resolve` dispatches on intent kind (`tool` → `input.path`; `path-values` → any matching value; `access-path` → `path.matchValues()`).
|
||
- `test/permission-manager-unified.test.ts` — migrate `checkPermission`/`checkPathPolicy` tests to `check(intent)`.
|
||
- `test/permission-resolver.test.ts` — migrate `resolve`/`resolvePathPolicy` tests to `resolve(intent)` (including an `access-path` unwrap test).
|
||
- `test/handlers/gates/path.test.ts`, `bash-path.test.ts`, `bash-external-directory.test.ts`, `external-directory-policy.test.ts`, `external-directory.test.ts`, `bash-command-metamorphic.test.ts` — update resolver-mock assertions to the intent shape.
|
||
- `test/handlers/external-directory-session-dedup.test.ts` — update the inline manager mocks (`checkPermission`/`checkPathPolicy`) to the single `check`.
|
||
- Grep `test/` for any inline `ScopedPermissionResolver` / `ScopedPermissionManager` mock not covered by the fixtures and migrate it in the same commit as the interface change.
|
||
|
||
### Changed — docs
|
||
|
||
- `docs/architecture/architecture.md` — mark Step 6 ✅ (heading + Mermaid `S6` node); update the "ScopedPermissionResolver surface" health-metric row to met (`resolve(intent)`); rewrite the `permission-resolver.ts`, `permission-manager.ts`, `bash-path.ts`, and `external-directory-policy.ts` directory-listing descriptions to the new surface; add `access-intent.ts` to the access-intent directory listing; refresh the line-622 resolver-surface-widening narrative.
|
||
- `.pi/skills/package-pi-permission-system/SKILL.md` — rewrite the `makeFakePermissionManager` / `makeResolver` / `makePathDispatchResolver` / `makeGateRunner` / `makeHandler` fixture notes for the single `check` / `resolve(intent)`; rewrite the [#393] / [#418] "wire the new method through the surface dispatcher" testing notes to state the false-green is now structurally impossible (one method).
|
||
|
||
## Test Impact Analysis
|
||
|
||
1. **New unit tests enabled.**
|
||
`resolve(intent)` can be tested per-variant in one place, including the `access-path` → `matchValues()` unwrap (previously only reachable indirectly through the external-directory gate).
|
||
`check(intent)` can be tested per-kind directly on the manager.
|
||
2. **Tests that become redundant.**
|
||
The separate `checkPermission` vs `checkPathPolicy` manager test groups merge into intent-kind cases of `check`.
|
||
The separate `resolve` vs `resolvePathPolicy` resolver test groups merge into intent-variant cases of `resolve`.
|
||
Consolidate, do not duplicate.
|
||
3. **Tests that must stay.**
|
||
The gate behavior tests (path, bash-path, external-directory single/bash, bash-command chain) still exercise gate → resolver → manager end-to-end; they only change the asserted mock shape.
|
||
The [#393] / [#418] integration tests (`external-directory-session-dedup.test.ts`, the `tool-call.test.ts` bash-path/external-directory blocks) stay — they pin that the unification did not reintroduce a silent `allow`.
|
||
|
||
## Invariants at risk
|
||
|
||
This step touches surfaces earlier Phase 6 / earlier-phase steps refactored.
|
||
List and pin:
|
||
|
||
- **[#393] false-green class** — pinned by the `external-directory-session-dedup` and `tool-call` integration tests routing through real instances.
|
||
After unification the class is structurally impossible (single `check`); the tests must still pass green.
|
||
- **[#418] external_directory alias matching (lexical ∪ canonical)** — pinned by `bash-external-directory.test.ts` / `external-directory.test.ts` asserting both typed and symlink-resolved patterns match; the `access-path` variant must resolve the same `matchValues()` set.
|
||
- **[#452] bash fail-closed sentinel** — pinned by the bash-command unparseable-command tests; `resolveBashCommandCheck` keeps the sentinel.
|
||
- **[#58] universal-default skip on `path`** — pinned by the path-gate tests asserting no prompt when only the universal default fired.
|
||
- **[#306] / [#301] bash chain most-restrictive** — pinned by `bash-command-metamorphic.test.ts`.
|
||
|
||
All invariants live in existing tests; none rely on prose only, so no new pinning test is required beyond the migrated assertions.
|
||
|
||
## TDD Order
|
||
|
||
Lift-and-shift where an existing name's signature changes, to avoid a single giant test rewrite (per the testing skill).
|
||
|
||
1. **Add `AccessIntent` types + manager `check(intent)` alongside the old pair.**
|
||
New `src/access-intent/access-intent.ts`; add `check` to the `ScopedPermissionManager` interface + `PermissionManager` class (delegating through the existing `buildCheckResult`), leaving `checkPermission`/`checkPathPolicy` in place; add a `check` stub to `makeFakePermissionManager` and route it in `makeHandler` alongside the existing dispatch.
|
||
Red: `permission-manager-unified.test.ts` cases for `check` covering `tool` and `path-values` intents.
|
||
Commit: `feat(pi-permission-system): add ScopedPermissionManager.check(intent) (#478)`.
|
||
2. **Migrate manager callers to `check`; remove `checkPermission`/`checkPathPolicy`.**
|
||
Switch resolver internals (`resolve`/`resolvePathPolicy`/raw `checkPermission` bodies), `permissions-service.ts`, `skill-prompt-sanitizer.ts`, `permission-event-rpc.ts` to `check`; remove the old pair from the interface + class; drop the old stubs from `makeFakePermissionManager`, `makeHandler`, and the inline dedup-test mocks; migrate `permission-manager-unified.test.ts`.
|
||
One commit (interface removal breaks all manager mocks at the type level).
|
||
Run `pnpm run check` immediately after.
|
||
The resolver's **public** surface is unchanged here, so gates and resolver fixtures are untouched.
|
||
Commit: `refactor(pi-permission-system): route all callers through manager.check (#478)`.
|
||
3. **Add resolver `resolveIntent(intent)` alongside the old pair; migrate gates incrementally.**
|
||
Add `resolveIntent` to the interface + class (with `toResolvedIntent`); add a `resolveIntent` stub to `makeResolver`/`makeGateRunner`/`makePathDispatchResolver` alongside the existing `resolve`/`resolvePathPolicy`; add resolver-level tests for all three intent variants.
|
||
Then migrate each gate + its tests to `resolveIntent`, one commit per gate: `path.ts`, `bash-command.ts` (+ the `tool-call-gate-pipeline` tool-resolve site + `runner.ts`), `bash-path.ts`, `external-directory-policy.ts` (+ external-directory test files).
|
||
Commits: `feat(pi-permission-system): add resolver resolveIntent seam (#478)` then `refactor(pi-permission-system): emit AccessIntent from <gate> (#478)` per gate.
|
||
4. **Remove `resolve(surface,input)` + `resolvePathPolicy`; rename `resolveIntent` → `resolve`.**
|
||
Drop the old pair from the interface + class + fixtures; rename `resolveIntent` to `resolve` across the migrated call sites and tests; migrate `permission-resolver.test.ts` to the final `resolve(intent)`.
|
||
One commit (mechanical rename + final interface narrowing).
|
||
Run `pnpm run check` immediately after.
|
||
Commit: `refactor(pi-permission-system): narrow ScopedPermissionResolver to resolve(intent) (#478)`.
|
||
5. **Docs.**
|
||
Update `architecture.md` (Step 6 ✅, surface metric, directory descriptions, access-intent listing) and the package `SKILL.md` fixture/testing notes.
|
||
Commit: `docs(pi-permission-system): record resolve(intent) narrowing (#478)`.
|
||
|
||
If `/tdd-plan` judges the per-gate lift-and-shift heavier than an atomic resolver narrowing (only six production call sites), it may collapse steps 3–4 into a single atomic interface-change commit — the interface removal forces all consumers into one commit either way.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- **Large interface-removal commits.**
|
||
Both the manager (step 2) and resolver (step 4) removals break every typed mock at once.
|
||
Mitigation: lift-and-shift the new method in first (steps 1, 3), grep `test/` for inline mocks before the removal commit, and run `pnpm run check` immediately after each removal.
|
||
- **Silent behavior change in the `tool`-intent branch.**
|
||
The trimmed-vs-original tool-name distinction (`deriveSource` uses trimmed; the result reports original) must be preserved.
|
||
Mitigation: keep `buildCheckResult(surface, values, extras, trimmedToolName, originalSurface, fullRules)` argument order; the manager-unified tests assert `source` and `toolName`.
|
||
- **`AccessPath` coupling creep.**
|
||
Mitigation: the manager consumes `ResolvedAccessIntent` (no `AccessPath`); only the resolver imports `AccessPath`, via `toResolvedIntent`.
|
||
- **Reintroducing the [#393] false-green during migration.**
|
||
Mitigation: the integration tests route through real instances; keep them green at every step.
|
||
|
||
## Open Questions
|
||
|
||
- Should the `path` surface match the canonical form like `external_directory`?
|
||
Filed as [#486]; resolving it gates the bash-path → `AccessPath` migration.
|
||
- Adopt `AccessPath` as the universal internal path representation?
|
||
Filed as [#487]; this step's `path-values` variant is the transitional accommodation that shrinks under that direction.
|
||
|
||
[#58]: https://github.com/gotgenes/pi-packages/issues/58
|
||
[#301]: https://github.com/gotgenes/pi-packages/issues/301
|
||
[#306]: https://github.com/gotgenes/pi-packages/issues/306
|
||
[#309]: https://github.com/gotgenes/pi-packages/issues/309
|
||
[#319]: https://github.com/gotgenes/pi-packages/issues/319
|
||
[#340]: https://github.com/gotgenes/pi-packages/issues/340
|
||
[#393]: https://github.com/gotgenes/pi-packages/issues/393
|
||
[#418]: https://github.com/gotgenes/pi-packages/issues/418
|
||
[#452]: https://github.com/gotgenes/pi-packages/issues/452
|
||
[#476]: https://github.com/gotgenes/pi-packages/issues/476
|
||
[#486]: https://github.com/gotgenes/pi-packages/issues/486
|
||
[#487]: https://github.com/gotgenes/pi-packages/issues/487
|