@planu/cli 5.0.0 → 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.
Files changed (112) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/dist/cli/commands/doctor.d.ts +22 -0
  3. package/dist/cli/commands/doctor.js +176 -2
  4. package/dist/cli/commands/spec.js +10 -1
  5. package/dist/core/spec-validator.js +32 -18
  6. package/dist/engine/autopilot/bootstrap.js +27 -0
  7. package/dist/engine/core-bridge.d.ts +28 -0
  8. package/dist/engine/core-bridge.js +67 -0
  9. package/dist/engine/drift-monitor.js +16 -18
  10. package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
  11. package/dist/engine/evidence-gates/artifact-reader.js +59 -2
  12. package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
  13. package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
  14. package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
  15. package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
  16. package/dist/engine/execution/operation-journal.js +10 -4
  17. package/dist/engine/living-spec/hash-tracker.js +26 -28
  18. package/dist/engine/minimality/policy-loader.js +247 -6
  19. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  20. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
  21. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  22. package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
  23. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  24. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
  25. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  26. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
  27. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  28. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
  29. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  30. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
  31. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  32. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
  33. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  34. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
  35. package/dist/engine/reverse-engineer/api-detector.js +2 -13
  36. package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
  37. package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
  38. package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
  39. package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
  40. package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
  41. package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
  42. package/dist/engine/spec-format/acceptance-criteria.js +13 -12
  43. package/dist/engine/spec-format/text-fences.js +20 -2
  44. package/dist/engine/spec-language/english-only.d.ts +14 -0
  45. package/dist/engine/spec-language/english-only.js +58 -0
  46. package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
  47. package/dist/engine/spec-migrator/drift-detector.js +10 -12
  48. package/dist/engine/spec-state-syncer.js +1 -1
  49. package/dist/engine/timing/budget.js +5 -1
  50. package/dist/engine/vector-store/tfidf.d.ts +13 -9
  51. package/dist/engine/vector-store/tfidf.js +26 -0
  52. package/dist/engine/worker-config-loader.d.ts +1 -1
  53. package/dist/engine/worker-config-loader.js +1 -11
  54. package/dist/engine/workers/schema.d.ts +0 -8
  55. package/dist/engine/workers/schema.js +0 -1
  56. package/dist/i18n/index.d.ts +18 -0
  57. package/dist/i18n/index.js +40 -1
  58. package/dist/server/routes/specs.js +7 -5
  59. package/dist/storage/global-store.d.ts +9 -0
  60. package/dist/storage/global-store.js +23 -0
  61. package/dist/storage/semantic-index-store.d.ts +23 -0
  62. package/dist/storage/semantic-index-store.js +105 -0
  63. package/dist/storage/status-store/self-healing.js +15 -17
  64. package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
  65. package/dist/tools/challenge-spec-helpers.d.ts +10 -1
  66. package/dist/tools/challenge-spec-helpers.js +63 -22
  67. package/dist/tools/challenge-spec.js +18 -3
  68. package/dist/tools/check-readiness.js +37 -13
  69. package/dist/tools/create-spec/spec-builder.d.ts +7 -0
  70. package/dist/tools/create-spec/spec-builder.js +19 -4
  71. package/dist/tools/create-spec.js +216 -101
  72. package/dist/tools/init-project/handler.js +78 -25
  73. package/dist/tools/learn.js +10 -8
  74. package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
  75. package/dist/tools/registry/auth.js +1 -11
  76. package/dist/tools/semantic-search-handler.js +5 -6
  77. package/dist/tools/status-handler.js +19 -6
  78. package/dist/tools/sync-spec-state-handler.js +49 -1
  79. package/dist/tools/update-status/batch.d.ts +6 -2
  80. package/dist/tools/update-status/batch.js +58 -1
  81. package/dist/tools/update-status/dod-gates.d.ts +16 -1
  82. package/dist/tools/update-status/dod-gates.js +191 -1
  83. package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
  84. package/dist/tools/update-status/done-receipt-verifier.js +37 -2
  85. package/dist/tools/update-status/evidence-gate.d.ts +4 -0
  86. package/dist/tools/update-status/evidence-gate.js +67 -2
  87. package/dist/tools/update-status/file-sync.d.ts +2 -2
  88. package/dist/tools/update-status/index.d.ts +23 -1
  89. package/dist/tools/update-status/index.js +201 -24
  90. package/dist/tools/update-status/transition-guard.js +13 -1
  91. package/dist/tools/validation-loop-handler.js +16 -15
  92. package/dist/tools/workspace-dashboard-handler.js +38 -0
  93. package/dist/types/evidence-autofill.d.ts +34 -0
  94. package/dist/types/evidence-autofill.js +2 -0
  95. package/dist/types/index.d.ts +1 -0
  96. package/dist/types/index.js +1 -0
  97. package/dist/types/spec/core.d.ts +6 -0
  98. package/dist/types/spec/inputs.d.ts +7 -2
  99. package/dist/types/spec-format.d.ts +1 -1
  100. package/dist/types/spec-language-translation.d.ts +18 -0
  101. package/dist/types/spec-language-translation.js +5 -0
  102. package/dist/types/spec-registry.d.ts +0 -2
  103. package/dist/types/status.d.ts +2 -0
  104. package/dist/types/transition-log.d.ts +1 -1
  105. package/dist/types/validation.d.ts +8 -2
  106. package/dist/types/vector-store.d.ts +18 -0
  107. package/dist/types/workers.d.ts +0 -3
  108. package/package.json +11 -10
  109. package/planu-native.json +8 -29
  110. package/planu-plugin.json +1 -1
  111. package/dist/engine/security/cve-refresher.d.ts +0 -12
  112. package/dist/engine/security/cve-refresher.js +0 -128
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.0.0",
7
+ "bom-ref": "pkg:cargo/planu-core@5.2.0",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.0.0",
10
+ "version": "5.2.0",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "fc9c98775c98b2f91d36ce9cb74567a547512ba5d11635052f230868af9ca9e3"
14
+ "content": "bb7d31e5232b18f0ec9e03196cffdc57e48ac6993b6df064ff85b60c37804b93"
15
15
  }
16
16
  ]
17
17
  }
@@ -291,9 +291,9 @@
291
291
  },
292
292
  {
293
293
  "type": "library",
294
- "bom-ref": "pkg:cargo/globset@0.4.19",
294
+ "bom-ref": "pkg:cargo/globset@0.4.20",
295
295
  "name": "globset",
296
- "version": "0.4.19",
296
+ "version": "0.4.20",
297
297
  "externalReferences": [
298
298
  {
299
299
  "type": "distribution",
@@ -529,9 +529,9 @@
529
529
  },
530
530
  {
531
531
  "type": "library",
532
- "bom-ref": "pkg:cargo/regex-automata@0.4.16",
532
+ "bom-ref": "pkg:cargo/regex-automata@0.4.18",
533
533
  "name": "regex-automata",
534
- "version": "0.4.16",
534
+ "version": "0.4.18",
535
535
  "externalReferences": [
536
536
  {
537
537
  "type": "distribution",
@@ -1046,12 +1046,12 @@
1046
1046
  ]
1047
1047
  },
1048
1048
  {
1049
- "ref": "pkg:cargo/globset@0.4.19",
1049
+ "ref": "pkg:cargo/globset@0.4.20",
1050
1050
  "dependsOn": [
1051
1051
  "pkg:cargo/aho-corasick@1.1.5",
1052
1052
  "pkg:cargo/bstr@1.13.0",
1053
1053
  "pkg:cargo/log@0.4.33",
1054
- "pkg:cargo/regex-automata@0.4.16",
1054
+ "pkg:cargo/regex-automata@0.4.18",
1055
1055
  "pkg:cargo/regex-syntax@0.8.11"
1056
1056
  ]
1057
1057
  },
@@ -1069,10 +1069,10 @@
1069
1069
  "ref": "pkg:cargo/ignore@0.4.25",
1070
1070
  "dependsOn": [
1071
1071
  "pkg:cargo/crossbeam-deque@0.8.7",
1072
- "pkg:cargo/globset@0.4.19",
1072
+ "pkg:cargo/globset@0.4.20",
1073
1073
  "pkg:cargo/log@0.4.33",
1074
1074
  "pkg:cargo/memchr@2.8.3",
1075
- "pkg:cargo/regex-automata@0.4.16",
1075
+ "pkg:cargo/regex-automata@0.4.18",
1076
1076
  "pkg:cargo/same-file@1.0.6",
1077
1077
  "pkg:cargo/walkdir@2.5.0",
1078
1078
  "pkg:cargo/winapi-util@0.1.11"
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.0.0",
1109
+ "ref": "pkg:cargo/planu-core@5.2.0",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1151,7 +1151,7 @@
1151
1151
  ]
1152
1152
  },
1153
1153
  {
1154
- "ref": "pkg:cargo/regex-automata@0.4.16",
1154
+ "ref": "pkg:cargo/regex-automata@0.4.18",
1155
1155
  "dependsOn": [
1156
1156
  "pkg:cargo/aho-corasick@1.1.5",
1157
1157
  "pkg:cargo/memchr@2.8.3",
@@ -1167,7 +1167,7 @@
1167
1167
  "dependsOn": [
1168
1168
  "pkg:cargo/aho-corasick@1.1.5",
1169
1169
  "pkg:cargo/memchr@2.8.3",
1170
- "pkg:cargo/regex-automata@0.4.16",
1170
+ "pkg:cargo/regex-automata@0.4.18",
1171
1171
  "pkg:cargo/regex-syntax@0.8.11"
1172
1172
  ]
1173
1173
  },
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { shouldIgnoreDir } from './walk-ignore.js';
3
4
  const ROUTE_PATTERNS = [
4
5
  // Express/Koa/Hono: app.get('/path', handler) or router.post(...)
5
6
  {
@@ -69,18 +70,6 @@ const SOURCE_EXTENSIONS = new Set([
69
70
  '.kt',
70
71
  '.rb',
71
72
  ]);
72
- const IGNORE_DIRS = new Set([
73
- 'node_modules',
74
- '.git',
75
- 'dist',
76
- 'build',
77
- 'out',
78
- '.next',
79
- '.nuxt',
80
- 'coverage',
81
- 'vendor',
82
- '__pycache__',
83
- ]);
84
73
  function collectSourceFiles(dir) {
85
74
  const results = [];
86
75
  const queue = [dir];
@@ -97,7 +86,7 @@ function collectSourceFiles(dir) {
97
86
  continue;
98
87
  }
99
88
  for (const entry of entries) {
100
- if (IGNORE_DIRS.has(entry.name)) {
89
+ if (shouldIgnoreDir(entry.name)) {
101
90
  continue;
102
91
  }
103
92
  const full = path.join(current, entry.name);
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { shouldIgnoreDir } from './walk-ignore.js';
3
4
  const SOURCE_EXTENSIONS = new Set([
4
5
  '.ts',
5
6
  '.tsx',
@@ -14,18 +15,6 @@ const SOURCE_EXTENSIONS = new Set([
14
15
  '.rs',
15
16
  '.rb',
16
17
  ]);
17
- const IGNORE_DIRS = new Set([
18
- 'node_modules',
19
- '.git',
20
- 'dist',
21
- 'build',
22
- 'out',
23
- '.next',
24
- '.nuxt',
25
- 'coverage',
26
- 'vendor',
27
- '__pycache__',
28
- ]);
29
18
  // Patterns that increase cyclomatic complexity
30
19
  const BRANCH_PATTERNS = [
31
20
  /\bif\s*\(/g,
@@ -73,7 +62,7 @@ function collectSourceFiles(dir) {
73
62
  continue;
74
63
  }
75
64
  for (const entry of entries) {
76
- if (IGNORE_DIRS.has(entry.name)) {
65
+ if (shouldIgnoreDir(entry.name)) {
77
66
  continue;
78
67
  }
79
68
  const full = path.join(current, entry.name);
@@ -1,6 +1,7 @@
1
1
  import { technologyValue } from '../technology-registry.js';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
+ import { shouldIgnoreDir } from './walk-ignore.js';
4
5
  const ENV_VAR_PATTERNS = [
5
6
  // JS/TS: process.env.VAR_NAME
6
7
  /process\.env\.([A-Z_][A-Z0-9_]*)/g,
@@ -55,18 +56,6 @@ const INFRA_INDICATORS = {
55
56
  'azure-pipelines.yml': 'Azure Pipelines',
56
57
  'cloudbuild.yaml': 'Google Cloud Build',
57
58
  };
58
- const IGNORE_DIRS = new Set([
59
- 'node_modules',
60
- '.git',
61
- 'dist',
62
- 'build',
63
- 'out',
64
- '.next',
65
- '.nuxt',
66
- 'coverage',
67
- 'vendor',
68
- '__pycache__',
69
- ]);
70
59
  const SOURCE_EXTENSIONS = new Set([
71
60
  '.ts',
72
61
  '.tsx',
@@ -97,7 +86,7 @@ function collectAllFiles(dir) {
97
86
  continue;
98
87
  }
99
88
  for (const entry of entries) {
100
- if (IGNORE_DIRS.has(entry.name)) {
89
+ if (shouldIgnoreDir(entry.name)) {
101
90
  continue;
102
91
  }
103
92
  const full = path.join(current, entry.name);
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { shouldIgnoreDir } from './walk-ignore.js';
3
4
  const IMPORT_PATTERNS = [
4
5
  // ES import: import ... from './foo'
5
6
  /(?:import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"])/g,
@@ -22,18 +23,6 @@ const SOURCE_EXTENSIONS = new Set([
22
23
  '.rs',
23
24
  '.rb',
24
25
  ]);
25
- const IGNORE_DIRS = new Set([
26
- 'node_modules',
27
- '.git',
28
- 'dist',
29
- 'build',
30
- 'out',
31
- '.next',
32
- '.nuxt',
33
- 'coverage',
34
- 'vendor',
35
- '__pycache__',
36
- ]);
37
26
  const HUB_THRESHOLD = 10;
38
27
  function collectSourceFiles(dir) {
39
28
  const results = [];
@@ -51,7 +40,7 @@ function collectSourceFiles(dir) {
51
40
  continue;
52
41
  }
53
42
  for (const entry of entries) {
54
- if (IGNORE_DIRS.has(entry.name)) {
43
+ if (shouldIgnoreDir(entry.name)) {
55
44
  continue;
56
45
  }
57
46
  const full = path.join(current, entry.name);
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { shouldIgnoreDir } from './walk-ignore.js';
3
4
  const TEST_PATTERNS = [
4
5
  /\.test\.\w+$/,
5
6
  /\.spec\.\w+$/,
@@ -25,18 +26,6 @@ const SOURCE_EXTENSIONS = new Set([
25
26
  '.c',
26
27
  '.swift',
27
28
  ]);
28
- const IGNORE_DIRS = new Set([
29
- 'node_modules',
30
- '.git',
31
- 'dist',
32
- 'build',
33
- 'out',
34
- '.next',
35
- '.nuxt',
36
- 'coverage',
37
- 'vendor',
38
- '__pycache__',
39
- ]);
40
29
  export function isTestFile(filePath) {
41
30
  const basename = path.basename(filePath);
42
31
  if (TEST_PATTERNS.some((p) => p.test(basename))) {
@@ -61,7 +50,7 @@ function collectFiles(dir) {
61
50
  continue;
62
51
  }
63
52
  for (const entry of entries) {
64
- if (IGNORE_DIRS.has(entry.name)) {
53
+ if (shouldIgnoreDir(entry.name)) {
65
54
  continue;
66
55
  }
67
56
  const full = path.join(current, entry.name);
@@ -0,0 +1,3 @@
1
+ export declare const WALK_IGNORE_DIRS: ReadonlySet<string>;
2
+ export declare function shouldIgnoreDir(name: string): boolean;
3
+ //# sourceMappingURL=walk-ignore.d.ts.map
@@ -0,0 +1,26 @@
1
+ // engine/reverse-engineer/walk-ignore.ts — shared directory-walk ignore list
2
+ // Consumed by all reverse-engineer analyzers (dependency-graph, test-analyzer,
3
+ // api-detector, config-analyzer, complexity-analyzer) so the ignore rules stay
4
+ // in one place instead of five duplicated copies.
5
+ export const WALK_IGNORE_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ 'dist',
9
+ 'build',
10
+ 'out',
11
+ '.next',
12
+ '.nuxt',
13
+ 'coverage',
14
+ 'vendor',
15
+ '__pycache__',
16
+ '.worktrees',
17
+ '.claude',
18
+ 'planu',
19
+ 'website',
20
+ 'data',
21
+ 'benchmarks',
22
+ ]);
23
+ export function shouldIgnoreDir(name) {
24
+ return WALK_IGNORE_DIRS.has(name);
25
+ }
26
+ //# sourceMappingURL=walk-ignore.js.map
@@ -1,6 +1,7 @@
1
1
  import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
2
2
  import { stripFrontmatter } from '../frontmatter-parser.js';
3
3
  import { extractListItems, extractSection } from './markdown-sections.js';
4
+ import { stripFencedBlocks } from './text-fences.js';
4
5
  import { createCriterionIdentity, normalizeCriterionText } from '../criterion-identity.js';
5
6
  const BDD_STEP = /^(GIVEN|WHEN|THEN|AND|DADO|CUANDO|ENTONCES|Y)\b/i;
6
7
  const BDD_START = /^(GIVEN|DADO)\b/i;
@@ -91,7 +92,7 @@ function extractSectionCriteria(section) {
91
92
  return extractBddBlocks(section);
92
93
  }
93
94
  function extractBodyCriteria(raw, options) {
94
- const body = stripFrontmatter(raw);
95
+ const body = stripFencedBlocks(stripFrontmatter(raw));
95
96
  const acceptanceCriteria = extractSection(body, 'acceptance criteria', 'criterios de aceptaci');
96
97
  if (acceptanceCriteria) {
97
98
  const sectionCriteria = extractSectionCriteria(acceptanceCriteria);
@@ -99,18 +100,18 @@ function extractBodyCriteria(raw, options) {
99
100
  return sectionCriteria;
100
101
  }
101
102
  }
102
- if (options.allowLegacyBodyFallback === false) {
103
- return [];
104
- }
105
- const checkboxCriteria = body
106
- .split('\n')
107
- .map((line) => line.trim())
108
- .filter((line) => /^-\s*\[[ x]\]/i.test(line))
109
- .map((line) => line.replace(/^-\s*\[[ x]\]\s*/i, '').trim());
110
- if (checkboxCriteria.length > 0) {
111
- return checkboxCriteria;
103
+ if (options.allowLegacyBodyFallback !== false) {
104
+ const checkboxCriteria = body
105
+ .split('\n')
106
+ .map((line) => line.trim())
107
+ .filter((line) => /^-\s*\[[ x]\]/i.test(line))
108
+ .map((line) => line.replace(/^-\s*\[[ x]\]\s*/i, '').trim());
109
+ if (checkboxCriteria.length > 0) {
110
+ return checkboxCriteria;
111
+ }
112
+ return extractBddBlocks(body);
112
113
  }
113
- return extractBddBlocks(body);
114
+ return [];
114
115
  }
115
116
  function buildCriterion(text, source, index) {
116
117
  const trimmed = text.trim();
@@ -4,7 +4,25 @@
4
4
  * Returns the remaining prose only, preserving line structure.
5
5
  */
6
6
  export function stripFencedBlocks(md) {
7
- // Match ``` or ~~~ fences (with optional language specifier) spanning multiple lines
8
- return md.replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1\s*$/gm, '');
7
+ const lines = md.split('\n');
8
+ let openFence;
9
+ return lines
10
+ .map((line) => {
11
+ if (openFence !== undefined) {
12
+ const close = new RegExp(`^ {0,3}${openFence.marker}{${String(openFence.length)},}[ \\t]*\\r?$`);
13
+ if (close.test(line)) {
14
+ openFence = undefined;
15
+ }
16
+ return '';
17
+ }
18
+ const opening = /^ {0,3}(`{3,}|~{3,})[^\r\n]*\r?$/.exec(line);
19
+ if (opening?.[1]) {
20
+ const marker = opening[1][0];
21
+ openFence = { marker, length: opening[1].length };
22
+ return '';
23
+ }
24
+ return line;
25
+ })
26
+ .join('\n');
9
27
  }
10
28
  //# sourceMappingURL=text-fences.js.map
@@ -1,4 +1,5 @@
1
1
  import type { EnglishOnlyArtifactKind, EnglishOnlyValidationResult } from '../../types/spec-language.js';
2
+ import type { EnglishOnlyGateResolution } from '../../types/spec-language-translation.js';
2
3
  /**
3
4
  * Validate that a Planu-owned persisted artifact is authored in English.
4
5
  *
@@ -12,5 +13,18 @@ export declare function validateEnglishOnlyArtifactText(text: string, kind?: Eng
12
13
  * Backward-compatible spec-specific entrypoint used by create_spec/update_status gates.
13
14
  */
14
15
  export declare function validateEnglishOnlySpecText(text: string): EnglishOnlyValidationResult;
16
+ /** clarificationAnswers key carrying the LLM-provided English translation of the spec title. */
17
+ export declare const TRANSLATED_TITLE_ANSWER_KEY = "Title-EN";
18
+ /** clarificationAnswers key carrying the LLM-provided English translation of the description. */
19
+ export declare const TRANSLATED_DESCRIPTION_ANSWER_KEY = "Desc-EN";
20
+ /**
21
+ * SPEC-1342: Resolve the English-only gate for create_spec's title/description.
22
+ *
23
+ * Instead of hard-rejecting non-English input, this offers one clarification round:
24
+ * the calling LLM is asked (via the existing clarificationAnswers mechanism) to supply
25
+ * the English translation inline. Only rejects if that translation is missing or itself
26
+ * still non-English after the round.
27
+ */
28
+ export declare function resolveEnglishOnlySpecGate(title: string, description: string, clarificationAnswers?: Record<string, string>): EnglishOnlyGateResolution;
15
29
  export declare function assertEnglishOnlyArtifactText(text: string, kind: EnglishOnlyArtifactKind): void;
16
30
  //# sourceMappingURL=english-only.d.ts.map
@@ -119,6 +119,64 @@ export function validateEnglishOnlyArtifactText(text, kind = 'spec') {
119
119
  export function validateEnglishOnlySpecText(text) {
120
120
  return validateEnglishOnlyArtifactText(text, 'spec');
121
121
  }
122
+ /** clarificationAnswers key carrying the LLM-provided English translation of the spec title. */
123
+ export const TRANSLATED_TITLE_ANSWER_KEY = 'Title-EN';
124
+ /** clarificationAnswers key carrying the LLM-provided English translation of the description. */
125
+ export const TRANSLATED_DESCRIPTION_ANSWER_KEY = 'Desc-EN';
126
+ /**
127
+ * SPEC-1342: Resolve the English-only gate for create_spec's title/description.
128
+ *
129
+ * Instead of hard-rejecting non-English input, this offers one clarification round:
130
+ * the calling LLM is asked (via the existing clarificationAnswers mechanism) to supply
131
+ * the English translation inline. Only rejects if that translation is missing or itself
132
+ * still non-English after the round.
133
+ */
134
+ export function resolveEnglishOnlySpecGate(title, description, clarificationAnswers) {
135
+ const validation = validateEnglishOnlySpecText(`${title}\n\n${description}`);
136
+ if (validation.ok) {
137
+ return { action: 'ok' };
138
+ }
139
+ const originalLanguage = validation.detectedLanguage === 'pt' ? 'pt' : 'es';
140
+ const titleAnswer = clarificationAnswers?.[TRANSLATED_TITLE_ANSWER_KEY]?.trim();
141
+ const descriptionAnswer = clarificationAnswers?.[TRANSLATED_DESCRIPTION_ANSWER_KEY]?.trim();
142
+ if (!titleAnswer || !descriptionAnswer) {
143
+ return {
144
+ action: 'ask',
145
+ originalLanguage,
146
+ questions: buildTranslationRequestQuestions(title),
147
+ };
148
+ }
149
+ const translatedValidation = validateEnglishOnlySpecText(`${titleAnswer}\n\n${descriptionAnswer}`);
150
+ if (!translatedValidation.ok) {
151
+ return { action: 'reject', originalLanguage, reason: validation.reason };
152
+ }
153
+ return {
154
+ action: 'translated',
155
+ originalLanguage,
156
+ englishTitle: titleAnswer,
157
+ englishDescription: descriptionAnswer,
158
+ };
159
+ }
160
+ function buildTranslationRequestQuestions(title) {
161
+ return [
162
+ {
163
+ question: `The spec title "${title}" is not in English. What is the English translation of the title?`,
164
+ header: TRANSLATED_TITLE_ANSWER_KEY,
165
+ options: [
166
+ { label: 'Provide translation', description: 'Type the English translation as free text.' },
167
+ ],
168
+ multiSelect: false,
169
+ },
170
+ {
171
+ question: 'What is the English translation of the spec description?',
172
+ header: TRANSLATED_DESCRIPTION_ANSWER_KEY,
173
+ options: [
174
+ { label: 'Provide translation', description: 'Type the English translation as free text.' },
175
+ ],
176
+ multiSelect: false,
177
+ },
178
+ ];
179
+ }
122
180
  export function assertEnglishOnlyArtifactText(text, kind) {
123
181
  const validation = validateEnglishOnlyArtifactText(text, kind);
124
182
  if (!validation.ok) {
@@ -2,7 +2,7 @@
2
2
  import { readFile, readdir, stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { checkCriteriaQuality } from '../criteria-quality-checker.js';
5
- import { isNativeActive, fastScanSpecs } from '../core-bridge.js';
5
+ import { fastScanSpecsAsync } from '../core-bridge.js';
6
6
  const SPEC_DIR_RE = /^SPEC-\d+/;
7
7
  /** Extract YAML frontmatter content (between first and second ---). */
8
8
  function extractFrontmatterAndBody(content) {
@@ -43,18 +43,16 @@ function extractField(frontmatter, field) {
43
43
  * Returns an array of ambiguous criteria with context for LLM rewriting.
44
44
  */
45
45
  export async function scanForAmbiguousCriteria(projectPath) {
46
- if (isNativeActive()) {
47
- const briefs = fastScanSpecs(projectPath);
48
- if (briefs) {
49
- return briefs.flatMap((b) => b.ambiguousCriteria.map((a) => ({
50
- specId: a.specId,
51
- specTitle: a.specTitle,
52
- specContext: '', // Native scanner doesn't return full body context yet
53
- criterionText: a.criterionText,
54
- score: a.score,
55
- reason: a.reason,
56
- })));
57
- }
46
+ const { value: briefs } = await fastScanSpecsAsync(projectPath);
47
+ if (briefs) {
48
+ return briefs.flatMap((b) => b.ambiguousCriteria.map((a) => ({
49
+ specId: a.specId,
50
+ specTitle: a.specTitle,
51
+ specContext: '', // Native scanner doesn't return full body context yet
52
+ criterionText: a.criterionText,
53
+ score: a.score,
54
+ reason: a.reason,
55
+ })));
58
56
  }
59
57
  const ambiguous = [];
60
58
  const specsDir = join(projectPath, 'planu/specs');
@@ -2,7 +2,7 @@
2
2
  import { readdir, stat, access, readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { isOldFormat } from './lean-migration.js';
5
- import { isNativeActive, fastScanSpecs } from '../core-bridge.js';
5
+ import { fastScanSpecsAsync } from '../core-bridge.js';
6
6
  import { isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalSpecFile, } from './planu-canonical-policy.js';
7
7
  async function scanPlanuRoot(projectPath) {
8
8
  const rootFiles = [];
@@ -82,22 +82,20 @@ export async function detectDrift(projectPath) {
82
82
  const specsDir = join(projectPath, 'planu', 'specs');
83
83
  const unifiedSpecs = [];
84
84
  const leanSpecs = [];
85
- if (isNativeActive()) {
86
- const briefs = fastScanSpecs(projectPath);
87
- if (briefs) {
88
- for (const b of briefs) {
89
- if (b.needsUnified) {
90
- unifiedSpecs.push(b.id);
91
- }
92
- if (b.needsLean) {
93
- leanSpecs.push(b.id);
94
- }
85
+ const { value: briefs } = await fastScanSpecsAsync(projectPath);
86
+ if (briefs) {
87
+ for (const b of briefs) {
88
+ if (b.needsUnified) {
89
+ unifiedSpecs.push(b.id);
90
+ }
91
+ if (b.needsLean) {
92
+ leanSpecs.push(b.id);
95
93
  }
96
94
  }
97
95
  }
98
96
  const rootFiles = await scanPlanuRoot(projectPath);
99
97
  const specDirEntries = await readdir(specsDir).catch(() => []);
100
- if (isNativeActive()) {
98
+ if (briefs) {
101
99
  for (const entry of specDirEntries) {
102
100
  const specPath = join(specsDir, entry);
103
101
  try {
@@ -72,7 +72,7 @@ async function processSingleSpec(filePath, dataStatusMap, report) {
72
72
  * The caller (tool layer) is responsible for persisting updates to storage.
73
73
  */
74
74
  export async function detectSpecStateDivergences(projectPath, dataEntries) {
75
- const report = { synced: 0, divergences: [], errors: [], rejected: [] };
75
+ const report = { synced: 0, divergences: [], errors: [], rejected: [], ghosts: [] };
76
76
  const updates = [];
77
77
  const dataStatusMap = new Map(dataEntries.map((s) => [s.id, s.status]));
78
78
  let specFiles;
@@ -107,7 +107,11 @@ export async function withTotalBudget(operationName, ceilingMs, fn) {
107
107
  timedOut: false,
108
108
  category: 'operation_failed',
109
109
  elapsedMs,
110
- warning: `[SPEC-713] Operation "${operationName}" failed after ${String(elapsedMs)}ms (operation_failed)`,
110
+ // SPEC-1351: keep the thrown error's message — the production bug this
111
+ // fixed surfaced only as an opaque "operation_failed" with no way to
112
+ // tell what actually broke — but never leak the raw call stack into
113
+ // this user-facing warning string.
114
+ warning: `[SPEC-713] Operation "${operationName}" failed after ${String(elapsedMs)}ms (operation_failed): ${outcome.error.message}`,
111
115
  };
112
116
  }
113
117
  return {
@@ -1,4 +1,6 @@
1
- import type { TFIDFConfig } from '../../types/index.js';
1
+ import type { TFIDFConfig, TFIDFExportedState } from '../../types/index.js';
2
+ /** Bumped when the exported state shape changes; persisted stores rebuild on mismatch. */
3
+ export declare const TFIDF_SCHEMA_VERSION = 1;
2
4
  /** Tokenize text: split on whitespace/punctuation, split camelCase, lowercase, remove stop words. */
3
5
  export declare function tokenize(text: string, stopWords?: Set<string>): string[];
4
6
  /** TF-IDF engine: stateful, maintains vocabulary (IDF) across documents. */
@@ -22,14 +24,16 @@ export declare class TFIDFEngine {
22
24
  /** Get document count in corpus. */
23
25
  getDocumentCount(): number;
24
26
  /** Export state for serialization. */
25
- exportState(): {
26
- docFreq: [string, number][];
27
- docCount: number;
28
- };
27
+ exportState(): TFIDFExportedState;
29
28
  /** Import state from serialized form. */
30
- importState(state: {
31
- docFreq: [string, number][];
32
- docCount: number;
33
- }): void;
29
+ importState(state: TFIDFExportedState): void;
30
+ /**
31
+ * Remove a document's contribution to the corpus (SPEC-1345: incremental
32
+ * updates). `tokens` must be the unique token set the document contributed
33
+ * when it was added via `addDocument` — callers persisting the index keep
34
+ * this alongside the content hash so a changed document can be decremented
35
+ * before its new version is added back in.
36
+ */
37
+ removeDocument(tokens: string[]): void;
34
38
  }
35
39
  //# sourceMappingURL=tfidf.d.ts.map
@@ -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), TIER (string)
39
+ * Known settings: ENABLED (bool), COOLDOWNMS (number), PRIORITY (number)
40
40
  */
41
41
  parseEnvOverride(workerName: string): WorkerOverride;
42
42
  /**