oreshnik-cli 0.1.0-alpha
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 +57 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +6269 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +2338 -0
- package/dist/index.js +3286 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/templates/central-doc.template.md +29 -0
- package/src/templates/task-board.template.json +6 -0
- package/src/templates/zone-map.template.json +13 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3286 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/core/git.service.ts
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
|
|
11
|
+
// src/types/index.ts
|
|
12
|
+
function ok(value) {
|
|
13
|
+
return { ok: true, value };
|
|
14
|
+
}
|
|
15
|
+
function err(error) {
|
|
16
|
+
return { ok: false, error };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/core/git.service.ts
|
|
20
|
+
var GitService = class {
|
|
21
|
+
constructor(cwd) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
cwd;
|
|
25
|
+
exec(args) {
|
|
26
|
+
const result = spawnSync("git", args, { cwd: this.cwd, encoding: "utf8", timeout: 3e4 });
|
|
27
|
+
return {
|
|
28
|
+
ok: result.status === 0,
|
|
29
|
+
output: (result.stdout || "").trim(),
|
|
30
|
+
error: (result.stderr || "").trim(),
|
|
31
|
+
status: result.status ?? 1
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
fail(message, args, stderr) {
|
|
35
|
+
return {
|
|
36
|
+
code: "GIT_ERROR",
|
|
37
|
+
message,
|
|
38
|
+
exitCode: 1,
|
|
39
|
+
gitCommand: `git ${args.join(" ")}`,
|
|
40
|
+
gitStderr: stderr
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Read Operations
|
|
44
|
+
currentBranch() {
|
|
45
|
+
const r = this.exec(["branch", "--show-current"]);
|
|
46
|
+
if (!r.ok) return err(this.fail("Failed to get current branch", ["branch", "--show-current"], r.error));
|
|
47
|
+
return ok(r.output || "DETACHED");
|
|
48
|
+
}
|
|
49
|
+
refExists(ref) {
|
|
50
|
+
return this.exec(["rev-parse", "--verify", ref]).ok;
|
|
51
|
+
}
|
|
52
|
+
resolveRef(ref) {
|
|
53
|
+
const r = this.exec(["rev-parse", "--verify", ref]);
|
|
54
|
+
if (!r.ok) return err(this.fail(`Ref not found: ${ref}`, ["rev-parse", "--verify", ref], r.error));
|
|
55
|
+
return ok(r.output);
|
|
56
|
+
}
|
|
57
|
+
statusPorcelain() {
|
|
58
|
+
const r = this.exec(["status", "--porcelain"]);
|
|
59
|
+
if (!r.ok) return err(this.fail("Failed to get working tree status", ["status", "--porcelain"], r.error));
|
|
60
|
+
const lines = r.output.split(/\r?\n/).filter(Boolean);
|
|
61
|
+
const entries = lines.map((line) => ({
|
|
62
|
+
status: line.slice(0, 2).trim(),
|
|
63
|
+
path: line.slice(3).replace(/^.* -> /, "")
|
|
64
|
+
}));
|
|
65
|
+
return ok(entries);
|
|
66
|
+
}
|
|
67
|
+
fetch(remote = "origin") {
|
|
68
|
+
const r = this.exec(["fetch", remote, "--prune", "--quiet"]);
|
|
69
|
+
if (!r.ok) return err(this.fail(`Failed to fetch from ${remote}`, ["fetch", remote, "--prune"], r.error));
|
|
70
|
+
return ok(void 0);
|
|
71
|
+
}
|
|
72
|
+
getMergeBase(a, b) {
|
|
73
|
+
const r = this.exec(["merge-base", a, b]);
|
|
74
|
+
if (!r.ok) return err(this.fail(`No merge base between ${a} and ${b}`, ["merge-base", a, b], r.error));
|
|
75
|
+
return ok(r.output);
|
|
76
|
+
}
|
|
77
|
+
discoverLatestMother(prefix = "MADRE") {
|
|
78
|
+
const r = this.exec([
|
|
79
|
+
"for-each-ref",
|
|
80
|
+
"--format=%(refname:short)",
|
|
81
|
+
`refs/heads/${prefix}`,
|
|
82
|
+
`refs/remotes/origin/${prefix}`
|
|
83
|
+
]);
|
|
84
|
+
if (!r.ok) {
|
|
85
|
+
return err(this.fail(`Failed to discover latest ${prefix} branch`, [
|
|
86
|
+
"for-each-ref",
|
|
87
|
+
"--format=%(refname:short)",
|
|
88
|
+
`refs/heads/${prefix}`,
|
|
89
|
+
`refs/remotes/origin/${prefix}`
|
|
90
|
+
], r.error));
|
|
91
|
+
}
|
|
92
|
+
const refs = r.output.split(/\r?\n/).filter(Boolean).map((ref) => ({
|
|
93
|
+
name: ref.replace(/^origin\//, ""),
|
|
94
|
+
remote: ref.startsWith("origin/")
|
|
95
|
+
}));
|
|
96
|
+
const latest = refs.map((ref) => {
|
|
97
|
+
const v = Number(ref.name.match(new RegExp(`^${prefix}/v(\\d+)`, "i"))?.[1] || 0);
|
|
98
|
+
return { ...ref, version: v };
|
|
99
|
+
}).filter((item) => item.version > 0).sort((a, b) => b.version - a.version || a.name.localeCompare(b.name))[0] || null;
|
|
100
|
+
if (!latest) return ok(null);
|
|
101
|
+
return ok({ name: latest.name, version: latest.version, remote: latest.remote });
|
|
102
|
+
}
|
|
103
|
+
diffNames(base, target, paths) {
|
|
104
|
+
const args = ["diff", "--name-only", `${base}...${target}`];
|
|
105
|
+
if (paths) args.push("--", ...paths);
|
|
106
|
+
const r = this.exec(args);
|
|
107
|
+
if (!r.ok) return err(this.fail(`Failed to diff ${base}...${target}`, args, r.error));
|
|
108
|
+
return ok(r.output.split(/\r?\n/).filter(Boolean));
|
|
109
|
+
}
|
|
110
|
+
unmergedFiles() {
|
|
111
|
+
const args = ["diff", "--name-only", "--diff-filter=U"];
|
|
112
|
+
const r = this.exec(args);
|
|
113
|
+
if (!r.ok) return err(this.fail("Failed to inspect unmerged files", args, r.error));
|
|
114
|
+
return ok(r.output.split(/\r?\n/).filter(Boolean));
|
|
115
|
+
}
|
|
116
|
+
showRef(ref, file) {
|
|
117
|
+
const r = this.exec(["show", `${ref}:${file}`]);
|
|
118
|
+
if (!r.ok) return ok(null);
|
|
119
|
+
return ok(r.output);
|
|
120
|
+
}
|
|
121
|
+
// Write Operations
|
|
122
|
+
createBranch(name, from) {
|
|
123
|
+
const args = ["checkout", "-b", name];
|
|
124
|
+
if (from) args.push(from);
|
|
125
|
+
const r = this.exec(args);
|
|
126
|
+
if (!r.ok) return err(this.fail(`Failed to create branch ${name}`, args, r.error));
|
|
127
|
+
return ok(void 0);
|
|
128
|
+
}
|
|
129
|
+
checkout(branch) {
|
|
130
|
+
const r = this.exec(["checkout", branch]);
|
|
131
|
+
if (!r.ok) return err(this.fail(`Failed to checkout ${branch}`, ["checkout", branch], r.error));
|
|
132
|
+
return ok(void 0);
|
|
133
|
+
}
|
|
134
|
+
merge(ref, options) {
|
|
135
|
+
const args = ["merge"];
|
|
136
|
+
if (options?.noCommit) args.push("--no-commit");
|
|
137
|
+
if (options?.noFF) args.push("--no-ff");
|
|
138
|
+
if (options?.strategy) args.push("--strategy", options.strategy);
|
|
139
|
+
if (options?.message) args.push("-m", options.message);
|
|
140
|
+
args.push(ref);
|
|
141
|
+
const r = this.exec(args);
|
|
142
|
+
if (!r.ok) return err(this.fail(`Failed to merge ${ref}`, args, r.error));
|
|
143
|
+
return ok(void 0);
|
|
144
|
+
}
|
|
145
|
+
push(remote, branch, force = false) {
|
|
146
|
+
const args = ["push", remote, branch];
|
|
147
|
+
if (force) args.push("--force");
|
|
148
|
+
const r = this.exec(args);
|
|
149
|
+
if (!r.ok) return err(this.fail(`Failed to push ${branch} to ${remote}`, args, r.error));
|
|
150
|
+
return ok(void 0);
|
|
151
|
+
}
|
|
152
|
+
pushTag(remote, tag) {
|
|
153
|
+
const args = ["push", remote, tag];
|
|
154
|
+
const r = this.exec(args);
|
|
155
|
+
if (!r.ok) return err(this.fail(`Failed to push tag ${tag} to ${remote}`, args, r.error));
|
|
156
|
+
return ok(void 0);
|
|
157
|
+
}
|
|
158
|
+
stage(files) {
|
|
159
|
+
const r = this.exec(["add", "--", ...files]);
|
|
160
|
+
if (!r.ok) return err(this.fail("Failed to stage files", ["add", ...files], r.error));
|
|
161
|
+
return ok(void 0);
|
|
162
|
+
}
|
|
163
|
+
commit(message) {
|
|
164
|
+
const r = this.exec(["commit", "-m", message]);
|
|
165
|
+
if (!r.ok) return err(this.fail("Failed to commit", ["commit", "-m", message], r.error));
|
|
166
|
+
return ok(void 0);
|
|
167
|
+
}
|
|
168
|
+
createTag(name, message) {
|
|
169
|
+
const r = this.exec(["tag", "-a", name, "-m", message]);
|
|
170
|
+
if (!r.ok) return err(this.fail(`Failed to create tag ${name}`, ["tag", "-a", name, "-m", message], r.error));
|
|
171
|
+
return ok(void 0);
|
|
172
|
+
}
|
|
173
|
+
deleteTag(name) {
|
|
174
|
+
const r = this.exec(["tag", "-d", name]);
|
|
175
|
+
if (!r.ok) return err(this.fail(`Failed to delete tag ${name}`, ["tag", "-d", name], r.error));
|
|
176
|
+
return ok(void 0);
|
|
177
|
+
}
|
|
178
|
+
// Destructive Operations
|
|
179
|
+
resetHard(ref) {
|
|
180
|
+
const r = this.exec(["reset", "--hard", ref]);
|
|
181
|
+
if (!r.ok) return err(this.fail(`Failed to reset to ${ref}`, ["reset", "--hard", ref], r.error));
|
|
182
|
+
return ok(void 0);
|
|
183
|
+
}
|
|
184
|
+
deleteBranch(branch, force = false) {
|
|
185
|
+
const args = ["branch", "-d", branch];
|
|
186
|
+
if (force) args.splice(1, 0, "-D");
|
|
187
|
+
const r = this.exec(args);
|
|
188
|
+
if (!r.ok) return err(this.fail(`Failed to delete branch ${branch}`, args, r.error));
|
|
189
|
+
return ok(void 0);
|
|
190
|
+
}
|
|
191
|
+
stashPush(message) {
|
|
192
|
+
const args = ["stash", "push"];
|
|
193
|
+
if (message) args.push("-m", message);
|
|
194
|
+
const r = this.exec(args);
|
|
195
|
+
if (!r.ok) return err(this.fail("Failed to stash", args, r.error));
|
|
196
|
+
return ok(r.output || null);
|
|
197
|
+
}
|
|
198
|
+
stashPop() {
|
|
199
|
+
const r = this.exec(["stash", "pop"]);
|
|
200
|
+
if (!r.ok) return err(this.fail("Failed to pop stash", ["stash", "pop"], r.error));
|
|
201
|
+
return ok(void 0);
|
|
202
|
+
}
|
|
203
|
+
// Lock Operations (remote git locks)
|
|
204
|
+
pushLockRef(zone, _lockContent) {
|
|
205
|
+
return err(this.fail(`Remote lock acquisition is not implemented for ${zone}`, ["push", "origin", `refs/oreshnik/locks/${zone}`]));
|
|
206
|
+
}
|
|
207
|
+
deleteLockRef(zone) {
|
|
208
|
+
return err(this.fail(`Remote lock release is not implemented for ${zone}`, ["push", "origin", "--delete", `refs/oreshnik/locks/${zone}`]));
|
|
209
|
+
}
|
|
210
|
+
fetchLockRefs() {
|
|
211
|
+
return err(this.fail("Remote lock listing is not implemented", ["for-each-ref", "refs/remotes/origin/oreshnik/locks/"]));
|
|
212
|
+
}
|
|
213
|
+
getLockContent(ref) {
|
|
214
|
+
return err(this.fail(`Remote lock content is not implemented for ${ref}`, ["show", ref]));
|
|
215
|
+
}
|
|
216
|
+
// Cherry-pick (for sync)
|
|
217
|
+
cherryPick(commit) {
|
|
218
|
+
const r = this.exec(["cherry-pick", commit]);
|
|
219
|
+
if (!r.ok) return err(this.fail(`Failed to cherry-pick ${commit}`, ["cherry-pick", commit], r.error));
|
|
220
|
+
return ok(void 0);
|
|
221
|
+
}
|
|
222
|
+
restoreFromHead(pattern) {
|
|
223
|
+
const r = this.exec(["checkout", "HEAD", "--", pattern]);
|
|
224
|
+
if (!r.ok) return err(this.fail(`Failed to restore ${pattern} from HEAD`, ["checkout", "HEAD", "--", pattern], r.error));
|
|
225
|
+
return ok(void 0);
|
|
226
|
+
}
|
|
227
|
+
// Utility
|
|
228
|
+
userConfig(key) {
|
|
229
|
+
const r = this.exec(["config", key]);
|
|
230
|
+
if (!r.ok) return err(this.fail(`Git config ${key} not set`, ["config", key]));
|
|
231
|
+
return ok(r.output);
|
|
232
|
+
}
|
|
233
|
+
mergeFileUnion(currentFile, baseFile, sourceFile) {
|
|
234
|
+
const r = spawnSync("git", ["merge-file", "--union", "-p", currentFile, baseFile, sourceFile], {
|
|
235
|
+
cwd: this.cwd,
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
timeout: 1e4
|
|
238
|
+
});
|
|
239
|
+
if (r.status !== 0) {
|
|
240
|
+
return err(this.fail("Failed to merge file with union strategy", ["merge-file", "--union", "-p", currentFile, baseFile, sourceFile], r.stderr || ""));
|
|
241
|
+
}
|
|
242
|
+
return ok((r.stdout || "").trim());
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function createGitService(cwd) {
|
|
246
|
+
return new GitService(cwd);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/core/zone-engine.ts
|
|
250
|
+
var ZoneEngine = class {
|
|
251
|
+
globToRegex(glob) {
|
|
252
|
+
const doubleStarToken = "__DOUBLE_STAR__";
|
|
253
|
+
const escaped = glob.replaceAll("**", doubleStarToken).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*").replaceAll(doubleStarToken, ".*");
|
|
254
|
+
return new RegExp(`^${escaped}$`);
|
|
255
|
+
}
|
|
256
|
+
findMatchingZone(file, zoneMap) {
|
|
257
|
+
for (const [pattern, zone] of Object.entries(zoneMap.zones)) {
|
|
258
|
+
if (this.globToRegex(pattern).test(file)) {
|
|
259
|
+
return { pattern, zone };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
check(files, operator, sprint, zoneMap) {
|
|
265
|
+
const violations = [];
|
|
266
|
+
const warnings = [];
|
|
267
|
+
for (const file of files) {
|
|
268
|
+
const match = this.findMatchingZone(file, zoneMap);
|
|
269
|
+
if (!match) {
|
|
270
|
+
warnings.push({ file, reason: `no-zone-entry: ${file} not mapped in zone-map.json` });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const { pattern, zone } = match;
|
|
274
|
+
const allowed = zone.sprints.includes("*") || zone.sprints.includes(sprint);
|
|
275
|
+
if (!allowed) {
|
|
276
|
+
warnings.push({ file, reason: `sprint-not-allowed: ${pattern} not mapped to sprint ${sprint}` });
|
|
277
|
+
}
|
|
278
|
+
const result = this.checkLock(file, pattern, zone.lock, zone.owner, operator);
|
|
279
|
+
if (result.violation) violations.push(result.violation);
|
|
280
|
+
if (result.warning) warnings.push(result.warning);
|
|
281
|
+
}
|
|
282
|
+
return { violations, warnings, filesChecked: files.length };
|
|
283
|
+
}
|
|
284
|
+
checkLock(file, zonePattern, lock, zoneOwner, operator) {
|
|
285
|
+
switch (lock) {
|
|
286
|
+
case "forbidden":
|
|
287
|
+
return {
|
|
288
|
+
violation: { file, zone: zonePattern, reason: "forbidden-zone: no operator may modify this zone" }
|
|
289
|
+
};
|
|
290
|
+
case "operator_exclusive":
|
|
291
|
+
if (zoneOwner !== operator) {
|
|
292
|
+
return {
|
|
293
|
+
violation: { file, zone: zonePattern, reason: `exclusive-zone: owned by ${zoneOwner}, operator is ${operator}` }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
case "operator_double":
|
|
298
|
+
return {
|
|
299
|
+
warning: { file, reason: `double-lock-required: ${zonePattern} needs coordination with ${zoneOwner}` }
|
|
300
|
+
};
|
|
301
|
+
case "shared":
|
|
302
|
+
break;
|
|
303
|
+
case "owner_per_sprint":
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
getModifiedZones(files, zoneMap) {
|
|
309
|
+
const zones = /* @__PURE__ */ new Set();
|
|
310
|
+
for (const file of files) {
|
|
311
|
+
const match = this.findMatchingZone(file, zoneMap);
|
|
312
|
+
if (match) zones.add(match.pattern);
|
|
313
|
+
}
|
|
314
|
+
return zones;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function createZoneEngine() {
|
|
318
|
+
return new ZoneEngine();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/core/state-manager.ts
|
|
322
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
323
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
324
|
+
import { join } from "path";
|
|
325
|
+
import { randomBytes } from "crypto";
|
|
326
|
+
var StateManager = class {
|
|
327
|
+
constructor(root) {
|
|
328
|
+
this.root = root;
|
|
329
|
+
this.runsDir = join(root, "var", "oreshnik");
|
|
330
|
+
mkdirSync(this.runsDir, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
root;
|
|
333
|
+
runsDir;
|
|
334
|
+
// ─── File Operations ───────────────────────────────────────
|
|
335
|
+
readJson(filePath) {
|
|
336
|
+
try {
|
|
337
|
+
if (!existsSync(filePath)) {
|
|
338
|
+
return err({ code: "STATE_ERROR", message: `File not found: ${filePath}`, exitCode: 1 });
|
|
339
|
+
}
|
|
340
|
+
const content = readFileSync(filePath, "utf8").replace(/^\uFEFF/, "");
|
|
341
|
+
return ok(JSON.parse(content));
|
|
342
|
+
} catch (e) {
|
|
343
|
+
return err({
|
|
344
|
+
code: "STATE_ERROR",
|
|
345
|
+
message: `Failed to read/parse ${filePath}: ${String(e)}`,
|
|
346
|
+
exitCode: 1
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
writeJson(filePath, value) {
|
|
351
|
+
try {
|
|
352
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
353
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
354
|
+
`, "utf8");
|
|
355
|
+
return ok(void 0);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
return err({
|
|
358
|
+
code: "STATE_ERROR",
|
|
359
|
+
message: `Failed to write ${filePath}: ${String(e)}`,
|
|
360
|
+
exitCode: 1
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
readText(filePath) {
|
|
365
|
+
try {
|
|
366
|
+
if (!existsSync(filePath)) return ok("");
|
|
367
|
+
return ok(readFileSync(filePath, "utf8"));
|
|
368
|
+
} catch (e) {
|
|
369
|
+
return err({
|
|
370
|
+
code: "STATE_ERROR",
|
|
371
|
+
message: `Failed to read ${filePath}: ${String(e)}`,
|
|
372
|
+
exitCode: 1
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
writeText(filePath, content) {
|
|
377
|
+
try {
|
|
378
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
379
|
+
writeFileSync(filePath, content, "utf8");
|
|
380
|
+
return ok(void 0);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return err({
|
|
383
|
+
code: "STATE_ERROR",
|
|
384
|
+
message: `Failed to write ${filePath}: ${String(e)}`,
|
|
385
|
+
exitCode: 1
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// ─── Merge Helpers ─────────────────────────────────────────
|
|
390
|
+
mergeTextUnion(file, base, current, source) {
|
|
391
|
+
const tmpDir = join(this.runsDir, ".merge-tmp");
|
|
392
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
393
|
+
const id = randomBytes(8).toString("hex");
|
|
394
|
+
const basePath = join(tmpDir, `base-${id}`);
|
|
395
|
+
const curPath = join(tmpDir, `current-${id}`);
|
|
396
|
+
const srcPath = join(tmpDir, `source-${id}`);
|
|
397
|
+
try {
|
|
398
|
+
writeFileSync(basePath, base, "utf8");
|
|
399
|
+
writeFileSync(curPath, current, "utf8");
|
|
400
|
+
writeFileSync(srcPath, source, "utf8");
|
|
401
|
+
const result = spawnSync2("git", ["merge-file", "--union", "-p", curPath, basePath, srcPath], {
|
|
402
|
+
cwd: this.root,
|
|
403
|
+
encoding: "utf8",
|
|
404
|
+
timeout: 1e4
|
|
405
|
+
});
|
|
406
|
+
return ok(result.stdout || "");
|
|
407
|
+
} catch (e) {
|
|
408
|
+
return err({
|
|
409
|
+
code: "STATE_ERROR",
|
|
410
|
+
message: `Merge failed for ${file}: ${String(e)}`,
|
|
411
|
+
exitCode: 1
|
|
412
|
+
});
|
|
413
|
+
} finally {
|
|
414
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
mergeJsonSmart(base, current, source) {
|
|
418
|
+
try {
|
|
419
|
+
const merged = this.mergeValue(base, current, source);
|
|
420
|
+
return ok(merged);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return err({
|
|
423
|
+
code: "STATE_ERROR",
|
|
424
|
+
message: `JSON merge failed: ${String(e)}`,
|
|
425
|
+
exitCode: 1
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
mergeValue(base, current, source) {
|
|
430
|
+
const key = (v) => JSON.stringify(v);
|
|
431
|
+
if (key(current) === key(source)) return current;
|
|
432
|
+
if (key(current) === key(base)) return source;
|
|
433
|
+
if (key(source) === key(base)) return current;
|
|
434
|
+
if (current && source && typeof current === "object" && typeof source === "object" && !Array.isArray(current) && !Array.isArray(source)) {
|
|
435
|
+
const result = { ...current };
|
|
436
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
437
|
+
...Object.keys(base || {}),
|
|
438
|
+
...Object.keys(current),
|
|
439
|
+
...Object.keys(source)
|
|
440
|
+
]);
|
|
441
|
+
for (const k of allKeys) {
|
|
442
|
+
result[k] = this.mergeValue(
|
|
443
|
+
base?.[k],
|
|
444
|
+
current[k],
|
|
445
|
+
source[k]
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
if (Array.isArray(current) && Array.isArray(source)) {
|
|
451
|
+
const seen = /* @__PURE__ */ new Set();
|
|
452
|
+
const merged = [...current, ...source].filter((item) => {
|
|
453
|
+
const k = key(item);
|
|
454
|
+
if (seen.has(k)) return false;
|
|
455
|
+
seen.add(k);
|
|
456
|
+
return true;
|
|
457
|
+
});
|
|
458
|
+
return merged;
|
|
459
|
+
}
|
|
460
|
+
return source;
|
|
461
|
+
}
|
|
462
|
+
// ─── Lock Management ───────────────────────────────────────
|
|
463
|
+
acquireLocalLock(lockName, timeoutMs = 5e3) {
|
|
464
|
+
const lockPath = join(this.runsDir, `.lock-${lockName}`);
|
|
465
|
+
const start = Date.now();
|
|
466
|
+
while (Date.now() - start < timeoutMs) {
|
|
467
|
+
try {
|
|
468
|
+
const fd = this.openExclusive(lockPath);
|
|
469
|
+
if (fd !== null) {
|
|
470
|
+
return ok({ path: lockPath, fd });
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
const waited = Date.now() - start;
|
|
475
|
+
if (waited < timeoutMs) {
|
|
476
|
+
const waitUntil = Date.now() + 50;
|
|
477
|
+
while (Date.now() < waitUntil) {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return err({
|
|
482
|
+
code: "STATE_ERROR",
|
|
483
|
+
message: `Could not acquire lock ${lockName} within ${timeoutMs}ms`,
|
|
484
|
+
exitCode: 1
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
openExclusive(path) {
|
|
488
|
+
try {
|
|
489
|
+
const fs = __require("fs");
|
|
490
|
+
const fd = fs.openSync(path, "wx");
|
|
491
|
+
return fd;
|
|
492
|
+
} catch {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
releaseLocalLock(handle) {
|
|
497
|
+
try {
|
|
498
|
+
const fs = __require("fs");
|
|
499
|
+
fs.closeSync(handle.fd);
|
|
500
|
+
fs.unlinkSync(handle.path);
|
|
501
|
+
return ok(void 0);
|
|
502
|
+
} catch (e) {
|
|
503
|
+
return err({
|
|
504
|
+
code: "STATE_ERROR",
|
|
505
|
+
message: `Failed to release lock: ${String(e)}`,
|
|
506
|
+
exitCode: 1
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
function createStateManager(root) {
|
|
512
|
+
return new StateManager(root);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/core/canonical.service.ts
|
|
516
|
+
import { join as join2 } from "path";
|
|
517
|
+
var CanonicalService = class {
|
|
518
|
+
constructor(state, root) {
|
|
519
|
+
this.state = state;
|
|
520
|
+
this.root = root;
|
|
521
|
+
}
|
|
522
|
+
state;
|
|
523
|
+
root;
|
|
524
|
+
checkCanonicalAlignment(taskBoard, derivedDocs, mother, relicTaskIds = [], assignmentTaskIds = []) {
|
|
525
|
+
const issues = [];
|
|
526
|
+
const tasks = taskBoard.tasks;
|
|
527
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
528
|
+
const openTasks = tasks.filter((t) => t.status !== "done");
|
|
529
|
+
const statusBoardDoc = derivedDocs.find((d) => d.type === "status-board");
|
|
530
|
+
for (const doc of derivedDocs) {
|
|
531
|
+
const fullPath = join2(this.root, doc.path);
|
|
532
|
+
const content = this.state.readText(fullPath);
|
|
533
|
+
if (!content.ok || !content.value) {
|
|
534
|
+
issues.push({
|
|
535
|
+
file: doc.path,
|
|
536
|
+
severity: "blocker",
|
|
537
|
+
reason: `${doc.path} is missing or empty.`
|
|
538
|
+
});
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const text = content.value;
|
|
542
|
+
for (const taskId of relicTaskIds) {
|
|
543
|
+
if (this.hasLegacyClosureConflict(text, taskId, taskById)) {
|
|
544
|
+
issues.push({
|
|
545
|
+
file: doc.path,
|
|
546
|
+
severity: "blocker",
|
|
547
|
+
reason: `${doc.path} still reports stale ${taskId} no-closure state, but task-board marks it done.`
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const taskId of assignmentTaskIds) {
|
|
552
|
+
if (this.hasAssignmentConflict(text, taskId, taskById)) {
|
|
553
|
+
const task = taskById.get(taskId);
|
|
554
|
+
issues.push({
|
|
555
|
+
file: doc.path,
|
|
556
|
+
severity: "blocker",
|
|
557
|
+
reason: `${doc.path} assigns ${taskId} away from canonical owner ${task?.owner}.`
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (statusBoardDoc) {
|
|
563
|
+
const statusPath = join2(this.root, statusBoardDoc.path);
|
|
564
|
+
const statusText = this.state.readText(statusPath);
|
|
565
|
+
if (statusText.ok && statusText.value) {
|
|
566
|
+
for (const task of openTasks) {
|
|
567
|
+
if (!statusText.value.includes(task.id)) {
|
|
568
|
+
issues.push({
|
|
569
|
+
file: statusBoardDoc.path,
|
|
570
|
+
severity: "warn",
|
|
571
|
+
reason: `Status board does not include open task ${task.id}: ${task.title}.`
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
aligned: issues.filter((i) => i.severity === "blocker").length === 0,
|
|
579
|
+
issues,
|
|
580
|
+
boardUpdatedAt: taskBoard.updatedAt
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
regenerateAllDerivedDocs(taskBoard, derivedDocs, mother, projectName) {
|
|
584
|
+
for (const doc of derivedDocs) {
|
|
585
|
+
let content;
|
|
586
|
+
if (doc.type === "central") {
|
|
587
|
+
content = this.generateCentral(taskBoard, mother, projectName, doc.extra);
|
|
588
|
+
} else if (doc.type === "collaborator" && doc.filter?.owner) {
|
|
589
|
+
content = this.generateCollaborator(taskBoard, doc.filter.owner, projectName);
|
|
590
|
+
} else if (doc.type === "status-board") {
|
|
591
|
+
content = this.generateStatusBoard(taskBoard, projectName);
|
|
592
|
+
} else {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const fullPath = join2(this.root, doc.path);
|
|
596
|
+
const writeResult = this.state.writeText(fullPath, content);
|
|
597
|
+
if (!writeResult.ok) return writeResult;
|
|
598
|
+
}
|
|
599
|
+
return ok(void 0);
|
|
600
|
+
}
|
|
601
|
+
generateCentral(taskBoard, mother, projectName, extra) {
|
|
602
|
+
const openTasks = taskBoard.tasks.filter((t) => t.status !== "done");
|
|
603
|
+
const readyTasks = openTasks.filter((t) => t.status === "ready");
|
|
604
|
+
const pendingTasks = openTasks.filter((t) => t.status === "pending");
|
|
605
|
+
const taskRows = (list) => list.length === 0 ? "| Ninguno | - | - | - |\n" : list.map((t) => `| ${t.id} | ${t.status} | ${t.owner} | ${t.title} | ${t.dependsOn?.join(", ") || "-"} |`).join("\n");
|
|
606
|
+
const extraFields = extra ? Object.entries(extra).map(([k, v]) => `| ${k} | ${v} |`).join("\n") : "";
|
|
607
|
+
return [
|
|
608
|
+
"---",
|
|
609
|
+
`type: master-dashboard`,
|
|
610
|
+
`project: "${projectName}"`,
|
|
611
|
+
`status: active-production`,
|
|
612
|
+
`phase: "Canonical Oreshnik task board governs current assignments"`,
|
|
613
|
+
`last_updated: "${this.nowISO()}"`,
|
|
614
|
+
`mother_branch: "${mother.current || "unknown"}"`,
|
|
615
|
+
"tags:",
|
|
616
|
+
' - "#central"',
|
|
617
|
+
' - "#status/live-source"',
|
|
618
|
+
"---",
|
|
619
|
+
"",
|
|
620
|
+
`# ${projectName} - Dashboard Canonico`,
|
|
621
|
+
"",
|
|
622
|
+
"> Fuente operativa: `var/oreshnik/task-board.json`. Los documentos de colaborador y status son derivados y deben ser regenerados si cambian las asignaciones.",
|
|
623
|
+
"",
|
|
624
|
+
"## Estado Actual",
|
|
625
|
+
"",
|
|
626
|
+
"| Campo | Valor |",
|
|
627
|
+
"|---|---|",
|
|
628
|
+
`| Task board actualizado | ${taskBoard.updatedAt || "unknown"} |`,
|
|
629
|
+
`| Rama madre | ${mother.current || "unknown"} |`,
|
|
630
|
+
extraFields,
|
|
631
|
+
"",
|
|
632
|
+
"## Orden de Ejecucion",
|
|
633
|
+
"",
|
|
634
|
+
...taskBoard.currentExecutionOrder.map((item) => `- ${item}`),
|
|
635
|
+
"",
|
|
636
|
+
"## Tareas Abiertas",
|
|
637
|
+
"",
|
|
638
|
+
"| Sprint | Estado | Owner | Scope | Depende de |",
|
|
639
|
+
"|---|---|---|---|---|",
|
|
640
|
+
taskRows(openTasks),
|
|
641
|
+
"",
|
|
642
|
+
"## Ready Ahora",
|
|
643
|
+
"",
|
|
644
|
+
"| Sprint | Owner | Scope |",
|
|
645
|
+
"|---|---|---|",
|
|
646
|
+
readyTasks.length ? readyTasks.map((t) => `| ${t.id} | ${t.owner} | ${t.title} |`).join("\n") : "| Ninguno | - | - |",
|
|
647
|
+
"",
|
|
648
|
+
"## Pendientes Bloqueados por Dependencias",
|
|
649
|
+
"",
|
|
650
|
+
"| Sprint | Owner | Scope | Depende de |",
|
|
651
|
+
"|---|---|---|---|",
|
|
652
|
+
pendingTasks.length ? pendingTasks.map((t) => `| ${t.id} | ${t.owner} | ${t.title} | ${t.dependsOn?.join(", ") || "-"} |`).join("\n") : "| Ninguno | - | - | - |",
|
|
653
|
+
""
|
|
654
|
+
].join("\n");
|
|
655
|
+
}
|
|
656
|
+
generateCollaborator(taskBoard, owner, projectName) {
|
|
657
|
+
const assigned = taskBoard.tasks.filter(
|
|
658
|
+
(t) => t.status !== "done" && (t.owner === owner || String(t.owner).includes(`${owner}+`) || String(t.owner).includes(`+${owner}`))
|
|
659
|
+
);
|
|
660
|
+
const ready = assigned.filter((t) => t.status === "ready");
|
|
661
|
+
const pending = assigned.filter((t) => t.status === "pending");
|
|
662
|
+
const acceptanceRows = (task) => (task.acceptance || []).map((item) => `- ${item}`).join("\n");
|
|
663
|
+
const taskRows = (list) => list.length === 0 ? "| Ninguno | - | - |" : list.map((t) => `| ${t.id} | ${t.title} | ${t.dependsOn?.join(", ") || "-"} |`).join("\n");
|
|
664
|
+
return [
|
|
665
|
+
"---",
|
|
666
|
+
`type: collaborator-status`,
|
|
667
|
+
`project: "${projectName}"`,
|
|
668
|
+
`operator: "${owner}"`,
|
|
669
|
+
`last_updated: "${this.nowISO()}"`,
|
|
670
|
+
`generated_by: "Oreshnik canonical-check"`,
|
|
671
|
+
`source: "var/oreshnik/task-board.json"`,
|
|
672
|
+
"---",
|
|
673
|
+
"",
|
|
674
|
+
`# Estado ${owner}`,
|
|
675
|
+
"",
|
|
676
|
+
"> Documento derivado. La fuente operativa es `var/oreshnik/task-board.json`.",
|
|
677
|
+
"",
|
|
678
|
+
"## Ready",
|
|
679
|
+
"",
|
|
680
|
+
"| Sprint | Scope | Depende de |",
|
|
681
|
+
"|---|---|---|",
|
|
682
|
+
taskRows(ready),
|
|
683
|
+
"",
|
|
684
|
+
"## Pending",
|
|
685
|
+
"",
|
|
686
|
+
"| Sprint | Scope | Depende de |",
|
|
687
|
+
"|---|---|---|",
|
|
688
|
+
taskRows(pending),
|
|
689
|
+
"",
|
|
690
|
+
"## Detalle de Aceptacion",
|
|
691
|
+
"",
|
|
692
|
+
assigned.length ? assigned.map(
|
|
693
|
+
(t) => `### ${t.id} - ${t.title}
|
|
694
|
+
|
|
695
|
+
Estado: \`${t.status}\`
|
|
696
|
+
|
|
697
|
+
${acceptanceRows(t)}
|
|
698
|
+
|
|
699
|
+
Zonas: ${(t.zone || []).map((z2) => `\`${z2}\``).join(", ") || "-"}
|
|
700
|
+
`
|
|
701
|
+
).join("\n") : "Sin tareas abiertas asignadas.",
|
|
702
|
+
""
|
|
703
|
+
].join("\n");
|
|
704
|
+
}
|
|
705
|
+
generateStatusBoard(taskBoard, projectName) {
|
|
706
|
+
const openTasks = taskBoard.tasks.filter((t) => t.status !== "done");
|
|
707
|
+
const doneTasks = taskBoard.tasks.filter((t) => t.status === "done");
|
|
708
|
+
const ownerRows = (owner) => {
|
|
709
|
+
const owned = openTasks.filter((t) => t.owner === owner || String(t.owner).includes(`${owner}+`) || String(t.owner).includes(`+${owner}`));
|
|
710
|
+
return owned.length ? owned.map((t) => `| ${t.id} | ${t.status} | ${t.title} | ${t.dependsOn?.join(", ") || "-"} |`).join("\n") : "| Ninguno | - | - | - |";
|
|
711
|
+
};
|
|
712
|
+
return [
|
|
713
|
+
"---",
|
|
714
|
+
`type: status-board`,
|
|
715
|
+
`project: "${projectName}"`,
|
|
716
|
+
`last_updated: "${this.nowISO()}"`,
|
|
717
|
+
`generated_by: "Oreshnik canonical-check"`,
|
|
718
|
+
`source: "var/oreshnik/task-board.json"`,
|
|
719
|
+
"---",
|
|
720
|
+
"",
|
|
721
|
+
"# STATUS BOARD - Realidad Canonica del Repositorio",
|
|
722
|
+
"",
|
|
723
|
+
"> Fuente operativa: `var/oreshnik/task-board.json`. Si este documento contradice el task board, el preflight debe bloquear.",
|
|
724
|
+
"",
|
|
725
|
+
"## Orden de Ejecucion Actual",
|
|
726
|
+
"",
|
|
727
|
+
...taskBoard.currentExecutionOrder.map((item) => `- ${item}`),
|
|
728
|
+
"",
|
|
729
|
+
"## Tareas Ready/Pending",
|
|
730
|
+
"",
|
|
731
|
+
"| Sprint | Estado | Owner | Scope | Depende de |",
|
|
732
|
+
"|---|---|---|---|---|",
|
|
733
|
+
openTasks.map((t) => `| ${t.id} | ${t.status} | ${t.owner} | ${t.title} | ${t.dependsOn?.join(", ") || "-"} |`).join("\n"),
|
|
734
|
+
"",
|
|
735
|
+
"## Hard Stops Vigentes",
|
|
736
|
+
"",
|
|
737
|
+
"- No credenciales en git.",
|
|
738
|
+
"- No sprint closure sin vault, handoff y validaciones.",
|
|
739
|
+
"",
|
|
740
|
+
"## Sprints Cerrados Segun Task Board",
|
|
741
|
+
"",
|
|
742
|
+
"| Sprint | Owner | Scope |",
|
|
743
|
+
"|---|---|---|",
|
|
744
|
+
doneTasks.map((t) => `| ${t.id} | ${t.owner} | ${t.title} |`).join("\n"),
|
|
745
|
+
""
|
|
746
|
+
].join("\n");
|
|
747
|
+
}
|
|
748
|
+
hasLegacyClosureConflict(content, taskId, taskById) {
|
|
749
|
+
const task = taskById.get(taskId);
|
|
750
|
+
if (task?.status !== "done") return false;
|
|
751
|
+
const pattern = new RegExp(
|
|
752
|
+
`${taskId}[\\s\\S]{0,700}(SIN CLOSURE|sin closure|NO EXISTE|CERO commits|sin codigo visible|no code visible)`,
|
|
753
|
+
"i"
|
|
754
|
+
);
|
|
755
|
+
return pattern.test(content);
|
|
756
|
+
}
|
|
757
|
+
hasAssignmentConflict(content, taskId, taskById) {
|
|
758
|
+
const task = taskById.get(taskId);
|
|
759
|
+
if (!task || task.status === "done") return false;
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
nowISO() {
|
|
763
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function createCanonicalService(state, root) {
|
|
767
|
+
return new CanonicalService(state, root);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/core/sync.service.ts
|
|
771
|
+
import { join as join3 } from "path";
|
|
772
|
+
var SyncService = class {
|
|
773
|
+
constructor(git, state, canonical, root) {
|
|
774
|
+
this.git = git;
|
|
775
|
+
this.state = state;
|
|
776
|
+
this.canonical = canonical;
|
|
777
|
+
this.root = root;
|
|
778
|
+
}
|
|
779
|
+
git;
|
|
780
|
+
state;
|
|
781
|
+
canonical;
|
|
782
|
+
root;
|
|
783
|
+
syncLatestMother(args) {
|
|
784
|
+
const result = {
|
|
785
|
+
success: false,
|
|
786
|
+
merged: false,
|
|
787
|
+
latestMother: "",
|
|
788
|
+
conflicts: [],
|
|
789
|
+
autoResolved: [],
|
|
790
|
+
manualRequired: []
|
|
791
|
+
};
|
|
792
|
+
const taskBoardRel = "var/oreshnik/task-board.json";
|
|
793
|
+
const derivedDocPaths = args.derivedDocPaths.map((p) => this.toGitPath(p));
|
|
794
|
+
const autoConflicts = /* @__PURE__ */ new Set([
|
|
795
|
+
...(args.canonicalAutoConflicts || []).map((p) => this.toGitPath(p)),
|
|
796
|
+
...derivedDocPaths,
|
|
797
|
+
taskBoardRel
|
|
798
|
+
]);
|
|
799
|
+
const fetchResult = this.git.fetch();
|
|
800
|
+
if (!fetchResult.ok) {
|
|
801
|
+
result.conflicts.push(`Fetch failed: ${fetchResult.error.message}`);
|
|
802
|
+
return result;
|
|
803
|
+
}
|
|
804
|
+
const motherResult = this.readMother();
|
|
805
|
+
if (!motherResult.ok) return result;
|
|
806
|
+
const currentMother = motherResult.value;
|
|
807
|
+
const latest = this.git.discoverLatestMother(args.motherPrefix);
|
|
808
|
+
if (!latest.ok || !latest.value) {
|
|
809
|
+
result.success = true;
|
|
810
|
+
result.latestMother = currentMother.current;
|
|
811
|
+
return result;
|
|
812
|
+
}
|
|
813
|
+
const latestRef = this.git.refExists(`origin/${latest.value.name}`) ? `origin/${latest.value.name}` : latest.value.name;
|
|
814
|
+
result.latestMother = latest.value.name;
|
|
815
|
+
if (latest.value.version <= (currentMother.version || 0)) {
|
|
816
|
+
result.success = true;
|
|
817
|
+
result.latestMother = currentMother.current;
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
const dirty = this.git.statusPorcelain();
|
|
821
|
+
if (!dirty.ok) {
|
|
822
|
+
result.conflicts.push(`Failed to inspect working tree: ${dirty.error.message}`);
|
|
823
|
+
return result;
|
|
824
|
+
}
|
|
825
|
+
const dirtyFiles = dirty.value.filter((e) => !this.toGitPath(e.path).startsWith("var/oreshnik/") && !this.toGitPath(e.path).startsWith("output/")).map((e) => this.toGitPath(e.path));
|
|
826
|
+
if (dirtyFiles.length > 0) {
|
|
827
|
+
result.conflicts.push(`Working tree has ${dirtyFiles.length} changed file(s).`);
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
830
|
+
const merge = this.git.merge(latestRef, {
|
|
831
|
+
noFF: true,
|
|
832
|
+
message: `chore(oreshnik): sync from ${latest.value.name}`
|
|
833
|
+
});
|
|
834
|
+
if (merge.ok) {
|
|
835
|
+
result.success = true;
|
|
836
|
+
result.merged = true;
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
const conflictFiles = this.git.unmergedFiles();
|
|
840
|
+
if (!conflictFiles.ok) {
|
|
841
|
+
result.conflicts.push(`Failed to inspect merge conflicts: ${conflictFiles.error.message}`);
|
|
842
|
+
return result;
|
|
843
|
+
}
|
|
844
|
+
const conflicts = conflictFiles.value.map((p) => this.toGitPath(p));
|
|
845
|
+
const manual = conflicts.filter((f) => !autoConflicts.has(f));
|
|
846
|
+
if (manual.length > 0) {
|
|
847
|
+
result.manualRequired = manual;
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
const taskBoardPath = taskBoardRel;
|
|
851
|
+
if (conflicts.includes(taskBoardRel)) {
|
|
852
|
+
const mergeResult = this.mergeTaskBoardById(taskBoardPath);
|
|
853
|
+
if (!mergeResult.ok) {
|
|
854
|
+
result.conflicts.push(mergeResult.error.message);
|
|
855
|
+
return result;
|
|
856
|
+
}
|
|
857
|
+
result.autoResolved.push(taskBoardRel);
|
|
858
|
+
}
|
|
859
|
+
const motherVersion = this.readMother();
|
|
860
|
+
const mother = motherVersion.ok ? motherVersion.value : { version: 0, current: "unknown", branches: [] };
|
|
861
|
+
const boardData = this.state.readJson(taskBoardPath);
|
|
862
|
+
const board = boardData.ok ? boardData.value : { project: args.projectName, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), currentExecutionOrder: [], tasks: [] };
|
|
863
|
+
const regenResult = this.canonical.regenerateAllDerivedDocs(
|
|
864
|
+
board,
|
|
865
|
+
args.derivedDocs.map((d) => ({ ...d, source: "task-board" })),
|
|
866
|
+
mother,
|
|
867
|
+
args.projectName
|
|
868
|
+
);
|
|
869
|
+
if (!regenResult.ok) {
|
|
870
|
+
result.conflicts.push(`Derived doc regeneration failed: ${regenResult.error.message}`);
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
const stageFiles = [taskBoardRel, ...derivedDocPaths];
|
|
874
|
+
const stageResult = this.git.stage(stageFiles);
|
|
875
|
+
if (!stageResult.ok) {
|
|
876
|
+
result.conflicts.push(`Failed to stage auto-resolved files: ${stageResult.error.message}`);
|
|
877
|
+
return result;
|
|
878
|
+
}
|
|
879
|
+
result.autoResolved.push(...derivedDocPaths);
|
|
880
|
+
const remaining = this.git.unmergedFiles();
|
|
881
|
+
if (!remaining.ok) {
|
|
882
|
+
result.conflicts.push(`Failed to inspect remaining merge conflicts: ${remaining.error.message}`);
|
|
883
|
+
return result;
|
|
884
|
+
}
|
|
885
|
+
if (remaining.value.length > 0) {
|
|
886
|
+
result.manualRequired = remaining.value.map((p) => this.toGitPath(p));
|
|
887
|
+
return result;
|
|
888
|
+
}
|
|
889
|
+
const commitResult = this.git.commit("chore(oreshnik): auto-resolve canonical conflicts");
|
|
890
|
+
if (!commitResult.ok) {
|
|
891
|
+
result.conflicts.push(`Failed to commit auto-resolved conflicts: ${commitResult.error.message}`);
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
result.success = true;
|
|
895
|
+
result.merged = true;
|
|
896
|
+
return result;
|
|
897
|
+
}
|
|
898
|
+
mergeTaskBoardById(fileRel) {
|
|
899
|
+
const fullPath = join3(this.root, ...fileRel.split("/"));
|
|
900
|
+
const oursRaw = this.git.showRef(":2", fileRel);
|
|
901
|
+
const theirsRaw = this.git.showRef(":3", fileRel);
|
|
902
|
+
if (!oursRaw.ok && !theirsRaw.ok) {
|
|
903
|
+
return err({ code: "STATE_ERROR", message: `Cannot read conflict stages for ${fileRel}`, exitCode: 1 });
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const ours = oursRaw.ok && oursRaw.value ? JSON.parse(oursRaw.value.replace(/^\uFEFF/, "")) : { project: "", updatedAt: "", currentExecutionOrder: [], tasks: [] };
|
|
907
|
+
const theirs = theirsRaw.ok && theirsRaw.value ? JSON.parse(theirsRaw.value.replace(/^\uFEFF/, "")) : { project: "", updatedAt: "", currentExecutionOrder: [], tasks: [] };
|
|
908
|
+
const merged = {
|
|
909
|
+
project: theirs.project || ours.project,
|
|
910
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
911
|
+
currentExecutionOrder: theirs.currentExecutionOrder || ours.currentExecutionOrder,
|
|
912
|
+
tasks: structuredClone(theirs.tasks),
|
|
913
|
+
reassignments: this.mergeUniqueReassignments(ours.reassignments || [], theirs.reassignments || [])
|
|
914
|
+
};
|
|
915
|
+
const mergedIds = new Set(merged.tasks.map((t) => t.id));
|
|
916
|
+
const oursOnly = [];
|
|
917
|
+
for (const task of ours.tasks || []) {
|
|
918
|
+
if (!mergedIds.has(task.id)) {
|
|
919
|
+
const releaseIdx = merged.tasks.findIndex((t) => t.id.includes("RELEASE"));
|
|
920
|
+
if (releaseIdx >= 0) merged.tasks.splice(releaseIdx, 0, task);
|
|
921
|
+
else merged.tasks.push(task);
|
|
922
|
+
mergedIds.add(task.id);
|
|
923
|
+
oursOnly.push(task.id);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const theirsOnly = (theirs.tasks || []).filter((t) => !new Set((ours.tasks || []).map((o) => o.id)).has(t.id)).map((t) => t.id);
|
|
927
|
+
const writeResult = this.state.writeJson(fullPath, merged);
|
|
928
|
+
if (!writeResult.ok) return writeResult;
|
|
929
|
+
return ok({ merged, oursOnly, theirsOnly });
|
|
930
|
+
} catch (e) {
|
|
931
|
+
return err({ code: "STATE_ERROR", message: `Failed to merge task board: ${String(e)}`, exitCode: 1 });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
mergeUniqueReassignments(a, b) {
|
|
935
|
+
const seen = /* @__PURE__ */ new Set();
|
|
936
|
+
return [...a, ...b].filter((item) => {
|
|
937
|
+
const key = JSON.stringify(item);
|
|
938
|
+
if (seen.has(key)) return false;
|
|
939
|
+
seen.add(key);
|
|
940
|
+
return true;
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
readMother() {
|
|
944
|
+
const path = join3(this.root, "var", "oreshnik", ".mother-version.json");
|
|
945
|
+
return this.state.readJson(path);
|
|
946
|
+
}
|
|
947
|
+
runsDir(file) {
|
|
948
|
+
return `var/oreshnik/${file}`;
|
|
949
|
+
}
|
|
950
|
+
toGitPath(path) {
|
|
951
|
+
return path.replace(/\\/g, "/");
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
function createSyncService(git, state, canonical, root) {
|
|
955
|
+
return new SyncService(git, state, canonical, root);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/core/vault-guard.ts
|
|
959
|
+
var VaultGuard = class {
|
|
960
|
+
constructor(git) {
|
|
961
|
+
this.git = git;
|
|
962
|
+
}
|
|
963
|
+
git;
|
|
964
|
+
check(vaultPath, obsidianConfigDir, force = false) {
|
|
965
|
+
const result = {
|
|
966
|
+
clean: true,
|
|
967
|
+
configDirty: [],
|
|
968
|
+
configRestored: false,
|
|
969
|
+
contentDirty: [],
|
|
970
|
+
unstagedStagedOverlap: [],
|
|
971
|
+
untracked: [],
|
|
972
|
+
errors: []
|
|
973
|
+
};
|
|
974
|
+
const configDirty = this.gitDirtyFiles(`${obsidianConfigDir}/`, vaultPath);
|
|
975
|
+
if (!configDirty.ok) {
|
|
976
|
+
result.clean = false;
|
|
977
|
+
result.errors.push(configDirty.error.message);
|
|
978
|
+
} else if (configDirty.value.length > 0) {
|
|
979
|
+
result.configDirty = configDirty.value;
|
|
980
|
+
if (force) {
|
|
981
|
+
const restore = this.git.restoreFromHead(`${obsidianConfigDir}/`);
|
|
982
|
+
if (restore.ok) {
|
|
983
|
+
result.configRestored = true;
|
|
984
|
+
} else {
|
|
985
|
+
result.clean = false;
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
result.clean = false;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const vaultDirty = this.gitDirtyFiles(`${vaultPath}/`, vaultPath, `:!${obsidianConfigDir}`);
|
|
992
|
+
if (!vaultDirty.ok) {
|
|
993
|
+
result.clean = false;
|
|
994
|
+
result.errors.push(vaultDirty.error.message);
|
|
995
|
+
} else if (vaultDirty.value.length > 0) {
|
|
996
|
+
result.contentDirty = vaultDirty.value;
|
|
997
|
+
}
|
|
998
|
+
const unstaged = this.gitDiffFiles(`${vaultPath}/`);
|
|
999
|
+
const staged = this.gitDiffFiles(`${vaultPath}/`, true);
|
|
1000
|
+
if (!unstaged.ok) {
|
|
1001
|
+
result.clean = false;
|
|
1002
|
+
result.errors.push(unstaged.error.message);
|
|
1003
|
+
}
|
|
1004
|
+
if (!staged.ok) {
|
|
1005
|
+
result.clean = false;
|
|
1006
|
+
result.errors.push(staged.error.message);
|
|
1007
|
+
}
|
|
1008
|
+
if (unstaged.ok && staged.ok && unstaged.value.length > 0 && staged.value.length > 0) {
|
|
1009
|
+
const both = unstaged.value.filter((f) => staged.value.includes(f));
|
|
1010
|
+
if (both.length > 0) {
|
|
1011
|
+
result.unstagedStagedOverlap = both;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const untracked = this.gitUntrackedFiles(`${obsidianConfigDir}/`);
|
|
1015
|
+
if (!untracked.ok) {
|
|
1016
|
+
result.clean = false;
|
|
1017
|
+
result.errors.push(untracked.error.message);
|
|
1018
|
+
} else if (untracked.value.length > 0) {
|
|
1019
|
+
result.untracked = untracked.value;
|
|
1020
|
+
}
|
|
1021
|
+
return result;
|
|
1022
|
+
}
|
|
1023
|
+
restoreConfig(obsidianConfigDir) {
|
|
1024
|
+
return this.git.restoreFromHead(`${obsidianConfigDir}/`);
|
|
1025
|
+
}
|
|
1026
|
+
gitDirtyFiles(pattern, _vaultPath, excludePattern) {
|
|
1027
|
+
const args = ["diff", "--name-only", "--", pattern];
|
|
1028
|
+
if (excludePattern) args.push(excludePattern);
|
|
1029
|
+
const r = this.git.exec(args);
|
|
1030
|
+
if (!r.ok) {
|
|
1031
|
+
return {
|
|
1032
|
+
ok: false,
|
|
1033
|
+
error: {
|
|
1034
|
+
code: "GIT_ERROR",
|
|
1035
|
+
message: `Failed to inspect dirty files for ${pattern}`,
|
|
1036
|
+
exitCode: 1,
|
|
1037
|
+
gitCommand: `git ${args.join(" ")}`,
|
|
1038
|
+
gitStderr: r.error
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
return { ok: true, value: r.output.split(/\r?\n/).filter(Boolean) };
|
|
1043
|
+
}
|
|
1044
|
+
gitDiffFiles(pattern, cached = false) {
|
|
1045
|
+
const args = ["diff", "--name-only"];
|
|
1046
|
+
if (cached) args.push("--cached");
|
|
1047
|
+
args.push("--", pattern);
|
|
1048
|
+
const r = this.git.exec(args);
|
|
1049
|
+
if (!r.ok) {
|
|
1050
|
+
return {
|
|
1051
|
+
ok: false,
|
|
1052
|
+
error: {
|
|
1053
|
+
code: "GIT_ERROR",
|
|
1054
|
+
message: `Failed to inspect diff files for ${pattern}`,
|
|
1055
|
+
exitCode: 1,
|
|
1056
|
+
gitCommand: `git ${args.join(" ")}`,
|
|
1057
|
+
gitStderr: r.error
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
return { ok: true, value: r.output.split(/\r?\n/).filter(Boolean) };
|
|
1062
|
+
}
|
|
1063
|
+
gitUntrackedFiles(pattern) {
|
|
1064
|
+
const r = this.git.exec(["ls-files", "--others", "--exclude-standard", "--", pattern]);
|
|
1065
|
+
if (!r.ok) {
|
|
1066
|
+
return {
|
|
1067
|
+
ok: false,
|
|
1068
|
+
error: {
|
|
1069
|
+
code: "GIT_ERROR",
|
|
1070
|
+
message: `Failed to inspect untracked files for ${pattern}`,
|
|
1071
|
+
exitCode: 1,
|
|
1072
|
+
gitCommand: `git ls-files --others --exclude-standard -- ${pattern}`,
|
|
1073
|
+
gitStderr: r.error
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
return { ok: true, value: r.output.split(/\r?\n/).filter(Boolean) };
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
function createVaultGuard(git) {
|
|
1081
|
+
return new VaultGuard(git);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/core/portfolio.service.ts
|
|
1085
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1086
|
+
import { isAbsolute, join as join4, resolve } from "path";
|
|
1087
|
+
|
|
1088
|
+
// src/types/schemas.ts
|
|
1089
|
+
import { z } from "zod";
|
|
1090
|
+
var TaskHistoryEntrySchema = z.object({
|
|
1091
|
+
at: z.string(),
|
|
1092
|
+
action: z.string(),
|
|
1093
|
+
operator: z.string().optional(),
|
|
1094
|
+
from: z.string().optional(),
|
|
1095
|
+
to: z.string().optional(),
|
|
1096
|
+
reason: z.string().optional(),
|
|
1097
|
+
branch: z.string().optional(),
|
|
1098
|
+
description: z.string().optional()
|
|
1099
|
+
});
|
|
1100
|
+
var TaskSchema = z.object({
|
|
1101
|
+
id: z.string().min(1).max(120),
|
|
1102
|
+
title: z.string().min(1).max(500),
|
|
1103
|
+
owner: z.string().min(1).max(50),
|
|
1104
|
+
backupOwner: z.string().optional(),
|
|
1105
|
+
status: z.enum(["ready", "active", "pending", "blocked", "done", "rolled_back"]),
|
|
1106
|
+
track: z.string().optional(),
|
|
1107
|
+
zone: z.array(z.string()).optional(),
|
|
1108
|
+
dependsOn: z.array(z.string()).optional(),
|
|
1109
|
+
acceptance: z.array(z.string()).optional(),
|
|
1110
|
+
handoff: z.string().optional(),
|
|
1111
|
+
history: z.array(TaskHistoryEntrySchema).optional()
|
|
1112
|
+
});
|
|
1113
|
+
var ReassignmentSchema = z.object({
|
|
1114
|
+
at: z.string(),
|
|
1115
|
+
task: z.string(),
|
|
1116
|
+
from: z.string(),
|
|
1117
|
+
to: z.string(),
|
|
1118
|
+
reason: z.string()
|
|
1119
|
+
});
|
|
1120
|
+
var TaskBoardSchema = z.object({
|
|
1121
|
+
project: z.string(),
|
|
1122
|
+
updatedAt: z.string(),
|
|
1123
|
+
resiliencePolicy: z.string().optional(),
|
|
1124
|
+
closurePolicy: z.string().optional(),
|
|
1125
|
+
baseTestMatrix: z.array(z.string()).optional(),
|
|
1126
|
+
currentExecutionOrder: z.array(z.string()),
|
|
1127
|
+
tasks: z.array(TaskSchema),
|
|
1128
|
+
reassignments: z.array(ReassignmentSchema).optional()
|
|
1129
|
+
});
|
|
1130
|
+
var MotherBranchEntrySchema = z.object({
|
|
1131
|
+
version: z.number(),
|
|
1132
|
+
name: z.string(),
|
|
1133
|
+
sprint: z.string(),
|
|
1134
|
+
operator: z.string(),
|
|
1135
|
+
date: z.string(),
|
|
1136
|
+
at: z.string(),
|
|
1137
|
+
previous: z.string(),
|
|
1138
|
+
description: z.string()
|
|
1139
|
+
});
|
|
1140
|
+
var MotherVersionSchema = z.object({
|
|
1141
|
+
version: z.number(),
|
|
1142
|
+
current: z.string(),
|
|
1143
|
+
branches: z.array(MotherBranchEntrySchema)
|
|
1144
|
+
});
|
|
1145
|
+
var ZoneEntrySchema = z.object({
|
|
1146
|
+
owner: z.string(),
|
|
1147
|
+
lock: z.enum(["operator_exclusive", "operator_double", "shared", "forbidden", "owner_per_sprint"]),
|
|
1148
|
+
sprints: z.array(z.string()),
|
|
1149
|
+
criticality: z.enum(["low", "medium", "high", "critical"]).optional()
|
|
1150
|
+
});
|
|
1151
|
+
var ZoneMapSchema = z.object({
|
|
1152
|
+
zones: z.record(z.string(), ZoneEntrySchema)
|
|
1153
|
+
});
|
|
1154
|
+
var DerivedDocConfigSchema = z.object({
|
|
1155
|
+
path: z.string().min(1),
|
|
1156
|
+
type: z.enum(["central", "collaborator", "status-board"]),
|
|
1157
|
+
source: z.literal("task-board"),
|
|
1158
|
+
filter: z.object({ owner: z.string() }).optional(),
|
|
1159
|
+
extra: z.record(z.string()).optional()
|
|
1160
|
+
});
|
|
1161
|
+
var CanonicalIssueSchema = z.object({
|
|
1162
|
+
file: z.string(),
|
|
1163
|
+
severity: z.enum(["blocker", "warn"]),
|
|
1164
|
+
reason: z.string()
|
|
1165
|
+
});
|
|
1166
|
+
var GateDefinitionSchema = z.object({
|
|
1167
|
+
name: z.string().min(1).max(100),
|
|
1168
|
+
command: z.string().min(1).max(500),
|
|
1169
|
+
args: z.array(z.string()).optional(),
|
|
1170
|
+
timeoutSeconds: z.number().min(10).max(3600).default(300)
|
|
1171
|
+
});
|
|
1172
|
+
var OreshnikConfigSchema = z.object({
|
|
1173
|
+
version: z.literal(1),
|
|
1174
|
+
project: z.object({
|
|
1175
|
+
name: z.string().min(1).max(100),
|
|
1176
|
+
mainBranch: z.string().min(1).max(100).default("main")
|
|
1177
|
+
}),
|
|
1178
|
+
operators: z.array(z.object({
|
|
1179
|
+
id: z.string().regex(/^[a-z0-9_-]+$/),
|
|
1180
|
+
name: z.string().min(1).max(100),
|
|
1181
|
+
email: z.string().email().optional()
|
|
1182
|
+
})).min(1),
|
|
1183
|
+
branching: z.object({
|
|
1184
|
+
motherPrefix: z.string().default("MADRE"),
|
|
1185
|
+
childFormat: z.string().default("{operator}/{sprint}-{desc}-{date}"),
|
|
1186
|
+
integrationPrefix: z.string().default("integration")
|
|
1187
|
+
}).optional().default({}),
|
|
1188
|
+
validation: z.object({
|
|
1189
|
+
gates: z.array(GateDefinitionSchema).default([])
|
|
1190
|
+
}).optional().default({}),
|
|
1191
|
+
hardStops: z.object({
|
|
1192
|
+
forbiddenPatterns: z.array(z.string()).default([]),
|
|
1193
|
+
doubleLockPatterns: z.array(z.string()).default([])
|
|
1194
|
+
}).optional().default({}),
|
|
1195
|
+
vault: z.object({
|
|
1196
|
+
enabled: z.boolean().default(true),
|
|
1197
|
+
path: z.string().default("docs/obsidian-vault"),
|
|
1198
|
+
centralDoc: z.string().default("00_CENTRAL.md")
|
|
1199
|
+
}).optional().default({}),
|
|
1200
|
+
canonical: z.object({
|
|
1201
|
+
derivedDocs: z.array(DerivedDocConfigSchema).optional(),
|
|
1202
|
+
knownLegacyTasks: z.array(z.string()).optional(),
|
|
1203
|
+
knownAssignmentTasks: z.array(z.string()).optional()
|
|
1204
|
+
}).optional(),
|
|
1205
|
+
sync: z.object({
|
|
1206
|
+
canonicalAutoConflicts: z.array(z.string()).optional()
|
|
1207
|
+
}).optional(),
|
|
1208
|
+
checkpoints: z.object({
|
|
1209
|
+
autoOnClose: z.boolean().default(true),
|
|
1210
|
+
autoPreRollback: z.boolean().default(true),
|
|
1211
|
+
snapshotDir: z.string().default("var/oreshnik/checkpoints")
|
|
1212
|
+
}).optional().default({}),
|
|
1213
|
+
security: z.object({
|
|
1214
|
+
requireCleanTree: z.boolean().default(true),
|
|
1215
|
+
secretScanning: z.boolean().default(true),
|
|
1216
|
+
blockEnvDiffs: z.boolean().default(true)
|
|
1217
|
+
}).optional().default({})
|
|
1218
|
+
});
|
|
1219
|
+
var PortfolioProjectSchema = z.object({
|
|
1220
|
+
projectId: z.string().min(1).max(50).regex(/^[a-z0-9_-]+$/),
|
|
1221
|
+
displayName: z.string().min(1).max(100),
|
|
1222
|
+
repoPath: z.string().min(1),
|
|
1223
|
+
defaultBranch: z.string().min(1).max(100),
|
|
1224
|
+
operators: z.array(z.string().min(1).max(50)).default([]),
|
|
1225
|
+
taskBoardPath: z.string().min(1).default("var/oreshnik/task-board.json"),
|
|
1226
|
+
zoneMapPath: z.string().min(1).default("docs/07_handoffs/zone-map.json"),
|
|
1227
|
+
validationGates: z.array(GateDefinitionSchema).default([]),
|
|
1228
|
+
injectionPolicy: z.enum(["proposal_only", "direct_with_approval"]).default("proposal_only"),
|
|
1229
|
+
priority: z.enum(["low", "medium", "high", "critical"]).default("medium")
|
|
1230
|
+
});
|
|
1231
|
+
var PortfolioConfigSchema = z.object({
|
|
1232
|
+
version: z.literal(1),
|
|
1233
|
+
portfolio: z.object({
|
|
1234
|
+
id: z.string().min(1).max(50).regex(/^[a-z0-9_-]+$/),
|
|
1235
|
+
name: z.string().min(1).max(100),
|
|
1236
|
+
sourceNote: z.string().optional(),
|
|
1237
|
+
continuityDocPath: z.string().min(1).optional()
|
|
1238
|
+
}),
|
|
1239
|
+
projects: z.array(PortfolioProjectSchema).min(1)
|
|
1240
|
+
});
|
|
1241
|
+
var DistributedLockSchema = z.object({
|
|
1242
|
+
zone: z.string().min(1),
|
|
1243
|
+
owner: z.string().min(1),
|
|
1244
|
+
acquiredAt: z.string(),
|
|
1245
|
+
expiresAt: z.string(),
|
|
1246
|
+
ttlMinutes: z.number().min(1).max(1440),
|
|
1247
|
+
sprint: z.string().optional(),
|
|
1248
|
+
reason: z.string().min(1).max(500)
|
|
1249
|
+
});
|
|
1250
|
+
var CheckpointSchema = z.object({
|
|
1251
|
+
id: z.string(),
|
|
1252
|
+
tag: z.string(),
|
|
1253
|
+
timestamp: z.string(),
|
|
1254
|
+
operator: z.string(),
|
|
1255
|
+
sprint: z.string().optional(),
|
|
1256
|
+
type: z.enum(["auto", "manual", "pre-rollback"]),
|
|
1257
|
+
git: z.object({
|
|
1258
|
+
tag: z.string(),
|
|
1259
|
+
commit: z.string(),
|
|
1260
|
+
branch: z.string(),
|
|
1261
|
+
motherBranch: z.string().optional(),
|
|
1262
|
+
motherVersion: z.number().optional()
|
|
1263
|
+
}),
|
|
1264
|
+
state: z.object({
|
|
1265
|
+
taskBoard: TaskBoardSchema,
|
|
1266
|
+
motherVersion: MotherVersionSchema,
|
|
1267
|
+
workingTreeDirty: z.boolean(),
|
|
1268
|
+
stashRef: z.string().nullable()
|
|
1269
|
+
}),
|
|
1270
|
+
validation: z.object({
|
|
1271
|
+
typecheck: z.enum(["passed", "failed", "skipped"]).optional(),
|
|
1272
|
+
build: z.enum(["passed", "failed", "skipped"]).optional(),
|
|
1273
|
+
tests: z.string().optional(),
|
|
1274
|
+
zoneCheck: z.enum(["clean", "violations"]).optional(),
|
|
1275
|
+
canonicalCheck: z.enum(["aligned", "drift"]).optional()
|
|
1276
|
+
}).optional()
|
|
1277
|
+
});
|
|
1278
|
+
var SprintEventSchema = z.object({
|
|
1279
|
+
sprint: z.string(),
|
|
1280
|
+
operator: z.string(),
|
|
1281
|
+
type: z.enum(["created", "started", "checkpoint", "gate_passed", "gate_failed", "closed", "rolled_back", "reassigned"]),
|
|
1282
|
+
date: z.string(),
|
|
1283
|
+
at: z.string(),
|
|
1284
|
+
branch: z.string().optional(),
|
|
1285
|
+
previousMother: z.string().optional(),
|
|
1286
|
+
nextMother: z.string().optional(),
|
|
1287
|
+
description: z.string().optional(),
|
|
1288
|
+
changedFiles: z.array(z.string()).optional(),
|
|
1289
|
+
gateResults: z.record(z.enum(["passed", "failed"])).optional()
|
|
1290
|
+
});
|
|
1291
|
+
var SprintIdSchema = z.string().min(1).max(50).regex(
|
|
1292
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
1293
|
+
"Sprint ID must contain only alphanumeric characters, hyphens, and underscores"
|
|
1294
|
+
);
|
|
1295
|
+
var OperatorIdSchema = z.string().min(1).max(50).regex(
|
|
1296
|
+
/^[a-zA-Z0-9_-]+$/,
|
|
1297
|
+
"Operator ID must contain only alphanumeric characters, hyphens, and underscores"
|
|
1298
|
+
);
|
|
1299
|
+
var DescriptionSchema = z.string().min(1).max(500);
|
|
1300
|
+
var TagNameSchema = z.string().min(1).max(100).regex(
|
|
1301
|
+
/^[a-zA-Z0-9._/-]+$/,
|
|
1302
|
+
"Tag name must contain only alphanumeric characters, dots, underscores, hyphens, and slashes"
|
|
1303
|
+
);
|
|
1304
|
+
var VaultGuardResultSchema = z.object({
|
|
1305
|
+
clean: z.boolean(),
|
|
1306
|
+
configDirty: z.array(z.string()),
|
|
1307
|
+
configRestored: z.boolean(),
|
|
1308
|
+
contentDirty: z.array(z.string()),
|
|
1309
|
+
unstagedStagedOverlap: z.array(z.string()),
|
|
1310
|
+
untracked: z.array(z.string()),
|
|
1311
|
+
errors: z.array(z.string())
|
|
1312
|
+
});
|
|
1313
|
+
var NoteTaskSchema = z.object({
|
|
1314
|
+
rawLine: z.string(),
|
|
1315
|
+
lineNumber: z.number(),
|
|
1316
|
+
checked: z.boolean(),
|
|
1317
|
+
projectIds: z.array(z.string()),
|
|
1318
|
+
proposedType: z.enum(["feature", "bug", "qa", "docs", "architecture", "migration", "ops"]),
|
|
1319
|
+
proposedOwner: z.string(),
|
|
1320
|
+
proposedPriority: z.enum(["low", "medium", "high", "critical"]),
|
|
1321
|
+
proposedSprintPrefix: z.string(),
|
|
1322
|
+
evidenceType: z.enum(["code", "ui", "docs", "integration", "prod"]),
|
|
1323
|
+
evidenceExpectation: z.string()
|
|
1324
|
+
});
|
|
1325
|
+
var NotesIngestionResultSchema = z.object({
|
|
1326
|
+
sourcePath: z.string(),
|
|
1327
|
+
portfolioId: z.string(),
|
|
1328
|
+
parsedAt: z.string(),
|
|
1329
|
+
dryRun: z.boolean(),
|
|
1330
|
+
totalLines: z.number(),
|
|
1331
|
+
taskLines: z.number(),
|
|
1332
|
+
pendingCount: z.number(),
|
|
1333
|
+
doneCount: z.number(),
|
|
1334
|
+
tasksByProject: z.record(z.string(), z.array(NoteTaskSchema)),
|
|
1335
|
+
unclassifiedTasks: z.array(NoteTaskSchema),
|
|
1336
|
+
warnings: z.array(z.string())
|
|
1337
|
+
});
|
|
1338
|
+
var ProjectInjectionResultSchema = z.object({
|
|
1339
|
+
projectId: z.string(),
|
|
1340
|
+
displayName: z.string(),
|
|
1341
|
+
injectionPolicy: z.enum(["proposal_only", "direct_with_approval"]),
|
|
1342
|
+
repoPath: z.string(),
|
|
1343
|
+
tasksInjected: z.number(),
|
|
1344
|
+
tasksSkipped: z.number(),
|
|
1345
|
+
checkpointTag: z.string().optional(),
|
|
1346
|
+
handoffPath: z.string().optional(),
|
|
1347
|
+
proposalPath: z.string().optional(),
|
|
1348
|
+
blocked: z.boolean(),
|
|
1349
|
+
blockReason: z.string().optional(),
|
|
1350
|
+
warnings: z.array(z.string())
|
|
1351
|
+
});
|
|
1352
|
+
var InjectionResultSchema = z.object({
|
|
1353
|
+
portfolioId: z.string(),
|
|
1354
|
+
injectedAt: z.string(),
|
|
1355
|
+
dryRun: z.boolean(),
|
|
1356
|
+
confirmed: z.boolean(),
|
|
1357
|
+
projects: z.array(ProjectInjectionResultSchema),
|
|
1358
|
+
summary: z.object({
|
|
1359
|
+
totalProjects: z.number(),
|
|
1360
|
+
projectsBlocked: z.number(),
|
|
1361
|
+
projectsInjected: z.number(),
|
|
1362
|
+
proposalsGenerated: z.number(),
|
|
1363
|
+
totalTasksInjected: z.number(),
|
|
1364
|
+
totalTasksSkipped: z.number()
|
|
1365
|
+
}),
|
|
1366
|
+
errors: z.array(z.string())
|
|
1367
|
+
});
|
|
1368
|
+
var EvidenceRecordSchema = z.object({
|
|
1369
|
+
taskId: z.string(),
|
|
1370
|
+
evidenceType: z.enum(["code", "ui", "docs", "integration", "prod"]),
|
|
1371
|
+
validatedAt: z.string(),
|
|
1372
|
+
validator: z.string(),
|
|
1373
|
+
result: z.enum(["passed", "failed", "pending"]),
|
|
1374
|
+
details: z.string(),
|
|
1375
|
+
artifacts: z.array(z.string()).optional()
|
|
1376
|
+
});
|
|
1377
|
+
var TaskEvidenceCheckSchema = z.object({
|
|
1378
|
+
taskId: z.string(),
|
|
1379
|
+
taskTitle: z.string(),
|
|
1380
|
+
evidenceType: z.enum(["code", "ui", "docs", "integration", "prod"]),
|
|
1381
|
+
hasEvidence: z.boolean(),
|
|
1382
|
+
requiredEvidence: z.string(),
|
|
1383
|
+
currentStatus: z.enum(["ready", "active", "pending", "blocked", "done", "rolled_back"]),
|
|
1384
|
+
missing: z.array(z.string())
|
|
1385
|
+
});
|
|
1386
|
+
var EvidenceGateResultSchema = z.object({
|
|
1387
|
+
checkedAt: z.string(),
|
|
1388
|
+
sprint: z.string().optional(),
|
|
1389
|
+
operator: z.string(),
|
|
1390
|
+
totalTasks: z.number(),
|
|
1391
|
+
doneTasks: z.number(),
|
|
1392
|
+
tasksWithEvidence: z.number(),
|
|
1393
|
+
tasksWithoutEvidence: z.number(),
|
|
1394
|
+
tasks: z.array(TaskEvidenceCheckSchema),
|
|
1395
|
+
passed: z.boolean(),
|
|
1396
|
+
blockers: z.array(z.string())
|
|
1397
|
+
});
|
|
1398
|
+
var ZoneLockSchema = z.object({
|
|
1399
|
+
zone: z.string(),
|
|
1400
|
+
operator: z.string(),
|
|
1401
|
+
sprint: z.string(),
|
|
1402
|
+
acquiredAt: z.string(),
|
|
1403
|
+
ttl: z.number().min(60).max(86400),
|
|
1404
|
+
expiresAt: z.string()
|
|
1405
|
+
});
|
|
1406
|
+
var LockResultSchema = z.object({
|
|
1407
|
+
zone: z.string(),
|
|
1408
|
+
acquired: z.boolean(),
|
|
1409
|
+
existingLock: ZoneLockSchema.optional(),
|
|
1410
|
+
error: z.string().optional()
|
|
1411
|
+
});
|
|
1412
|
+
var LockListSchema = z.object({
|
|
1413
|
+
fetchedAt: z.string(),
|
|
1414
|
+
locks: z.array(ZoneLockSchema),
|
|
1415
|
+
active: z.array(ZoneLockSchema),
|
|
1416
|
+
expired: z.array(ZoneLockSchema)
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
// src/core/portfolio.service.ts
|
|
1420
|
+
var PortfolioService = class {
|
|
1421
|
+
constructor(root) {
|
|
1422
|
+
this.root = root;
|
|
1423
|
+
}
|
|
1424
|
+
root;
|
|
1425
|
+
load(configPath = ".oreshnik.portfolio.json") {
|
|
1426
|
+
const fullPath = this.resolvePath(configPath);
|
|
1427
|
+
if (!existsSync2(fullPath)) {
|
|
1428
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
1429
|
+
}
|
|
1430
|
+
try {
|
|
1431
|
+
const raw = JSON.parse(readFileSync2(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
1432
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
1433
|
+
if (!parsed.success) {
|
|
1434
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`));
|
|
1435
|
+
}
|
|
1436
|
+
return ok(parsed.data);
|
|
1437
|
+
} catch (e) {
|
|
1438
|
+
return err(this.fail(`Failed to read portfolio config ${configPath}: ${String(e)}`));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
inspect(configPath = ".oreshnik.portfolio.json") {
|
|
1442
|
+
const config = this.load(configPath);
|
|
1443
|
+
if (!config.ok) return config;
|
|
1444
|
+
const projects = config.value.projects.map((project) => this.inspectProject(project));
|
|
1445
|
+
return ok({
|
|
1446
|
+
configPath,
|
|
1447
|
+
portfolioId: config.value.portfolio.id,
|
|
1448
|
+
portfolioName: config.value.portfolio.name,
|
|
1449
|
+
projectCount: config.value.projects.length,
|
|
1450
|
+
continuityDocPath: config.value.portfolio.continuityDocPath,
|
|
1451
|
+
projects
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
inspectProject(project) {
|
|
1455
|
+
const repoPath = this.resolvePath(project.repoPath);
|
|
1456
|
+
const repoExists = existsSync2(repoPath);
|
|
1457
|
+
const taskBoardExists = repoExists && existsSync2(join4(repoPath, ...project.taskBoardPath.split("/")));
|
|
1458
|
+
const zoneMapExists = repoExists && existsSync2(join4(repoPath, ...project.zoneMapPath.split("/")));
|
|
1459
|
+
const warnings = [];
|
|
1460
|
+
if (!repoExists) warnings.push("repo path not found");
|
|
1461
|
+
if (repoExists && !taskBoardExists) warnings.push("task-board missing");
|
|
1462
|
+
if (repoExists && !zoneMapExists) warnings.push("zone-map missing");
|
|
1463
|
+
if (project.validationGates.length === 0) warnings.push("validation gates missing");
|
|
1464
|
+
if (project.injectionPolicy === "direct_with_approval" && !taskBoardExists) warnings.push("direct injection blocked until task-board exists");
|
|
1465
|
+
return {
|
|
1466
|
+
projectId: project.projectId,
|
|
1467
|
+
displayName: project.displayName,
|
|
1468
|
+
repoPath: project.repoPath,
|
|
1469
|
+
repoExists,
|
|
1470
|
+
taskBoardExists,
|
|
1471
|
+
zoneMapExists,
|
|
1472
|
+
validationGateCount: project.validationGates.length,
|
|
1473
|
+
injectionPolicy: project.injectionPolicy,
|
|
1474
|
+
warnings
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
resolvePath(path) {
|
|
1478
|
+
return isAbsolute(path) ? path : resolve(this.root, path);
|
|
1479
|
+
}
|
|
1480
|
+
fail(message) {
|
|
1481
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
function createPortfolioService(root) {
|
|
1485
|
+
return new PortfolioService(root);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/core/notes-ingestion.service.ts
|
|
1489
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1490
|
+
import { isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
1491
|
+
var PROJECT_KEYWORDS = {
|
|
1492
|
+
heptacore: ["heptacore", "heptacor", "eptacor"],
|
|
1493
|
+
oreshnik: ["oreshnik", "oresnick", "ornik"],
|
|
1494
|
+
turpialsound: ["turpialsound", "turpial sound", "turpial zone"],
|
|
1495
|
+
turpialmarket: ["turpialmarket", "turpial market", "marketplace"],
|
|
1496
|
+
dropsocial: ["dropsocial", "drop social"],
|
|
1497
|
+
smsmantis: ["mantis", "smsmantis"]
|
|
1498
|
+
};
|
|
1499
|
+
var OPERATOR_KEYWORDS = {
|
|
1500
|
+
jean: "jean"
|
|
1501
|
+
};
|
|
1502
|
+
var TASK_LINE_RE = /^\s*[-*]\s+\[([ xX])\]\s+(.+)$/;
|
|
1503
|
+
var NotesIngestionService = class {
|
|
1504
|
+
constructor(root) {
|
|
1505
|
+
this.root = root;
|
|
1506
|
+
}
|
|
1507
|
+
root;
|
|
1508
|
+
ingest(options) {
|
|
1509
|
+
const configResult = this.loadPortfolio(options.configPath);
|
|
1510
|
+
if (!configResult.ok) return configResult;
|
|
1511
|
+
const config = configResult.value;
|
|
1512
|
+
const fullSourcePath = this.resolvePath(options.sourcePath);
|
|
1513
|
+
if (!existsSync3(fullSourcePath)) {
|
|
1514
|
+
return err(this.fail(`Source file not found: ${options.sourcePath}`));
|
|
1515
|
+
}
|
|
1516
|
+
let content;
|
|
1517
|
+
try {
|
|
1518
|
+
content = readFileSync3(fullSourcePath, "utf8");
|
|
1519
|
+
} catch (e) {
|
|
1520
|
+
return err(this.fail(`Failed to read source file: ${String(e)}`));
|
|
1521
|
+
}
|
|
1522
|
+
const lines = content.split("\n");
|
|
1523
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1524
|
+
const validProjectIds = new Set(config.projects.map((p) => p.projectId));
|
|
1525
|
+
const allTasks = [];
|
|
1526
|
+
const warnings = [];
|
|
1527
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1528
|
+
const line = lines[i];
|
|
1529
|
+
if (!line) continue;
|
|
1530
|
+
const match = line.match(TASK_LINE_RE);
|
|
1531
|
+
if (!match) continue;
|
|
1532
|
+
const checked = match[1].toLowerCase() === "x";
|
|
1533
|
+
const rawLine = match[2].trim();
|
|
1534
|
+
if (!rawLine) continue;
|
|
1535
|
+
const projectIds = this.classifyProject(rawLine, validProjectIds);
|
|
1536
|
+
const proposedOwner = this.classifyOwner(rawLine);
|
|
1537
|
+
const proposedType = this.classifyTaskType(rawLine);
|
|
1538
|
+
const proposedPriority = this.classifyPriority(rawLine);
|
|
1539
|
+
const proposedSprintPrefix = this.resolveSprintPrefix(projectIds);
|
|
1540
|
+
const evidence = this.classifyEvidence(rawLine);
|
|
1541
|
+
const task = {
|
|
1542
|
+
rawLine,
|
|
1543
|
+
lineNumber: i + 1,
|
|
1544
|
+
checked,
|
|
1545
|
+
projectIds,
|
|
1546
|
+
proposedType,
|
|
1547
|
+
proposedOwner,
|
|
1548
|
+
proposedPriority,
|
|
1549
|
+
proposedSprintPrefix,
|
|
1550
|
+
evidenceType: evidence.type,
|
|
1551
|
+
evidenceExpectation: evidence.expectation
|
|
1552
|
+
};
|
|
1553
|
+
allTasks.push(task);
|
|
1554
|
+
if (projectIds.length === 0) {
|
|
1555
|
+
warnings.push(`Line ${i + 1}: could not classify project for task, moved to unclassified`);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const tasksByProject = {};
|
|
1559
|
+
for (const pid of validProjectIds) {
|
|
1560
|
+
tasksByProject[pid] = [];
|
|
1561
|
+
}
|
|
1562
|
+
const unclassifiedTasks = [];
|
|
1563
|
+
for (const task of allTasks) {
|
|
1564
|
+
if (task.projectIds.length === 0) {
|
|
1565
|
+
unclassifiedTasks.push(task);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
for (const pid of task.projectIds) {
|
|
1569
|
+
if (validProjectIds.has(pid) && tasksByProject[pid]) {
|
|
1570
|
+
tasksByProject[pid].push(task);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const pendingTasks = allTasks.filter((t) => !t.checked);
|
|
1575
|
+
const doneTasks = allTasks.filter((t) => t.checked);
|
|
1576
|
+
const result = {
|
|
1577
|
+
sourcePath: options.sourcePath,
|
|
1578
|
+
portfolioId: config.portfolio.id,
|
|
1579
|
+
parsedAt: now,
|
|
1580
|
+
dryRun: options.dryRun !== false,
|
|
1581
|
+
totalLines: lines.length,
|
|
1582
|
+
taskLines: allTasks.length,
|
|
1583
|
+
pendingCount: pendingTasks.length,
|
|
1584
|
+
doneCount: doneTasks.length,
|
|
1585
|
+
tasksByProject,
|
|
1586
|
+
unclassifiedTasks,
|
|
1587
|
+
warnings
|
|
1588
|
+
};
|
|
1589
|
+
const parsed = NotesIngestionResultSchema.safeParse(result);
|
|
1590
|
+
if (!parsed.success) {
|
|
1591
|
+
return err(this.fail(`Invalid ingestion result: ${parsed.error.message}`));
|
|
1592
|
+
}
|
|
1593
|
+
return ok(result);
|
|
1594
|
+
}
|
|
1595
|
+
loadPortfolio(configPath = ".oreshnik.portfolio.json") {
|
|
1596
|
+
const fullPath = this.resolvePath(configPath);
|
|
1597
|
+
if (!existsSync3(fullPath)) {
|
|
1598
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
const raw = JSON.parse(readFileSync3(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
1602
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
1603
|
+
if (!parsed.success) {
|
|
1604
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
1605
|
+
}
|
|
1606
|
+
return ok(parsed.data);
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
classifyProject(text, validIds) {
|
|
1612
|
+
const lower = text.toLowerCase();
|
|
1613
|
+
const ids = [];
|
|
1614
|
+
for (const [pid, keywords] of Object.entries(PROJECT_KEYWORDS)) {
|
|
1615
|
+
if (!validIds.has(pid)) continue;
|
|
1616
|
+
for (const kw of keywords) {
|
|
1617
|
+
if (lower.includes(kw)) {
|
|
1618
|
+
ids.push(pid);
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
return ids;
|
|
1624
|
+
}
|
|
1625
|
+
classifyOwner(text) {
|
|
1626
|
+
const lower = text.toLowerCase();
|
|
1627
|
+
for (const [keyword, owner] of Object.entries(OPERATOR_KEYWORDS)) {
|
|
1628
|
+
if (lower.includes(keyword)) return owner;
|
|
1629
|
+
}
|
|
1630
|
+
return "architect";
|
|
1631
|
+
}
|
|
1632
|
+
classifyTaskType(text) {
|
|
1633
|
+
const lower = text.toLowerCase();
|
|
1634
|
+
if (/\bbug\b|error|falla|roto|corregir|arreglar|fix\b/.test(lower)) return "bug";
|
|
1635
|
+
if (/revisar\b|qa\b|test\b|validar\b|verificar\b/.test(lower)) return "qa";
|
|
1636
|
+
if (/documentar\b|doc\b|gu[ií]a\b|readme|notas\b/.test(lower)) return "docs";
|
|
1637
|
+
if (/arquitectura|dise[ñn]o\b|estructura|decisi[óo]n/.test(lower)) return "architecture";
|
|
1638
|
+
if (/separar\b|mover\b|migrar\b|aislar\b|extraer|split/.test(lower)) return "migration";
|
|
1639
|
+
if (/publicar\b|deploy\b|producci[óo]n|server|entorno/.test(lower)) return "ops";
|
|
1640
|
+
return "feature";
|
|
1641
|
+
}
|
|
1642
|
+
classifyPriority(text) {
|
|
1643
|
+
const lower = text.toLowerCase();
|
|
1644
|
+
if (/cr[ií]tico|urgente|bloquea|crash\b|roto\b|prioridad alta/.test(lower)) return "critical";
|
|
1645
|
+
if (/importante|alto impacto|prioridad/.test(lower)) return "high";
|
|
1646
|
+
if (/bajo|menor|opcional|low priority/.test(lower)) return "low";
|
|
1647
|
+
return "medium";
|
|
1648
|
+
}
|
|
1649
|
+
resolveSprintPrefix(projectIds) {
|
|
1650
|
+
const prefixes = {
|
|
1651
|
+
heptacore: "S-HC",
|
|
1652
|
+
oreshnik: "S-ORESHNIK",
|
|
1653
|
+
turpialsound: "S-TS",
|
|
1654
|
+
turpialmarket: "S-TM",
|
|
1655
|
+
dropsocial: "S-DS",
|
|
1656
|
+
smsmantis: "S-SM"
|
|
1657
|
+
};
|
|
1658
|
+
for (const pid of projectIds) {
|
|
1659
|
+
if (prefixes[pid]) return prefixes[pid];
|
|
1660
|
+
}
|
|
1661
|
+
return "S-GEN";
|
|
1662
|
+
}
|
|
1663
|
+
classifyEvidence(text) {
|
|
1664
|
+
const lower = text.toLowerCase();
|
|
1665
|
+
if (/implementar|crear\b|construir|c[óo]digo|funcionalidad|backend|api\b|endpoint/.test(lower)) {
|
|
1666
|
+
return { type: "code", expectation: "diff verificable, tests" };
|
|
1667
|
+
}
|
|
1668
|
+
if (/click|bot[óo]n|tarjeta|card\b|ui\b|dashboard|modal|interfaz|visual|landing|p[áa]gina/.test(lower)) {
|
|
1669
|
+
return { type: "ui", expectation: "QA manual o test automatizado de UI" };
|
|
1670
|
+
}
|
|
1671
|
+
if (/documentar|doc\b|leer\b|documentaci[óo]n|gu[ií]a|readme/.test(lower)) {
|
|
1672
|
+
return { type: "docs", expectation: "documentos actualizados en vault" };
|
|
1673
|
+
}
|
|
1674
|
+
if (/integrar|conectar|webhook|deploy\b|publicar|producci[óo]n/.test(lower)) {
|
|
1675
|
+
return { type: "integration", expectation: "dry-run verificable o test e2e" };
|
|
1676
|
+
}
|
|
1677
|
+
if (/producci[óo]n\b|release\b|publish\b|lanzar/.test(lower)) {
|
|
1678
|
+
return { type: "prod", expectation: "checklist manual, rollback plan y confirmaci[\xF3o]n humana" };
|
|
1679
|
+
}
|
|
1680
|
+
return { type: "code", expectation: "diff verificable" };
|
|
1681
|
+
}
|
|
1682
|
+
resolvePath(path) {
|
|
1683
|
+
return isAbsolute2(path) ? path : resolve2(this.root, path);
|
|
1684
|
+
}
|
|
1685
|
+
fail(message) {
|
|
1686
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function createNotesIngestionService(root) {
|
|
1690
|
+
return new NotesIngestionService(root);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/core/injection.service.ts
|
|
1694
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1695
|
+
import { isAbsolute as isAbsolute3, join as join5, resolve as resolve3 } from "path";
|
|
1696
|
+
|
|
1697
|
+
// src/utils/helpers.ts
|
|
1698
|
+
function sanitize(value, maxLength = 48) {
|
|
1699
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength);
|
|
1700
|
+
}
|
|
1701
|
+
function today() {
|
|
1702
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1703
|
+
}
|
|
1704
|
+
function nowISO() {
|
|
1705
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1706
|
+
}
|
|
1707
|
+
function nowDisplay(timeZone = "America/Caracas") {
|
|
1708
|
+
const now = /* @__PURE__ */ new Date();
|
|
1709
|
+
const formatter = new Intl.DateTimeFormat("es-VE", {
|
|
1710
|
+
timeZone,
|
|
1711
|
+
year: "2-digit",
|
|
1712
|
+
month: "2-digit",
|
|
1713
|
+
day: "2-digit",
|
|
1714
|
+
hour: "2-digit",
|
|
1715
|
+
minute: "2-digit",
|
|
1716
|
+
hour12: false
|
|
1717
|
+
});
|
|
1718
|
+
return {
|
|
1719
|
+
display: formatter.format(now).replace(",", ""),
|
|
1720
|
+
iso: now.toISOString(),
|
|
1721
|
+
date: now.toISOString().slice(0, 10)
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
function formatBranchName(template, vars) {
|
|
1725
|
+
let result = template;
|
|
1726
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1727
|
+
result = result.replace(`{${key}}`, sanitize(value));
|
|
1728
|
+
}
|
|
1729
|
+
return result;
|
|
1730
|
+
}
|
|
1731
|
+
function nonIgnoredDirtyFiles(porcelain) {
|
|
1732
|
+
return porcelain.filter((entry) => !entry.path.startsWith("var/oreshnik/") && !entry.path.startsWith("output/")).map((entry) => entry.path);
|
|
1733
|
+
}
|
|
1734
|
+
function isMotherBranch(branch) {
|
|
1735
|
+
return /^(main|master|MADRE\/|integration\/|prod\/)/i.test(branch);
|
|
1736
|
+
}
|
|
1737
|
+
function isChildBranch(branch, operator) {
|
|
1738
|
+
return new RegExp(`^${operator}/`, "i").test(branch);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/core/injection.service.ts
|
|
1742
|
+
var InjectionService = class {
|
|
1743
|
+
constructor(root) {
|
|
1744
|
+
this.root = root;
|
|
1745
|
+
}
|
|
1746
|
+
root;
|
|
1747
|
+
inject(options) {
|
|
1748
|
+
const configResult = this.loadPortfolio(options.configPath);
|
|
1749
|
+
if (!configResult.ok) return configResult;
|
|
1750
|
+
const config = configResult.value;
|
|
1751
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1752
|
+
const projectResults = [];
|
|
1753
|
+
const errors = [];
|
|
1754
|
+
const projectsToProcess = options.projectFilter ? config.projects.filter((p) => options.projectFilter.includes(p.projectId)) : config.projects;
|
|
1755
|
+
for (const project of projectsToProcess) {
|
|
1756
|
+
const tasks = options.ingestion.tasksByProject[project.projectId];
|
|
1757
|
+
if (!tasks || tasks.length === 0) continue;
|
|
1758
|
+
const pendingTasks = tasks.filter((t) => !t.checked);
|
|
1759
|
+
if (pendingTasks.length === 0) continue;
|
|
1760
|
+
const result2 = this.injectIntoProject(project, pendingTasks, options);
|
|
1761
|
+
projectResults.push(result2);
|
|
1762
|
+
if (result2.blocked && result2.blockReason) {
|
|
1763
|
+
errors.push(`${project.projectId}: ${result2.blockReason}`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
const blocked = projectResults.filter((r) => r.blocked);
|
|
1767
|
+
const injected = projectResults.filter((r) => !r.blocked && r.injectionPolicy === "direct_with_approval");
|
|
1768
|
+
const proposals = projectResults.filter((r) => !r.blocked && r.injectionPolicy === "proposal_only");
|
|
1769
|
+
const summary = {
|
|
1770
|
+
totalProjects: projectResults.length,
|
|
1771
|
+
projectsBlocked: blocked.length,
|
|
1772
|
+
projectsInjected: injected.length,
|
|
1773
|
+
proposalsGenerated: proposals.length,
|
|
1774
|
+
totalTasksInjected: injected.reduce((sum, r) => sum + r.tasksInjected, 0),
|
|
1775
|
+
totalTasksSkipped: projectResults.reduce((sum, r) => sum + r.tasksSkipped, 0)
|
|
1776
|
+
};
|
|
1777
|
+
const result = {
|
|
1778
|
+
portfolioId: config.portfolio.id,
|
|
1779
|
+
injectedAt: now,
|
|
1780
|
+
dryRun: options.dryRun !== false,
|
|
1781
|
+
confirmed: options.confirm === true,
|
|
1782
|
+
projects: projectResults,
|
|
1783
|
+
summary,
|
|
1784
|
+
errors
|
|
1785
|
+
};
|
|
1786
|
+
const parsed = InjectionResultSchema.safeParse(result);
|
|
1787
|
+
if (!parsed.success) {
|
|
1788
|
+
return err(this.fail(`Invalid injection result: ${parsed.error.message}`));
|
|
1789
|
+
}
|
|
1790
|
+
return ok(result);
|
|
1791
|
+
}
|
|
1792
|
+
injectIntoProject(project, tasks, options) {
|
|
1793
|
+
const repoPath = this.resolveProjectPath(project.repoPath);
|
|
1794
|
+
const warnings = [];
|
|
1795
|
+
if (!existsSync4(repoPath)) {
|
|
1796
|
+
return {
|
|
1797
|
+
projectId: project.projectId,
|
|
1798
|
+
displayName: project.displayName,
|
|
1799
|
+
injectionPolicy: project.injectionPolicy,
|
|
1800
|
+
repoPath: project.repoPath,
|
|
1801
|
+
tasksInjected: 0,
|
|
1802
|
+
tasksSkipped: 0,
|
|
1803
|
+
blocked: true,
|
|
1804
|
+
blockReason: "repo path not found",
|
|
1805
|
+
warnings
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
if (project.injectionPolicy === "proposal_only") {
|
|
1809
|
+
const proposal = this.generateProposal(project, tasks, options.dryRun !== false);
|
|
1810
|
+
return {
|
|
1811
|
+
projectId: project.projectId,
|
|
1812
|
+
displayName: project.displayName,
|
|
1813
|
+
injectionPolicy: "proposal_only",
|
|
1814
|
+
repoPath: project.repoPath,
|
|
1815
|
+
tasksInjected: 0,
|
|
1816
|
+
tasksSkipped: 0,
|
|
1817
|
+
proposalPath: proposal,
|
|
1818
|
+
blocked: false,
|
|
1819
|
+
warnings
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
if (project.injectionPolicy === "direct_with_approval") {
|
|
1823
|
+
if (!options.confirm) {
|
|
1824
|
+
return {
|
|
1825
|
+
projectId: project.projectId,
|
|
1826
|
+
displayName: project.displayName,
|
|
1827
|
+
injectionPolicy: "direct_with_approval",
|
|
1828
|
+
repoPath: project.repoPath,
|
|
1829
|
+
tasksInjected: 0,
|
|
1830
|
+
tasksSkipped: 0,
|
|
1831
|
+
blocked: true,
|
|
1832
|
+
blockReason: "confirmation required (use --confirm)",
|
|
1833
|
+
warnings
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
const zoneMapPath = join5(repoPath, ...project.zoneMapPath.split("/"));
|
|
1837
|
+
if (!existsSync4(zoneMapPath)) {
|
|
1838
|
+
return {
|
|
1839
|
+
projectId: project.projectId,
|
|
1840
|
+
displayName: project.displayName,
|
|
1841
|
+
injectionPolicy: "direct_with_approval",
|
|
1842
|
+
repoPath: project.repoPath,
|
|
1843
|
+
tasksInjected: 0,
|
|
1844
|
+
tasksSkipped: 0,
|
|
1845
|
+
blocked: true,
|
|
1846
|
+
blockReason: "zone-map missing",
|
|
1847
|
+
warnings
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
let zoneOperators = [];
|
|
1851
|
+
try {
|
|
1852
|
+
const zoneRaw = JSON.parse(readFileSync4(zoneMapPath, "utf8").replace(/^\uFEFF/, ""));
|
|
1853
|
+
const owners = /* @__PURE__ */ new Set();
|
|
1854
|
+
for (const [, zone] of Object.entries(zoneRaw.zones || {})) {
|
|
1855
|
+
const lock = zone.lock || "";
|
|
1856
|
+
const owner = zone.owner || "";
|
|
1857
|
+
if (lock.includes("exclusive") || lock === "operator_double") {
|
|
1858
|
+
if (owner) owners.add(owner);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (owners.size === 0) {
|
|
1862
|
+
for (const op of project.operators || []) {
|
|
1863
|
+
owners.add(op);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
zoneOperators = [...owners].filter((o) => o && o !== "none");
|
|
1867
|
+
} catch {
|
|
1868
|
+
zoneOperators = [];
|
|
1869
|
+
}
|
|
1870
|
+
if (zoneOperators.length === 0) {
|
|
1871
|
+
zoneOperators = project.operators?.length ? project.operators : ["architect"];
|
|
1872
|
+
}
|
|
1873
|
+
const getOwner = (taskIndex2) => {
|
|
1874
|
+
return zoneOperators[taskIndex2 % zoneOperators.length] || "architect";
|
|
1875
|
+
};
|
|
1876
|
+
const git = new GitService(repoPath);
|
|
1877
|
+
const statusResult = git.statusPorcelain();
|
|
1878
|
+
if (statusResult.ok && statusResult.value.length > 0) {
|
|
1879
|
+
return {
|
|
1880
|
+
projectId: project.projectId,
|
|
1881
|
+
displayName: project.displayName,
|
|
1882
|
+
injectionPolicy: "direct_with_approval",
|
|
1883
|
+
repoPath: project.repoPath,
|
|
1884
|
+
tasksInjected: 0,
|
|
1885
|
+
tasksSkipped: 0,
|
|
1886
|
+
blocked: true,
|
|
1887
|
+
blockReason: `working tree dirty (${statusResult.value.length} files)`,
|
|
1888
|
+
warnings
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
if (options.dryRun !== false) {
|
|
1892
|
+
return {
|
|
1893
|
+
projectId: project.projectId,
|
|
1894
|
+
displayName: project.displayName,
|
|
1895
|
+
injectionPolicy: "direct_with_approval",
|
|
1896
|
+
repoPath: project.repoPath,
|
|
1897
|
+
tasksInjected: tasks.length,
|
|
1898
|
+
tasksSkipped: 0,
|
|
1899
|
+
blocked: false,
|
|
1900
|
+
warnings: ["dry-run: no task-board was modified"]
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
const checkpointTag = this.createPreInjectionCheckpoint(git, project);
|
|
1904
|
+
const checkpointResult = git.createTag(checkpointTag, `pre-injection checkpoint for ${project.projectId}`);
|
|
1905
|
+
if (!checkpointResult.ok) {
|
|
1906
|
+
warnings.push(`checkpoint creation failed: ${checkpointResult.error.message}`);
|
|
1907
|
+
}
|
|
1908
|
+
const state = new StateManager(repoPath);
|
|
1909
|
+
const taskBoardPath = join5(repoPath, ...project.taskBoardPath.split("/"));
|
|
1910
|
+
const existingBoard = state.readJson(taskBoardPath);
|
|
1911
|
+
const board = existingBoard.ok ? existingBoard.value : {
|
|
1912
|
+
project: project.projectId,
|
|
1913
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1914
|
+
currentExecutionOrder: [],
|
|
1915
|
+
tasks: []
|
|
1916
|
+
};
|
|
1917
|
+
let injected = 0;
|
|
1918
|
+
let skipped = 0;
|
|
1919
|
+
let taskIndex = 0;
|
|
1920
|
+
for (const noteTask of tasks) {
|
|
1921
|
+
const taskId = this.generateTaskId(noteTask);
|
|
1922
|
+
const existingTask = board.tasks.find((t) => t.id === taskId);
|
|
1923
|
+
if (existingTask) {
|
|
1924
|
+
skipped++;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
const owner = getOwner(taskIndex);
|
|
1928
|
+
taskIndex++;
|
|
1929
|
+
const task = {
|
|
1930
|
+
id: taskId,
|
|
1931
|
+
title: noteTask.rawLine.slice(0, 200),
|
|
1932
|
+
owner,
|
|
1933
|
+
status: "ready",
|
|
1934
|
+
track: noteTask.proposedSprintPrefix,
|
|
1935
|
+
zone: [],
|
|
1936
|
+
acceptance: [noteTask.evidenceExpectation],
|
|
1937
|
+
handoff: `docs/07_handoffs/handoff-${owner}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.md`,
|
|
1938
|
+
history: [
|
|
1939
|
+
{
|
|
1940
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1941
|
+
action: "injected",
|
|
1942
|
+
operator: owner,
|
|
1943
|
+
description: `Injected from notes ingestion. Evidence: ${noteTask.evidenceType}`
|
|
1944
|
+
}
|
|
1945
|
+
]
|
|
1946
|
+
};
|
|
1947
|
+
board.tasks.push(task);
|
|
1948
|
+
injected++;
|
|
1949
|
+
}
|
|
1950
|
+
board.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1951
|
+
board.currentExecutionOrder = [...board.currentExecutionOrder, ...tasks.map((t) => this.generateTaskId(t))];
|
|
1952
|
+
const writeResult = state.writeJson(taskBoardPath, board);
|
|
1953
|
+
if (!writeResult.ok) {
|
|
1954
|
+
return {
|
|
1955
|
+
projectId: project.projectId,
|
|
1956
|
+
displayName: project.displayName,
|
|
1957
|
+
injectionPolicy: "direct_with_approval",
|
|
1958
|
+
repoPath: project.repoPath,
|
|
1959
|
+
tasksInjected: 0,
|
|
1960
|
+
tasksSkipped: 0,
|
|
1961
|
+
blocked: true,
|
|
1962
|
+
blockReason: `failed to write task-board: ${writeResult.error.message}`,
|
|
1963
|
+
warnings
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
const handoffPath = this.generateHandoff(project, tasks, repoPath);
|
|
1967
|
+
return {
|
|
1968
|
+
projectId: project.projectId,
|
|
1969
|
+
displayName: project.displayName,
|
|
1970
|
+
injectionPolicy: "direct_with_approval",
|
|
1971
|
+
repoPath: project.repoPath,
|
|
1972
|
+
tasksInjected: injected,
|
|
1973
|
+
tasksSkipped: skipped,
|
|
1974
|
+
checkpointTag: checkpointResult.ok ? checkpointTag : void 0,
|
|
1975
|
+
handoffPath,
|
|
1976
|
+
blocked: false,
|
|
1977
|
+
warnings
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
return {
|
|
1981
|
+
projectId: project.projectId,
|
|
1982
|
+
displayName: project.displayName,
|
|
1983
|
+
injectionPolicy: project.injectionPolicy,
|
|
1984
|
+
repoPath: project.repoPath,
|
|
1985
|
+
tasksInjected: 0,
|
|
1986
|
+
tasksSkipped: 0,
|
|
1987
|
+
blocked: true,
|
|
1988
|
+
blockReason: `unknown injection policy: ${project.injectionPolicy}`,
|
|
1989
|
+
warnings
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
generateProposal(project, tasks, dryRun) {
|
|
1993
|
+
const repoPath = this.resolveProjectPath(project.repoPath);
|
|
1994
|
+
const proposalDir = join5(repoPath, "var", "oreshnik");
|
|
1995
|
+
mkdirSync2(proposalDir, { recursive: true });
|
|
1996
|
+
const proposalPath = join5(proposalDir, "injection-proposal.json");
|
|
1997
|
+
const proposal = {
|
|
1998
|
+
projectId: project.projectId,
|
|
1999
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2000
|
+
dryRun,
|
|
2001
|
+
injectionPolicy: "proposal_only",
|
|
2002
|
+
proposedTasks: tasks.map((t) => ({
|
|
2003
|
+
taskId: this.generateTaskId(t),
|
|
2004
|
+
title: t.rawLine.slice(0, 200),
|
|
2005
|
+
owner: t.proposedOwner,
|
|
2006
|
+
priority: t.proposedPriority,
|
|
2007
|
+
evidenceType: t.evidenceType,
|
|
2008
|
+
evidenceExpectation: t.evidenceExpectation
|
|
2009
|
+
}))
|
|
2010
|
+
};
|
|
2011
|
+
if (!dryRun) {
|
|
2012
|
+
writeFileSync2(proposalPath, JSON.stringify(proposal, null, 2), "utf8");
|
|
2013
|
+
}
|
|
2014
|
+
return `var/oreshnik/injection-proposal.json`;
|
|
2015
|
+
}
|
|
2016
|
+
generateHandoff(project, tasks, repoPath) {
|
|
2017
|
+
const handoffDir = join5(repoPath, "docs", "07_handoffs");
|
|
2018
|
+
mkdirSync2(handoffDir, { recursive: true });
|
|
2019
|
+
const owners = new Set(tasks.map((t) => t.proposedOwner));
|
|
2020
|
+
for (const owner of owners) {
|
|
2021
|
+
const ownerTasks = tasks.filter((t) => t.proposedOwner === owner);
|
|
2022
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2023
|
+
const handoffPath = join5(handoffDir, `handoff-${owner}-${date}.md`);
|
|
2024
|
+
const lines = [
|
|
2025
|
+
`# Handoff \u2014 ${owner}`,
|
|
2026
|
+
`Date: ${date}`,
|
|
2027
|
+
`Project: ${project.displayName} (${project.projectId})`,
|
|
2028
|
+
``,
|
|
2029
|
+
`## Tasks Injected`,
|
|
2030
|
+
``
|
|
2031
|
+
];
|
|
2032
|
+
for (const task of ownerTasks) {
|
|
2033
|
+
lines.push(`- [ ] **${task.proposedSprintPrefix}** ${task.rawLine.slice(0, 150)}`);
|
|
2034
|
+
lines.push(` - Type: ${task.proposedType}`);
|
|
2035
|
+
lines.push(` - Priority: ${task.proposedPriority}`);
|
|
2036
|
+
lines.push(` - Evidence: ${task.evidenceType} \u2014 ${task.evidenceExpectation}`);
|
|
2037
|
+
lines.push(``);
|
|
2038
|
+
}
|
|
2039
|
+
writeFileSync2(handoffPath, lines.join("\n"), "utf8");
|
|
2040
|
+
}
|
|
2041
|
+
return `docs/07_handoffs/`;
|
|
2042
|
+
}
|
|
2043
|
+
createPreInjectionCheckpoint(git, project) {
|
|
2044
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2045
|
+
return `oreshnik/pre-inject-${project.projectId}-${timestamp}`;
|
|
2046
|
+
}
|
|
2047
|
+
generateTaskId(noteTask) {
|
|
2048
|
+
const base = sanitize(noteTask.rawLine.slice(0, 48));
|
|
2049
|
+
return base || `task-${Date.now()}`;
|
|
2050
|
+
}
|
|
2051
|
+
loadPortfolio(configPath = ".oreshnik.portfolio.json") {
|
|
2052
|
+
const fullPath = this.resolveLocalPath(configPath);
|
|
2053
|
+
if (!existsSync4(fullPath)) {
|
|
2054
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
2055
|
+
}
|
|
2056
|
+
try {
|
|
2057
|
+
const raw = JSON.parse(readFileSync4(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2058
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
2059
|
+
if (!parsed.success) {
|
|
2060
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
2061
|
+
}
|
|
2062
|
+
return ok(parsed.data);
|
|
2063
|
+
} catch (e) {
|
|
2064
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
resolveProjectPath(path) {
|
|
2068
|
+
return isAbsolute3(path) ? path : resolve3(this.root, path);
|
|
2069
|
+
}
|
|
2070
|
+
resolveLocalPath(path) {
|
|
2071
|
+
return isAbsolute3(path) ? path : resolve3(this.root, path);
|
|
2072
|
+
}
|
|
2073
|
+
fail(message) {
|
|
2074
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2075
|
+
}
|
|
2076
|
+
};
|
|
2077
|
+
function createInjectionService(root) {
|
|
2078
|
+
return new InjectionService(root);
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// src/core/evidence-gate.service.ts
|
|
2082
|
+
var EVIDENCE_REQUIREMENTS = {
|
|
2083
|
+
code: {
|
|
2084
|
+
minimum: "diff verificable, tests o build",
|
|
2085
|
+
items: ["changed files diff", "typecheck passed", "tests passed"]
|
|
2086
|
+
},
|
|
2087
|
+
ui: {
|
|
2088
|
+
minimum: "QA manual o test automatizado de UI",
|
|
2089
|
+
items: ["UI change description", "screenshot or test result", "operator confirmation"]
|
|
2090
|
+
},
|
|
2091
|
+
docs: {
|
|
2092
|
+
minimum: "documentos actualizados en vault",
|
|
2093
|
+
items: ["updated doc paths", "references in vault", "canonical alignment check"]
|
|
2094
|
+
},
|
|
2095
|
+
integration: {
|
|
2096
|
+
minimum: "dry-run verificable o test e2e",
|
|
2097
|
+
items: ["integration test output", "dry-run summary", "endpoint verification"]
|
|
2098
|
+
},
|
|
2099
|
+
prod: {
|
|
2100
|
+
minimum: "checklist manual, rollback plan y confirmacion humana",
|
|
2101
|
+
items: ["deployment checklist", "rollback plan", "human approval signature"]
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
var EvidenceGateService = class {
|
|
2105
|
+
check(taskBoard, operator, sprint) {
|
|
2106
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2107
|
+
const tasks = [];
|
|
2108
|
+
const blockers = [];
|
|
2109
|
+
let doneTasks = 0;
|
|
2110
|
+
let tasksWithEvidence = 0;
|
|
2111
|
+
let tasksWithoutEvidence = 0;
|
|
2112
|
+
for (const task of taskBoard.tasks) {
|
|
2113
|
+
if (task.status === "done") {
|
|
2114
|
+
doneTasks++;
|
|
2115
|
+
}
|
|
2116
|
+
const evidenceType = this.inferEvidenceType(task);
|
|
2117
|
+
const required = EVIDENCE_REQUIREMENTS[evidenceType];
|
|
2118
|
+
const check = this.evaluateTaskEvidence(task, evidenceType, required);
|
|
2119
|
+
tasks.push(check);
|
|
2120
|
+
if (task.status === "done" && !check.hasEvidence) {
|
|
2121
|
+
tasksWithoutEvidence++;
|
|
2122
|
+
blockers.push(`${task.id}: ${check.missing.join("; ")}`);
|
|
2123
|
+
} else if (task.status === "done" && check.hasEvidence) {
|
|
2124
|
+
tasksWithEvidence++;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
const result = {
|
|
2128
|
+
checkedAt: now,
|
|
2129
|
+
sprint,
|
|
2130
|
+
operator,
|
|
2131
|
+
totalTasks: taskBoard.tasks.length,
|
|
2132
|
+
doneTasks,
|
|
2133
|
+
tasksWithEvidence,
|
|
2134
|
+
tasksWithoutEvidence,
|
|
2135
|
+
tasks,
|
|
2136
|
+
passed: blockers.length === 0,
|
|
2137
|
+
blockers
|
|
2138
|
+
};
|
|
2139
|
+
const parsed = EvidenceGateResultSchema.safeParse(result);
|
|
2140
|
+
if (!parsed.success) {
|
|
2141
|
+
return err(this.fail(`Invalid evidence gate result: ${parsed.error.message}`));
|
|
2142
|
+
}
|
|
2143
|
+
return ok(result);
|
|
2144
|
+
}
|
|
2145
|
+
generateChecklist(taskBoard, operator) {
|
|
2146
|
+
const lines = [
|
|
2147
|
+
`# QA Evidence Checklist`,
|
|
2148
|
+
`Operator: ${operator}`,
|
|
2149
|
+
`Project: ${taskBoard.project}`,
|
|
2150
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2151
|
+
``,
|
|
2152
|
+
`## Instructions`,
|
|
2153
|
+
`Verify each task below. Mark [x] when evidence is confirmed. If a task lacks evidence, it will block sprint closure.`,
|
|
2154
|
+
``
|
|
2155
|
+
];
|
|
2156
|
+
for (const task of taskBoard.tasks) {
|
|
2157
|
+
if (task.status !== "done") continue;
|
|
2158
|
+
const evidenceType = this.inferEvidenceType(task);
|
|
2159
|
+
const required = EVIDENCE_REQUIREMENTS[evidenceType];
|
|
2160
|
+
lines.push(`### ${task.id}`);
|
|
2161
|
+
lines.push(`**Title:** ${task.title}`);
|
|
2162
|
+
lines.push(`**Evidence Type:** ${evidenceType}`);
|
|
2163
|
+
lines.push(`**Minimum Required:** ${required.minimum}`);
|
|
2164
|
+
lines.push(``);
|
|
2165
|
+
for (const item of required.items) {
|
|
2166
|
+
lines.push(`- [ ] ${item}`);
|
|
2167
|
+
}
|
|
2168
|
+
lines.push(`- [ ] Operator confirms task is complete`);
|
|
2169
|
+
lines.push(``);
|
|
2170
|
+
}
|
|
2171
|
+
return lines.join("\n");
|
|
2172
|
+
}
|
|
2173
|
+
evaluateTaskEvidence(task, evidenceType, required) {
|
|
2174
|
+
const missing = [];
|
|
2175
|
+
const hardMissing = [];
|
|
2176
|
+
const history = task.history || [];
|
|
2177
|
+
const acceptance = task.acceptance || [];
|
|
2178
|
+
const hasValidation = history.some(
|
|
2179
|
+
(h) => h.action === "validated" || h.action === "done" || h.action === "qa_passed"
|
|
2180
|
+
);
|
|
2181
|
+
if (!hasValidation) {
|
|
2182
|
+
hardMissing.push("no validation history entry found");
|
|
2183
|
+
}
|
|
2184
|
+
if (acceptance.length === 0) {
|
|
2185
|
+
hardMissing.push("no acceptance criteria defined");
|
|
2186
|
+
}
|
|
2187
|
+
if (task.status === "done" && !task.handoff) {
|
|
2188
|
+
hardMissing.push("no handoff path defined");
|
|
2189
|
+
}
|
|
2190
|
+
if (task.status === "done") {
|
|
2191
|
+
const combinedText = [...history.map((h) => h.description || ""), ...acceptance].join(" ").toLowerCase();
|
|
2192
|
+
let matchedItems = 0;
|
|
2193
|
+
for (const item of required.items) {
|
|
2194
|
+
if (combinedText.includes(item.toLowerCase())) {
|
|
2195
|
+
matchedItems++;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
for (const item of required.items) {
|
|
2199
|
+
if (!combinedText.includes(item.toLowerCase())) {
|
|
2200
|
+
missing.push(`evidence item not verified: ${item}`);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
if (matchedItems === 0) {
|
|
2204
|
+
hardMissing.push("no evidence items matched the task");
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const allMissing = [...hardMissing, ...missing];
|
|
2208
|
+
return {
|
|
2209
|
+
taskId: task.id,
|
|
2210
|
+
taskTitle: task.title,
|
|
2211
|
+
evidenceType,
|
|
2212
|
+
hasEvidence: task.status !== "done" || hardMissing.length === 0,
|
|
2213
|
+
requiredEvidence: required.minimum,
|
|
2214
|
+
currentStatus: task.status,
|
|
2215
|
+
missing: allMissing
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
inferEvidenceType(task) {
|
|
2219
|
+
const text = `${task.title} ${task.track || ""} ${(task.acceptance || []).join(" ")}`.toLowerCase();
|
|
2220
|
+
if (/ui\b|dashboard|modal|interfaz|visual|landing|p[áa]gina|card|bot[óo]n|click|css|style/.test(text)) return "ui";
|
|
2221
|
+
if (/documentar|doc\b|gu[ií]a|readme|vault|notas/.test(text)) return "docs";
|
|
2222
|
+
if (/integrar|conectar|webhook|deploy\b|publicar|api\b|endpoint/.test(text)) return "integration";
|
|
2223
|
+
if (/producci[óo]n\b|release\b|publish\b|lanzar|prod\b/.test(text)) return "prod";
|
|
2224
|
+
return "code";
|
|
2225
|
+
}
|
|
2226
|
+
fail(message) {
|
|
2227
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
function createEvidenceGateService() {
|
|
2231
|
+
return new EvidenceGateService();
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// src/core/bootstrap.service.ts
|
|
2235
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
2236
|
+
import { isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "path";
|
|
2237
|
+
var BootstrapService = class {
|
|
2238
|
+
constructor(root) {
|
|
2239
|
+
this.root = root;
|
|
2240
|
+
}
|
|
2241
|
+
root;
|
|
2242
|
+
bootstrap(configPath = ".oreshnik.portfolio.json", projectFilter) {
|
|
2243
|
+
const configResult = this.loadPortfolio(configPath);
|
|
2244
|
+
if (!configResult.ok) return configResult;
|
|
2245
|
+
const config = configResult.value;
|
|
2246
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2247
|
+
const projects = [];
|
|
2248
|
+
const targets = projectFilter ? config.projects.filter((p) => projectFilter.includes(p.projectId)) : config.projects;
|
|
2249
|
+
for (const project of targets) {
|
|
2250
|
+
projects.push(this.bootstrapProject(project));
|
|
2251
|
+
}
|
|
2252
|
+
return ok({
|
|
2253
|
+
portfolioId: config.portfolio.id,
|
|
2254
|
+
bootstrappedAt: now,
|
|
2255
|
+
projects
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
bootstrapProject(project) {
|
|
2259
|
+
const repoPath = this.resolveProjectPath(project.repoPath);
|
|
2260
|
+
const errors = [];
|
|
2261
|
+
let taskBoardCreated = false;
|
|
2262
|
+
let zoneMapCreated = false;
|
|
2263
|
+
let gatesDocCreated = false;
|
|
2264
|
+
if (!existsSync5(repoPath)) {
|
|
2265
|
+
return {
|
|
2266
|
+
projectId: project.projectId,
|
|
2267
|
+
displayName: project.displayName,
|
|
2268
|
+
repoPath: project.repoPath,
|
|
2269
|
+
repoExists: false,
|
|
2270
|
+
taskBoardCreated: false,
|
|
2271
|
+
zoneMapCreated: false,
|
|
2272
|
+
gatesDocCreated: false,
|
|
2273
|
+
errors: ["repo path not found"]
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
const varDir = join6(repoPath, "var", "oreshnik");
|
|
2277
|
+
const handoffsDir = join6(repoPath, "docs", "07_handoffs");
|
|
2278
|
+
const taskBoardPath = join6(repoPath, ...project.taskBoardPath.split("/"));
|
|
2279
|
+
const zoneMapPath = join6(repoPath, ...project.zoneMapPath.split("/"));
|
|
2280
|
+
try {
|
|
2281
|
+
mkdirSync3(varDir, { recursive: true });
|
|
2282
|
+
mkdirSync3(handoffsDir, { recursive: true });
|
|
2283
|
+
} catch (e) {
|
|
2284
|
+
errors.push(`failed to create directories: ${String(e)}`);
|
|
2285
|
+
return {
|
|
2286
|
+
projectId: project.projectId,
|
|
2287
|
+
displayName: project.displayName,
|
|
2288
|
+
repoPath: project.repoPath,
|
|
2289
|
+
repoExists: true,
|
|
2290
|
+
taskBoardCreated: false,
|
|
2291
|
+
zoneMapCreated: false,
|
|
2292
|
+
gatesDocCreated: false,
|
|
2293
|
+
errors
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
if (!existsSync5(taskBoardPath)) {
|
|
2297
|
+
try {
|
|
2298
|
+
const taskBoard = {
|
|
2299
|
+
project: project.projectId,
|
|
2300
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2301
|
+
currentExecutionOrder: [],
|
|
2302
|
+
tasks: []
|
|
2303
|
+
};
|
|
2304
|
+
writeFileSync3(taskBoardPath, JSON.stringify(taskBoard, null, 2) + "\n", "utf8");
|
|
2305
|
+
taskBoardCreated = true;
|
|
2306
|
+
} catch (e) {
|
|
2307
|
+
errors.push(`failed to create task-board: ${String(e)}`);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (!existsSync5(zoneMapPath)) {
|
|
2311
|
+
try {
|
|
2312
|
+
const zoneMap = {
|
|
2313
|
+
zones: {
|
|
2314
|
+
"docs/**": { owner: "shared", lock: "shared", sprints: ["*"] },
|
|
2315
|
+
"var/oreshnik/**": { owner: "shared", lock: "shared", sprints: ["*"] },
|
|
2316
|
+
"src/**": { owner: "architect", lock: "operator_exclusive", sprints: ["*"] },
|
|
2317
|
+
".env": { owner: "none", lock: "forbidden", sprints: ["*"], criticality: "critical" },
|
|
2318
|
+
".env.*": { owner: "none", lock: "forbidden", sprints: ["*"], criticality: "critical" }
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
writeFileSync3(zoneMapPath, JSON.stringify(zoneMap, null, 2) + "\n", "utf8");
|
|
2322
|
+
zoneMapCreated = true;
|
|
2323
|
+
} catch (e) {
|
|
2324
|
+
errors.push(`failed to create zone-map: ${String(e)}`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (project.validationGates.length === 0) {
|
|
2328
|
+
try {
|
|
2329
|
+
const gatesDocPath = join6(repoPath, "var", "oreshnik", "gates.json");
|
|
2330
|
+
const defaultGates = {
|
|
2331
|
+
project: project.projectId,
|
|
2332
|
+
gates: [
|
|
2333
|
+
{ name: "typecheck", command: "npm", args: ["run", "typecheck"], timeoutSeconds: 120 },
|
|
2334
|
+
{ name: "build", command: "npm", args: ["run", "build"], timeoutSeconds: 300 },
|
|
2335
|
+
{ name: "test", command: "npm", args: ["run", "test"], timeoutSeconds: 300 }
|
|
2336
|
+
]
|
|
2337
|
+
};
|
|
2338
|
+
writeFileSync3(gatesDocPath, JSON.stringify(defaultGates, null, 2) + "\n", "utf8");
|
|
2339
|
+
gatesDocCreated = true;
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
errors.push(`failed to create gates doc: ${String(e)}`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
projectId: project.projectId,
|
|
2346
|
+
displayName: project.displayName,
|
|
2347
|
+
repoPath: project.repoPath,
|
|
2348
|
+
repoExists: true,
|
|
2349
|
+
taskBoardCreated,
|
|
2350
|
+
zoneMapCreated,
|
|
2351
|
+
gatesDocCreated,
|
|
2352
|
+
errors
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
loadPortfolio(configPath) {
|
|
2356
|
+
const fullPath = this.resolveLocalPath(configPath);
|
|
2357
|
+
if (!existsSync5(fullPath)) {
|
|
2358
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
2359
|
+
}
|
|
2360
|
+
try {
|
|
2361
|
+
const raw = JSON.parse(readFileSync5(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2362
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
2363
|
+
if (!parsed.success) {
|
|
2364
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
2365
|
+
}
|
|
2366
|
+
return ok(parsed.data);
|
|
2367
|
+
} catch (e) {
|
|
2368
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
resolveProjectPath(path) {
|
|
2372
|
+
return isAbsolute4(path) ? path : resolve4(this.root, path);
|
|
2373
|
+
}
|
|
2374
|
+
resolveLocalPath(path) {
|
|
2375
|
+
return isAbsolute4(path) ? path : resolve4(this.root, path);
|
|
2376
|
+
}
|
|
2377
|
+
fail(message) {
|
|
2378
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
function createBootstrapService(root) {
|
|
2382
|
+
return new BootstrapService(root);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/core/dashboard.service.ts
|
|
2386
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
2387
|
+
import { isAbsolute as isAbsolute5, join as join7, resolve as resolve5 } from "path";
|
|
2388
|
+
|
|
2389
|
+
// src/core/lock.service.ts
|
|
2390
|
+
var LOCK_TAG_PREFIX = "oreshnik-lock/";
|
|
2391
|
+
var LockService = class {
|
|
2392
|
+
git;
|
|
2393
|
+
constructor(repoPath) {
|
|
2394
|
+
this.git = new GitService(repoPath);
|
|
2395
|
+
}
|
|
2396
|
+
acquire(zone, operator, sprint, ttl = 3600) {
|
|
2397
|
+
const sanitized = this.sanitizeRef(zone);
|
|
2398
|
+
const existingResult = this.check(zone);
|
|
2399
|
+
if (existingResult.ok && existingResult.value.active.length > 0) {
|
|
2400
|
+
const existing = existingResult.value.active[0];
|
|
2401
|
+
if (!existing) {
|
|
2402
|
+
return err(this.fail("Unexpected: active lock array element is undefined"));
|
|
2403
|
+
}
|
|
2404
|
+
return ok({
|
|
2405
|
+
zone,
|
|
2406
|
+
acquired: false,
|
|
2407
|
+
existingLock: existing,
|
|
2408
|
+
error: `Zone locked by ${existing.operator} until ${existing.expiresAt}`
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
const now = /* @__PURE__ */ new Date();
|
|
2412
|
+
const expiresAt = new Date(now.getTime() + ttl * 1e3);
|
|
2413
|
+
const lock = {
|
|
2414
|
+
zone,
|
|
2415
|
+
operator,
|
|
2416
|
+
sprint,
|
|
2417
|
+
acquiredAt: now.toISOString(),
|
|
2418
|
+
ttl,
|
|
2419
|
+
expiresAt: expiresAt.toISOString()
|
|
2420
|
+
};
|
|
2421
|
+
const parsed = ZoneLockSchema.safeParse(lock);
|
|
2422
|
+
if (!parsed.success) {
|
|
2423
|
+
return err(this.fail(`Invalid lock: ${parsed.error.message}`));
|
|
2424
|
+
}
|
|
2425
|
+
const tagName = `${LOCK_TAG_PREFIX}${sanitized}`;
|
|
2426
|
+
const annotation = JSON.stringify(parsed.data);
|
|
2427
|
+
const tagResult = this.git.createTag(tagName, annotation);
|
|
2428
|
+
if (!tagResult.ok) {
|
|
2429
|
+
return err(this.fail(`Failed to create lock tag: ${tagResult.error.message}`));
|
|
2430
|
+
}
|
|
2431
|
+
const pushResult = this.git.exec(["push", "origin", tagName]);
|
|
2432
|
+
if (!pushResult.ok) {
|
|
2433
|
+
const msg = pushResult.error || pushResult.output || "unknown error";
|
|
2434
|
+
if (typeof msg === "string" && (msg.includes("rejected") || msg.includes("failed"))) {
|
|
2435
|
+
this.git.deleteTag(tagName);
|
|
2436
|
+
return ok({
|
|
2437
|
+
zone,
|
|
2438
|
+
acquired: false,
|
|
2439
|
+
error: "Failed to push lock: another operator may have acquired it"
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
this.git.deleteTag(tagName);
|
|
2443
|
+
return err(this.fail(`Failed to push lock: ${msg}`));
|
|
2444
|
+
}
|
|
2445
|
+
return ok({ zone, acquired: true });
|
|
2446
|
+
}
|
|
2447
|
+
release(zone) {
|
|
2448
|
+
const sanitized = this.sanitizeRef(zone);
|
|
2449
|
+
const tagName = `${LOCK_TAG_PREFIX}${sanitized}`;
|
|
2450
|
+
const deleteResult = this.git.exec(["push", "origin", `:refs/tags/${tagName}`]);
|
|
2451
|
+
if (!deleteResult.ok) {
|
|
2452
|
+
const msg = deleteResult.error || deleteResult.output || "unknown error";
|
|
2453
|
+
return err(this.fail(`Failed to release lock: ${msg}`));
|
|
2454
|
+
}
|
|
2455
|
+
this.git.deleteTag(tagName);
|
|
2456
|
+
return ok({ zone, acquired: false });
|
|
2457
|
+
}
|
|
2458
|
+
check(zone) {
|
|
2459
|
+
const now = /* @__PURE__ */ new Date();
|
|
2460
|
+
const locks = [];
|
|
2461
|
+
const active = [];
|
|
2462
|
+
const expired = [];
|
|
2463
|
+
const fetchResult = this.git.exec(["ls-remote", "origin", `refs/tags/${LOCK_TAG_PREFIX}*`]);
|
|
2464
|
+
if (!fetchResult.ok) {
|
|
2465
|
+
return err(this.fail(`Failed to fetch locks: ${fetchResult.error || fetchResult.output}`));
|
|
2466
|
+
}
|
|
2467
|
+
const lines = (fetchResult.output || "").split("\n").filter(Boolean);
|
|
2468
|
+
for (const line of lines) {
|
|
2469
|
+
const parts = line.split(" ");
|
|
2470
|
+
if (parts.length < 2) continue;
|
|
2471
|
+
const refName = parts[1];
|
|
2472
|
+
if (!refName) continue;
|
|
2473
|
+
const tagName = refName.replace("refs/tags/", "");
|
|
2474
|
+
const contentResult = this.git.exec(["tag", "-l", "--format=%(contents)", tagName]);
|
|
2475
|
+
if (!contentResult.ok || !contentResult.output) continue;
|
|
2476
|
+
try {
|
|
2477
|
+
const raw = JSON.parse(contentResult.output.trim());
|
|
2478
|
+
const parsed = ZoneLockSchema.safeParse(raw);
|
|
2479
|
+
if (!parsed.success) continue;
|
|
2480
|
+
const lock = parsed.data;
|
|
2481
|
+
locks.push(lock);
|
|
2482
|
+
const expiresAt = new Date(lock.expiresAt);
|
|
2483
|
+
if (expiresAt > now) {
|
|
2484
|
+
active.push(lock);
|
|
2485
|
+
} else {
|
|
2486
|
+
expired.push(lock);
|
|
2487
|
+
}
|
|
2488
|
+
} catch {
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (zone) {
|
|
2493
|
+
const zoneActive = active.filter((l) => l.zone === zone);
|
|
2494
|
+
const zoneExpired = expired.filter((l) => l.zone === zone);
|
|
2495
|
+
return ok({
|
|
2496
|
+
fetchedAt: now.toISOString(),
|
|
2497
|
+
locks: [...zoneActive, ...zoneExpired],
|
|
2498
|
+
active: zoneActive,
|
|
2499
|
+
expired: zoneExpired
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
const result = {
|
|
2503
|
+
fetchedAt: now.toISOString(),
|
|
2504
|
+
locks,
|
|
2505
|
+
active,
|
|
2506
|
+
expired
|
|
2507
|
+
};
|
|
2508
|
+
const parsedResult = LockListSchema.safeParse(result);
|
|
2509
|
+
if (!parsedResult.success) {
|
|
2510
|
+
return err(this.fail(`Invalid lock list: ${parsedResult.error.message}`));
|
|
2511
|
+
}
|
|
2512
|
+
return ok(result);
|
|
2513
|
+
}
|
|
2514
|
+
sanitizeRef(path) {
|
|
2515
|
+
return path.replace(/\\/g, "/").replace(/\/\//g, "/").replace(/^\//, "").replace(/\/$/, "").replace(/\//g, "_").replace(/\*\*/g, "--").replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
2516
|
+
}
|
|
2517
|
+
fail(message) {
|
|
2518
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2519
|
+
}
|
|
2520
|
+
};
|
|
2521
|
+
function createLockService(repoPath) {
|
|
2522
|
+
return new LockService(repoPath);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/core/dashboard.service.ts
|
|
2526
|
+
var DashboardService = class {
|
|
2527
|
+
constructor(root) {
|
|
2528
|
+
this.root = root;
|
|
2529
|
+
this.lockService = new LockService(root);
|
|
2530
|
+
}
|
|
2531
|
+
root;
|
|
2532
|
+
lockService;
|
|
2533
|
+
generate(configPath = ".oreshnik.portfolio.json") {
|
|
2534
|
+
const configResult = this.loadPortfolio(configPath);
|
|
2535
|
+
if (!configResult.ok) return configResult;
|
|
2536
|
+
const config = configResult.value;
|
|
2537
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2538
|
+
const projects = [];
|
|
2539
|
+
let totalTasks = 0;
|
|
2540
|
+
let totalDone = 0;
|
|
2541
|
+
let totalPending = 0;
|
|
2542
|
+
let projectsWithRepo = 0;
|
|
2543
|
+
let projectsReady = 0;
|
|
2544
|
+
let projectsBlocked = 0;
|
|
2545
|
+
for (const project of config.projects) {
|
|
2546
|
+
const dash = this.inspectProject(project);
|
|
2547
|
+
projects.push(dash);
|
|
2548
|
+
if (dash.repoExists) {
|
|
2549
|
+
projectsWithRepo++;
|
|
2550
|
+
if (dash.warnings.length === 0) projectsReady++;
|
|
2551
|
+
else projectsBlocked++;
|
|
2552
|
+
} else {
|
|
2553
|
+
projectsBlocked++;
|
|
2554
|
+
}
|
|
2555
|
+
totalTasks += dash.taskCounts.total;
|
|
2556
|
+
totalDone += dash.taskCounts.done;
|
|
2557
|
+
totalPending += dash.taskCounts.total - dash.taskCounts.done;
|
|
2558
|
+
}
|
|
2559
|
+
return ok({
|
|
2560
|
+
portfolioId: config.portfolio.id,
|
|
2561
|
+
portfolioName: config.portfolio.name,
|
|
2562
|
+
generatedAt: now,
|
|
2563
|
+
projectCount: config.projects.length,
|
|
2564
|
+
projectsWithRepo,
|
|
2565
|
+
projectsReady,
|
|
2566
|
+
projectsBlocked,
|
|
2567
|
+
totalTasks,
|
|
2568
|
+
totalDone,
|
|
2569
|
+
totalPending,
|
|
2570
|
+
projects
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
2573
|
+
toMarkdown(dashboard) {
|
|
2574
|
+
const lines = [
|
|
2575
|
+
`# ${dashboard.portfolioName}`,
|
|
2576
|
+
`Generated: ${dashboard.generatedAt}`,
|
|
2577
|
+
``,
|
|
2578
|
+
`## Summary`,
|
|
2579
|
+
``,
|
|
2580
|
+
`| Metric | Value |`,
|
|
2581
|
+
`|---|---|`,
|
|
2582
|
+
`| Projects | ${dashboard.projectCount} |`,
|
|
2583
|
+
`| With repo | ${dashboard.projectsWithRepo} |`,
|
|
2584
|
+
`| Ready | ${dashboard.projectsReady} |`,
|
|
2585
|
+
`| Blocked | ${dashboard.projectsBlocked} |`,
|
|
2586
|
+
`| Total tasks | ${dashboard.totalTasks} |`,
|
|
2587
|
+
`| Done | ${dashboard.totalDone} |`,
|
|
2588
|
+
`| Pending | ${dashboard.totalPending} |`,
|
|
2589
|
+
``,
|
|
2590
|
+
`## Projects`,
|
|
2591
|
+
``
|
|
2592
|
+
];
|
|
2593
|
+
for (const project of dashboard.projects) {
|
|
2594
|
+
const status = project.repoExists ? project.warnings.length === 0 ? "ready" : "needs-attention" : "missing";
|
|
2595
|
+
const icon = project.repoExists ? project.warnings.length === 0 ? "v" : "~" : "x";
|
|
2596
|
+
lines.push(`### ${icon} ${project.displayName} (\`${project.projectId}\`) \u2014 ${status}`);
|
|
2597
|
+
lines.push(``);
|
|
2598
|
+
lines.push(`| Field | Value |`);
|
|
2599
|
+
lines.push(`|---|---|`);
|
|
2600
|
+
lines.push(`| Repo | ${project.repoExists ? project.repoPath : "not found"} |`);
|
|
2601
|
+
lines.push(`| Task-board | ${project.taskBoardExists ? "found" : "missing"} |`);
|
|
2602
|
+
lines.push(`| Zone-map | ${project.zoneMapExists ? "found" : "missing"} |`);
|
|
2603
|
+
lines.push(`| Injection | ${project.injectionPolicy} |`);
|
|
2604
|
+
lines.push(`| Priority | ${project.priority} |`);
|
|
2605
|
+
lines.push(``);
|
|
2606
|
+
if (project.repoExists) {
|
|
2607
|
+
lines.push(`### Task Status`);
|
|
2608
|
+
lines.push(``);
|
|
2609
|
+
lines.push(`| Status | Count |`);
|
|
2610
|
+
lines.push(`|---|---|`);
|
|
2611
|
+
lines.push(`| Total | ${project.taskCounts.total} |`);
|
|
2612
|
+
lines.push(`| Ready | ${project.taskCounts.ready} |`);
|
|
2613
|
+
lines.push(`| Active | ${project.taskCounts.active} |`);
|
|
2614
|
+
lines.push(`| Done | ${project.taskCounts.done} |`);
|
|
2615
|
+
lines.push(`| Blocked | ${project.taskCounts.blocked} |`);
|
|
2616
|
+
lines.push(`| Pending | ${project.taskCounts.pending} |`);
|
|
2617
|
+
lines.push(``);
|
|
2618
|
+
if (project.operatorBreakdown.length > 0) {
|
|
2619
|
+
lines.push(`### Operator Breakdown`);
|
|
2620
|
+
lines.push(``);
|
|
2621
|
+
lines.push(`| Operator | Total | Ready | Active | Done | Blocked | Pending |`);
|
|
2622
|
+
lines.push(`|---|---|---|---|---|---|---|`);
|
|
2623
|
+
for (const ob of project.operatorBreakdown) {
|
|
2624
|
+
lines.push(`| ${ob.operator} | ${ob.total} | ${ob.ready} | ${ob.active} | ${ob.done} | ${ob.blocked} | ${ob.pending} |`);
|
|
2625
|
+
}
|
|
2626
|
+
lines.push(``);
|
|
2627
|
+
}
|
|
2628
|
+
if (project.locks.length > 0) {
|
|
2629
|
+
lines.push(`### Active Locks`);
|
|
2630
|
+
lines.push(``);
|
|
2631
|
+
lines.push(`| Zone | Operator | Sprint | Expires | Remaining |`);
|
|
2632
|
+
lines.push(`|---|---|---|---|---|`);
|
|
2633
|
+
for (const l of project.locks) {
|
|
2634
|
+
lines.push(`| ${l.zone} | ${l.operator} | ${l.sprint} | ${l.expiresAt} | ${l.remainingMinutes}m |`);
|
|
2635
|
+
}
|
|
2636
|
+
lines.push(``);
|
|
2637
|
+
}
|
|
2638
|
+
if (project.lastUpdated) {
|
|
2639
|
+
lines.push(`Last updated: ${project.lastUpdated}`);
|
|
2640
|
+
lines.push(``);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
for (const warning of project.warnings) {
|
|
2644
|
+
lines.push(`- **Warning:** ${warning}`);
|
|
2645
|
+
}
|
|
2646
|
+
lines.push(``);
|
|
2647
|
+
}
|
|
2648
|
+
return lines.join("\n");
|
|
2649
|
+
}
|
|
2650
|
+
inspectProject(project) {
|
|
2651
|
+
const repoPath = this.resolvePath(project.repoPath);
|
|
2652
|
+
const repoExists = existsSync6(repoPath);
|
|
2653
|
+
const taskBoardPath = join7(repoPath, ...project.taskBoardPath.split("/"));
|
|
2654
|
+
const zoneMapPath = join7(repoPath, ...project.zoneMapPath.split("/"));
|
|
2655
|
+
const taskBoardExists = repoExists && existsSync6(taskBoardPath);
|
|
2656
|
+
const zoneMapExists = repoExists && existsSync6(zoneMapPath);
|
|
2657
|
+
const warnings = [];
|
|
2658
|
+
const taskCounts = {
|
|
2659
|
+
total: 0,
|
|
2660
|
+
ready: 0,
|
|
2661
|
+
active: 0,
|
|
2662
|
+
done: 0,
|
|
2663
|
+
blocked: 0,
|
|
2664
|
+
pending: 0
|
|
2665
|
+
};
|
|
2666
|
+
const operatorMap = /* @__PURE__ */ new Map();
|
|
2667
|
+
const tasks = [];
|
|
2668
|
+
let lastUpdated;
|
|
2669
|
+
if (taskBoardExists) {
|
|
2670
|
+
try {
|
|
2671
|
+
const raw = JSON.parse(readFileSync6(taskBoardPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2672
|
+
const parsed = TaskBoardSchema.safeParse(raw);
|
|
2673
|
+
if (parsed.success) {
|
|
2674
|
+
const board = parsed.data;
|
|
2675
|
+
taskCounts.total = board.tasks.length;
|
|
2676
|
+
lastUpdated = board.updatedAt;
|
|
2677
|
+
for (const task of board.tasks) {
|
|
2678
|
+
if (task.status === "ready") taskCounts.ready++;
|
|
2679
|
+
else if (task.status === "active") taskCounts.active++;
|
|
2680
|
+
else if (task.status === "done") taskCounts.done++;
|
|
2681
|
+
else if (task.status === "blocked") taskCounts.blocked++;
|
|
2682
|
+
else taskCounts.pending++;
|
|
2683
|
+
const owner = task.owner || "unassigned";
|
|
2684
|
+
let ob = operatorMap.get(owner);
|
|
2685
|
+
if (!ob) {
|
|
2686
|
+
ob = { operator: owner, ready: 0, active: 0, done: 0, blocked: 0, pending: 0, total: 0 };
|
|
2687
|
+
operatorMap.set(owner, ob);
|
|
2688
|
+
}
|
|
2689
|
+
ob.total++;
|
|
2690
|
+
if (task.status === "ready") ob.ready++;
|
|
2691
|
+
else if (task.status === "active") ob.active++;
|
|
2692
|
+
else if (task.status === "done") ob.done++;
|
|
2693
|
+
else if (task.status === "blocked") ob.blocked++;
|
|
2694
|
+
else ob.pending++;
|
|
2695
|
+
tasks.push({
|
|
2696
|
+
id: task.id,
|
|
2697
|
+
title: task.title.slice(0, 100),
|
|
2698
|
+
owner: task.owner || "unassigned",
|
|
2699
|
+
status: task.status,
|
|
2700
|
+
dependsOn: task.dependsOn || []
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
} else {
|
|
2704
|
+
warnings.push("task-board parse error");
|
|
2705
|
+
}
|
|
2706
|
+
} catch {
|
|
2707
|
+
warnings.push("task-board read error");
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const operatorBreakdown = Array.from(operatorMap.values()).sort((a, b) => b.total - a.total);
|
|
2711
|
+
const locks = [];
|
|
2712
|
+
try {
|
|
2713
|
+
const lockResult = this.lockService.check();
|
|
2714
|
+
if (lockResult.ok) {
|
|
2715
|
+
for (const l of lockResult.value.active) {
|
|
2716
|
+
const expiresAt = new Date(l.expiresAt);
|
|
2717
|
+
const remainingMinutes = Math.max(0, Math.ceil((expiresAt.getTime() - Date.now()) / 6e4));
|
|
2718
|
+
if (l.zone.includes(project.projectId) || l.zone.startsWith("**") || l.zone.startsWith("repo")) {
|
|
2719
|
+
locks.push({
|
|
2720
|
+
zone: l.zone,
|
|
2721
|
+
operator: l.operator,
|
|
2722
|
+
sprint: l.sprint,
|
|
2723
|
+
expiresAt: l.expiresAt,
|
|
2724
|
+
remainingMinutes
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
} catch {
|
|
2730
|
+
}
|
|
2731
|
+
if (!repoExists) warnings.push("repo missing");
|
|
2732
|
+
if (repoExists && !taskBoardExists) warnings.push("task-board missing");
|
|
2733
|
+
if (repoExists && !zoneMapExists) warnings.push("zone-map missing");
|
|
2734
|
+
if (project.validationGates.length === 0) warnings.push("validation gates missing");
|
|
2735
|
+
return {
|
|
2736
|
+
projectId: project.projectId,
|
|
2737
|
+
displayName: project.displayName,
|
|
2738
|
+
repoPath: project.repoPath,
|
|
2739
|
+
repoExists,
|
|
2740
|
+
taskBoardExists,
|
|
2741
|
+
zoneMapExists,
|
|
2742
|
+
injectionPolicy: project.injectionPolicy,
|
|
2743
|
+
priority: project.priority,
|
|
2744
|
+
taskCounts,
|
|
2745
|
+
operatorBreakdown,
|
|
2746
|
+
tasks,
|
|
2747
|
+
locks,
|
|
2748
|
+
lastUpdated,
|
|
2749
|
+
warnings
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
loadPortfolio(configPath) {
|
|
2753
|
+
const fullPath = this.resolvePath(configPath);
|
|
2754
|
+
if (!existsSync6(fullPath)) {
|
|
2755
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
2756
|
+
}
|
|
2757
|
+
try {
|
|
2758
|
+
const raw = JSON.parse(readFileSync6(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2759
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
2760
|
+
if (!parsed.success) {
|
|
2761
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
2762
|
+
}
|
|
2763
|
+
return ok(parsed.data);
|
|
2764
|
+
} catch (e) {
|
|
2765
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
resolvePath(path) {
|
|
2769
|
+
return isAbsolute5(path) ? path : resolve5(this.root, path);
|
|
2770
|
+
}
|
|
2771
|
+
fail(message) {
|
|
2772
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
function createDashboardService(root) {
|
|
2776
|
+
return new DashboardService(root);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// src/core/zone-check.service.ts
|
|
2780
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
|
|
2781
|
+
import { isAbsolute as isAbsolute6, join as join8, resolve as resolve6 } from "path";
|
|
2782
|
+
var ZoneCheckService = class {
|
|
2783
|
+
constructor(root) {
|
|
2784
|
+
this.root = root;
|
|
2785
|
+
}
|
|
2786
|
+
root;
|
|
2787
|
+
checkAll(configPath, operator, sprint, projectFilter) {
|
|
2788
|
+
const configResult = this.loadPortfolio(configPath);
|
|
2789
|
+
if (!configResult.ok) return configResult;
|
|
2790
|
+
const config = configResult.value;
|
|
2791
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2792
|
+
const targets = projectFilter ? config.projects.filter((p) => projectFilter.includes(p.projectId)) : config.projects;
|
|
2793
|
+
const projects = [];
|
|
2794
|
+
let projectsWithZoneMap = 0;
|
|
2795
|
+
let projectsWithViolations = 0;
|
|
2796
|
+
let projectsPassed = 0;
|
|
2797
|
+
let totalViolations = 0;
|
|
2798
|
+
let totalWarnings = 0;
|
|
2799
|
+
for (const project of targets) {
|
|
2800
|
+
const check = this.checkProject(project, operator, sprint);
|
|
2801
|
+
projects.push(check);
|
|
2802
|
+
if (check.zoneMapExists && check.zoneMapValid) {
|
|
2803
|
+
projectsWithZoneMap++;
|
|
2804
|
+
if (check.passed) projectsPassed++;
|
|
2805
|
+
if (check.violations.length > 0) projectsWithViolations++;
|
|
2806
|
+
totalViolations += check.violations.length;
|
|
2807
|
+
totalWarnings += check.warnings.length;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
return ok({
|
|
2811
|
+
portfolioId: config.portfolio.id,
|
|
2812
|
+
checkedAt: now,
|
|
2813
|
+
operator,
|
|
2814
|
+
sprint,
|
|
2815
|
+
projects,
|
|
2816
|
+
summary: {
|
|
2817
|
+
totalProjects: projects.length,
|
|
2818
|
+
projectsWithZoneMap,
|
|
2819
|
+
projectsWithViolations,
|
|
2820
|
+
projectsPassed,
|
|
2821
|
+
totalViolations,
|
|
2822
|
+
totalWarnings
|
|
2823
|
+
}
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
checkProject(project, operator, sprint) {
|
|
2827
|
+
const repoPath = this.resolvePath(project.repoPath);
|
|
2828
|
+
const errors = [];
|
|
2829
|
+
if (!existsSync7(repoPath)) {
|
|
2830
|
+
return {
|
|
2831
|
+
projectId: project.projectId,
|
|
2832
|
+
displayName: project.displayName,
|
|
2833
|
+
repoPath: project.repoPath,
|
|
2834
|
+
zoneMapExists: false,
|
|
2835
|
+
zoneMapValid: false,
|
|
2836
|
+
branch: "unknown",
|
|
2837
|
+
filesChecked: 0,
|
|
2838
|
+
violations: [],
|
|
2839
|
+
warnings: [],
|
|
2840
|
+
passed: false,
|
|
2841
|
+
errors: ["repo not found"]
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
const zoneMapPath = join8(repoPath, ...project.zoneMapPath.split("/"));
|
|
2845
|
+
const zoneMapExists = existsSync7(zoneMapPath);
|
|
2846
|
+
if (!zoneMapExists) {
|
|
2847
|
+
return {
|
|
2848
|
+
projectId: project.projectId,
|
|
2849
|
+
displayName: project.displayName,
|
|
2850
|
+
repoPath: project.repoPath,
|
|
2851
|
+
zoneMapExists: false,
|
|
2852
|
+
zoneMapValid: false,
|
|
2853
|
+
branch: "unknown",
|
|
2854
|
+
filesChecked: 0,
|
|
2855
|
+
violations: [],
|
|
2856
|
+
warnings: [],
|
|
2857
|
+
passed: false,
|
|
2858
|
+
errors: ["zone-map not found"]
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
let zoneMap;
|
|
2862
|
+
try {
|
|
2863
|
+
const raw = JSON.parse(readFileSync7(zoneMapPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2864
|
+
const parsed = ZoneMapSchema.safeParse(raw);
|
|
2865
|
+
if (!parsed.success) {
|
|
2866
|
+
return {
|
|
2867
|
+
projectId: project.projectId,
|
|
2868
|
+
displayName: project.displayName,
|
|
2869
|
+
repoPath: project.repoPath,
|
|
2870
|
+
zoneMapExists: true,
|
|
2871
|
+
zoneMapValid: false,
|
|
2872
|
+
branch: "unknown",
|
|
2873
|
+
filesChecked: 0,
|
|
2874
|
+
violations: [],
|
|
2875
|
+
warnings: [],
|
|
2876
|
+
passed: false,
|
|
2877
|
+
errors: [`invalid zone-map: ${parsed.error.message}`]
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
zoneMap = parsed.data;
|
|
2881
|
+
} catch (e) {
|
|
2882
|
+
return {
|
|
2883
|
+
projectId: project.projectId,
|
|
2884
|
+
displayName: project.displayName,
|
|
2885
|
+
repoPath: project.repoPath,
|
|
2886
|
+
zoneMapExists: true,
|
|
2887
|
+
zoneMapValid: false,
|
|
2888
|
+
branch: "unknown",
|
|
2889
|
+
filesChecked: 0,
|
|
2890
|
+
violations: [],
|
|
2891
|
+
warnings: [],
|
|
2892
|
+
passed: false,
|
|
2893
|
+
errors: [`failed to read zone-map: ${String(e)}`]
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2896
|
+
const git = new GitService(repoPath);
|
|
2897
|
+
const branchResult = git.currentBranch();
|
|
2898
|
+
const branch = branchResult.ok ? branchResult.value : "unknown";
|
|
2899
|
+
const statusResult = git.statusPorcelain();
|
|
2900
|
+
if (!statusResult.ok) {
|
|
2901
|
+
errors.push(`git status failed: ${statusResult.error.message}`);
|
|
2902
|
+
}
|
|
2903
|
+
const modifiedFiles = statusResult.ok ? statusResult.value.map((e) => e.path) : [];
|
|
2904
|
+
const zoneEngine = createZoneEngine();
|
|
2905
|
+
const checkResult = zoneEngine.check(
|
|
2906
|
+
modifiedFiles,
|
|
2907
|
+
operator,
|
|
2908
|
+
sprint || "*",
|
|
2909
|
+
zoneMap
|
|
2910
|
+
);
|
|
2911
|
+
return {
|
|
2912
|
+
projectId: project.projectId,
|
|
2913
|
+
displayName: project.displayName,
|
|
2914
|
+
repoPath: project.repoPath,
|
|
2915
|
+
zoneMapExists: true,
|
|
2916
|
+
zoneMapValid: true,
|
|
2917
|
+
branch,
|
|
2918
|
+
filesChecked: checkResult.filesChecked,
|
|
2919
|
+
violations: checkResult.violations,
|
|
2920
|
+
warnings: checkResult.warnings,
|
|
2921
|
+
passed: checkResult.violations.length === 0,
|
|
2922
|
+
errors
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
loadPortfolio(configPath) {
|
|
2926
|
+
const fullPath = this.resolvePath(configPath);
|
|
2927
|
+
if (!existsSync7(fullPath)) {
|
|
2928
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
2929
|
+
}
|
|
2930
|
+
try {
|
|
2931
|
+
const raw = JSON.parse(readFileSync7(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
2932
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
2933
|
+
if (!parsed.success) {
|
|
2934
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
2935
|
+
}
|
|
2936
|
+
return ok(parsed.data);
|
|
2937
|
+
} catch (e) {
|
|
2938
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
resolvePath(path) {
|
|
2942
|
+
return isAbsolute6(path) ? path : resolve6(this.root, path);
|
|
2943
|
+
}
|
|
2944
|
+
fail(message) {
|
|
2945
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
function createZoneCheckService(root) {
|
|
2949
|
+
return new ZoneCheckService(root);
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
// src/core/audit.service.ts
|
|
2953
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
2954
|
+
import { isAbsolute as isAbsolute7, join as join9, resolve as resolve7 } from "path";
|
|
2955
|
+
var AuditService = class {
|
|
2956
|
+
constructor(root) {
|
|
2957
|
+
this.root = root;
|
|
2958
|
+
}
|
|
2959
|
+
root;
|
|
2960
|
+
audit(configPath = ".oreshnik.portfolio.json", projectFilter) {
|
|
2961
|
+
const configResult = this.loadPortfolio(configPath);
|
|
2962
|
+
if (!configResult.ok) return configResult;
|
|
2963
|
+
const config = configResult.value;
|
|
2964
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2965
|
+
const targets = projectFilter ? config.projects.filter((p) => projectFilter.includes(p.projectId)) : config.projects;
|
|
2966
|
+
const projects = [];
|
|
2967
|
+
let projectsWithRepo = 0;
|
|
2968
|
+
let projectsDirty = 0;
|
|
2969
|
+
let projectsClean = 0;
|
|
2970
|
+
let totalTasks = 0;
|
|
2971
|
+
let totalCheckpoints = 0;
|
|
2972
|
+
for (const project of targets) {
|
|
2973
|
+
const audit = this.auditProject(project);
|
|
2974
|
+
projects.push(audit);
|
|
2975
|
+
if (audit.repoExists) {
|
|
2976
|
+
projectsWithRepo++;
|
|
2977
|
+
if (audit.modifiedFiles > 0) projectsDirty++;
|
|
2978
|
+
else projectsClean++;
|
|
2979
|
+
totalTasks += audit.taskCount;
|
|
2980
|
+
totalCheckpoints += audit.checkpointTags.length;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
return ok({
|
|
2984
|
+
portfolioId: config.portfolio.id,
|
|
2985
|
+
auditedAt: now,
|
|
2986
|
+
projects,
|
|
2987
|
+
summary: {
|
|
2988
|
+
totalProjects: projects.length,
|
|
2989
|
+
projectsWithRepo,
|
|
2990
|
+
projectsDirty,
|
|
2991
|
+
projectsClean,
|
|
2992
|
+
totalTasks,
|
|
2993
|
+
totalCheckpoints
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
secretScan(configPath = ".oreshnik.portfolio.json") {
|
|
2998
|
+
const configResult = this.loadPortfolio(configPath);
|
|
2999
|
+
if (!configResult.ok) return configResult;
|
|
3000
|
+
const config = configResult.value;
|
|
3001
|
+
const findings = [];
|
|
3002
|
+
const sensitivePatterns = [
|
|
3003
|
+
{ pattern: /\.env$/i, reason: "env file detected (should be in .gitignore)" },
|
|
3004
|
+
{ pattern: /credentials\.json$/i, reason: "credentials file detected" },
|
|
3005
|
+
{ pattern: /secret/i, reason: "file with 'secret' in name" },
|
|
3006
|
+
{ pattern: /\.pem$/i, reason: "private key file detected" }
|
|
3007
|
+
];
|
|
3008
|
+
for (const project of config.projects) {
|
|
3009
|
+
const repoPath = this.resolvePath(project.repoPath);
|
|
3010
|
+
if (!existsSync8(repoPath)) continue;
|
|
3011
|
+
try {
|
|
3012
|
+
const git = new GitService(repoPath);
|
|
3013
|
+
const statusResult = git.statusPorcelain();
|
|
3014
|
+
if (!statusResult.ok) continue;
|
|
3015
|
+
for (const entry of statusResult.value) {
|
|
3016
|
+
for (const { pattern, reason } of sensitivePatterns) {
|
|
3017
|
+
if (pattern.test(entry.path)) {
|
|
3018
|
+
findings.push(`${project.projectId}: ${entry.path} \u2014 ${reason}`);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return ok(findings);
|
|
3026
|
+
}
|
|
3027
|
+
auditProject(project) {
|
|
3028
|
+
const repoPath = this.resolvePath(project.repoPath);
|
|
3029
|
+
const errors = [];
|
|
3030
|
+
if (!existsSync8(repoPath)) {
|
|
3031
|
+
return {
|
|
3032
|
+
projectId: project.projectId,
|
|
3033
|
+
displayName: project.displayName,
|
|
3034
|
+
repoPath: project.repoPath,
|
|
3035
|
+
repoExists: false,
|
|
3036
|
+
branch: "unknown",
|
|
3037
|
+
checkpointTags: [],
|
|
3038
|
+
taskBoardExists: false,
|
|
3039
|
+
taskCount: 0,
|
|
3040
|
+
zoneMapExists: false,
|
|
3041
|
+
zoneMapValid: false,
|
|
3042
|
+
modifiedFiles: 0,
|
|
3043
|
+
errors: ["repo not found"]
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
const git = new GitService(repoPath);
|
|
3047
|
+
const branchResult = git.currentBranch();
|
|
3048
|
+
const branch = branchResult.ok ? branchResult.value : "unknown";
|
|
3049
|
+
let lastCommit;
|
|
3050
|
+
try {
|
|
3051
|
+
const logResult = git.exec(["log", "-1", "--format=%H|%s|%ai|%an"]);
|
|
3052
|
+
if (logResult.ok && logResult.output) {
|
|
3053
|
+
const parts = logResult.output.split("|");
|
|
3054
|
+
const hash = parts[0];
|
|
3055
|
+
if (hash) lastCommit = {
|
|
3056
|
+
hash: hash.slice(0, 8),
|
|
3057
|
+
message: parts[1] || "",
|
|
3058
|
+
date: parts[2] || "",
|
|
3059
|
+
author: parts[3] || ""
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
} catch {
|
|
3063
|
+
}
|
|
3064
|
+
const tagsResult = git.exec([
|
|
3065
|
+
"tag",
|
|
3066
|
+
"-l",
|
|
3067
|
+
"oreshnik/*",
|
|
3068
|
+
"--sort=-creatordate"
|
|
3069
|
+
]);
|
|
3070
|
+
const checkpointTags = tagsResult.ok ? tagsResult.output.split("\n").filter(Boolean) : [];
|
|
3071
|
+
const taskBoardPath = join9(repoPath, ...project.taskBoardPath.split("/"));
|
|
3072
|
+
const taskBoardExists = existsSync8(taskBoardPath);
|
|
3073
|
+
let taskCount = 0;
|
|
3074
|
+
if (taskBoardExists) {
|
|
3075
|
+
try {
|
|
3076
|
+
const raw = JSON.parse(readFileSync8(taskBoardPath, "utf8").replace(/^\uFEFF/, ""));
|
|
3077
|
+
taskCount = raw.tasks?.length || 0;
|
|
3078
|
+
} catch {
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const zoneMapPath = join9(repoPath, ...project.zoneMapPath.split("/"));
|
|
3082
|
+
const zoneMapExists = existsSync8(zoneMapPath);
|
|
3083
|
+
let zoneMapValid = false;
|
|
3084
|
+
if (zoneMapExists) {
|
|
3085
|
+
try {
|
|
3086
|
+
const raw = JSON.parse(readFileSync8(zoneMapPath, "utf8").replace(/^\uFEFF/, ""));
|
|
3087
|
+
zoneMapValid = raw.zones && Object.keys(raw.zones).length > 0;
|
|
3088
|
+
} catch {
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
const statusResult = git.statusPorcelain();
|
|
3092
|
+
const modifiedFiles = statusResult.ok ? statusResult.value.length : 0;
|
|
3093
|
+
return {
|
|
3094
|
+
projectId: project.projectId,
|
|
3095
|
+
displayName: project.displayName,
|
|
3096
|
+
repoPath: project.repoPath,
|
|
3097
|
+
repoExists: true,
|
|
3098
|
+
branch,
|
|
3099
|
+
lastCommit,
|
|
3100
|
+
checkpointTags,
|
|
3101
|
+
taskBoardExists,
|
|
3102
|
+
taskCount,
|
|
3103
|
+
zoneMapExists,
|
|
3104
|
+
zoneMapValid,
|
|
3105
|
+
modifiedFiles,
|
|
3106
|
+
errors
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
loadPortfolio(configPath) {
|
|
3110
|
+
const fullPath = this.resolvePath(configPath);
|
|
3111
|
+
if (!existsSync8(fullPath)) {
|
|
3112
|
+
return err(this.fail(`Portfolio config not found: ${configPath}`));
|
|
3113
|
+
}
|
|
3114
|
+
try {
|
|
3115
|
+
const raw = JSON.parse(readFileSync8(fullPath, "utf8").replace(/^\uFEFF/, ""));
|
|
3116
|
+
const parsed = PortfolioConfigSchema.safeParse(raw);
|
|
3117
|
+
if (!parsed.success) {
|
|
3118
|
+
return err(this.fail(`Invalid portfolio config: ${parsed.error.message}`));
|
|
3119
|
+
}
|
|
3120
|
+
return ok(parsed.data);
|
|
3121
|
+
} catch (e) {
|
|
3122
|
+
return err(this.fail(`Failed to read portfolio config: ${String(e)}`));
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
resolvePath(path) {
|
|
3126
|
+
return isAbsolute7(path) ? path : resolve7(this.root, path);
|
|
3127
|
+
}
|
|
3128
|
+
fail(message) {
|
|
3129
|
+
return { code: "STATE_ERROR", message, exitCode: 1 };
|
|
3130
|
+
}
|
|
3131
|
+
};
|
|
3132
|
+
function createAuditService(root) {
|
|
3133
|
+
return new AuditService(root);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
// src/utils/logger.ts
|
|
3137
|
+
import chalk from "chalk";
|
|
3138
|
+
function log(level, message) {
|
|
3139
|
+
const color = level === "OK" ? chalk.green : level === "FAIL" ? chalk.red : level === "WARN" ? chalk.yellow : chalk.cyan;
|
|
3140
|
+
console.log(` [ ${color(level.padEnd(4))} ] ${message}`);
|
|
3141
|
+
}
|
|
3142
|
+
function header(title, subtitle) {
|
|
3143
|
+
console.log("");
|
|
3144
|
+
console.log(chalk.bold("=".repeat(50)));
|
|
3145
|
+
console.log(chalk.bold(` ${title}`));
|
|
3146
|
+
if (subtitle) console.log(chalk.bold(` ${subtitle}`));
|
|
3147
|
+
console.log(chalk.bold("=".repeat(50)));
|
|
3148
|
+
console.log("");
|
|
3149
|
+
}
|
|
3150
|
+
function statusBox(lines) {
|
|
3151
|
+
const maxKey = Math.max(...lines.map(([k]) => k.length));
|
|
3152
|
+
console.log("");
|
|
3153
|
+
console.log(chalk.bold("+" + "=".repeat(maxKey + 42) + "+"));
|
|
3154
|
+
const title = "ORESHNIK STATUS";
|
|
3155
|
+
const padLeft = Math.floor((maxKey + 42 - title.length) / 2);
|
|
3156
|
+
console.log(chalk.bold("|" + " ".repeat(padLeft) + title + " ".repeat(maxKey + 42 - padLeft - title.length) + "|"));
|
|
3157
|
+
console.log(chalk.bold("+" + "=".repeat(maxKey + 42) + "+"));
|
|
3158
|
+
for (const [key, value] of lines) {
|
|
3159
|
+
console.log(chalk.bold("| ") + chalk.cyan(key.padEnd(maxKey)) + chalk.bold(" ") + value.padEnd(40) + chalk.bold("|"));
|
|
3160
|
+
}
|
|
3161
|
+
console.log(chalk.bold("+" + "=".repeat(maxKey + 42) + "+"));
|
|
3162
|
+
console.log("");
|
|
3163
|
+
}
|
|
3164
|
+
function resultSummary(blockers, warnings, operator, sprint, branch, mother) {
|
|
3165
|
+
console.log("");
|
|
3166
|
+
console.log(chalk.bold("PRE-FLIGHT RESULT"));
|
|
3167
|
+
console.log(` Blockers: ${blockers}`);
|
|
3168
|
+
console.log(` Warnings: ${warnings}`);
|
|
3169
|
+
console.log(` Operator: ${operator}`);
|
|
3170
|
+
console.log(` Sprint: ${sprint || "not specified"}`);
|
|
3171
|
+
console.log(` Branch: ${branch}`);
|
|
3172
|
+
console.log(` Mother: ${mother}`);
|
|
3173
|
+
console.log("");
|
|
3174
|
+
if (blockers > 0) {
|
|
3175
|
+
console.log(chalk.red.bold("[ORESHNIK] BLOCKED"));
|
|
3176
|
+
} else {
|
|
3177
|
+
console.log((warnings > 0 ? chalk.yellow : chalk.green).bold("[ORESHNIK] OK"));
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// src/utils/exec.ts
|
|
3182
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3183
|
+
function execCommand(command, args, options) {
|
|
3184
|
+
const result = spawnSync3(command, args, {
|
|
3185
|
+
cwd: options?.cwd,
|
|
3186
|
+
encoding: "utf8",
|
|
3187
|
+
timeout: options?.timeout ?? 3e4,
|
|
3188
|
+
stdio: "pipe",
|
|
3189
|
+
shell: true
|
|
3190
|
+
});
|
|
3191
|
+
if (result.status !== 0) {
|
|
3192
|
+
return err({
|
|
3193
|
+
code: "GIT_ERROR",
|
|
3194
|
+
message: `Command failed: ${command} ${args.join(" ")}`,
|
|
3195
|
+
exitCode: result.status ?? 1,
|
|
3196
|
+
gitCommand: `${command} ${args.join(" ")}`,
|
|
3197
|
+
gitStderr: (result.stderr || "").trim()
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
return { ok: true, value: { output: (result.stdout || "").trim(), exitCode: result.status } };
|
|
3201
|
+
}
|
|
3202
|
+
function execCommandInherit(command, args, options) {
|
|
3203
|
+
const result = spawnSync3(command, args, {
|
|
3204
|
+
cwd: options?.cwd,
|
|
3205
|
+
encoding: "utf8",
|
|
3206
|
+
timeout: options?.timeout ?? 3e4,
|
|
3207
|
+
stdio: "inherit",
|
|
3208
|
+
shell: true
|
|
3209
|
+
});
|
|
3210
|
+
if (result.status !== 0) {
|
|
3211
|
+
return err({
|
|
3212
|
+
code: "GIT_ERROR",
|
|
3213
|
+
message: `Command failed: ${command} ${args.join(" ")} (exit ${result.status})`,
|
|
3214
|
+
exitCode: result.status ?? 1
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
return { ok: true, value: { exitCode: result.status ?? 0 } };
|
|
3218
|
+
}
|
|
3219
|
+
export {
|
|
3220
|
+
AuditService,
|
|
3221
|
+
BootstrapService,
|
|
3222
|
+
CanonicalService,
|
|
3223
|
+
DashboardService,
|
|
3224
|
+
DerivedDocConfigSchema,
|
|
3225
|
+
EvidenceGateResultSchema,
|
|
3226
|
+
EvidenceGateService,
|
|
3227
|
+
EvidenceRecordSchema,
|
|
3228
|
+
GitService,
|
|
3229
|
+
InjectionResultSchema,
|
|
3230
|
+
InjectionService,
|
|
3231
|
+
LockListSchema,
|
|
3232
|
+
LockResultSchema,
|
|
3233
|
+
LockService,
|
|
3234
|
+
MotherVersionSchema,
|
|
3235
|
+
NoteTaskSchema,
|
|
3236
|
+
NotesIngestionResultSchema,
|
|
3237
|
+
NotesIngestionService,
|
|
3238
|
+
OperatorIdSchema,
|
|
3239
|
+
OreshnikConfigSchema,
|
|
3240
|
+
PortfolioConfigSchema,
|
|
3241
|
+
PortfolioProjectSchema,
|
|
3242
|
+
PortfolioService,
|
|
3243
|
+
ProjectInjectionResultSchema,
|
|
3244
|
+
SprintIdSchema,
|
|
3245
|
+
StateManager,
|
|
3246
|
+
SyncService,
|
|
3247
|
+
TaskBoardSchema,
|
|
3248
|
+
TaskEvidenceCheckSchema,
|
|
3249
|
+
VaultGuard,
|
|
3250
|
+
ZoneCheckService,
|
|
3251
|
+
ZoneEngine,
|
|
3252
|
+
ZoneLockSchema,
|
|
3253
|
+
ZoneMapSchema,
|
|
3254
|
+
createAuditService,
|
|
3255
|
+
createBootstrapService,
|
|
3256
|
+
createCanonicalService,
|
|
3257
|
+
createDashboardService,
|
|
3258
|
+
createEvidenceGateService,
|
|
3259
|
+
createGitService,
|
|
3260
|
+
createInjectionService,
|
|
3261
|
+
createLockService,
|
|
3262
|
+
createNotesIngestionService,
|
|
3263
|
+
createPortfolioService,
|
|
3264
|
+
createStateManager,
|
|
3265
|
+
createSyncService,
|
|
3266
|
+
createVaultGuard,
|
|
3267
|
+
createZoneCheckService,
|
|
3268
|
+
createZoneEngine,
|
|
3269
|
+
err,
|
|
3270
|
+
execCommand,
|
|
3271
|
+
execCommandInherit,
|
|
3272
|
+
formatBranchName,
|
|
3273
|
+
header,
|
|
3274
|
+
isChildBranch,
|
|
3275
|
+
isMotherBranch,
|
|
3276
|
+
log,
|
|
3277
|
+
nonIgnoredDirtyFiles,
|
|
3278
|
+
nowDisplay,
|
|
3279
|
+
nowISO,
|
|
3280
|
+
ok,
|
|
3281
|
+
resultSummary,
|
|
3282
|
+
sanitize,
|
|
3283
|
+
statusBox,
|
|
3284
|
+
today
|
|
3285
|
+
};
|
|
3286
|
+
//# sourceMappingURL=index.js.map
|