Commit Graph

5 Commits

Author SHA1 Message Date
bytecii 73f5bd7c21 chore: cleanup items 31+32 — nested-function rule, layout, CI audit, ls facets
Closes the two remaining small items of Block E. Both were largely
recon: much of what they filed had already been fixed, and the part
that had not turned up a real GNU divergence.

Item 31 (T3-7), rule/doc reconciliation
---------------------------------------

The `object`-parameter half is already done: #825's mypy inversion took
the package from the filed 28 down to 7, of which 3 are docstring prose
and the remaining 4 are protocol methods (`__contains__`, `pop`) whose
signatures typeshed dictates, all already listed in the no-object gate.
`utils/errors.py` reads `str | PathSpec` today, and `builders/sed.py` no
longer holds flag values at all -- item 22 moved that to the generic.

That left the nested-function rule, which the plan asked to decide
before gating. Measured: 39 nested defs, and **every one of them closes
over its enclosing scope** -- 38 by free variable, and `sed_helper._repl`
by binding through parameter defaults, the loop-variable idiom, with a
comment saying so. A flat "do not nest" is a rule the architecture
cannot keep (op factories, provision builders, the read-through cache,
every decorator's wrapper), so gating it as written would have meant 39
standing violations.

So the rule now says what it was reaching for: a nested def must capture
the scope around it, or it belongs at module level. That is mechanical
and enforceable -- `tests/test_nested_functions_are_closures.py` reads
free variables from `symtable` and parameter defaults from the AST. It
passes on the tree as-is and fires on a helper that reads only its own
arguments.

Layout: the eight test-only TS directories are flattened
(`ram/{cat,cut,grep,head,ls,tail,wc}/`, `ssh/ls/`), `awk_helper.ts` moves
up beside its nine siblings, and the `provision.ts` / `_provision.ts`
split is settled on Python's convention -- `_provision` for a backend's
own, bare `provision` for the shared ones (cli, generic_bind). That last
one was not cosmetic: Python already had `_provision.py` for github,
gmail, redis and email, so renaming **closed 4 real parity divergences**
(layout baseline 296 -> 292). The `findEval`/`findParse` line was
already done in #827.

Item 32 (T3-8), test/CI tidiness
--------------------------------

The two asymmetric `ls` conformance matrices are raised to symmetry, and
they pass: python `[ram,disk,redis]` vs typescript `[ram]` was stale
caution, not a divergence. Both runners now reject an asymmetric matrix
at load time unless the case carries a `divergence` key explaining why --
a case is a parity claim, and narrowing one side reads as coverage while
the side that still lists the backend goes green. Loading the corpus
under the new assertion proves no other case was asymmetric. The README
documented the override as "not yet needed"; it exists now.

Adds the `ts-audit` job, mirroring `test_python.yml`'s exactly (same
`continue-on-error`, same out-of-gate placement). Nothing ran `pnpm
audit` for the TS tree while `typescript/package.json` carried 27
hand-written CVE overrides that only a person remembering to check kept
current.

Integ facets for `ls`: `-A`, `-d`, `-r`, `-S`, `-h`, all pinned against
GNU coreutils 9.7 in docker. `-S` uses regular files only, because GNU
sorts a directory by its inode size while mirage counts it as 0 -- a
divergence CLAUDE.md documents deliberately.

The `-h` facet found a real bug
-------------------------------

`ls -h` disagreed with GNU three ways, and because the flag had zero
cross-backend coverage nothing caught it. GNU prints a count below one
unit with no suffix (`24`); mirage printed `24B`. GNU rounds *up* to the
precision shown (1025 bytes is `1.1K`); mirage gave `1.0K`. GNU drops
the decimal once the value reaches ten (`10K`); mirage gave `10.0K`.

One shared engine replaces both formatters in both languages -- GNU runs
`-h` and `-H` through one `human_readable`, and so do we now. Rounding
up can carry past the base (1048575 ceils to 1024K, which GNU shows as
`1.0M`), so the unit is re-chosen after rounding rather than once up
front. 21 GNU-read points are pinned as a table in each language.

Blast radius, all re-pinned: five integ cases that had recorded the `B`
suffix (du, discord, langfuse, email, gmail), and the du fan-out tests.
Those last ones needed care rather than a new number: they exist to
prove the total is humanized once instead of twice, and their 1500+1500
stops discriminating under correct rounding, since 3000 bytes and two
`1.5K` readings both render `3.0K`. They now use 1025+1025, where
single-rounding gives `2.1K` and double-rounding would give `2.2K`.

Verified: pre-commit clean, integ 8054/0 on ram+disk+redis in both
languages, conformance green in both, and the layout, spec, barrel,
docs, case-target and PathSpec gates all pass.

Not covered, still open on item 32: integ facets for `ls -t` (mtime
order is not stable across backends without a seeded fixture), mktemp,
gzip, the checksum `--check` companions, `df -H/-k/-a`, and `cmp
-n/-b/-i`; and the T2-7 helper unit-suite mirroring. Item 31 leaves the
provision *presence* sets unreconciled (Python has lancedb/qdrant, TS has
trello) -- that is a question about which backends should provision at
all, not about naming.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:57:08 -07:00
Zecheng Zhang d9ba2d2460 fix(himalaya): file the sender's own copy of a sent message (#761)
* fix(himalaya): file the sender's own copy of a sent message

`--send` opened one SMTP conversation and stopped, so mail was delivered
but the sender's Sent mailbox stayed empty. A Sent folder is not produced
by sending: SMTP keeps no record of itself, and the copy is a second,
separate IMAP APPEND that every mail client makes on its own. Four call
sites, not one: `message send` plus the route compose, reply and forward
share.

Upstream v2.0.0 spells that APPEND as `--save <MAILBOX>` on all four
verbs, having replaced 1.x's automatic `message.send.save-copy` config.
Both are here, because they answer different needs:

- `--save <MAILBOX>` is v2 parity. On its own, with no `--send`, it files
  the message without sending it, which is how a draft is written.
- `save_copy` is an account-level default, on, and is mirage's own
  divergence: an agent that never learned the flag still leaves the
  record a human sender would. `sent_folder` pins the mailbox.

The mailbox is asked for rather than guessed. A server implementing RFC
6154 tags one mailbox \Sent in its LIST reply, which is `[Gmail]/Sent
Mail` on Gmail and `Sent Items` on Exchange; failing that the configured
name, failing that `Sent`. This is ahead of upstream, whose v2 wizard
notes IMAP pins the reserved INBOX alone while it waits on LIST RETURN
(SPECIAL-USE) in io-imap.

Failure splits on whether a send already happened. A copy that fails
after a successful send is a warning on stderr with exit 0, because the
message is already gone and a non-zero exit invites a retry that sends it
twice. A `--save` that sends nothing fails loudly, because nothing
happened yet and retrying is safe. Upstream propagates in both cases.

Two bugs found on the way:

- aioimaplib joins command arguments with spaces verbatim and quotes
  nothing, so an unquoted mailbox holding a space arrives as two
  arguments and the server reads only the first word. That breaks on
  exactly the two providers above. `quote_mailbox` now covers the APPEND
  and the pre-existing `select_folder`, which had the same latent bug.
  imapflow needs none of it, building commands from typed attributes.
- The TypeScript test CONFIG was an untyped literal, so a new required
  field did not fail tsc and those tests passed vacuously through the
  `!saveCopy` early return. It is annotated now, and a global beforeEach
  stubs the accessor so no --send test reaches for a real connection.

GreenMail hands a new account nothing but an INBOX, where a real provider
ships a sent mailbox already made, so both integ seeders create one.

Verified against a live GreenMail on both hosts: the email and
cli-himalaya targets pass 86 and 74 cases including six new ones, and
compose, reply, forward and send each land in Sent with \Seen. Python
suite and TypeScript node suite green, pre-commit clean.

* fix(email): parse a LIST mailbox as an astring, not as a quoted string

Codex review on #761. A LIST mailbox is an astring, so a name needing
no quoting may legally arrive bare, and some servers emit it that way.
Splitting the line on quotes then reads the hierarchy delimiter as the
name: `(\HasNoChildren \Sent) "/" Sent` answered `/`.

That splitter predates this branch and already mis-listed every folder
on such a server, but this branch is what made it consequential: the
name now feeds an APPEND, so a sent copy would have been filed into a
mailbox called `/`.

The three tokens are walked in order instead. The atom form and the NIL
delimiter both parse, and a quoted name is now unescaped rather than cut
at the first inner quote, which the splitter also got wrong.

Verified against a live GreenMail on both hosts, whose own wire format
is the empty-attribute `() "." "INBOX"` shape.
2026-08-11 22:28:48 -07:00
Zecheng Zhang db807f304b fix(find): classify walked entries through stat, drop the is_dir_name heuristics (#719)
* fix(find): classify walked entries through stat, drop the is_dir_name heuristics

* fix(test): satisfy noUncheckedIndexedAccess in the gmail find fixture

* fix(dropbox): walk the native find over a scratch index

* test(integ): pin find -type f over attachments and uploads

* test(integ): find -type f matches a pdf upload too

* ci: retrigger after the Actions outage

* fix(email): route the TS find through the generic walk so every flag applies

* chore(deps): bump h2 to 4.4.1 for GHSA-6hr6-w5qg-qmwg
2026-08-06 18:27:42 -07:00
Zecheng Zhang 619be25d32 feat(himalaya): align the CLI with the upstream pimalaya grammar (#709)
* feat(himalaya): align the CLI with the upstream pimalaya grammar

- mailbox is -m/--mailbox, message id is a positional operand
- envelope search with upstream's query DSL, replacing list filter flags
- message compose writes RFC 5322 to stdout unless --send; message send
  takes raw MIME, so the two chain through a pipe
- upstream aliases: ls, sr, write, new, fwd

core/email/send.* is deleted: the email mount is read-only, so the
composer and the SMTP transport moved into the himalaya package.

Fixes three node-side bugs found while testing:
- sendMail({raw}) needs an explicit envelope; Bcc is stripped by hand
- parseSearchCriteria silently dropped unknown keys, so NOT SEEN matched
  every message
- parseImapDate rolled a bad month or day into a wrong date

* fix(himalaya): satisfy the CI eslint rules and stop the yapf/isort import flip

* fix(himalaya): search the Date header and bound envelope pages

Date conditions emit SENTON/SENTBEFORE/SENTSINCE rather than
ON/BEFORE/SINCE, which match the mailbox internal date, and the missing
upstream `before` condition is now parsed.

Envelope listing asked IMAP for every matching uid and fetched headers
for all of them before slicing one page. It now fetches the newest
page * page_size, capped by the account's max_messages.

* test(himalaya): cover every flag in integ, and fix what it found

Grows the himalaya integ suite from 38 to 64 cases: every search
condition, sorter, flag and alias, the composer flags, both quoting
options, stdin bodies, the send operand form, unknown flags and a
missing mailbox.

Three bugs it turned up, all in the email client:

- Five IMAP selects ignored their response, so a missing mailbox left
  the session in AUTH and the next command complained about that
  instead ('command SEARCH illegal in state AUTH' in python, imapflow's
  bare 'Command failed' in typescript). Both now say no such mailbox.
- A refused SEARCH returned an empty uid list, so criteria the server
  cannot answer read as 'matched nothing'. It now raises.
- Found because a mutation emitting a malformed date stayed green.
2026-08-04 22:41:23 -07:00
bytecii 5ba62a8f0d refactor(integ): nest JSON test cases as category/name/facet (#602)
Restructure the JSON-runner integ corpus from a flat
`category/command.json` layout into a three-level
`category/name/facet.json` tree, so cases are navigable by the flag or
feature they exercise (e.g. `unix/grep/n.json`, `unix/find/name.json`,
`unix/meta/chmod.json`, `resources/dify/search.json`).

Faceting rules:
- unix command files split by the primary flag on the named command,
  with reserved `basic` / `error` / `operand` buckets, and populate the
  previously-dormant `flags` field on those cases (370 cases).
- feature files plus bash/crossmount/resources split by the case-id's
  distinguishing segment (`meta_chmod_*` -> chmod, `col_parquet_*` ->
  parquet).
- 9 high-cardinality flat files (provision, printf, gmail, ...) collapse
  to a single `basic.json` instead of exploding into 40+ singletons.
- uppercase-only flags get a `.upper` suffix so filenames stay unique on
  case-insensitive filesystems (`grep -C` -> `C.upper.json`).

Discovery now walks recursively: `rglob` in the Python harness and the
existing `walkFiles` helper in the TypeScript harness.

177 flat files -> 701 facet files, 1898 cases. The migration preserves
each case's `seq`, so the global execution order and every case body are
byte-identical to before (only the additive `flags` field differs);
verified 0 content mismatches vs the flat tree, unchanged id order, and
the Python runner green (1582/1582 on ram). Adds
runners/tools/refile_facets.py, the deterministic, re-runnable migration
tool (with a case-collision guard).

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:10:49 -07:00