jackherd 0.0.1 → 1.0.1
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 +65 -0
- package/LICENSE +21 -0
- package/NOTICE +38 -0
- package/README.md +1122 -3
- package/assets/branding/README.md +62 -0
- package/assets/branding/jackherd-icon-256.png +0 -0
- package/assets/branding/jackherd-icon-512.png +0 -0
- package/assets/branding/jackherd-icon-64.png +0 -0
- package/assets/branding/jackherd-icon-small-32.png +0 -0
- package/assets/branding/jackherd-icon-small-64.png +0 -0
- package/assets/branding/jackherd-icon-small.svg +1 -0
- package/assets/branding/jackherd-icon.svg +1 -0
- package/assets/branding/jackherd-logo-1024.png +0 -0
- package/assets/branding/jackherd-logo-512.png +0 -0
- package/assets/branding/jackherd-logo-dark-1024.png +0 -0
- package/assets/branding/jackherd-logo-dark-512.png +0 -0
- package/assets/branding/jackherd-logo-dark.svg +1 -0
- package/assets/branding/jackherd-logo-mark-1024.png +0 -0
- package/assets/branding/jackherd-logo-mark-512.png +0 -0
- package/assets/branding/jackherd-logo-mark.svg +1 -0
- package/assets/branding/jackherd-logo.svg +1 -0
- package/dist/chunk-R6MGHCD7.js +93 -0
- package/dist/config.d.ts +167 -0
- package/dist/config.js +401 -0
- package/dist/plan-format.d.ts +325 -0
- package/dist/plan-format.js +636 -0
- package/docs/lanes.mmd +9 -0
- package/docs/lanes.svg +1 -0
- package/docs/skills.mmd +10 -0
- package/docs/skills.svg +1 -0
- package/docs/workflow.svg +172 -0
- package/herdr-plugin/herdr-plugin.toml +22 -0
- package/herdr-plugin/open-plans.ts +43 -0
- package/herdr-plugin/restore-plans.ts +107 -0
- package/install.sh +157 -0
- package/kit/hooks/hooks/git-guard/decision.ts +438 -0
- package/kit/hooks/hooks/git-guard/index.ts +38 -0
- package/kit/hooks/hooks/git-guard/shell.ts +407 -0
- package/kit/hooks/hooks/watch-guard/background.ts +36 -0
- package/kit/hooks/hooks/watch-guard/decision.ts +53 -0
- package/kit/hooks/hooks/watch-guard/index.ts +11 -0
- package/kit/hooks/hooks/watch-guard/shell-input.ts +32 -0
- package/kit/hooks/hooks/watch-guard/starts.ts +154 -0
- package/kit/hooks/hooks/watch-guard/text-fallback.ts +40 -0
- package/kit/hooks/hooks.json +30 -0
- package/kit/hooks/statusline.sh +72 -0
- package/kit/review/agents/devops.md +11 -0
- package/kit/review/agents/public-content.md +39 -0
- package/kit/review/agents/security-audit.md +11 -0
- package/kit/review/agents/uncle-bob.md +11 -0
- package/kit/review/skills/devops/SKILL.md +50 -0
- package/kit/review/skills/diagnose/SKILL.md +140 -0
- package/kit/review/skills/diagnose/references/feedback-loops.md +59 -0
- package/kit/review/skills/diagnose/scripts/hitl-loop.template.sh +71 -0
- package/kit/review/skills/pre-commit/SKILL.md +100 -0
- package/kit/review/skills/review/SKILL.md +43 -0
- package/kit/review/skills/security-audit/SKILL.md +65 -0
- package/kit/review/skills/uncle-bob/SKILL.md +43 -0
- package/kit/rules/rules/kit/arc42.md +89 -0
- package/kit/rules/rules/kit/architecture-files.md +87 -0
- package/kit/rules/rules/kit/code-style.md +159 -0
- package/kit/rules/rules/kit/pull-requests.md +59 -0
- package/kit/rules/rules/kit/testing.md +91 -0
- package/kit/rules/rules/kit/writing-for-agents.md +110 -0
- package/kit/workflow/agents/explore.md +25 -0
- package/kit/workflow/agents/implement.md +16 -0
- package/kit/workflow/agents/plan.md +13 -0
- package/kit/workflow/agents/spec-review.md +96 -0
- package/kit/workflow/skills/follow/SKILL.md +48 -0
- package/kit/workflow/skills/grill/SKILL.md +60 -0
- package/kit/workflow/skills/implement/SKILL.md +107 -0
- package/kit/workflow/skills/orchestrate/SKILL.md +565 -0
- package/kit/workflow/skills/orchestrate/handover/decision.ts +247 -0
- package/kit/workflow/skills/orchestrate/handover/index.ts +171 -0
- package/kit/workflow/skills/plan/SKILL.md +118 -0
- package/kit/workflow/skills/plan/references/template-English.md +92 -0
- package/kit/workflow/skills/plan/references/template-German.md +92 -0
- package/kit/workflow/skills/start/SKILL.md +47 -0
- package/package.json +63 -5
- package/schemas/jackherd.schema.json +601 -0
- package/src/check/changes.ts +45 -0
- package/src/check/command-line.ts +12 -0
- package/src/check/command.ts +136 -0
- package/src/check/deferral-arrows.ts +41 -0
- package/src/check/execute.ts +29 -0
- package/src/check/glob.ts +15 -0
- package/src/check/plan-structure-check.ts +135 -0
- package/src/check/plan.ts +106 -0
- package/src/check/public-content-check.ts +32 -0
- package/src/check/public-content-facts.ts +177 -0
- package/src/check/public-content.ts +216 -0
- package/src/check/report.ts +55 -0
- package/src/check/result.ts +42 -0
- package/src/check/roots.ts +36 -0
- package/src/check/routing.ts +22 -0
- package/src/check/schema.ts +83 -0
- package/src/check/settings.ts +44 -0
- package/src/claude/paths.ts +14 -0
- package/src/cli.ts +46 -0
- package/src/config/command.ts +45 -0
- package/src/config/detect.ts +196 -0
- package/src/config/index.ts +2 -0
- package/src/config/json-schema.ts +5 -0
- package/src/config/legacy-names.ts +56 -0
- package/src/config/link-kinds.ts +25 -0
- package/src/config/location.ts +46 -0
- package/src/config/read-config.ts +42 -0
- package/src/config/repository-facts.ts +19 -0
- package/src/config/repository-values.ts +46 -0
- package/src/config/schema.ts +302 -0
- package/src/context/command.ts +39 -0
- package/src/context/coordinator-name.ts +16 -0
- package/src/context/fill.ts +69 -0
- package/src/context/index.ts +10 -0
- package/src/context/level.ts +22 -0
- package/src/context/options.ts +29 -0
- package/src/context/report.ts +42 -0
- package/src/context/session-log.ts +91 -0
- package/src/plan-format/drift.ts +69 -0
- package/src/plan-format/header.ts +27 -0
- package/src/plan-format/index.ts +9 -0
- package/src/plan-format/lanes.ts +128 -0
- package/src/plan-format/markdown.ts +98 -0
- package/src/plan-format/phase-tally.ts +55 -0
- package/src/plan-format/phases.ts +224 -0
- package/src/plan-format/plan-structure.ts +150 -0
- package/src/plan-format/progress.ts +202 -0
- package/src/plan-format/sprint-document.ts +69 -0
- package/src/plan-format/terminal-text.ts +28 -0
- package/src/plans/board-choice.ts +60 -0
- package/src/plans/board-pick.ts +72 -0
- package/src/plans/board-rows.ts +62 -0
- package/src/plans/command.ts +60 -0
- package/src/plans/ensure-panes.ts +71 -0
- package/src/plans/error-text.ts +5 -0
- package/src/plans/fetch.ts +96 -0
- package/src/plans/git.ts +145 -0
- package/src/plans/github.ts +35 -0
- package/src/plans/lane-discovery.ts +34 -0
- package/src/plans/lane-plans.ts +80 -0
- package/src/plans/layout.ts +881 -0
- package/src/plans/link-chips.ts +74 -0
- package/src/plans/live.ts +336 -0
- package/src/plans/markdown-html.ts +140 -0
- package/src/plans/options.ts +15 -0
- package/src/plans/phase-source.ts +71 -0
- package/src/plans/plan-chips.ts +75 -0
- package/src/plans/plan-panes.ts +206 -0
- package/src/plans/planned.ts +117 -0
- package/src/plans/pull-requests.ts +107 -0
- package/src/plans/restart-decision.ts +42 -0
- package/src/plans/selection.ts +352 -0
- package/src/plans/settings.ts +68 -0
- package/src/plans/single-flight.ts +25 -0
- package/src/plans/snapshot.ts +300 -0
- package/src/plans/sprint-export.ts +103 -0
- package/src/plans/sprint-html.ts +301 -0
- package/src/plans/sprint-page.ts +287 -0
- package/src/plans/supervisor.ts +124 -0
- package/src/plans/symbols.ts +54 -0
- package/src/plans/texts.ts +103 -0
- package/src/plans/unlisted-lanes.ts +45 -0
- package/src/plans/view.ts +68 -0
- package/src/plans/worked-phases.ts +53 -0
- package/src/sync/checksum.ts +13 -0
- package/src/sync/choice.ts +14 -0
- package/src/sync/command-context.ts +113 -0
- package/src/sync/command.ts +94 -0
- package/src/sync/error-message.ts +1 -0
- package/src/sync/gitignore.ts +45 -0
- package/src/sync/global-kit.ts +176 -0
- package/src/sync/herdr-plugin.ts +175 -0
- package/src/sync/if-changed.ts +109 -0
- package/src/sync/init-command.ts +239 -0
- package/src/sync/init.ts +136 -0
- package/src/sync/kit-hooks.ts +18 -0
- package/src/sync/kit-path.ts +77 -0
- package/src/sync/kit-source.ts +169 -0
- package/src/sync/kit.ts +9 -0
- package/src/sync/legacy-files.ts +116 -0
- package/src/sync/manifest.ts +44 -0
- package/src/sync/next-steps.ts +42 -0
- package/src/sync/plan-file.ts +154 -0
- package/src/sync/plan-hooks.ts +165 -0
- package/src/sync/project-fs.ts +132 -0
- package/src/sync/render.ts +60 -0
- package/src/sync/report.ts +96 -0
- package/src/sync/settings-hooks.ts +90 -0
- package/src/sync/settings-migration.ts +94 -0
- package/src/sync/settings-offer.ts +168 -0
- package/src/sync/setup-questions.ts +70 -0
- package/src/sync/setup.ts +52 -0
- package/src/sync/sync-plan.ts +120 -0
- package/src/sync/sync-result.ts +9 -0
- package/src/sync/sync.ts +307 -0
- package/src/sync/tools.ts +52 -0
- package/src/terminal/herdr.ts +91 -0
- package/src/terminal/index.ts +13 -0
- package/src/terminal/none.ts +7 -0
- package/src/terminal/open.ts +39 -0
- package/src/terminal/text.ts +159 -0
- package/src/terminal/types.ts +30 -0
- package/src/terminal/width.ts +90 -0
- package/src/update/command.ts +65 -0
- package/src/update/package-manager.ts +56 -0
- package/src/update/update.ts +105 -0
- package/src/version/installed-version.ts +32 -0
- package/src/version/release.ts +22 -0
- package/src/version/skill-diff.ts +63 -0
- package/src/version/started-kit.ts +14 -0
- package/src/vocabulary/index.ts +132 -0
- package/src/watch/base-tip.ts +18 -0
- package/src/watch/command.ts +919 -0
- package/src/watch/coordinator-fill.ts +76 -0
- package/src/watch/feedback-read.ts +73 -0
- package/src/watch/feedback.ts +308 -0
- package/src/watch/gh.ts +22 -0
- package/src/watch/github-read.ts +175 -0
- package/src/watch/herdr.ts +11 -0
- package/src/watch/json-guards.ts +9 -0
- package/src/watch/kit-update.ts +18 -0
- package/src/watch/lane-fill.ts +36 -0
- package/src/watch/lane-message.ts +99 -0
- package/src/watch/lane-progress.ts +44 -0
- package/src/watch/lane-report-file.ts +53 -0
- package/src/watch/lane-report.ts +270 -0
- package/src/watch/lane-search.ts +41 -0
- package/src/watch/lane-sources.ts +46 -0
- package/src/watch/lane-watch.ts +121 -0
- package/src/watch/monitor-tool.ts +19 -0
- package/src/watch/options.ts +79 -0
- package/src/watch/plan-badges-read.ts +30 -0
- package/src/watch/plan-badges.ts +66 -0
- package/src/watch/plan-views.ts +59 -0
- package/src/watch/preview-read.ts +60 -0
- package/src/watch/previews.ts +234 -0
- package/src/watch/pull-requests.ts +117 -0
- package/src/watch/single-report.ts +15 -0
- package/src/watch/skill-change.ts +61 -0
- package/src/watch/source-dry-run-command.ts +35 -0
- package/src/watch/source-dry-run.ts +124 -0
- package/src/watch/source-fields.ts +9 -0
- package/src/watch/source-path.ts +40 -0
- package/src/watch/source-run.ts +90 -0
- package/src/watch/sources.ts +344 -0
- package/src/watch/stream-output.ts +14 -0
- package/src/watch/texts.ts +76 -0
- package/src/watch/watch-events.ts +399 -0
- package/src/watch/watcher-lock.ts +145 -0
- package/src/watch/workflow-runs.ts +96 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LANGUAGE_NAME,
|
|
3
|
+
PLAN_LANGUAGE,
|
|
4
|
+
PLAN_STATUS,
|
|
5
|
+
PLAN_VOCABULARY,
|
|
6
|
+
classifyStatus,
|
|
7
|
+
isPlanLanguage,
|
|
8
|
+
phaseHeading,
|
|
9
|
+
sectionHeading
|
|
10
|
+
} from "./chunk-R6MGHCD7.js";
|
|
11
|
+
|
|
12
|
+
// src/plan-format/terminal-text.ts
|
|
13
|
+
var UNSAFE_RANGES = [
|
|
14
|
+
[0, 31],
|
|
15
|
+
[127, 159],
|
|
16
|
+
[1564, 1564],
|
|
17
|
+
[8206, 8207],
|
|
18
|
+
[8232, 8233],
|
|
19
|
+
[8234, 8238],
|
|
20
|
+
[8294, 8297]
|
|
21
|
+
];
|
|
22
|
+
var escapeCodePoint = (codePoint) => `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
23
|
+
var UNSAFE_TERMINAL_TEXT = new RegExp(
|
|
24
|
+
`[${UNSAFE_RANGES.map(([from, to]) => `${escapeCodePoint(from)}-${escapeCodePoint(to)}`).join("")}]`,
|
|
25
|
+
"g"
|
|
26
|
+
);
|
|
27
|
+
var terminalSafe = (text) => text.replace(UNSAFE_TERMINAL_TEXT, "");
|
|
28
|
+
|
|
29
|
+
// src/plan-format/markdown.ts
|
|
30
|
+
var SECTION_END = /^##\s/;
|
|
31
|
+
var TABLE_SEPARATOR = /^\|[\s|:-]+\|$/;
|
|
32
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
+
var sectionStart = (markdown, headingText) => {
|
|
34
|
+
const heading = new RegExp(`^##\\s+${escapeRegExp(headingText)}\\s*$`);
|
|
35
|
+
const start = markdown.split("\n").findIndex((line) => heading.test(line));
|
|
36
|
+
return start === -1 ? -1 : start + 1;
|
|
37
|
+
};
|
|
38
|
+
var sectionLines = (markdown, headingText) => {
|
|
39
|
+
const start = sectionStart(markdown, headingText);
|
|
40
|
+
if (start === -1) return [];
|
|
41
|
+
const rest = markdown.split("\n").slice(start);
|
|
42
|
+
const end = rest.findIndex((line) => SECTION_END.test(line));
|
|
43
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
44
|
+
};
|
|
45
|
+
var tableCells = (line) => line.slice(1, line.endsWith("|") ? -1 : void 0).split("|").map((cell) => terminalSafe(cell).trim());
|
|
46
|
+
var firstTableRows = (lines) => {
|
|
47
|
+
const start = lines.findIndex((line) => line.startsWith("|"));
|
|
48
|
+
if (start === -1) return [];
|
|
49
|
+
const rest = lines.slice(start);
|
|
50
|
+
const end = rest.findIndex((line) => !line.startsWith("|"));
|
|
51
|
+
return (end === -1 ? rest : rest.slice(0, end)).map((line, offset) => ({ index: start + offset, line, cells: tableCells(line) })).slice(1).filter(({ line }) => !TABLE_SEPARATOR.test(line));
|
|
52
|
+
};
|
|
53
|
+
var firstTable = (lines) => firstTableRows(lines).map(({ cells }) => cells);
|
|
54
|
+
var headerField = (markdown, name) => {
|
|
55
|
+
const match = new RegExp(`^\\*\\*${escapeRegExp(name)}:\\*\\*\\s*(.+?)(?=\\n\\*\\*|\\n\\n)`, "ms").exec(markdown);
|
|
56
|
+
return match ? match[1].replace(/\s+/g, " ").trim() : "";
|
|
57
|
+
};
|
|
58
|
+
var firstHeading = (markdown, fallback) => terminalSafe(markdown.match(/^#\s+(.+)$/m)?.[1] ?? fallback).trim();
|
|
59
|
+
var HEADING = /^(#{2,4})\s+(.*?)\s*$/;
|
|
60
|
+
var subsectionLines = (markdown, headingText) => {
|
|
61
|
+
const lines = markdown.split("\n");
|
|
62
|
+
const start = lines.findIndex((line) => HEADING.exec(line)?.[2].startsWith(headingText));
|
|
63
|
+
if (start === -1) return [];
|
|
64
|
+
const level = HEADING.exec(lines[start])?.[1].length ?? 2;
|
|
65
|
+
const rest = lines.slice(start + 1);
|
|
66
|
+
const end = rest.findIndex((line) => {
|
|
67
|
+
const heading = HEADING.exec(line);
|
|
68
|
+
return heading !== null && heading[1].length <= level;
|
|
69
|
+
});
|
|
70
|
+
return end === -1 ? rest : rest.slice(0, end);
|
|
71
|
+
};
|
|
72
|
+
var firstTableWithHeader = (lines) => {
|
|
73
|
+
const start = lines.findIndex((line) => line.startsWith("|"));
|
|
74
|
+
if (start === -1) return { header: [], rows: [] };
|
|
75
|
+
return { header: tableCells(lines[start]), rows: firstTable(lines) };
|
|
76
|
+
};
|
|
77
|
+
var allTables = (lines) => {
|
|
78
|
+
const tables = [];
|
|
79
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
80
|
+
if (!lines[index].startsWith("|") || index > 0 && lines[index - 1].startsWith("|")) continue;
|
|
81
|
+
const above = lines.slice(0, index).reverse().find((line) => line.trim() !== "");
|
|
82
|
+
const caption = above && !above.startsWith("|") ? above.trim() : null;
|
|
83
|
+
tables.push({ caption, ...firstTableWithHeader(lines.slice(index)) });
|
|
84
|
+
}
|
|
85
|
+
return tables;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/plan-format/drift.ts
|
|
89
|
+
var OPEN_TABLE_CELL = /⬜/;
|
|
90
|
+
var DRIFT_WORDS = {
|
|
91
|
+
de: {
|
|
92
|
+
inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Aktiv)\b/i,
|
|
93
|
+
completion: /\b(done|abgeschlossen|umgesetzt|implementiert|implemented|fertig)\b/i,
|
|
94
|
+
open: /\b(offen|ausstehend|deferred|zurückgestellt)\b/i,
|
|
95
|
+
architecture: /\b(arc42|ADR|Architekturdokumentation|Dokumentation)\b/i,
|
|
96
|
+
architectureHeading: "Architekturdokumentation",
|
|
97
|
+
selfReported: (statusText) => `Status nennt offene Doku-Arbeit: "${statusText.trim()}"`,
|
|
98
|
+
silent: "arc42-Abschlussphase hat offene \u2B1C-Zeilen trotz abgeschlossenem Status"
|
|
99
|
+
},
|
|
100
|
+
en: {
|
|
101
|
+
inFlight: /^(Draft|In Progress|Proposed|Open|Planned|Review-Ready|Active)\b/i,
|
|
102
|
+
completion: /\b(done|completed|implemented|finished)\b/i,
|
|
103
|
+
open: /\b(open|outstanding|pending|deferred)\b/i,
|
|
104
|
+
architecture: /\b(arc42|ADR|architecture documentation|documentation)\b/i,
|
|
105
|
+
architectureHeading: "Architecture documentation",
|
|
106
|
+
selfReported: (statusText) => `Status names open documentation work: "${statusText.trim()}"`,
|
|
107
|
+
silent: "The arc42 closing phase has open \u2B1C rows despite a completed status"
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var statusLine = (content, name) => content.match(new RegExp(`^\\*\\*${escapeRegExp(name)}:\\*\\*\\s*(.+)$`, "m"))?.[1];
|
|
111
|
+
var architectureSection = (content, heading) => content.match(new RegExp(`#{2,4}[^\\n]*${escapeRegExp(heading)}[^\\n]*\\n([\\s\\S]*?)(?=\\n#{1,4} |$)`))?.[1];
|
|
112
|
+
var findDrift = (file, content, language) => {
|
|
113
|
+
const statusText = statusLine(content, PLAN_VOCABULARY[language].header.status);
|
|
114
|
+
const words = DRIFT_WORDS[language];
|
|
115
|
+
if (!statusText || words.inFlight.test(statusText.trim())) return [];
|
|
116
|
+
const completed = words.completion.test(statusText);
|
|
117
|
+
const caveat = words.open.test(statusText);
|
|
118
|
+
const selfReported = completed && caveat && words.architecture.test(statusText) ? words.selfReported(statusText) : null;
|
|
119
|
+
const section = completed && !caveat ? architectureSection(content, words.architectureHeading) : void 0;
|
|
120
|
+
const silent = section && OPEN_TABLE_CELL.test(section) ? words.silent : null;
|
|
121
|
+
return [selfReported, silent].filter((reason) => reason !== null).map((reason) => ({ file, reason }));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/plan-format/header.ts
|
|
125
|
+
var DECISION_ROW = /^\|\s*\*?\*?D\d+/gm;
|
|
126
|
+
var readPlanHeader = (markdown, language, fallbackTitle) => {
|
|
127
|
+
const { header } = PLAN_VOCABULARY[language];
|
|
128
|
+
const status = headerField(markdown, header.status);
|
|
129
|
+
return {
|
|
130
|
+
title: firstHeading(markdown, fallbackTitle),
|
|
131
|
+
date: headerField(markdown, header.date),
|
|
132
|
+
status,
|
|
133
|
+
statusWord: classifyStatus(language, status),
|
|
134
|
+
scope: headerField(markdown, header.scope)
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
var countDecisions = (markdown) => (markdown.match(DECISION_ROW) ?? []).length;
|
|
138
|
+
|
|
139
|
+
// src/plan-format/lanes.ts
|
|
140
|
+
import { posix } from "path";
|
|
141
|
+
var LANE_NAME = /`([^`]+)`/;
|
|
142
|
+
var CONTINUATION = "\u21B3";
|
|
143
|
+
var PLAN_LINK = /\[([^\]]+)\]\(([^)]+)\)/;
|
|
144
|
+
var PHASE_ID_PREFIX = "P";
|
|
145
|
+
var CLAUSE_SEPARATOR = /([;,]|\.(?=\s))\s*/;
|
|
146
|
+
var ENTRY_END = /* @__PURE__ */ new Set([";", "."]);
|
|
147
|
+
var phaseIdPatterns = (language) => {
|
|
148
|
+
const connectors = PLAN_VOCABULARY[language].phaseConnectors.join("|");
|
|
149
|
+
const id = `(?:\\*\\*)?${PHASE_ID_PREFIX}(\\d+[a-z]?)\\b`;
|
|
150
|
+
return {
|
|
151
|
+
leading: new RegExp(`^\\s*(?:(?:${connectors})\\s+)*${id}`),
|
|
152
|
+
afterConnector: new RegExp(`^\\s*(?:${connectors})\\s`),
|
|
153
|
+
connector: new RegExp(`\\b(?:${connectors})\\s+${id}`, "g")
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
var phaseIdsOf = (cell, patterns) => {
|
|
157
|
+
const parts = cell.split(CLAUSE_SEPARATOR);
|
|
158
|
+
const ids = [];
|
|
159
|
+
let listRunning = false;
|
|
160
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
161
|
+
const separator = index > 0 ? parts[index - 1] : null;
|
|
162
|
+
const clause = parts[index];
|
|
163
|
+
const entryStart = separator === null || ENTRY_END.has(separator);
|
|
164
|
+
if (entryStart) listRunning = false;
|
|
165
|
+
const leading = clause.match(patterns.leading);
|
|
166
|
+
const startsEntry = entryStart || listRunning || patterns.afterConnector.test(clause);
|
|
167
|
+
const counts = leading !== null && startsEntry;
|
|
168
|
+
if (leading && counts) ids.push(leading[1]);
|
|
169
|
+
ids.push(...[...clause.matchAll(patterns.connector)].map((match) => match[1]));
|
|
170
|
+
listRunning ||= counts;
|
|
171
|
+
}
|
|
172
|
+
return [...new Set(ids)];
|
|
173
|
+
};
|
|
174
|
+
var phaseIdsOfCell = (cell, language) => phaseIdsOf(cell, phaseIdPatterns(language));
|
|
175
|
+
var laneNameOf = (cell, previous) => cell.match(LANE_NAME)?.[1] ?? (cell.startsWith(CONTINUATION) ? previous : null);
|
|
176
|
+
var toStep = ([, , plan = "", phases = "", startsAfter = "", model = ""], phaseId) => {
|
|
177
|
+
const link = plan.match(PLAN_LINK);
|
|
178
|
+
return {
|
|
179
|
+
planTitle: link ? link[1] : plan,
|
|
180
|
+
planHref: link ? link[2] : null,
|
|
181
|
+
phaseIds: phaseIdsOf(phases, phaseId),
|
|
182
|
+
label: phases,
|
|
183
|
+
startsAfter,
|
|
184
|
+
model
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
var withoutRepeats = (lane, step) => {
|
|
188
|
+
const named = new Set(
|
|
189
|
+
lane.steps.filter((earlier) => earlier.planHref === step.planHref).flatMap((earlier) => earlier.phaseIds)
|
|
190
|
+
);
|
|
191
|
+
const phaseIds = step.phaseIds.filter((id) => !named.has(id));
|
|
192
|
+
if (phaseIds.length === step.phaseIds.length) return step;
|
|
193
|
+
return { ...step, phaseIds, namedPhaseIds: step.namedPhaseIds ?? step.phaseIds };
|
|
194
|
+
};
|
|
195
|
+
var parseSprintLanes = (markdown, language) => {
|
|
196
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
197
|
+
const phaseId = phaseIdPatterns(language);
|
|
198
|
+
const { laneClosed } = PLAN_VOCABULARY[language];
|
|
199
|
+
let previous = null;
|
|
200
|
+
for (const cells of firstTable(sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes))) {
|
|
201
|
+
const name = laneNameOf(cells[0] ?? "", previous);
|
|
202
|
+
if (!name) continue;
|
|
203
|
+
const port = Number.parseInt(cells[1] ?? "", 10);
|
|
204
|
+
const lane = lanes.get(name) ?? { name, port: Number.isNaN(port) ? null : port, steps: [], closed: false };
|
|
205
|
+
lane.closed ||= (cells[0] ?? "").includes(laneClosed);
|
|
206
|
+
lane.steps.push(withoutRepeats(lane, toStep(cells, phaseId)));
|
|
207
|
+
lanes.set(name, lane);
|
|
208
|
+
previous = name;
|
|
209
|
+
}
|
|
210
|
+
return [...lanes.values()];
|
|
211
|
+
};
|
|
212
|
+
var sprintTitleOf = (markdown, fallback) => firstHeading(markdown, fallback);
|
|
213
|
+
var resolvePlanPath = (sprintDirectory, href) => posix.normalize(posix.join(sprintDirectory, href.split("#")[0]));
|
|
214
|
+
|
|
215
|
+
// src/plan-format/phases.ts
|
|
216
|
+
var REVIEW_GATE = /^\s*\*\*review[- ]gate\b/i;
|
|
217
|
+
var PHASE_END = /^#{1,3}\s/;
|
|
218
|
+
var CHECKBOX = /^\s*[-*]\s+\[([ xX])\]\s+(.*)$/;
|
|
219
|
+
var SECTION_HEADING = /^##\s+(.+?)\s*$/;
|
|
220
|
+
var TOP_HEADING = /^#\s/;
|
|
221
|
+
var TABLE_STATUS = { done: "\u2705", open: "\u2B1C" };
|
|
222
|
+
var ROW_NUMBER = /^\d+(\.\d+)*[a-z]?$/;
|
|
223
|
+
var NUMBERED_SECTION = /^##\s+(\d+[a-z]?)\s*[—–:-]\s*(.*)$/;
|
|
224
|
+
var SECTION_POINTER = /`##\s+([^`]+?)\s*`/;
|
|
225
|
+
var DEFERRED_TO = new RegExp(`\\s\u2192\\s*${PHASE_ID_PREFIX}(\\d+[a-z]?)\\s*$`, "i");
|
|
226
|
+
var PHASE_ARROW = new RegExp(`\\s*\u2192\\s*${PHASE_ID_PREFIX}(\\d+[a-z]?)\\b`, "i");
|
|
227
|
+
var isReviewGateLine = (line) => REVIEW_GATE.test(line);
|
|
228
|
+
var endsPhase = (line) => PHASE_END.test(line);
|
|
229
|
+
var isCheckboxLine = (line) => CHECKBOX.test(line);
|
|
230
|
+
var isOpenCheckboxLine = (line) => line.match(CHECKBOX)?.[1] === " ";
|
|
231
|
+
var phaseHeadingPattern = (language) => new RegExp(`^###\\s+${escapeRegExp(PLAN_VOCABULARY[language].phase)}\\s+(\\d+[a-z]?)\\b\\s*[:\u2014\u2013-]?\\s*(.*)$`, "i");
|
|
232
|
+
var tableCells2 = (line) => {
|
|
233
|
+
const trimmed = line.trim();
|
|
234
|
+
if (!trimmed.startsWith("|")) return null;
|
|
235
|
+
return trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
|
236
|
+
};
|
|
237
|
+
var tableItemOf = (line) => {
|
|
238
|
+
const cells = tableCells2(line);
|
|
239
|
+
const statusIndex = cells?.findIndex((cell) => cell === TABLE_STATUS.done || cell === TABLE_STATUS.open) ?? -1;
|
|
240
|
+
if (!cells || statusIndex < 0) return null;
|
|
241
|
+
const rest = cells.filter((_, index) => index !== statusIndex);
|
|
242
|
+
const text = (ROW_NUMBER.test(rest[0] ?? "") ? rest.slice(1) : rest).filter((cell) => cell !== "").join(" \xB7 ");
|
|
243
|
+
return { done: cells[statusIndex] === TABLE_STATUS.done, text: terminalSafe(text).trim() };
|
|
244
|
+
};
|
|
245
|
+
var checkboxItemOf = (line) => {
|
|
246
|
+
const checkbox = line.match(CHECKBOX);
|
|
247
|
+
return checkbox ? { done: checkbox[1] !== " ", text: terminalSafe(checkbox[2] ?? "").trim() } : null;
|
|
248
|
+
};
|
|
249
|
+
var checklistItemOf = (line, gate = false) => {
|
|
250
|
+
const item = checkboxItemOf(line) ?? tableItemOf(line);
|
|
251
|
+
if (!item) return null;
|
|
252
|
+
return gate ? { ...item, gate: true } : item;
|
|
253
|
+
};
|
|
254
|
+
var sectionItems = (markdown) => {
|
|
255
|
+
const sections = /* @__PURE__ */ new Map();
|
|
256
|
+
let open = null;
|
|
257
|
+
for (const line of markdown.split("\n")) {
|
|
258
|
+
const heading = line.match(SECTION_HEADING);
|
|
259
|
+
if (heading?.[1]) {
|
|
260
|
+
open = [];
|
|
261
|
+
sections.set(heading[1], open);
|
|
262
|
+
} else if (TOP_HEADING.test(line)) {
|
|
263
|
+
open = null;
|
|
264
|
+
} else {
|
|
265
|
+
const item = checklistItemOf(line);
|
|
266
|
+
if (item) open?.push(item);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return sections;
|
|
270
|
+
};
|
|
271
|
+
var withPointedSections = (phases, pointers, markdown) => {
|
|
272
|
+
if (pointers.size === 0) return phases;
|
|
273
|
+
const sections = sectionItems(markdown);
|
|
274
|
+
return phases.map((phase) => {
|
|
275
|
+
const pointer = pointers.get(phase);
|
|
276
|
+
return phase.items.length === 0 && pointer ? { ...phase, items: sections.get(pointer) ?? [] } : phase;
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
var deferralTarget = (line, phaseId) => {
|
|
280
|
+
const checkbox = line.match(CHECKBOX);
|
|
281
|
+
if (checkbox?.[1] !== " ") return null;
|
|
282
|
+
const target = checkbox[2]?.match(DEFERRED_TO)?.[1]?.toLowerCase();
|
|
283
|
+
return target && target !== phaseId ? target : null;
|
|
284
|
+
};
|
|
285
|
+
var misplacedDeferralOf = (line, index) => {
|
|
286
|
+
const checkbox = line.match(CHECKBOX);
|
|
287
|
+
if (checkbox?.[1] !== " " || DEFERRED_TO.test(checkbox[2])) return [];
|
|
288
|
+
const arrow = checkbox[2].match(PHASE_ARROW);
|
|
289
|
+
if (!arrow) return [];
|
|
290
|
+
const rest = checkbox[2].replace(PHASE_ARROW, "").trim();
|
|
291
|
+
const marker = line.slice(0, line.length - checkbox[2].length);
|
|
292
|
+
return [{ line: index + 1, target: arrow[1], corrected: `${marker}${rest} \u2192 ${PHASE_ID_PREFIX}${arrow[1]}` }];
|
|
293
|
+
};
|
|
294
|
+
var misplacedDeferrals = (markdown) => markdown.split("\n").flatMap(misplacedDeferralOf);
|
|
295
|
+
var deferredItemOf = (line, phaseId) => {
|
|
296
|
+
const target = deferralTarget(line, phaseId);
|
|
297
|
+
const text = line.match(CHECKBOX)?.[2] ?? "";
|
|
298
|
+
return target ? { text: terminalSafe(text.replace(DEFERRED_TO, "")).trim(), target } : null;
|
|
299
|
+
};
|
|
300
|
+
var arrivedItem = ({ text }, origin) => ({
|
|
301
|
+
done: false,
|
|
302
|
+
text: `${text} \u2190 ${PHASE_ID_PREFIX}${origin.id}`
|
|
303
|
+
});
|
|
304
|
+
var withArrivedItems = (phases) => {
|
|
305
|
+
const ids = new Set(phases.map(({ id }) => id));
|
|
306
|
+
const moved = phases.flatMap((origin) => (origin.deferred ?? []).map((item) => ({ origin, item })));
|
|
307
|
+
if (moved.length === 0) return phases;
|
|
308
|
+
return phases.map((phase) => {
|
|
309
|
+
const arriving = moved.filter(({ item }) => item.target === phase.id).map(({ item, origin }) => arrivedItem(item, origin));
|
|
310
|
+
const stranded = (phase.deferred ?? []).filter(({ target }) => !ids.has(target));
|
|
311
|
+
const deferred = (phase.deferred ?? []).filter(({ target }) => ids.has(target));
|
|
312
|
+
const returned = stranded.map(({ text, target }) => ({
|
|
313
|
+
done: false,
|
|
314
|
+
text: `${text} \u2192 ${PHASE_ID_PREFIX}${target}`
|
|
315
|
+
}));
|
|
316
|
+
const { deferred: _, ...rest } = phase;
|
|
317
|
+
return { ...rest, items: [...phase.items, ...returned, ...arriving], ...deferred.length > 0 ? { deferred } : {} };
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
var fileLine = (phase, line, gate) => {
|
|
321
|
+
const deferred = deferredItemOf(line, phase.id);
|
|
322
|
+
if (deferred) {
|
|
323
|
+
phase.deferred = [...phase.deferred ?? [], deferred];
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const item = checklistItemOf(line, gate);
|
|
327
|
+
if (item) phase.items.push(item);
|
|
328
|
+
};
|
|
329
|
+
var notePointer = (pointers, phase, line) => {
|
|
330
|
+
const pointer = phase && !pointers.has(phase) ? line.match(SECTION_POINTER)?.[1] : void 0;
|
|
331
|
+
if (phase && pointer) pointers.set(phase, pointer);
|
|
332
|
+
};
|
|
333
|
+
var headingOf = (line, phaseHeading2) => line.match(phaseHeading2) ?? line.match(NUMBERED_SECTION);
|
|
334
|
+
var parsePlanPhases = (markdown, language) => {
|
|
335
|
+
const phaseHeading2 = phaseHeadingPattern(language);
|
|
336
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
337
|
+
const { phases } = markdown.split("\n").reduce(
|
|
338
|
+
({ phases: phases2, open, gate }, line) => {
|
|
339
|
+
const heading = headingOf(line, phaseHeading2);
|
|
340
|
+
if (heading) {
|
|
341
|
+
const phase = { id: (heading[1] ?? "").toLowerCase(), title: terminalSafe(heading[2] ?? "").trim(), items: [] };
|
|
342
|
+
return { phases: [...phases2, phase], open: phase, gate: false };
|
|
343
|
+
}
|
|
344
|
+
if (PHASE_END.test(line)) return { phases: phases2, open: null, gate: false };
|
|
345
|
+
const inGate = gate || REVIEW_GATE.test(line);
|
|
346
|
+
if (open) fileLine(open, line, inGate);
|
|
347
|
+
notePointer(pointers, open, line);
|
|
348
|
+
return { phases: phases2, open, gate: inGate };
|
|
349
|
+
},
|
|
350
|
+
{ phases: [], open: null, gate: false }
|
|
351
|
+
);
|
|
352
|
+
return withArrivedItems(withPointedSections(phases, pointers, markdown));
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// src/plan-format/phase-tally.ts
|
|
356
|
+
var DONE_MARK = /^\s*[-*]\s\[x\]|\|\s*✅\s*\|/gim;
|
|
357
|
+
var OPEN_MARK = /^\s*[-*]\s\[ \]|\|\s*⬜\s*\|/gm;
|
|
358
|
+
var TRAILING_SEPARATOR = /[:—–-]\s*$/;
|
|
359
|
+
var PHASE_ID = /^(\d+[a-z]?)/;
|
|
360
|
+
var modelFor = (markdown, phaseWord, phaseTitle) => {
|
|
361
|
+
const prefix = new RegExp(`^${escapeRegExp(phaseWord)}\\s*`, "i");
|
|
362
|
+
const number = /^(\d+)/.exec(phaseTitle.replace(prefix, ""))?.[1];
|
|
363
|
+
if (!number) return void 0;
|
|
364
|
+
const row = new RegExp(`^\\|\\s*${number}\\s*[\u2014\u2013-][^|]*\\|\\s*([^|]+?)\\s*\\|`, "m").exec(markdown);
|
|
365
|
+
return row?.[1].trim();
|
|
366
|
+
};
|
|
367
|
+
var sectionOf = (section, heading, markdown, phaseWord) => {
|
|
368
|
+
const match = heading.exec(section);
|
|
369
|
+
if (!match) return null;
|
|
370
|
+
const title = match[1].replace(TRAILING_SEPARATOR, "").trim();
|
|
371
|
+
const phaseId = PHASE_ID.exec(title)?.[1]?.toLowerCase() ?? "";
|
|
372
|
+
const targets = section.split("\n").flatMap((line) => deferralTarget(line, phaseId) ?? []);
|
|
373
|
+
const done = (section.match(DONE_MARK) ?? []).length;
|
|
374
|
+
const open = (section.match(OPEN_MARK) ?? []).length - targets.length;
|
|
375
|
+
return { title, done, open, model: modelFor(markdown, phaseWord, title), phaseId, targets };
|
|
376
|
+
};
|
|
377
|
+
var tallyPhases = (markdown, language) => {
|
|
378
|
+
const phaseWord = PLAN_VOCABULARY[language].phase;
|
|
379
|
+
const heading = new RegExp(`^###?\\s+(?:${escapeRegExp(phaseWord)}\\s*)?(\\d+[^\\n]*)`);
|
|
380
|
+
const sections = markdown.split(/\n(?=###?\s)/).flatMap((section) => sectionOf(section, heading, markdown, phaseWord) ?? []);
|
|
381
|
+
const ids = new Set(sections.map(({ phaseId }) => phaseId));
|
|
382
|
+
return sections.flatMap(({ phaseId, targets, ...tally }) => {
|
|
383
|
+
const stranded = targets.filter((target) => !ids.has(target)).length;
|
|
384
|
+
const arriving = sections.flatMap((each) => each.targets).filter((target) => target === phaseId).length;
|
|
385
|
+
const open = tally.open + stranded + arriving;
|
|
386
|
+
return tally.done + open === 0 ? [] : [{ ...tally, open }];
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/plan-format/plan-structure.ts
|
|
391
|
+
var noGatePattern = (language) => new RegExp(`^\\s*\\*\\*${escapeRegExp(PLAN_VOCABULARY[language].noReviewGate)}\\b`, "i");
|
|
392
|
+
var tickCount = (phase) => phase.items.filter((item) => item.done).length;
|
|
393
|
+
var tasksDone = (phase) => {
|
|
394
|
+
const tasks = phase.items.filter((item) => !item.gate);
|
|
395
|
+
return tasks.length > 0 && tasks.every((item) => item.done);
|
|
396
|
+
};
|
|
397
|
+
var phaseSections = (markdown, language) => {
|
|
398
|
+
const heading = phaseHeadingPattern(language);
|
|
399
|
+
const noGate = noGatePattern(language);
|
|
400
|
+
const sections = [];
|
|
401
|
+
let open = null;
|
|
402
|
+
for (const [index, line] of markdown.split("\n").entries()) {
|
|
403
|
+
const match = line.match(heading);
|
|
404
|
+
if (match) {
|
|
405
|
+
open = {
|
|
406
|
+
line: index + 1,
|
|
407
|
+
id: match[1].toLowerCase(),
|
|
408
|
+
title: terminalSafe(match[2]).trim(),
|
|
409
|
+
tasks: false,
|
|
410
|
+
gate: false,
|
|
411
|
+
openLines: []
|
|
412
|
+
};
|
|
413
|
+
sections.push(open);
|
|
414
|
+
} else if (endsPhase(line)) {
|
|
415
|
+
open = null;
|
|
416
|
+
} else if (open) {
|
|
417
|
+
open.tasks ||= isCheckboxLine(line);
|
|
418
|
+
open.gate ||= isReviewGateLine(line) || noGate.test(line);
|
|
419
|
+
if (isOpenCheckboxLine(line) && deferralTarget(line, open.id) === null) open.openLines.push(index + 1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return sections;
|
|
423
|
+
};
|
|
424
|
+
var phaseIdsOfPlan = (markdown, language) => new Set(phaseSections(markdown, language).map(({ id }) => id));
|
|
425
|
+
var newPhasesWithoutReviewGate = (markdown, language, previousIds) => phaseSections(markdown, language).filter(({ id, tasks, gate }) => tasks && !gate && !previousIds.has(id)).map(({ line, id, title }) => ({ line, id, title }));
|
|
426
|
+
var hasLanesTable = (markdown, language) => sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes).some((line) => line.startsWith("|"));
|
|
427
|
+
var mergedPhasesWithOpenBoxes = (markdown, base, language) => {
|
|
428
|
+
const baseById = new Map(parsePlanPhases(base, language).map((phase) => [phase.id, phase]));
|
|
429
|
+
const currentById = new Map(parsePlanPhases(markdown, language).map((phase) => [phase.id, phase]));
|
|
430
|
+
const isMerged = (id) => {
|
|
431
|
+
const [basePhase, currentPhase2] = [baseById.get(id), currentById.get(id)];
|
|
432
|
+
return basePhase !== void 0 && currentPhase2 !== void 0 && tasksDone(basePhase) && tickCount(currentPhase2) <= tickCount(basePhase);
|
|
433
|
+
};
|
|
434
|
+
return phaseSections(markdown, language).filter(({ id, openLines }) => openLines.length > 0 && isMerged(id)).map(({ line, id, title, openLines }) => ({ line, id, title, openLines }));
|
|
435
|
+
};
|
|
436
|
+
var boardRowsWithoutPhase = (markdown, language, previousLines) => {
|
|
437
|
+
const start = sectionStart(markdown, PLAN_VOCABULARY[language].sections.lanes);
|
|
438
|
+
if (start === -1) return [];
|
|
439
|
+
const closed = new Set(
|
|
440
|
+
parseSprintLanes(markdown, language).filter((lane) => lane.closed).map((lane) => lane.name)
|
|
441
|
+
);
|
|
442
|
+
const rows = [];
|
|
443
|
+
let previous = null;
|
|
444
|
+
for (const { index, line, cells } of firstTableRows(
|
|
445
|
+
sectionLines(markdown, PLAN_VOCABULARY[language].sections.lanes)
|
|
446
|
+
)) {
|
|
447
|
+
const [name = "", , , phases = ""] = cells;
|
|
448
|
+
const lane = laneNameOf(name, previous);
|
|
449
|
+
previous = lane ?? previous;
|
|
450
|
+
const counts = lane !== null && !closed.has(lane) && !previousLines.has(line);
|
|
451
|
+
if (counts && phaseIdsOfCell(phases, language).length === 0) rows.push({ line: start + index + 1, lane, phases });
|
|
452
|
+
}
|
|
453
|
+
return rows;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// src/plan-format/progress.ts
|
|
457
|
+
var sumPhases = (phases) => ({
|
|
458
|
+
done: phases.reduce((sum, phase) => sum + phase.done, 0),
|
|
459
|
+
total: phases.reduce((sum, phase) => sum + phase.total, 0)
|
|
460
|
+
});
|
|
461
|
+
var baseId = (id) => id.replace(/[a-z]$/, "");
|
|
462
|
+
var findPhase = (phases, id) => phases.find((phase) => phase.id === id) ?? phases.find((phase) => phase.id === baseId(id));
|
|
463
|
+
var keptPhaseIds = (phases, ids) => {
|
|
464
|
+
const shown = new Set(ids.flatMap((id) => phases.filter((phase) => phase.id === id)));
|
|
465
|
+
return ids.filter((id) => {
|
|
466
|
+
const phase = findPhase(phases, id);
|
|
467
|
+
if (!phase || phase.id === id) return true;
|
|
468
|
+
if (shown.has(phase)) return false;
|
|
469
|
+
shown.add(phase);
|
|
470
|
+
return true;
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
var phaseProgress = (phases, id) => {
|
|
474
|
+
const phase = findPhase(phases, id);
|
|
475
|
+
const items = phase?.items ?? [];
|
|
476
|
+
return {
|
|
477
|
+
id,
|
|
478
|
+
title: phase?.title ?? null,
|
|
479
|
+
done: items.filter((item) => item.done).length,
|
|
480
|
+
total: items.length,
|
|
481
|
+
items,
|
|
482
|
+
...phase?.deferred ? { deferred: phase.deferred } : {},
|
|
483
|
+
...phase?.touched ? { touched: true } : {}
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
var isOpen = ({ done, total }) => done < total;
|
|
487
|
+
var isWritten = ({ total }) => total > 0;
|
|
488
|
+
var newestWrittenStepIndex = (steps) => steps.reduce((last, { phases }, index) => phases.some(isWritten) ? index : last, -1);
|
|
489
|
+
var newestRowIndex = (steps) => steps.reduce((last, { step }, index) => step.withoutRow ? last : index, -1);
|
|
490
|
+
var isTouched = (phase) => phase.touched === true && isWritten(phase);
|
|
491
|
+
var isTickedUnmerged = (phase) => isTouched(phase) && !isOpen(phase);
|
|
492
|
+
var newestStepWith = (steps, pick) => {
|
|
493
|
+
for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex--) {
|
|
494
|
+
const phase = pick(steps[stepIndex].phases);
|
|
495
|
+
if (phase) return { stepIndex, phaseId: phase.id };
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
};
|
|
499
|
+
var shownPhase = (phases, id) => phases.find((phase) => phase.id === id) ?? phases.find((phase) => phase.id === baseId(id));
|
|
500
|
+
var earlierPhase = (steps, before, id) => {
|
|
501
|
+
const planHref = steps[before].step.planHref;
|
|
502
|
+
for (let stepIndex = before - 1; stepIndex >= 0; stepIndex--) {
|
|
503
|
+
if (steps[stepIndex].step.planHref !== planHref) continue;
|
|
504
|
+
const phase = shownPhase(steps[stepIndex].phases, id);
|
|
505
|
+
if (phase) return { phase, current: { stepIndex, phaseId: phase.id } };
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
};
|
|
509
|
+
var rowPhases = (steps, stepIndex) => {
|
|
510
|
+
const row = steps[stepIndex];
|
|
511
|
+
if (!row) return [];
|
|
512
|
+
const own = row.phases.map((phase) => ({ phase, current: { stepIndex, phaseId: phase.id } }));
|
|
513
|
+
const named = row.step.namedPhaseIds;
|
|
514
|
+
if (!named) return own;
|
|
515
|
+
return named.flatMap((id) => {
|
|
516
|
+
const located = own.find((entry) => entry.phase.id === id) ?? earlierPhase(steps, stepIndex, id);
|
|
517
|
+
return located ? [located] : [];
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
var touchedOpenPhaseOf = (phases) => phases.find((phase) => isTouched(phase) && isOpen(phase));
|
|
521
|
+
var lastTickedUnmergedOf = (phases) => phases.filter(isTickedUnmerged).at(-1);
|
|
522
|
+
var newestRowPhase = (steps) => {
|
|
523
|
+
const located = rowPhases(steps, newestRowIndex(steps)).filter(({ phase: phase2 }) => isWritten(phase2));
|
|
524
|
+
const phases = located.map(({ phase: phase2 }) => phase2);
|
|
525
|
+
const phase = touchedOpenPhaseOf(phases) ?? lastTickedUnmergedOf(phases) ?? phases.find(isOpen);
|
|
526
|
+
return located.find((entry) => entry.phase === phase)?.current ?? null;
|
|
527
|
+
};
|
|
528
|
+
var phaseFromTicks = (steps) => {
|
|
529
|
+
const touched = newestStepWith(steps, touchedOpenPhaseOf) ?? newestStepWith(steps, lastTickedUnmergedOf);
|
|
530
|
+
if (touched) return touched;
|
|
531
|
+
const lastIndex = newestWrittenStepIndex(steps);
|
|
532
|
+
const phase = steps[lastIndex]?.phases.find(isOpen);
|
|
533
|
+
return phase ? { stepIndex: lastIndex, phaseId: phase.id } : null;
|
|
534
|
+
};
|
|
535
|
+
var currentPhaseOf = (steps) => newestRowPhase(steps) ?? phaseFromTicks(steps);
|
|
536
|
+
var laneProgress = (lane, plans) => {
|
|
537
|
+
const steps = lane.steps.map((step) => {
|
|
538
|
+
const phases = step.planHref ? plans[step.planHref] : void 0;
|
|
539
|
+
return { step, phases: phases ? keptPhaseIds(phases, step.phaseIds).map((id) => phaseProgress(phases, id)) : [] };
|
|
540
|
+
});
|
|
541
|
+
return { lane, steps, current: currentPhaseOf(steps), ...sumPhases(steps.flatMap((step) => step.phases)) };
|
|
542
|
+
};
|
|
543
|
+
var currentPhase = (progress) => {
|
|
544
|
+
if (!progress.current) return null;
|
|
545
|
+
const { stepIndex, phaseId } = progress.current;
|
|
546
|
+
return progress.steps[stepIndex].phases.find((phase) => phase.id === phaseId) ?? null;
|
|
547
|
+
};
|
|
548
|
+
var activeItem = (phase) => phase.items.find((item) => !item.done) ?? null;
|
|
549
|
+
var shortTitle = (progress) => {
|
|
550
|
+
const phase = currentPhase(progress);
|
|
551
|
+
if (phase) return `${PHASE_ID_PREFIX}${phase.id} \xB7 ${phase.done}/${phase.total}`;
|
|
552
|
+
return progress.total > 0 ? `\u2713 ${progress.done}/${progress.total}` : null;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// src/plan-format/sprint-document.ts
|
|
556
|
+
var PHASE_ID_IN_CELL = /^(\d+[a-z]?)\b/;
|
|
557
|
+
var paragraphs = (lines) => lines.join("\n").split(/\n\s*\n/).map((paragraph) => paragraph.trim()).filter(Boolean);
|
|
558
|
+
var PLAN_LINK2 = /\[([^\]]+)\]\(([^)\s]+)\)/;
|
|
559
|
+
var planRowOf = (cells) => {
|
|
560
|
+
const link = cells.map((cell) => PLAN_LINK2.exec(cell)).find((match) => match !== null);
|
|
561
|
+
return { cells, title: link?.[1] ?? null, href: link?.[2] ?? null };
|
|
562
|
+
};
|
|
563
|
+
var textBeforeFirstSection = (markdown) => {
|
|
564
|
+
const lines = markdown.split("\n");
|
|
565
|
+
const end = lines.findIndex((line) => /^##\s/.test(line));
|
|
566
|
+
const intro = (end === -1 ? lines : lines.slice(0, end)).filter((line) => !/^#\s/.test(line));
|
|
567
|
+
return paragraphs(intro).filter((paragraph) => !/^\*\*[^*]+:\*\*/.test(paragraph));
|
|
568
|
+
};
|
|
569
|
+
var readSprintDocument = (markdown, language) => {
|
|
570
|
+
const { goal, history, plans } = PLAN_VOCABULARY[language].sections;
|
|
571
|
+
const goalParagraphs = paragraphs(sectionLines(markdown, goal));
|
|
572
|
+
return {
|
|
573
|
+
planTables: allTables(sectionLines(markdown, plans)).map(({ caption, header, rows }) => ({
|
|
574
|
+
caption,
|
|
575
|
+
header,
|
|
576
|
+
rows: rows.map(planRowOf)
|
|
577
|
+
})),
|
|
578
|
+
goal: goalParagraphs.length > 0 ? goalParagraphs : textBeforeFirstSection(markdown),
|
|
579
|
+
history: firstTableWithHeader(sectionLines(markdown, history))
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
var readPhaseDependencies = (planMarkdown, language) => {
|
|
583
|
+
const { sections, columns } = PLAN_VOCABULARY[language];
|
|
584
|
+
const { header, rows } = firstTableWithHeader(subsectionLines(planMarkdown, sections.dependencies));
|
|
585
|
+
const column = header.indexOf(columns.dependsOn);
|
|
586
|
+
if (column === -1) return {};
|
|
587
|
+
const entries = rows.flatMap((cells) => {
|
|
588
|
+
const id = PHASE_ID_IN_CELL.exec(cells[0] ?? "")?.[1];
|
|
589
|
+
return id ? [[id, cells[column] ?? ""]] : [];
|
|
590
|
+
});
|
|
591
|
+
return Object.fromEntries(entries);
|
|
592
|
+
};
|
|
593
|
+
export {
|
|
594
|
+
LANGUAGE_NAME,
|
|
595
|
+
PHASE_ID_PREFIX,
|
|
596
|
+
PLAN_LANGUAGE,
|
|
597
|
+
PLAN_STATUS,
|
|
598
|
+
PLAN_VOCABULARY,
|
|
599
|
+
activeItem,
|
|
600
|
+
boardRowsWithoutPhase,
|
|
601
|
+
classifyStatus,
|
|
602
|
+
countDecisions,
|
|
603
|
+
currentPhase,
|
|
604
|
+
currentPhaseOf,
|
|
605
|
+
deferralTarget,
|
|
606
|
+
endsPhase,
|
|
607
|
+
findDrift,
|
|
608
|
+
hasLanesTable,
|
|
609
|
+
isCheckboxLine,
|
|
610
|
+
isOpenCheckboxLine,
|
|
611
|
+
isPlanLanguage,
|
|
612
|
+
isReviewGateLine,
|
|
613
|
+
isTickedUnmerged,
|
|
614
|
+
keptPhaseIds,
|
|
615
|
+
laneNameOf,
|
|
616
|
+
laneProgress,
|
|
617
|
+
mergedPhasesWithOpenBoxes,
|
|
618
|
+
misplacedDeferrals,
|
|
619
|
+
newPhasesWithoutReviewGate,
|
|
620
|
+
parsePlanPhases,
|
|
621
|
+
parseSprintLanes,
|
|
622
|
+
phaseHeading,
|
|
623
|
+
phaseHeadingPattern,
|
|
624
|
+
phaseIdsOfCell,
|
|
625
|
+
phaseIdsOfPlan,
|
|
626
|
+
readPhaseDependencies,
|
|
627
|
+
readPlanHeader,
|
|
628
|
+
readSprintDocument,
|
|
629
|
+
resolvePlanPath,
|
|
630
|
+
sectionHeading,
|
|
631
|
+
shortTitle,
|
|
632
|
+
sprintTitleOf,
|
|
633
|
+
sumPhases,
|
|
634
|
+
tallyPhases,
|
|
635
|
+
withoutRepeats
|
|
636
|
+
};
|
package/docs/lanes.mmd
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
coordinator[Coordinator<br/>/orchestrate] -- prompts --> laneA[Lane a<br/>worktree a]
|
|
3
|
+
coordinator -- open decisions,<br/>one approval per PR --> user([You])
|
|
4
|
+
coordinator -- prompts --> laneB[Lane b<br/>worktree b]
|
|
5
|
+
laneA -- questions as text --> coordinator
|
|
6
|
+
laneB -- questions as text --> coordinator
|
|
7
|
+
watcher[jh watch] -- wakes --> coordinator
|
|
8
|
+
laneA -. state, PR, checks .-> watcher
|
|
9
|
+
laneB -. state, PR, checks .-> watcher
|