Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(rm -rf ~/.vscode/extensions/nimblesite.typediagram-0.1.0 ~/.vscode/extensions/nimblesite.typediagram-0.3.0)",
"Read(//Users/christianfindlay/.vscode/extensions/**)"
]
},
"autoMemoryEnabled": false
}
19 changes: 19 additions & 0 deletions .deslop.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# agent-pmo:372ce7f
# Deslop duplication gate — REPO-STANDARDS-SPEC [CI-DESLOP].
# Single source of truth for this repo's duplication budget. Committed, PR-reviewed,
# ratcheted DOWN only (never up without written justification).
# CI runs `deslop .`, which reads this file and exits 3 (tanks the build) when
# duplication exceeds the ceiling below.
# Docs: https://deslop.live/docs/for-ai/

[defaults]
# Exclude ONLY build/coverage artifacts — never source, and NEVER tests. Tests are
# first-class code here and stay in the duplication budget. node_modules/target/build
# are already default-excluded; dist + coverage are this repo's generated-output dirs.
exclude = [
"**/coverage/**",
"**/dist/**",
]

[threshold]
max_duplication_percent = 15.0
10 changes: 7 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"_agent_pmo": "b636503",
"name": "typeDiagram (Node.js)",
"_agent_pmo": "372ce7f",
"name": "typeDiagram (Node.js + Rust)",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22",
"features": {
"ghcr.io/devcontainers/features/rust:1": {}
},
"remoteUser": "vscode",
"postCreateCommand": "make setup",
"customizations": {
Expand All @@ -10,7 +13,8 @@
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"usernamehw.errorlens",
"ms-vscode.vscode-typescript-next"
"ms-vscode.vscode-typescript-next",
"rust-lang.rust-analyzer"
],
"settings": {
"editor.formatOnSave": true,
Expand Down
55 changes: 52 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,67 @@
# [SWR-SEC-ACTION-PINNING] Keeps SHA-pinned actions and npm deps fresh.
# Grouped so each ecosystem opens ONE combined PR per run.
# agent-pmo:372ce7f
# Dependabot — every bump routed to the long-lived `dependabot-upgrades` STAGING
# branch, never straight to main ([GITHUB-DEPENDABOT]).
#
# * VERSION updates -> `target-branch: dependabot-upgrades`. The bump PR opens
# against staging; ci.yml / codeql.yml only trigger on `pull_request: [main]`
# (matched on the PR *base*), so staging PRs run NO build/test/CodeQL.
# * SECURITY updates -> GitHub IGNORES `target-branch` and always opens them
# against `main`; they must be manually staged into the same consolidation
# branch, because privileged auto-merging of PR heads is an unsafe workflow
# pattern.
# * groups -> each ecosystem collapses ALL bump types (patch+minor+major)
# into ONE PR; a parallel `*-security` group does the same for CVE bumps.
#
# The whole batch reaches main via ONE `dependabot-upgrades -> main` consolidation
# PR, where CI + CodeQL run exactly once.
#
# REQUIREMENT: the `dependabot-upgrades` branch must exist, cut from main AFTER
# this file is on main.
version: 2

updates:
# GitHub Actions — applies to every repo with workflows. Version-only (no
# Dependabot advisory channel for actions). Pinned SHAs are a supply-chain target.
- package-ecosystem: github-actions
directory: /
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
actions:
github-actions:
patterns: ["*"]

# npm / Node — package.json (root workspaces + packages/*)
- package-ecosystem: npm
directory: /
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
npm:
applies-to: version-updates
patterns: ["*"]
npm-security:
applies-to: security-updates
patterns: ["*"]

# Cargo / Rust — the incoming Rust workspace (crates/*). Active once a crate with
# dependencies lands; harmless before then.
- package-ecosystem: cargo
directory: /
target-branch: "dependabot-upgrades"
schedule:
interval: weekly
open-pull-requests-limit: 5
labels: ["dependencies"]
groups:
cargo:
applies-to: version-updates
patterns: ["*"]
cargo-security:
applies-to: security-updates
patterns: ["*"]
25 changes: 21 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# agent-pmo:b636503
# agent-pmo:372ce7f
name: CI

on:
Expand All @@ -21,11 +21,16 @@ defaults:
jobs:
ci:
name: CI
# Dependabot version PRs target `dependabot-upgrades`; security PRs that
# GitHub forces to main must be manually staged there. CI runs once, on the
# `dependabot-upgrades -> main` consolidation PR.
# ([GITHUB-DEPENDABOT])
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
# TIMEOUT EXCEPTION: vitest + bundle-size + eslint across 4 workspaces exceeds 10 min on cold npm cache.
timeout-minutes: 15
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Full history so the change-scope step can diff against the PR base.
Expand Down Expand Up @@ -62,7 +67,7 @@ jobs:
echo "==> Scope: WEB (website/docs-only changes)"
fi

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22
cache: npm
Expand Down Expand Up @@ -102,6 +107,18 @@ jobs:
if: steps.scope.outputs.scope == 'full'
run: make ci

# Deslop duplication gate ([CI-DESLOP]). Threshold lives in committed
# .deslop.toml — ratcheted DOWN only. `deslop .` reads it and exits 3 (tanks
# the build) when repo-wide duplication exceeds it. Full scope only (web/docs
# PRs touch no Deslop-scanned source).
- name: Deslop duplication gate
if: steps.scope.outputs.scope == 'full'
env:
DESLOP_VERSION: "0.5.1" # pin — https://github.com/Nimblesite/Deslop/releases
run: |
curl -sSfL "https://github.com/Nimblesite/Deslop/releases/download/v${DESLOP_VERSION}/deslop-${DESLOP_VERSION}-linux-x64.tar.gz" | tar -xz
"./deslop-${DESLOP_VERSION}-linux-x64/deslop" .

# [SWR-GATE-CI] [SWR-GATE-VERIFY-BINARIES] manifest + CLI version contract.
- name: Shipwright acceptance gates
if: steps.scope.outputs.scope == 'full'
Expand All @@ -112,7 +129,7 @@ jobs:
node packages/cli/dist/bin.js --version --json

- name: Upload coverage
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: coverage-report
Expand Down
149 changes: 149 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# agent-pmo:372ce7f
name: CodeQL

# CodeQL static security analysis ([GITHUB-CODE-SCANNING]).
#
# SEPARATE from ci.yml on purpose: CodeQL feeds GitHub code-scanning alerts and
# needs `security-events: write` + a weekly schedule, while ci.yml owns
# lint/test/build. It does NOT overlap with `make lint` (style/correctness) or
# the `npm audit` vuln-gate (vulnerable packages) — CodeQL finds vulnerable CODE.
# Never add security-rule linter plugins that re-cover CodeQL: no doubling up.
#
# Matrix = (languages in this repo) ∩ (CodeQL-supported at skill-run time):
# javascript-typescript — the packages/* monorepo (core, cli, web, vscode)
# rust — the incoming crates/* workspace (build-mode: none)
# actions — always, scans the workflow files themselves
# Action SHAs are kept current by the github-actions Dependabot group.
on:
pull_request:
branches: [main]
schedule:
# Weekly, so newly-published CodeQL queries re-scan even without a push.
- cron: "27 4 * * 1"
# release.yml calls this with gate=true on every v* tag to scan the exact
# released SHA with the current query set and BLOCK publishing on any
# High/Critical finding. The PR scan covers the diff, the weekly scan covers
# query drift, the gated call covers the released commit itself — as a HARD
# gate, not advice: a finding FAILS the release. This replaces the old
# standalone `push: [tags]` scan, which could only file alerts AFTER the
# artifact had already shipped — useless as a gate. [GITHUB-CODE-SCANNING]
workflow_call:
inputs:
gate:
description: >-
When true (release calls), fail the job on any High/Critical finding so
the calling release workflow cannot publish. PR/weekly runs leave this
false and stay advisory (the PR check-failure threshold governs merges).
type: boolean
default: false

permissions:
contents: read

concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 20
# Code scanning (SARIF upload) requires GitHub Advanced Security on PRIVATE
# repos. Gating on public visibility lets a private repo skip cleanly (no red
# X) and self-enable the moment it is made public — no follow-up edit needed.
# Dependabot version PRs target `dependabot-upgrades`; security PRs that
# GitHub forces to main must be manually staged there. CodeQL runs on the
# consolidation PR instead. ([GITHUB-DEPENDABOT])
if: github.event.repository.visibility == 'public' && github.actor != 'dependabot[bot]'
permissions:
security-events: write
actions: read
contents: read
strategy:
fail-fast: false
matrix:
# (repo language ∩ CodeQL-supported at runtime). `build-mode: none` suits
# interpreted langs + rust; it avoids re-compiling what ci already builds.
include:
- language: actions # scans the workflow files themselves
build-mode: none
- language: javascript-typescript
build-mode: none
- language: rust
build-mode: none
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
queries: security-extended
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{ matrix.language }}"
# Drop SARIF on disk so the gate step can read it. `upload` stays on
# (default) so alerts still post to code scanning on every run.
output: sarif-results
# Release gate. `security-severity` is the 0-10 CVSS-style score CodeQL
# attaches to each security rule; >= 7.0 == High or Critical. Enforced ONLY
# on gated (release) calls — PR/weekly runs skip this and stay advisory.
# Caveat: this reads freshly produced SARIF, which does NOT reflect alert
# dismissals — a dismissed false positive re-blocks until excluded via a
# CodeQL config. FAILS CLOSED: missing/malformed SARIF errors, never passes.
# [GITHUB-CODE-SCANNING]
- name: Enforce no high/critical findings (release gate)
if: inputs.gate
shell: bash
env:
SARIF_DIR: sarif-results
SEVERITY_THRESHOLD: "7.0"
run: |-
set -euo pipefail
shopt -s nullglob
# Fail closed: no SARIF means we cannot prove the code is clean.
sarifs=( "${SARIF_DIR}"/*.sarif )
if [ "${#sarifs[@]}" -eq 0 ]; then
echo "::error::CodeQL gate: no SARIF in ${SARIF_DIR}; cannot verify findings — failing closed."
exit 1
fi
offenders=0
for sarif in "${sarifs[@]}"; do
if ! jq -e '.runs' "${sarif}" >/dev/null 2>&1; then
echo "::error::CodeQL gate: ${sarif} is not valid SARIF (no .runs) — failing closed."
exit 1
fi
# Observability: a clean scan logs results=0 with a non-zero
# severity_rules count, proving real SARIF was parsed.
jq -r --arg f "${sarif##*/}" '
([ (.runs[].tool.driver.rules // [])[],
(.runs[].tool.extensions[]?.rules // [])[] ]) as $rules
| "CodeQL gate: \($f): results=\([.runs[].results[]?]|length) severity_rules=\([$rules[]|select(.properties["security-severity"])]|length)"
' "${sarif}"
# CodeQL puts query rules in tool.extensions[].rules (driver.rules is
# empty in CodeQL output); union both, then keep results >= threshold.
hits="$(jq -r --argjson t "${SEVERITY_THRESHOLD}" '
.runs[]
| ( [ (.tool.driver.rules // [])[],
(.tool.extensions[]?.rules // [])[] ]
| map({ key: .id,
value: ((.properties["security-severity"] // "0") | tonumber) })
| from_entries
) as $severity
| .results[]
| select( ($severity[.ruleId] // 0) >= $t )
| .ruleId
' "${sarif}" | sort | uniq -c | sort -rn)"
if [ -n "${hits}" ]; then
echo "::error::High/critical CodeQL findings in ${sarif}:"
echo "${hits}"
offenders=$((offenders + 1))
fi
done
if [ "${offenders}" -gt 0 ]; then
echo "::error::CodeQL gate failed — release blocked. Fix or dismiss-and-exclude the findings, then re-tag."
exit 1
fi
echo "CodeQL gate passed: nothing at or above severity ${SEVERITY_THRESHOLD}."
44 changes: 44 additions & 0 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# agent-pmo:372ce7f
name: Dependabot auto-merge

# Records Dependabot PRs for the long-lived `dependabot-upgrades` staging branch
# ([GITHUB-DEPENDABOT]). Two kinds of PR land here:
#
# * VERSION updates -> Dependabot opens them against `dependabot-upgrades`
# directly (.github/dependabot.yml `target-branch`).
# * SECURITY updates -> GitHub IGNORES `target-branch` for these and ALWAYS
# opens them against the default branch (`main`).
#
# This workflow intentionally does not checkout or merge PR head refs with write
# permissions. That pattern trips CodeQL's untrusted-checkout rule and is unsafe
# for privileged automation. The consolidation PR into `main` is still where the
# full build/test (ci.yml) + CodeQL (codeql.yml) gate runs.
#
# Lives at the repo root so it is present on `dependabot-upgrades` (cut from
# main): for `pull_request` the workflow is read from the PR's base branch, so
# BOTH `main` and the staging branch must carry this file.
on:
pull_request:
branches:
- dependabot-upgrades
- main

permissions:
contents: read
pull-requests: read

jobs:
sweep:
name: Record Dependabot PR
if: github.actor == 'dependabot[bot]'
# Deliberately the standard runner, NOT a larger/paid one: a trivial merge
# bot must not consume CI minutes meant for the real build matrix.
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Record Dependabot PR for manual staging
env:
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
set -euo pipefail
echo "::notice::Dependabot PR requires staging into dependabot-upgrades: ${PR_URL}"
Loading
Loading