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.
@@ -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
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 navigation data: pass" \
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.23.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-30 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
@@ -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: