diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1fd657e..af48b45 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "forgekit", "displayName": "Forge", - "version": "0.4.0", + "version": "0.5.0", "description": "One config, every AI coding tool — cognitive substrate, tools, crew, guards, atlas, lean, recall from one source.", "author": { "name": "CodeWithJuber" }, "license": "MIT", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 73b316e..2799d83 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "forgekit", - "version": "0.4.0", + "version": "0.5.0", "description": "One config, every AI coding tool — cognitive substrate, MCP tools, guards, atlas, recall, and routing from one source.", "author": { "name": "CodeWithJuber", "url": "https://github.com/CodeWithJuber" }, "homepage": "https://github.com/CodeWithJuber/forgekit#readme", diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml new file mode 100644 index 0000000..4bd1a35 --- /dev/null +++ b/.github/workflows/bump.yml @@ -0,0 +1,67 @@ +# One-click release kickoff: Actions -> "Bump version" -> Run workflow. +# Runs the tests, bumps every version field via scripts/bump.mjs (package.json, +# package-lock.json, both plugin manifests, CITATION.cff, landing page, CHANGELOG +# rotation), commits "chore(release): vX.Y.Z", tags vX.Y.Z, and pushes commit + tag. +# +# Pushing the v* tag is what normally triggers release.yml — but pushes made with +# the default GITHUB_TOKEN deliberately do NOT trigger other workflows (GitHub's +# recursion guard). So this workflow also dispatches release.yml on the new tag +# explicitly, which needs `actions: write` (granted below). No PAT required. +name: Bump version + +on: + workflow_dispatch: + inputs: + bump: + description: "Bump type (auto = conventional commits since last tag: BREAKING -> major, feat -> minor, else patch)" + type: choice + default: auto + options: + - auto + - patch + - minor + - major + +permissions: + contents: write # push the release commit + tag + actions: write # dispatch release.yml on the new tag + +concurrency: + group: bump + cancel-in-progress: false + +jobs: + bump: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # full history + tags so "auto" can read commits since the last tag + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test # never tag a broken tree + - name: Bump version fields + rotate CHANGELOG + id: bump + env: + BUMP: ${{ inputs.bump }} + run: | + VERSION="$(node scripts/bump.mjs "$BUMP")" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Commit, tag, push + env: + VERSION: ${{ steps.bump.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "chore(release): v$VERSION" + git tag -a "v$VERSION" -m "v$VERSION" + git push origin HEAD "v$VERSION" + - name: Trigger the release workflow on the new tag + env: + GH_TOKEN: ${{ github.token }} + VERSION: ${{ steps.bump.outputs.version }} + run: gh workflow run release.yml --ref "v$VERSION" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab35964..cd2c215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ -# CI gate for every push and PR: test matrix (Node 18/20/22), Biome lint+format, ShellCheck, -# the zero-runtime-dependency assertion, and dependency review. All must pass to merge. +# CI gate for every push and PR: test matrix (Node 20/22 — matches the ">=20" engines +# field; 18 is EOL), Biome lint+format, ShellCheck, the zero-runtime-dependency assertion, +# the version-drift guard, and dependency review. All must pass to merge. name: CI on: @@ -17,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - node: [18, 20, 22] + node: [20, 22] steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 @@ -58,6 +59,15 @@ jobs: - run: node -e "process.exit(Object.keys(require('./package.json').dependencies||{}).length)" - run: npm pack --dry-run + version-drift: + name: Version fields agree + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + # package.json, package-lock.json, .claude-plugin/plugin.json, .codex-plugin/plugin.json, + # and CITATION.cff must all carry the same version (scripts/bump.mjs keeps them in sync). + - run: node scripts/bump.mjs check + dependency-review: name: Dependency review runs-on: ubuntu-latest diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..42b45d9 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,32 @@ +# CodeQL static analysis for the JavaScript sources (src/, hooks/, test/). Findings land +# in Security -> Code scanning. This is CodeQL "advanced setup": if the repo ever had +# "default setup" enabled (Settings -> Code security), disable it there so the two don't +# fight over the same analysis slot. +name: CodeQL + +on: + push: + branches: [main, master] + pull_request: + schedule: + - cron: "23 4 * * 1" # weekly, Monday 04:23 UTC — catches new queries, not just new code + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (javascript-typescript) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload SARIF results to code scanning + steps: + - uses: actions/checkout@v7 + - uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + build-mode: none # interpreted — nothing to compile + - uses: github/codeql-action/analyze@v4 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69a1f12..ab8853a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,17 @@ -# Push a v* tag -> test -> publish to public npm (with provenance) -> cut a GitHub Release. -# Requires one repo secret: NPM_TOKEN (an npm "Automation" token). See docs/RELEASING.md. +# Runs on a v* tag push, or via workflow_dispatch ON A TAG REF (bump.yml uses the +# dispatch path because pushes made with the default GITHUB_TOKEN never trigger +# other workflows). Flow: test -> assert tag == package.json version -> publish to +# public npm (with provenance) -> cut a GitHub Release. +# +# npm publish is SKIPPED with a warning — not failed — when the NPM_TOKEN secret is +# missing, so the GitHub Release is still created. Add an npm "Automation" token as +# the NPM_TOKEN repo secret to enable publishing. See docs/RELEASING.md. name: Release on: push: tags: ["v*"] + workflow_dispatch: # dispatched by bump.yml with --ref vX.Y.Z; must target a tag permissions: contents: read @@ -15,7 +22,15 @@ jobs: permissions: contents: write # create the GitHub Release id-token: write # npm provenance (supply-chain attestation) + env: + # secrets are not directly usable in step `if:`, so surface presence here. + HAS_NPM_TOKEN: ${{ secrets.NPM_TOKEN != '' }} steps: + - name: Assert this run targets a v* tag + if: github.ref_type != 'tag' + run: | + echo "::error::Release must run on a v* tag ref (got '${{ github.ref }}'). Use the 'Bump version' workflow to cut one." + exit 1 - uses: actions/checkout@v7 with: fetch-depth: 0 # full history so release notes can diff from the last tag @@ -26,9 +41,24 @@ jobs: cache: npm - run: npm ci - run: npm test - - run: npm publish --provenance --access public + - name: Assert tag matches package.json version + env: + TAG: ${{ github.ref_name }} + run: | + PKG="v$(node -p "require('./package.json').version")" + if [ "$TAG" != "$PKG" ]; then + echo "::error::Tag $TAG does not match package.json version $PKG. Re-run the 'Bump version' workflow instead of tagging by hand." + exit 1 + fi + - name: Publish to npm (with provenance) + if: env.HAS_NPM_TOKEN == 'true' + run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Skip npm publish (NPM_TOKEN not set) + if: env.HAS_NPM_TOKEN != 'true' + run: | + echo "::warning::NPM_TOKEN secret is not set — SKIPPING npm publish. The GitHub Release is still created. To publish, add an npm Automation token as the NPM_TOKEN repo secret (Settings -> Secrets and variables -> Actions) and re-run this workflow. See docs/RELEASING.md." - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/repo-settings.yml b/.github/workflows/repo-settings.yml index 8402fc2..16df971 100644 --- a/.github/workflows/repo-settings.yml +++ b/.github/workflows/repo-settings.yml @@ -3,15 +3,18 @@ # # NOTE: editing repo settings needs an ADMIN-scoped token. The default GITHUB_TOKEN # cannot be granted `administration`, so add a fine-grained PAT (Administration: write, -# Metadata: read) as the repo secret ADMIN_TOKEN. Without it the job will 403 — in that -# case just run the two `gh` commands below locally once (they need no workflow): +# Metadata: read) as the repo secret ADMIN_TOKEN. Without it the job SKIPS with a +# warning (soft fail) — in that case just run the two `gh` commands below locally once +# (they need no workflow): # # gh repo edit CodeWithJuber/forgekit \ # --description "One config for every AI coding agent — cross-tool config + a cognitive substrate (memory, blast-radius, guardrails) for Claude Code, Codex, Cursor, Gemini, Aider, and more." \ # --homepage "https://github.com/CodeWithJuber/forgekit#readme" \ -# --add-topic claude-code --add-topic ai-coding --add-topic ai-agents --add-topic mcp \ -# --add-topic agents-md --add-topic cross-tool --add-topic cognitive-substrate \ -# --add-topic developer-tools --add-topic cli --add-topic codex --add-topic cursor +# --add-topic ai-agents --add-topic claude-code --add-topic codex --add-topic cursor \ +# --add-topic developer-tools --add-topic llm --add-topic memory --add-topic mcp \ +# --add-topic agent-memory --add-topic cost-optimization --add-topic code-quality \ +# --add-topic cli --add-topic zero-dependency --add-topic crdt --add-topic team-memory \ +# --add-topic ai-coding --add-topic agents-md --add-topic cross-tool --add-topic cognitive-substrate # gh api --method PATCH /repos/CodeWithJuber/forgekit -F has_discussions=true name: Repo settings @@ -27,15 +30,22 @@ jobs: steps: - name: Apply About, topics, and Discussions env: - GH_TOKEN: ${{ secrets.ADMIN_TOKEN || github.token }} + GH_TOKEN: ${{ secrets.ADMIN_TOKEN }} REPO: ${{ github.repository }} run: | + if [ -z "$GH_TOKEN" ]; then + echo "::warning::ADMIN_TOKEN secret is not set — skipping. Editing repo settings needs a fine-grained PAT (Administration: write, Metadata: read) saved as the ADMIN_TOKEN repo secret; the default GITHUB_TOKEN cannot be granted 'administration'. Alternatively run the gh commands from this workflow's header comment locally." + exit 0 + fi set -e + # 19 topics (GitHub caps at 20): discoverability terms first, project-specific last. gh repo edit "$REPO" \ --description "One config for every AI coding agent — cross-tool config + a cognitive substrate (memory, blast-radius, guardrails) for Claude Code, Codex, Cursor, Gemini, Aider, and more." \ --homepage "https://github.com/CodeWithJuber/forgekit#readme" \ - --add-topic claude-code --add-topic ai-coding --add-topic ai-agents --add-topic mcp \ - --add-topic agents-md --add-topic cross-tool --add-topic cognitive-substrate \ - --add-topic developer-tools --add-topic cli --add-topic codex --add-topic cursor + --add-topic ai-agents --add-topic claude-code --add-topic codex --add-topic cursor \ + --add-topic developer-tools --add-topic llm --add-topic memory --add-topic mcp \ + --add-topic agent-memory --add-topic cost-optimization --add-topic code-quality \ + --add-topic cli --add-topic zero-dependency --add-topic crdt --add-topic team-memory \ + --add-topic ai-coding --add-topic agents-md --add-topic cross-tool --add-topic cognitive-substrate gh api --method PATCH "/repos/$REPO" -F has_discussions=true echo "Applied About, topics, and Discussions for $REPO." diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..45431e4 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,35 @@ +# OSSF Scorecard: weekly supply-chain posture check (branch protection, token permissions, +# pinned dependencies, ...). publish_results feeds the public viewer/API at +# https://scorecard.dev/viewer/?uri=github.com/CodeWithJuber/forgekit — required for the +# Scorecard badge. Results also land in Security -> Code scanning as SARIF. +# Scorecard itself will flag tag-pinned actions; this repo pins to major tags and lets +# Dependabot track them (see .github/dependabot.yml) — accepted trade-off, documented here. +name: Scorecard + +on: + push: + branches: [master] # default branch only — publish_results requires it + schedule: + - cron: "30 5 * * 1" # weekly, Monday 05:30 UTC + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write # upload SARIF to code scanning + id-token: write # sign results so scorecard.dev will publish them + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false # scorecard checks for exactly this + - uses: ossf/scorecard-action@v2 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + - uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: results.sarif diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..67dcc6b --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,30 @@ +# Secret scanning gate: gitleaks over the FULL git history on every push and PR. +# Deliberately blocking — a real credential in the tree or history should fail the build, +# not warn. (Rotate first, then rewrite/land; a red X is the cheap part of a leak.) +# +# Why this passes on our own redaction tests: test/_fixtures.js assembles secret-LOOKING +# strings at RUNTIME via .join() precisely so no secret-format literal exists in any blob +# for a scanner to match. Verified clean with gitleaks 8.16 across the whole history and +# working tree before this workflow was added — no .gitleaks.toml allowlist is needed. +name: Security + +on: + push: + branches: [main, master] + pull_request: + +permissions: + contents: read + +jobs: + gitleaks: + name: Secret scan (gitleaks) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # gitleaks scans commit history, not just the checkout + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ github.token }} # PR comments / job summary + # GITLEAKS_LICENSE is only required for ORG-owned repos; this repo is user-owned. diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index bb749a5..5bfb7ef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -5,6 +5,7 @@ name: Stale on: schedule: - cron: "0 3 * * *" + workflow_dispatch: # allow a manual run to test the config permissions: issues: write diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 30e4277..78d3565 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -67,6 +67,21 @@ Stop-guard, so it works whether or not the model invokes it), **recall** (memory `substrate_check` / `predict_impact` / `assumption_gate`) composes atlas, preflight, route, scope, cortex, and verify into one pre-action contract before mutating work. +**PCM ledger** (`.forge/ledger/`; decision recorded in +[`docs/adr/0006-proof-carrying-memory.md`](docs/adr/0006-proof-carrying-memory.md)) — +the convergent store all memory subsystems write to. Every stored unit (cortex lesson, +`recall`/`brain` fact, reuse artifact, design fingerprint, doom-loop diagnosis) is a +content-addressed *claim*: claim bytes are a pure function of (kind, body, scope) — +byte-identical on every replica — evidence and tombstones are append-only hash-deduped +logs, and confidence (`val`) is a decayed Beta posterior moved only by independent +oracles (tests, CI, human accept/revert). Merge is a join-semilattice (property-tested: +commutative, associative, idempotent), so teammate ledgers converge in any order over +plain git — `forge init` emits the union-merge `.gitattributes` rule; `forge ledger +merge` folds in any other ledger tree. The legacy stores (`lessons/`, `recall`, `brain`) +remain the read path; the ledger is where their events converge. Surface: `forge ledger +stats | verify | show | blame | query | merge | import` (`--personal` for the +per-user ledger beside the global recall store). + ## Component map — the reuse ledger (30 components) **Reuse (rename + swap brand token, logic unchanged):** @@ -132,6 +147,17 @@ forgekit/ sync.js # emitter (source → per-tool targets); hash + DO-NOT-EDIT doctor.js # health checks emit/ # one module per tool (claude, codex, cursor, gemini, aider, copilot, windsurf, zed, continue) + mcp + ledger.js # PCM core: content-addressed claims, oracle taxonomy, decayed Beta val, Eq. 3 retrieval, semilattice merge (ADR-0006) + ledger_store.js # git-native on-disk ledger (.forge/ledger/): sharded claims, append-only evidence/tombstone logs, normal-form verify + ledger_bridge.js # legacy-store bridge: cortex/recall/brain shadow-writes + idempotent `ledger import` + reuse.js # proof-carrying artifact cache: fingerprint (MinHash+LSH), exact→near→adapt→miss ladder, atlas revalidation + context.js # budgeted context assembly + completeness gate: R(edit) set cover, compression ladder, computed missing-set + diagnose.js # doom-loop diagnosis: normalized failure signatures; 3× = diagnosis claim + one-tier escalation + imagine.js # consequence simulation (Eq. 4): predicted breaks + minimal dry-run suite via greedy set cover + uifingerprint.js # deterministic design fingerprint + slop-distance / conformance gate (no LLM, no screenshots) + dash.js # localhost-only read-only dashboard over the ledger, metrics, and blast radius (node:http, one HTML page) + metrics.js # stage-tagged .forge/metrics.jsonl — the measured events every cost figure is computed from + cost_report.js # per-stage cost factors as pure arithmetic over metrics.jsonl; composes ONLY measured stages source/ rules.json # THE canonical rules source (git · testing · security · style) substrate.json # cognitive-substrate defaults (thresholds, routing, llm knobs) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d980a..9808ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,42 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.5.0] - 2026-07-07 + ### Added +- Security & OSS hardening: CodeQL, gitleaks secret-scan (blocking; verified clean on the + full history), and OSSF Scorecard workflows; refreshed repo topics; SECURITY.md now + states the supported line (0.5.x) and documents the ledger's forgery-resistance + properties (content-hash verification; oracle weights never trusted from records). +- **UI fingerprints resolve CSS `var()` indirection**, so design systems declared as + custom properties fingerprint fully (the dashboard now reads as a 6-value 4px scale + with two radius levels instead of one lonely spacing value), and the five taste + profiles gain machine-readable constraint JSONs (`global/taste/.json`) wired + into `forge uicheck design --taste ` — with auto-pickup from a + `forge taste`-managed DESIGN.md. Prose steers generation; the JSON is what the gate + checks. +- **One-click release automation.** `scripts/bump.mjs` (node stdlib only, unit-tested) + bumps every version field in one shot — `package.json`, `package-lock.json`, both + plugin manifests, `CITATION.cff`, the landing page — rotates the CHANGELOG + `[Unreleased]` section under a dated heading, and prints the new version; + `npm run bump -- ` (auto = conventional commits since the last + tag: BREAKING → major, feat → minor, else patch). The new `bump.yml` workflow makes a + release one click from the Actions tab (commit + tag + dispatch of `release.yml`); + `release.yml` now soft-skips npm publish when `NPM_TOKEN` is missing instead of + failing, and CI gained a version-drift guard (`node scripts/bump.mjs check`). +- **Benchmark harness (`npm run bench`) + measured results doc.** `bench/bench.mjs` + (node stdlib only) measures the substrate primitives as medians of N runs after + warmup — atlas build/incremental/impact latency on this repo, ledger + mint+put/loadClaims/mergeDirs/val() on seeded synthetic fixtures, reuse fingerprint + + exact/near-LSH lookup at 100 and 1000 artifacts, `assemble()` and full + `substrateCheck` wall time — and writes the tables plus an environment block into + `reports/benchmarks.md`. The same run scores `impact()` precision/recall/F1 against a + committed, hand-labeled case set from this repo's real import graph + (`bench/impact_cases.mjs`, every reference cited; one known-miss alias case kept in on + purpose), reported next to — never blended with — the paper prototype's + mutation-derived numbers, plus a structural-only contrast with adjacent tools (note + stores, LLM gateways, plain RAG), every row checkable from the named source. - **Loop closure (P5 of the substrate-v2 plan): doom-loop diagnosis, imagination, CUSUM drift, checkpoint cadence.** `forge diagnose ""` hashes each failure into a signature (line numbers, addresses, timestamps, and absolute paths normalized out) and @@ -18,8 +52,12 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). static half of the consequence simulator (paper Eq. 4): entities → blast radius → predicted breaks with confidence, plus the minimal dry-run test suite via weighted greedy set cover (weight = file size as a duration proxy; classic ln-n approximation) and - `riskScore = Σ confidence` — the sandboxed worktree runner that executes the suite is the - P5 follow-up. `anchor.cusum()` adds the M4 one-sided CUSUM control chart (k = 0.35, + `riskScore = Σ confidence`. **`forge imagine --run` executes that minimal suite in a + sandboxed ephemeral git worktree** (HEAD-only — refused on a dirty tree unless + `--allow-dirty`), parses the TAP summary into per-file verdicts, always removes the + worktree (verified in a finally), and meters the run (`stage: "imagine"`); on this repo + the 8-test selected suite measured 1.3 s where the full suite takes ~60 s. + `anchor.cusum()` adds the M4 one-sided CUSUM control chart (k = 0.35, h = 1.0): sustained small drift alarms, a single exploratory spike drains back to zero. `verify.checkpointCadence()` prices M6's "when to check?" as the optimal-stopping threshold rule `n* = ⌈checkCost / (pErr·tokensPerStep·costPerToken)⌉`, clamped to @@ -344,7 +382,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). check; coverage + type-checking (`tsc --checkJs`); 2026 production-standard rules; OWASP-LLM / NIST SSDF / SLSA control mapping. -[Unreleased]: https://github.com/CodeWithJuber/forgekit/compare/v0.3.1...HEAD +[Unreleased]: https://github.com/CodeWithJuber/forgekit/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/CodeWithJuber/forgekit/compare/v0.4.0...v0.5.0 +[Unreleased]: https://github.com/CodeWithJuber/forgekit/compare/v0.4.0...HEAD +[0.4.0]: https://github.com/CodeWithJuber/forgekit/compare/v0.3.1...v0.4.0 [0.3.1]: https://github.com/CodeWithJuber/forgekit/compare/v0.3.0...v0.3.1 [0.3.0]: https://github.com/CodeWithJuber/forgekit/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/CodeWithJuber/forgekit/compare/v0.1.0...v0.2.0 diff --git a/CITATION.cff b/CITATION.cff index 5b769ad..e9db382 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,8 +1,8 @@ cff-version: 1.2.0 message: "If you use forgekit, please cite it using this metadata." title: "Forge (forgekit)" -version: 0.4.0 -date-released: "2026-07-06" +version: 0.5.0 +date-released: "2026-07-07" abstract: >- One config for every AI coding agent — a cross-tool configuration layer plus a cognitive substrate that gives a frozen model the memory, blast-radius awareness, and diff --git a/ONBOARDING.md b/ONBOARDING.md index 94afd25..0e5bd34 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -73,6 +73,22 @@ forge recall list # facts the recall-load guard injects next session forge catalog # the Start-Here index of everything ``` +## 6. Day two: the ledger is learning + +Everything the substrate learned on day one — cortex lessons, remembered facts, +verified code — landed as claims in `.forge/ledger/`. Now it starts paying off: + +```bash +forge ledger stats # what the repo knows, by kind and trust level +forge ledger blame # who minted a claim, every oracle outcome, per-author trust +forge reuse query "" # verified code you already have — with its proof +``` + +A `forge reuse query` hit points at working, test-confirmed code and the +`forge ledger blame` command that proves it — reuse it instead of regenerating. And +after `git pull`, `forge ledger merge ` folds a teammate's ledger in +conflict-free, so their lessons arrive with their provenance intact. + --- ## Forge principles diff --git a/README.md b/README.md index b09ca6e..b3e42d5 100644 --- a/README.md +++ b/README.md @@ -154,18 +154,24 @@ forge catalog Start-Here index of every tool / crew / guard forge substrate full pre-action cognitive-substrate check forge preflight assumption check — what a task names that the repo doesn't define +forge context budgeted context assembly + completeness gate — what an edit NEEDS known forge route cheapest capable model for a task (+ gateway config) forge impact predict blast radius for a symbol or file +forge imagine consequence simulation — predicted breaks + the minimal dry-run test suite forge scope decompose files into independent clusters forge anchor goal-drift check — are your git changes still on the stated goal? +forge diagnose doom-loop check — 3× the same failure mints a diagnosis + escalation forge verify independent verification — tests + hallucinated-symbol check -forge uicheck deterministic WCAG contrast check +forge uicheck deterministic UI checks — WCAG contrast · fingerprint · design gate +forge ledger proof-carrying memory — stats / verify / show / blame / query / merge / import +forge reuse proof-carrying code cache — query / mint --file / stats forge cortex self-correcting memory — status / why forge atlas build / query the code-graph (where-is-X, has-symbol) forge recall cross-session memory (list / add / consolidate) forge brain portable project memory inlined into AGENTS.md -forge cost real per-day spend via ccusage + the cost ceiling +forge dash local dashboard over the ledger, metrics, and blast radius +forge cost real per-day spend via ccusage · measured stage factors (--stages) forge scan vet a skill/MCP for injection/RCE before install forge harden wire gitleaks pre-commit + sandbox settings forge brand print the active brand token map @@ -174,15 +180,36 @@ forge brand print the active brand token map **→ Every command with a worked example, real output, and how to extend it: [`docs/GUIDE.md`](docs/GUIDE.md).** +## Team memory in three commands + +Everything the substrate learns — Cortex lessons, `forge remember` facts, verified +reuse artifacts — lands as content-addressed claims in a git-native ledger +(`.forge/ledger/`) built to merge without conflicts: + +```bash +forge init # once — also emits the .gitattributes union-merge rule the ledger needs +# …work normally: cortex and `forge remember` shadow claims into the ledger as you go… +git pull && forge ledger merge # fold in a teammate's ledger — conflict-free, any order +``` + +Identical knowledge minted independently converges to **one** claim with every author +preserved in its provenance; `forge ledger blame ` shows who minted it, every oracle +outcome, and per-author trust. No server, no sync service — it's just files in git. + ## Honest limits Forge states its own ceiling everywhere. In short: **guards reduce, don't eliminate** the "ignored my rules" problem; `recall`/`cortex` are file memory, **not** weight-level learning; the `atlas`/`impact` graph is regex-approximate (conservative, not a sound call -graph); and the substrate's rubrics are heuristic, not benchmarked. What's *asserted* is -safe to gate on (repo grounding, graph traversal, routing arithmetic, the test commands); -everything else is *advisory*. **Tests and human corrections always win.** Full list: -[docs/GUIDE.md → Honest limits](docs/GUIDE.md#honest-limits). +graph); and the substrate's rubrics are heuristic, not benchmarked. The newer subsystems +state theirs too: `forge reuse`'s MinHash near-match is weak on very short specs (a +few words hash to too few shingles to rank reliably); the UI fingerprint doesn't resolve +CSS `var()` indirection yet, so tokenized palettes are partially invisible to it; and +`forge cost --stages` reports **measured stages only** — a stage with no events says "no +data", never a default, and the ~90 % figure is a labeled *target*, not a claim. What's +*asserted* is safe to gate on (repo grounding, graph traversal, routing arithmetic, the +test commands); everything else is *advisory*. **Tests and human corrections always +win.** Full list: [docs/GUIDE.md → Honest limits](docs/GUIDE.md#honest-limits). ## Documentation diff --git a/SECURITY.md b/SECURITY.md index 8db16b1..d932055 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,12 +2,12 @@ ## Supported Versions -Only the latest release of forgekit receives security updates. +Only the latest minor line of forgekit receives security updates. -| Version | Supported | -| -------- | --------- | -| latest | ✅ Yes | -| < latest | ❌ No | +| Version | Supported | +| ------- | --------- | +| 0.5.x | ✅ Yes | +| < 0.5 | ❌ No | ## Reporting a Vulnerability @@ -35,19 +35,43 @@ supply-chain risk — always review a third-party skill/plugin before installing ## Threat model (what's in and out of scope) -**In scope:** the forgekit CLI and its emitters/guards; the `skill-gate` scanner; secret -handling in guards; the install channels; the supply chain of our own dev dependencies. +**In scope:** the forgekit CLI and its emitters/guards; the `skill-gate` scanner +(including prompt-injection patterns `forge scan` should catch but doesn't); secret +handling in guards; the published npm package (`@codewithjuber/forgekit`) and its +install channels; the team ledger's integrity properties (below); the supply chain of +our own dev dependencies. **Out of scope:** vulnerabilities in the third-party tools Forge configures (Claude Code, Cursor, Codex, …) or the MCP servers/skills you choose to install — report those upstream. Forge reduces risk (vetting before install, secret redaction, sandbox wiring) but is not itself a sandbox. +## Ledger forgery resistance + +The team ledger (proof-carrying memory) is shared, mergeable state — so it is designed to +resist forged confidence, not just corruption: + +- **Content-addressed claims.** A claim's id *is* the hash of its canonical JSON bytes. + `forge ledger verify` recomputes every content hash and reports any record whose bytes + don't match its address as tampered/invalid. +- **Oracle weights are never trusted from records.** `val()` re-reads each oracle's weight + from the in-code `ORACLES` table at read time; a `w` field stored in an evidence record + is audit metadata only. A forged record cannot grant itself extra weight. +- **Append-only, hash-deduped evidence logs.** Merge is a conflict-free set union; corrupt + or unknown-oracle lines are skipped rather than counted. Confidence is earned from valid + evidence, never asserted by a record. + +Protocol details: [docs/plans/substrate-v2/01-pcm-protocol.md](./docs/plans/substrate-v2/01-pcm-protocol.md). +A way to make the ledger report a `val` its evidence doesn't support is a security bug — +report it. + ## Hardening for maintainers - Require 2FA for anyone with write access; use least-privilege tokens. - Branch-protect `master`: require PR + green CI + review; block force-push. -- Enable GitHub secret scanning and CodeQL **default setup** (Settings → Code security). +- Enable GitHub secret scanning + push protection (Settings → Code security). In CI: + CodeQL (`.github/workflows/codeql.yml` — advanced setup, so keep *default setup* off), + gitleaks (`security.yml`, blocking), and OSSF Scorecard (`scorecard.yml`). - Dependency updates land via Dependabot (7-day cooldown); the `dependency-review` job blocks PRs that introduce known-vulnerable or disallowed-license dependencies. - Releases publish from CI with npm provenance (OIDC) + a scoped `NPM_TOKEN` — never a diff --git a/bench/bench.mjs b/bench/bench.mjs new file mode 100644 index 0000000..1e8b693 --- /dev/null +++ b/bench/bench.mjs @@ -0,0 +1,524 @@ +// forge bench — deterministic micro/meso benchmarks for the substrate primitives. +// Discipline (docs/plans/substrate-v2/05-cost-model.md, whitepaper C6): a number is an +// assumption until measured. Everything here is MEASURED on the machine that runs it — +// median of N timed runs after warmup, environment recorded next to every table, no +// projections, no blending with the paper's prototype numbers. +// +// Node stdlib only. Run: `npm run bench`. Output: a table on stdout AND the measured +// section of reports/benchmarks.md (between the BENCH:RESULTS markers — the prose +// around it is hand-written and not touched by this script). +// +// What is benchmarked on what: +// - atlas/context/substrate run against a COPY of this repo in os.tmpdir (bench/ +// excluded so the harness's own imports don't perturb the impact-quality eval; +// .forge excluded so the copy starts cold). The copy is deleted afterwards. +// - ledger/reuse run against synthetic fixtures generated with a seeded PRNG +// (mulberry32) — deterministic across runs and machines, built in os.tmpdir, +// cleaned up. +import { execFileSync } from "node:child_process"; +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { arch, cpus, platform, tmpdir, totalmem } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { performance } from "node:perf_hooks"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { build as buildAtlas, impact } from "../src/atlas.js"; +import { assemble } from "../src/context.js"; +import { evalImpact } from "../src/eval.js"; +import { mintClaim, outcomeRecord, val } from "../src/ledger.js"; +import { appendEvidence, loadClaims, mergeDirs, putClaim } from "../src/ledger_store.js"; +import { artifactClaim, fingerprint, lookup } from "../src/reuse.js"; +import { substrateCheck } from "../src/substrate.js"; +import { IMPACT_CASES } from "./impact_cases.mjs"; + +// --------------------------------------------------------------------------- +// Pure helpers (exported for the smoke test — no benchmark runs on import). +// --------------------------------------------------------------------------- + +/** Median of a sample list (even n → mean of the middle two). */ +export function median(xs) { + if (!xs.length) return NaN; + const s = [...xs].sort((a, b) => a - b); + const mid = s.length >> 1; + return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2; +} + +/** Nearest-rank p95 (with n < 20 this is simply the max — say so, don't smooth). */ +export function p95(xs) { + if (!xs.length) return NaN; + const s = [...xs].sort((a, b) => a - b); + return s[Math.min(s.length - 1, Math.ceil(0.95 * s.length) - 1)]; +} + +/** ms with sensible precision: 0.042 ms / 1.3 ms / 412 ms / 1503 ms. */ +export function fmtMs(ms) { + if (!Number.isFinite(ms)) return "n/a"; + if (ms < 0.1) return `${ms.toFixed(3)} ms`; + if (ms < 10) return `${ms.toFixed(2)} ms`; + if (ms < 100) return `${ms.toFixed(1)} ms`; + return `${Math.round(ms)} ms`; +} + +/** Integer rate with thousands separators: 12,340/s. */ +export function fmtRate(perSec) { + if (!Number.isFinite(perSec)) return "n/a"; + return `${Math.round(perSec).toLocaleString("en-US")}/s`; +} + +/** Plain monospace table (also valid Markdown when `markdown` is set). */ +export function formatTable(headers, rows, { markdown = false } = {}) { + const all = [headers, ...rows.map((r) => r.map(String))]; + const widths = headers.map((_, c) => Math.max(...all.map((r) => (r[c] ?? "").length))); + const line = (r, pad = " ") => + `|${r.map((cell, c) => ` ${(cell ?? "").padEnd(widths[c], pad)} `).join("|")}|`; + const sep = markdown + ? `|${widths.map((w) => `${"-".repeat(w + 2)}`).join("|")}|` + : line( + headers.map(() => ""), + "-", + ); + return [line(headers), sep, ...rows.map((r) => line(r.map(String)))].join("\n"); +} + +/** Time fn: `warmup` unmeasured runs, then `runs` measured ones. Returns samples + + * stats. fn receives a counter that NEVER repeats across warmup and measured runs, + * so fixtures keyed by it (fresh directories) can't collide — a measured run must + * never silently hit a warmup run's already-written state. */ +export function timeIt(fn, { runs = 5, warmup = 1 } = {}) { + let k = 0; + for (let i = 0; i < warmup; i++) fn(k++); + const samples = []; + for (let i = 0; i < runs; i++) { + const t0 = performance.now(); + fn(k++); + samples.push(performance.now() - t0); + } + return { samples, median: median(samples), p95: p95(samples), runs, warmup }; +} + +// Deterministic PRNG — fixture generation must be identical on every run/machine. +export function mulberry32(seed) { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const VOCAB = + `add remove update handle parse render validate merge sort filter cache load store fetch + compute batch stream retry queue flush index search rank group split join trim wrap + pagination cursor listing endpoint request response payload schema record entry field + column widget layout panel button badge banner report summary metric signal window` + .split(/\s+/) + .filter(Boolean); + +/** Deterministic prose-like spec of `len` tokens (no idents/paths/numbers, no secrets). */ +export function makeSpec(rand, len = 40) { + const words = []; + for (let i = 0; i < len; i++) words.push(VOCAB[Math.floor(rand() * VOCAB.length)]); + return words.join(" "); +} + +// --------------------------------------------------------------------------- +// Fixture builders +// --------------------------------------------------------------------------- + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = dirname(HERE); +const SKIP_COPY = new Set(["node_modules", "bench"]); + +function copyRepo() { + const dst = mkdtempSync(join(tmpdir(), "forge-bench-repo-")); + cpSync(REPO_ROOT, dst, { + recursive: true, + filter: (src) => { + const name = basename(src); + return !(SKIP_COPY.has(name) || (name.startsWith(".") && src !== REPO_ROOT)); + }, + }); + return dst; +} + +function fillLedger(dir, { count, offset = 0, evidenceEvery = 4 }) { + for (let i = 0; i < count; i++) { + const n = offset + i; + const minted = mintClaim({ + kind: "fact", + body: { name: `bench-fact-${n}`, text: `deterministic bench fact number ${n}` }, + scope: { level: "repo" }, + provenance: { agent: "bench", author: "bench" }, + t: 0, + }); + if (!minted.ok) throw new Error(minted.reason); + putClaim(dir, minted.claim); + if (n % evidenceEvery === 0) { + const o = outcomeRecord({ + oracle: "test.run", + result: "confirm", + ref: `bench:run:${n}`, + author: "bench", + t: 0, + }); + if (o.ok) appendEvidence(dir, minted.claim.id, o.outcome); + } + } +} + +/** n in-memory artifact claims, each with one test.run confirm so it clears SERVE_FLOOR. */ +function makeArtifacts(n) { + const rand = mulberry32(42); + const claims = []; + const specs = []; + for (let i = 0; i < n; i++) { + const spec = makeSpec(rand, 40); + specs.push(spec); + const minted = artifactClaim( + { spec, code: { inline: `// artifact ${i}` }, iface: [`benchExport${i}`], deps: [] }, + 0, + ); + if (!minted.ok) throw new Error(minted.reason); + const o = outcomeRecord({ + oracle: "test.run", + result: "confirm", + ref: `bench:artifact:${i}`, + author: "bench", + t: 0, + }); + minted.claim.evidence = o.ok ? [o.outcome] : []; + claims.push(minted.claim); + } + return { claims, specs }; +} + +const coldSketches = (claims) => { + for (const c of claims) delete c._sketch; +}; + +// --------------------------------------------------------------------------- +// The run +// --------------------------------------------------------------------------- + +function environment() { + let commit = "unknown"; + try { + commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: REPO_ROOT }).toString().trim(); + } catch {} + return { + node: process.version, + cpu: cpus()[0]?.model ?? "unknown", + cores: cpus().length, + memGB: Math.round(totalmem() / 2 ** 30), + platform: platform(), + arch: arch(), + commit, + date: new Date().toISOString(), + }; +} + +function runBenchmarks() { + process.env.FORGE_AUTHOR = "bench"; // no git subprocess variance inside timed loops + const cleanup = []; + const rows = []; // [suite, benchmark, median, p95, runs, notes] + const push = (suite, name, t, notes = "") => + rows.push([suite, name, fmtMs(t.median), fmtMs(t.p95), `${t.runs}`, notes]); + + try { + // --- atlas: build / incremental / impact query, on a copy of this repo --------- + const repo = copyRepo(); + cleanup.push(repo); + let atlas = null; + const tFull = timeIt( + () => { + rmSync(join(repo, ".forge"), { recursive: true, force: true }); + atlas = buildAtlas({ root: repo }); + }, + { runs: 5, warmup: 1 }, + ); + push( + "atlas", + "full build (this repo)", + tFull, + `${atlas.files} files, ${atlas.symbols.length} symbols, ${atlas.edges.length} edges`, + ); + const tIncr = timeIt( + () => { + atlas = buildAtlas({ root: repo }); + }, + { runs: 5, warmup: 1 }, + ); + push("atlas", "incremental rebuild (unchanged)", tIncr, "per-file hash cache hit"); + const target = "claimText"; + let impactResult = impact(atlas, target); // warms the memoized adjacency index + const tImpact = timeIt( + () => { + impactResult = impact(atlas, target); + }, + { runs: 30, warmup: 3 }, + ); + push( + "atlas", + `impact("${target}") (warm adjacency)`, + tImpact, + `${impactResult.impactedFiles.length} files impacted`, + ); + + // --- impact-oracle quality on the committed labeled cases ---------------------- + const quality = evalImpact(atlas, IMPACT_CASES); + + // --- ledger: mint+put, loadClaims, mergeDirs, val ------------------------------- + const ledgerScratch = mkdtempSync(join(tmpdir(), "forge-bench-ledger-")); + cleanup.push(ledgerScratch); + const N_CLAIMS = 1000; + let mintDir = ""; + const tMint = timeIt( + (i) => { + mintDir = join(ledgerScratch, `mint-${i}`); + fillLedger(mintDir, { count: N_CLAIMS }); + }, + { runs: 5, warmup: 1 }, // disk-bound: 5 runs, median damps fs-cache noise + ); + push("ledger", `mint+put ${N_CLAIMS} claims`, tMint, fmtRate(N_CLAIMS / (tMint.median / 1000))); + let claims1k = []; + const tLoad = timeIt( + () => { + claims1k = loadClaims(mintDir); + }, + { runs: 5, warmup: 1 }, + ); + push("ledger", `loadClaims at ${claims1k.length} claims`, tLoad, "full state from disk"); + const srcA = join(ledgerScratch, "replica-a"); + const srcB = join(ledgerScratch, "replica-b"); + fillLedger(srcA, { count: 500, offset: 0 }); + fillLedger(srcB, { count: 500, offset: 250 }); // 250 shared, 250 new each side + const MERGE_RUNS = { runs: 3, warmup: 1 }; + for (let k = 0; k < MERGE_RUNS.runs + MERGE_RUNS.warmup; k++) + cpSync(srcA, join(ledgerScratch, `merge-${k}`), { recursive: true }); // setup, untimed + let mergeStats = { claims: 0, records: 0 }; + const tMerge = timeIt((k) => { + mergeStats = mergeDirs(join(ledgerScratch, `merge-${k}`), srcB); + }, MERGE_RUNS); + push( + "ledger", + "mergeDirs 2×500-claim replicas (250 shared)", + tMerge, + `+${mergeStats.claims} claims, +${mergeStats.records} records`, + ); + let valSum = 0; + const tVal = timeIt( + () => { + valSum = 0; + for (const c of claims1k) valSum += val(c, 30); + }, + { runs: 20, warmup: 2 }, + ); + push( + "ledger", + `val() over ${claims1k.length} claims`, + tVal, + `${fmtRate(claims1k.length / (tVal.median / 1000))} (mean val ${(valSum / claims1k.length).toFixed(2)})`, + ); + + // --- reuse: fingerprint throughput, lookup at 100 and 1000 artifacts ----------- + const rand = mulberry32(7); + const fpSpecs = Array.from({ length: 2000 }, () => makeSpec(rand, 40)); + const tFp = timeIt( + () => { + for (const s of fpSpecs) fingerprint(s); + }, + { runs: 5, warmup: 1 }, + ); + push( + "reuse", + `fingerprint ${fpSpecs.length} specs`, + tFp, + fmtRate(fpSpecs.length / (tFp.median / 1000)), + ); + for (const n of [100, 1000]) { + const { claims, specs } = makeArtifacts(n); + const exactQ = specs[n >> 1]; + const nearQ = `${specs[n >> 1]} gently`; // superset tokens → Jaccard ≈ 0.97 → near tier + let hit = null; + const tExact = timeIt( + () => { + coldSketches(claims); // fresh-process behavior: no memoized sketches + hit = lookup(claims, exactQ); + }, + { runs: 10, warmup: 2 }, + ); + push("reuse", `lookup exact @ ${n} artifacts`, tExact, `tier=${hit.tier}`); + const tNear = timeIt( + () => { + coldSketches(claims); + hit = lookup(claims, nearQ); + }, + { runs: 5, warmup: 1 }, + ); + push( + "reuse", + `lookup near (LSH) @ ${n} artifacts`, + tNear, + `tier=${hit.tier}, j=${hit.jaccard?.toFixed(2) ?? "-"}`, + ); + } + + // --- context: assemble() on this repo for a representative task ---------------- + const task = + "add a keywords field to `mintClaim` in src/ledger.js and update `claimText` and `val`"; + let ctx = null; + const tCtx = timeIt( + () => { + ctx = assemble(repo, task, { atlas }); + }, + { runs: 10, warmup: 2 }, + ); + push( + "context", + "assemble() (this repo, 3-symbol task)", + tCtx, + `${ctx.tokens}/${ctx.budget} tokens, ${ctx.required.length} required, ${ctx.ok ? "complete" : "incomplete"}`, + ); + + // --- substrate: full substrateCheck wall time ----------------------------------- + let sub = null; + const tSub = timeIt( + () => { + sub = substrateCheck(repo, task, { allowBuild: true, llm: false }); + }, + { runs: 3, warmup: 1 }, + ); + push( + "substrate", + "substrateCheck (allowBuild, llm off)", + tSub, + `${sub.impact?.impactedFiles?.length ?? 0} impacted files, route ${sub.route?.tier ?? "?"}`, + ); + + return { rows, quality }; + } finally { + for (const dir of cleanup) rmSync(dir, { recursive: true, force: true }); + } +} + +// --------------------------------------------------------------------------- +// Reporting +// --------------------------------------------------------------------------- + +const RESULT_HEADERS = ["suite", "benchmark", "median", "p95", "runs", "notes"]; +const QUALITY_HEADERS = ["case (target)", "precision", "recall", "F1", "predicted", "truth"]; +const SERIES_HEADERS = ["series", "precision", "recall", "F1", "ground truth"]; + +/** Paper prototype vs this repo — two different methodologies, side by side and + * labeled, NEVER averaged or blended. The paper row is a constant from the + * whitepaper (Figure 5 / deliverable-package.md), not something this harness ran. */ +export function seriesRows(quality) { + return [ + [ + "paper prototype (Python, mutation-derived)", + "0.63", + "1.00", + "0.75", + "mutation testing against a real suite", + ], + [ + "this repo (regex atlas, hand-labeled)", + quality.oracle.precision.toFixed(2), + quality.oracle.recall.toFixed(2), + quality.oracle.f1.toFixed(2), + `${quality.n} hand-labeled cases (bench/impact_cases.mjs)`, + ], + ]; +} + +export function qualityRows(quality) { + const rows = quality.perCase.map((p) => [ + p.target, + p.oracle.precision.toFixed(2), + p.oracle.recall.toFixed(2), + p.oracle.f1.toFixed(2), + `${p.oracle.predicted}`, + `${p.oracle.groundTruth}`, + ]); + rows.push([ + `mean of ${quality.n}`, + quality.oracle.precision.toFixed(2), + quality.oracle.recall.toFixed(2), + quality.oracle.f1.toFixed(2), + "", + "", + ]); + return rows; +} + +const BEGIN = ""; +const END = ""; + +function resultsMarkdown(env, rows, quality) { + const q = qualityRows(quality); + return [ + BEGIN, + "", + "### Environment (machine section)", + "", + "```json", + JSON.stringify(env, null, 2), + "```", + "", + "### Measured results", + "", + formatTable(RESULT_HEADERS, rows, { markdown: true }), + "", + "### Impact-oracle quality (hand-labeled cases, this repo)", + "", + formatTable(QUALITY_HEADERS, q, { markdown: true }), + "", + `Edited-file-only baseline recall over the same cases: **${quality.baseline.recall.toFixed(2)}**.`, + "", + "Two methodologies, side by side — different codebases, different ground-truth", + "derivations, so the rows are comparable in spirit only and are never blended:", + "", + formatTable(SERIES_HEADERS, seriesRows(quality), { markdown: true }), + "", + END, + ].join("\n"); +} + +function writeReport(env, rows, quality) { + const path = join(REPO_ROOT, "reports", "benchmarks.md"); + const section = resultsMarkdown(env, rows, quality); + let text; + try { + text = readFileSync(path, "utf8"); + } catch { + text = `# Benchmarks\n\n${BEGIN}\n${END}\n`; + } + const begin = text.indexOf(BEGIN); + const end = text.indexOf(END); + text = + begin !== -1 && end !== -1 + ? text.slice(0, begin) + section + text.slice(end + END.length) + : `${text.trimEnd()}\n\n${section}\n`; + writeFileSync(path, text); + return path; +} + +function main() { + const env = environment(); + const t0 = performance.now(); + const { rows, quality } = runBenchmarks(); + const total = performance.now() - t0; + process.stdout.write( + `forge bench — median of N runs after warmup (node ${env.node}, ${env.cpu})\n\n`, + ); + process.stdout.write(`${formatTable(RESULT_HEADERS, rows)}\n\n`); + process.stdout.write("impact-oracle quality (hand-labeled cases, this repo):\n\n"); + process.stdout.write(`${formatTable(QUALITY_HEADERS, qualityRows(quality))}\n\n`); + process.stdout.write("vs the paper prototype (different methodology — never blended):\n\n"); + process.stdout.write(`${formatTable(SERIES_HEADERS, seriesRows(quality))}\n\n`); + const path = writeReport(env, rows, quality); + process.stdout.write(`wrote ${path} (total bench time ${fmtMs(total)})\n`); +} + +const isMain = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url; +if (isMain) main(); diff --git a/bench/impact_cases.mjs b/bench/impact_cases.mjs new file mode 100644 index 0000000..a11bd14 --- /dev/null +++ b/bench/impact_cases.mjs @@ -0,0 +1,103 @@ +// Labeled impact-oracle cases derived from THIS repo's real import graph. +// +// Labeling rule (hand-verified by reading the actual source, then double-checked with +// grep): `expected` = the defining file PLUS every file with a DIRECT reference to the +// target symbol — an `import { X }` with a use, or a call site of `X(`. The defining +// file is always labeled: an edit to the target trivially impacts its own file (same +// convention as the existing fixture in test/eval.test.js and the paper's mutation +// methodology, where the mutated file itself counts). Transitive dependents are +// deliberately NOT labeled: the oracle predicts them (its job — err toward inclusion), +// so they count against precision here, exactly like the paper's mutation-derived +// scoring penalized over-approximation. Every label is grep-checkable by anyone. +// +// These cases are evaluated against an atlas built over a copy of this repo that +// EXCLUDES bench/ — otherwise the harness's own imports of these symbols would +// perturb the measurement it is taking. +// +// Verified references, per case (as of the commit this file lands in): +// +// normalizeSpec (src/reuse.js) +// - src/reuse.js defines it; fingerprint() and artifactClaim() call it +// - test/reuse.test.js imports { normalizeSpec } and calls it directly +// +// evalImpact (src/eval.js) +// - src/eval.js defines it (no other same-file caller) +// - test/eval.test.js imports { evalImpact } and calls it — the only referencer +// +// isStale (src/atlas.js) +// - src/atlas.js defines it +// - src/verify.js imports { isStale } from ./atlas.js and calls it +// - src/doctor.js imports { isStale } from ./atlas.js and calls it +// - test/atlas.test.js imports { isStale } and calls it +// +// mergeStates (src/ledger.js) +// - src/ledger.js defines it +// - src/ledger_store.js imports { mergeStates } (importState calls it) +// - test/ledger.test.js imports { mergeStates } and calls it +// +// claimText (src/ledger.js) +// - src/ledger.js defines it; sketchOf() calls it (same-file caller) +// - src/context.js imports { claimText } and calls it +// - src/dash.js imports { claimText } and calls it +// - src/cli.js dynamic-imports { claimText } and calls it +// - test/ledger.test.js imports { claimText } and calls it +// (test/dash.test.js mentions the name only inside an assertion message — a string, +// not a reference — so it is NOT labeled as a dependent.) +// +// contentHash (src/util.js) — the deliberately hard case: wide fan-out plus one +// reference the regex atlas is KNOWN to miss (src/atlas.js binds it to an alias, +// `const hash = contentHash;`, with no call parentheses — and the JS import regex +// captures module paths, not named bindings — so no edge exists; that is a real, +// documented false negative, kept in the labels on purpose). +// - src/util.js defines it +// - src/atlas.js imports { contentHash }, aliases it (const hash = contentHash) +// - src/cortex_hook.js imports { contentHash } and calls it +// - src/diagnose.js imports { contentHash } and calls it +// - src/ledger.js imports { contentHash } and calls it +// - src/reuse.js imports { contentHash } and calls it + +export const IMPACT_CASES = [ + { + target: "normalizeSpec", + expected: ["src/reuse.js", "test/reuse.test.js"], + editedFile: "src/reuse.js", + }, + { + target: "evalImpact", + expected: ["src/eval.js", "test/eval.test.js"], + editedFile: "src/eval.js", + }, + { + target: "isStale", + expected: ["src/atlas.js", "src/verify.js", "src/doctor.js", "test/atlas.test.js"], + editedFile: "src/atlas.js", + }, + { + target: "mergeStates", + expected: ["src/ledger.js", "src/ledger_store.js", "test/ledger.test.js"], + editedFile: "src/ledger.js", + }, + { + target: "claimText", + expected: [ + "src/ledger.js", + "src/context.js", + "src/dash.js", + "src/cli.js", + "test/ledger.test.js", + ], + editedFile: "src/ledger.js", + }, + { + target: "contentHash", + expected: [ + "src/util.js", + "src/atlas.js", + "src/cortex_hook.js", + "src/diagnose.js", + "src/ledger.js", + "src/reuse.js", + ], + editedFile: "src/util.js", + }, +]; diff --git a/biome.json b/biome.json index 19e1794..1a74a63 100644 --- a/biome.json +++ b/biome.json @@ -3,7 +3,7 @@ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": true, - "includes": ["src/**", "test/**"] + "includes": ["src/**", "test/**", "scripts/**", "bench/**"] }, "formatter": { "enabled": true, diff --git a/docs/GUIDE.md b/docs/GUIDE.md index b5bd614..e654d94 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -266,15 +266,219 @@ Forge cortex — self-correcting project memory `forge cortex why ` shows the lessons that would be injected when you touch it. -### `forge uicheck ` — deterministic WCAG contrast +### `forge ledger` — proof-carrying team memory -Exact contrast math for UI work — asserted, never guessed. +The convergent store behind cortex, `recall`, `brain`, and `reuse`: every stored unit is +a content-addressed claim whose confidence (`val`) only independent oracles can move. +Lives in `.forge/ledger/` — git-committable, conflict-free merge. ```console -$ forge uicheck "#777" "#fff" +$ forge ledger stats +Forge ledger — proof-carrying memory + + claims: 12 (tombstoned 1) + lesson: 7 + fact: 4 + artifact: 1 + val: trusted 5 · uncertain 6 · dormant 1 + + stored in .forge/ledger/ (git-committable, conflict-free merge) +``` + +`forge ledger blame ` is the accountability view — every mint, every oracle +outcome, every retraction, and per-author trust: + +```console +$ forge ledger blame 3f2a +Forge ledger blame — lesson 3f2a91c04d7e + + val 0.82 (trust-weighted 0.84) + minted day 20640 by juber + confirm day 20641 test.run → npm-test#a41 by juber + confirm day 20643 human.accept → pr-118 by sam + + author trust (earned from oracle outcomes on their claims): + 0.93 juber +``` + +The rest of the surface, briefly: `forge ledger merge ` folds in any other ledger +tree (a teammate's checkout, a worktree, a backup) — `merged: 3 new claim(s), 5 new +record(s) — conflict-free`, in any order; `query ""` ranks live claims by the +paper's Eq. 3; `show ` prints one claim with its computed `val`; `verify` recomputes +every content hash (CI-friendly, exit 1 on tampering); `import` back-fills legacy +lessons/facts idempotently. Add `--personal` to target the per-user ledger beside the +global recall store, `--json` for scripts. + +### `forge reuse` — proof-carrying code cache + +Verified code becomes an `artifact` claim keyed by a normalized task fingerprint; a +lookup walks exact → near → adapt → miss. An artifact serves **only while its proof +holds** — confidence above the 0.6 floor and every declared dependency still in the atlas. + +```console +$ forge reuse query "debounce user input before firing search" + NEAR hit (similarity 0.87) — module at src/lib/debounce.js + claim 9c41d2ab77e0 — `forge ledger blame 9c41d2ab` for its proof + +$ forge reuse query "quantum blockchain" + miss — nothing verified matches; generate, then `forge reuse mint` it +``` + +Mint after verification — without an evidence ref it sits at the 0.5 prior and does +**not** serve: + +```console +$ forge reuse mint "debounce helper for search input" --file src/lib/debounce.js --ref npm-test#green + minted: 9c41d2ab77e0 (1 export(s), 0 dep(s)) + serving: yes — verification evidence attached +``` + +`forge reuse stats` shows lookups by outcome + estimated tokens saved (from +`.forge/metrics.jsonl`). Honest limit: the MinHash near-match is weak on very short +specs — a few words hash to too few shingles to rank reliably. + +### `forge context ""` — budgeted assembly + completeness gate + +Derives the required-knowledge set for an edit (the target's definitions, hop-1 +dependents, sibling tests, trusted lessons), covers it under a token budget with a +compression ladder (full → head → pointer), and computes what's *missing* as a set +difference — not a feeling. + +```console +$ forge context "change verifyToken in src/auth.js to reject short tokens" +Forge context — budgeted assembly + completeness gate + + budget: 1840/12000 tokens · required 4 · COMPLETE + + def:src/auth.js [full] 620t + + deps:verifyToken [head] 410t + + tests:test/auth.test.js [full] 480t + + fact:c1d2e3f4 [full] 330t +``` + +On an incomplete assembly it lists the missing items and derived clarifying questions +("the task names `X` but the repo doesn't define it — which file implements it?") and +exits 1. `--budget ` tightens the window; a tight budget downgrades granularity +instead of silently dropping coverage. + +### `forge diagnose ""` — doom-loop check + +Hashes a failure into a normalized signature (line numbers, addresses, timestamps, +paths stripped) and counts recurrences. The 3rd identical hit is thrash: it mints a +`diagnosis` claim into the team ledger and tells the agent to stop retrying. + +```console +$ forge diagnose "TypeError: Cannot read properties of undefined (reading 'user')" --file src/session.js +Forge diagnose — doom-loop check + + signature: a41f7c20be91 · seen 3× in the recent failure window + diagnosis claim: 5d0e88c21f3a (`forge ledger show 5d0e88c2`) + + STOP retrying this fix. State the diagnosis out loud (claim 5d0e88c2 — `forge ledger show 5d0e88c2`, add what you already tried to its triedFixes), then escalate ONE model tier with the diagnosis as the head of the new prompt. The escalation must carry the diagnosis — never just "try again, but more expensive". +``` + +Below the threshold it just records and says keep going. Advisory — halting the retry +loop is the agent's move, not an exit code. Because the claim rides the team ledger, the +same loop becomes a one-per-team event, not one-per-session. + +### `forge imagine ""` — consequence simulation + +The static half of the paper's Eq. 4: entities → blast radius → predicted breaks with +confidence, plus the minimal test suite that covers them (weighted greedy set cover). + +```console +$ forge imagine "change verifyToken in src/auth.js to reject short tokens" +Forge imagine — consequence simulation (pre-action) + + targets: verifyToken, src/auth.js + risk score: 2.30 (Σ confidence over predicted breaks) + + predicted breaks (3): + 0.90 src/auth.js + 0.70 src/login.js + 0.70 src/session.js + + minimal dry-run suite (1) — run these, in this order: + - test/auth.test.js + + (sandboxed worktree dry-run of this suite lands as the P5 follow-up) +``` + +It also flags predicted breaks **no test covers** — the risk you can't dry-run away. + +### `forge uicheck` — deterministic UI checks + +Three subcommands, all static parsing — no LLM, no screenshots. + +**`contrast `** — exact WCAG math, asserted, never guessed (bare +`forge uicheck ` still works): + +```console +$ forge uicheck contrast "#777" "#fff" contrast #777 on #fff: 4.48:1 → fail (FAILS AA) ``` +**`fingerprint [--mint]`** — the design feature vector of your UI files: +palette (hue histogram), spacing base + on-scale fraction, fonts, radius/shadow levels. +`--mint` stores it as a shared `fingerprint` ledger claim — the design gate's "home": + +```console +$ forge uicheck fingerprint src/components/*.jsx --mint +Forge uicheck fingerprint — the design feature vector + + palette: 9 color(s), hue bins [2 0 0 1 3 0 0 0 0 2 1 0] + spacing: 4, 8, 16, 24 px — base 4, 96% on-scale + type: Inter, ui-monospace + shape: radii 6, 12 (2 level(s)) · 1 shadow level(s) + + minted fingerprint claim e7a90b12cd34 — the gate's "home" +``` + +**`design `** — the two-sided gate for generated UI (exit 1 on fail): slop +distance to known generic templates must stay HIGH, conformance to your minted project +fingerprint must stay LOW, plus scale-conformance checks (spacing on base, level caps). +Failures are actionable per-feature edits, never a bare score. Honest limit: the +fingerprint doesn't resolve CSS `var()` indirection yet — fully tokenized palettes are +partially invisible to it. + +### `forge dash [--port N]` — the local dashboard + +A read-only lens over `.forge/` — stdlib `node:http`, localhost-only, one +self-contained HTML page (no CDN, no build step). Panels: Ledger (claims with val bars, +contested claims, per-author trust), Cost/Cache (measured stage counters), and Impact +(blast-radius explorer). Every claim row shows its `forge ledger blame` command. + +```console +$ forge dash +Forge dash — read-only lens on .forge/ + + http://127.0.0.1:4242 (localhost-only · Ctrl-C to stop) +``` + +### `forge cost --stages` — the measured cost report + +Per-stage cost factors as pure arithmetic over `.forge/metrics.jsonl`. A stage with no +events says **no data** — never a default; the composed figure is a lower bound over +measured stages only. + +```console +$ forge cost --stages +Forge cost — measured stage factors (.forge/metrics.jsonl) + + stage factor events + gate 6.2% 16 + cache no data 0 + route no data 0 + context no data 0 + + composed measured reduction: 6.2% (from: gate) — lower bound, measured stages only + totals: 16 metric event(s) · ~0 tokens saved (stage self-estimates) + + context (not a local measurement): the paper measured a 62% routing saving on live tokens (paper §9) + target (unmet until measured): the plan's composed target is ~90% (docs/plans/substrate-v2/05-cost-model.md) +``` + +Plain `forge cost` remains the per-day spend view via `ccusage`. + ### The rest | Command | Answers | @@ -284,7 +488,7 @@ $ forge uicheck "#777" "#fff" | `forge doctor` | Health check: layers, install, drift, cortex. | | `forge catalog` | Start-Here index of every tool / crew / guard. | | `forge brain` / `forge remember` | Portable project memory inlined into `AGENTS.md`. | -| `forge cost` | Real per-day spend (via `ccusage`) + the cost ceiling. | +| `forge cost` | Real per-day spend (via `ccusage`) + the cost ceiling; `--stages` for the measured report. | | `forge scan ` | Vet a skill/MCP for injection/RCE before install. | | `forge harden` | Wire gitleaks pre-commit + sandbox settings. | | `forge spec [init\|lock\|check]` | Spec-as-contract drift check. | @@ -459,6 +663,13 @@ Edit `brand.json` (`FORGE_BRAND`), the `bin` key in `package.json`, and `name` i - **`recall` / `cortex` are file + prompt memory**, not weight-level learning. - **The atlas graph is regex-approximate** — conservative, not a sound call graph; dynamic dispatch and generated code can be missed. +- **`forge reuse`'s MinHash near-match is weak on very short specs** — a few words hash + to too few shingles to rank reliably; write a sentence, not a keyword. +- **The UI fingerprint doesn't resolve CSS `var()` indirection yet** — a fully + tokenized palette is partially invisible to the design gate. +- **`forge cost --stages` reports measured stages only** — a stage with no events says + "no data", never a default; the composed figure is a lower bound and ~90 % is a + labeled *target*, not a claim. - **The substrate's rubrics are heuristic, not benchmarked** — judge them after real use. What's *asserted* (safe to gate on): repo grounding, graph traversal, scope decomposition, routing arithmetic, and the test/build commands. Everything else is diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 151d127..ae35080 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,46 +1,99 @@ # Releasing Forge -Releases are automated by [`.github/workflows/release.yml`](../.github/workflows/release.yml): -**push a `v*` tag → it runs the tests, publishes to public npm (with provenance), and -cuts a GitHub Release** with auto-generated notes. +Releases are one click. Two workflows do everything: -## One-time setup (maintainer) +1. [`bump.yml`](../.github/workflows/bump.yml) — **Actions → "Bump version" → Run + workflow**. It runs the tests, bumps every version field, rotates the CHANGELOG, + commits `chore(release): vX.Y.Z`, tags `vX.Y.Z`, pushes both, and kicks off the + release workflow on the new tag. +2. [`release.yml`](../.github/workflows/release.yml) — runs on any `v*` tag: tests → + npm publish (with provenance, if `NPM_TOKEN` is set) → GitHub Release with + auto-generated notes. -The workflow needs a single secret to publish: +## The one-click flow -1. Create an [npmjs.com](https://www.npmjs.com/) account and make sure it can publish to - the `@codewithjuber` scope. +Go to **Actions → Bump version → Run workflow** and pick a bump type: + +| choice | effect | +| ------- | ---------------------------------------------------------------------------- | +| `auto` | derived from conventional commits since the last tag: `BREAKING CHANGE` / `type!:` → major, `feat:` → minor, anything else → patch. Falls back to the CHANGELOG `[Unreleased]` body (BREAKING → major, `### Added` → minor, other content → patch). | +| `patch` / `minor` / `major` | explicit | + +What happens, in order: + +1. `npm ci && npm test` — a broken tree is never tagged. +2. `node scripts/bump.mjs ` updates **every** version field: + `package.json`, `package-lock.json` (both fields), `.claude-plugin/plugin.json`, + `.codex-plugin/plugin.json`, `CITATION.cff` (version + release date), the landing + page footer, and moves the CHANGELOG `[Unreleased]` section under + `## [X.Y.Z] - ` (compare links included). +3. Commit `chore(release): vX.Y.Z`, annotated tag `vX.Y.Z`, push commit + tag. +4. `gh workflow run release.yml --ref vX.Y.Z`. (This explicit dispatch exists because + pushes made with the default `GITHUB_TOKEN` intentionally do **not** trigger other + workflows — GitHub's recursion guard. A tag pushed by a human still triggers + `release.yml` the normal way.) + +`release.yml` then re-runs the tests, asserts the tag matches `package.json`, publishes +`@codewithjuber/forgekit@X.Y.Z` to public npm with provenance, and creates the GitHub +Release. Verify: + +- npm: +- releases: + +## NPM_TOKEN setup (one-time, optional but recommended) + +Publishing needs one repo secret: + +1. Create an [npmjs.com](https://www.npmjs.com/) account that can publish to the + `@codewithjuber` scope. 2. Generate an **Automation** access token (npm → Access Tokens → Generate → *Automation*). -3. Add it to the repo: **Settings → Secrets and variables → Actions → New repository +3. Add it as a repo secret: **Settings → Secrets and variables → Actions → New repository secret**, name **`NPM_TOKEN`**. -That's the only manual step. Without it the publish step fails; everything else still runs. +**Soft-skip behavior:** if `NPM_TOKEN` is missing, `release.yml` does **not** fail — it +skips the npm publish with a loud warning and still creates the GitHub Release. Add the +token later and re-run the workflow from the release tag: `npm publish` refuses to +overwrite an existing version, so re-runs are safe and only the missing step takes effect. -## Cutting a release +## Local / manual usage ```bash -# 1. bump the version (updates package.json + package-lock.json, no tag yet) -npm version minor --no-git-tag-version # or: patch / major +npm run bump -- patch # or minor / major / auto — edits files, prints new version +npm run bump -- auto --dry-run # compute only, write nothing +node scripts/bump.mjs check # assert all version fields agree (same guard CI runs) +npm pack --dry-run # inspect exactly what would ship to npm +``` -# 2. move the [Unreleased] notes into a new dated section in CHANGELOG.md, then commit -git add -A && git commit -m "chore(release): v$(node -p "require('./package.json').version")" +If you bump locally instead of via the Actions tab, finish the job by hand — commit, +tag, push (a human-pushed tag *does* trigger `release.yml`): -# 3. tag and push — this is what triggers the release workflow +```bash V="v$(node -p "require('./package.json').version")" +git add -A && git commit -m "chore(release): $V" git tag -a "$V" -m "$V" git push origin master "$V" ``` -The workflow then publishes `@codewithjuber/forgekit@` to npm and creates the -matching GitHub Release. Verify: +## Guard rails -- npm: -- releases: +- **CI version-drift guard**: `node scripts/bump.mjs check` fails CI if `package.json`, + `package-lock.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, or + `CITATION.cff` disagree about the version. +- **Tag/version assert**: `release.yml` refuses a tag that doesn't match `package.json` + (hand-rolled tags that skipped the bump script fail fast with a clear error). +- `scripts/bump.mjs` refuses to rotate the CHANGELOG onto a version that already has a + section, and errors when `auto` finds nothing to release. + +## Related workflow secrets + +- `NPM_TOKEN` (above) — npm publish; missing = publish skipped, release still cut. +- `ADMIN_TOKEN` — only used by `repo-settings.yml` (repo description/topics/Discussions + need a fine-grained PAT with *Administration: write*; the default `GITHUB_TOKEN` + cannot get that scope). Missing = that workflow skips with a warning; the equivalent + `gh` commands are in its header comment. -## Notes +## Semver notes -- **Semver**: `patch` = fixes, `minor` = new commands/flags (backward compatible), - `major` = breaking changes. Pre-`1.0`, breaking changes may ship in a `minor`. -- Consumers install with no token: `npm install -g @codewithjuber/forgekit`. -- Re-running a failed release is safe — `npm publish` refuses to overwrite a version that - already exists, so only the missing steps take effect. +`patch` = fixes, `minor` = new commands/flags (backward compatible), `major` = breaking +changes. Pre-`1.0`, breaking changes may ship in a `minor`. Consumers install with no +token: `npm install -g @codewithjuber/forgekit`. diff --git a/global/taste/brutalist.json b/global/taste/brutalist.json new file mode 100644 index 0000000..35b9d9c --- /dev/null +++ b/global/taste/brutalist.json @@ -0,0 +1,9 @@ +{ + "name": "brutalist", + "why": "Raw, honest, high-contrast (brutalist.md): pure #000/#FFF plus ONE loud accent -> max_hues 1 with chroma pinned high (chroma = HSL saturation/100 proxy, 0-1); 'DON'T round corners' -> radius_levels [0,0]; no SOFT shadows but a hard offset shadow is idiomatic brutalism -> shadow_levels [0,1]; visible size jumps -> wide geometric ratio; tau_slop is the highest of all profiles because brutalism must sit maximally far from the generic soft-template look.", + "palette": { "max_hues": 1, "chroma": [0.7, 1.0], "neutrals": "stark" }, + "space": { "scale": "geometric", "ratio": [1.5, 2.0], "base": [4, 8] }, + "type": { "max_families": 2 }, + "shape": { "radius_levels": [0, 0], "shadow_levels": [0, 1] }, + "gate": { "tau_slop": 0.45, "tau_conform": 0.5 } +} diff --git a/global/taste/corporate.json b/global/taste/corporate.json new file mode 100644 index 0000000..3871392 --- /dev/null +++ b/global/taste/corporate.json @@ -0,0 +1,9 @@ +{ + "name": "corporate", + "why": "Trustworthy, clear, accessible (corporate.md): one professional primary (#2563EB) plus three semantic states -> max_hues 4; mid-to-high but never neon chroma (chroma = HSL saturation/100 proxy, 0-1); cool slate neutrals; 'consistent 8px spacing' -> base [8], linear scale with modest ratio; one clean sans -> max_families 1; moderate rounding and subtle elevation -> radius [1,2] / shadow [0,2]; tau_slop is the LOWEST of all profiles because corporate deliberately stays near convention, and tau_conform is tight because consistency is the whole point.", + "palette": { "max_hues": 4, "chroma": [0.3, 0.9], "neutrals": "cool" }, + "space": { "scale": "linear", "ratio": [1.25, 1.5], "base": [8] }, + "type": { "max_families": 1 }, + "shape": { "radius_levels": [1, 2], "shadow_levels": [0, 2] }, + "gate": { "tau_slop": 0.2, "tau_conform": 0.4 } +} diff --git a/global/taste/editorial.json b/global/taste/editorial.json new file mode 100644 index 0000000..a43fd2b --- /dev/null +++ b/global/taste/editorial.json @@ -0,0 +1,9 @@ +{ + "name": "editorial", + "why": "Magazine-grade (editorial.md): warm paper + rich ink + ONE muted accent, but max_hues 2 because the HSL saturation/100 chroma proxy (0-1) counts warm paper itself (#FBFAF7-class, s well above the neutral cutoff) as a hue bin — one accent + the paper cast; low-to-mid chroma band (#7C6A4E sits near 0.23); warm neutrals; spacious geometric rhythm near the classic 1.4-1.62 ratios; 'exactly one serif + one sans', 'DON'T use more than two families' -> max_families 2; thin rules over boxes -> radius [0,1], shadow [0,1]; tau_slop 0.35 (the spec Section 3 example) because considered typography must clear the template look by a wide margin.", + "palette": { "max_hues": 2, "chroma": [0.1, 0.45], "neutrals": "warm" }, + "space": { "scale": "geometric", "ratio": [1.4, 1.62], "base": [4, 8] }, + "type": { "max_families": 2 }, + "shape": { "radius_levels": [0, 1], "shadow_levels": [0, 1] }, + "gate": { "tau_slop": 0.35, "tau_conform": 0.5 } +} diff --git a/global/taste/minimalist.json b/global/taste/minimalist.json new file mode 100644 index 0000000..9a7d314 --- /dev/null +++ b/global/taste/minimalist.json @@ -0,0 +1,9 @@ +{ + "name": "minimalist", + "why": "Restraint (minimalist.md): near-monochrome with exactly ONE saturated accent for the primary action -> max_hues 1, chroma band around a committed accent like #2563EB (chroma = HSL saturation/100 proxy, 0-1); neutral near-white/ink base; '8px spacing scale' -> base [8], calm linear-to-wide steps; one sans, 2 weights -> max_families 1; hairlines only, 'no bounce, no shadow' / 'DON'T add drop shadows' -> shadow_levels [0,0], radius [0,1]; tau_slop 0.3 because minimal surfaces are CLOSE to flat templates by construction yet must still avoid the default-blue-card centroid, tau_conform tight because a minimal system tolerates zero drift.", + "palette": { "max_hues": 1, "chroma": [0.4, 0.9], "neutrals": "neutral" }, + "space": { "scale": "linear", "ratio": [1.5, 2.0], "base": [8] }, + "type": { "max_families": 1 }, + "shape": { "radius_levels": [0, 1], "shadow_levels": [0, 0] }, + "gate": { "tau_slop": 0.3, "tau_conform": 0.4 } +} diff --git a/global/taste/playful.json b/global/taste/playful.json new file mode 100644 index 0000000..702c48a --- /dev/null +++ b/global/taste/playful.json @@ -0,0 +1,9 @@ +{ + "name": "playful", + "why": "Friendly, energetic (playful.md): '2-3 saturated hues' -> max_hues 3, the most permissive palette of the five, with chroma pinned high but short of neon-on-neon (chroma = HSL saturation/100 proxy, 0-1); warm soft background neutrals; grid-aligned but generous -> geometric 1.25-1.5 on a 4/8 base; one rounded sans plus an optional display face -> max_families 2; 'rounded fills (radius 12-20px)' and soft shadows -> radius [1,3], shadow [1,2] (some rounding and lift is REQUIRED, not just allowed); default tau_slop 0.25 — playful legitimately lives nearer the card-and-radius territory templates also occupy.", + "palette": { "max_hues": 3, "chroma": [0.55, 1.0], "neutrals": "warm" }, + "space": { "scale": "geometric", "ratio": [1.25, 1.5], "base": [4, 8] }, + "type": { "max_families": 2 }, + "shape": { "radius_levels": [1, 3], "shadow_levels": [1, 2] }, + "gate": { "tau_slop": 0.25, "tau_conform": 0.5 } +} diff --git a/landing/index.html b/landing/index.html index 0e0c4b3..7096679 100644 --- a/landing/index.html +++ b/landing/index.html @@ -1097,7 +1097,7 @@

Give your agent a memory that gets better the more it's wrong.