@planu/cli 5.1.1 → 5.2.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 +27 -0
- package/dist/cli/commands/doctor.d.ts +22 -0
- package/dist/cli/commands/doctor.js +176 -2
- 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/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/validation-loop-handler.js +16 -15
- 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/vector-store.d.ts +18 -0
- package/dist/types/workers.d.ts +0 -3
- package/package.json +9 -9
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// engine/vector-store/tfidf.ts — TF-IDF embedding engine.
|
|
2
2
|
// SPEC-075 AC-01: Tokenize, compute TF-IDF, generate dense vectors.
|
|
3
3
|
import { normalizeVector } from './similarity.js';
|
|
4
|
+
/** Bumped when the exported state shape changes; persisted stores rebuild on mismatch. */
|
|
5
|
+
export const TFIDF_SCHEMA_VERSION = 1;
|
|
4
6
|
const DEFAULT_STOP_WORDS = new Set([
|
|
5
7
|
// English
|
|
6
8
|
'the',
|
|
@@ -238,6 +240,7 @@ export class TFIDFEngine {
|
|
|
238
240
|
/** Export state for serialization. */
|
|
239
241
|
exportState() {
|
|
240
242
|
return {
|
|
243
|
+
schemaVersion: TFIDF_SCHEMA_VERSION,
|
|
241
244
|
docFreq: [...this.docFreq.entries()],
|
|
242
245
|
docCount: this.docCount,
|
|
243
246
|
};
|
|
@@ -251,5 +254,28 @@ export class TFIDFEngine {
|
|
|
251
254
|
this.docCount = state.docCount;
|
|
252
255
|
this.vocabDirty = true;
|
|
253
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Remove a document's contribution to the corpus (SPEC-1345: incremental
|
|
259
|
+
* updates). `tokens` must be the unique token set the document contributed
|
|
260
|
+
* when it was added via `addDocument` — callers persisting the index keep
|
|
261
|
+
* this alongside the content hash so a changed document can be decremented
|
|
262
|
+
* before its new version is added back in.
|
|
263
|
+
*/
|
|
264
|
+
removeDocument(tokens) {
|
|
265
|
+
for (const term of new Set(tokens)) {
|
|
266
|
+
const freq = this.docFreq.get(term);
|
|
267
|
+
if (freq === undefined) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (freq <= 1) {
|
|
271
|
+
this.docFreq.delete(term);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.docFreq.set(term, freq - 1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
this.docCount = Math.max(0, this.docCount - 1);
|
|
278
|
+
this.vocabDirty = true;
|
|
279
|
+
}
|
|
254
280
|
}
|
|
255
281
|
//# sourceMappingURL=tfidf.js.map
|
|
@@ -36,7 +36,7 @@ export declare class WorkerConfigLoader {
|
|
|
36
36
|
*
|
|
37
37
|
* Slug-to-env: 'test-gaps' → 'TEST_GAPS'
|
|
38
38
|
* Full pattern: PLANU_WORKER_{WORKER_ENV}_{SETTING}
|
|
39
|
-
* Known settings: ENABLED (bool), COOLDOWNMS (number), PRIORITY (number)
|
|
39
|
+
* Known settings: ENABLED (bool), COOLDOWNMS (number), PRIORITY (number)
|
|
40
40
|
*/
|
|
41
41
|
parseEnvOverride(workerName: string): WorkerOverride;
|
|
42
42
|
/**
|
|
@@ -103,9 +103,6 @@ export class WorkerConfigLoader {
|
|
|
103
103
|
if (typeof raw.priority === 'number') {
|
|
104
104
|
override.priority = raw.priority;
|
|
105
105
|
}
|
|
106
|
-
if (raw.tier === 'free' || raw.tier === 'pro') {
|
|
107
|
-
override.tier = raw.tier;
|
|
108
|
-
}
|
|
109
106
|
return override;
|
|
110
107
|
}
|
|
111
108
|
/**
|
|
@@ -113,7 +110,7 @@ export class WorkerConfigLoader {
|
|
|
113
110
|
*
|
|
114
111
|
* Slug-to-env: 'test-gaps' → 'TEST_GAPS'
|
|
115
112
|
* Full pattern: PLANU_WORKER_{WORKER_ENV}_{SETTING}
|
|
116
|
-
* Known settings: ENABLED (bool), COOLDOWNMS (number), PRIORITY (number)
|
|
113
|
+
* Known settings: ENABLED (bool), COOLDOWNMS (number), PRIORITY (number)
|
|
117
114
|
*/
|
|
118
115
|
parseEnvOverride(workerName) {
|
|
119
116
|
const envName = workerName.toUpperCase().replace(/-/g, '_');
|
|
@@ -137,10 +134,6 @@ export class WorkerConfigLoader {
|
|
|
137
134
|
override.priority = parsed;
|
|
138
135
|
}
|
|
139
136
|
}
|
|
140
|
-
const tierRaw = process.env[`${prefix}TIER`];
|
|
141
|
-
if (tierRaw === 'free' || tierRaw === 'pro') {
|
|
142
|
-
override.tier = tierRaw;
|
|
143
|
-
}
|
|
144
137
|
return override;
|
|
145
138
|
}
|
|
146
139
|
/**
|
|
@@ -158,9 +151,6 @@ export class WorkerConfigLoader {
|
|
|
158
151
|
if (override.priority !== undefined) {
|
|
159
152
|
result.priority = override.priority;
|
|
160
153
|
}
|
|
161
|
-
if (override.tier !== undefined) {
|
|
162
|
-
result.tier = override.tier;
|
|
163
|
-
}
|
|
164
154
|
return result;
|
|
165
155
|
}
|
|
166
156
|
}
|
|
@@ -26,10 +26,6 @@ export declare const WorkerDefinitionSchema: z.ZodObject<{
|
|
|
26
26
|
priority: z.ZodNumber;
|
|
27
27
|
cooldownMs: z.ZodNumber;
|
|
28
28
|
enabled: z.ZodBoolean;
|
|
29
|
-
tier: z.ZodEnum<{
|
|
30
|
-
free: "free";
|
|
31
|
-
pro: "pro";
|
|
32
|
-
}>;
|
|
33
29
|
}, z.core.$strip>;
|
|
34
30
|
export declare const WorkerRegistrySchema: z.ZodObject<{
|
|
35
31
|
workers: z.ZodArray<z.ZodObject<{
|
|
@@ -49,10 +45,6 @@ export declare const WorkerRegistrySchema: z.ZodObject<{
|
|
|
49
45
|
priority: z.ZodNumber;
|
|
50
46
|
cooldownMs: z.ZodNumber;
|
|
51
47
|
enabled: z.ZodBoolean;
|
|
52
|
-
tier: z.ZodEnum<{
|
|
53
|
-
free: "free";
|
|
54
|
-
pro: "pro";
|
|
55
|
-
}>;
|
|
56
48
|
}, z.core.$strip>>;
|
|
57
49
|
}, z.core.$strip>;
|
|
58
50
|
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -33,7 +33,6 @@ export const WorkerDefinitionSchema = z.object({
|
|
|
33
33
|
priority: z.number().int().min(1).max(10).describe('Priority 1-10, higher = more important'),
|
|
34
34
|
cooldownMs: z.number().int().min(0).describe('Minimum milliseconds between executions'),
|
|
35
35
|
enabled: z.boolean().describe('Whether the worker is active'),
|
|
36
|
-
tier: z.enum(['free', 'pro']).describe('License tier: free | pro'),
|
|
37
36
|
});
|
|
38
37
|
export const WorkerRegistrySchema = z.object({
|
|
39
38
|
workers: z.array(WorkerDefinitionSchema).max(1000).describe('List of worker definitions'),
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { SupportedLocale } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* SPEC-1347: Resolve the locale from environment variables only — no I/O.
|
|
4
|
+
* Priority: PLANU_LOCALE env var, then the LANG/LC_ALL prefix, then 'en'.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveLocaleFromEnv(env?: NodeJS.ProcessEnv): SupportedLocale;
|
|
7
|
+
/**
|
|
8
|
+
* SPEC-1347: Resolve the locale to use at startup. Priority order:
|
|
9
|
+
* (1) persisted global config value, (2) PLANU_LOCALE env var,
|
|
10
|
+
* (3) LANG/LC_ALL env prefix mapped to a supported locale, (4) 'en' fallback.
|
|
11
|
+
* Never throws — unreadable/missing config falls through to env detection.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveInitialLocale(env?: NodeJS.ProcessEnv): SupportedLocale;
|
|
14
|
+
/**
|
|
15
|
+
* SPEC-1347: Recompute and apply the initial locale. Called once at module load to replace
|
|
16
|
+
* the previously hardcoded 'en' default; also exported so tests (and any future explicit
|
|
17
|
+
* re-init after a config change) can force recomputation deterministically.
|
|
18
|
+
*/
|
|
19
|
+
export declare function initLocale(env?: NodeJS.ProcessEnv): SupportedLocale;
|
|
2
20
|
/**
|
|
3
21
|
* Get the currently active locale.
|
|
4
22
|
*/
|
package/dist/i18n/index.js
CHANGED
|
@@ -4,11 +4,50 @@
|
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
+
import { readPersistedGlobalConfigSync } from '../storage/global-store.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Internal state
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
const SUPPORTED_LOCALES = ['en', 'es', 'pt'];
|
|
11
|
-
|
|
12
|
+
/**
|
|
13
|
+
* SPEC-1347: Resolve the locale from environment variables only — no I/O.
|
|
14
|
+
* Priority: PLANU_LOCALE env var, then the LANG/LC_ALL prefix, then 'en'.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveLocaleFromEnv(env = process.env) {
|
|
17
|
+
const explicit = env.PLANU_LOCALE;
|
|
18
|
+
if (explicit && isSupportedLocale(explicit)) {
|
|
19
|
+
return explicit;
|
|
20
|
+
}
|
|
21
|
+
const langRaw = env.LANG ?? env.LC_ALL;
|
|
22
|
+
const prefix = langRaw?.slice(0, 2).toLowerCase();
|
|
23
|
+
if (prefix && isSupportedLocale(prefix)) {
|
|
24
|
+
return prefix;
|
|
25
|
+
}
|
|
26
|
+
return 'en';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SPEC-1347: Resolve the locale to use at startup. Priority order:
|
|
30
|
+
* (1) persisted global config value, (2) PLANU_LOCALE env var,
|
|
31
|
+
* (3) LANG/LC_ALL env prefix mapped to a supported locale, (4) 'en' fallback.
|
|
32
|
+
* Never throws — unreadable/missing config falls through to env detection.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveInitialLocale(env = process.env) {
|
|
35
|
+
const persisted = readPersistedGlobalConfigSync();
|
|
36
|
+
if (persisted && isSupportedLocale(persisted.defaultLocale)) {
|
|
37
|
+
return persisted.defaultLocale;
|
|
38
|
+
}
|
|
39
|
+
return resolveLocaleFromEnv(env);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* SPEC-1347: Recompute and apply the initial locale. Called once at module load to replace
|
|
43
|
+
* the previously hardcoded 'en' default; also exported so tests (and any future explicit
|
|
44
|
+
* re-init after a config change) can force recomputation deterministically.
|
|
45
|
+
*/
|
|
46
|
+
export function initLocale(env = process.env) {
|
|
47
|
+
currentLocale = resolveInitialLocale(env);
|
|
48
|
+
return currentLocale;
|
|
49
|
+
}
|
|
50
|
+
let currentLocale = resolveInitialLocale();
|
|
12
51
|
/** Locale -> flattened key->value map */
|
|
13
52
|
const messageCache = new Map();
|
|
14
53
|
// ---------------------------------------------------------------------------
|
|
@@ -15,6 +15,15 @@ export declare function updateGlobalConfig(updates: Partial<GlobalConfig>): Prom
|
|
|
15
15
|
* Set the default locale globally.
|
|
16
16
|
*/
|
|
17
17
|
export declare function setDefaultLocale(locale: SupportedLocale): Promise<GlobalConfig>;
|
|
18
|
+
/**
|
|
19
|
+
* SPEC-1347: Synchronous, best-effort read of the persisted global config.
|
|
20
|
+
*
|
|
21
|
+
* i18n needs a locale value before any async I/O can complete (module init time), so this
|
|
22
|
+
* mirrors `getGlobalConfig()` but reads the file synchronously and returns `null` — instead
|
|
23
|
+
* of defaults — when no config has ever been persisted. That distinction lets callers tell
|
|
24
|
+
* "never configured" (fall back to env detection) apart from "explicitly persisted".
|
|
25
|
+
*/
|
|
26
|
+
export declare function readPersistedGlobalConfigSync(): GlobalConfig | null;
|
|
18
27
|
/**
|
|
19
28
|
* Set the default experience level globally.
|
|
20
29
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readJson, writeJson, globalDataDir } from './base-store.js';
|
|
2
2
|
import { withFileLock } from './file-mutex.js';
|
|
3
3
|
import { applyLearnedLimit, markVerified } from '../engine/registry-extender.js';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { reportClassifiedDegradation } from '../errors/classified-degradation.js';
|
|
4
6
|
// --- file paths ---
|
|
5
7
|
function configFile() {
|
|
6
8
|
return `${globalDataDir()}/config.json`;
|
|
@@ -45,6 +47,27 @@ export async function updateGlobalConfig(updates) {
|
|
|
45
47
|
export async function setDefaultLocale(locale) {
|
|
46
48
|
return updateGlobalConfig({ defaultLocale: locale });
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* SPEC-1347: Synchronous, best-effort read of the persisted global config.
|
|
52
|
+
*
|
|
53
|
+
* i18n needs a locale value before any async I/O can complete (module init time), so this
|
|
54
|
+
* mirrors `getGlobalConfig()` but reads the file synchronously and returns `null` — instead
|
|
55
|
+
* of defaults — when no config has ever been persisted. That distinction lets callers tell
|
|
56
|
+
* "never configured" (fall back to env detection) apart from "explicitly persisted".
|
|
57
|
+
*/
|
|
58
|
+
export function readPersistedGlobalConfigSync() {
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(configFile(), 'utf-8');
|
|
61
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code === 'ENOENT') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
reportClassifiedDegradation('GLOBAL_CONFIG_SYNC_READ_FAILURE', error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
48
71
|
/**
|
|
49
72
|
* Set the default experience level globally.
|
|
50
73
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TFIDFEngine } from '../engine/vector-store/tfidf.js';
|
|
2
|
+
import type { SemanticIndexDoc } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Persists a TF-IDF index for one logical corpus (e.g. all specs in a search
|
|
5
|
+
* scope, or a pattern type) and reconciles it incrementally against the
|
|
6
|
+
* current documents each time it is loaded.
|
|
7
|
+
*/
|
|
8
|
+
export declare class SemanticIndexStore {
|
|
9
|
+
private readonly filePath;
|
|
10
|
+
constructor(projectId: string, indexKey: string);
|
|
11
|
+
/**
|
|
12
|
+
* Load the persisted index and reconcile it with `docs`.
|
|
13
|
+
* - Unchanged documents are never re-embedded (`addDocument` is not called for them).
|
|
14
|
+
* - Changed or new documents are individually re-embedded and the index is re-persisted.
|
|
15
|
+
* - A missing, corrupted, schema-mismatched, or doc-removed index triggers a
|
|
16
|
+
* transparent full rebuild so search always returns results.
|
|
17
|
+
*/
|
|
18
|
+
loadOrBuild(docs: SemanticIndexDoc[]): Promise<TFIDFEngine>;
|
|
19
|
+
/** Full rebuild: fresh engine, `addDocument` for every doc, persist a new snapshot. */
|
|
20
|
+
private rebuildFull;
|
|
21
|
+
private persist;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=semantic-index-store.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// storage/semantic-index-store.ts — Persisted TF-IDF index with incremental updates (SPEC-1345).
|
|
2
|
+
// Avoids full corpus rebuilds on every semantic_search / learn call: persists the TF-IDF
|
|
3
|
+
// engine state plus a per-document content-hash manifest, and only re-embeds documents
|
|
4
|
+
// whose content changed since the last snapshot. A missing, corrupted, schema-mismatched,
|
|
5
|
+
// or doc-removed index self-heals via a transparent full rebuild — never errors out.
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { readJson, writeJson, projectDataDir } from './base-store.js';
|
|
8
|
+
import { withFileLock } from './file-mutex.js';
|
|
9
|
+
import { TFIDFEngine, TFIDF_SCHEMA_VERSION, tokenize } from '../engine/vector-store/tfidf.js';
|
|
10
|
+
function hashContent(content) {
|
|
11
|
+
return createHash('sha256').update(content).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
function uniqueTokens(content) {
|
|
14
|
+
return [...new Set(tokenize(content))];
|
|
15
|
+
}
|
|
16
|
+
function isValidPersistedIndex(value) {
|
|
17
|
+
if (!value || typeof value !== 'object') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const v = value;
|
|
21
|
+
return (v.schemaVersion === TFIDF_SCHEMA_VERSION &&
|
|
22
|
+
typeof v.docHashes === 'object' &&
|
|
23
|
+
v.docHashes !== null &&
|
|
24
|
+
typeof v.docTokens === 'object' &&
|
|
25
|
+
v.docTokens !== null &&
|
|
26
|
+
typeof v.tfidfState === 'object' &&
|
|
27
|
+
v.tfidfState !== null);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Persists a TF-IDF index for one logical corpus (e.g. all specs in a search
|
|
31
|
+
* scope, or a pattern type) and reconciles it incrementally against the
|
|
32
|
+
* current documents each time it is loaded.
|
|
33
|
+
*/
|
|
34
|
+
export class SemanticIndexStore {
|
|
35
|
+
filePath;
|
|
36
|
+
constructor(projectId, indexKey) {
|
|
37
|
+
this.filePath = `${projectDataDir(projectId)}/semantic-index-${indexKey}.json`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load the persisted index and reconcile it with `docs`.
|
|
41
|
+
* - Unchanged documents are never re-embedded (`addDocument` is not called for them).
|
|
42
|
+
* - Changed or new documents are individually re-embedded and the index is re-persisted.
|
|
43
|
+
* - A missing, corrupted, schema-mismatched, or doc-removed index triggers a
|
|
44
|
+
* transparent full rebuild so search always returns results.
|
|
45
|
+
*/
|
|
46
|
+
async loadOrBuild(docs) {
|
|
47
|
+
const persisted = await readJson(this.filePath, null);
|
|
48
|
+
if (!isValidPersistedIndex(persisted)) {
|
|
49
|
+
return this.rebuildFull(docs);
|
|
50
|
+
}
|
|
51
|
+
const currentIds = new Set(docs.map((d) => d.id));
|
|
52
|
+
const hadRemoval = Object.keys(persisted.docHashes).some((id) => !currentIds.has(id));
|
|
53
|
+
if (hadRemoval) {
|
|
54
|
+
return this.rebuildFull(docs);
|
|
55
|
+
}
|
|
56
|
+
const engine = new TFIDFEngine();
|
|
57
|
+
engine.importState(persisted.tfidfState);
|
|
58
|
+
const docHashes = { ...persisted.docHashes };
|
|
59
|
+
const docTokens = { ...persisted.docTokens };
|
|
60
|
+
let changed = false;
|
|
61
|
+
for (const doc of docs) {
|
|
62
|
+
const hash = hashContent(doc.content);
|
|
63
|
+
if (docHashes[doc.id] === hash) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const oldTokens = docTokens[doc.id];
|
|
67
|
+
if (oldTokens) {
|
|
68
|
+
engine.removeDocument(oldTokens);
|
|
69
|
+
}
|
|
70
|
+
engine.addDocument(doc.content);
|
|
71
|
+
docHashes[doc.id] = hash;
|
|
72
|
+
docTokens[doc.id] = uniqueTokens(doc.content);
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
if (changed) {
|
|
76
|
+
await this.persist(engine, docHashes, docTokens);
|
|
77
|
+
}
|
|
78
|
+
return engine;
|
|
79
|
+
}
|
|
80
|
+
/** Full rebuild: fresh engine, `addDocument` for every doc, persist a new snapshot. */
|
|
81
|
+
async rebuildFull(docs) {
|
|
82
|
+
const engine = new TFIDFEngine();
|
|
83
|
+
const docHashes = {};
|
|
84
|
+
const docTokens = {};
|
|
85
|
+
for (const doc of docs) {
|
|
86
|
+
engine.addDocument(doc.content);
|
|
87
|
+
docHashes[doc.id] = hashContent(doc.content);
|
|
88
|
+
docTokens[doc.id] = uniqueTokens(doc.content);
|
|
89
|
+
}
|
|
90
|
+
await this.persist(engine, docHashes, docTokens);
|
|
91
|
+
return engine;
|
|
92
|
+
}
|
|
93
|
+
async persist(engine, docHashes, docTokens) {
|
|
94
|
+
const state = {
|
|
95
|
+
schemaVersion: TFIDF_SCHEMA_VERSION,
|
|
96
|
+
docHashes,
|
|
97
|
+
docTokens,
|
|
98
|
+
tfidfState: engine.exportState(),
|
|
99
|
+
};
|
|
100
|
+
await withFileLock(this.filePath, async () => {
|
|
101
|
+
await writeJson(this.filePath, state);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=semantic-index-store.js.map
|
|
@@ -110,7 +110,7 @@ export async function quarantineCorruptStatus(statusPath, projectPath) {
|
|
|
110
110
|
}
|
|
111
111
|
return { quarantinedAt: new Date().toISOString(), quarantinePath };
|
|
112
112
|
}
|
|
113
|
-
import {
|
|
113
|
+
import { fastScanSpecsAsync } from '../../engine/core-bridge.js';
|
|
114
114
|
/**
|
|
115
115
|
* Rebuild a fresh status.json by reading all spec.md frontmatters.
|
|
116
116
|
* Returns the new ProjectStatus object (not written to disk — caller decides).
|
|
@@ -120,23 +120,21 @@ export async function rebuildStatusFromFrontmatters(params) {
|
|
|
120
120
|
const byStatus = {};
|
|
121
121
|
const byType = {};
|
|
122
122
|
let totalSpecs = 0;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
byType[b.specType] = (byType[b.specType] ?? 0) + 1;
|
|
130
|
-
}
|
|
131
|
-
return {
|
|
132
|
-
updatedAt: new Date().toISOString(),
|
|
133
|
-
totalSpecs,
|
|
134
|
-
byStatus,
|
|
135
|
-
byType,
|
|
136
|
-
recentChanges: [],
|
|
137
|
-
version: PLANU_VERSION,
|
|
138
|
-
};
|
|
123
|
+
const { value: briefs } = await fastScanSpecsAsync(projectPath);
|
|
124
|
+
if (briefs) {
|
|
125
|
+
for (const b of briefs) {
|
|
126
|
+
totalSpecs++;
|
|
127
|
+
byStatus[b.status] = (byStatus[b.status] ?? 0) + 1;
|
|
128
|
+
byType[b.specType] = (byType[b.specType] ?? 0) + 1;
|
|
139
129
|
}
|
|
130
|
+
return {
|
|
131
|
+
updatedAt: new Date().toISOString(),
|
|
132
|
+
totalSpecs,
|
|
133
|
+
byStatus,
|
|
134
|
+
byType,
|
|
135
|
+
recentChanges: [],
|
|
136
|
+
version: PLANU_VERSION,
|
|
137
|
+
};
|
|
140
138
|
}
|
|
141
139
|
const specsDir = join(projectPath, 'planu', 'specs');
|
|
142
140
|
const byStatusResult = {};
|
|
@@ -61,7 +61,11 @@ const CAPABILITY_SIGNALS = {
|
|
|
61
61
|
/\b(?:email\s+address|phone\s+number|social\s+security\s+number)\b/i,
|
|
62
62
|
],
|
|
63
63
|
events: [
|
|
64
|
-
|
|
64
|
+
// Negative lookahead excludes runtime compounds like "event-loop"/"event-driven": a
|
|
65
|
+
// hyphen right after "event" means the match is a prefix of a different word, not the
|
|
66
|
+
// noun "event" itself (SPEC-1262 — handler identifiers must not cross clause boundaries
|
|
67
|
+
// into unrelated event-loop prose).
|
|
68
|
+
/\b(?:process|handle|publish|consume)\w*(?:\s+[a-z0-9_-]+){0,3}\s+event\b(?!-)/i,
|
|
65
69
|
/\b(?:event\s+(?:schema|contract|producer|consumer|handler|stream)|message\s+(?:broker|queue|consumer|producer))\b/i,
|
|
66
70
|
/\b(?:kafka|rabbitmq|pubsub|nats|eventbridge|sqs|sns|dead[ -]?letter\s+queue|dlq)\b/i,
|
|
67
71
|
/\b(?:publish|produce|consume)(?:es|d|r|rs|ing)?\s+(?:an?\s+)?(?:event|message)\b/i,
|
|
@@ -17,7 +17,7 @@ import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-techni
|
|
|
17
17
|
import { extractFilesFromSpecBody } from '../engine/spec-format/technical-md-populator.js';
|
|
18
18
|
import { buildCanonicalUnifiedSpecContent, validateUnifiedSpecCandidate, } from '../engine/spec-format/unified-spec-builder.js';
|
|
19
19
|
import { buildImplementationContractSection } from '../engine/implementation-contract/index.js';
|
|
20
|
-
import {
|
|
20
|
+
import { resolveEnglishOnlySpecGate } from '../engine/spec-language/english-only.js';
|
|
21
21
|
import { FallbackGenerator } from '../engine/spec-generator/index.js';
|
|
22
22
|
import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
|
|
23
23
|
import { trackCost } from '../engine/cost-tracking/operation-tracker.js';
|
|
@@ -880,28 +880,50 @@ function validateCreateSpecEnums(input) {
|
|
|
880
880
|
}
|
|
881
881
|
const DESCRIPTION_MAX_CHARS = 10_000;
|
|
882
882
|
const DESCRIPTION_EXCEED_MSG = 'Description exceeds 10000 chars. Create with a summary (<3k) and use reconcile_spec to add the rest.';
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
if (validation.ok) {
|
|
886
|
-
return null;
|
|
887
|
-
}
|
|
883
|
+
/** SPEC-1342: Terminal rejection — only reached after one translation clarification round failed. */
|
|
884
|
+
function buildEnglishGateRejectionResult(gate) {
|
|
888
885
|
return {
|
|
889
886
|
content: [
|
|
890
887
|
{
|
|
891
888
|
type: 'text',
|
|
892
|
-
text: `English-only spec gate blocked create_spec.\n\n${
|
|
889
|
+
text: `English-only spec gate blocked create_spec.\n\n${gate.reason ?? 'Non-English prose detected.'}\n\n` +
|
|
893
890
|
'Rewrite the title and description in English, then call create_spec again. User-facing responses may be localized, but spec.md must be English.',
|
|
894
891
|
},
|
|
895
892
|
],
|
|
896
893
|
isError: true,
|
|
897
894
|
structuredContent: {
|
|
898
895
|
error: 'SPEC_LANGUAGE_GATE_BLOCKED',
|
|
899
|
-
detectedLanguage:
|
|
900
|
-
signals: validation.signals,
|
|
896
|
+
detectedLanguage: gate.originalLanguage,
|
|
901
897
|
fixHint: 'Rewrite title and description in English before creating the spec.',
|
|
902
898
|
},
|
|
903
899
|
};
|
|
904
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* SPEC-1342: Apply an already-resolved English-only gate outcome. Returns:
|
|
903
|
+
* - `{ earlyReturn: null, englishTitle?, englishDescription? }` when the caller may proceed
|
|
904
|
+
* (input was already English, or was successfully translated).
|
|
905
|
+
* - `{ earlyReturn: ToolResult }` when the caller must return early (translation clarification
|
|
906
|
+
* question, or terminal rejection after one failed clarification round).
|
|
907
|
+
*/
|
|
908
|
+
function applyEnglishOnlyGate(gate) {
|
|
909
|
+
switch (gate.action) {
|
|
910
|
+
case 'ok':
|
|
911
|
+
return { earlyReturn: null };
|
|
912
|
+
case 'translated':
|
|
913
|
+
return {
|
|
914
|
+
earlyReturn: null,
|
|
915
|
+
englishTitle: gate.englishTitle,
|
|
916
|
+
englishDescription: gate.englishDescription,
|
|
917
|
+
};
|
|
918
|
+
case 'reject':
|
|
919
|
+
return { earlyReturn: buildEnglishGateRejectionResult(gate) };
|
|
920
|
+
case 'ask':
|
|
921
|
+
return {
|
|
922
|
+
earlyReturn: interactiveResult(gate.questions ?? [], 'The spec title/description are not in English. Provide the English translation ' +
|
|
923
|
+
'(e.g. via AskUserQuestion), then retry create_spec with clarificationAnswers.', 'universal'),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
905
927
|
function quotePosixShellArgument(value) {
|
|
906
928
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
907
929
|
}
|
|
@@ -1209,15 +1231,30 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1209
1231
|
inputParams.specId) {
|
|
1210
1232
|
return handleAgentTeamSynthesis(inputParams.specId, resolvedPath, inputParams.agentTeamFindings);
|
|
1211
1233
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
// SPEC-584: Gate check — block if pending clarification token exists and no answers provided
|
|
1234
|
+
// SPEC-584: Gate check — block if pending clarification token exists and no answers provided.
|
|
1235
|
+
// Runs before the English-only gate so a second call with no translation answer hits the
|
|
1236
|
+
// existing well-formed "must call AskUserQuestion" mandate instead of re-asking forever.
|
|
1217
1237
|
const gateError = await runClarificationGate(resolvedPath, 'create_spec', inputParams.clarificationAnswers);
|
|
1218
1238
|
if (gateError) {
|
|
1219
1239
|
return gateError;
|
|
1220
1240
|
}
|
|
1241
|
+
// SPEC-1342: English-only gate — offers a translation clarification round instead of
|
|
1242
|
+
// hard-rejecting non-English title/description.
|
|
1243
|
+
const languageGateResult = resolveEnglishOnlySpecGate(resolvedInputParams.title, resolvedInputParams.description, resolvedInputParams.clarificationAnswers);
|
|
1244
|
+
const languageGate = applyEnglishOnlyGate(languageGateResult);
|
|
1245
|
+
if (languageGate.earlyReturn) {
|
|
1246
|
+
if (languageGateResult.action === 'ask') {
|
|
1247
|
+
await persistClarificationToken(languageGate.earlyReturn, hashProjectPath(resolvedPath), 'create_spec');
|
|
1248
|
+
}
|
|
1249
|
+
return languageGate.earlyReturn;
|
|
1250
|
+
}
|
|
1251
|
+
if (languageGate.englishTitle !== undefined) {
|
|
1252
|
+
resolvedInputParams.title = languageGate.englishTitle;
|
|
1253
|
+
}
|
|
1254
|
+
if (languageGate.englishDescription !== undefined) {
|
|
1255
|
+
resolvedInputParams.description = languageGate.englishDescription;
|
|
1256
|
+
}
|
|
1257
|
+
const originalLanguage = languageGateResult.originalLanguage;
|
|
1221
1258
|
const idempotencyKey = computeIdempotencyKey(inputParams.title, resolvedPath, inputParams.idempotencyKey);
|
|
1222
1259
|
const cutoff = Date.now() - IDEMPOTENCY_MATCH_WINDOW_MS;
|
|
1223
1260
|
const storeOnlyCandidate = await findStoreByIdempotencyKey(hashProjectPath(resolvedPath), idempotencyKey, cutoff);
|
|
@@ -1571,7 +1608,15 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
1571
1608
|
if (!claimLifecycle.committed) {
|
|
1572
1609
|
throw new Error('create_spec critical path completed without a durable commit');
|
|
1573
1610
|
}
|
|
1574
|
-
|
|
1611
|
+
const committedResult = criticalResult.value.data.committedResult;
|
|
1612
|
+
// SPEC-1342: surface the original (pre-translation) language on the created spec's response.
|
|
1613
|
+
if (originalLanguage && !committedResult.isError) {
|
|
1614
|
+
committedResult.structuredContent = {
|
|
1615
|
+
...committedResult.structuredContent,
|
|
1616
|
+
originalLanguage,
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
return finishRecoveredOperation(operationJournal, operationKey, committedResult);
|
|
1575
1620
|
}
|
|
1576
1621
|
catch (error) {
|
|
1577
1622
|
const message = error instanceof Error ? error.message : String(error);
|