Skip to content

Make Desktop own durable project changes - #4

Merged
Error-Zhang merged 28 commits into
mainfrom
codex/editor-owner-gaps
Aug 16, 2026
Merged

Error-Zhang merged 28 commits into
mainfrom
codex/editor-owner-gaps

Conversation

@Error-Zhang

Copy link
Copy Markdown
Contributor

Summary

  • keep the VelarScript Desktop as one VelarScript project instead of splitting renderer and main source surfaces
  • add explicit host/environment contracts while keeping capability ownership in velar/desktop
  • own finite project changes, review, apply, rollback, recovery, and task execution at the Desktop boundary
  • consume the published @velaros-ai/project 2.0.1 transaction primitive through a checked host seam
  • add the worker build, CLI and Desktop tour coverage, docs, and acceptance proof

Dependency boundary

The borrowed Platform package owns framework-neutral transaction mechanics. VelarScript still owns language semantics, permissions, Desktop host contracts, and the user-visible capability surface. The package is installed from GitHub Packages through an environment-token npm configuration; no credential is committed.

Validation

  • npm run check
  • npm test: 1466 passed, 0 failed
  • all 4 discovered example projects passed the unit gate
  • registry installation verified without a local workspace link
  • @velaros-ai/project@2.0.1 resolves agent 0.5.1, core 0.4.0, and kernel 0.1.1

Error-Zhang and others added 28 commits August 16, 2026 19:50
…ed in

The changelog stopped at wave C2 and never covered the 56 commits after it,
which is most of what the language now is. This writes them up under 0.10.0
rather than under a new version, because 0.10.0 was finalized eleven minutes
after the last of those commits landed: they are in the tree it releases, not
in a follow-up to it. Nothing was ever published, so there is no released
version to break from, and the two changes that read as breaking — computed
becoming a declaration, props becoming mutable — are simply what the first
release contains.

The reactive-grid entry is written as one story because it is one defect. The
owner reported it twice from two directions without the two looking related: a
const that froze a reactive read, and a watch body that could never run. Both
came from state being a declaration while computed was a function, which
forced every derived value into a const — and const is the thing that freezes.
The entry says that, because a changelog that lists the two diagnostics
separately would hide the reason either exists.

Two numbers I would have written from memory turned out wrong, so they are
written from the record instead. The derived package set did not catch a list
that had drifted — both copies in the repo were correct, and the failure was
outside it, in a brief I truncated to six of eight names. And the tour did not
surface five formatter defects; it surfaced ten findings, of which the two
that matter are a formatter that broke compiling source and an emitter that
passed check and failed build.

Verified with check:docs, which exits 0 and reports 79 of 110 fragments not
checked in full — the changelog itself adds no fences, which is the D70/D71
mistake not repeated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The headline claimed a division of labour — AI writes, humans read and own —
and the owner's verdict was that it did not express what the language is. He
is right, and the reason is that a division of labour is downstream: being
extensible with the framework unified into the language is what the thing is,
one language covering markup, styling, state, tests, and the server is what
that buys, and AI writing the code is what makes the resulting strictness and
the refusal of compatibility affordable. The headline had the last link where
the first belongs.

Two corrections of mine are recorded in D78 rather than quietly fixed. My
first attempt promoted the owner's founding motivation — that nobody using Vel
should have to design its replacement — into the headline, and he answered
that this is his motivation, not the language's positioning; the two are
different questions and the document now gives each its own section. My second
attempt wrote "language framework", which reads as a layer sitting on top of a
language, when the fact is the opposite: component, state and look are
keywords, not imports.

The positioning was never new. Core knowing nothing about the DOM, stylesheets,
the filesystem or windows is the thing every boundary ruling this session was
defending — unsafe css belonging to the Web extension rather than Core, Core's
own rosters, the per-extension surface audit. Unification shrinks what has to
be verified to one language; extensibility is what keeps that from turning Core
into the pile it was built to escape. Neither half stands alone, so D78 states
them as one claim.

why-velarscript.md is rebuilt on the eight questions the owner asked in order,
including the last one, which is the one that matters: what it does not
promise. Never backward compatibility, no stable channel yet, ecosystem breadth
costs you a declared boundary, no new runtime, and migration is not free. A
positioning document that lists only promises is an advertisement.

check:docs passes: 211 examples, unchanged fence counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ates

Section 1 still said the language exists so AI can write code a human owner can
read — the framing D78 replaced. What makes the substitution easy is that rule
5 has always been the other half of the new positioning: keep target
capabilities in explicit extensions rather than hiding Web, Node, or Desktop
behavior in Core. The charter was enforcing extensibility as a rule while the
prose above it advertised a division of labour.

So the opening now says both halves and says why they are one: unification puts
markup, styling, state, tests, and the server in a single checked language, and
that is only affordable because capability lives in extensions instead of
accumulating in Core. Rule 5 is labelled as positioning rather than hygiene so
a future editor does not treat it as an implementation detail and relax it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tour coverage gate had eighteen categories and every one of them was a
name — keywords, module exports, Look properties, namespace members. A
syntactic form built out of already-covered keywords was therefore invisible to
it, which is how D53's inline foreign-source blocks shipped with sixteen unit
tests and not one use in any .vel file in the repository. `extern`, `js` and
`unsafe` were all observed by `extern module` and `import js unsafe` in tour
chapter 13, so the gate stayed green over a construct nobody had written.

The new category derives from CoreStatement, the union parseStatement returns,
through a mapped type placed beside it. That turns "a new declaration form must
join this roster" into "it cannot compile otherwise" — verified by adding a
member and watching tsc reject the roster rather than by trusting the shape.
The parser dispatch was rejected as the authority because it is an if-chain
rather than a table, and the emitter switch because it is not enumerable at run
time. Extension statements never join that union, so they reach the roster
through a new syntax slot in the extension protocol, and an extension that
registers a parser while publishing no constructs is itself a gate failure —
syntax cannot arrive invisibly.

Three red-proofs, each deleting something different from a copy of the tour:
the inline JavaScript blocks, the inline CSS block with its `import css unsafe`
siblings left in place, and the Web extension's syntax slot. All three name what
is missing. The second is the interesting one: `unsafe css` shares a node kind
with the external import and is still separately required, because its source is
a tagged union.

That is also the limit, and the gate prints it rather than hiding it. `extern
js` and `unsafe js` are one node told apart by a boolean, as are const/let,
def/async def, class/abstract class, for/async for. The category can only
require the kind, so deleting one of a pair would not go red. Closing it means
giving those nodes a discriminated form, which is an AST change and needs a
ruling, not a gate edit.

The tour now writes all three blocks for real: a captured `extern js` reaching
`crypto.getRandomValues` — a global has no package, so before D53 there was no
spelling for it at all — an `unsafe js` reading process internals, and an
`unsafe css` for paged media, which Look excludes by design so nothing is being
worked around.

The wave also found that the blocks were documented nowhere: zero hits across
the charter, escape hatches, the JavaScript bridge and the skill brief, with the
charter's own module-boundary list omitting them. A construct with no prose is
effectively unshipped when the KPI is blind writability, so the charter gains a
specification and the escape-hatch playbook gains the case these blocks exist
for — JavaScript with no package to import from — including the honest limit
that a captured block cannot use top-level await.

One repair of mine: the positioning commit pushed the skill brief eight lines
over its 750-line budget and turned two tests red. The wave proved the
provenance with a pristine HEAD snapshot rather than assuming it. Fixed by
cutting the opening back to its original size, not by raising the ceiling,
which is what the assertion asks for.

All four gates: check=0, test=0 (1438/1438), test:packages=0, test:browser=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…than hidden

Neither blocks the release, and that is the point: the gate prints what it
cannot reach instead of routing it through the exemption table, so there is no
green that claims more than it checked.

Rule 198 is granularity. Five node kinds carry two spellings each — extern js
against unsafe js, const against let, def against async def, class against
abstract class, for against async for — and no compiler-owned table enumerates
those splits, so the category can only require the kind. The Web extension
already shows the fix working: unsafe css shares a node with the external
import and is still separately required, because its source is a tagged union.
I recommend doing it, and I split the recommendation, because the honest cost
is not one union: four of the five are core nodes whose discriminants would
touch parsing, types, emission and the formatter. The embedded-JavaScript node
is the cheap one and the only one with demonstrated need, so it stands alone as
a first step and the other four are a separate argument.

Rule 199 is a defect that happens to block the expression-side version of this
work. Core's own parser produces ExtensionExpression:core:duration as a bare
`as Expression` cast with no interface and no union membership, so a roster
mapped over CoreExpression would silently miss it — the expression category
would have been false green, which is why the wave scoped itself to statements
and said so. Worth fixing whether or not expressions ever get covered: an
undeclared node produced by the compiler's own parser is the hand-maintained
vocabulary family showing up in the AST.

The negative result is recorded too, because it is the useful half: every other
expression, pattern, type-syntax and class-body kind is already exercised by
the tour, and the one exception is a parse-recovery node the AST itself
documents as never reaching code generation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wave W1 hit both while writing the website against the real compiler, and both
had the charter contradicting itself within twenty lines while the compiler
agreed with the later half.

The reserved globals were still listed as computed, mount and tick. They are
cached, mount and tick — computed became a contextual keyword and cached took
the reserved slot, which the bullet list twelve lines below already said
correctly. A charter that names the wrong reserved word is worse than silence,
because refusing `const cached = 1` is exactly the kind of rejection a reader
would come here to understand.

The contextual-keyword count and roster were each one short: eleven words, and
computed is the missing one. It is worth stating explicitly rather than just
adding to the list, because computed is the word most likely to be assumed
reserved now that it declares — it declares in its own shape and is an ordinary
name everywhere else, like the other ten.

Section 3 now names WEB_CONTEXTUAL_KEYWORDS as the owner of that roster, the
way the sentence beside it already names CORE_CONTEXTUAL_KEYWORDS. The prose is
still a copy rather than a derivation and nothing gates it, which is how it
drifted; naming the authority at least tells the next editor where the truth
lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ists

Two failures, and they are the same shape twice: an assertion written as
"exists" where the truth needed "exactly".

The website pins @velarscript/cli 0.10.0, nothing by that name is published,
and what is actually installed comes from a manual bootstrap against local
source. So the compiler in node_modules stops at whoever last ran that command,
and nothing watches it. Swapping in a current build turned samples:check red on
sixteen samples at once — eleven stale spellings, an inverted counter-example
publishing the correct spelling as an error, a type-name collision, an enum
that had grown two members, and three D74 props cases where the prose was wrong
and not only the code. Those three are the reason this is filed as severe: the
site was publishing an unconditional guarantee the language had deliberately
withdrawn, and a stale guarantee is a promise the language no longer keeps.

The second is narrower and older. A counter-example passes when the codes it
quotes appear, not when they are the only ones emitted, so one sample taught
VEL4001 while silently also emitting VEL5065. Worse, a sample can be carried by
a neighbour: one published VEL3007 for a rejection the compiler stopped making,
and stayed green because a different line in the same block still produced that
code. Set equality closes both.

That makes three appearances of this family — the standard-module surface audit
was first, where a substring assertion pretended to resolve links. Worth
naming, because it keeps arriving disguised as a different bug.

My recommendation on the toolchain is to make sameness a gate rather than a
step: assert the installed compiler and the language repo agree before
samples:check runs. Publishing to npm and pinning a real version does not fix
it — it only makes the drift better dressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The adversarial audit found a filename with a legal `)` in it walking straight
past the relative-address gate: `url("./mark).svg")` checked clean, built
clean, emitted no diagnostic, and shipped a stylesheet into dist/assets/ whose
address resolves one directory away from the file. Measured, not reasoned —
root asset 200, CSS-resolved asset 404. The same regex refused a real value in
the other direction, rejecting `content: "url(./not-an-asset.svg)"`, which is
text rather than a reference.

Both directions from one cause: the scanner treated a quote-blind `[^"')]+` as
CSS. Patching the character class would only move the next shape, and the two
findings are already that argument — one regex, wrong twice, in opposite
directions.

So the address gate now reads a CSS Syntax Level 3 tokenizer. Strings are
skipped as strings, comments as comments, escapes are decoded, and bad-url and
bad-string recover where the specification says a browser drops the
declaration. It reports the address a browser would actually request rather
than the source text, which is why two findings are about the gate naming the
wrong file rather than missing it.

Tokenizing surfaced twenty more shapes the regex got wrong, in all three
classes: six silent bypasses of the same severity as the reported one, two
where the gate fired and named a truncated or still-escaped address, and twelve
real values wrongly refused — among them `myurl(`, which is not a url token at
all, and an escape sequence spelling an absolute path.

The @import check was a second hand-written scanner in the same file with lower
coverage than the first, which is the derived-vocabulary family again: two
copies of one job, and the copy drifts. It reads the same token stream now, and
that alone fixed two more false negatives — an at-rule with no separator in
front of it, and an escaped at-keyword.

The regression pins token grammar rather than adding samples, runs every case
through both the inline and external CSS sources, and asserts the built
artifact: a refused build leaves no dist at all, and an accepted one is
re-tokenized so that every address in the emitted stylesheet is root-absolute,
a fragment, or schemed, and every root address exists on disk.

All four gates green: check=0, test=0 (1443/1443), test:packages=0,
test:browser=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tokenizing CSS made two more spellings visible that the regex could never have
shown: src() in @font-face, and the bare string arguments of image-set. Both
carry relative addresses, both get extracted into assets/, and both 404 there
exactly the way url() did. The rule's own diagnostic says why it exists — so
extracted asset ownership stays explicit — and asset ownership cannot tell
those spellings apart. The argument already covered them; only the wording
stopped at a function name.

That is the same failure as publishing a name nobody can reach, from the other
side: a rule that does not reach a position where its own reason applies lets
an author write something that genuinely breaks while the tool says nothing.

The ruling carries a guard rather than a list to implement. The spellings came
from the wave's tokenizer observing them, not from my checking what browsers
actually request, so each has to be confirmed writable before it is covered,
and any that cannot be confirmed stays out and gets said so. A rule that
forbids a spelling nobody can write is the unreachable-name defect wearing the
opposite coat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The adversarial audit found `try await import("./dep.vel")` answering false for
a module that exists — clean check, exit 0, and a build with no dep.js in it.
Same cause in a `using` initializer, a test body, a class getter, `@dispose`
and `@iterate`, where it surfaces as a runtime module-not-found instead.

dependenciesOf carried a second hand-written switch over statement and
expression kinds. That is the derived-vocabulary family living in the module
graph, and it was already drifting from work landed the same day: `@iterate`
shipped this morning and the switch had never heard of it.

My brief was wrong about the repair and the wave said so instead of following
it. I asserted the AST had a general visitor to reuse; it does not. What exists
is the ContainsDirectAwait family, which does have cases for the very
containers the audit named — probably what the audit saw — but answers a
different question and is deliberately wrong for this one: a test body returns
false because it is its own async frame, and so does an arrow function.
Reusing it would have rebuilt A-010 with a fresh set of holes.

So dependency discovery no longer walks by kind at all. The AST gained a
structural descent that never asks what a node is, and the lookup is a filter
over it. A container added tomorrow needs no edit anywhere, which a test pins
directly with a synthetic extension node of an unknown shape nested three deep.

The same defect had a second copy in the extension protocol —
CompilerDependencyContext and the two visitDependency hooks let every extension
keep its own hand-maintained mirror of the AST, each free to drift
independently. Removed, with the Web implementation behind it. Net -193/+80.

The regression is a matrix rather than a sample, because "one example exists"
is exactly what let this through. Every distinct expression position in the
tour and app corpus is rewritten into a dynamic import and re-inspected — 2445
positions — and the assertion needs no notion of what a container is: whatever
node the parser produced, the graph must have it. The one position that refuses
quotes the language's own diagnostic as its reason, so the single exclusion is
not mine. A generated project then carries a distinct module through 34 core
containers under check, run, build and test.

Under the old code that matrix misses 240 chains, and every one attributes to
the six containers the audit named. No seventh — recorded as a result rather
than as an absence.

All four gates green: check=0, test=0 (1460/1460), test:packages=0,
test:browser=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eft open

The changelog gains the audit itself, not just its repairs. A release that was
attacked before shipping should say so, and say what the attack found: 26
findings, five that compiled clean and answered wrong. The three gate failures
are grouped deliberately — a gate that overstates its reach is worse than a
missing one, because it is trusted, and all three were built during this same
release.

D82 records what the traversal wave left open. The one decision is the
asymmetry it exposed: the statement roster is complete by construction, since
its mapped type makes tsc refuse an incomplete copy, while the expression side
has no roster at all and the test hand-types nineteen kinds. Adding an
expression kind would redden nothing — it would be silently classified as not
an expression and drop out of coverage. I recommend publishing the roster: the
runtime fix is safe either way, but the gate's completeness now rests on a copy,
which is the exact family this wave existed to remove.

Three things are recorded so they are not rediscovered or "fixed" later. The
remaining hand-maintained switch with a silent default is named as the next
place to audit, without being claimed as a defect, because the wave could not
construct a live failure and said so. A container that appears only in
shorthand is invisible to a corpus-derived roster — the real limit of deriving
from the tour, now written into the test rather than left as folklore. And
static imports still scan only the top level, which is correct and load-bearing:
a nested import is refused by the language, so it can never need to be in the
graph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ter copy

All three gates were built during this release and all three overstated their
reach, which is worse than not having them, because a gate is trusted.

The tour gate credited a name the moment its import specifier parsed. Keeping
the import and deleting the only call still reported full coverage, next to a
failure message that says "no module uses it". A named import now counts only
when the analyzer resolved a reference to its local binding, and an import that
is never referenced is named as the dead line it is. Both directions are
tested, which is the part that was missing: the old suite only ever deleted
imports, so it could never have found this.

That fix uncovered a fourth instance in the same file, and it contradicts a
comment I had just written asserting it was impossible: namespace members were
credited by the import's local spelling rather than its resolved symbol, so a
local object literal with the right field name forged coverage for a standard
module export. Both maps are keyed by symbol id now.

The documentation gate's fence extractor required column zero, backticks only,
and a closing fence exactly as long as the opening — three things CommonMark
does not say. It now implements §4.5, including dedenting the opening
indentation off every content line, which is load-bearing for an
indentation-sensitive language. Where the scanner genuinely cannot reach — a
fence inside a block quote or a deeply nested list — it names the line and
fails rather than skipping in silence. Exactly one fence in the tree was
invisible to the old regex, it compiles clean, and no prose needed changing.

The package gate derived its pack list and nothing after it: content checks,
the install list, and the build chain each walked their own literals, and a
fifth copy sat in the installed-browser acceptance where a new package with no
dependents would have been packed and imported by one job and silently skipped
by the other. All of them read the derived set now, and the contract asked of a
package comes from its own manifest — every path promised through main, types,
exports, bin and velar.entry must be in the tarball, and every published
specifier must actually import in a clean consumer.

Two latent bugs fell out. The documentation gate declared its failures array
after the walk that pushes into it, so a checkout with no README crashed
instead of reporting. And the build order is now derived from each manifest's
own workspace dependencies rather than a hand-written chain.

All four gates green: check=0, test=0 (1462/1462), test:packages=0,
test:browser=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Error-Zhang
Error-Zhang merged commit ef68c78 into main Aug 16, 2026
8 checks passed
@Error-Zhang
Error-Zhang deleted the codex/editor-owner-gaps branch August 26, 2026 11:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant