jekyll-theme-zer0 1.23.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/_data/features.yml +847 -34
- data/_includes/components/feature-card.html +15 -0
- data/_includes/components/zer0-env-var.html +7 -1
- data/scripts/bin/validate +17 -1
- data/scripts/validate-features.rb +142 -0
- metadata +3 -2
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
- compact (bool) : Compact mode — hides sub-features list (optional, default: false)
|
|
20
20
|
- features_limit (int) : Max sub-features shown in the list (optional, default: 5)
|
|
21
21
|
|
|
22
|
+
Feature fields surfaced (from _data/features.yml): title, description,
|
|
23
|
+
features[], references (when show_refs), id, version, tags, docs, link, and
|
|
24
|
+
provenance (provenance.pr / provenance.commit / provenance.issue → GitHub
|
|
25
|
+
links built from site.github_user + site.repository_name).
|
|
26
|
+
|
|
22
27
|
Usage:
|
|
23
28
|
{% assign f = site.data.features.features | where: "id", "ZER0-001" | first %}
|
|
24
29
|
{% include components/feature-card.html feature=f style="primary" icon="bi-bootstrap" icon_color="text-primary" %}
|
|
@@ -78,6 +83,16 @@
|
|
|
78
83
|
<span class="badge bg-light text-dark">{{ tag }}</span>
|
|
79
84
|
{% endfor %}
|
|
80
85
|
</div>
|
|
86
|
+
|
|
87
|
+
{% if f.provenance %}
|
|
88
|
+
{% assign gh = "https://github.com/" | append: site.github_user | append: "/" | append: site.repository_name %}
|
|
89
|
+
<div class="mt-2 small text-muted feature-provenance">
|
|
90
|
+
<i class="bi bi-clock-history me-1" aria-hidden="true"></i>
|
|
91
|
+
{% if f.provenance.pr %}<a href="{{ gh }}/pull/{{ f.provenance.pr }}" class="text-decoration-none me-2">PR #{{ f.provenance.pr }}</a>{% endif %}
|
|
92
|
+
{% if f.provenance.commit %}<a href="{{ gh }}/commit/{{ f.provenance.commit }}" class="text-decoration-none me-2"><code>{{ f.provenance.commit }}</code></a>{% endif %}
|
|
93
|
+
{% if f.provenance.issue %}<a href="{{ gh }}/issues/{{ f.provenance.issue }}" class="text-decoration-none me-2">#{{ f.provenance.issue }}</a>{% endif %}
|
|
94
|
+
</div>
|
|
95
|
+
{% endif %}
|
|
81
96
|
</div>
|
|
82
97
|
{% if f.docs %}
|
|
83
98
|
<div class="card-footer">
|
|
@@ -57,6 +57,12 @@
|
|
|
57
57
|
document.querySelector('#envTable tbody').appendChild(row);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Escape user-entered text before it is interpolated into innerHTML, so a
|
|
61
|
+
// key/value containing markup cannot inject script (DOM XSS).
|
|
62
|
+
const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => (
|
|
63
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
64
|
+
));
|
|
65
|
+
|
|
60
66
|
document.getElementById('submit').addEventListener('click', function() {
|
|
61
67
|
const rows = document.querySelectorAll('#envTable tbody tr');
|
|
62
68
|
let codeBlockText = '';
|
|
@@ -64,7 +70,7 @@
|
|
|
64
70
|
const key = row.querySelector('td:nth-child(1) input').value;
|
|
65
71
|
const value = row.querySelector('td:nth-child(2) input').value;
|
|
66
72
|
sessionStorage.setItem(key, value);
|
|
67
|
-
codeBlockText += `<span class="nb">export </span><span class="nv">${key}</span><span class="o">=</span>${value}\n`;
|
|
73
|
+
codeBlockText += `<span class="nb">export </span><span class="nv">${escapeHtml(key)}</span><span class="o">=</span>${escapeHtml(value)}\n`;
|
|
68
74
|
});
|
|
69
75
|
document.getElementById('codeBlock').innerHTML = codeBlockText;
|
|
70
76
|
|
data/scripts/bin/validate
CHANGED
|
@@ -500,6 +500,19 @@ RUBY
|
|
|
500
500
|
success "Navigation data is valid"
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
validate_features_registry() {
|
|
504
|
+
step "Validating feature registry (features.yml)..."
|
|
505
|
+
|
|
506
|
+
# Delegates to the shared canonical checker so this gate and the `features`
|
|
507
|
+
# test suite never drift. Hard-fails on master/_data drift, schema
|
|
508
|
+
# violations, stale references, and missing provenance; warns (until
|
|
509
|
+
# FEATURES_STRICT=1) on missing tests. See
|
|
510
|
+
# .github/instructions/features.instructions.md.
|
|
511
|
+
FEATURES_STRICT="${FEATURES_STRICT:-0}" ruby "$REPO_ROOT/scripts/validate-features.rb"
|
|
512
|
+
|
|
513
|
+
success "Feature registry is valid"
|
|
514
|
+
}
|
|
515
|
+
|
|
503
516
|
run_jekyll_build() {
|
|
504
517
|
if [[ "$SKIP_JEKYLL" == "true" ]]; then
|
|
505
518
|
warn "Skipping Jekyll build"
|
|
@@ -612,6 +625,9 @@ main() {
|
|
|
612
625
|
validate_navigation_data
|
|
613
626
|
echo ""
|
|
614
627
|
|
|
628
|
+
validate_features_registry
|
|
629
|
+
echo ""
|
|
630
|
+
|
|
615
631
|
run_jekyll_build
|
|
616
632
|
echo ""
|
|
617
633
|
|
|
@@ -628,7 +644,7 @@ main() {
|
|
|
628
644
|
print_summary "Validation Complete" \
|
|
629
645
|
"Repository files: pass" \
|
|
630
646
|
"Version consistency: pass" \
|
|
631
|
-
"YAML, config contract, and
|
|
647
|
+
"YAML, config contract, navigation data, and feature registry: pass" \
|
|
632
648
|
"Jekyll build: $([[ "$SKIP_JEKYLL" == "true" ]] && echo skipped || echo pass)" \
|
|
633
649
|
"Jekyll doctor: $([[ "$SKIP_DOCTOR" == "true" ]] && echo skipped || echo pass)" \
|
|
634
650
|
"Compiled assets: $([[ "$SKIP_ASSETS" == "true" ]] && echo skipped || echo pass)" \
|
|
@@ -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.
|
|
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-
|
|
11
|
+
date: 2026-07-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: jekyll
|
|
@@ -526,6 +526,7 @@ files:
|
|
|
526
526
|
- scripts/utils/fix-markdown
|
|
527
527
|
- scripts/utils/setup
|
|
528
528
|
- scripts/validate
|
|
529
|
+
- scripts/validate-features.rb
|
|
529
530
|
- scripts/vendor-install.sh
|
|
530
531
|
homepage: https://github.com/bamr87/zer0-mistakes
|
|
531
532
|
licenses:
|