@ryuenn3123/agentic-senior-core 4.1.0 → 4.2.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/.agent-context/prompts/compact-natural-mode.md +100 -0
- package/.agent-context/prompts/init-project.md +1 -0
- package/.agent-context/prompts/refactor.md +1 -0
- package/.agent-context/review-checklists/pr-checklist.md +1 -0
- package/.agent-context/rules/architecture.md +10 -0
- package/.agent-context/rules/naming-conv.md +6 -3
- package/AGENTS.md +5 -7
- package/README.md +95 -117
- package/benchmarks/README.md +40 -0
- package/benchmarks/compact-natural-mode/fixtures.mjs +359 -0
- package/benchmarks/compact-natural-mode/scorer.mjs +331 -0
- package/benchmarks/runtime-token-saver/fixtures.mjs +613 -0
- package/bin/agentic-senior-core.js +6 -0
- package/bin/ascx.js +23 -0
- package/lib/cli/adaptive-context/catalog.mjs +428 -0
- package/lib/cli/adaptive-context/file-signals.mjs +100 -0
- package/lib/cli/adaptive-context/implications.mjs +44 -0
- package/lib/cli/adaptive-context.mjs +365 -0
- package/lib/cli/ascx/adapters/git-diff.mjs +223 -0
- package/lib/cli/ascx/adapters/git-status.mjs +145 -0
- package/lib/cli/ascx/adapters/npm-test.mjs +120 -0
- package/lib/cli/ascx/fixture-evaluator.mjs +180 -0
- package/lib/cli/ascx/formatter.mjs +46 -0
- package/lib/cli/ascx/lexer.mjs +113 -0
- package/lib/cli/ascx/runtime.mjs +188 -0
- package/lib/cli/ascx/tee-writer.mjs +38 -0
- package/lib/cli/ascx/token-estimate.mjs +15 -0
- package/lib/cli/commands/context.mjs +140 -0
- package/lib/cli/commands/init.mjs +2 -1
- package/lib/cli/commands/optimize.mjs +143 -2
- package/lib/cli/commands/upgrade.mjs +2 -0
- package/lib/cli/compiler.mjs +9 -0
- package/lib/cli/token-optimization.mjs +161 -6
- package/lib/cli/utils.mjs +15 -1
- package/package.json +10 -3
- package/scripts/adaptive-context/fixtures.mjs +188 -0
- package/scripts/adaptive-context-benchmark.mjs +9 -0
- package/scripts/ascx-runtime-token-saver-benchmark.mjs +9 -0
- package/scripts/compact-natural-mode-benchmark.mjs +9 -0
- package/scripts/validate/config.mjs +1 -0
- package/scripts/validate.mjs +2 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
const MAX_VISIBLE_STATUS_ENTRIES_PER_SECTION = 15;
|
|
2
|
+
|
|
3
|
+
function cleanStatusLine(line) {
|
|
4
|
+
return line.trim().replace(/\s+/g, ' ');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function createSection(title) {
|
|
8
|
+
return {
|
|
9
|
+
title,
|
|
10
|
+
entries: [],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pushSectionEntry(section, line) {
|
|
15
|
+
const cleanedLine = cleanStatusLine(line);
|
|
16
|
+
if (cleanedLine && !section.entries.includes(cleanedLine)) {
|
|
17
|
+
section.entries.push(cleanedLine);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseLongStatus(lines) {
|
|
22
|
+
const sections = [];
|
|
23
|
+
let activeSection = null;
|
|
24
|
+
let isClean = false;
|
|
25
|
+
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const trimmedLine = line.trim();
|
|
28
|
+
|
|
29
|
+
if (trimmedLine.includes('nothing to commit, working tree clean')) {
|
|
30
|
+
isClean = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (trimmedLine === 'Changes to be committed:') {
|
|
34
|
+
activeSection = createSection('staged');
|
|
35
|
+
sections.push(activeSection);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (trimmedLine === 'Changes not staged for commit:') {
|
|
40
|
+
activeSection = createSection('unstaged');
|
|
41
|
+
sections.push(activeSection);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (trimmedLine === 'Untracked files:') {
|
|
46
|
+
activeSection = createSection('untracked');
|
|
47
|
+
sections.push(activeSection);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!activeSection || !line.startsWith('\t')) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pushSectionEntry(activeSection, line);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { sections, isClean };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseShortStatus(lines) {
|
|
62
|
+
const shortEntries = lines
|
|
63
|
+
.map((line) => line.trimEnd())
|
|
64
|
+
.filter((line) => /^[ MADRCU?!]{1,2}\s+.+/u.test(line));
|
|
65
|
+
|
|
66
|
+
if (shortEntries.length === 0) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
title: 'short-status',
|
|
73
|
+
entries: shortEntries,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatSections(sections) {
|
|
79
|
+
const outputLines = [];
|
|
80
|
+
let truncated = false;
|
|
81
|
+
|
|
82
|
+
for (const section of sections) {
|
|
83
|
+
outputLines.push(`${section.title}: ${section.entries.length}`);
|
|
84
|
+
const visibleEntries = section.entries.slice(0, MAX_VISIBLE_STATUS_ENTRIES_PER_SECTION);
|
|
85
|
+
|
|
86
|
+
for (const entry of visibleEntries) {
|
|
87
|
+
outputLines.push(`- ${entry}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (section.entries.length > visibleEntries.length) {
|
|
91
|
+
truncated = true;
|
|
92
|
+
outputLines.push(`... truncated ${section.entries.length - visibleEntries.length} more ${section.title} entries`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
outputLines,
|
|
98
|
+
truncated,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function compressGitStatusOutput({ stdout, stderr, exitCode }) {
|
|
103
|
+
const rawOutput = [stdout, stderr].filter(Boolean).join('\n');
|
|
104
|
+
const lines = rawOutput.split(/\r?\n/u);
|
|
105
|
+
const longStatus = parseLongStatus(lines);
|
|
106
|
+
const shortStatusSections = parseShortStatus(lines);
|
|
107
|
+
const sections = longStatus.sections.length > 0 ? longStatus.sections : shortStatusSections;
|
|
108
|
+
|
|
109
|
+
if (exitCode === 0 && longStatus.isClean) {
|
|
110
|
+
return {
|
|
111
|
+
filterName: 'git-status-summary',
|
|
112
|
+
confident: true,
|
|
113
|
+
truncated: false,
|
|
114
|
+
output: 'git status: working tree clean',
|
|
115
|
+
preservedFields: {
|
|
116
|
+
changedFileList: true,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (sections.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
filterName: 'git-status-raw-parse-uncertain',
|
|
124
|
+
confident: false,
|
|
125
|
+
truncated: false,
|
|
126
|
+
output: rawOutput,
|
|
127
|
+
preservedFields: {},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const formattedSections = formatSections(sections);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
filterName: 'git-status-summary',
|
|
135
|
+
confident: true,
|
|
136
|
+
truncated: formattedSections.truncated,
|
|
137
|
+
output: [
|
|
138
|
+
'git status summary:',
|
|
139
|
+
...formattedSections.outputLines,
|
|
140
|
+
].join('\n'),
|
|
141
|
+
preservedFields: {
|
|
142
|
+
changedFileList: true,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const FAILURE_LINE_PATTERN = /(?:^not ok\b|AssertionError|Error:|TypeError:|ReferenceError|SyntaxError|Expected|Received|actual:|expected:|operator:|ERR!|failed|failure|FAIL\b|✖|×)/iu;
|
|
2
|
+
const FILE_LINE_PATTERN = /(?:[A-Za-z]:)?[^:\s]+?\.(?:cjs|mjs|js|jsx|ts|tsx):\d+(?::\d+)?/u;
|
|
3
|
+
const SUMMARY_LINE_PATTERN = /^#\s+(?:tests|suites|pass|fail|cancelled|skipped|todo|duration_ms)\b/u;
|
|
4
|
+
|
|
5
|
+
function pushUniqueLine(lines, nextLine) {
|
|
6
|
+
const normalizedLine = String(nextLine || '').trimEnd();
|
|
7
|
+
if (normalizedLine && !lines.includes(normalizedLine)) {
|
|
8
|
+
lines.push(normalizedLine);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function extractTapSummary(lines) {
|
|
13
|
+
return lines.filter((line) => SUMMARY_LINE_PATTERN.test(line.trim()));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractFailureLines(lines) {
|
|
17
|
+
const keptLines = [];
|
|
18
|
+
let lastSubtestLine = '';
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const trimmedLine = line.trim();
|
|
22
|
+
|
|
23
|
+
if (trimmedLine.startsWith('# Subtest:')) {
|
|
24
|
+
lastSubtestLine = trimmedLine;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (SUMMARY_LINE_PATTERN.test(trimmedLine)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isFailureLine = FAILURE_LINE_PATTERN.test(trimmedLine) || FILE_LINE_PATTERN.test(trimmedLine);
|
|
33
|
+
if (!isFailureLine) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (lastSubtestLine) {
|
|
38
|
+
pushUniqueLine(keptLines, lastSubtestLine);
|
|
39
|
+
}
|
|
40
|
+
pushUniqueLine(keptLines, line);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return keptLines;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasTestSummary(summaryLines) {
|
|
47
|
+
return summaryLines.some((line) => /^#\s+tests\b/u.test(line.trim()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getTapSummaryCount(summaryLines, summaryKey) {
|
|
51
|
+
const summaryLine = summaryLines.find((line) => {
|
|
52
|
+
return line.trim().startsWith(`# ${summaryKey} `);
|
|
53
|
+
});
|
|
54
|
+
const countMatch = summaryLine?.trim().match(/^#\s+\w+\s+(\d+)$/u);
|
|
55
|
+
|
|
56
|
+
return countMatch ? Number.parseInt(countMatch[1], 10) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function compressNpmTestOutput({ stdout, stderr, exitCode }) {
|
|
60
|
+
const rawOutput = [stdout, stderr].filter(Boolean).join('\n');
|
|
61
|
+
const lines = rawOutput.split(/\r?\n/u);
|
|
62
|
+
const summaryLines = extractTapSummary(lines);
|
|
63
|
+
const failCount = getTapSummaryCount(summaryLines, 'fail');
|
|
64
|
+
const shouldPreserveFailureEvidence = exitCode !== 0 || (typeof failCount === 'number' && failCount > 0);
|
|
65
|
+
const failureLines = shouldPreserveFailureEvidence ? extractFailureLines(lines) : [];
|
|
66
|
+
const outputLines = ['npm test summary:'];
|
|
67
|
+
|
|
68
|
+
if (summaryLines.length > 0) {
|
|
69
|
+
outputLines.push(...summaryLines);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (failureLines.length > 0) {
|
|
73
|
+
outputLines.push('failures:');
|
|
74
|
+
outputLines.push(...failureLines.slice(0, 80));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const truncated = failureLines.length > 80;
|
|
78
|
+
if (truncated) {
|
|
79
|
+
outputLines.push(`... truncated ${failureLines.length - 80} more failure evidence lines`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (exitCode === 0 && failureLines.length === 0) {
|
|
83
|
+
if (!hasTestSummary(summaryLines)) {
|
|
84
|
+
outputLines.push('result: passed');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
filterName: 'npm-test-summary',
|
|
89
|
+
confident: true,
|
|
90
|
+
truncated: false,
|
|
91
|
+
output: outputLines.join('\n'),
|
|
92
|
+
preservedFields: {
|
|
93
|
+
exitCode: true,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (failureLines.length === 0 && summaryLines.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
filterName: 'npm-test-raw-parse-uncertain',
|
|
101
|
+
confident: false,
|
|
102
|
+
truncated: false,
|
|
103
|
+
output: rawOutput,
|
|
104
|
+
preservedFields: {},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
filterName: 'npm-test-summary',
|
|
110
|
+
confident: true,
|
|
111
|
+
truncated,
|
|
112
|
+
output: outputLines.join('\n'),
|
|
113
|
+
preservedFields: {
|
|
114
|
+
rootError: failureLines.length > 0,
|
|
115
|
+
filePath: failureLines.some((line) => FILE_LINE_PATTERN.test(line)),
|
|
116
|
+
failingTestName: failureLines.some((line) => line.trim().startsWith('# Subtest:')),
|
|
117
|
+
assertionMessage: failureLines.some((line) => /AssertionError|Expected|Received|actual:|expected:/iu.test(line)),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { runAscx } from './runtime.mjs';
|
|
5
|
+
import { estimateOutputTokens } from './token-estimate.mjs';
|
|
6
|
+
|
|
7
|
+
function combineOutput(stdout, stderr) {
|
|
8
|
+
return [stdout, stderr].filter(Boolean).join('\n');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function fileExists(filePath) {
|
|
12
|
+
if (!filePath) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(filePath);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildFakeExecutor(fixtureEntry) {
|
|
25
|
+
return async () => ({
|
|
26
|
+
stdout: fixtureEntry.capture.stdout,
|
|
27
|
+
stderr: fixtureEntry.capture.stderr,
|
|
28
|
+
exitCode: fixtureEntry.capture.exitCode,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function evaluateContinuationChecks(fixtureEntry, output, teeExists) {
|
|
33
|
+
return (fixtureEntry.continuationChecks || []).map((continuationCheck) => {
|
|
34
|
+
const missingRequiredSubstrings = (continuationCheck.requiredSubstrings || [])
|
|
35
|
+
.filter((requiredSubstring) => !output.includes(requiredSubstring));
|
|
36
|
+
const presentForbiddenSubstrings = (continuationCheck.forbiddenSubstrings || [])
|
|
37
|
+
.filter((forbiddenSubstring) => output.includes(forbiddenSubstring));
|
|
38
|
+
const teeStateMatched = typeof continuationCheck.expectTee === 'boolean'
|
|
39
|
+
? continuationCheck.expectTee === teeExists
|
|
40
|
+
: true;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: continuationCheck.id,
|
|
44
|
+
action: continuationCheck.action,
|
|
45
|
+
passed: missingRequiredSubstrings.length === 0
|
|
46
|
+
&& presentForbiddenSubstrings.length === 0
|
|
47
|
+
&& teeStateMatched,
|
|
48
|
+
missingRequiredSubstrings,
|
|
49
|
+
presentForbiddenSubstrings,
|
|
50
|
+
expectedTee: continuationCheck.expectTee ?? null,
|
|
51
|
+
teeStateMatched,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function evaluateFixture(fixtureEntry, options) {
|
|
57
|
+
const result = await runAscx(fixtureEntry.commandArguments, {
|
|
58
|
+
cwd: options.cwd,
|
|
59
|
+
executeCommand: buildFakeExecutor(fixtureEntry),
|
|
60
|
+
teeDirectoryPath: options.teeDirectoryPath,
|
|
61
|
+
});
|
|
62
|
+
const output = combineOutput(result.stdout, result.stderr);
|
|
63
|
+
const missingRequiredSubstrings = (fixtureEntry.requiredSubstrings || [])
|
|
64
|
+
.filter((requiredSubstring) => !output.includes(requiredSubstring));
|
|
65
|
+
const presentForbiddenSubstrings = (fixtureEntry.forbiddenSubstrings || [])
|
|
66
|
+
.filter((forbiddenSubstring) => output.includes(forbiddenSubstring));
|
|
67
|
+
const exitCodePreserved = result.exitCode === fixtureEntry.capture.exitCode;
|
|
68
|
+
const compressionStateMatched = result.compressed === fixtureEntry.expectCompressed;
|
|
69
|
+
const teeExists = await fileExists(result.rawTeePath);
|
|
70
|
+
const teeStateMatched = fixtureEntry.expectTee === teeExists;
|
|
71
|
+
const falseSuccess = fixtureEntry.capture.exitCode !== 0 && result.exitCode === 0;
|
|
72
|
+
const rawOutput = combineOutput(fixtureEntry.capture.stdout, fixtureEntry.capture.stderr);
|
|
73
|
+
const rawTokens = estimateOutputTokens(rawOutput);
|
|
74
|
+
const outputTokens = estimateOutputTokens(output);
|
|
75
|
+
const continuationChecks = evaluateContinuationChecks(fixtureEntry, output, teeExists);
|
|
76
|
+
const failedContinuationChecks = continuationChecks.filter((continuationCheck) => {
|
|
77
|
+
return !continuationCheck.passed;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id: fixtureEntry.id,
|
|
82
|
+
passed: missingRequiredSubstrings.length === 0
|
|
83
|
+
&& presentForbiddenSubstrings.length === 0
|
|
84
|
+
&& exitCodePreserved
|
|
85
|
+
&& compressionStateMatched
|
|
86
|
+
&& teeStateMatched
|
|
87
|
+
&& !falseSuccess
|
|
88
|
+
&& failedContinuationChecks.length === 0,
|
|
89
|
+
command: fixtureEntry.commandArguments.join(' '),
|
|
90
|
+
classification: result.classification,
|
|
91
|
+
exitCode: result.exitCode,
|
|
92
|
+
expectedExitCode: fixtureEntry.capture.exitCode,
|
|
93
|
+
exitCodePreserved,
|
|
94
|
+
compressed: result.compressed,
|
|
95
|
+
expectedCompressed: fixtureEntry.expectCompressed,
|
|
96
|
+
rawTeePath: result.rawTeePath,
|
|
97
|
+
teeExists,
|
|
98
|
+
expectedTee: fixtureEntry.expectTee,
|
|
99
|
+
missingRequiredSubstrings,
|
|
100
|
+
requiredSubstringCount: (fixtureEntry.requiredSubstrings || []).length,
|
|
101
|
+
presentForbiddenSubstrings,
|
|
102
|
+
forbiddenSubstringCount: (fixtureEntry.forbiddenSubstrings || []).length,
|
|
103
|
+
falseSuccess,
|
|
104
|
+
continuationChecks,
|
|
105
|
+
continuationCheckCount: continuationChecks.length,
|
|
106
|
+
failedContinuationChecks,
|
|
107
|
+
rawTokens,
|
|
108
|
+
outputTokens,
|
|
109
|
+
reductionPercent: rawTokens === 0
|
|
110
|
+
? 0
|
|
111
|
+
: Number((((rawTokens - outputTokens) / rawTokens) * 100).toFixed(2)),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function evaluateAscxFixtures(fixtures, options = {}) {
|
|
116
|
+
const cwd = options.cwd || process.cwd();
|
|
117
|
+
const teeDirectoryPath = path.resolve(
|
|
118
|
+
options.teeDirectoryPath || path.join(cwd, '.agent-context', 'state', 'token-saver', 'tee')
|
|
119
|
+
);
|
|
120
|
+
const results = [];
|
|
121
|
+
|
|
122
|
+
for (const fixtureEntry of fixtures) {
|
|
123
|
+
results.push(await evaluateFixture(fixtureEntry, {
|
|
124
|
+
cwd,
|
|
125
|
+
teeDirectoryPath,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const failedResults = results.filter((result) => !result.passed);
|
|
130
|
+
const evidenceCheckCount = results.reduce((totalCount, result) => {
|
|
131
|
+
return totalCount
|
|
132
|
+
+ result.requiredSubstringCount
|
|
133
|
+
+ result.forbiddenSubstringCount
|
|
134
|
+
+ 4;
|
|
135
|
+
}, 0);
|
|
136
|
+
const failedEvidenceCheckCount = results.reduce((totalCount, result) => {
|
|
137
|
+
return totalCount
|
|
138
|
+
+ result.missingRequiredSubstrings.length
|
|
139
|
+
+ result.presentForbiddenSubstrings.length
|
|
140
|
+
+ (result.exitCodePreserved ? 0 : 1)
|
|
141
|
+
+ (result.compressed === result.expectedCompressed ? 0 : 1)
|
|
142
|
+
+ (result.teeExists === result.expectedTee ? 0 : 1)
|
|
143
|
+
+ (result.falseSuccess ? 1 : 0);
|
|
144
|
+
}, 0);
|
|
145
|
+
const rawTokens = results.reduce((totalCount, result) => totalCount + result.rawTokens, 0);
|
|
146
|
+
const outputTokens = results.reduce((totalCount, result) => totalCount + result.outputTokens, 0);
|
|
147
|
+
const continuationCheckCount = results.reduce((totalCount, result) => {
|
|
148
|
+
return totalCount + result.continuationCheckCount;
|
|
149
|
+
}, 0);
|
|
150
|
+
const failedContinuationCheckCount = results.reduce((totalCount, result) => {
|
|
151
|
+
return totalCount + result.failedContinuationChecks.length;
|
|
152
|
+
}, 0);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
reportName: 'ascx-runtime-token-saver-benchmark',
|
|
156
|
+
generatedAt: new Date().toISOString(),
|
|
157
|
+
fixtureCount: results.length,
|
|
158
|
+
passed: failedResults.length === 0,
|
|
159
|
+
passedCount: results.length - failedResults.length,
|
|
160
|
+
failedCount: failedResults.length,
|
|
161
|
+
summary: {
|
|
162
|
+
rawTokens,
|
|
163
|
+
outputTokens,
|
|
164
|
+
estimatedTokenReductionPercent: rawTokens === 0
|
|
165
|
+
? 0
|
|
166
|
+
: Number((((rawTokens - outputTokens) / rawTokens) * 100).toFixed(2)),
|
|
167
|
+
evidencePreservationPassRate: evidenceCheckCount === 0
|
|
168
|
+
? 1
|
|
169
|
+
: Number(((evidenceCheckCount - failedEvidenceCheckCount) / evidenceCheckCount).toFixed(4)),
|
|
170
|
+
falseSuccessCount: results.filter((result) => result.falseSuccess).length,
|
|
171
|
+
teeWriteFailures: results.filter((result) => result.expectedTee && !result.teeExists).length,
|
|
172
|
+
continuationPassRate: continuationCheckCount === 0
|
|
173
|
+
? 1
|
|
174
|
+
: Number(((continuationCheckCount - failedContinuationCheckCount) / continuationCheckCount).toFixed(4)),
|
|
175
|
+
continuationCheckCount,
|
|
176
|
+
failedContinuationCheckCount,
|
|
177
|
+
},
|
|
178
|
+
results,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateReductionPercent,
|
|
3
|
+
estimateOutputTokens,
|
|
4
|
+
ASCX_TOKEN_ESTIMATE_METHOD,
|
|
5
|
+
} from './token-estimate.mjs';
|
|
6
|
+
|
|
7
|
+
export const HIGH_RISK_REDUCTION_PERCENT = 80;
|
|
8
|
+
|
|
9
|
+
export function buildAscxFooter({
|
|
10
|
+
classification,
|
|
11
|
+
commandText,
|
|
12
|
+
compactOutput,
|
|
13
|
+
exitCode,
|
|
14
|
+
filterName,
|
|
15
|
+
rawOutput,
|
|
16
|
+
rawTeePath,
|
|
17
|
+
}) {
|
|
18
|
+
const rawTokens = estimateOutputTokens(rawOutput);
|
|
19
|
+
const outputTokens = estimateOutputTokens(compactOutput);
|
|
20
|
+
const reductionPercent = calculateReductionPercent(rawTokens, outputTokens);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
rawTokens,
|
|
24
|
+
outputTokens,
|
|
25
|
+
reductionPercent,
|
|
26
|
+
text: [
|
|
27
|
+
'[ascx]',
|
|
28
|
+
`command: ${commandText}`,
|
|
29
|
+
`exit: ${exitCode}`,
|
|
30
|
+
`classification: ${classification}`,
|
|
31
|
+
`filter: ${filterName}`,
|
|
32
|
+
`token_method: ${ASCX_TOKEN_ESTIMATE_METHOD}`,
|
|
33
|
+
`raw_tokens: ${rawTokens}`,
|
|
34
|
+
`output_tokens: ${outputTokens}`,
|
|
35
|
+
`reduction: ${reductionPercent}%`,
|
|
36
|
+
`raw_output: ${rawTeePath || 'none'}`,
|
|
37
|
+
].join('\n'),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function shouldWriteSafetyTee({ adapterResult, exitCode, reductionPercent }) {
|
|
42
|
+
return exitCode !== 0
|
|
43
|
+
|| adapterResult.truncated === true
|
|
44
|
+
|| adapterResult.confident === false
|
|
45
|
+
|| reductionPercent >= HIGH_RISK_REDUCTION_PERCENT;
|
|
46
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const SHELL_OPERATOR_TOKENS = new Set([
|
|
2
|
+
'|',
|
|
3
|
+
'||',
|
|
4
|
+
'&',
|
|
5
|
+
'&&',
|
|
6
|
+
';',
|
|
7
|
+
'>',
|
|
8
|
+
'>>',
|
|
9
|
+
'<',
|
|
10
|
+
'2>',
|
|
11
|
+
'2>>',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function isEnvironmentAssignment(argumentValue) {
|
|
15
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=.+$/u.test(argumentValue);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hasCommandSubstitution(argumentValue) {
|
|
19
|
+
return argumentValue.includes('$(') || argumentValue.includes('`');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasRedirectToken(argumentValue) {
|
|
23
|
+
return /^(?:[12]?>|[12]?>>|<)/u.test(argumentValue);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseAscxCommand(commandArguments = []) {
|
|
27
|
+
const rawArguments = commandArguments.map((argumentValue) => String(argumentValue));
|
|
28
|
+
const unsafeTokens = rawArguments.filter((argumentValue) => {
|
|
29
|
+
return SHELL_OPERATOR_TOKENS.has(argumentValue)
|
|
30
|
+
|| hasCommandSubstitution(argumentValue)
|
|
31
|
+
|| hasRedirectToken(argumentValue);
|
|
32
|
+
});
|
|
33
|
+
const environment = [];
|
|
34
|
+
let executableIndex = 0;
|
|
35
|
+
|
|
36
|
+
while (
|
|
37
|
+
executableIndex < rawArguments.length
|
|
38
|
+
&& isEnvironmentAssignment(rawArguments[executableIndex])
|
|
39
|
+
) {
|
|
40
|
+
environment.push(rawArguments[executableIndex]);
|
|
41
|
+
executableIndex += 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const executable = rawArguments[executableIndex] || '';
|
|
45
|
+
const args = executable ? rawArguments.slice(executableIndex + 1) : [];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
rawArguments,
|
|
49
|
+
commandText: rawArguments.join(' '),
|
|
50
|
+
environment,
|
|
51
|
+
executable,
|
|
52
|
+
args,
|
|
53
|
+
unsafeTokens,
|
|
54
|
+
hasShellSyntax: unsafeTokens.length > 0,
|
|
55
|
+
hasEnvironmentPrefix: environment.length > 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function classifyAscxInvocation(parsedCommand) {
|
|
60
|
+
if (!parsedCommand.executable) {
|
|
61
|
+
return {
|
|
62
|
+
kind: 'passthrough',
|
|
63
|
+
adapterName: null,
|
|
64
|
+
reason: 'missing executable',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (parsedCommand.hasShellSyntax) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'unsafe-for-compression',
|
|
71
|
+
adapterName: null,
|
|
72
|
+
reason: `shell syntax detected: ${parsedCommand.unsafeTokens.join(', ')}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (parsedCommand.hasEnvironmentPrefix) {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'passthrough',
|
|
79
|
+
adapterName: null,
|
|
80
|
+
reason: 'environment prefix detected',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (parsedCommand.executable === 'git' && parsedCommand.args[0] === 'status') {
|
|
85
|
+
return {
|
|
86
|
+
kind: 'compressible',
|
|
87
|
+
adapterName: 'git-status',
|
|
88
|
+
reason: 'supported git status adapter',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (parsedCommand.executable === 'git' && parsedCommand.args[0] === 'diff') {
|
|
93
|
+
return {
|
|
94
|
+
kind: 'compressible',
|
|
95
|
+
adapterName: 'git-diff',
|
|
96
|
+
reason: 'supported git diff adapter',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (parsedCommand.executable === 'npm' && parsedCommand.args[0] === 'test') {
|
|
101
|
+
return {
|
|
102
|
+
kind: 'compressible',
|
|
103
|
+
adapterName: 'npm-test',
|
|
104
|
+
reason: 'supported npm test adapter',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
kind: 'passthrough',
|
|
110
|
+
adapterName: null,
|
|
111
|
+
reason: 'unsupported command',
|
|
112
|
+
};
|
|
113
|
+
}
|