@entelligentsia/forgecli 0.8.4 → 0.9.1

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 (170) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +165 -2
  3. package/dist/bin/argv.d.ts +2 -2
  4. package/dist/bin/argv.js +17 -0
  5. package/dist/bin/argv.js.map +1 -1
  6. package/dist/bin/config.d.ts +69 -0
  7. package/dist/bin/config.js +315 -0
  8. package/dist/bin/config.js.map +1 -0
  9. package/dist/bin/doctor.d.ts +1 -0
  10. package/dist/bin/doctor.js +12 -0
  11. package/dist/bin/doctor.js.map +1 -1
  12. package/dist/bin/forge.js +7 -0
  13. package/dist/bin/forge.js.map +1 -1
  14. package/dist/extensions/forgecli/config-command.d.ts +8 -0
  15. package/dist/extensions/forgecli/config-command.js +66 -0
  16. package/dist/extensions/forgecli/config-command.js.map +1 -0
  17. package/dist/extensions/forgecli/config-layer.d.ts +38 -0
  18. package/dist/extensions/forgecli/config-layer.js +68 -0
  19. package/dist/extensions/forgecli/config-layer.js.map +1 -0
  20. package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
  21. package/dist/extensions/forgecli/config-tui/component.js +236 -0
  22. package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
  23. package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
  24. package/dist/extensions/forgecli/config-tui/handler.js +240 -0
  25. package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
  26. package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
  27. package/dist/extensions/forgecli/config-tui/index.js +5 -0
  28. package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
  29. package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
  30. package/dist/extensions/forgecli/config-tui/keys.js +33 -0
  31. package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
  32. package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
  33. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
  34. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
  35. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
  36. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
  37. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
  38. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
  39. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
  40. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
  41. package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
  42. package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
  43. package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
  44. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
  45. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
  46. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
  47. package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
  48. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
  49. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
  50. package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
  51. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
  52. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
  53. package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
  54. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
  55. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
  56. package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
  57. package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
  58. package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
  59. package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
  60. package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
  61. package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
  62. package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
  63. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
  64. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
  65. package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
  66. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
  67. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
  68. package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
  69. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
  70. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
  71. package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
  72. package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
  73. package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
  74. package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
  75. package/dist/extensions/forgecli/config-tui/screens.js +78 -0
  76. package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
  77. package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
  78. package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
  79. package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
  80. package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
  81. package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
  82. package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
  83. package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
  84. package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
  85. package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
  86. package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
  87. package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
  88. package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
  89. package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
  90. package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
  91. package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
  92. package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
  93. package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
  94. package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
  95. package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
  96. package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
  97. package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
  98. package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
  99. package/dist/extensions/forgecli/config-tui/state.js +11 -0
  100. package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
  101. package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
  102. package/dist/extensions/forgecli/config-tui/theme.js +88 -0
  103. package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
  104. package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
  105. package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
  106. package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
  107. package/dist/extensions/forgecli/config-writer.d.ts +16 -0
  108. package/dist/extensions/forgecli/config-writer.js +63 -0
  109. package/dist/extensions/forgecli/config-writer.js.map +1 -0
  110. package/dist/extensions/forgecli/fix-bug.js +85 -1
  111. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  112. package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
  113. package/dist/extensions/forgecli/forge-commands.js +3 -8
  114. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  115. package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
  116. package/dist/extensions/forgecli/forge-subagent.js +19 -0
  117. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  118. package/dist/extensions/forgecli/index.js +16 -0
  119. package/dist/extensions/forgecli/index.js.map +1 -1
  120. package/dist/extensions/forgecli/input-router.d.ts +33 -0
  121. package/dist/extensions/forgecli/input-router.js +133 -0
  122. package/dist/extensions/forgecli/input-router.js.map +1 -0
  123. package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
  124. package/dist/extensions/forgecli/model-resolver.js +65 -0
  125. package/dist/extensions/forgecli/model-resolver.js.map +1 -0
  126. package/dist/extensions/forgecli/model-validator.d.ts +29 -0
  127. package/dist/extensions/forgecli/model-validator.js +107 -0
  128. package/dist/extensions/forgecli/model-validator.js.map +1 -0
  129. package/dist/extensions/forgecli/run-sprint.js +59 -0
  130. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  131. package/dist/extensions/forgecli/run-task.js +93 -1
  132. package/dist/extensions/forgecli/run-task.js.map +1 -1
  133. package/dist/extensions/forgecli/thread-switcher.js +5 -2
  134. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  135. package/dist/extensions/forgecli/whats-new-widget.js +5 -2
  136. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  137. package/package.json +11 -3
  138. package/dist/extensions/forgecli/review-command.d.ts +0 -2
  139. package/dist/extensions/forgecli/review-command.js +0 -184
  140. package/dist/extensions/forgecli/review-command.js.map +0 -1
  141. package/dist/forge-payload/.tools/banners.cjs +0 -435
  142. package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
  143. package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
  144. package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
  145. package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
  146. package/dist/forge-payload/.tools/collate.cjs +0 -1041
  147. package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
  148. package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
  149. package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
  150. package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
  151. package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
  152. package/dist/forge-payload/.tools/lib/result.js +0 -40
  153. package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
  154. package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
  155. package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
  156. package/dist/forge-payload/.tools/lib/validate.js +0 -141
  157. package/dist/forge-payload/.tools/manage-config.cjs +0 -340
  158. package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
  159. package/dist/forge-payload/.tools/package.json +0 -3
  160. package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
  161. package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
  162. package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
  163. package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
  164. package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
  165. package/dist/forge-payload/.tools/seed-store.cjs +0 -237
  166. package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
  167. package/dist/forge-payload/.tools/store-query.cjs +0 -319
  168. package/dist/forge-payload/.tools/store.cjs +0 -315
  169. package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
  170. 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 };