feat(ms-ai-architect): gjør reduksjonsenheten til dokumentet, ikke feltet [skip-docs]
Linje-cappen på 25 var ikke en reduksjon, men en seleksjon etter fast rekkefølge. Målt 2026-08-26 (bake-off arm A2b, flatt org/, 55 korpusdokumenter): 61 filer og 183 felt inn, 24 felt ut — alle fra 8 filer — mens 1 013 av de 4 000 verditegnene sto ubrukt. 53 filer var usynlige, og usynligheten var stille: brukeren tror pluginen er onboardet mens rådet er generisk. Sammendraget bygges ambient ved sesjonsstart, FØR det finnes et spørsmål, så det finnes ingen relevans å rangere mot. Når man ikke kan rangere, er det ærlige alternativet å dekke: - linjebudsjettet utledes fra filantallet (én linje per fil) i stedet for å være konstant, begrenset av MIN_COVERAGE_VALUE_LEN = 60 tegn per linje — forankret i den designede onboardingens egne feltlengder (median 34, snitt 75, maks 278) - seleksjonen er bredde-først på tvers av filer: hver fil gir fra seg sitt første felt før noen fil gir fra seg sitt andre. Emisjonsrekkefølgen er fortsatt filrekkefølgen, så leserekkefølgen er uendret - overflow-markøren oppgir hvor mange FILER som er utelatt, ikke bare felt Målt mot bake-off-harnesset (forhåndsregistrert gullsett, 40 spørsmål x 4 frikontekst-lengder), kontrollmålt mot git stash: n=55 A2b 10/160 -> 160/160 (kostnad 4 443 -> 4 923 tegn) n=25 A2b 10/160 -> 68/160 n=10 A2b 10/160 -> 24/160 A2b treffer nå last-alt-taket (identisk på alle fire korpusstørrelser) til 4 923 tegn mot 33 127. Ved den designede onboardingen (6 filer, 19 felt) er hookens output byte-identisk med før — kostnaden øker kun der den gamle koden skjulte dokumenter. Lesesiden verifisert ende-til-ende: hooken emitterer 62 linjer mot 25, med markøren "0 av 61 filer helt utelatt". Reach-metrikken sier at dokumentet er representert, ikke at svaret er riktig; hver fil bæres av ~65 tegn av sitt første felt. Den harde faktum-metrikken er ugyldig — dens egen kjent-positiv (last-alt) treffer bare 4/40. Tester: 7 nye i test-user-data.test.mjs (3 røde mot HEAD, verifisert med git stash). Sporet suite 1070/1070. [skip-docs]: intern reduksjonsmekanikk. Ingen kommando, agent, skill eller hook endrer grensesnitt; README/CLAUDE.md beskriver ingen av disse konstantene.
This commit is contained in:
parent
5a46818dd6
commit
31d2b62aed
2 changed files with 206 additions and 18 deletions
|
|
@ -375,3 +375,86 @@ test('buildOrgSummary — overflow without free-context is unchanged (back-compa
|
|||
assert.equal(lines[8], 'Felt8: verdi8');
|
||||
assert.match(lines[9], /flere felt/);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// (E) Reduksjon ved SKALA — the unit of reduction is the DOCUMENT, not the
|
||||
// field. Measured 2026-08-26 (bake-off arm A2b, flat org/, n=55 corpus
|
||||
// docs): 61 files and 183 fields went in, 24 fields came out, and those 24
|
||||
// came from 8 files — 53 files were invisible while 1013 of the 4000 value
|
||||
// chars stood unspent. What remained was selection by FIXED ORDER, which is
|
||||
// relevance-blind, and the summary is built ambiently at session start —
|
||||
// before any question exists — so there is no relevance signal to rank
|
||||
// against. The honest substitute for ranking is COVERAGE.
|
||||
// ===========================================================================
|
||||
|
||||
test('buildOrgSummary — every file contributes at least one field when the budget allows it', () => {
|
||||
const all = {};
|
||||
for (let f = 0; f < 40; f++) {
|
||||
all[`fil${String(f).padStart(2, '0')}.md`] = Array.from(
|
||||
{ length: 3 },
|
||||
(_, i) => `## F${f}H${i}\nverdi fra fil ${f} felt ${i}`,
|
||||
).join('\n\n');
|
||||
}
|
||||
const s = buildOrgSummary(all);
|
||||
const missing = [];
|
||||
for (let f = 0; f < 40; f++) {
|
||||
if (!new RegExp(`^F${f}H\\d: `, 'm').test(s)) missing.push(f);
|
||||
}
|
||||
assert.deepEqual(missing, [], `every file must reach the summary; ${missing.length}/40 did not`);
|
||||
});
|
||||
|
||||
test('buildOrgSummary — selection is breadth-first across files, not fixed order within one', () => {
|
||||
const all = {
|
||||
'organization-profile.md': '## A0\na0\n\n## A1\na1\n\n## A2\na2',
|
||||
'technology-stack.md': '## B0\nb0\n\n## B1\nb1\n\n## B2\nb2',
|
||||
'security-compliance.md': '## C0\nc0\n\n## C1\nc1\n\n## C2\nc2',
|
||||
};
|
||||
// cap 4 => 3 field lines + marker. Fixed order would spend all three on the
|
||||
// first file; coverage spends one on each.
|
||||
const lines = buildOrgSummary(all, { cap: 4 }).split('\n');
|
||||
assert.deepEqual(lines.slice(0, 3), ['A0: a0', 'B0: b0', 'C0: c0']);
|
||||
assert.match(lines[3], /flere felt/);
|
||||
});
|
||||
|
||||
test('buildOrgSummary — the overflow marker reports how many FILES were cut, not only fields', () => {
|
||||
const all = {};
|
||||
for (let f = 0; f < 200; f++) all[`fil${String(f).padStart(3, '0')}.md`] = `## H${f}\nverdi ${f}`;
|
||||
const s = buildOrgSummary(all);
|
||||
const marker = s.split('\n').at(-1);
|
||||
assert.match(marker, /flere felt/, 'field residue still reported');
|
||||
assert.match(marker, /\d+ filer/, 'file residue must be named — a silent drop is the defect');
|
||||
});
|
||||
|
||||
test('buildOrgSummary — coverage yields to the char budget, never starving lines to fragments', () => {
|
||||
const all = {};
|
||||
for (let f = 0; f < 200; f++) all[`fil${String(f).padStart(3, '0')}.md`] = `## H${f}\n${'x'.repeat(400)}`;
|
||||
const s = buildOrgSummary(all);
|
||||
const values = s.split('\n').filter((l) => /^H\d+: /.test(l)).map((l) => l.slice(l.indexOf(': ') + 2));
|
||||
assert.ok(values.length > 0, 'fields emitted');
|
||||
for (const v of values) {
|
||||
assert.ok(v.length >= 60, `a coverage line must stay readable, got ${v.length}`);
|
||||
}
|
||||
assert.ok(
|
||||
values.reduce((a, v) => a + v.length, 0) <= 4000,
|
||||
'the total value budget still binds at scale',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildOrgSummary — the designed onboarding size is untouched by the coverage rule', () => {
|
||||
const all = {};
|
||||
for (const f of ORG_FILES) {
|
||||
all[f] = Array.from({ length: 4 }, (_, i) => `## ${f}-Felt${i}\nverdi${i}`).join('\n\n');
|
||||
}
|
||||
const lines = buildOrgSummary(all).split('\n');
|
||||
assert.equal(lines.length, 20, '20 fields over 5 files fit; no marker, no reordering');
|
||||
assert.equal(lines[0], 'organization-profile.md-Felt0: verdi0');
|
||||
assert.equal(lines[3], 'organization-profile.md-Felt3: verdi3');
|
||||
});
|
||||
|
||||
test('buildOrgSummary — Fri kontekst still survives the cut at scale', () => {
|
||||
const all = {};
|
||||
for (let f = 0; f < 40; f++) all[`fil${String(f).padStart(2, '0')}.md`] = `## H${f}\nverdi ${f}`;
|
||||
all['free-context.md'] = 'Den ene sloten der brukeren skriver fritt.';
|
||||
const s = buildOrgSummary(all, { cap: 10 });
|
||||
assert.match(s, /Fri kontekst: Den ene sloten/, 'free-context must not be the first field dropped');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue