@entelligentsia/forgecli 0.8.4 → 0.9.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 (169) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/bin/argv.d.ts +2 -2
  3. package/dist/bin/argv.js +17 -0
  4. package/dist/bin/argv.js.map +1 -1
  5. package/dist/bin/config.d.ts +69 -0
  6. package/dist/bin/config.js +315 -0
  7. package/dist/bin/config.js.map +1 -0
  8. package/dist/bin/doctor.d.ts +1 -0
  9. package/dist/bin/doctor.js +12 -0
  10. package/dist/bin/doctor.js.map +1 -1
  11. package/dist/bin/forge.js +7 -0
  12. package/dist/bin/forge.js.map +1 -1
  13. package/dist/extensions/forgecli/config-command.d.ts +8 -0
  14. package/dist/extensions/forgecli/config-command.js +66 -0
  15. package/dist/extensions/forgecli/config-command.js.map +1 -0
  16. package/dist/extensions/forgecli/config-layer.d.ts +38 -0
  17. package/dist/extensions/forgecli/config-layer.js +68 -0
  18. package/dist/extensions/forgecli/config-layer.js.map +1 -0
  19. package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
  20. package/dist/extensions/forgecli/config-tui/component.js +236 -0
  21. package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
  22. package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
  23. package/dist/extensions/forgecli/config-tui/handler.js +240 -0
  24. package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
  25. package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
  26. package/dist/extensions/forgecli/config-tui/index.js +5 -0
  27. package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
  28. package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
  29. package/dist/extensions/forgecli/config-tui/keys.js +33 -0
  30. package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
  31. package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
  32. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
  33. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
  34. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
  35. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
  36. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
  37. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
  38. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
  39. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
  40. package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
  41. package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
  42. package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
  43. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
  44. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
  45. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
  46. package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
  47. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
  48. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
  49. package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
  50. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
  51. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
  52. package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
  53. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
  54. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
  55. package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
  56. package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
  57. package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
  58. package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
  59. package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
  60. package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
  61. package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
  62. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
  63. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
  64. package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
  65. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
  66. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
  67. package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
  68. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
  69. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
  70. package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
  71. package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
  72. package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
  73. package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
  74. package/dist/extensions/forgecli/config-tui/screens.js +78 -0
  75. package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
  76. package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
  77. package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
  78. package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
  79. package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
  80. package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
  81. package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
  82. package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
  83. package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
  84. package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
  85. package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
  86. package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
  87. package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
  88. package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
  89. package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
  90. package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
  91. package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
  92. package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
  93. package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
  94. package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
  95. package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
  96. package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
  97. package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
  98. package/dist/extensions/forgecli/config-tui/state.js +11 -0
  99. package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
  100. package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
  101. package/dist/extensions/forgecli/config-tui/theme.js +88 -0
  102. package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
  103. package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
  104. package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
  105. package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
  106. package/dist/extensions/forgecli/config-writer.d.ts +16 -0
  107. package/dist/extensions/forgecli/config-writer.js +63 -0
  108. package/dist/extensions/forgecli/config-writer.js.map +1 -0
  109. package/dist/extensions/forgecli/fix-bug.js +85 -1
  110. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  111. package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
  112. package/dist/extensions/forgecli/forge-commands.js +3 -8
  113. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  114. package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
  115. package/dist/extensions/forgecli/forge-subagent.js +19 -0
  116. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  117. package/dist/extensions/forgecli/index.js +16 -0
  118. package/dist/extensions/forgecli/index.js.map +1 -1
  119. package/dist/extensions/forgecli/input-router.d.ts +33 -0
  120. package/dist/extensions/forgecli/input-router.js +133 -0
  121. package/dist/extensions/forgecli/input-router.js.map +1 -0
  122. package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
  123. package/dist/extensions/forgecli/model-resolver.js +65 -0
  124. package/dist/extensions/forgecli/model-resolver.js.map +1 -0
  125. package/dist/extensions/forgecli/model-validator.d.ts +29 -0
  126. package/dist/extensions/forgecli/model-validator.js +107 -0
  127. package/dist/extensions/forgecli/model-validator.js.map +1 -0
  128. package/dist/extensions/forgecli/run-sprint.js +59 -0
  129. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  130. package/dist/extensions/forgecli/run-task.js +93 -1
  131. package/dist/extensions/forgecli/run-task.js.map +1 -1
  132. package/dist/extensions/forgecli/thread-switcher.js +5 -2
  133. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  134. package/dist/extensions/forgecli/whats-new-widget.js +5 -2
  135. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  136. package/package.json +11 -3
  137. package/dist/extensions/forgecli/review-command.d.ts +0 -2
  138. package/dist/extensions/forgecli/review-command.js +0 -184
  139. package/dist/extensions/forgecli/review-command.js.map +0 -1
  140. package/dist/forge-payload/.tools/banners.cjs +0 -435
  141. package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
  142. package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
  143. package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
  144. package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
  145. package/dist/forge-payload/.tools/collate.cjs +0 -1041
  146. package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
  147. package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
  148. package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
  149. package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
  150. package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
  151. package/dist/forge-payload/.tools/lib/result.js +0 -40
  152. package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
  153. package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
  154. package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
  155. package/dist/forge-payload/.tools/lib/validate.js +0 -141
  156. package/dist/forge-payload/.tools/manage-config.cjs +0 -340
  157. package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
  158. package/dist/forge-payload/.tools/package.json +0 -3
  159. package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
  160. package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
  161. package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
  162. package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
  163. package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
  164. package/dist/forge-payload/.tools/seed-store.cjs +0 -237
  165. package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
  166. package/dist/forge-payload/.tools/store-query.cjs +0 -319
  167. package/dist/forge-payload/.tools/store.cjs +0 -315
  168. package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
  169. package/dist/forge-payload/.tools/validate-store.cjs +0 -593
@@ -1,162 +0,0 @@
1
- 'use strict';
2
- // store-facade.cjs — StoreFacade, extractExcerpt, loadForgeConfig
3
- // Used by store-query.cjs. No dependency on store.cjs or schemas.
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
-
8
- class StoreFacade {
9
- constructor(storeDir) {
10
- this.storeDir = storeDir;
11
- }
12
-
13
- _loadDir(dir) {
14
- const full = path.join(this.storeDir, dir);
15
- if (!fs.existsSync(full)) return [];
16
- return fs.readdirSync(full)
17
- .filter(f => f.endsWith('.json'))
18
- .map(f => {
19
- try { return JSON.parse(fs.readFileSync(path.join(full, f), 'utf8')); }
20
- catch { return null; }
21
- })
22
- .filter(Boolean);
23
- }
24
-
25
- listSprints(filter = {}) {
26
- return this._filterEntities(this._loadDir('sprints'), filter);
27
- }
28
-
29
- listTasks(filter = {}) {
30
- return this._filterEntities(
31
- this._loadDir('tasks').filter(e => e.taskId && !e.taskId.includes('BUG')),
32
- filter
33
- );
34
- }
35
-
36
- listBugs(filter = {}) {
37
- return this._filterEntities(this._loadDir('bugs'), filter);
38
- }
39
-
40
- listFeatures(filter = {}) {
41
- return this._filterEntities(this._loadDir('features'), filter);
42
- }
43
-
44
- getEntity(type, id) {
45
- const dir = { tasks: 'tasks', bugs: 'bugs', sprints: 'sprints', features: 'features' }[type];
46
- if (!dir) return null;
47
- const filePath = path.join(this.storeDir, dir, `${id}.json`);
48
- if (!fs.existsSync(filePath)) return null;
49
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
50
- catch { return null; }
51
- }
52
-
53
- followFK(entity, fkField) {
54
- const val = entity[fkField];
55
- if (!val) return null;
56
- const fkMap = {
57
- sprintId: 'sprints',
58
- featureId: 'features',
59
- blockedBy: 'bugs',
60
- blocksTask: 'tasks',
61
- taskId: 'tasks',
62
- bugId: 'bugs',
63
- };
64
- const targetType = fkMap[fkField];
65
- if (!targetType) return null;
66
- if (Array.isArray(val)) {
67
- return val.map(v => this.getEntity(targetType, v)).filter(Boolean);
68
- }
69
- return this.getEntity(targetType, val);
70
- }
71
-
72
- _filterEntities(entities, filter) {
73
- return entities.filter(e => {
74
- for (const [key, val] of Object.entries(filter)) {
75
- if (Array.isArray(val)) {
76
- if (!val.includes(e[key])) return false;
77
- } else if (e[key] !== val) {
78
- return false;
79
- }
80
- }
81
- return true;
82
- });
83
- }
84
- }
85
-
86
- function extractExcerpt(indexPath, maxSentences = 4) {
87
- if (!indexPath || !fs.existsSync(indexPath)) return null;
88
- try {
89
- const content = fs.readFileSync(indexPath, 'utf8');
90
- const body = content.replace(/^---[\s\S]*?---\n*/, '');
91
- const lines = body.split('\n')
92
- .map(l => l.trim())
93
- .filter(l =>
94
- l &&
95
- !l.startsWith('#') &&
96
- !l.startsWith('<!--') &&
97
- !l.startsWith('|') &&
98
- !l.startsWith('---') &&
99
- !l.startsWith('**Status**') &&
100
- !l.startsWith('**Sprint**') &&
101
- !l.startsWith('[Back to')
102
- );
103
- const text = lines.join(' ');
104
- const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
105
- return sentences.slice(0, maxSentences).join(' ').trim() || null;
106
- } catch {
107
- return null;
108
- }
109
- }
110
-
111
- let _cfgCache = null;
112
- function loadForgeConfig(cwd) {
113
- if (_cfgCache) return _cfgCache;
114
- const root = cwd || process.cwd();
115
- const configPath = path.join(root, '.forge', 'config.json');
116
- let cfg = {};
117
- if (fs.existsSync(configPath)) {
118
- try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
119
- }
120
- const prefix = cfg.project?.prefix || 'WI';
121
- const kbRel = cfg.paths?.engineering || 'engineering';
122
- const storeRel = cfg.paths?.store || '.forge/store';
123
- _cfgCache = {
124
- prefix,
125
- kbPath: fs.existsSync(path.join(root, kbRel)) ? kbRel : (fs.existsSync(path.join(root, 'engineering')) ? 'engineering' : null),
126
- storePathRel: storeRel,
127
- storePathAbs: path.join(root, storeRel),
128
- projectName: cfg.project?.name || null,
129
- };
130
- return _cfgCache;
131
- }
132
-
133
- function resetConfigCache() {
134
- _cfgCache = null;
135
- }
136
-
137
- function findIndexPath(entity, kbPath) {
138
- if (entity.path) {
139
- const p = entity.path.replace(/\/$/, '');
140
- return `${p}/INDEX.md`;
141
- }
142
- if (!kbPath) return null;
143
- if (entity.sprintId && !entity.taskId && !entity.bugId) {
144
- return path.join(kbPath, 'sprints', entity.sprintId, 'INDEX.md');
145
- }
146
- if (entity.taskId) {
147
- const match = entity.taskId.match(/-S(\d+)-/);
148
- if (match) {
149
- const taskNum = entity.taskId.split('-').pop().replace('T', 'task_');
150
- return path.join(kbPath, 'sprints', `S${match[1]}`, 'tasks', taskNum, 'INDEX.md');
151
- }
152
- }
153
- if (entity.bugId) {
154
- const slug = entity.title
155
- ? entity.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '')
156
- : entity.bugId;
157
- return path.join(kbPath, 'bugs', `${entity.bugId}-${slug}`, 'INDEX.md');
158
- }
159
- return null;
160
- }
161
-
162
- module.exports = { StoreFacade, extractExcerpt, loadForgeConfig, resetConfigCache, findIndexPath };
@@ -1,250 +0,0 @@
1
- 'use strict';
2
- // store-nlp.cjs — deterministic rule-based NLP intent parser
3
- // No LLM, no network. Maps natural language to a traversal plan.
4
-
5
- const { loadForgeConfig } = require('./store-facade.cjs');
6
-
7
- const ENTITY_SYNONYMS = {
8
- sprints: ['sprint', 'sprints', 'release', 'releases', 'iteration', 'iterations'],
9
- tasks: ['task', 'tasks', 'item', 'items', 'work item', 'work items', 'todo', 'todos'],
10
- bugs: ['bug', 'bugs', 'defect', 'defects', 'issue', 'issues', 'problem', 'problems'],
11
- features: ['feature', 'features', 'epic', 'epics', 'capability', 'capabilities'],
12
- };
13
-
14
- const STATUS_MAP = {
15
- 'open': { tasks: 'planned', bugs: 'in-progress', sprints: 'active', features: 'active' },
16
- 'active': { tasks: 'implementing', bugs: 'in-progress', sprints: 'active', features: 'active' },
17
- 'in progress': { tasks: 'implementing', bugs: 'in-progress', sprints: 'active', features: 'active' },
18
- 'in-progress': { tasks: 'implementing', bugs: 'in-progress', sprints: 'active', features: 'active' },
19
- 'completed': { tasks: 'committed', bugs: 'fixed', sprints: 'completed', features: 'shipped' },
20
- 'done': { tasks: 'committed', bugs: 'fixed', sprints: 'completed', features: 'shipped' },
21
- 'fixed': { bugs: 'fixed' },
22
- 'planned': { tasks: 'planned', sprints: 'planning' },
23
- 'planning': { sprints: 'planning' },
24
- 'implementing': { tasks: 'implementing' },
25
- 'implemented': { tasks: 'implemented' },
26
- 'committed': { tasks: 'committed' },
27
- 'draft': { tasks: 'draft', features: 'draft' },
28
- 'abandoned': { tasks: 'abandoned', sprints: 'abandoned' },
29
- 'retired': { features: 'retired' },
30
- 'shipped': { features: 'shipped' },
31
- 'triaged': { bugs: 'triaged' },
32
- 'reported': { bugs: 'reported' },
33
- 'blocked': { tasks: 'blocked' },
34
- 'critical': { _field: 'severity', bugs: 'critical' },
35
- 'major': { _field: 'severity', bugs: 'major' },
36
- 'minor': { _field: 'severity', bugs: 'minor' },
37
- };
38
-
39
- const ALL_STOP_WORDS = new Set([
40
- 'list','all','the','show','find','what','which','are','in','for','about','related','to',
41
- 'of','and','with','details','status','how','many','there','a','an','is','that','this','on',
42
- 'by','me','give','get','tell','please','can','do','does','did','was','were','been','being',
43
- 'have','has','had','will','would','could','should','may','might',
44
- 'blocking','blocked','block','severity','titles','title',
45
- ...Object.values(ENTITY_SYNONYMS).flat(),
46
- ]);
47
-
48
- function idRegexes() {
49
- const p = loadForgeConfig().prefix;
50
- const esc = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
- return {
52
- taskId: new RegExp(`\\b${esc}-S\\d+-T\\d+\\b`, 'i'),
53
- bugId: new RegExp(`\\b${esc}-BUG-\\d+\\b`, 'i'),
54
- taskIdAnchored: new RegExp(`^${esc}-S\\d+-T\\d+$`),
55
- bugIdAnchored: new RegExp(`^${esc}-BUG-\\d+$`),
56
- };
57
- }
58
-
59
- function parseIntentNLP(intent) {
60
- const stripped = String(intent).replace(/^\s*forge[-_ ]?store\s*:?\s*/i, '');
61
- const lower = stripped.toLowerCase().replace(/[^\w\s-]/g, ' ').replace(/\s+/g, ' ').trim();
62
- const plan = {
63
- traverse: {
64
- primary: null,
65
- filter: {},
66
- follow: [],
67
- keywordMatch: { field: 'title', terms: [] },
68
- },
69
- };
70
-
71
- const consumed = new Set();
72
- const _idRe = idRegexes();
73
-
74
- // ── Stage 1: ID patterns ──
75
- const idPatterns = [
76
- { re: _idRe.taskId, filter: 'taskId', entity: 'tasks' },
77
- { re: _idRe.bugId, filter: 'bugId', entity: 'bugs' },
78
- { re: /\bFEAT-\d+\b/i, filter: 'featureId', entity: null },
79
- { re: /\bS\d+\b/i, filter: 'sprintId', entity: null },
80
- { re: /sprint\s+(\d+)/i, filter: 'sprintId', entity: null, format: v => 'S' + v },
81
- ];
82
- let idEntityHint = null;
83
- for (const { re, filter: filterKey, entity, format } of idPatterns) {
84
- const m = lower.match(re);
85
- if (m) {
86
- const value = format ? format(m[1]) : m[0].toUpperCase();
87
- plan.traverse.filter[filterKey] = value;
88
- if (entity) idEntityHint = entity;
89
- const words = lower.split(/\s+/);
90
- const matchText = m[0].toLowerCase();
91
- words.forEach((w, i) => { if (matchText.includes(w)) consumed.add(i); });
92
- break;
93
- }
94
- }
95
-
96
- // ── Stage 2: Entity detection ──
97
- const words = lower.split(/\s+/);
98
- let detectedEntity = null;
99
- outer:
100
- for (let i = 0; i < words.length; i++) {
101
- if (consumed.has(i)) continue;
102
- const w = words[i];
103
- if (i + 1 < words.length) {
104
- const bigram = w + ' ' + words[i + 1];
105
- for (const [entity, synonyms] of Object.entries(ENTITY_SYNONYMS)) {
106
- if (synonyms.includes(bigram)) {
107
- detectedEntity = entity;
108
- consumed.add(i);
109
- consumed.add(i + 1);
110
- break outer;
111
- }
112
- }
113
- }
114
- for (const [entity, synonyms] of Object.entries(ENTITY_SYNONYMS)) {
115
- if (synonyms.includes(w)) {
116
- detectedEntity = entity;
117
- consumed.add(i);
118
- break outer;
119
- }
120
- }
121
- }
122
-
123
- if (!detectedEntity && !idEntityHint && plan.traverse.filter.sprintId) {
124
- plan.traverse.primary = 'sprints';
125
- } else {
126
- plan.traverse.primary = detectedEntity || idEntityHint || 'tasks';
127
- }
128
-
129
- // ── Stage 3: Status/severity filters ──
130
- for (let i = 0; i < words.length; i++) {
131
- if (consumed.has(i)) continue;
132
- if (i + 1 < words.length) {
133
- const bigram = words[i] + ' ' + words[i + 1];
134
- if (STATUS_MAP[bigram]) {
135
- const mapping = STATUS_MAP[bigram];
136
- const field = mapping._field || 'status';
137
- const value = mapping[plan.traverse.primary];
138
- if (value) {
139
- plan.traverse.filter[field] = value;
140
- consumed.add(i);
141
- consumed.add(i + 1);
142
- continue;
143
- }
144
- }
145
- }
146
- const w = words[i];
147
- if (STATUS_MAP[w]) {
148
- const mapping = STATUS_MAP[w];
149
- const field = mapping._field || 'status';
150
- const value = mapping[plan.traverse.primary];
151
- if (value) {
152
- plan.traverse.filter[field] = value;
153
- consumed.add(i);
154
- }
155
- }
156
- }
157
-
158
- // ── Stage 4: FK follow phrases ──
159
- if (/\bwith\s+sprints?\b/.test(lower) || /\bsprint\s+for\b/.test(lower) || /\bwhich sprint\b/.test(lower)) {
160
- if (!plan.traverse.follow.includes('sprintId')) plan.traverse.follow.push('sprintId');
161
- }
162
- if (/\bwith\s+features?\b/.test(lower) || /\bfeature\s+for\b/.test(lower)) {
163
- if (!plan.traverse.follow.includes('featureId')) plan.traverse.follow.push('featureId');
164
- }
165
- if (/\bblock/i.test(lower) || /\bblocking\b/.test(lower)) {
166
- if (plan.traverse.primary === 'bugs' && !plan.traverse.follow.includes('blockedBy')) {
167
- plan.traverse.follow.push('blockedBy');
168
- }
169
- if (plan.traverse.primary === 'tasks' && !plan.traverse.follow.includes('blocksTask')) {
170
- plan.traverse.follow.push('blocksTask');
171
- }
172
- }
173
-
174
- // ── Stage 4b: Ordering / limit / count ──
175
- let orderDir = null;
176
- let limitN = null;
177
- let countMode = false;
178
-
179
- const biMap = [
180
- { phrase: 'most recent', dir: 'desc', limit: 1 },
181
- { phrase: 'how many', count: true },
182
- { phrase: 'count of', count: true },
183
- { phrase: 'number of', count: true },
184
- ];
185
- for (let i = 0; i < words.length - 1; i++) {
186
- if (consumed.has(i) || consumed.has(i + 1)) continue;
187
- const bg = words[i] + ' ' + words[i + 1];
188
- const hit = biMap.find(b => b.phrase === bg);
189
- if (hit) {
190
- if (hit.dir) orderDir = orderDir || hit.dir;
191
- if (hit.limit && !limitN) limitN = hit.limit;
192
- if (hit.count) countMode = true;
193
- consumed.add(i); consumed.add(i + 1);
194
- }
195
- }
196
-
197
- for (let i = 0; i < words.length; i++) {
198
- if (consumed.has(i)) continue;
199
- const w = words[i];
200
- const next = words[i + 1];
201
- const isNum = next && /^\d+$/.test(next);
202
- if ((w === 'top' || w === 'first' || w === 'last') && isNum) {
203
- limitN = parseInt(next, 10);
204
- if (w === 'last') orderDir = orderDir || 'desc';
205
- else orderDir = orderDir || (w === 'top' ? 'desc' : 'asc');
206
- consumed.add(i); consumed.add(i + 1);
207
- continue;
208
- }
209
- if (w === 'latest' || w === 'newest' || w === 'recent' || w === 'last') {
210
- orderDir = orderDir || 'desc';
211
- if (!limitN) limitN = 1;
212
- consumed.add(i);
213
- } else if (w === 'oldest' || w === 'earliest' || w === 'first') {
214
- orderDir = orderDir || 'asc';
215
- if (!limitN) limitN = 1;
216
- consumed.add(i);
217
- } else if (w === 'count') {
218
- countMode = true;
219
- consumed.add(i);
220
- }
221
- }
222
-
223
- if (orderDir) plan.traverse.sort = orderDir;
224
- if (limitN) plan.traverse.limit = limitN;
225
- if (countMode) plan.traverse.count = true;
226
-
227
- // ── Stage 5: Keyword extraction ──
228
- const keywords = words.filter(
229
- (w, i) => !consumed.has(i) && w.length > 1 && !ALL_STOP_WORDS.has(w) && !/^\d+$/.test(w)
230
- );
231
- plan.traverse.keywordMatch.terms = [...new Set(keywords)];
232
-
233
- return plan;
234
- }
235
-
236
- function extractKeywordsFromIntent(intent) {
237
- const stopWords = new Set([
238
- 'list','all','the','show','find','what','which','are','in','for','about','related','to',
239
- 'of','and','with','sprints','tasks','bugs','features','details','status','open','closed',
240
- 'active','completed','how','many','there','a','an','is','that','this','on','by','me',
241
- 'give','get','tell','please','can','do','does','did','was','were','been','being',
242
- 'have','has','had','will','would','could','should','may','might',
243
- ]);
244
- return intent.toLowerCase()
245
- .replace(/[^a-z0-9\s-]/g, ' ')
246
- .split(/[\s-]+/)
247
- .filter(w => w.length > 2 && !stopWords.has(w));
248
- }
249
-
250
- module.exports = { parseIntentNLP, extractKeywordsFromIntent, ENTITY_SYNONYMS, STATUS_MAP };
@@ -1,272 +0,0 @@
1
- 'use strict';
2
- // store-query-exec.cjs — query execution, result assembly, FK traversal
3
-
4
- const path = require('path');
5
- const { extractExcerpt, loadForgeConfig, findIndexPath } = require('./store-facade.cjs');
6
- const { extractKeywordsFromIntent } = require('./store-nlp.cjs');
7
-
8
- function escapeRe(s) {
9
- return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10
- }
11
-
12
- // Word-boundary match. Prevents "store" matching "restore".
13
- function kwMatches(text, term) {
14
- if (!term) return false;
15
- const re = new RegExp(`(?:^|[^a-z0-9])${escapeRe(term.toLowerCase())}(?:$|[^a-z0-9])`, 'i');
16
- return re.test(String(text || ''));
17
- }
18
-
19
- function sortKeyFor(entity, primary) {
20
- const tail = s => { const m = String(s || '').match(/(\d+)$/); return m ? parseInt(m[1], 10) : 0; };
21
- switch (primary) {
22
- case 'sprints': return tail(entity.sprintId);
23
- case 'tasks': return tail(entity.sprintId) * 10000 + tail(entity.taskId);
24
- case 'bugs': return tail(entity.bugId);
25
- case 'features': return tail(entity.featureId || entity.feature_id);
26
- }
27
- return 0;
28
- }
29
-
30
- function buildResult(entity, type, store, includeExcerpts) {
31
- const cfg = loadForgeConfig();
32
- const idField = type === 'sprint' ? 'sprintId'
33
- : type === 'task' ? 'taskId'
34
- : type === 'bug' ? 'bugId'
35
- : (entity.featureId ? 'featureId' : 'id');
36
- const id = entity[idField] || 'unknown';
37
-
38
- const result = {
39
- id,
40
- title: entity.title || entity.name || '',
41
- status: entity.status || '',
42
- type,
43
- relationships: {},
44
- };
45
-
46
- if (entity.sprintId) result.relationships.sprintId = entity.sprintId;
47
- if (entity.featureId || entity.feature_id) result.relationships.featureId = entity.featureId || entity.feature_id;
48
- if (entity.blockedBy) result.relationships.blockedBy = entity.blockedBy;
49
- if (entity.blocksTask) result.relationships.blocksTask = entity.blocksTask;
50
-
51
- const pluralType = type === 'bug' ? 'bugs' : type === 'task' ? 'tasks' : type === 'sprint' ? 'sprints' : 'features';
52
- const jsonPath = path.join(cfg.storePathRel, pluralType, `${id}.json`);
53
- const mdPath = findIndexPath(entity, cfg.kbPath) || '';
54
- result.fileRefs = { json: jsonPath, md: mdPath };
55
- result.storeRef = jsonPath;
56
- result.indexRef = mdPath;
57
- result.excerpt = includeExcerpts ? extractExcerpt(mdPath) : null;
58
-
59
- return result;
60
- }
61
-
62
- function buildFieldValidators() {
63
- const { loadForgeConfig: cfg } = require('./store-facade.cjs');
64
- const p = cfg().prefix;
65
- const esc = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
- return {
67
- sprints: {
68
- sprintId: /^S\d+$/,
69
- status: ['planning', 'active', 'completed', 'retrospective-done', 'blocked', 'partially-completed', 'abandoned'],
70
- },
71
- tasks: {
72
- taskId: new RegExp(`^${esc}-S\\d+-T\\d+$`),
73
- sprintId: /^S\d+$/,
74
- featureId: /^FEAT-\d+$/,
75
- status: ['draft', 'planned', 'plan-approved', 'implementing', 'implemented', 'review-approved', 'approved', 'committed', 'plan-revision-required', 'code-revision-required', 'blocked', 'escalated', 'abandoned'],
76
- },
77
- bugs: {
78
- bugId: new RegExp(`^${esc}-BUG-\\d+$`),
79
- sprintId: /^S\d+$/,
80
- severity: ['critical', 'major', 'minor'],
81
- status: ['reported', 'triaged', 'in-progress', 'fixed', 'verified'],
82
- },
83
- features: {
84
- featureId: /^FEAT-\d+$/,
85
- status: ['active', 'draft', 'shipped', 'retired'],
86
- },
87
- };
88
- }
89
-
90
- function validatePlan(plan) {
91
- const validators = buildFieldValidators();
92
- const warnings = [];
93
- let confidence = 'high';
94
- const traverse = plan.traverse || plan;
95
- const singMap = { bug: 'bugs', task: 'tasks', sprint: 'sprints', feature: 'features' };
96
- const primary = singMap[traverse.primary] || traverse.primary;
97
- const valid = new Set(['tasks', 'bugs', 'sprints', 'features', ...Object.keys(singMap)]);
98
-
99
- if (!valid.has(primary)) {
100
- warnings.push(`invalid primary "${traverse.primary}"`);
101
- confidence = 'low';
102
- }
103
-
104
- const fields = validators[primary];
105
- const filter = traverse.filter || {};
106
- if (fields) {
107
- for (const [key, val] of Object.entries(filter)) {
108
- if (!fields[key]) {
109
- warnings.push(`invalid filter key "${key}" for ${primary}`);
110
- confidence = 'low';
111
- } else if (fields[key] instanceof RegExp) {
112
- if (!fields[key].test(String(val))) {
113
- warnings.push(`filter ${key}="${val}" does not match pattern`);
114
- confidence = 'low';
115
- }
116
- } else if (Array.isArray(fields[key])) {
117
- if (!fields[key].includes(String(val))) {
118
- warnings.push(`filter ${key}="${val}" not in enum`);
119
- confidence = 'low';
120
- }
121
- }
122
- }
123
- }
124
- return { confidence, warnings };
125
- }
126
-
127
- function executeQuery(plan, store, cfg) {
128
- const trace = [];
129
- const traverse = plan.traverse || plan;
130
- const singMap = { bug: 'bugs', task: 'tasks', sprint: 'sprints', feature: 'features' };
131
- const primary = singMap[traverse.primary] || traverse.primary || 'tasks';
132
- let filter = { ...(traverse.filter || {}) };
133
- const follow = traverse.follow || [];
134
- const kwMatch = traverse.keywordMatch || {};
135
- const includeExcerpts = cfg.noExcerpts !== true;
136
-
137
- trace.push('intent parsed via NLP rules');
138
-
139
- // Validate + strip invalid filters
140
- const validation = validatePlan(plan);
141
- if (validation.warnings.length > 0) {
142
- const validators = buildFieldValidators();
143
- const fields = validators[primary];
144
- if (fields) {
145
- for (const key of Object.keys(filter)) {
146
- if (!fields[key]) {
147
- trace.push(`stripped invalid filter key: ${key}`);
148
- delete filter[key];
149
- } else {
150
- const spec = fields[key];
151
- if (spec instanceof RegExp && !spec.test(String(filter[key]))) {
152
- trace.push(`stripped filter ${key}="${filter[key]}" (value mismatch)`);
153
- delete filter[key];
154
- } else if (Array.isArray(spec) && !spec.includes(String(filter[key]))) {
155
- trace.push(`stripped filter ${key}="${filter[key]}" (not in enum)`);
156
- delete filter[key];
157
- }
158
- }
159
- }
160
- }
161
- }
162
-
163
- // List primary entities
164
- let entities;
165
- switch (primary) {
166
- case 'sprints': entities = store.listSprints(filter); break;
167
- case 'tasks': entities = store.listTasks(filter); break;
168
- case 'bugs': entities = store.listBugs(filter); break;
169
- case 'features': entities = store.listFeatures(filter); break;
170
- default: entities = [];
171
- }
172
-
173
- // Keyword filter
174
- const hasMeaningfulKw = kwMatch.field && kwMatch.terms && kwMatch.terms.length > 0
175
- && !kwMatch.terms.some(t => t.length > 20);
176
- if (hasMeaningfulKw) {
177
- const before = entities.length;
178
- entities = entities.filter(e => kwMatch.terms.some(t => kwMatches(e[kwMatch.field] || '', t)));
179
- trace.push(`keyword matched ${kwMatch.terms.join(', ')} on ${kwMatch.field}: ${before} → ${entities.length}`);
180
- } else if (Object.keys(filter).length > 0) {
181
- trace.push(`listed ${primary} with filter ${JSON.stringify(filter)}: ${entities.length} results`);
182
- } else {
183
- trace.push(`listed ${primary}: ${entities.length} results`);
184
- }
185
-
186
- const totalMatched = entities.length;
187
-
188
- // Sort
189
- if (traverse.sort) {
190
- entities.sort((a, b) => {
191
- const ka = sortKeyFor(a, primary);
192
- const kb = sortKeyFor(b, primary);
193
- return traverse.sort === 'desc' ? kb - ka : ka - kb;
194
- });
195
- trace.push(`sorted ${primary} ${traverse.sort}`);
196
- }
197
-
198
- // Count mode
199
- if (traverse.count) {
200
- trace.push(`count mode: ${totalMatched}`);
201
- return { query: cfg.query || '', path: 'intent-nlp', traversalTrace: trace, count: totalMatched, results: [], relatedFileRefs: [], totalMatched };
202
- }
203
-
204
- // Limit
205
- if (traverse.limit && entities.length > traverse.limit) {
206
- trace.push(`limited to ${traverse.limit} (of ${totalMatched})`);
207
- entities = entities.slice(0, traverse.limit);
208
- }
209
-
210
- // Build results + follow FKs
211
- const allResults = [];
212
- const relatedFiles = [];
213
- const singType = primary.replace(/s$/, '');
214
-
215
- for (const entity of entities) {
216
- allResults.push(buildResult(entity, singType, store, includeExcerpts));
217
- for (const fk of follow) {
218
- const related = store.followFK(entity, fk);
219
- if (related) {
220
- const relatedList = Array.isArray(related) ? related : [related];
221
- const fkSingType = fk.replace(/Id$/, '').replace(/s$/, '');
222
- for (const r of relatedList) {
223
- allResults.push(buildResult(r, fkSingType, store, includeExcerpts));
224
- }
225
- trace.push(`followed ${fk} → ${relatedList.length} related`);
226
- }
227
- }
228
- }
229
-
230
- for (const r of allResults) {
231
- if (r.fileRefs?.md) relatedFiles.push(r.fileRefs.md);
232
- if (r.fileRefs?.json) relatedFiles.push(r.fileRefs.json);
233
- }
234
-
235
- trace.push(`plan confidence: ${validation.confidence}`);
236
-
237
- // Auto-retry on zero results
238
- if (allResults.length === 0 && (Object.keys(filter).length > 0 || hasMeaningfulKw)) {
239
- trace.push('0 results — retrying with title-only keyword search');
240
- const retryTerms = extractKeywordsFromIntent(cfg.query || '');
241
- if (retryTerms.length > 0) {
242
- let retryEntities;
243
- switch (primary) {
244
- case 'sprints': retryEntities = store.listSprints({}); break;
245
- case 'tasks': retryEntities = store.listTasks({}); break;
246
- case 'bugs': retryEntities = store.listBugs({}); break;
247
- case 'features': retryEntities = store.listFeatures({}); break;
248
- default: retryEntities = [];
249
- }
250
- retryEntities = retryEntities.filter(e => retryTerms.some(t => kwMatches(e.title || '', t)));
251
- trace.push(`retry: keyword "${retryTerms.join(', ')}" in ${primary}: ${retryEntities.length} results`);
252
- for (const entity of retryEntities) {
253
- allResults.push(buildResult(entity, singType, store, includeExcerpts));
254
- }
255
- if (allResults.length > 0) trace.push('overall confidence: low (required retry)');
256
- }
257
- }
258
-
259
- return {
260
- query: cfg.query || '',
261
- path: 'intent-nlp',
262
- traversalTrace: trace,
263
- results: allResults,
264
- totalMatched,
265
- returned: allResults.length,
266
- limit: traverse.limit || null,
267
- sort: traverse.sort || null,
268
- relatedFileRefs: [...new Set(relatedFiles)],
269
- };
270
- }
271
-
272
- module.exports = { executeQuery, buildResult, kwMatches };