diff --git a/.serena/memories/backend/core.md b/.serena/memories/backend/core.md index b7a9199965..34a272dedc 100644 --- a/.serena/memories/backend/core.md +++ b/.serena/memories/backend/core.md @@ -92,6 +92,10 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. * **Linting:** `pnpm run lint` from the repository root. * **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs. +**Before linting:** if delimiter errors are suspected (after LLM edits), run +`tools/paren-repair.bb` on the affected files first. Delimiter errors produce +misleading linter/compiler output. See `mem:tools/paren-repair`. + ## Testing IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. diff --git a/.serena/memories/common/core.md b/.serena/memories/common/core.md index a74939d58c..474fb8d87e 100644 --- a/.serena/memories/common/core.md +++ b/.serena/memories/common/core.md @@ -48,7 +48,7 @@ Components, variants, and debugging: Text and tests: - Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`. -- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/test-setup`. +- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`. ## Areas without focused memories diff --git a/.serena/memories/common/test-setup.md b/.serena/memories/common/testing.md similarity index 60% rename from .serena/memories/common/test-setup.md rename to .serena/memories/common/testing.md index 32f87710e9..bfd126d26f 100644 --- a/.serena/memories/common/test-setup.md +++ b/.serena/memories/common/testing.md @@ -1,24 +1,27 @@ -# Common Module Test Setup +# Common Testing and Verification `common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved. -## Running tests +## Unit tests + +Common tests live under `common/test/common_tests/` and use `clojure.test`. +They are CLJC and run on both JVM and JS. From `common/`: +- Full JVM test run: `clojure -M:dev:test` +- Full JS test run: `pnpm run test:quiet` +- Focus a JVM test namespace: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test` +- Focus a JVM test var: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch` +- Focus a JS test namespace: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test` +- Focus a JS test var: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute` +- Quiet logging during a JS run: append `--log-level warn` (or `trace|debug|info|warn|error`) +- Build JS test target only: `pnpm run build:test` +- After `pnpm run build:test`, direct compiled runner: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn` +- Watch tests: `pnpm run watch:test` -```bash -pnpm run test:jvm -clojure -M:dev:test -pnpm run test:jvm --focus common-tests.logic.variants-switch-test -clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch -pnpm run test:js -pnpm run test:quiet -pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test -pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn -pnpm run watch:test -``` - -Use `test:quiet` for non-interactive JS runs; it buffers `build:test` output and forwards runner args. Common JS runner args support `--focus ` and `--log-level trace|debug|info|warn|error`. After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`. New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags compose as a union. +New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; +new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags +compose as a union. ## Test helpers @@ -45,4 +48,4 @@ For geometry-sensitive tests, read `mem:common/geometry-invariants` before posit ## Debugging -Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes. \ No newline at end of file +Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes. diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md index 496bcf6f2e..23881c6f89 100644 --- a/.serena/memories/critical-info.md +++ b/.serena/memories/critical-info.md @@ -14,6 +14,11 @@ You are working on the GitHub project `penpot/penpot`, a monorepo. - Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool. - Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps. *After making changes, run the applicable lint and format checks for the affected module before considering the work done (per example `mem:backend/core` or `mem:frontend/core`). +- Align `let` binding values: when a `let` form has multiple bindings spanning + several lines, align the value forms to the same column with spaces. +- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files, + fix them with `tools/paren-repair.bb` BEFORE running lint/format checks. + See `mem:tools/paren-repair` for usage. - Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra. # Project modules @@ -39,6 +44,9 @@ module. You can read it from `mem:/core` When working on devenv startup, compose layout, instance config (`defaults.env`), tmux session lifecycle, MinIO provisioning, or anything in `manage.sh`'s `*-devenv` commands, read `mem:devenv/core`. +- `tools/` contains standalone dev utilities: `nrepl-eval.mjs` (backend REPL eval), + `paren-repair.bb` (delimiter-error fixer, see `mem:tools/paren-repair`), and + `taiga.py` / `gh.py` (issue management helpers). - `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it. - `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it. diff --git a/.serena/memories/frontend/core.md b/.serena/memories/frontend/core.md index 7e0638ece2..7edf80bbe1 100644 --- a/.serena/memories/frontend/core.md +++ b/.serena/memories/frontend/core.md @@ -26,6 +26,11 @@ From `frontend/`: - Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. - Translation formatting after i18n edits: `pnpm run translations`. +**Before linting:** if delimiter errors are suspected (after LLM edits, or +lint/compiler reports syntax errors), run `tools/paren-repair.bb` on the +affected files first. Delimiter errors produce misleading linter output. +See `mem:tools/paren-repair`. + ## Focused memory routing UI and packages: diff --git a/.serena/memories/frontend/handling-errors-and-debugging.md b/.serena/memories/frontend/handling-errors-and-debugging.md index 29c7929112..ede8c1581b 100644 --- a/.serena/memories/frontend/handling-errors-and-debugging.md +++ b/.serena/memories/frontend/handling-errors-and-debugging.md @@ -10,6 +10,12 @@ You have access to two tools for finding errors in Clojure source code (which yo The latter is needed because syntax errors in parentheses give an uninformative compiler error, and the second tool can often find the exact location of such errors. +When delimiter errors are detected (typically from lint or compiler output), +fix the affected files with `tools/paren-repair.bb`. The `clj_check_parentheses` +MCP tool can also pinpoint the error location when available, but it is not +required — standard build errors are usually enough. +See `mem:tools/paren-repair`. + ## Runtime patching with `set!` Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching. diff --git a/.serena/memories/tools/paren-repair.md b/.serena/memories/tools/paren-repair.md new file mode 100644 index 0000000000..e3817ca839 --- /dev/null +++ b/.serena/memories/tools/paren-repair.md @@ -0,0 +1,29 @@ +# Paren-Repair + +`tools/paren-repair.bb` fixes mismatched parentheses, brackets, and braces in +Clojure/ClojureScript files, then reformats them with cljfmt. + +## When to use + +- After LLM edits introduce broken delimiters — proactively run it on files + you just touched. +- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax + errors mentioning mismatched/unclosed delimiters, reader errors, or + unexpected EOF. +- Before running lint/format checks — delimiter errors make linter output + misleading. Fix them first, then lint. + +## How to use + +```bash +# File mode (in-place fix + format) +bb tools/paren-repair.bb path/to/file.clj + +# Pipe mode (stdin → fixed code to stdout) +echo '(def x 1' | bb tools/paren-repair.bb + +# Help +bb tools/paren-repair.bb --help +``` + +`bb` must be invoked from the repo root so the path `tools/paren-repair.bb` resolves. diff --git a/CHANGES.md b/CHANGES.md index 13b92ada75..0fa79fab8e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -127,6 +127,13 @@ - Fix open overlay board mispositioned with repeated image artifacts in viewer WebGL [#10180](https://github.com/penpot/penpot/issues/10180) (PR: [#10191](https://github.com/penpot/penpot/pull/10191)) - Fix color picker preview when browser has non-100% zoom level [#10265](https://github.com/penpot/penpot/issues/10265) (PR: [#10305](https://github.com/penpot/penpot/pull/10305)) +## 2.16.2 + +### :bug: Bugs fixed + +- Fix error 500 when submitting the contact form [#10178](https://github.com/penpot/penpot/issues/10178) (PR: [#10419](https://github.com/penpot/penpot/pull/10419)) +- Fix text editor modifying content and detaching applied typography tokens [#10389](https://github.com/penpot/penpot/issues/10389) (PR: [#10402](https://github.com/penpot/penpot/pull/10402)) + ## 2.16.1 ### :sparkles: New features & Enhancements diff --git a/tools/paren-repair.bb b/tools/paren-repair.bb new file mode 100755 index 0000000000..aba0ee26ac --- /dev/null +++ b/tools/paren-repair.bb @@ -0,0 +1,361 @@ +#!/usr/bin/env bb + +;; ── Dependencies (resolved once, cached forever by Babashka) ── +(babashka.deps/add-deps + '{:deps {dev.weavejester/cljfmt {:mvn/version "0.15.5"} + parinferish/parinferish {:mvn/version "0.8.0"}}}) + +(ns paren-repair + "Standalone CLI tool for fixing delimiter errors and formatting Clojure files. + + Single-file consolidation of: + clojure-mcp-light.delimiter-repair (detection + repair engine) + clojure-mcp-light.hook (file detection + combine repair+format) + clojure-mcp-light.paren-repair (CLI / main entry point) + + Stripped: stats logging, timbre, tools.cli, apply-patch, tmp sessions. + Includes a fix for the process-stdin destructuring bug in the original." + + (:require [babashka.fs :as fs] + [cheshire.core :as json] + [cljfmt.core :as cljfmt] + [cljfmt.main] + [clojure.java.io :as io] + [clojure.java.shell :as shell] + [clojure.string :as string] + [edamame.core :as e] + [parinferish.core :as parinferish])) + +;; ═══════════════════════════════════════════════════════════════════════════════ +;; Section 1: Delimiter Detection & Repair +;; (from delimiter_repair.clj — stats calls removed) +;; ═══════════════════════════════════════════════════════════════════════════════ + +(def ^:dynamic *signal-on-bad-parse* + "When true, non-delimiter parse errors still trigger parinfer as a safety net. + Running parinfer on valid code is generally benign." + true) + +(defn delimiter-error? + "Returns true if the string has a delimiter error specifically. + Checks that it's an :edamame/error with :edamame/opened-delimiter info. + Uses :all true to enable all standard Clojure reader features: + function literals, regex, quotes, syntax-quote, deref, var, etc. + Also enables :read-cond :allow to support reader conditionals. + Handles unknown data readers gracefully with a default reader fn." + [s] + (try + (e/parse-string-all s {:all true + :features #{:bb :clj :cljs :cljr :default} + :read-cond :allow + :readers (fn [_tag] (fn [data] data)) + :auto-resolve name}) + false ;; No error = no delimiter error + (catch clojure.lang.ExceptionInfo ex + (let [data (ex-data ex)] + (and (= :edamame/error (:type data)) + (contains? data :edamame/opened-delimiter)))) + (catch Exception _ + ;; Non-edamame parse error — run parinfer as a safety net + ;; (parinfer on valid code is generally benign) + *signal-on-bad-parse*))) + +(defn actual-delimiter-error? + "Like delimiter-error? but never falls back to parinfer on unknown parse errors." + [s] + (binding [*signal-on-bad-parse* false] + (delimiter-error? s))) + +(defn parinferish-repair + "Attempts to repair delimiter errors using parinferish (pure Clojure). + Returns a map with :success, :text, and :error." + [s] + (try + (let [repaired (parinferish/flatten + (parinferish/parse s {:mode :indent}))] + {:success true + :text repaired + :error nil}) + (catch Exception e + {:success false + :error (.getMessage e)}))) + +(def parinfer-rust-available? + "Check if parinfer-rust binary is available on PATH. Result is memoized." + (memoize + (fn [] + (try + (let [result (shell/sh "which" "parinfer-rust")] + (zero? (:exit result))) + (catch Exception _ + false))))) + +(defn parinfer-repair + "Attempts to repair delimiter errors using parinfer-rust (external binary). + Returns a map with :success, :text, and :error. + Uses JSON output format from parinfer-rust." + [s] + (let [result (shell/sh "parinfer-rust" + "--mode" "indent" + "--language" "clojure" + "--output-format" "json" + :in s) + exit-code (:exit result)] + (if (zero? exit-code) + (try + (json/parse-string (:out result) true) + (catch Exception _ + {:success false})) + {:success false}))) + +(defn repair-delimiters + "Unified delimiter repair: prefers parinfer-rust when available, + falls back to parinferish (pure Clojure). + Returns a map with :success, :text, and :error." + [s] + (if (parinfer-rust-available?) + (parinfer-repair s) + (parinferish-repair s))) + +(defn fix-delimiters + "Takes a Clojure string and attempts to fix delimiter errors. + Returns the repaired string if successful, or nil if unfixable. + If no delimiter errors exist, returns the original string unchanged." + [s] + (if (delimiter-error? s) + (let [{:keys [text success]} (repair-delimiters s)] + (when (and success text (not (delimiter-error? text))) + text)) + s)) + +;; ═══════════════════════════════════════════════════════════════════════════════ +;; Section 2: File Processing +;; (from hook.clj — stripped of stats, timbre, backup/restore, hook dispatch) +;; ═══════════════════════════════════════════════════════════════════════════════ + +(def ^:dynamic *enable-cljfmt* + "When true, files are reformatted with cljfmt after delimiter repair." + false) + +(defn- babashka-shebang? + "Checks if a file starts with a Babashka shebang line." + [file-path] + (when (fs/exists? file-path) + (try + (with-open [r (io/reader file-path)] + (let [line (-> r line-seq first)] + (and line + (re-matches #"^#!/[^\s]+/(bb|env\s{1,3}bb)(\s.*)?$" line)))) + (catch Exception _ false)))) + +(defn clojure-file? + "Checks if a file path has a Clojure-related extension or Babashka shebang. + Supported extensions: .clj .cljs .cljc .cljd .bb .edn .lpy + Also detects files with a Babashka shebang (#!/.../bb)." + [file-path] + (when file-path + (let [lower-path (string/lower-case file-path)] + (or (string/ends-with? lower-path ".clj") + (string/ends-with? lower-path ".cljs") + (string/ends-with? lower-path ".cljc") + (string/ends-with? lower-path ".cljd") + (string/ends-with? lower-path ".bb") + (string/ends-with? lower-path ".lpy") + (string/ends-with? lower-path ".edn") + (babashka-shebang? file-path))))) + +(defn run-cljfmt + "Check if file needs formatting (via cljfmt.core), then reformat in-place + (via cljfmt.main to respect user cljfmt config). Returns true if file was + reformatted, false otherwise." + [file-path] + (when *enable-cljfmt* + (try + (let [original (slurp file-path :encoding "UTF-8") + formatted (cljfmt/reformat-string original)] + (if (not= original formatted) + (do + (cljfmt.main/-main "fix" file-path) + true) + false)) + (catch Exception _ + false)))) + +(defn fix-and-format-file! + "Core logic for fixing delimiters and (optionally) formatting a Clojure file + in-place. Returns a map with :success, :delimiter-fixed, :formatted, and + :message." + [file-path enable-cljfmt] + (try + (let [file-content (slurp file-path :encoding "UTF-8") + has-delimiter-error? (delimiter-error? file-content)] + (if has-delimiter-error? + ;; Has delimiter error — try to fix + (if-let [fixed-content (fix-delimiters file-content)] + (do + (spit file-path fixed-content :encoding "UTF-8") + (let [formatted? (binding [*enable-cljfmt* enable-cljfmt] + (run-cljfmt file-path))] + {:success true + :delimiter-fixed true + :formatted (boolean formatted?) + :message "Delimiter errors fixed and formatted"})) + {:success false + :delimiter-fixed false + :formatted false + :message "Could not fix delimiter errors"}) + ;; No delimiter error — just format if enabled + (let [formatted? (binding [*enable-cljfmt* enable-cljfmt] + (run-cljfmt file-path))] + {:success true + :delimiter-fixed false + :formatted (boolean formatted?) + :message (if formatted? "Formatted" "No changes needed")}))) + (catch Exception e + {:success false + :delimiter-fixed false + :formatted false + :message (str "Error: " (.getMessage e))}))) + +;; ═══════════════════════════════════════════════════════════════════════════════ +;; Section 3: CLI +;; (from paren_repair.clj — with process-stdin bug fix, no timbre) +;; ═══════════════════════════════════════════════════════════════════════════════ + +(defn has-stdin-data? + "Check if stdin has data available (not a TTY). + Returns true if stdin is ready to be read (e.g., piped input or heredoc)." + [] + (try + (.ready *in*) + (catch Exception _ false))) + +(defn process-stdin + "Process code from stdin: fix delimiters and format. + Outputs result to stdout. + Returns a map with :success and :changed." + [] + (let [input (slurp *in*) + fixed (fix-delimiters input)] + (if fixed + ;; fix-delimiters succeeded (or no errors) — format and print + (let [formatted (try + (cljfmt/reformat-string fixed) + (catch Exception _ + fixed)) + changed? (not= input formatted)] + (print formatted) + (flush) + {:success true + :changed changed?}) + ;; fix-delimiters returned nil (unfixable) + (do + (binding [*out* *err*] + (println "Error: Could not fix delimiter errors")) + {:success false + :changed false})))) + +(defn process-file + "Process a single file: fix delimiters and format in-place. + Returns a map with :success, :file-path, :message, :delimiter-fixed, + and :formatted." + [file-path] + (cond + (not (fs/exists? file-path)) + {:success false + :file-path file-path + :message "File does not exist" + :delimiter-fixed false + :formatted false} + + (not (clojure-file? file-path)) + {:success false + :file-path file-path + :message "Not a Clojure file (skipping)" + :delimiter-fixed false + :formatted false} + + :else + (assoc (fix-and-format-file! file-path true) + :file-path file-path))) + +(defn show-help [] + (println "Usage: paren-repair [FILE ...]") + (println " echo CODE | paren-repair") + (println " paren-repair <<'EOF' ... EOF") + (println) + (println "Fix delimiter errors and format Clojure code.") + (println) + (println "When no files are provided, reads from stdin and writes to stdout.") + (println "If no changes are needed, echoes the input unchanged.") + (println) + (println "Options:") + (println " -h, --help Show this help message")) + +(defn -main [& args] + (let [show-help? (some #{"--help" "-h"} args) + file-args (remove #{"--help" "-h"} args)] + + (cond + ;; Help requested + show-help? + (do + (show-help) + (System/exit 0)) + + ;; No file args — check for stdin + (empty? file-args) + (if (has-stdin-data?) + ;; Stdin mode: read, process, output to stdout + (let [result (process-stdin)] + (System/exit (if (:success result) 0 1))) + ;; No stdin and no files — show help + (do + (show-help) + (System/exit 1))) + + ;; File mode + :else + (try + (let [results (doall (map process-file file-args)) + successes (filter :success results) + failures (filter (complement :success) results) + success-count (count successes) + failure-count (count failures)] + + ;; Print results + (println) + (println "paren-repair Results") + (println "========================") + (println) + + (doseq [{:keys [file-path message delimiter-fixed formatted]} results] + (let [tags (when (or delimiter-fixed formatted) + (str " [" + (string/join ", " + (filter some? + [(when delimiter-fixed "delimiter-fixed") + (when formatted "formatted")])) + "]"))] + (println (str " " file-path ": " message tags)))) + + (println) + (println "Summary:") + (println " Success:" success-count) + (println " Failed: " failure-count) + (println) + + (if (zero? failure-count) + (System/exit 0) + (System/exit 1))) + (catch Exception e + (binding [*out* *err*] + (println "Fatal error:" (.getMessage e))) + (System/exit 1)))))) + +;; ═══════════════════════════════════════════════════════════════════════════════ +;; Entry point — only run -main when executed directly (not loaded as lib) +;; ═══════════════════════════════════════════════════════════════════════════════ + +(when (= *file* (System/getProperty "babashka.file")) + (apply -main *command-line-args*))