@planu/cli 4.7.2 → 4.7.4
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 +19 -1
- package/README.md +6 -3
- package/dist/config/token-waste-autopilot.json +16 -0
- package/dist/engine/cascade-hooks/core/append-releases.d.ts +2 -1
- package/dist/engine/cascade-hooks/core/append-releases.js +30 -3
- 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/evidence-gates/artifact-reader.js +15 -2
- package/dist/engine/evidence-gates/lifecycle-gate.js +14 -2
- package/dist/engine/handoff-packager.js +6 -49
- package/dist/engine/readiness-checker.js +13 -7
- package/dist/engine/spec-format/acceptance-criteria.d.ts +5 -0
- package/dist/engine/spec-format/acceptance-criteria.js +106 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +16 -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/universal-rules/rules/planu-release-policy.js +8 -5
- package/dist/engine/web-fetcher/docs-fetcher.js +5 -1
- package/dist/native/lightweight-command-catalog.d.ts +1 -1
- package/dist/native/lightweight-command-catalog.js +1 -1
- package/dist/resources/process.js +3 -2
- package/dist/tools/challenge-spec/event-challenge-scenarios.js +10 -6
- package/dist/tools/git/hook-ops.js +1 -1
- package/dist/tools/init-project/per-client-files-writer.js +2 -1
- package/dist/tools/init-project/rules-generator.js +14 -0
- package/dist/tools/safe-handler.js +4 -1
- package/dist/tools/token-usage-handler.js +5 -3
- package/dist/tools/update-status/evidence-gate.js +5 -9
- package/dist/types/cascade-hooks.d.ts +5 -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/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec-format.d.ts +6 -0
- package/dist/types/token-optimization.d.ts +2 -0
- package/dist/types/token-waste-autopilot.d.ts +15 -0
- package/package.json +18 -18
- package/planu-native.json +30 -9
- package/planu-plugin.json +35 -7
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
|
|
2
|
+
import { stripFrontmatter } from '../frontmatter-parser.js';
|
|
3
|
+
import { extractListItems, extractSection } from '../validator/extractors.js';
|
|
4
|
+
function normalizeCriterionValue(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/^-\s*\[[ x]\]\s*/i, '')
|
|
7
|
+
.replace(/[`*_()[\]:]/g, ' ')
|
|
8
|
+
.replace(/\s+/g, ' ')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
function stripCriterionIdPrefix(value) {
|
|
13
|
+
return value.replace(/^(AC\d+)\s*[:.)-]?\s*/i, '').trim();
|
|
14
|
+
}
|
|
15
|
+
function detectCriterionId(value, index) {
|
|
16
|
+
const explicit = /^(AC\d+)\b/i.exec(value)?.[1];
|
|
17
|
+
return (explicit ?? `AC${String(index + 1)}`).toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
function extractSectionCriteria(section) {
|
|
20
|
+
const listItems = extractListItems(section);
|
|
21
|
+
if (listItems.length > 0) {
|
|
22
|
+
return listItems;
|
|
23
|
+
}
|
|
24
|
+
const lines = section.split('\n').map((line) => line.trim());
|
|
25
|
+
if (lines.length === 0) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const bddBlocks = [];
|
|
29
|
+
let current = [];
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.length === 0) {
|
|
32
|
+
if (current.length > 0) {
|
|
33
|
+
bddBlocks.push(current.join(' '));
|
|
34
|
+
current = [];
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (/^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i.test(line)) {
|
|
39
|
+
if (/^(GIVEN|DADO)\b/i.test(line) && current.length > 0) {
|
|
40
|
+
bddBlocks.push(current.join(' '));
|
|
41
|
+
current = [];
|
|
42
|
+
}
|
|
43
|
+
current.push(line.replace(/^[-*+]\s*/, ''));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (current.length > 0) {
|
|
47
|
+
current.push(line);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (current.length > 0) {
|
|
51
|
+
bddBlocks.push(current.join(' '));
|
|
52
|
+
}
|
|
53
|
+
return bddBlocks;
|
|
54
|
+
}
|
|
55
|
+
function extractBodyCriteria(raw) {
|
|
56
|
+
const body = stripFrontmatter(raw);
|
|
57
|
+
const acceptanceCriteria = extractSection(body, 'acceptance criteria', 'criterios de aceptaci');
|
|
58
|
+
if (acceptanceCriteria) {
|
|
59
|
+
const sectionCriteria = extractSectionCriteria(acceptanceCriteria);
|
|
60
|
+
if (sectionCriteria.length > 0) {
|
|
61
|
+
return sectionCriteria;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return body
|
|
65
|
+
.split('\n')
|
|
66
|
+
.map((line) => line.trim())
|
|
67
|
+
.filter((line) => /^-\s*\[[ x]\]/i.test(line) || /^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(line))
|
|
68
|
+
.map((line) => line.replace(/^-\s*\[[ x]\]\s*/i, '').trim());
|
|
69
|
+
}
|
|
70
|
+
function buildCriterion(text, source, index) {
|
|
71
|
+
const trimmed = text.trim();
|
|
72
|
+
if (trimmed.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const id = detectCriterionId(trimmed, index);
|
|
76
|
+
const withoutId = stripCriterionIdPrefix(trimmed);
|
|
77
|
+
const aliases = new Set([
|
|
78
|
+
normalizeCriterionValue(trimmed),
|
|
79
|
+
normalizeCriterionValue(withoutId),
|
|
80
|
+
normalizeCriterionValue(id),
|
|
81
|
+
]);
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
text: withoutId,
|
|
85
|
+
source,
|
|
86
|
+
aliases: [...aliases].filter((alias) => alias.length > 0),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function extractNormalizedAcceptanceCriteria(raw) {
|
|
90
|
+
const bodyCriteria = extractBodyCriteria(raw)
|
|
91
|
+
.map((criterion, index) => buildCriterion(criterion, 'body', index))
|
|
92
|
+
.filter((criterion) => criterion !== null);
|
|
93
|
+
if (bodyCriteria.length > 0) {
|
|
94
|
+
return bodyCriteria;
|
|
95
|
+
}
|
|
96
|
+
return parseFrontmatterScenarios(raw)
|
|
97
|
+
.map((scenario, index) => buildCriterion(scenario.title, 'frontmatter', index))
|
|
98
|
+
.filter((criterion) => criterion !== null);
|
|
99
|
+
}
|
|
100
|
+
export function extractAcceptanceCriteriaTexts(raw) {
|
|
101
|
+
return extractNormalizedAcceptanceCriteria(raw).map((criterion) => criterion.text);
|
|
102
|
+
}
|
|
103
|
+
export function normalizeAcceptanceCriterionToken(value) {
|
|
104
|
+
return normalizeCriterionValue(stripCriterionIdPrefix(value));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=acceptance-criteria.js.map
|
|
@@ -133,6 +133,13 @@ async function walkSpecDirectory(projectPath, specDir, result) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
async function shouldDeleteOrphanSpecDir(specDir) {
|
|
137
|
+
const entries = await readdir(specDir).catch(() => []);
|
|
138
|
+
if (entries.includes('spec.md')) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return !entries.some((entry) => mustMergeBeforeDeleteSpecFile(entry));
|
|
142
|
+
}
|
|
136
143
|
export async function runStrictPlanuCleanup(projectPath) {
|
|
137
144
|
const result = { deleted: [], merged: [], gitignoreUpdated: false };
|
|
138
145
|
const planuDir = join(projectPath, 'planu');
|
|
@@ -170,6 +177,11 @@ export async function runStrictPlanuCleanup(projectPath) {
|
|
|
170
177
|
result.deleted.push(relative(projectPath, full));
|
|
171
178
|
continue;
|
|
172
179
|
}
|
|
180
|
+
if (await shouldDeleteOrphanSpecDir(full)) {
|
|
181
|
+
await removePath(projectPath, full);
|
|
182
|
+
result.deleted.push(relative(projectPath, full));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
173
185
|
await walkSpecDirectory(projectPath, full, result);
|
|
174
186
|
}
|
|
175
187
|
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
@@ -210,6 +222,10 @@ export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
|
210
222
|
offenders.push(relative(projectPath, full));
|
|
211
223
|
continue;
|
|
212
224
|
}
|
|
225
|
+
if (await shouldDeleteOrphanSpecDir(full)) {
|
|
226
|
+
offenders.push(relative(projectPath, full));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
213
229
|
for (const entry of await readdir(full).catch(() => [])) {
|
|
214
230
|
const entryPath = join(full, entry);
|
|
215
231
|
const isDir = await pathIsDirectory(entryPath);
|
|
@@ -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
|
*/
|
|
@@ -28,6 +28,16 @@ export class OptimizationReporter {
|
|
|
28
28
|
record.tokensSaved += tokensSaved;
|
|
29
29
|
this.totalSaved += tokensSaved;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Record measured savings from reversible compaction.
|
|
33
|
+
*/
|
|
34
|
+
recordCompaction(toolName, tokensSaved, retrievals = 0) {
|
|
35
|
+
const record = this.getOrCreateRecord(toolName);
|
|
36
|
+
record.compactionTokensSaved = (record.compactionTokensSaved ?? 0) + tokensSaved;
|
|
37
|
+
record.artifactRetrievals = (record.artifactRetrievals ?? 0) + retrievals;
|
|
38
|
+
record.tokensSaved += tokensSaved;
|
|
39
|
+
this.totalSaved += tokensSaved;
|
|
40
|
+
}
|
|
31
41
|
/**
|
|
32
42
|
* Record a cache miss for a tool.
|
|
33
43
|
*/
|
|
@@ -106,7 +116,8 @@ export class OptimizationReporter {
|
|
|
106
116
|
for (const record of toolRecords) {
|
|
107
117
|
lines.push(`- ${record.toolName}: ${String(record.totalTokens)} tokens, ` +
|
|
108
118
|
`${String(record.callCount)} calls, ` +
|
|
109
|
-
`${String(record.tokensSaved)} saved`
|
|
119
|
+
`${String(record.tokensSaved)} saved ` +
|
|
120
|
+
`(compaction ${String(record.compactionTokensSaved ?? 0)}, retrievals ${String(record.artifactRetrievals ?? 0)})`);
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
return lines.join('\n');
|
|
@@ -134,6 +145,8 @@ export class OptimizationReporter {
|
|
|
134
145
|
cacheHits: 0,
|
|
135
146
|
cacheMisses: 0,
|
|
136
147
|
tokensSaved: 0,
|
|
148
|
+
compactionTokensSaved: 0,
|
|
149
|
+
artifactRetrievals: 0,
|
|
137
150
|
};
|
|
138
151
|
this.toolRecords.set(toolName, record);
|
|
139
152
|
}
|
|
@@ -6,23 +6,26 @@ Auto-generated by \`init_project\`. Do not edit manually.
|
|
|
6
6
|
|
|
7
7
|
## Rule
|
|
8
8
|
|
|
9
|
-
When implementation changes are complete and validated, always address release explicitly:
|
|
9
|
+
When implementation changes are complete and validated, always address release explicitly with a local-first flow:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
11
|
+
- canonical local gates: \`pnpm validate\` then \`pnpm check:strict\`
|
|
12
|
+
- canonical local release command: \`pnpm release:local\`
|
|
13
|
+
- publish externally only when the user has explicitly requested shipping/publishing
|
|
14
|
+
- use \`main\` as the default release branch; treat \`develop\` as optional gitflow compatibility only when the repository actively uses it
|
|
13
15
|
|
|
14
16
|
## Required Release Checklist
|
|
15
17
|
|
|
16
|
-
-
|
|
18
|
+
- local validation commands passed
|
|
17
19
|
- changelog or release notes updated when applicable
|
|
18
20
|
- package version and git tag stay aligned
|
|
19
|
-
-
|
|
21
|
+
- release metadata under \`planu/releases/pending.json\` contains only genuinely pending entries
|
|
20
22
|
- published package smoke check is run after release when applicable
|
|
21
23
|
|
|
22
24
|
## Hard Blocks
|
|
23
25
|
|
|
24
26
|
- Do not silently skip release discussion after completed product/runtime changes.
|
|
25
27
|
- Do not publish without validation evidence.
|
|
28
|
+
- Do not let hosted CI, branch protection, or release bots redefine the canonical local release contract.
|
|
26
29
|
- Do not leave release notes promising capabilities that are not in the public runtime.
|
|
27
30
|
`;
|
|
28
31
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Planu — Web Fetcher: consultDocs orchestration
|
|
2
2
|
import { searchAndRegisterFramework, saveRegistry } from '../registry-updater.js';
|
|
3
3
|
import { loadDocsRegistry, findDocsEntry, setDocsRegistryCache, getStoredRegistryPath, } from './registry-loader.js';
|
|
4
|
+
import { DEFAULT_CONFIG } from './cache.js';
|
|
4
5
|
import { fetchUrl } from './http-client.js';
|
|
5
6
|
import { extractTextContent, extractCodeBlocks, extractBestPractices, buildSummaryFromContent, buildPlaceholderSummary, buildPlaceholderExamples, buildPlaceholderBestPractices, } from './content-extractor.js';
|
|
6
7
|
export async function consultDocsImpl(topic, framework) {
|
|
@@ -25,7 +26,10 @@ export async function consultDocsImpl(topic, framework) {
|
|
|
25
26
|
: `https://www.google.com/search?q=${encodeURIComponent(`${resolvedFramework} ${topic} documentation`)}`;
|
|
26
27
|
const baseUrl = docsEntry?.base ?? '';
|
|
27
28
|
if (baseUrl) {
|
|
28
|
-
const fetchResult = await fetchUrl(docsUrl
|
|
29
|
+
const fetchResult = await fetchUrl(docsUrl, {
|
|
30
|
+
...DEFAULT_CONFIG,
|
|
31
|
+
timeoutMs: 2_500,
|
|
32
|
+
});
|
|
29
33
|
if (fetchResult) {
|
|
30
34
|
const textContent = extractTextContent(fetchResult.content);
|
|
31
35
|
const codeBlocks = extractCodeBlocks(fetchResult.content);
|
|
@@ -47,7 +47,7 @@ export declare const LIGHTWEIGHT_COMMANDS: readonly [{
|
|
|
47
47
|
}, {
|
|
48
48
|
readonly id: "planu.release.check";
|
|
49
49
|
readonly title: "Check release readiness";
|
|
50
|
-
readonly description: "Check local branch cleanliness and
|
|
50
|
+
readonly description: "Check local-first release readiness, branch cleanliness, and optional gitflow drift.";
|
|
51
51
|
readonly invocation: "planu release check";
|
|
52
52
|
readonly hosts: readonly ["codex", "claude-code"];
|
|
53
53
|
readonly requiresMcp: false;
|
|
@@ -54,7 +54,7 @@ export const LIGHTWEIGHT_COMMANDS = [
|
|
|
54
54
|
{
|
|
55
55
|
id: 'planu.release.check',
|
|
56
56
|
title: 'Check release readiness',
|
|
57
|
-
description: 'Check local branch cleanliness and
|
|
57
|
+
description: 'Check local-first release readiness, branch cleanliness, and optional gitflow drift.',
|
|
58
58
|
invocation: 'planu release check',
|
|
59
59
|
hosts: ['codex', 'claude-code'],
|
|
60
60
|
requiresMcp: false,
|
|
@@ -93,8 +93,9 @@ function getSddProcess() {
|
|
|
93
93
|
id: 'documenting',
|
|
94
94
|
name: '8. Document & Ship',
|
|
95
95
|
tools: ['generate_docs', 'generate_docs_site', 'integrate_pm', 'learn_pattern'],
|
|
96
|
-
description: 'Generate documentation
|
|
97
|
-
'
|
|
96
|
+
description: 'Generate documentation, run the local-first release contract ' +
|
|
97
|
+
'(`pnpm validate`, `pnpm check:strict`, `pnpm release:local` when shipping), ' +
|
|
98
|
+
'sync with project management tools, and learn patterns for future estimations.',
|
|
98
99
|
},
|
|
99
100
|
],
|
|
100
101
|
specLifecycle: ['draft', 'review', 'approved', 'implementing', 'done'],
|
|
@@ -6,12 +6,8 @@ import { detectBrokers, getBrokerConfig, requiresIdempotence, } from '../../engi
|
|
|
6
6
|
* Returns true when the spec or project has event-driven characteristics.
|
|
7
7
|
*/
|
|
8
8
|
export function isEventDrivenSpec(spec, specContent, knowledge) {
|
|
9
|
-
const brokers = detectBrokers(knowledge.stack);
|
|
10
|
-
if (brokers.length > 0 && brokers[0] !== 'unknown') {
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
9
|
const lower = `${spec.title} ${spec.tags.join(' ')} ${specContent}`.toLowerCase();
|
|
14
|
-
|
|
10
|
+
const specSignals = lower.includes('event') ||
|
|
15
11
|
lower.includes('kafka') ||
|
|
16
12
|
lower.includes('rabbitmq') ||
|
|
17
13
|
lower.includes('pubsub') ||
|
|
@@ -21,7 +17,15 @@ export function isEventDrivenSpec(spec, specContent, knowledge) {
|
|
|
21
17
|
lower.includes('sns') ||
|
|
22
18
|
lower.includes('message') ||
|
|
23
19
|
lower.includes('consumer') ||
|
|
24
|
-
lower.includes('producer')
|
|
20
|
+
lower.includes('producer');
|
|
21
|
+
if (spec.target === 'frontend' && !specSignals) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const brokers = detectBrokers(knowledge.stack);
|
|
25
|
+
if (specSignals) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return brokers.length > 0 && brokers[0] !== 'unknown' && spec.target !== 'frontend';
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* Generate event-driven failure scenarios for challenge_spec.
|
|
@@ -265,7 +265,7 @@ export async function handleSetupHooks(projectId, config) {
|
|
|
265
265
|
/* v8 ignore start */
|
|
266
266
|
const protectedBranches = mergedConfig.protectedBranches ?? DEFAULT_PROTECTED_BRANCHES;
|
|
267
267
|
const stalenessThreshold = mergedConfig.stalenessThreshold ?? 50;
|
|
268
|
-
const baseBranch = mergedConfig.baseBranch ?? '
|
|
268
|
+
const baseBranch = mergedConfig.baseBranch ?? 'main';
|
|
269
269
|
/* v8 ignore end */
|
|
270
270
|
const preCommitScript = buildPreCommitScript(
|
|
271
271
|
/* v8 ignore next */ mergedConfig.requireSpecInCommit ?? true, stalenessThreshold, baseBranch);
|
|
@@ -63,6 +63,7 @@ Planu enforces Spec Driven Development — spec first, then implement, then vali
|
|
|
63
63
|
|
|
64
64
|
- NEVER write production code before spec is \`approved\`
|
|
65
65
|
- NEVER skip \`validate\` after implementation
|
|
66
|
+
- Specs stay \`spec.md\`-only. Do not recreate standalone \`technical.md\` or \`progress.md\`.
|
|
66
67
|
`;
|
|
67
68
|
const CODEX_NATIVE_SKILL_CONTENT = `# planu-native — Planu Lightweight Native Surface
|
|
68
69
|
|
|
@@ -79,7 +80,7 @@ Use Planu's lightweight CLI surface for common SDD checks without loading the fu
|
|
|
79
80
|
- \`planu spec list\` — list specs
|
|
80
81
|
- \`planu spec validate SPEC-001\` — validate one spec
|
|
81
82
|
- \`planu audit debt\` — read-only technical debt audit
|
|
82
|
-
- \`planu release check\` — local release readiness check
|
|
83
|
+
- \`planu release check\` — local-first release readiness check
|
|
83
84
|
|
|
84
85
|
## Escalate to Full MCP
|
|
85
86
|
|