@ryuenn3123/agentic-senior-core 4.1.0 → 4.2.1

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.
Files changed (50) hide show
  1. package/.agent-context/prompts/compact-natural-mode.md +100 -0
  2. package/.agent-context/prompts/init-project.md +1 -0
  3. package/.agent-context/prompts/refactor.md +1 -0
  4. package/.agent-context/review-checklists/pr-checklist.md +1 -0
  5. package/.agent-context/rules/architecture.md +10 -0
  6. package/.agent-context/rules/naming-conv.md +6 -3
  7. package/.agent-context/state/README.md +2 -1
  8. package/AGENTS.md +6 -8
  9. package/README.md +95 -117
  10. package/benchmarks/README.md +60 -0
  11. package/benchmarks/compact-natural-mode/fixtures.mjs +359 -0
  12. package/benchmarks/compact-natural-mode/scorer.mjs +331 -0
  13. package/benchmarks/runtime-token-saver/fixtures.mjs +714 -0
  14. package/bin/agentic-senior-core.js +6 -0
  15. package/bin/ascx.js +23 -0
  16. package/lib/cli/adaptive-context/catalog.mjs +428 -0
  17. package/lib/cli/adaptive-context/file-signals.mjs +100 -0
  18. package/lib/cli/adaptive-context/implications.mjs +44 -0
  19. package/lib/cli/adaptive-context.mjs +365 -0
  20. package/lib/cli/ascx/adapters/git-diff.mjs +223 -0
  21. package/lib/cli/ascx/adapters/git-status.mjs +145 -0
  22. package/lib/cli/ascx/adapters/npm-run-build.mjs +99 -0
  23. package/lib/cli/ascx/adapters/npm-test.mjs +120 -0
  24. package/lib/cli/ascx/adapters/rg.mjs +39 -0
  25. package/lib/cli/ascx/fixture-evaluator.mjs +180 -0
  26. package/lib/cli/ascx/formatter.mjs +47 -0
  27. package/lib/cli/ascx/lexer.mjs +129 -0
  28. package/lib/cli/ascx/runtime.mjs +192 -0
  29. package/lib/cli/ascx/tee-writer.mjs +63 -0
  30. package/lib/cli/ascx/token-estimate.mjs +15 -0
  31. package/lib/cli/backup.mjs +37 -4
  32. package/lib/cli/commands/context.mjs +140 -0
  33. package/lib/cli/commands/init.mjs +14 -2
  34. package/lib/cli/commands/optimize.mjs +143 -2
  35. package/lib/cli/commands/upgrade/design-intent-seed.mjs +46 -0
  36. package/lib/cli/commands/upgrade/token-optimization-state.mjs +51 -0
  37. package/lib/cli/commands/upgrade.mjs +34 -45
  38. package/lib/cli/compiler.mjs +9 -0
  39. package/lib/cli/project-scaffolder/prompt-builders.mjs +1 -0
  40. package/lib/cli/token-optimization.mjs +161 -6
  41. package/lib/cli/utils.mjs +15 -1
  42. package/package.json +10 -3
  43. package/scripts/adaptive-context/fixtures.mjs +188 -0
  44. package/scripts/adaptive-context-benchmark.mjs +9 -0
  45. package/scripts/ascx-runtime-token-saver-benchmark.mjs +9 -0
  46. package/scripts/build-release-benchmark-bundle.mjs +1 -3
  47. package/scripts/clean-local-artifacts.mjs +2 -0
  48. package/scripts/compact-natural-mode-benchmark.mjs +9 -0
  49. package/scripts/validate/config.mjs +6 -0
  50. 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,99 @@
1
+ const FAILURE_LINE_PATTERN = /(?:^Error:|^TypeError:|^SyntaxError|^ReferenceError|^TS\d+:\s|^Failed to compile|Module not found|ERR!|error\b|ERROR\b|×|✖|failure|FAIL\b)/iu;
2
+ const FILE_LINE_PATTERN = /(?:[A-Za-z]:)?[^:\s]+?\.(?:cjs|mjs|js|jsx|ts|tsx|vue|svelte|css|scss|html|json):\d+(?::\d+)?/u;
3
+
4
+ function pushUniqueLine(lines, nextLine) {
5
+ const normalizedLine = String(nextLine || '').trimEnd();
6
+ if (normalizedLine && !lines.includes(normalizedLine)) {
7
+ lines.push(normalizedLine);
8
+ }
9
+ }
10
+
11
+ function extractFailureLines(lines) {
12
+ const keptLines = [];
13
+ let contextBuffer = [];
14
+
15
+ for (const line of lines) {
16
+ const trimmedLine = line.trim();
17
+
18
+ // Preserve empty lines in context buffer to maintain readability if we decide to flush it
19
+ if (!trimmedLine) {
20
+ if (contextBuffer.length > 0) {
21
+ contextBuffer.push(line);
22
+ }
23
+ continue;
24
+ }
25
+
26
+ const isFailureLine = FAILURE_LINE_PATTERN.test(trimmedLine) || FILE_LINE_PATTERN.test(trimmedLine);
27
+
28
+ if (isFailureLine) {
29
+ // If we found a failure, flush the preceding short context (e.g., file paths that didn't match the regex but preceded an error)
30
+ for (const ctxLine of contextBuffer) {
31
+ pushUniqueLine(keptLines, ctxLine);
32
+ }
33
+ contextBuffer = [];
34
+ pushUniqueLine(keptLines, line);
35
+ } else {
36
+ // Keep a small rolling buffer of context lines (max 2) before an error
37
+ contextBuffer.push(line);
38
+ if (contextBuffer.length > 2) {
39
+ contextBuffer.shift();
40
+ }
41
+ }
42
+ }
43
+
44
+ return keptLines;
45
+ }
46
+
47
+ export function compressNpmRunBuildOutput({ stdout, stderr, exitCode }) {
48
+ const rawOutput = [stdout, stderr].filter(Boolean).join('\n');
49
+ const lines = rawOutput.split(/\r?\n/u);
50
+
51
+ // If exit code is 0, build probably succeeded, just return a short success message.
52
+ if (exitCode === 0) {
53
+ return {
54
+ filterName: 'npm-run-build-summary',
55
+ confident: true,
56
+ truncated: false,
57
+ output: 'npm run build summary:\nresult: passed',
58
+ preservedFields: {
59
+ exitCode: true,
60
+ },
61
+ };
62
+ }
63
+
64
+ const failureLines = extractFailureLines(lines);
65
+ const outputLines = ['npm run build summary:'];
66
+
67
+ if (failureLines.length > 0) {
68
+ outputLines.push('failures:');
69
+ outputLines.push(...failureLines.slice(0, 100));
70
+ }
71
+
72
+ const truncated = failureLines.length > 100;
73
+ if (truncated) {
74
+ outputLines.push(`... truncated ${failureLines.length - 100} more failure evidence lines`);
75
+ }
76
+
77
+ if (failureLines.length === 0) {
78
+ // We couldn't parse the errors clearly, fallback to raw
79
+ return {
80
+ filterName: 'npm-run-build-raw-parse-uncertain',
81
+ confident: false,
82
+ truncated: false,
83
+ output: rawOutput,
84
+ preservedFields: {},
85
+ };
86
+ }
87
+
88
+ return {
89
+ filterName: 'npm-run-build-summary',
90
+ confident: true,
91
+ truncated,
92
+ output: outputLines.join('\n'),
93
+ preservedFields: {
94
+ rootError: failureLines.length > 0,
95
+ filePath: failureLines.some((line) => FILE_LINE_PATTERN.test(line)),
96
+ exitCode: true,
97
+ },
98
+ };
99
+ }
@@ -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,39 @@
1
+ export function compressRgOutput({ stdout, stderr, exitCode }) {
2
+ const rawOutput = [stdout, stderr].filter(Boolean).join('\n');
3
+ const lines = rawOutput.split(/\r?\n/u);
4
+
5
+ // If exit code is non-zero and no output, rg found nothing or errored
6
+ if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) {
7
+ return {
8
+ filterName: 'rg-summary',
9
+ confident: true,
10
+ truncated: false,
11
+ output: exitCode === 0 ? 'No matches found.' : `ripgrep exited with code ${exitCode}`,
12
+ preservedFields: {
13
+ exitCode: true,
14
+ },
15
+ };
16
+ }
17
+
18
+ const outputLines = ['ripgrep results:'];
19
+ const maxLines = 80;
20
+
21
+ if (lines.length > maxLines) {
22
+ outputLines.push(...lines.slice(0, maxLines));
23
+ outputLines.push(`... truncated ${lines.length - maxLines} more lines of matches`);
24
+ outputLines.push(`truncation: full search results available in the raw tee output`);
25
+ } else {
26
+ outputLines.push(...lines);
27
+ }
28
+
29
+ return {
30
+ filterName: 'rg-summary',
31
+ confident: true,
32
+ truncated: lines.length > maxLines,
33
+ output: outputLines.join('\n'),
34
+ preservedFields: {
35
+ exitCode: true,
36
+ filePath: true, // We assume rg output contains file paths
37
+ },
38
+ };
39
+ }
@@ -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,47 @@
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
+ if (exitCode !== 0) return true;
43
+ if (adapterResult.truncated === true) return true;
44
+ if (adapterResult.confident === false) return true;
45
+ if (adapterResult.confident !== true && reductionPercent >= HIGH_RISK_REDUCTION_PERCENT) return true;
46
+ return false;
47
+ }