cleargate 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -1
- package/dist/MANIFEST.json +40 -26
- package/dist/chunk-HZPJ5QX4.js +459 -0
- package/dist/chunk-HZPJ5QX4.js.map +1 -0
- package/dist/cli.cjs +419 -202
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +387 -513
- package/dist/cli.js.map +1 -1
- package/dist/lib/lifecycle-reconcile.cjs +497 -0
- package/dist/lib/lifecycle-reconcile.cjs.map +1 -0
- package/dist/lib/lifecycle-reconcile.d.cts +136 -0
- package/dist/lib/lifecycle-reconcile.d.ts +136 -0
- package/dist/lib/lifecycle-reconcile.js +20 -0
- package/dist/lib/lifecycle-reconcile.js.map +1 -0
- package/dist/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/dist/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/dist/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/dist/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/dist/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/dist/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/dist/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/dist/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/dist/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/dist/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/dist/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/dist/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/dist/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/dist/templates/cleargate-planning/CLAUDE.md +3 -1
- package/dist/templates/cleargate-planning/MANIFEST.json +40 -26
- package/package.json +8 -5
- package/templates/cleargate-planning/.claude/agents/architect.md +55 -2
- package/templates/cleargate-planning/.claude/agents/developer.md +22 -0
- package/templates/cleargate-planning/.claude/agents/devops.md +249 -0
- package/templates/cleargate-planning/.claude/agents/qa.md +41 -0
- package/templates/cleargate-planning/.claude/agents/reporter.md +44 -8
- package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +21 -0
- package/templates/cleargate-planning/.claude/hooks/stamp-and-gate.sh +12 -1
- package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +21 -1
- package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +200 -29
- package/templates/cleargate-planning/.cleargate/knowledge/mid-sprint-triage-rubric.md +160 -0
- package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +41 -9
- package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +98 -16
- package/templates/cleargate-planning/.cleargate/scripts/gate-checks.json +3 -3
- package/templates/cleargate-planning/.cleargate/scripts/init_sprint.mjs +86 -10
- package/templates/cleargate-planning/.cleargate/scripts/run_script.sh +173 -87
- package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +150 -22
- package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
- package/templates/cleargate-planning/.cleargate/scripts/validate_state.mjs +32 -8
- package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +12 -1
- package/templates/cleargate-planning/.cleargate/templates/Bug.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/CR.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/epic.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/hotfix.md +3 -0
- package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
- package/templates/cleargate-planning/.cleargate/templates/sprint_context.md +8 -0
- package/templates/cleargate-planning/.cleargate/templates/story.md +3 -0
- package/templates/cleargate-planning/CLAUDE.md +3 -1
- package/templates/cleargate-planning/MANIFEST.json +40 -26
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/lib/lifecycle-reconcile.ts
|
|
32
|
+
var lifecycle_reconcile_exports = {};
|
|
33
|
+
__export(lifecycle_reconcile_exports, {
|
|
34
|
+
ARTIFACT_TERMINAL_STATUSES: () => ARTIFACT_TERMINAL_STATUSES,
|
|
35
|
+
VERB_STATUS_MAP: () => VERB_STATUS_MAP,
|
|
36
|
+
checkVerbMismatch: () => checkVerbMismatch,
|
|
37
|
+
parseCommitMessage: () => parseCommitMessage,
|
|
38
|
+
reconcileCrossSprintOrphans: () => reconcileCrossSprintOrphans,
|
|
39
|
+
reconcileDecomposition: () => reconcileDecomposition,
|
|
40
|
+
reconcileLifecycle: () => reconcileLifecycle
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(lifecycle_reconcile_exports);
|
|
43
|
+
var fs = __toESM(require("fs"), 1);
|
|
44
|
+
var path = __toESM(require("path"), 1);
|
|
45
|
+
var import_node_child_process = require("child_process");
|
|
46
|
+
|
|
47
|
+
// src/wiki/parse-frontmatter.ts
|
|
48
|
+
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
49
|
+
function parseFrontmatter(raw) {
|
|
50
|
+
const lines = raw.split("\n");
|
|
51
|
+
if (lines[0] !== "---") {
|
|
52
|
+
throw new Error("parseFrontmatter: input does not start with ---");
|
|
53
|
+
}
|
|
54
|
+
let closeIdx = -1;
|
|
55
|
+
for (let i = 1; i < lines.length; i++) {
|
|
56
|
+
if (lines[i] === "---") {
|
|
57
|
+
closeIdx = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (closeIdx === -1) {
|
|
62
|
+
throw new Error("parseFrontmatter: missing closing ---");
|
|
63
|
+
}
|
|
64
|
+
const yamlText = lines.slice(1, closeIdx).join("\n");
|
|
65
|
+
const bodyLines = lines.slice(closeIdx + 1);
|
|
66
|
+
if (bodyLines[0] === "") bodyLines.shift();
|
|
67
|
+
const body = bodyLines.join("\n");
|
|
68
|
+
if (yamlText.trim() === "") {
|
|
69
|
+
return { fm: {}, body };
|
|
70
|
+
}
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = import_js_yaml.default.load(yamlText, { schema: import_js_yaml.default.CORE_SCHEMA });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw new Error(`parseFrontmatter: invalid YAML: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
if (parsed === null || parsed === void 0) {
|
|
78
|
+
return { fm: {}, body };
|
|
79
|
+
}
|
|
80
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
81
|
+
throw new Error("parseFrontmatter: frontmatter is not a YAML mapping");
|
|
82
|
+
}
|
|
83
|
+
return { fm: parsed, body };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/lib/lifecycle-reconcile.ts
|
|
87
|
+
var ARTIFACT_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
88
|
+
"Done",
|
|
89
|
+
"Completed",
|
|
90
|
+
"Verified",
|
|
91
|
+
"Abandoned",
|
|
92
|
+
"Closed",
|
|
93
|
+
"Resolved",
|
|
94
|
+
"Escalated",
|
|
95
|
+
"Parking Lot"
|
|
96
|
+
]);
|
|
97
|
+
var VERB_STATUS_MAP = {
|
|
98
|
+
feat: {
|
|
99
|
+
types: ["STORY", "EPIC", "CR"],
|
|
100
|
+
expected: ["Done", "Completed"]
|
|
101
|
+
},
|
|
102
|
+
fix: {
|
|
103
|
+
types: ["BUG", "HOTFIX"],
|
|
104
|
+
expected: ["Verified", "Done", "Completed"]
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var ID_PATTERN = /\b(STORY-\d{3}-\d{2}|(CR|BUG|EPIC|HOTFIX)-\d{3}|(PROPOSAL|PROP)-\d{3})\b/g;
|
|
108
|
+
function normalizeId(raw) {
|
|
109
|
+
return raw.replace(/^PROP-(\d+)$/, "PROPOSAL-$1");
|
|
110
|
+
}
|
|
111
|
+
function idType(id) {
|
|
112
|
+
if (/^STORY-\d{3}-\d{2}$/.test(id)) return "STORY";
|
|
113
|
+
if (/^CR-\d{3}$/.test(id)) return "CR";
|
|
114
|
+
if (/^BUG-\d{3}$/.test(id)) return "BUG";
|
|
115
|
+
if (/^EPIC-\d{3}$/.test(id)) return "EPIC";
|
|
116
|
+
if (/^PROPOSAL-\d{3}$/.test(id)) return "PROPOSAL";
|
|
117
|
+
if (/^HOTFIX-\d{3}$/.test(id)) return "HOTFIX";
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function parseCommitMessage(msg) {
|
|
121
|
+
const lines = msg.split("\n");
|
|
122
|
+
const subject = lines[0] ?? "";
|
|
123
|
+
let firstBodyLine = "";
|
|
124
|
+
for (let i = 1; i < lines.length; i++) {
|
|
125
|
+
if (lines[i]?.trim()) {
|
|
126
|
+
firstBodyLine = lines[i];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const verbMatch = /^(\w+)[(!]/.exec(subject) ?? /^(\w+):/.exec(subject);
|
|
131
|
+
const verb = verbMatch ? verbMatch[1].toLowerCase() : "";
|
|
132
|
+
const searchText = subject + (firstBodyLine ? "\n" + firstBodyLine : "");
|
|
133
|
+
const results = [];
|
|
134
|
+
const seen = /* @__PURE__ */ new Set();
|
|
135
|
+
let m;
|
|
136
|
+
ID_PATTERN.lastIndex = 0;
|
|
137
|
+
while ((m = ID_PATTERN.exec(searchText)) !== null) {
|
|
138
|
+
const rawId = m[0];
|
|
139
|
+
const id = normalizeId(rawId);
|
|
140
|
+
if (seen.has(id)) continue;
|
|
141
|
+
seen.add(id);
|
|
142
|
+
const type = idType(id);
|
|
143
|
+
if (!type) continue;
|
|
144
|
+
results.push({ verb, id, type });
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
}
|
|
148
|
+
function findArtifactFile(deliveryRoot, id) {
|
|
149
|
+
const prefix = `${id}_`;
|
|
150
|
+
const dirs = [
|
|
151
|
+
{ rel: "pending-sync", inArchive: false },
|
|
152
|
+
{ rel: "archive", inArchive: true }
|
|
153
|
+
];
|
|
154
|
+
for (const { rel, inArchive } of dirs) {
|
|
155
|
+
const dir = path.join(deliveryRoot, rel);
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = fs.readdirSync(dir);
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const match = entries.find(
|
|
163
|
+
(e) => (e.startsWith(prefix) || e === `${id}.md`) && e.endsWith(".md")
|
|
164
|
+
);
|
|
165
|
+
if (match) {
|
|
166
|
+
const absPath = path.join(dir, match);
|
|
167
|
+
return { absPath, inArchive, relPath: `${rel}/${match}` };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
function readArtifactStatus(absPath) {
|
|
173
|
+
let raw;
|
|
174
|
+
try {
|
|
175
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
176
|
+
} catch {
|
|
177
|
+
return { status: null, carryOver: false };
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const { fm } = parseFrontmatter(raw);
|
|
181
|
+
const status = typeof fm["status"] === "string" ? fm["status"] : null;
|
|
182
|
+
const carryOver = fm["carry_over"] === true;
|
|
183
|
+
return { status, carryOver };
|
|
184
|
+
} catch {
|
|
185
|
+
return { status: null, carryOver: false };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function reconcileLifecycle(opts) {
|
|
189
|
+
const { since, until = /* @__PURE__ */ new Date(), deliveryRoot, repoRoot } = opts;
|
|
190
|
+
const gitRunner = opts.gitRunner ?? ((cmd, args) => {
|
|
191
|
+
const result = (0, import_node_child_process.spawnSync)(cmd, args, { encoding: "utf8", cwd: repoRoot });
|
|
192
|
+
return result.stdout ?? "";
|
|
193
|
+
});
|
|
194
|
+
const sinceIso = since.toISOString();
|
|
195
|
+
const untilIso = until.toISOString();
|
|
196
|
+
const logOutput = gitRunner("git", [
|
|
197
|
+
"log",
|
|
198
|
+
`--after=${sinceIso}`,
|
|
199
|
+
`--before=${untilIso}`,
|
|
200
|
+
"--format=%H%x00%s%x00%b%x00---COMMIT---",
|
|
201
|
+
"--"
|
|
202
|
+
]);
|
|
203
|
+
const idToItem = /* @__PURE__ */ new Map();
|
|
204
|
+
const cleanIds = /* @__PURE__ */ new Set();
|
|
205
|
+
if (logOutput.trim()) {
|
|
206
|
+
const rawCommits = logOutput.split("---COMMIT---\n").filter((c) => c.trim());
|
|
207
|
+
for (const raw of rawCommits) {
|
|
208
|
+
const [sha = "", subject = "", body = ""] = raw.split("\0");
|
|
209
|
+
const trimSha = sha.trim();
|
|
210
|
+
const trimSubject = subject.trim();
|
|
211
|
+
const trimBody = body.trim();
|
|
212
|
+
if (!trimSha || !trimSubject) continue;
|
|
213
|
+
const commitMsg = trimSubject + (trimBody ? "\n\n" + trimBody : "");
|
|
214
|
+
const parsed = parseCommitMessage(commitMsg);
|
|
215
|
+
for (const { verb, id, type } of parsed) {
|
|
216
|
+
if (verb === "merge" || verb === "chore" || verb === "docs" || verb === "refactor" || verb === "test" || verb === "file" || verb === "plan") {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (type === "PROPOSAL") continue;
|
|
220
|
+
const verbConfig = VERB_STATUS_MAP[verb];
|
|
221
|
+
if (!verbConfig) continue;
|
|
222
|
+
const found = findArtifactFile(deliveryRoot, id);
|
|
223
|
+
if (!found) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const { status, carryOver } = readArtifactStatus(found.absPath);
|
|
227
|
+
if (carryOver) continue;
|
|
228
|
+
let expectedStatuses;
|
|
229
|
+
if (verb === "feat" && type === "BUG") {
|
|
230
|
+
expectedStatuses = ["Verified", "Done", "Completed"];
|
|
231
|
+
} else if (!verbConfig.types.includes(type)) {
|
|
232
|
+
continue;
|
|
233
|
+
} else {
|
|
234
|
+
expectedStatuses = verbConfig.expected;
|
|
235
|
+
}
|
|
236
|
+
const isTerminal = status !== null && expectedStatuses.includes(status);
|
|
237
|
+
const isArchived = found.inArchive;
|
|
238
|
+
if (isTerminal && isArchived) {
|
|
239
|
+
cleanIds.add(id);
|
|
240
|
+
idToItem.delete(id);
|
|
241
|
+
} else if (!idToItem.has(id)) {
|
|
242
|
+
const expectedStr = expectedStatuses[0] ?? "Done";
|
|
243
|
+
idToItem.set(id, {
|
|
244
|
+
id,
|
|
245
|
+
type,
|
|
246
|
+
expected_status: expectedStr,
|
|
247
|
+
actual_status: status,
|
|
248
|
+
file_path: found.relPath,
|
|
249
|
+
in_archive: isArchived,
|
|
250
|
+
commit_shas: [trimSha],
|
|
251
|
+
carry_over: carryOver
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
const existing = idToItem.get(id);
|
|
255
|
+
if (!existing.commit_shas.includes(trimSha)) {
|
|
256
|
+
existing.commit_shas.push(trimSha);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const id of cleanIds) {
|
|
263
|
+
idToItem.delete(id);
|
|
264
|
+
}
|
|
265
|
+
const drift = Array.from(idToItem.values());
|
|
266
|
+
return { drift, clean: cleanIds.size };
|
|
267
|
+
}
|
|
268
|
+
function reconcileCrossSprintOrphans(opts) {
|
|
269
|
+
const { deliveryRoot, sprintRunsRoot } = opts;
|
|
270
|
+
const TERMINAL_STATE_JSON = /* @__PURE__ */ new Set(["Done", "Escalated", "Parking Lot"]);
|
|
271
|
+
let activeSprintId = null;
|
|
272
|
+
try {
|
|
273
|
+
activeSprintId = fs.readFileSync(path.join(sprintRunsRoot, ".active"), "utf8").trim();
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
const pendingDir = path.join(deliveryRoot, "pending-sync");
|
|
277
|
+
let pendingFiles;
|
|
278
|
+
try {
|
|
279
|
+
pendingFiles = fs.readdirSync(pendingDir).filter(
|
|
280
|
+
(f) => f.endsWith(".md") && !f.startsWith(".")
|
|
281
|
+
);
|
|
282
|
+
} catch {
|
|
283
|
+
pendingFiles = [];
|
|
284
|
+
}
|
|
285
|
+
const pendingMap = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const fileName of pendingFiles) {
|
|
287
|
+
const absPath = path.join(pendingDir, fileName);
|
|
288
|
+
const { status } = readArtifactStatus(absPath);
|
|
289
|
+
if (status === null) continue;
|
|
290
|
+
if (ARTIFACT_TERMINAL_STATUSES.has(status)) continue;
|
|
291
|
+
const fileNameNoExt = fileName.endsWith(".md") ? fileName.slice(0, -3) : fileName;
|
|
292
|
+
const prefixPart = fileNameNoExt.split("_")[0] ?? fileNameNoExt;
|
|
293
|
+
const rawId = prefixPart;
|
|
294
|
+
const id = normalizeId(rawId);
|
|
295
|
+
const type = idType(id);
|
|
296
|
+
if (!type || type === "PROPOSAL") continue;
|
|
297
|
+
pendingMap.set(id, {
|
|
298
|
+
status,
|
|
299
|
+
filePath: path.join("pending-sync", fileName),
|
|
300
|
+
type
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (pendingMap.size === 0) {
|
|
304
|
+
return { drift: [], clean: 0 };
|
|
305
|
+
}
|
|
306
|
+
let sprintDirs;
|
|
307
|
+
try {
|
|
308
|
+
sprintDirs = fs.readdirSync(sprintRunsRoot).filter((entry) => {
|
|
309
|
+
if (entry.startsWith(".")) return false;
|
|
310
|
+
try {
|
|
311
|
+
return fs.statSync(path.join(sprintRunsRoot, entry)).isDirectory();
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
} catch {
|
|
317
|
+
sprintDirs = [];
|
|
318
|
+
}
|
|
319
|
+
const drift = [];
|
|
320
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
321
|
+
let clean = 0;
|
|
322
|
+
for (const sprintDir of sprintDirs) {
|
|
323
|
+
if (activeSprintId && sprintDir === activeSprintId) continue;
|
|
324
|
+
const stateFile = path.join(sprintRunsRoot, sprintDir, "state.json");
|
|
325
|
+
let stateJson;
|
|
326
|
+
try {
|
|
327
|
+
const raw = fs.readFileSync(stateFile, "utf8");
|
|
328
|
+
stateJson = JSON.parse(raw);
|
|
329
|
+
} catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const stories = stateJson["stories"];
|
|
333
|
+
if (!stories || typeof stories !== "object") continue;
|
|
334
|
+
for (const [id, storyEntry] of Object.entries(stories)) {
|
|
335
|
+
if (flagged.has(id)) continue;
|
|
336
|
+
const pending = pendingMap.get(id);
|
|
337
|
+
if (!pending) continue;
|
|
338
|
+
const stateInJson = storyEntry?.state ?? "";
|
|
339
|
+
if (TERMINAL_STATE_JSON.has(stateInJson)) {
|
|
340
|
+
flagged.add(id);
|
|
341
|
+
drift.push({
|
|
342
|
+
id,
|
|
343
|
+
type: pending.type,
|
|
344
|
+
pending_sync_status: pending.status,
|
|
345
|
+
state_json_state: stateInJson,
|
|
346
|
+
state_json_sprint: sprintDir,
|
|
347
|
+
file_path: pending.filePath
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
clean++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { drift, clean };
|
|
355
|
+
}
|
|
356
|
+
function reconcileDecomposition(opts) {
|
|
357
|
+
const { sprintPlanPath, deliveryRoot } = opts;
|
|
358
|
+
let raw;
|
|
359
|
+
try {
|
|
360
|
+
raw = fs.readFileSync(sprintPlanPath, "utf8");
|
|
361
|
+
} catch {
|
|
362
|
+
return { missing: [], clean: 0 };
|
|
363
|
+
}
|
|
364
|
+
let fm;
|
|
365
|
+
try {
|
|
366
|
+
({ fm } = parseFrontmatter(raw));
|
|
367
|
+
} catch {
|
|
368
|
+
return { missing: [], clean: 0 };
|
|
369
|
+
}
|
|
370
|
+
const epics = Array.isArray(fm["epics"]) ? fm["epics"].map(String) : [];
|
|
371
|
+
const proposals = Array.isArray(fm["proposals"]) ? fm["proposals"].map(String) : [];
|
|
372
|
+
const pendingDir = path.join(deliveryRoot, "pending-sync");
|
|
373
|
+
const archiveDir = path.join(deliveryRoot, "archive");
|
|
374
|
+
function listMdFiles(dir) {
|
|
375
|
+
try {
|
|
376
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
377
|
+
} catch {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const pendingFiles = listMdFiles(pendingDir);
|
|
382
|
+
const archiveFiles = listMdFiles(archiveDir);
|
|
383
|
+
const allFiles = [...pendingFiles, ...archiveFiles];
|
|
384
|
+
const missing = [];
|
|
385
|
+
let clean = 0;
|
|
386
|
+
for (const epicId of epics) {
|
|
387
|
+
const epicFile = allFiles.find(
|
|
388
|
+
(f) => f.startsWith(`${epicId}_`) || f === `${epicId}.md`
|
|
389
|
+
);
|
|
390
|
+
if (!epicFile) {
|
|
391
|
+
missing.push({
|
|
392
|
+
id: epicId,
|
|
393
|
+
type: "epic",
|
|
394
|
+
reason: "file-missing",
|
|
395
|
+
expected_files: [`pending-sync/${epicId}_<name>.md`]
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const childStories = findChildStories(
|
|
400
|
+
epicId,
|
|
401
|
+
pendingDir,
|
|
402
|
+
pendingFiles,
|
|
403
|
+
archiveDir,
|
|
404
|
+
archiveFiles
|
|
405
|
+
);
|
|
406
|
+
if (childStories.length === 0) {
|
|
407
|
+
missing.push({
|
|
408
|
+
id: epicId,
|
|
409
|
+
type: "epic",
|
|
410
|
+
reason: "no-child-stories",
|
|
411
|
+
expected_files: [
|
|
412
|
+
`pending-sync/${epicId.replace("EPIC-", "STORY-")}-01_<name>.md`
|
|
413
|
+
]
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
clean++;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const proposalId of proposals) {
|
|
420
|
+
const decomposedEpic = findDecomposedEpic(
|
|
421
|
+
proposalId,
|
|
422
|
+
pendingDir,
|
|
423
|
+
pendingFiles
|
|
424
|
+
);
|
|
425
|
+
if (!decomposedEpic) {
|
|
426
|
+
missing.push({
|
|
427
|
+
id: proposalId,
|
|
428
|
+
type: "proposal",
|
|
429
|
+
reason: "no-decomposed-epic",
|
|
430
|
+
expected_files: [`pending-sync/EPIC-<NNN>_<name>.md with context_source citing ${proposalId}`]
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
clean++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return { missing, clean };
|
|
437
|
+
}
|
|
438
|
+
function findChildStories(epicId, pendingDir, pendingFiles, archiveDir, archiveFiles) {
|
|
439
|
+
const results = [];
|
|
440
|
+
const epicNumMatch = /^EPIC-(\d+)$/.exec(epicId);
|
|
441
|
+
if (!epicNumMatch) return results;
|
|
442
|
+
const epicNum = epicNumMatch[1];
|
|
443
|
+
const storyPrefix = `STORY-${epicNum}-`;
|
|
444
|
+
for (const [files, dir] of [[pendingFiles, pendingDir], [archiveFiles, archiveDir]]) {
|
|
445
|
+
for (const f of files) {
|
|
446
|
+
if (!f.startsWith(storyPrefix) && !f.startsWith("STORY-")) continue;
|
|
447
|
+
if (!f.includes(storyPrefix)) continue;
|
|
448
|
+
const absPath = path.join(dir, f);
|
|
449
|
+
try {
|
|
450
|
+
const raw = fs.readFileSync(absPath, "utf8");
|
|
451
|
+
const { fm } = parseFrontmatter(raw);
|
|
452
|
+
const parentRef = fm["parent_epic_ref"];
|
|
453
|
+
if (parentRef === epicId) {
|
|
454
|
+
results.push(f);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return results;
|
|
461
|
+
}
|
|
462
|
+
function findDecomposedEpic(proposalId, pendingDir, pendingFiles) {
|
|
463
|
+
for (const f of pendingFiles) {
|
|
464
|
+
if (!f.startsWith("EPIC-")) continue;
|
|
465
|
+
const absPath = path.join(pendingDir, f);
|
|
466
|
+
try {
|
|
467
|
+
const raw = fs.readFileSync(absPath, "utf8");
|
|
468
|
+
const { fm } = parseFrontmatter(raw);
|
|
469
|
+
const contextSource = fm["context_source"];
|
|
470
|
+
if (typeof contextSource === "string" && contextSource.includes(proposalId)) {
|
|
471
|
+
return f;
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function checkVerbMismatch(verb, type) {
|
|
479
|
+
if (verb === "feat" && type === "BUG") {
|
|
480
|
+
return `verb 'feat' unusual for BUG; expected 'fix'`;
|
|
481
|
+
}
|
|
482
|
+
if (verb === "fix" && (type === "STORY" || type === "EPIC" || type === "CR")) {
|
|
483
|
+
return `verb 'fix' unusual for ${type}; expected 'feat'`;
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
488
|
+
0 && (module.exports = {
|
|
489
|
+
ARTIFACT_TERMINAL_STATUSES,
|
|
490
|
+
VERB_STATUS_MAP,
|
|
491
|
+
checkVerbMismatch,
|
|
492
|
+
parseCommitMessage,
|
|
493
|
+
reconcileCrossSprintOrphans,
|
|
494
|
+
reconcileDecomposition,
|
|
495
|
+
reconcileLifecycle
|
|
496
|
+
});
|
|
497
|
+
//# sourceMappingURL=lifecycle-reconcile.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/lifecycle-reconcile.ts","../../src/wiki/parse-frontmatter.ts"],"sourcesContent":["/**\n * lifecycle-reconcile.ts — CR-017 Lifecycle Status Reconciliation + Decomposition Gate\n *\n * Public API:\n * reconcileLifecycle(opts) → { drift: DriftItem[], clean: number }\n * reconcileDecomposition(opts) → { missing: MissingDecomp[], clean: number }\n * parseCommitMessage(msg) → Array<{ verb, id, type }>\n * VERB_STATUS_MAP — verb-to-expected-status table\n *\n * TERMINAL_STATES referenced from .cleargate/scripts/constants.mjs:45.\n * Do NOT redefine; duplicate literal with source citation.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { parseFrontmatter } from '../wiki/parse-frontmatter.js';\n\n// ─── Constants ─────────────────────────────────────────────────────────────────\n\n/**\n * Terminal statuses for artifact lifecycle.\n * Source: .cleargate/scripts/constants.mjs:45 TERMINAL_STATES.\n * NOTE: These are the *artifact* terminal statuses (Done, Completed, Verified, etc.),\n * not state.json story states (Done, Escalated, Parking Lot).\n */\nexport const ARTIFACT_TERMINAL_STATUSES = new Set([\n 'Done',\n 'Completed',\n 'Verified',\n 'Abandoned',\n 'Closed',\n 'Resolved',\n 'Escalated',\n 'Parking Lot',\n]);\n\n/**\n * Verb-to-expected-status map (v1).\n * Key: verb pattern (lower-case), Value: { types, expected }.\n * types: which artifact types this verb applies to.\n * expected: accepted terminal statuses for this verb.\n */\nexport const VERB_STATUS_MAP: Readonly<Record<string, { types: string[]; expected: string[] }>> = {\n feat: {\n types: ['STORY', 'EPIC', 'CR'],\n expected: ['Done', 'Completed'],\n },\n fix: {\n types: ['BUG', 'HOTFIX'],\n expected: ['Verified', 'Done', 'Completed'],\n },\n};\n\n// ─── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface DriftItem {\n id: string;\n type: 'STORY' | 'CR' | 'BUG' | 'EPIC' | 'PROPOSAL' | 'HOTFIX';\n expected_status: string;\n actual_status: string | null;\n file_path: string | null;\n in_archive: boolean;\n commit_shas: string[];\n carry_over: boolean;\n}\n\nexport interface ReconcileLifecycleResult {\n drift: DriftItem[];\n clean: number;\n}\n\nexport interface ReconcileLifecycleOpts {\n since: Date;\n until?: Date;\n deliveryRoot: string;\n repoRoot: string;\n /** Test seam: replace spawnSync git calls */\n gitRunner?: (cmd: string, args: string[]) => string;\n}\n\nexport interface MissingDecomp {\n id: string;\n type: 'epic' | 'proposal';\n reason: 'no-child-stories' | 'no-decomposed-epic' | 'file-missing';\n expected_files: string[];\n}\n\nexport interface ReconcileDecompositionResult {\n missing: MissingDecomp[];\n clean: number;\n}\n\nexport interface ReconcileDecompositionOpts {\n sprintPlanPath: string;\n deliveryRoot: string;\n}\n\n// ─── ID shape regex (longest-alternative-first per BUG-010 + assert_story_files.mjs) ──\n\nconst ID_PATTERN = /\\b(STORY-\\d{3}-\\d{2}|(CR|BUG|EPIC|HOTFIX)-\\d{3}|(PROPOSAL|PROP)-\\d{3})\\b/g;\n\n/** Artifact type names recognized by the reconciler */\ntype ArtifactType = 'STORY' | 'CR' | 'BUG' | 'EPIC' | 'PROPOSAL' | 'HOTFIX';\n\nfunction normalizeId(raw: string): string {\n // PROP-NNN → PROPOSAL-NNN (BUG-009 lesson)\n return raw.replace(/^PROP-(\\d+)$/, 'PROPOSAL-$1');\n}\n\nfunction idType(id: string): ArtifactType | null {\n if (/^STORY-\\d{3}-\\d{2}$/.test(id)) return 'STORY';\n if (/^CR-\\d{3}$/.test(id)) return 'CR';\n if (/^BUG-\\d{3}$/.test(id)) return 'BUG';\n if (/^EPIC-\\d{3}$/.test(id)) return 'EPIC';\n if (/^PROPOSAL-\\d{3}$/.test(id)) return 'PROPOSAL';\n if (/^HOTFIX-\\d{3}$/.test(id)) return 'HOTFIX';\n return null;\n}\n\n// ─── parseCommitMessage ────────────────────────────────────────────────────────\n\n/**\n * Parse a commit message (subject + optional first body line) for work-item IDs.\n * Returns one entry per ID found with the verb inferred from conventional prefix.\n *\n * commit format: `<verb>(<scope>): <description>\\n\\n<body>`\n * multi-ID: `fix(cli)!: BUG-001 fix + CR-001 align`\n * merge: `merge: STORY-001-01 → main`\n */\nexport function parseCommitMessage(\n msg: string,\n): Array<{ verb: string; id: string; type: string }> {\n const lines = msg.split('\\n');\n const subject = lines[0] ?? '';\n\n // First non-empty body line (if any) after the blank separator\n let firstBodyLine = '';\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim()) {\n firstBodyLine = lines[i]!;\n break;\n }\n }\n\n // Extract verb from subject: `feat(...)`, `fix(...)`, `merge:`, `chore(...)`, etc.\n const verbMatch = /^(\\w+)[(!]/.exec(subject) ?? /^(\\w+):/.exec(subject);\n const verb = verbMatch ? verbMatch[1]!.toLowerCase() : '';\n\n // Scan subject + first body line for IDs\n const searchText = subject + (firstBodyLine ? '\\n' + firstBodyLine : '');\n const results: Array<{ verb: string; id: string; type: string }> = [];\n const seen = new Set<string>();\n\n let m: RegExpExecArray | null;\n ID_PATTERN.lastIndex = 0;\n while ((m = ID_PATTERN.exec(searchText)) !== null) {\n const rawId = m[0]!;\n const id = normalizeId(rawId);\n if (seen.has(id)) continue;\n seen.add(id);\n const type = idType(id);\n if (!type) continue;\n results.push({ verb, id, type });\n }\n\n return results;\n}\n\n// ─── File finders ─────────────────────────────────────────────────────────────\n\ninterface FoundFile {\n absPath: string;\n inArchive: boolean;\n relPath: string; // relative to deliveryRoot\n}\n\nfunction findArtifactFile(deliveryRoot: string, id: string): FoundFile | null {\n const prefix = `${id}_`;\n const dirs: Array<{ rel: string; inArchive: boolean }> = [\n { rel: 'pending-sync', inArchive: false },\n { rel: 'archive', inArchive: true },\n ];\n for (const { rel, inArchive } of dirs) {\n const dir = path.join(deliveryRoot, rel);\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n continue;\n }\n // match `ID_*.md` OR `ID.md`\n const match = entries.find(\n (e) => (e.startsWith(prefix) || e === `${id}.md`) && e.endsWith('.md'),\n );\n if (match) {\n const absPath = path.join(dir, match);\n return { absPath, inArchive, relPath: `${rel}/${match}` };\n }\n }\n return null;\n}\n\nfunction readArtifactStatus(absPath: string): { status: string | null; carryOver: boolean } {\n let raw: string;\n try {\n raw = fs.readFileSync(absPath, 'utf8');\n } catch {\n return { status: null, carryOver: false };\n }\n try {\n const { fm } = parseFrontmatter(raw);\n const status = typeof fm['status'] === 'string' ? fm['status'] : null;\n const carryOver = fm['carry_over'] === true;\n return { status, carryOver };\n } catch {\n return { status: null, carryOver: false };\n }\n}\n\n// ─── reconcileLifecycle ────────────────────────────────────────────────────────\n\n/**\n * Scan git log in [since, until] range and reconcile artifact statuses.\n *\n * For each commit touching feat/fix verbs with IDs:\n * - Find the artifact file in pending-sync or archive\n * - Check if status is at expected terminal status\n * - Report drift items for non-terminal artifacts\n * - Skip artifacts with carry_over: true\n */\nexport function reconcileLifecycle(opts: ReconcileLifecycleOpts): ReconcileLifecycleResult {\n const { since, until = new Date(), deliveryRoot, repoRoot } = opts;\n\n const gitRunner =\n opts.gitRunner ??\n ((cmd: string, args: string[]) => {\n const result = spawnSync(cmd, args, { encoding: 'utf8', cwd: repoRoot });\n return (result.stdout ?? '') as string;\n });\n\n // git log --format=\"%H %s%n%b%n---COMMIT---\" --after=<since> --before=<until>\n const sinceIso = since.toISOString();\n const untilIso = until.toISOString();\n const logOutput = gitRunner('git', [\n 'log',\n `--after=${sinceIso}`,\n `--before=${untilIso}`,\n '--format=%H%x00%s%x00%b%x00---COMMIT---',\n '--',\n ]);\n\n // Map: id → DriftItem (accumulates SHAs for bundled-commit grouping)\n // We track each id independently; bundled-commit = multiple SHAs per id\n const idToItem = new Map<string, DriftItem>();\n // Track ids that were found CLEAN (fully reconciled)\n const cleanIds = new Set<string>();\n\n if (logOutput.trim()) {\n // Split by commit separator\n const rawCommits = logOutput.split('---COMMIT---\\n').filter((c) => c.trim());\n\n for (const raw of rawCommits) {\n // Each commit entry: sha\\0subject\\0body\\0\n const [sha = '', subject = '', body = ''] = raw.split('\\x00');\n const trimSha = sha.trim();\n const trimSubject = subject.trim();\n const trimBody = body.trim();\n\n if (!trimSha || !trimSubject) continue;\n\n const commitMsg = trimSubject + (trimBody ? '\\n\\n' + trimBody : '');\n const parsed = parseCommitMessage(commitMsg);\n\n for (const { verb, id, type } of parsed) {\n // Skip merge, chore, docs, refactor, test, file, plan verbs (no expectation)\n if (verb === 'merge' || verb === 'chore' || verb === 'docs' || verb === 'refactor'\n || verb === 'test' || verb === 'file' || verb === 'plan') {\n continue;\n }\n\n // Skip PROPOSAL types — proposals aren't shipped via feat/fix commits\n if (type === 'PROPOSAL') continue;\n\n const verbConfig = VERB_STATUS_MAP[verb];\n if (!verbConfig) continue;\n\n // Verb mismatch: feat(BUG-NNN) → soft warning only, handled at call site\n // We still need to find the file and check status for the call site to report\n\n // Find the artifact file\n const found = findArtifactFile(deliveryRoot, id);\n if (!found) {\n // Unknown ID — log once at info level (no drift)\n // We skip unknown IDs (no file found); call site logs info\n continue;\n }\n\n // Read status + carry_over from CURRENT frontmatter\n const { status, carryOver } = readArtifactStatus(found.absPath);\n\n // carry_over: true → skip silently\n if (carryOver) continue;\n\n // Determine expected statuses for this (verb, type) pair\n let expectedStatuses: string[];\n if (verb === 'feat' && type === 'BUG') {\n // verb mismatch — soft warning, does not block; still check status\n // Use 'Verified' as expected for BUG even with feat verb\n expectedStatuses = ['Verified', 'Done', 'Completed'];\n } else if (!verbConfig.types.includes(type)) {\n // Type not covered by this verb's map — skip\n continue;\n } else {\n expectedStatuses = verbConfig.expected;\n }\n\n const isTerminal = status !== null && expectedStatuses.includes(status);\n const isArchived = found.inArchive;\n\n if (isTerminal && isArchived) {\n // Clean\n cleanIds.add(id);\n // If we previously recorded drift for this id (from another commit), remove it\n // (Most recent status check wins — carry_over already handled above)\n idToItem.delete(id);\n } else if (!idToItem.has(id)) {\n // New drift item\n const expectedStr = expectedStatuses[0] ?? 'Done';\n idToItem.set(id, {\n id,\n type: type as DriftItem['type'],\n expected_status: expectedStr,\n actual_status: status,\n file_path: found.relPath,\n in_archive: isArchived,\n commit_shas: [trimSha],\n carry_over: carryOver,\n });\n } else {\n // Existing drift item — add SHA if not already present\n const existing = idToItem.get(id)!;\n if (!existing.commit_shas.includes(trimSha)) {\n existing.commit_shas.push(trimSha);\n }\n }\n }\n }\n }\n\n // Remove from drift any IDs that ended up in cleanIds\n for (const id of cleanIds) {\n idToItem.delete(id);\n }\n\n const drift = Array.from(idToItem.values());\n return { drift, clean: cleanIds.size };\n}\n\n// ─── reconcileCrossSprintOrphans ──────────────────────────────────────────────\n\n/**\n * Orphan drift item: a file in pending-sync/ with a non-terminal status\n * that has been marked Done (or another terminal state) in a closed sprint's\n * state.json — indicating it was completed but never archived.\n */\nexport interface OrphanDriftItem {\n id: string;\n type: 'CR' | 'STORY' | 'BUG' | 'EPIC' | 'HOTFIX';\n pending_sync_status: string;\n state_json_state: string;\n state_json_sprint: string;\n file_path: string;\n}\n\nexport interface ReconcileOrphansOpts {\n /** Path to .cleargate/delivery */\n deliveryRoot: string;\n /** Path to .cleargate/sprint-runs */\n sprintRunsRoot: string;\n}\n\nexport interface ReconcileOrphansResult {\n drift: OrphanDriftItem[];\n clean: number;\n}\n\n/**\n * Detect cross-sprint orphan drift: items in pending-sync/ with status: Ready\n * (or any non-terminal status) that are recorded as Done in a closed sprint's\n * state.json. These were completed but never archived at sprint close.\n *\n * Active-sprint exclusion: reads .active sentinel to identify the current\n * sprint and skips that sprint's state.json (in-flight items are not orphans).\n *\n * Scope: only scans pending-sync/*.md files matching the work-item-ID pattern.\n * Does NOT scan .script-incidents/ or any subdirectory.\n */\nexport function reconcileCrossSprintOrphans(opts: ReconcileOrphansOpts): ReconcileOrphansResult {\n const { deliveryRoot, sprintRunsRoot } = opts;\n\n // Terminal states from state.json (story-level states, not artifact statuses)\n const TERMINAL_STATE_JSON = new Set(['Done', 'Escalated', 'Parking Lot']);\n\n // Read the active sprint sentinel (to exclude it from orphan detection)\n let activeSprintId: string | null = null;\n try {\n activeSprintId = fs.readFileSync(path.join(sprintRunsRoot, '.active'), 'utf8').trim();\n } catch {\n // No .active file — no active sprint; scan all sprints\n }\n\n // Collect all pending-sync *.md files (no subdirectory traversal)\n const pendingDir = path.join(deliveryRoot, 'pending-sync');\n let pendingFiles: string[];\n try {\n pendingFiles = fs.readdirSync(pendingDir).filter(\n (f) => f.endsWith('.md') && !f.startsWith('.'),\n );\n } catch {\n pendingFiles = [];\n }\n\n // Build a map: id → { status, filePath } for each pending-sync item\n interface PendingItem {\n status: string;\n filePath: string;\n type: OrphanDriftItem['type'];\n }\n const pendingMap = new Map<string, PendingItem>();\n\n for (const fileName of pendingFiles) {\n const absPath = path.join(pendingDir, fileName);\n const { status } = readArtifactStatus(absPath);\n if (status === null) continue;\n // Skip already-terminal items in pending-sync (shouldn't be there but be safe)\n if (ARTIFACT_TERMINAL_STATUSES.has(status)) continue;\n\n // Extract ID from filename: filenames use <ID>_<slug>.md or <ID>.md format.\n // ID_PATTERN uses \\b word-boundaries which don't fire between a digit and '_'\n // (since '_' is a word char), so we extract the prefix before the first '_' or '.'.\n const fileNameNoExt = fileName.endsWith('.md') ? fileName.slice(0, -3) : fileName;\n const prefixPart = fileNameNoExt.split('_')[0] ?? fileNameNoExt;\n const rawId = prefixPart;\n const id = normalizeId(rawId);\n const type = idType(id);\n if (!type || type === 'PROPOSAL') continue;\n\n pendingMap.set(id, {\n status,\n filePath: path.join('pending-sync', fileName),\n type: type as OrphanDriftItem['type'],\n });\n }\n\n if (pendingMap.size === 0) {\n return { drift: [], clean: 0 };\n }\n\n // Walk sprint-runs directories for state.json files\n let sprintDirs: string[];\n try {\n sprintDirs = fs.readdirSync(sprintRunsRoot).filter((entry) => {\n // Skip the .active sentinel file and any hidden files\n if (entry.startsWith('.')) return false;\n // Skip non-directories (e.g. files in root)\n try {\n return fs.statSync(path.join(sprintRunsRoot, entry)).isDirectory();\n } catch {\n return false;\n }\n });\n } catch {\n sprintDirs = [];\n }\n\n const drift: OrphanDriftItem[] = [];\n // Track which IDs we've flagged to avoid duplicates (first sprint that shows Done wins)\n const flagged = new Set<string>();\n let clean = 0;\n\n for (const sprintDir of sprintDirs) {\n // Skip the active sprint\n if (activeSprintId && sprintDir === activeSprintId) continue;\n\n const stateFile = path.join(sprintRunsRoot, sprintDir, 'state.json');\n let stateJson: Record<string, unknown>;\n try {\n const raw = fs.readFileSync(stateFile, 'utf8');\n stateJson = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n const stories = stateJson['stories'] as Record<string, { state: string }> | undefined;\n if (!stories || typeof stories !== 'object') continue;\n\n for (const [id, storyEntry] of Object.entries(stories)) {\n // Skip if already flagged from an earlier sprint\n if (flagged.has(id)) continue;\n\n const pending = pendingMap.get(id);\n if (!pending) continue; // not in pending-sync\n\n const stateInJson = storyEntry?.state ?? '';\n if (TERMINAL_STATE_JSON.has(stateInJson)) {\n // This item is Done in a closed sprint but still in pending-sync — orphan drift\n flagged.add(id);\n drift.push({\n id,\n type: pending.type,\n pending_sync_status: pending.status,\n state_json_state: stateInJson,\n state_json_sprint: sprintDir,\n file_path: pending.filePath,\n });\n } else {\n // Item is in pending-sync AND in state.json but NOT terminal — correctly in-flight\n clean++;\n }\n }\n }\n\n return { drift, clean };\n}\n\n// ─── reconcileDecomposition ───────────────────────────────────────────────────\n\n/**\n * Read the sprint plan's epics: and proposals: frontmatter arrays and verify\n * that each referenced epic has ≥1 child story file, and each proposal has\n * a decomposed epic.\n */\nexport function reconcileDecomposition(opts: ReconcileDecompositionOpts): ReconcileDecompositionResult {\n const { sprintPlanPath, deliveryRoot } = opts;\n\n // Parse sprint plan frontmatter\n let raw: string;\n try {\n raw = fs.readFileSync(sprintPlanPath, 'utf8');\n } catch {\n return { missing: [], clean: 0 };\n }\n\n let fm: Record<string, unknown>;\n try {\n ({ fm } = parseFrontmatter(raw));\n } catch {\n return { missing: [], clean: 0 };\n }\n\n const epics: string[] = Array.isArray(fm['epics']) ? fm['epics'].map(String) : [];\n const proposals: string[] = Array.isArray(fm['proposals']) ? fm['proposals'].map(String) : [];\n\n const pendingDir = path.join(deliveryRoot, 'pending-sync');\n const archiveDir = path.join(deliveryRoot, 'archive');\n\n // Read both dirs for all .md files\n function listMdFiles(dir: string): string[] {\n try {\n return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));\n } catch {\n return [];\n }\n }\n const pendingFiles = listMdFiles(pendingDir);\n const archiveFiles = listMdFiles(archiveDir);\n const allFiles = [...pendingFiles, ...archiveFiles];\n\n const missing: MissingDecomp[] = [];\n let clean = 0;\n\n // Check epics\n for (const epicId of epics) {\n // Find the epic file\n const epicFile = allFiles.find(\n (f) => f.startsWith(`${epicId}_`) || f === `${epicId}.md`,\n );\n if (!epicFile) {\n missing.push({\n id: epicId,\n type: 'epic',\n reason: 'file-missing',\n expected_files: [`pending-sync/${epicId}_<name>.md`],\n });\n continue;\n }\n\n // Find child stories: any STORY-*.md with parent_epic_ref: epicId\n const childStories = findChildStories(\n epicId,\n pendingDir,\n pendingFiles,\n archiveDir,\n archiveFiles,\n );\n\n if (childStories.length === 0) {\n missing.push({\n id: epicId,\n type: 'epic',\n reason: 'no-child-stories',\n expected_files: [\n `pending-sync/${epicId.replace('EPIC-', 'STORY-')}-01_<name>.md`,\n ],\n });\n } else {\n clean++;\n }\n }\n\n // Check proposals\n for (const proposalId of proposals) {\n // Find a decomposed epic that cites this proposal in context_source\n const decomposedEpic = findDecomposedEpic(\n proposalId,\n pendingDir,\n pendingFiles,\n );\n if (!decomposedEpic) {\n missing.push({\n id: proposalId,\n type: 'proposal',\n reason: 'no-decomposed-epic',\n expected_files: [`pending-sync/EPIC-<NNN>_<name>.md with context_source citing ${proposalId}`],\n });\n } else {\n clean++;\n }\n }\n\n return { missing, clean };\n}\n\n/**\n * Find story files in pending-sync or archive that have parent_epic_ref: epicId.\n */\nfunction findChildStories(\n epicId: string,\n pendingDir: string,\n pendingFiles: string[],\n archiveDir: string,\n archiveFiles: string[],\n): string[] {\n const results: string[] = [];\n const epicNumMatch = /^EPIC-(\\d+)$/.exec(epicId);\n if (!epicNumMatch) return results;\n const epicNum = epicNumMatch[1]!;\n\n const storyPrefix = `STORY-${epicNum}-`;\n\n for (const [files, dir] of [[pendingFiles, pendingDir], [archiveFiles, archiveDir]] as const) {\n for (const f of files) {\n if (!f.startsWith(storyPrefix) && !f.startsWith('STORY-')) continue;\n // Quick filename match first\n if (!f.includes(storyPrefix)) continue;\n const absPath = path.join(dir, f);\n try {\n const raw = fs.readFileSync(absPath, 'utf8');\n const { fm } = parseFrontmatter(raw);\n const parentRef = fm['parent_epic_ref'];\n if (parentRef === epicId) {\n results.push(f);\n }\n } catch {\n // skip malformed files\n }\n }\n }\n return results;\n}\n\n/**\n * Find an epic file in pending-sync whose context_source cites proposalId.\n */\nfunction findDecomposedEpic(\n proposalId: string,\n pendingDir: string,\n pendingFiles: string[],\n): string | null {\n for (const f of pendingFiles) {\n if (!f.startsWith('EPIC-')) continue;\n const absPath = path.join(pendingDir, f);\n try {\n const raw = fs.readFileSync(absPath, 'utf8');\n const { fm } = parseFrontmatter(raw);\n const contextSource = fm['context_source'];\n if (\n typeof contextSource === 'string' &&\n contextSource.includes(proposalId)\n ) {\n return f;\n }\n } catch {\n // skip\n }\n }\n return null;\n}\n\n// ─── Verb mismatch checker (exported for test use) ────────────────────────────\n\n/**\n * Check if a (verb, type) combination is a mismatch (soft warning only in v1).\n * Returns a warning message or null if no mismatch.\n */\nexport function checkVerbMismatch(verb: string, type: string): string | null {\n if (verb === 'feat' && type === 'BUG') {\n return `verb 'feat' unusual for BUG; expected 'fix'`;\n }\n if (verb === 'fix' && (type === 'STORY' || type === 'EPIC' || type === 'CR')) {\n return `verb 'fix' unusual for ${type}; expected 'feat'`;\n }\n return null;\n}\n","/**\n * YAML frontmatter parser backed by js-yaml with CORE_SCHEMA (YAML 1.2 core).\n *\n * Parses `---\\n<yaml>\\n---\\n<body>` into a typed frontmatter map + body string.\n * Preserves native types (null, boolean, number, string), nested maps, and\n * arrays. Uses CORE_SCHEMA so ISO-8601 timestamp strings are NOT coerced to\n * Date objects (YAML 1.1's quirk).\n *\n * Historical note: an earlier hand-rolled parser flattened indented nested\n * maps into top-level keys and stringified null/boolean scalars. See\n * BUG-001 and FLASHCARD entry `#yaml #frontmatter`.\n */\n\nimport yaml from 'js-yaml';\n\nexport function parseFrontmatter(raw: string): { fm: Record<string, unknown>; body: string } {\n const lines = raw.split('\\n');\n if (lines[0] !== '---') {\n throw new Error('parseFrontmatter: input does not start with ---');\n }\n let closeIdx = -1;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i] === '---') { closeIdx = i; break; }\n }\n if (closeIdx === -1) {\n throw new Error('parseFrontmatter: missing closing ---');\n }\n\n const yamlText = lines.slice(1, closeIdx).join('\\n');\n const bodyLines = lines.slice(closeIdx + 1);\n // strip one leading blank line if present\n if (bodyLines[0] === '') bodyLines.shift();\n const body = bodyLines.join('\\n');\n\n if (yamlText.trim() === '') {\n return { fm: {}, body };\n }\n\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });\n } catch (err) {\n throw new Error(`parseFrontmatter: invalid YAML: ${(err as Error).message}`);\n }\n\n if (parsed === null || parsed === undefined) {\n return { fm: {}, body };\n }\n if (typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('parseFrontmatter: frontmatter is not a YAML mapping');\n }\n\n return { fm: parsed as Record<string, unknown>, body };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,SAAoB;AACpB,WAAsB;AACtB,gCAA0B;;;ACF1B,qBAAiB;AAEV,SAAS,iBAAiB,KAA4D;AAC3F,QAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,MAAI,MAAM,CAAC,MAAM,OAAO;AACtB,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,MAAM,CAAC,MAAM,OAAO;AAAE,iBAAW;AAAG;AAAA,IAAO;AAAA,EACjD;AACA,MAAI,aAAa,IAAI;AACnB,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AAEA,QAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,EAAE,KAAK,IAAI;AACnD,QAAM,YAAY,MAAM,MAAM,WAAW,CAAC;AAE1C,MAAI,UAAU,CAAC,MAAM,GAAI,WAAU,MAAM;AACzC,QAAM,OAAO,UAAU,KAAK,IAAI;AAEhC,MAAI,SAAS,KAAK,MAAM,IAAI;AAC1B,WAAO,EAAE,IAAI,CAAC,GAAG,KAAK;AAAA,EACxB;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,eAAAA,QAAK,KAAK,UAAU,EAAE,QAAQ,eAAAA,QAAK,YAAY,CAAC;AAAA,EAC3D,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,mCAAoC,IAAc,OAAO,EAAE;AAAA,EAC7E;AAEA,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,EAAE,IAAI,CAAC,GAAG,KAAK;AAAA,EACxB;AACA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,SAAO,EAAE,IAAI,QAAmC,KAAK;AACvD;;;AD3BO,IAAM,6BAA6B,oBAAI,IAAI;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQM,IAAM,kBAAqF;AAAA,EAChG,MAAM;AAAA,IACJ,OAAO,CAAC,SAAS,QAAQ,IAAI;AAAA,IAC7B,UAAU,CAAC,QAAQ,WAAW;AAAA,EAChC;AAAA,EACA,KAAK;AAAA,IACH,OAAO,CAAC,OAAO,QAAQ;AAAA,IACvB,UAAU,CAAC,YAAY,QAAQ,WAAW;AAAA,EAC5C;AACF;AAgDA,IAAM,aAAa;AAKnB,SAAS,YAAY,KAAqB;AAExC,SAAO,IAAI,QAAQ,gBAAgB,aAAa;AAClD;AAEA,SAAS,OAAO,IAAiC;AAC/C,MAAI,sBAAsB,KAAK,EAAE,EAAG,QAAO;AAC3C,MAAI,aAAa,KAAK,EAAE,EAAG,QAAO;AAClC,MAAI,cAAc,KAAK,EAAE,EAAG,QAAO;AACnC,MAAI,eAAe,KAAK,EAAE,EAAG,QAAO;AACpC,MAAI,mBAAmB,KAAK,EAAE,EAAG,QAAO;AACxC,MAAI,iBAAiB,KAAK,EAAE,EAAG,QAAO;AACtC,SAAO;AACT;AAYO,SAAS,mBACd,KACmD;AACnD,QAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,QAAM,UAAU,MAAM,CAAC,KAAK;AAG5B,MAAI,gBAAgB;AACpB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,MAAM,CAAC,GAAG,KAAK,GAAG;AACpB,sBAAgB,MAAM,CAAC;AACvB;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY,aAAa,KAAK,OAAO,KAAK,UAAU,KAAK,OAAO;AACtE,QAAM,OAAO,YAAY,UAAU,CAAC,EAAG,YAAY,IAAI;AAGvD,QAAM,aAAa,WAAW,gBAAgB,OAAO,gBAAgB;AACrE,QAAM,UAA6D,CAAC;AACpE,QAAM,OAAO,oBAAI,IAAY;AAE7B,MAAI;AACJ,aAAW,YAAY;AACvB,UAAQ,IAAI,WAAW,KAAK,UAAU,OAAO,MAAM;AACjD,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,KAAK,YAAY,KAAK;AAC5B,QAAI,KAAK,IAAI,EAAE,EAAG;AAClB,SAAK,IAAI,EAAE;AACX,UAAM,OAAO,OAAO,EAAE;AACtB,QAAI,CAAC,KAAM;AACX,YAAQ,KAAK,EAAE,MAAM,IAAI,KAAK,CAAC;AAAA,EACjC;AAEA,SAAO;AACT;AAUA,SAAS,iBAAiB,cAAsB,IAA8B;AAC5E,QAAM,SAAS,GAAG,EAAE;AACpB,QAAM,OAAmD;AAAA,IACvD,EAAE,KAAK,gBAAgB,WAAW,MAAM;AAAA,IACxC,EAAE,KAAK,WAAW,WAAW,KAAK;AAAA,EACpC;AACA,aAAW,EAAE,KAAK,UAAU,KAAK,MAAM;AACrC,UAAM,MAAW,UAAK,cAAc,GAAG;AACvC,QAAI;AACJ,QAAI;AACF,gBAAa,eAAY,GAAG;AAAA,IAC9B,QAAQ;AACN;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ;AAAA,MACpB,CAAC,OAAO,EAAE,WAAW,MAAM,KAAK,MAAM,GAAG,EAAE,UAAU,EAAE,SAAS,KAAK;AAAA,IACvE;AACA,QAAI,OAAO;AACT,YAAM,UAAe,UAAK,KAAK,KAAK;AACpC,aAAO,EAAE,SAAS,WAAW,SAAS,GAAG,GAAG,IAAI,KAAK,GAAG;AAAA,IAC1D;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAgE;AAC1F,MAAI;AACJ,MAAI;AACF,UAAS,gBAAa,SAAS,MAAM;AAAA,EACvC,QAAQ;AACN,WAAO,EAAE,QAAQ,MAAM,WAAW,MAAM;AAAA,EAC1C;AACA,MAAI;AACF,UAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,UAAM,SAAS,OAAO,GAAG,QAAQ,MAAM,WAAW,GAAG,QAAQ,IAAI;AACjE,UAAM,YAAY,GAAG,YAAY,MAAM;AACvC,WAAO,EAAE,QAAQ,UAAU;AAAA,EAC7B,QAAQ;AACN,WAAO,EAAE,QAAQ,MAAM,WAAW,MAAM;AAAA,EAC1C;AACF;AAaO,SAAS,mBAAmB,MAAwD;AACzF,QAAM,EAAE,OAAO,QAAQ,oBAAI,KAAK,GAAG,cAAc,SAAS,IAAI;AAE9D,QAAM,YACJ,KAAK,cACJ,CAAC,KAAa,SAAmB;AAChC,UAAM,aAAS,qCAAU,KAAK,MAAM,EAAE,UAAU,QAAQ,KAAK,SAAS,CAAC;AACvE,WAAQ,OAAO,UAAU;AAAA,EAC3B;AAGF,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,YAAY,UAAU,OAAO;AAAA,IACjC;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,YAAY,QAAQ;AAAA,IACpB;AAAA,IACA;AAAA,EACF,CAAC;AAID,QAAM,WAAW,oBAAI,IAAuB;AAE5C,QAAM,WAAW,oBAAI,IAAY;AAEjC,MAAI,UAAU,KAAK,GAAG;AAEpB,UAAM,aAAa,UAAU,MAAM,gBAAgB,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC;AAE3E,eAAW,OAAO,YAAY;AAE5B,YAAM,CAAC,MAAM,IAAI,UAAU,IAAI,OAAO,EAAE,IAAI,IAAI,MAAM,IAAM;AAC5D,YAAM,UAAU,IAAI,KAAK;AACzB,YAAM,cAAc,QAAQ,KAAK;AACjC,YAAM,WAAW,KAAK,KAAK;AAE3B,UAAI,CAAC,WAAW,CAAC,YAAa;AAE9B,YAAM,YAAY,eAAe,WAAW,SAAS,WAAW;AAChE,YAAM,SAAS,mBAAmB,SAAS;AAE3C,iBAAW,EAAE,MAAM,IAAI,KAAK,KAAK,QAAQ;AAEvC,YAAI,SAAS,WAAW,SAAS,WAAW,SAAS,UAAU,SAAS,cACnE,SAAS,UAAU,SAAS,UAAU,SAAS,QAAQ;AAC1D;AAAA,QACF;AAGA,YAAI,SAAS,WAAY;AAEzB,cAAM,aAAa,gBAAgB,IAAI;AACvC,YAAI,CAAC,WAAY;AAMjB,cAAM,QAAQ,iBAAiB,cAAc,EAAE;AAC/C,YAAI,CAAC,OAAO;AAGV;AAAA,QACF;AAGA,cAAM,EAAE,QAAQ,UAAU,IAAI,mBAAmB,MAAM,OAAO;AAG9D,YAAI,UAAW;AAGf,YAAI;AACJ,YAAI,SAAS,UAAU,SAAS,OAAO;AAGrC,6BAAmB,CAAC,YAAY,QAAQ,WAAW;AAAA,QACrD,WAAW,CAAC,WAAW,MAAM,SAAS,IAAI,GAAG;AAE3C;AAAA,QACF,OAAO;AACL,6BAAmB,WAAW;AAAA,QAChC;AAEA,cAAM,aAAa,WAAW,QAAQ,iBAAiB,SAAS,MAAM;AACtE,cAAM,aAAa,MAAM;AAEzB,YAAI,cAAc,YAAY;AAE5B,mBAAS,IAAI,EAAE;AAGf,mBAAS,OAAO,EAAE;AAAA,QACpB,WAAW,CAAC,SAAS,IAAI,EAAE,GAAG;AAE5B,gBAAM,cAAc,iBAAiB,CAAC,KAAK;AAC3C,mBAAS,IAAI,IAAI;AAAA,YACf;AAAA,YACA;AAAA,YACA,iBAAiB;AAAA,YACjB,eAAe;AAAA,YACf,WAAW,MAAM;AAAA,YACjB,YAAY;AAAA,YACZ,aAAa,CAAC,OAAO;AAAA,YACrB,YAAY;AAAA,UACd,CAAC;AAAA,QACH,OAAO;AAEL,gBAAM,WAAW,SAAS,IAAI,EAAE;AAChC,cAAI,CAAC,SAAS,YAAY,SAAS,OAAO,GAAG;AAC3C,qBAAS,YAAY,KAAK,OAAO;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,MAAM,UAAU;AACzB,aAAS,OAAO,EAAE;AAAA,EACpB;AAEA,QAAM,QAAQ,MAAM,KAAK,SAAS,OAAO,CAAC;AAC1C,SAAO,EAAE,OAAO,OAAO,SAAS,KAAK;AACvC;AAyCO,SAAS,4BAA4B,MAAoD;AAC9F,QAAM,EAAE,cAAc,eAAe,IAAI;AAGzC,QAAM,sBAAsB,oBAAI,IAAI,CAAC,QAAQ,aAAa,aAAa,CAAC;AAGxE,MAAI,iBAAgC;AACpC,MAAI;AACF,qBAAoB,gBAAkB,UAAK,gBAAgB,SAAS,GAAG,MAAM,EAAE,KAAK;AAAA,EACtF,QAAQ;AAAA,EAER;AAGA,QAAM,aAAkB,UAAK,cAAc,cAAc;AACzD,MAAI;AACJ,MAAI;AACF,mBAAkB,eAAY,UAAU,EAAE;AAAA,MACxC,CAAC,MAAM,EAAE,SAAS,KAAK,KAAK,CAAC,EAAE,WAAW,GAAG;AAAA,IAC/C;AAAA,EACF,QAAQ;AACN,mBAAe,CAAC;AAAA,EAClB;AAQA,QAAM,aAAa,oBAAI,IAAyB;AAEhD,aAAW,YAAY,cAAc;AACnC,UAAM,UAAe,UAAK,YAAY,QAAQ;AAC9C,UAAM,EAAE,OAAO,IAAI,mBAAmB,OAAO;AAC7C,QAAI,WAAW,KAAM;AAErB,QAAI,2BAA2B,IAAI,MAAM,EAAG;AAK5C,UAAM,gBAAgB,SAAS,SAAS,KAAK,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;AACzE,UAAM,aAAa,cAAc,MAAM,GAAG,EAAE,CAAC,KAAK;AAClD,UAAM,QAAQ;AACd,UAAM,KAAK,YAAY,KAAK;AAC5B,UAAM,OAAO,OAAO,EAAE;AACtB,QAAI,CAAC,QAAQ,SAAS,WAAY;AAElC,eAAW,IAAI,IAAI;AAAA,MACjB;AAAA,MACA,UAAe,UAAK,gBAAgB,QAAQ;AAAA,MAC5C;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,WAAW,SAAS,GAAG;AACzB,WAAO,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE;AAAA,EAC/B;AAGA,MAAI;AACJ,MAAI;AACF,iBAAgB,eAAY,cAAc,EAAE,OAAO,CAAC,UAAU;AAE5D,UAAI,MAAM,WAAW,GAAG,EAAG,QAAO;AAElC,UAAI;AACF,eAAU,YAAc,UAAK,gBAAgB,KAAK,CAAC,EAAE,YAAY;AAAA,MACnE,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,iBAAa,CAAC;AAAA,EAChB;AAEA,QAAM,QAA2B,CAAC;AAElC,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,QAAQ;AAEZ,aAAW,aAAa,YAAY;AAElC,QAAI,kBAAkB,cAAc,eAAgB;AAEpD,UAAM,YAAiB,UAAK,gBAAgB,WAAW,YAAY;AACnE,QAAI;AACJ,QAAI;AACF,YAAM,MAAS,gBAAa,WAAW,MAAM;AAC7C,kBAAY,KAAK,MAAM,GAAG;AAAA,IAC5B,QAAQ;AACN;AAAA,IACF;AAEA,UAAM,UAAU,UAAU,SAAS;AACnC,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAE7C,eAAW,CAAC,IAAI,UAAU,KAAK,OAAO,QAAQ,OAAO,GAAG;AAEtD,UAAI,QAAQ,IAAI,EAAE,EAAG;AAErB,YAAM,UAAU,WAAW,IAAI,EAAE;AACjC,UAAI,CAAC,QAAS;AAEd,YAAM,cAAc,YAAY,SAAS;AACzC,UAAI,oBAAoB,IAAI,WAAW,GAAG;AAExC,gBAAQ,IAAI,EAAE;AACd,cAAM,KAAK;AAAA,UACT;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,qBAAqB,QAAQ;AAAA,UAC7B,kBAAkB;AAAA,UAClB,mBAAmB;AAAA,UACnB,WAAW,QAAQ;AAAA,QACrB,CAAC;AAAA,MACH,OAAO;AAEL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;AASO,SAAS,uBAAuB,MAAgE;AACrG,QAAM,EAAE,gBAAgB,aAAa,IAAI;AAGzC,MAAI;AACJ,MAAI;AACF,UAAS,gBAAa,gBAAgB,MAAM;AAAA,EAC9C,QAAQ;AACN,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,EACjC;AAEA,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,GAAG,IAAI,iBAAiB,GAAG;AAAA,EAChC,QAAQ;AACN,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,EACjC;AAEA,QAAM,QAAkB,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,MAAM,IAAI,CAAC;AAChF,QAAM,YAAsB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,GAAG,WAAW,EAAE,IAAI,MAAM,IAAI,CAAC;AAE5F,QAAM,aAAkB,UAAK,cAAc,cAAc;AACzD,QAAM,aAAkB,UAAK,cAAc,SAAS;AAGpD,WAAS,YAAY,KAAuB;AAC1C,QAAI;AACF,aAAU,eAAY,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC;AAAA,IAC5D,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACA,QAAM,eAAe,YAAY,UAAU;AAC3C,QAAM,eAAe,YAAY,UAAU;AAC3C,QAAM,WAAW,CAAC,GAAG,cAAc,GAAG,YAAY;AAElD,QAAM,UAA2B,CAAC;AAClC,MAAI,QAAQ;AAGZ,aAAW,UAAU,OAAO;AAE1B,UAAM,WAAW,SAAS;AAAA,MACxB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,KAAK,MAAM,GAAG,MAAM;AAAA,IACtD;AACA,QAAI,CAAC,UAAU;AACb,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB,CAAC,gBAAgB,MAAM,YAAY;AAAA,MACrD,CAAC;AACD;AAAA,IACF;AAGA,UAAM,eAAe;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB;AAAA,UACd,gBAAgB,OAAO,QAAQ,SAAS,QAAQ,CAAC;AAAA,QACnD;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAGA,aAAW,cAAc,WAAW;AAElC,UAAM,iBAAiB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,gBAAgB;AACnB,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB,CAAC,gEAAgE,UAAU,EAAE;AAAA,MAC/F,CAAC;AAAA,IACH,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAKA,SAAS,iBACP,QACA,YACA,cACA,YACA,cACU;AACV,QAAM,UAAoB,CAAC;AAC3B,QAAM,eAAe,eAAe,KAAK,MAAM;AAC/C,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,UAAU,aAAa,CAAC;AAE9B,QAAM,cAAc,SAAS,OAAO;AAEpC,aAAW,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,cAAc,UAAU,GAAG,CAAC,cAAc,UAAU,CAAC,GAAY;AAC5F,eAAW,KAAK,OAAO;AACrB,UAAI,CAAC,EAAE,WAAW,WAAW,KAAK,CAAC,EAAE,WAAW,QAAQ,EAAG;AAE3D,UAAI,CAAC,EAAE,SAAS,WAAW,EAAG;AAC9B,YAAM,UAAe,UAAK,KAAK,CAAC;AAChC,UAAI;AACF,cAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,cAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,cAAM,YAAY,GAAG,iBAAiB;AACtC,YAAI,cAAc,QAAQ;AACxB,kBAAQ,KAAK,CAAC;AAAA,QAChB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,mBACP,YACA,YACA,cACe;AACf,aAAW,KAAK,cAAc;AAC5B,QAAI,CAAC,EAAE,WAAW,OAAO,EAAG;AAC5B,UAAM,UAAe,UAAK,YAAY,CAAC;AACvC,QAAI;AACF,YAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,YAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,YAAM,gBAAgB,GAAG,gBAAgB;AACzC,UACE,OAAO,kBAAkB,YACzB,cAAc,SAAS,UAAU,GACjC;AACA,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,kBAAkB,MAAc,MAA6B;AAC3E,MAAI,SAAS,UAAU,SAAS,OAAO;AACrC,WAAO;AAAA,EACT;AACA,MAAI,SAAS,UAAU,SAAS,WAAW,SAAS,UAAU,SAAS,OAAO;AAC5E,WAAO,0BAA0B,IAAI;AAAA,EACvC;AACA,SAAO;AACT;","names":["yaml"]}
|