@astudioplus/compressor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +20 -0
- package/README.md +167 -0
- package/dist/adapters/agents-md.d.ts +2 -0
- package/dist/adapters/agents-md.js +91 -0
- package/dist/adapters/apply.d.ts +3 -0
- package/dist/adapters/apply.js +83 -0
- package/dist/adapters/claude-code.d.ts +2 -0
- package/dist/adapters/claude-code.js +403 -0
- package/dist/adapters/copilot.d.ts +2 -0
- package/dist/adapters/copilot.js +418 -0
- package/dist/adapters/cursor.d.ts +2 -0
- package/dist/adapters/cursor.js +149 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +19 -0
- package/dist/adapters/markers.d.ts +7 -0
- package/dist/adapters/markers.js +129 -0
- package/dist/adapters/types.d.ts +44 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bench/ablate.d.ts +35 -0
- package/dist/bench/ablate.js +163 -0
- package/dist/bench/cell.d.ts +33 -0
- package/dist/bench/cell.js +437 -0
- package/dist/bench/results.d.ts +37 -0
- package/dist/bench/results.js +157 -0
- package/dist/bench/runner.d.ts +24 -0
- package/dist/bench/runner.js +121 -0
- package/dist/bench/tasks.d.ts +4 -0
- package/dist/bench/tasks.js +147 -0
- package/dist/bench/types.d.ts +109 -0
- package/dist/bench/types.js +1 -0
- package/dist/claude/transcripts.d.ts +30 -0
- package/dist/claude/transcripts.js +154 -0
- package/dist/cli/commands/benchmark.d.ts +33 -0
- package/dist/cli/commands/benchmark.js +203 -0
- package/dist/cli/commands/compress.d.ts +8 -0
- package/dist/cli/commands/compress.js +45 -0
- package/dist/cli/commands/count.d.ts +5 -0
- package/dist/cli/commands/count.js +25 -0
- package/dist/cli/commands/hook.d.ts +6 -0
- package/dist/cli/commands/hook.js +30 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +76 -0
- package/dist/cli/commands/report.d.ts +90 -0
- package/dist/cli/commands/report.js +464 -0
- package/dist/cli/commands/savings.d.ts +38 -0
- package/dist/cli/commands/savings.js +196 -0
- package/dist/cli/commands/set-mode.d.ts +5 -0
- package/dist/cli/commands/set-mode.js +13 -0
- package/dist/cli/commands/stats.d.ts +5 -0
- package/dist/cli/commands/stats.js +51 -0
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.js +11 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.js +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +146 -0
- package/dist/copilot-hook-entry.d.ts +1 -0
- package/dist/copilot-hook-entry.js +36 -0
- package/dist/copilot-hook.js +1000 -0
- package/dist/engine/detect.d.ts +2 -0
- package/dist/engine/detect.js +47 -0
- package/dist/engine/index.d.ts +4 -0
- package/dist/engine/index.js +90 -0
- package/dist/engine/policy.d.ts +2 -0
- package/dist/engine/policy.js +48 -0
- package/dist/engine/tiers/code.d.ts +7 -0
- package/dist/engine/tiers/code.js +206 -0
- package/dist/engine/tiers/logs.d.ts +4 -0
- package/dist/engine/tiers/logs.js +139 -0
- package/dist/engine/tiers/structural.d.ts +28 -0
- package/dist/engine/tiers/structural.js +199 -0
- package/dist/engine/types.d.ts +71 -0
- package/dist/engine/types.js +5 -0
- package/dist/hook/copilot.d.ts +5 -0
- package/dist/hook/copilot.js +136 -0
- package/dist/hook/core.d.ts +36 -0
- package/dist/hook/core.js +138 -0
- package/dist/hook/exit.d.ts +22 -0
- package/dist/hook/exit.js +56 -0
- package/dist/hook/post-tool-use.d.ts +5 -0
- package/dist/hook/post-tool-use.js +57 -0
- package/dist/hook-entry.d.ts +1 -0
- package/dist/hook-entry.js +35 -0
- package/dist/hook.js +946 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/ledger/read.d.ts +9 -0
- package/dist/ledger/read.js +91 -0
- package/dist/ledger/write.d.ts +29 -0
- package/dist/ledger/write.js +61 -0
- package/dist/packs/atoms.d.ts +3 -0
- package/dist/packs/atoms.js +108 -0
- package/dist/packs/modes.d.ts +3 -0
- package/dist/packs/modes.js +34 -0
- package/dist/packs/render.d.ts +24 -0
- package/dist/packs/render.js +115 -0
- package/dist/packs/types.d.ts +32 -0
- package/dist/packs/types.js +1 -0
- package/dist/paths.d.ts +29 -0
- package/dist/paths.js +87 -0
- package/dist/tokens/estimate.d.ts +12 -0
- package/dist/tokens/estimate.js +23 -0
- package/dist/tokens/exact.d.ts +5 -0
- package/dist/tokens/exact.js +16 -0
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.js +2 -0
- package/package.json +77 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MARKER_BEGIN_LINE_RE, MARKER_END } from "../packs/render.js";
|
|
2
|
+
/** Code fence delimiter: up to 3 leading spaces, then ``` / ~~~ (CommonMark). */
|
|
3
|
+
const FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
4
|
+
/** 4+ spaces or a tab opens an indented code block — never our marker. */
|
|
5
|
+
const INDENTED_CODE_RE = /^(?: {4,}|\t)/;
|
|
6
|
+
/**
|
|
7
|
+
* Per-line mask: true ⇨ the line belongs to a CLOSED fenced code block
|
|
8
|
+
* (including its delimiter lines) and must be ignored when locating markers.
|
|
9
|
+
*
|
|
10
|
+
* CommonMark-faithful where it matters for safety:
|
|
11
|
+
* - a fence closes only on the same character, at least as long as the
|
|
12
|
+
* opener, with nothing but whitespace after it — a ``` line inside an open
|
|
13
|
+
* ~~~ block is literal text, not a toggle;
|
|
14
|
+
* - a backtick opening fence cannot carry backticks in its info string;
|
|
15
|
+
* - a fence left open at EOF deliberately does NOT mask its content: install
|
|
16
|
+
* appends our section after such files, and hiding those lines would break
|
|
17
|
+
* idempotency (duplicate sections) and strand the section with no
|
|
18
|
+
* uninstall path.
|
|
19
|
+
*/
|
|
20
|
+
function closedFenceMask(lines) {
|
|
21
|
+
const mask = new Array(lines.length).fill(false);
|
|
22
|
+
let open = null;
|
|
23
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
24
|
+
const match = FENCE_LINE_RE.exec(lines[i] ?? '');
|
|
25
|
+
if (match === null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const seq = match[1] ?? '';
|
|
29
|
+
const rest = match[2] ?? '';
|
|
30
|
+
const char = seq.charAt(0);
|
|
31
|
+
if (open === null) {
|
|
32
|
+
if (char === '~' || !rest.includes('`')) {
|
|
33
|
+
open = { char, len: seq.length, from: i };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (char === open.char &&
|
|
37
|
+
seq.length >= open.len &&
|
|
38
|
+
rest.trim() === '') {
|
|
39
|
+
for (let j = open.from; j <= i; j += 1) {
|
|
40
|
+
mask[j] = true;
|
|
41
|
+
}
|
|
42
|
+
open = null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return mask;
|
|
46
|
+
}
|
|
47
|
+
function isBeginLine(line) {
|
|
48
|
+
return !INDENTED_CODE_RE.test(line) && MARKER_BEGIN_LINE_RE.test(line.trim());
|
|
49
|
+
}
|
|
50
|
+
function isEndLine(line) {
|
|
51
|
+
return !INDENTED_CODE_RE.test(line) && line.trim() === MARKER_END;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Locate our marked section. Safety rules:
|
|
55
|
+
* - a begin line must match the FULL marker grammar (prefix-only prose lines
|
|
56
|
+
* are ignored);
|
|
57
|
+
* - markers inside closed code fences or indented code blocks are examples,
|
|
58
|
+
* not boundaries;
|
|
59
|
+
* - an end line pairs with the NEAREST preceding begin: an orphan begin
|
|
60
|
+
* (user hand-deleted our end) re-anchors to a later real begin instead of
|
|
61
|
+
* greedily swallowing the user content in between.
|
|
62
|
+
*/
|
|
63
|
+
function findSpan(lines) {
|
|
64
|
+
const fenced = closedFenceMask(lines);
|
|
65
|
+
let start = -1;
|
|
66
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
67
|
+
if (fenced[i] === true) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const line = lines[i] ?? '';
|
|
71
|
+
if (isBeginLine(line)) {
|
|
72
|
+
start = i;
|
|
73
|
+
}
|
|
74
|
+
else if (start !== -1 && isEndLine(line)) {
|
|
75
|
+
return { start, end: i };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function asBlock(section) {
|
|
81
|
+
return section.endsWith('\n') ? section.slice(0, -1) : section;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Replace the compressor section in place if present; else append with exactly
|
|
85
|
+
* one blank line of separation. Bytes outside the markers are never modified.
|
|
86
|
+
*/
|
|
87
|
+
export function upsertMarkedSection(existing, section) {
|
|
88
|
+
const block = asBlock(section);
|
|
89
|
+
if (existing === null) {
|
|
90
|
+
return `${block}\n`;
|
|
91
|
+
}
|
|
92
|
+
const lines = existing.split('\n');
|
|
93
|
+
const span = findSpan(lines);
|
|
94
|
+
if (span !== null) {
|
|
95
|
+
return [
|
|
96
|
+
...lines.slice(0, span.start),
|
|
97
|
+
...block.split('\n'),
|
|
98
|
+
...lines.slice(span.end + 1),
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
const trimmed = existing.replace(/\n+$/u, '');
|
|
102
|
+
return trimmed === '' ? `${block}\n` : `${trimmed}\n\n${block}\n`;
|
|
103
|
+
}
|
|
104
|
+
export function removeMarkedSection(existing) {
|
|
105
|
+
const lines = existing.split('\n');
|
|
106
|
+
const span = findSpan(lines);
|
|
107
|
+
if (span === null) {
|
|
108
|
+
return existing;
|
|
109
|
+
}
|
|
110
|
+
const before = lines.slice(0, span.start);
|
|
111
|
+
const after = lines.slice(span.end + 1);
|
|
112
|
+
if (before.length > 0 && before[before.length - 1] === '') {
|
|
113
|
+
before.pop();
|
|
114
|
+
}
|
|
115
|
+
else if (after.length > 0 && after[0] === '') {
|
|
116
|
+
after.shift();
|
|
117
|
+
}
|
|
118
|
+
// No whitespace collapsing here: residue bytes (e.g. a whitespace-only
|
|
119
|
+
// user file that received our section) must survive removal untouched.
|
|
120
|
+
return [...before, ...after].join('\n');
|
|
121
|
+
}
|
|
122
|
+
export function readMarkedSection(existing) {
|
|
123
|
+
const lines = existing.split('\n');
|
|
124
|
+
const span = findSpan(lines);
|
|
125
|
+
if (span === null) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return lines.slice(span.start, span.end + 1).join('\n');
|
|
129
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AgentName, PackMode } from '../packs/types.ts';
|
|
2
|
+
/** Mode argument accepted by the CLI; 'full' maps to uninstall (true baseline). */
|
|
3
|
+
export type ModeArg = PackMode | 'full';
|
|
4
|
+
export interface AdapterContext {
|
|
5
|
+
/** project root (cwd) */
|
|
6
|
+
projectDir: string;
|
|
7
|
+
/** os.homedir() in production; overridden in tests */
|
|
8
|
+
homeDir: string;
|
|
9
|
+
/** install at user level instead of project level */
|
|
10
|
+
global: boolean;
|
|
11
|
+
/** command line for the PostToolUse hook entry, resolved at install time */
|
|
12
|
+
hookCommand: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A planned file mutation. `before === null` means the file did not exist;
|
|
16
|
+
* `after === null` means the file is deleted. The CLI renders these as diffs
|
|
17
|
+
* for --dry-run and applies them otherwise. Adapters never write directly.
|
|
18
|
+
*/
|
|
19
|
+
export interface FileChange {
|
|
20
|
+
path: string;
|
|
21
|
+
before: string | null;
|
|
22
|
+
after: string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface AdapterStatus {
|
|
25
|
+
agent: AgentName;
|
|
26
|
+
installed: boolean;
|
|
27
|
+
mode?: PackMode;
|
|
28
|
+
/** human line, e.g. 'output style + hook installed (project)' or 'unknown layout — not touching' */
|
|
29
|
+
detail: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adapters are pure planners: every method returns the FileChanges that WOULD
|
|
33
|
+
* make the target state true, computed from current disk content. Idempotent:
|
|
34
|
+
* planning install over an existing install yields replace-in-place, never
|
|
35
|
+
* duplicates. Uninstall touches only compressor-owned files/sections/entries.
|
|
36
|
+
*/
|
|
37
|
+
export interface Adapter {
|
|
38
|
+
name: AgentName;
|
|
39
|
+
/** is this agent plausibly used in this project/home? */
|
|
40
|
+
detect(ctx: AdapterContext): Promise<boolean>;
|
|
41
|
+
install(mode: PackMode, ctx: AdapterContext): Promise<FileChange[]>;
|
|
42
|
+
uninstall(ctx: AdapterContext): Promise<FileChange[]>;
|
|
43
|
+
status(ctx: AdapterContext): Promise<AdapterStatus>;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Mode } from '../engine/types.ts';
|
|
2
|
+
import type { AtomCategory } from '../packs/types.ts';
|
|
3
|
+
import type { Variant } from './types.ts';
|
|
4
|
+
/** Atom categories ablatable wholesale via --ablate-group. */
|
|
5
|
+
export declare const ABLATE_GROUPS: readonly AtomCategory[];
|
|
6
|
+
export interface BuildVariantsOptions {
|
|
7
|
+
modes: Mode[];
|
|
8
|
+
/** atom ids removed one at a time from the optimized baseline */
|
|
9
|
+
ablate: string[];
|
|
10
|
+
/** REJECTED atom ids added one at a time to the optimized baseline */
|
|
11
|
+
ablateAdd: string[];
|
|
12
|
+
/** atom categories ('output' | 'behavior') removed wholesale from the optimized baseline */
|
|
13
|
+
ablateGroups: string[];
|
|
14
|
+
hook: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Extra args appended to the hook command of EVERY hook-bearing variant
|
|
17
|
+
* (Variant.hookArgs), e.g. '--marker-style informative'. Whitespace-only
|
|
18
|
+
* values are ignored.
|
|
19
|
+
*/
|
|
20
|
+
hookArgs?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Marker-style ARMS: each hook-bearing variant fans out into one variant
|
|
23
|
+
* per style (id '<variant>-marker-<style>', hookArgs '--marker-style
|
|
24
|
+
* <style>') so all arms coexist IN THE SAME RUN. Running arms as separate
|
|
25
|
+
* `--hook-args` runs gives each its own --max-budget-usd ceiling and its
|
|
26
|
+
* own truncation point — a more expensive arm loses later trials/tasks
|
|
27
|
+
* while others complete, unbalancing the comparison. In-run arms share one
|
|
28
|
+
* ceiling and the runner's variants-innermost, group-atomic scheduling
|
|
29
|
+
* keeps every arm present on exactly the same task×trial groups.
|
|
30
|
+
*/
|
|
31
|
+
markerStyles?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Atom ids become style/file names — dots would read as extensions. */
|
|
34
|
+
export declare function sanitizeAtomId(id: string): string;
|
|
35
|
+
export declare function buildVariants(opts: BuildVariantsOptions): Variant[];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { getAtom } from "../packs/atoms.js";
|
|
2
|
+
import { atomsForMode, MODE_DESCRIPTIONS } from "../packs/modes.js";
|
|
3
|
+
import { renderOutputStyle, renderOutputStyleFromAtoms } from "../packs/render.js";
|
|
4
|
+
/** Atom categories ablatable wholesale via --ablate-group. */
|
|
5
|
+
export const ABLATE_GROUPS = ['output', 'behavior'];
|
|
6
|
+
function isAblateGroup(value) {
|
|
7
|
+
return ABLATE_GROUPS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
const MARKER_STYLES = ['plain', 'deterrent', 'informative'];
|
|
10
|
+
function isMarkerStyle(value) {
|
|
11
|
+
return MARKER_STYLES.includes(value);
|
|
12
|
+
}
|
|
13
|
+
/** Atom ids become style/file names — dots would read as extensions. */
|
|
14
|
+
export function sanitizeAtomId(id) {
|
|
15
|
+
return id.replaceAll('.', '-');
|
|
16
|
+
}
|
|
17
|
+
function modeVariant(mode, hook) {
|
|
18
|
+
if (mode === 'full') {
|
|
19
|
+
return { id: 'full', baseMode: 'full', styleBody: null, styleName: null, hook: false };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
id: mode,
|
|
23
|
+
baseMode: mode,
|
|
24
|
+
styleBody: renderOutputStyle(mode).body,
|
|
25
|
+
styleName: `compressor-${mode}`,
|
|
26
|
+
hook,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function buildVariants(opts) {
|
|
30
|
+
const variants = opts.modes.map((mode) => modeVariant(mode, opts.hook));
|
|
31
|
+
if (opts.ablateAdd.length > 0 && !opts.modes.includes('optimized')) {
|
|
32
|
+
throw new Error("--ablate-add requires 'optimized' in --modes — rejected atoms are added back to the optimized baseline");
|
|
33
|
+
}
|
|
34
|
+
const baseline = atomsForMode('optimized', 'claude-code');
|
|
35
|
+
const slimBaseline = atomsForMode('slim', 'claude-code');
|
|
36
|
+
// Atoms in the optimized baseline ablate against optimized; slim-only atoms
|
|
37
|
+
// (out.explanation-budget, out.code-only-default) have no optimized data
|
|
38
|
+
// path, so they ablate against the slim baseline as slim-minus-<id> —
|
|
39
|
+
// otherwise the per-atom gate is structurally unanswerable for them.
|
|
40
|
+
for (const id of opts.ablate) {
|
|
41
|
+
if (getAtom(id) === undefined) {
|
|
42
|
+
throw new Error(`--ablate: unknown atom id '${id}'`);
|
|
43
|
+
}
|
|
44
|
+
const inOptimized = baseline.some((atom) => atom.id === id);
|
|
45
|
+
const inSlim = slimBaseline.some((atom) => atom.id === id);
|
|
46
|
+
if (!inOptimized && !inSlim) {
|
|
47
|
+
throw new Error(`--ablate: atom '${id}' is not in the optimized baseline or the slim baseline — removing it would change nothing`);
|
|
48
|
+
}
|
|
49
|
+
const baseMode = inOptimized ? 'optimized' : 'slim';
|
|
50
|
+
if (!opts.modes.includes(baseMode)) {
|
|
51
|
+
throw new Error(inOptimized
|
|
52
|
+
? `--ablate: atom '${id}' is measured against the optimized baseline — include 'optimized' in --modes`
|
|
53
|
+
: `--ablate: atom '${id}' is not in the optimized baseline — it is slim-only; include 'slim' in --modes to measure slim-minus-${sanitizeAtomId(id)}`);
|
|
54
|
+
}
|
|
55
|
+
const baseAtoms = inOptimized ? baseline : slimBaseline;
|
|
56
|
+
const variantId = `${baseMode}-minus-${sanitizeAtomId(id)}`;
|
|
57
|
+
const rendered = renderOutputStyleFromAtoms(baseAtoms.filter((atom) => atom.id !== id), `compressor-${variantId}`, `${MODE_DESCRIPTIONS[baseMode]} (minus ${id})`);
|
|
58
|
+
variants.push({
|
|
59
|
+
id: variantId,
|
|
60
|
+
baseMode,
|
|
61
|
+
styleBody: rendered.body,
|
|
62
|
+
styleName: `compressor-${variantId}`,
|
|
63
|
+
hook: opts.hook,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const group of opts.ablateGroups) {
|
|
67
|
+
if (!isAblateGroup(group)) {
|
|
68
|
+
throw new Error(`--ablate-group: unknown group '${group}' (valid groups: ${ABLATE_GROUPS.join(', ')})`);
|
|
69
|
+
}
|
|
70
|
+
if (!opts.modes.includes('optimized')) {
|
|
71
|
+
throw new Error(`--ablate-group: group '${group}' is measured against the optimized baseline — include 'optimized' in --modes`);
|
|
72
|
+
}
|
|
73
|
+
const variantId = `optimized-minus-${group}-atoms`;
|
|
74
|
+
const styleName = `compressor-ablate-no-${group}`;
|
|
75
|
+
// No empty-result guard needed: if removing the group leaves zero atoms,
|
|
76
|
+
// renderOutputStyleFromAtoms still emits frontmatter + empty sections —
|
|
77
|
+
// the variant measures "style file present but says nothing of that category".
|
|
78
|
+
const rendered = renderOutputStyleFromAtoms(baseline.filter((atom) => atom.category !== group), styleName, `${MODE_DESCRIPTIONS.optimized} (minus all ${group} atoms)`);
|
|
79
|
+
variants.push({
|
|
80
|
+
id: variantId,
|
|
81
|
+
baseMode: 'optimized',
|
|
82
|
+
styleBody: rendered.body,
|
|
83
|
+
styleName,
|
|
84
|
+
hook: opts.hook,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const id of opts.ablateAdd) {
|
|
88
|
+
const atom = getAtom(id);
|
|
89
|
+
if (atom === undefined) {
|
|
90
|
+
throw new Error(`--ablate-add: unknown atom id '${id}'`);
|
|
91
|
+
}
|
|
92
|
+
if (atom.rejected === undefined) {
|
|
93
|
+
throw new Error(`--ablate-add: atom '${id}' is not rejected — active atoms belong in --modes/--ablate; --ablate-add exists to test rejected atoms against data`);
|
|
94
|
+
}
|
|
95
|
+
const variantId = `optimized-plus-${sanitizeAtomId(id)}`;
|
|
96
|
+
const rendered = renderOutputStyleFromAtoms([...baseline, atom], `compressor-${variantId}`, `${MODE_DESCRIPTIONS.optimized} (plus rejected ${id})`);
|
|
97
|
+
variants.push({
|
|
98
|
+
id: variantId,
|
|
99
|
+
baseMode: 'optimized',
|
|
100
|
+
styleBody: rendered.body,
|
|
101
|
+
styleName: `compressor-${variantId}`,
|
|
102
|
+
hook: opts.hook,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const hookArgs = opts.hookArgs?.trim();
|
|
106
|
+
if (hookArgs !== undefined && hookArgs !== '') {
|
|
107
|
+
const hooked = variants.filter((variant) => variant.hook);
|
|
108
|
+
if (hooked.length === 0) {
|
|
109
|
+
throw new Error('--hook-args: no hook-bearing variants to apply it to — remove --no-hook and include optimized/slim in --modes');
|
|
110
|
+
}
|
|
111
|
+
for (const variant of hooked) {
|
|
112
|
+
variant.hookArgs = hookArgs;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const markerStyles = opts.markerStyles ?? [];
|
|
116
|
+
let expanded = variants;
|
|
117
|
+
if (markerStyles.length > 0) {
|
|
118
|
+
for (const style of markerStyles) {
|
|
119
|
+
if (!isMarkerStyle(style)) {
|
|
120
|
+
throw new Error(`--marker-styles: unknown style '${style}' (valid: ${MARKER_STYLES.join(', ')})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (new Set(markerStyles).size !== markerStyles.length) {
|
|
124
|
+
throw new Error('--marker-styles: duplicate style');
|
|
125
|
+
}
|
|
126
|
+
// The hook entries take the FIRST --marker-style on their command line;
|
|
127
|
+
// a shared --hook-args value carrying the flag would silently override
|
|
128
|
+
// every arm, collapsing the experiment to one style.
|
|
129
|
+
if (hookArgs !== undefined && hookArgs.includes('--marker-style')) {
|
|
130
|
+
throw new Error('--marker-styles cannot be combined with --hook-args containing --marker-style — the shared value would override every arm');
|
|
131
|
+
}
|
|
132
|
+
if (!variants.some((variant) => variant.hook)) {
|
|
133
|
+
throw new Error('--marker-styles: no hook-bearing variants to fan out — remove --no-hook and include optimized/slim in --modes');
|
|
134
|
+
}
|
|
135
|
+
expanded = variants.flatMap((variant) => variant.hook
|
|
136
|
+
? markerStyles.map((style) => ({
|
|
137
|
+
...variant,
|
|
138
|
+
id: `${variant.id}-marker-${style}`,
|
|
139
|
+
// unique style file per arm (same body): keeps the duplicate
|
|
140
|
+
// checks meaningful and the installed outputStyle traceable
|
|
141
|
+
styleName: variant.styleName === null ? null : `${variant.styleName}-marker-${style}`,
|
|
142
|
+
hookArgs: variant.hookArgs === undefined
|
|
143
|
+
? `--marker-style ${style}`
|
|
144
|
+
: `${variant.hookArgs} --marker-style ${style}`,
|
|
145
|
+
}))
|
|
146
|
+
: [variant]);
|
|
147
|
+
}
|
|
148
|
+
const seenIds = new Set();
|
|
149
|
+
const seenStyles = new Set();
|
|
150
|
+
for (const variant of expanded) {
|
|
151
|
+
if (seenIds.has(variant.id)) {
|
|
152
|
+
throw new Error(`duplicate variant id '${variant.id}' (repeated mode or atom id?)`);
|
|
153
|
+
}
|
|
154
|
+
seenIds.add(variant.id);
|
|
155
|
+
if (variant.styleName !== null) {
|
|
156
|
+
if (seenStyles.has(variant.styleName)) {
|
|
157
|
+
throw new Error(`duplicate variant style name '${variant.styleName}'`);
|
|
158
|
+
}
|
|
159
|
+
seenStyles.add(variant.styleName);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return expanded;
|
|
163
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CellResult, CellSpec, Variant } from './types.ts';
|
|
2
|
+
export interface CellContext {
|
|
3
|
+
runId: string;
|
|
4
|
+
fixturesDir: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Hook command installed in a cell: the resolved bundle command plus the
|
|
8
|
+
* variant's extra args (Variant.hookArgs, e.g. '--marker-style informative')
|
|
9
|
+
* so experiments can vary engine behavior per variant. `root` is exposed for
|
|
10
|
+
* tests only; production callers use the package default.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hookCommandForVariant(variant: Variant, root?: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Environment for the claude child process (and therefore for the PostToolUse
|
|
15
|
+
* hook it spawns). CLAUDE_CONFIG_DIR isolates the cell; COMPRESSOR_NO_LEDGER
|
|
16
|
+
* keeps benchmark cells out of the user's LIVE savings ledger — hook-bearing
|
|
17
|
+
* cells run the real hook, and without the kill switch every worthwhile
|
|
18
|
+
* compression would append a synthetic event to ~/.compressor/ledger,
|
|
19
|
+
* corrupting what `compressor savings` reports. Exported for tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cellEnv(scratch: string): NodeJS.ProcessEnv;
|
|
22
|
+
/**
|
|
23
|
+
* Transcript totals and summed per-turn result JSONs count the same API
|
|
24
|
+
* responses, so they must roughly agree. Divergence beyond this relative
|
|
25
|
+
* tolerance means one of the two known failure topologies happened: a
|
|
26
|
+
* resumed session forked ids and the final transcript does NOT carry the
|
|
27
|
+
* full copied history (transcript ≪ sum: usage silently undercounts to
|
|
28
|
+
* roughly the last turn), or per-turn result JSONs report cumulative
|
|
29
|
+
* session usage (sum ≫ transcript: the fallback double-counts). Neither is
|
|
30
|
+
* detectable from one side alone; the cell is flagged data-quality-suspect.
|
|
31
|
+
*/
|
|
32
|
+
export declare const USAGE_MISMATCH_TOLERANCE = 0.25;
|
|
33
|
+
export declare function runCell(spec: CellSpec, ctx: CellContext): Promise<CellResult>;
|