--- issue: 505 issue_title: "pi-permission-system: dissolve the path-utils grab-bag behind AccessPath (Phase 7 Step 4)" --- # Dissolve the `path-utils.ts` grab-bag into cohesive path modules ## Release Recommendation **Release:** ship independently This is Phase 7 Step 4 ([#505]) of the [#487] roadmap, tagged `Release: independent` — a pure structural refactor with no behavior change. It is not part of the `symlink-resistant-path-matching` batch (Steps 1–3, already shipped), so it carries no batch obligation. The commits are `refactor:` / `docs:` (changelog-`hidden`), so this work lands on `main` and auto-batches into the next `feat:`/`fix:` release rather than cutting one on its own. ## Problem Statement `path-utils.ts` is the package's path-derivation grab-bag — 18 symbols spanning four unrelated jobs (representation derivation, geometric containment, tool-input extraction, static lookup sets), 13 fan-in, and an accelerating churn hotspot (266 churn over six months, trending up). It is the ad-hoc "re-derive path representations" surface the [#487] vision exists to consolidate. After Steps 1–3 ([#502], [#503], [#504]) routed the per-tool gate and service/RPC queries onto `AccessPath`, the lexical/canonical/policy-value derivation in this file is consumed almost entirely by `AccessPath` — so it belongs *with* `AccessPath` in the `access-intent/` domain, and the rest belongs in focused single-job modules. ## Goals - Dissolve `path-utils.ts` entirely into cohesive, single-responsibility modules. - Relocate the representation derivation (`normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues` + private helpers, `PathPolicyValueOptions`) into `src/access-intent/path-normalization.ts` as `AccessPath`'s backing. - Keep the geometric-containment predicates (`isPathWithinDirectory`, `isPathOutsideWorkingDirectory`) together in a focused `src/path-containment.ts`. - Split the remaining jobs into focused modules: safe-system paths, Pi infrastructure-read, tool-input extraction, and the surface/tool lookup sets. - Preserve behavior exactly — this is non-breaking at every surface (no public API, config, schema, or decision change). - Mirror the module split in the test suite: each new module gets a focused test file; `path-utils.test.ts` is dissolved. ## Non-Goals - **No config-pattern derivation.** Rule patterns (`*.env`, `src/*`) stay raw globs/regex — they are never path-derived (a standing Phase 7 Non-goal). Nothing in this work touches the matcher. - **No change to `AccessPath`'s outputs or accessors.** `matchValues()` / `boundaryValue()` / `value()` keep their exact results; only the import path of their backing functions moves. - **No deeper boundary rework.** We do not re-express `isPathOutsideWorkingDirectory` as an `AccessPath` method or collapse it into the gate flow beyond the minimal prep refactor below — the `path-values` boundary formalization is Step 5 ([#506]), a separate issue. - **No `PathNormalizer` API change.** Its public method surface (`forPath`/`forLiteral`/`isAbsolute`/`isWithinDirectory`/`isOutsideWorkingDirectory`/`comparableValue`/`isInfrastructureRead`/…) is unchanged; only its private delegation targets move. - The frozen historical doc `docs/architecture/history/phase-6-access-intent-extraction.md` references `path-utils` as it was — it is a snapshot and is **not** edited. ## Background Relevant existing modules: - `src/path-utils.ts` — the grab-bag being dissolved (full inventory in Design Overview). - `src/access-intent/access-path.ts` — `AccessPath` value object; `forPath` composes `normalizePathForComparison` + `getPathPolicyValues` + `canonicalNormalizePathForComparison`. - `src/path-normalizer.ts` — `PathNormalizer` facade, constructed at the session edge with `platform` + `cwd` baked in; delegates to the `path-utils` leaf functions. The **sole** caller of the free `isPathOutsideWorkingDirectory`. - `src/access-intent/bash/bash-path-resolver.ts` — imports `isSafeSystemPath`, `normalizePathPolicyLiteral`. - `src/rule.ts`, `src/permission-manager.ts`, `src/input-normalizer.ts` — import `PATH_SURFACES`. - `src/pattern-suggest.ts`, `src/handlers/gates/tool.ts` — import `PATH_BEARING_TOOLS`. - `src/handlers/gates/{path,external-directory,tool,tool-call-gate-pipeline}.ts` — import `getToolInputPath` / `getPathBearingToolPath`. Constraints from AGENTS.md / the package skill that apply: - The ESLint `no-restricted-syntax` guard forbids `process.platform` in `pi-permission-system/src/**` (except `index.ts`). Every relocated leaf must keep taking an injected `platform` parameter — no `= process.platform` defaults. - `docs/architecture/architecture.md` module-tree, Phase 7 step list, Mermaid node, findings metrics, and the "PathNormalizer platform seam" prose all name `path-utils.ts` and must be updated in the implementation doc commit (mark Step 4 ✅ there, not at ship). - `.pi/skills/package-pi-permission-system/SKILL.md` names `src/path-utils.ts` in prose — reworded mechanism prose carries no removed symbol, so it must be grepped and updated. - `subagent-context.ts` has its **own** private `isPathWithinDirectoryForSubagent` — it does not consume `path-utils` and is out of scope. ## Design Overview ### The carve: representation vs geometry, on a shared primitive Every function in `path-utils.ts` operates on the **accessed-path side** (a path the agent is trying to touch), not the config side. The real seam is two different jobs done to that path: | Job | Functions | Feeds | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Representation (derivation) | `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues` (+ 2 private helpers) | `AccessPath` match/boundary/display values → resolver pattern test | | Geometry (containment) | `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` | gate bypass / auto-allow boundary decisions | | Extraction | `getToolInputPath`, `getPathBearingToolPath` | pulls the path out of a tool payload | | Lookup sets | `PATH_BEARING_TOOLS`, `READ_ONLY_PATH_BEARING_TOOLS`, `PATH_SURFACES` | static surface membership | | Safe-system | `SAFE_SYSTEM_PATHS`, `isSafeSystemPath` | OS device-file allowlist | | Infra-read | `isPiInfrastructureRead` (+ private `containsGlobChars`) | Pi infrastructure auto-allow | `isPathWithinDirectory` is the truly foundational piece: pure `path.relative` math, zero dependencies, used by representation (for the cwd-relative alias), by both boundary predicates, and by `PathNormalizer` directly. ### Breaking the apparent cycle: "prepare the data, then ask" A naïve split (representation → `access-intent/`, both containment funcs → `path-containment.ts`) appears to force a module cycle: `getPathPolicyValues` (representation) calls `isPathWithinDirectory` (geometry), while `isPathOutsideWorkingDirectory` (geometry) calls `canonicalNormalizePathForComparison` (representation). The cycle is not fundamental — it is an artifact of `isPathOutsideWorkingDirectory` being mis-factored. It bundles "prepare the data" with "ask the question": ```typescript // today: derives BOTH operands inline, then asks — the only geometry→representation edge export function isPathOutsideWorkingDirectory(pathValue, cwd, platform) { const normalizedCwd = canonicalNormalizePathForComparison(cwd, cwd, platform); const normalizedPath = canonicalNormalizePathForComparison(pathValue, cwd, platform); if (!normalizedCwd || !normalizedPath) return false; if (isSafeSystemPath(normalizedPath)) return false; return !isPathWithinDirectory(normalizedPath, normalizedCwd, platform); } ``` Its sibling `isPiInfrastructureRead` already follows the right discipline: it receives the already-canonical `accessPath.boundaryValue()` from `PathNormalizer` and never re-derives. `isPathWithinDirectory` already follows it too: pure geometry over normalized operands. The fix is to make `isPathOutsideWorkingDirectory` pure geometry as well — receive prepared canonical operands, and push the canonicalization up to its single caller, `PathNormalizer.isOutsideWorkingDirectory`, which already owns `cwd` and `platform`: ```typescript // path-containment.ts — pure geometry, no representation import export function isPathOutsideWorkingDirectory( canonicalPath: string, canonicalCwd: string, platform: NodeJS.Platform, ): boolean { if (!canonicalCwd || !canonicalPath) return false; if (isSafeSystemPath(canonicalPath)) return false; return !isPathWithinDirectory(canonicalPath, canonicalCwd, platform); } // path-normalizer.ts — the caller prepares the operands (canonical cwd cached once) isOutsideWorkingDirectory(pathValue: string): boolean { const canonicalPath = canonicalNormalizePathForComparison( pathValue, this.cwd, this.platform, ); return isPathOutsideWorkingDirectory(canonicalPath, this.canonicalCwd, this.platform); } ``` With the inline derivation removed, geometry no longer imports representation, and the dependency graph is a strict DAG: ```text safe-system-paths.ts ─┐ ▼ path-containment.ts (isPathWithinDirectory, isPathOutsideWorkingDirectory — pure geometry) ▲ access-intent/path-normalization.ts (all representation; calls the geometry primitive downward) ▲ access-intent/access-path.ts (AccessPath) ▲ path-normalizer.ts (derives canonical operands, hands them to geometry) ``` This honors the issue's literal grouping: representation lands in **one** `access-intent/path-normalization.ts`, and both containment predicates stay together in **one** focused `path-containment.ts`. ### Final module set (six modules; `path-utils.ts` deleted) | New module | Residents | Imports | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `src/access-intent/path-normalization.ts` | `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`, `PathPolicyValueOptions`, private `getAbsolutePathPolicyValues` / `getCwdRelativePathPolicyValues` | `isPathWithinDirectory` (path-containment), `expandHomePath`, `canonicalizePath`, `node:path` | | `src/path-containment.ts` | `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` (pure geometry) | `isSafeSystemPath` (safe-system-paths), `node:path` | | `src/safe-system-paths.ts` | `SAFE_SYSTEM_PATHS`, `isSafeSystemPath` | — | | `src/pi-infrastructure-read.ts` | `isPiInfrastructureRead`, private `containsGlobChars` | `isPathWithinDirectory` (path-containment), `READ_ONLY_PATH_BEARING_TOOLS` (path-surfaces), `expandHomePath`, `wildcardMatch`, `node:path` | | `src/tool-input-path.ts` | `getToolInputPath`, `getPathBearingToolPath` | `PATH_BEARING_TOOLS` (path-surfaces), `getNonEmptyString` / `toRecord` (value-guards), `ToolAccessExtractorLookup` (type) | | `src/path-surfaces.ts` | `PATH_BEARING_TOOLS`, `READ_ONLY_PATH_BEARING_TOOLS`, `PATH_SURFACES` | — | DAG verification: `path-surfaces` and `safe-system-paths` are leaves; `path-containment → safe-system-paths`; `path-normalization → path-containment`; `pi-infrastructure-read → {path-containment, path-surfaces}`; `tool-input-path → path-surfaces`. No cycles. ### Edge cases preserved - `isPathOutsideWorkingDirectory` keeps its empty-operand guard, safe-system short-circuit, and `platform` (for `path.relative` separator choice) — only the canonicalization moves out. - `PathNormalizer` canonicalizes its `cwd` once at construction (`this.canonicalCwd`), matching today's per-call result but avoiding a redundant `realpathSync` per query (a behavior-equivalent improvement; cwd symlink target does not change mid-session). - All leaf functions keep their injected `platform` parameter (the `no-restricted-syntax` guard stays green). ## Module-Level Changes ### Source — new files - `src/access-intent/path-normalization.ts` — representation derivation (see table). - `src/path-containment.ts` — `isPathWithinDirectory` + pure-geometry `isPathOutsideWorkingDirectory`. - `src/safe-system-paths.ts` — `SAFE_SYSTEM_PATHS`, `isSafeSystemPath`. - `src/pi-infrastructure-read.ts` — `isPiInfrastructureRead` + `containsGlobChars`. - `src/tool-input-path.ts` — `getToolInputPath`, `getPathBearingToolPath`. - `src/path-surfaces.ts` — the three lookup sets. ### Source — deleted - `src/path-utils.ts` — removed once empty (its last residents, the containment pair, move to `path-containment.ts`). ### Source — importer updates (every removed export breaks importers at the type level → updated in the same commit as its move) - `src/path-normalizer.ts` — re-point `normalizePathForComparison`, `canonicalNormalizePathForComparison` → `access-intent/path-normalization`; `isPathWithinDirectory`, `isPathOutsideWorkingDirectory` → `path-containment`; `isPiInfrastructureRead` → `pi-infrastructure-read`. Add the `isOutsideWorkingDirectory` prep refactor (canonicalize operands; cache `canonicalCwd`). - `src/access-intent/access-path.ts` — re-point the three derivation imports → `access-intent/path-normalization`. - `src/access-intent/bash/bash-path-resolver.ts` — `isSafeSystemPath` → `safe-system-paths`; `normalizePathPolicyLiteral` → `access-intent/path-normalization`. - `src/rule.ts`, `src/permission-manager.ts`, `src/input-normalizer.ts` — `PATH_SURFACES` → `path-surfaces`. - `src/pattern-suggest.ts` — `PATH_BEARING_TOOLS` → `path-surfaces`. - `src/handlers/gates/tool.ts` — `getPathBearingToolPath` → `tool-input-path`; `PATH_BEARING_TOOLS` → `path-surfaces`. - `src/handlers/gates/path.ts`, `src/handlers/gates/external-directory.ts` — `getToolInputPath` → `tool-input-path`. - `src/handlers/gates/tool-call-gate-pipeline.ts` — `getPathBearingToolPath` → `tool-input-path`. ### Tests — split to mirror modules - `test/path-utils.test.ts` (695 LOC) is dissolved; each `describe` block moves to a focused file carrying the same `node:os` / `node:fs` mocks where needed: - `test/path-normalization.test.ts` — `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`. - `test/path-containment.test.ts` — `isPathWithinDirectory`; pure-geometry `isPathOutsideWorkingDirectory` (operands pre-canonicalized — no `realpathSync` needed). - `test/safe-system-paths.test.ts` — `SAFE_SYSTEM_PATHS`, `isSafeSystemPath`. - `test/tool-input-path.test.ts` — `getToolInputPath`, `getPathBearingToolPath`. - `test/path-surfaces.test.ts` — the three lookup sets. - `test/pi-infrastructure-read.test.ts` — re-point import → `#src/pi-infrastructure-read`; absorb the overlapping `isPiInfrastructureRead` `describe` from `path-utils.test.ts` (de-duplicate). - `test/permission-manager-unified.test.ts` — re-point `getPathPolicyValues` → `#src/access-intent/path-normalization`. - `test/path-normalizer.test.ts` — gains the symlink/`./link/hosts`/safe-system integration cases that move off `path-utils.test.ts`'s `isPathOutsideWorkingDirectory` block (they exercise canonicalization, now owned by `PathNormalizer`). ### Docs — updated in the implementation doc commit - `packages/pi-permission-system/docs/architecture/architecture.md`: - Module-tree: replace the `path-utils.ts` entry with the six new module entries; update the `access-path.ts` and `path-normalizer.ts` entries to name `access-intent/path-normalization.ts` + `path-containment.ts` (and note `isOutsideWorkingDirectory` now canonicalizes operands before the pure check). - Phase 7 Step 4 heading → ✅; update its Outcome line; flip the Mermaid `S4` node to ✅. - Findings metric row (`path-utils.ts` fan-in → distributed) and the prose at the grab-bag references (the "residual ad-hoc path handling" bullet, the findings paragraph) → past-tense / resolved. - "Related: PathNormalizer platform seam" section: update "`path-utils`/`AccessPath` primitives … Phase 7 [#505] can later move them behind it" (now done), "leaf `platform` parameters in `path-utils.ts` persist" → "in the relocated path modules persist", and the `isPiInfrastructureRead` / `isPathWithinDirectory` / `isPathOutsideWorkingDirectory` references → new module names. - `.pi/skills/package-pi-permission-system/SKILL.md`: - `getToolInputPath` (`src/path-utils.ts`) → `src/tool-input-path.ts`. - "every path-utils / `canonicalize-path` / `rule.ts` / `subagent-context.ts` leaf takes an injected `platform`" → re-word "path-utils" to the relocated path modules. `README.md` and `docs/configuration.md` name no internal symbols here (verified) — no update. ## Test Impact Analysis 1. **New tests the split enables.** The pure-geometry `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, platform)` becomes testable with plain string operands — no `node:fs`/`realpathSync` mock and no symlink fixture, a faster and more direct unit. Each focused test file documents one module's surface instead of a 695-line catch-all. 2. **Tests that become redundant.** `path-utils.test.ts`'s `isPathOutsideWorkingDirectory` cases that exercised *canonicalization* (symlink resolution, `./link/hosts`, safe-system device files) are redundant at the pure-geometry layer — they move to `path-normalizer.test.ts`, which now owns canonicalization (some overlap already exists there). The `isPiInfrastructureRead` `describe` duplicated across `path-utils.test.ts` and `pi-infrastructure-read.test.ts` collapses into the latter. 3. **Tests that must stay as-is.** The representation tests (`normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`) genuinely exercise derivation and move verbatim to `path-normalization.test.ts`. The `isPathWithinDirectory` geometry tests move verbatim to `path-containment.test.ts`. The lookup-set and extraction tests move verbatim. ## Invariants at risk This refactor touches surfaces that Phase 7 Steps 1–3 and the [#510]/[#511] seam already refactored: - **[#502]/[#503] — per-tool & service/RPC paths match lexical ∪ canonical.** `AccessPath` outputs must not change. Pinned by `test/path-normalizer.test.ts` (`forPath().matchValues()` / `value()` / `boundaryValue()`) and the external-directory / gate integration tests. Keep green. - **[#382] — win32 canonical lowercasing.** Pinned by `path-normalizer.test.ts` win32 cases and the (relocating) `canonicalNormalizePathForComparison` tests. - **[#418] — symlink pattern matching.** Pinned by the external-directory integration tests (unchanged). - **[#510]/[#511] — no interior `process.platform`; the `PathNormalizer` facade is the platform seam.** The ESLint `no-restricted-syntax` guard must stay green — every relocated leaf keeps its injected `platform`. `PathNormalizer.isOutsideWorkingDirectory(pathValue)`'s observable contract is unchanged; pinned by `path-normalizer.test.ts` (augmented with the moved integration cases). Run `pnpm run lint` after the prep refactor to confirm the guard. ## TDD Order Step 1 is a genuine behavior-contract change (red→green). Steps 2–7 are pure relocations: the safety net is the **existing** suite staying green after each move + importer update (`pnpm run check` + `pnpm -r run test`). Each step leaves the build valid and is committed separately. 1. **Prep refactor — make `isPathOutsideWorkingDirectory` pure geometry ("tidy first").** Red: add `test/path-containment.test.ts` (or extend in place) asserting `isPathOutsideWorkingDirectory(canonicalPath, canonicalCwd, platform)` over **pre-canonicalized** operands (within/outside/equal/empty/safe-system). Green: change the signature to receive prepared operands (drop the inline `canonicalNormalizePathForComparison` calls); move canonicalization into `PathNormalizer.isOutsideWorkingDirectory` (cache `canonicalCwd` in the constructor); update the `isPathOutsideWorkingDirectory` cases in `path-utils.test.ts` to pass canonical operands and migrate the symlink/safe-system integration cases into `path-normalizer.test.ts`. Run `pnpm run check` + `pnpm run lint` (interface change + guard). Commit `refactor(pi-permission-system): make isPathOutsideWorkingDirectory pure geometry over prepared operands`. 2. **Extract `path-surfaces.ts`.** Move the three lookup sets; update `rule.ts`, `permission-manager.ts`, `input-normalizer.ts`, `pattern-suggest.ts`, `handlers/gates/tool.ts`, and the `path-utils.ts` internal `READ_ONLY` user; move the lookup-set `describe`s to `test/path-surfaces.test.ts`. Commit `refactor(pi-permission-system): extract path-surfaces module`. 3. **Extract `safe-system-paths.ts`.** Move `SAFE_SYSTEM_PATHS` + `isSafeSystemPath`; update `bash-path-resolver.ts`, the `path-utils.ts` `isPathOutsideWorkingDirectory` user, and `path-containment.ts` (once it exists, step 7 — until then `path-utils.ts`); move the `describe`s to `test/safe-system-paths.test.ts`. Commit `refactor(pi-permission-system): extract safe-system-paths module`. 4. **Extract `tool-input-path.ts`.** Move `getToolInputPath` + `getPathBearingToolPath`; update `handlers/gates/{path,external-directory,tool,tool-call-gate-pipeline}.ts`; move the `describe`s to `test/tool-input-path.test.ts`. Commit `refactor(pi-permission-system): extract tool-input-path module`. 5. **Extract `pi-infrastructure-read.ts`.** Move `isPiInfrastructureRead` + `containsGlobChars`; update `path-normalizer.ts`; re-point and de-duplicate `test/pi-infrastructure-read.test.ts` (absorb the `path-utils.test.ts` `isPiInfrastructureRead` block). Commit `refactor(pi-permission-system): extract pi-infrastructure-read module`. 6. **Extract `access-intent/path-normalization.ts` (representation).** Move `normalizePathForComparison`, `canonicalNormalizePathForComparison`, `normalizePathPolicyLiteral`, `getPathPolicyValues`, `PathPolicyValueOptions`, and the two private helpers; update `access-path.ts`, `bash-path-resolver.ts`, `path-normalizer.ts`, `permission-manager-unified.test.ts`; move the derivation `describe`s to `test/path-normalization.test.ts`. Run `pnpm run check`. Commit `refactor(pi-permission-system): relocate path representation into access-intent`. 7. **Rename the residue to `path-containment.ts` and delete `path-utils.ts`.** The only remaining residents are `isPathWithinDirectory` + `isPathOutsideWorkingDirectory` — rename the file to `src/path-containment.ts`; update its importers (`path-normalizer.ts`, `pi-infrastructure-read.ts`, `path-normalization.ts`); finish migrating `test/path-containment.test.ts`; delete `test/path-utils.test.ts`. Run `pnpm fallow dead-code`. Commit `refactor(pi-permission-system): rename path-utils residue to path-containment`. 8. **Docs — mark Step 4 complete and re-point module references.** Update `docs/architecture/architecture.md` (module tree, Step 4 ✅ + Outcome, Mermaid `S4` ✅, findings metric, PathNormalizer-seam prose) and `.pi/skills/package-pi-permission-system/SKILL.md` (module-name references). Commit `docs(pi-permission-system): record path-utils dissolution (Phase 7 Step 4)`. ## Risks and Mitigations - **Silent default drift from a moved lookup set or interface.** `esbuild` does not reject unknown properties at runtime, so a mis-pointed import could fail only at runtime. Mitigation: run `pnpm run check` (a moved export becomes `TS2305` if mis-pointed) plus the full `pnpm -r run test` after each step. - **The prep refactor (Step 1) changing observable behavior.** Mitigation: it is behavior-preserving by construction (canonicalization merely relocates from callee to single caller); the `path-normalizer.test.ts` integration cases pin the end-to-end `isOutsideWorkingDirectory(pathValue)` contract, and the new pure-geometry tests pin the extracted function. - **An orphaned import after a test `describe` moves.** Biome's `noUnusedImports` is warning-level (exit 0). Mitigation: re-check each touched test file's imports as part of its step; the pre-completion reviewer is the backstop. - **A missed `path-utils` reference in prose.** Mitigation: after Step 8, `grep -rn "path-utils" packages/pi-permission-system/{src,test,docs/architecture/architecture.md} .pi/skills/package-pi-permission-system/SKILL.md` returns only the frozen `history/` snapshot. - **`fallow dead-code` flagging a newly-unreferenced export.** Mitigation: these are all moves (every export keeps its consumer); run `pnpm fallow dead-code` at Step 7. ## Open Questions - None blocking. Step 5 ([#506], the `path-values` boundary decision) is already a separate roadmap issue and is unaffected by this relocation. [#487]: https://github.com/gotgenes/pi-packages/issues/487 [#502]: https://github.com/gotgenes/pi-packages/issues/502 [#503]: https://github.com/gotgenes/pi-packages/issues/503 [#504]: https://github.com/gotgenes/pi-packages/issues/504 [#505]: https://github.com/gotgenes/pi-packages/issues/505 [#506]: https://github.com/gotgenes/pi-packages/issues/506 [#510]: https://github.com/gotgenes/pi-packages/issues/510 [#511]: https://github.com/gotgenes/pi-packages/issues/511 [#382]: https://github.com/gotgenes/pi-packages/issues/382 [#418]: https://github.com/gotgenes/pi-packages/issues/418