jekyll-theme-zer0 1.10.0 → 1.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +4 -4
- data/_data/backlog.yml +161 -0
- data/_includes/components/theme-preview-gallery.html +1 -1
- data/scripts/docs/check-freshness.sh +123 -0
- data/scripts/docs/check-links.sh +123 -0
- data/scripts/docs/lint-frontmatter.sh +197 -0
- data/scripts/docs/validate.sh +90 -0
- data/scripts/sync-backlog.rb +309 -0
- data/scripts/sync-backlog.sh +21 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98ca7799830fd5c793814a39656893d742627b0ce2800e32761ce53bc32b8263
|
|
4
|
+
data.tar.gz: 8b247e3c6002ec51bfc9c2c423f2cb531afa97f65c59b8c5526fe9251f70df22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f742ce6d82fdc8cb006f2604a8e8cb17927fd4991eea0817f69dceb9a8b4d1924e95c0f933dc50e5ea903b3abc81afb90cebd41ef26c942551c8561688a0ce03
|
|
7
|
+
data.tar.gz: ee9047c92ee91c57c357698f680c43de518c38db8c6b29c7133937009a5ec1ecfffd535f068449ef2969b449eeb91498dcaec57241b31980127bb32f6b76543d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.11.1] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Version bump: patch release
|
|
7
|
+
|
|
8
|
+
### Commits in this release
|
|
9
|
+
- d4a53d51 docs: consolidate, standardize, and add maintenance system (#112)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [1.11.0] - 2026-06-01
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Version bump: minor release
|
|
16
|
+
|
|
17
|
+
### Commits in this release
|
|
18
|
+
- 8a5ba7e2 feat(ci): add continuous-evolution backlog loop (#114)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **Continuous-evolution loop**: a self-sustaining backlog mechanism so AI agents can keep improving the repo between human sessions.
|
|
25
|
+
- `_data/backlog.yml` — tactical task queue (single source of truth), mirroring the `_data/roadmap.yml` pattern.
|
|
26
|
+
- `scripts/sync-backlog.rb` (+ `scripts/sync-backlog.sh`) — schema validator and GitHub Issues sync (idempotent via `<!-- backlog-id -->` markers).
|
|
27
|
+
- `.github/workflows/backlog-sync.yml` — syncs the backlog to issues on push to `main`; validates schema on PRs.
|
|
28
|
+
- `.github/workflows/auto-merge.yml` — enables native auto-merge for low-risk (`docs`/`deps`/`lint`) PRs once CI is green.
|
|
29
|
+
- `.github/prompts/repo-audit.prompt.md` (`/repo-audit`) and `.github/prompts/backlog-implement.prompt.md` (`/backlog-implement`) — the audit and implement routines.
|
|
30
|
+
- `.github/instructions/backlog.instructions.md` — file-scoped guidance for the backlog.
|
|
31
|
+
- `docs/systems/continuous-evolution.md` — full design, autonomy policy, and setup.
|
|
32
|
+
- `CLAUDE.md` — Claude Code pointer to `AGENTS.md` (per the documented convention).
|
|
3
33
|
## [1.10.0] - 2026-06-01
|
|
4
34
|
|
|
5
35
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: zer0-mistakes
|
|
3
3
|
sub-title: AI-Native Jekyll Theme
|
|
4
4
|
description: AI-native Jekyll theme for GitHub Pages — Docker-first development, AI-powered installation, multi-agent integration (Copilot, Codex, Cursor, Claude), AI preview-image generation, and AIEO content optimization with Bootstrap 5.3.
|
|
5
|
-
version: 1.
|
|
5
|
+
version: 1.11.1
|
|
6
6
|
layout: landing
|
|
7
7
|
tags:
|
|
8
8
|
- jekyll
|
|
@@ -20,7 +20,7 @@ categories:
|
|
|
20
20
|
- bootstrap
|
|
21
21
|
- ai-tooling
|
|
22
22
|
created: 2024-02-10T23:51:11.480Z
|
|
23
|
-
lastmod: 2026-06-
|
|
23
|
+
lastmod: 2026-06-01T13:28:14.000Z
|
|
24
24
|
draft: false
|
|
25
25
|
permalink: /
|
|
26
26
|
slug: zer0
|
|
@@ -901,7 +901,7 @@ git push origin feature/awesome-feature
|
|
|
901
901
|
|
|
902
902
|
| Metric | Value |
|
|
903
903
|
|--------|-------|
|
|
904
|
-
| **Current Version** | 1.
|
|
904
|
+
| **Current Version** | 1.11.1 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
|
|
905
905
|
| **Documented Features** | 43 ([Feature Registry](https://github.com/bamr87/zer0-mistakes/blob/main/_data/features.yml)) |
|
|
906
906
|
| **Setup Time** | 2-5 minutes ([install.sh benchmarks](https://github.com/bamr87/zer0-mistakes/blob/main/install.sh)) |
|
|
907
907
|
| **Documentation Pages** | 70+ ([browse docs](https://zer0-mistakes.com/pages/)) |
|
|
@@ -956,6 +956,6 @@ And these AI partners that make zer0-mistakes truly AI-native:
|
|
|
956
956
|
|
|
957
957
|
**Built with ❤️ — and a little help from our AI partners — for the Jekyll community**
|
|
958
958
|
|
|
959
|
-
**v1.
|
|
959
|
+
**v1.11.1** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md) • [AI Agent Guide](AGENTS.md)
|
|
960
960
|
|
|
961
961
|
|
data/_data/backlog.yml
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# zer0-mistakes Backlog — Tactical Task Queue (Single Source of Truth)
|
|
3
|
+
# =============================================================================
|
|
4
|
+
#
|
|
5
|
+
# This file is the canonical, machine-readable backlog of *tactical* tasks
|
|
6
|
+
# (the granular, pickup-able work items). It complements `_data/roadmap.yml`,
|
|
7
|
+
# which holds the *strategic* milestones.
|
|
8
|
+
#
|
|
9
|
+
# It powers the continuous-evolution loop:
|
|
10
|
+
#
|
|
11
|
+
# 1. The AUDIT routine (`.github/prompts/repo-audit.prompt.md`) appends new
|
|
12
|
+
# tasks here when it reviews the repo.
|
|
13
|
+
# 2. `scripts/sync-backlog.rb` (run by `.github/workflows/backlog-sync.yml`)
|
|
14
|
+
# mirrors each open task to a GitHub Issue and closes issues for tasks
|
|
15
|
+
# marked `done`.
|
|
16
|
+
# 3. The IMPLEMENT routine (`.github/prompts/backlog-implement.prompt.md`)
|
|
17
|
+
# picks the highest-priority open task, implements it, and opens a PR.
|
|
18
|
+
#
|
|
19
|
+
# See `docs/systems/continuous-evolution.md` for the full design.
|
|
20
|
+
#
|
|
21
|
+
# To add a task by hand:
|
|
22
|
+
# 1. Copy the schema block below; give it the next free `T-NNN` id.
|
|
23
|
+
# 2. Set status/priority/area/risk/effort/source and acceptance criteria.
|
|
24
|
+
# 3. Commit. The sync workflow creates the matching GitHub Issue on push.
|
|
25
|
+
#
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Schema
|
|
28
|
+
# =============================================================================
|
|
29
|
+
#
|
|
30
|
+
# meta:
|
|
31
|
+
# title: Display title
|
|
32
|
+
# updated: Last-reviewed date (YYYY-MM-DD)
|
|
33
|
+
# next_id: Next free task id number (the audit routine increments this)
|
|
34
|
+
#
|
|
35
|
+
# tasks:
|
|
36
|
+
# - id: Stable task id, "T-NNN" (never reused; matches the GitHub Issue)
|
|
37
|
+
# title: One line — becomes the GitHub Issue title
|
|
38
|
+
# status: open | in-progress | blocked | done
|
|
39
|
+
# priority: P0 (urgent) | P1 | P2 | P3 (nice-to-have)
|
|
40
|
+
# area: tests | docs | feat | infra | a11y | perf | deps | lint
|
|
41
|
+
# risk: low (auto-merge eligible) | standard (human review required)
|
|
42
|
+
# effort: S | M | L
|
|
43
|
+
# source: audit | roadmap | issue | user
|
|
44
|
+
# summary: 1–2 lines of context
|
|
45
|
+
# acceptance: List of checkable done-criteria the implement routine verifies
|
|
46
|
+
# links: { issue: <#|null>, pr: <#|null>, roadmap: "<version>|null" }
|
|
47
|
+
# created: YYYY-MM-DD
|
|
48
|
+
# updated: YYYY-MM-DD
|
|
49
|
+
#
|
|
50
|
+
# `risk: low` + area in {docs, deps, lint} makes a task auto-merge eligible once
|
|
51
|
+
# CI is green (see `.github/prompts/backlog-implement.prompt.md`). Everything
|
|
52
|
+
# else stays PR-only for human review.
|
|
53
|
+
#
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
meta:
|
|
57
|
+
title: "zer0-mistakes Backlog"
|
|
58
|
+
updated: 2026-05-31
|
|
59
|
+
next_id: 7
|
|
60
|
+
|
|
61
|
+
tasks:
|
|
62
|
+
# --- Housekeeping (seeded so the loop has work on day one) ------------------
|
|
63
|
+
|
|
64
|
+
- id: T-001
|
|
65
|
+
title: "Reconcile roadmap milestone numbering with the published gem version"
|
|
66
|
+
status: open
|
|
67
|
+
priority: P2
|
|
68
|
+
area: docs
|
|
69
|
+
risk: standard
|
|
70
|
+
effort: M
|
|
71
|
+
source: user
|
|
72
|
+
summary: >-
|
|
73
|
+
`_data/roadmap.yml` milestones run 0.17–1.0 while the gem ships as 1.9.9.
|
|
74
|
+
Decide on a single numbering scheme (or document the mapping) so the
|
|
75
|
+
roadmap, README, and RubyGems version no longer contradict each other.
|
|
76
|
+
acceptance:
|
|
77
|
+
- "Roadmap version line and gem version no longer contradict (either remapped or an explicit mapping note added to `_data/roadmap.yml` and `pages/roadmap.md`)."
|
|
78
|
+
- "`./scripts/generate-roadmap.sh --check` passes after the change."
|
|
79
|
+
- "No edit to `lib/jekyll-theme-zer0/version.rb`."
|
|
80
|
+
links: { issue: null, pr: null, roadmap: null }
|
|
81
|
+
created: 2026-05-31
|
|
82
|
+
updated: 2026-05-31
|
|
83
|
+
|
|
84
|
+
- id: T-002
|
|
85
|
+
title: "Refresh roadmap `updated:` date and review milestone statuses"
|
|
86
|
+
status: open
|
|
87
|
+
priority: P3
|
|
88
|
+
area: docs
|
|
89
|
+
risk: low
|
|
90
|
+
effort: S
|
|
91
|
+
source: user
|
|
92
|
+
summary: >-
|
|
93
|
+
`_data/roadmap.yml` `meta.updated` is 2026-04-18. Refresh it and confirm
|
|
94
|
+
the 0.22 "active" milestone still reflects reality.
|
|
95
|
+
acceptance:
|
|
96
|
+
- "`meta.updated` set to the current date."
|
|
97
|
+
- "Active/planned statuses reviewed against shipped work."
|
|
98
|
+
- "`./scripts/generate-roadmap.sh --check` passes."
|
|
99
|
+
links: { issue: null, pr: null, roadmap: null }
|
|
100
|
+
created: 2026-05-31
|
|
101
|
+
updated: 2026-05-31
|
|
102
|
+
|
|
103
|
+
- id: T-003
|
|
104
|
+
title: "Add GitHub issue templates and a pull-request template"
|
|
105
|
+
status: open
|
|
106
|
+
priority: P2
|
|
107
|
+
area: infra
|
|
108
|
+
risk: standard
|
|
109
|
+
effort: M
|
|
110
|
+
source: user
|
|
111
|
+
summary: >-
|
|
112
|
+
The repo has no `.github/ISSUE_TEMPLATE/` or PR template. Add a bug-report
|
|
113
|
+
and feature-request issue form plus a PR template that nudges contributors
|
|
114
|
+
toward conventional commits, CHANGELOG updates, and the test checklist.
|
|
115
|
+
acceptance:
|
|
116
|
+
- "`.github/ISSUE_TEMPLATE/bug_report.yml` and `feature_request.yml` exist and render."
|
|
117
|
+
- "`.github/pull_request_template.md` exists with a conventional-commit + CHANGELOG + tests checklist."
|
|
118
|
+
- "Agent-filed backlog issues remain compatible with the new templates (no broken automation)."
|
|
119
|
+
links: { issue: null, pr: null, roadmap: null }
|
|
120
|
+
created: 2026-05-31
|
|
121
|
+
updated: 2026-05-31
|
|
122
|
+
|
|
123
|
+
- id: T-004
|
|
124
|
+
title: "Docs-freshness sweep: reconcile docs/ ↔ pages/_docs/ and fix broken links"
|
|
125
|
+
status: open
|
|
126
|
+
priority: P2
|
|
127
|
+
area: docs
|
|
128
|
+
risk: low
|
|
129
|
+
effort: M
|
|
130
|
+
source: user
|
|
131
|
+
summary: >-
|
|
132
|
+
Run the markdown-link checker and audit the two-tier docs for drift,
|
|
133
|
+
stale dates/versions, and any user/technical pages that fell out of sync
|
|
134
|
+
after the recent alignment commit.
|
|
135
|
+
acceptance:
|
|
136
|
+
- "`markdown-link-check` (config in `.github/config/`) reports no broken internal links."
|
|
137
|
+
- "Any stale version/date references in `docs/` and `pages/_docs/` are corrected."
|
|
138
|
+
- "Cross-links between the user tier and technical tier resolve both ways."
|
|
139
|
+
links: { issue: null, pr: null, roadmap: null }
|
|
140
|
+
created: 2026-05-31
|
|
141
|
+
updated: 2026-05-31
|
|
142
|
+
|
|
143
|
+
- id: T-005
|
|
144
|
+
title: "Coverage baseline: identify the lowest-covered subsystems toward the v1.0 90% goal"
|
|
145
|
+
status: open
|
|
146
|
+
priority: P1
|
|
147
|
+
area: tests
|
|
148
|
+
risk: standard
|
|
149
|
+
effort: L
|
|
150
|
+
source: roadmap
|
|
151
|
+
summary: >-
|
|
152
|
+
The v1.0 milestone targets 90%+ automated test coverage. Produce a
|
|
153
|
+
coverage baseline and file follow-up tasks for the lowest-covered areas
|
|
154
|
+
(start with the modular installer and the Obsidian resolver paths).
|
|
155
|
+
acceptance:
|
|
156
|
+
- "A coverage baseline is recorded (test/coverage or a docs note)."
|
|
157
|
+
- "The 2–3 lowest-covered subsystems are identified and filed as new backlog tasks."
|
|
158
|
+
- "No reduction in existing passing tests (`./scripts/bin/test` stays green)."
|
|
159
|
+
links: { issue: null, pr: null, roadmap: "1.0" }
|
|
160
|
+
created: 2026-05-31
|
|
161
|
+
updated: 2026-05-31
|
|
@@ -442,7 +442,7 @@ docker-compose up
|
|
|
442
442
|
<div class="card-body d-flex flex-column gap-4">
|
|
443
443
|
<div>
|
|
444
444
|
<p class="small text-body-secondary mb-1">Breadcrumb</p>
|
|
445
|
-
<nav aria-label="
|
|
445
|
+
<nav aria-label="Breadcrumb example">
|
|
446
446
|
<ol class="breadcrumb mb-0">
|
|
447
447
|
<li class="breadcrumb-item"><a href="#">Home</a></li>
|
|
448
448
|
<li class="breadcrumb-item"><a href="#">Settings</a></li>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =========================================================================
|
|
3
|
+
# scripts/docs/check-freshness.sh — Staleness detector for docs/
|
|
4
|
+
# =========================================================================
|
|
5
|
+
# Flags docs where the lastmod front matter field is more than THRESHOLD
|
|
6
|
+
# days behind the file's most recent git commit. This surfaces docs that
|
|
7
|
+
# describe code that has since changed without a corresponding doc update.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ./scripts/docs/check-freshness.sh
|
|
11
|
+
# ./scripts/docs/check-freshness.sh --threshold 90 # custom days (default: 60)
|
|
12
|
+
# ./scripts/docs/check-freshness.sh --verbose
|
|
13
|
+
# =========================================================================
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
19
|
+
source "$SCRIPT_DIR/../lib/common.sh"
|
|
20
|
+
source "$SCRIPT_DIR/../lib/frontmatter.sh"
|
|
21
|
+
|
|
22
|
+
THRESHOLD_DAYS=60
|
|
23
|
+
STALE_COUNT=0
|
|
24
|
+
FILES_CHECKED=0
|
|
25
|
+
STALE_FILES=""
|
|
26
|
+
|
|
27
|
+
parse_args() {
|
|
28
|
+
while [[ $# -gt 0 ]]; do
|
|
29
|
+
case "$1" in
|
|
30
|
+
--threshold) shift; THRESHOLD_DAYS="$1" ;;
|
|
31
|
+
--verbose) export VERBOSE=true ;;
|
|
32
|
+
--help|-h) show_usage; exit 0 ;;
|
|
33
|
+
*) warn "Unknown option: $1" ;;
|
|
34
|
+
esac
|
|
35
|
+
shift
|
|
36
|
+
done
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
show_usage() {
|
|
40
|
+
cat << EOF
|
|
41
|
+
Freshness Checker for docs/
|
|
42
|
+
|
|
43
|
+
USAGE:
|
|
44
|
+
./scripts/docs/check-freshness.sh [OPTIONS]
|
|
45
|
+
|
|
46
|
+
OPTIONS:
|
|
47
|
+
--threshold N Days of drift before flagging as stale (default: $THRESHOLD_DAYS)
|
|
48
|
+
--verbose Show all files, not just stale ones
|
|
49
|
+
|
|
50
|
+
A doc is stale when:
|
|
51
|
+
git log -1 date for that file > (lastmod front matter + THRESHOLD days)
|
|
52
|
+
|
|
53
|
+
This catches docs describing code that changed but whose lastmod wasn't updated.
|
|
54
|
+
EOF
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Returns seconds since epoch for a date string, or empty on failure
|
|
58
|
+
date_to_epoch() {
|
|
59
|
+
local d="$1"
|
|
60
|
+
ruby -rtime -e "puts Time.parse('$d').to_i" 2>/dev/null || true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
parse_args "$@"
|
|
64
|
+
|
|
65
|
+
THRESHOLD_SECS=$(( THRESHOLD_DAYS * 86400 ))
|
|
66
|
+
|
|
67
|
+
log "Checking freshness of docs (threshold: ${THRESHOLD_DAYS} days)..."
|
|
68
|
+
echo ""
|
|
69
|
+
|
|
70
|
+
for filepath in $(find "$REPO_ROOT/docs" -name "*.md" -not -name "README.md" -not -path "*/archive/*" | sort); do
|
|
71
|
+
relpath="${filepath#$REPO_ROOT/}"
|
|
72
|
+
|
|
73
|
+
# Skip files without front matter
|
|
74
|
+
local_first=""
|
|
75
|
+
IFS= read -r local_first < "$filepath" || true
|
|
76
|
+
if [[ ! "$local_first" =~ ^--- ]]; then
|
|
77
|
+
debug " SKIP (no front matter): $relpath"
|
|
78
|
+
continue
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
lastmod_str="$(get_frontmatter_field "$filepath" "lastmod" 2>/dev/null || true)"
|
|
82
|
+
if [[ -z "$lastmod_str" ]]; then
|
|
83
|
+
debug " SKIP (no lastmod field): $relpath"
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
lastmod_epoch="$(date_to_epoch "$lastmod_str")"
|
|
88
|
+
[[ -z "$lastmod_epoch" ]] && continue
|
|
89
|
+
|
|
90
|
+
# Get last git commit date for this file
|
|
91
|
+
git_date_str="$(git -C "$REPO_ROOT" log -1 --format="%aI" -- "$relpath" 2>/dev/null || true)"
|
|
92
|
+
[[ -z "$git_date_str" ]] && continue
|
|
93
|
+
|
|
94
|
+
git_epoch="$(date_to_epoch "$git_date_str")"
|
|
95
|
+
[[ -z "$git_epoch" ]] && continue
|
|
96
|
+
|
|
97
|
+
drift=$(( git_epoch - lastmod_epoch ))
|
|
98
|
+
|
|
99
|
+
if [[ $drift -gt $THRESHOLD_SECS ]]; then
|
|
100
|
+
drift_days=$(( drift / 86400 ))
|
|
101
|
+
warn " STALE (${drift_days}d behind): $relpath"
|
|
102
|
+
warn " lastmod: $lastmod_str"
|
|
103
|
+
warn " git: $git_date_str"
|
|
104
|
+
STALE_FILES="$STALE_FILES $relpath"
|
|
105
|
+
STALE_COUNT=$((STALE_COUNT + 1))
|
|
106
|
+
else
|
|
107
|
+
debug " FRESH: $relpath"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
FILES_CHECKED=$((FILES_CHECKED + 1))
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
log "Results: $FILES_CHECKED files checked, $STALE_COUNT stale"
|
|
115
|
+
|
|
116
|
+
if [[ $STALE_COUNT -gt 0 ]]; then
|
|
117
|
+
echo ""
|
|
118
|
+
info "Stale files (update lastmod after reviewing content):"
|
|
119
|
+
for f in $STALE_FILES; do
|
|
120
|
+
echo " - $f"
|
|
121
|
+
done
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =========================================================================
|
|
3
|
+
# scripts/docs/check-links.sh — Internal link checker for docs/
|
|
4
|
+
# =========================================================================
|
|
5
|
+
# Validates that relative markdown links in docs/**/*.md resolve to files
|
|
6
|
+
# that actually exist in the repository. Does not validate external URLs.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./scripts/docs/check-links.sh
|
|
10
|
+
# ./scripts/docs/check-links.sh --verbose
|
|
11
|
+
# =========================================================================
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
17
|
+
source "$SCRIPT_DIR/../lib/common.sh"
|
|
18
|
+
|
|
19
|
+
ERRORS=0
|
|
20
|
+
FILES_CHECKED=0
|
|
21
|
+
|
|
22
|
+
parse_args() {
|
|
23
|
+
while [[ $# -gt 0 ]]; do
|
|
24
|
+
case "$1" in
|
|
25
|
+
--verbose) export VERBOSE=true ;;
|
|
26
|
+
--help|-h) show_usage; exit 0 ;;
|
|
27
|
+
*) warn "Unknown option: $1" ;;
|
|
28
|
+
esac
|
|
29
|
+
shift
|
|
30
|
+
done
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
show_usage() {
|
|
34
|
+
cat << 'EOF'
|
|
35
|
+
Internal Link Checker for docs/
|
|
36
|
+
|
|
37
|
+
USAGE:
|
|
38
|
+
./scripts/docs/check-links.sh [--verbose]
|
|
39
|
+
|
|
40
|
+
Checks relative markdown links ([text](path)) in docs/**/*.md to verify
|
|
41
|
+
the target file exists. The contributor docs under docs/ use filesystem
|
|
42
|
+
relative links between markdown files, so they can be resolved on disk.
|
|
43
|
+
|
|
44
|
+
Not covered here:
|
|
45
|
+
- pages/_docs/**: the user-facing Jekyll site uses permalink URLs and
|
|
46
|
+
the relative_url filter, which are not filesystem paths; those links
|
|
47
|
+
are validated by htmlproofer against the built _site in CI.
|
|
48
|
+
- External URLs (http://, https://, mailto:, etc.)
|
|
49
|
+
- Anchor-only links (#section)
|
|
50
|
+
- Absolute site URLs (/docs/..., /faq/, ...)
|
|
51
|
+
EOF
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Extract relative markdown links from a file, excluding external, anchor-only,
|
|
55
|
+
# and absolute site URLs. Uses Ruby (already required by this repo) so the
|
|
56
|
+
# extraction is portable across GNU and BSD/macOS environments.
|
|
57
|
+
extract_relative_links() {
|
|
58
|
+
local filepath="$1"
|
|
59
|
+
ruby -e '
|
|
60
|
+
in_fence = false
|
|
61
|
+
File.foreach(ARGV[0], encoding: "UTF-8") do |line|
|
|
62
|
+
# Toggle fenced code blocks (``` or ~~~); skip their contents,
|
|
63
|
+
# which are examples rather than real links.
|
|
64
|
+
if line =~ /\A\s*(```|~~~)/
|
|
65
|
+
in_fence = !in_fence
|
|
66
|
+
next
|
|
67
|
+
end
|
|
68
|
+
next if in_fence
|
|
69
|
+
# Ignore inline code spans so `[x](y)` written as code is skipped.
|
|
70
|
+
scrubbed = line.gsub(/`[^`]*`/, "")
|
|
71
|
+
scrubbed.scan(/\]\(([^)]+)\)/) do |m|
|
|
72
|
+
link = m[0].strip
|
|
73
|
+
next if link.empty?
|
|
74
|
+
next if link.include?("{{") || link.include?("{%") # Liquid template
|
|
75
|
+
next if link =~ %r{\A[a-z][a-z0-9+.-]*:}i # scheme: http:, mailto:, etc.
|
|
76
|
+
next if link.start_with?("#") # anchor-only
|
|
77
|
+
next if link.start_with?("/") # absolute site URL (Jekyll permalink)
|
|
78
|
+
link = link.split("#", 2).first # strip anchor fragment
|
|
79
|
+
puts link unless link.nil? || link.empty?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
' "$filepath" 2>/dev/null || true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
parse_args "$@"
|
|
86
|
+
|
|
87
|
+
log "Checking internal links in docs/..."
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
# archive/ holds superseded historical docs; templates/ holds scaffolding with
|
|
91
|
+
# intentional placeholder links (link-to-config, etc.) — neither is "live" docs.
|
|
92
|
+
for filepath in $(find "$REPO_ROOT/docs" -name "*.md" \
|
|
93
|
+
-not -path "*/archive/*" -not -path "*/templates/*" 2>/dev/null | sort); do
|
|
94
|
+
relpath="${filepath#$REPO_ROOT/}"
|
|
95
|
+
filedir="$(dirname "$filepath")"
|
|
96
|
+
file_errors=0
|
|
97
|
+
|
|
98
|
+
while IFS= read -r link; do
|
|
99
|
+
[[ -z "$link" ]] && continue
|
|
100
|
+
|
|
101
|
+
# Resolve relative to the file's directory (absolute site URLs are
|
|
102
|
+
# filtered out in extract_relative_links)
|
|
103
|
+
target="$filedir/$link"
|
|
104
|
+
|
|
105
|
+
# Normalize (remove ./ and resolve ..)
|
|
106
|
+
target="$(cd "$(dirname "$target")" 2>/dev/null && pwd)/$(basename "$target")" 2>/dev/null || target="$filedir/$link"
|
|
107
|
+
|
|
108
|
+
if [[ ! -f "$target" && ! -d "$target" ]]; then
|
|
109
|
+
warn " BROKEN: $relpath → $link"
|
|
110
|
+
file_errors=$((file_errors + 1))
|
|
111
|
+
ERRORS=$((ERRORS + 1))
|
|
112
|
+
else
|
|
113
|
+
debug " OK: $relpath → $link"
|
|
114
|
+
fi
|
|
115
|
+
done < <(extract_relative_links "$filepath")
|
|
116
|
+
|
|
117
|
+
FILES_CHECKED=$((FILES_CHECKED + 1))
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
echo ""
|
|
121
|
+
log "Results: $FILES_CHECKED files checked, $ERRORS broken links"
|
|
122
|
+
|
|
123
|
+
[[ $ERRORS -eq 0 ]] || exit 1
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =========================================================================
|
|
3
|
+
# scripts/docs/lint-frontmatter.sh — Front matter validator for docs/
|
|
4
|
+
# =========================================================================
|
|
5
|
+
# Checks every content .md file under docs/ (excluding READMEs and archive/)
|
|
6
|
+
# for the required front matter fields defined in the schema.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./scripts/docs/lint-frontmatter.sh # validate
|
|
10
|
+
# ./scripts/docs/lint-frontmatter.sh --fix # inject skeleton where missing
|
|
11
|
+
# ./scripts/docs/lint-frontmatter.sh --verbose # show per-file results
|
|
12
|
+
# =========================================================================
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
18
|
+
source "$SCRIPT_DIR/../lib/common.sh"
|
|
19
|
+
source "$SCRIPT_DIR/../lib/frontmatter.sh"
|
|
20
|
+
|
|
21
|
+
FIX_MODE=false
|
|
22
|
+
REQUIRED_FIELDS=(title description date lastmod categories tags author)
|
|
23
|
+
ERRORS=0
|
|
24
|
+
FILES_OK=0
|
|
25
|
+
FILES_MISSING=0
|
|
26
|
+
|
|
27
|
+
# Bash 3.2-compatible file-list builder (no mapfile)
|
|
28
|
+
list_docs_files() {
|
|
29
|
+
find "$REPO_ROOT/docs" -name "*.md" \
|
|
30
|
+
-not -name "README.md" \
|
|
31
|
+
-not -path "*/archive/*" \
|
|
32
|
+
| sort
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
parse_args() {
|
|
36
|
+
while [[ $# -gt 0 ]]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--fix) FIX_MODE=true ;;
|
|
39
|
+
--verbose) export VERBOSE=true ;;
|
|
40
|
+
--help|-h) show_usage; exit 0 ;;
|
|
41
|
+
*) warn "Unknown option: $1" ;;
|
|
42
|
+
esac
|
|
43
|
+
shift
|
|
44
|
+
done
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
show_usage() {
|
|
48
|
+
cat << 'EOF'
|
|
49
|
+
Front Matter Linter for docs/
|
|
50
|
+
|
|
51
|
+
USAGE:
|
|
52
|
+
./scripts/docs/lint-frontmatter.sh [OPTIONS]
|
|
53
|
+
|
|
54
|
+
REQUIRED FIELDS (per .github/instructions/documentation.instructions.md):
|
|
55
|
+
title, description, date, lastmod, categories, tags, author
|
|
56
|
+
|
|
57
|
+
EXCLUDES:
|
|
58
|
+
- README.md files (directory indexes)
|
|
59
|
+
- docs/archive/** (historical docs)
|
|
60
|
+
EOF
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Determine tags from directory name
|
|
64
|
+
dir_tags() {
|
|
65
|
+
local dir="$1"
|
|
66
|
+
case "$dir" in
|
|
67
|
+
ui) echo "[ui, styling, theme]" ;;
|
|
68
|
+
architecture) echo "[architecture, design]" ;;
|
|
69
|
+
development) echo "[development, contributing]" ;;
|
|
70
|
+
systems) echo "[systems, automation]" ;;
|
|
71
|
+
installation) echo "[installation, setup]" ;;
|
|
72
|
+
features) echo "[features]" ;;
|
|
73
|
+
implementation) echo "[implementation, changelog]" ;;
|
|
74
|
+
releases) echo "[releases]" ;;
|
|
75
|
+
templates) echo "[templates]" ;;
|
|
76
|
+
*) echo "[docs]" ;;
|
|
77
|
+
esac
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Inject skeleton front matter at the top of a file
|
|
81
|
+
inject_frontmatter() {
|
|
82
|
+
local filepath="$1"
|
|
83
|
+
local relpath="${filepath#$REPO_ROOT/}"
|
|
84
|
+
local subdir
|
|
85
|
+
subdir="$(echo "$relpath" | cut -d'/' -f2)"
|
|
86
|
+
|
|
87
|
+
# Extract title from first H1 heading
|
|
88
|
+
local title
|
|
89
|
+
title="$(grep -m1 "^# " "$filepath" 2>/dev/null | sed 's/^# //' || true)"
|
|
90
|
+
[[ -z "$title" ]] && title="$(basename "$filepath" .md | tr '-' ' ' | sed 's/\b./\u&/g')"
|
|
91
|
+
|
|
92
|
+
# Dates from git history
|
|
93
|
+
local date lastmod now
|
|
94
|
+
now="$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")"
|
|
95
|
+
date="$(git -C "$REPO_ROOT" log --diff-filter=A --follow --format="%aI" -- "$relpath" 2>/dev/null | tail -1 || true)"
|
|
96
|
+
lastmod="$(git -C "$REPO_ROOT" log -1 --format="%aI" -- "$relpath" 2>/dev/null || true)"
|
|
97
|
+
[[ -z "$date" ]] && date="$now"
|
|
98
|
+
[[ -z "$lastmod" ]] && lastmod="$now"
|
|
99
|
+
|
|
100
|
+
# Normalize to ISO 8601 with milliseconds
|
|
101
|
+
date="$(echo "$date" | ruby -rtime -e 'puts Time.parse(STDIN.read.strip).utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")' 2>/dev/null || echo "$now")"
|
|
102
|
+
lastmod="$(echo "$lastmod" | ruby -rtime -e 'puts Time.parse(STDIN.read.strip).utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")' 2>/dev/null || echo "$now")"
|
|
103
|
+
|
|
104
|
+
local tags
|
|
105
|
+
tags="$(dir_tags "$subdir")"
|
|
106
|
+
|
|
107
|
+
local skeleton
|
|
108
|
+
skeleton="---
|
|
109
|
+
title: \"${title}\"
|
|
110
|
+
description: \"TODO: Add a 120-160 character description of this document.\"
|
|
111
|
+
date: ${date}
|
|
112
|
+
lastmod: ${lastmod}
|
|
113
|
+
categories: [docs]
|
|
114
|
+
tags: ${tags}
|
|
115
|
+
author: bamr87
|
|
116
|
+
---"
|
|
117
|
+
|
|
118
|
+
# Prepend skeleton to file
|
|
119
|
+
local tmpfile
|
|
120
|
+
tmpfile="$(mktemp)"
|
|
121
|
+
printf '%s\n\n' "$skeleton" | cat - "$filepath" > "$tmpfile"
|
|
122
|
+
mv "$tmpfile" "$filepath"
|
|
123
|
+
info " Injected front matter: $relpath"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
parse_args "$@"
|
|
127
|
+
|
|
128
|
+
total_files=0
|
|
129
|
+
for f in $(list_docs_files); do total_files=$((total_files + 1)); done
|
|
130
|
+
log "Checking $total_files docs files for required front matter..."
|
|
131
|
+
echo ""
|
|
132
|
+
|
|
133
|
+
for filepath in $(list_docs_files); do
|
|
134
|
+
relpath="${filepath#$REPO_ROOT/}"
|
|
135
|
+
missing=()
|
|
136
|
+
|
|
137
|
+
# Check if front matter exists at all
|
|
138
|
+
local_first_line=""
|
|
139
|
+
IFS= read -r local_first_line < "$filepath" || true
|
|
140
|
+
|
|
141
|
+
if [[ ! "$local_first_line" =~ ^--- ]]; then
|
|
142
|
+
if [[ "$FIX_MODE" == "true" ]]; then
|
|
143
|
+
inject_frontmatter "$filepath"
|
|
144
|
+
FILES_OK=$((FILES_OK + 1))
|
|
145
|
+
continue
|
|
146
|
+
else
|
|
147
|
+
warn " MISSING front matter: $relpath"
|
|
148
|
+
FILES_MISSING=$((FILES_MISSING + 1))
|
|
149
|
+
ERRORS=$((ERRORS + 1))
|
|
150
|
+
continue
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# Check each required field
|
|
155
|
+
fm="$(extract_frontmatter "$filepath" 2>/dev/null || true)"
|
|
156
|
+
missing_list=""
|
|
157
|
+
for field in "${REQUIRED_FIELDS[@]}"; do
|
|
158
|
+
if ! echo "$fm" | grep -q "^${field}:"; then
|
|
159
|
+
missing_list="$missing_list $field"
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
# Description quality: presence alone is not enough. Reject the --fix
|
|
164
|
+
# skeleton placeholder and obvious stubs so a "compliant" run can't ship
|
|
165
|
+
# docs with a TODO description. Schema target is a 120-160 char sentence.
|
|
166
|
+
desc_problem=""
|
|
167
|
+
if echo "$fm" | grep -q "^description:"; then
|
|
168
|
+
desc_value="$(echo "$fm" | grep -m1 "^description:" | sed -E 's/^description:[[:space:]]*//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/')"
|
|
169
|
+
if echo "$desc_value" | grep -qi "TODO"; then
|
|
170
|
+
desc_problem="placeholder TODO description"
|
|
171
|
+
elif [[ "${#desc_value}" -lt 40 ]]; then
|
|
172
|
+
desc_problem="description too short (${#desc_value} chars; aim for 120-160)"
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if [[ -n "$missing_list" || -n "$desc_problem" ]]; then
|
|
177
|
+
local_msg=" INCOMPLETE ($relpath):"
|
|
178
|
+
[[ -n "$missing_list" ]] && local_msg="$local_msg missing:$missing_list"
|
|
179
|
+
[[ -n "$desc_problem" ]] && local_msg="$local_msg $desc_problem"
|
|
180
|
+
warn "$local_msg"
|
|
181
|
+
ERRORS=$((ERRORS + 1))
|
|
182
|
+
else
|
|
183
|
+
debug " OK: $relpath"
|
|
184
|
+
FILES_OK=$((FILES_OK + 1))
|
|
185
|
+
fi
|
|
186
|
+
done
|
|
187
|
+
|
|
188
|
+
incomplete=$((ERRORS - FILES_MISSING))
|
|
189
|
+
echo ""
|
|
190
|
+
log "Results: ${FILES_OK} OK, ${FILES_MISSING} missing front matter, ${incomplete} incomplete"
|
|
191
|
+
|
|
192
|
+
if [[ $ERRORS -gt 0 ]]; then
|
|
193
|
+
if [[ "$FIX_MODE" == "false" ]]; then
|
|
194
|
+
info "Run with --fix to inject skeleton front matter into missing files."
|
|
195
|
+
fi
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =========================================================================
|
|
3
|
+
# scripts/docs/validate.sh — Docs validation orchestrator
|
|
4
|
+
# =========================================================================
|
|
5
|
+
# Runs all docs health checks: front matter, links, and freshness.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ./scripts/docs/validate.sh # run all checks
|
|
9
|
+
# ./scripts/docs/validate.sh --lint # front matter only
|
|
10
|
+
# ./scripts/docs/validate.sh --links # links only
|
|
11
|
+
# ./scripts/docs/validate.sh --freshness # freshness only
|
|
12
|
+
# ./scripts/docs/validate.sh --fix # inject missing front matter
|
|
13
|
+
# ./scripts/docs/validate.sh --verbose # detailed output
|
|
14
|
+
# =========================================================================
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
source "$SCRIPT_DIR/../lib/common.sh"
|
|
20
|
+
|
|
21
|
+
RUN_LINT=true
|
|
22
|
+
RUN_LINKS=true
|
|
23
|
+
RUN_FRESHNESS=true
|
|
24
|
+
FIX_MODE=false
|
|
25
|
+
OVERALL_STATUS=0
|
|
26
|
+
|
|
27
|
+
parse_args() {
|
|
28
|
+
while [[ $# -gt 0 ]]; do
|
|
29
|
+
case "$1" in
|
|
30
|
+
--lint) RUN_LINKS=false; RUN_FRESHNESS=false ;;
|
|
31
|
+
--links) RUN_LINT=false; RUN_FRESHNESS=false ;;
|
|
32
|
+
--freshness) RUN_LINT=false; RUN_LINKS=false ;;
|
|
33
|
+
--fix) FIX_MODE=true ;;
|
|
34
|
+
--verbose) export VERBOSE=true ;;
|
|
35
|
+
--help|-h) show_usage; exit 0 ;;
|
|
36
|
+
*) warn "Unknown option: $1" ;;
|
|
37
|
+
esac
|
|
38
|
+
shift
|
|
39
|
+
done
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
show_usage() {
|
|
43
|
+
cat << 'EOF'
|
|
44
|
+
Docs Validation Suite for zer0-mistakes
|
|
45
|
+
|
|
46
|
+
USAGE:
|
|
47
|
+
./scripts/docs/validate.sh [OPTIONS]
|
|
48
|
+
|
|
49
|
+
OPTIONS:
|
|
50
|
+
--lint Front matter compliance only
|
|
51
|
+
--links Link checking only
|
|
52
|
+
--freshness Staleness check only
|
|
53
|
+
--fix Inject skeleton front matter into files that lack it
|
|
54
|
+
--verbose Detailed output
|
|
55
|
+
--help Show this message
|
|
56
|
+
|
|
57
|
+
CHECKS:
|
|
58
|
+
lint Every docs/**/*.md (non-README, non-archive) has required
|
|
59
|
+
front matter: title, description, date, lastmod, categories,
|
|
60
|
+
tags, author
|
|
61
|
+
links Internal markdown links resolve to existing files
|
|
62
|
+
freshness Files where lastmod trails the last git commit by > 60 days
|
|
63
|
+
EOF
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
run_check() {
|
|
67
|
+
local name="$1"; local script="$2"; shift 2
|
|
68
|
+
log "Running: $name"
|
|
69
|
+
if "$script" "$@"; then
|
|
70
|
+
log " PASS: $name"
|
|
71
|
+
else
|
|
72
|
+
warn " FAIL: $name"
|
|
73
|
+
OVERALL_STATUS=1
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parse_args "$@"
|
|
78
|
+
|
|
79
|
+
[[ "$FIX_MODE" == "true" ]] && EXTRA="--fix" || EXTRA=""
|
|
80
|
+
|
|
81
|
+
[[ "$RUN_LINT" == "true" ]] && run_check "Front matter lint" "$SCRIPT_DIR/lint-frontmatter.sh" $EXTRA
|
|
82
|
+
[[ "$RUN_LINKS" == "true" ]] && run_check "Link check" "$SCRIPT_DIR/check-links.sh"
|
|
83
|
+
[[ "$RUN_FRESHNESS" == "true" ]] && run_check "Freshness check" "$SCRIPT_DIR/check-freshness.sh"
|
|
84
|
+
|
|
85
|
+
if [[ $OVERALL_STATUS -eq 0 ]]; then
|
|
86
|
+
log "All docs checks passed."
|
|
87
|
+
else
|
|
88
|
+
error "One or more docs checks failed."
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# sync-backlog.rb
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Mirrors `_data/backlog.yml` (the tactical task queue) to GitHub Issues.
|
|
9
|
+
#
|
|
10
|
+
# - Each task with status open|in-progress|blocked -> an OPEN issue.
|
|
11
|
+
# - Each task with status done -> its issue is CLOSED.
|
|
12
|
+
#
|
|
13
|
+
# Issues are matched back to tasks idempotently by a hidden marker embedded in
|
|
14
|
+
# the issue body: `<!-- backlog-id: T-001 -->`. Re-running the sync updates the
|
|
15
|
+
# title/body/labels in place rather than creating duplicates.
|
|
16
|
+
#
|
|
17
|
+
# Managed labels (created with `gh label create --force` if missing):
|
|
18
|
+
# agent-ready · priority:P0..P3 · area:<area> · risk:<low|standard> · agent-hold
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# ruby scripts/sync-backlog.rb # create/update/close issues via `gh`
|
|
22
|
+
# ruby scripts/sync-backlog.rb --check # validate schema only (no `gh`, CI/PR gate)
|
|
23
|
+
# ruby scripts/sync-backlog.rb --dry-run # print intended `gh` calls, make no changes
|
|
24
|
+
#
|
|
25
|
+
# Requires the `gh` CLI authenticated with `issues: write` for the write path.
|
|
26
|
+
# `--check` needs only Ruby stdlib (used as the pull-request gate).
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
require 'yaml'
|
|
30
|
+
require 'date'
|
|
31
|
+
require 'json'
|
|
32
|
+
require 'optparse'
|
|
33
|
+
require 'open3'
|
|
34
|
+
require 'shellwords'
|
|
35
|
+
|
|
36
|
+
ROOT = File.expand_path('..', __dir__)
|
|
37
|
+
DATA_FILE = File.join(ROOT, '_data', 'backlog.yml')
|
|
38
|
+
|
|
39
|
+
VALID_STATUS = %w[open in-progress blocked done].freeze
|
|
40
|
+
VALID_PRIORITY = %w[P0 P1 P2 P3].freeze
|
|
41
|
+
VALID_AREA = %w[tests docs feat infra a11y perf deps lint].freeze
|
|
42
|
+
VALID_RISK = %w[low standard].freeze
|
|
43
|
+
VALID_EFFORT = %w[S M L].freeze
|
|
44
|
+
VALID_SOURCE = %w[audit roadmap issue user].freeze
|
|
45
|
+
|
|
46
|
+
OPEN_STATUSES = %w[open in-progress blocked].freeze
|
|
47
|
+
|
|
48
|
+
# Labels this script owns. On each sync we reconcile a task's labels to exactly
|
|
49
|
+
# the managed set it should carry, leaving any human-applied labels untouched.
|
|
50
|
+
def managed_labels(task)
|
|
51
|
+
labels = ['agent-ready', "priority:#{task['priority']}", "area:#{task['area']}", "risk:#{task['risk']}"]
|
|
52
|
+
labels << 'agent-hold' if task['status'] == 'blocked'
|
|
53
|
+
labels
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
ALL_MANAGED_LABELS = (
|
|
57
|
+
['agent-ready', 'agent-hold'] +
|
|
58
|
+
VALID_PRIORITY.map { |p| "priority:#{p}" } +
|
|
59
|
+
VALID_AREA.map { |a| "area:#{a}" } +
|
|
60
|
+
VALID_RISK.map { |r| "risk:#{r}" }
|
|
61
|
+
).freeze
|
|
62
|
+
|
|
63
|
+
LABEL_COLORS = {
|
|
64
|
+
'agent-ready' => '0e8a16',
|
|
65
|
+
'agent-hold' => 'b60205'
|
|
66
|
+
}.freeze
|
|
67
|
+
PRIORITY_COLOR = 'd93f0b'
|
|
68
|
+
AREA_COLOR = '1d76db'
|
|
69
|
+
RISK_COLOR = 'fbca04'
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Load + validate
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def load_data
|
|
76
|
+
# Mirror generate-roadmap.rb: permit Date/Time, and fall back for the older
|
|
77
|
+
# macOS system Ruby (2.6) whose safe loader signature differs.
|
|
78
|
+
begin
|
|
79
|
+
YAML.load_file(DATA_FILE, permitted_classes: [Date, Time])
|
|
80
|
+
rescue ArgumentError
|
|
81
|
+
YAML.safe_load(File.read(DATA_FILE), permitted_classes: [Date, Time], aliases: false)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate(data)
|
|
86
|
+
errors = []
|
|
87
|
+
errors << 'Missing top-level `meta:` mapping.' unless data.is_a?(Hash) && data['meta'].is_a?(Hash)
|
|
88
|
+
tasks = data.is_a?(Hash) ? data['tasks'] : nil
|
|
89
|
+
return ['Missing or empty `tasks:` list.'] unless tasks.is_a?(Array) && !tasks.empty?
|
|
90
|
+
|
|
91
|
+
seen_ids = {}
|
|
92
|
+
tasks.each_with_index do |task, i|
|
|
93
|
+
where = "tasks[#{i}]"
|
|
94
|
+
unless task.is_a?(Hash)
|
|
95
|
+
errors << "#{where}: each task must be a mapping."
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
id = task['id']
|
|
99
|
+
where = id ? "task #{id}" : where
|
|
100
|
+
errors << "#{where}: missing `id`." if id.to_s.empty?
|
|
101
|
+
errors << "#{where}: `id` must match T-NNN (got #{id.inspect})." if id && id !~ /\AT-\d{3,}\z/
|
|
102
|
+
if id && seen_ids[id]
|
|
103
|
+
errors << "#{where}: duplicate id #{id} (also at #{seen_ids[id]})."
|
|
104
|
+
elsif id
|
|
105
|
+
seen_ids[id] = where
|
|
106
|
+
end
|
|
107
|
+
errors << "#{where}: missing `title`." if task['title'].to_s.strip.empty?
|
|
108
|
+
check_enum(errors, where, task, 'status', VALID_STATUS)
|
|
109
|
+
check_enum(errors, where, task, 'priority', VALID_PRIORITY)
|
|
110
|
+
check_enum(errors, where, task, 'area', VALID_AREA)
|
|
111
|
+
check_enum(errors, where, task, 'risk', VALID_RISK)
|
|
112
|
+
check_enum(errors, where, task, 'effort', VALID_EFFORT) if task['effort']
|
|
113
|
+
check_enum(errors, where, task, 'source', VALID_SOURCE) if task['source']
|
|
114
|
+
unless task['acceptance'].is_a?(Array) && !task['acceptance'].empty?
|
|
115
|
+
errors << "#{where}: `acceptance` must be a non-empty list."
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
errors
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_enum(errors, where, task, field, allowed)
|
|
122
|
+
value = task[field]
|
|
123
|
+
return if allowed.include?(value)
|
|
124
|
+
|
|
125
|
+
errors << "#{where}: `#{field}` must be one of #{allowed.join('|')} (got #{value.inspect})."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Issue body rendering
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def marker(id)
|
|
133
|
+
"<!-- backlog-id: #{id} -->"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_body(task)
|
|
137
|
+
accept = (task['acceptance'] || []).map { |a| "- [ ] #{a}" }.join("\n")
|
|
138
|
+
roadmap = task.dig('links', 'roadmap')
|
|
139
|
+
meta_row = [
|
|
140
|
+
"**Priority:** #{task['priority']}",
|
|
141
|
+
"**Area:** #{task['area']}",
|
|
142
|
+
"**Risk:** #{task['risk']}",
|
|
143
|
+
"**Effort:** #{task['effort']}",
|
|
144
|
+
"**Source:** #{task['source']}"
|
|
145
|
+
].join(' · ')
|
|
146
|
+
|
|
147
|
+
<<~BODY.strip
|
|
148
|
+
#{marker(task['id'])}
|
|
149
|
+
> Auto-managed from [`_data/backlog.yml`](../blob/main/_data/backlog.yml) by `scripts/sync-backlog.rb`.
|
|
150
|
+
> Edit the backlog file, not this issue body — changes here are overwritten on the next sync.
|
|
151
|
+
|
|
152
|
+
#{meta_row}#{roadmap ? " · **Roadmap:** v#{roadmap}" : ''}
|
|
153
|
+
|
|
154
|
+
#{task['summary'].to_s.strip}
|
|
155
|
+
|
|
156
|
+
## Acceptance criteria
|
|
157
|
+
|
|
158
|
+
#{accept}
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
Picked up by the IMPLEMENT routine (`.github/prompts/backlog-implement.prompt.md`).
|
|
162
|
+
See [`docs/systems/continuous-evolution.md`](../blob/main/docs/systems/continuous-evolution.md).
|
|
163
|
+
BODY
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
# gh helpers
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
class Gh
|
|
171
|
+
def initialize(dry_run:)
|
|
172
|
+
@dry_run = dry_run
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Read-only call. Always executed (even in dry-run) so we can compute a diff;
|
|
176
|
+
# degrades to a default value if `gh` is unavailable/unauthenticated.
|
|
177
|
+
def read(args, default:)
|
|
178
|
+
out, _err, status = Open3.capture3('gh', *args)
|
|
179
|
+
return default unless status.success?
|
|
180
|
+
|
|
181
|
+
out
|
|
182
|
+
rescue Errno::ENOENT
|
|
183
|
+
default
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Mutating call. Printed (not executed) in dry-run mode.
|
|
187
|
+
def write(args)
|
|
188
|
+
if @dry_run
|
|
189
|
+
puts "DRY-RUN gh #{args.map { |a| a.to_s.include?(' ') ? a.inspect : a }.join(' ')}"
|
|
190
|
+
return true
|
|
191
|
+
end
|
|
192
|
+
_out, err, status = Open3.capture3('gh', *args)
|
|
193
|
+
warn "gh #{args.first} failed: #{err.strip}" unless status.success?
|
|
194
|
+
status.success?
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def ensure_labels(gh)
|
|
199
|
+
LABEL_COLORS.each { |name, color| gh.write(['label', 'create', name, '--color', color, '--force']) }
|
|
200
|
+
VALID_PRIORITY.each { |p| gh.write(['label', 'create', "priority:#{p}", '--color', PRIORITY_COLOR, '--force']) }
|
|
201
|
+
VALID_AREA.each { |a| gh.write(['label', 'create', "area:#{a}", '--color', AREA_COLOR, '--force']) }
|
|
202
|
+
VALID_RISK.each { |r| gh.write(['label', 'create', "risk:#{r}", '--color', RISK_COLOR, '--force']) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Map of backlog id -> existing issue {number, state, labels} via the body marker.
|
|
206
|
+
def existing_issues(gh)
|
|
207
|
+
raw = gh.read(
|
|
208
|
+
['issue', 'list', '--label', 'agent-ready', '--state', 'all', '--limit', '500',
|
|
209
|
+
'--json', 'number,body,state,labels'],
|
|
210
|
+
default: '[]'
|
|
211
|
+
)
|
|
212
|
+
index = {}
|
|
213
|
+
JSON.parse(raw).each do |issue|
|
|
214
|
+
next unless issue['body'] =~ /<!-- backlog-id: (T-\d+) -->/
|
|
215
|
+
|
|
216
|
+
index[Regexp.last_match(1)] = {
|
|
217
|
+
'number' => issue['number'],
|
|
218
|
+
'state' => issue['state'].to_s.downcase,
|
|
219
|
+
'labels' => (issue['labels'] || []).map { |l| l['name'] }
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
index
|
|
223
|
+
rescue JSON::ParserError
|
|
224
|
+
{}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Sync
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def label_args(desired, current)
|
|
232
|
+
desired_set = desired
|
|
233
|
+
# Only remove labels we manage; never touch human-applied ones.
|
|
234
|
+
to_remove = (current & ALL_MANAGED_LABELS) - desired_set
|
|
235
|
+
to_add = desired_set - current
|
|
236
|
+
args = []
|
|
237
|
+
to_add.each { |l| args.push('--add-label', l) }
|
|
238
|
+
to_remove.each { |l| args.push('--remove-label', l) }
|
|
239
|
+
args
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def sync(data, gh)
|
|
243
|
+
ensure_labels(gh)
|
|
244
|
+
index = existing_issues(gh)
|
|
245
|
+
created = updated = closed = 0
|
|
246
|
+
|
|
247
|
+
(data['tasks'] || []).each do |task|
|
|
248
|
+
id = task['id']
|
|
249
|
+
title = task['title']
|
|
250
|
+
body = render_body(task)
|
|
251
|
+
want_open = OPEN_STATUSES.include?(task['status'])
|
|
252
|
+
issue = index[id]
|
|
253
|
+
|
|
254
|
+
if issue.nil?
|
|
255
|
+
next unless want_open # never create an issue for an already-done task
|
|
256
|
+
|
|
257
|
+
args = ['issue', 'create', '--title', title, '--body', body]
|
|
258
|
+
managed_labels(task).each { |l| args.push('--label', l) }
|
|
259
|
+
created += 1 if gh.write(args)
|
|
260
|
+
next
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
number = issue['number'].to_s
|
|
264
|
+
gh.write(['issue', 'edit', number, '--title', title, '--body', body] +
|
|
265
|
+
label_args(managed_labels(task), issue['labels']))
|
|
266
|
+
updated += 1
|
|
267
|
+
|
|
268
|
+
if want_open && issue['state'] != 'open'
|
|
269
|
+
gh.write(['issue', 'reopen', number])
|
|
270
|
+
elsif !want_open && issue['state'] != 'closed'
|
|
271
|
+
gh.write(['issue', 'close', number, '--reason', 'completed'])
|
|
272
|
+
closed += 1
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
puts "Backlog sync complete: #{created} created, #{updated} updated, #{closed} closed."
|
|
277
|
+
0
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Main
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def main
|
|
285
|
+
mode = :sync
|
|
286
|
+
OptionParser.new do |opts|
|
|
287
|
+
opts.banner = 'Usage: sync-backlog.rb [--check|--dry-run]'
|
|
288
|
+
opts.on('--check', 'Validate schema only; make no gh calls') { mode = :check }
|
|
289
|
+
opts.on('--dry-run', 'Print intended gh calls without executing') { mode = :dry_run }
|
|
290
|
+
end.parse!
|
|
291
|
+
|
|
292
|
+
data = load_data
|
|
293
|
+
errors = validate(data)
|
|
294
|
+
unless errors.empty?
|
|
295
|
+
warn '✗ _data/backlog.yml failed validation:'
|
|
296
|
+
errors.each { |e| warn " - #{e}" }
|
|
297
|
+
return 1
|
|
298
|
+
end
|
|
299
|
+
task_count = (data['tasks'] || []).size
|
|
300
|
+
|
|
301
|
+
if mode == :check
|
|
302
|
+
puts "✓ _data/backlog.yml is valid (#{task_count} tasks)."
|
|
303
|
+
return 0
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
sync(data, Gh.new(dry_run: mode == :dry_run))
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
exit main if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# sync-backlog.sh
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Thin wrapper around scripts/sync-backlog.rb.
|
|
7
|
+
#
|
|
8
|
+
# Mirrors `_data/backlog.yml` (the tactical task queue) to GitHub Issues:
|
|
9
|
+
# open tasks become open issues; tasks marked `done` close their issue.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ./scripts/sync-backlog.sh # create/update/close issues via gh
|
|
13
|
+
# ./scripts/sync-backlog.sh --check # validate schema only (CI/PR gate)
|
|
14
|
+
# ./scripts/sync-backlog.sh --dry-run # print intended gh calls only
|
|
15
|
+
#
|
|
16
|
+
# =============================================================================
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
exec ruby "${SCRIPT_DIR}/sync-backlog.rb" "$@"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jekyll-theme-zer0
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amr Abdel
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- README.md
|
|
80
80
|
- _data/README.md
|
|
81
81
|
- _data/authors.yml
|
|
82
|
+
- _data/backlog.yml
|
|
82
83
|
- _data/content_statistics.yml
|
|
83
84
|
- _data/features.yml
|
|
84
85
|
- _data/generate_statistics.rb
|
|
@@ -367,6 +368,10 @@ files:
|
|
|
367
368
|
- scripts/build
|
|
368
369
|
- scripts/convert-notebooks.sh
|
|
369
370
|
- scripts/docker-publish
|
|
371
|
+
- scripts/docs/check-freshness.sh
|
|
372
|
+
- scripts/docs/check-links.sh
|
|
373
|
+
- scripts/docs/lint-frontmatter.sh
|
|
374
|
+
- scripts/docs/validate.sh
|
|
370
375
|
- scripts/example-usage.sh
|
|
371
376
|
- scripts/features/generate-preview-images
|
|
372
377
|
- scripts/features/install-preview-generator
|
|
@@ -462,6 +467,8 @@ files:
|
|
|
462
467
|
- scripts/post-template-setup.sh
|
|
463
468
|
- scripts/release
|
|
464
469
|
- scripts/setup.sh
|
|
470
|
+
- scripts/sync-backlog.rb
|
|
471
|
+
- scripts/sync-backlog.sh
|
|
465
472
|
- scripts/test-auto-version.sh
|
|
466
473
|
- scripts/test-mermaid.sh
|
|
467
474
|
- scripts/test-notebook-conversion.sh
|