+
+
+
+
Deterministic spec governance for Git
+
+ Govern the specs your code is supposed to honor.
+
+
+ SpecGov keeps requirements, ADRs, product docs, and spec folders
+ aligned with pull requests without API keys, hosted services, or
+ model calls.
+
+
+
+
+
+
+
+ No new spec format
+ Bring your existing docs, ADRs, `.specs`, Kiro, or Spec Kit
+ files.
+
+
+ No secrets
+ Runs locally and in CI with deterministic checks.
+
+
+ No surprise blocking
+ Start advisory, then opt into strict enforcement.
+
+
+
+
+
+
The drift problem
+
Specs fail when Git can bypass them.
+
+
+
+ Modern teams write requirements, ADRs, implementation plans, and
+ AI-facing specs, then review code in a separate workflow. SpecGov
+ connects those layers so reviewers can see when a pull request
+ changes implementation without updating the artifacts that explain
+ the intended behavior.
+
+
+ It is intentionally small: a YAML manifest, deterministic file
+ discovery, code-to-spec mappings, lifecycle metadata, Markdown
+ reports, JSON trace output, and a GitHub Action wrapper.
+
+
+
+
+
+
+
Mechanics
+
+ A manifest turns your repo into a governed map.
+
+
+
+
+ 01
+ Declare artifacts
+
+ Mark docs, ADRs, requirements, and spec folders as governed
+ artifacts.
+
+
+
+ 02
+ Map code paths
+
+ Connect implementation directories to the artifacts that must move
+ with them.
+
+
+
+ 03
+ Check pull requests
+
+ Report missing spec impact, unmapped code, lifecycle gaps, and
+ drift.
+
+
+
+
+
+
+
+
Quick start
+
+ Install from source, then govern any repo.
+
+
+ SpecGov is pre-release. Until the first npm package and version tag
+ are published, use the source build.
+
+
+
+
Install
+
git clone https://github.com/paladini/specgov.git
+cd specgov
+npm ci
+npm run build
+npm link
+
Use in your repository
+
specgov init
+specgov scan
+specgov check-pr --changed-file src/auth/session.ts
+specgov trace --out .specgov.trace.json
+specgov drift
+
+
+
+
+
+
Manifest
+
Adopt with one `.specgov.yml` file.
+
+ Start with one high-value mapping. Expand gradually after the first
+ advisory findings make sense to reviewers.
+
+
+
+
version: 1
+mode: advisory
+
+artifacts:
+ - path: "docs/**/*.md"
+ kind: documentation
+ owner: docs
+ - path: "adr/**/*.md"
+ kind: decision
+ owner: architecture
+
+mappings:
+ - code: "src/auth/**"
+ specs:
+ - "docs/auth/**"
+ - "adr/auth/**"
+
+rules:
+ require_spec_impact_for_code_changes: true
+ stale_after_days: 180
+
+
+
+
+
+
Commands
+
+ Five commands cover setup, review, and maintenance.
+
+
+
+
+ specgov init
+ Create a starter manifest.
+
+
+ specgov scan
+ Discover governed artifacts and lifecycle findings.
+
+
+ specgov check-pr
+ Compare changed files against code-to-spec mappings.
+
+
+ specgov trace
+ Write machine-readable trace JSON.
+
+
+ specgov drift
+ Report stale, empty, orphaned, or superseded artifacts.
+
+
+
+
+
+
+
GitHub Action
+
Put governance in the pull request.
+
+ Advisory mode gives teams visibility without blocking adoption.
+ Strict mode turns warnings into a failing check.
+
+
+
+
name: SpecGov
+
+on:
+ pull_request:
+
+jobs:
+ specgov:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - uses: paladini/specgov@main
+ with:
+ mode: advisory
+ base-ref: ${{ github.event.pull_request.base.sha }}
+ head-ref: ${{ github.event.pull_request.head.sha }}
+
+
+
+
+
+
Adoption paths
+
Use your existing repository style.
+
+
+
+ Docs-only
+ Map `src/**` to `docs/**` for product or engineering docs.
+
+ View example
+
+
+
+ ADR-heavy
+
+ Require code areas to move with matching ADR and product docs.
+
+
+ View example
+
+
+
+ Framework folders
+ Govern `.specs`, `.kiro/specs`, or custom framework folders.
+
+ View example
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/script.js b/docs/script.js
new file mode 100644
index 0000000..47f88cc
--- /dev/null
+++ b/docs/script.js
@@ -0,0 +1,84 @@
+/* global document, navigator, window */
+
+const blocks = document.querySelectorAll(".copy-block");
+
+for (const block of blocks) {
+ const shell = document.createElement("div");
+ shell.className = "copy-shell";
+ block.before(shell);
+ shell.append(block);
+
+ const button = document.createElement("button");
+ button.className = "copy-button";
+ button.type = "button";
+ button.textContent = "Copy";
+ button.setAttribute("aria-label", "Copy code block");
+
+ button.addEventListener("click", async () => {
+ const code = block.querySelector("code");
+ const text = code?.textContent ?? "";
+ const copied = await copyText(text.trim());
+ if (copied) {
+ showTemporaryStatus(button, "Copied");
+ } else if (selectCode(code)) {
+ showTemporaryStatus(button, "Selected");
+ } else {
+ button.textContent = "Select";
+ }
+ });
+
+ shell.append(button);
+}
+
+async function copyText(text) {
+ try {
+ if (navigator.clipboard?.writeText) {
+ await navigator.clipboard.writeText(text);
+ return true;
+ }
+ } catch {
+ return fallbackCopy(text);
+ }
+
+ return fallbackCopy(text);
+}
+
+function fallbackCopy(text) {
+ const textarea = document.createElement("textarea");
+ textarea.value = text;
+ textarea.setAttribute("readonly", "");
+ textarea.style.position = "fixed";
+ textarea.style.left = "-9999px";
+ document.body.append(textarea);
+ textarea.select();
+
+ try {
+ return document.execCommand("copy");
+ } finally {
+ textarea.remove();
+ }
+}
+
+function selectCode(code) {
+ if (!code) {
+ return false;
+ }
+
+ const selection = window.getSelection();
+ if (!selection) {
+ return false;
+ }
+
+ const range = document.createRange();
+ range.selectNodeContents(code);
+ selection.removeAllRanges();
+ selection.addRange(range);
+ return true;
+}
+
+function showTemporaryStatus(button, status) {
+ button.textContent = status;
+ window.setTimeout(() => {
+ button.textContent = "Copy";
+ }, 1400);
+}
diff --git a/docs/styles.css b/docs/styles.css
new file mode 100644
index 0000000..c51d9a2
--- /dev/null
+++ b/docs/styles.css
@@ -0,0 +1,522 @@
+:root {
+ color-scheme: light;
+ --page: oklch(98% 0.012 125);
+ --paper: oklch(99.2% 0.006 125);
+ --ink: oklch(19% 0.028 150);
+ --muted: oklch(42% 0.032 155);
+ --line: oklch(84% 0.036 145);
+ --green: oklch(55% 0.15 150);
+ --green-dark: oklch(31% 0.1 150);
+ --blue: oklch(45% 0.13 245);
+ --coral: oklch(59% 0.17 30);
+ --shadow: 0 24px 60px color-mix(in oklch, var(--ink) 12%, transparent);
+ --radius: 8px;
+ --space-xs: clamp(0.5rem, 0.45rem + 0.2vw, 0.75rem);
+ --space-sm: clamp(0.75rem, 0.65rem + 0.4vw, 1rem);
+ --space-md: clamp(1rem, 0.8rem + 0.8vw, 1.5rem);
+ --space-lg: clamp(1.5rem, 1.05rem + 1.8vw, 3rem);
+ --space-xl: clamp(3rem, 1.9rem + 4.5vw, 6rem);
+ --max: 1180px;
+ font-family: "Aptos", "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ font-kerning: normal;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ background:
+ linear-gradient(
+ 90deg,
+ color-mix(in oklch, var(--line) 28%, transparent) 1px,
+ transparent 1px
+ )
+ 0 0 / 72px 72px,
+ linear-gradient(
+ color-mix(in oklch, var(--line) 26%, transparent) 1px,
+ transparent 1px
+ )
+ 0 0 / 72px 72px,
+ var(--page);
+ color: var(--ink);
+ font-size: 1rem;
+ line-height: 1.6;
+}
+
+a {
+ color: inherit;
+}
+
+code,
+pre {
+ font-family:
+ "Cascadia Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+}
+
+.skip-link {
+ position: fixed;
+ left: 1rem;
+ top: 1rem;
+ z-index: 20;
+ transform: translateY(-160%);
+ background: var(--ink);
+ color: var(--paper);
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius);
+}
+
+.skip-link:focus {
+ transform: translateY(0);
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.85rem max(1rem, env(safe-area-inset-left));
+ border-bottom: 1px solid color-mix(in oklch, var(--line) 72%, transparent);
+ background: color-mix(in oklch, var(--paper) 88%, transparent);
+ backdrop-filter: blur(16px);
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.7rem;
+ color: var(--ink);
+ font-weight: 800;
+ text-decoration: none;
+}
+
+.brand-mark {
+ display: inline-grid;
+ width: 2.1rem;
+ height: 2.1rem;
+ place-items: center;
+ border: 1px solid var(--ink);
+ background: var(--green);
+ color: var(--paper);
+ font-size: 0.78rem;
+ letter-spacing: 0;
+}
+
+.nav-links {
+ display: none;
+ align-items: center;
+ gap: 1.2rem;
+ color: var(--muted);
+ font-size: 0.92rem;
+ font-weight: 650;
+}
+
+.nav-links a {
+ text-decoration: none;
+}
+
+.nav-links a:hover,
+.nav-links a:focus-visible {
+ color: var(--green-dark);
+}
+
+.hero {
+ position: relative;
+ min-height: calc(100svh - 4rem);
+ display: grid;
+ align-items: center;
+ overflow: hidden;
+ padding: clamp(2.4rem, 5vw, 5rem) 1rem clamp(2.5rem, 5vw, 5rem);
+ border-bottom: 1px solid var(--line);
+}
+
+.hero::after {
+ content: "";
+ position: absolute;
+ inset: auto 0 0;
+ height: 32%;
+ background: linear-gradient(
+ to top,
+ color-mix(in oklch, var(--page) 94%, transparent),
+ transparent
+ );
+ pointer-events: none;
+}
+
+.hero-visual {
+ position: absolute;
+ right: max(-30rem, -42vw);
+ top: clamp(5rem, 12vh, 9rem);
+ width: min(980px, 118vw);
+ max-width: none;
+ filter: drop-shadow(var(--shadow));
+ opacity: 0.88;
+}
+
+.hero-copy {
+ position: relative;
+ z-index: 1;
+ width: min(720px, 100%);
+ margin-inline: auto;
+ padding-right: clamp(0rem, 22vw, 18rem);
+}
+
+.eyebrow {
+ margin: 0 0 0.9rem;
+ color: var(--green-dark);
+ font-size: 0.78rem;
+ font-weight: 850;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1 {
+ max-width: 15ch;
+ margin-bottom: 1.25rem;
+ font-size: clamp(2.8rem, 5.6vw, 5.2rem);
+ line-height: 0.98;
+ letter-spacing: 0;
+}
+
+h2 {
+ max-width: 14ch;
+ margin-bottom: 1rem;
+ font-size: clamp(2.1rem, 5vw, 4.8rem);
+ line-height: 1;
+ letter-spacing: 0;
+}
+
+h3 {
+ margin-bottom: 0.55rem;
+ font-size: 1.22rem;
+ line-height: 1.2;
+}
+
+.lede {
+ max-width: 58ch;
+ margin-bottom: 1.5rem;
+ color: color-mix(in oklch, var(--ink) 84%, var(--green-dark));
+ font-size: clamp(1.12rem, 1.5vw, 1.35rem);
+ line-height: 1.55;
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.8rem;
+}
+
+.button {
+ display: inline-flex;
+ min-height: 44px;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--ink);
+ border-radius: var(--radius);
+ font-weight: 800;
+ text-decoration: none;
+ transition:
+ transform 160ms ease,
+ background-color 160ms ease;
+}
+
+.button:hover,
+.button:focus-visible {
+ transform: translateY(-2px);
+}
+
+.button.primary {
+ background: var(--ink);
+ color: var(--paper);
+}
+
+.button.secondary {
+ background: color-mix(in oklch, var(--paper) 86%, transparent);
+ color: var(--ink);
+}
+
+.signal-band {
+ display: grid;
+ gap: 1px;
+ max-width: var(--max);
+ margin: -3rem auto var(--space-xl);
+ padding-inline: 1rem;
+}
+
+.signal-band div {
+ display: grid;
+ gap: 0.3rem;
+ padding: 1.1rem;
+ border: 1px solid var(--line);
+ background: var(--paper);
+ box-shadow: 0 10px 30px color-mix(in oklch, var(--ink) 7%, transparent);
+}
+
+.signal-band strong {
+ font-size: 0.95rem;
+}
+
+.signal-band span {
+ color: var(--muted);
+}
+
+.section {
+ max-width: var(--max);
+ margin-inline: auto;
+ padding: var(--space-xl) 1rem;
+}
+
+.section-heading {
+ display: grid;
+ gap: 0.4rem;
+ margin-bottom: var(--space-lg);
+}
+
+.section-heading p:not(.eyebrow) {
+ max-width: 56ch;
+ color: var(--muted);
+}
+
+.section-heading.compact {
+ align-self: start;
+ margin-bottom: 0;
+}
+
+.two-column {
+ display: grid;
+ gap: var(--space-md);
+ color: var(--muted);
+ font-size: 1.08rem;
+}
+
+.flow-grid,
+.recipe-grid {
+ display: grid;
+ gap: 1rem;
+}
+
+.flow-step,
+.recipe {
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ background: var(--paper);
+ padding: clamp(1rem, 2.4vw, 1.7rem);
+}
+
+.step-number {
+ display: inline-block;
+ margin-bottom: 1.25rem;
+ color: var(--coral);
+ font-weight: 900;
+ font-variant-numeric: tabular-nums;
+}
+
+.flow-step p,
+.recipe p {
+ margin-bottom: 0;
+ color: var(--muted);
+}
+
+.split-section {
+ display: grid;
+ gap: var(--space-lg);
+ align-items: start;
+}
+
+.code-panel {
+ border: 1px solid color-mix(in oklch, var(--ink) 88%, var(--green));
+ border-radius: var(--radius);
+ background: oklch(17% 0.026 155);
+ color: oklch(95% 0.018 130);
+ overflow: hidden;
+ box-shadow: var(--shadow);
+}
+
+.panel-title {
+ padding: 0.78rem 1rem 0;
+ color: oklch(74% 0.13 150);
+ font-size: 0.76rem;
+ font-weight: 850;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+pre {
+ margin: 0;
+ overflow-x: auto;
+ padding: 1rem;
+ font-size: 0.9rem;
+ line-height: 1.55;
+}
+
+.copy-shell {
+ position: relative;
+}
+
+.copy-button {
+ position: absolute;
+ top: 0.65rem;
+ right: 0.65rem;
+ min-height: 36px;
+ border: 1px solid color-mix(in oklch, var(--paper) 32%, transparent);
+ border-radius: 6px;
+ background: color-mix(in oklch, var(--paper) 8%, transparent);
+ color: oklch(96% 0.015 130);
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.command-list {
+ display: grid;
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ background: var(--paper);
+}
+
+.command-row {
+ display: grid;
+ gap: 0.5rem;
+ padding: 1rem;
+ border-bottom: 1px solid var(--line);
+}
+
+.command-row:last-child {
+ border-bottom: 0;
+}
+
+.command-row code {
+ color: var(--green-dark);
+ font-weight: 850;
+}
+
+.command-row span {
+ color: var(--muted);
+}
+
+.recipe {
+ display: grid;
+ gap: 0.7rem;
+}
+
+.recipe a {
+ color: var(--blue);
+ font-weight: 850;
+}
+
+.site-footer {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-width: var(--max);
+ margin-inline: auto;
+ padding: var(--space-lg) 1rem;
+ border-top: 1px solid var(--line);
+ color: var(--muted);
+}
+
+.site-footer div {
+ display: grid;
+ gap: 0.2rem;
+}
+
+.site-footer strong {
+ color: var(--ink);
+}
+
+.site-footer a {
+ color: var(--green-dark);
+ font-weight: 850;
+}
+
+@media (min-width: 720px) {
+ .site-header {
+ padding-inline: 1.5rem;
+ }
+
+ .nav-links {
+ display: flex;
+ }
+
+ .hero {
+ padding-inline: 1.5rem;
+ }
+
+ .hero-copy {
+ margin-left: max(1rem, calc((100vw - var(--max)) / 2));
+ margin-right: 0;
+ }
+
+ .hero-visual {
+ right: max(-36rem, calc((100vw - var(--max)) / 2 - 21rem));
+ width: min(900px, 70vw);
+ }
+
+ .signal-band {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .two-column,
+ .split-section {
+ grid-template-columns: minmax(0, 0.9fr) minmax(340px, 1.1fr);
+ }
+
+ .flow-grid,
+ .recipe-grid {
+ grid-template-columns: repeat(3, 1fr);
+ }
+
+ .command-row {
+ grid-template-columns: minmax(220px, 0.55fr) minmax(0, 1fr);
+ align-items: center;
+ }
+
+ .site-footer {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+}
+
+@media (max-width: 719px) {
+ .hero-visual {
+ right: -34rem;
+ opacity: 0.14;
+ }
+
+ .hero-copy {
+ padding-right: 0;
+ }
+
+ h1 {
+ max-width: 10.5ch;
+ font-size: clamp(2.75rem, 13vw, 3.6rem);
+ line-height: 0.98;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ transition: none !important;
+ }
+}