feat: vendor permission system source

This commit is contained in:
云服务部-叶林立
2026-08-19 14:35:19 +08:00
parent 198584daf8
commit 410c50a3e5
809 changed files with 157793 additions and 139 deletions
@@ -0,0 +1,182 @@
---
issue: 314
issue_title: "Split tool-input-preview.ts into cohesive modules"
---
# Split `tool-input-preview.ts` into cohesive modules
## Problem Statement
`src/tool-input-preview.ts` is the package's sole remaining `fallow` refactoring target: complexity density 0.33 (above the 0.3 threshold) with 6 dependents amplifying every change.
The module is a flat bag of 8 functions mixing three unrelated concerns — prompt formatting, text utilities, and serialization.
The density comes almost entirely from the prompt formatters (`formatEditInputForPrompt` in particular).
Separating the prompt-formatting concern into its own module gives that concern a clear home and lets the remaining utility module fall back under the density threshold.
## Goals
- Extract the three prompt formatters (`formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt`) plus their shared `getPromptPath` helper into a new `src/tool-input-prompt-formatters.ts`.
- Keep `tool-input-preview.ts` as the home for the text utilities (`truncateInlineText`, `countTextLines`, `formatCount`), `serializeToolInputPreview`, and the three limit constants.
- Update every consumer to import from the new boundary; verify a consumer imports each new export so `fallow` does not flag a dead re-export.
- Behavior-preserving — every existing test stays green; no behavior moves.
- Drop `tool-input-preview.ts` below the 0.3 density threshold and off the refactoring-target list (targets 1 → 0).
## Non-Goals
- No renaming, signature change, or behavior change to any moved or retained function.
- No change to `ToolPreviewFormatter` (`tool-preview-formatter.ts`) beyond its import statements — its dispatch logic and methods are untouched.
- No change to the configurable-limit plumbing (`resolveToolPreviewLimits`, the constants' fallback role).
- The other Phase 3 roadmap steps (#315#321) are independent follow-ups and are out of scope.
## Background
Relevant modules:
- `src/tool-input-preview.ts` — the target.
Exports 3 constants + 8 functions.
Imports `getNonEmptyString`, `toRecord` from `./common` and `safeJsonStringify` from `./logging`.
- `src/tool-preview-formatter.ts` — the `ToolPreviewFormatter` class (extracted in [#282]).
Imports the three prompt formatters, `getPromptPath`, `serializeToolInputPreview`, `truncateInlineText`, and the three constants from `tool-input-preview.ts`.
It calls the prompt formatters in its `formatToolInputForPrompt` dispatch switch and calls `getPromptPath` in `formatSearchInputForPrompt`.
- `src/builtin-tool-input-formatters.ts` — imports only `truncateInlineText` (a retained utility); no change.
Consumer audit (every importer of the four symbols being moved):
| Importer | Imports being moved | Action |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `src/tool-preview-formatter.ts` | `formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt`, `getPromptPath` | Repoint these four to the new module; keep `serializeToolInputPreview`, `truncateInlineText`, constants from `tool-input-preview` |
| `test/tool-input-preview.test.ts` | `formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt`, `getPromptPath` | Move these four describe blocks into the new sibling test file |
| `src/builtin-tool-input-formatters.ts` | (none — only `truncateInlineText`) | No change |
| `test/permission-prompts.test.ts` | (none — only the 3 constants) | No change |
| `test/tool-preview-formatter.test.ts` | (none — only the 3 constants) | No change |
| `test/handlers/gates/tool.test.ts` | (none — only the 3 constants) | No change |
Constraints from AGENTS.md and the package skill that apply:
- Within a package, import siblings via the `#src/` / `#test/` path aliases (test files), and `./` relative imports (production files, matching existing style in `tool-input-preview.ts` and `tool-preview-formatter.ts`).
- `@typescript-eslint/require-await` is on for `src/` — not relevant here (no async).
- When a step removes an export, every importing module and its tests break at the type level in the same commit — fold the extraction, the consumer update, and the consumer-test update into one step (lesson from the [#282] retro, which split extraction from consumer threading and found it unbuildable).
- When a module is moved/added, update `docs/architecture/` listings that reference it.
## Design Overview
This is a cohesion split by concern, not statement-level procedure-splitting: each moved function is already a complete, independently-tested pure function that returns a value, and the three prompt formatters + their shared `getPromptPath` helper form a single cohesive concern (rendering tool input for a permission prompt).
### New module: `src/tool-input-prompt-formatters.ts`
Holds the prompt-formatting concern.
The three formatters call `countTextLines` and `formatCount` (text utilities that stay in `tool-input-preview.ts`), and `getPromptPath` calls `getNonEmptyString` from `./common`.
```typescript
import { getNonEmptyString, toRecord } from "./common";
import { countTextLines, formatCount } from "./tool-input-preview";
export function getPromptPath(input: Record<string, unknown>): string | null { /* unchanged */ }
export function formatEditInputForPrompt(input: Record<string, unknown>): string { /* unchanged */ }
export function formatWriteInputForPrompt(input: Record<string, unknown>): string { /* unchanged */ }
export function formatReadInputForPrompt(input: Record<string, unknown>): string { /* unchanged */ }
```
Dependency direction: `tool-input-prompt-formatters.ts``tool-input-preview.ts` (one-way; the utilities have no knowledge of the formatters).
No cycle: `tool-input-preview.ts` will no longer reference any prompt formatter after the move.
### Retained module: `src/tool-input-preview.ts`
After the move it holds only the text utilities, serialization, and constants.
Its `./common` import drops to nothing — `getNonEmptyString` was used only by `getPromptPath` and `toRecord` only by `formatEditInputForPrompt`, both of which move out.
Only the `safeJsonStringify` import from `./logging` (used by `serializeToolInputPreview`) remains.
```typescript
import { safeJsonStringify } from "./logging";
export const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
export const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
export const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
export function truncateInlineText(value: string, maxLength: number): string { /* unchanged */ }
export function countTextLines(value: string): number { /* unchanged */ }
export function formatCount(value: number, singular: string, plural: string): string { /* unchanged */ }
export function serializeToolInputPreview(input: unknown): string { /* unchanged */ }
```
### Consumer call site (`tool-preview-formatter.ts`)
The class's dispatch and search-formatting call sites are unchanged; only the import source changes.
```typescript
// imports split across two modules:
import { serializeToolInputPreview, truncateInlineText,
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH, TOOL_INPUT_PREVIEW_MAX_LENGTH,
TOOL_TEXT_SUMMARY_MAX_LENGTH } from "./tool-input-preview";
import { formatEditInputForPrompt, formatReadInputForPrompt,
formatWriteInputForPrompt, getPromptPath } from "./tool-input-prompt-formatters";
```
This follows Tell-Don't-Ask and the Law of Demeter trivially — the consumer calls free functions with the data they need and uses the returned string.
No new collaborator, no output argument, no reach-through is introduced.
### Dead-re-export check
All four new exports (`getPromptPath`, `formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt`) are imported by `tool-preview-formatter.ts`, so `fallow` will see a live consumer for each.
No barrel (`index.ts`) re-exports these symbols — `src/index.ts` does not reference `tool-input-preview`, so no barrel update is needed.
### Edge cases
- No behavior changes, so no new edge cases.
The moved functions' existing edge-case tests (empty edits, missing path, CR/LF/CRLF line counting, offset/limit absence) move with them verbatim.
## Module-Level Changes
| File | Change |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/tool-input-prompt-formatters.ts` | **New.** Add `getPromptPath`, `formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt` (verbatim). Import `getNonEmptyString`, `toRecord` from `./common` and `countTextLines`, `formatCount` from `./tool-input-preview`. |
| `src/tool-input-preview.ts` | Remove the four moved functions. Change the `./common` import line — drop `getNonEmptyString` and `toRecord` (now unused here); keep only `import { safeJsonStringify } from "./logging"`. |
| `src/tool-preview-formatter.ts` | Split the import: keep `serializeToolInputPreview`, `truncateInlineText`, and the three constants from `./tool-input-preview`; move `formatEditInputForPrompt`, `formatReadInputForPrompt`, `formatWriteInputForPrompt`, `getPromptPath` to a new import from `./tool-input-prompt-formatters`. |
| `test/tool-input-prompt-formatters.test.ts` | **New.** Move the `getPromptPath`, `formatEditInputForPrompt`, `formatWriteInputForPrompt`, `formatReadInputForPrompt` describe blocks here, importing from `#src/tool-input-prompt-formatters`. No `vi.mock("../src/logging.js")` needed (these functions never serialize). |
| `test/tool-input-preview.test.ts` | Remove the four moved describe blocks and their imports. Keep `constants`, `truncateInlineText`, `countTextLines`, `formatCount`, `serializeToolInputPreview` blocks and the `logging` mock. |
| `docs/architecture/architecture.md` | Module listing (~line 537): add `tool-input-prompt-formatters.ts` entry, refine `tool-input-preview.ts` description. Health table (~line 769): `Refactoring targets` 1 (medium) → 0. Finding #2 (~line 782): mark resolved. Roadmap step 1 (~line 790): mark ✅ shipped. |
| `docs/architecture/v3-architecture.md` | Module listing (~line 67): add `tool-input-prompt-formatters.ts`; refine `tool-input-preview.ts` description. |
The pre-[#266] finding block (~lines 580600) is a historical snapshot of a completed phase — leave it as-is.
## Test Impact Analysis
1. New unit tests the extraction enables: none.
The three prompt formatters and `getPromptPath` are already exported pure functions with direct unit coverage in `tool-input-preview.test.ts`.
The extraction relocates that coverage into a sibling test file matching the new module boundary; it does not unlock previously-impractical tests.
2. Tests that become redundant: none.
No higher-level test duplicates the moved functions' coverage; the moved describe blocks remain the sole coverage and simply change file.
3. Tests that must stay as-is: the retained blocks in `tool-input-preview.test.ts` (`constants`, `truncateInlineText`, `countTextLines`, `formatCount`, `serializeToolInputPreview`) genuinely exercise the utilities staying in that module — they stay verbatim with the `logging` mock intact.
## TDD Order
This is behavior-preserving.
Because removing the four exports from `tool-input-preview.ts` breaks `tool-preview-formatter.ts` and `tool-input-preview.test.ts` at the type level in the same commit, the extraction, the consumer update, and both test-file edits must land together (the [#282] retro lesson).
No new red test is written — the existing suite is the regression net.
1. `refactor:` — Create `src/tool-input-prompt-formatters.ts` with the four functions; remove them from `tool-input-preview.ts` and drop its now-unused `./common` import; repoint `tool-preview-formatter.ts` imports; create `test/tool-input-prompt-formatters.test.ts` with the four moved describe blocks and trim them out of `test/tool-input-preview.test.ts`.
Test surface: `test/tool-input-prompt-formatters.test.ts` (relocated coverage) + the trimmed `test/tool-input-preview.test.ts`; the full suite stays green.
Verify with `pnpm --filter @gotgenes/pi-permission-system run check` and `... run test`.
Commit: `refactor: split prompt formatters into tool-input-prompt-formatters (#314)`
2. `docs:` — Update `docs/architecture/architecture.md` (module listing, health table `Refactoring targets` 1 → 0, finding #2 resolved, roadmap step 1 ✅) and `docs/architecture/v3-architecture.md` (module listing).
Optionally re-run `fallow health --targets` to confirm 0 targets and cite the figure.
Commit: `docs: record tool-input-preview split in architecture (#314)`
## Risks and Mitigations
- Risk: a missed importer of a moved symbol breaks the build.
Mitigation: the consumer audit table above is exhaustive (grep of `src/` and `test/` for all four symbols); removing the exports makes TypeScript flag any miss immediately in step 1, and `pnpm run check` runs before commit.
- Risk: `fallow` flags a new module export as a dead re-export.
Mitigation: all four new exports are consumed by `tool-preview-formatter.ts`; no speculative re-exports are added and no barrel re-exports them.
- Risk: density does not fall below 0.3 after the split.
Mitigation: `formatEditInputForPrompt` (the dominant contributor) moves out, so the retained module is four small utilities + constants; step 2 re-runs `fallow health --targets` to confirm targets 1 → 0 before recording the outcome.
- Risk: introduced import cycle between the two modules.
Mitigation: dependency is strictly one-way (`tool-input-prompt-formatters.ts``tool-input-preview.ts`); the utility module references no formatter after the move.
## Open Questions
- None.
The proposed change is unambiguous and behavior-preserving.
[#266]: https://github.com/gotgenes/pi-packages/issues/266
[#282]: https://github.com/gotgenes/pi-packages/issues/282