Skip to content

Commit 0e807af

Browse files
committed
feat(prompt-variance): residuals-only rows as single source for five families
Deny decision (default residuals-only): the package owns residual TEXT; tool denial stays live in ModelFamilyPolicy.advertisedToolDeny, applied at mount time by run.ts. Drops advertisedToolDeny, sectionOmit, per-model overrides, and the AssembledPromptVariance shape. Fold tool-budget (CL-8297) + ceremony (CL-8296) + claude (CL-8309) + gpt (CL-8310) rows into rows.ts; policy residual names alias the rows. Package renamed to @corbits/prompt-variance at 0.1.0; bun.lock synced. Size table gains claude/gpt columns; fixture mirrors the director tail-append for muse discipline rules.
1 parent 1b97633 commit 0e807af

14 files changed

Lines changed: 342 additions & 262 deletions

‎bun.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/prompt-variance/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "prompt-variance",
3-
"version": "1.4.0",
2+
"name": "@corbits/prompt-variance",
3+
"version": "0.1.0",
44
"private": true,
55
"type": "module",
66
"license": "SEE LICENSE IN LICENSE.md",
Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,36 @@
11
import { describe, expect, test } from "bun:test";
22
import { assemble } from "./assemble.js";
3-
import { defaultRow, grokRow, museRow } from "./rows.js";
4-
5-
// CL-8269 RED: assemble does not exist yet — every test below fails until
6-
// the GREEN lands packages/prompt-variance.
3+
import { claudeRow, defaultRow, gptRow, grokRow, museRow } from "./rows.js";
74

85
const SECTIONS = ["contract", "Tools (names only): read_file", "context"];
9-
const TOOLS = ["read_file", "skill_search", "use_skill", "run_shell"];
106

117
describe("assemble", () => {
128
test("joins sections with the residual as the last section", () => {
13-
const out = assemble(SECTIONS, grokRow, TOOLS);
14-
expect(out.systemPrompt).toContain("contract");
15-
expect(out.systemPrompt.trimEnd().endsWith(grokRow.residual)).toBe(true);
9+
const out = assemble(SECTIONS, grokRow);
10+
expect(out).toContain("contract");
11+
expect(out.trimEnd().endsWith(grokRow.residual)).toBe(true);
1612
});
1713

1814
test("a row without residual leaves the sections unchanged", () => {
19-
const out = assemble(SECTIONS, defaultRow, TOOLS);
20-
expect(out.systemPrompt).toBe(SECTIONS.join("\n\n"));
15+
expect(assemble(SECTIONS, defaultRow)).toBe(SECTIONS.join("\n\n"));
2116
});
2217

2318
test("muse residual closes the prompt like the shipped tail append", () => {
24-
const out = assemble(SECTIONS, museRow, TOOLS);
25-
expect(out.systemPrompt.trimEnd().endsWith(museRow.residual)).toBe(true);
26-
});
27-
28-
test("filters advertisedToolDeny from the tool names, preserving order", () => {
29-
const out = assemble(SECTIONS, grokRow, TOOLS);
30-
expect([...out.toolNames]).toEqual(["read_file", "use_skill", "run_shell"]);
31-
});
32-
33-
test("variance cannot grant tools: output is always a subset of the input", () => {
34-
for (const row of [defaultRow, museRow, grokRow]) {
35-
const out = assemble(SECTIONS, row, TOOLS);
36-
for (const name of out.toolNames) {
37-
expect(TOOLS).toContain(name);
38-
}
39-
expect(out.toolNames.length).toBeLessThanOrEqual(TOOLS.length);
40-
}
19+
const out = assemble(SECTIONS, museRow);
20+
expect(out.trimEnd().endsWith(museRow.residual)).toBe(true);
4121
});
4222

43-
test("variance cannot grant tools: unknown mounted names pass through untouched", () => {
44-
const out = assemble(SECTIONS, defaultRow, ["alpha", "beta"]);
45-
expect([...out.toolNames]).toEqual(["alpha", "beta"]);
23+
test("claude and gpt residuals close the prompt the same way", () => {
24+
expect(
25+
assemble(SECTIONS, claudeRow).trimEnd().endsWith(claudeRow.residual),
26+
).toBe(true);
27+
expect(assemble(SECTIONS, gptRow).trimEnd().endsWith(gptRow.residual)).toBe(
28+
true,
29+
);
4630
});
4731

48-
test("refuses a row that denies use_skill", () => {
49-
const bad = { ...grokRow, advertisedToolDeny: ["use_skill"] };
50-
expect(() => assemble(SECTIONS, bad, TOOLS)).toThrow(/use_skill/);
32+
test("tool mounting is out of scope: assemble returns a string, not tool names", () => {
33+
const out = assemble(SECTIONS, grokRow);
34+
expect(typeof out).toBe("string");
5135
});
5236
});
Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,16 @@
11
import type { PromptVarianceRow } from "./rows.js";
22

3-
export interface AssembledPromptVariance {
4-
systemPrompt: string;
5-
toolNames: readonly string[];
6-
}
7-
83
/**
9-
* Assemble a prompt from sections plus a family variance row (CL-8269).
10-
* The row residual renders last; advertised denies filter the mounted
11-
* tool names. Variance is subtractive only: the output names are always
12-
* a subset of the input names, in input order.
4+
* Append a family variance row's residual at the tail of the assembled
5+
* sections (CL-8269). An empty residual leaves the sections unchanged.
6+
* Residuals render last so they cannot disturb the cached prompt prefix.
7+
* Tool mounting is out of scope: run.ts applies
8+
* ModelFamilyPolicy.advertisedToolDeny at mount time.
139
*/
1410
export function assemble(
1511
sections: readonly string[],
1612
familyRow: PromptVarianceRow,
17-
tools: readonly string[],
18-
): AssembledPromptVariance {
19-
if (familyRow.advertisedToolDeny.includes("use_skill")) {
20-
throw new Error(
21-
`prompt-variance: row "${familyRow.id}" denies use_skill — brief-named skills always load directly`,
22-
);
23-
}
24-
const systemPrompt =
25-
familyRow.residual.length > 0
26-
? [...sections, familyRow.residual].join("\n\n")
27-
: sections.join("\n\n");
28-
const deny = new Set(familyRow.advertisedToolDeny);
29-
return {
30-
systemPrompt,
31-
toolNames: tools.filter((name) => !deny.has(name)),
32-
};
13+
): string {
14+
if (familyRow.residual.length === 0) return sections.join("\n\n");
15+
return [...sections, familyRow.residual].join("\n\n");
3316
}
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
export { assemble, type AssembledPromptVariance } from "./assemble.js";
2-
export { applyRowOverride, resolvePromptVariance } from "./resolve.js";
1+
export { assemble } from "./assemble.js";
2+
export { resolvePromptVariance } from "./resolve.js";
33
export {
4+
claudeRow,
45
defaultRow,
56
FAMILY_IDS,
67
FAMILY_ROWS,
8+
gptRow,
79
grokRow,
10+
grokToolBudgetResidual,
811
museRow,
912
type PromptVarianceFamily,
10-
type PromptVarianceRender,
1113
type PromptVarianceRow,
12-
type PromptVarianceRowOverride,
1314
} from "./rows.js";
Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,37 @@
11
import { describe, expect, test } from "bun:test";
2-
import { applyRowOverride, resolvePromptVariance } from "./resolve.js";
2+
import { resolvePromptVariance } from "./resolve.js";
33
import { grokRow } from "./rows.js";
44

5-
// CL-8269 RED: the resolver does not exist yet — every test below fails
6-
// until the GREEN lands packages/prompt-variance.
7-
85
describe("resolvePromptVariance", () => {
96
test("resolves each family to its row", () => {
107
expect(resolvePromptVariance({ family: "default" }).id).toBe("default");
118
expect(resolvePromptVariance({ family: "muse" }).id).toBe("muse");
129
expect(resolvePromptVariance({ family: "grok" }).id).toBe("grok");
10+
expect(resolvePromptVariance({ family: "claude" }).id).toBe("claude");
11+
expect(resolvePromptVariance({ family: "gpt" }).id).toBe("gpt");
1312
});
1413

15-
test("grok keeps its finish-bias residual and skill_search deny on leaves", () => {
14+
test("grok keeps its finish-bias residual on leaves", () => {
1615
const row = resolvePromptVariance({ family: "grok", orchestrator: false });
1716
expect(row.residual).toBe(grokRow.residual);
18-
expect(row.advertisedToolDeny).toContain("skill_search");
1917
});
2018

21-
test("grok on orchestrators falls back to the default shape", () => {
19+
test("grok on orchestrators falls back to the default row", () => {
2220
const row = resolvePromptVariance({ family: "grok", orchestrator: true });
21+
expect(row.id).toBe("default");
2322
expect(row.residual).toBe("");
24-
expect([...row.advertisedToolDeny]).toEqual([]);
23+
});
24+
25+
test("claude keeps its task_guidance block on leaves, not orchestrators", () => {
26+
expect(
27+
resolvePromptVariance({ family: "claude", orchestrator: false }).residual,
28+
).toContain("<task_guidance>");
29+
const orch = resolvePromptVariance({
30+
family: "claude",
31+
orchestrator: true,
32+
});
33+
expect(orch.id).toBe("default");
34+
expect(orch.residual).toBe("");
2535
});
2636

2737
test("muse keeps its residual on leaves and orchestrators alike", () => {
@@ -33,27 +43,12 @@ describe("resolvePromptVariance", () => {
3343
).not.toBe("");
3444
});
3545

36-
test("applies per-model-id overrides from the row", () => {
37-
const row = applyRowOverride(
38-
{
39-
...grokRow,
40-
overrides: {
41-
"grok-4-special": { advertisedToolDeny: [] },
42-
},
43-
},
44-
"GROK-4-SPECIAL",
45-
);
46-
expect([...row.advertisedToolDeny]).toEqual([]);
47-
expect(row.residual).toBe(grokRow.residual);
48-
});
49-
50-
test("an unknown model id resolves to the base row", () => {
51-
const row = resolvePromptVariance({
52-
family: "grok",
53-
orchestrator: false,
54-
model: "grok-9-unknown",
55-
});
56-
expect(row.residual).toBe(grokRow.residual);
57-
expect([...row.advertisedToolDeny]).toEqual(["skill_search"]);
46+
test("gpt keeps its narrate-before-tools nudge on leaves and orchestrators alike", () => {
47+
expect(
48+
resolvePromptVariance({ family: "gpt", orchestrator: false }).residual,
49+
).toContain("Narrate before tools");
50+
expect(
51+
resolvePromptVariance({ family: "gpt", orchestrator: true }).residual,
52+
).toContain("Narrate before tools");
5853
});
5954
});

‎packages/prompt-variance/src/resolve.ts‎

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,21 @@ import {
66
} from "./rows.js";
77

88
/**
9-
* Apply a row's per-model-id override, if any. Model ids match
10-
* case-insensitively against the row's lowercase override keys; an
11-
* unknown id resolves to the base row unchanged.
12-
*/
13-
export function applyRowOverride(
14-
row: PromptVarianceRow,
15-
model?: string,
16-
): PromptVarianceRow {
17-
if (model === undefined) return row;
18-
const override = row.overrides?.[model.trim().toLowerCase()];
19-
if (override === undefined) return row;
20-
return {
21-
...row,
22-
residual: override.residual ?? row.residual,
23-
advertisedToolDeny: override.advertisedToolDeny ?? row.advertisedToolDeny,
24-
sectionOmit: override.sectionOmit ?? row.sectionOmit,
25-
};
26-
}
27-
28-
/**
29-
* Resolve the variance row for a family, mirroring the leaves-only gate:
30-
* the grok finish-bias residual only makes sense on leaf workers, so
31-
* orchestrators fall back to the default shape. Muse keeps its residual
32-
* on both — its constructors append the same text at the tail either way.
9+
* Resolve the variance row for a family, mirroring the leaves-only gates in
10+
* the model family policy: the grok finish-bias residual and the claude
11+
* task_guidance block only make sense on leaf workers, so orchestrators
12+
* fall back to the default row. Muse keeps its residual on both — the
13+
* constructors append the same text at the tail either way — and gpt keeps
14+
* its narrate-before-tools nudge on primaries and leaves alike.
3315
*/
3416
export function resolvePromptVariance(input: {
3517
family: PromptVarianceFamily;
3618
orchestrator?: boolean;
37-
model?: string;
3819
}): PromptVarianceRow {
39-
if (input.family === "grok" && input.orchestrator === true) {
40-
return applyRowOverride(defaultRow, input.model);
20+
if (input.orchestrator === true) {
21+
if (input.family === "grok" || input.family === "claude") {
22+
return defaultRow;
23+
}
4124
}
42-
return applyRowOverride(FAMILY_ROWS[input.family], input.model);
25+
return FAMILY_ROWS[input.family];
4326
}

0 commit comments

Comments
 (0)