diff --git a/.git-hooks/pre_commit/check_executable_shebangs.rb b/.git-hooks/pre_commit/check_executable_shebangs.rb new file mode 100644 index 0000000..5829de1 --- /dev/null +++ b/.git-hooks/pre_commit/check_executable_shebangs.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Any tracked file with the executable bit set must start with a shebang + # (previously pre-commit's check-executables-have-shebangs and + # check-shebang-scripts-are-executable, combined). + class CheckExecutableShebangs < Base + def run + errors = [] + + `git ls-files -sz`.split("\0").each do |entry| + mode_part, path = entry.split("\t", 2) + mode = mode_part.split(' ').first.to_i(8) & 0o111 + next if mode.zero? + + next unless File.exist?(path) && !File.directory?(path) + + first = File.open(path) { |f| f.readline } rescue '' + if !first.start_with?('#!') + errors << "#{path}: executable file is missing a shebang" + elsif (File.stat(path).mode & 0o111).zero? + errors << "#{path}: script starts with a shebang but is not executable" + end + end + + return :pass if errors.empty? + + [:fail, errors.join("\n")] + end + end +end diff --git a/.git-hooks/pre_commit/codespell.rb b/.git-hooks/pre_commit/codespell.rb new file mode 100644 index 0000000..cfd1756 --- /dev/null +++ b/.git-hooks/pre_commit/codespell.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Runs `codespell` on the files being committed. + class Codespell < Base + def run + result = execute( + %w[codespell --ignore-words-list=Claus,Technik --skip=_site,.git,node_modules], + args: applicable_files, + ) + return :pass if result.success? + + [:fail, (result.stdout + result.stderr).chomp] + end + end +end diff --git a/.git-hooks/pre_commit/final_newline.rb b/.git-hooks/pre_commit/final_newline.rb new file mode 100644 index 0000000..7c4e18d --- /dev/null +++ b/.git-hooks/pre_commit/final_newline.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Checks that files end with a single newline (previously pre-commit's + # end-of-file-fixer, without the auto-fix). + class FinalNewline < Base + def run + errors = [] + + applicable_files.each do |file| + next unless File.exist?(file) && !File.directory?(file) + + content = File.binread(file) + # Skip binaries and files that are not UTF-8 text (ends-of-files + # only make sense for text; the previous pre-commit hook ignored + # binaries too). + next if content.empty? + next if content.include?("\0") || !content.valid_encoding? + + unless content.end_with?("\n") + errors << "#{file}: missing final newline" + end + end + + return :pass if errors.empty? + + [:fail, errors.join("\n")] + end + end +end diff --git a/.git-hooks/pre_commit/refactoring_sample_width.rb b/.git-hooks/pre_commit/refactoring_sample_width.rb new file mode 100644 index 0000000..278fc6c --- /dev/null +++ b/.git-hooks/pre_commit/refactoring_sample_width.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Enforces the 72-char pane width on refactoring sample sources + # (see the refactoring skill's LESSONS.md 'pane width'). + class RefactoringSampleWidth < Base + MAX_WIDTH = 72 + + def run + errors = [] + + applicable_files.each do |file| + next unless File.exist?(file) + + File.foreach(file).with_index(1) do |line, line_no| + content = line.chomp + next if content.length <= MAX_WIDTH + + errors << "#{file}: line #{line_no} is #{content.length} chars " \ + "(> #{MAX_WIDTH})" + end + end + + return :pass if errors.empty? + + [:fail, errors.join("\n")] + end + end +end diff --git a/.git-hooks/pre_commit/spellr.rb b/.git-hooks/pre_commit/spellr.rb new file mode 100644 index 0000000..b520f7d --- /dev/null +++ b/.git-hooks/pre_commit/spellr.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Overcommit::Hook::PreCommit + # Runs spellr (the Ruby spell checker) on the files being committed. + # spellr splits camelCase and snake_case identifiers, skips URLs and + # hex-like strings via heuristics, obeys .gitignore, and consults + # .spellr.yml plus the wordlists in .spellr_wordlists/. It runs from + # the repo root so the config is found wherever the hook triggers. + # + # NOTE: spellr applies .spellr.yml excludes only when given a directory + # or nothing; explicit file arguments bypass them. Overcommit passes + # files, so re-apply the same excludes here. + class Spellr < Base + EXCLUDES = [ + # match on the repo-relative path and the bare basename, because + # overcommit passes absolute paths in --run mode + lambda do |path| + rel = path.sub(%r{#{Regexp.escape(Overcommit::Utils.repo_root)}/?}, '') + rel =~ %r{^\.claude/} || + rel =~ %r{\.(scala|hs|js)$} || + rel == 'run.sh' || + rel =~ %r{^pages/refactorings/} || + rel == '.gitignore' || + File.basename(rel).start_with?('.') + end + ].freeze + + def run + files = applicable_files.reject { |f| EXCLUDES.any? { |re| re.call(f) } } + return :pass if files.empty? + + result = execute(%w[bundle exec spellr], args: files) + return :pass if result.success? + + [:fail, (result.stdout + result.stderr).chomp] + end + end +end diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..df94bfd --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + pull_request: + branches: [master] + push: + branches: [master] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + + - name: Run the lint suite + run: scripts/lint.sh + + - name: Build the site + run: bundle exec jekyll build diff --git a/.gitignore b/.gitignore index 51dfb96..c3a3d18 100644 --- a/.gitignore +++ b/.gitignore @@ -71,3 +71,6 @@ typings/ dist-newstyle/ *.hi *.o + +# Local linting venv (see README "Lint") +/.venv/ diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..1c3046d --- /dev/null +++ b/.mdlrc @@ -0,0 +1,12 @@ +# Markdown style for the site, shared by every entry point that runs mdl +# (the overcommit Mdl hook, `scripts/lint.sh`, CI, and ad-hoc manual runs). +# mdl picks this up automatically from the current directory. + +# Jekyll pages and posts start with YAML front matter, which is not prose. +ignore_front_matter true + +# When given a directory, only look at files git tracks, so generated +# output (_site/, .jekyll-cache/) and dependency dirs are never linted. +git_recurse true + +style "#{File.dirname(__FILE__)}/mdl_style.rb" diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 0000000..3fbb89d --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,94 @@ +# Linting for the site, Ruby/bundler based (replaces the previous +# pre-commit/python setup with the same checks): +# +# Local hooks: bundle exec overcommit --install +# Whole repo: scripts/lint.sh (= `bundle exec overcommit --run`) +# CI: .github/workflows/lint.yml runs scripts/lint.sh +# +# Local hooks and CI execute this same configuration, so they cannot drift. +# +# Signature verification is off so CI can run `overcommit --run` on a fresh +# checkout (signatures live in the local git config, which CI never has). +verify_signatures: false + +# Make the installed git hooks resolve gems through the bundle, so hooks find +# `mdl` and friends without a system-wide gem install. +gemfile: Gemfile + +# Only print hook output when a hook warns or fails. +quiet: true + +PreCommit: + AuthorName: + enabled: false + + AuthorEmail: + enabled: false + + # Hygiene checks (previously pre-commit-hooks): + # trailing whitespace -> TrailingWhitespace, CRLF check dropped (repo is + # LF only), case conflicts -> CaseConflicts, merge markers -> + # MergeConflicts, YAML/JSON syntax -> YamlSyntax/JsonSyntax, final + # newline -> FinalNewline, shebang/exec-bit consistency -> + # CheckExecutableShebangs. + + CaseConflicts: + enabled: true + + MergeConflicts: + enabled: true + + TrailingWhitespace: + enabled: true + requires_files: true + + JsonSyntax: + enabled: true + requires_files: true + exclude: + # search.json is a Jekyll/Liquid template with front matter, + # not a plain JSON file. + - 'assets/data/search.json' + + YamlSyntax: + enabled: true + requires_files: true + + FinalNewline: + enabled: true + requires_files: true + + CheckExecutableShebangs: + enabled: true + requires_files: true + + # Markdown style (previously markdownlint-cli2). Rules live in .mdlrc + + # mdl_style.rb, shared with any editor or manual `mdl` run. + Mdl: + enabled: true + requires_files: true + command: ['bundle', 'exec', 'mdl'] + exclude: + # The refactorings section carries raw-HTML citation blocks (single + #
  • lines with entities/URLs) and ref-link prose that the + # refactoring skill writes deliberately; those can't be wrapped to + # 80 cols without breaking the markup. Exempt only that section. + - 'pages/refactorings/**/*' + # .claude/skills are agent-internal docs, not site prose. + - '.claude/**/*' + # The catalogue index page has the same citation conventions. + - 'pages/refactorings.md' + + # Spelling with spellr (Ruby, replaces codespell — see + # .git-hooks/pre_commit/spellr.rb). Wordlist: .spellr_wordlists/. + Spellr: + enabled: true + requires_files: true + + # Refactoring sample sources must stay within the 72-char pane width + # (previously scripts/check-sample-width.sh via pre-commit). + RefactoringSampleWidth: + enabled: true + requires_files: true + include: + - 'pages/refactorings/**/*.{scala,hs}' diff --git a/.spellr.yml b/.spellr.yml new file mode 100644 index 0000000..f5bb93a --- /dev/null +++ b/.spellr.yml @@ -0,0 +1,29 @@ +# Spellr configuration (https://github.com/robotdana/spellr), the Ruby/bundler +# spell checker. Checked files obey .gitignore; these excludes are for +# Jekyll/generated trees that git ignores anyway, kept explicit so `spellr` +# behaves identically from any directory. +# +# Policy: spellr checks the site's prose (Markdown, YAML/JSON data, HTML, +# SCSS). Non-prose files are excluded: refactoring source samples (.scala .hs) +# and scripts are code with their own vocabulary, and .claude/skills are +# agent-internal docs full of tool-specific terms — none are site spelling. +excludes: + - .git/ + - _site/ + - .jekyll-cache/ + - vendor/ + - node_modules/ + - .venv/ + - Gemfile.lock + - .claude/ + - '*.scala' + - '*.hs' + - '*.js' + - 'run.sh' + - pages/refactorings/ + +# Site-specific spelling lives in .spellr_wordlists/english.txt; regenerate +# or extend it with `bundle exec spellr --wordlist`. +languages: + english: + locale: US diff --git a/.spellr_wordlists/english.txt b/.spellr_wordlists/english.txt new file mode 100644 index 0000000..2cbeb5d --- /dev/null +++ b/.spellr_wordlists/english.txt @@ -0,0 +1,195 @@ +addons +adt +affero +agpl +apec +apu +arial +artefact +artefacts +async +avro +backend +barcamp +baseurl +behaviour +bfc +bodireel +btn +catalogue +cdn +centred +cet +charset +cli +cname +codebase +codebases +codespell +colour +colours +concat +config +consolas +const +copyleft +cov +crlf +cta +cto +ctx +deliverables +deps +dirname +dirs +disqus +doctype +dotenv +dpr +droste +elsif +enablement +endcapture +endcase +endcomment +endfor +endif +endunless +ensp +env +eslint +eslintcache +euo +eval +evals +favicon +fira +fowler +fraunces +gadt +gemfile +gfx +github +gitignore +glm +globals +gpl +gsap +headerless +helvetica +href +ico +img +instituto +intec +ior +javascript +jmh +jscoverage +json +jspm +katex +kinded +kleisli +kramdown +kvk +lambek +linecap +linejoin +linkedin +linted +linting +llm +markdownlint +mdl +mdlrc +memoised +menlo +mixin +modernisation +msa +navbar +nbsp +nda +neue +newline +newlines +noncommercially +noopener +noreply +npm +num +onclick +opdyke +opsz +oss +otravo +pacifico +peaceiris +perceus +permalink +pexels +pids +png +pointerleave +pointermove +preconnect +programme +pucmm +pullquote +readme +recurse +reddit +refactorings +refactorings +repl +rhansen +rivoli +roboto +rodolfohansen +rohan +rossjrw +rss +runtimes +scaffeine +scalafix +scoverage +scss +segoe +seo +specialised +src +statsub +strix +stylesheet +subdirs +svg +sylhare +tagless +technik +tecnológico +testid +tful +tgz +thinsp +tok +txt +uniq +universidad +unrepresentable +unstyled +untyped +usr +utf +utils +venv +viewport +viewports +visibilitychange +webfont +wght +wipo +wordlist +wscript +yaml +yml diff --git a/CNAME b/CNAME index 5ace4e5..747f0e4 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -www.constructive.dev \ No newline at end of file +www.constructive.dev diff --git a/Gemfile b/Gemfile index 1291498..f90b0df 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,11 @@ source "https://rubygems.org" -gemspec \ No newline at end of file +gemspec + +group :development do + # Linting harness + markdown linter. Hooks are installed with + # `bundle exec overcommit --install`; CI runs the same checks via + # scripts/lint.sh (see .overcommit.yml). + gem "overcommit", "~> 0.68" + gem "mdl", "~> 0.13" + gem "spellr", "~> 0.12" +end diff --git a/README.md b/README.md index 51d6110..631cf59 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,32 @@ # website -Source for [www.constructive.dev](https://www.constructive.dev). Jekyll, built on the [Type-on-Strap](https://github.com/sylhare/Type-on-Strap) remote theme, deployed to GitHub Pages from the `gh-pages` branch. +Source for [www.constructive.dev](https://www.constructive.dev). Jekyll, +built on the [Type-on-Strap](https://github.com/sylhare/Type-on-Strap) remote +theme, deployed to GitHub Pages from the `gh-pages` branch. -- **Build & deploy** (`.github/workflows/build.yml`): on push to `master`, builds with `JEKYLL_ENV=production` and force-pushes `_site/` to `gh-pages`. -- **PR previews** (`.github/workflows/preview.yml`): each PR gets a preview at `https://www.constructive.dev/pr-preview/pr-/` via [rossjrw/pr-preview-action](https://github.com/rossjrw/pr-preview-action). +- **Build & deploy** (`.github/workflows/build.yml`): on push to `master`, + builds with `JEKYLL_ENV=production` and force-pushes `_site/` to `gh-pages`. +- **PR previews** (`.github/workflows/preview.yml`): each PR gets a preview at + `https://www.constructive.dev/pr-preview/pr-/` via + [rossjrw/pr-preview-action](https://github.com/rossjrw/pr-preview-action). +- **Lint** (`.github/workflows/lint.yml`): runs + [`scripts/lint.sh`](scripts/lint.sh) — which is exactly + `bundle exec overcommit --run` — on every PR and push to `master`. The + same overcommit configuration runs locally on commit; install once, then + it just happens: + + ```sh + bundle install + bundle exec overcommit --install + ``` + + Rules: Markdown (`mdl`, style in [`.mdlrc`](.mdlrc) + + [`mdl_style.rb`](mdl_style.rb)), spelling (spellr, wordlist in + [`.spellr_wordlists/`](.spellr_wordlists/)), hygiene checks + (trailing whitespace, final newline, YAML/JSON syntax, merge markers, + case conflicts, executable/shebang consistency) and a 72-char pane-width + cap on the refactoring sample sources. Run everything at any time with + `scripts/lint.sh`. ```sh bundle install diff --git a/_config.yml b/_config.yml index 8a7b84b..068d713 100644 --- a/_config.yml +++ b/_config.yml @@ -11,7 +11,9 @@ favicon: assets/img/favicon.svg # Hero header_feature_image: "" header_text: Constructive Programming -header_subtitle: Ship at AI speed. Skip the gamble. Production code with AI, inside engineering guardrails that make most wrong versions impossible to build. +header_subtitle: >- + Ship at AI speed. Skip the gamble. Production code with AI, inside + engineering guardrails that make most wrong versions impossible to build. # Footer footer_text: "Copyright Constructive Programming," diff --git a/mdl_style.rb b/mdl_style.rb new file mode 100644 index 0000000..418e74e --- /dev/null +++ b/mdl_style.rb @@ -0,0 +1,30 @@ +# mdl style for the site (loaded from .mdlrc). Start from mdl's default +# style and adjust for the site's actual content conventions. + +all + +# Fenced code blocks may omit a language tag where it adds nothing. +exclude_rule 'fenced-code-language' + +# Pages start with a heading only after their front matter (which mdl skips +# entirely), and Liquid-driven pages have no heading at all. +exclude_rule 'first-line-h1' + +# The refactoring pages carry citations and koans as raw HTML blocks (the +# refactoring skill writes them that way because kramdown won't process +# markdown inside raw block HTML). Inline HTML is fine here by design. +exclude_rule 'no-inline-html' + +# The refactorings post steps are numbered 1., 2., 3., ...; require a +# consistent ordered sequence instead of mdl's default "one" style. +rule 'MD029', :style => :ordered + +# Headings may end in '?' (e.g. '## Not a fit?') — markdownlint's MD026 +# default punctuation (".,;:!") is what this site always used. +rule 'MD026', :punctuation => '.,;:!' + +# Prose wraps at 80 chars. Code blocks, tables and headings carry their own +# width rules (the refactoring panes are capped at 72 chars by +# scripts/check-sample-width.sh), so exempt them here. +rule 'MD013', :line_length => 80, :ignore_code_blocks => true, + :tables => false, :headings => false diff --git a/pages/about.md b/pages/about.md index b70233d..2c42fd9 100644 --- a/pages/about.md +++ b/pages/about.md @@ -22,10 +22,11 @@ criteria: - **Termination** — every computation eventually finishes. The methodology is the lens through which we read code, design APIs, and -prepare a codebase to be co-piloted by LLMs. The [Libraries]({{ '/libraries/' | relative_url }}) -page lists the OSS that puts the methodology into Scala practice, and the -home page introduces [Fixed](https://github.com/Constructive-Programming/fixed) — -a new language being designed around the same principles. +prepare a codebase to be co-piloted by LLMs. The +[Libraries]({{ '/libraries/' | relative_url }}) page lists the OSS that +puts the methodology into Scala practice, and the home page introduces +[Fixed](https://github.com/Constructive-Programming/fixed) — a new +language being designed around the same principles. ## The team @@ -63,7 +64,8 @@ delivering at web scale and web speed, and constantly asking what the type system could do to make wrong programs harder to write. **Find Rodolfo:** [LinkedIn](https://www.linkedin.com/in/rodolfohansen/) · -[GitHub](https://github.com/kryptt) · [Medium](https://rodolfohansen.medium.com/) +[GitHub](https://github.com/kryptt) · +[Medium](https://rodolfohansen.medium.com/) #### Talks @@ -104,9 +106,10 @@ banner, get in touch at ## Working with us We work on architecture-review, fractional-advisory, embedded-IC, and -workshops/training shapes — see the [home page]({{ '/' | relative_url }}#consulting) -for the engagement bullets. We are available for select engagements; -based in the Netherlands; comfortable with EU MSAs and standard NDAs. +workshops/training shapes — see the +[home page]({{ '/' | relative_url }}#consulting) for the engagement bullets. We +are available for select engagements; based in the Netherlands; comfortable +with EU MSAs and standard NDAs. The fastest path is [booking a 30-minute scoping call](https://cal.constructive.dev/rhansen) diff --git a/pages/engagements.md b/pages/engagements.md index 9121e6d..8357e04 100644 --- a/pages/engagements.md +++ b/pages/engagements.md @@ -9,8 +9,9 @@ tags: engagements We have three main offerings. Each has a fixed structure so you know what you are buying; the actual content is shaped on the scoping call. -If your need does not fit any of the three, the [scoping call](https://cal.constructive.dev/rhansen) -is the right place to figure it out — there is no charge for it. +If your need does not fit any of the three, the +[scoping call](https://cal.constructive.dev/rhansen) is the right place to +figure it out — there is no charge for it. ## Which one is right for you @@ -121,4 +122,5 @@ the scoping call. ## Get in touch [Book a 30-minute scoping call](https://cal.constructive.dev/rhansen) -or email [consulting@constructive.dev](mailto:consulting@constructive.dev?subject=Engagement%20inquiry). +or email +[consulting@constructive.dev](mailto:consulting@constructive.dev?subject=Engagement%20inquiry). diff --git a/pages/libraries.md b/pages/libraries.md index 802108e..4b7f91d 100644 --- a/pages/libraries.md +++ b/pages/libraries.md @@ -44,5 +44,6 @@ or moving toward agent-native development with constructive guardrails, [book a 30-minute scoping call](https://cal.constructive.dev/rhansen) or email [consulting@constructive.dev](mailto:consulting@constructive.dev?subject=Scoping%20call). -The libraries above are the ground we stand on; the [consulting offer]({{ '/#consulting' | relative_url }}) -is how we bring it to your codebase. +The libraries above are the ground we stand on; the +[consulting offer]({{ '/#consulting' | relative_url }}) is how we +bring it to your codebase. diff --git a/pages/refactorings/extract-method.md b/pages/refactorings/extract-method.md index 63be413..3573939 100644 --- a/pages/refactorings/extract-method.md +++ b/pages/refactorings/extract-method.md @@ -408,6 +408,3 @@ with hedgehog installed or Docker, and ends with `all properties passed`.
  • Jacob Stanley and contributors. Hedgehog: release with confidence, state-of-the-art property testing. https://github.com/hedgehogqa/haskell-hedgehog and https://github.com/hedgehogqa/scala-hedgehog
  • Mirko Stocker. Scala Refactoring. Master’s thesis, HSR Hochschule für Technik Rapperswil, 2010. https://eprints.ost.ch/id/eprint/286/
  • - - - diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..a65fc3c --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Run the full lint suite against every tracked file. This is the exact same +# configuration that runs locally on commit (bundle exec overcommit --install); +# CI calls this script, so local and CI can't drift. +set -euo pipefail +cd "$(dirname "$0")/.." +# OVERCOMMIT_NO_VERIFY: overcommit records hook signatures in the local +# git config, which a fresh checkout (CI) never has, so skip that check. +# (verify_signatures is already off in .overcommit.yml; this makes the very +# first run work without an interactive `overcommit --sign`.) +export OVERCOMMIT_NO_VERIFY=1 +export GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-CI}" +export GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-ci@example.com}" +exec bundle exec overcommit --run diff --git a/type-on-strap.gemspec b/type-on-strap.gemspec index ee7ab36..637bd65 100644 --- a/type-on-strap.gemspec +++ b/type-on-strap.gemspec @@ -28,7 +28,7 @@ Gem::Specification.new do |spec| * Use _data/ for social and language customization - * Vist https://github.com/sylhare/Type-on-Strap + * Visit https://github.com/sylhare/Type-on-Strap for more info. ----------------------------------------------------------