@magnusekdahl/parallix 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +190 -185
- package/lib/agents/agents.js +737 -838
- package/lib/agents/agents.ts +930 -0
- package/lib/agents/claude-telemetry.js +149 -154
- package/lib/agents/claude-telemetry.ts +233 -0
- package/lib/agents/claude.js +121 -131
- package/lib/agents/claude.ts +156 -0
- package/lib/agents/codex-telemetry.js +176 -164
- package/lib/agents/codex-telemetry.ts +205 -0
- package/lib/agents/codex.js +168 -201
- package/lib/agents/codex.ts +236 -0
- package/lib/agents/limit-hit.js +216 -226
- package/lib/agents/limit-hit.ts +262 -0
- package/lib/agents/mistral-telemetry.js +7 -10
- package/lib/agents/mistral-telemetry.ts +44 -0
- package/lib/agents/mistral.js +40 -58
- package/lib/agents/mistral.ts +84 -0
- package/lib/agents/opencode-export.js +126 -127
- package/lib/agents/opencode-export.ts +160 -0
- package/lib/agents/opencode-telemetry.js +282 -261
- package/lib/agents/opencode-telemetry.ts +350 -0
- package/lib/agents/opencode.js +263 -305
- package/lib/agents/opencode.ts +370 -0
- package/lib/agents/stage-telemetry.js +23 -24
- package/lib/agents/stage-telemetry.ts +47 -0
- package/lib/commands/active.js +502 -579
- package/lib/commands/active.ts +665 -0
- package/lib/commands/checkpoint.js +98 -74
- package/lib/commands/checkpoint.ts +79 -0
- package/lib/commands/config.js +61 -31
- package/lib/commands/config.ts +47 -0
- package/lib/commands/coverage-gate.js +271 -246
- package/lib/commands/coverage-gate.ts +362 -0
- package/lib/commands/diff.js +140 -119
- package/lib/commands/diff.ts +125 -0
- package/lib/commands/draft.js +770 -923
- package/lib/commands/draft.ts +1021 -0
- package/lib/commands/handoff.js +588 -608
- package/lib/commands/handoff.ts +694 -0
- package/lib/commands/integrate.js +1332 -1463
- package/lib/commands/integrate.ts +1688 -0
- package/lib/commands/mission-start.js +279 -237
- package/lib/commands/mission-start.ts +263 -0
- package/lib/commands/rebase.js +606 -582
- package/lib/commands/rebase.ts +635 -0
- package/lib/commands/repair-handoff.js +181 -179
- package/lib/commands/repair-handoff.ts +228 -0
- package/lib/commands/resolve-conflict.js +121 -102
- package/lib/commands/resolve-conflict.ts +112 -0
- package/lib/commands/review.js +3 -4
- package/lib/commands/review.ts +14 -0
- package/lib/commands/setup-review.js +4 -5
- package/lib/commands/setup-review.ts +14 -0
- package/lib/commands/setup.js +3 -3
- package/lib/commands/setup.ts +2 -0
- package/lib/commands/stats-backfill.js +394 -359
- package/lib/commands/stats-backfill.ts +418 -0
- package/lib/commands/stats.js +1424 -1665
- package/lib/commands/stats.ts +2186 -0
- package/lib/commands/status.js +219 -200
- package/lib/commands/status.ts +219 -0
- package/lib/commands/verify.js +3 -1
- package/lib/commands/verify.ts +2 -0
- package/lib/core/fmt.js +140 -211
- package/lib/core/fmt.ts +189 -0
- package/lib/core/git.js +121 -102
- package/lib/core/git.ts +146 -0
- package/lib/core/gitignore.js +59 -106
- package/lib/core/gitignore.ts +103 -0
- package/lib/core/mission-utils.js +833 -875
- package/lib/core/mission-utils.ts +998 -0
- package/lib/core/nels.js +112 -127
- package/lib/core/nels.ts +189 -0
- package/lib/core/persistent-data-migration.js +247 -205
- package/lib/core/persistent-data-migration.ts +255 -0
- package/lib/core/product-config.js +400 -476
- package/lib/core/product-config.ts +518 -0
- package/lib/core/runtime-matrix.js +41 -87
- package/lib/core/runtime-matrix.ts +84 -0
- package/lib/core/spawn-tee.js +152 -192
- package/lib/core/spawn-tee.ts +202 -0
- package/lib/core/state-map.js +92 -92
- package/lib/core/state-map.ts +115 -0
- package/lib/core/storage.js +101 -129
- package/lib/core/storage.ts +182 -0
- package/lib/core/subagent-limit.js +15 -16
- package/lib/core/subagent-limit.ts +25 -0
- package/lib/core/verification.js +148 -138
- package/lib/core/verification.ts +200 -0
- package/lib/index.js +142 -59
- package/lib/index.ts +134 -0
- package/lib/review/rebase.js +142 -145
- package/lib/review/rebase.ts +188 -0
- package/lib/review/review-adapter.js +99 -87
- package/lib/review/review-adapter.ts +198 -0
- package/lib/review/review-artifacts.js +486 -629
- package/lib/review/review-artifacts.ts +622 -0
- package/lib/review/review-commands.js +1220 -1373
- package/lib/review/review-commands.ts +1579 -0
- package/lib/review/review-events.js +544 -886
- package/lib/review/review-events.ts +954 -0
- package/lib/review/review-loop.js +945 -1020
- package/lib/review/review-loop.ts +1174 -0
- package/lib/review/review-polling.js +86 -115
- package/lib/review/review-polling.ts +194 -0
- package/lib/review/review-prompts.js +154 -189
- package/lib/review/review-prompts.ts +182 -0
- package/lib/review/review-state.js +246 -225
- package/lib/review/review-state.ts +353 -0
- package/lib/review/review.js +63 -84
- package/lib/review/review.ts +138 -0
- package/lib/tools/backlog.js +694 -680
- package/lib/tools/backlog.ts +835 -0
- package/lib/tools/forgejo.js +1270 -1405
- package/lib/tools/forgejo.ts +1793 -0
- package/lib/tools/gatekeeper.js +106 -96
- package/lib/tools/gatekeeper.ts +124 -0
- package/lib/tools/redgreen.js +200 -181
- package/lib/tools/redgreen.ts +221 -0
- package/lib/tools/sessions.js +58 -52
- package/lib/tools/sessions.ts +81 -0
- package/lib/tools/setup-review.js +902 -975
- package/lib/tools/setup-review.ts +1152 -0
- package/package.json +7 -5
- package/prompts/draft.md +1 -0
- package/prompts/portfolio.md +1 -1
- package/px.js +226 -210
|
@@ -1,327 +1,370 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
const fs = __importStar(require("node:fs"));
|
|
36
|
+
const path = __importStar(require("node:path"));
|
|
37
|
+
const fmt = __importStar(require("../core/fmt.js"));
|
|
38
|
+
const stats = require("./stats.js");
|
|
39
|
+
const git_js_1 = require("../core/git.js");
|
|
40
|
+
const backlog_js_1 = require("../tools/backlog.js");
|
|
41
|
+
const mission_utils_js_1 = require("../core/mission-utils.js");
|
|
42
|
+
function getStats() {
|
|
43
|
+
return stats;
|
|
44
|
+
}
|
|
15
45
|
function hasCheckpointFiles(missionDir) {
|
|
16
|
-
|
|
17
|
-
|
|
46
|
+
if (!missionDir || !fs.existsSync(missionDir)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return fs.readdirSync(missionDir).some(file => /^CP-\d+\.md$/i.test(file) || /^CHECKPOINT_FINAL\.md$/i.test(file));
|
|
18
50
|
}
|
|
19
|
-
|
|
20
51
|
function listHistoricalMissionSlugs(rootDir = process.cwd()) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
const missionsRoot = path.join(rootDir, 'docs', 'missions');
|
|
53
|
+
if (!fs.existsSync(missionsRoot)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const slugs = [];
|
|
57
|
+
for (const year of fs.readdirSync(missionsRoot).sort()) {
|
|
58
|
+
const yearDir = path.join(missionsRoot, year);
|
|
59
|
+
if (!/^\d{4}$/.test(year) || !fs.statSync(yearDir).isDirectory()) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const slug of fs.readdirSync(yearDir).sort()) {
|
|
63
|
+
if (!/^task-\d+/i.test(slug)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const missionDir = path.join(yearDir, slug);
|
|
67
|
+
if (!fs.statSync(missionDir).isDirectory()) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!hasCheckpointFiles(missionDir)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
slugs.push(slug);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return slugs;
|
|
37
77
|
}
|
|
38
|
-
|
|
39
|
-
/** @param {*} value */
|
|
40
78
|
function extractDateOnly(value) {
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
const match = String(value || '').match(/^(\d{4}-\d{2}-\d{2})/);
|
|
80
|
+
return match ? match[1] : null;
|
|
43
81
|
}
|
|
44
|
-
|
|
45
|
-
/** @param {*} value */
|
|
46
82
|
function normalizeHistoricalImplementer(value) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
84
|
+
if (!normalized) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (/(^|[^a-z])codex([^a-z]|$)/.test(normalized)) {
|
|
88
|
+
return 'codex';
|
|
89
|
+
}
|
|
90
|
+
if (/(^|[^a-z])claude([^a-z]|$)/.test(normalized)) {
|
|
91
|
+
return 'claude';
|
|
92
|
+
}
|
|
93
|
+
if (/(^|[^a-z])gemini([^a-z]|$)/.test(normalized)) {
|
|
94
|
+
return 'gemini';
|
|
95
|
+
}
|
|
96
|
+
if (/(^|[^a-z])custom([^a-z]|$)/.test(normalized)) {
|
|
97
|
+
return 'custom';
|
|
98
|
+
}
|
|
99
|
+
if (/(^|[^a-z])mistral([^a-z]|$)/.test(normalized)) {
|
|
100
|
+
return 'mistral';
|
|
101
|
+
}
|
|
102
|
+
if (/(^|[^a-z])magnus([^a-z]|$)/.test(normalized)) {
|
|
103
|
+
return 'magnus';
|
|
104
|
+
}
|
|
105
|
+
if (/(^|[^a-z])human([^a-z]|$)/.test(normalized)) {
|
|
106
|
+
return 'human';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
57
109
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
110
|
+
function deriveImplementerFromGitHistory(slug, taskFile, rootDir = process.cwd()) {
|
|
111
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, rootDir);
|
|
112
|
+
const targets = [missionDir, taskFile].filter((x) => Boolean(x));
|
|
113
|
+
if (targets.length === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const result = (0, git_js_1.git)(['-C', rootDir, 'log', '--all', '--format=%an|%ae', '--', ...targets]);
|
|
117
|
+
if (result.status !== 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const authors = result.stdout
|
|
121
|
+
.split('\n')
|
|
122
|
+
.map(line => line.trim())
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.map(line => {
|
|
125
|
+
const [name = '', email = ''] = line.split('|');
|
|
126
|
+
return normalizeHistoricalImplementer(email) || normalizeHistoricalImplementer(name);
|
|
74
127
|
})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
if (authors.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const uniqueAuthors = [...new Set(authors)];
|
|
133
|
+
return uniqueAuthors.length === 1 ? uniqueAuthors[0] : uniqueAuthors[0];
|
|
80
134
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
function deriveDateFromGitHistory(slug, taskFile, rootDir = process.cwd()) {
|
|
136
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, rootDir);
|
|
137
|
+
const targets = [missionDir, taskFile].filter((x) => Boolean(x));
|
|
138
|
+
if (targets.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const result = (0, git_js_1.git)(['-C', rootDir, 'log', '--all', '-1', '--format=%cs', '--', ...targets]);
|
|
142
|
+
if (result.status !== 0) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return extractDateOnly(result.stdout.trim());
|
|
90
146
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
147
|
+
function inferHistoricalClassificationFromMissionDoc(slug, rootDir = process.cwd()) {
|
|
148
|
+
const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, rootDir);
|
|
149
|
+
if (!missionDir) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const missionFile = path.join(missionDir, 'MISSION.md');
|
|
153
|
+
if (!fs.existsSync(missionFile)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const text = fs.readFileSync(missionFile, 'utf8').toLowerCase();
|
|
157
|
+
const productSurfacePatterns = [
|
|
158
|
+
/web-client\//g,
|
|
159
|
+
/\bios\b/g,
|
|
160
|
+
/\bandroid\b/g,
|
|
161
|
+
/\bwearos\b/g,
|
|
162
|
+
/\bauth-server\b/g,
|
|
163
|
+
/\bserver\/src\b/g,
|
|
164
|
+
/\bgrocer(?:y|ies)\b/g,
|
|
165
|
+
/\bboard\b/g,
|
|
166
|
+
/\bdelta\b/g,
|
|
167
|
+
/\bsync\b/g,
|
|
168
|
+
/\bprice\b/g,
|
|
169
|
+
/\bstore\b/g,
|
|
170
|
+
/\bshopping\b/g,
|
|
171
|
+
/\bui\b/g,
|
|
172
|
+
/\bclient\b/g,
|
|
173
|
+
/\bcheapest\b/g,
|
|
174
|
+
];
|
|
175
|
+
const workflowPatterns = [
|
|
176
|
+
/workflow\//g,
|
|
177
|
+
/node parallix\/index\.js/g,
|
|
178
|
+
/\bworkflow\b/g,
|
|
179
|
+
/\bagent\b/g,
|
|
180
|
+
/\bbacklog\b/g,
|
|
181
|
+
/\bforgejo\b/g,
|
|
182
|
+
/\breview loop\b/g,
|
|
183
|
+
/\breview\b/g,
|
|
184
|
+
/\bdraft\b/g,
|
|
185
|
+
/\bintegrate\b/g,
|
|
186
|
+
/\brebase\b/g,
|
|
187
|
+
/\bcheckpoint\b/g,
|
|
188
|
+
/\bprompt\b/g,
|
|
189
|
+
/\bstats\b/g,
|
|
190
|
+
/\bworktree\b/g,
|
|
191
|
+
/\bcli\b/g,
|
|
192
|
+
/\bgit\b/g,
|
|
193
|
+
/\bcoverage\b/g,
|
|
194
|
+
/\bverification\b/g,
|
|
195
|
+
];
|
|
196
|
+
const score = (patterns) => patterns.reduce((sum, pattern) => sum + ((text.match(pattern) || []).length), 0);
|
|
197
|
+
const productScore = score(productSurfacePatterns);
|
|
198
|
+
const workflowScore = score(workflowPatterns);
|
|
199
|
+
if (productScore === 0 && workflowScore === 0) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (workflowScore >= productScore + 2) {
|
|
203
|
+
return 'ai_sdlc';
|
|
204
|
+
}
|
|
205
|
+
if (productScore >= workflowScore + 2) {
|
|
206
|
+
return 'user_value';
|
|
207
|
+
}
|
|
208
|
+
const titleLine = text.split('\n')[0] || '';
|
|
209
|
+
if (/\bworkflow|agent|forgejo|backlog|review|rebase|checkpoint|stats|cli\b/.test(titleLine)) {
|
|
210
|
+
return 'ai_sdlc';
|
|
211
|
+
}
|
|
212
|
+
if (/\bweb client|web-client|ios|android|grocer|board|delta|sync|store|price\b/.test(titleLine)) {
|
|
213
|
+
return 'user_value';
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
157
216
|
}
|
|
158
|
-
|
|
159
|
-
/** @param {string} slug @param {string} taskFile @param {string} [rootDir] */
|
|
160
217
|
function resolveHistoricalClassification(slug, taskFile, rootDir = process.cwd()) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// Classification missing or invalid — fall through to fallbacks.
|
|
166
|
-
const legacy = stats._internals.normalizeClassification(getTaskFrontmatterValue(taskFile, 'classification'));
|
|
167
|
-
if (legacy) {
|
|
168
|
-
return { value: legacy, source: 'backlog-classification' };
|
|
169
|
-
}
|
|
170
|
-
const inferred = inferHistoricalClassificationFromMissionDoc(slug, rootDir);
|
|
171
|
-
if (inferred) {
|
|
172
|
-
return { value: inferred, source: 'mission-doc-heuristic' };
|
|
173
|
-
}
|
|
174
|
-
return { value: null, source: null };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** @param {string} [rootDir] @param {string|null} [filePath] */
|
|
178
|
-
function collectHistoricalStatsBackfill(rootDir = process.cwd(), filePath = null) {
|
|
179
|
-
filePath = filePath || stats.resolveStatsPath();
|
|
180
|
-
const repoName = stats.resolveStatsRepoName(rootDir);
|
|
181
|
-
const existingMissions = new Set(
|
|
182
|
-
stats.loadStatsCsv(filePath, { rootDir }).rows
|
|
183
|
-
.filter(row => String(row.repo || '').trim() === repoName)
|
|
184
|
-
.map(row => row.mission)
|
|
185
|
-
);
|
|
186
|
-
const rows = [];
|
|
187
|
-
const unresolved = [];
|
|
188
|
-
const skipped = [];
|
|
189
|
-
|
|
190
|
-
for (const slug of listHistoricalMissionSlugs(rootDir)) {
|
|
191
|
-
if (existingMissions.has(slug)) {continue;}
|
|
192
|
-
|
|
193
|
-
const taskResolution = resolveTaskFile(slug, rootDir);
|
|
194
|
-
if (!taskResolution.ok) {
|
|
195
|
-
unresolved.push({
|
|
196
|
-
slug,
|
|
197
|
-
reason: 'task-resolution',
|
|
198
|
-
detail: taskResolution.reason,
|
|
199
|
-
});
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const taskFile = /** @type {string} */ (taskResolution.taskFile);
|
|
204
|
-
const status = getTaskStatus(taskFile);
|
|
205
|
-
if (status !== 'done') {
|
|
206
|
-
skipped.push({ slug, reason: `status=${status || 'unknown'}` });
|
|
207
|
-
continue;
|
|
218
|
+
const s = getStats();
|
|
219
|
+
const resolution = s.resolveMissionClassification(slug, rootDir);
|
|
220
|
+
if (resolution.classification) {
|
|
221
|
+
return { value: resolution.classification, source: 'backlog-label' };
|
|
208
222
|
}
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let implementerError = null;
|
|
215
|
-
try {
|
|
216
|
-
implementerInfo = stats.deriveImplementerAndFixRounds(slug, rootDir);
|
|
217
|
-
} catch (/** @type {unknown} */ error) {
|
|
218
|
-
implementerError = /** @type{Error} */(error).message;
|
|
223
|
+
// Classification missing or invalid — fall through to fallbacks.
|
|
224
|
+
const classificationValue = (0, backlog_js_1.getTaskFrontmatterValue)(taskFile, 'classification');
|
|
225
|
+
const legacy = s._internals.normalizeClassification(classificationValue ?? '');
|
|
226
|
+
if (legacy) {
|
|
227
|
+
return { value: legacy, source: 'backlog-classification' };
|
|
219
228
|
}
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
if (gitHistoryImplementer) {
|
|
224
|
-
implementerInfo = {
|
|
225
|
-
implementer: gitHistoryImplementer,
|
|
226
|
-
prFixRounds: 0,
|
|
227
|
-
source: 'git-history-author',
|
|
228
|
-
};
|
|
229
|
-
}
|
|
229
|
+
const inferred = inferHistoricalClassificationFromMissionDoc(slug, rootDir);
|
|
230
|
+
if (inferred) {
|
|
231
|
+
return { value: inferred, source: 'mission-doc-heuristic' };
|
|
230
232
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
233
|
+
return { value: null, source: null };
|
|
234
|
+
}
|
|
235
|
+
function collectHistoricalStatsBackfill(rootDir = process.cwd(), filePath = null) {
|
|
236
|
+
const s = getStats();
|
|
237
|
+
filePath = filePath || s.resolveStatsPath();
|
|
238
|
+
const repoName = s.resolveStatsRepoName(rootDir);
|
|
239
|
+
const existingMissions = new Set(s.loadStatsCsv(filePath, { rootDir }).rows
|
|
240
|
+
.filter((row) => String(row.repo || '').trim() === repoName)
|
|
241
|
+
.map((row) => row.mission));
|
|
242
|
+
const rows = [];
|
|
243
|
+
const unresolved = [];
|
|
244
|
+
const skipped = [];
|
|
245
|
+
for (const slug of listHistoricalMissionSlugs(rootDir)) {
|
|
246
|
+
if (existingMissions.has(slug)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const taskResolution = (0, backlog_js_1.resolveTaskFile)(slug, rootDir);
|
|
250
|
+
if (!taskResolution.ok) {
|
|
251
|
+
unresolved.push({
|
|
252
|
+
slug,
|
|
253
|
+
reason: 'task-resolution',
|
|
254
|
+
detail: taskResolution.reason,
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const taskFile = taskResolution.taskFile;
|
|
259
|
+
const status = (0, backlog_js_1.getTaskStatus)(taskFile);
|
|
260
|
+
if (status !== 'done') {
|
|
261
|
+
skipped.push({ slug, reason: `status=${status || 'unknown'}` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const date = extractDateOnly((0, backlog_js_1.getTaskFrontmatterValue)(taskFile, 'updated_date') ?? '') || deriveDateFromGitHistory(slug, taskFile, rootDir);
|
|
265
|
+
const classification = resolveHistoricalClassification(slug, taskFile, rootDir);
|
|
266
|
+
let implementerInfo = null;
|
|
267
|
+
let implementerError = null;
|
|
268
|
+
try {
|
|
269
|
+
implementerInfo = s.deriveImplementerAndFixRounds(slug, rootDir);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
implementerError = error instanceof Error ? error.message : String(error);
|
|
273
|
+
}
|
|
274
|
+
if (!implementerInfo?.implementer || implementerInfo.implementer === 'unknown') {
|
|
275
|
+
const gitHistoryImplementer = deriveImplementerFromGitHistory(slug, taskFile, rootDir);
|
|
276
|
+
if (gitHistoryImplementer) {
|
|
277
|
+
implementerInfo = {
|
|
278
|
+
implementer: gitHistoryImplementer,
|
|
279
|
+
prFixRounds: 0,
|
|
280
|
+
source: 'git-history-author',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!date || !classification.value || !implementerInfo?.implementer) {
|
|
285
|
+
unresolved.push({
|
|
286
|
+
slug,
|
|
287
|
+
reason: 'missing-fields',
|
|
288
|
+
date: date || null,
|
|
289
|
+
classification: classification.value || null,
|
|
290
|
+
implementer: implementerInfo?.implementer || null,
|
|
291
|
+
prFixRounds: implementerInfo?.prFixRounds ?? null,
|
|
292
|
+
sources: {
|
|
293
|
+
classification: classification.source,
|
|
294
|
+
implementer: implementerInfo?.source || null,
|
|
295
|
+
},
|
|
296
|
+
missing: [
|
|
297
|
+
...(!date ? ['date'] : []),
|
|
298
|
+
...(!classification.value ? ['classification'] : []),
|
|
299
|
+
...(!implementerInfo?.implementer ? ['implementer'] : []),
|
|
300
|
+
],
|
|
301
|
+
detail: implementerError,
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
rows.push({
|
|
306
|
+
date,
|
|
307
|
+
repo: repoName,
|
|
308
|
+
mission: slug,
|
|
309
|
+
classification: classification.value,
|
|
310
|
+
implementer: implementerInfo.implementer,
|
|
311
|
+
pr_fix_rounds: String(implementerInfo.prFixRounds),
|
|
312
|
+
sources: {
|
|
313
|
+
date: 'backlog-updated_date',
|
|
314
|
+
classification: classification.source,
|
|
315
|
+
implementer: implementerInfo.source,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
252
318
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
mission: slug,
|
|
258
|
-
classification: classification.value,
|
|
259
|
-
implementer: implementerInfo.implementer,
|
|
260
|
-
pr_fix_rounds: String(implementerInfo.prFixRounds),
|
|
261
|
-
sources: {
|
|
262
|
-
date: 'backlog-updated_date',
|
|
263
|
-
classification: classification.source,
|
|
264
|
-
implementer: implementerInfo.source,
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
rows.sort((a, b) => a.date.localeCompare(b.date) || a.mission.localeCompare(b.mission));
|
|
270
|
-
unresolved.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
271
|
-
skipped.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
272
|
-
|
|
273
|
-
return { rows, unresolved, skipped };
|
|
319
|
+
rows.sort((a, b) => a.date.localeCompare(b.date) || a.mission.localeCompare(b.mission));
|
|
320
|
+
unresolved.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
321
|
+
skipped.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
322
|
+
return { rows, unresolved, skipped };
|
|
274
323
|
}
|
|
275
|
-
|
|
276
|
-
/** @param {{rows: any[], unresolved: any[], skipped: any[]}} report */
|
|
277
324
|
function renderBackfillSummary(report) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
lines.push('');
|
|
283
|
-
|
|
284
|
-
if (report.rows.length > 0) {
|
|
285
|
-
lines.push('Resolved:');
|
|
286
|
-
for (const row of report.rows) {
|
|
287
|
-
lines.push(`- ${row.date} ${row.mission} ${row.classification} ${row.implementer} ${row.pr_fix_rounds} [${row.sources.classification}/${row.sources.implementer}]`);
|
|
288
|
-
}
|
|
325
|
+
const lines = [];
|
|
326
|
+
lines.push(`Resolved rows: ${report.rows.length}`);
|
|
327
|
+
lines.push(`Unresolved missions: ${report.unresolved.length}`);
|
|
328
|
+
lines.push(`Skipped missions: ${report.skipped.length}`);
|
|
289
329
|
lines.push('');
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const detail = item.detail ? ` detail=${item.detail}` : '';
|
|
297
|
-
lines.push(`- ${item.slug} reason=${item.reason}${missing}${detail}`);
|
|
330
|
+
if (report.rows.length > 0) {
|
|
331
|
+
lines.push('Resolved:');
|
|
332
|
+
for (const row of report.rows) {
|
|
333
|
+
lines.push(`- ${row.date} ${row.mission} ${row.classification} ${row.implementer} ${row.pr_fix_rounds} [${row.sources.classification}/${row.sources.implementer}]`);
|
|
334
|
+
}
|
|
335
|
+
lines.push('');
|
|
298
336
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
337
|
+
if (report.unresolved.length > 0) {
|
|
338
|
+
lines.push('Unresolved:');
|
|
339
|
+
for (const item of report.unresolved) {
|
|
340
|
+
const missing = Array.isArray(item.missing) && item.missing.length > 0 ? ` missing=${item.missing.join(',')}` : '';
|
|
341
|
+
const detail = item.detail ? ` detail=${item.detail}` : '';
|
|
342
|
+
lines.push(`- ${item.slug} reason=${item.reason}${missing}${detail}`);
|
|
343
|
+
}
|
|
344
|
+
lines.push('');
|
|
306
345
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
346
|
+
if (report.skipped.length > 0) {
|
|
347
|
+
lines.push('Skipped:');
|
|
348
|
+
for (const item of report.skipped) {
|
|
349
|
+
lines.push(`- ${item.slug} ${item.reason}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return lines.join('\n');
|
|
310
353
|
}
|
|
311
|
-
|
|
312
|
-
/** @param {any[]} rows @param {string|null} [filePath] @param {string} [rootDir] */
|
|
313
354
|
function applyBackfillRows(rows, filePath = null, rootDir = process.cwd()) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
355
|
+
const s = getStats();
|
|
356
|
+
filePath = filePath || s.resolveStatsPath({ ensureDir: true });
|
|
357
|
+
let changed = 0;
|
|
358
|
+
for (const row of rows) {
|
|
359
|
+
const result = s.upsertStatsRow(row, { filePath, rootDir });
|
|
360
|
+
if (result.changed) {
|
|
361
|
+
changed += 1;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return changed;
|
|
321
365
|
}
|
|
322
|
-
|
|
323
366
|
function printUsage(log = fmt.log.plain) {
|
|
324
|
-
|
|
367
|
+
log(`Usage: px stats-backfill [--apply] [--json] [--csv-file <path>]
|
|
325
368
|
|
|
326
369
|
Examples:
|
|
327
370
|
px stats-backfill
|
|
@@ -334,69 +377,61 @@ Notes:
|
|
|
334
377
|
- It uses strict workflow stats derivation for implementer/fix rounds and historical fallbacks for classification.
|
|
335
378
|
- Non-done missions are skipped and unresolved missions are reported without being written.`);
|
|
336
379
|
}
|
|
337
|
-
|
|
338
|
-
/** @param {string[]} args @param {{log?: Function, error?: Function, rootDir?: string}} options */
|
|
339
380
|
function statsBackfill(args, options = {}) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
filePath = filePath || stats.resolveStatsPath({ ensureDir: apply });
|
|
371
|
-
|
|
381
|
+
const opts = options;
|
|
382
|
+
const log = opts.log || fmt.log.plain;
|
|
383
|
+
const error = opts.error || fmt.log.plainError;
|
|
384
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
385
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
386
|
+
printUsage(log);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
let apply = false;
|
|
390
|
+
let json = false;
|
|
391
|
+
let filePath = null;
|
|
392
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
393
|
+
const arg = args[i];
|
|
394
|
+
if (arg === '--apply') {
|
|
395
|
+
apply = true;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (arg === '--json') {
|
|
399
|
+
json = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (arg === '--csv-file' && i + 1 < args.length) {
|
|
403
|
+
filePath = args[i + 1];
|
|
404
|
+
i += 1;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const s = getStats();
|
|
408
|
+
filePath = filePath || s.resolveStatsPath({ ensureDir: apply });
|
|
372
409
|
const report = collectHistoricalStatsBackfill(rootDir, filePath);
|
|
373
410
|
const changed = apply ? applyBackfillRows(report.rows, filePath, rootDir) : 0;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
411
|
+
const payload = {
|
|
412
|
+
resolved: report.rows.length,
|
|
413
|
+
unresolved: report.unresolved.length,
|
|
414
|
+
skipped: report.skipped.length,
|
|
415
|
+
changed,
|
|
416
|
+
rows: report.rows,
|
|
417
|
+
unresolvedItems: report.unresolved,
|
|
418
|
+
skippedItems: report.skipped,
|
|
419
|
+
};
|
|
420
|
+
if (json) {
|
|
421
|
+
log(JSON.stringify(payload, null, 2));
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
log(renderBackfillSummary(report));
|
|
425
|
+
if (apply) {
|
|
426
|
+
log('');
|
|
427
|
+
log(fmt.status('PASS', `Applied ${changed} stats rows to ${filePath}`));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (apply && report.unresolved.length > 0) {
|
|
431
|
+
error(fmt.status('INFO', `${report.unresolved.length} missions remain unresolved and were not written.`));
|
|
432
|
+
}
|
|
397
433
|
}
|
|
398
|
-
|
|
434
|
+
statsBackfill.collectHistoricalStatsBackfill = collectHistoricalStatsBackfill;
|
|
435
|
+
statsBackfill.inferHistoricalClassificationFromMissionDoc = inferHistoricalClassificationFromMissionDoc;
|
|
436
|
+
statsBackfill.extractDateOnly = extractDateOnly;
|
|
399
437
|
module.exports = statsBackfill;
|
|
400
|
-
module.exports.collectHistoricalStatsBackfill = collectHistoricalStatsBackfill;
|
|
401
|
-
module.exports.inferHistoricalClassificationFromMissionDoc = inferHistoricalClassificationFromMissionDoc;
|
|
402
|
-
module.exports.extractDateOnly = extractDateOnly;
|