@planu/cli 5.1.1 → 5.3.0
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 +54 -0
- package/dist/cli/commands/doctor.d.ts +22 -0
- package/dist/cli/commands/doctor.js +176 -2
- package/dist/config/environment-schema.json +161 -21
- package/dist/config/release-policy.json +0 -8
- package/dist/engine/autopilot/bootstrap.js +27 -0
- package/dist/engine/core-bridge.d.ts +28 -0
- package/dist/engine/core-bridge.js +67 -0
- package/dist/engine/drift-monitor.js +16 -18
- package/dist/engine/living-spec/hash-tracker.js +26 -28
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
- package/dist/engine/spec-language/english-only.d.ts +14 -0
- package/dist/engine/spec-language/english-only.js +58 -0
- package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
- package/dist/engine/spec-migrator/drift-detector.js +10 -12
- package/dist/engine/validation/validation-freshness.d.ts +20 -1
- package/dist/engine/validation/validation-freshness.js +181 -21
- package/dist/engine/validator/analyzer.d.ts +5 -2
- package/dist/engine/validator/analyzer.js +81 -23
- package/dist/engine/validator/deep-code-checker.d.ts +10 -0
- package/dist/engine/validator/deep-code-checker.js +33 -0
- package/dist/engine/validator/test-evidence-grounding.d.ts +56 -0
- package/dist/engine/validator/test-evidence-grounding.js +187 -0
- package/dist/engine/validator.js +15 -4
- package/dist/engine/vector-store/tfidf.d.ts +13 -9
- package/dist/engine/vector-store/tfidf.js +26 -0
- package/dist/engine/worker-config-loader.d.ts +1 -1
- package/dist/engine/worker-config-loader.js +1 -11
- package/dist/engine/workers/schema.d.ts +0 -8
- package/dist/engine/workers/schema.js +0 -1
- package/dist/i18n/index.d.ts +18 -0
- package/dist/i18n/index.js +40 -1
- package/dist/storage/global-store.d.ts +9 -0
- package/dist/storage/global-store.js +23 -0
- package/dist/storage/semantic-index-store.d.ts +23 -0
- package/dist/storage/semantic-index-store.js +105 -0
- package/dist/storage/status-store/self-healing.js +15 -17
- package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
- package/dist/tools/create-spec.js +60 -15
- package/dist/tools/init-project/handler.js +78 -25
- package/dist/tools/learn.js +10 -8
- package/dist/tools/registry/auth.js +1 -11
- package/dist/tools/semantic-search-handler.js +5 -6
- package/dist/tools/status-handler.js +19 -6
- package/dist/tools/validate.js +6 -2
- package/dist/tools/validation-loop-handler.js +16 -15
- package/dist/types/analysis.d.ts +2 -0
- package/dist/types/spec-language-translation.d.ts +18 -0
- package/dist/types/spec-language-translation.js +5 -0
- package/dist/types/spec-registry.d.ts +0 -2
- package/dist/types/status.d.ts +2 -0
- package/dist/types/validation-receipt.d.ts +7 -0
- package/dist/types/vector-store.d.ts +18 -0
- package/dist/types/workers.d.ts +0 -3
- package/package.json +20 -20
- package/planu-native.json +8 -29
- package/planu-plugin.json +1 -1
- package/dist/engine/security/cve-refresher.d.ts +0 -12
- package/dist/engine/security/cve-refresher.js +0 -128
|
@@ -11,6 +11,7 @@ import { analyzeProject, checkProjectCompleteness } from '../../engine/analyzer.
|
|
|
11
11
|
import { parseProjectQualityRules } from '../../engine/auditor-claude-md.js';
|
|
12
12
|
import { detectMissingAreas, buildRequestPrompt } from '../../engine/context-merger.js';
|
|
13
13
|
import { detectProjectPrivacyConfig, maybeGenerateEnvExample, buildBeginnerEnvGuide, } from './helpers.js';
|
|
14
|
+
import { buildDefaultPrivacyConfig } from '../../engine/pii-detector.js';
|
|
14
15
|
import { checkBundledVersionGap } from '../../engine/version-detector/bundled-version-checker.js';
|
|
15
16
|
import { checkAndFixBundledVersion } from '../../engine/mcp-config/mcp-config-writer.js';
|
|
16
17
|
import { buildInitProjectResult } from './result-builder.js';
|
|
@@ -86,6 +87,25 @@ function detectMultipleFrameworks(stack) {
|
|
|
86
87
|
}
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* SPEC-1343: Runs one independent, best-effort pipeline stage in isolation so it can be
|
|
92
|
+
* batched with `Promise.all` alongside other independent stages. A rejection never
|
|
93
|
+
* propagates to the caller — it resolves to `fallback` and is flagged via `failed`/`error`
|
|
94
|
+
* so init_project can still succeed and surface the failure in its result summary (AC2).
|
|
95
|
+
*/
|
|
96
|
+
async function runOptionalStage(name, fallback, task) {
|
|
97
|
+
try {
|
|
98
|
+
return { name, value: await task(), failed: false };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
name,
|
|
103
|
+
value: fallback,
|
|
104
|
+
failed: true,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
89
109
|
// eslint-disable-next-line max-lines-per-function
|
|
90
110
|
export async function handleInitProject(params, server) {
|
|
91
111
|
// eslint-disable-next-line max-lines-per-function, complexity
|
|
@@ -256,8 +276,14 @@ export async function handleInitProject(params, server) {
|
|
|
256
276
|
const globalConfig = await globalStore.getGlobalConfig();
|
|
257
277
|
const effectiveLocale = locale ?? globalConfig.defaultLocale;
|
|
258
278
|
const effectiveExperience = experienceLevel ?? globalConfig.defaultExperienceLevel;
|
|
259
|
-
//
|
|
260
|
-
|
|
279
|
+
// SPEC-1343: analyzeProject (full tree scan) and detectStackPatterns are both
|
|
280
|
+
// independent read-only scans of the same project tree — run them concurrently
|
|
281
|
+
// instead of back-to-back so wall time is ~1x the slower stage, not the sum (AC1).
|
|
282
|
+
const [knowledge, stackResult] = await Promise.all([
|
|
283
|
+
analyzeProject(projectPath, projectId, effectiveLocale, effectiveExperience),
|
|
284
|
+
detectStackPatterns(projectPath),
|
|
285
|
+
]);
|
|
286
|
+
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
261
287
|
if (approvedTechnologyContract !== null) {
|
|
262
288
|
knowledge.language = approvedTechnologyContract.language ?? knowledge.language;
|
|
263
289
|
knowledge.framework =
|
|
@@ -330,9 +356,6 @@ export async function handleInitProject(params, server) {
|
|
|
330
356
|
if (hourlyRate !== undefined) {
|
|
331
357
|
await globalStore.updateGlobalConfig({ hourlyRate });
|
|
332
358
|
}
|
|
333
|
-
// SPEC-033: Detect modern architecture patterns and stack info
|
|
334
|
-
const stackResult = await detectStackPatterns(projectPath);
|
|
335
|
-
const { modernPatterns, dddConfig, agileDetection, orchestratorInfo, composeMultiplatform } = stackResult;
|
|
336
359
|
// SPEC-568: Smart tool group activation based on detected stack.
|
|
337
360
|
// Runs before saveKnowledge so the snapshot can be persisted in one write.
|
|
338
361
|
const groupProfileSnapshot = applyStackBasedGroupActivations(knowledge.stack);
|
|
@@ -352,16 +375,54 @@ export async function handleInitProject(params, server) {
|
|
|
352
375
|
addProject(projectPath).catch(() => {
|
|
353
376
|
/* best-effort — never block init_project */
|
|
354
377
|
});
|
|
378
|
+
// SPEC-1343: buildProjectConfig -> runSpecMigrations must stay ordered (config must be
|
|
379
|
+
// written before migrations run against it, and runSpecMigrations is invoked exactly
|
|
380
|
+
// once per run, AC3). That ordered chain is independent of the read-only discovery
|
|
381
|
+
// batch below, so both run concurrently (AC1/AC4).
|
|
355
382
|
// SPEC-131: Build legal profile and generate planu.json / What's New
|
|
356
383
|
// SPEC-313: Pass workMode to persist in planu.json
|
|
357
|
-
const
|
|
384
|
+
const configAndMigrations = (async () => {
|
|
385
|
+
const configResult = await buildProjectConfig(projectPath, projectId, knowledge, workMode);
|
|
386
|
+
// Run spec migrations (v0.30 -> v0.40, legacy paths, prefix normalization)
|
|
387
|
+
const migrations = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
388
|
+
repositoryMigration: 'initialization',
|
|
389
|
+
});
|
|
390
|
+
return { configResult, migrations };
|
|
391
|
+
})();
|
|
392
|
+
// SPEC-1343: Independent, best-effort discovery stages (skills fetch, privacy
|
|
393
|
+
// detection, health check, convention scan) — none of these feed each other, so they
|
|
394
|
+
// batch into a single Promise.all with per-stage error isolation. A failing optional
|
|
395
|
+
// stage falls back to a safe default instead of failing init_project (AC2).
|
|
396
|
+
const [recommendedSkillsOutcome, privacyConfigOutcome, healthReportOutcome, conventionScanOutcome,] = await Promise.all([
|
|
397
|
+
runOptionalStage('fetchRecommendedSkills', [], () => fetchRecommendedSkills(knowledge)),
|
|
398
|
+
runOptionalStage('detectProjectPrivacyConfig', buildDefaultPrivacyConfig('GDPR', []), () => detectProjectPrivacyConfig(projectPath)),
|
|
399
|
+
runOptionalStage('runHealthCheckWithBaseline', null, async () => {
|
|
400
|
+
const allSpecs = await listProjectSpecs(projectId);
|
|
401
|
+
const specSummaries = allSpecs.map((s) => ({
|
|
402
|
+
specId: s.id,
|
|
403
|
+
status: s.status,
|
|
404
|
+
gitBranch: s.gitBranch,
|
|
405
|
+
updatedAt: s.updatedAt,
|
|
406
|
+
}));
|
|
407
|
+
return runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
408
|
+
}),
|
|
409
|
+
runOptionalStage('runConventionScanSafe', 'failed', () => runConventionScanSafe(projectPath)),
|
|
410
|
+
]);
|
|
411
|
+
const recommendedSkills = recommendedSkillsOutcome.value;
|
|
412
|
+
const privacyConfig = privacyConfigOutcome.value;
|
|
413
|
+
const healthReport = healthReportOutcome.value;
|
|
414
|
+
const conventionScanStatus = conventionScanOutcome.value;
|
|
415
|
+
const optionalStageFailures = [
|
|
416
|
+
recommendedSkillsOutcome,
|
|
417
|
+
privacyConfigOutcome,
|
|
418
|
+
healthReportOutcome,
|
|
419
|
+
conventionScanOutcome,
|
|
420
|
+
]
|
|
421
|
+
.filter((outcome) => outcome.failed)
|
|
422
|
+
.map((outcome) => `${outcome.name}: ${outcome.error ?? 'unknown error'}`);
|
|
423
|
+
const { configResult, migrations } = await configAndMigrations;
|
|
358
424
|
const { planuConfigPath, planuConfigGenerated, whatsNew } = configResult;
|
|
359
|
-
|
|
360
|
-
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = await runSpecMigrations(projectPath, projectId, knowledge, {
|
|
361
|
-
repositoryMigration: 'initialization',
|
|
362
|
-
});
|
|
363
|
-
// Auto-fetch recommended skills based on detected stack
|
|
364
|
-
const recommendedSkills = await fetchRecommendedSkills(knowledge);
|
|
425
|
+
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, changedPaths: migrationChangedPaths = [], } = migrations;
|
|
365
426
|
// Auto-install or queue skills (opt-out via planu.json or input param, SPEC-185)
|
|
366
427
|
const autoInstallFromConfig = autoInstallSkills ?? (await readAutoInstallFlag(planuConfigPath));
|
|
367
428
|
const { skillsAutoInstalled, skillsPendingInstall, skillsSkipped } = await orchestrateSkillInstalls(recommendedSkills, projectPath, autoInstallFromConfig);
|
|
@@ -393,17 +454,6 @@ export async function handleInitProject(params, server) {
|
|
|
393
454
|
const beginnerEnvGuide = effectiveExperience === 'beginner' && envVars.length > 0
|
|
394
455
|
? buildBeginnerEnvGuide(envVars)
|
|
395
456
|
: null;
|
|
396
|
-
// SPEC-030: Detect legal framework and third-party data processors
|
|
397
|
-
const privacyConfig = await detectProjectPrivacyConfig(projectPath);
|
|
398
|
-
// SPEC-180: Project health check (best-effort)
|
|
399
|
-
const allSpecs = await listProjectSpecs(projectId);
|
|
400
|
-
const specSummaries = allSpecs.map((s) => ({
|
|
401
|
-
specId: s.id,
|
|
402
|
-
status: s.status,
|
|
403
|
-
gitBranch: s.gitBranch,
|
|
404
|
-
updatedAt: s.updatedAt,
|
|
405
|
-
}));
|
|
406
|
-
const healthReport = await runHealthCheckWithBaseline(projectPath, projectId, knowledge, specSummaries);
|
|
407
457
|
// SPEC-491: Auto-cleanup planu/ — remove legacy HTMLs, migrate to lean spec format (fire-and-forget)
|
|
408
458
|
import('../../engine/spec-migrator/planu-root-cleaner.js')
|
|
409
459
|
.then(({ cleanPlanuRoot }) => cleanPlanuRoot(join(projectPath, 'planu')))
|
|
@@ -414,8 +464,6 @@ export async function handleInitProject(params, server) {
|
|
|
414
464
|
void withAudit(projectPath, 'init_project', 'regeneratePages', () => regeneratePages(projectPath, ['knowledge', 'risks', 'decisions']), (pages) => ({ pages: pages.length })).catch(() => {
|
|
415
465
|
/* best-effort — never fail init */
|
|
416
466
|
});
|
|
417
|
-
// SPEC-228: Deep scan — detect conventions and write planu/conventions.json (best-effort)
|
|
418
|
-
const conventionScanStatus = await runConventionScanSafe(projectPath);
|
|
419
467
|
// SPEC-645: Detect LLM client and cache in conventions.json (fire-and-forget)
|
|
420
468
|
void import('../../engine/client-detection.js')
|
|
421
469
|
.then(({ detectAndCacheClient }) => detectAndCacheClient(projectPath))
|
|
@@ -480,6 +528,11 @@ export async function handleInitProject(params, server) {
|
|
|
480
528
|
}
|
|
481
529
|
// SPEC-469: Build autopilot summary from setup steps performed
|
|
482
530
|
const collector = new AutopilotSummaryCollector();
|
|
531
|
+
// SPEC-1343 AC2: surface optional discovery-stage failures in the result summary
|
|
532
|
+
// instead of failing init_project.
|
|
533
|
+
for (const failure of optionalStageFailures) {
|
|
534
|
+
collector.pushFail('discovery-stage-failed', failure);
|
|
535
|
+
}
|
|
483
536
|
if (knowledge.stack.length > 0) {
|
|
484
537
|
collector.pushOk('stack-detection', `Stack detected: ${knowledge.stack.join(', ')}`);
|
|
485
538
|
}
|
package/dist/tools/learn.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// to pattern-store for future use by estimation, auditing, and spec generation.
|
|
4
4
|
// SPEC-011: adds dispatchFeedbackEvent for feedback loop dispatch.
|
|
5
5
|
import { patternStore, knowledgeStore } from '../storage/index.js';
|
|
6
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
6
7
|
import { t, ti } from '../i18n/index.js';
|
|
7
|
-
import { TFIDFEngine } from '../engine/vector-store/tfidf.js';
|
|
8
8
|
import { cosineSimilarity } from '../engine/vector-store/similarity.js';
|
|
9
9
|
import { compactJson } from './output-formatter.js';
|
|
10
10
|
/** Default cosine similarity threshold for semantic deduplication. */
|
|
@@ -45,7 +45,7 @@ export async function handleLearn(args) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
// 3. Semantic deduplication check (SPEC-075 AC-08)
|
|
48
|
-
const dedupWarning = checkSemanticDuplicate(existingPatterns, pattern, type);
|
|
48
|
+
const dedupWarning = await checkSemanticDuplicate(projectId, existingPatterns, pattern, type);
|
|
49
49
|
// 4. Create new pattern (store even if semantically similar — just warn)
|
|
50
50
|
const patternId = `PAT-${type.substring(0, 3).toUpperCase()}-${Date.now().toString(36).toUpperCase()}`;
|
|
51
51
|
const learnedPattern = {
|
|
@@ -93,18 +93,20 @@ export async function handleLearn(args) {
|
|
|
93
93
|
/**
|
|
94
94
|
* SPEC-075 AC-08: Check existing patterns for semantic similarity using TF-IDF.
|
|
95
95
|
* Returns a DedupWarning if a pattern with cosine similarity > threshold exists.
|
|
96
|
+
* SPEC-1345: the same-type corpus is loaded from a persisted, incrementally
|
|
97
|
+
* updated TF-IDF index instead of being rebuilt from scratch on every call.
|
|
96
98
|
*/
|
|
97
|
-
function checkSemanticDuplicate(existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
99
|
+
async function checkSemanticDuplicate(projectId, existingPatterns, newPattern, patternType, threshold = DEFAULT_DEDUP_THRESHOLD) {
|
|
98
100
|
// Filter to same-type patterns only
|
|
99
101
|
const sameType = existingPatterns.filter((p) => p.type === patternType);
|
|
100
102
|
if (sameType.length === 0) {
|
|
101
103
|
return undefined;
|
|
102
104
|
}
|
|
103
|
-
|
|
104
|
-
const tfidf =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
const indexStore = new SemanticIndexStore(projectId, `pattern-${patternType}`);
|
|
106
|
+
const tfidf = await indexStore.loadOrBuild(sameType.map((p) => ({ id: p.id, content: p.pattern })));
|
|
107
|
+
// Embed the candidate in-memory only (it isn't stored yet, so it never
|
|
108
|
+
// becomes part of the persisted corpus) so the IDF reflects it, matching
|
|
109
|
+
// the previous non-persisted behavior.
|
|
108
110
|
tfidf.addDocument(newPattern);
|
|
109
111
|
// Embed the new pattern
|
|
110
112
|
const newEmbedding = tfidf.embed(newPattern);
|
|
@@ -22,8 +22,6 @@ export async function handleRegistryLogin(args) {
|
|
|
22
22
|
const token = args.token;
|
|
23
23
|
const user = args.user;
|
|
24
24
|
const email = args.email;
|
|
25
|
-
const rawPlan = args.plan;
|
|
26
|
-
const plan = rawPlan ?? 'free';
|
|
27
25
|
const expiresAt = args.expiresAt;
|
|
28
26
|
if (!token || !user || !email) {
|
|
29
27
|
return {
|
|
@@ -37,7 +35,6 @@ export async function handleRegistryLogin(args) {
|
|
|
37
35
|
token: await storeSecretField('global/registry/pat', 'registry-pat', token),
|
|
38
36
|
user,
|
|
39
37
|
email,
|
|
40
|
-
plan,
|
|
41
38
|
expiresAt: expiresAt ?? new Date(Date.now() + TOKEN_EXPIRY_MS).toISOString(),
|
|
42
39
|
};
|
|
43
40
|
const credPath = credentialsPath();
|
|
@@ -45,12 +42,7 @@ export async function handleRegistryLogin(args) {
|
|
|
45
42
|
await chmod(resolveStorageLayout().secrets, 0o700);
|
|
46
43
|
await writeFile(credPath, JSON.stringify(credentials, null, 2), 'utf-8');
|
|
47
44
|
await chmod(credPath, 0o600);
|
|
48
|
-
return compactResult([
|
|
49
|
-
`Done: Logged in successfully`,
|
|
50
|
-
`**user**: ${user}`,
|
|
51
|
-
`**email**: ${email}`,
|
|
52
|
-
`**plan**: ${plan}`,
|
|
53
|
-
].join('\n'));
|
|
45
|
+
return compactResult([`Done: Logged in successfully`, `**user**: ${user}`, `**email**: ${email}`].join('\n'));
|
|
54
46
|
}
|
|
55
47
|
catch (error) {
|
|
56
48
|
/* v8 ignore start -- unexpected runtime errors */
|
|
@@ -100,7 +92,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
100
92
|
const info = {
|
|
101
93
|
user: cred.user,
|
|
102
94
|
email: cred.email,
|
|
103
|
-
plan: cred.plan,
|
|
104
95
|
expiresAt: cred.expiresAt,
|
|
105
96
|
expired: isExpired,
|
|
106
97
|
authenticated: !isExpired,
|
|
@@ -108,7 +99,6 @@ export async function handleRegistryWhoami(_args) {
|
|
|
108
99
|
return compactResult([
|
|
109
100
|
`**user**: ${info.user}`,
|
|
110
101
|
`**email**: ${info.email}`,
|
|
111
|
-
`**plan**: ${info.plan}`,
|
|
112
102
|
`**expiresAt**: ${info.expiresAt}`,
|
|
113
103
|
`**expired**: ${String(info.expired)}`,
|
|
114
104
|
`**authenticated**: ${String(info.authenticated)}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// tools/semantic-search-handler.ts — Handler for semantic_search tool.
|
|
2
2
|
// SPEC-075 AC-07: Generates TF-IDF embedding of query, searches backend.
|
|
3
3
|
import { getOrCreateBackend } from '../storage/vector-store/backend-factory.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SemanticIndexStore } from '../storage/semantic-index-store.js';
|
|
5
5
|
/**
|
|
6
6
|
* Handle a semantic_search request.
|
|
7
7
|
* Embeds the query with TF-IDF, then searches the vector backend.
|
|
@@ -33,11 +33,10 @@ export async function handleSemanticSearch(args) {
|
|
|
33
33
|
],
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
36
|
+
// SPEC-1345: load the persisted TF-IDF index for this scope, incrementally
|
|
37
|
+
// reconciled against the current corpus (no full rebuild for unchanged docs).
|
|
38
|
+
const indexStore = new SemanticIndexStore(projectId, scope);
|
|
39
|
+
const tfidf = await indexStore.loadOrBuild(allDocs.map((doc) => ({ id: doc.id, content: doc.content })));
|
|
41
40
|
// Embed the query
|
|
42
41
|
const queryEmbedding = tfidf.embed(query);
|
|
43
42
|
// Search
|
|
@@ -79,18 +79,28 @@ function parseSpecNumber(id) {
|
|
|
79
79
|
const match = /SPEC-(\d+)/i.exec(id);
|
|
80
80
|
return match?.[1] !== undefined ? parseInt(match[1], 10) : 0;
|
|
81
81
|
}
|
|
82
|
+
function selectActiveSpec(specs) {
|
|
83
|
+
// Lifecycle-first ordering: implementing always outranks review, regardless of SPEC number.
|
|
84
|
+
// Within the same lifecycle status, the tie-break is descending SPEC number (most recent first).
|
|
85
|
+
for (const status of ['implementing', 'review']) {
|
|
86
|
+
const candidates = specs
|
|
87
|
+
.filter((s) => s.status === status)
|
|
88
|
+
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
89
|
+
const candidate = candidates[0];
|
|
90
|
+
if (candidate !== undefined) {
|
|
91
|
+
return { id: candidate.id, title: candidate.title, status };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
82
96
|
async function getStatusSpecSnapshot(projectId) {
|
|
83
97
|
try {
|
|
84
98
|
const specs = await listSpecs(projectId);
|
|
85
|
-
const
|
|
86
|
-
.filter((s) => s.status === 'implementing' || s.status === 'review')
|
|
87
|
-
.sort((a, b) => parseSpecNumber(b.id) - parseSpecNumber(a.id));
|
|
99
|
+
const active = selectActiveSpec(specs.filter((s) => s.status === 'implementing' || s.status === 'review'));
|
|
88
100
|
const approved = specs
|
|
89
101
|
.filter((s) => s.status === 'approved')
|
|
90
102
|
.sort((a, b) => parseSpecNumber(a.id) - parseSpecNumber(b.id));
|
|
91
|
-
const activeItem = inProgress[0];
|
|
92
103
|
const nextItem = approved[0];
|
|
93
|
-
const active = activeItem !== undefined ? { id: activeItem.id, title: activeItem.title } : null;
|
|
94
104
|
const next = nextItem !== undefined ? { id: nextItem.id, title: nextItem.title } : null;
|
|
95
105
|
return { active, next, queueCount: approved.length };
|
|
96
106
|
}
|
|
@@ -130,6 +140,9 @@ async function getSLABreaches(projectId) {
|
|
|
130
140
|
// ---------------------------------------------------------------------------
|
|
131
141
|
function buildSuggestion(snapshot) {
|
|
132
142
|
if (snapshot.active !== null) {
|
|
143
|
+
if (snapshot.active.status === 'review') {
|
|
144
|
+
return `Resolve review for ${snapshot.active.id} — it is not yet implementing`;
|
|
145
|
+
}
|
|
133
146
|
return `Continue ${snapshot.active.id} — it may be blocking others`;
|
|
134
147
|
}
|
|
135
148
|
if (snapshot.next !== null) {
|
|
@@ -145,7 +158,7 @@ function buildOutput(params) {
|
|
|
145
158
|
const lines = ['Planu Status', '━━━━━━━━━━━━━━━'];
|
|
146
159
|
if (snapshot.active !== null) {
|
|
147
160
|
const title = snapshot.active.title.slice(0, 40);
|
|
148
|
-
lines.push(`ACTIVE ${snapshot.active.id} ${title} (
|
|
161
|
+
lines.push(`ACTIVE ${snapshot.active.id} ${title} (${snapshot.active.status})`);
|
|
149
162
|
}
|
|
150
163
|
else {
|
|
151
164
|
lines.push(`ACTIVE (none)`);
|
package/dist/tools/validate.js
CHANGED
|
@@ -730,7 +730,9 @@ function buildValidateStructuredContent(args) {
|
|
|
730
730
|
const minimalityBlockingFindings = args.minimalityReport?.findings.filter((finding) => finding.blocksDone) ?? [];
|
|
731
731
|
const blockers = [
|
|
732
732
|
...args.result.missing.map((missing) => `missing: ${missing}`),
|
|
733
|
-
|
|
733
|
+
// SPEC-1367 review finding 6: matches detectDrift's "needs-evidence"
|
|
734
|
+
// wording — display text only, the `indeterminate` field/enum is unchanged.
|
|
735
|
+
...args.indeterminate.map((item) => `needs-evidence: ${item}`),
|
|
734
736
|
...args.qualityGateSummary.failures.map((failure) => `gate: ${failure}`),
|
|
735
737
|
...args.qualityIssues
|
|
736
738
|
.filter((issue) => isBlockingQualitySeverity(issue.severity))
|
|
@@ -872,7 +874,9 @@ function buildCompactValidateText(args) {
|
|
|
872
874
|
const MAX_FAILURES = 5;
|
|
873
875
|
const labelledFailures = [
|
|
874
876
|
...missing.map((criterion) => ({ label: 'missing', criterion })),
|
|
875
|
-
|
|
877
|
+
// SPEC-1367 review finding 6: display label only — matches detectDrift's
|
|
878
|
+
// "needs-evidence" wording; the underlying `indeterminate` array is unchanged.
|
|
879
|
+
...indeterminate.map((criterion) => ({ label: 'needs-evidence', criterion })),
|
|
876
880
|
];
|
|
877
881
|
const shownFailures = labelledFailures.slice(0, MAX_FAILURES);
|
|
878
882
|
const remainingCount = labelledFailures.length - shownFailures.length;
|
|
@@ -34,22 +34,20 @@ export async function handleSpecHealthCheck(input) {
|
|
|
34
34
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
import {
|
|
37
|
+
import { fastScanSpecsAsync } from '../engine/core-bridge.js';
|
|
38
38
|
export async function handleHealthCheckAll(input) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
52
|
-
}
|
|
39
|
+
const { value: briefs, degradationNotice } = await fastScanSpecsAsync(input.projectPath);
|
|
40
|
+
if (briefs) {
|
|
41
|
+
// Sort worst to best
|
|
42
|
+
briefs.sort((a, b) => a.healthScore - b.healthScore);
|
|
43
|
+
const lines = [
|
|
44
|
+
`Health check for all ${briefs.length} specs (sorted worst to best) [NATIVE]:`,
|
|
45
|
+
``,
|
|
46
|
+
...briefs.map((b) => ` ${b.id}: ${b.healthScore}/100`),
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
50
|
+
};
|
|
53
51
|
}
|
|
54
52
|
const projectId = hashProjectPath(input.projectPath);
|
|
55
53
|
const specs = await specStore.listSpecs(projectId);
|
|
@@ -66,6 +64,9 @@ export async function handleHealthCheckAll(input) {
|
|
|
66
64
|
``,
|
|
67
65
|
...scores.map((s) => ` ${s.specId}: ${s.total}/100`),
|
|
68
66
|
];
|
|
67
|
+
if (degradationNotice !== undefined) {
|
|
68
|
+
lines.push('', degradationNotice);
|
|
69
|
+
}
|
|
69
70
|
return {
|
|
70
71
|
content: [{ type: 'text', text: lines.join('\n') }],
|
|
71
72
|
};
|
package/dist/types/analysis.d.ts
CHANGED
|
@@ -203,6 +203,8 @@ export interface ValidateResult {
|
|
|
203
203
|
export type CriterionEvidenceStatus = 'proven' | 'missing' | 'indeterminate';
|
|
204
204
|
export interface CriterionEvidenceResult {
|
|
205
205
|
status: CriterionEvidenceStatus;
|
|
206
|
+
/** SPEC-1367: citation for how the status was grounded (e.g. probe runner output, matched literal). */
|
|
207
|
+
evidence?: string;
|
|
206
208
|
}
|
|
207
209
|
export interface CodeState {
|
|
208
210
|
affectedFiles: string[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InteractiveQuestion } from './interactive-question.js';
|
|
2
|
+
/** Outcome of resolving the English-only gate for create_spec's title/description. */
|
|
3
|
+
export type EnglishOnlyGateAction = 'ok' | 'ask' | 'translated' | 'reject';
|
|
4
|
+
/** Result of `resolveEnglishOnlySpecGate`. Shape depends on `action`. */
|
|
5
|
+
export interface EnglishOnlyGateResolution {
|
|
6
|
+
action: EnglishOnlyGateAction;
|
|
7
|
+
/** Set for 'ask' | 'translated' | 'reject' — the language detected in the original input. */
|
|
8
|
+
originalLanguage?: 'es' | 'pt';
|
|
9
|
+
/** Set for 'ask' — the translation clarification questions to relay via AskUserQuestion. */
|
|
10
|
+
questions?: InteractiveQuestion[];
|
|
11
|
+
/** Set for 'reject' — human-readable reason from the underlying language detector. */
|
|
12
|
+
reason?: string;
|
|
13
|
+
/** Set for 'translated' — the English title to use instead of the original. */
|
|
14
|
+
englishTitle?: string;
|
|
15
|
+
/** Set for 'translated' — the English description to use instead of the original. */
|
|
16
|
+
englishDescription?: string;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=spec-language-translation.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// types/spec-language-translation.ts — SPEC-1342: translation-request shapes for the
|
|
2
|
+
// English-only spec gate. Lets create_spec offer an in-band translation clarification
|
|
3
|
+
// round instead of hard-rejecting non-English title/description input.
|
|
4
|
+
export {};
|
|
5
|
+
//# sourceMappingURL=spec-language-translation.js.map
|
package/dist/types/status.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ export interface GitState {
|
|
|
10
10
|
export interface SessionState {
|
|
11
11
|
lastCiStatus?: string;
|
|
12
12
|
}
|
|
13
|
+
export type StatusActiveLifecycleStatus = 'implementing' | 'review';
|
|
13
14
|
export interface StatusSpecSnapshot {
|
|
14
15
|
active: {
|
|
15
16
|
id: string;
|
|
16
17
|
title: string;
|
|
18
|
+
status: StatusActiveLifecycleStatus;
|
|
17
19
|
} | null;
|
|
18
20
|
next: {
|
|
19
21
|
id: string;
|
|
@@ -28,6 +28,13 @@ export interface ValidationFreshnessLease {
|
|
|
28
28
|
readonly epoch: string;
|
|
29
29
|
readonly generation: number;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Classification of one raw filesystem watcher event feeding the freshness
|
|
33
|
+
* barrier: a direct relevant/ignored child path, a synthetic root event (the
|
|
34
|
+
* host OS reporting activity on the watched directory itself), or an
|
|
35
|
+
* ambiguous filename-less event that needs authoritative verification.
|
|
36
|
+
*/
|
|
37
|
+
export type ValidationWatchEventClassification = 'direct-relevant' | 'ignored' | 'synthetic' | 'ambiguous';
|
|
31
38
|
export interface ValidationFreshnessWatchState {
|
|
32
39
|
readonly epoch: string;
|
|
33
40
|
readonly expiresAt: number;
|
|
@@ -21,6 +21,24 @@ export interface TFIDFConfig {
|
|
|
21
21
|
minDocFrequency: number;
|
|
22
22
|
stopWords: Set<string>;
|
|
23
23
|
}
|
|
24
|
+
/** Serializable TF-IDF engine state (SPEC-1345: persisted index). */
|
|
25
|
+
export interface TFIDFExportedState {
|
|
26
|
+
schemaVersion: number;
|
|
27
|
+
docFreq: [string, number][];
|
|
28
|
+
docCount: number;
|
|
29
|
+
}
|
|
30
|
+
/** A single corpus document as seen by the persisted semantic index (SPEC-1345). */
|
|
31
|
+
export interface SemanticIndexDoc {
|
|
32
|
+
id: string;
|
|
33
|
+
content: string;
|
|
34
|
+
}
|
|
35
|
+
/** On-disk shape of a persisted TF-IDF index + hash manifest (SPEC-1345). */
|
|
36
|
+
export interface PersistedSemanticIndex {
|
|
37
|
+
schemaVersion: number;
|
|
38
|
+
docHashes: Record<string, string>;
|
|
39
|
+
docTokens: Record<string, string[]>;
|
|
40
|
+
tfidfState: TFIDFExportedState;
|
|
41
|
+
}
|
|
24
42
|
/** Configuration for the HNSW index. */
|
|
25
43
|
export interface HNSWConfig {
|
|
26
44
|
M: number;
|
package/dist/types/workers.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ export interface WorkerTrigger {
|
|
|
8
8
|
/** Interval in ms for schedule triggers */
|
|
9
9
|
intervalMs?: number;
|
|
10
10
|
}
|
|
11
|
-
export type WorkerTier = 'free' | 'pro';
|
|
12
11
|
export interface WorkerDefinition {
|
|
13
12
|
name: string;
|
|
14
13
|
description: string;
|
|
@@ -19,7 +18,6 @@ export interface WorkerDefinition {
|
|
|
19
18
|
/** Minimum ms between executions */
|
|
20
19
|
cooldownMs: number;
|
|
21
20
|
enabled: boolean;
|
|
22
|
-
tier: WorkerTier;
|
|
23
21
|
}
|
|
24
22
|
export type WorkerFindingSeverity = 'critical' | 'high' | 'medium' | 'low' | 'warning' | 'info';
|
|
25
23
|
export interface WorkerFinding {
|
|
@@ -66,7 +64,6 @@ export interface WorkerOverride {
|
|
|
66
64
|
enabled?: boolean;
|
|
67
65
|
cooldownMs?: number;
|
|
68
66
|
priority?: number;
|
|
69
|
-
tier?: WorkerTier;
|
|
70
67
|
}
|
|
71
68
|
export interface WorkerProjectConfig {
|
|
72
69
|
overrides: Record<string, WorkerConfigOverride>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
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",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"packageName": "@planu/core"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@planu/core-darwin-arm64": "5.
|
|
39
|
-
"@planu/core-darwin-x64": "5.
|
|
40
|
-
"@planu/core-linux-arm64-gnu": "5.
|
|
41
|
-
"@planu/core-linux-arm64-musl": "5.
|
|
42
|
-
"@planu/core-linux-x64-gnu": "5.
|
|
43
|
-
"@planu/core-linux-x64-musl": "5.
|
|
44
|
-
"@planu/core-win32-arm64-msvc": "5.
|
|
45
|
-
"@planu/core-win32-x64-msvc": "5.
|
|
38
|
+
"@planu/core-darwin-arm64": "5.3.0",
|
|
39
|
+
"@planu/core-darwin-x64": "5.3.0",
|
|
40
|
+
"@planu/core-linux-arm64-gnu": "5.3.0",
|
|
41
|
+
"@planu/core-linux-arm64-musl": "5.3.0",
|
|
42
|
+
"@planu/core-linux-x64-gnu": "5.3.0",
|
|
43
|
+
"@planu/core-linux-x64-musl": "5.3.0",
|
|
44
|
+
"@planu/core-win32-arm64-msvc": "5.3.0",
|
|
45
|
+
"@planu/core-win32-x64-msvc": "5.3.0"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=24.0.0"
|
|
@@ -161,8 +161,8 @@
|
|
|
161
161
|
"license": "SEE LICENSE IN LICENSE",
|
|
162
162
|
"dependencies": {
|
|
163
163
|
"@anthropic-ai/sdk": "^0.115.0",
|
|
164
|
-
"@hono/node-server": "2.0
|
|
165
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
164
|
+
"@hono/node-server": "2.1.0",
|
|
165
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
166
166
|
"glob": "^13.0.6",
|
|
167
167
|
"yaml": "^2.9.0",
|
|
168
168
|
"zod": "^4.4.3"
|
|
@@ -173,16 +173,16 @@
|
|
|
173
173
|
"@commitlint/config-conventional": "^21.2.0",
|
|
174
174
|
"@eslint/js": "^10.0.1",
|
|
175
175
|
"@lhci/cli": "0.15.1",
|
|
176
|
-
"@napi-rs/cli": "3.
|
|
176
|
+
"@napi-rs/cli": "3.8.2",
|
|
177
177
|
"@noble/hashes": "2.2.0",
|
|
178
|
-
"@playwright/test": "1.62.
|
|
178
|
+
"@playwright/test": "1.62.1",
|
|
179
179
|
"@scure/base": "2.2.0",
|
|
180
180
|
"@secretlint/secretlint-rule-no-homedir": "^13.0.4",
|
|
181
181
|
"@secretlint/secretlint-rule-preset-recommend": "^13.0.4",
|
|
182
182
|
"@stryker-mutator/core": "^9.6.1",
|
|
183
183
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
184
|
-
"@supabase/supabase-js": "^2.
|
|
185
|
-
"@types/node": "^26.1.
|
|
184
|
+
"@supabase/supabase-js": "^2.112.0",
|
|
185
|
+
"@types/node": "^26.1.2",
|
|
186
186
|
"@types/qrcode": "1.5.6",
|
|
187
187
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
188
188
|
"@vitejs/plugin-vue": "^6.0.8",
|
|
@@ -196,17 +196,17 @@
|
|
|
196
196
|
"husky": "^9.1.7",
|
|
197
197
|
"javascript-obfuscator": "^5.5.0",
|
|
198
198
|
"jiti": "2.7.0",
|
|
199
|
-
"knip": "^6.
|
|
200
|
-
"lint-staged": "^17.
|
|
199
|
+
"knip": "^6.31.0",
|
|
200
|
+
"lint-staged": "^17.3.0",
|
|
201
201
|
"madge": "^8.0.0",
|
|
202
202
|
"prettier": "^3.9.6",
|
|
203
203
|
"qrcode": "1.5.4",
|
|
204
204
|
"secretlint": "^13.0.4",
|
|
205
205
|
"tsc-alias": "^1.9.1",
|
|
206
|
-
"type-coverage": "^2.
|
|
206
|
+
"type-coverage": "^2.30.1",
|
|
207
207
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
208
|
-
"typescript-eslint": "^8.
|
|
209
|
-
"vite": "^8.
|
|
208
|
+
"typescript-eslint": "^8.66.0",
|
|
209
|
+
"vite": "^8.2.0",
|
|
210
210
|
"vitest": "^4.1.10",
|
|
211
211
|
"vue": "^3.5.40"
|
|
212
212
|
}
|