@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.
- package/CHANGELOG.md +61 -0
- package/README.md +165 -2
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +17 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/forge.js +7 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +16 -0
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.js +5 -2
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/package.json +11 -3
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// store-query.cjs — Forge store query engine CLI
|
|
4
|
-
// Implements exact-args, keyword, and NLP intent paths.
|
|
5
|
-
// Spawned by store-cli.cjs query/nlp/schema dispatch.
|
|
6
|
-
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
process.on('uncaughtException', (e) => {
|
|
10
|
-
process.stderr.write(`Error: ${e.message}\n`);
|
|
11
|
-
process.exit(1);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const { StoreFacade, loadForgeConfig, resetConfigCache } = require('./lib/store-facade.cjs');
|
|
15
|
-
const { parseIntentNLP, ENTITY_SYNONYMS, STATUS_MAP } = require('./lib/store-nlp.cjs');
|
|
16
|
-
const { executeQuery, buildResult, kwMatches } = require('./lib/store-query-exec.cjs');
|
|
17
|
-
|
|
18
|
-
// ── Argument parser ───────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
function parseArgs(argv) {
|
|
21
|
-
const args = { _raw: argv.join(' ') };
|
|
22
|
-
const intentWords = [];
|
|
23
|
-
let i = 0;
|
|
24
|
-
while (i < argv.length) {
|
|
25
|
-
const a = argv[i];
|
|
26
|
-
switch (a) {
|
|
27
|
-
case '--sprint': args.sprint = argv[++i]; break;
|
|
28
|
-
case '--task': args.task = argv[++i]; break;
|
|
29
|
-
case '--bug': args.bug = argv[++i]; break;
|
|
30
|
-
case '--feature': args.feature = argv[++i]; break;
|
|
31
|
-
case '--status': args.status = argv[++i]; break;
|
|
32
|
-
case '--keyword': args.keyword = argv[++i]; break;
|
|
33
|
-
case '--type': args.type = argv[++i]; break;
|
|
34
|
-
case '--mode': args.mode = argv[++i]; break;
|
|
35
|
-
case '--with-blockers': args.withBlockers = true; break;
|
|
36
|
-
case '--with-blocked-tasks':args.withBlockedTasks = true; break;
|
|
37
|
-
case '--with-sprint': args.withSprint = true; break;
|
|
38
|
-
case '--with-feature': args.withFeature = true; break;
|
|
39
|
-
case '--no-excerpts': args.noExcerpts = true; break;
|
|
40
|
-
case '--list-sprints': args.listSprints = true; break;
|
|
41
|
-
case '--help': case '-h': args.help = true; break;
|
|
42
|
-
default:
|
|
43
|
-
if (!a.startsWith('-')) intentWords.push(a);
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
i++;
|
|
47
|
-
}
|
|
48
|
-
if (intentWords.length > 0) args.intent = intentWords.join(' ');
|
|
49
|
-
return args;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function printHelp() {
|
|
53
|
-
process.stdout.write(`Usage: store-query <command> [options]
|
|
54
|
-
|
|
55
|
-
Commands:
|
|
56
|
-
query Query the Forge store (exact args or NLP intent)
|
|
57
|
-
nlp Query the Forge store using NLP intent parser
|
|
58
|
-
schema Dump project schema and grammar reference
|
|
59
|
-
|
|
60
|
-
Query options:
|
|
61
|
-
--sprint <id> List tasks/bugs for a sprint
|
|
62
|
-
--task <id> Get task details
|
|
63
|
-
--bug <id> Get bug details
|
|
64
|
-
--feature <id> Get feature + related tasks
|
|
65
|
-
--list-sprints List all sprints
|
|
66
|
-
--status <status> Filter by status
|
|
67
|
-
--keyword <term> Search entity titles
|
|
68
|
-
--type <entity> Limit --keyword to sprints|tasks|bugs|features
|
|
69
|
-
--with-blockers Follow blockedBy FK on tasks
|
|
70
|
-
--with-blocked-tasks Follow blocksTask FK on bugs
|
|
71
|
-
--with-sprint Follow sprintId FK
|
|
72
|
-
--with-feature Follow featureId FK
|
|
73
|
-
--no-excerpts Omit INDEX.md excerpts
|
|
74
|
-
--mode strict|nlp|off Engine mode (default: nlp for intent, strict for flags)
|
|
75
|
-
`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ── Exact-args query path ─────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
function cmdQueryExact(args, store, cfg) {
|
|
81
|
-
const trace = [];
|
|
82
|
-
const results = [];
|
|
83
|
-
const relatedFiles = [];
|
|
84
|
-
const includeExcerpts = !args.noExcerpts;
|
|
85
|
-
const filter = {};
|
|
86
|
-
if (args.status) filter.status = args.status;
|
|
87
|
-
|
|
88
|
-
if (args.sprint === 'all' || args.listSprints) {
|
|
89
|
-
const sprints = store.listSprints(filter);
|
|
90
|
-
trace.push(`listed all sprints: ${sprints.length}`);
|
|
91
|
-
for (const s of sprints) results.push(buildResult(s, 'sprint', store, includeExcerpts));
|
|
92
|
-
} else if (args.sprint) {
|
|
93
|
-
const sprint = store.getEntity('sprints', args.sprint);
|
|
94
|
-
const tasks = store.listTasks({ sprintId: args.sprint, ...filter });
|
|
95
|
-
const bugs = store.listBugs({ sprintId: args.sprint, ...filter });
|
|
96
|
-
trace.push(`sprint ${args.sprint}: ${tasks.length} tasks, ${bugs.length} bugs`);
|
|
97
|
-
if (sprint) results.push(buildResult(sprint, 'sprint', store, includeExcerpts));
|
|
98
|
-
for (const t of tasks) {
|
|
99
|
-
results.push(buildResult(t, 'task', store, includeExcerpts));
|
|
100
|
-
if (args.withBlockers && t.blockedBy) {
|
|
101
|
-
for (const bugId of (Array.isArray(t.blockedBy) ? t.blockedBy : [t.blockedBy])) {
|
|
102
|
-
const bug = store.getEntity('bugs', bugId);
|
|
103
|
-
if (bug) { results.push(buildResult(bug, 'bug', store, includeExcerpts)); trace.push(`followed blockedBy → ${bugId}`); }
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
for (const b of bugs) results.push(buildResult(b, 'bug', store, includeExcerpts));
|
|
108
|
-
} else if (args.task) {
|
|
109
|
-
const task = store.getEntity('tasks', args.task);
|
|
110
|
-
if (task) {
|
|
111
|
-
results.push(buildResult(task, 'task', store, includeExcerpts));
|
|
112
|
-
if (args.withSprint && task.sprintId) {
|
|
113
|
-
const s = store.getEntity('sprints', task.sprintId);
|
|
114
|
-
if (s) { results.push(buildResult(s, 'sprint', store, includeExcerpts)); trace.push(`followed sprintId → ${task.sprintId}`); }
|
|
115
|
-
}
|
|
116
|
-
if (args.withFeature && (task.featureId || task.feature_id)) {
|
|
117
|
-
const fid = task.featureId || task.feature_id;
|
|
118
|
-
const f = store.getEntity('features', fid);
|
|
119
|
-
if (f) { results.push(buildResult(f, 'feature', store, includeExcerpts)); trace.push(`followed featureId → ${fid}`); }
|
|
120
|
-
}
|
|
121
|
-
trace.push(`task ${args.task} found`);
|
|
122
|
-
} else {
|
|
123
|
-
trace.push(`task ${args.task} not found`);
|
|
124
|
-
}
|
|
125
|
-
} else if (args.bug) {
|
|
126
|
-
const bug = store.getEntity('bugs', args.bug);
|
|
127
|
-
if (bug) {
|
|
128
|
-
results.push(buildResult(bug, 'bug', store, includeExcerpts));
|
|
129
|
-
if (args.withBlockedTasks && bug.blocksTask) {
|
|
130
|
-
for (const tid of (Array.isArray(bug.blocksTask) ? bug.blocksTask : [bug.blocksTask])) {
|
|
131
|
-
const t = store.getEntity('tasks', tid);
|
|
132
|
-
if (t) { results.push(buildResult(t, 'task', store, includeExcerpts)); trace.push(`followed blocksTask → ${tid}`); }
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
trace.push(`bug ${args.bug} found`);
|
|
136
|
-
} else {
|
|
137
|
-
trace.push(`bug ${args.bug} not found`);
|
|
138
|
-
}
|
|
139
|
-
} else if (args.feature) {
|
|
140
|
-
const feat = store.getEntity('features', args.feature);
|
|
141
|
-
if (feat) {
|
|
142
|
-
results.push(buildResult(feat, 'feature', store, includeExcerpts));
|
|
143
|
-
const tasks = store.listTasks({ featureId: args.feature, ...filter });
|
|
144
|
-
trace.push(`feature ${args.feature}: ${tasks.length} tasks`);
|
|
145
|
-
for (const t of tasks) results.push(buildResult(t, 'task', store, includeExcerpts));
|
|
146
|
-
} else {
|
|
147
|
-
trace.push(`feature ${args.feature} not found`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
for (const r of results) {
|
|
152
|
-
if (r.fileRefs?.md) relatedFiles.push(r.fileRefs.md);
|
|
153
|
-
if (r.fileRefs?.json) relatedFiles.push(r.fileRefs.json);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return { query: args._raw, path: 'exact', traversalTrace: trace, results, relatedFileRefs: [...new Set(relatedFiles)], config: { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' } };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── Keyword search path ───────────────────────────────────────────────────────
|
|
160
|
-
|
|
161
|
-
function cmdKeywordSearch(args, store, cfg) {
|
|
162
|
-
const trace = [];
|
|
163
|
-
const results = [];
|
|
164
|
-
const relatedFiles = [];
|
|
165
|
-
const includeExcerpts = !args.noExcerpts;
|
|
166
|
-
const keyword = args.keyword;
|
|
167
|
-
const targetType = args.type || null;
|
|
168
|
-
|
|
169
|
-
const entityTypes = targetType
|
|
170
|
-
? [targetType]
|
|
171
|
-
: ['sprints', 'tasks', 'bugs', 'features'];
|
|
172
|
-
const singMap = { sprints: 'sprint', tasks: 'task', bugs: 'bug', features: 'feature' };
|
|
173
|
-
|
|
174
|
-
for (const plural of entityTypes) {
|
|
175
|
-
const singular = singMap[plural];
|
|
176
|
-
let entities;
|
|
177
|
-
switch (plural) {
|
|
178
|
-
case 'sprints': entities = store.listSprints(); break;
|
|
179
|
-
case 'tasks': entities = store.listTasks(); break;
|
|
180
|
-
case 'bugs': entities = store.listBugs(); break;
|
|
181
|
-
case 'features': entities = store.listFeatures(); break;
|
|
182
|
-
default: entities = [];
|
|
183
|
-
}
|
|
184
|
-
const before = entities.length;
|
|
185
|
-
const matched = entities.filter(e => kwMatches(e.title || '', keyword));
|
|
186
|
-
if (matched.length > 0) {
|
|
187
|
-
trace.push(`keyword "${keyword}" in ${plural}: ${before} → ${matched.length}`);
|
|
188
|
-
for (const m of matched) results.push(buildResult(m, singular, store, includeExcerpts));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
for (const r of results) {
|
|
193
|
-
if (r.fileRefs?.md) relatedFiles.push(r.fileRefs.md);
|
|
194
|
-
if (r.fileRefs?.json) relatedFiles.push(r.fileRefs.json);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return { query: args._raw, path: 'keyword', traversalTrace: trace, results, relatedFileRefs: [...new Set(relatedFiles)], config: { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' } };
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ── Schema command ────────────────────────────────────────────────────────────
|
|
201
|
-
|
|
202
|
-
function cmdSchema(cfg) {
|
|
203
|
-
const { buildFieldValidators } = require('./lib/store-query-exec.cjs');
|
|
204
|
-
// build validators inline since not exported — rebuild here
|
|
205
|
-
const p = cfg.prefix;
|
|
206
|
-
const esc = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
207
|
-
const fv = {
|
|
208
|
-
sprints: { sprintId: /^S\d+$/, status: ['planning','active','completed','retrospective-done','blocked','partially-completed','abandoned'] },
|
|
209
|
-
tasks: { taskId: new RegExp(`^${esc}-S\\d+-T\\d+$`), sprintId: /^S\d+$/, featureId: /^FEAT-\d+$/, status: ['draft','planned','plan-approved','implementing','implemented','review-approved','approved','committed','plan-revision-required','code-revision-required','blocked','escalated','abandoned'] },
|
|
210
|
-
bugs: { bugId: new RegExp(`^${esc}-BUG-\\d+$`), sprintId: /^S\d+$/, severity: ['critical','major','minor'], status: ['reported','triaged','in-progress','fixed','verified'] },
|
|
211
|
-
features: { featureId: /^FEAT-\d+$/, status: ['active','draft','shipped','retired'] },
|
|
212
|
-
};
|
|
213
|
-
const toEnum = spec => Array.isArray(spec) ? spec : (spec instanceof RegExp ? `pattern: ${spec}` : null);
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
project: { prefix: cfg.prefix, name: cfg.projectName, kbPath: cfg.kbPath, storePath: cfg.storePathRel },
|
|
217
|
-
entities: {
|
|
218
|
-
sprints: { idField: 'sprintId', idPattern: fv.sprints.sprintId.toString(), status: toEnum(fv.sprints.status), fks: [] },
|
|
219
|
-
tasks: { idField: 'taskId', idPattern: fv.tasks.taskId.toString(), status: toEnum(fv.tasks.status), fks: ['sprintId','featureId','blockedBy'] },
|
|
220
|
-
bugs: { idField: 'bugId', idPattern: fv.bugs.bugId.toString(), status: toEnum(fv.bugs.status), severity: toEnum(fv.bugs.severity), fks: ['sprintId','blocksTask'] },
|
|
221
|
-
features: { idField: 'featureId', idPattern: fv.features.featureId.toString(),status: toEnum(fv.features.status), fks: [] },
|
|
222
|
-
},
|
|
223
|
-
entitySynonyms: ENTITY_SYNONYMS,
|
|
224
|
-
statusSynonyms: STATUS_MAP,
|
|
225
|
-
grammar: {
|
|
226
|
-
recency: ['last','latest','newest','recent','most recent','oldest','earliest','first'],
|
|
227
|
-
limit: ['top N','first N','last N'],
|
|
228
|
-
count: ['how many','count of','number of','count'],
|
|
229
|
-
fkPhrases: ['with sprint','with feature','blocking','blocked','which sprint','sprint for','feature for'],
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
function main() {
|
|
237
|
-
const argv = process.argv.slice(2);
|
|
238
|
-
const startMs = Date.now();
|
|
239
|
-
|
|
240
|
-
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
241
|
-
printHelp();
|
|
242
|
-
process.exit(0);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const command = argv[0];
|
|
246
|
-
const rest = argv.slice(1);
|
|
247
|
-
const args = parseArgs(rest);
|
|
248
|
-
|
|
249
|
-
if (args.help) { printHelp(); process.exit(0); }
|
|
250
|
-
|
|
251
|
-
const cfg = loadForgeConfig();
|
|
252
|
-
const store = new StoreFacade(cfg.storePathAbs);
|
|
253
|
-
|
|
254
|
-
let result;
|
|
255
|
-
|
|
256
|
-
switch (command) {
|
|
257
|
-
case 'schema': {
|
|
258
|
-
result = cmdSchema(cfg);
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
case 'nlp': {
|
|
262
|
-
if (!args.intent) {
|
|
263
|
-
process.stderr.write('Usage: store-query nlp "<natural language query>"\n');
|
|
264
|
-
process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
const plan = parseIntentNLP(args.intent);
|
|
267
|
-
result = executeQuery(plan, store, { query: args.intent, noExcerpts: args.noExcerpts });
|
|
268
|
-
result.query = args.intent;
|
|
269
|
-
result.path = 'intent-nlp';
|
|
270
|
-
result.config = { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' };
|
|
271
|
-
break;
|
|
272
|
-
}
|
|
273
|
-
case 'query': {
|
|
274
|
-
const mode = args.mode || 'auto';
|
|
275
|
-
const hasExactFlags = args.sprint || args.task || args.bug || args.feature || args.listSprints;
|
|
276
|
-
const hasKeyword = !!args.keyword;
|
|
277
|
-
|
|
278
|
-
if (mode === 'strict' || mode === 'off') {
|
|
279
|
-
if (args.intent && !hasExactFlags && !hasKeyword) {
|
|
280
|
-
process.stderr.write('Mode is strict — intent strings not accepted. Use --sprint/--task/--bug/--feature flags.\n');
|
|
281
|
-
process.exit(1);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (hasExactFlags) {
|
|
286
|
-
result = cmdQueryExact(args, store, cfg);
|
|
287
|
-
} else if (hasKeyword) {
|
|
288
|
-
result = cmdKeywordSearch(args, store, cfg);
|
|
289
|
-
} else if (args.intent) {
|
|
290
|
-
const plan = parseIntentNLP(args.intent);
|
|
291
|
-
result = executeQuery(plan, store, { query: args.intent, noExcerpts: args.noExcerpts });
|
|
292
|
-
result.query = args.intent;
|
|
293
|
-
result.path = 'intent-nlp';
|
|
294
|
-
result.config = { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' };
|
|
295
|
-
} else {
|
|
296
|
-
process.stderr.write('Provide an entity flag (--sprint, --task, --bug, --feature) or an intent string.\n');
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
default: {
|
|
302
|
-
process.stderr.write(`Unknown command: ${command}\n`);
|
|
303
|
-
printHelp();
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Attach meta timing block
|
|
309
|
-
const totalMs = Date.now() - startMs;
|
|
310
|
-
result.meta = {
|
|
311
|
-
mode: args.mode || 'auto',
|
|
312
|
-
engineVersion: '1.0.0',
|
|
313
|
-
totalTimeMs: totalMs,
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
main();
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { findProjectRoot } = require('./lib/project-root.cjs');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Store Facade for Forge
|
|
9
|
-
* Provides a backend-agnostic interface for CRUD operations on core store entities.
|
|
10
|
-
*/
|
|
11
|
-
class Store {
|
|
12
|
-
constructor(implementation) {
|
|
13
|
-
this.impl = implementation;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// --- Sprints ---
|
|
17
|
-
getSprint(id) { return this.impl.getSprint(id); }
|
|
18
|
-
listSprints(filter) { return this.impl.listSprints(filter); }
|
|
19
|
-
writeSprint(data) { return this.impl.writeSprint(data); }
|
|
20
|
-
deleteSprint(id) { return this.impl.deleteSprint(id); }
|
|
21
|
-
|
|
22
|
-
// --- Tasks ---
|
|
23
|
-
getTask(id) { return this.impl.getTask(id); }
|
|
24
|
-
listTasks(filter) { return this.impl.listTasks(filter); }
|
|
25
|
-
writeTask(data) { return this.impl.writeTask(data); }
|
|
26
|
-
deleteTask(id) { return this.impl.deleteTask(id); }
|
|
27
|
-
|
|
28
|
-
// --- Bugs ---
|
|
29
|
-
getBug(id) { return this.impl.getBug(id); }
|
|
30
|
-
listBugs(filter) { return this.impl.listBugs(filter); }
|
|
31
|
-
writeBug(data) { return this.impl.writeBug(data); }
|
|
32
|
-
deleteBug(id) { return this.impl.deleteBug(id); }
|
|
33
|
-
|
|
34
|
-
// --- Events ---
|
|
35
|
-
getEvent(id, sprintId) { return this.impl.getEvent(id, sprintId); }
|
|
36
|
-
listEvents(sprintId, filter) { return this.impl.listEvents(sprintId, filter); }
|
|
37
|
-
writeEvent(sprintId, data) { return this.impl.writeEvent(sprintId, data); }
|
|
38
|
-
deleteEvent(id, sprintId) { return this.impl.deleteEvent(id, sprintId); }
|
|
39
|
-
renameEvent(sprintId, oldFilename, newEventId) { return this.impl.renameEvent(sprintId, oldFilename, newEventId); }
|
|
40
|
-
|
|
41
|
-
// --- Features ---
|
|
42
|
-
getFeature(id) { return this.impl.getFeature(id); }
|
|
43
|
-
listFeatures(filter) { return this.impl.listFeatures(filter); }
|
|
44
|
-
writeFeature(data) { return this.impl.writeFeature(data); }
|
|
45
|
-
deleteFeature(id) { return this.impl.deleteFeature(id); }
|
|
46
|
-
|
|
47
|
-
// --- Collation State ---
|
|
48
|
-
writeCollationState(data) { return this.impl.writeCollationState(data); }
|
|
49
|
-
readCollationState() { return this.impl.readCollationState(); }
|
|
50
|
-
|
|
51
|
-
// --- Event Operations (extended) ---
|
|
52
|
-
/**
|
|
53
|
-
* Purge all event files for a sprint directory.
|
|
54
|
-
* @param {string} sprintId
|
|
55
|
-
* @param {{ dryRun?: boolean }} opts - dryRun: return file list without deleting
|
|
56
|
-
* @returns {{ purged: boolean, fileCount: number, files: string[] }}
|
|
57
|
-
*/
|
|
58
|
-
purgeEvents(sprintId, opts) { return this.impl.purgeEvents(sprintId, opts); }
|
|
59
|
-
/**
|
|
60
|
-
* List event filenames for a sprint directory.
|
|
61
|
-
* @param {string} sprintId
|
|
62
|
-
* @returns {{ filename: string, id: string }[]}
|
|
63
|
-
*/
|
|
64
|
-
listEventFilenames(sprintId) { return this.impl.listEventFilenames(sprintId); }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Filesystem Implementation of the Store facade.
|
|
69
|
-
* Manages JSON flat-files in the .forge/store directory.
|
|
70
|
-
*/
|
|
71
|
-
class FSImpl {
|
|
72
|
-
constructor(configPath = '.forge/config.json') {
|
|
73
|
-
this.configPath = configPath;
|
|
74
|
-
this.storeRoot = this._resolveStoreRoot();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
_resolveStoreRoot() {
|
|
78
|
-
const configPathIsAbsolute = path.isAbsolute(this.configPath);
|
|
79
|
-
const projectRoot = configPathIsAbsolute ? null : findProjectRoot();
|
|
80
|
-
try {
|
|
81
|
-
const resolved = projectRoot ? path.join(projectRoot, this.configPath) : this.configPath;
|
|
82
|
-
const config = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
83
|
-
const storePath = config.paths.store;
|
|
84
|
-
return path.isAbsolute(storePath) ? storePath
|
|
85
|
-
: projectRoot ? path.join(projectRoot, storePath) : storePath;
|
|
86
|
-
} catch (e) {
|
|
87
|
-
// Fallback to default if config is missing or corrupt
|
|
88
|
-
return projectRoot ? path.join(projectRoot, '.forge', 'store') : '.forge/store';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
_getPath(entity, id) {
|
|
93
|
-
const entityMap = {
|
|
94
|
-
sprint: 'sprints',
|
|
95
|
-
task: 'tasks',
|
|
96
|
-
bug: 'bugs',
|
|
97
|
-
event: 'events',
|
|
98
|
-
feature: 'features'
|
|
99
|
-
};
|
|
100
|
-
const dir = entityMap[entity];
|
|
101
|
-
if (!dir) throw new Error(`Unknown entity type: ${entity}`);
|
|
102
|
-
return path.join(this.storeRoot, dir, `${id}.json`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
_readJson(filePath) {
|
|
106
|
-
if (!fs.existsSync(filePath)) return null;
|
|
107
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
_writeJson(filePath, data) {
|
|
111
|
-
const dir = path.dirname(filePath);
|
|
112
|
-
if (!fs.existsSync(dir)) {
|
|
113
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
114
|
-
}
|
|
115
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
116
|
-
return data;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Sprints
|
|
120
|
-
getSprint(id) { return this._readJson(this._getPath('sprint', id)); }
|
|
121
|
-
listSprints(filter) {
|
|
122
|
-
const dir = path.join(this.storeRoot, 'sprints');
|
|
123
|
-
if (!fs.existsSync(dir)) return [];
|
|
124
|
-
return fs.readdirSync(dir)
|
|
125
|
-
.filter(f => f.endsWith('.json'))
|
|
126
|
-
.map(f => this._readJson(path.join(dir, f)))
|
|
127
|
-
.filter(s => !s || (filter ? this._matches(s, filter) : true));
|
|
128
|
-
}
|
|
129
|
-
writeSprint(data) {
|
|
130
|
-
return this._writeJson(this._getPath('sprint', data.sprintId), data);
|
|
131
|
-
}
|
|
132
|
-
deleteSprint(id) {
|
|
133
|
-
const p = this._getPath('sprint', id);
|
|
134
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Tasks
|
|
138
|
-
getTask(id) { return this._readJson(this._getPath('task', id)); }
|
|
139
|
-
listTasks(filter) {
|
|
140
|
-
const dir = path.join(this.storeRoot, 'tasks');
|
|
141
|
-
if (!fs.existsSync(dir)) return [];
|
|
142
|
-
return fs.readdirSync(dir)
|
|
143
|
-
.filter(f => f.endsWith('.json'))
|
|
144
|
-
.map(f => this._readJson(path.join(dir, f)))
|
|
145
|
-
.filter(t => !t || (filter ? this._matches(t, filter) : true));
|
|
146
|
-
}
|
|
147
|
-
writeTask(data) {
|
|
148
|
-
return this._writeJson(this._getPath('task', data.taskId), data);
|
|
149
|
-
}
|
|
150
|
-
deleteTask(id) {
|
|
151
|
-
const p = this._getPath('task', id);
|
|
152
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Bugs
|
|
156
|
-
getBug(id) { return this._readJson(this._getPath('bug', id)); }
|
|
157
|
-
listBugs(filter) {
|
|
158
|
-
const dir = path.join(this.storeRoot, 'bugs');
|
|
159
|
-
if (!fs.existsSync(dir)) return [];
|
|
160
|
-
return fs.readdirSync(dir)
|
|
161
|
-
.filter(f => f.endsWith('.json'))
|
|
162
|
-
.map(f => this._readJson(path.join(dir, f)))
|
|
163
|
-
.filter(b => !b || (filter ? this._matches(b, filter) : true));
|
|
164
|
-
}
|
|
165
|
-
writeBug(data) {
|
|
166
|
-
return this._writeJson(this._getPath('bug', data.bugId), data);
|
|
167
|
-
}
|
|
168
|
-
deleteBug(id) {
|
|
169
|
-
const p = this._getPath('bug', id);
|
|
170
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Events
|
|
174
|
-
getEvent(id, sprintId) {
|
|
175
|
-
const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
|
|
176
|
-
return this._readJson(p);
|
|
177
|
-
}
|
|
178
|
-
listEvents(sprintId, filter) {
|
|
179
|
-
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
180
|
-
if (!fs.existsSync(dir)) return [];
|
|
181
|
-
return fs.readdirSync(dir)
|
|
182
|
-
.filter(f => f.endsWith('.json'))
|
|
183
|
-
.map(f => this._readJson(path.join(dir, f)))
|
|
184
|
-
.filter(e => !e || (filter ? this._matches(e, filter) : true));
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Find an event file whose internal eventId matches the given eventId,
|
|
188
|
-
* but whose filename does not. Returns the mismatched filename (without
|
|
189
|
-
* .json extension), or null if none found. Skips _-prefixed (ephemeral) files.
|
|
190
|
-
*/
|
|
191
|
-
_findEventFileByContentId(sprintId, eventId) {
|
|
192
|
-
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
193
|
-
if (!fs.existsSync(dir)) return null;
|
|
194
|
-
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
|
|
195
|
-
for (const file of files) {
|
|
196
|
-
const filename = file.slice(0, -5); // strip .json
|
|
197
|
-
if (filename === eventId) continue; // already canonical
|
|
198
|
-
const rec = this._readJson(path.join(dir, file));
|
|
199
|
-
if (rec && rec.eventId === eventId) {
|
|
200
|
-
return filename;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Rename an event file from oldFilename to match newEventId.
|
|
207
|
-
* Throws if the target file already exists (collision).
|
|
208
|
-
*/
|
|
209
|
-
renameEvent(sprintId, oldFilename, newEventId) {
|
|
210
|
-
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
211
|
-
const oldPath = path.join(dir, `${oldFilename}.json`);
|
|
212
|
-
const newPath = path.join(dir, `${newEventId}.json`);
|
|
213
|
-
if (oldPath === newPath) return; // no-op
|
|
214
|
-
if (fs.existsSync(newPath)) {
|
|
215
|
-
throw new Error(`Cannot rename event: target file already exists: ${newPath}`);
|
|
216
|
-
}
|
|
217
|
-
if (fs.existsSync(oldPath)) {
|
|
218
|
-
fs.renameSync(oldPath, newPath);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
writeEvent(sprintId, data) {
|
|
222
|
-
// Detect ghost file: an existing file whose content eventId matches but
|
|
223
|
-
// whose filename does not. Rename it to the canonical name before writing.
|
|
224
|
-
const ghostFilename = this._findEventFileByContentId(sprintId, data.eventId);
|
|
225
|
-
if (ghostFilename !== null) {
|
|
226
|
-
this.renameEvent(sprintId, ghostFilename, data.eventId);
|
|
227
|
-
}
|
|
228
|
-
const p = path.join(this.storeRoot, 'events', sprintId, `${data.eventId}.json`);
|
|
229
|
-
return this._writeJson(p, data);
|
|
230
|
-
}
|
|
231
|
-
deleteEvent(id, sprintId) {
|
|
232
|
-
const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
|
|
233
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Features
|
|
237
|
-
getFeature(id) { return this._readJson(this._getPath('feature', id)); }
|
|
238
|
-
listFeatures(filter) {
|
|
239
|
-
const dir = path.join(this.storeRoot, 'features');
|
|
240
|
-
if (!fs.existsSync(dir)) return [];
|
|
241
|
-
return fs.readdirSync(dir)
|
|
242
|
-
.filter(f => f.endsWith('.json'))
|
|
243
|
-
.map(f => this._readJson(path.join(dir, f)))
|
|
244
|
-
.filter(f => !f || (filter ? this._matches(f, filter) : true));
|
|
245
|
-
}
|
|
246
|
-
writeFeature(data) {
|
|
247
|
-
return this._writeJson(this._getPath('feature', data.feature_id), data);
|
|
248
|
-
}
|
|
249
|
-
deleteFeature(id) {
|
|
250
|
-
const p = this._getPath('feature', id);
|
|
251
|
-
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Collation State
|
|
255
|
-
writeCollationState(data) {
|
|
256
|
-
const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
|
|
257
|
-
return this._writeJson(filePath, data);
|
|
258
|
-
}
|
|
259
|
-
readCollationState() {
|
|
260
|
-
const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
|
|
261
|
-
return this._readJson(filePath);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Event Operations (extended)
|
|
265
|
-
/**
|
|
266
|
-
* Purge the events directory for a given sprint.
|
|
267
|
-
* Includes a path-traversal guard: the resolved directory must remain
|
|
268
|
-
* within the events base directory. Throws on escape attempt.
|
|
269
|
-
* Note: fileCount reflects .json files only, but the directory is removed
|
|
270
|
-
* entirely (including any non-.json files).
|
|
271
|
-
*/
|
|
272
|
-
purgeEvents(sprintId, { dryRun = false } = {}) {
|
|
273
|
-
const eventsBase = path.resolve(this.storeRoot, 'events');
|
|
274
|
-
const eventsDir = path.resolve(eventsBase, sprintId);
|
|
275
|
-
// Guard: resolved path must stay within the events base directory.
|
|
276
|
-
if (!eventsDir.startsWith(eventsBase + path.sep) && eventsDir !== eventsBase) {
|
|
277
|
-
throw new Error(`Resolved events path '${eventsDir}' escapes store root — aborting purge`);
|
|
278
|
-
}
|
|
279
|
-
if (!fs.existsSync(eventsDir)) {
|
|
280
|
-
return { purged: false, fileCount: 0, files: [] };
|
|
281
|
-
}
|
|
282
|
-
const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.json'));
|
|
283
|
-
if (dryRun) {
|
|
284
|
-
return { purged: false, fileCount: files.length, files };
|
|
285
|
-
}
|
|
286
|
-
fs.rmSync(eventsDir, { recursive: true, force: true });
|
|
287
|
-
return { purged: true, fileCount: files.length, files };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* List all event filenames for a sprint directory.
|
|
292
|
-
* Returns { filename, id } objects for ALL .json files including
|
|
293
|
-
* _-prefixed ephemeral sidecars. Callers filter internally.
|
|
294
|
-
*/
|
|
295
|
-
listEventFilenames(sprintId) {
|
|
296
|
-
const dir = path.join(this.storeRoot, 'events', sprintId);
|
|
297
|
-
if (!fs.existsSync(dir)) return [];
|
|
298
|
-
return fs.readdirSync(dir)
|
|
299
|
-
.filter(f => f.endsWith('.json'))
|
|
300
|
-
.map(f => ({
|
|
301
|
-
filename: f,
|
|
302
|
-
id: f.slice(0, -5) // strip .json extension
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
_matches(record, filter) {
|
|
307
|
-
if (!filter) return true;
|
|
308
|
-
return Object.entries(filter).every(([key, value]) => record[key] === value);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Export a singleton instance for the plugin, plus classes for testing
|
|
313
|
-
module.exports = new Store(new FSImpl());
|
|
314
|
-
module.exports.Store = Store;
|
|
315
|
-
module.exports.FSImpl = FSImpl;
|