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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"approvals": [],
"reopenings": []
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion fledge.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
9 changes: 5 additions & 4 deletions scripts/check-element-bundle.mjs
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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 });
}
100 changes: 100 additions & 0 deletions scripts/check-element-bundle.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
7 changes: 6 additions & 1 deletion specs/ThreeMD/ThreeMD.spec.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
module: ThreeMD
version: 10
version: 11
status: active
files:
- Sources/ThreeMD/Axis.swift
Expand Down Expand Up @@ -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. |
Expand Down
7 changes: 6 additions & 1 deletion specs/ThreeMDElement/ThreeMDElement.spec.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
module: ThreeMDElement
version: 1
version: 2
status: draft
files:
- element/src/three-md.ts
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 `<three-md>` 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. |
6 changes: 6 additions & 0 deletions specs/ThreeMDElement/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion specs/ThreeMDElement/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading