fix(gsd): consolidate string-array normalizer functions into shared utility (#1009)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ecb6c6abb
commit
e8de7dfa30
4 changed files with 34 additions and 33 deletions
|
|
@ -74,11 +74,6 @@ export interface DoctorSummary {
|
|||
byCode: Array<{ code: DoctorIssueCode; count: number }>;
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] | undefined {
|
||||
if (!Array.isArray(value)) return undefined;
|
||||
const items = value.filter((item): item is string => typeof item === "string").map(item => item.trim()).filter(Boolean);
|
||||
return items.length > 0 ? Array.from(new Set(items)) : undefined;
|
||||
}
|
||||
|
||||
function validatePreferenceShape(preferences: GSDPreferences): string[] {
|
||||
const issues: string[] = [];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// Zero Pi dependencies — uses only exported helpers from files.ts.
|
||||
|
||||
import { splitFrontmatter, parseFrontmatterMap, extractBoldField } from '../files.js';
|
||||
import { normalizeStringArray } from '../../shared/format-utils.js';
|
||||
|
||||
import type {
|
||||
PlanningRoadmap,
|
||||
|
|
@ -366,11 +367,6 @@ function parseRequiresArray(raw: unknown): PlanningSummaryRequires[] {
|
|||
});
|
||||
}
|
||||
|
||||
function toStringArray(val: unknown): string[] {
|
||||
if (Array.isArray(val)) return val.map(String);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse YAML-like frontmatter lines into a flat key-value map.
|
||||
* Like parseFrontmatterMap but supports hyphenated keys (e.g. `tech-stack:`).
|
||||
|
|
@ -459,14 +455,14 @@ function parseSummaryFrontmatter(fm: Record<string, unknown>): PlanningSummaryFr
|
|||
phase: unquote(fm.phase),
|
||||
plan: unquote(fm.plan),
|
||||
subsystem: unquote(fm.subsystem),
|
||||
tags: toStringArray(fm.tags),
|
||||
tags: normalizeStringArray(fm.tags),
|
||||
requires: parseRequiresArray(fm.requires),
|
||||
provides: toStringArray(fm.provides),
|
||||
affects: toStringArray(fm.affects),
|
||||
'tech-stack': toStringArray(fm['tech-stack']),
|
||||
'key-files': toStringArray(fm['key-files']),
|
||||
'key-decisions': toStringArray(fm['key-decisions']),
|
||||
'patterns-established': toStringArray(fm['patterns-established']),
|
||||
provides: normalizeStringArray(fm.provides),
|
||||
affects: normalizeStringArray(fm.affects),
|
||||
'tech-stack': normalizeStringArray(fm['tech-stack']),
|
||||
'key-files': normalizeStringArray(fm['key-files']),
|
||||
'key-decisions': normalizeStringArray(fm['key-decisions']),
|
||||
'patterns-established': normalizeStringArray(fm['patterns-established']),
|
||||
duration: unquote(fm.duration),
|
||||
completed: unquote(fm.completed),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { PostUnitHookConfig, PreDispatchHookConfig, BudgetEnforcementMode,
|
|||
import type { DynamicRoutingConfig } from "./model-router.js";
|
||||
import { defaultRoutingConfig } from "./model-router.js";
|
||||
import { VALID_BRANCH_NAME } from "./git-service.js";
|
||||
import { normalizeStringArray } from "../shared/format-utils.js";
|
||||
|
||||
const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
|
||||
const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
|
||||
|
|
@ -869,10 +870,10 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
}
|
||||
}
|
||||
|
||||
validated.always_use_skills = normalizeStringList(preferences.always_use_skills);
|
||||
validated.prefer_skills = normalizeStringList(preferences.prefer_skills);
|
||||
validated.avoid_skills = normalizeStringList(preferences.avoid_skills);
|
||||
validated.custom_instructions = normalizeStringList(preferences.custom_instructions);
|
||||
validated.always_use_skills = normalizeStringArray(preferences.always_use_skills);
|
||||
validated.prefer_skills = normalizeStringArray(preferences.prefer_skills);
|
||||
validated.avoid_skills = normalizeStringArray(preferences.avoid_skills);
|
||||
validated.custom_instructions = normalizeStringArray(preferences.custom_instructions);
|
||||
|
||||
if (preferences.skill_rules) {
|
||||
const validRules: GSDSkillRule[] = [];
|
||||
|
|
@ -888,7 +889,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
}
|
||||
const validatedRule: GSDSkillRule = { when };
|
||||
for (const action of SKILL_ACTIONS) {
|
||||
const values = normalizeStringList((rule as unknown as Record<string, unknown>)[action]);
|
||||
const values = normalizeStringArray((rule as unknown as Record<string, unknown>)[action]);
|
||||
if (values.length > 0) {
|
||||
validatedRule[action as keyof GSDSkillRule] = values as never;
|
||||
}
|
||||
|
|
@ -1052,7 +1053,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
errors.push(`duplicate post_unit_hooks name: ${name}`);
|
||||
continue;
|
||||
}
|
||||
const after = normalizeStringList(hook.after);
|
||||
const after = normalizeStringArray(hook.after);
|
||||
if (after.length === 0) {
|
||||
errors.push(`post_unit_hooks "${name}" missing after`);
|
||||
continue;
|
||||
|
|
@ -1119,7 +1120,7 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
errors.push(`duplicate pre_dispatch_hooks name: ${name}`);
|
||||
continue;
|
||||
}
|
||||
const before = normalizeStringList(hook.before);
|
||||
const before = normalizeStringArray(hook.before);
|
||||
if (before.length === 0) {
|
||||
errors.push(`pre_dispatch_hooks "${name}" missing before`);
|
||||
continue;
|
||||
|
|
@ -1382,21 +1383,14 @@ export function validatePreferences(preferences: GSDPreferences): {
|
|||
|
||||
function mergeStringLists(base?: unknown, override?: unknown): string[] | undefined {
|
||||
const merged = [
|
||||
...normalizeStringList(base),
|
||||
...normalizeStringList(override),
|
||||
...normalizeStringArray(base),
|
||||
...normalizeStringArray(override),
|
||||
]
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function mergePostUnitHooks(
|
||||
base?: PostUnitHookConfig[],
|
||||
|
|
|
|||
|
|
@ -93,3 +93,19 @@ export function sparkline(values: number[]): string {
|
|||
export function stripAnsi(s: string): string {
|
||||
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
// ─── String Array Normalization ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Normalize an unknown value to a string array.
|
||||
* Filters to string items, trims whitespace, removes empty strings.
|
||||
* Optionally deduplicates.
|
||||
*/
|
||||
export function normalizeStringArray(value: unknown, options?: { dedupe?: boolean }): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const items = value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
return options?.dedupe ? [...new Set(items)] : items;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue