Repository navigation
Description
Goal
Add a small, opt-in typed definition boundary for tool configuration and extension-specific settings. This is S5 of the incremental refactoring plan: prepare a useful configuration model for a later activation facade without changing runtime behavior or requiring existing extensions to migrate.
S5 does not depend on the deferred source-build/provenance work.
Inspection baseline: 459caa508d57919d904d72d584e89ad79d32add0. Start from current main, retaining the settings and lifecycle characterization and the merged formatter lifetime fixes.
Current limitation
ToolConfig.settingsDefaultsisRecord<string, unknown>.IBaseSettingsdeliberately has an open[key: string]: unknownextension surface.getWorkspaceSettings,getGlobalSettings, andgetExtensionSettingsreturn that unparameterized base type, even when the caller supplied a known tool-specific settings shape.
An extension cannot currently express and retain its settings contract through this boundary without casts. Do not solve that by making the wire contract closed or by inventing a runtime schema framework.
Bounded implementation
- Add an exported typed tool-definition/settings surface. A
ToolDefinition<TSettings>annotation, optionally with a lightweight helper if genuinely useful, should check declared default values and preserve declared custom field types in resolved settings. - Keep definitions structurally usable by existing functions accepting
ToolConfig. Existing unparameterizedToolConfig,IBaseSettings,IInitOptionsand public APIs must remain source-compatible. - Give the three settings resolver entry points a typed path without losing their legacy path. Use the smallest overload/generic/type arrangement; do not propagate generics through every lifecycle class.
- Export the additions through the existing public barrel and document a short, working adoption example.
- Add focused compile-time checks and runtime compatibility assertions using existing tools and settings fixtures.
Exact names and helper signatures may be refined during implementation. Prefer one simple model over parallel configuration systems, builders or adapters that copy/normalize settings.
Example: useful typed adoption
Illustrative names; the complete example must compile against the final public exports.
interface LintSettings {
ignorePatterns: string[];
severity: Record<string, string>;
enabled: boolean;
}
const tool: ToolDefinition<LintSettings> = {
toolId: 'example',
toolDisplayName: 'Example',
toolModule: 'example',
minimumPythonVersion: { major: 3, minor: 10 },
configFiles: ['tool.toml'],
serverScript,
settingsDefaults: {
ignorePatterns: [],
severity: { E: 'Error' },
enabled: false,
},
trackedSettings: [
'args', 'path', 'interpreter',
'ignorePatterns', 'severity', 'envFile',
],
};
const legacyCompatible: ToolConfig = tool;
const resolved = await getWorkspaceSettings('example', workspace, tool);
const patterns: string[] = resolved.ignorePatterns;
const enabled: boolean = resolved.enabled;
const futureOption: unknown = resolved.futureOption;enabled: false is a default, not a promise that every resolved value is literally false. Required/optional fields must be modeled honestly: do not promise a required custom result field that the definition/resolver never materializes. Normal interfaces should work without requiring consumers to add an open index signature or use any.
Examples of compile-time failures to protect:
- A string supplied for the declared boolean
enableddefault. - An undeclared default field in an explicitly typed definition.
- Treating resolved
ignorePatternsas a number orenabledas a string. - Using an unknown future wire value without narrowing it.
- A custom declaration that contradicts a common base field's type.
Keep the legacy open configuration path available; new checks must not retroactively reject legacy consumers.
Compatibility invariants
- No runtime payload/default changes. Initialization options remain
{ settings, globalSettings }; do not add a wire version, discriminator, definition object or metadata. - Preserve unknown extension fields and the existing open wire boundary. Typed known properties must not silently become
any. - Preserve workspace
importStrategy: useBundledversus globalfromEnvironment, existing Python fallbacks, raw global workspace path versus workspace URI, empty arrays/false, shallow object replacement, variable-resolution phases andextraPathshandling. settingsDefaultscurrently determines which custom settings are projected. Do not automatically copy every configuration key or change merging/coercion/filtering.- Keep
trackedSettingsexplicit. Do not derive it from defaults or serialized fields. A watched client-only key such asenvFileneed not appear in serialized custom defaults. Stronger key typing is optional only if these legitimate cases remain expressible without unsafe casts. - Leave extension manifest contributions/defaults authoritative where they are today. Do not collapse distinct TS/Python/manifest fallback contexts into a new universal default.
- No activation, scheduler, formatter, interpreter, notification or disposal changes.
The shared producer.json and legacy-partial.json, together with existing TypeScript settings/server tests, are the compatibility oracle. Keep their expected payloads unchanged.
Expected scope
Primarily typescript/src/types.ts, settings.ts, index.ts, focused tests and a short public API example in existing documentation. One small definition module is acceptable if it is clearer than expanding types.ts.
Do not add Python runtime validation, code generation, a new schema manifest, dependencies, lockfile changes, consumer migrations, the activation facade, source-build tools, physical moves or broad formatting. The later facade should be able to consume the new model, but is not implemented here.
Validation and acceptance
- Existing
ToolConfig-based callers/tests compile unchanged; a typed definition passes directly to legacy lifecycle/configuration APIs. - Typed workspace/global/multi-root results retain declared custom field types, required/optional correctness, common field types and unknown future fields.
- Positive and negative checks compile through the public exports, using existing TypeScript compilation and descriptive
@ts-expect-errorwhere appropriate. Ensure CI actually compiles the checks; do not add an unexecuted type-check file or a new type-testing dependency. - Representative legacy linter and formatter/daemon-style configuration examples compile. A real pinned consumer compilation is useful where practical; distinguish it from local type fixtures and report dependency-lock blockers honestly. Deferred build/provenance adoption is not a prerequisite.
- Typed and legacy resolver paths produce identical fixture-backed settings and initialization payloads. Add only a few meaningful runtime cases rather than duplicating the characterization suites.
- Existing TypeScript tests, build, source lint, relevant changed-test lint and
git diff --checkpass. Use.nvmrc; the current reference is Node 24.17.0, with 160 passing TS tests before S5. - No changes to fixture expectations, defaults, wire shape or extension behavior.
Suggested commands from the worktree root:
npm --prefix .\typescript test -- --grep 'settings|Settings|definition|Definition'
npm --prefix .\typescript test
npm --prefix .\typescript run build
npm --prefix .\typescript run lint
git diff --checkThe existing pretest compiles typescript/tests using tsconfig.test.json. Run changed-file ESLint as needed because the standard lint command covers src. The known pre-existing full TypeScript format-check failures are not part of this issue.
Activity