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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e3dadf187dfaae9c1186aa1e0864f19ca74e9564cee0b0fa3136657fda6cf03
4
- data.tar.gz: '024782a85ed83017188dac310e4aa75ed22f41d604576758f4f7f49564cba984'
3
+ metadata.gz: 98ca7799830fd5c793814a39656893d742627b0ce2800e32761ce53bc32b8263
4
+ data.tar.gz: 8b247e3c6002ec51bfc9c2c423f2cb531afa97f65c59b8c5526fe9251f70df22
5
5
  SHA512:
6
- metadata.gz: 692271e54f6065325496b8bcfc9956194e085bd07d7a53f3c9adc108b146e846f712d126e5794b52cdbd1cad75e4dbfa91260767820fee2903894472b76af7a7
7
- data.tar.gz: f0dd37b5871364845af1f663f41f59910f5e9e4ed2d5a6c3033fe56433f9f5006d8e99bcd3054c1f4352b542d9857232082c1a219d15a7cb5f1a1defedc72ecc
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.10.0
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-01T03:29:07.000Z
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.10.0 ([RubyGems](https://rubygems.org/gems/jekyll-theme-zer0), [CHANGELOG](/CHANGELOG)) |
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.10.0** • [Changelog](CHANGELOG.md) • [License](LICENSE) • [Contributing](CONTRIBUTING.md) • [AI Agent Guide](AGENTS.md)
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="breadcrumb">
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.10.0
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