@thxgg/steward 0.1.18 → 0.1.19
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{BA4e9-N5.js → BCiXS3ZV.js} +2 -2
- package/.output/public/_nuxt/BfUfZSFp.js +60 -0
- package/.output/public/_nuxt/{C_HVaH3B.js → BfV4oCiT.js} +1 -1
- package/.output/public/_nuxt/{CGzrvVc6.js → Bs6UO7IT.js} +1 -1
- package/.output/public/_nuxt/C9AsKFSQ.js +1 -0
- package/.output/public/_nuxt/{CJlXUkTg.js → C9jB6HQI.js} +1 -1
- package/.output/public/_nuxt/CCruYste.js +3 -0
- package/.output/public/_nuxt/CIIE0-WR.js +1 -0
- package/.output/public/_nuxt/{-z_Gr0GN.js → Cg4hnDua.js} +1 -1
- package/.output/public/_nuxt/CwdD8083.js +30 -0
- package/.output/public/_nuxt/{DAnnHVQP.js → D2RLSKEu.js} +1 -1
- package/.output/public/_nuxt/{DEr8q68O.js → D30YtxUg.js} +1 -1
- package/.output/public/_nuxt/{QAzsKGuP.js → DEekox9p.js} +1 -1
- package/.output/public/_nuxt/{WUF6Thhn.js → DddVAa3N.js} +1 -1
- package/.output/public/_nuxt/{TSsR_oCL.js → MO41rxll.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/627332c0-2e14-4849-8cac-e350f64ed513.json +1 -0
- package/.output/public/_nuxt/entry.Dp3jx0Yw.css +1 -0
- package/.output/public/_nuxt/f7vKgp5U.js +1 -0
- package/.output/public/_nuxt/xc3v2JZH.js +1 -0
- package/.output/server/chunks/_/prd-service.mjs +101 -68
- package/.output/server/chunks/_/prd-service.mjs.map +1 -1
- package/.output/server/chunks/_/repos.mjs +3 -179
- package/.output/server/chunks/_/repos.mjs.map +1 -1
- package/.output/server/chunks/_/task-graph.mjs +8 -4
- package/.output/server/chunks/_/task-graph.mjs.map +1 -1
- package/.output/server/chunks/_/watcher.mjs +2 -32
- package/.output/server/chunks/_/watcher.mjs.map +1 -1
- package/.output/server/chunks/build/{Detail-BQSkP9Zm.mjs → Detail-BcQGdJY5.mjs} +5 -6
- package/.output/server/chunks/build/Detail-BcQGdJY5.mjs.map +1 -0
- package/.output/server/chunks/build/{_prd_-CBR_wm9i.mjs → _prd_-CD_Bds_B.mjs} +81 -6
- package/.output/server/chunks/build/_prd_-CD_Bds_B.mjs.map +1 -0
- package/.output/server/chunks/build/client.precomputed.mjs +1 -1
- package/.output/server/chunks/build/{default-Cao5eO80.mjs → default-BKKgG7HJ.mjs} +221 -23
- package/.output/server/chunks/build/default-BKKgG7HJ.mjs.map +1 -0
- package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +2 -0
- package/.output/server/chunks/build/error-500-D_bcARXN.mjs +2 -0
- package/.output/server/chunks/build/{index-ljj9uTXI.mjs → index-DE1tjHAd.mjs} +4 -3
- package/.output/server/chunks/build/index-DE1tjHAd.mjs.map +1 -0
- package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
- package/.output/server/chunks/build/{repo-graph-CVnkmn8i.mjs → repo-graph-CBfhpnd5.mjs} +26 -11
- package/.output/server/chunks/build/repo-graph-CBfhpnd5.mjs.map +1 -0
- package/.output/server/chunks/build/server.mjs +15 -13
- package/.output/server/chunks/build/styles.mjs +2 -2
- package/.output/server/chunks/build/{usePrd-f7ylhIqs.mjs → usePrd-hXZOmvAv.mjs} +113 -9
- package/.output/server/chunks/build/usePrd-hXZOmvAv.mjs.map +1 -0
- package/.output/server/chunks/nitro/nitro.mjs +1292 -580
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.get.mjs +2 -1
- package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.post.mjs +2 -2
- package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +27 -4
- package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/archive.post.mjs +93 -0
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/archive.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +2 -2
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +3 -3
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +3 -3
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +3 -3
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs +2 -2
- package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +27 -4
- package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/runtime.get.mjs +2 -0
- package/.output/server/chunks/routes/api/runtime.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/state-migration/status.get.mjs +21 -0
- package/.output/server/chunks/routes/api/state-migration/status.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/watch.get.mjs +4 -3
- package/.output/server/chunks/routes/api/watch.get.mjs.map +1 -1
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/index.mjs +3 -1
- package/.output/server/index.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/README.md +3 -0
- package/dist/server/utils/db.js +15 -0
- package/dist/server/utils/prd-archive.js +53 -0
- package/dist/server/utils/prd-service.js +26 -6
- package/dist/server/utils/prd-state.js +11 -2
- package/dist/server/utils/state-migration.js +225 -0
- package/dist/server/utils/state-schema.js +181 -4
- package/dist/server/utils/task-graph.js +10 -3
- package/package.json +1 -1
- package/.output/public/_nuxt/5LlyHjkF.js +0 -60
- package/.output/public/_nuxt/BA0u_CRT.js +0 -1
- package/.output/public/_nuxt/BO8EM227.js +0 -3
- package/.output/public/_nuxt/C0XT5P3Q.js +0 -1
- package/.output/public/_nuxt/CZsXZugv.js +0 -1
- package/.output/public/_nuxt/DrXxYwWw.js +0 -30
- package/.output/public/_nuxt/builds/meta/19e0e040-a531-4c25-b46d-a6ca54a1ae3e.json +0 -1
- package/.output/public/_nuxt/entry.LcDOtJnR.css +0 -1
- package/.output/public/_nuxt/i9wn3hS7.js +0 -1
- package/.output/server/chunks/build/Detail-BQSkP9Zm.mjs.map +0 -1
- package/.output/server/chunks/build/_prd_-CBR_wm9i.mjs.map +0 -1
- package/.output/server/chunks/build/default-Cao5eO80.mjs.map +0 -1
- package/.output/server/chunks/build/index-ByZO4Bvq.mjs +0 -76
- package/.output/server/chunks/build/index-ByZO4Bvq.mjs.map +0 -1
- package/.output/server/chunks/build/index-ljj9uTXI.mjs.map +0 -1
- package/.output/server/chunks/build/repo-graph-CVnkmn8i.mjs.map +0 -1
- package/.output/server/chunks/build/usePrd-f7ylhIqs.mjs.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRenderer, getRequestDependencies, getPreloadLinks, getPrefetchLinks } from 'vue-bundle-renderer/runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { n as joinRelativeURL, u as useRuntimeConfig, o as getResponseStatusText, q as getResponseStatus, t as defineRenderHandler, g as getQuery, c as createError, v as destr, w as getRouteRules, x as joinURL, y as useNitroApp } from '../nitro/nitro.mjs';
|
|
3
3
|
import { renderToString } from 'vue/server-renderer';
|
|
4
4
|
import { createHead as createHead$1, propsToString, renderSSRHead } from 'unhead/server';
|
|
5
5
|
import { stringify, uneval } from 'devalue';
|
package/.output/server/index.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import process from 'node:process';globalThis._importMeta_={url:import.meta.url,env:process.env};import 'node:http';
|
|
2
2
|
import 'node:https';
|
|
3
|
-
export {
|
|
3
|
+
export { O as default } from './chunks/nitro/nitro.mjs';
|
|
4
4
|
import 'node:events';
|
|
5
5
|
import 'node:buffer';
|
|
6
6
|
import 'node:fs';
|
|
7
7
|
import 'node:path';
|
|
8
8
|
import 'node:crypto';
|
|
9
|
+
import 'node:os';
|
|
10
|
+
import 'zod';
|
|
9
11
|
import 'node:url';
|
|
10
12
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"names":[],"mappings":";;;;;;;;;;"}
|
package/README.md
CHANGED
|
@@ -146,6 +146,9 @@ Steward reads local filesystem and git metadata by design.
|
|
|
146
146
|
- UI/API accept loopback requests only
|
|
147
147
|
- Non-loopback requests are rejected
|
|
148
148
|
- Treat as a workstation tool, not a hosted multi-user service
|
|
149
|
+
- `npm run dev` skips loopback enforcement because Nuxt dev proxying can mask loopback source addresses
|
|
150
|
+
|
|
151
|
+
On startup, Steward also performs a one-time automatic state migration when legacy `progress_json` data is detected. During this migration, the UI shows a blocking progress overlay until migration completes.
|
|
149
152
|
|
|
150
153
|
## Storage
|
|
151
154
|
|
package/dist/server/utils/db.js
CHANGED
|
@@ -182,7 +182,22 @@ async function initializeDatabase() {
|
|
|
182
182
|
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
|
|
183
183
|
);
|
|
184
184
|
|
|
185
|
+
CREATE TABLE IF NOT EXISTS prd_archives (
|
|
186
|
+
repo_id TEXT NOT NULL,
|
|
187
|
+
slug TEXT NOT NULL,
|
|
188
|
+
archived_at TEXT NOT NULL,
|
|
189
|
+
PRIMARY KEY (repo_id, slug),
|
|
190
|
+
FOREIGN KEY (repo_id) REFERENCES repos(id) ON DELETE CASCADE
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
CREATE TABLE IF NOT EXISTS app_meta (
|
|
194
|
+
key TEXT PRIMARY KEY,
|
|
195
|
+
value TEXT NOT NULL,
|
|
196
|
+
updated_at TEXT NOT NULL
|
|
197
|
+
);
|
|
198
|
+
|
|
185
199
|
CREATE INDEX IF NOT EXISTS idx_prd_states_repo_id ON prd_states(repo_id);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_prd_archives_repo_id ON prd_archives(repo_id);
|
|
186
201
|
`);
|
|
187
202
|
return adapter;
|
|
188
203
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { emitChange } from './change-events.js';
|
|
2
|
+
import { dbAll, dbGet, dbRun } from './db.js';
|
|
3
|
+
function toArchiveState(archivedAt) {
|
|
4
|
+
if (!archivedAt) {
|
|
5
|
+
return { archived: false };
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
archived: true,
|
|
9
|
+
archivedAt
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function getPrdArchiveMap(repoId) {
|
|
13
|
+
const rows = await dbAll('SELECT slug, archived_at FROM prd_archives WHERE repo_id = ?', [repoId]);
|
|
14
|
+
const archiveMap = new Map();
|
|
15
|
+
for (const row of rows) {
|
|
16
|
+
archiveMap.set(row.slug, row.archived_at);
|
|
17
|
+
}
|
|
18
|
+
return archiveMap;
|
|
19
|
+
}
|
|
20
|
+
export async function getPrdArchiveState(repoId, slug) {
|
|
21
|
+
const row = await dbGet('SELECT archived_at FROM prd_archives WHERE repo_id = ? AND slug = ?', [repoId, slug]);
|
|
22
|
+
return toArchiveState(row?.archived_at);
|
|
23
|
+
}
|
|
24
|
+
export async function setPrdArchived(repoId, slug, archived) {
|
|
25
|
+
if (archived) {
|
|
26
|
+
const archivedAt = new Date().toISOString();
|
|
27
|
+
const result = await dbRun(`
|
|
28
|
+
INSERT INTO prd_archives (repo_id, slug, archived_at)
|
|
29
|
+
VALUES (?, ?, ?)
|
|
30
|
+
ON CONFLICT(repo_id, slug) DO NOTHING
|
|
31
|
+
`, [repoId, slug, archivedAt]);
|
|
32
|
+
const row = await dbGet('SELECT archived_at FROM prd_archives WHERE repo_id = ? AND slug = ?', [repoId, slug]);
|
|
33
|
+
if (result.changes > 0) {
|
|
34
|
+
emitChange({
|
|
35
|
+
type: 'change',
|
|
36
|
+
path: `state://${repoId}/${slug}.archive`,
|
|
37
|
+
repoId,
|
|
38
|
+
category: 'prd'
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return toArchiveState(row?.archived_at || archivedAt);
|
|
42
|
+
}
|
|
43
|
+
const result = await dbRun('DELETE FROM prd_archives WHERE repo_id = ? AND slug = ?', [repoId, slug]);
|
|
44
|
+
if (result.changes > 0) {
|
|
45
|
+
emitChange({
|
|
46
|
+
type: 'change',
|
|
47
|
+
path: `state://${repoId}/${slug}.archive`,
|
|
48
|
+
repoId,
|
|
49
|
+
category: 'prd'
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return { archived: false };
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
3
|
import { resolveCommitRepo } from './git.js';
|
|
4
|
+
import { getPrdArchiveMap, getPrdArchiveState } from './prd-archive.js';
|
|
4
5
|
import { getPrdState, getPrdStateSummaries } from './prd-state.js';
|
|
5
6
|
import { discoverGitRepos, updateRepoGitRepos } from './repos.js';
|
|
6
7
|
const PRD_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
|
|
@@ -83,14 +84,18 @@ export async function readPrdDocument(repo, prdSlug) {
|
|
|
83
84
|
catch {
|
|
84
85
|
throw new Error('PRD not found');
|
|
85
86
|
}
|
|
87
|
+
const archiveState = await getPrdArchiveState(repo.id, prdSlug);
|
|
86
88
|
return {
|
|
87
89
|
slug: prdSlug,
|
|
88
90
|
name: extractPrdTitle(content, prdSlug),
|
|
89
91
|
content,
|
|
90
|
-
metadata: parseMetadata(content)
|
|
92
|
+
metadata: parseMetadata(content),
|
|
93
|
+
archived: archiveState.archived,
|
|
94
|
+
...(archiveState.archivedAt && { archivedAt: archiveState.archivedAt })
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
|
-
export async function listPrdDocuments(repo) {
|
|
97
|
+
export async function listPrdDocuments(repo, options = {}) {
|
|
98
|
+
const includeArchived = options.includeArchived === true;
|
|
94
99
|
const prdDir = join(repo.path, 'docs', 'prd');
|
|
95
100
|
let prdFiles = [];
|
|
96
101
|
try {
|
|
@@ -106,9 +111,17 @@ export async function listPrdDocuments(repo) {
|
|
|
106
111
|
catch {
|
|
107
112
|
return [];
|
|
108
113
|
}
|
|
109
|
-
const stateSummaries = await
|
|
110
|
-
|
|
114
|
+
const [stateSummaries, archiveMap] = await Promise.all([
|
|
115
|
+
getPrdStateSummaries(repo.id),
|
|
116
|
+
getPrdArchiveMap(repo.id)
|
|
117
|
+
]);
|
|
118
|
+
const items = (await Promise.all(prdFiles.map(async (filename) => {
|
|
111
119
|
const slug = basename(filename, '.md');
|
|
120
|
+
const archivedAt = archiveMap.get(slug);
|
|
121
|
+
const archived = typeof archivedAt === 'string';
|
|
122
|
+
if (!includeArchived && archived) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
112
125
|
const filePath = join(prdDir, filename);
|
|
113
126
|
let name = slug;
|
|
114
127
|
let modifiedAt = 0;
|
|
@@ -130,11 +143,18 @@ export async function listPrdDocuments(repo) {
|
|
|
130
143
|
source: `docs/prd/${filename}`,
|
|
131
144
|
hasState: !!stateSummary?.hasState,
|
|
132
145
|
modifiedAt,
|
|
146
|
+
archived,
|
|
147
|
+
...(archivedAt && { archivedAt }),
|
|
133
148
|
...(stateSummary?.taskCount !== undefined && { taskCount: stateSummary.taskCount }),
|
|
134
149
|
...(stateSummary?.completedCount !== undefined && { completedCount: stateSummary.completedCount })
|
|
135
150
|
};
|
|
136
|
-
}));
|
|
137
|
-
items.sort((a, b) =>
|
|
151
|
+
}))).filter((item) => item !== null);
|
|
152
|
+
items.sort((a, b) => {
|
|
153
|
+
if (a.archived !== b.archived) {
|
|
154
|
+
return a.archived ? 1 : -1;
|
|
155
|
+
}
|
|
156
|
+
return b.modifiedAt - a.modifiedAt;
|
|
157
|
+
});
|
|
138
158
|
return items;
|
|
139
159
|
}
|
|
140
160
|
export async function readPrdTasks(repo, prdSlug) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emitChange } from './change-events.js';
|
|
2
2
|
import { dbAll, dbGet, dbRun } from './db.js';
|
|
3
|
-
import {
|
|
3
|
+
import { ensureStateMigrationReady } from './state-migration.js';
|
|
4
|
+
import { parseProgressFile, parseStoredProgressFile, parseTasksFile } from './state-schema.js';
|
|
4
5
|
function parseStoredJson(raw, fieldName, parseValue) {
|
|
5
6
|
if (!raw) {
|
|
6
7
|
return null;
|
|
@@ -23,6 +24,7 @@ function getTaskCounts(tasksFile) {
|
|
|
23
24
|
return { taskCount, completedCount };
|
|
24
25
|
}
|
|
25
26
|
export async function getPrdState(repoId, slug) {
|
|
27
|
+
await ensureStateMigrationReady();
|
|
26
28
|
const row = await dbGet(`
|
|
27
29
|
SELECT repo_id, slug, tasks_json, progress_json, notes_md, updated_at
|
|
28
30
|
FROM prd_states
|
|
@@ -32,7 +34,12 @@ export async function getPrdState(repoId, slug) {
|
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
34
36
|
const tasks = parseStoredJson(row.tasks_json, 'prd_states.tasks_json', parseTasksFile);
|
|
35
|
-
const
|
|
37
|
+
const tasksCountHint = Array.isArray(tasks?.tasks) ? tasks.tasks.length : undefined;
|
|
38
|
+
const prdNameFallback = tasks?.prd?.name || row.slug;
|
|
39
|
+
const progress = parseStoredJson(row.progress_json, 'prd_states.progress_json', (value) => parseStoredProgressFile(value, {
|
|
40
|
+
totalTasksHint: tasksCountHint,
|
|
41
|
+
prdNameFallback
|
|
42
|
+
}));
|
|
36
43
|
return {
|
|
37
44
|
slug: row.slug,
|
|
38
45
|
tasks,
|
|
@@ -42,6 +49,7 @@ export async function getPrdState(repoId, slug) {
|
|
|
42
49
|
};
|
|
43
50
|
}
|
|
44
51
|
export async function getPrdStateSummaries(repoId) {
|
|
52
|
+
await ensureStateMigrationReady();
|
|
45
53
|
const rows = await dbAll('SELECT slug, tasks_json FROM prd_states WHERE repo_id = ?', [repoId]);
|
|
46
54
|
const summaries = new Map();
|
|
47
55
|
for (const row of rows) {
|
|
@@ -64,6 +72,7 @@ export async function getPrdStateSummaries(repoId) {
|
|
|
64
72
|
return summaries;
|
|
65
73
|
}
|
|
66
74
|
export async function upsertPrdState(repoId, slug, update) {
|
|
75
|
+
await ensureStateMigrationReady();
|
|
67
76
|
const validatedTasks = update.tasks === undefined
|
|
68
77
|
? undefined
|
|
69
78
|
: (update.tasks === null ? null : parseTasksFile(update.tasks));
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { emitChange } from './change-events.js';
|
|
2
|
+
import { dbAll, dbExec, dbGet, dbRun } from './db.js';
|
|
3
|
+
import { needsProgressMigration, parseStoredProgressFile, parseTasksFile } from './state-schema.js';
|
|
4
|
+
const MIGRATION_VERSION = 'progress-json-v2';
|
|
5
|
+
const MIGRATION_META_KEY = `state-migration:${MIGRATION_VERSION}`;
|
|
6
|
+
let status = {
|
|
7
|
+
state: 'idle',
|
|
8
|
+
version: MIGRATION_VERSION,
|
|
9
|
+
startedAt: null,
|
|
10
|
+
completedAt: null,
|
|
11
|
+
totalRows: 0,
|
|
12
|
+
processedRows: 0,
|
|
13
|
+
migratedRows: 0,
|
|
14
|
+
failedRows: 0,
|
|
15
|
+
currentSlug: null,
|
|
16
|
+
errorMessage: null,
|
|
17
|
+
percent: 0
|
|
18
|
+
};
|
|
19
|
+
let migrationPromise = null;
|
|
20
|
+
function nowIso() {
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
}
|
|
23
|
+
function toPercent(processedRows, totalRows) {
|
|
24
|
+
if (totalRows <= 0) {
|
|
25
|
+
return 100;
|
|
26
|
+
}
|
|
27
|
+
return Math.min(100, Math.floor((processedRows / totalRows) * 100));
|
|
28
|
+
}
|
|
29
|
+
function resetRunningStatus(totalRows) {
|
|
30
|
+
status = {
|
|
31
|
+
state: 'running',
|
|
32
|
+
version: MIGRATION_VERSION,
|
|
33
|
+
startedAt: nowIso(),
|
|
34
|
+
completedAt: null,
|
|
35
|
+
totalRows,
|
|
36
|
+
processedRows: 0,
|
|
37
|
+
migratedRows: 0,
|
|
38
|
+
failedRows: 0,
|
|
39
|
+
currentSlug: null,
|
|
40
|
+
errorMessage: null,
|
|
41
|
+
percent: totalRows === 0 ? 100 : 0
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function markCompleted(marker) {
|
|
45
|
+
const completedAt = marker?.completedAt || nowIso();
|
|
46
|
+
const totalRows = marker?.totalRows ?? status.totalRows;
|
|
47
|
+
const migratedRows = marker?.migratedRows ?? status.migratedRows;
|
|
48
|
+
status = {
|
|
49
|
+
...status,
|
|
50
|
+
state: 'completed',
|
|
51
|
+
completedAt,
|
|
52
|
+
totalRows,
|
|
53
|
+
processedRows: totalRows,
|
|
54
|
+
migratedRows,
|
|
55
|
+
failedRows: 0,
|
|
56
|
+
currentSlug: null,
|
|
57
|
+
errorMessage: null,
|
|
58
|
+
percent: 100
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function markFailed(message) {
|
|
62
|
+
status = {
|
|
63
|
+
...status,
|
|
64
|
+
state: 'failed',
|
|
65
|
+
completedAt: nowIso(),
|
|
66
|
+
currentSlug: null,
|
|
67
|
+
errorMessage: message,
|
|
68
|
+
percent: toPercent(status.processedRows, status.totalRows)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function ensureMetaTable() {
|
|
72
|
+
await dbExec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS app_meta (
|
|
74
|
+
key TEXT PRIMARY KEY,
|
|
75
|
+
value TEXT NOT NULL,
|
|
76
|
+
updated_at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
async function readMigrationMarker() {
|
|
81
|
+
const row = await dbGet('SELECT value FROM app_meta WHERE key = ?', [MIGRATION_META_KEY]);
|
|
82
|
+
if (!row?.value) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(row.value);
|
|
87
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.version !== MIGRATION_VERSION) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof parsed.completedAt !== 'string') {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (typeof parsed.totalRows !== 'number' || typeof parsed.migratedRows !== 'number') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function writeMigrationMarker(totalRows, migratedRows) {
|
|
106
|
+
const completedAt = nowIso();
|
|
107
|
+
const marker = {
|
|
108
|
+
version: MIGRATION_VERSION,
|
|
109
|
+
completedAt,
|
|
110
|
+
totalRows,
|
|
111
|
+
migratedRows
|
|
112
|
+
};
|
|
113
|
+
await dbRun(`
|
|
114
|
+
INSERT INTO app_meta (key, value, updated_at)
|
|
115
|
+
VALUES (?, ?, ?)
|
|
116
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
117
|
+
value = excluded.value,
|
|
118
|
+
updated_at = excluded.updated_at
|
|
119
|
+
`, [MIGRATION_META_KEY, JSON.stringify(marker), completedAt]);
|
|
120
|
+
}
|
|
121
|
+
async function migrateProgressRows() {
|
|
122
|
+
await ensureMetaTable();
|
|
123
|
+
const marker = await readMigrationMarker();
|
|
124
|
+
if (marker) {
|
|
125
|
+
markCompleted(marker);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const rows = await dbAll(`
|
|
129
|
+
SELECT repo_id, slug, tasks_json, progress_json
|
|
130
|
+
FROM prd_states
|
|
131
|
+
WHERE progress_json IS NOT NULL
|
|
132
|
+
ORDER BY repo_id ASC, slug ASC
|
|
133
|
+
`);
|
|
134
|
+
resetRunningStatus(rows.length);
|
|
135
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
136
|
+
const row = rows[index];
|
|
137
|
+
status.currentSlug = row.slug;
|
|
138
|
+
try {
|
|
139
|
+
const parsedProgress = row.progress_json
|
|
140
|
+
? JSON.parse(row.progress_json)
|
|
141
|
+
: null;
|
|
142
|
+
let tasksCountHint;
|
|
143
|
+
let prdNameFallback;
|
|
144
|
+
if (row.tasks_json) {
|
|
145
|
+
try {
|
|
146
|
+
const tasks = parseTasksFile(JSON.parse(row.tasks_json));
|
|
147
|
+
tasksCountHint = tasks.tasks.length;
|
|
148
|
+
prdNameFallback = tasks.prd.name;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Keep fallback below when tasks_json is malformed.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const shouldMigrate = needsProgressMigration(parsedProgress);
|
|
155
|
+
if (shouldMigrate) {
|
|
156
|
+
const normalized = parseStoredProgressFile(parsedProgress, {
|
|
157
|
+
prdNameFallback: prdNameFallback || row.slug,
|
|
158
|
+
totalTasksHint: tasksCountHint
|
|
159
|
+
});
|
|
160
|
+
const updatedAt = nowIso();
|
|
161
|
+
await dbRun(`
|
|
162
|
+
UPDATE prd_states
|
|
163
|
+
SET progress_json = ?, updated_at = ?
|
|
164
|
+
WHERE repo_id = ? AND slug = ?
|
|
165
|
+
`, [JSON.stringify(normalized), updatedAt, row.repo_id, row.slug]);
|
|
166
|
+
status.migratedRows += 1;
|
|
167
|
+
emitChange({
|
|
168
|
+
type: 'change',
|
|
169
|
+
path: `state://${row.repo_id}/${row.slug}/progress.json`,
|
|
170
|
+
repoId: row.repo_id,
|
|
171
|
+
category: 'progress'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
status.failedRows += 1;
|
|
177
|
+
}
|
|
178
|
+
status.processedRows = index + 1;
|
|
179
|
+
status.percent = toPercent(status.processedRows, status.totalRows);
|
|
180
|
+
}
|
|
181
|
+
if (status.failedRows > 0) {
|
|
182
|
+
markFailed(`Failed to migrate ${status.failedRows} PRD progress row(s).`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await writeMigrationMarker(status.totalRows, status.migratedRows);
|
|
186
|
+
markCompleted();
|
|
187
|
+
}
|
|
188
|
+
async function runMigration() {
|
|
189
|
+
try {
|
|
190
|
+
await migrateProgressRows();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
markFailed(message);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export function getStateMigrationStatus() {
|
|
198
|
+
return { ...status };
|
|
199
|
+
}
|
|
200
|
+
export function startStateMigration() {
|
|
201
|
+
if (!migrationPromise) {
|
|
202
|
+
if (status.state === 'idle') {
|
|
203
|
+
status = {
|
|
204
|
+
...status,
|
|
205
|
+
state: 'running',
|
|
206
|
+
startedAt: nowIso(),
|
|
207
|
+
completedAt: null,
|
|
208
|
+
errorMessage: null,
|
|
209
|
+
percent: 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
migrationPromise = runMigration().finally(() => {
|
|
213
|
+
migrationPromise = null;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return migrationPromise;
|
|
217
|
+
}
|
|
218
|
+
export async function ensureStateMigrationReady() {
|
|
219
|
+
if (status.state !== 'completed') {
|
|
220
|
+
await startStateMigration();
|
|
221
|
+
}
|
|
222
|
+
if (status.state === 'failed') {
|
|
223
|
+
throw new Error(status.errorMessage || 'State migration failed');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
@@ -6,6 +6,7 @@ const commitRefSchema = z.object({
|
|
|
6
6
|
sha: z.string().min(1),
|
|
7
7
|
repo: z.string()
|
|
8
8
|
});
|
|
9
|
+
const taskStatusValues = new Set(['pending', 'in_progress', 'completed']);
|
|
9
10
|
const taskSchema = z.object({
|
|
10
11
|
id: z.string().min(1),
|
|
11
12
|
category: taskCategorySchema,
|
|
@@ -38,6 +39,19 @@ const taskLogSchema = z.object({
|
|
|
38
39
|
learnings: z.string().optional(),
|
|
39
40
|
commits: z.array(z.union([z.string().min(1), commitRefSchema])).optional()
|
|
40
41
|
});
|
|
42
|
+
const progressPatternSchema = z.union([
|
|
43
|
+
z.string().trim().min(1).transform((value) => ({
|
|
44
|
+
name: value,
|
|
45
|
+
description: value
|
|
46
|
+
})),
|
|
47
|
+
z.object({
|
|
48
|
+
name: z.string().trim().min(1),
|
|
49
|
+
description: z.string().trim().min(1).optional()
|
|
50
|
+
}).transform((value) => ({
|
|
51
|
+
name: value.name,
|
|
52
|
+
description: value.description ?? value.name
|
|
53
|
+
}))
|
|
54
|
+
]);
|
|
41
55
|
const progressFileSchema = z.object({
|
|
42
56
|
prdName: z.string(),
|
|
43
57
|
shortcutStory: z.string().optional(),
|
|
@@ -47,12 +61,175 @@ const progressFileSchema = z.object({
|
|
|
47
61
|
blocked: z.number(),
|
|
48
62
|
startedAt: z.string().nullable(),
|
|
49
63
|
lastUpdated: z.string(),
|
|
50
|
-
patterns: z.array(
|
|
51
|
-
name: z.string(),
|
|
52
|
-
description: z.string()
|
|
53
|
-
})),
|
|
64
|
+
patterns: z.array(progressPatternSchema),
|
|
54
65
|
taskLogs: z.array(taskLogSchema)
|
|
55
66
|
});
|
|
67
|
+
const progressInputSchema = z.object({
|
|
68
|
+
prdName: z.string().trim().min(1).optional(),
|
|
69
|
+
startedAt: z.union([z.string(), z.null()]).optional(),
|
|
70
|
+
started: z.union([z.string(), z.null()]).optional(),
|
|
71
|
+
totalTasks: z.number().optional(),
|
|
72
|
+
completed: z.union([z.number(), z.array(z.unknown())]).optional(),
|
|
73
|
+
inProgress: z.union([z.number(), z.null()]).optional(),
|
|
74
|
+
blocked: z.number().optional(),
|
|
75
|
+
lastUpdated: z.string().optional(),
|
|
76
|
+
patterns: z.array(z.unknown()).optional(),
|
|
77
|
+
taskLogs: z.array(z.unknown()).optional(),
|
|
78
|
+
taskProgress: z.record(z.object({
|
|
79
|
+
status: z.string().optional(),
|
|
80
|
+
startedAt: z.string().optional(),
|
|
81
|
+
completedAt: z.string().optional(),
|
|
82
|
+
implemented: z.string().optional(),
|
|
83
|
+
filesChanged: z.array(z.string()).optional(),
|
|
84
|
+
learnings: z.string().optional(),
|
|
85
|
+
commits: z.array(z.union([z.string().min(1), commitRefSchema])).optional()
|
|
86
|
+
})).optional()
|
|
87
|
+
}).passthrough();
|
|
88
|
+
function coerceNonNegativeNumber(value) {
|
|
89
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return Math.max(0, Math.floor(value));
|
|
93
|
+
}
|
|
94
|
+
function normalizeTaskLogStatus(value) {
|
|
95
|
+
if (typeof value === 'string' && taskStatusValues.has(value)) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return 'pending';
|
|
99
|
+
}
|
|
100
|
+
function normalizeLegacyTaskProgress(taskProgress, now) {
|
|
101
|
+
if (!taskProgress) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return Object.entries(taskProgress).map(([taskId, value]) => ({
|
|
105
|
+
taskId,
|
|
106
|
+
status: normalizeTaskLogStatus(value.status),
|
|
107
|
+
startedAt: typeof value.startedAt === 'string' ? value.startedAt : now,
|
|
108
|
+
...(typeof value.completedAt === 'string' && { completedAt: value.completedAt }),
|
|
109
|
+
...(typeof value.implemented === 'string' && { implemented: value.implemented }),
|
|
110
|
+
...(Array.isArray(value.filesChanged) && { filesChanged: value.filesChanged }),
|
|
111
|
+
...(typeof value.learnings === 'string' && { learnings: value.learnings }),
|
|
112
|
+
...(Array.isArray(value.commits) && { commits: value.commits })
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
function normalizePatterns(patterns) {
|
|
116
|
+
if (!patterns) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const normalized = [];
|
|
120
|
+
for (const pattern of patterns) {
|
|
121
|
+
const parsed = progressPatternSchema.safeParse(pattern);
|
|
122
|
+
if (parsed.success) {
|
|
123
|
+
normalized.push(parsed.data);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
function normalizeTaskLogs(taskLogs, now) {
|
|
129
|
+
if (!taskLogs) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const normalized = [];
|
|
133
|
+
for (const taskLog of taskLogs) {
|
|
134
|
+
const parsed = taskLogSchema.safeParse(taskLog);
|
|
135
|
+
if (parsed.success) {
|
|
136
|
+
normalized.push(parsed.data);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!taskLog || typeof taskLog !== 'object' || Array.isArray(taskLog)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const raw = taskLog;
|
|
143
|
+
if (typeof raw.taskId !== 'string' || raw.taskId.trim().length === 0) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const startedAt = typeof raw.startedAt === 'string' ? raw.startedAt : now;
|
|
147
|
+
const commits = Array.isArray(raw.commits)
|
|
148
|
+
? raw.commits.filter((commit) => {
|
|
149
|
+
if (typeof commit === 'string' && commit.trim().length > 0) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (!commit || typeof commit !== 'object' || Array.isArray(commit)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const ref = commit;
|
|
156
|
+
return typeof ref.sha === 'string' && ref.sha.trim().length > 0 && typeof ref.repo === 'string';
|
|
157
|
+
})
|
|
158
|
+
: undefined;
|
|
159
|
+
normalized.push({
|
|
160
|
+
taskId: raw.taskId,
|
|
161
|
+
status: normalizeTaskLogStatus(raw.status),
|
|
162
|
+
startedAt,
|
|
163
|
+
...(typeof raw.completedAt === 'string' && { completedAt: raw.completedAt }),
|
|
164
|
+
...(typeof raw.implemented === 'string' && { implemented: raw.implemented }),
|
|
165
|
+
...(Array.isArray(raw.filesChanged) && {
|
|
166
|
+
filesChanged: raw.filesChanged.filter((file) => typeof file === 'string')
|
|
167
|
+
}),
|
|
168
|
+
...(typeof raw.learnings === 'string' && { learnings: raw.learnings }),
|
|
169
|
+
...(commits && commits.length > 0 && { commits })
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
export function normalizeProgressFile(value, options = {}) {
|
|
175
|
+
const now = options.now || new Date().toISOString();
|
|
176
|
+
const parsed = progressInputSchema.safeParse(value);
|
|
177
|
+
const input = parsed.success ? parsed.data : {};
|
|
178
|
+
const prdName = input.prdName
|
|
179
|
+
|| options.prdNameFallback
|
|
180
|
+
|| 'Unknown PRD';
|
|
181
|
+
const patterns = normalizePatterns(input.patterns);
|
|
182
|
+
const taskLogs = input.taskLogs && input.taskLogs.length > 0
|
|
183
|
+
? normalizeTaskLogs(input.taskLogs, now)
|
|
184
|
+
: normalizeLegacyTaskProgress(input.taskProgress, now);
|
|
185
|
+
const completed = typeof input.completed === 'number'
|
|
186
|
+
? Math.max(0, Math.floor(input.completed))
|
|
187
|
+
: (Array.isArray(input.completed) ? input.completed.length : taskLogs.filter((log) => log.status === 'completed').length);
|
|
188
|
+
const inProgress = coerceNonNegativeNumber(input.inProgress)
|
|
189
|
+
?? taskLogs.filter((log) => log.status === 'in_progress').length;
|
|
190
|
+
const blocked = coerceNonNegativeNumber(input.blocked) ?? 0;
|
|
191
|
+
const totalTasks = coerceNonNegativeNumber(input.totalTasks)
|
|
192
|
+
?? coerceNonNegativeNumber(options.totalTasksHint)
|
|
193
|
+
?? Math.max(completed + inProgress + blocked, taskLogs.length);
|
|
194
|
+
return progressFileSchema.parse({
|
|
195
|
+
prdName,
|
|
196
|
+
shortcutStory: undefined,
|
|
197
|
+
totalTasks,
|
|
198
|
+
completed,
|
|
199
|
+
inProgress,
|
|
200
|
+
blocked,
|
|
201
|
+
startedAt: input.startedAt ?? input.started ?? null,
|
|
202
|
+
lastUpdated: input.lastUpdated || now,
|
|
203
|
+
patterns,
|
|
204
|
+
taskLogs
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
export function parseStoredProgressFile(value, options = {}) {
|
|
208
|
+
return normalizeProgressFile(value, options);
|
|
209
|
+
}
|
|
210
|
+
export function needsProgressMigration(value) {
|
|
211
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
const record = value;
|
|
215
|
+
if (Object.prototype.hasOwnProperty.call(record, 'started')) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
if (Object.prototype.hasOwnProperty.call(record, 'taskProgress')) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
if (!Array.isArray(record.patterns) || !Array.isArray(record.taskLogs)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(record.completed)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (record.inProgress === null) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const result = progressFileSchema.safeParse(value);
|
|
231
|
+
return !result.success;
|
|
232
|
+
}
|
|
56
233
|
export function parseTasksFile(value) {
|
|
57
234
|
return tasksFileSchema.parse(value);
|
|
58
235
|
}
|