@ryuenn3123/agentic-senior-core 4.0.3 → 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.
Files changed (63) 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/api-docs.md +14 -0
  6. package/.agent-context/rules/api-versioning.md +93 -0
  7. package/.agent-context/rules/architecture.md +10 -0
  8. package/.agent-context/rules/background-jobs.md +93 -0
  9. package/.agent-context/rules/config-and-flags.md +79 -0
  10. package/.agent-context/rules/database-design.md +32 -0
  11. package/.agent-context/rules/frontend-architecture.md +35 -0
  12. package/.agent-context/rules/migrations.md +84 -0
  13. package/.agent-context/rules/naming-conv.md +6 -3
  14. package/.agent-context/rules/observability.md +69 -0
  15. package/.agent-context/rules/resilience.md +78 -0
  16. package/.agent-context/rules/security.md +28 -0
  17. package/AGENTS.md +13 -15
  18. package/README.md +102 -91
  19. package/benchmarks/README.md +40 -0
  20. package/benchmarks/compact-natural-mode/fixtures.mjs +359 -0
  21. package/benchmarks/compact-natural-mode/scorer.mjs +331 -0
  22. package/benchmarks/runtime-token-saver/fixtures.mjs +613 -0
  23. package/bin/agentic-senior-core.js +6 -0
  24. package/bin/ascx.js +23 -0
  25. package/lib/cli/adaptive-context/catalog.mjs +428 -0
  26. package/lib/cli/adaptive-context/file-signals.mjs +100 -0
  27. package/lib/cli/adaptive-context/implications.mjs +44 -0
  28. package/lib/cli/adaptive-context.mjs +365 -0
  29. package/lib/cli/ascx/adapters/git-diff.mjs +223 -0
  30. package/lib/cli/ascx/adapters/git-status.mjs +145 -0
  31. package/lib/cli/ascx/adapters/npm-test.mjs +120 -0
  32. package/lib/cli/ascx/fixture-evaluator.mjs +180 -0
  33. package/lib/cli/ascx/formatter.mjs +46 -0
  34. package/lib/cli/ascx/lexer.mjs +113 -0
  35. package/lib/cli/ascx/runtime.mjs +188 -0
  36. package/lib/cli/ascx/tee-writer.mjs +38 -0
  37. package/lib/cli/ascx/token-estimate.mjs +15 -0
  38. package/lib/cli/commands/context.mjs +140 -0
  39. package/lib/cli/commands/init.mjs +2 -1
  40. package/lib/cli/commands/optimize.mjs +143 -2
  41. package/lib/cli/commands/upgrade.mjs +2 -0
  42. package/lib/cli/compiler.mjs +9 -0
  43. package/lib/cli/token-optimization.mjs +161 -6
  44. package/lib/cli/utils.mjs +15 -1
  45. package/package.json +11 -5
  46. package/scripts/adaptive-context/fixtures.mjs +188 -0
  47. package/scripts/adaptive-context-benchmark.mjs +9 -0
  48. package/scripts/ascx-runtime-token-saver-benchmark.mjs +9 -0
  49. package/scripts/audit-cache-layer-contract.mjs +5 -0
  50. package/scripts/audit-caching-scope-hygiene.mjs +5 -0
  51. package/scripts/clean-local-artifacts.mjs +0 -1
  52. package/scripts/compact-natural-mode-benchmark.mjs +9 -0
  53. package/scripts/frontend-usability-audit.mjs +5 -8
  54. package/scripts/release-gate/static-checks.mjs +7 -7
  55. package/scripts/validate/config.mjs +1 -2
  56. package/scripts/validate/coverage-checks.mjs +1 -42
  57. package/scripts/validate.mjs +11 -7
  58. package/scripts/migrate-rule-format/id-prefix-table.mjs +0 -37
  59. package/scripts/migrate-rule-format/parse-legacy.mjs +0 -180
  60. package/scripts/migrate-rule-format/render-new.mjs +0 -169
  61. package/scripts/migrate-rule-format/roundtrip-validate.mjs +0 -89
  62. package/scripts/migrate-rule-format.mjs +0 -192
  63. package/scripts/v3-purge-audit.mjs +0 -236
@@ -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
+ }