@planu/cli 4.1.1 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/config/license-plans.json +65 -361
- package/dist/engine/core-bridge.js +35 -4
- package/dist/engine/hooks/file-watcher.d.ts +6 -0
- package/dist/engine/hooks/file-watcher.js +69 -16
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// engine/hooks/file-watcher.ts — File system watcher with normalized events (SPEC-129)
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { watch } from 'node:fs';
|
|
3
|
+
import { readdirSync, watch } from 'node:fs';
|
|
4
4
|
import { stat } from 'node:fs/promises';
|
|
5
5
|
import { normalize, join } from 'node:path';
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -73,15 +73,22 @@ export function matchesPatterns(filePath, include, exclude) {
|
|
|
73
73
|
// Default excludes
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
75
|
const DEFAULT_EXCLUDES = ['data/**', 'node_modules/**', '.git/**'];
|
|
76
|
+
const DEFAULT_MAX_TS_WATCHERS = 128;
|
|
77
|
+
function maxTypeScriptWatchers() {
|
|
78
|
+
const raw = Number(process.env.PLANU_MAX_TS_WATCHERS ?? DEFAULT_MAX_TS_WATCHERS);
|
|
79
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_TS_WATCHERS;
|
|
80
|
+
}
|
|
76
81
|
// ---------------------------------------------------------------------------
|
|
77
82
|
// FileWatcher
|
|
78
83
|
// ---------------------------------------------------------------------------
|
|
79
84
|
import { isNativeActive, startNativeWatcher } from '../core-bridge.js';
|
|
80
85
|
export class FileWatcher extends EventEmitter {
|
|
81
86
|
watcher = null;
|
|
87
|
+
watchers = new Set();
|
|
82
88
|
rootDir;
|
|
83
89
|
include;
|
|
84
90
|
exclude;
|
|
91
|
+
maxWatchers = maxTypeScriptWatchers();
|
|
85
92
|
/** Track known files to distinguish create vs modify */
|
|
86
93
|
knownFiles = new Set();
|
|
87
94
|
closed = false;
|
|
@@ -112,21 +119,11 @@ export class FileWatcher extends EventEmitter {
|
|
|
112
119
|
});
|
|
113
120
|
}
|
|
114
121
|
else {
|
|
115
|
-
this.
|
|
116
|
-
if (!filename || this.closed) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
// Normalize path separator
|
|
120
|
-
const relPath = filename.replace(/\\/g, '/');
|
|
121
|
-
void this.handleFsEvent(relPath);
|
|
122
|
-
});
|
|
123
|
-
this.watcher.on('error', (err) => {
|
|
124
|
-
this.emit('error', err);
|
|
125
|
-
});
|
|
122
|
+
this.startTypeScriptWatcher();
|
|
126
123
|
}
|
|
127
124
|
}
|
|
128
125
|
catch (err) {
|
|
129
|
-
this.
|
|
126
|
+
this.emitWatcherError(err);
|
|
130
127
|
}
|
|
131
128
|
}
|
|
132
129
|
/**
|
|
@@ -148,15 +145,71 @@ export class FileWatcher extends EventEmitter {
|
|
|
148
145
|
*/
|
|
149
146
|
close() {
|
|
150
147
|
this.closed = true;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.watcher = null;
|
|
148
|
+
for (const watcher of this.watchers) {
|
|
149
|
+
watcher.close();
|
|
154
150
|
}
|
|
151
|
+
this.watchers.clear();
|
|
152
|
+
this.watcher = null;
|
|
155
153
|
this.removeAllListeners();
|
|
156
154
|
}
|
|
157
155
|
// ---------------------------------------------------------------------------
|
|
158
156
|
// Private
|
|
159
157
|
// ---------------------------------------------------------------------------
|
|
158
|
+
startTypeScriptWatcher() {
|
|
159
|
+
this.watchDirectory(this.rootDir, '');
|
|
160
|
+
this.watchSubdirectories(this.rootDir, '');
|
|
161
|
+
}
|
|
162
|
+
watchDirectory(absDir, relDir) {
|
|
163
|
+
const watcher = watch(absDir, {}, (_eventType, filename) => {
|
|
164
|
+
if (!filename || this.closed) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const relPath = join(relDir, filename).replace(/\\/g, '/');
|
|
168
|
+
void this.handleFsEvent(relPath);
|
|
169
|
+
});
|
|
170
|
+
watcher.on('error', (err) => {
|
|
171
|
+
this.emitWatcherError(err);
|
|
172
|
+
});
|
|
173
|
+
this.watchers.add(watcher);
|
|
174
|
+
this.watcher ??= watcher;
|
|
175
|
+
}
|
|
176
|
+
watchSubdirectories(absDir, relDir) {
|
|
177
|
+
if (this.watchers.size >= this.maxWatchers) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
let entries;
|
|
181
|
+
try {
|
|
182
|
+
entries = readdirSync(absDir, { withFileTypes: true });
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.isDirectory()) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const childRel = join(relDir, entry.name).replace(/\\/g, '/');
|
|
192
|
+
if (this.exclude.some((p) => matchesGlobPattern(childRel, p))) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const childAbs = join(absDir, entry.name);
|
|
196
|
+
try {
|
|
197
|
+
this.watchDirectory(childAbs, childRel);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (this.watchers.size >= this.maxWatchers) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.watchSubdirectories(childAbs, childRel);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
emitWatcherError(err) {
|
|
209
|
+
if (this.listenerCount('error') > 0) {
|
|
210
|
+
this.emit('error', err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
160
213
|
async handleFsEvent(relPath) {
|
|
161
214
|
try {
|
|
162
215
|
// Check exclude patterns first
|
|
@@ -219,19 +219,33 @@ function buildPostCommitDriftScript() {
|
|
|
219
219
|
'[ -z "$CHANGED" ] && exit 0',
|
|
220
220
|
'',
|
|
221
221
|
'# Build JSON array of file paths',
|
|
222
|
-
'FILES_JSON
|
|
223
|
-
'
|
|
224
|
-
'
|
|
225
|
-
|
|
226
|
-
'
|
|
227
|
-
'
|
|
228
|
-
|
|
222
|
+
'FILES_JSON="["',
|
|
223
|
+
'FIRST=1',
|
|
224
|
+
'while IFS= read -r FILE_PATH; do',
|
|
225
|
+
' ESCAPED=$(printf \'%s\' "$FILE_PATH" | sed \'s/\\\\/\\\\\\\\/g; s/"/\\\\"/g\')',
|
|
226
|
+
' if [ "$FIRST" -eq 0 ]; then',
|
|
227
|
+
' FILES_JSON="$FILES_JSON,"',
|
|
228
|
+
' fi',
|
|
229
|
+
' FILES_JSON="$FILES_JSON\\"$ESCAPED\\""',
|
|
230
|
+
' FIRST=0',
|
|
231
|
+
'done <<PLANU_CHANGED_FILES',
|
|
232
|
+
'$CHANGED',
|
|
233
|
+
'PLANU_CHANGED_FILES',
|
|
234
|
+
'FILES_JSON="$FILES_JSON]"',
|
|
229
235
|
'',
|
|
230
236
|
"TS=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u)",
|
|
231
237
|
'',
|
|
232
238
|
'# Check for drift-worthy files (dep manifests or uncommon extensions)',
|
|
233
|
-
'
|
|
234
|
-
|
|
239
|
+
'if echo "$CHANGED" | grep -qE \'(^|/)(package\\.json|go\\.mod|requirements\\.txt|Cargo\\.toml|pom\\.xml|build\\.gradle|pyproject\\.toml|Gemfile|composer\\.json)$\' 2>/dev/null; then',
|
|
240
|
+
' HAS_DEP=1',
|
|
241
|
+
'else',
|
|
242
|
+
' HAS_DEP=0',
|
|
243
|
+
'fi',
|
|
244
|
+
"if echo \"$CHANGED\" | grep -oE '\\.[a-zA-Z]+$' | grep -qvE '\\.(ts|tsx|js|jsx|mjs|cjs|json|md|mdx|sh|bash|txt|yaml|yml|toml|lock|sum|mod|py|go|rs|java|kt|rb|php|cs|dart|swift|html|css|scss|vue|svelte|env|sql|xml|csv)$' 2>/dev/null; then",
|
|
245
|
+
' HAS_EXT=1',
|
|
246
|
+
'else',
|
|
247
|
+
' HAS_EXT=0',
|
|
248
|
+
'fi',
|
|
235
249
|
'',
|
|
236
250
|
'# Only write if there is something worth processing',
|
|
237
251
|
'if [ "$HAS_DEP" -gt 0 ] || [ "$HAS_EXT" -gt 0 ]; then',
|
|
@@ -14,6 +14,9 @@ import { safe, safeLicensed, safeTracked } from '../safe-handler.js';
|
|
|
14
14
|
import { handleActivateLicense, handleDeactivateLicense } from '../activate-license.js';
|
|
15
15
|
import { handleLicenseStatus } from '../license-status.js';
|
|
16
16
|
import { LicenseStatusOutputSchema } from '../schemas/index.js';
|
|
17
|
+
// ── OAuth tools (register-oauth-tools.ts / register-configure-oauth-tool.ts) ─
|
|
18
|
+
import { handleStartOAuthFlow, handleOAuthStatus, StartOAuthFlowInputSchema, OAuthStatusInputSchema, } from '../oauth-handler.js';
|
|
19
|
+
import { handleConfigureOAuth } from '../configure-oauth-handler.js';
|
|
17
20
|
// ── Usage tools (register-usage-tools.ts) ────────────────────────────────────
|
|
18
21
|
import { handleUsageStats } from '../usage-stats.js';
|
|
19
22
|
import { handleUsageReport } from '../usage-report.js';
|
|
@@ -126,6 +129,25 @@ export function registerInfraGroupTools(server) {
|
|
|
126
129
|
outputSchema: LicenseStatusOutputSchema,
|
|
127
130
|
annotations: { title: 'License Status', readOnlyHint: true },
|
|
128
131
|
}, safe(async (args) => handleLicenseStatus(args)));
|
|
132
|
+
// ── OAuth tools ────────────────────────────────────────────────────────────
|
|
133
|
+
server.registerTool('start_oauth_flow', {
|
|
134
|
+
description: 'Start guided OAuth setup for supported integrations such as Figma, GitHub, Sentry, or Supabase.',
|
|
135
|
+
inputSchema: StartOAuthFlowInputSchema,
|
|
136
|
+
annotations: { title: 'Start OAuth Flow', readOnlyHint: false },
|
|
137
|
+
}, safeLicensed('start_oauth_flow', async (args) => handleStartOAuthFlow(args)));
|
|
138
|
+
server.registerTool('oauth_status', {
|
|
139
|
+
description: 'Show which OAuth-backed integrations are connected for this project.',
|
|
140
|
+
inputSchema: OAuthStatusInputSchema,
|
|
141
|
+
annotations: { title: 'OAuth Status', readOnlyHint: true },
|
|
142
|
+
}, safeLicensed('oauth_status', async (args) => handleOAuthStatus(args)));
|
|
143
|
+
server.registerTool('configure_oauth', {
|
|
144
|
+
description: 'Inspect Planu MCP OAuth/API-key auth status or test a bearer token without exposing secrets.',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
action: z.enum(['status', 'test']).describe('Action: status | test'),
|
|
147
|
+
token: z.string().max(4096).optional().describe('Bearer token to test when action=test'),
|
|
148
|
+
},
|
|
149
|
+
annotations: { title: 'Configure OAuth', readOnlyHint: true },
|
|
150
|
+
}, safe(async (args) => handleConfigureOAuth(args)));
|
|
129
151
|
// ── Usage tools ────────────────────────────────────────────────────────────
|
|
130
152
|
server.registerTool('usage_stats', {
|
|
131
153
|
description: 'View your Planu usage statistics — tool calls, top tools, remaining daily calls. ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
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.3",
|
|
36
|
+
"@planu/core-darwin-x64": "4.1.3",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.1.3",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.1.3",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.1.3",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.1.3"
|
|
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
|