diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml new file mode 100644 index 0000000..b84a7cb --- /dev/null +++ b/.gitea/workflows/verify.yml @@ -0,0 +1,39 @@ +name: Verify + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: verify-${{ gitea.repository }}-${{ gitea.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + CI: "true" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + - name: Select declared npm version + run: npm install --global npm@11.17.0 + - name: Install dependencies + run: npm ci + - name: Audit runtime dependencies + run: npm audit --omit=dev --audit-level=moderate + - name: Check, test, and build + run: npm run check + - name: Install browser engines + run: npx playwright install --with-deps chromium firefox webkit + - name: Browser tests + run: npm run test:browser diff --git a/CHANGELOG.md b/CHANGELOG.md index a4e5957..0640062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add stable retry voting, multi-file cases, adapter-defined predicates and + exported reproducibility evidence for flaky or structured failures. + ## 0.1.0 - 2026-09-01 - Add bounded structure-aware JSON/XML and textual delta debugging diff --git a/README.md b/README.md index dbf2399..143e78b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Reduce a failing input while repeatedly proving that the selected failure still occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas Toolbox. -## v0.1.0 capabilities +## v0.2.0 capabilities - Minimize plain text by lines, lexical tokens, and Unicode code points - Apply structure-aware reductions before textual delta debugging for bounded @@ -18,6 +18,11 @@ Toolbox. timeout; cancel any run and retain the last complete result - Enforce explicit predicate-test and wall-time budgets and show every accepted size reduction +- Sample predicates with single, majority or strict stability profiles and + report observed flaky candidates rather than silently trusting one run +- Minimize bounded multi-file bundles first by file membership and then by + structure-aware file content, using deterministic JSON-map, delimited or + primary-file adapter profiles - Export the minimized case and a deterministic report containing the predicate, engine version, limits, and reduction trace @@ -34,6 +39,9 @@ Minimization finds a locally minimal case under the attempted transformations and budget, not a mathematically globally shortest input. Regex timing depends on the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30 seconds, regex worker execution at one second, and stylesheets at 128 KiB. +Bundles are limited to 100 files, 4 MiB total and 2 MiB per file. Adapter +profiles serialize bundle candidates for an existing predicate; they do not +emulate a filesystem or execute a target program. ## Development diff --git a/SOURCE.md b/SOURCE.md index a9198ac..d2e64de 100644 --- a/SOURCE.md +++ b/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Minimize Tools 0.1.0 is available at: +The corresponding source for Minimize Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1010c6d..eb2e3a0 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Minimize Tools 0.1.0 directly depends on these runtime packages: +Minimize Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/package-lock.json b/package-lock.json index eee59b7..6a3b9e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "minimize-tools", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "minimize-tools", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", @@ -42,24 +42,25 @@ } }, "node_modules/@add-ideas/toolbox-contract": { - "version": "0.2.3", - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz", + "integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==", + "license": "Apache-2.0" }, "node_modules/@add-ideas/toolbox-helpers": { - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-3.0-or-later", "engines": { "node": ">=22" } }, "node_modules/@add-ideas/toolbox-shell-react": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz", + "integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==", "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "peerDependencies": { "react": ">=18 <20", @@ -67,17 +68,16 @@ } }, "node_modules/@add-ideas/toolbox-testkit": { - "version": "0.2.3", + "version": "0.3.0", + "resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz", + "integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3" + "@add-ideas/toolbox-contract": "0.3.0" }, "bin": { "toolbox-check": "dist/cli.js" - }, - "engines": { - "node": ">=20" } }, "node_modules/@adobe/css-tools": { diff --git a/package.json b/package.json index d5b4fbf..fec42fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minimize-tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Reduce failing inputs while preserving a bounded local failure predicate.", "license": "GPL-3.0-or-later", "author": "Albrecht Degering", @@ -39,14 +39,14 @@ "release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force" }, "dependencies": { - "@add-ideas/toolbox-contract": "0.2.3", - "@add-ideas/toolbox-helpers": "0.1.0", - "@add-ideas/toolbox-shell-react": "0.2.3", + "@add-ideas/toolbox-contract": "0.3.0", + "@add-ideas/toolbox-helpers": "0.2.0", + "@add-ideas/toolbox-shell-react": "0.3.0", "react": "19.2.8", "react-dom": "19.2.8" }, "devDependencies": { - "@add-ideas/toolbox-testkit": "0.2.3", + "@add-ideas/toolbox-testkit": "0.3.0", "@eslint/js": "10.0.1", "@playwright/test": "1.62.1", "@testing-library/jest-dom": "6.9.1", diff --git a/playwright.config.ts b/playwright.config.ts index 7ed3eec..2fedeb5 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,7 +15,25 @@ export default defineConfig({ timeout: 180_000, }, projects: [ - { name: "chromium", use: { ...devices["Desktop Chrome"] } }, - { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { + name: "chromium", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Firefox"] }, + }, + { + name: "webkit", + testIgnore: /responsive\.spec\.ts/, + use: { ...devices["Desktop Safari"] }, + }, + { + name: "mobile-chromium", + testMatch: /responsive\.spec\.ts/, + use: { ...devices["Pixel 5"] }, + }, ], }); diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index a4e5957..0640062 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.2.0 - 2026-09-02 + +- Add stable retry voting, multi-file cases, adapter-defined predicates and + exported reproducibility evidence for flaky or structured failures. + ## 0.1.0 - 2026-09-01 - Add bounded structure-aware JSON/XML and textual delta debugging diff --git a/public/LICENSES/npm-runtime-licenses.txt b/public/LICENSES/npm-runtime-licenses.txt index 45a7c95..4f412f1 100644 --- a/public/LICENSES/npm-runtime-licenses.txt +++ b/public/LICENSES/npm-runtime-licenses.txt @@ -1,5 +1,5 @@ ============================================================================== -@add-ideas/toolbox-contract@0.2.3 +@add-ideas/toolbox-contract@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- @@ -198,7 +198,7 @@ Declared licence: Apache-2.0 ============================================================================== -@add-ideas/toolbox-helpers@0.1.0 +@add-ideas/toolbox-helpers@0.2.0 Declared licence: GPL-3.0-or-later ============================================================================== --- LICENSE --- @@ -879,7 +879,7 @@ Public License instead of this License. But first, please read ============================================================================== -@add-ideas/toolbox-shell-react@0.2.3 +@add-ideas/toolbox-shell-react@0.3.0 Declared licence: Apache-2.0 ============================================================================== --- LICENSE --- diff --git a/public/README.md b/public/README.md index dbf2399..143e78b 100644 --- a/public/README.md +++ b/public/README.md @@ -4,7 +4,7 @@ Reduce a failing input while repeatedly proving that the selected failure still occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas Toolbox. -## v0.1.0 capabilities +## v0.2.0 capabilities - Minimize plain text by lines, lexical tokens, and Unicode code points - Apply structure-aware reductions before textual delta debugging for bounded @@ -18,6 +18,11 @@ Toolbox. timeout; cancel any run and retain the last complete result - Enforce explicit predicate-test and wall-time budgets and show every accepted size reduction +- Sample predicates with single, majority or strict stability profiles and + report observed flaky candidates rather than silently trusting one run +- Minimize bounded multi-file bundles first by file membership and then by + structure-aware file content, using deterministic JSON-map, delimited or + primary-file adapter profiles - Export the minimized case and a deterministic report containing the predicate, engine version, limits, and reduction trace @@ -34,6 +39,9 @@ Minimization finds a locally minimal case under the attempted transformations and budget, not a mathematically globally shortest input. Regex timing depends on the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30 seconds, regex worker execution at one second, and stylesheets at 128 KiB. +Bundles are limited to 100 files, 4 MiB total and 2 MiB per file. Adapter +profiles serialize bundle candidates for an existing predicate; they do not +emulate a filesystem or execute a target program. ## Development diff --git a/public/SOURCE.md b/public/SOURCE.md index a9198ac..d2e64de 100644 --- a/public/SOURCE.md +++ b/public/SOURCE.md @@ -1,7 +1,7 @@ # Corresponding source -The corresponding source for Minimize Tools 0.1.0 is available at: +The corresponding source for Minimize Tools 0.2.0 is available at: -https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.1.0 +https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.2.0 Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`. diff --git a/public/THIRD_PARTY_NOTICES.md b/public/THIRD_PARTY_NOTICES.md index 1010c6d..eb2e3a0 100644 --- a/public/THIRD_PARTY_NOTICES.md +++ b/public/THIRD_PARTY_NOTICES.md @@ -1,12 +1,12 @@ # Third-party notices -Minimize Tools 0.1.0 directly depends on these runtime packages: +Minimize Tools 0.2.0 directly depends on these runtime packages: | Package | Pinned version | Declared licence | | -------------------------------- | -------------: | ---------------- | -| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 | -| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later | -| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 | +| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 | +| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later | +| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 | | `react` | 19.2.8 | MIT | | `react-dom` | 19.2.8 | MIT | diff --git a/public/sw.js b/public/sw.js index f46057e..20caab6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ const CACHE_PREFIX = "minimize-tools-shell-"; -const CACHE_NAME = CACHE_PREFIX + "0.1.0"; +const CACHE_NAME = CACHE_PREFIX + "0.2.0"; const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"]; self.addEventListener("install", (event) => { event.waitUntil( diff --git a/public/toolbox-app.json b/public/toolbox-app.json index 06dcdda..d15c031 100644 --- a/public/toolbox-app.json +++ b/public/toolbox-app.json @@ -3,7 +3,7 @@ "schemaVersion": 1, "id": "de.add-ideas.minimize-tools", "name": "Minimize Tools", - "version": "0.1.0", + "version": "0.2.0", "description": "Reduce failing inputs while preserving the failure locally.", "entry": "./", "icon": "./favicon.svg", @@ -23,11 +23,28 @@ }, "requirements": { "secureContext": false, - "workers": true, + "workers": false, "indexedDb": false, "crossOriginIsolated": false, "topLevelContext": false }, + "io": { + "accepts": [ + { "mediaType": "text/plain", "extensions": [".txt"] }, + { "mediaType": "application/json", "extensions": [".json"] }, + { + "mediaType": "application/xml", + "extensions": [".xml", ".xsl", ".xslt"] + } + ], + "produces": [ + { "mediaType": "text/plain", "extensions": [".txt"] }, + { "mediaType": "application/json", "extensions": [".json"] }, + { "mediaType": "application/xml", "extensions": [".xml"] }, + { "mediaType": "application/zip", "extensions": [".zip"] } + ] + }, + "capabilities": { "required": [], "optional": ["workers"] }, "privacy": { "processing": "local", "fileUploads": true, diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index cc05359..7ed6f8e 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -2,6 +2,7 @@ import { useMemo, useRef, useState, type ChangeEvent } from "react"; import { stableStringify, triggerBlobDownload, + triggerBlobDownloads, } from "@add-ideas/toolbox-helpers"; import { byteLength, @@ -9,7 +10,15 @@ import { type InputStructure, type MinimizeProgress, type MinimizeResult, + type PredicateStability, } from "../minimize/engine"; +import { + bundlePredicateAdapter, + minimizeBundle, + type BundleAdapterProfile, + type BundleFile, + type BundleMinimizeResult, +} from "../minimize/bundle"; import { createPredicate, type PredicateKind, @@ -159,6 +168,13 @@ export function Workbench() { const [preserveSignature, setPreserveSignature] = useState(true); const [maxTests, setMaxTests] = useState(500); const [maxSeconds, setMaxSeconds] = useState(10); + const [stabilityProfile, setStabilityProfile] = useState< + "single" | "majority" | "strict" + >("majority"); + const [bundleFiles, setBundleFiles] = useState([]); + const [bundleAdapter, setBundleAdapter] = + useState("concatenated"); + const [bundleResult, setBundleResult] = useState(); const [result, setResult] = useState(); const [resultRecipe, setResultRecipe] = useState(); const [resultLimits, setResultLimits] = useState<{ @@ -183,6 +199,12 @@ export function Workbench() { stylesheet, preserveFailureSignature: preserveSignature, }; + const stability: PredicateStability = + stabilityProfile === "single" + ? { samples: 1, requiredPasses: 1 } + : stabilityProfile === "strict" + ? { samples: 3, requiredPasses: 3 } + : { samples: 3, requiredPasses: 2 }; const report = useMemo( () => @@ -199,6 +221,8 @@ export function Workbench() { accepted: result.accepted, exhausted: result.exhausted, steps: result.steps, + stability: result.stability, + unstableCandidates: result.unstableCandidates, }, 2, { maxTextChars: 2 * 1024 * 1024, maxDepth: 16, maxNodes: 20_000 }, @@ -219,6 +243,19 @@ export function Workbench() { setError(undefined); try { const predicate = createPredicate(recipe, runner.evaluate); + if (bundleFiles.length) { + const next = await minimizeBundle( + bundleFiles, + { maxTests, maxSeconds, stability }, + bundlePredicateAdapter(predicate, bundleAdapter), + nextController.signal, + ); + setBundleResult(next); + setResult(undefined); + setResultRecipe(structuredClone(recipe)); + setResultLimits({ maxTests, maxSeconds }); + return; + } const next = await minimizeInput( source, { @@ -229,12 +266,14 @@ export function Workbench() { ? Math.min(maxTests, 200) : maxTests, maxSeconds, + stability, }, predicate, nextController.signal, setProgress, ); setResult(next); + setBundleResult(undefined); setResultRecipe(structuredClone(recipe)); setResultLimits({ maxTests: @@ -270,6 +309,7 @@ export function Workbench() { return; } setSource(await file.text()); + setBundleFiles([]); setSourceName(file.name); setStructure( file.name.toLowerCase().endsWith(".json") @@ -281,6 +321,27 @@ export function Workbench() { setError(undefined); }; + const openBundle = async (event: ChangeEvent) => { + const files = [...(event.target.files ?? [])]; + event.target.value = ""; + try { + if (files.length > 100) + throw new RangeError("Bundles are limited to 100 files."); + const loaded = await Promise.all( + files.map(async (file) => ({ + path: file.webkitRelativePath || file.name, + content: await file.text(), + })), + ); + setBundleFiles(loaded); + setError(undefined); + } catch (reason) { + setError( + reason instanceof Error ? reason.message : "Bundle loading failed.", + ); + } + }; + const applyPreset = (name: "json" | "xml" | "regex") => { if (name === "json") { setSource(JSON_SAMPLE); @@ -349,6 +410,14 @@ export function Workbench() { onChange={(event) => void openFile(event)} /> +