main
13 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
003753c21a |
feat(python3): parse CPython's option table, and seed sys.path on pyodide (#743)
* fix(ids): stamp uuid7 from the clock, not from the previous id
uuid6's uuid7 orders ids inside one millisecond by bumping the
timestamp past the previous one. The borrowed value becomes the next
call's floor, so the error compounds: a burst of 5000 ids stamps every
later one 4991 ms into the future, and it stays there until the clock
catches up. These are workspace and session ids, used as time-ordered
database keys.
Mint the id here instead, with RFC 9562's dedicated counter in rand_a:
48-bit timestamp, 12-bit intra-millisecond counter, 62 random bits. The
timestamp is only ever a real clock reading, and an exhausted
millisecond waits for the next rather than borrowing.
Drops the uuid6 dependency, which nothing else used. TypeScript already
had this right, via the uuid package's counter.
* feat(python3): parse CPython's option table, and seed sys.path on pyodide
python3's spec carried one option, -c, and a free-text rest, so every
other switch fell through to the operand slot: 'python3 -u s.py' ran
'/-u' as the script and failed, 'python3 -zz -c ...' exited 0 with the
flag swallowed, and -V and -h were read as paths.
Two spec knobs fix the class rather than the command. stop_at_operand
turns off the parser's dash-operand leniency and ends option parsing at
the first operand, which is CPython's own rule; ends_options marks the
options that carry a program (-c, -m), so the words after them are the
program's argv rather than python3's. Both mirror in TypeScript.
On top of that: -m runs through runpy, with a find_spec probe so a
missing module is CPython's one line instead of a traceback; the four
source doors each report the argv[0] CPython gives them; and the
interpreter-init switches ride in RunArgs.flags, honored where the
engine can and reported on stderr where it cannot.
argv[0] needed one more piece. The local and wasi tiers hand CPython
the program through -c, so it hardcoded argv[0] to '-c' and named every
frame '<string>'. bootstrap() re-compiles under the real name, which
fixes argv[0] and the traceback filename together.
Pyodide gains sysPath, which glob-expands and prepends mount paths once
the mounts are in place, so a vendored wheel imports without the
sys.path.append incantation. A glob that matches nothing is reported on
the run's stderr; the seed cannot report it itself, since it runs
before stderr is captured. packages, packageBaseUrl and lockFileURL
reach a prebuilt distribution, and the wrapper honors -O, -B, -W, -X,
-E and -I.
Verified against CPython 3.13.7: all 14 probed lines match on stdout,
stderr and exit code.
* refactor(spec): borrow argparse's REMAINDER instead of two bespoke parser knobs
python3's line needs opposite treatment of an unknown dash word on
either side of the script: `-zz` before it is python3's usage error,
`--foo` after it is data the script must receive. The first pass added
CommandSpec.stop_at_operand and Option.ends_options to say that, which
put two per-command dialect fields on dataclasses every command shares.
The first half is not ours to invent: it is argparse's
nargs=argparse.REMAINDER, and POSIX's own option order (GNU's permuting
default is the extension, which `POSIXLY_CORRECT=1 ls a -1` shows).
argparse spells it on the positional slot, so it goes on Operand under
argparse's name, and CommandSpec is untouched.
The second half argparse cannot express at all: `add_argument('-c')`
beside nargs=REMAINDER answers `unrecognized arguments: -u`, and CPython
parses its own command line in C for that reason. So it stays out of the
grammar. POSIX already spells the handoff `--`, and the parser already
consumes one, so a table in workspace/route inserts it after -c/-m's
value, beside the other command-name-keyed line rules. The marker is
added unconditionally: CPython passes a typed `--` through as data
(`python3 -c p -- -u` gives the program ['-c','--','-u']), so the parser
eats exactly the one added here.
Net: Option and CommandSpec return to what they were, Operand gains one
borrowed field. CLAUDE.md now states the rule as a literal test, since a
CLISpec is a CommandSpec and cannot be used to escape it.
* docs: require POSIX and argparse, not either, for a shared grammar field
POSIX alone specifies an option whose argument is a program (sh -c, and
python3's own synopsis), so an either-or rule would license the Option
field the section exists to refuse.
* fix(python3): address review on init switches, and declare the four missing ones
- pyodide restores warnings.filters after a run, so -W error no longer
follows every later line on the warm interpreter
- -OOO saturates at compile()'s max of 2 rather than raising ValueError
- -E/-I stop deleting PYTHON* from os.environ: CPython's -E stops those
variables configuring startup, it does not hide them from the program
- pyodide reports -E/-I/-s/-S through unhonoredNotice instead of faking
them; unhonored() grew a honored subset, and now reports -OO too
- declare -b, -P, -x and --check-hash-based-pycs, which CPython accepts
and mirage was refusing. -x is answered by the source resolver, since
handing it to an engine that runs code via -c would honor it nowhere
- end_options_after_program walks a short cluster letter by letter, so
-uc 'p' -v hands -v to the program, and steps over long value options
* fix(integ): argv[0] for a payload is -c on every engine, monty included
Monty's DEFAULT_PROG placeholder only applied because nothing ever told
it a name. The python3 command now supplies CPython's own answer, so
these four policy-routing steps read -c. They still distinguish monty
from local, since argv is a monty global and CPython would NameError.
Also picks up the formatting of the new flags test: it was untracked
when pre-commit ran, and --all-files goes through git ls-files.
* fix(python3): do not rewrite a shadowed name, and stop over-claiming -W and -X
- the `--` handoff is skipped when a shell function shadows python3.
bash's rule gives the function the line, and it has no CPython option
table to read a marker with. `command python3` masks the function for
its inner run, so the rewrite applies there again. A CLI cannot reach
this at all: register_cli refuses a shell builtin's name.
- an invalid -W filter is reported as CPython reports it and the program
still runs, instead of _OptionError escaping the wrapper and killing a
line every other runtime completes.
- a known -X name is reported as unhonored, since populating
sys._xoptions is all a warm interpreter can do for one. An arbitrary
name stays silent, which is all CPython does with it either.
- -O's reach is written down: the payload is compiled at the requested
level, a module imported from sys.path is not, and sys.flags stays 0.
|
||
|
|
07bca1b503 |
fix(archive): tar and zip archive a directory, and -C re-bases the operands after it (#738)
* fix(archive): tar and zip archive a directory, and -C re-bases the operands after it tar -cf d.tar d exited 1 with "tar: <hostpath>/d: Is a directory": every operand went straight to read_bytes, with no isdir check and no recursion, so the most common tar invocation there is could not work. zip -r <dir> had the identical defect. Both now walk. Create is a two-phase pass in each: decide every member, then write. Both plans are built on one traversal, scan_operand / scanOperand (generic/archive/walk), which merges three sources no single one can see: the backend walk (find's walk_find, so an entry is classified through stat and never by name), the namespace's symlinks, and the mount table. It reports paths, never names, because naming is where the two formats part company; the two things they disagree about in the traversal itself are parameters, so tar passes recurse=True and dereference=-h while zip passes recurse=-r and dereference=not -y. A directory is its own member, so an empty one survives a round trip, and both extractors now mkdir for one. A symlink is a symlink member under tar (SYMTYPE, target in linkname) and under zip -y (mode 0120777). An unreadable operand is reported in virtual path space with the archiver's own wording, because the raw IsADirectoryError was leaking the host path behind a disk mount. tar -C was ignored: `tar -czf /work/out.tgz -C /work/check my_paper` failed as "paths span multiple mounts", the same defect class as unzip -p in #725, because the operand resolved against the session cwd and the router then saw a phantom mount span. -C is not a flag the command reads once, it is a chdir for the operands typed after it, so it is now declared in the spec (CommandSpec.operand_base) and resolved by the one component that walks the line positionally: the parser reports a base per word and the classifier resolves each operand against it. MountView is how a command sees mount boundaries, offered the way LinkView is (name a `mounts` parameter, nothing else). A traversal that renders lines gets this free from the executor's fan-out; one that emits a single binary object cannot, which is why the archivers read the table themselves. A descendant mount is not crossed: the mountpoint stays an entry and its contents are dropped with GNU's --one-file-system wording, since descending would archive by accident what MountRootPolicy now refuses on purpose for tar, zip and cp in a source slot. Semantics pinned against GNU tar 1.35 and Info-ZIP 3.0 on debian:stable-slim, including Info-ZIP's inverted defaults, its anchored -x, its silent leading-slash strip, and "Nothing to do!" exiting 12 with no archive written. * chore(spec): the dumps state what a command declares, not what it defaulted to Both generators emitted every field of every dataclass, so `truncate`, which declares one thing (`-s`/`--size` takes a string), spent 27 lines of spec body restating 21 defaults that read identically in all 93 files. `zip` was 198 lines for five flags, 11 of the 13 keys on each option being defaults. Anything equal to its default is now dropped on both sides. `type` survives even at its default, because what a token is is the first thing a reader looks for, and `"rest": {}` says less than `"rest": {"type": "path"}`. The defaults come from the dataclass fields in python and from a default-constructed instance in typescript, rather than from a table either side could let drift. The two must drop exactly the same keys, and the parity gate reports every command if they do not. This narrows that gate's blast radius rather than widening it. When `operand_base` was missing from gen-specs.ts, python emitted the key in all 93 files and typescript in none, so parity reported ~95 divergences to sift. The same bug now reports one, on tar, which is the only command that sets it. check_spec_parity's option-diff renderer keyed options by `o["long"] or o["short"]`, which raises once an option carries only the spelling it declared. 279 files, 22113 lines of restated defaults gone. * fix(archive): strip the mount prefix by scan, not by a backtracking regex childSpec measured the backend key with `.replace(/^\/+|\/+$/g, '')`. The trailing alternative backtracks on a run of slashes, which CodeQL flags as js/polynomial-redos, and the input is a resource path a mount supplies. utils/slash.ts already has stripSlash, which walks the two ends by charCode and cannot backtrack. Use it. slash.test.ts pins it against the regex it replaces. * fix(archive): the five symlink and mode bugs the codex review found All five reproduced against a live workspace first, then pinned against GNU tar 1.35 and Info-ZIP 3.0 on debian:stable-slim. A symlink operand never reached the planner as a link. tar and zip were absent from NO_FOLLOW_COMMANDS, so the router rewrote the operand through the link table and `tar -cf o.tar link` stored a regular file holding the target's bytes where GNU stores a symlink member of size 0. It also skipped the planner's cross-mount refusal, since by then there was no link left to refuse. Both archivers now lstat, and carry no DEREFERENCE_FLAGS entry on purpose: -h and -y are the planner's to read. Only the last -C was checked. `tar -cf m.tar -C missing x -C good y` reported `x: Cannot stat` and still wrote an archive holding y, where GNU chdirs at each -C and dies at the first it cannot enter. The option accumulates now and the planner walks the list, so the first bad one is fatal and no members are written. Two links to one target were called a loop, and a real loop was not caught at all. Both were the same mistake: an operand-wide `seen` set doing detection the namespace already does properly under a hop limit. Deleting it archives both names, as GNU and Info-ZIP do, and catching the CycleError that resolve raises turns a genuine cycle into one fatal problem per member with GNU's "Too many levels of symbolic links", keeping the directory entry and exiting 2 instead of throwing out of the planner for a bare exit 1. The mount-root refusal denied member selectors. Under -t and -x an operand names something inside the archive, so `tar -tf a.tar data` was refused as busy when `data` happened to spell a mount. Gated on create mode now, dashless first word included. A test asserted the loop bug rather than catching it; it is replaced. Five integ cases cover the lot end to end in both languages. * test(archive): build the test LinkView's stat through FileStat, not a cast |
||
|
|
fdfb8a075b |
fix(tar): accept GNU's old option style (#734)
* fix(tar): accept GNU's old option style `tar xzf a.tgz` failed with "must specify -c, -x, or -t" because a first word with no leading dash was read as an operand. It is a cluster of option letters whose arguments follow as separate words, expanded now by expand_old_style / expandOldStyle before anything else reads the line. Per letter, not one -xzf token: a cluster may hold several argument-taking letters (tar xfC a.tgz out) and may continue past one (tar cfz a.tgz f gzips), neither of which getopt clustering can express. * chore(spec): regenerate spec dumps for old_option_style |
||
|
|
745044f5d2 |
feat(jq): support the rest of jq's flag surface (#713)
* feat(jq): support the rest of jq's flag surface
Only -r, -c and -s were accepted, with no long forms at all. Adds
-n/-R/-j/-a/-S/-e/-M/-f/-h, every long spelling, --raw-output0, --tab,
--indent, --unbuffered, --arg, --argjson, --rawfile, --slurpfile,
--args, --jsonargs, --stream and --seq, in Python and TypeScript.
Two spec-parser additions carry it: Option(pair=True) for two-token
options like --arg name value, and Operand(text_when=...) so --args
turns later operands into positional strings instead of input files.
Also fixes -s to slurp across every operand rather than per file, and
an empty input to print nothing instead of erroring.
* fix(jq): read inputs as a call, not as the word
The detector matched `.inputs`, `{inputs}`, `$inputs`, a module member
and any string or comment, and a match switches the run into drain mode,
so `jq -c '.inputs'` over a two-document stream printed one line where
jq prints two. Both languages now blank string bodies, comments and
object-shorthand keys before searching for the token, keeping
interpolations as code. $ARGS is read the same way.
The native fixture also tolerates EPIPE on the child's stdin: `jq -n`
exits without reading, which raced the fixture's write and failed the
whole node package on unhandled errors with every test passing.
Adds integ coverage for the rest of the flag surface, including the long
spellings, the refused options and the usage errors.
|
||
|
|
edd20a6544 |
feat(spec): one ValueType axis: merge value_kind and type (#689)
* feat(spec): merge value_kind and type into one ValueType axis (python) * feat(spec): merge value_kind and type into one ValueType axis (typescript + dumps) * test(spec): float coverage, lint fixpoint * fix(spec): linear-time FLOAT_VALUE regex (CodeQL polynomial-redos) * fix(spec): numeric refusal before choices in option_error, add FlagView.as_float |
||
|
|
a039a174b8 |
feat(spec): mirror argparse: aliases, long-option abbreviation, typed int values (#687)
* feat(spec): mirror argparse: subcommand aliases, long-option abbreviation, typed int values * fix(spec): coalesce synonym longs by signature, report scan errors in encounter order |
||
|
|
03a38a0493 |
Declarative count, multiple, choices, required and default option fields (#679)
* feat(spec): declarative count, multiple, choices, required and default option fields * fix(types): widen fanout depth-flag helper to the int-carrying flag union * fix(spec): a default on a multiple option lands as a one-element list |
||
|
|
640f4200ab |
Cross-language command spec parity check (#666)
* feat(spec): cross-language command spec parity check Add scripts/check_spec_parity.py and wire it into CI next to the existing spec drift gate. It diffs the generated python and typescript spec trees command by command: every option (help text, value kind, repeatability, shorthand), every operand, the _meta flags, and the resource set each command registers under. Structural divergences live in spec/parity_exceptions.json with a reason, and a stale entry fails the check so an exception cannot outlive what it documents. gen-specs.ts could only see command groups the package index re-exports, so HISTORY_COMMANDS and GRIDFS_COMMANDS were silently missing from the dump. It now asserts every builtin *_COMMANDS is reachable, and both backends are exported. The check found four real bugs: shuf required a write op it only needs for -o. TypeScript marked the builder write:true and threw before doing anything, so shuf did not register on 21 read-only backends. Python registered it but eagerly called ops.require(WRITE), so every invocation raised. Both now resolve the write op lazily, matching sed -i / sort -o / uniq -o. The TypeScript factory only gated on the generic write op, so truncate registered on databricks_volume, dropbox and the hf backends and rmdir on the hf backends, none of which have those ops. Ported Python's requirements mechanism; the tables are now identical in both languages. TypeScript search carried no cost provisioning on qdrant, lancedb or mem0, where Python does. js and node had no help text in TypeScript, and curl, history and seq differed. Aligned per option. * fix(spec): compare spec metadata per resource, not as unions Addresses two review findings on the parity gate. gen-specs.ts treated any index.ts read failure as an absent file, so a permission or IO error silently dropped every command group in that directory, defeating the reachability assertion exactly when the source scan was incomplete. Only ENOENT continues now. The _meta union flags cannot say which resource carries a provision, an aggregate, the write flag or a filetype, so dropping one backend's provision while another kept it left every union unchanged and the gate accepted the regression. Both generators now emit _meta.by_resource and the checker compares keyed by resource, falling back to the unions only once the per-resource entries agree. That found six more divergences the unions had masked: Python's history cat, grep, head, tail and wc carried no aggregate where TypeScript did, and the same for github and email grep. Added them. TypeScript had no github du provision where Python does. Added it. TypeScript still has no github grep provision. Python's is a bespoke cost model that walks the index and reaches IndexCacheStore internals, so it is recorded in parity_exceptions.json for its own change rather than ported here. Exemptions are now granular: by_resource names one resource and one key, so exempting github grep's provision cannot also hide the aggregate divergences on the same command. An exemption counts as used only when it suppresses a live divergence, so the stale check no longer misreports an exemption that fully covers its diff. |
||
|
|
e8e2f5f14a |
fix(jq,gws): evaluate multi-document input per value, paginate gws GETs (#650)
* fix(jq,gws): evaluate multi-document input per value, paginate gws GETs
jq computed output arity with a naive "[]" substring test while jq_eval used a
depth-and-string-aware predicate, so any program with a nested [] inside a
collector had its single array exploded one element per line. Reuse the careful
predicate in both places.
jq also read .json files with strict orjson, so a file holding several JSON
values failed, and the stdin path collapsed a multi-document stream into one
list. Real jq reads a stream of values from any input and evaluates the program
per document. Both paths now do that; -s still slurps.
gws list methods made exactly one HTTP call and dropped nextPageToken, so
'gws drive files list' silently returned the first 100 of N at exit 0.
Pagination is now the default (a truncated listing is indistinguishable from a
complete one) with --page-limit to opt out. Single-response GETs keep their
exact bytes; only real multi-page streams are newline-delimited NDJSON.
Adds 'gws --help' and 'gws <service> --help' matching googleworkspace/cli, and
teaches the integ mock server to paginate. Two existing goldens encoded the jq
bug and are corrected.
* fix(gws): port pagination and help to TypeScript, regenerate specs
The epilog field added to CommandSpec was missing from the committed spec
dumps, which is what the pre-commit spec-drift step caught. Regenerated
both trees and taught the TypeScript side about the field, so the two
dumps keep the same shape.
The PR description claimed TypeScript had no gws passthroughs. It has all
25, so the unpaginated-GET bug was live there too. Ported pagination,
--page-limit, the per-method --help descriptions, and the
gws --help / gws <service> --help surface. render_services() and
render_service_methods() output is byte-identical between the two.
Two defects surfaced during the port:
- withHelpSupport rebuilds the spec from a hand-listed init object to
inject --help, so it dropped epilog on every command. Python is immune
because it uses dataclasses.replace. Typecheck and unit tests both
passed; only the integ case caught it.
- gws --help was registered for gdrive and gsheets only, so a gdocs-,
gslides- or gmail-only mount had the passthroughs but no help. Now
registered for all five in both languages.
Structural alignment between the two implementations:
- drop the orphaned GWS_API_SPEC, replaced by gws_method_spec
- add _parse_page_limit mirroring parsePageLimit, so a bad --page-limit
reports the same message instead of leaking int()'s ValueError text
- re-export the help commands from the gws package so backends stop
reaching into gws.help, matching gws/index.ts
- make the four help description constants module-private on both sides
- drop the now-dead non-GET guard before invalidate_mount_listing
- give each page its own params dict instead of mutating one across calls
gws integ coverage is no longer blocked: g_help_lists_services,
g_help_lists_service_methods, g_files_list_paginates_by_default and
g_files_list_page_limit_truncates run on both hosts.
Docs: a Pagination section on the Drive page covering the default-on
behavior, the NDJSON output, --page-limit and the deliberate divergence
from googleworkspace/cli; the same note on the Gmail passthrough section;
and a JSON with jq section in the bash docs for the stream-of-values
semantics, verified against the jq binary.
* fix(spec): trim the help epilog without a polynomial regex
CodeQL flagged js/polynomial-redos (high) on the `/\n+$/` I used to strip
the epilog's trailing newlines: it backtracks on a long run of '\n'. The
trim now walks backwards, which is what Python's rstrip('\n') already did,
so that side was never affected. Verified identical output on 'Services:',
a single trailing newline, three trailing newlines, and an all-newline
epilog; a 100k-newline epilog now renders instantly.
Also align --page-limit validation across the two. Python used bare
isdigit(), which accepts non-ASCII digits that TypeScript's /^\d+$/
rejects: '١٢' parsed as 12 on one side and errored on the other, and '²'
passed the check then crashed int(). Python now requires isascii() too,
and both suites reject the same five inputs.
* fix(gws): register help commands per resource, drop the nested handler
Both findings from the codex review on #650.
P2, resource filtering. MountEntry.register keys commands by
(name, filetype) and ignores RegisteredCommand.resource, so registering
the whole GWS_SERVICE_HELP_COMMANDS list on every backend made a
gdocs-only mount answer `gws gmail --help` and `gws drive --help` with a
method listing it cannot execute. Reproduced on a gdocs-only workspace
before the fix. gws_help_commands(resource) / gwsHelpCommands(resource)
now build one registration per reachable service, bound to the single
resource asked for, which is what the TypeScript backends were doing with
an explicit filter. A drive mount still reaches docs, sheets and slides.
P1, nested function. make_service_help_command defined its handler inline
and closed over the body, against the repo's no-nested-functions rule.
The handler is now module-scope run_help, bound to its listing with
functools.partial, the same way run_gws_method is bound to its method.
Both languages end up with the same shape, so the per-backend filter
duplication in TypeScript is gone too. ROOT_DESCRIPTION is module-private
on both sides.
|
||
|
|
d813bd342a |
feat(cp/mv): GNU update/backup/target-dir/exchange flag semantics (#609 Tier 1) (#629)
* feat(cp/mv): GNU update/backup/target-dir/exchange flag semantics (#609 Tier 1) Implement the cp/mv "semantics flags" slice of #609 Tier 1 with Python/ TypeScript parity, pinned against GNU coreutils 9.7. cp gains -u/--update[=all|none|none-fail|older], -b/--backup[=CONTROL] + -S/--suffix, -t/--target-directory, -T/--no-target-directory, the long spellings of -r/-a/-f/-n/-v, and -f/-i/--strip-trailing-slashes as documented no-ops, plus GNU overwrite-type guards and arity errors. mv additionally gains --exchange (atomic swap via three renames), --no-copy (cross-mount refusal), and rename-level -n/-u/-b gating, sharing cp's transfer/backup/policy engine. Supporting fixes that fell out of end-to-end testing: - New Option.short_value flag so an optional-value short (cp -b/-u) stays a clusterable boolean (`-bv`) instead of eating the cluster remainder as a value; only --backup=/--update= carry values (GNU). - cp/mv now route path-valued flags (cp -t /other/mount/dir) through cross-mount detection, and see the touch/chmod stat overlay so -u freshness matches ls/stat. - Single per-source entryKind probe + overwrite-gate early-out so API backends pay no extra stat per entry when no gating flag is set. New shared backup helper (utils/backup) implements GNU version-control naming (simple/numbered/existing, ~ and .~N~ suffixes); env-less default is `existing`, VERSION_CONTROL/SIMPLE_BACKUP_SUFFIX deliberately not read. Coverage: Python + TS unit tests for every new flag and error path, and 26 new integ cases across integ/unix/{cp,mv}. Full integ battery green on ram (1849) and disk (1837), 0 failures, on both hosts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cp/mv): address codex review + green the CI (#609 Tier 1 follow-up) CI fixes - Spec drift: the new cp/mv options were never regenerated, and the new Option.short_value field leaked into spec/python via asdict() while the explicit TS serializer omitted it, breaking the two trees' key parity. Emit short_value from gen-specs.ts too and regenerate all three trees. - integ-shared-{py,ts}: the 26 new cp/mv cases listed hf/hf-prefix, whose backend rejects these mutations ("Operation not supported"). Every pre-existing cp/mv case excludes it; drop it to match the canonical set. - integ-shared-ts opfs: OPFS_CP/OPFS_MV passed the raw backend stat, so -u freshness could not see touch -d (OPFS has no setattr; touched times live in the namespace overlay). Read opts.statOverlay like OPFS_LS already does. Fixed properly rather than by excluding the target — mtime cases include opfs by convention (the meta_overlay facet exists for it). Codex findings, each pinned against GNU coreutils 9.7 - P1 --exchange staged through a fixed `<target>.~xchg~`, silently clobbering a real file of that name, and left the operands half-moved on a mid-sequence failure. GNU's renameat2(RENAME_EXCHANGE) touches nothing else, so probe for a free staging name and roll back on failure; report the leftover path when the rollback itself fails. Docstrings no longer claim three renames are atomic. - P1 A files-only find loop dropped every directory holding no files once any update/backup mode was set, and copied an empty tree to nothing. Narrow per_entry_native so the no-op modes (--update=all, --backup=none) keep the whole-tree dir_copy, and recreate directories via a new optional NativeCopy.mkdir on the genuinely-gating path. - P1 The backup version scan swallowed readdir failures and read them as "no numbered backups", which then picks .~1~ and overwrites backup history. Propagate, and abort the overwrite instead. - P2 -u and --update are one GNU option, so the last spelling wins; flags are stored per spelling, which let a fixed read order override command-line order. Mirror aliases in the parser for optional-value options only — repeatable ones accumulate (sort -k/--key concatenates both lists and would double). - P2 mv -b -T refused a nonempty directory target before the backup could displace it; GNU renames it aside and installs the source. - P2 A cross-mount directory backup called read_bytes on a directory. Walk the tree for the primitive strategies and defer to dir_copy on the native one. Also fixes the same swallowed-readdir shape in mv's -T emptiness probe, where "empty" was the clobbering direction. Coverage - 18 new integ cases across both hosts: the behaviors above plus gaps the PR left uncovered — mv --no-copy (cross-mount refusal, source kept, same-mount no-op), mv --backup=numbered, mv -S, mv --update=none, cp --backup=none, and the long spellings (--suffix, --target-directory, --no-target-directory, --recursive, --verbose, --strip-trailing-slashes). - 20 new unit tests (py+ts) for the staging-name collision, both rollback paths, dir preservation, no-op-mode dir_copy retention, backup-scan failure, the -b -T backup, and last-wins aliasing. Verified: pre-commit clean, full py suite, ts core 4991 + browser 167, integ ram/disk/opfs green and byte-identical across the py and ts hosts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(test): satisfy tsc --noEmit in the new cp/mv tests vitest transpiles without typechecking, so these slipped past a local `pnpm --filter mirage-core test` and only the CI Typecheck step caught them. - cp.test.ts: the typed-find helper declared its own `{ type?: string }` options shape, which is not assignable to FindFn's FindOptions under exactOptionalPropertyTypes (FindOptions.type is `string | null`). Use FindOptions itself. - mv.test.ts: eacces() takes one argument, not two. Verified with the recursive typecheck CI runs (`pnpm -r typecheck`, all 7 packages clean) rather than the single-package filter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: scope two new integ cases to capable backends; mirror attached short values integ (the 6 remaining integ-shared failures, identical on both hosts) - mv_b_T_backs_up_nonempty_dir backs the target up by *renaming a directory*, which the dirless object stores cannot do — a directory there is a key prefix with no object of its own, so s3 reports "The specified key does not exist" and gridfs "No such file or directory". Dropped s3, s3-prefix, gridfs, gridfs-prefix (20 -> 16 targets). No existing case asserts a successful same-mount directory rename, which is consistent with this being a backend limitation rather than a regression. - xm_mv_no_copy_same_mount_ok needs a same-mount mv, i.e. a rename op, which hf does not register; its sibling cases pass there only because they refuse before touching the backend. Dropped hf/hf-prefix. - Both crossmount groups now work inside their own subdirectories instead of writing to the top level of /data and /data2, so they leave no residue in the shared battery session. parser - The attached-short-value path (`-d10`) was the one write site my alias mirroring missed, so last-wins held for `--long=` but not for the short form: `split --numeric-suffixes=3 -d10` resolved to 3 instead of 10. Python only — the TS regex already covered all 7 call sites. Regression test on both sides pins the mirror and both orderings. Verified: full py suite (8782 passed), all 7 packages typecheck, integ ram/disk/opfs green on both hosts with identical counts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: yapf line-wrapping on the alias-mirroring call sites Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(cp/mv): drop the object-typed backup param, share the flag helpers Follow-up to the review round. Claude's review listed the `object`-typed backup helper params among what it examined; it ruled them out as a bug, but CLAUDE.md forbids `object` on a parameter outright ("only acceptable as the value type of an opaque flag bag"), and chasing it surfaced a py/ts divergence next to it. - backup_control's `value` was `object`. FlagView.raw() legitimately returns object (the bag IS opaque), but that stops at the boundary: for -b/--backup the real type is `str | bool | None`. Narrow it in a new shared `backup_raw()` and type the parameter accordingly. TS mirrors it — backupRaw/backupControl were `unknown`. - mv.py had reimplemented backup_raw and target_flags inline, 15 lines of duplicated flag interpretation, while TS already exported backupRaw and targetFlags and mv.ts used both. Un-privatized the two cp.py helpers and used them from mv.py, so the two languages now share the same shape and flag semantics live in one place per CLAUDE.md ("adding or changing a flag should touch the spec and the generic, not N wrappers"). No behavior change: mypy clean (1555 files), pre-commit clean, full py suite 8782 passed, ts core 4992, integ ram/disk/opfs green on both hosts with identical counts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: bytecii <bytecii@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
33ac93684c |
Cross-mount shared parse, --color no-ops, GNU option errors (#474)
* fix(executor): cross-mount parses through the shared flag helper, warnings survive * feat(spec): optional-value long options, declare grep/rg/ls --color as GNU no-ops (#471) * feat(spec): unknown options refuse with GNU errors, cat display flags, sort -b (#470) * style: pre-commit formatting, displayLines module-private * refactor(spec): USAGE_EXIT table lives in spec constants |
||
|
|
b2d41a4b1d | chore(spec): regenerate JSON specs; emit repeatable and provided_by, fix py collector for factory commands | ||
|
|
f1747921fa | feat(spec): export command specs to JSON for python and typescript (#71) |