diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/approvals.json b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/approvals.json new file mode 100644 index 00000000..87b9bafb --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/approvals.json @@ -0,0 +1,4 @@ +{ + "approvals": [], + "reopenings": [] +} diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/change.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/change.md new file mode 100644 index 00000000..a544e4a3 --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/change.md @@ -0,0 +1,25 @@ +--- +id: keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths +state: draft +type: bug_fix +base_commit: c19963be38ec9843b738c1dc335c53809bb99207 +--- + +# Keep element bundle verification clean on failure and usable from spaced checkout paths + +## Intent + +Keep element bundle verification clean on failure and usable from spaced checkout paths + +## Affected Canonical Specs + +- `ThreeMD` +- `ThreeMDElement` + +## Acceptance Criteria + +- Matching bundles pass; stale web/package bundles, storage leakage, oversized output and compiler errors fail without modifying bundles or leaving scratch directories; checkouts containing spaces work; all seven subprocess regressions and the actual bundle check run in the existing native lane. + +## No-spec Rationale + +Not applicable diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/context.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/context.md new file mode 100644 index 00000000..204059ae --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/context.md @@ -0,0 +1,8 @@ +--- +change: keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths +artifact: context +--- + +# Context + +The existing bundle checker exited from inside its try block on validation failure, skipping asynchronous cleanup. It also passed URL.pathname to the compiler, so percent-encoded spaces were treated as literal path characters. Both issues were reproduced in disposable fixtures on current main. This is a maintenance fix under the requested project audit. Human definition approval and implementation acceptance have not been recorded. diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMD.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMD.md new file mode 100644 index 00000000..7772192e --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMD.md @@ -0,0 +1,18 @@ +# ThreeMD delta + +## MODIFIED + +### Spec Section Dependencies + +- Swift Foundation for text, Data, JSON and localized errors. +- Conditional Apple system Compression for optional LZFSE; no third-party Swift + dependency or CryptoKit requirement. The original text surface stays portable. +- TypeScript adds no production dependency. Rust adds exact unicode-normalization 0.1.25 for new canonical key comparison, locked with its small dependencies. Existing text parsing/serialization behavior is unchanged. +- ThreeMD 2.1 adds no runtime dependency in any language. Rust keeps + `unicode-normalization = "=0.1.25"` as its only dependency and adds + `#![forbid(unsafe_code)]`. The fast CRC is table-driven code in each port, + with no hardware intrinsics required. +- Repository validation runs the element bundle checker and its subprocess + regressions through the existing `element-bundle` lane step. The checker + preserves bundle bytes, removes temporary output after success or failure, + and supports checkout paths containing spaces. This adds no library dependency. diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMDElement.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMDElement.md new file mode 100644 index 00000000..b1e8ade7 --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/deltas/ThreeMDElement.md @@ -0,0 +1,36 @@ +# ThreeMDElement delta + +## MODIFIED + +### Spec Section Invariants + +1. Planes are positioned relative to the focus (`d = idx - focus`), so the + selected plane is always frontmost at the largest true Z. A fixed + `z = -idx * 150` layout would leave plane 0 in front no matter what is + selected, which Safari exposes because it paints by true Z and ignores + `z-index` inside `preserve-3d`. +2. `render()` is synchronous and is called on every interaction. It does not + depend on `requestAnimationFrame`, so it stays correct when iOS Low Power Mode + throttles or pauses rAF. The rAF loop only adds optional idle drift and the + element is fully usable if it never runs. +3. The stage uses `touch-action: none` so a finger drives the model: a vertical + drag moves along Z, a horizontal drag orbits. The element never overflows + horizontally, from 320px to 1440px wide. +4. The element bundle contains no storage code and stays within a size budget. + The freshly built bundle (`bun build element/src/three-md.ts --format esm + --target browser --minify`) contains none of the markers `TextDecoder`, + `Int32Array`, `Float32Array`, `DocumentStorageError`, + `Scalar fields cannot contain`, `structured payload` and `getBigUint64`, and + is at most 50,000 bytes (2.0.0: 48,816 bytes). This holds because the + library's `storage.ts`, `structured.ts` and `checksum.ts` have no top-level + side effects (every module-level initializer is a literal, a function or class + declaration, or an expression marked `/* @__PURE__ */`; CRC tables, the + `TextDecoder`, `TextEncoder`, `DataView` scratch buffers and typed arrays are + created lazily), and because `canonicalNumber` lives in the pure + `js/src/number.ts`, so the parser has no import edge into `storage.ts`. The + fresh build still equals `web/assets/three-md.js` and, when present, + `element/dist/three-md.js`. +5. Bundle verification removes its temporary build directory after both success + and failure, preserves the committed bundles, and accepts checkout paths + containing spaces. A failed validation exits nonzero without printing the + success message. diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/design.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/design.md new file mode 100644 index 00000000..cb456c9b --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/design.md @@ -0,0 +1,8 @@ +--- +change: keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths +artifact: design +--- + +# Design + +Keep bundle contents, parser-only restrictions, byte budget and validation exit codes unchanged. Set process.exitCode on validation failure so finally removes the scratch directory, and emit success only on the success branch. Decode the source file URL with fileURLToPath. Test copied actual checker processes against minimal browser module fixtures, inspecting scratch before parent cleanup. Wire the regressions into the existing element-bundle task. diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/state.json b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/state.json new file mode 100644 index 00000000..cd33f280 --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/state.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "workflow_version": 2, + "workflow_origin_version": 2, + "id": "keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths", + "slug": "keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths", + "title": "Keep element bundle verification clean on failure and usable from spaced checkout paths", + "description": "Keep element bundle verification clean on failure and usable from spaced checkout paths", + "kind": "bug_fix", + "state": "draft", + "base_commit": "c19963be38ec9843b738c1dc335c53809bb99207", + "created_at": 1792120266, + "updated_at": 1792121365, + "affected_specs": [ + "ThreeMD", + "ThreeMDElement" + ], + "affected_paths": [ + "scripts/check-element-bundle.mjs", + "scripts/check-element-bundle.test.ts", + "fledge.toml", + "specs/ThreeMD", + "specs/ThreeMDElement" + ], + "no_spec_change": false, + "no_spec_change_rationale": null, + "acceptance_criteria": [ + "Matching bundles pass; stale web/package bundles, storage leakage, oversized output and compiler errors fail without modifying bundles or leaving scratch directories; checkouts containing spaces work; all seven subprocess regressions and the actual bundle check run in the existing native lane." + ], + "selected_artifacts": [ + "context", + "testing", + "tasks", + "design" + ], + "dependencies": [], + "answers": { + "architecture_risk": "no", + "constraints": "Preserve the parser-only element bundle, the 50000-byte budget, exact published bundle bytes, source/library formats, signer policy and truthful human lifecycle boundaries; run substantive subprocess negative controls.", + "public_contract": "no" + } +} diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/tasks.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/tasks.md new file mode 100644 index 00000000..1b83609c --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/tasks.md @@ -0,0 +1,14 @@ +--- +change: keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths +artifact: tasks +--- + +# Tasks + +- [x] Reproduce four validation scratch leaks and spaced checkout failure. +- [x] Fix cleanup and path decoding without changing bundle bytes. +- [x] Add seven actual-subprocess regressions and wire them into native verification. +- [x] Complete targeted, strict contract, full native and independent agent review. +- [ ] Recheck committed-range risk/provenance and remote head before any publication. +- [ ] Obtain genuine human definition/implementation acceptance before lifecycle closure. +- [ ] Review hosted CI and requested corvid-agent review; no merge in this preparation. diff --git a/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/testing.md b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/testing.md new file mode 100644 index 00000000..e9d9dee3 --- /dev/null +++ b/.specsync/changes/keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths/testing.md @@ -0,0 +1,8 @@ +--- +change: keep-element-bundle-verification-clean-on-failure-and-usable-from-spaced-checkout-paths +artifact: testing +--- + +# Testing + +Corrected before-fix receipt:20261016T030801Z-D4-39c625 (2 pass,5 fail). After-fix task:20261016T030826Z-D4-bb8919 (7 pass,0 fail, real48840-byte bundle). Full pinned Trust1.2.2/native gate:20261016T031300Z-D4-2b036d, exit0,258.729s. It ran322 Swift tests,431 TypeScript tests,115 Rust tests including doc tests with1 existing ignored case,17991 imports across9 writer/reader pairs, the7 checker regressions, real bundle drift and editor grammar. Latest released SpecSync6.1.1 strict/100%coverage:20261016T030945Z-D4-9a6ae5, exit0. A separate read-only GPT reviewer found no actionable defect. Native Trust was run on the working tree before a product commit, so its empty committed-range provenance does not qualify a future commit. No hosted CI, human review, signature or merge is claimed. An earlier attempted all-process network sandbox failed Swift nested sandbox creation; the successful gate retains Swift’s sandbox and confines dependency installation to a network-denied wrapper using existing caches. diff --git a/fledge.toml b/fledge.toml index aa7f3c90..501457f9 100644 --- a/fledge.toml +++ b/fledge.toml @@ -11,7 +11,7 @@ fmt = "swift-format format --in-place --recursive Sources Tests Package.swift" lint = "swift-format lint --strict --recursive Sources Tests Package.swift" build = "swift build" test = "swift test" -element-bundle = "bun scripts/check-element-bundle.mjs" +element-bundle = "bun test scripts/check-element-bundle.test.ts && bun scripts/check-element-bundle.mjs" interchange = "swift run threemd-interchange" [tasks.editor] diff --git a/scripts/check-element-bundle.mjs b/scripts/check-element-bundle.mjs index 5c461a98..833a8b4c 100644 --- a/scripts/check-element-bundle.mjs +++ b/scripts/check-element-bundle.mjs @@ -1,6 +1,7 @@ import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { $ } from "bun"; const root = new URL("../", import.meta.url); @@ -19,7 +20,7 @@ const maximumBundleBytes = 50_000; const builtBundle = join(tempDir, "three-md.js"); try { - await $`bun build ${source.pathname} --outfile ${builtBundle} --format esm --target browser --minify`; + await $`bun build ${fileURLToPath(source)} --outfile ${builtBundle} --format esm --target browser --minify`; const built = await Bun.file(builtBundle).text(); const web = await Bun.file(webBundle).text(); @@ -49,10 +50,10 @@ try { } if (failed) { - process.exit(1); + process.exitCode = 1; + } else { + console.log(`element bundle is current (${bundleBytes} bytes, no storage code)`); } - - console.log(`element bundle is current (${bundleBytes} bytes, no storage code)`); } finally { await rm(tempDir, { recursive: true, force: true }); } diff --git a/scripts/check-element-bundle.test.ts b/scripts/check-element-bundle.test.ts new file mode 100644 index 00000000..bc6e9dd1 --- /dev/null +++ b/scripts/check-element-bundle.test.ts @@ -0,0 +1,100 @@ +import { expect, test } from "bun:test"; +import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const checker = fileURLToPath(new URL("./check-element-bundle.mjs", import.meta.url)); + +type Scenario = { + source?: string; + web?: string; + dist?: string; + spaces?: boolean; + buildError?: boolean; +}; + +async function check(scenario: Scenario = {}) { + const root = await mkdtemp(join(tmpdir(), scenario.spaces ? "element check " : "element-check-")); + try { + for (const directory of ["scripts", "element/src", "element/dist", "web/assets", "tmp"]) { + await mkdir(join(root, directory), { recursive: true }); + } + await cp(checker, join(root, "scripts/check-element-bundle.mjs")); + const source = scenario.source ?? "export const value = 42;"; + const entry = join(root, "element/src/three-md.ts"); + await writeFile(entry, source); + const built = await Bun.build({ entrypoints: [entry], format: "esm", target: "browser", minify: true }); + const bundle = built.success ? await built.outputs[0].text() : "invalid source"; + const web = scenario.web ?? bundle; + const dist = scenario.dist ?? bundle; + await writeFile(join(root, "web/assets/three-md.js"), web); + await writeFile(join(root, "element/dist/three-md.js"), dist); + if (scenario.buildError) await writeFile(entry, "export const = ;"); + + const child = Bun.spawn([process.execPath, "scripts/check-element-bundle.mjs"], { + cwd: root, + env: { + ...process.env, + PATH: `${dirname(process.execPath)}:${process.env.PATH ?? ""}`, + TMPDIR: join(root, "tmp"), + }, + stdout: "pipe", + stderr: "pipe", + }); + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + return { + exitCode, + stdout, + stderr, + scratch: await readdir(join(root, "tmp")), + webUnchanged: await readFile(join(root, "web/assets/three-md.js"), "utf8") === web, + distUnchanged: await readFile(join(root, "element/dist/three-md.js"), "utf8") === dist, + }; + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +test("a matching bundle succeeds without leaving scratch output", async () => { + const result = await check(); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("element bundle is current"); + expect(result.scratch).toEqual([]); + expect(result.webUnchanged && result.distUnchanged).toBe(true); +}); + +for (const [name, scenario, diagnostic] of [ + ["stale web bundle", { web: "stale web" }, "web/assets/three-md.js is stale"], + ["stale package bundle", { dist: "stale dist" }, "element/dist/three-md.js is stale"], + ["storage code", { source: 'export const value = "DocumentStorageError";' }, "contains storage code"], + ["oversized bundle", { source: `export const value = ${JSON.stringify("x".repeat(50_001))};` }, "over the 50000-byte budget"], +] as const) { + test(`${name} fails without leaking scratch output or changing bundles`, async () => { + const result = await check(scenario); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain(diagnostic); + expect(result.stdout).not.toContain("element bundle is current"); + expect(result.scratch).toEqual([]); + expect(result.webUnchanged && result.distUnchanged).toBe(true); + }); +} + +test("a checkout path with spaces is built correctly", async () => { + const result = await check({ spaces: true }); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("element bundle is current"); + expect(result.scratch).toEqual([]); +}); + +test("a build error fails and cleans up its scratch output", async () => { + const result = await check({ buildError: true }); + expect(result.exitCode).not.toBe(0); + expect(result.stdout).not.toContain("element bundle is current"); + expect(result.scratch).toEqual([]); + expect(result.webUnchanged && result.distUnchanged).toBe(true); +}); diff --git a/specs/ThreeMD/ThreeMD.spec.md b/specs/ThreeMD/ThreeMD.spec.md index b8dc701f..3d78b800 100644 --- a/specs/ThreeMD/ThreeMD.spec.md +++ b/specs/ThreeMD/ThreeMD.spec.md @@ -1,6 +1,6 @@ --- module: ThreeMD -version: 10 +version: 11 status: active files: - Sources/ThreeMD/Axis.swift @@ -686,11 +686,16 @@ returns no document and no encoded bytes. `unicode-normalization = "=0.1.25"` as its only dependency and adds `#![forbid(unsafe_code)]`. The fast CRC is table-driven code in each port, with no hardware intrinsics required. +- Repository validation runs the element bundle checker and its subprocess + regressions through the existing `element-bundle` lane step. The checker + preserves bundle bytes, removes temporary output after success or failure, + and supports checkout paths containing spaces. This adds no library dependency. ## Change Log | Version | Date | Changes | |---------|------|---------| +| 11 | 2026-10-15 | Run element bundle-check failure cleanup and spaced-path regressions in the native verification lane. | | Editing preparation | 2026-10-05 | Additive stable identities, exact-revision typed patches and structured diagnostics; Swift-first release preparation with unchanged text compatibility and historical archives. | | 1 | 2026-06-23 | Initial text parser/serializer and HTML rendering spec. | | 1 | 2026-06-24 | Shared text contract across Swift, TypeScript and Rust. | diff --git a/specs/ThreeMDElement/ThreeMDElement.spec.md b/specs/ThreeMDElement/ThreeMDElement.spec.md index ee0966d7..19bf78cb 100644 --- a/specs/ThreeMDElement/ThreeMDElement.spec.md +++ b/specs/ThreeMDElement/ThreeMDElement.spec.md @@ -1,6 +1,6 @@ --- module: ThreeMDElement -version: 1 +version: 2 status: draft files: - element/src/three-md.ts @@ -106,6 +106,10 @@ through CSS custom properties (for example `--three-md-accent`, `--three-md-bg`, `js/src/number.ts`, so the parser has no import edge into `storage.ts`. The fresh build still equals `web/assets/three-md.js` and, when present, `element/dist/three-md.js`. +5. Bundle verification removes its temporary build directory after both success + and failure, preserves the committed bundles, and accepts checkout paths + containing spaces. A failed validation exits nonzero without printing the + success message. ## Behavioral Examples @@ -153,4 +157,5 @@ Then a planechange event is dispatched once with detail { index: 1, z: 1, label, | Version | Date | Changes | |---------|------|---------| +| 2 | 2026-10-15 | Verify scratch cleanup on every bundle-check outcome and support checkout paths containing spaces. | | 1 | 2026-06-23 | Initial spec for the `` element: attributes (`src`, `mode`), properties (`document`, `currentIndex`, `mode`), methods (`goTo`, `setSource`, `render`), the `planechange` event, the seven render modes, theming through CSS custom properties and `::part`, and the three rendering invariants. Renders format 1.0; package version 1.0.0. | diff --git a/specs/ThreeMDElement/context.md b/specs/ThreeMDElement/context.md index bba9b3a2..fa80445e 100644 --- a/specs/ThreeMDElement/context.md +++ b/specs/ThreeMDElement/context.md @@ -124,6 +124,12 @@ Source lives in `element/src/three-md.ts`, a single module: ## Rendering pipeline +The element bundle checker is exercised through seven disposable subprocess +fixtures before checking the real bundle. Validation failures set a nonzero +exit code without exiting before the cleanup `finally`; source URL paths are +decoded before passing them to the build command. These checks preserve the +published bundle and parser-only size/storage boundaries. + The element runs a small fixed flow: 1. **Build.** On first connect (or first `setSource`), attach the shadow root if diff --git a/specs/ThreeMDElement/tasks.md b/specs/ThreeMDElement/tasks.md index e62be79b..ff4a9bdb 100644 --- a/specs/ThreeMDElement/tasks.md +++ b/specs/ThreeMDElement/tasks.md @@ -53,9 +53,11 @@ by cross-browser tests. Near-term work that builds directly on the 1.0 surface. -- [ ] Extend `scripts/check-element-bundle.mjs` with the no-storage-code markers +- [x] Extend `scripts/check-element-bundle.mjs` with the no-storage-code markers and the 50,000-byte budget, and keep the bundle free of storage code while the library adds the ThreeMD 2.1 structured payload +- [x] Clean scratch builds on failed bundle validation and accept checkout paths + containing spaces, with subprocess regression controls. - [ ] Test coverage for the non-default render modes and the `mode` attribute override (including axis-name mapping) - [ ] Test coverage for the error paths (blank source, invalid 3md, failing diff --git a/specs/ThreeMDElement/testing.md b/specs/ThreeMDElement/testing.md index e3065fd6..fd967e16 100644 --- a/specs/ThreeMDElement/testing.md +++ b/specs/ThreeMDElement/testing.md @@ -94,6 +94,14 @@ Grouped by behavior, with the asserting check of each test. and `getBigUint64`, and its size is at most 50,000 bytes (2.0.0: 48,816 bytes). A storage module with a top-level side effect, or a parser import edge into `storage.ts`, fails the step. +- `scripts/check-element-bundle.test.ts` runs the actual checker in disposable + fixtures with matching bundles, stale web/package bundles, storage markers, + an oversized bundle, a checkout path containing spaces, and a compiler error. + It asserts exit status, diagnostic messages, unchanged bundle files, and no + remaining scratch directory. These seven subprocess regressions run before + the real bundle check in the native verification lane. Against the previous + checker, the four failed-validation cleanup cases and the spaced-path case + fail; the matching-bundle and compiler-error controls pass. ### Gaps and future test ideas