jekyll-theme-zer0 1.22.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ verify_close.py — the deterministic gate of the Issue Autopilot verify-and-close lane.
4
+
5
+ The issue-verifier (an LLM, read-only) inspects each `verify_candidate` issue and
6
+ writes its verdicts to `.issues/verify.json`. This script is the DETERMINISTIC half:
7
+ it selects which of those verdicts may actually close an issue, and it does so ONLY
8
+ when `main`'s full CI/CD gate suite is green. The autopilot never closes a human
9
+ issue on an LLM's say-so alone — the green-CI gate here is the hard backstop.
10
+
11
+ A verdict closes its issue iff ALL of:
12
+ 1. verdict.resolved is true,
13
+ 2. verdict.confidence == "high" and verdict.evidence is non-empty,
14
+ 3. the issue is a `verify_candidate` in plan.json (defense in depth — never a
15
+ protected/backlog-managed/bot/epic issue, even if the LLM names one),
16
+ 4. `main`'s combined commit status + every check-run on its HEAD are green.
17
+
18
+ `select` prints `<number>\t<evidence>` TSV for closable issues (the workflow does
19
+ the actual `gh issue close`, keeping the irreversible mutation in an auditable
20
+ step). If the CI/CD gate is not green, it prints NOTHING and the whole batch is
21
+ held — "close only if they pass all CI/CD gates" is all-or-nothing per run.
22
+
23
+ READ-ONLY against GitHub: this script only ever calls `gh api` GET endpoints. It
24
+ never closes, comments, or labels. Verdict text is treated strictly as DATA.
25
+
26
+ Subcommands:
27
+ select print closable `<number>\t<evidence>` lines (gated on green CI)
28
+ gate print "green" / "not-green: <why>" for main's CI suite, then exit 0/1
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ import subprocess
35
+ import sys
36
+ from pathlib import Path
37
+ from typing import Any, Optional
38
+
39
+ ISSUES_DIR = Path(".issues")
40
+ PLAN_PATH = ISSUES_DIR / "plan.json"
41
+ VERIFY_PATH = ISSUES_DIR / "verify.json"
42
+
43
+ # check-run conclusions that do NOT block a close (everything else does, and any
44
+ # check-run that has not `completed` blocks too — something is still running).
45
+ OK_CONCLUSIONS = {"success", "neutral", "skipped"}
46
+
47
+
48
+ def warn(msg: str) -> None:
49
+ print(f"::warning::verify_close: {msg}", file=sys.stderr)
50
+
51
+
52
+ def _gh_api(path: str) -> Optional[Any]:
53
+ """GET a gh api endpoint, returning parsed JSON or None on any failure."""
54
+ try:
55
+ out = subprocess.run(
56
+ ["gh", "api", "-H", "Accept: application/vnd.github+json", path],
57
+ capture_output=True, text=True, timeout=60,
58
+ )
59
+ except (OSError, subprocess.SubprocessError) as exc:
60
+ warn(f"gh api {path} failed to run: {exc}")
61
+ return None
62
+ if out.returncode != 0:
63
+ warn(f"gh api {path} exited {out.returncode}: {out.stderr.strip()[:200]}")
64
+ return None
65
+ try:
66
+ return json.loads(out.stdout or "null")
67
+ except json.JSONDecodeError as exc:
68
+ warn(f"gh api {path} returned non-JSON: {exc}")
69
+ return None
70
+
71
+
72
+ def ci_gate(repo: str, ref: str = "main") -> tuple[bool, str]:
73
+ """
74
+ True iff `main`'s combined commit status AND every check-run on its HEAD are
75
+ green. Fails CLOSED: any API error, pending run, or failure -> (False, why).
76
+ """
77
+ if not repo:
78
+ return False, "no repo configured"
79
+
80
+ status = _gh_api(f"repos/{repo}/commits/{ref}/status")
81
+ if status is None:
82
+ return False, "could not read commit status (failing closed)"
83
+ state = str(status.get("state") or "")
84
+ total = int(status.get("total_count") or 0)
85
+ # A commit with zero classic statuses reports state=pending; that's only a
86
+ # blocker if there are actually statuses. With statuses, require success.
87
+ if total > 0 and state != "success":
88
+ return False, f"combined commit status is '{state}' ({total} status(es))"
89
+
90
+ runs = _gh_api(f"repos/{repo}/commits/{ref}/check-runs?per_page=100")
91
+ if runs is None:
92
+ return False, "could not read check-runs (failing closed)"
93
+ for run in runs.get("check_runs") or []:
94
+ name = str(run.get("name") or "?")
95
+ if str(run.get("status")) != "completed":
96
+ return False, f"check-run '{name}' is not completed (status={run.get('status')})"
97
+ if str(run.get("conclusion")) not in OK_CONCLUSIONS:
98
+ return False, f"check-run '{name}' concluded '{run.get('conclusion')}'"
99
+ return True, f"green (state={state or 'none'}, {total} status(es), all check-runs passed)"
100
+
101
+
102
+ def _load(path: Path) -> Optional[Any]:
103
+ if not path.exists():
104
+ warn(f"{path} not found")
105
+ return None
106
+ try:
107
+ return json.loads(path.read_text(encoding="utf-8"))
108
+ except (OSError, json.JSONDecodeError) as exc:
109
+ warn(f"{path} unreadable: {exc}")
110
+ return None
111
+
112
+
113
+ def closable(plan: dict[str, Any], verify: dict[str, Any]) -> list[dict[str, Any]]:
114
+ """Verdicts that pass the policy filter (steps 1-3), before the CI gate."""
115
+ candidates = {
116
+ int(r["number"]): r
117
+ for r in (plan.get("issues") or [])
118
+ if r.get("verify_candidate") and r.get("number") is not None
119
+ }
120
+ out: list[dict[str, Any]] = []
121
+ for v in (verify.get("verdicts") or []):
122
+ try:
123
+ num = int(v.get("number"))
124
+ except (TypeError, ValueError):
125
+ continue
126
+ if not v.get("resolved"):
127
+ continue
128
+ if str(v.get("confidence") or "").lower() != "high":
129
+ continue
130
+ evidence = str(v.get("evidence") or "").strip()
131
+ if not evidence:
132
+ continue
133
+ if num not in candidates: # defense in depth — never close a non-candidate
134
+ warn(f"#{num} verdict resolved but not a verify_candidate — skipping")
135
+ continue
136
+ out.append({"number": num, "evidence": evidence})
137
+ return out
138
+
139
+
140
+ def main(argv: Optional[list[str]] = None) -> int:
141
+ ap = argparse.ArgumentParser(description=__doc__)
142
+ ap.add_argument("command", choices=["select", "gate"])
143
+ ap.add_argument("--repo", default=None, help="owner/name (else from plan.json)")
144
+ ap.add_argument("--ref", default="main", help="git ref to gate on (default: main)")
145
+ args = ap.parse_args(argv)
146
+
147
+ plan = _load(PLAN_PATH) or {}
148
+ repo = args.repo or str(plan.get("repo") or "")
149
+
150
+ if args.command == "gate":
151
+ green, why = ci_gate(repo, args.ref)
152
+ print("green" if green else f"not-green: {why}")
153
+ return 0 if green else 1
154
+
155
+ # select
156
+ verify = _load(VERIFY_PATH)
157
+ if not isinstance(verify, dict):
158
+ print("no verdicts to act on.", file=sys.stderr)
159
+ return 0
160
+ picks = closable(plan, verify)
161
+ if not picks:
162
+ print("no resolved+high-confidence verdicts for a verify_candidate.", file=sys.stderr)
163
+ return 0
164
+
165
+ green, why = ci_gate(repo, args.ref)
166
+ if not green:
167
+ warn(f"CI/CD gate not green — holding {len(picks)} close(s) this run: {why}")
168
+ return 0
169
+
170
+ print(f"CI/CD gate {why}", file=sys.stderr)
171
+ for p in picks:
172
+ # TSV: the workflow closes these; evidence goes into the close comment.
173
+ evidence = p["evidence"].replace("\t", " ").replace("\n", " ")
174
+ print(f"{p['number']}\t{evidence}")
175
+ return 0
176
+
177
+
178
+ if __name__ == "__main__":
179
+ raise SystemExit(main())
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # ---------------------------------------------------------------------------
5
+ # validate-features.rb — canonical integrity checker for the feature registry.
6
+ #
7
+ # Single source of truth shared by `scripts/bin/validate` (preflight) and
8
+ # `test/test_features.sh` (the `features` test suite). Governance:
9
+ # .github/instructions/features.instructions.md.
10
+ #
11
+ # HARD failures (exit 1):
12
+ # - features/features.yml and _data/features.yml are not byte-identical
13
+ # - the header is missing a `# Last Updated:` line
14
+ # - schema violations (missing required field, bad id/version, duplicate id)
15
+ # - implemented:false without `removed_in:`
16
+ # - a reference path on an ACTIVE feature does not exist in the repo
17
+ # - missing/malformed `provenance:` block on an active feature (since PR B)
18
+ # - missing/unresolvable `tests:` linkage on an active feature (since PR C):
19
+ # every entry must be a real test path or `{na: reason}`
20
+ #
21
+ # WARNINGS (non-fatal unless FEATURES_STRICT=1):
22
+ # - header `# Version:` not tracking the gem version
23
+ # - id gaps (IDs are never reused, but gaps are worth a human glance)
24
+ #
25
+ # Portable across Ruby 2.6 (macOS system) and 3.x (CI/Docker).
26
+ # ---------------------------------------------------------------------------
27
+
28
+ require 'date'
29
+ require 'yaml'
30
+
31
+ MASTER = 'features/features.yml'
32
+ MIRROR = '_data/features.yml'
33
+ REQUIRED = %w[id title description implemented version link docs tags date].freeze
34
+ STRICT = ENV['FEATURES_STRICT'] == '1'
35
+
36
+ def die(msg)
37
+ warn " \e[31m✗\e[0m #{msg}"
38
+ exit 1
39
+ end
40
+
41
+ # Ruby 3.1+ defaults YAML.load_file to safe loading (rejects Date); 2.6 does
42
+ # not accept the keywords. Try the strict form, fall back for old Ruby.
43
+ def load_yaml(path)
44
+ YAML.load_file(path, permitted_classes: [Date, Time], aliases: true)
45
+ rescue ArgumentError
46
+ YAML.load_file(path)
47
+ end
48
+
49
+ def ref_paths(refs)
50
+ return [] unless refs.is_a?(Hash)
51
+
52
+ refs.values.flat_map { |v| v.is_a?(Array) ? v : [v] }.select { |v| v.is_a?(String) }
53
+ end
54
+
55
+ [MASTER, MIRROR].each { |f| die "#{f} is missing" unless File.file?(f) }
56
+
57
+ # 1. Sync contract — the two registries must be byte-identical.
58
+ if File.binread(MASTER) != File.binread(MIRROR)
59
+ die "#{MASTER} and #{MIRROR} differ — run `cp #{MASTER} #{MIRROR}` (must be byte-identical)"
60
+ end
61
+
62
+ data = load_yaml(MIRROR)
63
+ feats = data && data['features']
64
+ die 'top-level `features:` list missing' unless feats.is_a?(Array)
65
+
66
+ warnings = []
67
+
68
+ # Header sanity.
69
+ header = File.foreach(MIRROR).first(8).join
70
+ gem_version = File.read('lib/jekyll-theme-zer0/version.rb')[/VERSION\s*=\s*"([^"]+)"/, 1]
71
+ warnings << "header `# Version:` should match gem version #{gem_version}" unless header.include?("# Version: #{gem_version}")
72
+ die 'header missing `# Last Updated: YYYY-MM-DD` line' unless header =~ /# Last Updated: \d{4}-\d{2}-\d{2}/
73
+
74
+ seen = {}
75
+ feats.each_with_index do |f, i|
76
+ die "entry ##{i} is not a mapping" unless f.is_a?(Hash)
77
+ id = f['id'] || "(index #{i})"
78
+
79
+ REQUIRED.each do |key|
80
+ val = f[key]
81
+ die "#{id}: required field `#{key}` is missing/empty" if val.nil? || (val.respond_to?(:empty?) && val.empty?)
82
+ end
83
+
84
+ die "#{id}: id must match ZER0-NNN" unless f['id'] =~ /\AZER0-\d{3}\z/
85
+ die "#{id}: duplicate id" if seen[f['id']]
86
+ seen[f['id']] = true
87
+ die "#{id}: version must be X.Y.Z" unless f['version'].to_s =~ /\A\d+\.\d+\.\d+/
88
+ die "#{id}: tags must be a non-empty list" unless f['tags'].is_a?(Array) && !f['tags'].empty?
89
+
90
+ if f['implemented'] == false
91
+ # Removed features keep their (now-absent) references for history but must
92
+ # record when they went away.
93
+ die "#{id}: implemented:false requires `removed_in:`" unless f['removed_in']
94
+ next
95
+ end
96
+
97
+ # 2. Every reference path on an active feature must exist.
98
+ ref_paths(f['references']).each do |p|
99
+ ok = p.end_with?('/') ? File.directory?(p) : File.exist?(p)
100
+ die "#{id}: reference path does not exist: #{p}" unless ok
101
+ end
102
+
103
+ # 3a. Provenance is REQUIRED on every active feature (backfilled in PR B).
104
+ prov = f['provenance']
105
+ die "#{id}: missing `provenance:` block" unless prov.is_a?(Hash)
106
+ die "#{id}: provenance.introduced_in must be a version string" unless prov['introduced_in'].is_a?(String) && prov['introduced_in'] =~ /\A\d+\.\d+\.\d+/
107
+ die "#{id}: provenance.commit must be a short git hash" unless prov['commit'].is_a?(String) && prov['commit'] =~ /\A[0-9a-f]{7,40}\z/
108
+ die "#{id}: provenance.pr must be an integer or null" unless prov['pr'].nil? || prov['pr'].is_a?(Integer)
109
+ die "#{id}: provenance.issue must be an integer or null" unless prov['issue'].nil? || prov['issue'].is_a?(Integer)
110
+
111
+ # 3b. Test linkage is REQUIRED on every active feature (backfilled in PR C).
112
+ tests = f['tests']
113
+ die "#{id}: missing/empty `tests:` (a real test path or `{na: reason}`)" unless tests.is_a?(Array) && !tests.empty?
114
+ tests.each do |t|
115
+ if t.is_a?(String)
116
+ die "#{id}: tests entry does not exist: #{t}" unless File.exist?(t)
117
+ elsif t.is_a?(Hash)
118
+ die "#{id}: tests `na:` must carry a non-empty reason" unless t['na'].is_a?(String) && !t['na'].strip.empty?
119
+ else
120
+ die "#{id}: tests entries must be a path string or `{na: reason}`"
121
+ end
122
+ end
123
+ end
124
+
125
+ # Sequential-id sanity (gaps allowed — never reuse an ID — but flagged).
126
+ nums = seen.keys.map { |k| k.split('-').last.to_i }.sort
127
+ gaps = (1..nums.last).to_a - nums
128
+ warnings << "id gaps (verify intentional): #{gaps.map { |n| format('ZER0-%03d', n) }.join(', ')}" unless gaps.empty?
129
+
130
+ unless warnings.empty?
131
+ verbose = STRICT || ENV['FEATURES_VERBOSE'] == '1'
132
+ prov = warnings.count { |w| w.include?('provenance') }
133
+ tests = warnings.count { |w| w.include?('`tests:`') }
134
+ other = warnings.length - prov - tests
135
+ warn " \e[33m⚠\e[0m feature registry: #{warnings.length} warning(s) — " \
136
+ "provenance:#{prov} tests:#{tests} other:#{other} (FEATURES_VERBOSE=1 for the full list)"
137
+ warnings.each { |w| warn " - #{w}" } if verbose
138
+ die "#{warnings.length} warning(s) treated as errors (FEATURES_STRICT=1)" if STRICT
139
+ end
140
+
141
+ active = feats.count { |f| f['implemented'] != false }
142
+ puts "Feature registry valid: #{feats.length} entries (#{active} active), refs resolved, master/_data in sync"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-zer0
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.22.0
4
+ version: 1.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amr Abdel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-29 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -78,6 +78,7 @@ files:
78
78
  - LICENSE
79
79
  - README.md
80
80
  - _data/README.md
81
+ - _data/ai.yml
81
82
  - _data/authors.yml
82
83
  - _data/backlog.yml
83
84
  - _data/content_statistics.yml
@@ -171,6 +172,7 @@ files:
171
172
  - _includes/content/toc.html
172
173
  - _includes/content/transclude.html
173
174
  - _includes/core/branding.html
175
+ - _includes/core/color-mode-init.html
174
176
  - _includes/core/footer-fabs.html
175
177
  - _includes/core/footer.html
176
178
  - _includes/core/head.html
@@ -384,6 +386,7 @@ files:
384
386
  - scripts/bin/test
385
387
  - scripts/bin/validate
386
388
  - scripts/build
389
+ - scripts/ci/classify_changes.py
387
390
  - scripts/content-review.rb
388
391
  - scripts/convert-notebooks.sh
389
392
  - scripts/dev/css-diff.sh
@@ -449,6 +452,10 @@ files:
449
452
  - scripts/install/template.sh
450
453
  - scripts/install/tui.sh
451
454
  - scripts/install/upgrade.sh
455
+ - scripts/issues/dispatch.py
456
+ - scripts/issues/test_verify_close.py
457
+ - scripts/issues/triage.py
458
+ - scripts/issues/verify_close.py
452
459
  - scripts/lib/README.md
453
460
  - scripts/lib/audit.sh
454
461
  - scripts/lib/changelog.sh
@@ -519,6 +526,7 @@ files:
519
526
  - scripts/utils/fix-markdown
520
527
  - scripts/utils/setup
521
528
  - scripts/validate
529
+ - scripts/validate-features.rb
522
530
  - scripts/vendor-install.sh
523
531
  homepage: https://github.com/bamr87/zer0-mistakes
524
532
  licenses: