Modern API - #3
Conversation
This is the beginning of the new command style strcture. Commands will be added as self managing classes that know how to generate and send the actual command text as well as process parsed responses. The base class implements a common strategy of sending a tagged command and listening for a tagged response, capturing everything in between. The no-op is the most simple implementation of this command system.
Adding command classes for the ID and Capability commands, which will be common to run on intiating the connection. Also included is the first command encoding function for creating IMAP safe strings.
This class will eventually serve as the base class that is exposed and that other systems would interface with. This will contain the public- facing API for the library and will be both a mirror and an update of existing functionality. Note that this commit does not expose this class nor remove or change the original `Connection` class which currently acts as the public interface. Eventually the `Connection` class will be cleaned up and it will serve purely as the connection for the session without any logic for actual commands.
Pulled everything out of the connection class that is not actually related to the connection. This means almost all IMAP related commands and controls will be in a new class. The code went from almost 2k lines to around 300, which should be a lot easier to understand. From a practical perspective, I chose to write a brand new class and just remove the old copy completely. The old code was inter-weaved with a lot of implicit assumptions and untangling that would have been a nightmare. This does, however, mean this is all (or almost all) new code. Which is extra terrifying. But that's a future me problem.
Actually connects and exposes the Session class so that it functions and can create and manage an IMAP session! This is exciting, and I was able to connect a new session to GMail and get the capabilities and server info 💃 **Test plan:** Connect to GMail using the Session class Celebrate 🙌
Instead of just using A0, A1, etc. we want to use A1, B1, etc. This us to send *a lot* more commands before we reuse tags
Creating a baseline E2E suite that can be used to test compatibility against various servers. **Test plan:** yarn test:e2e
With the update to tag names, the tests needed updating. **Test plan:** `yarn test` `yarn test:e2e`
Testing out reporting coverage via GH Actions. **Test plan:** Hope that this all works because GH actions are a pain sometimes
Languages that use indentation as syntax should burn. Spacing should not determine validity 🔥
Switching to only create a single new context for isolated commands rather than premptively making a new context. This was causing issues when the next command queue was empty. We wouldn't properly idle. **Test plan:** `yarn test`
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1. aggregate: sort ProfileResult.tests and problems arrays for diffable JSON output run-to-run 2. render: add cell() helper that collapses whitespace and escapes | as \|; apply to all table cell interpolations; append … only when 160-char truncation actually occurs; Math.floor in pct() so 0.995 → 99% 3. aggregate: failed test with no failureKind annotation defaults to "violation" (vitest timeout bypasses catch) 4. aggregate: warn when a test cites a req under a profile not in that req's profiles list 5. compliance-reporter: print console summary before file writes; wrap mkdir+writes in try/catch with clear one-line error 6. New tests: __tests__/render.test.ts (5 tests), 4 new aggregate tests — 55 total, all green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full client-binding catalog for the closing family: 186 new requirements (catalog 770 to 956). Per-source: RFC5255 LANGUAGE/I18NLEVEL 21, RFC5259 CONVERT 26, RFC4467 URLAUTH 16, RFC5524 URLAUTH=BINARY 7, RFC5802 SCRAM-SHA-1 29, RFC7677 SCRAM-SHA-256 2, RFC4505 ANONYMOUS 12, RFC4422 EXTERNAL +3 (Appendix A append), RFC2221 LOGIN-REFERRALS 8, RFC2193 MAILBOX-REFERRALS 11, RFC3348 CHILDREN 3, X-GM-EXT-1 vendor 23; plus 5 reconciliation-delta sources: RFC7889 APPENDLIMIT 6, RFC8438 STATUS=SIZE 3, RFC8440 LIST-MYRIGHTS 4, RFC8970 PREVIEW 6, RFC9585 INPROGRESS 6. Mechanical quote verification per source (X-GM-EXT-1 against the Google Developers page, 5 WebFetch passes cross-checked). Cross-catalog adjudication: CHILDREN all rev1-only vs RFC9051-7.3.1/RFC5258; STATUS=SIZE dual w/ cross-ref (rev2 folds the caution not the wire vocabulary); SCRAM-SHA-256 cross-refs 5802 without duplication; EXTERNAL append-only, existing 16 entries untouched. Corrections caught: 63-bit not 64-bit SIZE (verbatim RFC); registry references RFC 9755 not 6855 for UTF8 (flagged). REAL-signal findings noted for the spec batches: REFERRAL resp-code hits the same AtomTextCode bare-argument-drop defect as Phase 5's NOUPDATE/ UNDEFINED-FILTER (kind survives, URL dropped - third instance of that root cause); INPROGRESS's parenthesized argument parses cleanly (genuine testable); \HasChildren likely real passes. problems=0, schema valid, 956 catalog requirements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 audit fixes across 6 catalog files (7 items): - rfc5255.ts: fixed a spliced-artifact quote on RFC5255-3.1-3 -- the section 3.1 text wrongly borrowed section 7's "re- issue" line-wrap space; corrected to the unbroken "re-issue" that section 3.1 actually uses, with the section 7 restatement moved to a side observation in notes. - rfc5524.ts: (a) RFC5524-1-1 was mis-sectioned as section "1" when its quoted sentence lives in section 3 -- renumbered to RFC5524-3-1 / section "3" (verified no collision with the 3.1-*/3.2-* ids) and updated all cross-references; (b) corrected the extractionNote's keyword-less-exception list, which wrongly included RFC5524-3.2-2 despite its text containing MUST/SHOULD, and removed the overstated claim that the Abstract names the URLAUTH=BINARY capability string. - rfc4467.ts: (a) fixed RFC4467-9-2's notes, which wrongly described url-rump as the URL without any ";URLAUTH=" suffix -- per section 6, url-rump retains ";URLAUTH=<access>" and drops only ":<mech>:<token>"; (b) added RFC4467-3-6, a new testable entry for the client's duty to supply a valid access identifier in its GENURLAUTH url-rump argument, evidenced by the section 5 worked BAD example. - rfc5259.ts: (a) fixed RFC5259-6-1's note, which wrongly located the "CONVERT/ UID CONVERT" line-wrap at a page 6/7 boundary -- it is a plain line-wrap within page 6; (b) added RFC5259-8.2-1 for the client's MIME-type-match expectation/error-treatment duty; (c) added RFC5259-9-8 for the ERROR TEMPFAIL mm client wait-time SHOULD, and corrected RFC5259-9-4's note, which wrongly claimed RFC5259-9-1 already covered this distinct ERROR-phrase form. - rfc2193.ts: corrected three stale notes (RFC2193-5.1-1, RFC2193-5.2-1, and the extractionNote) claiming the rlist/rlsub driver verbs don't exist -- they were added in commit 74a9620 and throw NotImplementedError (self-actualizing unimplemented). - xgmext1.ts: (a) fixed X-GM-EXT-1-labels-8's note, which claimed no worked SEARCH example was found -- added the confirmed example from the Google page; (b) added X-GM-EXT-1-labels-10 for the XLIST-based label-list-retrieval sentence, graded MAY/untestable (internal- decision) given XLIST's documented deprecation. Full yarn test:compliance run: the three meta suites (catalog, registry-coverage, import-hygiene) all pass with zero schema problems; catalog entry count rose from 956 to 960 (+4), matching the four new entries added (RFC4467-3-6, RFC5259-8.2-1, RFC5259-9-8, X-GM-EXT-1-labels-10). All new/changed quotes were verified as verbatim substrings of the source RFC .txt files (or cross-checked WebFetch passes for the vendor doc) via a throwaway Node script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 Task 8 (G3 — legacy/vendor/deltas): spec batch for LOGIN-REFERRALS
(RFC 2221), MAILBOX-REFERRALS (RFC 2193), CHILDREN (RFC 3348, rev1-only),
the X-GM-EXT-1 Gmail vendor extension, and the small reconciliation-delta
sources (APPENDLIMIT/RFC 7889, STATUS=SIZE/RFC 8438, LIST-MYRIGHTS/RFC 8440,
PREVIEW/RFC 8970, INPROGRESS/RFC 9585).
Coverage (8 new files, 67 tests total):
- referrals-2221-2193.test.ts: REFERRAL resp-code acceptance across LOGIN/
AUTHENTICATE (RFC2221-4.1-1/-2/-3, RFC2221-4.2-1), SELECT/EXAMINE/DELETE/
SUBSCRIBE/UNSUBSCRIBE/STATUS/APPEND/CREATE/RENAME/COPY (RFC2193-3-2/-3-3,
RFC2193-4.1-1/-4.2-1/-4.3-1/-4.4-1), plus RLIST/RLSUB command forms
(RFC2193-5.1-1/-5.2-1). 22 tests: 12 genuine passes (kind-acceptance legs),
6 genuine violations (URL-exposure legs), 4 unimplemented (RLIST/RLSUB).
- children-3348.test.ts: RFC3348-3-3 (LSUB not authoritative for CHILDREN
hierarchy), rev1-only. 1 genuine pass.
- xgm-ext1.test.ts: X-GM-MSGID/X-GM-THRID/X-GM-LABELS FETCH+SEARCH forms,
X-GM-LABELS STORE (+ADD) form, X-GM-RAW search passthrough, capability
gate, and CREATE-based label lifecycle. 20 tests: 2 genuine passes
(capability parsing), 18 unimplemented (no fetch/search/store/create
surface).
- appendlimit-7889.test.ts, statussize-8438.test.ts, listmyrights-8440.test.ts,
preview-8970.test.ts: STATUS/LIST-STATUS/APPEND/FETCH wire-form specs for
each delta capability, all self-actualizing unimplemented (22 tests).
- inprogress-9585.test.ts: RFC9585-4-1, all four ABNF-permitted INPROGRESS
shapes (bare, all-NIL, counting-with-unknown-goal, known-goal triple).
8 genuine passes.
Real-signal findings confirmed:
- REFERRAL (RFC 2221/2193): the `[REFERRAL imap://...]` resp-code parses via
the AtomTextCode fallback — kind === "REFERRAL" survives intact (genuine
PASS on kind-acceptance, 12 tests across LOGIN/AUTHENTICATE/OK/BYE/SELECT/
CREATE/COPY), but the bare (unparenthesized) URL argument is dropped by
splitSpaceSeparatedList's default '(' start-token (genuine VIOLATION on
URL-exposure, 6 tests including multi-URL and RENAME-pair cases) — the 4th
confirmed instance of this AtomTextCode bare-arg-drop defect after
RFC5466 UNDEFINED-FILTER.
- INPROGRESS (RFC 9585): `[INPROGRESS ("A001" 454 1000)]` and all three
sibling ABNF shapes parse cleanly (parenthesized argument survives the
same start-token mechanism that drops REFERRAL's bare argument) — 8
genuine passes keying on the exact parsed tag/progress/goal tuple values.
- \HasChildren/\HasNoChildren (RFC 3348): confirmed real — the client
parses LIST/LSUB attributes; RFC3348-3-3 (LSUB not authoritative) is a
genuine pass: a stale LSUB \HasNoChildren does not suppress or override a
subsequent LIST's \HasChildren for the same mailbox name.
Full `yarn test:compliance` run: 0 problems (no stale hints, no unknown ids,
no profile mismatches). `yarn test`: 229 passed, the pre-existing
newline.transform failure only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 batch G1 (i18n + URL): three new spec files covering RFC 5255
(LANGUAGE/I18NLEVEL/COMPARATOR), RFC 5259 (CONVERT), RFC 4467 (URLAUTH),
and RFC 5524 (URLAUTH=BINARY, folded into the URLAUTH file per its small
7-entry delta). All four catalog sources now have zero untested-testable
requirements; full yarn test:compliance run confirms problems: [] and
yarn test shows only the pre-existing newline.transform failure.
Coverage:
- language-5255.test.ts: 21/21 RFC5255 entries (LANGUAGE command forms,
COMPARATOR command forms, [BADCOMPARATOR] resp-code, active-comparator
scoping over SEARCH/SORT/THREAD).
- convert-5259.test.ts: 28/28 RFC5259 entries (CONVERT/UID CONVERT/
CONVERSIONS command forms, CONVERTED/CONVERSION response acceptance,
TEMPFAIL/MAXCONVERTMESSAGES/MAXCONVERTPARTS resp-codes, ERROR-phrase
shapes, cross-session caching prohibition).
- urlauth-4467.test.ts: 17/17 RFC4467 + 7/7 RFC5524 entries (RESETKEY/
GENURLAUTH/URLFETCH command forms, access-identifier construction,
GENURLAUTH/URLFETCH response acceptance, URLMECH resp-code, extended
URLFETCH BINARY/BODYPARTSTRUCTURE/BODY parameters).
Real-signal findings (probed via connectLow before writing, per
REAL-SIGNAL-FIRST):
- LANGUAGE, COMPARATOR, CONVERTED, CONVERSION, GENURLAUTH, and URLFETCH
(base and RFC 5524 extended forms) untagged responses have NO parser
handler. UntaggedResponse's fixed matcher checklist has no entry for any
of them, so the constructor throws ParsingError. Connection attaches no
'error' listener to the Parser transform, so the parser stream dies
silently mid-connection — no event surfaces, and every subsequent
response on the wire is lost too (identical failure shape to the Phase 5
QRESYNC VANISHED finding). All six response families are measured as
honest, non-vacuous violations (expectFailure: "violation") asserting
both acceptance and stream survival.
- BADCOMPARATOR, TEMPFAIL, MAXCONVERTMESSAGES, and MAXCONVERTPARTS
resp-codes DO parse (no throw) via text.code.ts's generic AtomTextCode
fallback for unrecognized resp-text-code atoms — kind is exposed
correctly. But a CONFIRMED defect recurs across all of them: AtomTextCode
calls splitSpaceSeparatedList with the function's default "(" / ")"
delimiters (not null, null, unlike AppendUIDTextCode/CopyUIDTextCode), so
a bare unparenthesized trailing argument (a charset word, a retry-sizing
number) never finds an opening paren and is silently dropped — contents
ends up [] instead of carrying the argument. Argument-less forms
([BADCOMPARATOR], [TEMPFAIL]) are clean REAL passes; argument-bearing
forms are measured as honest violations pinning the dropped value.
- URLMECH resp-code is a clean REAL pass (both the RESETKEY tagged-OK and
the SELECT/EXAMINE untagged-OK placements) since RFC4467-8-1 requires
only non-erroring kind acceptance, not argument fidelity.
- Every command-emission duty (LANGUAGE, COMPARATOR, CONVERT/UID CONVERT/
CONVERSIONS, RESETKEY/GENURLAUTH/URLFETCH, extended URLFETCH parameters)
self-actualizes as unimplemented — language()/convert()/genurlauth()/
urlfetch()/resetkey() all throw NotImplementedError, so each wire form
is pinned via a tightly-anchored scripted-server matcher for when a
driver surface exists.
Distribution across the three files (72 language + 76 convert/urlauth-ish
test instances, both profiles): a handful of clean REAL passes (URLMECH
x2, BADCOMPARATOR/TEMPFAIL no-argument forms), a set of REAL honest
violations (the six untagged-response families, the argument-drop
defects), and the remainder self-actualizing unimplemented — no vacuous
passes anywhere.
Catalog concerns surfaced (informational only, no catalog edits made):
none beyond what's already documented in the RFC5259/RFC4467 extraction
notes (CONVERT's near-zero real-world deployment, URLAUTH's heavy
server-side key-management skew).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 6 Task 7 (G2) — remaining SASL mechanism family: SCRAM-SHA-1/-256
(RFC 5802 + RFC 7677), ANONYMOUS (RFC 4505), and EXTERNAL (RFC 4422
Appendix A). All specs self-actualize as 'unimplemented' since
driver.authenticate() throws NotImplementedError today; every matcher
independently recomputes the expected wire bytes so the same assertions
become genuine once AUTHENTICATE lands.
test/compliance/specs/ext/sasl-scram-5802-7677.test.ts
20 tests (10 x 2 profiles). Cites: RFC5802-5-1, -5-2, -5-3, -5.1-2,
-5.1-4, -5.1-6, -5.1-7, -5.1-8, -5.1-9, -5.1-10, -5.1-12, -5.1-13,
-6-1, -6-3, -7-1, -7-2, -7-3, -3-1; RFC7677-3-1. Skipped (untestable/
out-of-scope, see catalog notes): RFC5802-5.1-1 (authzid escaping,
not exercised), -5.1-3/-3-2 (SASLprep, internal-decision), -5.1-5/
-15/-16 (extension attributes, none exercised), -5.1-11 (cbind-data
content, needs real TLS channel binding), -5.1-14 (no-final-message
failure shape), -6-2/-6.1-1/-6.1-2 (channel-binding preconditions
unmet), RFC7677-4-1 (-PLUS session-hash, channel binding unmet).
SCRAM-SHA-1 derivation (RFC 5802 §5 worked example, user/pencil,
independently recomputed via Node crypto -- PBKDF2-HMAC-SHA-1 for
Hi(), HMAC-SHA-1 for ClientKey/ServerKey/ClientSignature/
ServerSignature, SHA-1 for StoredKey):
c-nonce="fyko+d2lbbFgONRv9qkxdawL" s-nonce="3rfcNHYJY1ZVvWVs7j"
salt=base64("QSXCR+Q6sek8bf92") iterations=4096
client-first-message = "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"
server-first-message = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"
client-final-message = "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="
recomputed p= == "v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=" (matches RFC verbatim)
recomputed v= (ServerSignature) == "rmF9pqV8S7suAoZWja4dJRkFsKQ=" (matches RFC verbatim)
SCRAM-SHA-256 derivation (RFC 7677 §3 worked example, user/pencil,
same formula with SHA-256 substituted for HMAC()/H()):
c-nonce="rOprNGfwEbeRWgbNEkqO" s-nonce="%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0"
salt=base64("W22ZaJ0SNY7soEsUEjb6gQ==") iterations=4096
client-first-message = "n,,n=user,r=rOprNGfwEbeRWgbNEkqO"
server-first-message = "r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
recomputed p= == "dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=" (matches RFC verbatim)
recomputed v= (ServerSignature) == "6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=" (matches RFC verbatim)
Username escaping (RFC5802-5.1-4): escape(",")->"=2C", escape("=")->
"=3D", e.g. escape("a=b,c") = "a=3Db=2Cc".
Both vectors were independently recomputed with a Node scratch script
(crypto.pbkdf2Sync + createHmac + createHash, manual XOR of Buffers)
and matched each RFC's own published client-first-message, server-
first-message, p=, and v= constants byte-for-byte before being pasted
into the spec file's header comment.
Matcher-rigor: constructed and confirmed rejection of -- wrong
gs2-cbind-flag ('y'/'p=' when 'n' expected), unescaped comma/equals
in the username, a nonce containing ',', reversed attribute order
(r= before n=, or p= before r=), non-base64 'p=' proof, spurious
cbind-data appended to the gs2-header for a non-channel-binding
flag, a wrong/truncated echoed nonce, and a missing gs2-header in
'c='. Dedicated legs: nonce-uniqueness across two exchanges
(RFC5802-5.1-7), nonce-echo-mismatch abort via '*' (RFC5802-5.1-8),
ServerSignature-mismatch rejection despite a tagged OK
(RFC5802-5-3/-5.1-13), and 'e=' server-error acceptance.
test/compliance/specs/ext/sasl-anonymous-4505.test.ts
8 tests (4 x 2 profiles). Cites: RFC4505-2-1, -2-2, -2-3, -2-5, -2-6,
-3-1, -3-4. Skipped (untestable, out-of-band/internal-decision, see
catalog notes): RFC4505-2-4/-5-1 (permission-before-sending --
out-of-band user consent), -3-2/-3-3 (no-mapping/no-normalization --
internal-decision), -3-5 (bidirectional check -- internal-decision).
Base64 derivation: email "fred@example.com" -> ZnJlZEBleGFtcGxlLmNvbQ==;
RFC 4505 Section 4's own worked-example token "tim" -> dGlt; empty
trace -> "" (zero-length, accepted as "=" or a bare empty line).
Matcher-rigor: rejects a token containing '@' (violates the
email-or-non-'@'-token dichotomy), a token exceeding 255 Unicode
characters, non-base64 wire bytes, and a StringPrep-prohibited
ASCII control character (U+0001) surviving verbatim in the prepared
message.
test/compliance/specs/ext/sasl-external-4422a.test.ts
8 tests (4 x 2 profiles). Cites: RFC4422-A.1-1, -A.1-2, -A.1-3 (all
three Appendix A entries; none skipped).
Base64 derivation: authzid "userA" -> dXNlckE=; empty authzid -> ""
(zero-length initial response).
Matcher-rigor: rejects a non-empty response when an empty authzid
was requested (and vice versa), a response containing a NUL byte,
invalid UTF-8, and a second unsolicited AUTHENTICATE data line after
the single initial-response/challenge-response pair (A.1-3's
exactly-one-round-trip rule). Both client-first sequences A.1-1
permits (inline SASL-IR vs. bare AUTHENTICATE + server empty
challenge) are scripted as separate legs.
yarn test:compliance: problems: [] (clean). yarn test: only the
pre-existing test/unit/newline.transform.test.ts failure remains
(229 passed, 1 failed, unchanged baseline).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes all 21 untested-testable catalog ids flagged by a Phase 6 spec review,
restoring the compliance suite's zero-untested-testable invariant. Builds on
the review's 3 uncommitted header-comment fixes (sasl-scram-5802-7677,
referrals-2221-2193, inprogress-9585 — gap disclosures, kept as-is).
Coverage added per file:
- sasl-scram-5802-7677.test.ts (RFC5802/RFC7677): RFC5802-3-2, -5.1-1,
-5.1-3, -5.1-5, -5.1-11, -5.1-14, -5.1-15, -5.1-16, -6-2, -6.1-1, -6.1-2,
RFC7677-4-1. All self-actualizing (authenticate() throws
NotImplementedError) with tight wire matchers scripting the exact SCRAM
exchange fragment each duty binds (authzid escaping, SASLprep-or-refuse,
m= rejection, cbind-data content, no-final-message-on-failure, extension
handling, 'p' flag selection, tls-unique default, -PLUS session-hash
requirement) so a wrong future implementation would fail these once
AUTHENTICATE lands.
- referrals-2221-2193.test.ts (RFC2221/RFC2193): RFC2221-3-1
(self-actualizing — no referral-following surface exists; LOGIN throws
NotImplementedError before an 11-hop chain could even be scripted) and
RFC2193-3-1 (REAL pass — a non-IMAP-scheme URL in a REFERRAL resp-code
still surfaces with kind === "REFERRAL" and the session continues
normally, confirming the client is "prepared for" an arbitrary URL type).
- appendlimit-7889.test.ts (RFC7889): RFC7889-2-1 is a REAL pass — probed
and confirmed that CapabilityList.add() (src/parser/structure/capability.ts)
stores each CAPABILITY token verbatim, uppercased, as its own Map key, so
driver.hasCapability("APPENDLIMIT=1234") and
driver.hasCapability("APPENDLIMIT") are genuinely distinguishable today —
this closes a stale "no dedicated capability-parsing surface" claim in the
file's old header. RFC7889-3.2-2 and RFC7889-4-2 are self-actualizing
(status()/list()/append() all throw NotImplementedError).
- listmyrights-8440.test.ts (RFC8440): RFC8440-6-1 (self-actualizing —
list() throws NotImplementedError; scripts a scoped-vs-wildcard LIST
RETURN (MYRIGHTS) pattern matcher).
- preview-8970.test.ts (RFC8970): RFC8970-3.3-1 and RFC8970-4.2-1
(self-actualizing — fetch()/uidFetch() throw NotImplementedError
unconditionally; script the UTF-8/no-content-transfer-decoding duty and
the re-request-cadence duty respectively).
- inprogress-9585.test.ts (RFC9585): RFC9585-5-1 is a REAL, non-vacuous
VIOLATION — empirically probed (throwaway harness script, since removed)
that src/parser/structure/text.code.ts's match() keys its resp-text-code
kind off the raw, un-case-folded lexer token value with no normalization
step. A scripted lower-case '* OK [inprogress ("A001" 175 NIL)]' produces
a parsed serverStatus event with content.text.code.kind === "inprogress"
(verbatim lower-case), not "INPROGRESS" — a genuine, wire-observable
failure to accept the resp-text-code case-insensitively, distinct from
RFC9585-4-1's canonical-case pass.
Verification: yarn test:compliance run to completion; reconciliation
report confirms untested-testable: 0 and problems: [] (both required
invariants restored). No catalog entries were re-tagged — every one of the
21 ids proved genuinely testable once probed/scripted correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote all Phase 6 tokens from PENDING to live cataloged registry entries: the 13 scope tokens (LANGUAGE, I18NLEVEL=1/2, CONVERT, URLAUTH, URLAUTH=BINARY, CHILDREN, AUTH=SCRAM-SHA-1, AUTH=SCRAM-SHA-256, AUTH=ANONYMOUS, AUTH=EXTERNAL, LOGIN-REFERRALS, MAILBOX-REFERRALS, X-GM-EXT-1), the 5 reconciliation-delta tokens (APPENDLIMIT, STATUS=SIZE, LIST-MYRIGHTS, PREVIEW, INPROGRESS), and UTF8=ONLY (source RFC6855 w/ the RFC 9755 maintenance note), each with measured-outcome notes. UPGRADE specs/meta/registry-coverage.test.ts from internal-consistency to the LIVE IANA cross-check: every token in the committed iana-snapshot.ts (78 tokens) must have a coverage entry; every cataloged source must resolve to a module that actually carries requirements (not an empty skeleton); every non-cataloged entry must carry an explanatory note; no duplicate capability. Case-insensitive token match (IMAP capabilities are case-insensitive; IANA renders IMAP4REV1 vs the conventional IMAP4rev1). Meta-tests 15/15 green. The registry checklist is now COMPLETE - every IANA IMAP-capabilities token is reconciled to an auditable status. Suite state: problems=0, untested-testable=0, 960 requirements, 61 sources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Append the closing untestability delta: 32 new untestable entries across 6 existing themes (internal-decision +18 dominant, out-of-band +6), no new theme. Records the FINAL combined totals - 249 untestable across the 960-requirement catalog, taxonomy final at 11 themes (Phases 4-6 added 116 untestable entries with zero new themes, confirming completeness). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IMAP Spec-Compliance — Progress ReportA black-box specification-compliance test suite for the Where the client stands960 requirements across 61 sources, both profiles (1,581 requirement×profile cells):
Of the duties the client actually has a surface for today ( Coverage breakdown (960 requirements / 61 sources)Core protocol — 288
Connection & security — 169
Mailbox, listing & metadata — 136
Search, sort, sync & events — 180
i18n, misc & vendor — 187
Beyond the cataloged sources, every remaining IANA capability token is reconciled with an explicit status ( |
…ator) Structures the four baseline review roles as Claude Code subagents in .claude/agents/ so they're directly invocable via the Agent tool, with a shared findings template as the single source of truth for output format. Lens-specific agents are left for a future iteration; review-runner spins up ad-hoc lens sub-agents until predefined ones exist.
context-gathering/review-runner get sonnet (comprehension and coordination work); review-validator/review-orchestrator get opus, since they're the trust-critical gatekeeper and the final human-facing output respectively. Pinning also keeps review rigor consistent regardless of what model the invoking session happens to be using.
Adds 11 predefined review lenses (correctness, security, test-quality, error-handling, api-compatibility, performance, maintainability, state, fidelity, dependency, spec-compliance), each structured as categories of things to check with grounding examples rather than a numbered procedure — lenses are a taxonomy applied to code, not a workflow, so the review-runner and review-orchestrator steps are the only agents in this set that use sequential steps. review-runner now selects 2-5 of these predefined lenses per group instead of defining lenses ad hoc, with a fallback to a custom lens for risk areas none of them cover. review-orchestrator's rules updated to match.
- All 11 lenses: findings are no longer implicitly limited to confirmed bugs. Category output guidance loosened from a narrow 2-3 value mapping per lens to open, example-only lists that include Assumption, Uncertainty, and Improvement alongside Bug — strict category lists were constraining what lenses would surface. - api-compatibility-lens: added a Developer Experience category (API shape value, ergonomics, clarity to consumers); internal-only code worth exposing is now explicitly in scope instead of excluded; Category list now includes Versioning and Documentation as first-class options. - maintainability-lens: Abstraction Fit now explicitly hunts for over-engineering (a common agentic-coding failure mode) with a grounding example; Readability now covers comment verbosity and redundant comments, not just staleness. - test-quality-lens: added a Test Necessity category — overtesting (fragile, low-value tests) is flagged as a real cost, not just gaps. - review-runner: lens selection changed from a 2-5 cap to "at least 2, no fixed upper bound" so large/high-risk groups aren't under-scoped.
Review reliability note (PR #13 review, M1): orchestration runs stalled because sub-agents could be spawned in the background. Both spawning agents (review-orchestrator, review-runner) now explicitly require run_in_background: false on every Agent call, with parallelism achieved by batching synchronous calls in a single message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M17/H1 from the PR #13 review: the 'if (i > 0)' guard meant a completed context at index 0 was never spliced out, so a completed isolated command pinned its context and the next context's commands never ran (run() early-returns on the still-'running' stale context). The queue also never emitted 'idle'. Guard is now 'i > -1', with a regression test covering the isolated-command-then-followup path and idle emission, plus assertions that isIsolated actually propagates through CommandQueue.add(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses H2 from the PR #13 review: ComplianceReporter rewrote compliance.json/COMPLIANCE.md on every invocation, so a filtered 'vitest run <file>' (or an interrupted run) clobbered the report — marking every requirement that simply didn't run as 'untested'. The reporter now walks specs/**/*.test.ts and only writes when every spec file was part of the run and it wasn't interrupted; it also refuses to write when a complete run yields zero compliance-annotated records (broken meta plumbing would otherwise report everything untested). Both guards print an explicit diagnostic instead of silently overwriting, and are covered by new reporter tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M8 from the PR #13 review: drainLines() only ran the bare-LF check when the buffer contained no CRLF anywhere, so a bare-LF line arriving in the same chunk as a later valid CRLF line was silently absorbed into that line's segment. The check now flags any bare LF that appears before the first CRLF, and gains direct harness tests (previously disabling the check left every meta-test green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M9 and M11 from the PR #13 review: - selectExchange (rev1) defaulted to exists=0 with UNSEEN 1, scripting '* 0 EXISTS' + '* OK [UNSEEN 1]' — a jointly-invalid state no real server sends. UNSEEN is now omitted for an empty mailbox unless the caller explicitly sets it. - The BODY[1.MIME] FETCH fixtures declared {42} for a 44-byte literal (the trailing CRLF was uncounted); a correct client would read 42 octets and desync. Both occurrences now declare {44}. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M5 from the PR #13 review: the README claimed the expired-cert fixture was 'intentionally absent until Phase 3', but the same directory ships expired-cert.pem/expired-key.pem and generate.sh has the expired block. Also invoke the script as 'sh generate.sh' since it is committed non-executable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M7 from the PR #13 review: 'tsc -p test/compliance --noEmit' produced 50+ errors (import.meta under the inherited commonjs module setting, unresolvable node:* specifiers under @types/node@12, and third-party .d.ts written for newer TypeScript), so the scoped tsconfig's only purpose — editor/CI type-checking — was non-functional. - module es2020 + moduleResolution node (import.meta support without flipping useDefineForClassFields for the pulled-in src/ files) - skipLibCheck for the vitest/vite/rollup .d.ts written for newer TS - local shim declaring node:* specifiers until @types/node is bumped - serverInfo() now returns ReadonlyMap, matching Session's actual type - new 'yarn typecheck:compliance' script runs the check Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M10 from the PR #13 review: connect()/connectLow() returned the client promise with no independent timeout or teardown, so a hung TLS connect (the client never settles on rejected certificate identity) died at vitest's generic 'Test timed out' with no diagnosis, and end() could hang on the half-open client. Both connect paths now race the client promise against a backstop timer (client timeout + 1s so the client's own timeout gets a head start). On expiry the driver rejects with a diagnosable message and best-effort tears the client down. Covered by a new driver TLS test that asserts identity rejection produces a rejection, never a hang. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
Addresses M12/M13/M21 and the M3 count assertion from the PR #13 review — properties the suite relied on but never exercised: - command(): multi-token verbs now require exactly one SP between tokens (a doubled SP was collapsed, hiding UID-prefixed framing bugs) and preserve args spacing instead of re-joining tokens; both behaviors plus exact-string args matching gain direct tests. - Transcript.clientLines() — the primitive behind every credential- redaction assertion — gains its own harness tests. - The LITERAL+ test now asserts no continuation was sent; the chunked-send test now asserts chunking actually happened. - New reporter guard: on a complete run, any testable-but-untested requirement becomes an explicit problems[] entry, enforcing the 'untested-testable = 0' headline invariant. - New meta-test: every catalog module file on disk must be registered in allCatalogModules exactly once (an unwired file was silently unscored), and sources must be unique. - New meta-test pinning the IANA reconciliation tallies (92 coverage rows = 73 cataloged + 5 + 5 + 9; 78 snapshot tokens) so documented numbers can't drift from code. - compliance-test.ts and acceptance-table.ts now share one registerCompliance() core (title construction, meta assignment, classify-and-rethrow), and the classification wiring is unit-tested via runComplianceBody. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYD9rsp96v3hR7kSkCw5yq
IMAP client spec-compliance test suite
# Conflicts: # yarn.lock
…onfig - Switch package manager from yarn to npm (remove yarn.lock, add package-lock.json, update CI to npm ci) - Migrate the full test suite off jest onto vitest: new root vitest.config.ts covers unit/integration/e2e, jest.fn/mock/MockedClass/ MockedFunction usages converted to vitest's vi equivalents, jest/ts-jest/ @types/jest removed - Bump TypeScript to ^6.0.3 (not 7.x - that hard-crashes typescript-eslint 8.x today) and @types/node to ^22.9.4; fix the ~180 stricter compile errors this surfaced without changing runtime behavior, including a few genuine latent bugs found along the way (a copy-paste index bug in parser/encoding.ts, an unguarded socket access race in connection.ts, an uninitialized FlagList.hasWildcard) - Raise the Node engines floor to >=22 (18/20 are both past EOL and vitest 4.x/ESLint 10 require >=20 anyway); update CI's Node matrix to 22.x/24.x - Migrate ESLint to flat config (eslint.config.js) with ESLint 10 + the unified typescript-eslint 8 package, replacing .eslintrc.js/.eslintignore; preserve the old warn-level severity for no-explicit-any/no-unused-vars since the v8 recommended config promotes them to errors by default - Delete the dead pre-TypeScript test/test.js + test-connection-*.js files (they called a since-removed default Imap export); the specific historical-bug scenarios they encoded are written up in docs/superpowers/specs/2026-07-12-legacy-regression-scenarios-to-reverify.md for re-verification once FETCH/IDLE are (re)implemented - Add prettier as a real, runnable devDependency (config already existed but the package was never installed); wire the compliance suite into CI (it existed but was never run there); fix vitest's reportOnFailure default so coverage still gets reported when a test fails, matching jest's old behavior
Proposes the three-layer public interface (ImapClient/MailboxSession over typed command classes over Connection/parser), the developer experience it targets, and works backwards to the implementation subsystems (CommandWriter, response router, queue state machine, TLS identity, parser hardening, SASL framework) and a six-milestone phasing plan driven by the compliance suite, grounded in today's measured report (960 requirements, ~23% MUST score, 73 distinct violations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
All recommended options adopted: ImapClient replaces Session outright with no alias (unreleased code, version bumps to 1.0 at M6), UID-first message operations, extension facets, Connection/response classes public at 1.0 with lexer internals package-private, satisfied-by-mechanism adjudication for product-decision SHOULDs, and suite-annotation churn as milestone DoD. Status updated to Accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
The spec (docs/superpowers/specs/2026-07-12-modern-api-spec.md) pins down the implementable contract: package exports and target source layout, the full ImapClient/MailboxSession/facet signatures, the client state machine transition table, the connect() ritual, error hierarchy, SequenceSet/ search-criteria/fetch data types, typed response-code registry, queue pipelining modes and the literal gate, the Command/CommandWriter/ ResponseCollector Layer-2 contracts, response-router claim ordering, the SASL framework and mechanism-selection algorithm, the single TLS policy module, the parser-hardening contract, and a table of 13 compliance invariants mapped to catalog requirement IDs. The plan (docs/superpowers/plans/2026-07-12-modern-api-implementation-plan.md) sequences it: ground rules (compliance ratchet, per-task definition of done, driver wiring, no parallel wire-writers), M0 and M1 at task granularity with files and which catalog requirements each task flips, M2-M6 scoped with exit criteria, the dependency graph, and standing risks. Also corrects two RFC number mixups against the catalog (RFC 9585 is INPROGRESS; UIDONLY is RFC 9586) and schedules catalog extractions for the two registry entries the suite currently defers (RFC 3691 UNSELECT in M2, RFC 9586 UIDONLY in M5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
- Rename the 'enable' config option to 'extensions' across proposal, spec,
and plan: 'enable' reads as an on/off switch for the client itself rather
than a list of server extensions to ENABLE.
- Add spec §5.6 'Closed vocabularies': RFC-closed value sets become
string-literal unions (SpecialUse, SystemFlag/Flag, SortKey — previously
referenced but undefined — and ThreadAlgorithm), with a two-grade rule:
strict unions for client-sent values, Known | (string & {}) for
server-sent positions so unknown wire values stay type-safe data.
Applied to create(), MailboxInfo, the flag methods, and thread().
- Drop the ~800-line PR size cap from the plan's ground rules: PRs are
sized by coherent unit of work; automated review tooling absorbs large
diffs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E67Ejgvhi7ZnBnacKcuqGU
Goals
Have clean and easy to understand API that utilizes modern JS functionality. Under the hood, the code should also better differentiate between an IMAP connection and an IMAP session, and utilize the rewritten parser engine.
Work
async/await/promisemethods