Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/core/store/settings-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ const FAIL: Outcome = { ok: false };
const vString: Check = (input) => (typeof input === "string" ? { ok: true, value: input } : FAIL);
const vBoolean: Check = (input) => (typeof input === "boolean" ? { ok: true, value: input } : FAIL);

/**
* Validates that the input is a number, optionally requiring integer-ness and positivity.
*
* @param options - Validation modifiers:
* - `int`: require the number to be an integer
* - `positive`: require the number to be greater than zero
* @returns `{ ok: true, value: number }` if the input satisfies the checks, `{ ok: false }` otherwise.
*/
function vNumber(options: { int?: boolean; positive?: boolean } = {}): Check {
return (input) => {
if (typeof input !== "number" || Number.isNaN(input)) {
Expand All @@ -119,10 +127,22 @@ function vNumber(options: { int?: boolean; positive?: boolean } = {}): Check {
};
}

/**
* Creates a validator that accepts only the specified string literals.
*
* @param allowed - The allowed string values for the validator (variadic).
* @returns An Outcome with `ok: true` and the validated string when the input matches one of `allowed`, otherwise the failure outcome.
*/
function vLiteral(...allowed: string[]): Check {
return (input) => (typeof input === "string" && allowed.includes(input) ? { ok: true, value: input } : FAIL);
}

/**
* Creates a validator that accepts arrays whose elements pass the given item validator.
*
* @param item - Validator applied to each array element; every element must pass for the array to be valid
* @returns An Outcome: `ok: true` with an array of validated element values when `input` is an array and all elements pass `item`, or `ok: false` on failure
*/
function vArray(item: Check): Check {
return (input) => {
if (!Array.isArray(input)) {
Expand All @@ -140,10 +160,24 @@ function vArray(item: Check): Check {
};
}

/**
* Creates a validator that treats `undefined` as an absent (optional) value.
*
* @param inner - Validator to run when the input is not `undefined`
* @returns `OK_ABSENT` if the input is `undefined`, otherwise the validation outcome produced by `inner`
*/
function vOptional(inner: Check): Check {
return (input) => (input === undefined ? OK_ABSENT : inner(input));
}

/**
* Validates that an input is a plain object and returns a new object containing only the validated keys defined by `shape`.
*
* The input must be a non-null, non-array object. Each key in `shape` is validated against the corresponding value from the input; if any check fails, validation fails. Optional checks that return `undefined` cause the key to be omitted from the output. Any keys not listed in `shape` are dropped.
*
* @param shape - A record mapping object keys to `Check` validator functions that validate and transform each field
* @returns An `Outcome` with `{ ok: true, value: Record<string, unknown> }` containing only successfully validated keys, or `FAIL` if the input is not an object or any field check fails
*/
Comment on lines +178 to +180
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Avoid referencing the internal FAIL constant in the public-facing JSDoc return description.

Referencing FAIL here exposes an internal detail that callers don’t need. Please describe the failure case in terms of the public return shape instead (e.g. “or { ok: false } if the input is not an object or any field check fails”).

Suggested change
* @param shape - A record mapping object keys to `Check` validator functions that validate and transform each field
* @returns An `Outcome` with `{ ok: true, value: Record<string, unknown> }` containing only successfully validated keys, or `FAIL` if the input is not an object or any field check fails
*/
* @param shape - A record mapping object keys to `Check` validator functions that validate and transform each field
* @returns An `Outcome` with `{ ok: true, value: Record<string, unknown> }` containing only successfully validated keys, or `{ ok: false }` if the input is not an object or any field check fails
*/

function vObject(shape: Record<string, Check>): Check {
return (input) => {
if (typeof input !== "object" || input === null || Array.isArray(input)) {
Expand Down Expand Up @@ -401,7 +435,11 @@ const settingsCheck = vObject({
}),
});

/** Validate raw settings, returning stripped, type-checked data or failure (matching the legacy zod safeParse). */
/**
* Validate raw settings and produce a validated, unknown-key-stripped settings object.
*
* @returns The validation result: `{ success: true, data: ParsedSettings }` when validation succeeds, or `{ success: false }` when validation fails.
*/
export function validateSettings(raw: unknown): SettingsValidationResult {
const result = settingsCheck(raw);
if (!result.ok) {
Expand Down
25 changes: 25 additions & 0 deletions src/core/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,16 @@ function normalizeItemTypeDefinition(definition: ItemTypeDefinition): ItemTypeDe
};
}

/**
* Produce a normalized, deduplicated, and alphabetically ordered list of item type definitions.
*
* Treats `undefined` as an empty input and drops any invalid definitions. When multiple definitions
* share the same name (case-insensitive), only one entry per name is kept. The returned list is
* sorted by `name` using locale-aware string comparison.
*
* @param definitions - The input list of item type definitions to normalize (may be `undefined`)
* @returns An array of validated `ItemTypeDefinition` objects with duplicates removed and sorted by name
*/
export function normalizeItemTypeDefinitions(definitions: ItemTypeDefinition[] | undefined): ItemTypeDefinition[] {
const normalized = (definitions ?? [])
.map((definition) => normalizeItemTypeDefinition(definition))
Expand All @@ -374,6 +384,12 @@ export function normalizeItemTypeDefinitions(definitions: ItemTypeDefinition[] |
return [...dedupedByName.values()].sort((left, right) => left.name.localeCompare(right.name));
}

/**
* Merge a validated settings object with defaults and produce a fully populated, normalized settings object.
*
* @param settings - A validated ParsedSettings input (already conforming to the expected schema).
* @returns A complete PmSettings with defaults applied, governance knobs resolved, `item_format` coerced when required, and nested sections normalized (validation, agent_guidance, item_types, schema, extensions, providers, vector_store, context, search, telemetry, workflow, testing, etc.).
*/
function mergeSettings(settings: ParsedSettings): PmSettings {
const defaults = cloneDefaults();
const governance = resolveGovernanceKnobs({
Expand Down Expand Up @@ -677,6 +693,15 @@ export function serializeSettings(settings: PmSettings): string {
return `${JSON.stringify(ordered, null, 2)}\n`;
}

/**
* Read project settings from disk, validate and merge them with defaults, and return settings plus metadata and warnings.
*
* @param pmRoot - Filesystem path of the project root used to locate the settings file
* @returns An object containing:
* - `settings`: the merged and normalized project settings
* - `metadata.has_explicit_item_format`: `true` when the original file explicitly specified an item format, `false` otherwise
* - `warnings`: array of warning codes or messages produced while reading, validating, merging, or scaffolding settings
*/
export async function readSettingsWithMetadata(pmRoot: string): Promise<SettingsReadResult> {
const settingsPath = getSettingsPath(pmRoot);
const raw = await readFileIfExists(settingsPath);
Expand Down