beacon-framework 1.2.2__tar.gz → 1.3.1__tar.gz
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.
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/PKG-INFO +1 -1
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/pyproject.toml +5 -1
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/__init__.py +1 -1
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/cli.py +22 -7
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/doctor.py +41 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/manifest.py +6 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/roadmap.md +3 -1
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/roadmap.py +53 -19
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/.gitignore +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/BEACON.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/LICENSE +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/README.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/pragmatic-principles.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/__main__.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/bullet.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/docs.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/epic.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/installer.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/integrations/__init__.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/integrations/claude_code.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/integrations/release.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/language.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/beacon.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/help/commands.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/help/doctor.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/help/git-flow.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/help/phases.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/help/quickstart.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/CLAUDE.md.fragment +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/agents/beacon-reviewer.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/constitution.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/continue.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/implement.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/plan.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/prd.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/review.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/seed.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/specify.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/status.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/beacon/tasks.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/design/diagram.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/design/evaluate.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/design/wardley.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/git/feature.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/git/pr.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/git/release.gitflow.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/git/release.trunk.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/commands/init.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/settings.json +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/skills/beacon-build/SKILL.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/skills/beacon-design/SKILL.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/skills/beacon-seed/SKILL.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/claude_code/skills/beacon-ship/SKILL.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/PUBLISHING.gitflow.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/PUBLISHING.trunk.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/psr_config.gitflow.toml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/psr_config.trunk.toml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/workflows/release.gitflow.yml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/integrations/release/workflows/release.trunk.yml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/languages/node.toml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/languages/python.toml +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/.beacon/doctor.toml.example +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/ADRs/ADR-000-template.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/ADRs/README.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Background/00-problem-statement.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Background/01-final-architecture-document.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Roadmap/README.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Roadmap/archive/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Roadmap/epics/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Roadmap/epics/EPIC-TEMPLATE.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Roadmap/epics/archive/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Work/README.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Work/analysis/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Work/branches/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Work/planning/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/project-management/Work/sessions/.gitkeep +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/roadmap_template.html +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/README.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/explanation/index.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/how-to/index.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/reference/index.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/tutorials/getting-started.md +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/seed.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/upgrader.py +0 -0
- {beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: beacon-framework
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Light-touch, pragmatic, artifact-driven framework for AI-assisted software delivery
|
|
5
5
|
Project-URL: Repository, https://github.com/darth-veitcher/beacon
|
|
6
6
|
Author: darth-veitcher
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "beacon-framework"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.1"
|
|
8
8
|
description = "Light-touch, pragmatic, artifact-driven framework for AI-assisted software delivery"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -76,6 +76,8 @@ Repository = "https://github.com/darth-veitcher/beacon"
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
|
|
80
|
+
|
|
79
81
|
|
|
80
82
|
|
|
81
83
|
|
|
@@ -185,6 +187,8 @@ exclude_commit_patterns = ["^chore\\(release\\):"]
|
|
|
185
187
|
|
|
186
188
|
|
|
187
189
|
|
|
190
|
+
|
|
191
|
+
|
|
188
192
|
|
|
189
193
|
|
|
190
194
|
|
|
@@ -1271,8 +1271,8 @@ def roadmap_export(
|
|
|
1271
1271
|
stdout: bool = typer.Option(
|
|
1272
1272
|
False, "--stdout", help="Print to stdout instead of writing a file."
|
|
1273
1273
|
),
|
|
1274
|
-
|
|
1275
|
-
False, "--
|
|
1274
|
+
exclude_done: bool = typer.Option(
|
|
1275
|
+
False, "--exclude-done", help="Exclude Done/archived epics from the roadmap."
|
|
1276
1276
|
),
|
|
1277
1277
|
) -> None:
|
|
1278
1278
|
"""Generate ROADMAP.md — version-controlled Markdown + Mermaid source of truth.
|
|
@@ -1280,10 +1280,14 @@ def roadmap_export(
|
|
|
1280
1280
|
Renders epics, spec progress, ADRs, and active bullets into a Mermaid-enhanced
|
|
1281
1281
|
Markdown document. Safe to commit alongside beacon.md; regenerate on demand or
|
|
1282
1282
|
wire into CI with ``beacon roadmap export``.
|
|
1283
|
+
|
|
1284
|
+
Done/archived epics are included by default (up to ``roadmap_done_limit`` in
|
|
1285
|
+
your manifest, defaulting to 3). Pass ``--exclude-done`` to suppress them.
|
|
1283
1286
|
"""
|
|
1284
1287
|
from datetime import date
|
|
1285
1288
|
|
|
1286
1289
|
from beacon import roadmap as _roadmap
|
|
1290
|
+
from beacon.manifest import Manifest
|
|
1287
1291
|
|
|
1288
1292
|
root = _resolve_root(here, path)
|
|
1289
1293
|
if not (root / "project-management").is_dir():
|
|
@@ -1293,9 +1297,11 @@ def roadmap_export(
|
|
|
1293
1297
|
"[dim]beacon roadmap export /path/to/project[/dim]"
|
|
1294
1298
|
)
|
|
1295
1299
|
raise typer.Exit(1)
|
|
1300
|
+
_m = Manifest.load(root)
|
|
1301
|
+
done_limit = 0 if exclude_done else (_m.roadmap_done_limit if _m else 3)
|
|
1296
1302
|
today = date.today().isoformat()
|
|
1297
1303
|
data = _roadmap.collect(root)
|
|
1298
|
-
content = _roadmap.render_markdown(data, today,
|
|
1304
|
+
content = _roadmap.render_markdown(data, today, done_limit=done_limit)
|
|
1299
1305
|
if stdout:
|
|
1300
1306
|
console.print(content)
|
|
1301
1307
|
return
|
|
@@ -1303,13 +1309,13 @@ def roadmap_export(
|
|
|
1303
1309
|
dest.write_text(content, encoding="utf-8")
|
|
1304
1310
|
console.print(f"[green]✓[/green] Roadmap exported → [cyan]{dest.relative_to(root)}[/cyan]")
|
|
1305
1311
|
if not data.epics:
|
|
1306
|
-
if data.archived_epics and
|
|
1312
|
+
if data.archived_epics and exclude_done:
|
|
1307
1313
|
n = len(data.archived_epics)
|
|
1308
1314
|
plural = "s" if n > 1 else ""
|
|
1309
1315
|
console.print(
|
|
1310
1316
|
f"[dim] ({n} archived epic{plural}"
|
|
1311
|
-
" —
|
|
1312
|
-
"[/dim cyan][dim] to include completed work)[/dim]"
|
|
1317
|
+
" — remove [/dim][dim cyan]--exclude-done"
|
|
1318
|
+
"[/dim cyan][dim] to include recent completed work)[/dim]"
|
|
1313
1319
|
)
|
|
1314
1320
|
elif not data.archived_epics:
|
|
1315
1321
|
console.print(
|
|
@@ -1325,6 +1331,9 @@ def roadmap_render(
|
|
|
1325
1331
|
output: Path | None = typer.Option(
|
|
1326
1332
|
None, "--output", "-o", help="Output path (default: ROADMAP.html in project root)."
|
|
1327
1333
|
),
|
|
1334
|
+
exclude_done: bool = typer.Option(
|
|
1335
|
+
False, "--exclude-done", help="Exclude Done/archived epics from the visualization."
|
|
1336
|
+
),
|
|
1328
1337
|
) -> None:
|
|
1329
1338
|
"""Generate ROADMAP.html — Structural Breath interactive visualization (gitignored).
|
|
1330
1339
|
|
|
@@ -1332,10 +1341,14 @@ def roadmap_render(
|
|
|
1332
1341
|
cards, active-bullet feed, and ADR index — all styled with the Structural Breath
|
|
1333
1342
|
design system (dark, blueprint-grid, engineering-whitepaper aesthetic). Open
|
|
1334
1343
|
directly in a browser, screenshot into a slide deck, or host as a static page.
|
|
1344
|
+
|
|
1345
|
+
Done/archived epics are included by default (up to ``roadmap_done_limit`` in
|
|
1346
|
+
your manifest, defaulting to 3). Pass ``--exclude-done`` to suppress them.
|
|
1335
1347
|
"""
|
|
1336
1348
|
from datetime import date
|
|
1337
1349
|
|
|
1338
1350
|
from beacon import roadmap as _roadmap
|
|
1351
|
+
from beacon.manifest import Manifest
|
|
1339
1352
|
|
|
1340
1353
|
root = _resolve_root(here, path)
|
|
1341
1354
|
if not (root / "project-management").is_dir():
|
|
@@ -1345,9 +1358,11 @@ def roadmap_render(
|
|
|
1345
1358
|
"[dim]beacon roadmap render /path/to/project[/dim]"
|
|
1346
1359
|
)
|
|
1347
1360
|
raise typer.Exit(1)
|
|
1361
|
+
_m = Manifest.load(root)
|
|
1362
|
+
done_limit = 0 if exclude_done else (_m.roadmap_done_limit if _m else 3)
|
|
1348
1363
|
today = date.today().isoformat()
|
|
1349
1364
|
data = _roadmap.collect(root)
|
|
1350
|
-
content = _roadmap.render_html(data, today)
|
|
1365
|
+
content = _roadmap.render_html(data, today, done_limit=done_limit)
|
|
1351
1366
|
dest = output or (root / "ROADMAP.html")
|
|
1352
1367
|
dest.write_text(content, encoding="utf-8")
|
|
1353
1368
|
console.print(f"[green]✓[/green] Roadmap rendered → [cyan]{dest.relative_to(root)}[/cyan]")
|
|
@@ -901,6 +901,39 @@ def _check_epic_adr_coverage(root: Path, config: Config = DEFAULT_CONFIG) -> Che
|
|
|
901
901
|
)
|
|
902
902
|
|
|
903
903
|
|
|
904
|
+
def _check_epic_adr_broken_paths(root: Path) -> Check:
|
|
905
|
+
"""Every ADR entry in every epic's ## ADRs section must resolve to a real file.
|
|
906
|
+
|
|
907
|
+
Silently broken paths (typos, wrong relative paths, unrecognised markdown
|
|
908
|
+
link syntax) render as ``(file missing)`` in the DECISIONS tab without any
|
|
909
|
+
warning. This check surfaces them before a render is needed.
|
|
910
|
+
"""
|
|
911
|
+
from beacon.roadmap import _parse_adr # local import to avoid circular
|
|
912
|
+
|
|
913
|
+
all_epics = _epic.list_active(root) + _epic.list_archived(root)
|
|
914
|
+
broken: list[str] = []
|
|
915
|
+
for e in all_epics:
|
|
916
|
+
epic_dir = e.path.parent if e.path else None
|
|
917
|
+
for adr_path in e.adrs:
|
|
918
|
+
summary = _parse_adr(root, adr_path, epic_dir)
|
|
919
|
+
if summary.title == "(file missing)":
|
|
920
|
+
broken.append(f"{e.slug}: {adr_path!r}")
|
|
921
|
+
if broken:
|
|
922
|
+
head = "; ".join(broken[:3]) + ("…" if len(broken) > 3 else "")
|
|
923
|
+
return Check(
|
|
924
|
+
"epic-adr-broken-paths",
|
|
925
|
+
Status.WARN,
|
|
926
|
+
f"{len(broken)} ADR entry/entries resolve to missing file(s): {head}. "
|
|
927
|
+
"Use bare paths (e.g. `project-management/ADRs/ADR-001-foo.md`) or "
|
|
928
|
+
"markdown links with a correct relative href.",
|
|
929
|
+
)
|
|
930
|
+
return Check(
|
|
931
|
+
"epic-adr-broken-paths",
|
|
932
|
+
Status.OK,
|
|
933
|
+
"All ADR entries in epics resolve to existing files.",
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
|
|
904
937
|
ROADMAP_REVIEW_AGE = timedelta(days=90)
|
|
905
938
|
"""Roadmap is stale if `Last reviewed:` is older than this."""
|
|
906
939
|
|
|
@@ -1917,6 +1950,7 @@ CHECKS: tuple[tuple[str, object, bool], ...] = (
|
|
|
1917
1950
|
("epic-spec-lifecycle", _check_epic_spec_lifecycle, False),
|
|
1918
1951
|
("epic-rollup", _check_epic_rollup, False),
|
|
1919
1952
|
("epic-adr-coverage", _check_epic_adr_coverage, True),
|
|
1953
|
+
("epic-adr-broken-paths", _check_epic_adr_broken_paths, False),
|
|
1920
1954
|
("roadmap-staleness", _check_roadmap_staleness, True),
|
|
1921
1955
|
("publishing-mode", _check_publishing_mode, False),
|
|
1922
1956
|
("release-config-alignment", _check_release_config_alignment, False),
|
|
@@ -2074,6 +2108,13 @@ CHECK_DESCRIPTIONS: dict[str, str] = {
|
|
|
2074
2108
|
"probably went unrecorded — creating/editing an epic is a BEACON "
|
|
2075
2109
|
"DESIGN-phase activity that should produce ADRs."
|
|
2076
2110
|
),
|
|
2111
|
+
"epic-adr-broken-paths": (
|
|
2112
|
+
"Every ADR entry in every epic's `## ADRs` section must resolve to a "
|
|
2113
|
+
"file that exists on disk. WARN means one or more entries are broken — "
|
|
2114
|
+
"typo, wrong relative path, or unsupported markdown link syntax. "
|
|
2115
|
+
"Broken entries render silently as `(file missing)` in `beacon roadmap "
|
|
2116
|
+
"render` without this check."
|
|
2117
|
+
),
|
|
2077
2118
|
"roadmap-staleness": (
|
|
2078
2119
|
"`Roadmap/README.md` should carry a `Last reviewed: YYYY-MM-DD` "
|
|
2079
2120
|
"header newer than `roadmap_review_age_days` (default 90). WARN means "
|
|
@@ -50,6 +50,11 @@ class Manifest:
|
|
|
50
50
|
# `--no-speckit` / declining the offer sets this False; pre-PR installs
|
|
51
51
|
# without the field default False (no nag for users who never opted in).
|
|
52
52
|
speckit_requested: bool = False
|
|
53
|
+
# How many of the most-recently-completed (archived) epics to include in
|
|
54
|
+
# roadmap output by default. 0 = exclude Done epics entirely; any positive
|
|
55
|
+
# integer caps the count. Use ``--exclude-done`` on the CLI to override to
|
|
56
|
+
# 0 for a single run without touching this value.
|
|
57
|
+
roadmap_done_limit: int = 3
|
|
53
58
|
|
|
54
59
|
@classmethod
|
|
55
60
|
def fresh(cls) -> Manifest:
|
|
@@ -71,6 +76,7 @@ class Manifest:
|
|
|
71
76
|
hooks_enabled=bool(data.get("hooks_enabled", True)),
|
|
72
77
|
git_flow=str(data.get("git_flow", "trunk")),
|
|
73
78
|
speckit_requested=bool(data.get("speckit_requested", False)),
|
|
79
|
+
roadmap_done_limit=int(data.get("roadmap_done_limit", 3)),
|
|
74
80
|
)
|
|
75
81
|
|
|
76
82
|
def save(self, project_root: Path) -> Path:
|
|
@@ -17,6 +17,8 @@ Run:
|
|
|
17
17
|
beacon roadmap export
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
This includes the most-recently-completed epics by default (controlled by `roadmap_done_limit` in the manifest, defaulting to 3). Pass `--exclude-done` to suppress Done/archived epics.
|
|
21
|
+
|
|
20
22
|
If the user passed `render` as an argument (i.e. `$ARGUMENTS` contains the word `render`), also run:
|
|
21
23
|
```bash
|
|
22
24
|
beacon roadmap render
|
|
@@ -26,7 +28,7 @@ beacon roadmap render
|
|
|
26
28
|
|
|
27
29
|
Read the just-written `ROADMAP.md` and report concisely:
|
|
28
30
|
|
|
29
|
-
- **Epic counts** by status: Active / Planning / Paused / Done
|
|
31
|
+
- **Epic counts** by status: Active / Planning / Paused / Done (including any archived epics shown)
|
|
30
32
|
- **Fidelity** for each epic — show the badge (`S+/S? A+/A? T:N%`) and flag any that are `S?` (no specs) or `A?` (no ADRs)
|
|
31
33
|
- **In-flight bullets** — how many worktrees have active bullets and which epics they're attached to
|
|
32
34
|
- **Roadmap coverage** — fraction of Active epics that have both specs and ADRs filled in
|
|
@@ -33,6 +33,9 @@ _ADR_TITLE_RE = re.compile(r"^#\s*ADR-\d+[:\s]+(.+?)\s*$", re.MULTILINE)
|
|
|
33
33
|
# Valid ADR statuses
|
|
34
34
|
_ADR_STATUSES = {"Proposed", "Accepted", "Deprecated", "Superseded"}
|
|
35
35
|
|
|
36
|
+
# Markdown link href extractor: [text](href)
|
|
37
|
+
_MD_LINK_RE = re.compile(r"\[.*?\]\(([^)]+)\)")
|
|
38
|
+
|
|
36
39
|
|
|
37
40
|
# ---------------------------------------------------------------------------
|
|
38
41
|
# Dataclasses
|
|
@@ -176,15 +179,23 @@ def _fidelity_badge(f: EpicFidelity) -> str:
|
|
|
176
179
|
return f"{spec} {adr} {task}"
|
|
177
180
|
|
|
178
181
|
|
|
179
|
-
def _parse_adr(root: Path, adr_path: str) -> AdrSummary:
|
|
182
|
+
def _parse_adr(root: Path, adr_path: str, epic_dir: Path | None = None) -> AdrSummary:
|
|
180
183
|
"""Extract token, title, and status from an ADR file. Never returns None."""
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
# Extract href from markdown link syntax [text](href) if present
|
|
185
|
+
md_m = _MD_LINK_RE.search(adr_path)
|
|
186
|
+
raw = md_m.group(1) if md_m else adr_path
|
|
184
187
|
|
|
185
188
|
token_m = _ADR_TOKEN_RE.search(adr_path)
|
|
186
189
|
token = token_m.group(1).upper() if token_m else "ADR-???"
|
|
187
190
|
|
|
191
|
+
raw = raw.strip().rstrip("/")
|
|
192
|
+
if raw.startswith(("../", "./")):
|
|
193
|
+
# Relative path — resolve against the epic file's directory
|
|
194
|
+
base = epic_dir or (root / "project-management/Roadmap/epics")
|
|
195
|
+
full = (base / raw).resolve()
|
|
196
|
+
else:
|
|
197
|
+
full = root / raw.lstrip("./")
|
|
198
|
+
|
|
188
199
|
if not full.exists():
|
|
189
200
|
return AdrSummary(path=adr_path, token=token, title="(file missing)", status="Unknown")
|
|
190
201
|
|
|
@@ -208,6 +219,15 @@ def _parse_adr(root: Path, adr_path: str) -> AdrSummary:
|
|
|
208
219
|
return AdrSummary(path=adr_path, token=token, title=title, status=status)
|
|
209
220
|
|
|
210
221
|
|
|
222
|
+
def _select_done_epics(archived: list[Epic], done_limit: int) -> list[Epic]:
|
|
223
|
+
"""Return the N most-recently-started archived epics, sorted chronologically."""
|
|
224
|
+
if done_limit <= 0:
|
|
225
|
+
return []
|
|
226
|
+
by_recency = sorted(archived, key=lambda e: e.started or "0000-00-00", reverse=True)
|
|
227
|
+
selected = by_recency[:done_limit]
|
|
228
|
+
return sorted(selected, key=lambda e: e.started or "9999-99-99")
|
|
229
|
+
|
|
230
|
+
|
|
211
231
|
def _mermaid_gantt_src(
|
|
212
232
|
epics: list[Epic],
|
|
213
233
|
rollups: dict[str, Rollup],
|
|
@@ -290,12 +310,13 @@ def _markdown_status_table(
|
|
|
290
310
|
return "\n".join(rows)
|
|
291
311
|
|
|
292
312
|
|
|
293
|
-
def _to_json_data(data: RoadmapData, generated_at: str, *,
|
|
313
|
+
def _to_json_data(data: RoadmapData, generated_at: str, *, done_limit: int = 3) -> dict:
|
|
294
314
|
"""Serialise RoadmapData to the shape embedded in ROADMAP.html."""
|
|
295
|
-
|
|
315
|
+
done_epics = _select_done_epics(data.archived_epics, done_limit)
|
|
316
|
+
all_epics = data.epics + done_epics
|
|
296
317
|
epics_list = []
|
|
297
318
|
for e in all_epics:
|
|
298
|
-
if
|
|
319
|
+
if done_limit == 0 and e.status == "Done":
|
|
299
320
|
continue
|
|
300
321
|
roll = data.rollups.get(e.slug)
|
|
301
322
|
f = data.fidelity.get(e.slug)
|
|
@@ -354,7 +375,7 @@ def _to_json_data(data: RoadmapData, generated_at: str, *, include_done: bool =
|
|
|
354
375
|
# Build gantt_bars for D3
|
|
355
376
|
gantt_bars = []
|
|
356
377
|
for e in all_epics:
|
|
357
|
-
if
|
|
378
|
+
if done_limit == 0 and e.status == "Done":
|
|
358
379
|
continue
|
|
359
380
|
if not e.started:
|
|
360
381
|
continue
|
|
@@ -422,12 +443,13 @@ def collect(root: Path) -> RoadmapData:
|
|
|
422
443
|
adr_count=len(e.adrs),
|
|
423
444
|
)
|
|
424
445
|
|
|
446
|
+
epic_dir = e.path.parent if e.path else None
|
|
425
447
|
for adr_path in e.adrs:
|
|
426
448
|
m = _ADR_TOKEN_RE.search(adr_path)
|
|
427
449
|
if m:
|
|
428
450
|
tok = m.group(1).upper()
|
|
429
451
|
if tok not in seen_adrs:
|
|
430
|
-
seen_adrs[tok] = _parse_adr(root, adr_path)
|
|
452
|
+
seen_adrs[tok] = _parse_adr(root, adr_path, epic_dir)
|
|
431
453
|
|
|
432
454
|
adrs = sorted(seen_adrs.values(), key=lambda a: a.token)
|
|
433
455
|
|
|
@@ -452,7 +474,7 @@ def render_markdown(
|
|
|
452
474
|
data: RoadmapData,
|
|
453
475
|
generated_at: str,
|
|
454
476
|
*,
|
|
455
|
-
|
|
477
|
+
done_limit: int = 3,
|
|
456
478
|
) -> str:
|
|
457
479
|
"""Render ROADMAP.md content from collected data."""
|
|
458
480
|
lines: list[str] = []
|
|
@@ -465,7 +487,8 @@ def render_markdown(
|
|
|
465
487
|
lines.append(f"> {data.vision_excerpt}")
|
|
466
488
|
lines.append("")
|
|
467
489
|
|
|
468
|
-
|
|
490
|
+
done_epics = _select_done_epics(data.archived_epics, done_limit)
|
|
491
|
+
visible_epics = data.epics + done_epics
|
|
469
492
|
|
|
470
493
|
if not visible_epics:
|
|
471
494
|
if data.archived_epics:
|
|
@@ -473,7 +496,8 @@ def render_markdown(
|
|
|
473
496
|
plural = "s" if n > 1 else ""
|
|
474
497
|
lines.append(
|
|
475
498
|
f"_No active epics — {n} epic{plural} archived. "
|
|
476
|
-
|
|
499
|
+
"Remove `--exclude-done` or raise `roadmap_done_limit` in your manifest"
|
|
500
|
+
" to include completed work._"
|
|
477
501
|
)
|
|
478
502
|
else:
|
|
479
503
|
lines.append(
|
|
@@ -497,7 +521,7 @@ def render_markdown(
|
|
|
497
521
|
lines.append("")
|
|
498
522
|
lines.append("```mermaid")
|
|
499
523
|
lines.append(
|
|
500
|
-
_mermaid_gantt_src(visible_epics, data.rollups, generated_at, include_done=
|
|
524
|
+
_mermaid_gantt_src(visible_epics, data.rollups, generated_at, include_done=(done_limit > 0))
|
|
501
525
|
)
|
|
502
526
|
lines.append("```")
|
|
503
527
|
lines.append("")
|
|
@@ -545,7 +569,7 @@ def render_markdown(
|
|
|
545
569
|
return "\n".join(lines)
|
|
546
570
|
|
|
547
571
|
|
|
548
|
-
def render_html(data: RoadmapData, generated_at: str) -> str:
|
|
572
|
+
def render_html(data: RoadmapData, generated_at: str, *, done_limit: int = 3) -> str:
|
|
549
573
|
"""Render ROADMAP.html by injecting JSON into the Structural Breath template."""
|
|
550
574
|
from importlib import resources as _res
|
|
551
575
|
|
|
@@ -554,7 +578,7 @@ def render_html(data: RoadmapData, generated_at: str) -> str:
|
|
|
554
578
|
)
|
|
555
579
|
template = template_bytes.decode("utf-8")
|
|
556
580
|
|
|
557
|
-
json_data = _to_json_data(data, generated_at)
|
|
581
|
+
json_data = _to_json_data(data, generated_at, done_limit=done_limit)
|
|
558
582
|
# Use json.dumps with separators for compact embedding; escape </script> sequences
|
|
559
583
|
json_str = json.dumps(json_data, ensure_ascii=False).replace("</script>", "<\\/script>")
|
|
560
584
|
|
|
@@ -568,26 +592,36 @@ def generate_markdown(
|
|
|
568
592
|
root: Path,
|
|
569
593
|
output: Path | None = None,
|
|
570
594
|
*,
|
|
571
|
-
|
|
595
|
+
done_limit: int | None = None,
|
|
572
596
|
) -> Path:
|
|
573
597
|
"""Collect data, render, and write ROADMAP.md. Returns the written path."""
|
|
574
598
|
from datetime import date
|
|
575
599
|
|
|
600
|
+
from beacon.manifest import Manifest
|
|
601
|
+
|
|
602
|
+
if done_limit is None:
|
|
603
|
+
m = Manifest.load(root)
|
|
604
|
+
done_limit = m.roadmap_done_limit if m else 3
|
|
576
605
|
today = date.today().isoformat()
|
|
577
606
|
data = collect(root)
|
|
578
|
-
content = render_markdown(data, today,
|
|
607
|
+
content = render_markdown(data, today, done_limit=done_limit)
|
|
579
608
|
dest = output or (root / "ROADMAP.md")
|
|
580
609
|
dest.write_text(content, encoding="utf-8")
|
|
581
610
|
return dest
|
|
582
611
|
|
|
583
612
|
|
|
584
|
-
def generate_html(root: Path, output: Path | None = None) -> Path:
|
|
613
|
+
def generate_html(root: Path, output: Path | None = None, *, done_limit: int | None = None) -> Path:
|
|
585
614
|
"""Collect data, render, and write ROADMAP.html. Returns the written path."""
|
|
586
615
|
from datetime import date
|
|
587
616
|
|
|
617
|
+
from beacon.manifest import Manifest
|
|
618
|
+
|
|
619
|
+
if done_limit is None:
|
|
620
|
+
m = Manifest.load(root)
|
|
621
|
+
done_limit = m.roadmap_done_limit if m else 3
|
|
588
622
|
today = date.today().isoformat()
|
|
589
623
|
data = collect(root)
|
|
590
|
-
content = render_html(data, today)
|
|
624
|
+
content = render_html(data, today, done_limit=done_limit)
|
|
591
625
|
dest = output or (root / "ROADMAP.html")
|
|
592
626
|
dest.write_text(content, encoding="utf-8")
|
|
593
627
|
return dest
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/languages/python.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/roadmap_template.html
RENAMED
|
File without changes
|
{beacon_framework-1.2.2 → beacon_framework-1.3.1}/src/beacon/resources/user-facing-docs/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|