@planu/cli 4.7.1 → 4.7.3
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 +18 -0
- package/dist/config/minimal-implementation-gate.json +110 -0
- package/dist/config/token-waste-autopilot.json +16 -0
- package/dist/engine/compact/compact-middleware.d.ts +2 -2
- package/dist/engine/compact/compact-middleware.js +68 -7
- package/dist/engine/context-artifacts/index.d.ts +2 -0
- package/dist/engine/context-artifacts/index.js +2 -0
- package/dist/engine/context-artifacts/store.d.ts +5 -0
- package/dist/engine/context-artifacts/store.js +176 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +112 -0
- package/dist/engine/handoff-artifacts/schemas.js +40 -0
- package/dist/engine/minimality/analyzer.d.ts +3 -0
- package/dist/engine/minimality/analyzer.js +140 -0
- package/dist/engine/minimality/formatter.d.ts +3 -0
- package/dist/engine/minimality/formatter.js +25 -0
- package/dist/engine/minimality/index.d.ts +4 -0
- package/dist/engine/minimality/index.js +4 -0
- package/dist/engine/minimality/policy-loader.d.ts +3 -0
- package/dist/engine/minimality/policy-loader.js +133 -0
- package/dist/engine/token-optimizer/content-aware-compactor.d.ts +4 -0
- package/dist/engine/token-optimizer/content-aware-compactor.js +230 -0
- package/dist/engine/token-optimizer/index.d.ts +1 -0
- package/dist/engine/token-optimizer/index.js +1 -0
- package/dist/engine/token-optimizer/output-filter.js +18 -2
- package/dist/engine/token-optimizer/policy-loader.js +12 -0
- package/dist/engine/token-optimizer/reporter.d.ts +4 -0
- package/dist/engine/token-optimizer/reporter.js +14 -1
- package/dist/engine/validator/validation-report-writer.d.ts +2 -0
- package/dist/engine/validator/validation-report-writer.js +19 -0
- package/dist/engine/web-fetcher/docs-fetcher.js +5 -1
- package/dist/tools/challenge-spec.js +25 -0
- package/dist/tools/package-handoff.js +23 -1
- package/dist/tools/safe-handler.js +4 -1
- package/dist/tools/token-usage-handler.js +5 -3
- package/dist/tools/update-status/dod-gates.js +9 -0
- package/dist/tools/validate.js +34 -0
- package/dist/types/compact/compact-mode.d.ts +5 -0
- package/dist/types/context-artifacts.d.ts +96 -0
- package/dist/types/context-artifacts.js +2 -0
- package/dist/types/handoff-artifacts.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/minimal-implementation-gate.d.ts +92 -0
- package/dist/types/minimal-implementation-gate.js +2 -0
- package/dist/types/token-optimization.d.ts +2 -0
- package/dist/types/token-waste-autopilot.d.ts +15 -0
- package/package.json +17 -17
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { formatMinimalImplementationReport } from './formatter.js';
|
|
2
|
+
const RISK_ORDER = ['low', 'medium', 'high', 'max'];
|
|
3
|
+
function riskRank(risk) {
|
|
4
|
+
const index = RISK_ORDER.indexOf(risk);
|
|
5
|
+
return index >= 0 ? index : 1;
|
|
6
|
+
}
|
|
7
|
+
function isExcludedPath(path, policy) {
|
|
8
|
+
return policy.excludedPathPatterns.some((pattern) => {
|
|
9
|
+
const escaped = pattern
|
|
10
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
11
|
+
.replace(/\*\*/g, '.*')
|
|
12
|
+
.replace(/\*/g, '[^/]*');
|
|
13
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function firstMatchingSafetyExclusion(text, policy) {
|
|
17
|
+
const lower = text.toLowerCase();
|
|
18
|
+
return policy.safetyExclusions.find((term) => lower.includes(term.toLowerCase())) ?? null;
|
|
19
|
+
}
|
|
20
|
+
function trimEvidence(value, maxChars) {
|
|
21
|
+
const compact = value.replace(/\s+/g, ' ').trim();
|
|
22
|
+
return compact.length > maxChars ? `${compact.slice(0, maxChars - 1)}...` : compact;
|
|
23
|
+
}
|
|
24
|
+
function lineForMatch(content, index) {
|
|
25
|
+
return content.slice(0, index).split('\n').length;
|
|
26
|
+
}
|
|
27
|
+
function ruleBlocksDone(rule, specRisk) {
|
|
28
|
+
if (rule.severity !== 'blocker') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (rule.blockWhenRiskAtLeast === undefined) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return riskRank(specRisk) >= riskRank(rule.blockWhenRiskAtLeast);
|
|
35
|
+
}
|
|
36
|
+
function makeEvidence(input, rule) {
|
|
37
|
+
return [...(input.evidence ?? []), { source: 'policy', key: rule.id }];
|
|
38
|
+
}
|
|
39
|
+
function scanText(args) {
|
|
40
|
+
for (const pattern of args.rule.patterns) {
|
|
41
|
+
const index = args.content.toLowerCase().indexOf(pattern.toLowerCase());
|
|
42
|
+
if (index < 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const evidence = trimEvidence(args.content.slice(index, index + 220), args.policy.report.maxEvidenceChars);
|
|
46
|
+
const safetyTerm = firstMatchingSafetyExclusion(args.content, args.policy);
|
|
47
|
+
if (safetyTerm !== null) {
|
|
48
|
+
return {
|
|
49
|
+
safety: {
|
|
50
|
+
target: args.target,
|
|
51
|
+
reason: `Matched safety exclusion: ${safetyTerm}`,
|
|
52
|
+
evidence,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
finding: {
|
|
58
|
+
ruleId: args.rule.id,
|
|
59
|
+
tag: args.rule.tag,
|
|
60
|
+
severity: args.rule.severity,
|
|
61
|
+
confidence: args.rule.confidence,
|
|
62
|
+
target: args.target,
|
|
63
|
+
line: args.target.includes(':') ? undefined : lineForMatch(args.content, index),
|
|
64
|
+
evidence,
|
|
65
|
+
replacementGuidance: args.rule.replacementGuidance,
|
|
66
|
+
blocksDone: ruleBlocksDone(args.rule, args.input.specRisk),
|
|
67
|
+
evidenceSource: makeEvidence(args.input, args.rule),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
function collectSources(input) {
|
|
74
|
+
const virtual = [];
|
|
75
|
+
if (input.specText) {
|
|
76
|
+
virtual.push({ path: 'spec.md', content: input.specText });
|
|
77
|
+
}
|
|
78
|
+
if (input.handoffText) {
|
|
79
|
+
virtual.push({ path: 'handoff', content: input.handoffText });
|
|
80
|
+
}
|
|
81
|
+
if (input.packageManifestText) {
|
|
82
|
+
virtual.push({ path: 'package.json', content: input.packageManifestText });
|
|
83
|
+
}
|
|
84
|
+
return [...virtual, ...(input.files ?? [])];
|
|
85
|
+
}
|
|
86
|
+
export function analyzeMinimalImplementation(input) {
|
|
87
|
+
const { policy } = input;
|
|
88
|
+
if (!policy.enabled) {
|
|
89
|
+
return {
|
|
90
|
+
enabled: false,
|
|
91
|
+
blocked: false,
|
|
92
|
+
findings: [],
|
|
93
|
+
safetyExceptions: [],
|
|
94
|
+
debtEvidence: input.acceptedDebt ?? [],
|
|
95
|
+
evidence: input.evidence ?? [],
|
|
96
|
+
markdown: '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const findings = [];
|
|
100
|
+
const safetyExceptions = [];
|
|
101
|
+
for (const source of collectSources(input)) {
|
|
102
|
+
if (isExcludedPath(source.path, policy) || source.content === undefined) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const rule of policy.rules) {
|
|
106
|
+
const result = scanText({
|
|
107
|
+
target: source.path,
|
|
108
|
+
content: source.content,
|
|
109
|
+
policy,
|
|
110
|
+
input,
|
|
111
|
+
rule,
|
|
112
|
+
});
|
|
113
|
+
if (result.safety) {
|
|
114
|
+
safetyExceptions.push(result.safety);
|
|
115
|
+
}
|
|
116
|
+
if (result.finding) {
|
|
117
|
+
findings.push(result.finding);
|
|
118
|
+
}
|
|
119
|
+
if (findings.length >= policy.report.maxFindings) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (findings.length >= policy.report.maxFindings) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const report = {
|
|
128
|
+
enabled: true,
|
|
129
|
+
blocked: findings.some((finding) => finding.blocksDone),
|
|
130
|
+
findings,
|
|
131
|
+
safetyExceptions,
|
|
132
|
+
debtEvidence: input.acceptedDebt ?? [],
|
|
133
|
+
evidence: input.evidence ?? [],
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
...report,
|
|
137
|
+
markdown: formatMinimalImplementationReport({ ...report, markdown: '' }, policy),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=analyzer.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MinimalImplementationPolicy, MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function formatMinimalImplementationReport(report: MinimalImplementationReport, policy?: MinimalImplementationPolicy): string;
|
|
3
|
+
//# sourceMappingURL=formatter.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatMinimalImplementationReport(report, policy) {
|
|
2
|
+
if (!report.enabled) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
const maxLines = policy?.report.maxMarkdownLines ?? 12;
|
|
6
|
+
const lines = ['## Minimal Implementation', ''];
|
|
7
|
+
if (report.findings.length === 0) {
|
|
8
|
+
lines.push('- No avoidable complexity findings from the current policy.');
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
for (const finding of report.findings.slice(0, Math.max(1, maxLines - 4))) {
|
|
12
|
+
const location = finding.line !== undefined ? `${finding.target}:${String(finding.line)}` : finding.target;
|
|
13
|
+
const marker = finding.blocksDone ? 'BLOCK' : finding.severity.toUpperCase();
|
|
14
|
+
lines.push(`- [${marker}] ${finding.tag} ${location}: ${finding.replacementGuidance}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (report.safetyExceptions.length > 0) {
|
|
18
|
+
lines.push(`- Safety exceptions preserved: ${String(report.safetyExceptions.length)} required safeguard(s).`);
|
|
19
|
+
}
|
|
20
|
+
if (report.debtEvidence.length > 0) {
|
|
21
|
+
lines.push(`- Structured debt evidence recorded: ${String(report.debtEvidence.length)} item(s).`);
|
|
22
|
+
}
|
|
23
|
+
return lines.slice(0, maxLines).join('\n');
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LoadedMinimalImplementationPolicy, MinimalImplementationPolicyLoadOptions } from '../../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function loadMinimalImplementationPolicy(projectPath?: string, options?: MinimalImplementationPolicyLoadOptions): Promise<LoadedMinimalImplementationPolicy>;
|
|
3
|
+
//# sourceMappingURL=policy-loader.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function configPath() {
|
|
5
|
+
return join(dirname(fileURLToPath(import.meta.url)), '../../config/minimal-implementation-gate.json');
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function mergeDeep(base, override) {
|
|
11
|
+
const result = { ...base };
|
|
12
|
+
for (const [key, value] of Object.entries(override)) {
|
|
13
|
+
const current = result[key];
|
|
14
|
+
result[key] = isRecord(current) && isRecord(value) ? mergeDeep(current, value) : value;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function assertString(value, path) {
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: string is required`);
|
|
21
|
+
}
|
|
22
|
+
if (value === null) {
|
|
23
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: null is not allowed`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected string`);
|
|
27
|
+
}
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: empty string is not allowed`);
|
|
30
|
+
}
|
|
31
|
+
if (value.trim().length === 0) {
|
|
32
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: whitespace-only string is not allowed`);
|
|
33
|
+
}
|
|
34
|
+
if (value.length > 500) {
|
|
35
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: string is too long`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertStringArray(value, path) {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected string[]`);
|
|
41
|
+
}
|
|
42
|
+
value.forEach((item, index) => {
|
|
43
|
+
assertString(item, `${path}.${String(index)}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function validateRule(value, path) {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected object`);
|
|
49
|
+
}
|
|
50
|
+
assertString(value.id, `${path}.id`);
|
|
51
|
+
assertString(value.tag, `${path}.tag`);
|
|
52
|
+
if (!['info', 'warning', 'blocker'].includes(String(value.severity))) {
|
|
53
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.severity`);
|
|
54
|
+
}
|
|
55
|
+
if (!['low', 'medium', 'high'].includes(String(value.confidence))) {
|
|
56
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.confidence`);
|
|
57
|
+
}
|
|
58
|
+
assertStringArray(value.patterns, `${path}.patterns`);
|
|
59
|
+
assertString(value.replacementGuidance, `${path}.replacementGuidance`);
|
|
60
|
+
if (value.blockWhenRiskAtLeast !== undefined &&
|
|
61
|
+
(typeof value.blockWhenRiskAtLeast !== 'string' ||
|
|
62
|
+
!['low', 'medium', 'high', 'max'].includes(value.blockWhenRiskAtLeast))) {
|
|
63
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.blockWhenRiskAtLeast`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function validatePolicy(value) {
|
|
68
|
+
if (!isRecord(value)) {
|
|
69
|
+
throw new Error('Invalid minimal implementation policy: expected object');
|
|
70
|
+
}
|
|
71
|
+
if (value.version !== 1) {
|
|
72
|
+
throw new Error('Invalid minimal implementation policy: version must be 1');
|
|
73
|
+
}
|
|
74
|
+
const policy = value;
|
|
75
|
+
if (typeof policy.enabled !== 'boolean') {
|
|
76
|
+
throw new Error('Invalid minimal implementation policy at enabled: expected boolean');
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(policy.report)) {
|
|
79
|
+
throw new Error('Invalid minimal implementation policy at report: expected object');
|
|
80
|
+
}
|
|
81
|
+
for (const key of ['maxFindings', 'maxMarkdownLines', 'maxEvidenceChars']) {
|
|
82
|
+
if (!Number.isFinite(policy.report[key]) || policy.report[key] < 1) {
|
|
83
|
+
throw new Error(`Invalid minimal implementation policy at report.${key}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(policy.tags)) {
|
|
87
|
+
throw new Error('Invalid minimal implementation policy at tags: expected object');
|
|
88
|
+
}
|
|
89
|
+
for (const [tag, definition] of Object.entries(policy.tags)) {
|
|
90
|
+
assertString(tag, `tags.${tag}`);
|
|
91
|
+
if (!isRecord(definition)) {
|
|
92
|
+
throw new Error(`Invalid minimal implementation policy at tags.${tag}: expected object`);
|
|
93
|
+
}
|
|
94
|
+
assertString(definition.description, `tags.${tag}.description`);
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(policy.rules) || policy.rules.length === 0) {
|
|
97
|
+
throw new Error('Invalid minimal implementation policy at rules: expected non-empty array');
|
|
98
|
+
}
|
|
99
|
+
policy.rules.forEach((rule, index) => validateRule(rule, `rules.${String(index)}`));
|
|
100
|
+
assertStringArray(policy.safetyExclusions, 'safetyExclusions');
|
|
101
|
+
assertStringArray(policy.excludedPathPatterns, 'excludedPathPatterns');
|
|
102
|
+
if (!isRecord(policy.debtEvidence)) {
|
|
103
|
+
throw new Error('Invalid minimal implementation policy at debtEvidence: expected object');
|
|
104
|
+
}
|
|
105
|
+
assertStringArray(policy.debtEvidence.requiredFields, 'debtEvidence.requiredFields');
|
|
106
|
+
return policy;
|
|
107
|
+
}
|
|
108
|
+
async function readJson(path) {
|
|
109
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
110
|
+
}
|
|
111
|
+
export async function loadMinimalImplementationPolicy(projectPath, options = {}) {
|
|
112
|
+
const defaultPolicyPath = options.defaultPolicyPath ?? configPath();
|
|
113
|
+
const defaultPolicy = validatePolicy(await readJson(defaultPolicyPath));
|
|
114
|
+
const evidence = [
|
|
115
|
+
{ source: 'policy', key: defaultPolicyPath },
|
|
116
|
+
];
|
|
117
|
+
let merged = defaultPolicy;
|
|
118
|
+
if (projectPath !== undefined) {
|
|
119
|
+
const planuConfigPath = join(projectPath, 'planu.json');
|
|
120
|
+
try {
|
|
121
|
+
const config = await readJson(planuConfigPath);
|
|
122
|
+
if (isRecord(config) && isRecord(config.minimalImplementationGate)) {
|
|
123
|
+
merged = validatePolicy(mergeDeep(defaultPolicy, config.minimalImplementationGate));
|
|
124
|
+
evidence.push({ source: 'project-config', key: 'planu.json.minimalImplementationGate' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Missing or malformed project config does not block default policy loading.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { policy: merged, evidence };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=policy-loader.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ContentAwareCompactionInput, ContentAwareCompactionResult, ContextArtifactContentType } from '../../types/context-artifacts.js';
|
|
2
|
+
export declare function classifyContentType(kind: string | undefined, text: string): ContextArtifactContentType;
|
|
3
|
+
export declare function compactContentAware(input: ContentAwareCompactionInput): ContentAwareCompactionResult;
|
|
4
|
+
//# sourceMappingURL=content-aware-compactor.d.ts.map
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { countTokens } from './counter.js';
|
|
2
|
+
import { storeContextArtifact } from '../context-artifacts/store.js';
|
|
3
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
const DEFAULT_MIN_TOKENS = 200;
|
|
5
|
+
function redact(text, patterns) {
|
|
6
|
+
return patterns.reduce((current, pattern) => {
|
|
7
|
+
const re = new RegExp(pattern, 'gi');
|
|
8
|
+
return current.replace(re, '[redacted]');
|
|
9
|
+
}, text);
|
|
10
|
+
}
|
|
11
|
+
function escapeInert(text) {
|
|
12
|
+
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
13
|
+
}
|
|
14
|
+
function bounded(value, maxChars) {
|
|
15
|
+
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}...`;
|
|
16
|
+
}
|
|
17
|
+
function isLikelyJson(text) {
|
|
18
|
+
const trimmed = text.trim();
|
|
19
|
+
return ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
20
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']')));
|
|
21
|
+
}
|
|
22
|
+
function isSearchResult(text) {
|
|
23
|
+
return text
|
|
24
|
+
.split('\n')
|
|
25
|
+
.some((line) => /^[^:\n]+:\d+(?::\d+)?:/.test(line) || /^[^:\n]+\(\d+,\d+\):/.test(line));
|
|
26
|
+
}
|
|
27
|
+
function isCode(text) {
|
|
28
|
+
const lines = text.split('\n');
|
|
29
|
+
const codeLines = lines.filter((line) => /^\s*(import|export|function|class|interface|type |const |let |var |def |fn |pub |func |struct |enum )/.test(line));
|
|
30
|
+
return lines.length > 0 && codeLines.length / lines.length > 0.12;
|
|
31
|
+
}
|
|
32
|
+
function isSpecOrHandoff(text) {
|
|
33
|
+
return (/\bSPEC-\d+\b/.test(text) ||
|
|
34
|
+
/\b(acceptance criteria|handoff|validation score|next action)\b/i.test(text));
|
|
35
|
+
}
|
|
36
|
+
export function classifyContentType(kind, text) {
|
|
37
|
+
const normalized = kind?.toLowerCase() ?? '';
|
|
38
|
+
if (normalized.includes('json') || isLikelyJson(text)) {
|
|
39
|
+
return 'json';
|
|
40
|
+
}
|
|
41
|
+
if (normalized.includes('test') ||
|
|
42
|
+
/\b(vitest|jest|failed tests?|test files?|assertionerror)\b/i.test(text)) {
|
|
43
|
+
return 'test-log';
|
|
44
|
+
}
|
|
45
|
+
if (normalized.includes('log') ||
|
|
46
|
+
/\b(error|warn|exception|stack trace|exit code)\b/i.test(text)) {
|
|
47
|
+
return 'runtime-log';
|
|
48
|
+
}
|
|
49
|
+
if (normalized.includes('search') || isSearchResult(text)) {
|
|
50
|
+
return 'search-results';
|
|
51
|
+
}
|
|
52
|
+
if (normalized.includes('code') || isCode(text)) {
|
|
53
|
+
return 'code';
|
|
54
|
+
}
|
|
55
|
+
if (normalized.includes('spec') || normalized.includes('handoff') || isSpecOrHandoff(text)) {
|
|
56
|
+
return 'spec-or-handoff';
|
|
57
|
+
}
|
|
58
|
+
return 'generic-text';
|
|
59
|
+
}
|
|
60
|
+
function summarizeJsonValue(value, depth = 0) {
|
|
61
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'number') {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'string') {
|
|
65
|
+
return value.length <= 80 ? value : `${value.slice(0, 80)}...`;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
return {
|
|
69
|
+
length: value.length,
|
|
70
|
+
sample: value.slice(0, 3).map((item) => summarizeJsonValue(item, depth + 1)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === 'object') {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, child] of Object.entries(value).slice(0, depth > 1 ? 12 : 40)) {
|
|
76
|
+
if (/^(status|state|count|total|id|uuid|name|ok|success|error|errors|warning|warnings)$/i.test(key)) {
|
|
77
|
+
out[key] = summarizeJsonValue(child, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
else if (depth < 2) {
|
|
80
|
+
out[key] = summarizeJsonValue(child, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'undefined') {
|
|
86
|
+
return 'undefined';
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === 'bigint') {
|
|
89
|
+
return value.toString();
|
|
90
|
+
}
|
|
91
|
+
if (typeof value === 'symbol') {
|
|
92
|
+
return value.description ?? 'symbol';
|
|
93
|
+
}
|
|
94
|
+
return '[unsupported]';
|
|
95
|
+
}
|
|
96
|
+
function compactJson(text) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(text);
|
|
99
|
+
return JSON.stringify(summarizeJsonValue(parsed), null, 2);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return compactGenericText(text);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function compactLog(text, maxLines) {
|
|
106
|
+
const lines = text.split('\n');
|
|
107
|
+
const important = lines.filter((line) => /\b(error|fail|failed|failure|exception|stack|trace|warning|warn|duration|total|passed|exit code|exit status)\b/i.test(line));
|
|
108
|
+
const selected = important.length > 0 ? important : lines;
|
|
109
|
+
return [...new Set(selected)].slice(0, maxLines).join('\n');
|
|
110
|
+
}
|
|
111
|
+
function compactSearchResults(text, maxLines, maxSnippetChars) {
|
|
112
|
+
const grouped = new Map();
|
|
113
|
+
for (const line of text.split('\n')) {
|
|
114
|
+
const match = /^([^:\n]+):(\d+)(?::\d+)?:\s?(.*)$/.exec(line);
|
|
115
|
+
if (!match) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const [, path, lineNumber, snippet] = match;
|
|
119
|
+
if (path === undefined || lineNumber === undefined) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const entries = grouped.get(path) ?? [];
|
|
123
|
+
entries.push(` ${lineNumber}: ${bounded(snippet ?? '', maxSnippetChars)}`);
|
|
124
|
+
grouped.set(path, [...new Set(entries)]);
|
|
125
|
+
}
|
|
126
|
+
if (grouped.size === 0) {
|
|
127
|
+
return compactGenericText(text);
|
|
128
|
+
}
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const [path, entries] of grouped.entries()) {
|
|
131
|
+
out.push(path, ...entries.slice(0, maxLines));
|
|
132
|
+
}
|
|
133
|
+
return out.slice(0, maxLines).join('\n');
|
|
134
|
+
}
|
|
135
|
+
function bracesBalanced(text) {
|
|
136
|
+
let balance = 0;
|
|
137
|
+
for (const char of text) {
|
|
138
|
+
if (char === '{') {
|
|
139
|
+
balance += 1;
|
|
140
|
+
}
|
|
141
|
+
else if (char === '}') {
|
|
142
|
+
balance -= 1;
|
|
143
|
+
}
|
|
144
|
+
if (balance < 0) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return balance === 0;
|
|
149
|
+
}
|
|
150
|
+
function compactCode(text) {
|
|
151
|
+
if (!bracesBalanced(text)) {
|
|
152
|
+
return compactGenericText(text);
|
|
153
|
+
}
|
|
154
|
+
const preserved = text
|
|
155
|
+
.split('\n')
|
|
156
|
+
.filter((line) => /^\s*(import|export|interface|type |class |function |async function|const [A-Z0-9_]+|enum |struct |def |fn |pub )/.test(line));
|
|
157
|
+
return preserved.length > 0 ? preserved.join('\n') : compactGenericText(text);
|
|
158
|
+
}
|
|
159
|
+
function compactSpecOrHandoff(text) {
|
|
160
|
+
const lines = text.split('\n');
|
|
161
|
+
const important = lines.filter((line) => /\b(SPEC-\d+|title:|status|acceptance criteria|blocker|blocked|files?|validation score|risk|security|privacy|next action|implementation|handoff)\b/i.test(line) ||
|
|
162
|
+
/^-\s*\[[ xX]\]\s+/.test(line) ||
|
|
163
|
+
/^#{1,3}\s/.test(line));
|
|
164
|
+
return (important.length > 0 ? important : lines).slice(0, 80).join('\n');
|
|
165
|
+
}
|
|
166
|
+
function compactGenericText(text) {
|
|
167
|
+
const lines = text.split('\n').filter((line) => line.trim().length > 0);
|
|
168
|
+
const important = lines.filter((line) => /^#{1,3}\s/.test(line) ||
|
|
169
|
+
/\b(error|failed|blocked|warning|todo|next action|pnpm|npm|git|https?:\/\/|\w+\/[\w./-]+\.\w+)\b/i.test(line));
|
|
170
|
+
return (important.length > 0 ? important : lines).slice(0, 60).join('\n');
|
|
171
|
+
}
|
|
172
|
+
function compactByType(type, text, maxLines, maxSnippetChars) {
|
|
173
|
+
switch (type) {
|
|
174
|
+
case 'json':
|
|
175
|
+
return compactJson(text);
|
|
176
|
+
case 'test-log':
|
|
177
|
+
case 'runtime-log':
|
|
178
|
+
return compactLog(text, maxLines);
|
|
179
|
+
case 'search-results':
|
|
180
|
+
return compactSearchResults(text, maxLines, maxSnippetChars);
|
|
181
|
+
case 'code':
|
|
182
|
+
return compactCode(text);
|
|
183
|
+
case 'spec-or-handoff':
|
|
184
|
+
return compactSpecOrHandoff(text);
|
|
185
|
+
case 'generic-text':
|
|
186
|
+
return compactGenericText(text);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function compactContentAware(input) {
|
|
190
|
+
const contentType = classifyContentType(input.kind, input.text);
|
|
191
|
+
const strategyConfig = input.policy.contentCompaction?.strategies?.[contentType];
|
|
192
|
+
const maxLines = strategyConfig?.maxLines ?? 60;
|
|
193
|
+
const maxSnippetChars = strategyConfig?.maxSnippetChars ?? input.policy.redaction.maxSnippetChars;
|
|
194
|
+
const redacted = redact(input.text, input.policy.redaction.redactPatterns);
|
|
195
|
+
const compact = escapeInert(compactByType(contentType, redacted, maxLines, maxSnippetChars));
|
|
196
|
+
const originalTokens = countTokens(input.text).tokens;
|
|
197
|
+
const compactTokens = countTokens(compact).tokens;
|
|
198
|
+
const result = {
|
|
199
|
+
text: compact,
|
|
200
|
+
originalTokens,
|
|
201
|
+
compactTokens,
|
|
202
|
+
tokensSaved: Math.max(0, originalTokens - compactTokens),
|
|
203
|
+
strategy: contentType,
|
|
204
|
+
contentType,
|
|
205
|
+
};
|
|
206
|
+
const artifactsEnabled = input.policy.contextArtifacts?.enabled !== false;
|
|
207
|
+
const minTokens = input.policy.contextArtifacts?.minTokens ?? DEFAULT_MIN_TOKENS;
|
|
208
|
+
if (!artifactsEnabled || input.projectPath === undefined || originalTokens < minTokens) {
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
const stored = storeContextArtifact({
|
|
212
|
+
projectPath: input.projectPath,
|
|
213
|
+
originalContent: input.text,
|
|
214
|
+
compactContent: compact,
|
|
215
|
+
contentType,
|
|
216
|
+
strategy: contentType,
|
|
217
|
+
originalTokens,
|
|
218
|
+
compactTokens,
|
|
219
|
+
ttlMs: input.policy.contextArtifacts?.ttlMs ?? DEFAULT_TTL_MS,
|
|
220
|
+
sourcePath: input.sourcePath,
|
|
221
|
+
flow: input.flow,
|
|
222
|
+
metadata: input.metadata,
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
...result,
|
|
226
|
+
...(stored.metadata !== undefined ? { artifact: stored.metadata } : {}),
|
|
227
|
+
...(stored.refusedReason !== undefined ? { refusedReason: stored.refusedReason } : {}),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=content-aware-compactor.js.map
|
|
@@ -9,6 +9,7 @@ export { OptimizationReporter } from './reporter.js';
|
|
|
9
9
|
export { TokenOptimizer } from './optimizer.js';
|
|
10
10
|
export { aggregateEntries, computeDailyBreakdown, detectTrend, detectAnomalies, computeBudgetStatus, getTopConsumers, computeOptimizationSavings, } from './analytics.js';
|
|
11
11
|
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
12
|
+
export { classifyContentType, compactContentAware } from './content-aware-compactor.js';
|
|
12
13
|
export { analyzeContextPreflight } from './context-preflight.js';
|
|
13
14
|
export { filterVerboseOutput } from './output-filter.js';
|
|
14
15
|
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
@@ -10,6 +10,7 @@ export { OptimizationReporter } from './reporter.js';
|
|
|
10
10
|
export { TokenOptimizer } from './optimizer.js';
|
|
11
11
|
export { aggregateEntries, computeDailyBreakdown, detectTrend, detectAnomalies, computeBudgetStatus, getTopConsumers, computeOptimizationSavings, } from './analytics.js';
|
|
12
12
|
export { loadTokenWastePolicy } from './policy-loader.js';
|
|
13
|
+
export { classifyContentType, compactContentAware } from './content-aware-compactor.js';
|
|
13
14
|
export { analyzeContextPreflight } from './context-preflight.js';
|
|
14
15
|
export { filterVerboseOutput } from './output-filter.js';
|
|
15
16
|
export { recommendRelevantTools, toolsFromPolicyGroups } from './tool-relevance.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compactContentAware } from './content-aware-compactor.js';
|
|
1
2
|
function escapeInert(text) {
|
|
2
3
|
return text.replace(/</g, '<').replace(/>/g, '>');
|
|
3
4
|
}
|
|
@@ -46,7 +47,21 @@ export function filterVerboseOutput(input) {
|
|
|
46
47
|
const strategy = selectStrategy(input);
|
|
47
48
|
const originalLines = input.text.split('\n').length;
|
|
48
49
|
const lines = compactLines(input, strategy);
|
|
49
|
-
|
|
50
|
+
let compact = redact(escapeInert(lines.join('\n')), input.policy.redaction.redactPatterns);
|
|
51
|
+
let compaction;
|
|
52
|
+
if (input.policy.contextArtifacts?.enabled === true && input.projectPath !== undefined) {
|
|
53
|
+
const contentAware = compactContentAware({
|
|
54
|
+
text: input.text,
|
|
55
|
+
policy: input.policy,
|
|
56
|
+
kind: input.kind,
|
|
57
|
+
projectPath: input.projectPath,
|
|
58
|
+
sourcePath: input.sourcePath,
|
|
59
|
+
flow: input.flow,
|
|
60
|
+
metadata: input.metadata,
|
|
61
|
+
});
|
|
62
|
+
compact = contentAware.text;
|
|
63
|
+
compaction = contentAware.artifact;
|
|
64
|
+
}
|
|
50
65
|
const decisions = [
|
|
51
66
|
{
|
|
52
67
|
decision: originalLines > lines.length ? 'summarize' : 'include',
|
|
@@ -59,8 +74,9 @@ export function filterVerboseOutput(input) {
|
|
|
59
74
|
return {
|
|
60
75
|
text: compact,
|
|
61
76
|
originalLines,
|
|
62
|
-
returnedLines:
|
|
77
|
+
returnedLines: compact.split('\n').length,
|
|
63
78
|
...(input.fullOutputRef !== undefined ? { fullOutputRef: input.fullOutputRef } : {}),
|
|
79
|
+
...(compaction !== undefined ? { compaction } : {}),
|
|
64
80
|
decisions,
|
|
65
81
|
};
|
|
66
82
|
}
|
|
@@ -39,6 +39,18 @@ function validatePolicy(value) {
|
|
|
39
39
|
if (!isRecord(policy.outputs.strategies)) {
|
|
40
40
|
throw new Error('Invalid token waste policy at outputs.strategies: expected object');
|
|
41
41
|
}
|
|
42
|
+
if (policy.contextArtifacts !== undefined) {
|
|
43
|
+
if (!Number.isFinite(policy.contextArtifacts.ttlMs) || policy.contextArtifacts.ttlMs < 1) {
|
|
44
|
+
throw new Error('Invalid token waste policy at contextArtifacts.ttlMs: expected positive number');
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isFinite(policy.contextArtifacts.minTokens) ||
|
|
47
|
+
policy.contextArtifacts.minTokens < 1) {
|
|
48
|
+
throw new Error('Invalid token waste policy at contextArtifacts.minTokens: expected positive number');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (policy.contentCompaction !== undefined && !isRecord(policy.contentCompaction.strategies)) {
|
|
52
|
+
throw new Error('Invalid token waste policy at contentCompaction.strategies: expected object');
|
|
53
|
+
}
|
|
42
54
|
if (!isRecord(policy.tools.groups)) {
|
|
43
55
|
throw new Error('Invalid token waste policy at tools.groups: expected object');
|
|
44
56
|
}
|
|
@@ -18,6 +18,10 @@ export declare class OptimizationReporter {
|
|
|
18
18
|
* Record a cache hit for a tool.
|
|
19
19
|
*/
|
|
20
20
|
recordCacheHit(toolName: string, tokensSaved: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Record measured savings from reversible compaction.
|
|
23
|
+
*/
|
|
24
|
+
recordCompaction(toolName: string, tokensSaved: number, retrievals?: number): void;
|
|
21
25
|
/**
|
|
22
26
|
* Record a cache miss for a tool.
|
|
23
27
|
*/
|