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:
parent
55384e5b39
commit
ad2dc5759a
2 changed files with 69 additions and 2 deletions
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { parseManifest, OPTIONAL_KEYS } from '../../lib/parsers/manifest-yaml.mjs';
|
||||
import { parseManifest, OPTIONAL_KEYS, OPTIONAL_STRING_KEYS } from '../../lib/parsers/manifest-yaml.mjs';
|
||||
|
||||
const BASE = `### Step 1: Cover
|
||||
- Manifest:
|
||||
|
|
@ -90,3 +90,44 @@ test('extension does NOT break REQUIRED_KEYS contract', () => {
|
|||
assert.ok(k in r.parsed, `required key ${k} missing after extension`);
|
||||
}
|
||||
});
|
||||
|
||||
// v4.1 Step 3 — OPTIONAL_STRING_KEYS dispatch (profile_used)
|
||||
|
||||
test('OPTIONAL_STRING_KEYS exports profile_used', () => {
|
||||
assert.deepEqual(
|
||||
[...OPTIONAL_STRING_KEYS].sort(),
|
||||
['profile_used'].sort(),
|
||||
'OPTIONAL_STRING_KEYS export drift — pin contract',
|
||||
);
|
||||
});
|
||||
|
||||
test('profile_used: economy parses successfully (SC #10 forward-compat)', () => {
|
||||
const r = parseManifest(bodyWithExtras(' profile_used: economy'));
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.profile_used, 'economy');
|
||||
});
|
||||
|
||||
test('profile_used: numeric rejected with MANIFEST_OPTIONAL_TYPE', () => {
|
||||
const r = parseManifest(bodyWithExtras(' profile_used: 42'));
|
||||
assert.equal(r.valid, false);
|
||||
const found = r.errors.find(e => e.code === 'MANIFEST_OPTIONAL_TYPE');
|
||||
assert.ok(found, `expected MANIFEST_OPTIONAL_TYPE, got: ${JSON.stringify(r.errors)}`);
|
||||
assert.match(found.message, /profile_used/);
|
||||
assert.match(found.message, /string/);
|
||||
});
|
||||
|
||||
test('absence of profile_used: field is NOT in parsed (NOT defaulted, unlike boolean)', () => {
|
||||
const r = parseManifest(bodyOnlyRequired());
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
// Absence semantics differ from boolean: parsed should NOT contain the key
|
||||
assert.equal('profile_used' in r.parsed, false,
|
||||
'profile_used must NOT be auto-defaulted when absent — string-key semantics');
|
||||
});
|
||||
|
||||
test('profile_used works alongside boolean optional keys (skip_commit_check + memory_write)', () => {
|
||||
const r = parseManifest(bodyWithExtras(' skip_commit_check: true\n memory_write: true\n profile_used: balanced'));
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.skip_commit_check, true);
|
||||
assert.equal(r.parsed.memory_write, true);
|
||||
assert.equal(r.parsed.profile_used, 'balanced');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue