@planu/cli 4.1.1 → 4.1.2
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 +11 -0
- package/dist/config/license-plans.json +65 -361
- package/dist/tools/git/hook-ops.js +23 -9
- package/dist/tools/tool-registry/group-infra.js +22 -0
- package/package.json +7 -7
- package/dist/engine/escalator/index.d.ts +0 -5
- package/dist/engine/escalator/index.js +0 -5
- package/dist/engine/freeze/retro-audit.d.ts +0 -6
- package/dist/engine/freeze/retro-audit.js +0 -24
- package/dist/engine/heal/backup.d.ts +0 -9
- package/dist/engine/heal/backup.js +0 -21
- package/dist/engine/idioma-validator/index.d.ts +0 -17
- package/dist/engine/idioma-validator/index.js +0 -89
- package/dist/engine/saga/index.d.ts +0 -4
- package/dist/engine/saga/index.js +0 -4
- package/dist/engine/spec-state-machine/index.d.ts +0 -3
- package/dist/engine/spec-state-machine/index.js +0 -2
- package/dist/engine/spec-summary-html/dashboard-renderer.d.ts +0 -6
- package/dist/engine/spec-summary-html/dashboard-renderer.js +0 -333
- package/dist/engine/triagier/index.d.ts +0 -5
- package/dist/engine/triagier/index.js +0 -5
- package/dist/engine/universal-rules/index.d.ts +0 -5
- package/dist/engine/universal-rules/index.js +0 -6
- package/dist/testing/cassette/index.d.ts +0 -23
- package/dist/testing/cassette/index.js +0 -26
- package/dist/tools/domain-bundle-handler.d.ts +0 -37
- package/dist/tools/domain-bundle-handler.js +0 -71
- package/dist/tools/figma/rules-file.d.ts +0 -5
- package/dist/tools/figma/rules-file.js +0 -45
- package/dist/tools/heal-planu-root.d.ts +0 -8
- package/dist/tools/heal-planu-root.js +0 -144
- package/dist/tools/opencode-host-adapter.d.ts +0 -3
- package/dist/tools/opencode-host-adapter.js +0 -33
- package/dist/tools/plan-team-distribution.d.ts +0 -3
- package/dist/tools/plan-team-distribution.js +0 -71
- package/dist/tools/reconcile-status-json.d.ts +0 -4
- package/dist/tools/reconcile-status-json.js +0 -209
- package/dist/tools/register-all-tools.d.ts +0 -8
- package/dist/tools/register-all-tools.js +0 -239
- package/dist/tools/tool-registry/group-analysis-monitoring.d.ts +0 -3
- package/dist/tools/tool-registry/group-analysis-monitoring.js +0 -942
- package/dist/tools/tool-registry/group-integrations.d.ts +0 -3
- package/dist/tools/tool-registry/group-integrations.js +0 -1046
- package/dist/tools/tool-registry/group-misc.d.ts +0 -3
- package/dist/tools/tool-registry/group-misc.js +0 -1367
- package/dist/tools/tool-registry/group-platform.d.ts +0 -3
- package/dist/tools/tool-registry/group-platform.js +0 -1681
- package/dist/tools/tool-registry/group-session-knowledge.d.ts +0 -3
- package/dist/tools/tool-registry/group-session-knowledge.js +0 -1416
- package/dist/tools/tool-registry/group-spec-ops.d.ts +0 -3
- package/dist/tools/tool-registry/group-spec-ops.js +0 -917
- package/dist/tools/workspace-overview.d.ts +0 -4
- package/dist/tools/workspace-overview.js +0 -316
- package/dist/transports/middleware/index.d.ts +0 -9
- package/dist/transports/middleware/index.js +0 -7
- package/dist/transports/middleware/with-sandbox.d.ts +0 -21
- package/dist/transports/middleware/with-sandbox.js +0 -68
- package/dist/types/heal.d.ts +0 -18
- package/dist/types/heal.js +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.1.
|
|
36
|
-
"@planu/core-darwin-x64": "4.1.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.1.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.1.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.1.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.1.
|
|
35
|
+
"@planu/core-darwin-arm64": "4.1.2",
|
|
36
|
+
"@planu/core-darwin-x64": "4.1.2",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.1.2",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.1.2",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.1.2",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.1.2"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=24.0.0"
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { RETRY_BUDGETS, nextDecision } from './policy.js';
|
|
2
|
-
export { incrementCounter, readCounter, resetCounter } from './retry-counter.js';
|
|
3
|
-
export type { RetryKey } from './retry-counter.js';
|
|
4
|
-
export { withEscalation } from './with-escalation.js';
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// engine/escalator/index.ts — SPEC-729: Public API re-exports
|
|
2
|
-
export { RETRY_BUDGETS, nextDecision } from './policy.js';
|
|
3
|
-
export { incrementCounter, readCounter, resetCounter } from './retry-counter.js';
|
|
4
|
-
export { withEscalation } from './with-escalation.js';
|
|
5
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// engine/freeze/retro-audit.ts — SPEC-747: Append retro_audit entries to transition log
|
|
2
|
-
import { appendTransitionEvent } from '../../storage/transition-log.js';
|
|
3
|
-
/**
|
|
4
|
-
* Record a retro-audit event for a frozen spec edit.
|
|
5
|
-
*/
|
|
6
|
-
export async function recordRetroAudit(input) {
|
|
7
|
-
await appendTransitionEvent({
|
|
8
|
-
projectId: input.projectId,
|
|
9
|
-
specId: input.specId,
|
|
10
|
-
eventType: 'retro_audit',
|
|
11
|
-
from: 'done',
|
|
12
|
-
to: 'done',
|
|
13
|
-
actor: input.actor ?? 'system',
|
|
14
|
-
reason: input.reason,
|
|
15
|
-
sessionId: input.sessionId,
|
|
16
|
-
modelId: input.modelId,
|
|
17
|
-
meta: {
|
|
18
|
-
specVersionBefore: input.versionBefore,
|
|
19
|
-
specVersionAfter: input.versionAfter,
|
|
20
|
-
forceEdit: input.forceEdit ?? false,
|
|
21
|
-
},
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
//# sourceMappingURL=retro-audit.js.map
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a backup of `filePath` at `<filePath>.bak.<unix-ms>`.
|
|
3
|
-
* Must be called BEFORE the atomic rename of the new content.
|
|
4
|
-
*
|
|
5
|
-
* @returns The backup file path.
|
|
6
|
-
* @throws If the source file doesn't exist or the copy fails.
|
|
7
|
-
*/
|
|
8
|
-
export declare function backupFile(filePath: string, timestampMs?: number): Promise<string>;
|
|
9
|
-
//# sourceMappingURL=backup.d.ts.map
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// engine/heal/backup.ts — SPEC-745
|
|
2
|
-
// Creates timestamped backup files before any tier-1 heal.
|
|
3
|
-
import { copyFile } from 'node:fs/promises';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
/**
|
|
6
|
-
* Create a backup of `filePath` at `<filePath>.bak.<unix-ms>`.
|
|
7
|
-
* Must be called BEFORE the atomic rename of the new content.
|
|
8
|
-
*
|
|
9
|
-
* @returns The backup file path.
|
|
10
|
-
* @throws If the source file doesn't exist or the copy fails.
|
|
11
|
-
*/
|
|
12
|
-
export async function backupFile(filePath, timestampMs) {
|
|
13
|
-
const ts = timestampMs ?? Date.now();
|
|
14
|
-
const backupPath = `${filePath}.bak.${ts}`;
|
|
15
|
-
if (!existsSync(filePath)) {
|
|
16
|
-
throw new Error(`Cannot backup non-existent file: ${filePath}`);
|
|
17
|
-
}
|
|
18
|
-
await copyFile(filePath, backupPath);
|
|
19
|
-
return backupPath;
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=backup.js.map
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export type IdiomaVerdict = 'es' | 'en' | 'mixed' | 'unknown' | 'abstain';
|
|
2
|
-
export interface IdiomaResult {
|
|
3
|
-
verdict: IdiomaVerdict;
|
|
4
|
-
reason?: string;
|
|
5
|
-
wordCount?: number;
|
|
6
|
-
esScore?: number;
|
|
7
|
-
enScore?: number;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Detect the language of a spec's prose content.
|
|
11
|
-
*
|
|
12
|
-
* - Strips fenced code blocks before analysis.
|
|
13
|
-
* - Abstains when prose word count is below 50 (SPEC-724: avoids noise on early drafts).
|
|
14
|
-
* - Returns 'mixed' when both ES and EN markers exceed threshold.
|
|
15
|
-
*/
|
|
16
|
-
export declare function detectIdioma(text: string): IdiomaResult;
|
|
17
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// engine/idioma-validator/index.ts — SPEC-724: Spanish/English mixing detector for spec prose.
|
|
2
|
-
// Skips fenced code blocks and abstains when prose word count is below 50 words.
|
|
3
|
-
import { stripFencedBlocks } from '../spec-format/text-fences.js';
|
|
4
|
-
const SPANISH_MARKERS = [
|
|
5
|
-
'de',
|
|
6
|
-
'del',
|
|
7
|
-
'la',
|
|
8
|
-
'el',
|
|
9
|
-
'en',
|
|
10
|
-
'con',
|
|
11
|
-
'para',
|
|
12
|
-
'por',
|
|
13
|
-
'una',
|
|
14
|
-
'los',
|
|
15
|
-
'las',
|
|
16
|
-
'al',
|
|
17
|
-
'se',
|
|
18
|
-
'que',
|
|
19
|
-
'es',
|
|
20
|
-
'son',
|
|
21
|
-
'fue',
|
|
22
|
-
'está',
|
|
23
|
-
'este',
|
|
24
|
-
'esta',
|
|
25
|
-
'estos',
|
|
26
|
-
'estas',
|
|
27
|
-
'un',
|
|
28
|
-
'unos',
|
|
29
|
-
'unas',
|
|
30
|
-
];
|
|
31
|
-
const ENGLISH_MARKERS = [
|
|
32
|
-
'the',
|
|
33
|
-
'and',
|
|
34
|
-
'for',
|
|
35
|
-
'with',
|
|
36
|
-
'that',
|
|
37
|
-
'this',
|
|
38
|
-
'are',
|
|
39
|
-
'from',
|
|
40
|
-
'have',
|
|
41
|
-
'has',
|
|
42
|
-
'will',
|
|
43
|
-
'should',
|
|
44
|
-
'must',
|
|
45
|
-
'when',
|
|
46
|
-
'where',
|
|
47
|
-
'which',
|
|
48
|
-
];
|
|
49
|
-
/**
|
|
50
|
-
* Detect the language of a spec's prose content.
|
|
51
|
-
*
|
|
52
|
-
* - Strips fenced code blocks before analysis.
|
|
53
|
-
* - Abstains when prose word count is below 50 (SPEC-724: avoids noise on early drafts).
|
|
54
|
-
* - Returns 'mixed' when both ES and EN markers exceed threshold.
|
|
55
|
-
*/
|
|
56
|
-
export function detectIdioma(text) {
|
|
57
|
-
const prose = stripFencedBlocks(text);
|
|
58
|
-
const words = prose.split(/\s+/).filter(Boolean);
|
|
59
|
-
const wordCount = words.length;
|
|
60
|
-
// SPEC-724: abstain on short prose stubs — not enough signal for reliable detection
|
|
61
|
-
if (wordCount < 50) {
|
|
62
|
-
return { verdict: 'abstain', reason: 'INSUFFICIENT_PROSE', wordCount };
|
|
63
|
-
}
|
|
64
|
-
const lower = prose.toLowerCase();
|
|
65
|
-
const tokens = lower.split(/[\s\W]+/).filter((t) => t.length > 0);
|
|
66
|
-
const totalTokens = tokens.length || 1;
|
|
67
|
-
const esHits = tokens.filter((t) => SPANISH_MARKERS.includes(t)).length;
|
|
68
|
-
const enHits = tokens.filter((t) => ENGLISH_MARKERS.includes(t)).length;
|
|
69
|
-
const esScore = esHits / totalTokens;
|
|
70
|
-
const enScore = enHits / totalTokens;
|
|
71
|
-
const THRESHOLD = 0.03; // >3% hit rate = language present
|
|
72
|
-
const hasEs = esScore > THRESHOLD;
|
|
73
|
-
const hasEn = enScore > THRESHOLD;
|
|
74
|
-
let verdict;
|
|
75
|
-
if (hasEs && hasEn) {
|
|
76
|
-
verdict = 'mixed';
|
|
77
|
-
}
|
|
78
|
-
else if (hasEs) {
|
|
79
|
-
verdict = 'es';
|
|
80
|
-
}
|
|
81
|
-
else if (hasEn) {
|
|
82
|
-
verdict = 'en';
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
verdict = 'unknown';
|
|
86
|
-
}
|
|
87
|
-
return { verdict, wordCount, esScore, enScore };
|
|
88
|
-
}
|
|
89
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { Spec } from '../../types/index.js';
|
|
2
|
-
export declare function getInlineCss(): string;
|
|
3
|
-
export declare function getInlineScript(): string;
|
|
4
|
-
/** Generate the full dashboard HTML document. */
|
|
5
|
-
export declare function generateDashboardHtml(specs: Spec[], availablePages?: string[]): string;
|
|
6
|
-
//# sourceMappingURL=dashboard-renderer.d.ts.map
|
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { buildNavbar } from '../doc-generator/portal/portal-navbar.js';
|
|
2
|
-
import { getPortalThemeCSS } from '../doc-generator/portal/portal-theme.js';
|
|
3
|
-
import { buildQuickLinksSection } from '../doc-generator/portal/portal-landing-cards.js';
|
|
4
|
-
const STATUS_COLORS = {
|
|
5
|
-
draft: { bg: '#e5e7eb', fg: '#374151' },
|
|
6
|
-
review: { bg: '#dbeafe', fg: '#1d4ed8' },
|
|
7
|
-
approved: { bg: '#d1fae5', fg: '#065f46' },
|
|
8
|
-
implementing: { bg: '#fef3c7', fg: '#92400e' },
|
|
9
|
-
done: { bg: '#d1fae5', fg: '#065f46' },
|
|
10
|
-
discarded: { bg: '#f3f4f6', fg: '#9ca3af' },
|
|
11
|
-
};
|
|
12
|
-
const RISK_ICONS = { low: '🟢', medium: '🟡', high: '🔴' };
|
|
13
|
-
function escapeHtml(text) {
|
|
14
|
-
return text
|
|
15
|
-
.replace(/&/g, '&')
|
|
16
|
-
.replace(/</g, '<')
|
|
17
|
-
.replace(/>/g, '>')
|
|
18
|
-
.replace(/"/g, '"');
|
|
19
|
-
}
|
|
20
|
-
function statusBadge(status) {
|
|
21
|
-
const colors = STATUS_COLORS[status] ?? { bg: '#e5e7eb', fg: '#374151' };
|
|
22
|
-
return `<span class="badge" data-i18n-status="${escapeHtml(status)}" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(status)}</span>`;
|
|
23
|
-
}
|
|
24
|
-
function specNumericId(spec) {
|
|
25
|
-
const m = /SPEC-(\d+)/.exec(spec.id);
|
|
26
|
-
return m?.[1] ? parseInt(m[1], 10) : 0;
|
|
27
|
-
}
|
|
28
|
-
function sortSpecs(specs) {
|
|
29
|
-
return [...specs].sort((a, b) => specNumericId(a) - specNumericId(b));
|
|
30
|
-
}
|
|
31
|
-
function specFolder(spec) {
|
|
32
|
-
if (!spec.specPath) {
|
|
33
|
-
return '';
|
|
34
|
-
}
|
|
35
|
-
const parts = spec.specPath.split('/');
|
|
36
|
-
return parts.length >= 2 ? (parts[parts.length - 2] ?? '') : '';
|
|
37
|
-
}
|
|
38
|
-
function renderSpecRow(spec) {
|
|
39
|
-
const risk = RISK_ICONS[spec.risk] ?? '⚪';
|
|
40
|
-
const tags = spec.tags.length > 0
|
|
41
|
-
? spec.tags.map((t) => `<span class="tag">${escapeHtml(t)}</span>`).join('')
|
|
42
|
-
: '';
|
|
43
|
-
const folder = specFolder(spec);
|
|
44
|
-
const titleHtml = folder
|
|
45
|
-
? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="spec-link">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</a>`
|
|
46
|
-
: `<span class="spec-title">${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</span>`;
|
|
47
|
-
const reportsHtml = folder
|
|
48
|
-
? `<a href="specs/${escapeHtml(folder)}/executive-report.html" class="report-link" title="Executive Report">📊</a> <a href="specs/${escapeHtml(folder)}/technical-report.html" class="report-link" title="Technical Report">🔧</a>`
|
|
49
|
-
: '';
|
|
50
|
-
return `<tr data-status="${escapeHtml(spec.status)}" data-search="${escapeHtml(`${spec.id} ${spec.title} ${spec.tags.join(' ')} ${spec.type} ${spec.status}`.toLowerCase())}">
|
|
51
|
-
<td>${titleHtml}</td>
|
|
52
|
-
<td>${statusBadge(spec.status)}</td>
|
|
53
|
-
<td><span class="type-badge">${escapeHtml(spec.type)}</span></td>
|
|
54
|
-
<td>${risk} <span data-i18n-risk="${escapeHtml(spec.risk)}">${escapeHtml(spec.risk)}</span></td>
|
|
55
|
-
<td class="num">${String(spec.estimation.devHours)}h</td>
|
|
56
|
-
<td class="num">$${String(spec.estimation.totalCostUsd)}</td>
|
|
57
|
-
<td class="tags-cell">${tags}</td>
|
|
58
|
-
<td class="reports-cell">${reportsHtml}</td>
|
|
59
|
-
</tr>`;
|
|
60
|
-
}
|
|
61
|
-
function renderMetricCard(labelEn, labelEs, value, color) {
|
|
62
|
-
return `<div class="metric-card">
|
|
63
|
-
<div class="metric-value" style="color:${color}">${value}</div>
|
|
64
|
-
<div class="metric-label" data-i18n-en="${escapeHtml(labelEn)}" data-i18n-es="${escapeHtml(labelEs)}">${escapeHtml(labelEn)}</div>
|
|
65
|
-
</div>`;
|
|
66
|
-
}
|
|
67
|
-
function renderProgressBar(done, total) {
|
|
68
|
-
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
69
|
-
const color = pct >= 80 ? '#22c55e' : pct >= 40 ? '#f59e0b' : '#ef4444';
|
|
70
|
-
return `<div class="progress-bar">
|
|
71
|
-
<div class="progress-fill" style="width:${String(pct)}%;background:${color}"></div>
|
|
72
|
-
<span class="progress-text">${String(pct)}% <span data-i18n-en="complete" data-i18n-es="completado">complete</span></span>
|
|
73
|
-
</div>`;
|
|
74
|
-
}
|
|
75
|
-
export function getInlineCss() {
|
|
76
|
-
return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
77
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1f2937; line-height: 1.5; max-width: 1280px; margin: 0 auto; padding: 24px; }
|
|
78
|
-
.header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
|
|
79
|
-
h1 { font-size: 1.5rem; color: #111827; }
|
|
80
|
-
.subtitle { color: #6b7280; font-size: 0.85rem; margin-bottom: 20px; }
|
|
81
|
-
.lang-toggle { display: flex; gap: 4px; }
|
|
82
|
-
.lang-btn { padding: 4px 10px; border: 1px solid #d1d5db; border-radius: 4px; background: #fff; cursor: pointer; font-size: 0.75rem; font-weight: 600; color: #6b7280; transition: all 0.15s; }
|
|
83
|
-
.lang-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
|
|
84
|
-
.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
|
85
|
-
.metric-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
|
|
86
|
-
.metric-value { font-size: 1.6rem; font-weight: 700; line-height: 1.2; }
|
|
87
|
-
.metric-label { font-size: 0.72rem; color: #6b7280; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
88
|
-
.progress-bar { background: #e5e7eb; border-radius: 99px; height: 20px; position: relative; overflow: hidden; margin-bottom: 20px; }
|
|
89
|
-
.progress-fill { height: 100%; border-radius: 99px; transition: width 0.3s; }
|
|
90
|
-
.progress-text { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 0.7rem; font-weight: 600; color: #374151; }
|
|
91
|
-
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
|
|
92
|
-
.search-input { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.85rem; outline: none; }
|
|
93
|
-
.search-input:focus { border-color: #4F46E5; box-shadow: 0 0 0 2px rgba(79,70,229,0.15); }
|
|
94
|
-
.filter-btn { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.78rem; color: #374151; transition: all 0.15s; }
|
|
95
|
-
.filter-btn:hover { border-color: #4F46E5; color: #4F46E5; }
|
|
96
|
-
.filter-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
|
|
97
|
-
.table-wrap { overflow-x: auto; }
|
|
98
|
-
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; table-layout: fixed; }
|
|
99
|
-
thead th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; color: #6b7280; font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }
|
|
100
|
-
th:nth-child(1) { width: 38%; } th:nth-child(2) { width: 8%; } th:nth-child(3) { width: 7%; }
|
|
101
|
-
th:nth-child(4) { width: 8%; } th:nth-child(5) { width: 6%; } th:nth-child(6) { width: 7%; }
|
|
102
|
-
th:nth-child(7) { width: 18%; } th:nth-child(8) { width: 8%; }
|
|
103
|
-
tbody td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; overflow: hidden; text-overflow: ellipsis; }
|
|
104
|
-
tbody tr:hover { background: #f9fafb; }
|
|
105
|
-
tbody tr.hidden { display: none; }
|
|
106
|
-
.badge { display: inline-block; padding: 2px 10px; border-radius: 99px; font-size: 0.72rem; font-weight: 600; white-space: nowrap; text-transform: capitalize; }
|
|
107
|
-
.type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #f3f4f6; color: #6b7280; font-size: 0.72rem; }
|
|
108
|
-
.tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: #ede9fe; color: #5b21b6; font-size: 0.65rem; margin: 1px 2px; }
|
|
109
|
-
.tags-cell { max-width: 200px; overflow: hidden; }
|
|
110
|
-
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
111
|
-
.spec-link { color: #4F46E5; text-decoration: none; font-weight: 500; }
|
|
112
|
-
.spec-link:hover { text-decoration: underline; }
|
|
113
|
-
.spec-title { font-weight: 500; }
|
|
114
|
-
.report-link { text-decoration: none; font-size: 1rem; padding: 2px 3px; border-radius: 4px; transition: background 0.15s; }
|
|
115
|
-
.report-link:hover { background: #f3f4f6; }
|
|
116
|
-
.reports-cell { white-space: nowrap; text-align: center; }
|
|
117
|
-
.footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid #e5e7eb; color: #9ca3af; font-size: 0.72rem; text-align: center; }
|
|
118
|
-
.footer a { color: #4F46E5; text-decoration: none; }
|
|
119
|
-
.empty { text-align: center; padding: 48px 20px; color: #9ca3af; }
|
|
120
|
-
.pagination { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 16px; flex-wrap: wrap; }
|
|
121
|
-
.page-btn { padding: 5px 12px; border: 1px solid #d1d5db; border-radius: 6px; background: #fff; cursor: pointer; font-size: 0.78rem; color: #374151; transition: all 0.15s; min-width: 36px; text-align: center; }
|
|
122
|
-
.page-btn:hover { border-color: #4F46E5; color: #4F46E5; }
|
|
123
|
-
.page-btn.active { background: #4F46E5; color: #fff; border-color: #4F46E5; }
|
|
124
|
-
.page-btn:disabled { opacity: 0.4; cursor: default; }
|
|
125
|
-
.page-info { font-size: 0.78rem; color: #6b7280; margin: 0 8px; }
|
|
126
|
-
@media print { body { padding: 0; max-width: 100%; } .toolbar, .lang-toggle { display: none; } }
|
|
127
|
-
@media (max-width: 640px) { .metrics { grid-template-columns: repeat(2, 1fr); } .tags-cell { display: none; } th:nth-child(7), td:nth-child(7) { display: none; } }`;
|
|
128
|
-
}
|
|
129
|
-
// Inline JS script split into named sections to satisfy max-lines-per-function.
|
|
130
|
-
const SCRIPT_INIT = `(function() {
|
|
131
|
-
var rows = Array.from(document.querySelectorAll('#specs-body tr'));
|
|
132
|
-
var search = document.getElementById('search');
|
|
133
|
-
var filters = document.querySelectorAll('.filter-btn');
|
|
134
|
-
var langBtns = document.querySelectorAll('.lang-btn');
|
|
135
|
-
var pagination = document.getElementById('pagination');
|
|
136
|
-
var activeFilter = 'all';
|
|
137
|
-
var currentPage = 1;
|
|
138
|
-
var PAGE_SIZE = 25;
|
|
139
|
-
var currentLang = 'en';
|
|
140
|
-
var i18n = {
|
|
141
|
-
en: {
|
|
142
|
-
status: { draft: 'Draft', review: 'Review', approved: 'Approved', implementing: 'Building', done: 'Done', discarded: 'Discarded' },
|
|
143
|
-
risk: { low: 'low', medium: 'medium', high: 'high' },
|
|
144
|
-
filter: { all: 'All', implementing: 'Building', approved: 'Approved', review: 'Review', draft: 'Draft', done: 'Done', discarded: 'Discarded' },
|
|
145
|
-
of: 'of', prev: 'Prev', next: 'Next'
|
|
146
|
-
},
|
|
147
|
-
es: {
|
|
148
|
-
status: { draft: 'Borrador', review: 'Revisión', approved: 'Aprobado', implementing: 'En progreso', done: 'Hecho', discarded: 'Descartado' },
|
|
149
|
-
risk: { low: 'bajo', medium: 'medio', high: 'alto' },
|
|
150
|
-
filter: { all: 'Todos', implementing: 'En progreso', approved: 'Aprobados', review: 'Revisión', draft: 'Borrador', done: 'Hechos', discarded: 'Descartados' },
|
|
151
|
-
of: 'de', prev: 'Ant', next: 'Sig'
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
function getVisibleRows() {
|
|
155
|
-
var q = (search ? search.value : '').toLowerCase();
|
|
156
|
-
return rows.filter(function(row) {
|
|
157
|
-
var matchFilter = activeFilter === 'all' || row.dataset.status === activeFilter;
|
|
158
|
-
var matchSearch = !q || (row.dataset.search || '').includes(q);
|
|
159
|
-
return matchFilter && matchSearch;
|
|
160
|
-
});
|
|
161
|
-
}`;
|
|
162
|
-
const SCRIPT_PAGINATION = `
|
|
163
|
-
function renderPagination(visible) {
|
|
164
|
-
if (!pagination) return;
|
|
165
|
-
var totalPages = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
|
|
166
|
-
if (currentPage > totalPages) currentPage = totalPages;
|
|
167
|
-
var dict = i18n[currentLang] || i18n.en;
|
|
168
|
-
var start = (currentPage - 1) * PAGE_SIZE + 1;
|
|
169
|
-
var end = Math.min(currentPage * PAGE_SIZE, visible.length);
|
|
170
|
-
if (visible.length === 0) { pagination.innerHTML = ''; return; }
|
|
171
|
-
var html = '';
|
|
172
|
-
html += '<button class="page-btn" data-page="prev"' + (currentPage === 1 ? ' disabled' : '') + '>' + dict.prev + '</button>';
|
|
173
|
-
var pages = [];
|
|
174
|
-
if (totalPages <= 7) {
|
|
175
|
-
for (var i = 1; i <= totalPages; i++) pages.push(i);
|
|
176
|
-
} else {
|
|
177
|
-
pages.push(1);
|
|
178
|
-
if (currentPage > 3) pages.push('...');
|
|
179
|
-
for (var j = Math.max(2, currentPage - 1); j <= Math.min(totalPages - 1, currentPage + 1); j++) pages.push(j);
|
|
180
|
-
if (currentPage < totalPages - 2) pages.push('...');
|
|
181
|
-
pages.push(totalPages);
|
|
182
|
-
}
|
|
183
|
-
pages.forEach(function(p) {
|
|
184
|
-
if (p === '...') { html += '<span class="page-info">…</span>'; }
|
|
185
|
-
else { html += '<button class="page-btn' + (p === currentPage ? ' active' : '') + '" data-page="' + p + '">' + p + '</button>'; }
|
|
186
|
-
});
|
|
187
|
-
html += '<button class="page-btn" data-page="next"' + (currentPage === totalPages ? ' disabled' : '') + '>' + dict.next + '</button>';
|
|
188
|
-
html += '<span class="page-info">' + start + '–' + end + ' ' + dict.of + ' ' + visible.length + '</span>';
|
|
189
|
-
pagination.innerHTML = html;
|
|
190
|
-
pagination.querySelectorAll('.page-btn').forEach(function(btn) {
|
|
191
|
-
btn.addEventListener('click', function() {
|
|
192
|
-
var p = btn.dataset.page;
|
|
193
|
-
if (p === 'prev' && currentPage > 1) currentPage--;
|
|
194
|
-
else if (p === 'next' && currentPage < totalPages) currentPage++;
|
|
195
|
-
else if (p !== 'prev' && p !== 'next') currentPage = parseInt(p, 10);
|
|
196
|
-
applyFilters();
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
function applyFilters() {
|
|
201
|
-
var visible = getVisibleRows();
|
|
202
|
-
rows.forEach(function(row) { row.classList.add('hidden'); });
|
|
203
|
-
var start = (currentPage - 1) * PAGE_SIZE;
|
|
204
|
-
var end = start + PAGE_SIZE;
|
|
205
|
-
visible.forEach(function(row, idx) { if (idx >= start && idx < end) row.classList.remove('hidden'); });
|
|
206
|
-
renderPagination(visible);
|
|
207
|
-
}`;
|
|
208
|
-
const SCRIPT_LANG_AND_BOOT = `
|
|
209
|
-
function setLang(lang) {
|
|
210
|
-
currentLang = lang;
|
|
211
|
-
var dict = i18n[lang] || i18n.en;
|
|
212
|
-
document.querySelectorAll('[data-i18n-status]').forEach(function(el) {
|
|
213
|
-
var key = el.dataset.i18nStatus; if (dict.status[key]) el.textContent = dict.status[key];
|
|
214
|
-
});
|
|
215
|
-
document.querySelectorAll('[data-i18n-risk]').forEach(function(el) {
|
|
216
|
-
var key = el.dataset.i18nRisk; if (dict.risk[key]) el.textContent = dict.risk[key];
|
|
217
|
-
});
|
|
218
|
-
document.querySelectorAll('.filter-btn[data-filter]').forEach(function(el) {
|
|
219
|
-
var key = el.dataset.filter; if (dict.filter[key]) el.textContent = dict.filter[key];
|
|
220
|
-
});
|
|
221
|
-
document.querySelectorAll('[data-i18n-' + lang + ']').forEach(function(el) {
|
|
222
|
-
el.textContent = el.getAttribute('data-i18n-' + lang);
|
|
223
|
-
});
|
|
224
|
-
var thMap = lang === 'es'
|
|
225
|
-
? { Spec: 'Spec', Status: 'Estado', Type: 'Tipo', Risk: 'Riesgo', Hours: 'Horas', Cost: 'Costo', Tags: 'Etiquetas', Reports: 'Informes' }
|
|
226
|
-
: { Spec: 'Spec', Estado: 'Status', Tipo: 'Type', Riesgo: 'Risk', Horas: 'Hours', Costo: 'Cost', Etiquetas: 'Tags', Informes: 'Reports' };
|
|
227
|
-
document.querySelectorAll('thead th').forEach(function(th) {
|
|
228
|
-
if (thMap[th.textContent]) th.textContent = thMap[th.textContent];
|
|
229
|
-
});
|
|
230
|
-
if (search) search.placeholder = lang === 'es' ? 'Buscar specs\\u2026' : 'Search specs\\u2026';
|
|
231
|
-
langBtns.forEach(function(b) { b.classList.toggle('active', b.dataset.lang === lang); });
|
|
232
|
-
try { localStorage.setItem('planu-lang', lang); } catch(e) { /* localStorage unavailable in private browsing — ignore */ }
|
|
233
|
-
applyFilters();
|
|
234
|
-
}
|
|
235
|
-
if (search) search.addEventListener('input', function() { currentPage = 1; applyFilters(); });
|
|
236
|
-
filters.forEach(function(btn) {
|
|
237
|
-
btn.addEventListener('click', function() {
|
|
238
|
-
filters.forEach(function(b) { b.classList.remove('active'); });
|
|
239
|
-
btn.classList.add('active');
|
|
240
|
-
activeFilter = btn.dataset.filter || 'all';
|
|
241
|
-
currentPage = 1; applyFilters();
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
langBtns.forEach(function(btn) { btn.addEventListener('click', function() { setLang(btn.dataset.lang); }); });
|
|
245
|
-
var saved = null;
|
|
246
|
-
try { saved = localStorage.getItem('planu-lang'); } catch(e) { /* localStorage unavailable in private browsing — ignore */ }
|
|
247
|
-
var autoLang = saved || (navigator.language.startsWith('es') ? 'es' : 'en');
|
|
248
|
-
setLang(autoLang);
|
|
249
|
-
})();`;
|
|
250
|
-
export function getInlineScript() {
|
|
251
|
-
return SCRIPT_INIT + SCRIPT_PAGINATION + SCRIPT_LANG_AND_BOOT;
|
|
252
|
-
}
|
|
253
|
-
function renderBody(specs, generatedAt) {
|
|
254
|
-
const statusCounts = {};
|
|
255
|
-
for (const s of specs) {
|
|
256
|
-
statusCounts[s.status] = (statusCounts[s.status] ?? 0) + 1;
|
|
257
|
-
}
|
|
258
|
-
const discardedCount = statusCounts.discarded ?? 0;
|
|
259
|
-
const activeSpecs = specs.filter((sp) => sp.status !== 'discarded');
|
|
260
|
-
const totalSpecs = specs.length;
|
|
261
|
-
const doneCount = statusCounts.done ?? 0;
|
|
262
|
-
const implementingCount = statusCounts.implementing ?? 0;
|
|
263
|
-
const totalDevHours = activeSpecs.reduce((s, sp) => s + sp.estimation.devHours, 0);
|
|
264
|
-
const totalCost = activeSpecs.reduce((s, sp) => s + sp.estimation.totalCostUsd, 0);
|
|
265
|
-
const sortedSpecs = sortSpecs(specs);
|
|
266
|
-
const rows = sortedSpecs.map((s) => renderSpecRow(s)).join('\n');
|
|
267
|
-
const metricsHtml = `<div class="metrics">
|
|
268
|
-
${renderMetricCard('Specs', 'Specs', String(totalSpecs), '#4F46E5')}
|
|
269
|
-
${renderMetricCard('Done', 'Hechos', String(doneCount), '#22c55e')}
|
|
270
|
-
${renderMetricCard('Building', 'En progreso', String(implementingCount), '#f59e0b')}
|
|
271
|
-
${discardedCount > 0 ? renderMetricCard('Discarded', 'Descartados', String(discardedCount), '#9ca3af') : ''}
|
|
272
|
-
${renderMetricCard('Dev Hours', 'Horas Dev', String(Math.round(totalDevHours * 10) / 10), '#6366f1')}
|
|
273
|
-
${renderMetricCard('Est. Cost', 'Costo Est.', '$' + String(Math.round(totalCost)), '#4F46E5')}
|
|
274
|
-
</div>`;
|
|
275
|
-
const activeTotal = totalSpecs - discardedCount;
|
|
276
|
-
const tableHtml = totalSpecs === 0
|
|
277
|
-
? '<div class="empty"><p data-i18n-en="No specs yet. Create one with create_spec." data-i18n-es="No hay specs aún. Crea uno con create_spec.">No specs yet. Create one with <code>create_spec</code>.</p></div>'
|
|
278
|
-
: `<div class="toolbar">
|
|
279
|
-
<input type="text" class="search-input" placeholder="Search specs…" id="search">
|
|
280
|
-
<button class="filter-btn active" data-filter="all">All</button>
|
|
281
|
-
<button class="filter-btn" data-filter="implementing">Building</button>
|
|
282
|
-
<button class="filter-btn" data-filter="approved">Approved</button>
|
|
283
|
-
<button class="filter-btn" data-filter="review">Review</button>
|
|
284
|
-
<button class="filter-btn" data-filter="draft">Draft</button>
|
|
285
|
-
<button class="filter-btn" data-filter="done">Done</button>
|
|
286
|
-
<button class="filter-btn" data-filter="discarded">Discarded</button>
|
|
287
|
-
</div>
|
|
288
|
-
<div class="table-wrap">
|
|
289
|
-
<table>
|
|
290
|
-
<thead><tr><th>Spec</th><th>Status</th><th>Type</th><th>Risk</th><th>Hours</th><th>Cost</th><th>Tags</th><th>Reports</th></tr></thead>
|
|
291
|
-
<tbody id="specs-body">${rows}</tbody>
|
|
292
|
-
</table>
|
|
293
|
-
</div>
|
|
294
|
-
<div class="pagination" id="pagination"></div>`;
|
|
295
|
-
return `<div class="header-row">
|
|
296
|
-
<h1>📋 Planu — Spec Dashboard</h1>
|
|
297
|
-
<div class="lang-toggle">
|
|
298
|
-
<button class="lang-btn" data-lang="en">EN</button>
|
|
299
|
-
<button class="lang-btn" data-lang="es">ES</button>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
<p class="subtitle" data-i18n-en="Auto-generated by Planu · ${escapeHtml(generatedAt)}" data-i18n-es="Generado automáticamente por Planu · ${escapeHtml(generatedAt)}">Auto-generated by Planu · ${escapeHtml(generatedAt)}</p>
|
|
303
|
-
${metricsHtml}
|
|
304
|
-
${renderProgressBar(doneCount, activeTotal)}
|
|
305
|
-
${tableHtml}
|
|
306
|
-
<div class="footer">Generated by <a href="https://planu.dev">Planu</a> — Spec Driven Development</div>`;
|
|
307
|
-
}
|
|
308
|
-
/** Generate the full dashboard HTML document. */
|
|
309
|
-
export function generateDashboardHtml(specs, availablePages = []) {
|
|
310
|
-
const generatedAt = new Date().toLocaleString('en-US', {
|
|
311
|
-
dateStyle: 'medium',
|
|
312
|
-
timeStyle: 'short',
|
|
313
|
-
});
|
|
314
|
-
// Dashboard is at planu/ root — basePath is empty string
|
|
315
|
-
const navbar = buildNavbar('dashboard', '', availablePages);
|
|
316
|
-
const quickLinks = buildQuickLinksSection(availablePages);
|
|
317
|
-
return `<!DOCTYPE html>
|
|
318
|
-
<html lang="en">
|
|
319
|
-
<head>
|
|
320
|
-
<meta charset="UTF-8">
|
|
321
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
322
|
-
<title>Planu — Spec Dashboard</title>
|
|
323
|
-
<style>${getPortalThemeCSS()}${getInlineCss()}</style>
|
|
324
|
-
</head>
|
|
325
|
-
<body>
|
|
326
|
-
${navbar}
|
|
327
|
-
${quickLinks}
|
|
328
|
-
${renderBody(specs, generatedAt)}
|
|
329
|
-
<script>${getInlineScript()}</script>
|
|
330
|
-
</body>
|
|
331
|
-
</html>`;
|
|
332
|
-
}
|
|
333
|
-
//# sourceMappingURL=dashboard-renderer.js.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { classifyIntent, heuristicClassify } from './classifier.js';
|
|
2
|
-
export { routeToNextStep } from './router.js';
|
|
3
|
-
export { runWithGuard } from './cost-guard.js';
|
|
4
|
-
export type { CostGuardOpts, FallbackResult } from './cost-guard.js';
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// engine/triagier/index.ts — SPEC-726: Public API for the Triagier engine
|
|
2
|
-
export { classifyIntent, heuristicClassify } from './classifier.js';
|
|
3
|
-
export { routeToNextStep } from './router.js';
|
|
4
|
-
export { runWithGuard } from './cost-guard.js';
|
|
5
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export { UNIVERSAL_RULES } from './catalog.js';
|
|
2
|
-
export { writeRuleForHost } from './host-writer.js';
|
|
3
|
-
export { installUniversalRules } from './installer.js';
|
|
4
|
-
export { hashContent, readManifest, isUserModified, upsertManifestEntry, } from './user-edit-detector.js';
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
// engine/universal-rules/index.ts — SPEC-779 public API barrel
|
|
2
|
-
export { UNIVERSAL_RULES } from './catalog.js';
|
|
3
|
-
export { writeRuleForHost } from './host-writer.js';
|
|
4
|
-
export { installUniversalRules } from './installer.js';
|
|
5
|
-
export { hashContent, readManifest, isUserModified, upsertManifestEntry, } from './user-edit-detector.js';
|
|
6
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export { computeRequestSha } from './signature.js';
|
|
2
|
-
export type { AnthropicRequest } from './signature.js';
|
|
3
|
-
export { loadCassetteFromDisk as loadCassette, saveCassetteToDisk, upsertCassetteEntry, getCassettePath, type Cassette, type CassetteEntry, } from './store.js';
|
|
4
|
-
export { createInterceptor, isRecordingMode, CassetteMissError } from './intercept.js';
|
|
5
|
-
export type { MessageCreateFn } from './intercept.js';
|
|
6
|
-
import { type CassetteEntry } from './store.js';
|
|
7
|
-
/**
|
|
8
|
-
* withCassette — Higher-order helper for test cases.
|
|
9
|
-
* Runs a test function within a cassette scope.
|
|
10
|
-
* Actual interception must be installed by the caller via createInterceptor().
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* it('generates intake JSON', () => withCassette('handoff-artifacts/intake', async () => {
|
|
14
|
-
* const result = await generateIntakeJson(spec);
|
|
15
|
-
* expect(result).toMatchSnapshot();
|
|
16
|
-
* }));
|
|
17
|
-
*/
|
|
18
|
-
export declare function withCassette<T>(_cassetteName: string, fn: () => Promise<T>): Promise<T>;
|
|
19
|
-
/**
|
|
20
|
-
* recordCassette — Write a new entry to a cassette file (record mode only).
|
|
21
|
-
*/
|
|
22
|
-
export declare function recordCassette(name: string, entry: CassetteEntry): void;
|
|
23
|
-
//# sourceMappingURL=index.d.ts.map
|