feat(ms-ai-architect): skjerm Fri kontekst mot overflow-kuttet [skip-docs]

Operatoerbeslutning: naar linje-cappen (25) sprekker, skal fri kontekst
overleve i stedet for aa vaere det foerste feltet som forsvinner.

Fri kontekst legges SIST fordi den skal LESES sist. Under et rent head-slice
gjorde nettopp det den til den foerste som falt ut - og det er den ene sloten
brukeren skriver fritt i, som onboarding-agent.md:125 selv kaller den
enkeltkategorien som gjoer raadene mest treffsikre. Posisjon i sammendraget er
en lese-rekkefoelge, ikke en prioritetsrekkefoelge; kuttet blandet de to.

Ved overflow beholdes naa (cap-2) strukturerte felt + fri kontekst + markoer,
altsaa fortsatt noeyaktig cap linjer. Uten fri kontekst er kuttet uendret.

Dette ble den bindende grensen foerst etter a7098e0: med readdir aapnet kan
brukeren legge til filer fritt, saa terskelen paa 26 felt naas naa av
brukerhandling og ikke bare av design.

Verifisert paa den ekte hooken: 40 felt + fri kontekst gir 25 linjer, med
"Fri kontekst" som siste felt og markoeren "(+22 flere felt)" til slutt.

TDD: 2 nye tester, skjermingstesten roed foerst mot forrige commit. Den andre
pinner at overflow UTEN fri kontekst er uendret (back-compat). Suite 1064/1064.

[skip-docs]: ren intern mekanikk - ingen endring i utoverrettet flate.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-26 10:41:28 +02:00
commit 5a46818dd6
2 changed files with 41 additions and 2 deletions

View file

@ -234,16 +234,33 @@ export function buildOrgSummary(orgFiles, opts = {}) {
// one "Fri kontekst" field regardless of internal markdown structure, so a
// plain paragraph is never silently dropped (acceptance K3).
const freeContent = orgFiles[FREE_CONTEXT_FILE];
let hasFreeContext = false;
if (typeof freeContent === 'string' && freeContent.trim()) {
const body = collapseProse(freeContent);
if (body) fields.push([FREE_CONTEXT_LABEL, body]);
if (body) {
fields.push([FREE_CONTEXT_LABEL, body]);
hasFreeContext = true;
}
}
if (fields.length === 0) return '';
// Budget 1 — lines: over cap, keep (cap-1) fields plus a marker for the rest.
//
// "Fri kontekst" is exempt from that cut. It is appended LAST so it reads last,
// which under a plain head-slice made it the FIRST field to disappear — and it
// is the one slot the user writes freely in, which onboarding-agent.md calls
// the single category that most sharpens the advice. Position in the summary
// is a reading order, not a priority order; the cut must not conflate them.
const overflow = fields.length > cap;
const kept = overflow ? fields.slice(0, cap - 1) : fields;
const room = Math.max(0, cap - 1); // fields emittable alongside the marker
let kept = fields;
if (overflow) {
kept =
hasFreeContext && room >= 1
? [...fields.slice(0, room - 1), fields[fields.length - 1]]
: fields.slice(0, room);
}
// Budget 2 — chars: shared max-min fairly over the fields actually emitted.
const allowance = allocateBudget(kept.map(([, value]) => value.length), maxTotalLen);

View file

@ -353,3 +353,25 @@ test('orderOrgFiles — non-.md entries and garbage input are dropped', () => {
assert.deepEqual(orderOrgFiles(['notater.txt', '.DS_Store', 'alfa.md']), ['alfa.md']);
assert.deepEqual(orderOrgFiles(null), []);
});
test('buildOrgSummary — Fri kontekst survives the overflow cut (it is added last, not least)', () => {
const many = Array.from({ length: 40 }, (_, i) => `## Felt${i}\nverdi${i}`).join('\n\n');
const s = buildOrgSummary({
'organization-profile.md': many,
'free-context.md': 'Den ene sloten der brukeren skriver fritt.',
});
const lines = s.split('\n');
assert.ok(lines.length <= 25, `line budget still binds, got ${lines.length}`);
assert.match(s, /Fri kontekst: Den ene sloten/, 'free-context must not be the first field dropped');
assert.match(lines[lines.length - 1], /flere felt/, 'marker still last');
assert.match(lines[lines.length - 2], /^Fri kontekst:/, 'free-context kept as the last field');
});
test('buildOrgSummary — overflow without free-context is unchanged (back-compat)', () => {
const many = Array.from({ length: 40 }, (_, i) => `## Felt${i}\nverdi${i}`).join('\n\n');
const lines = buildOrgSummary({ 'organization-profile.md': many }, { cap: 10 }).split('\n');
assert.equal(lines.length, 10);
assert.equal(lines[0], 'Felt0: verdi0');
assert.equal(lines[8], 'Felt8: verdi8');
assert.match(lines[9], /flere felt/);
});