feat(voyage): add OPTIONAL_STRING_KEYS path to manifest-yaml — profile_used additive

Step 3 av v4.1-execute (Wave 1, Session 1).

Legg ny eksportert const OPTIONAL_STRING_KEYS = ['profile_used'] parallel
til eksisterende OPTIONAL_KEYS. Utvid parseManifest med ny dispatch-loop
etter OPTIONAL_BOOLEAN_KEYS. Returnerer MANIFEST_OPTIONAL_TYPE hvis
profile_used finnes men ikke er string.

Forskjell fra OPTIONAL_BOOLEAN_KEYS: absence == not-present (NOT defaulted
til false, unlike boolean). Downstream-konsumenter kan dermed skille mellom
unset og empty-string.

Tester (5 nye, baseline 372 → 377):
- OPTIONAL_STRING_KEYS export drift-pin
- profile_used: economy parses successfully (SC #10 forward-compat)
- profile_used: numeric rejected
- absence: field NOT in parsed (string-key semantics)
- profile_used + skip_commit_check + memory_write co-existence

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-09 09:23:32 +02:00
commit ad2dc5759a
2 changed files with 69 additions and 2 deletions

View file

@ -31,7 +31,20 @@ const OPTIONAL_KEYS = [
const OPTIONAL_BOOLEAN_KEYS = new Set(OPTIONAL_KEYS);
export { OPTIONAL_KEYS };
// Optional string-typed manifest keys (v4.1 Step 3 — additive forward-compat).
// `profile_used`: name of the model profile (economy|balanced|premium|<custom>) the
// step was executed under. Absence is fine (v4.0 manifests have no
// profile concept); presence MUST be a string.
// Unlike OPTIONAL_BOOLEAN_KEYS, absence is NOT defaulted — the field is simply
// missing from `parsed` so downstream consumers can distinguish "not set" from
// "explicitly empty string".
const OPTIONAL_STRING_KEYS = [
'profile_used',
];
const OPTIONAL_STRING_KEYS_SET = new Set(OPTIONAL_STRING_KEYS);
export { OPTIONAL_KEYS, OPTIONAL_STRING_KEYS };
/**
* Extract the first fenced YAML block whose first non-blank line begins with
@ -110,6 +123,19 @@ export function parseManifest(stepBody) {
}
}
// v4.1 Step 3 — string-typed optional keys. Absence == not-present (no default,
// unlike boolean keys above) so downstream can distinguish unset vs empty string.
for (const k of OPTIONAL_STRING_KEYS_SET) {
if (k in parsed) {
if (typeof parsed[k] !== 'string') {
errors.push(issue(
'MANIFEST_OPTIONAL_TYPE',
`${k} must be string if present (got ${typeof parsed[k]})`,
));
}
}
}
return { valid: errors.length === 0, errors, warnings, parsed };
}