mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
214 lines
14 KiB
Markdown
214 lines
14 KiB
Markdown
---
|
||
issue: 332
|
||
issue_title: "`toolInputPreviewMaxLength` (and `toolTextSummaryMaxLength`) in `config.json` are silently ignored — preview is always truncated at the hardcoded default"
|
||
---
|
||
|
||
# Fix `toolInputPreviewMaxLength` / `toolTextSummaryMaxLength` loader gap
|
||
|
||
## Problem Statement
|
||
|
||
Setting `toolInputPreviewMaxLength` or `toolTextSummaryMaxLength` in `config.json` has no effect.
|
||
The permission-prompt preview is always truncated at the hardcoded default of 200 (or 80 for text summaries), regardless of the configured value.
|
||
|
||
The downstream machinery is correct: `normalizePermissionSystemConfig()` parses both fields, `resolveToolPreviewLimits()` reads them, and `ToolPreviewFormatter` applies them.
|
||
The break is in the loader pipeline.
|
||
`loadAndMergeConfigs()` produces a `UnifiedPermissionConfig` (`src/config-loader.ts`) as its intermediate type, and that type does **not** declare the two fields.
|
||
As a result:
|
||
|
||
- `normalizeUnifiedConfig()` never reads the fields from raw JSON — they are silently dropped.
|
||
- `mergeUnifiedConfigs()` iterates only over `["debugLog", "permissionReviewLog", "yoloMode"]` when merging scalars, so even a present value would not survive the merge.
|
||
- By the time `ConfigStore.refresh()` calls `normalizePermissionSystemConfig(mergeResult.merged)`, the fields are already gone, so `resolveToolPreviewLimits()` falls back to the hardcoded constants.
|
||
|
||
A secondary symptom exists in `ConfigStore.save()` (`src/config-store.ts`).
|
||
The merge `{ ...existing.config, debugLog, permissionReviewLog, yoloMode }` loads `existing.config` through the same broken loader, so a user-set value is dropped from `existing.config` and therefore deleted from the global file on the next modal save.
|
||
This symptom resolves automatically once the loader is fixed: the `...existing.config` spread will carry the parsed fields through unchanged (see Design Overview).
|
||
|
||
## Goals
|
||
|
||
- Make `toolInputPreviewMaxLength` and `toolTextSummaryMaxLength` flow through the load/merge pipeline so configured values reach `ToolPreviewFormatter`.
|
||
- Ensure `ConfigStore.save()` preserves an existing global value rather than deleting it.
|
||
- Keep schema, example config, docs, loader, and TypeScript types aligned.
|
||
|
||
## Non-Goals
|
||
|
||
- The `toolInputFormatters` extension point and smart MCP formatters from the parent enhancement (#266) — already shipped; not touched here.
|
||
- Editing the two preview-length fields from the `/permission-system` config modal UI — out of scope.
|
||
- Changing the merge semantics of the existing scalar knobs or the `permission` object.
|
||
- Schema, `config/config.example.json`, and `docs/configuration.md` field documentation — already present and correct (verified during planning); no edits needed.
|
||
|
||
## Background
|
||
|
||
Relevant modules:
|
||
|
||
- `src/config-loader.ts` — owns `UnifiedPermissionConfig`, `normalizeUnifiedConfig()`, `mergeUnifiedConfigs()`, `loadAndMergeConfigs()`, `loadUnifiedConfig()`.
|
||
This is the loader layer; it currently imports only `./common`, `./config-paths`, `./permission-merge`, and `./types`.
|
||
- `src/extension-config.ts` — owns `PermissionSystemExtensionConfig`, `normalizePermissionSystemConfig()`, and `normalizeOptionalPositiveInt()`.
|
||
This is the higher-level config-shape layer.
|
||
- `src/common.ts` — shared, dependency-light helpers (`toRecord`, `getNonEmptyString`, `isPermissionState`, …).
|
||
Both `config-loader.ts` and `extension-config.ts` already import from it; it imports nothing from either.
|
||
- `src/config-store.ts` — `ConfigStore.refresh()` (load → normalize → store) and `ConfigStore.save()` (load existing → merge → write global).
|
||
- `src/tool-preview-formatter.ts` — `resolveToolPreviewLimits()` and `ToolPreviewFormatter` (the correct, already-wired consumer).
|
||
|
||
Constraint from the package skill: "Keep schema, example config, `docs/configuration.md`, `README.md`, and TypeScript types/loaders aligned."
|
||
Verified during planning that the schema (`schemas/permissions.schema.json`), example (`config/config.example.json`), and `docs/configuration.md` all already document both fields — only the loader is out of sync.
|
||
|
||
Constraint from the package skill: "Treat any declared config field not read at runtime as a maintenance trap."
|
||
This plan closes exactly such a trap: the fields are declared on `PermissionSystemExtensionConfig` and documented, but never read from disk.
|
||
|
||
## Design Overview
|
||
|
||
### Shared `normalizeOptionalPositiveInt`
|
||
|
||
The loader needs the same positive-integer normalization that `extension-config.ts` already uses.
|
||
`normalizeOptionalPositiveInt` is currently exported from `extension-config.ts`.
|
||
Importing it into `config-loader.ts` would not create a literal import cycle today (verified: neither module imports the other), but it would make the low-level loader depend on the higher-level config-shape module — the wrong direction.
|
||
|
||
Move `normalizeOptionalPositiveInt` to `src/common.ts` (the dependency-light shared module both layers already import) and re-export nothing speculative.
|
||
`extension-config.ts` imports it from `common`; `config-loader.ts` imports it from `common`.
|
||
|
||
### Loader changes (the actual fix)
|
||
|
||
Add the two fields to `UnifiedPermissionConfig`, parse them in `normalizeUnifiedConfig()`, and include them in the `mergeUnifiedConfigs()` scalar loop:
|
||
|
||
```typescript
|
||
export interface UnifiedPermissionConfig {
|
||
debugLog?: boolean;
|
||
permissionReviewLog?: boolean;
|
||
yoloMode?: boolean;
|
||
toolInputPreviewMaxLength?: number;
|
||
toolTextSummaryMaxLength?: number;
|
||
permission?: FlatPermissionConfig;
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// in normalizeUnifiedConfig()
|
||
const toolInputPreviewMaxLength = normalizeOptionalPositiveInt(
|
||
record.toolInputPreviewMaxLength,
|
||
);
|
||
if (toolInputPreviewMaxLength !== undefined)
|
||
config.toolInputPreviewMaxLength = toolInputPreviewMaxLength;
|
||
|
||
const toolTextSummaryMaxLength = normalizeOptionalPositiveInt(
|
||
record.toolTextSummaryMaxLength,
|
||
);
|
||
if (toolTextSummaryMaxLength !== undefined)
|
||
config.toolTextSummaryMaxLength = toolTextSummaryMaxLength;
|
||
```
|
||
|
||
```typescript
|
||
// in mergeUnifiedConfigs()
|
||
for (const key of [
|
||
"debugLog",
|
||
"permissionReviewLog",
|
||
"yoloMode",
|
||
"toolInputPreviewMaxLength",
|
||
"toolTextSummaryMaxLength",
|
||
] as const) {
|
||
const value = override[key] ?? base[key];
|
||
if (value !== undefined) {
|
||
merged[key] = value;
|
||
}
|
||
}
|
||
```
|
||
|
||
Merge semantics match the existing scalars: override (project / per-agent) replaces base (global) when present; last writer wins.
|
||
This matches how `mergeFlatPermissions` treats per-surface overrides and is what users expect from the documented precedence (global → project → per-agent).
|
||
|
||
### Save path (no code change — relies on the spread)
|
||
|
||
`ConfigStore.save()` already merges via:
|
||
|
||
```typescript
|
||
const existing = loadUnifiedConfig(globalPath); // now parses both fields
|
||
const merged = {
|
||
...existing.config, // carries toolInputPreviewMaxLength / toolTextSummaryMaxLength through unchanged
|
||
debugLog: normalized.debugLog,
|
||
permissionReviewLog: normalized.permissionReviewLog,
|
||
yoloMode: normalized.yoloMode,
|
||
};
|
||
```
|
||
|
||
Once `loadUnifiedConfig()` parses the two fields, `existing.config` carries them, and the spread preserves whatever is in the global file verbatim.
|
||
No further change to `save()` is needed.
|
||
|
||
This is deliberately preferred over explicitly writing `normalized.toolInputPreviewMaxLength` into the merge (the issue's proposed fix).
|
||
The in-memory `normalized` config is the *merged* value (global + project + per-agent); writing it into the global file would bake a project-level or per-agent override into global.
|
||
The three booleans are editable in the modal, so persisting their merged value to global is the intended save behavior; the two preview-length fields are not modal-editable, so the correct behavior is to leave the on-disk global value untouched — exactly what the spread does. (Decision confirmed with the user during planning.)
|
||
|
||
### Edge cases
|
||
|
||
- Invalid values (zero, negative, non-integer, non-number) — `normalizeOptionalPositiveInt` returns `undefined`, so the field is omitted and the default applies.
|
||
Same semantics already enforced by `normalizePermissionSystemConfig`.
|
||
- Field present in project but not global — survives the merge as the override; `refresh()` picks it up.
|
||
- Field present in global, modal save toggles a boolean — spread preserves the global value; nothing deleted.
|
||
|
||
## Module-Level Changes
|
||
|
||
- `src/common.ts` — add `normalizeOptionalPositiveInt` (moved verbatim from `extension-config.ts`).
|
||
- `src/extension-config.ts` — remove the local `normalizeOptionalPositiveInt` definition; import it from `./common`.
|
||
`normalizePermissionSystemConfig` keeps using it unchanged.
|
||
- `src/config-loader.ts` — add the two optional fields to `UnifiedPermissionConfig`; import `normalizeOptionalPositiveInt` from `./common`; parse both fields in `normalizeUnifiedConfig()`; extend the `mergeUnifiedConfigs()` scalar loop.
|
||
- `src/config-store.ts` — no change (the spread in `save()` does the work once the loader is fixed).
|
||
- `test/common.test.ts` — receive the migrated `normalizeOptionalPositiveInt` unit tests.
|
||
- `test/extension-config.test.ts` — drop the `normalizeOptionalPositiveInt` direct tests (now in `common.test.ts`); keep the `normalizePermissionSystemConfig` tests that exercise the two fields end-to-end through the higher-level normalizer.
|
||
- `test/config-loader.test.ts` — add coverage for parsing and merging the two fields.
|
||
- `test/config-store.test.ts` — add a regression test that `save()` preserves an existing global preview-length value.
|
||
|
||
No exports are removed except the relocation of `normalizeOptionalPositiveInt` from `extension-config.ts` to `common.ts`.
|
||
|
||
### Consumers of the relocated symbol
|
||
|
||
`normalizeOptionalPositiveInt` is currently imported from `extension-config` by:
|
||
|
||
- `test/extension-config.test.ts` (direct unit tests) — move these to `test/common.test.ts`.
|
||
|
||
No `src/` module other than `extension-config.ts` itself imports it today, and the package skill (`.pi/skills/package-pi-permission-system/SKILL.md`) does not reference it.
|
||
After the move, both `extension-config.ts` and `config-loader.ts` import it from `common`.
|
||
|
||
## Test Impact Analysis
|
||
|
||
This is a bug fix plus a small symbol relocation, not an extraction that unlocks new isolated units.
|
||
|
||
1. New tests enabled: loader-level tests for the two fields (parse in `normalizeUnifiedConfig`, survive `mergeUnifiedConfigs`) — previously the fields could not be exercised at the loader layer because the type omitted them.
|
||
A `save()` preservation regression test that was meaningless before (the value never reached `existing.config`).
|
||
2. Redundant tests: none become redundant.
|
||
The `normalizeOptionalPositiveInt` direct tests are relocated, not deleted — they continue to assert the same contract from `common.test.ts`.
|
||
The `normalizePermissionSystemConfig` field tests in `extension-config.test.ts` stay; they verify the higher-level normalizer, a different layer from the loader.
|
||
3. Tests that must stay as-is: the `normalizePermissionSystemConfig` and `resolveToolPreviewLimits` suites genuinely exercise the downstream layers that were always correct; they remain unchanged.
|
||
|
||
## TDD Order
|
||
|
||
1. Relocate `normalizeOptionalPositiveInt` to `common`.
|
||
Move the function to `src/common.ts`, import it into `src/extension-config.ts`, and move its direct unit tests from `test/extension-config.test.ts` to `test/common.test.ts`.
|
||
This is a single atomic step: removing the export from `extension-config.ts` and updating its sole test consumer must land together so the type checker stays green.
|
||
Suggested commit: `refactor: move normalizeOptionalPositiveInt to common module`.
|
||
2. Parse the two fields in the loader (red → green).
|
||
Add the fields to `UnifiedPermissionConfig`, import `normalizeOptionalPositiveInt` from `common`, parse both in `normalizeUnifiedConfig()`.
|
||
Add `test/config-loader.test.ts` cases: valid positive integers are parsed; invalid values (0, negative, float, string) are omitted; absent fields stay absent.
|
||
Suggested commit: `fix: parse tool preview length fields in unified config loader`.
|
||
3. Merge the two fields (red → green).
|
||
Extend the `mergeUnifiedConfigs()` scalar loop.
|
||
Add `test/config-loader.test.ts` cases: override value wins over base; base value survives when override omits it; both absent yields absent.
|
||
Suggested commit: `fix: merge tool preview length fields across config layers`.
|
||
4. Regression-test save-path preservation (red → green).
|
||
Add a `test/config-store.test.ts` case under `save()` asserting that when `loadUnifiedConfig` returns a config containing `toolInputPreviewMaxLength`, the written global config retains it.
|
||
With steps 2–3 in place the spread already preserves it, so this test confirms the secondary symptom is closed.
|
||
Suggested commit: `test: confirm save preserves configured tool preview length`.
|
||
|
||
If step 4 passes immediately on the parse/merge fix (expected), keep it as a guard rather than forcing a separate production change.
|
||
|
||
## Risks and Mitigations
|
||
|
||
- Risk: the relocation of `normalizeOptionalPositiveInt` breaks an unseen importer.
|
||
Mitigation: grep `src/` and `test/` for the symbol before finalizing (done during planning — only `extension-config.ts` and `extension-config.test.ts` reference it); the type checker catches any miss in step 1.
|
||
- Risk: merge precedence surprises (e.g. project value unexpectedly overriding a global value).
|
||
Mitigation: precedence mirrors the existing booleans and the documented global → project → per-agent order; step 3 tests both directions.
|
||
- Risk: baking a merged override into the global file on save.
|
||
Mitigation: rely on the `...existing.config` spread rather than writing the in-memory merged value; step 4 guards the global-preservation behavior.
|
||
|
||
## Open Questions
|
||
|
||
None.
|
||
The schema, example, and docs already document both fields; the fix is confined to the loader plus a symbol relocation.
|