stepwell-core 0.0.0-stage → 0.2.4

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.
@@ -0,0 +1,12 @@
1
+ import type { Priority, Status } from "./types.ts";
2
+ /**
3
+ * ASCII-Aliase je Emoji-Wert (T3): Die CLI akzeptiert zusätzlich zu den Icons
4
+ * diese Aliase (case-insensitiv). Das MCP-Schema bleibt bei den Emojis —
5
+ * Agenten emittieren sie zuverlässig; Menschen tippen die Aliase.
6
+ */
7
+ export declare const PRIORITY_ALIASES: Readonly<Record<Exclude<Priority, "unknown">, readonly string[]>>;
8
+ export declare const STATUS_ALIASES: Readonly<Record<Exclude<Status, "unknown">, readonly string[]>>;
9
+ export declare function resolvePriority(token: string): Priority | undefined;
10
+ export declare function resolveStatus(token: string): Status | undefined;
11
+ export declare function priorityAliasHelp(): string;
12
+ export declare function statusAliasHelp(): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * ASCII-Aliase je Emoji-Wert (T3): Die CLI akzeptiert zusätzlich zu den Icons
3
+ * diese Aliase (case-insensitiv). Das MCP-Schema bleibt bei den Emojis —
4
+ * Agenten emittieren sie zuverlässig; Menschen tippen die Aliase.
5
+ */
6
+ export const PRIORITY_ALIASES = {
7
+ "🔴": ["red", "kritisch", "p1"],
8
+ "🟠": ["orange", "hoch", "p2"],
9
+ "🟡": ["yellow", "mittel", "p3"],
10
+ "🟢": ["green", "niedrig", "p4"],
11
+ "🔵": ["blue", "test", "p5"],
12
+ };
13
+ export const STATUS_ALIASES = {
14
+ "⬜": ["open"],
15
+ "🔄": ["running", "wip"],
16
+ "✅": ["done"],
17
+ "⛔": ["blocked"],
18
+ };
19
+ const PRIORITY_KEYS = Object.keys(PRIORITY_ALIASES);
20
+ const STATUS_KEYS = Object.keys(STATUS_ALIASES);
21
+ export function resolvePriority(token) {
22
+ if (PRIORITY_KEYS.includes(token))
23
+ return token;
24
+ if (token === "unknown")
25
+ return "unknown";
26
+ const t = token.toLowerCase();
27
+ for (const key of PRIORITY_KEYS) {
28
+ if (PRIORITY_ALIASES[key].includes(t))
29
+ return key;
30
+ }
31
+ return undefined;
32
+ }
33
+ export function resolveStatus(token) {
34
+ if (STATUS_KEYS.includes(token))
35
+ return token;
36
+ if (token === "unknown")
37
+ return "unknown";
38
+ const t = token.toLowerCase();
39
+ for (const key of STATUS_KEYS) {
40
+ if (STATUS_ALIASES[key].includes(t))
41
+ return key;
42
+ }
43
+ return undefined;
44
+ }
45
+ export function priorityAliasHelp() {
46
+ return (PRIORITY_KEYS.map((key) => `${key}=${PRIORITY_ALIASES[key].join("/")}`).join(", ") +
47
+ ", unknown");
48
+ }
49
+ export function statusAliasHelp() {
50
+ return (STATUS_KEYS.map((key) => `${key}=${STATUS_ALIASES[key].join("/")}`).join(", ") + ", unknown");
51
+ }
@@ -0,0 +1,5 @@
1
+ import type { ArchiveItem, BacklogItem, ParseResult, PhaseBlock } from "./types.ts";
2
+ export declare function parseBacklogArchive(content: string, file?: string): ParseResult<ArchiveItem[]>;
3
+ /** Archive view of a parsed item: picks the done line out of the block body. */
4
+ export declare function toArchiveItem(item: BacklogItem): ArchiveItem;
5
+ export declare function parseProgressArchive(content: string, file?: string): ParseResult<PhaseBlock[]>;
@@ -0,0 +1,24 @@
1
+ import { parseBacklog } from "./backlog.js";
2
+ import { parseProgress } from "./progress.js";
3
+ import { allSynonyms, escapeRegExp } from "./profile.js";
4
+ const ERLEDIGT_LINE = new RegExp(`^-\\s+\\*\\*(?:${allSynonyms("doneLabel").map(escapeRegExp).join("|")}):\\*\\*\\s*(.+?)\\s*$`, "u");
5
+ export function parseBacklogArchive(content, file = "docs/archive/BACKLOG_ARCHIVE.md") {
6
+ const result = parseBacklog(content, file);
7
+ return { value: result.value.items.map(toArchiveItem), warnings: result.warnings };
8
+ }
9
+ /** Archive view of a parsed item: picks the done line out of the block body. */
10
+ export function toArchiveItem(item) {
11
+ const archive = { ...item, doneLine: "" };
12
+ for (const line of item.text.split("\n")) {
13
+ const m = ERLEDIGT_LINE.exec(line);
14
+ if (m) {
15
+ archive.doneLine = m[1];
16
+ break;
17
+ }
18
+ }
19
+ return archive;
20
+ }
21
+ export function parseProgressArchive(content, file = "docs/archive/PROGRESS_ARCHIVE.md") {
22
+ const result = parseProgress(content, file);
23
+ return { value: result.value.phases, warnings: result.warnings };
24
+ }
@@ -0,0 +1,11 @@
1
+ import type { Backlog, ParseResult } from "./types.ts";
2
+ /**
3
+ * H10/30.4: path-like tokens extracted from the location bullet — the cheap
4
+ * prediction surface for workstream overlaps. Tolerant for backticks, `file.ts:12`
5
+ * line refs, parenthetical comments and comma/semicolon lists. Only path-shaped
6
+ * tokens pass (a separator or a file extension, at least one letter): prose,
7
+ * dates (11/2026) and globs are skipped — no invention, no glob expansion.
8
+ */
9
+ export declare function extractFiles(location: string | undefined): string[];
10
+ export declare function parseBacklog(rawContent: string, file?: string): ParseResult<Backlog>;
11
+ export declare function readBacklog(root: string): ParseResult<Backlog>;
@@ -0,0 +1,380 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { allSynonyms, escapeRegExp, stripBom, synonymPattern } from "./profile.js";
4
+ import { BACKLOG_PATH, REQUIRED_FILES, missingProjectFiles, ProjectNotInitializedError, } from "./project-files.js";
5
+ const PRIORITY_EMOJIS = ["🔴", "🟠", "🟡", "🟢", "🔵"];
6
+ const PICTOGRAPHIC = /^[\p{Extended_Pictographic}\uFE0F\u200D]+$/u;
7
+ const SEPARATOR = /^(.*?)\s+—\s+([\s\S]*)$/;
8
+ const TITLE_SUFFIX = /^(.*\S)\s+—\s+(\S+)$/u;
9
+ const OPTIONAL_SUFFIX = /\s*\*\([^()]*\)\*$/u;
10
+ const BULLET = /^-\s+\*\*.+?:\*\*/u;
11
+ const union = (words) => words.map(escapeRegExp).join("|");
12
+ const LOCATION_BULLET = new RegExp(`^-\\s+\\*\\*(?:${union(allSynonyms("locationLabel"))}):\\*\\*\\s+(.+?)\\s*$`, "u");
13
+ // M21/30.6: optional dependency bullet — DE variant read silently (union matching)
14
+ const DEPENDS_BULLET = new RegExp(`^-\\s+\\*\\*(?:${union(allSynonyms("dependsOnLabel"))}):\\*\\*\\s+(.+?)\\s*$`, "u");
15
+ const DEP_ID = /^[A-Z][0-9]+$/u;
16
+ /**
17
+ * H10/30.4: path-like tokens extracted from the location bullet — the cheap
18
+ * prediction surface for workstream overlaps. Tolerant for backticks, `file.ts:12`
19
+ * line refs, parenthetical comments and comma/semicolon lists. Only path-shaped
20
+ * tokens pass (a separator or a file extension, at least one letter): prose,
21
+ * dates (11/2026) and globs are skipped — no invention, no glob expansion.
22
+ */
23
+ export function extractFiles(location) {
24
+ if (location === undefined || location.trim() === "")
25
+ return [];
26
+ const HAS_SEPARATOR = /[\\/]/u;
27
+ const HAS_EXT = /\.[A-Za-z0-9]{1,6}$/u;
28
+ const allNumericSegments = (token) => token.split(/[\\/]/u).every((segment) => /^\d+$/u.test(segment));
29
+ const files = [];
30
+ const seen = new Set();
31
+ const push = (token) => {
32
+ const cleaned = token.trim().replace(/^[`'"]+|[`'",.;:]+$/gu, "").trim();
33
+ if (cleaned === "" || cleaned.includes("*"))
34
+ return;
35
+ const path = /^(.*?\.[A-Za-z0-9]+)(?::\d+)?$/u.exec(cleaned)?.[1] ?? cleaned;
36
+ if (!HAS_SEPARATOR.test(path) && !HAS_EXT.test(path))
37
+ return;
38
+ if (!/[A-Za-z]/u.test(path) || allNumericSegments(path))
39
+ return;
40
+ if (!seen.has(path)) {
41
+ seen.add(path);
42
+ files.push(path);
43
+ }
44
+ };
45
+ for (const m of location.matchAll(/`([^`]+)`/gu))
46
+ push(m[1]);
47
+ const rest = location.replace(/`[^`]*`/gu, " ");
48
+ for (const token of rest.split(/[,;()[\]]|\s+/u))
49
+ push(token);
50
+ return files;
51
+ }
52
+ const DONE_LINE = new RegExp(`^-\\s+(.+?)\\s+—\\s+(.+?)\\s+—\\s+(?:${union(allSynonyms("doneWord"))})(?:\\s+in\\s+\`([^\`]+)\`)?`, "u");
53
+ const DONE_INDEX_HEADING = synonymPattern("doneIndexHeading");
54
+ const SEPARATOR_RULE = /^-{3,}\s*$/u;
55
+ const TABLE_ROW = /^\s*\|.*\|\s*$/u;
56
+ const TABLE_SEPARATOR = /^\s*\|(\s*:?-+:?\s*\|)+\s*$/u;
57
+ const BACKTICKED = /`([^`]+)`/u;
58
+ const FIRST_TOKEN = /\S+/u;
59
+ const isKnownPriority = (token) => PRIORITY_EMOJIS.includes(token);
60
+ const stripQuote = (line) => line.replace(/^>\s?/, "");
61
+ function splitEmoji(heading) {
62
+ const m = /^(\S+)\s+([\s\S]*)$/.exec(heading);
63
+ if (m && PICTOGRAPHIC.test(m[1])) {
64
+ return { emoji: m[1], title: m[2].trim() };
65
+ }
66
+ return { emoji: "", title: heading.trim() };
67
+ }
68
+ /**
69
+ * Erledigt-Index als GFM-Tabelle (tolerierte Variante, Realformat internal-pwa):
70
+ * Spalten [Serie, Item, Commit/Phase] — `id` = erstes Token der Item-Spalte,
71
+ * `summary` = Rest; `sha` = erstes Backtick-Token der Commit/Phase-Spalte.
72
+ * Zwei Spalten ([Item, Commit/Phase]) lesen die ID aus der ersten Spalte.
73
+ */
74
+ function parseDoneTableRow(line, lineNo) {
75
+ const cells = line
76
+ .trim()
77
+ .replace(/^\|/, "")
78
+ .replace(/\|$/, "")
79
+ .split("|")
80
+ .map((cell) => cell.trim());
81
+ if (cells.length < 2)
82
+ return undefined;
83
+ const itemCell = cells.length >= 3 ? cells[1] : cells[0];
84
+ const commitCell = cells.length >= 3 ? cells[2] : cells[1];
85
+ const token = FIRST_TOKEN.exec(itemCell)?.[0];
86
+ if (!token)
87
+ return undefined;
88
+ const entry = { id: token, summary: itemCell.slice(token.length).trim(), line: lineNo };
89
+ const sha = BACKTICKED.exec(commitCell);
90
+ if (sha) {
91
+ entry.sha = sha[1];
92
+ }
93
+ return entry;
94
+ }
95
+ function extractTitleParts(rest) {
96
+ const sep = SEPARATOR.exec(rest);
97
+ let id;
98
+ let title;
99
+ if (sep) {
100
+ id = sep[1].trim();
101
+ title = sep[2].trim();
102
+ }
103
+ else {
104
+ id = rest.trim();
105
+ title = "";
106
+ }
107
+ let knownCount = 0;
108
+ let known;
109
+ let unknownEmoji = false;
110
+ for (;;) {
111
+ const stripped = title.replace(OPTIONAL_SUFFIX, "").trim();
112
+ let changed = stripped !== title;
113
+ title = stripped;
114
+ const m = TITLE_SUFFIX.exec(title);
115
+ if (m) {
116
+ const token = m[2];
117
+ if (isKnownPriority(token)) {
118
+ knownCount += 1;
119
+ known = token;
120
+ title = m[1].trim();
121
+ changed = true;
122
+ }
123
+ else if (!unknownEmoji && PICTOGRAPHIC.test(token)) {
124
+ unknownEmoji = true;
125
+ title = m[1].trim();
126
+ changed = true;
127
+ }
128
+ }
129
+ if (!changed)
130
+ break;
131
+ }
132
+ return { id, title, known, duplicate: knownCount > 1, unknownEmoji };
133
+ }
134
+ function buildItem(raw, section, file) {
135
+ const warnings = [];
136
+ const { id, title, known, duplicate, unknownEmoji } = extractTitleParts(raw.rest);
137
+ if (duplicate) {
138
+ warnings.push({
139
+ code: "PRIO_DUPLICATE",
140
+ file,
141
+ line: raw.headingLine,
142
+ message: `Item "${id}" carries multiple priority markers in its title — only one is captured.`,
143
+ });
144
+ }
145
+ let priority;
146
+ if (known !== undefined) {
147
+ priority = known;
148
+ }
149
+ else if (unknownEmoji) {
150
+ priority = "unknown";
151
+ warnings.push({
152
+ code: "PRIO_UNKNOWN",
153
+ file,
154
+ line: raw.headingLine,
155
+ message: `Item "${id}" carries an unknown priority emoji — priority is "unknown".`,
156
+ });
157
+ }
158
+ else if (section !== null && isKnownPriority(section.emoji)) {
159
+ priority = section.emoji;
160
+ warnings.push({
161
+ code: "PRIO_MISSING",
162
+ file,
163
+ line: raw.headingLine,
164
+ message: `Item "${id}" has no priority suffix in its title — priority taken from the section emoji.`,
165
+ });
166
+ }
167
+ else {
168
+ priority = "unknown";
169
+ warnings.push({
170
+ code: "PRIO_MISSING",
171
+ file,
172
+ line: raw.headingLine,
173
+ message: `Item "${id}" has neither a title suffix nor a known section emoji — priority is "unknown".`,
174
+ });
175
+ }
176
+ if (id !== "" && title === "") {
177
+ warnings.push({
178
+ code: "TITLE_EMPTY",
179
+ file,
180
+ line: raw.headingLine,
181
+ message: `Item "${id}" has no title after the separator.`,
182
+ });
183
+ }
184
+ let bodyEnd = raw.headingLine + raw.body.length;
185
+ while (bodyEnd > raw.headingLine && (raw.body[bodyEnd - raw.headingLine - 1] ?? "").trim() === "") {
186
+ bodyEnd -= 1;
187
+ }
188
+ const bodyLines = raw.body.slice(0, bodyEnd - raw.headingLine);
189
+ const contentLines = bodyLines.filter((l) => l.trim() !== "");
190
+ if (contentLines.length > 0 && !contentLines.some((l) => BULLET.test(l))) {
191
+ warnings.push({
192
+ code: "BLOCK_UNSTRUCTURED",
193
+ file,
194
+ line: raw.headingLine,
195
+ message: `Item "${id}" has no structured block (no **Label:** bullets) — text captured as free prose.`,
196
+ });
197
+ }
198
+ const item = {
199
+ id,
200
+ title,
201
+ priority,
202
+ section: section?.title ?? "",
203
+ open: raw.open,
204
+ files: [],
205
+ dependsOn: [],
206
+ blockedBy: [],
207
+ ready: raw.open,
208
+ text: bodyLines.join("\n"),
209
+ span: { start: raw.headingLine, end: bodyEnd },
210
+ raw: [raw.heading, ...bodyLines].join("\n"),
211
+ };
212
+ for (const line of bodyLines) {
213
+ const location = LOCATION_BULLET.exec(line);
214
+ if (location) {
215
+ item.location = location[1];
216
+ item.files = extractFiles(location[1]);
217
+ break;
218
+ }
219
+ }
220
+ for (const line of bodyLines) {
221
+ const depends = DEPENDS_BULLET.exec(line);
222
+ if (depends === null)
223
+ continue;
224
+ for (const token of depends[1].split(/[,;]/u)) {
225
+ const id = token.trim();
226
+ if (DEP_ID.test(id) && !item.dependsOn.includes(id))
227
+ item.dependsOn.push(id);
228
+ }
229
+ }
230
+ return { item, warnings };
231
+ }
232
+ export function parseBacklog(rawContent, file = "BACKLOG.md") {
233
+ const content = stripBom(rawContent);
234
+ const lines = content.split(/\r?\n/);
235
+ const warnings = [];
236
+ const sections = [];
237
+ const headerRules = [];
238
+ const items = [];
239
+ const doneIndex = [];
240
+ let currentSection = null;
241
+ let inDoneIndex = false;
242
+ let doneTableData = false;
243
+ let headerRuleOpen = false;
244
+ let currentItem = null;
245
+ const flushItem = () => {
246
+ if (currentItem === null)
247
+ return;
248
+ const built = buildItem(currentItem, currentSection, file);
249
+ items.push(built.item);
250
+ warnings.push(...built.warnings);
251
+ currentItem = null;
252
+ };
253
+ const endSection = () => {
254
+ flushItem();
255
+ currentSection = null;
256
+ headerRuleOpen = false;
257
+ };
258
+ for (const [idx, line] of lines.entries()) {
259
+ const lineNo = idx + 1;
260
+ const sectionMatch = /^##\s+(.*)$/.exec(line);
261
+ if (sectionMatch) {
262
+ endSection();
263
+ const heading = sectionMatch[1].trim();
264
+ if (DONE_INDEX_HEADING.test(heading)) {
265
+ inDoneIndex = true;
266
+ doneTableData = false;
267
+ continue;
268
+ }
269
+ inDoneIndex = false;
270
+ doneTableData = false;
271
+ const { emoji, title } = splitEmoji(heading);
272
+ currentSection = { title, emoji, headerRule: "" };
273
+ headerRules.push([]);
274
+ sections.push(currentSection);
275
+ headerRuleOpen = true;
276
+ continue;
277
+ }
278
+ const itemMatch = /^###\s+\[(.)\]\s*([\s\S]*)$/u.exec(line);
279
+ if (itemMatch) {
280
+ if (inDoneIndex)
281
+ continue;
282
+ flushItem();
283
+ headerRuleOpen = false;
284
+ currentItem = {
285
+ heading: line,
286
+ headingLine: lineNo,
287
+ open: itemMatch[1] === " ",
288
+ rest: itemMatch[2] ?? "",
289
+ body: [],
290
+ };
291
+ continue;
292
+ }
293
+ if (SEPARATOR_RULE.test(line)) {
294
+ endSection();
295
+ inDoneIndex = false;
296
+ doneTableData = false;
297
+ continue;
298
+ }
299
+ if (currentItem !== null) {
300
+ currentItem.body.push(line);
301
+ continue;
302
+ }
303
+ if (inDoneIndex) {
304
+ const done = DONE_LINE.exec(line);
305
+ if (done) {
306
+ const entry = { id: done[1].trim(), summary: done[2].trim(), line: lineNo };
307
+ if (done[3] !== undefined) {
308
+ entry.sha = done[3];
309
+ }
310
+ doneIndex.push(entry);
311
+ continue;
312
+ }
313
+ if (TABLE_ROW.test(line)) {
314
+ if (TABLE_SEPARATOR.test(line)) {
315
+ doneTableData = true;
316
+ }
317
+ else if (doneTableData) {
318
+ const entry = parseDoneTableRow(line, lineNo);
319
+ if (entry) {
320
+ doneIndex.push(entry);
321
+ }
322
+ }
323
+ }
324
+ continue;
325
+ }
326
+ if (currentSection !== null && headerRuleOpen) {
327
+ const ruleParts = headerRules[sections.indexOf(currentSection)];
328
+ if (line.startsWith(">")) {
329
+ ruleParts?.push(stripQuote(line));
330
+ }
331
+ else if (line.trim() !== "") {
332
+ headerRuleOpen = false;
333
+ }
334
+ }
335
+ }
336
+ endSection();
337
+ for (const [i, section] of sections.entries()) {
338
+ section.headerRule = (headerRules[i] ?? []).join("\n");
339
+ }
340
+ // M21/30.6: derive blockedBy (transitive OPEN ancestors) + ready per item —
341
+ // a cross-item pass, so it runs after every item is built. Unknown refs are
342
+ // skipped here (DEP_UNKNOWN surfaces at validate); the visited set guards
343
+ // against cycles without an infinite loop (cycle members block themselves).
344
+ const byId = new Map(items.map((i) => [i.id, i]));
345
+ for (const item of items) {
346
+ const visited = new Set();
347
+ const stack = [...item.dependsOn];
348
+ const blocked = new Set();
349
+ while (stack.length > 0) {
350
+ const id = stack.pop();
351
+ if (visited.has(id))
352
+ continue;
353
+ visited.add(id);
354
+ const ref = byId.get(id);
355
+ if (ref === undefined)
356
+ continue;
357
+ if (ref.open)
358
+ blocked.add(id);
359
+ stack.push(...ref.dependsOn);
360
+ }
361
+ item.blockedBy = [...blocked].sort();
362
+ item.ready = item.open && item.blockedBy.length === 0;
363
+ }
364
+ return { value: { sections, items, doneIndex }, warnings };
365
+ }
366
+ export function readBacklog(root) {
367
+ const path = join(root, BACKLOG_PATH);
368
+ let content;
369
+ try {
370
+ content = readFileSync(path, "utf8");
371
+ }
372
+ catch {
373
+ const missing = missingProjectFiles(root);
374
+ if (missing.length === REQUIRED_FILES.length) {
375
+ throw new ProjectNotInitializedError(root, missing);
376
+ }
377
+ throw new Error(`missing required file: ${path}`);
378
+ }
379
+ return parseBacklog(content, path);
380
+ }
@@ -0,0 +1,15 @@
1
+ import type { BacklogItem, PhaseBlock, ProgressRow } from "./types.ts";
2
+ /**
3
+ * Phase-Kontext zur Lesezeit (G5/12.6): Phase verbatim, Tabellen-Zeilen der Phase und
4
+ * die per ID-Ref gemergten Backlog-Item-Bodies in Step-Reihenfolge — kein Duplikat in
5
+ * den Dateien, kein Schreibpfad, kein Cache (Zero-Cache bleibt).
6
+ */
7
+ export interface PhaseContext {
8
+ phase: PhaseBlock;
9
+ rows: ProgressRow[];
10
+ items: BacklogItem[];
11
+ /** Explizite Refs `(X/n)`, die weder offen noch im Archiv auflösbar sind — toleriert, kein Crash (Decision 6). */
12
+ unresolved: string[];
13
+ markdown: string;
14
+ }
15
+ export declare function phaseContext(root: string, phase: string): PhaseContext;
@@ -0,0 +1,76 @@
1
+ import { canonical, detectLocale } from "./profile.js";
2
+ import { loadProject } from "./project.js";
3
+ import { phaseMatches, scopeSteps } from "./progress.js";
4
+ const EXPLICIT_REF = /\(([A-Z]+[0-9]+)\/[0-9]+\)/gu;
5
+ const ID_TOKEN = /\b([A-Z]{1,3}[0-9]{1,4})\b/gu;
6
+ function findBlock(docs, phase) {
7
+ return (docs.progress().value.phases.find((b) => phaseMatches(b, phase)) ??
8
+ docs.progressArchive().value.find((b) => phaseMatches(b, phase)));
9
+ }
10
+ function collectRefs(block) {
11
+ const ids = [];
12
+ const seen = new Set();
13
+ const explicitRefs = new Map();
14
+ const push = (id, rawRef) => {
15
+ if (!seen.has(id)) {
16
+ seen.add(id);
17
+ ids.push(id);
18
+ }
19
+ if (rawRef !== undefined)
20
+ explicitRefs.set(id, rawRef);
21
+ };
22
+ for (const entry of block.scope) {
23
+ for (const m of entry.matchAll(EXPLICIT_REF))
24
+ push(m[1], m[0].slice(1, -1));
25
+ for (const m of entry.matchAll(ID_TOKEN))
26
+ push(m[1], undefined);
27
+ }
28
+ return { ids, explicitRefs };
29
+ }
30
+ function resolveItem(docs, id) {
31
+ return (docs.backlog().value.items.find((i) => i.id === id) ??
32
+ docs.backlogArchive().value.find((i) => i.id === id));
33
+ }
34
+ function renderMarkdown(block, rows, items, unresolved) {
35
+ const locale = detectLocale(block.raw);
36
+ const progressLabel = canonical("progressHeading", locale);
37
+ const lines = [block.raw, "", `**${progressLabel}:**`, "", "| # | Step | Status |", "|---|------|--------|"];
38
+ for (const row of rows) {
39
+ lines.push(`| ${row.step} | ${row.name} | ${row.status} |`);
40
+ }
41
+ for (const item of items) {
42
+ lines.push("", `### ${item.id} — ${item.title} — ${item.priority}`, "", item.text);
43
+ }
44
+ if (unresolved.length > 0) {
45
+ lines.push("", `> unresolved refs: ${unresolved.join(", ")}`);
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ export function phaseContext(root, phase) {
50
+ const docs = loadProject(root);
51
+ const block = findBlock(docs, phase);
52
+ if (block === undefined) {
53
+ throw new Error(`unknown phase: ${phase} (neither a running phase nor in the archive)`);
54
+ }
55
+ const steps = new Set(scopeSteps(block));
56
+ const rows = docs.progress().value.rows.filter((r) => steps.has(r.step));
57
+ const { ids, explicitRefs } = collectRefs(block);
58
+ const items = [];
59
+ const unresolved = [];
60
+ for (const id of ids) {
61
+ const item = resolveItem(docs, id);
62
+ if (item !== undefined) {
63
+ items.push(item);
64
+ }
65
+ else if (explicitRefs.has(id)) {
66
+ unresolved.push(explicitRefs.get(id));
67
+ }
68
+ }
69
+ return {
70
+ phase: block,
71
+ rows,
72
+ items,
73
+ unresolved,
74
+ markdown: renderMarkdown(block, rows, items, unresolved),
75
+ };
76
+ }
package/dist/diff.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function lineDiff(before: string, after: string, context?: number): string;
package/dist/diff.js ADDED
@@ -0,0 +1,34 @@
1
+ export function lineDiff(before, after, context = 2) {
2
+ const splitLines = (content) => content === "" ? [] : content.replace(/\r?\n$/u, "").split("\n");
3
+ const a = splitLines(before);
4
+ const b = splitLines(after);
5
+ let prefix = 0;
6
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) {
7
+ prefix += 1;
8
+ }
9
+ let suffix = 0;
10
+ while (suffix < a.length - prefix &&
11
+ suffix < b.length - prefix &&
12
+ a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) {
13
+ suffix += 1;
14
+ }
15
+ const removed = a.slice(prefix, a.length - suffix);
16
+ const added = b.slice(prefix, b.length - suffix);
17
+ if (removed.length === 0 && added.length === 0) {
18
+ return "";
19
+ }
20
+ const lines = [];
21
+ for (const line of a.slice(Math.max(0, prefix - context), prefix)) {
22
+ lines.push(` ${line}`);
23
+ }
24
+ for (const line of removed) {
25
+ lines.push(`-${line}`);
26
+ }
27
+ for (const line of added) {
28
+ lines.push(`+${line}`);
29
+ }
30
+ for (const line of a.slice(a.length - suffix, a.length - suffix + context)) {
31
+ lines.push(` ${line}`);
32
+ }
33
+ return lines.join("\n");
34
+ }
@@ -0,0 +1,10 @@
1
+ import { DOC_FILES } from "./project-files.ts";
2
+ export { DOC_FILES };
3
+ /**
4
+ * SHA256 je gelesener Doku-Datei (E2/12.5) — Clients vergleichen gegen den letzten
5
+ * bekannten Stand und überspringen Voll-Lese. Tolerant: fehlende Dateien bekommen
6
+ * keinen Schlüssel (Decision 6), kein Crash. M25/32.4: derived doc file set —
7
+ * existing archive shards are part of the manifest (the fast path would never
8
+ * see shard changes otherwise → stale hashes → consumers read stale states).
9
+ */
10
+ export declare function fileHashes(root: string): Record<string, string>;
package/dist/hashes.js ADDED
@@ -0,0 +1,22 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { docFileSet, DOC_FILES } from "./project-files.js";
5
+ export { DOC_FILES };
6
+ /**
7
+ * SHA256 je gelesener Doku-Datei (E2/12.5) — Clients vergleichen gegen den letzten
8
+ * bekannten Stand und überspringen Voll-Lese. Tolerant: fehlende Dateien bekommen
9
+ * keinen Schlüssel (Decision 6), kein Crash. M25/32.4: derived doc file set —
10
+ * existing archive shards are part of the manifest (the fast path would never
11
+ * see shard changes otherwise → stale hashes → consumers read stale states).
12
+ */
13
+ export function fileHashes(root) {
14
+ const hashes = {};
15
+ for (const rel of docFileSet(root)) {
16
+ const abs = join(root, ...rel.split("/"));
17
+ if (!existsSync(abs))
18
+ continue;
19
+ hashes[rel] = createHash("sha256").update(readFileSync(abs, "utf8")).digest("hex");
20
+ }
21
+ return hashes;
22
+ }
@@ -0,0 +1,20 @@
1
+ export { parseBacklog, readBacklog } from "./backlog.ts";
2
+ export { parseProgress, readProgress, scopeSteps, appendChunkOrder, latestStampKey, } from "./progress.ts";
3
+ export { parseBacklogArchive, parseProgressArchive } from "./archive.ts";
4
+ export { backlogShow, loadProject } from "./project.ts";
5
+ export type { ProjectDocs } from "./project.ts";
6
+ export { ProjectNotInitializedError, REQUIRED_FILES, archiveShardPaths, docFileSet, missingProjectFiles, type ArchiveKind, } from "./project-files.ts";
7
+ export { lineDiff } from "./diff.ts";
8
+ export { applyArchivePlan, applyProgressPlan, applyBacklogAddPlan, applyBacklogRemovePlan, EMPTY_MARKER_CANONICAL, applyBacklogUpdatePlan, applyPhasePlan, planArchiveItem, planBacklogAdd, planBacklogRemove, planBacklogUpdate, planPhase, planProgressUpdate, stampNow, type ArchiveItemOptions, type BacklogAddOptions, type BacklogRemoveOptions, type BacklogUpdateOptions, type PhaseOptions, type PhaseStep, type ProgressUpdateOptions, } from "./mutations.ts";
9
+ export { docsRepair } from "./repair.ts";
10
+ export type { DocsRepairAction, DocsRepairOptions, DocsRepairPlan } from "./types.ts";
11
+ export { allSynonyms, canonical, detectLocale, getFieldByRole, stripBom, synonymPattern, LOCALES, SYNONYMS, type Locale, type SynonymRole, } from "./profile.ts";
12
+ export { fileHashes, DOC_FILES } from "./hashes.ts";
13
+ export { phaseContext, type PhaseContext } from "./context.ts";
14
+ export { docsStatus } from "./status.ts";
15
+ export { docsValidate } from "./validate.ts";
16
+ export { TEMPLATE_KINDS, projectTemplates, type TemplateKind } from "./templates.ts";
17
+ export { PRIORITY_ALIASES, STATUS_ALIASES, priorityAliasHelp, resolvePriority, resolveStatus, statusAliasHelp, } from "./aliases.ts";
18
+ export type { ArchiveItem, ArchiveItemPlan, ApplyResult, Backlog, BacklogAddPlan, BacklogEntry, BacklogItem, BacklogRemovePlan, BacklogSection, BacklogUpdatePlan, DoneEntry, DocsStatus, ParseResult, PhaseBlock, PhasePlan, PlanChange, Priority, Progress, ProgressRow, ProgressUpdatePlan, Status, ValidateResult, Warning, } from "./types.ts";
19
+ export declare const TOOL_NAME = "stepwell";
20
+ export declare const TOOL_VERSION: string;