feat: carry unrun wikilink node and tests onto current refactor
This commit is contained in:
1 parent
bbec5e97f1
commit
8feda29eb4
9 files changed
+550
-2
No files matched your search
@@ -68,3 +68,56 @@ HTML/entity writer for dirty blocks; Q&A stays text and is a known limit.
|
||||
Measurement VM setup matches loadEditor. Browser TMPDIR must use the short
|
||||
non-/tmp scratch root. Sweep Node/Chromium after mutant/timeout and at clear.
|
||||
All remain authored and unrun while253 owns the slot.
|
||||
|
||||
Filbert follow-up at2026-10-22T16:45:34Z: the authored N3 browser fixture
|
||||
now explicitly asserts the separate reference-definition block ends in its
|
||||
original exact bytes after the same-block edit. This test change is unrun
|
||||
and uncommitted with the other product/test authoring, pending CLEAR.
|
||||
|
||||
## R2 restart on current refactor, sage-4164
|
||||
|
||||
New worktree: /home/jwoltje/.t3/worktrees/mosaic-stack/herb-row214-r2.
|
||||
Branch herb/row214-r2, base487500a16f4ab5902c318509fdec6acc56a543ca.
|
||||
Carried dee9f3d8d and e2710e92a as67ac3c25a andde42a59d4. Copied only
|
||||
Herb's local model, new editor test/helper, measure.mjs and pending BUILD
|
||||
note. The old worktree remains untouched. No stash or force push.
|
||||
|
||||
The current editor model/build/toolchain/BUILD.json/shipped bundle and
|
||||
loader are identical to the old base. Entry/view and package dependencies
|
||||
are unchanged. The current test fixture adds configDirectory:root; its
|
||||
landed change is retained. The C2-11a brief section is unchanged. The
|
||||
visual-mode condition now lives at public/bus.js:1054. No product behavior
|
||||
beyond the accepted inert inline node is added by the rebase.
|
||||
|
||||
Row266 overlap: none. Our paths do not touch chat*.mjs, chat.js or chat.css,
|
||||
and the intersection with origin/fethry/row266's actual diff is empty.
|
||||
HASH-PINS.json records that ref's SHA and merge base.
|
||||
|
||||
Hash audit: grepped packages/*/tests for each actual/planned changed file
|
||||
and filename, and scanned495 test/fixture files for literal prior SHA256
|
||||
or Git-blob pins. No such pins found. Existing editor-provenance checks
|
||||
read BUILD.json dynamically; regenerating it and the bundle is required.
|
||||
The loader and browser/route tests use editor.js but pin no source hash.
|
||||
No tests or pin asserts were weakened. The complete references and planned
|
||||
generated paths are in HASH-PINS.json.
|
||||
|
||||
Sage-4164 explicitly permits the unrun node/tests to be committed now. This
|
||||
supersedes the earlier packet's instruction to keep them uncommitted until
|
||||
green. Static authoring is not a tested candidate. SOURCE.json records its
|
||||
source hashes and the unbuilt generated-output status. The measurement now
|
||||
pins this current execution base, not the historical accepted-doc base.
|
||||
|
||||
No Node, npm, measurement, compiler, test, browser, Docker or mutant has
|
||||
run. Claim already succeeded at rev3234 on10-22; no new queue move is
|
||||
needed. Push authorization is only herb/row214-r2. Row257's landing gate
|
||||
and then266 own the slot; all execution remains held for Sage's CLEAR.
|
||||
|
||||
Planned focused grant: baseline measurement; the same-block structural
|
||||
red; pinned npm ci/build; whole editor-wikilink.test.mjs (10 no-edit tests
|
||||
with/without a final newline and3 browser tests), editor.test.mjs,
|
||||
editor-browser.test.mjs and editor-provenance.test.mjs singly; one
|
||||
serializer-removal mutant and restored focused green. Set
|
||||
MOSAIC_REQUIRE_EDITOR_BUILD=1 for provenance and the later whole webui
|
||||
gate. Capture all skip names, raw logs, short scratch TMPDIR and final
|
||||
Node/Chromium process sweep. Two failures stop a gate; no rerun after an
|
||||
out-of-row failure without reporting to Sage.
|
||||
@@ -126,3 +126,27 @@ N6: every granted browser/test process gets TMPDIR from
|
||||
mktemp -d /mnt/storage/scratch/tmp/gXXX. Never /tmp. Sweep owned Chromium
|
||||
and Node processes by cwd and the temp-root path after the mutant, after
|
||||
any timeout, and before clearing the slot. Retain/report uncertain owners.
|
||||
|
||||
## R2 current-base check, sage-4164 (2026-10-24)
|
||||
|
||||
The execution base is now487500a16, on herb/row214-r2, not the historical
|
||||
aca16404c above. The two accepted document commits were cherry-picked
|
||||
cleanly. The model, build script, toolchain, BUILD.json, editor.js, view,
|
||||
entry and editor test loader are byte-identical between those bases. The
|
||||
current fixture's new configDirectory isolation is retained; it is not
|
||||
copied from the old worktree. The visual-mode check is now bus.js:1054.
|
||||
The current C2-11a brief section is unchanged. Row266's chat modules,
|
||||
public/chat.js and public/chat.css are outside this row, with no overlap.
|
||||
|
||||
measure.mjs now pins487500a16's Git objects and BUILD.json. The baseline
|
||||
bundle still lacks the atom, and the first executed structural regression
|
||||
will use it before any rebuild. The required measurement, red/green, pinned
|
||||
rebuild, mutant kill and full gate remain unrun and required. Runtime
|
||||
validation is not inferred from the unchanged source or this static check.
|
||||
|
||||
Sage-4164 explicitly authorizes committing the preserved local node/tests
|
||||
on the new branch before execution. The authored source commit is static
|
||||
work, not a green candidate. No Node, npm, rebuild or browser starts until
|
||||
Sage sends CLEAR. HASH-PINS.json records the test-tree audit and row266
|
||||
intersection. The gate still requires MOSAIC_REQUIRE_EDITOR_BUILD=1, a
|
||||
named skip list, whole webui and every scripts/test-*.sh.
|
||||
@@ -0,0 +1,276 @@
|
||||
{
|
||||
"base": "487500a16f4ab5902c318509fdec6acc56a543ca",
|
||||
"testFilesScanned": 495,
|
||||
"plannedPaths": [
|
||||
"packages/webui/editor/model.mjs",
|
||||
"packages/webui/tests/editor-wikilink.test.mjs",
|
||||
"packages/webui/tests/helpers/wikilink-cases.mjs",
|
||||
"agents/herb/work/queue-214/measure.mjs",
|
||||
"agents/herb/work/queue-214/DESIGN.md",
|
||||
"agents/herb/work/queue-214/MANIFEST.md",
|
||||
"agents/herb/work/queue-214/BUILD.md",
|
||||
"packages/webui/src/public/editor.js",
|
||||
"packages/webui/editor/BUILD.json",
|
||||
"agents/herb/work/queue-214/HASH-PINS.json"
|
||||
],
|
||||
"literalPriorBlobPins": [],
|
||||
"pathReferences": [
|
||||
{
|
||||
"test": "packages/webui/tests/editor-wikilink.test.mjs",
|
||||
"line": 6,
|
||||
"text": "import { WIKILINK_CASES } from './helpers/wikilink-cases.mjs';"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/browser.test.mjs",
|
||||
"line": 21,
|
||||
"text": "// Rows 129, 154, 191, 194 and 294: chat.js, editor.js, processing.js, agents.js and federation.js are the scripts added, same origin; the list is the pin, so another script is a visible change."
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/browser.test.mjs",
|
||||
"line": 22,
|
||||
"text": "assert.deepEqual(await b.evaluate('[...document.querySelectorAll(\"script\")].map(s => s.getAttribute(\"src\"))'), ['/editor.js', '/processing.js', '/federation.js', '/brand.js', '/app.js', '/markdown-preview.js', '/chat.js', '/agents.js', '/bus.js']);"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/sso.test.mjs",
|
||||
"line": 1,
|
||||
"text": "// Q175 phase 1 (#1690, design agents/sage/work/q175-sso/DESIGN.md section 9): the Console as an"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/user-option-swap.test.mjs",
|
||||
"line": 133,
|
||||
"text": "['GET', '/app.js'], ['GET', '/bus.js'], ['GET', '/agents.js'], ['GET', '/editor.js'], ['GET', '/shared/app.css'], ['GET', '/index.html'],"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/serve.test.mjs",
|
||||
"line": 38,
|
||||
"text": "for (const path of ['/', '/app.js', '/brand.js', '/console.css', '/live.css', '/editor.js', '/editor.css', '/shared/app.css', '/assets/fonts/manrope-400.woff2']) {"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-provenance.test.mjs",
|
||||
"line": 18,
|
||||
"text": "const build = JSON.parse(readFileSync(join(webui, 'editor/BUILD.json'), 'utf8'));"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-provenance.test.mjs",
|
||||
"line": 92,
|
||||
"text": "const bundle = readFileSync(join(webui, 'src/public/editor.js'), 'utf8');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-provenance.test.mjs",
|
||||
"line": 167,
|
||||
"text": "const info = await run(out, join(out, 'BUILD.json'));"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-provenance.test.mjs",
|
||||
"line": 171,
|
||||
"text": "assert.equal(readFileSync(join(out, 'BUILD.json'), 'utf8'), readFileSync(join(webui, 'editor/BUILD.json'), 'utf8'));"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-browser.test.mjs",
|
||||
"line": 103,
|
||||
"text": "assert.equal(await h.q('typeof window.MosaicEditor.mount'), 'function', 'editor.js ran under script-src self');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor-browser.test.mjs",
|
||||
"line": 423,
|
||||
"text": "// (The editor's own reading of a file can give a single line break in a title or alt, never a blank line; see BUILD.md.)"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/granted-user.test.mjs",
|
||||
"line": 231,
|
||||
"text": "['GET', '/app.js'], ['GET', '/bus.js'], ['GET', '/agents.js'], ['GET', '/editor.js'], ['GET', '/shared/app.css'], ['GET', '/index.html'],"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 20,
|
||||
"text": "for (const [path, type] of [['/editor.js', 'text/javascript'], ['/editor.css', 'text/css']]) {"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 29,
|
||||
"text": "assert.ok(index.indexOf('src=\"/editor.js\"') < index.indexOf('src=\"/bus.js\"'), 'the editor loads before the page script, so bus.js can mount it');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 60,
|
||||
"text": "for (const file of ['entry.mjs', 'model.mjs', 'view.mjs']) {"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 64,
|
||||
"text": "const bundle = readFileSync(join(webui, 'src/public/editor.js'), 'utf8');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 65,
|
||||
"text": "assert.doesNotMatch(bundle, /\\beval\\s*\\(|\\bnew\\s+Function\\b/, 'src/public/editor.js');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/editor.test.mjs",
|
||||
"line": 69,
|
||||
"text": "const model = readFileSync(join(webui, 'editor/model.mjs'), 'utf8');"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/helpers/editor-bundle.mjs",
|
||||
"line": 1,
|
||||
"text": "// Loads src/public/editor.js (and markdown-preview.js, the link gate it needs) into a vm context with a"
|
||||
},
|
||||
{
|
||||
"test": "packages/webui/tests/helpers/editor-bundle.mjs",
|
||||
"line": 15,
|
||||
"text": "for (const f of ['markdown-preview.js', 'editor.js']) vm.runInContext(readFileSync(join(pub, f), 'utf8'), sandbox, { filename: f });"
|
||||
},
|
||||
{
|
||||
"test": "packages/plugins/tests/harness.test.mjs",
|
||||
"line": 44,
|
||||
"text": "// pi 0.85.1 --thinking and claude 2.1.302 --effort, as probed (BUILD.md); claude has no off or minimal."
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/revoke-window.test.mjs",
|
||||
"line": 4,
|
||||
"text": "// a limit stub and a usage stub for the limit stop. BUILD.md (agents/morgana/work/queue-306) names"
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/access-revoke.test.mjs",
|
||||
"line": 5,
|
||||
"text": "// lstat key; `edit` changes the document and the stamp together, as a hand edit does. BUILD.md"
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/land.test.mjs",
|
||||
"line": 410,
|
||||
"text": "// landHeld still counts it, so it stays listed (BUILD.md follow-up 5)."
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/session-clear.test.mjs",
|
||||
"line": 13,
|
||||
"text": "// docs/plans/2026-10-21_t1-sessions.md, the contract in agents/gizmoduck/work/queue-271/BUILD.md."
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/grant-executor.test.mjs",
|
||||
"line": 1027,
|
||||
"text": "// credential-unavailable; BUILD.md records the difference)."
|
||||
},
|
||||
{
|
||||
"test": "packages/bus/tests/host-run.test.mjs",
|
||||
"line": 343,
|
||||
"text": "// the options are checked here and the call runs with an alias no user file holds (BUILD.md)."
|
||||
},
|
||||
{
|
||||
"test": "packages/business/tests/workflow-roles.test.mjs",
|
||||
"line": 123,
|
||||
"text": "// of staff launches it. The broker and launcher states are row 137's tests (BUILD.md, round 3)."
|
||||
},
|
||||
{
|
||||
"test": "packages/business/tests/access.test.mjs",
|
||||
"line": 1,
|
||||
"text": "// FED R1 (#414, row 201): the access file loader and validator, DESIGN.md"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/revoke-window.test.mjs",
|
||||
"line": 9,
|
||||
"text": "// poll, with threadsState replaced by the caller. BUILD.md (agents/morgana/work/queue-306) names the"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-instances.test.mjs",
|
||||
"line": 3,
|
||||
"text": "// T table: agents/darkwing/work/queue-190/DESIGN.md D7. The unit, port and release cases are T56 in"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/session-launch.test.mjs",
|
||||
"line": 297,
|
||||
"text": "// Q2 (BUILD.md:177-179, t1-sessions.md:109): an earlier host's sessionless run of the instance is"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"line": 2,
|
||||
"text": "// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release.test.mjs",
|
||||
"line": 4,
|
||||
"text": "// agents/darkwing/work/queue-162/BUILD.md. Every run uses a temp data root, temp units and stand-in"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release.test.mjs",
|
||||
"line": 915,
|
||||
"text": "// COUNTED or dropped from it shows here. Row 181's BUILD.md names each writer."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release.test.mjs",
|
||||
"line": 928,
|
||||
"text": "task_comment_cursors:'no time column; a restore drops its advances uncounted (BUILD.md)',"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release.test.mjs",
|
||||
"line": 1668,
|
||||
"text": "// refusal is the guard for a file that changes after that (BUILD.md, round 2)."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release.test.mjs",
|
||||
"line": 2331,
|
||||
"text": "// agents/darkwing/work/queue-190/DESIGN.md. T-I1, T-I2, T-I5, T-I10 and the loader half of T-I7 are in"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/publish-socket.test.mjs",
|
||||
"line": 236,
|
||||
"text": "// sage-1440 on BUILD.md 14: a push between checkPublish and the git push refuses, with one code"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-upgrade.test.mjs",
|
||||
"line": 3,
|
||||
"text": "// agents/darkwing/work/queue-195/DESIGN.md, as the I3 brief changes them; deviations:"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-upgrade.test.mjs",
|
||||
"line": 4,
|
||||
"text": "// agents/darkwing/work/queue-233/BUILD.md. The stand-ins and the module fixture are"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/stack-release-external.test.mjs",
|
||||
"line": 2,
|
||||
"text": "// test list, agents/darkwing/work/queue-195/DESIGN.md; deviations: agents/darkwing/work/queue-232/BUILD.md."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/case1-hostcheck.test.mjs",
|
||||
"line": 3,
|
||||
"text": "// (agents/darkwing/work/queue-195/DESIGN.md at b7239bb66). Every case copies the script into a scratch"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/thread-revoke.test.mjs",
|
||||
"line": 5,
|
||||
"text": "// The bus half is packages/bus/tests/access-revoke.test.mjs; BUILD.md"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/access.test.mjs",
|
||||
"line": 308,
|
||||
"text": "// outside R5 (BUILD.md). The test pins it, and nothing is written."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/access.test.mjs",
|
||||
"line": 316,
|
||||
"text": "// Row 193c (#1705): `user add` and `user remove`. BUILD.md (agents/megavolt/work/queue-207) names"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/access.test.mjs",
|
||||
"line": 455,
|
||||
"text": "// refuses access.reload alone, as a bus would refuse a call it can't take. BUILD.md"
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/launcher.test.mjs",
|
||||
"line": 799,
|
||||
"text": "// The run directory exists by then (BUILD.md residual); the persona is read before any bundle file."
|
||||
},
|
||||
{
|
||||
"test": "packages/cli/tests/lifecycle.test.mjs",
|
||||
"line": 778,
|
||||
"text": "// N6 (sage-2246, DESIGN.md:118-120): an end signals the runs the verb named and the instance's runs"
|
||||
}
|
||||
],
|
||||
"row266": {
|
||||
"ref": "origin/fethry/row266",
|
||||
"tip": "6418ec063389728e0d8883c94812b12d2074eb43",
|
||||
"diffBase": "487500a16f4ab5902c318509fdec6acc56a543ca",
|
||||
"overlap": []
|
||||
},
|
||||
"interpretation": "No literal SHA256 or Git-blob hash pins found for planned changed files. Editor provenance compares generated output to BUILD.json and requires regeneration/rebuild, not an external fixed hash."
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
# Row214 implementation manifest, draft before measurement
|
||||
|
||||
Pinned base: aca16404c. Brief: docs/plans/2026-10-20_case2-rows.md,
|
||||
Execution base: 487500a16f4ab5902c318509fdec6acc56a543ca on herb/row214-r2.
|
||||
Historical accepted design base: aca16404c. Brief: docs/plans/2026-10-20_case2-rows.md,
|
||||
C2-11a. Also read at gosalyn/case2-06-idea-caps51cc0445c; that section is
|
||||
unchanged. Q222's reply-limit settings belong to C2-06, not this row.
|
||||
|
||||
@@ -15,9 +16,17 @@ unchanged. Q222's reply-limit settings belong to C2-06, not this row.
|
||||
| agents/herb/work/queue-214/DESIGN.md | both measurement outcomes, implementation and acceptance |
|
||||
| agents/herb/work/queue-214/BUILD.md | rulings and receipts, pending runs |
|
||||
| agents/herb/work/queue-214/MANIFEST.md | this reviewed path list |
|
||||
| agents/herb/work/queue-214/HASH-PINS.json | static test hash-pin and row266 overlap audit |
|
||||
| agents/herb/work/queue-214/SOURCE.json | authored source hashes and pending generated-output status |
|
||||
|
||||
editor.css is an unchanged build output; no source CSS is touched. No
|
||||
entry/view, library, route, save, click, documentPath or dependency changes.
|
||||
Generated changes do not exist yet. This is a design/path manifest, not a
|
||||
frozen tested candidate. Candidate hashes and measurement results follow
|
||||
only after the granted runs. Filbert reviews the plan before the build.
|
||||
|
||||
R2 under sage-4164: the node, measurement and tests are committed as static,
|
||||
unrun authoring on the current base. Filbert accepted the design/path scope
|
||||
on2026-10-22; runtime evidence and regenerated files still require CLEAR.
|
||||
No chat*.mjs or public/chat.{js,css} path changes. The baseline measurement
|
||||
now reads487500a16 Git blobs, still verified against its BUILD.json.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"base": "487500a16f4ab5902c318509fdec6acc56a543ca",
|
||||
"status": "authored; no execution; not a gate-ready frozen candidate",
|
||||
"sourcePaths": [
|
||||
{
|
||||
"path": "packages/webui/editor/model.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "d56b75264c2c099fa0fc8c9f7a48d8ee701d8657ca64b9bd9492ec27c9446901"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/editor-wikilink.test.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "73360cfc64b2f4d2f7888b576743baea59ea9a53557934507744775fafdc8ab5"
|
||||
},
|
||||
{
|
||||
"path": "packages/webui/tests/helpers/wikilink-cases.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "afa4dc7188548457b2d1966b9437561ba411d7bc9aaaa38bff626c235a64e19c"
|
||||
},
|
||||
{
|
||||
"path": "agents/herb/work/queue-214/measure.mjs",
|
||||
"mode": "100644",
|
||||
"sha256": "f81987cdae5c4f7e5ce353162674b3c3510e44f22d6d3beb262a5d198db4e88d"
|
||||
}
|
||||
],
|
||||
"pendingGeneratedPaths": [
|
||||
"packages/webui/src/public/editor.js",
|
||||
"packages/webui/editor/BUILD.json"
|
||||
],
|
||||
"unchangedGeneratedCss": "packages/webui/src/public/editor.css",
|
||||
"runtimeEvidence": null
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Run only under Sage's explicit CLEAR. Reads the pinned baseline Git objects,
|
||||
// not the edited model or rebuilt candidate bundle. Never writes product files.
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { WIKILINK_CASES } from '../../../../packages/webui/tests/helpers/wikilink-cases.mjs';
|
||||
|
||||
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '../../../..');
|
||||
const base = '487500a16f4ab5902c318509fdec6acc56a543ca';
|
||||
const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
||||
const read = path => git('show', `${base}:${path}`);
|
||||
const source = read('packages/webui/src/public/editor.js');
|
||||
const build = JSON.parse(read('packages/webui/editor/BUILD.json'));
|
||||
const sha256 = createHash('sha256').update(source).digest('hex');
|
||||
if (sha256 !== build.outputs['src/public/editor.js'].sha256) throw Error('baseline bundle differs from its recorded build');
|
||||
const context = { console }; context.window = context; context.globalThis = context;
|
||||
vm.createContext(context);
|
||||
for (const [name, text] of [['markdown-preview.js', read('packages/webui/src/public/markdown-preview.js')], ['editor.js', source]])
|
||||
vm.runInContext(text, context, { filename: name });
|
||||
const results = WIKILINK_CASES.map(({ name, text }) => {
|
||||
const output = context.MosaicEditor.roundTrip(text);
|
||||
return { name, input: text, output, equal: output === text };
|
||||
});
|
||||
console.log(JSON.stringify({ base: git('rev-parse', base).trim(), bundleSha256: sha256,
|
||||
measuredAt: new Date().toISOString(), allEqual: results.every(r => r.equal), results }, null, 2));
|
||||
@@ -23,6 +23,13 @@ export const linkTarget = raw => {
|
||||
return typeof gate === 'function' ? gate(raw) : null;
|
||||
};
|
||||
|
||||
// C2-11a: an inert single-line atom keeps its exact source, including labels
|
||||
// and heading fragments. HTML/entity-like targets stay ordinary text so the
|
||||
// existing escaped-text serialiser still protects rewritten blocks.
|
||||
const wikilinkSource = /^!?\[\[[^\[\]\r\n<>&]+\]\]$/;
|
||||
const wikilinkAt = /!?\[\[[^\[\]\r\n<>&]+\]\]/y;
|
||||
const isWikilink = value => typeof value === 'string' && wikilinkSource.test(value);
|
||||
|
||||
const kept = { raw: { default: null }, sig: { default: null }, lead: { default: '' } };
|
||||
const block = attrs => ({ ...kept, ...attrs });
|
||||
|
||||
@@ -34,7 +41,7 @@ export const schema = new Schema({
|
||||
horizontal_rule: { group: 'block', attrs: block({}), parseDOM: [{ tag: 'hr' }], toDOM() { return ['div', ['hr']]; } },
|
||||
heading: {
|
||||
attrs: block({ level: { default: 1 } }),
|
||||
content: '(text | image)*', group: 'block', defining: true,
|
||||
content: '(text | image | wikilink)*', group: 'block', defining: true,
|
||||
parseDOM: [1, 2, 3, 4, 5, 6].map(level => ({ tag: 'h' + level, attrs: { level } })),
|
||||
toDOM(node) { return ['h' + +node.attrs.level, 0]; },
|
||||
},
|
||||
@@ -79,6 +86,16 @@ export const schema = new Schema({
|
||||
return ['span', { class: 'md-image', 'data-src': a.src, 'data-alt': a.alt, 'data-title': a.title }, shown];
|
||||
},
|
||||
},
|
||||
// No href, image, target resolution or click handler. C2-11b owns opening.
|
||||
wikilink: {
|
||||
inline: true, group: 'inline', atom: true,
|
||||
attrs: { source: {} },
|
||||
parseDOM: [{ tag: 'span.md-wikilink[data-source]', getAttrs: dom => {
|
||||
const source = dom.getAttribute('data-source');
|
||||
return isWikilink(source) ? { source } : false;
|
||||
} }],
|
||||
toDOM(node) { return ['span', { class: 'md-wikilink', 'data-source': node.attrs.source }, node.attrs.source]; },
|
||||
},
|
||||
hard_break: { inline: true, group: 'inline', selectable: false, parseDOM: [{ tag: 'br' }], toDOM() { return ['br']; } },
|
||||
},
|
||||
marks: {
|
||||
@@ -118,6 +135,16 @@ export const schema = new Schema({
|
||||
// ------------------------------------------------------------------ Markdown to nodes
|
||||
const splitter = MarkdownIt('commonmark', { html: true }).enable('table').enable('strikethrough');
|
||||
const tokenizer = MarkdownIt('commonmark', { html: false }).enable('table').enable('strikethrough');
|
||||
// The escape and backtick rules run first. A sticky match avoids copying the
|
||||
// paragraph suffix at every bracket, and never crosses a link-label boundary.
|
||||
tokenizer.inline.ruler.before('link', 'wikilink', (state, silent) => {
|
||||
wikilinkAt.lastIndex = state.pos;
|
||||
const found = wikilinkAt.exec(state.src);
|
||||
if (!found || state.pos + found[0].length > state.posMax) return false;
|
||||
if (!silent) { const token = state.push('wikilink', '', 0); token.content = found[0]; }
|
||||
state.pos += found[0].length;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Soft breaks stay line breaks in the text, so a wrapped paragraph keeps its wrapping. A link whose
|
||||
// target the gate refuses makes the whole block source text (`refused`), so nothing rewrites it.
|
||||
@@ -151,6 +178,7 @@ const parser = new MarkdownParser(schema, prepared, {
|
||||
hr: { node: 'horizontal_rule' },
|
||||
image: { node: 'image', getAttrs: tok => ({ src: tok.attrGet('src'), title: tok.attrGet('title') || null, alt: tok.content || null }) },
|
||||
hardbreak: { node: 'hard_break' },
|
||||
wikilink: { node: 'wikilink', getAttrs: tok => ({ source: tok.content }) },
|
||||
em: { mark: 'em' },
|
||||
strong: { mark: 'strong' },
|
||||
link: { mark: 'link', getAttrs: tok => ({ href: tok.attrGet('href'), title: tok.attrGet('title') || null }) },
|
||||
@@ -192,6 +220,12 @@ const serializer = new MarkdownSerializer({
|
||||
const title = oneLine(a.title);
|
||||
state.write(']/g, '\\$&') + (title ? ' "' + title.replace(/[\\"]/g, '\\$&') + '"' : '') + ')');
|
||||
},
|
||||
wikilink(state, node) {
|
||||
const source = node.attrs.source, valid = isWikilink(source);
|
||||
// A programmatic invalid atom must not bypass the same text escaping that
|
||||
// protects pasted HTML and one-line attributes elsewhere in this model.
|
||||
state.text(valid ? source : oneLine(String(source ?? '')), !valid);
|
||||
},
|
||||
raw_block(state, node) { state.text(node.textContent, false); state.closeBlock(node); },
|
||||
}, {
|
||||
...defaultMarkdownSerializer.marks,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
// C2-11a. Unchanged files alone cannot detect escaped wikilinks: the editor
|
||||
// keeps raw source. The browser checks the structured node and a dirty block.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { loadEditor } from './helpers/editor-bundle.mjs';
|
||||
import { WIKILINK_CASES } from './helpers/wikilink-cases.mjs';
|
||||
import { fixture } from './fixture.mjs';
|
||||
import { browser } from './browser.mjs';
|
||||
|
||||
const editor = loadEditor();
|
||||
for (const { name, text } of WIKILINK_CASES) test(`wikilink round-trip: ${name}`, () => {
|
||||
assert.equal(editor.roundTrip(text), text);
|
||||
assert.equal(editor.roundTrip(text.trimEnd()), text.trimEnd(), 'no trailing newline');
|
||||
});
|
||||
|
||||
const page = async t => {
|
||||
const f = await fixture(); t.after(() => f.close());
|
||||
const b = await browser(); t.after(() => b.close());
|
||||
await b.navigate(f.base);
|
||||
return b;
|
||||
};
|
||||
const mount = text => `(() => {
|
||||
window.__wikiEditor?.destroy();
|
||||
const host = document.createElement('div'); document.body.append(host);
|
||||
window.__wikiEditor = window.MosaicEditor.mount(host, {text: ${JSON.stringify(text)}});
|
||||
return true;
|
||||
})()`;
|
||||
const atoms = `(() => {
|
||||
const result = [];
|
||||
window.__wikiEditor.view.state.doc.descendants(node => {
|
||||
if (node.type.name === 'wikilink') result.push({source: node.attrs.source, atom: node.isAtom, inline: node.isInline});
|
||||
}); return result;
|
||||
})()`;
|
||||
|
||||
test('wikilinks are inline atoms and survive a neighbouring edit in every required context', { timeout: 120000 }, async t => {
|
||||
const b = await page(t);
|
||||
for (const { name, text, sources } of WIKILINK_CASES) {
|
||||
await b.evaluate(mount(text));
|
||||
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), name);
|
||||
assert.equal(await b.evaluate('document.querySelectorAll(".md-wikilink a, .md-wikilink img, .md-wikilink iframe").length'), 0, 'no navigation or resource elements');
|
||||
const changed = await b.evaluate(`(() => {
|
||||
const e = window.__wikiEditor; let end;
|
||||
e.view.state.doc.descendants((node, pos) => { if (node.isTextblock && node.type.name !== 'raw_block') end = pos + node.nodeSize - 1; });
|
||||
e.view.dispatch(e.view.state.tr.insertText(' changed', end));
|
||||
const output = e.text(); e.setText(output); return output;
|
||||
})()`);
|
||||
assert.ok(changed.includes(' changed'), name);
|
||||
for (const source of sources) assert.ok(changed.includes(source), `${name}: spelling survives dirty-block serialisation`);
|
||||
assert.deepEqual(await b.evaluate(atoms), sources.map(source => ({ source, atom: true, inline: true })), `${name}: survives reparse`);
|
||||
if (text.startsWith('---')) assert.ok(changed.startsWith(text.slice(0, text.indexOf('\n\n') + 2)), 'front matter is untouched');
|
||||
if (name === 'reference-definition collision')
|
||||
assert.ok(changed.endsWith('\n\n[a]: u\n'), 'the separate reference-definition block keeps its exact bytes');
|
||||
}
|
||||
});
|
||||
|
||||
test('code, escaped openers, malformed syntax and HTML-like targets stay outside wikilink atoms', { timeout: 120000 }, async t => {
|
||||
const b = await page(t);
|
||||
const text = '\\[[escaped]] `[[inline-code]]`\n\n```\n[[fenced-code]]\n```\n\n[[open\n\n[[]]\n\n[[Q&A]]\n\n[[<img src=x onerror=alert(1)>]]\n';
|
||||
assert.equal(editor.roundTrip(text), text);
|
||||
await b.evaluate(mount(text));
|
||||
assert.deepEqual(await b.evaluate(atoms), []);
|
||||
assert.equal(await b.evaluate('document.querySelector(".ProseMirror").querySelectorAll("img, iframe, script").length'), 0);
|
||||
});
|
||||
|
||||
test('wikilink DOM paste accepts only bounded single-line inert syntax; malformed attrs serialise as escaped text', { timeout: 120000 }, async t => {
|
||||
const b = await page(t); await b.evaluate(mount('[[a]]\n'));
|
||||
const result = await b.evaluate(`(() => {
|
||||
const e = window.__wikiEditor, spec = e.view.state.schema.nodes.wikilink.spec;
|
||||
const attrs = source => spec.parseDOM[0].getAttrs({getAttribute: () => source});
|
||||
const invalid = ['not a link', '[[a]]\\n\\n<script>x</script>', '[[<img src=x>]]', '[[a©]]', null].map(source => attrs(source));
|
||||
const bad = e.view.state.schema.nodes.wikilink.create({source:'[[a]]\\n\\n<img src=x onerror=alert(1)>'});
|
||||
e.view.dispatch(e.view.state.tr.replaceWith(1, e.view.state.doc.content.size - 1, bad));
|
||||
return {valid:attrs('[[a#h|label]]'), invalid, output:e.text()};
|
||||
})()`);
|
||||
assert.deepEqual(result.valid, { source: '[[a#h|label]]' });
|
||||
assert.deepEqual(result.invalid, [false, false, false, false, false]);
|
||||
assert.ok(!result.output.includes('\n\n<img'));
|
||||
assert.doesNotMatch(result.output, /(?<!\\)<img/);
|
||||
assert.match(result.output, /\\<img/);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
// C2-11a byte fixtures shared by the baseline measurement and regression tests.
|
||||
export const WIKILINK_CASES = [
|
||||
{ name: 'plain', text: '[[a]]\n', sources: ['[[a]]'] },
|
||||
{ name: 'labelled', text: '[[a|b]]\n', sources: ['[[a|b]]'] },
|
||||
{ name: 'heading target', text: '[[a#h]]\n', sources: ['[[a#h]]'] },
|
||||
{ name: 'list item', text: '- [[a]]\n', sources: ['[[a]]'] },
|
||||
{ name: 'next to front matter', text: '---\nrelated: "[[metadata]]"\n---\n\n[[a]]\n', sources: ['[[a]]'] },
|
||||
{ name: 'heading block and combined label', text: '# See [[a#h|Title]]\n', sources: ['[[a#h|Title]]'] },
|
||||
{ name: 'spaces, Unicode and adjacent links', text: 'See [[ Héllo there # A heading | a label ]][[next]]\n', sources: ['[[ Héllo there # A heading | a label ]]', '[[next]]'] },
|
||||
{ name: 'reference-definition collision', text: '[[a]]\n\n[a]: u\n', sources: ['[[a]]'] },
|
||||
{ name: 'inside an ordinary link', text: '[x [[a]]](u)\n', sources: ['[[a]]'] },
|
||||
{ name: 'inert embed', text: '![[cover.png]]\n', sources: ['![[cover.png]]'] },
|
||||
];
|
||||
Reference in new issue
Block a user