Releases: Koniverse/Senti-MCP
Release list
v2.8.1
[2.8.1] — 2026-08-25 — the API Keys dashboard URL, and what the default base URL actually pairs with
The startup error for a missing SENTI_API_KEY sent people to
https://stage.sentitrade.xyz/account/api-keys. It now sends them to
https://app.sentitrade.xyz/account/api-keys — the host the Senti landing pages and
docs already use — and README.md and SETUP.md follow. Functionally the
two dashboards are interchangeable for this purpose, which is the point of the
second half of this entry.
What makes the swap safe is that the two hosts are the same front door: the served
production bundles of app.sentitrade.xyz and stage.sentitrade.xyz both carry
REACT_APP_API_URL: "https://be-dev.sentitrade.xyz" (checked 2026-08-25), so a key
from either dashboard is issued by the same backend. The environment-pairing
warnings in README.md and SETUP.md now say that, replacing the older
"keys are currently issued from the staging dashboard" phrasing, which read as
though the dashboard host picked the environment.
The warnings otherwise stand as they were: keys are environment-bound, and a 401
on a valid-looking key is more often a mismatched SENTI_API_BASE_URL than a bad
key. Whether the default https://api.sentitrade.xyz accepts a dashboard-issued key
is still not established — from outside the deployment it answers every
unauthenticated probe byte-identically to be-dev, which is consistent with one
origin behind two hostnames but does not prove it. The default is unchanged;
settling it is a deployment call, not this server's.
No tool, schema or response changed. 673 unit tests pass.
v2.8.0
[2.8.0] — 2026-08-21 — compile_draft, and EPIC-8's close
The tool that closes the loop. compile_draft runs the static-safety scan and the MQL5
compiler over a draft and every indicator attached to it, and returns the verdict, the
diagnostics and the compiler log (US-8.4).
It is the seventh and last tool of EPIC-8, and like the other six it
is registered only when SENTI_ENABLE_AUTHORING_WRITE is set.
A failed build is not an error. The API returns 200 with ok: false and diagnostics; the
tool returns a success result carrying them. Marking it isError would tell a model its
call malfunctioned, and a model's correct response to that is to retry — against a globally
serial compile slot, for a build that will fail again identically.
Nothing retries, and the abort message says why that matters here. The compile slot is one
per account and the compile server is serial across all accounts, so a 409 means someone
else's compile is running and a 503 carries a wait this server reports rather than sleeps on.
The 15-second client timeout is not raised — but aborting the fetch does not cancel the
compile, so the account's slot stays busy and the next call would be a 409. The message says
exactly that and sends the model to get_draft for lastCompileStatus.
It is the only tool in this repo that parses diagnostics strictly, because
POST /drafts/{draftId}/compile is the only route in the whole document that declares their
shape. get_draft and list_drafts parse theirs loosely and will keep doing so — see below.
The write smoke test, and the two things it settled
npm run test:smoke with SENTI_SMOKE_WRITES=1 now creates a real draft, attaches an
indicator, compiles it and deletes it, cleaning up in a finally. It exists to discharge two
gaps EPIC-7 closed with, both of which needed a write:
1. The GET's diagnostics match the compile response's — and the parse stays loose anyway.
No live draft had ever carried a non-empty lastCompileDiagnostics, so get_draft's
loose-parse/tight-render pair had never met real data. Measured on a deliberately broken draft:
the two arrays are identical, same six keys, same values. The render path is confirmed. The
parse does not tighten, because the observation is about the service and the parse is a bet
on the contract — which still declares the array untyped (CONTEXT D44).
2. An attachment existed for the first time. The smoke account has held zero attachments
since 2.2.0, so every attachment branch in list_draft_attachments was test-covered and none
was live-covered. The write smoke creates one.
Two undocumented behaviours found on the way, recorded in CONTEXT D44: a
draft's name derives its .mq5 filename with non-alphanumerics replaced by underscores — so
diagnostics[].file will not string-match the draft name — and the compiler log carries the
compile host's absolute Windows path, C:\MT5\compile_jobs\<draftId>\…, CRLF-terminated.
EPIC-8 closes
Seven of the Authoring tag's eight write operations now have a tool. What the close does
not claim is in EPIC-8 §What this close does not claim:
register is unimplemented, and the two delete tools are unusable on a host without MCP
elicitation support.
v2.7.0
[2.7.0] — 2026-08-21 — the three attachment writes
add_draft_attachment, update_draft_attachment and delete_draft_attachment complete the
indicator sub-resource (US-8.3), whose read
half US-7.4 shipped in 2.4.0. All
three are registered only when SENTI_ENABLE_AUTHORING_WRITE is set.
Filenames collide case-insensitively, and the message says why. MyInd.mq5 and
myind.mq5 are the same file to this platform, because the compile host writes every
attachment into one flat Windows directory. A 409 from add_draft_attachment reports that
rather than a bare "already exists", so a model does not try the same name in another case.
The filename is immutable, so update_draft_attachment does not accept one. An EA embeds
an indicator by name — #resource "MyInd.ex5" — and a rename would orphan every reference and
turn a working draft into a static-safety violation. To rename: delete, re-add, and update the
EA source. Accepting a filename the API would ignore is worse than not accepting one, so the
input schema is draftId, attachmentId, sourceCode and nothing else.
Attaching does not wire up, and deleting does not unwire. add_draft_attachment names the
exact #resource "<stem>.ex5" and iCustom(_Symbol, _Period, "::<stem>.ex5", …) lines the EA
still needs, derived from the filename you gave it. delete_draft_attachment says the opposite
thing: the EA still references a file that is gone, and the next compile_draft fails on it
unless update_draft removes those lines first. A draft that compiles a file it never
references reads as a success otherwise, and one that references a file it no longer has reads
as a compiler problem.
delete_draft_attachment is the second and last tool in EPIC-8
that pauses for a human confirmation, on the same reasoning as delete_draft
(CONTEXT D42).
A 404 on either attachment-id tool carries a cause the draft 404 does not: the
attachment may exist and belong to a different draft. That is an easy mistake to make with
two ids in one path, and a message that only said "not found" would send the reader to check
the wrong one.
v2.6.0
[2.6.0] — 2026-08-21 — update_draft and delete_draft
The two draft writes that can destroy work, and the first tool in this server that pauses for
a human (US-8.2). Both are registered
only when SENTI_ENABLE_AUTHORING_WRITE is set.
update_draft is a FULL REPLACE, and it is annotated destructiveHint: true despite its
name. The API declares no partial-update verb: name and sourceCode are both always
written, so a model that sends only the function it changed deletes the rest of the file. The
warning is in the tool's description, where a model reads it before choosing the argument,
which is the only place a warning about a destructive argument can still help.
It reports the bytes it wrote, not the bytes it replaced. A before/after delta would need
the pre-write size, and the PUT response carries only the new draft — a hidden GET would
double the latency of every edit and race any concurrent writer. When a previous compile no
longer matches the new source, the text says so and names compile_draft.
delete_draft asks first, and a "no" is not an error. It is one of two tools in
EPIC-8 that pause for an explicit human confirmation; the other
five do not. That is a deliberate line, not an oversight: update_draft fires on every save
in an edit loop, and a confirmation a user sees fifty times in a session is one they stop
reading — a rubber-stamp laundered into the appearance of consent is worse than no prompt
(CONTEXT D42). The two deletes are the only operations in this epic that no
other tool in it can undo.
A declined confirmation returns a success carrying { id: null, deleted: false } and a
note saying no request was sent. isError: true would tell a model something malfunctioned
and invite a retry, and a user saying no is neither.
Two mechanics worth knowing if you build on the seam. It identifies the round by an opaque
requestState it mints, not by the answer: acceptedContent() reports a decline and a first
entry identically — both undefined — so branching on the answer alone re-asks on every
decline and spins until the client's round cap. And a forged requestState cannot skip the
confirmation, because only accepted content reaches the request.
A host without elicitation support cannot use delete_draft. That is accepted rather than
worked around: a silent fallback to deleting without confirmation would make the guardrail a
function of the client, which is the one property a guardrail must not have. Every other tool
in this release works normally on such a host.
core/tool.ts now imports two runtime values from @modelcontextprotocol/server
(inputRequired, acceptedContent), so it joins src/server.ts and src/index.ts as the
third file that does. AGENTS.md said server.ts was the only one; corrected.
v2.5.0
[2.5.0] — 2026-08-21 — create_draft, and the write path opens
The first tool in this server that changes something. create_draft calls
POST /api/v1/drafts under a seventh scope, authoring:write, and is the first of
seven tools EPIC-8 opens over the Authoring tag's write
operations (US-8.1).
It is not registered unless you ask for it. SENTI_ENABLE_AUTHORING_WRITE=1 (or
true) registers the authoring write tools; anything else — including 0, false, no
and off — leaves them unregistered, and a host that never sets it sees the same fourteen
read tools 2.4.0 shipped. The flag is authoring-only: it enables no trading write at
any setting, because closing a position is a different surface with a flag of its own that
does not exist yet (EPIC-3). Keeping them separate is the point —
enabling an agent to edit MQL5 must never be the same act as enabling it to close a
position.
The response does not echo your source back. POST /drafts returns the complete draft,
up to 192 KiB of source plus five attachments at 64 KiB each — content the model supplied in
the same call. Returning it would bill it a third and fourth time, through content and
structuredContent both (CONTEXT D34). The tool returns the new id, the
byte count written, the compile state and an attachment summary, and notes points at
get_draft for a read-back (CONTEXT D39). A note records loss, so creating
an empty draft writes none.
The Idempotency-Key is a fresh UUID per call, not one derived from the body. The
design specified a content-derived key so that an identical repeat would replay the original
201 instead of colliding with a 409, and left the retention window as the open question
that would decide it. Measured against be-dev on 2026-08-21: an idempotency record
outlives a delete. Create → delete → byte-identical create replayed the original response
and returned a draftId that no longer existed. Since create, delete, create again is what
iterating on a draft looks like — and delete-then-recreate is the API's own prescribed way to
rename an attachment — the derived key was replaced with randomUUID(), which still gives the
protection the header is actually for: one request delivered twice by the transport creates
one draft (CONTEXT D43, revising D41).
Nothing retries anything. The write path adds 413, 422, 502, 503 and 504, and
none of them is retried. Retry-After is read and quoted in the message, never slept on: a
tool call that waits holds the host's turn open for an interval the server chose. A 503
without Retry-After is reported as meaning a retry cannot help, which is what the API
documents (CONTEXT D40).
A 403 no longer assumes it is about a scope. On every read it was; on create_draft it
also means the draft cap is full, and against that cause the old wording — "the key is
missing that scope, not that the account is off limits" — sends the reader to mint a key
they already hold. The new forbiddenMeans option lets each endpoint say what its 403
means, and the read tools keep the old wording byte for byte by passing nothing.
registerWriteTool is a second registrar, not a flag. registerReadTool is unchanged
and still pins readOnlyHint: true as a constant, so no call to it can produce a write
whatever its arguments (CONTEXT D38). A test asserts it directly.
Also corrected in this release: three files in this repo stated that
POST /drafts/{draftId}/register puts an EA into a real trading account. It does not — it
creates a permanently private EaDefinition, and deploying is
POST /accounts/{accountId}/strategies under the separate strategies:write scope. The
claim had been inferred from the operation's name rather than read from its description
(CONTEXT D36). register is still out of scope, for a different reason: it is
the only write in the tag that creates a resource the tag cannot then delete.
What this release does not do: compile, update, delete or attach anything. Those are
2.6.0 through 2.8.0. create_draft writes a draft and stops; nothing is compiled until
compile_draft exists.
v2.4.0
[2.4.0] — 2026-08-20 — list_draft_attachments: the fourteenth tool, and EPIC-7's close
list_draft_attachments reads GET /api/v1/drafts/{draftId}/attachments under
authoring:read and returns the indicator source files a draft's EA embeds via
#resource — the source US-7.2 deliberately
leaves out of get_draft. It is the fourth and last tool over the Authoring tag
(US-7.4), and this release closes
EPIC-7: all 14 of the Senti Quant Public API's GET
operations now have a tool.
A budget, not a truncation. At maxAttachmentsPerDraft: 5 × maxAttachmentBytes: 65536 the endpoint's ceiling is 320 KiB ≈ 82,000 tokens; counting both content and
structuredContent, which both reach the model, the tool's own worst case is ≈ 33,000
tokens (CONTEXT D34). With filename supplied, the tool returns at most one
attachment whole, whatever its size, and cuts nothing else — filenames are not guaranteed
unique within a draft, so if more than one attachment shares the requested name, only the
first is returned and notes says how many were skipped, rather than returning every
match and bypassing the budget entirely. The note states how many were skipped, one
fewer than the number that share the name. A filtered read also says it is filtered —
Filtered by filename "X" — 1 of N attachment(s) on this draft — so a host that surfaces
content alone cannot read the filtered count as the draft's whole attachment set.
With filename omitted, attachments are returned whole, in the API's own order, while
the running total including the one just added stays within 65,536 bytes — the first
attachment is always returned whole regardless of its own size, and once one attachment
is cut, every attachment after it is cut too — and the note says exactly that, rather
than claiming each cut file exceeded the budget: a 1-byte attachment that follows a breach
is cut without exceeding anything, and a model told otherwise will not re-read it.
Checking the total after inclusion rather than before is what caps the response at 64 KiB
instead of admitting 127 KiB. A cut
attachment keeps its metadata and reports sourceCode: null; source is never partial —
an empty file is '' with sourceBytes: 0, and the two are never confused. A filename
that matches nothing returns an empty result and names, in the text, every filename that
does exist, which is also how to discover what a cut left out.
The API has no way to request one attachment by id. GET /drafts/{draftId}/attachments/{attachmentId} does not exist, though PUT and DELETE
on that exact path do — raised with the API as an asymmetry. The filename filter is a
client-side workaround: the set is always fetched whole, so it bounds the model's
context, not the wire.
Untested live. The smoke account holds 4 drafts and 0 attachments in all 4 — measured
again this release, unchanged since 2.2.0 and 2.3.0 — so the budget, the cut rule and
the filename filter are proven only against synthetic sizes in
list-draft-attachments.test.ts. EPIC-7's close states this rather than letting a green
suite imply otherwise; see
EPIC-7 §What this close does not claim.
Added
list_draft_attachments— a byte-budgeted attachment read
(src/tools/authoring/list-draft-attachments.ts).ATTACHMENT_BUDGET_BYTES,
AttachmentsOutputSchema,parseAttachments,shapeAttachments,formatAttachments.
Registered afterget_draftinsrc/server.ts, so theauthoring/group reads
conventions → list → read → attachments.
Notes
- EPIC-7 closes
done. All 14 of the API'sGEToperations have a tool. Its close
names four branches that never ran against the live service — every attachment code
path across all three draft tools, this tool's budget andfilenamefilter,get_draft's
DiagnosticSchemarender path, andDRAFT_NOT_FOUND's 404 — and what would discharge
each. The open question on whether aGET'slastCompileDiagnosticselement really
matches the compile response's diagnostic shape is still open. - A second fix wave, C1-C9, corrected what the four authoring tools say about their own
payloads — seven sentences that were not true of the payload in hand, and the missing
per-tool coverage insrc/server.test.tsthat let them through. See
CONTEXT D35. No version moves: none of2.1.0-2.4.0has been published.
v2.3.0
[2.3.0] — 2026-08-20 — list_drafts: the thirteenth tool, and the largest payload in the API cut four ways
list_drafts reads GET /api/v1/drafts under authoring:read and lists every MQL5 draft
the API key owns, most recently updated first, with each draft's compile status, size,
attachment count and registered-EA id — the tool that answers "what am I working on" or
"which of my drafts are broken". It is the third tool over the Authoring tag
(US-7.3), and its DraftSummarySchema is
derived from 2.2.0's DraftSchema by .omit() and .extend() rather than hand-written,
so a field the API adds upstream cannot silently bypass the cut.
The largest payload the API can produce. The route takes no query parameters at all —
no ?include=, no ?fields=, no pagination, no filter — and returns every draft's full
sourceCode, every attachment's full sourceCode, and every draft's lastCompileLog
whole. At the US-7.1
ceilings that arithmetic is 20 × (192 KiB source + 5 × 64 KiB attachments + 16 KiB log) = 10.3 MiB ≈ 2.7 million tokens. Measured live on 2026-08-20 against an account holding 4
drafts and 0 attachments: 19,853 B → 1,898 B, 90.4% removed — matching the design
spec's 2026-08-19 measurement exactly. See CONTEXT D32.
Four cuts, one note. sourceCode (→ sourceBytes), attachments[].sourceCode (→
sourceBytes), lastCompileLog (and logTruncated, which describes a field that is no
longer there), and lastCompileDiagnostics (→ diagnosticsCount). All four lose
information, so all four are reported — but as one sentence naming the draft and attachment
counts, the KiB removed, and both get_draft and list_draft_attachments as the way to
read what was cut, rather than four notes a reader would learn to skim past. notes stays
empty on an empty collection, so its presence never implies a cut occurred.
No parameter turns the cut off. The ceiling is two orders of magnitude past
get_performance_breakdowns', the API's next-largest payload, so an opt-out here would be a
footgun with a documented safety catch rather than a real escape hatch — get_draft already
is one, and it returns one draft rather than twenty.
Added
list_drafts— every draft, shaped (src/tools/authoring/list-drafts.ts).
DraftSummarySchema,DraftsOutputSchema,parseDrafts,shapeDrafts,formatDrafts.
Registered betweenget_authoring_conventionsandget_draftinsrc/server.ts, so the
file reads conventions → list → read. Text output marks a draftSUCCESS-and-up-to-date as
"ready to register" and renders anulllastCompileStatusas "never compiled" rather
than printingnull.
Notes
- The API's
AuthoringGETsurface is now three of four tooled.
list_draft_attachmentsremains, tracked in EPIC-7. src/smoke.test.tsnow parsesGET /api/v1/draftsthroughparseDraftsrather than
casting it, so the live smoke path exercises the schema instead of bypassing it.
v2.2.0
[2.2.0] — 2026-08-19 — get_draft: the twelfth tool, and the schemas the last two EPIC-7 tools import
get_draft reads GET /api/v1/drafts/{draftId} under authoring:read and returns one MQL5
draft's full source code, its compiler log, its diagnostics, and whether the last compile still
matches the current source — the tool that answers "why did this fail to compile" or "show me
the code". It is the second tool over the Authoring tag
(US-7.2), and the module that owns DraftSchema and
AttachmentSchema: list_drafts and list_draft_attachments import both from here rather than
redeclaring them, so one shape drifting only breaks in one place.
One cut. At the US-7.1
ceilings, one draft is worth up to 192 KiB of EA source + 5 × 64 KiB of attachment source + 16
KiB of compile log — roughly 135,000 tokens, more than most context windows. Attachment
sourceCode is replaced with sourceBytes; everything else, including the EA's own source,
returns whole — this is the tool a caller reaches for because they want the source, so
truncating it here would leave no tool able to return it.
Read live on 2026-08-19 against the account holding drafts: the key set matched the design
spec's twelve exactly, four drafts existed, none FAILED and none carrying an attachment, so
no real lastCompileDiagnostics element was available to check against DiagnosticSchema — a
shape transcribed from POST /drafts/{draftId}/compile, which types that field where the two
GET paths do not. The parse stays z.array(z.unknown()) regardless: parseOrThrow is
all-or-nothing, so transcribing the compile response's shape onto an untyped GET field would
take both draft tools down the day they diverge. See
US-7.2 §Implementation notes for the observed key
set.
Added
get_draft— one MQL5 draft, full fidelity (src/tools/authoring/get-draft.ts).
DraftSchema,AttachmentSchema,DiagnosticSchema,AttachmentSummarySchema,
DraftOutputSchema,byteLength,parseDraft,shapeDraft,formatDraft. Diagnostics
render opportunistically —DiagnosticSchema.safeParseper element, a readable
file:line:columnline on a match, the raw element otherwise — so a shape mismatch costs a
less readable line, never a failed call. The text also composes the API's documented
register-readiness question (lastCompileStatus === 'SUCCESS' && compiledUpToDate) as a
derived sentence rather than a stored field, since both operands are already in the output.- Attachment source is cut, not truncated.
attachments[].sourceCodenever reaches
either channel;sourceBytesreplaces it, and anotesentry — repeated in the text —
nameslist_draft_attachmentsand thedraftIdthat undoes the cut. - No parameter reopens the cut, and the EA's own source is never truncated. Half an
MQL5 file reads as a complete one to a model that did not write it.
- Attachment source is cut, not truncated.
Notes
- The API's
AuthoringGETsurface is now two of four tooled.list_draftsand
list_draft_attachmentsremain, both importing this release's schemas rather than
redeclaring them (EPIC-7).
v2.1.0
[2.1.0] — 2026-08-19 — get_authoring_conventions: the eleventh tool, and a new tag opens
The Senti Quant Public API grew a new Authoring tag, and with it from 17 operations to
29 — 14 of them now GET, not 10. get_authoring_conventions is the first tool over that
tag: it reads GET /api/v1/authoring/conventions under a new authoring:read scope and
publishes the platform's own MQL5 authoring contract — the hard-safety constraints, the
trading-safety requirements, the static analyzer's forbidden-construct list, and the five
limits ceilings the API enforces on drafts, attachments and registered EAs. Read live on
2026-08-19: maxDrafts 20, maxAttachmentsPerDraft 5, maxAttachmentBytes 65536 (64 KiB),
maxSourceBytes 196608 (192 KiB), maxRegisteredEas 10. Those numbers matter beyond this
release — they are what size the cuts EPIC-7's remaining three
tools make against a payload the API can otherwise grow past a context window
(US-7.1).
This release also lands the substrate the rest of EPIC-7 stands on:
draftPath/DRAFT_NOT_FOUND in src/core/client.ts, extracted from accountPath over a
shared private segmentPath guard rather than copied
(CONTEXT D33) — no path-traversal guard now has a second copy to drift out of
sync with the first.
Added
get_authoring_conventions— the platform's MQL5 authoring contract, as data
(src/tools/authoring/conventions.ts).ConventionsOutputSchema,parseConventions,
formatConventions. No path parameter, no409—client.getis called with ascope
only, nonotFoundMeansorconflictMeans. The tool description tells the model to call
this before generating any MQL5 source: code that violates these rules is rejected by
a static scan before it reaches the compiler, and compile slots are globally serial, so
discovering a rule by failing a compile is expensive and still fails.- Every rule is rendered whole, never summarized. These are instructions a model must
follow, not records it might skim —formatConventionsnumbers every constraint and
requirement and reproduces everyforbiddenConstructs[].patternverbatim, escapes
intact. - The patterns are reported, not evaluated.
patternvalues are regular expressions
the API's own static analyzer applies; this tool does not compile or run them, and says
so — the document types them as bare strings and never names the regex dialect, so
building aRegExpfrom an undeclared dialect would be a crash or a silent mismatch. - No cuts and no
notesfield. The whole response is roughly 2 KB and static per
deploy, so unlikeget_performance_breakdownsandget_equity_timeseriesthere is
nothing here to shape.
- Every rule is rendered whole, never summarized. These are instructions a model must
draftPathandDRAFT_NOT_FOUNDinsrc/core/client.ts— the substrate the other
threeEPIC-7tools build their paths on. A privatesegmentPath(prefix, segments, hint)
now ownsPATH_SEGMENT,encodeURIComponentand the traversal-rejection loop;
accountPathis re-expressed over it with its signature and error message unchanged, and
draftPathis a second prefix over the same guard rather than a second copy of it
(CONTEXT D33).
Notes
- The API's operation count is now 29, not 17 — a new
Authoringtag adds 12 operations,
four of themGET.get_authoring_conventionsis the first of those four;
EPIC-7 tracks the remaining three (get_draft,list_drafts,
list_draft_attachments).
v2.0.1
[2.0.1] — 2026-08-14 — the floor gets a gate, and the toolchain catches up
Nothing that runs has changed. All 17 dist/**/*.js files are byte-identical to
what 2.0.0 published — verified by checksumming every file in both tarballs, not by
reading the diff. No tool was added, removed or altered; no payload, argument or
structuredContent shape moved. A consumer on 2.0.0 gains no behaviour by upgrading.
What this release actually carries is the work of
EPIC-5's last three stories, none of which cut a version of its
own: a gate that stops the Node floor drifting, a dependency bot, and a compiler decision.
Against the published 2.0.0 tarball exactly four things differ — three .js.map files
(source-position attribution, from the new compiler), two reworded sentences in
README.md, and the devDependencies block of package.json, which npm does not install
for consumers.
It is a patch because docs/RELEASE.md §Step 1 measures semver against the
tool surface rather than the diff size, and the tool surface is untouched.
Added
-
release:checknow guards the Node floor. It comparespackage.json
engines.node— the canonical value — against every floor claim inREADME.mdand
docs/SETUP.md, and fails when they disagree or when an artifact states no floor at
all. The floor was stated in three places and compared by nothing, which is the
LESSONS 4 shape that letpackage-lock.jsonsit eight releases behind its
version string (US-5.2).A floor claim is defined narrowly and the narrowness is the contract: a semver
immediately preceded by>=or≥, on a line mentioning Node. The operator is what
separates the floor from the other Node versions in the same prose —AbortSignal.any's
20.3.0is written "landed in 20.3.0", never ">= 20.3.0", so it is excluded without
being special-cased. The practical consequence when the floor next moves: prose about
an old floor must not use the operator form — "the old 20.6.0 floor", not "the old
>= 20.6.0floor". Two README sentences were rephrased accordingly; no claim changed
meaning. CI pins in.github/workflows/are deliberately not checked, because they bind
nobody outside CI andpublishdiffers from the floor on purpose
(LESSONS 7).Ten tests, watched go red before the implementation existed, plus a mutation of the real
docs/SETUP.md—grep-confirmed on disk before the red result was believed
(LESSONS 1) — proving the gate names the exact file, line and both values. -
.github/dependabot.yml— weekly npm updates with minor and patch grouped into one
PR, so currency stops depending on someone remembering to runnpm outdated
(US-5.3). Twoignore
entries, each carrying the reason it exists and the condition under which it should be
removed:@types/nodemajors (see below) andtypescriptmajors (TypeScript 7 is a native
compiler rewrite and gets its own decision in
US-5.4, rather than riding intomain
inside a grouped refresh).Its header states plainly what a green Dependabot PR does not prove: no workflow runs
on a pull request here —release.ymlfires on av*tag and nothing else — so those PRs
arrive with no typecheck, no test run and no tarball verification, and the file lists the
commands to run locally instead. Enabling the bot without saying so would ship a false
signal.
Changed
-
Development toolchain brought current:
vitest3.2.7 → 4.1.10 andtsx4.23.6 →
4.23.12 (in-range, lockfile only —package.json's^4.19.0did not move). The suite is
unchanged at 20 files / 439 tests, 1 skipped, measured against a baseline taken
immediately before the bump: a count that dropped would have meant a silently-skipped
suite rather than a clean upgrade.vitest.config.ts'ssrc/-anchoredinclude
(CONTEXT D13) was re-proven under the major by planting a decoy test under
.claude/worktrees/and confirming it is still not collected — a major version is exactly
when a default changes underneath you. -
@types/nodedeliberately stays on the Node floor's major and is now a written rule
rather than a pin that looks like neglect (CONTEXT D28). Types newer than the
floor lettscaccept calls to APIs the supported runtime does not have: the build stays
green and the failure lands on the user, at run time — the same shape
CONTEXT D5 raised the floor to fix.npm outdatedwill keep reporting
@types/nodeas behind, and that output is now expected. -
typescript5.9.3 → 7.0.2 — the native compiler port
(CONTEXT D29,
US-5.4).tscis this repo's build,
not only its typechecker —binpoints intodist/andfilespublishes it — so the
emit was compared before the decision rather than after: all 17dist/**/*.jsfiles are
byte-identical to the 5.9.3 build, verified by checksumming every file in both trees.Three
.js.mapfiles differ —core/client,core/errorsandserver, which are
exactly the three sources using a parameter default or a parameter property — and only in
which source positions the generated defaults and field assignments are attributed to. The
JavaScript at those sites is character-for-character the same..js.mapships, so the
tarball does change in debug metadata; it stays at 54 entries and nothing that runs is
affected.Typecheck is clean on both tsconfigs, and each was mutation-tested to confirm it still
catches errors — a new compiler exiting 0 proves it ran, not that it checked. The reason
to move is measured rather than assumed: typecheck~1428 ms → ~503 ms, full build
~1412 ms → ~393 ms(~3.6×). Without a number like that the decision would have been
to stay, since a compiler emitting the same JavaScript buys nothing on its own. -
.github/dependabot.ymldrops thetypescriptmajorsignoreadded one story earlier,
which is what its own comment instructed the deciding commit to do. -
docs/RELEASE.md §Step 2 and §Step 5 describe the floor as a second set of
files that must move together, and therelease:checkfailure table gains three rows.
docs/README.md's pre-commit checklist gains a floor item — a floor change is
not a version bump, so nothing else in that list would have caught one.