@planu/cli 4.3.4 → 4.3.6

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 (55) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/config/license-plans.json +2 -0
  3. package/dist/engine/ai-cost-estimator/core.d.ts +1 -1
  4. package/dist/engine/ai-cost-estimator/core.js +2 -2
  5. package/dist/engine/ai-cost-estimator/spec-loader.d.ts +2 -2
  6. package/dist/engine/ai-cost-estimator/spec-loader.js +17 -46
  7. package/dist/engine/ai-cost-estimator/token-estimator.d.ts +1 -1
  8. package/dist/engine/ai-cost-estimator/token-estimator.js +1 -1
  9. package/dist/engine/code-graph-configurator.d.ts +1 -1
  10. package/dist/engine/code-graph-configurator.js +7 -0
  11. package/dist/engine/context-intelligence/compression-guards.d.ts +8 -0
  12. package/dist/engine/context-intelligence/compression-guards.js +74 -0
  13. package/dist/engine/context-intelligence/context-graph-provider.d.ts +9 -0
  14. package/dist/engine/context-intelligence/context-graph-provider.js +98 -0
  15. package/dist/engine/context-intelligence/eval-harness.d.ts +8 -0
  16. package/dist/engine/context-intelligence/eval-harness.js +45 -0
  17. package/dist/engine/context-intelligence/impact-map.d.ts +6 -0
  18. package/dist/engine/context-intelligence/impact-map.js +47 -0
  19. package/dist/engine/context-intelligence/index.d.ts +7 -0
  20. package/dist/engine/context-intelligence/index.js +6 -0
  21. package/dist/engine/context-intelligence/safe-context-compressor.d.ts +3 -0
  22. package/dist/engine/context-intelligence/safe-context-compressor.js +75 -0
  23. package/dist/engine/dashboard/data-loader.js +9 -11
  24. package/dist/engine/dashboard/templates-project.d.ts +1 -1
  25. package/dist/engine/dashboard/templates-project.js +6 -4
  26. package/dist/engine/docs-site-generator/index.js +2 -11
  27. package/dist/engine/drift-monitor.js +13 -11
  28. package/dist/engine/qa-gate.js +6 -1
  29. package/dist/engine/readiness-checker.js +3 -3
  30. package/dist/engine/spec-conflict-graph.d.ts +1 -1
  31. package/dist/engine/spec-conflict-graph.js +2 -3
  32. package/dist/engine/spec-format/read-technical-section.d.ts +1 -7
  33. package/dist/engine/spec-format/read-technical-section.js +4 -30
  34. package/dist/engine/spec-registry/scorer.d.ts +1 -1
  35. package/dist/engine/spec-registry/scorer.js +3 -4
  36. package/dist/engine/validator/extractors.js +4 -2
  37. package/dist/engine/validator.d.ts +1 -1
  38. package/dist/engine/validator.js +40 -2
  39. package/dist/tools/code-graph-handler.js +4 -0
  40. package/dist/tools/create-spec/post-creation.d.ts +2 -0
  41. package/dist/tools/create-spec/post-creation.js +5 -0
  42. package/dist/tools/create-spec.js +2 -2
  43. package/dist/tools/license-gate.js +6 -1
  44. package/dist/tools/output-integrity-guard.d.ts +11 -0
  45. package/dist/tools/output-integrity-guard.js +53 -0
  46. package/dist/tools/register-sdd-tools.d.ts +1 -1
  47. package/dist/tools/register-sdd-tools.js +2 -0
  48. package/dist/tools/safe-handler.js +11 -9
  49. package/dist/tools/tool-registry/group-infra.js +26 -0
  50. package/dist/tools/update-status/dod-gates.js +1 -1
  51. package/dist/types/code-graph-integration.d.ts +2 -2
  52. package/dist/types/context-intelligence.d.ts +61 -0
  53. package/dist/types/context-intelligence.js +2 -0
  54. package/dist/types/qa-gate.d.ts +1 -1
  55. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [4.3.5] - 2026-05-24
2
+
3
+ ### Features
4
+ - feat(codegraph): expose Colby CodeGraph setup and status tools on the official MCP surface
5
+
6
+ ### Bug Fixes
7
+ - fix(create-spec): render structured post-creation suggestions as readable next steps
8
+ - fix(mcp): guard malformed human-facing tool output before it reaches clients
9
+
10
+
1
11
  ## [4.3.4] - 2026-05-22
2
12
 
3
13
  **Tarball SHA-256:** `e1ac042fd623c1421d7a0788fed14c369472489180ffe80eb8bce4d422d360d8`
@@ -3938,4 +3948,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
3938
3948
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
3939
3949
  - Multi-language i18n (EN/ES/PT) for generated specs
3940
3950
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
3941
- - 10,857 tests with ≥95% coverage
3951
+ - 10,857 tests with ≥95% coverage
@@ -42,12 +42,14 @@
42
42
  "check_update_status",
43
43
  "check_versions",
44
44
  "clarify_requirements",
45
+ "code_graph_status",
45
46
  "compliance_coverage_report",
46
47
  "compliance_gap_analysis",
47
48
  "compliance_gate_status",
48
49
  "compliance_score_report",
49
50
  "config_health",
50
51
  "configure_approval_policy",
52
+ "configure_code_graph",
51
53
  "configure_compliance",
52
54
  "configure_deploy_target",
53
55
  "configure_memory",
@@ -1,7 +1,7 @@
1
1
  import type { AiCostEstimateResult, EstimateAiCostInput } from '../../types/index.js';
2
2
  /**
3
3
  * Estimates the AI cost for implementing a spec.
4
- * Reads the spec from planu/specs/ (or legacy docs/sdd/specs/), loads pricing from config JSON,
4
+ * Reads the canonical spec.md from planu/specs/, loads pricing from config JSON,
5
5
  * and returns a full AiCostEstimateResult with per-model breakdown.
6
6
  */
7
7
  export declare function estimateAiCost(input: EstimateAiCostInput): Promise<AiCostEstimateResult>;
@@ -34,13 +34,13 @@ function getComplexityLevel(acCount) {
34
34
  // ---------------------------------------------------------------------------
35
35
  /**
36
36
  * Estimates the AI cost for implementing a spec.
37
- * Reads the spec from planu/specs/ (or legacy docs/sdd/specs/), loads pricing from config JSON,
37
+ * Reads the canonical spec.md from planu/specs/, loads pricing from config JSON,
38
38
  * and returns a full AiCostEstimateResult with per-model breakdown.
39
39
  */
40
40
  export async function estimateAiCost(input) {
41
41
  const { specId, projectPath } = input;
42
42
  // 1. Load spec content
43
- const { content: specContent } = await loadSpecContent(specId);
43
+ const { content: specContent } = await loadSpecContent(specId, projectPath);
44
44
  const acCount = countAcceptanceCriteria(specContent);
45
45
  // 2. Load pricing config
46
46
  const config = await loadPricingConfig();
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Reads spec content from a sdd/specs directory.
2
+ * Reads canonical spec content from planu/specs.
3
3
  * Tries to find the spec directory by matching specId prefix.
4
4
  */
5
- export declare function loadSpecContent(specId: string): Promise<{
5
+ export declare function loadSpecContent(specId: string, projectPath?: string): Promise<{
6
6
  content: string;
7
7
  specDir: string;
8
8
  }>;
@@ -1,66 +1,37 @@
1
1
  // @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
2
2
  // engine/ai-cost-estimator/spec-loader.ts — Loads spec content and counts acceptance criteria.
3
3
  // Extracted from core.ts to keep file sizes within limits.
4
- import { readFile } from 'node:fs/promises';
4
+ import { readFile, readdir } from 'node:fs/promises';
5
5
  import { join } from 'node:path';
6
6
  import { stripFrontmatter } from '../frontmatter-parser.js';
7
7
  /**
8
- * Reads spec content from a sdd/specs directory.
8
+ * Reads canonical spec content from planu/specs.
9
9
  * Tries to find the spec directory by matching specId prefix.
10
10
  */
11
- export async function loadSpecContent(specId) {
12
- // Try modern location first, fallback to legacy
13
- let specsRoot = 'planu/specs';
11
+ export async function loadSpecContent(specId, projectPath = process.cwd()) {
12
+ const specsRoot = join(projectPath, 'planu', 'specs');
13
+ let topEntries;
14
14
  try {
15
- const { access: fsAccess } = await import('node:fs/promises');
16
- await fsAccess(specsRoot);
17
- }
18
- catch {
19
- specsRoot = 'docs/sdd/specs';
20
- }
21
- const { readdir } = await import('node:fs/promises');
22
- let specDir = null;
23
- try {
24
- const topEntries = await readdir(specsRoot);
25
- // Flat layout: planu/specs/SPEC-069-name/ (or legacy docs/sdd/specs/SPEC-069-name/)
26
- const flatMatch = topEntries.find((e) => e.startsWith(specId));
27
- if (flatMatch) {
28
- specDir = join(specsRoot, flatMatch);
29
- }
15
+ topEntries = await readdir(specsRoot);
30
16
  }
31
17
  catch {
32
18
  throw new Error(`Spec directory not found at "${specsRoot}". ` +
33
19
  'Make sure you are running from the project root.');
34
20
  }
35
- if (!specDir) {
36
- throw new Error(`Spec "${specId}" not found in "${specsRoot}". ` + 'Check that the spec directory exists.');
21
+ const flatMatch = topEntries.find((entry) => entry.startsWith(specId));
22
+ if (!flatMatch) {
23
+ throw new Error(`Spec "${specId}" not found in "${specsRoot}". Check that the spec directory exists.`);
37
24
  }
38
- const parts = [];
39
- // Try modern names first, fallback to legacy — only read one of each pair
40
- for (const [modern, legacy] of [
41
- ['spec.md', 'HU.md'],
42
- ['technical.md', 'FICHA-TECNICA.md'],
43
- ]) {
44
- let found = false;
45
- for (const filename of [modern, legacy]) {
46
- if (found) {
47
- break;
48
- }
49
- try {
50
- const raw = await readFile(join(specDir, filename), 'utf-8');
51
- parts.push(stripFrontmatter(raw));
52
- found = true;
53
- }
54
- catch {
55
- // File not present — try next
56
- }
57
- }
25
+ const specDir = join(specsRoot, flatMatch);
26
+ const specPath = join(specDir, 'spec.md');
27
+ try {
28
+ const raw = await readFile(specPath, 'utf-8');
29
+ return { content: stripFrontmatter(raw), specDir };
58
30
  }
59
- if (parts.length === 0) {
60
- throw new Error(`Spec "${specId}" has no spec files in "${specDir}". ` +
61
- 'Cannot estimate without spec content.');
31
+ catch {
32
+ throw new Error(`Spec "${specId}" has no canonical spec.md at "${specPath}". ` +
33
+ 'Run Planu migration/strict cleanup before estimating cost.');
62
34
  }
63
- return { content: parts.join('\n\n'), specDir };
64
35
  }
65
36
  /** Counts acceptance criteria (lines starting with "- [ ]" or "- [x]") in spec content. */
66
37
  export function countAcceptanceCriteria(specContent) {
@@ -6,7 +6,7 @@ import type { TokenEstimate, AiModelPricingConfig } from '../../types/index.js';
6
6
  export declare function detectProjectLanguage(projectPath: string): Promise<string>;
7
7
  /**
8
8
  * Estimates input tokens for an AI agent session based on three components:
9
- * 1. Spec content (HU.md + FICHA-TECNICA.md)
9
+ * 1. Spec content (canonical spec.md)
10
10
  * 2. Codebase context (if projectPath is provided)
11
11
  * 3. MCP tool definitions overhead (fixed constant)
12
12
  */
@@ -119,7 +119,7 @@ function isSourceFile(filename) {
119
119
  // ---------------------------------------------------------------------------
120
120
  /**
121
121
  * Estimates input tokens for an AI agent session based on three components:
122
- * 1. Spec content (HU.md + FICHA-TECNICA.md)
122
+ * 1. Spec content (canonical spec.md)
123
123
  * 2. Codebase context (if projectPath is provided)
124
124
  * 3. MCP tool definitions overhead (fixed constant)
125
125
  */
@@ -19,5 +19,5 @@ export declare function buildStatusResult(config: ClaudeJsonConfig): CodeGraphSt
19
19
  /**
20
20
  * Return the key used in .claude.json for a given action.
21
21
  */
22
- export declare function actionToProviderKey(action: 'setup-codegraph-context' | 'setup-pathfinder' | 'setup-axon'): string;
22
+ export declare function actionToProviderKey(action: 'setup-colby-codegraph' | 'setup-codegraph-context' | 'setup-pathfinder' | 'setup-axon'): string;
23
23
  //# sourceMappingURL=code-graph-configurator.d.ts.map
@@ -2,6 +2,12 @@
2
2
  // Provider definitions
3
3
  // ---------------------------------------------------------------------------
4
4
  const PROVIDER_DEFS = [
5
+ {
6
+ key: 'codegraph',
7
+ provider: 'colby-codegraph',
8
+ command: 'npx',
9
+ args: ['-y', '@colbymchenry/codegraph', 'serve', '--mcp'],
10
+ },
5
11
  {
6
12
  key: 'codegraphcontext',
7
13
  provider: 'codegraph-context',
@@ -71,6 +77,7 @@ export function buildStatusResult(config) {
71
77
  */
72
78
  export function actionToProviderKey(action) {
73
79
  const map = {
80
+ 'setup-colby-codegraph': 'codegraph',
74
81
  'setup-codegraph-context': 'codegraphcontext',
75
82
  'setup-pathfinder': 'code-pathfinder',
76
83
  'setup-axon': 'axon',
@@ -0,0 +1,8 @@
1
+ export declare function getSensitivePathRefusal(path: string): string | null;
2
+ export declare function collectPreservedFragments(content: string): Map<string, string[]>;
3
+ export declare function verifyPreservedFragments(original: string, candidate: string): {
4
+ ok: boolean;
5
+ checks: string[];
6
+ };
7
+ export declare function shouldBypassLeanMode(flow: string): boolean;
8
+ //# sourceMappingURL=compression-guards.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { basename } from 'node:path';
2
+ const SENSITIVE_PATH_SEGMENTS = new Set([
3
+ '.ssh',
4
+ '.aws',
5
+ '.gnupg',
6
+ '.kube',
7
+ '.docker',
8
+ 'secrets',
9
+ 'credentials',
10
+ ]);
11
+ const SENSITIVE_FILE_PATTERNS = [
12
+ /^\.env(?:\.|$)/i,
13
+ /^\.npmrc$/i,
14
+ /(?:secret|credential|password|token|private[-_]?key|api[-_]?key)/i,
15
+ /\.(?:p12|pfx|pem|key)$/i,
16
+ ];
17
+ const PRESERVE_PATTERNS = [
18
+ { label: 'spec-id', pattern: /\bSPEC-\d+\b/g },
19
+ { label: 'acceptance-criteria', pattern: /^-\s*\[[ xX]\]\s+.+$/gm },
20
+ { label: 'fenced-code', pattern: /```[\s\S]*?```|~~~[\s\S]*?~~~/g },
21
+ { label: 'inline-code', pattern: /`[^`\n]+`/g },
22
+ { label: 'url', pattern: /\bhttps?:\/\/[^\s)]+/g },
23
+ { label: 'file-path', pattern: /\b[\w.-]+(?:\/[\w.-]+)+\.[A-Za-z0-9]+\b/g },
24
+ { label: 'command', pattern: /\b(?:pnpm|npm|bun|node|git|gh|npx|tsx|tsc)\s+[^\n]+/g },
25
+ { label: 'hash', pattern: /\b[a-f0-9]{32,64}\b/gi },
26
+ {
27
+ label: 'validation-failure',
28
+ pattern: /\b(?:error|failed|failure|blocked|validate|validation)\b[^\n]*/gi,
29
+ },
30
+ { label: 'approval-request', pattern: /\b(?:approve|approval|approved|forceApprove)\b[^\n]*/gi },
31
+ {
32
+ label: 'security-warning',
33
+ pattern: /\b(?:security|secret|credential|token|password|destructive)\b[^\n]*/gi,
34
+ },
35
+ ];
36
+ export function getSensitivePathRefusal(path) {
37
+ const normalized = path.replace(/\\/g, '/');
38
+ const segments = normalized.split('/').filter(Boolean);
39
+ if (segments.some((segment) => SENSITIVE_PATH_SEGMENTS.has(segment.toLowerCase()))) {
40
+ return `Refusing sensitive path segment in ${path}`;
41
+ }
42
+ const fileName = basename(normalized);
43
+ if (SENSITIVE_FILE_PATTERNS.some((pattern) => pattern.test(fileName))) {
44
+ return `Refusing sensitive file name ${path}`;
45
+ }
46
+ return null;
47
+ }
48
+ export function collectPreservedFragments(content) {
49
+ const fragments = new Map();
50
+ for (const { label, pattern } of PRESERVE_PATTERNS) {
51
+ const matches = [...content.matchAll(pattern)].map((match) => match[0]);
52
+ if (matches.length > 0) {
53
+ fragments.set(label, [...new Set(matches)]);
54
+ }
55
+ }
56
+ return fragments;
57
+ }
58
+ export function verifyPreservedFragments(original, candidate) {
59
+ const fragments = collectPreservedFragments(original);
60
+ const checks = [];
61
+ for (const [label, values] of fragments.entries()) {
62
+ for (const value of values) {
63
+ if (!candidate.includes(value)) {
64
+ return { ok: false, checks: [`missing:${label}:${value.slice(0, 80)}`] };
65
+ }
66
+ }
67
+ checks.push(label);
68
+ }
69
+ return { ok: true, checks };
70
+ }
71
+ export function shouldBypassLeanMode(flow) {
72
+ return /\b(security|validation|validate|approval|approved|release|destructive|delete|rollback|force)\b/i.test(flow);
73
+ }
74
+ //# sourceMappingURL=compression-guards.js.map
@@ -0,0 +1,9 @@
1
+ import type { ContextGraphInput, ContextGraphProvider, ContextImpactMap } from '../../types/context-intelligence.js';
2
+ export declare class PlanuContextGraphProvider implements ContextGraphProvider {
3
+ buildImpactMap(input: ContextGraphInput): Promise<ContextImpactMap>;
4
+ private resolveCandidateFiles;
5
+ private readSpecReferencedPaths;
6
+ private mapFile;
7
+ }
8
+ export declare function createContextGraphProvider(): ContextGraphProvider;
9
+ //# sourceMappingURL=context-graph-provider.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ import { glob } from 'glob';
4
+ import { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
5
+ const SOURCE_EXTENSIONS = '{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,rb,cs}';
6
+ function normalizePath(path) {
7
+ return path.replace(/\\/g, '/');
8
+ }
9
+ function extractPaths(text) {
10
+ const matches = text.match(/\b(?:src|tests|packages|lib)\/[\w./-]+\.[A-Za-z0-9]+\b/g) ?? [];
11
+ return [...new Set(matches.map(normalizePath))];
12
+ }
13
+ function extractImports(content) {
14
+ const imports = new Set();
15
+ const patterns = [
16
+ /from\s+['"]([^'"]+)['"]/g,
17
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
18
+ /require\(\s*['"]([^'"]+)['"]\s*\)/g,
19
+ ];
20
+ for (const pattern of patterns) {
21
+ for (const match of content.matchAll(pattern)) {
22
+ if (match[1]?.startsWith('.')) {
23
+ imports.add(match[1]);
24
+ }
25
+ }
26
+ }
27
+ return [...imports];
28
+ }
29
+ function extractSymbols(content, path) {
30
+ const symbols = [];
31
+ const lines = content.split('\n');
32
+ const pattern = /\b(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const)\s+([A-Za-z_][\w]*)/;
33
+ for (let index = 0; index < lines.length; index += 1) {
34
+ const match = pattern.exec(lines[index] ?? '');
35
+ if (match?.[1]) {
36
+ symbols.push({ name: match[1], path, line: index + 1, confidence: 0.75 });
37
+ }
38
+ }
39
+ return symbols;
40
+ }
41
+ export class PlanuContextGraphProvider {
42
+ async buildImpactMap(input) {
43
+ const candidates = await this.resolveCandidateFiles(input);
44
+ if (candidates.length === 0) {
45
+ return emptyImpactMap();
46
+ }
47
+ const maps = await Promise.all(candidates.map((path) => this.mapFile(input.projectPath, path)));
48
+ return mergeImpactMaps(maps, input.tokenBudget);
49
+ }
50
+ async resolveCandidateFiles(input) {
51
+ const fromChanged = (input.changedFiles ?? []).map(normalizePath);
52
+ const fromSpec = input.specId ? await this.readSpecReferencedPaths(input) : [];
53
+ const direct = [...new Set([...fromChanged, ...fromSpec])].filter(Boolean);
54
+ if (direct.length > 0) {
55
+ return direct.slice(0, estimateImpactBudget(input.tokenBudget) * 2);
56
+ }
57
+ const maxFiles = estimateImpactBudget(input.tokenBudget);
58
+ return (await glob(`**/*.${SOURCE_EXTENSIONS}`, {
59
+ cwd: input.projectPath,
60
+ nodir: true,
61
+ ignore: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/coverage/**'],
62
+ }))
63
+ .map(normalizePath)
64
+ .slice(0, maxFiles);
65
+ }
66
+ async readSpecReferencedPaths(input) {
67
+ const specFiles = await glob(`planu/specs/${input.specId}-*/spec.md`, {
68
+ cwd: input.projectPath,
69
+ nodir: true,
70
+ });
71
+ const specPath = specFiles[0];
72
+ if (!specPath) {
73
+ return [];
74
+ }
75
+ const content = await readFile(join(input.projectPath, specPath), 'utf-8').catch(() => '');
76
+ return extractPaths(content);
77
+ }
78
+ async mapFile(projectPath, path) {
79
+ const normalized = normalizePath(path);
80
+ const absolute = join(projectPath, normalized);
81
+ const content = await readFile(absolute, 'utf-8').catch(() => '');
82
+ const imports = extractImports(content);
83
+ return {
84
+ files: [{ path: normalized, reason: 'direct-context-candidate', confidence: 0.9 }],
85
+ symbols: extractSymbols(content, normalized),
86
+ edges: imports.map((target) => ({
87
+ from: normalized,
88
+ to: normalizePath(relative(projectPath, join(absolute, '..', target))),
89
+ type: 'import',
90
+ })),
91
+ truncated: false,
92
+ };
93
+ }
94
+ }
95
+ export function createContextGraphProvider() {
96
+ return new PlanuContextGraphProvider();
97
+ }
98
+ //# sourceMappingURL=context-graph-provider.js.map
@@ -0,0 +1,8 @@
1
+ import type { ContextEvalCase, ContextEvalMode, ContextEvalResult } from '../../types/context-intelligence.js';
2
+ export declare function evaluateContextModes(cases: ContextEvalCase[]): ContextEvalResult[];
3
+ export declare function summarizeContextEval(results: ContextEvalResult[]): {
4
+ cases: number;
5
+ averageSavingsVsBaselinePct: number;
6
+ bestModeCounts: Record<ContextEvalMode, number>;
7
+ };
8
+ //# sourceMappingURL=eval-harness.d.ts.map
@@ -0,0 +1,45 @@
1
+ function pctSaved(baseline, candidate) {
2
+ if (baseline <= 0) {
3
+ return 0;
4
+ }
5
+ return Math.max(0, Math.round(((baseline - candidate) / baseline) * 1000) / 10);
6
+ }
7
+ export function evaluateContextModes(cases) {
8
+ return cases.map((item) => {
9
+ const modes = {
10
+ baseline: item.baselineTokens,
11
+ 'spec-guided': item.specGuidedTokens,
12
+ 'graph-guided': item.graphGuidedTokens,
13
+ lean: item.leanTokens,
14
+ 'graph+lean': item.graphLeanTokens,
15
+ };
16
+ const bestMode = Object.entries(modes).sort((a, b) => a[1] - b[1])[0]?.[0] ??
17
+ 'baseline';
18
+ return {
19
+ id: item.id,
20
+ bestMode,
21
+ savingsVsBaselinePct: pctSaved(item.baselineTokens, modes[bestMode]),
22
+ modes,
23
+ };
24
+ });
25
+ }
26
+ export function summarizeContextEval(results) {
27
+ const bestModeCounts = {
28
+ baseline: 0,
29
+ 'spec-guided': 0,
30
+ 'graph-guided': 0,
31
+ lean: 0,
32
+ 'graph+lean': 0,
33
+ };
34
+ let totalSavings = 0;
35
+ for (const result of results) {
36
+ bestModeCounts[result.bestMode] += 1;
37
+ totalSavings += result.savingsVsBaselinePct;
38
+ }
39
+ return {
40
+ cases: results.length,
41
+ averageSavingsVsBaselinePct: results.length === 0 ? 0 : Math.round((totalSavings / results.length) * 10) / 10,
42
+ bestModeCounts,
43
+ };
44
+ }
45
+ //# sourceMappingURL=eval-harness.js.map
@@ -0,0 +1,6 @@
1
+ import type { ContextImpactMap } from '../../types/context-intelligence.js';
2
+ export declare function emptyImpactMap(): ContextImpactMap;
3
+ export declare function clampConfidence(value: number): number;
4
+ export declare function estimateImpactBudget(tokenBudget: number): number;
5
+ export declare function mergeImpactMaps(maps: ContextImpactMap[], tokenBudget: number): ContextImpactMap;
6
+ //# sourceMappingURL=impact-map.d.ts.map
@@ -0,0 +1,47 @@
1
+ export function emptyImpactMap() {
2
+ return { files: [], symbols: [], edges: [], truncated: false };
3
+ }
4
+ export function clampConfidence(value) {
5
+ return Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0));
6
+ }
7
+ export function estimateImpactBudget(tokenBudget) {
8
+ if (tokenBudget <= 0) {
9
+ return 1;
10
+ }
11
+ return Math.max(1, Math.min(50, Math.floor(tokenBudget / 120)));
12
+ }
13
+ export function mergeImpactMaps(maps, tokenBudget) {
14
+ const maxFiles = estimateImpactBudget(tokenBudget);
15
+ const files = new Map();
16
+ const symbols = new Map();
17
+ const edges = new Map();
18
+ let sourceCount = 0;
19
+ for (const map of maps) {
20
+ for (const file of map.files) {
21
+ sourceCount += 1;
22
+ const existing = files.get(file.path);
23
+ if (!existing || existing.confidence < file.confidence) {
24
+ files.set(file.path, { ...file, confidence: clampConfidence(file.confidence) });
25
+ }
26
+ }
27
+ for (const symbol of map.symbols) {
28
+ symbols.set(`${symbol.path}:${symbol.name}:${String(symbol.line ?? '')}`, {
29
+ ...symbol,
30
+ confidence: clampConfidence(symbol.confidence),
31
+ });
32
+ }
33
+ for (const edge of map.edges) {
34
+ edges.set(`${edge.from}->${edge.to}:${edge.type}`, edge);
35
+ }
36
+ }
37
+ const rankedFiles = [...files.values()].sort((a, b) => b.confidence - a.confidence);
38
+ const selectedFiles = rankedFiles.slice(0, maxFiles);
39
+ const selectedPaths = new Set(selectedFiles.map((file) => file.path));
40
+ return {
41
+ files: selectedFiles,
42
+ symbols: [...symbols.values()].filter((symbol) => selectedPaths.has(symbol.path)),
43
+ edges: [...edges.values()].filter((edge) => selectedPaths.has(edge.from) || selectedPaths.has(edge.to)),
44
+ truncated: rankedFiles.length < sourceCount || rankedFiles.length > selectedFiles.length,
45
+ };
46
+ }
47
+ //# sourceMappingURL=impact-map.js.map
@@ -0,0 +1,7 @@
1
+ export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
2
+ export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
3
+ export { compressSafeContext } from './safe-context-compressor.js';
4
+ export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
5
+ export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
6
+ export type { ContextEvalCase, ContextEvalMode, ContextEvalResult, ContextGraphInput, ContextGraphProvider, ContextImpactEdge, ContextImpactFile, ContextImpactMap, ContextImpactSymbol, SafeCompressionResult, SafeContextCompressionInput, } from '../../types/context-intelligence.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export { PlanuContextGraphProvider, createContextGraphProvider } from './context-graph-provider.js';
2
+ export { getSensitivePathRefusal, collectPreservedFragments, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
3
+ export { compressSafeContext } from './safe-context-compressor.js';
4
+ export { evaluateContextModes, summarizeContextEval } from './eval-harness.js';
5
+ export { emptyImpactMap, estimateImpactBudget, mergeImpactMaps } from './impact-map.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { SafeCompressionResult, SafeContextCompressionInput } from '../../types/context-intelligence.js';
2
+ export declare function compressSafeContext(input: SafeContextCompressionInput): SafeCompressionResult;
3
+ //# sourceMappingURL=safe-context-compressor.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { getSensitivePathRefusal, verifyPreservedFragments, shouldBypassLeanMode, } from './compression-guards.js';
3
+ const DEFAULT_MAX_BYTES = 200_000;
4
+ function byteLength(value) {
5
+ return Buffer.byteLength(value, 'utf-8');
6
+ }
7
+ function compressParagraph(paragraph) {
8
+ if (paragraph.startsWith('```') ||
9
+ paragraph.startsWith('~~~') ||
10
+ paragraph.startsWith('- [') ||
11
+ /`[^`\n]+`|\bSPEC-\d+\b|\bhttps?:\/\//.test(paragraph)) {
12
+ return paragraph;
13
+ }
14
+ return paragraph
15
+ .split('\n')
16
+ .map((line) => line.replace(/\s+/g, ' ').trim())
17
+ .filter(Boolean)
18
+ .join(' ');
19
+ }
20
+ export function compressSafeContext(input) {
21
+ const originalBytes = byteLength(input.content);
22
+ if (input.sourcePath) {
23
+ const refusal = getSensitivePathRefusal(input.sourcePath);
24
+ if (refusal) {
25
+ return {
26
+ ok: false,
27
+ originalBytes,
28
+ compressedBytes: originalBytes,
29
+ refusedReason: refusal,
30
+ preservedChecks: [],
31
+ };
32
+ }
33
+ }
34
+ if (originalBytes > (input.maxBytes ?? DEFAULT_MAX_BYTES)) {
35
+ return {
36
+ ok: false,
37
+ originalBytes,
38
+ compressedBytes: originalBytes,
39
+ refusedReason: `Refusing content over ${String(input.maxBytes ?? DEFAULT_MAX_BYTES)} bytes`,
40
+ preservedChecks: [],
41
+ };
42
+ }
43
+ if (input.flow && shouldBypassLeanMode(input.flow)) {
44
+ return {
45
+ ok: true,
46
+ originalBytes,
47
+ compressedBytes: originalBytes,
48
+ preservedChecks: ['bypass-sensitive-flow'],
49
+ content: input.content,
50
+ };
51
+ }
52
+ const compressed = input.content
53
+ .split(/\n{2,}/)
54
+ .map((paragraph) => compressParagraph(paragraph.trim()))
55
+ .filter(Boolean)
56
+ .join('\n\n');
57
+ const preserved = verifyPreservedFragments(input.content, compressed);
58
+ if (!preserved.ok) {
59
+ return {
60
+ ok: false,
61
+ originalBytes,
62
+ compressedBytes: byteLength(compressed),
63
+ refusedReason: `Compression would drop preserved fragment: ${preserved.checks[0] ?? 'unknown'}`,
64
+ preservedChecks: preserved.checks,
65
+ };
66
+ }
67
+ return {
68
+ ok: true,
69
+ originalBytes,
70
+ compressedBytes: byteLength(compressed),
71
+ preservedChecks: preserved.checks,
72
+ content: compressed,
73
+ };
74
+ }
75
+ //# sourceMappingURL=safe-context-compressor.js.map
@@ -3,6 +3,7 @@
3
3
  import { readFile, readdir, stat } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { computeDashboardMetrics, computeHealthScore } from './kanban-logic.js';
6
+ import { extractSectionBody } from '../spec-format/read-technical-section.js';
6
7
  /** Root data directory for all project data */
7
8
  const DATA_DIR = join(process.cwd(), 'data');
8
9
  /** Safely read and parse a JSON file; returns null on any error */
@@ -190,11 +191,10 @@ export function computeHealthMetrics(project) {
190
191
  /** Read spec markdown files for the detail view */
191
192
  export async function loadSpecDetail(projectId, specId) {
192
193
  const specDir = join(DATA_DIR, 'projects', projectId, 'specs', specId);
193
- const [hu, ficha, progress] = await Promise.all([
194
- readTextFile(join(specDir, 'HU.md')),
195
- readTextFile(join(specDir, 'FICHA-TECNICA.md')),
196
- readTextFile(join(specDir, 'PROGRESS.md')),
197
- ]);
194
+ const specMd = await readTextFile(join(specDir, 'spec.md'));
195
+ const hu = specMd;
196
+ const ficha = specMd !== null ? extractSectionBody(specMd, 'Technical') || null : null;
197
+ const progress = specMd !== null ? extractSectionBody(specMd, 'Progress') || null : null;
198
198
  return { hu, ficha, progress };
199
199
  }
200
200
  /** Extract code links (file references) from a technical markdown */
@@ -257,12 +257,10 @@ export function parseFrontmatter(content) {
257
257
  /** Load enhanced spec detail with code links and frontmatter */
258
258
  export async function loadSpecDetailEnhanced(projectId, specId) {
259
259
  const specDir = join(DATA_DIR, 'projects', projectId, 'specs', specId);
260
- const [hu, ficha, progress, specMd] = await Promise.all([
261
- readTextFile(join(specDir, 'HU.md')),
262
- readTextFile(join(specDir, 'FICHA-TECNICA.md')),
263
- readTextFile(join(specDir, 'PROGRESS.md')),
264
- readTextFile(join(specDir, 'spec.md')),
265
- ]);
260
+ const specMd = await readTextFile(join(specDir, 'spec.md'));
261
+ const hu = specMd;
262
+ const ficha = specMd !== null ? extractSectionBody(specMd, 'Technical') || null : null;
263
+ const progress = specMd !== null ? extractSectionBody(specMd, 'Progress') || null : null;
266
264
  const codeLinks = ficha ? extractCodeLinks(ficha) : [];
267
265
  const frontmatter = specMd ? parseFrontmatter(specMd) : {};
268
266
  return { hu, ficha, progress, codeLinks, frontmatter };