Skip to content

feat: zipnative-cli 1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0 - #6

Merged
Nizoka merged 32 commits into
mainfrom
release/v1.0.0
Sep 5, 2026
Merged

feat: zipnative-cli 1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0#6
Nizoka merged 32 commits into
mainfrom
release/v1.0.0

Conversation

@Nizoka

@Nizoka Nizoka commented Sep 5, 2026

Copy link
Copy Markdown
Owner

feat: zipnative-cli 1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0

PR title: the line above, verbatim — the commitlint job requires a Conventional
Commits subject for the title because a squash merge turns it into the commit subject.
Branch: release/v1.0.0 → main
Type: First release (establishes the 1.x machine contract: envelopes, E_* classes, the ZIP_* mapping, exit codes, schema subjects)
zipnative: ^1.0.0 (sole runtime dependency; external in the bundle, zipnative/worker included)
Support policy: Node.js >= 22; CI matrix Ubuntu 22 + 24, Windows 22 + 24 (blocking), macOS 22; veraZIP Linux + Windows on every PR

Summary

  1. Fifteen commands in four groups over zipnative's frozen 1.0 surface — Create & modify
    (create, modify), Read & extract (list, inspect, cat, extract, stream),
    Integrity & codecs (verify, crc32, inflate), Automation & meta (batch, doctor,
    schema, completion, govern).
  2. A thin dispatch layer with no ZIP parsing of its own: every core call goes through
    src/core-bridge/index.ts and is wrapped by mapZipError / guard, the only place that
    reads the engine's err.code.
  3. The agent contract: --json envelopes carrying code (13 stable E_* classes) and
    zipCode (the 39 frozen ZIP_* causes, verbatim) plus entryName / detail;
    --dry-run on seven commands; --strict; --summary / --fields / compact JSON; the
    eight --max-* bounds plus --max-input-size; 22 schema subjects and a capability
    manifest; doctor; llms.txt, AGENTS.md and docs/data/errors.json in the tarball.
  4. Secure-by-default extraction with the CLI as the proven filesystem trust boundary
    (one sink: safeJoin containment, realpath re-check against planted links, exclusive
    opens, uniform overwrite refusal, case-fold check, partial-file removal, signal cleanup,
    symlinks never materialised, skip-not-write opt-outs) over the engine's refusals.
  5. stream over unseekable input with the trust caveat explicit; modify append-only vs
    --compact with the data-remanence and 7-Zip caveats surfaced and every re-emitted entry
    verified; batch --manifest pipelines with a codec-load policy and one stdout document
    under --json.
  6. The veraZIP conformance gate: the ISO/IEC 21320-1:2015 validator vendored from the engine
    (commit 4f1bc36), independent by construction, over a 37-archive corpus written by the
    built CLI (33 PASS + 4 XFAIL) — blocking in CI on Linux and Windows on every PR and
    pre-publish.
  7. Governance and supply chain: CI on three OSes, CodeQL, Scorecard, Dependabot, Trusted
    Publishing with provenance, an attested SBOM and a verified bin-only tarball, the
    AI-governance / HITL files mirrored by govern and pinned by a test, agent guidance files
    for the coding assistants the ecosystem supports.
  8. Two independent audits (A: CLI / UX / supply chain, B: engine coverage) arbitrated into 74
    accepted findings, implemented in eight batches on this branch (below); six deferred to the
    roadmap, one rejected.

Changes

package.json

  • name zipnative-cli, version 1.0.0, dependencies.zipnative ^1.0.0, engines.node
    >=22, bin.zipnativedist/cli.cjs, files = dist (no maps), LICENSE, README.md,
    AGENTS.md, llms.txt, docs/data/errors.json; publishConfig.provenance; no module /
    types / sideEffects (bin-only); repository.url git+https://….
  • Scripts: build (tsup → dist/cli.cjs only), test, test:coverage, lint (src/ and
    tests/), typecheck:all, corpus:zip, validate:zip (= build + corpus + validator).

src/core-bridge/index.ts

  • Selective re-exports grouped like the engine's own src/index.ts (the 77-export ledger
    mapped in docs/KNOWLEDGE_BASE.md §8); ensureCodecsReady() (memoised
    initNodeZipCodecs) and loadParallelZip() (lazy zipnative/worker import with the
    worker script resolved through the exports map).

Commands (src/commands/)

  • create.ts — plan (walk / manifest / stdin) → createZip | createParallelZip
    toBytes() | stream(); every name pre-checked with sanitizeEntryPath; --order insertion = argv order; --comment-file; manifest extraFields / commentBase64;
    --parallel refuses writer-shaping codec modules; envelope layout / tier.
  • modify.ts — eager open, fixed-order edits through createZipModifier, verifyEntry() on
    every re-emitted entry, save() | saveCompact(); --in-place via an exclusive temp file
    • rename; --comment-file; manifest mode / extraFields / commentBase64.
  • list.ts, inspect.ts (eager open, stats, determinism verdict split into deterministic
    / canonicalLayout, 19 --check assertions, commentHex), cat.ts (readEntryStream /
    readEntryRaw, sync-only codec fallback), extract.ts (two-phase sink, --skip-unsupported),
    stream.ts (iterateZipEntries; attribute flags refused; trust: "local-headers-only";
    summary descriptorEntries / bytesKnown).
  • verify.ts (verifyZip + counters, --entry via verifyEntry, E_VERIFY_FAILED with
    zipCode), crc32.ts, inflate.ts (createInflator with a mandatory bound; --sync /
    --method; bytesConsumed).
  • batch.ts (directory create / verify with a 1–64 pool; --manifest pipelines; one stdout
    document under --json via captureStdout), doctor.ts (limits as numbers), schema.ts
    (22 subjects, including errors and manifest), completion.ts (the COMMANDS table,
    PATH_FLAGS, four shells), govern.ts.

Utilities (src/utils/)

  • ziperr.tsZIP_TO_CLI (39 codes → class + exit, satisfies Record<ZipErrorCode, …>),
    ZIP_DIAGNOSTIC_CODES (11), mapZipError / guard, isFsError.
  • error.ts (13 E_* codes, CliError with zipCode / entryName / detail),
    agent.ts (envelopes, emitStatus, progress), diagnostics.ts (the sink),
    limits.ts (eight CWE-tagged flags + --max-input-size), engine.ts (prepareEngine),
    codecs.ts (--codec loader, overridesBuiltin), io.ts (validatePath for manifest
    values, safeJoin, bounded reads, exclusive writes, captureStdout, 50 MB cap, streams),
    sink.ts (the one extraction sink), inflight.ts (signal cleanup), flags.ts (the
    boolean-flag table), zipops.ts (UTC dates, modes, extra fields), entryfmt.ts
    (rawNameHex, commentHex, 4-digit unixMode), walk.ts (preserveInputOrder),
    glob.ts, sizes.ts, manifest.ts, projection.ts, config.ts (codec refused),
    version.ts, governance.ts, colors.ts (stderr, FORCE_COLOR, TERM=dumb), args.ts.

Wiring (single source of truth respected)

  • src/index.ts — USAGE for 15 commands + the global block (exit codes, environment),
    COMMAND_USAGE, loadCommand(), global flags → ZIPNATIVE_* env, config merge, the EPIPE
    guard, the signal handler, the agent error envelope.
  • src/commands/completion.tsCOMMANDS (with group), GLOBAL_FLAGS, PATH_FLAGS,
    DRY_RUN_COMMANDS; src/utils/flags.ts BOOLEAN_FLAGS; src/utils/projection.ts
    PROJECTED_COMMANDS; src/utils/manifest.ts MANIFEST_COMMANDS; src/utils/config.ts
    KNOWN_COMMANDS.

scripts/ & workflows (veraZIP, CI)

  • scripts/generate-zip-corpus.mjs — drives the built CLI (plus the raw builder for
    canaries) to write 37 archives + manifest.json to test-output/zip/: 33 conformant
    (every writer path incl. binary comments, --order insertion, manifest extraFields;
    3 hostile-but-conformant with refusedBy) + 4 raw-crafted negatives (WF/ENTRY-OVERLAP,
    WF/CD-COUNT, WF/LFH-SIZE-MISMATCH, WF/LFH-NAME-MISMATCH).
  • scripts/validate-zip.mjs — vendored from zipnative/scripts/validate-zip.ts (commit
    4f1bc36, hashes recorded in the header); raw parser, never imports the engine; level 0
    always, level 1 foreign tools SKIP when absent, VERAZIP_REQUIRED=1 fail-closed; exit
    0/1/2/3; coverage canary over REQUIRED_NEGATIVE_CHECKS.
  • scripts/helpers/interop-tools.mjs — bsdtar / unzip / 7z / python-zipfile / jar integrity
    checkers with per-tool exit contracts.
  • .github/workflows/: ci.yml (Ubuntu 22/24, Windows 22/24, macOS 22; docs changes run
    the suite), verazip.yml (blocking, Linux + Windows, no path filter), publish.yml (the
    full gate, veraZIP, attested SBOM, tarball verification, Trusted Publishing), codeql.yml,
    scorecard.yml; dependabot.yml; ai-governance.json, AGENT_RULES.md,
    drafts/README.md + two upstream drafts, ISSUE_TEMPLATE/config.yml,
    copilot-instructions.md.

Samples

  • samples/<command>/ .sh + .ps1 pairs (41 demos), samples/agent/ (the recommended
    loop), samples/run-all.js (73 jobs), samples/README.md. All offline.

Docs

  • README (badges, What's new, Highlights, Supported Features with the security-defaults
    table, Conformance status, Quick Start, Command Reference from the USAGE strings, Global
    options with the limit defaults, environment, memory and exit codes, Driving from AI
    agents, Security), docs/KNOWLEDGE_BASE.md (12 sections incl. the 39-code mapping and the
    77-export map), AGENTS.md, llms.txt, CHANGELOG.md (every audit finding tagged),
    ROADMAP.md, SECURITY.md, CONTRIBUTING.md (veraZIP, CI matrix, branch protection,
    pinned docs), SUPPORT.md, CODE_OF_CONDUCT.md, CITATION.cff,
    release-notes/TEMPLATE.md, release-notes/v1.0.0.md, docs/data/core-exports.json,
    docs/data/errors.json.

Tests

  • In-process vitest suites per command and util (tests/helpers/capture.ts), the
    engine-independent tests/helpers/raw-zip-builder.ts for adversarial shapes, two
    foreign-provenance interop fixtures (tests/fixtures/README.md + policy test), one spawn
    smoke test against dist/cli.cjs (incl. EPIPE and SIGINT), the veraZIP vendor test,
    tests/utils/governance-sync.test.ts, and tests/docs/consistency.test.ts (command
    counts, E_* codes, the ZIP_* mapping, the 77-export map, the limits table, the schema
    subjects, USAGE ↔ COMMANDS ↔ README / KB tables, status enum ↔ emitStatus callers,
    CITATION version, environment variables, tarball paths).
  • 1202 tests, 61 files, all green (1193 passed + 9 platform-conditional skips).
    Coverage statements 96.32 / branches 92.52 / functions 97.93 / lines 96.91 against the
    enforced thresholds 93 / 88 / 94 / 93.

The audit pass, batch by batch

Two independent audits (A: CLI, UX, agent contract, supply chain; B: engine coverage) were
arbitrated into 81 canonical findings (four merged pairs): 74 accepted, 6 deferred
(A-11 lazy engine require, A-25 fuzz tests, A-36 one governance schema, B-21 manifest
externalAttributes, B-39 list --extra, B-42 inflate-tier visibility — all on the roadmap),
1 rejected (B-44). Each batch is one commit on this branch.

  • B1 — parser and process contract (456a854; A-01, A-03, A-08, A-29, A-37): the
    boolean-flag table (utils/flags.ts) so a boolean never consumes the next token and global
    flags work before the command name; EPIPE → exit 0; TTY guard on implicit stdin; unknown
    command → exit 2 / E_USAGE; --format, -f everywhere; ≤ 80-column USAGE; -lq refused.
  • B2 — dates (ecda18a; A-02, B-31): ISO dates are UTC wall-clock and time-zone
    independent; clamp warnings (1980–2107, odd seconds, --chunk-size range).
  • B3 — sink hardening and input bound (efb4c1e; A-06, A-07, A-09, A-14, A-16, A-22,
    A-26): one sink module with realpath containment and exclusive opens; --max-input-size;
    uniform --overwrite policy on every writer; argv .. accepted (manifest values still
    checked); threat-model rows.
  • B4 — modify verification and codec truth (9640bdc; B-02, B-03, B-07 + B-41,
    B-33): eager open + verifyEntry() on every re-emitted entry, no opt-out; honest codec
    claims (a method 0/8 module shapes the writer); create --parallel refuses writer-shaping
    modules; modify envelope tier.
  • B5 — engine coverage (31dc1c3; B-01, B-04, B-05, B-06, B-14, B-15, B-16, B-17, B-18,
    B-43): --order insertion = argv order; manifest extraFields, mode, commentBase64;
    --comment-file; rawNameHex / commentHex; verify --entry; extract --skip-unsupported; cat sync-codec fallback; inflate bytesConsumed; three corpus
    entries (37 archives).
  • B6 — agent contract (5c2e9fc; A-05, A-12, A-13, A-21, A-30, B-19 + A-46, B-29, B-36,
    B-37, B-40): one stdout document for batch --json; env-driven dry-run; colours on
    stderr; unsafe names are E_INPUT; doctor limits as numbers; stream --summary
    markers; no empty entryName; 4-digit unixMode; zipCode on every E_NOT_FOUND.
  • B7 — hygiene (6f1f05b; A-10, A-23, A-31, A-38, A-40, A-43, B-32): governance sync
    test; remedies in every refusal; dead exports removed; SIGINT / SIGTERM cleanup; path
    completions; --concurrency cap; --chunk-size with --stdin-name.
  • B8 — CI and packaging (857261c; A-04, A-27, A-33, A-41, A-42, A-44): docs changes run
    CI; macOS job and Windows 22/24; coverage ratchet; attested SBOM and tarball verification;
    CJS-only bin package; lint covers tests.
  • Step 2 (ce41623): inspect separates reproducibility from layout
    (canonicalLayout, --check canonical-layout); create reports layout.
  • Documentation pass (A-15, A-18, A-19, A-20, A-24, A-28, A-32, A-34, A-35, A-39, A-45,
    B-20, B-23–B-28, B-30, B-34, B-35, B-38, B-45): README / KB / AGENTS / llms.txt aligned,
    consistency-test relations extended, CONTRIBUTING branch protection, release notes with
    Security first, roadmap sentences for every deferred item, ISSUE_TEMPLATE/config.yml, the
    two upstream drafts.

Test plan

Every command below was run on this branch at HEAD and must be green again on the PR:

  • npm run typecheck:all — clean
  • npm run lint — 0 errors (src/ and tests/)
  • npm run test:coverage — 1193 passed + 9 skipped across 61 files; statements 96.32 /
    branches 92.52 / functions 97.93 / lines 96.91 ≥ thresholds 93 / 88 / 94 / 93
  • npm run builddist/cli.cjs only (no dist/cli.js, no .d.ts, no maps)
  • npx vitest run tests/integration/built-binary-smoke.test.ts — post-build spawn suite
    (--help, --version --json, schema manifest = 15 commands, EPIPE exit 0, SIGINT exit 130
    on POSIX)
  • npm run validate:zip33 PASS + 4 XFAIL, 0 FAIL, exit 0 (level 1 with the tools
    present; VERAZIP_REQUIRED=1 in CI)
  • node samples/run-all.js — 73/73
  • npm pack --dry-run — 7 files, 120.0 kB packed
  • npm audit --audit-level=high — 0 vulnerabilities
  • Built-binary drive: create --deterministicinspect --check deterministic
    verify --strictextractcrc32 cross-check; create --parallel resolves the worker
    script from the bundle; doctor reports deflate-tier: node-zlib; create --stream is
    deterministic: true, canonicalLayout: false
  • Zero-network guarantee: no net / http / fetch / dns import anywhere in src/

Human follow-ups (not performed by any agent)

  • Push the branch, open this PR, tag v1.0.0, create the GitHub Release (the publish
    workflow runs on the published release).
  • Configure npm Trusted Publishing for zipnative-cli (repository Nizoka/zipnative-cli,
    workflow publish.yml) — the 0.0.1 placeholder was token-published, so the OIDC trust
    relationship does not exist yet.
  • Apply the branch protection recorded in CONTRIBUTING.md (required checks ci (22),
    ci (24), windows (22), windows (24), macos, verazip-linux, verazip-windows).
  • Enable GitHub Discussions on this repository (SUPPORT.md and ISSUE_TEMPLATE/config.yml
    point at the engine's board until then); decide on a security@zipnative.dev inbox
    (SECURITY.md names the shared security@pdfnative.dev fallback today).
  • File the four upstream engine notes drafted locally in .github/drafts/ (git-ignored, on
    the release machine) under your own identity after review: DOS time encoded from local
    getters (the CLI compensates with UTC wall-clock components); iterateZipEntries().data()
    pumping a registered custom-method entry through the inflater instead of refusing before
    the first byte; the node-zlib inflate tier leaking raw Z_DATA_ERROR / Z_BUF_ERROR
    instead of ZIP_DEFLATE_* (the CLI maps them in ziperr.ts); verifyEntry() not
    reporting the skipped reason verifyZip() knows (the CLI re-derives it). Each passes
    govern verify-issue.
  • Follow-up PR in zipnative (ecosystem.json, README, ROADMAP) announcing the CLI.

Backward compatibility

  • First release — the surface documented here is the 1.x baseline: envelope fields, the 13
    E_* classes, the 39-entry ZIP_* mapping, exit codes (0/1/2, plus 130/143 on signals) and
    the 22 schema subjects are frozen for 1.x; additions are minor, changes are major.
  • The engine's deterministic: true bytes are never post-processed by the CLI, so
    create --deterministic output inherits zipnative's frozen byte contract.

Out of scope (recorded in ROADMAP)

  • zipnative-mcp (separate repository).
  • Read-only AES decryption — blocked on a core crypto-provider seam (the engine's non-goals).
  • Streamed entries > 4 GiB — blocked on the core's per-entry zip64 streaming ADR.
  • Custom-method entries in stream — waits on the engine (upstream draft).
  • --explain <ZIP_CODE>, a deflate command, inspect --diff, create --from-list,
    --store-symlinks and manifest externalAttributes, list --extra, inflate-tier visibility
    in doctor, user-level config, positional arguments in manifest tasks, lazy engine require,
    fuzz tests, noUncheckedIndexedAccess, one ecosystem governance schema, man pages,
    scripts/verify-docs.mjs, veraZIP sync automation.

Self-review checklist

  • npm run typecheck:all clean
  • npm run lint 0 errors (src and tests)
  • npm run test:coverage green, thresholds (93 / 88 / 94 / 93) met
  • npm run build + built-binary smoke test (node dist/cli.cjs --help, every command,
    schema manifest, create --parallel, doctor on the node-zlib tier)
  • npm run validate:zip passes locally (33 PASS + 4 XFAIL, exit 0) and in verazip.yml
  • No ZIP parsing in the CLI — every core call through src/core-bridge/index.ts and
    wrapped by mapZipError / guard
  • No security default loosened (traversal, symlinks, duplicates, every ZipLimits bound,
    --max-input-size, safeJoin + realpath containment, exclusive opens, overwrite
    refusal, modify verification, --codec argv-only, manifest codec gate)
  • 77/77 engine exports mapped in docs/KNOWLEDGE_BASE.md §8 (docs/data/core-exports.json)
  • 39/39 engine codes mapped in ZIP_TO_CLI and documented (docs/data/errors.json,
    AGENTS.md); 11/11 diagnostics listed
  • tests/docs/consistency.test.ts and tests/utils/governance-sync.test.ts green
  • CHANGELOG.md (Keep a Changelog, every audit id tagged) and release-notes/v1.0.0.md dated
  • No new runtime dependency (zipnative remains the only one); no socket anywhere
  • Docs + samples + completions + schemas cover the whole 15-command surface
  • No autonomous GitHub writes — this draft is committed for human review (HITL)

…entries, accept '-' as a flag value

- io.ts: writeFileStream resolves/rejects only after the write stream's
  'close' so a refusal on the first pull can no longer leave an empty
  placeholder behind unlinkQuiet (fd opened asynchronously).
- stream.ts: --skip-unsupported now skip()s the entry it could not decode
  before advancing (the core refused with ZIP_API_MISUSE otherwise).
- args.ts: a lone '-' after a flag is a value (stdin/stdout), not a flag.
- govern.ts: validate --format json|text (exit 2 otherwise).
- index.ts: --stream help no longer claims byte identity with the buffered
  layout (file inputs use addStream → data-descriptor layout); --strict
  wording for verify; positional archive shown for inspect/verify.
…t-binary spawn smoke

Utilities (frozen 39-code ZIP_* → E_* table, 13 E_* codes), every command,
integration round-trips (create → verify → extract, modify remanence vs
compact, forward stream read, parallel byte-identity, refusal posture on
seven raw-crafted hostile shapes), foreign-provenance interop fixtures with
a 20 KB budget, docs consistency and veraZIP vendoring drift checks.
…LI-driven corpus, negative canaries

scripts/validate-zip.mjs is a 1:1 port of zipnative's scripts/validate-zip.ts
(commit 4f1bc36, independent raw parser, never imports the engine) with the
pdfnative outcome vocabulary PASS/FAIL/XFAIL/XPASS/INFRA/SKIP, exit 0/1/2/3,
VERAZIP_REQUIRED fail-closed and per-file JSON reports.
scripts/generate-zip-corpus.mjs drives dist/cli.cjs to write 30 conformant
archives (3 of them hostile-but-conformant, asserted refused by the CLI) and
crafts 4 negative canaries with its own raw writer. 30 PASS + 4 XFAIL.
…ows, publish with Trusted Publishing + SBOM, CodeQL, Scorecard), governance and templates
…E.md, llms.txt, changelog, roadmap, security, contributing, release notes v1.0.0 + draft PR
…ds projects --summary output

- list/stream: the -l short alias swallowed the following positional as its
  value; --long is the only spelling now (USAGE updated).
- crc32: a failed --expect no longer emits a success status envelope before
  the error envelope; the success envelope now carries expect/matched.
- projection: --fields applies to the --summary document instead of being
  ignored when both are given.
…m verdict; create reports layout

- inspect: determinism.deterministic = epoch timestamps + canonical order +
  UTF-8 flags (reproducibility); new canonicalLayout mirrors noDataDescriptors.
  A streamed archive (data descriptors) is reproducible run-to-run, so it no
  longer fails --check deterministic; --check canonical-layout is the alias of
  no-data-descriptor. Text verdict reads 'reproducible, layout canonical'.
- create: status envelope carries layout: buffered | data-descriptor.
- schema inspect / inspect-summary / status updated; help and docs aligned.
…es (audit A-01, A-03, A-08, A-29, A-37)

- utils/flags.ts lists every boolean flag (global + per command); parseArgs
  never consumes the next token for them, so `zipnative --json list …`
  dispatches and `list --long a.zip` keeps its positional. A dash-digit
  token is a value; combined short flags (-lq) are refused with E_USAGE;
  --flag=false negates a boolean for hasFlag/getBoolFlag.
- io: a terminal with nothing piped is refused (E_USAGE) instead of
  blocking; an explicit '-' still reads stdin. Process-wide EPIPE guard on
  stdout/stderr: a closed downstream pipe ends the run quietly with exit 0.
- index: unknown command and flags-without-command are usage errors
  (exit 2, E_USAGE envelope); --format, -f documented on every command.
…mp warnings (audit A-02, B-31)

The engine encodes a Date through its local getters, so a zoned --date
produced different DOS fields (and a different --deterministic hash) per
host time zone. Every explicit date (--date, manifest date on create and
modify) is now normalised to its UTC components before it reaches the
engine; a string without a zone designator is read as UTC. Years outside
1980-2107, odd seconds and a --chunk-size outside 1 KiB..16 MiB emit a
warning instead of being clamped silently.
…niform --overwrite policy

Audit findings A-06, A-07, A-09, A-14, A-16, A-22, A-26 (arbitrated).

- utils/sink.ts: the one extraction sink shared by `extract` and `stream` —
  lexical containment (safeJoin), duplicate policy, physical containment
  (the nearest existing ancestor and the created directory are realpath'd
  under the root's realpath, so a symlink/junction planted inside the
  destination cannot redirect mkdir or the write), exclusive `wx` opens
  without --overwrite (no check-then-write window), partial files removed.
- utils/io.ts: `WriteOptions { exclusive }` on writeOutput /
  writeStreamingOutput / writeFileStream with the uniform E_IO refusal
  "Refusing to overwrite existing file <path> (pass --overwrite)"; the
  source is no longer pulled once the open has failed.
- --overwrite on create, modify, cat, inflate and batch (directory mode):
  an existing output is refused (E_IO) and left intact; extract/stream
  unchanged. `modify --in-place` uses an unpredictable, exclusively created
  temp name.
- --max-input-size <size> (global, default 4 GiB, "none" disables with one
  warning): bounds every buffered read — stdin (byte-counted, read aborted)
  and files (size checked before reading) for list/inspect/verify/extract/
  cat/modify, create --stdin-name and inflate --sync — with E_LIMIT and
  detail { limit: "maxInputSize", configured, observed }.
- argv-typed paths are no longer refused for containing "..": they are the
  user's own filesystem authority. validatePath() stays for manifest values
  (batch tasks, create/modify entry paths, the extraction sink).
- corpus generator removes each target before regenerating it.
- tests: utils/sink, utils/io (exclusive, bounds), commands/overwrite-policy,
  junction-inside-destination for extract and stream, modify temp name,
  --max-input-size parsing and readArchiveBytes bound.
…; --parallel refuses writer-shaping modules

Audit findings B-03, B-07 (+B-41), B-02, B-33 (arbitrated).

- modify opens eagerly (overlap / CD-LFH structure checked before any edit)
  and cross-checks every untouched entry with reader.verifyEntry() before
  the save — CRC-32, sizes and local header vs central directory, one
  decompress pass, never a recompress. A lying record is refused with
  E_DATA (ZIP_CRC_MISMATCH / ZIP_SIZE_MISMATCH) or E_SECURITY
  (ZIP_CD_LFH_MISMATCH) naming the entry instead of being laundered into a
  canonical-looking archive; runs under --dry-run too; no opt-out. Encrypted
  entries and stream-only-codec entries are copied as-is and counted. The
  envelope gains verified, verifySkipped and tier (schema status updated).
- The "read-side only" codec claim was false: the engine resolves methods
  0/8 through the registry, so a --codec module registering method 8
  replaces the writer's compressor (even under --deterministic), and a
  deflateImpl replaces the sync tier unless --deterministic. Every doc site
  (README, SECURITY, KB, AGENTS, CHANGELOG, release notes, copilot
  instructions, --help) now says so; LoadedCodecModule.overridesBuiltin
  lists the writer-resolved methods a module registers.
- create --parallel refuses (exit 2) a module registering method 0/8, and a
  deflateImpl without --deterministic: the worker pool never sees the
  module and the envelope would lie. Sequential writes announce the
  override with a warning line (silent under --dry-run).
- create: static `progress` import (two dynamic imports removed).
- tests: hostile archives from the raw builder (CRC lie, LFH/CD mismatch,
  overlap, encrypted survivor), create-codec-write suite (isolated file:
  codec registration is process-global), overridesBuiltin.
…, raw-name hex, verify --entry, extract --skip-unsupported, cat sync-codec fallback, inflate bytesConsumed

Audit findings B-04, B-01, B-05, B-06, B-14, B-15, B-16, B-17, B-18, B-43 (arbitrated).

- create --order insertion now means the argv order (each directory still
  walks name-sorted): walkPaths(preserveInputOrder) skips the global sort,
  so an EPUB `mimetype` listed first is written first. Canonical (default)
  is untouched.
- Manifests: entries[].extraFields [{ id, hex | base64 }] (create; modify
  add/replace/add-dir), modify edits[].mode, and commentBase64 (both, exclusive
  with comment) reach AddEntryOptions.extraFields / externalAttributes /
  setComment(Uint8Array). parseMode / externalAttributesFor / parseExtraFields
  live in utils/zipops.ts, shared by both commands.
- --comment-file <path> on create and modify (raw bytes, "-" = stdin,
  exclusive with --comment, 65535-byte cap): the binary-comment path.
- Forensic hex: entries[].rawNameHex and commentHex under --long
  (list / inspect --entries / stream --long), archive.commentHex on list
  and inspect whenever the archive has a comment.
- verify --entry <name> (repeatable): eager open, then verifyEntry() per
  name; report carries `selected`, encrypted / stream-only-codec entries are
  skipped like verifyZip, unknown names are E_NOT_FOUND (ZIP_ENTRY_NOT_FOUND)
  before any output, a structural refusal lands in report.error.
- extract --skip-unsupported: encrypted entries and methods with no
  registered codec are skipped (reason "unsupported") instead of aborting.
- cat falls back to readEntry() for a --codec method that decompresses
  synchronously only (was ZIP_UNSUPPORTED_CODEC_MODE).
- inflate envelope gains bytesConsumed (the inflator's exact figure on the
  streaming path).
- schema: entry rows, create/modify manifests, entries/inspect archive,
  verify (+summary), status updated; completions and --help updated.
- corpus: comment-binary.zip, order-insertion-epub.zip,
  extra-fields-manifest.zip (37 archives: 33 conformant + 4 canaries).
- tests: engine-coverage suite (order, extra fields, comments, hex, verify
  --entry, skip-unsupported, sync-only codec, bytesConsumed, walk order).
… dry-run, colours on stderr, data error classes, envelope nits

Audit findings A-05, A-12, A-13, A-21, A-30, B-19 (+A-46), B-29, B-36, B-37, B-40 (arbitrated).

- batch --manifest under --json / --format json: stdout is ONE batch
  document. Every task runs under captureStdout() (utils/io.ts, 64 MiB cap
  → E_LIMIT captureBytes) and its output lands in tasks[i].report (parsed
  JSON, or an array for NDJSON), .stdout (text) and .stdoutBytes. Tasks that
  would write their artefact to stdout — create/modify/cat/inflate without
  "output", stream --cat — are refused at validation (E_USAGE, also under
  --dry-run). Text mode keeps the interleaved contract.
- create/extract --dry-run print no text plan when agent mode comes from
  the environment (ZIPNATIVE_JSON), not only from the flag.
- colours: style() decides on stderr (where the progress lines go);
  FORCE_COLOR turns colour on, TERM=dumb off, NO_COLOR always wins.
- Unsafe ENTRY NAMES are data, not usage: modify --add/--rename/--add-dir
  and create --stdin-name refuse them with E_INPUT (exit 1) carrying
  entryName; modify --add "dir/=payload" (flags and manifest) is E_INPUT
  pointing at --add-dir.
- doctor: the limits check carries data { ...ZipLimits, maxInputSize }
  with "none" for a disabled bound, and counts --max-input-size overrides.
- stream --summary gains descriptorEntries and bytesKnown (data-descriptor
  local headers carry zero sizes); a forward-read failure before the first
  header no longer carries an empty entryName.
- unixMode is four octal digits ("0000", "0644", "4755").
- Every CLI-side E_NOT_FOUND (cat, inspect, stream --cat, verify --entry)
  carries zipCode ZIP_ENTRY_NOT_FOUND and names the remedy.
- schema: batch tasks[].report/stdout/stdoutBytes, doctor checks[].data,
  stream-summary, entries unixMode pattern; --help updated.
- tests: agent-contract suite, captureStdout, colours (stderr/FORCE_COLOR/
  TERM=dumb); error-class expectations updated.
…exports, path completions, concurrency cap

Audit findings A-10, A-23, A-31, A-38, A-40, A-43, B-32 (arbitrated).

- governance: AI_GOVERNANCE_POLICY now deep-equals .github/ai-governance.json
  (spec_updated, compliance_report.description, capability_manifest,
  verification.advisory_in_ci, references) and tests/utils/governance-sync
  pins the policy and every AGENT_RULES rule line to the .github files.
- SIGINT / SIGTERM: utils/inflight.ts tracks the files being written
  (writeOutput opens first and registers only a file it created;
  writeFileStream registers on 'open'); the handler removes exactly those
  paths and exits 130 / 143. Smoke test on POSIX (spawn + SIGINT).
- remedies: every refusal in cat / extract / stream / batch / create /
  modify names the next action (--skip-unsafe, --add-dir, --dry-run, the
  directory-mode rules, `zipnative list` …).
- dead exports removed with their tests: die, ensureDir, parentDir,
  entryBasename, guardAsync, LIMIT_DEFAULTS (deprecate and getBoolFlag stay).
- completions: PATH_FLAGS complete files after path flags (bash _filedir /
  compgen -f, zsh _files, fish -r -F); every other value flag is fish -r.
- batch --concurrency goes through parsePositiveInt with a 64 cap;
  create --chunk-size is accepted with --stdin-name (the chunked writer);
  the last dynamic import in create.ts is static.
…every PR, attested SBOM, CJS-only package, lint tests, coverage ratchet

Audit findings A-04, A-27, A-33, A-41, A-42, A-44 (arbitrated).

- ci.yml: paths-ignore reduced to LICENSE/.editorconfig/.gitignore/FUNDING/
  issue templates (tests/docs/consistency.test.ts pins the docs, so docs
  changes must run the suite); Windows matrix 22 + 24; new macOS job (the
  second case-insensitive filesystem the sink handles); dist checks expect
  the CJS bin only.
- verazip.yml: no `paths:` filter — a required check must run on every PR.
- publish.yml: top-level permissions read-only (the job requests id-token /
  contents / attestations); the release tag reaches the shell through the
  environment; the SBOM is attested (actions/attest-build-provenance,
  SHA-pinned); the packed tarball is verified (bin, AGENTS.md, llms.txt,
  errors.json; no maps, no tests) before publish.
- package: CJS bin only (tsup format cjs, no dts, no sourcemap; `module`,
  `types`, `sideEffects` removed), `files` adds AGENTS.md and
  docs/data/errors.json and excludes maps, repository URL git+https.
  Tarball: 7 files, 102 kB packed.
- lint covers tests/ (relaxed test-ergonomics override; three findings
  fixed); coverage thresholds 93 / 88 / 94 / 93 from the measured
  96.3 / 92.3 / 97.9 / 96.8; tsconfig "dom" lib annotated.
…edge base, AGENTS, llms.txt, changelog, roadmap, security, contributing, release notes, drafts)

Audit findings A-15 (+B-22), A-17/A-18, A-19, A-20, A-24, A-28, A-32, A-34, A-35, A-39, A-45, B-20, B-23…B-28, B-30, B-34, B-35, B-38, B-45.

- README / llms.txt / samples README / CLAUDE.md / copilot + instruction
  files: every command table regenerated from --help; global options gain
  --max-input-size and the colour rules; new process contract, exit codes,
  environment and memory tables; the false "every file-path argument is
  validated against ../" claim replaced by the manifest-values-only posture;
  sink, overwrite policy, modify verification, codec truth, insertion order,
  UTC dates, --stream layout vs canonicalLayout, batch --json single
  document, CJS-only package, CI matrix, measured numbers.
- Knowledge base / AGENTS.md / docs/data: per-command reference and
  envelope catalogue rebuilt from the built binary; §8 mapping re-graded
  with a Reach column (capability vs type); diagnostics carry a raisedBy
  column (errors.json + KB + AGENTS); core-exports.json rows gain reach/via.
- CHANGELOG [1.0.0]: one line per accepted audit id; stale lines corrected.
  ROADMAP: released items moved, the six deferred sentences and the A-39 /
  A-44 notes added verbatim, AES item links the engine non-goals.
  SECURITY: sink, overwrite policy, input validation scope, modify
  verification, codec statement, signals, supply chain. CONTRIBUTING:
  CI matrix, branch-protection checks, pinned-docs relations, lint scope,
  thresholds. CITATION date. SUPPORT aligned with SECURITY.
- Release notes (security first, compatibility notes) and the draft PR
  body (batches B1–B8, gate commands, human follow-ups).
- New: .github/ISSUE_TEMPLATE/config.yml; two upstream issue drafts under
  .github/drafts/ (engine DOS time from local getters; forward stream
  should refuse custom-method entries) — both pass govern verify-issue.
- --help: every line within 80 columns; global usage documents exit codes
  (130/143) and every ZIPNATIVE_* variable; batch usage lists the
  forwarded create flags. Bridge re-exports the worker ByteSource /
  StreamOptions aliases (B-45). tests/docs/consistency.test.ts pins
  USAGE ↔ COMMANDS ↔ flag table, 80-column help, status enum ↔
  emitStatus callers, CITATION version, raisedBy, the absence of
  "read-side only", llms.txt tarball paths, README/KB flag coverage.
…6 majors, 11 minors, 4 nits) with four contract fixes

Doc audit D (23 findings) verified by E: 21 confirmed, 1 adjusted, 1 rejected, 1 new.

Code (the documentation stated the intended contract):
- govern verify-issue is bounded by --max-input-size like every other
  buffered read (D-03).
- utils/ziperr.ts maps the unwrapped node:zlib errors of the sync tier
  (Z_DATA_ERROR / Z_NEED_DICT → ZIP_DEFLATE_CORRUPT, Z_BUF_ERROR →
  ZIP_DEFLATE_TRUNCATED, both E_PARSE): `inflate --sync` on node-zlib no
  longer reports a corrupt or truncated stream as E_RUNTIME without a
  zipCode — the class never depends on the codec tier (D-04).
- stream: under --json, --summary / --fields select the json report
  instead of being silently ignored by the NDJSON default (D-05).
- verify --entry: the E_NOT_FOUND message names the remedy (D-12).

Documentation: the EPUB example now runs as written (inputs are looked up
where they are, --base only rebases names); the Docker sample creates
/out; stream --long carries rawNameHex only (no commentHex outside the
central directory); inspect --summary example gains canonicalLayout;
`schema` = 22 subjects (20 schemas + errors + manifest); unixMode null
depends on the producer, not the host; --fields array-leaf null rule;
crc32 envelope expect?/matched?; stream list-mode envelope fields;
inspect text verdict "(streamed)" suffix; E_NOT_FOUND remedies per
command; Discussions pointer consistent with SUPPORT.md; ROADMAP deferred
items carry their audit ids; verazip.yml header 37 archives; bug template
Node 22/24 only; create --help envelope lists dryRun and layout; refreshed
counts (57 files / 1184 tests, coverage 96.27 / 92.36 / 97.91 / 96.81,
tarball 116.7 kB).

tests: doc-audit-fixes suite (govern bound, zlib mapping, inflate --sync
classes, stream --json --summary, verify --entry remedy).
… one formatDiagnosticLine() for every stderr diagnostic

Review finding Q2-F1: list --format ndjson and stream --list printed the
engine diagnostics on stderr through process.stderr.write, so --quiet did
not apply to them although --help documents it as suppressing every text
diagnostic. They now go through progress(); the line format is shared by
the sink and the listings (utils/diagnostics.ts formatDiagnosticLine).
…nvelope (and a remedy: line in text mode)

Review finding Q2-F2: engine refusals carry the engine message, which
names library options (rejectTraversal: false, onDuplicate: 'first')
that do not exist on the command line, so an agent had to branch on
zipCode through a table of its own.

- error.remedy (optional, top level next to detail): the CLI flag(s) or
  command that lift the refusal. One table, ZIP_REMEDY in utils/agent.ts
  (type-only import of the code union: the start-up path never loads the
  engine), plus an explicit CliError option for CLI-side refusals without
  a zipCode (--overwrite, --max-input-size, batch task output); a
  ZipLimitError names its exact --max-* flag (LIMIT_FLAGS). Absent when
  nothing lifts it (structural refusals, corrupt data, usage errors).
- schema error (additionalProperties: false) and schema errors /
  docs/data/errors.json (cli.remedy) carry it; batch task errors too.
  Text mode prints "remedy: …" after the message, never suppressed.
- The bridge rule is pinned: no src/ module specifier references the
  zipnative package outside core-bridge, except version.ts's
  zipnative/package.json metadata probe (kept out of the bridge so
  --version never loads the engine).
- tests: utils/remedy, envelope/schema/consistency pins updated.
- docs: AGENTS §2/§3/§7, llms.txt, README, knowledge base, ROADMAP
  (--explain fed by the same table), CHANGELOG [Unreleased].
…itizeEntryPath()

Review finding Q2-F3: `verify` proves integrity and structure (CRC,
sizes, local headers, overlap) and says ok on a zip-slip archive, and the
--check vocabulary had no path-safety assertion, so "verify + inspect
--check" looked like a safety gate it was not.

- inspect: stats.unsafeNames (names the engine's sanitizeEntryPath()
  refuses — traversal, absolute, drive/UNC, NUL, ADS, device names;
  directory names checked without their slash) and the `safe-names`
  check; the text report prints the count on the names line.
- verify --help, README, knowledge base: verify is not path safety —
  gate with `inspect --check safe-names,no-symlinks` or `extract --dry-run`.
- schema inspect, AGENTS §7 gate, README/KB check lists (20 assertions),
  samples inspect/02-check-gates, CHANGELOG.
- tests: inspect-safe-names (hostile raw archive → 2 unsafe, check fails
  after the report; clean archive passes; verify still ok on the same file).
…eprecation policy, locale stance, secret scanning

Review findings Q2-F4 and Q4 P1-5 / P0-3 / P1-7: AGENTS.md, llms.txt,
README and the knowledge base carry the two rules an unattended caller
must encode (--no-config / --config; the envelope is the last stderr
line starting with a brace); CONTRIBUTING gains the versioning,
stability and deprecation policy (public surface, semver rules, the
deprecate() ladder) with a README summary, names secret scanning and
push protection next to the branch-protection ruleset, and states the
npm requirements and why there is no packageManager field; README
states the locale stance.
…le build, moderate audit, commit-message gate, start-up guard

Review findings Q4 P0-2, P1-1, P1-2, P1-8, P2-5, P2-6.

- @cyclonedx/cyclonedx-npm 6.0.1 is an exact-pinned devDependency
  installed from the lockfile; publish.yml runs `npx cyclonedx-npm
  --omit dev` instead of an unpinned `npx --yes …@^1` fetched inside the
  job that holds the OIDC token (that range, 1.8.0, also declared
  engines.npm 6 - 9 and could not have run on the npm >= 11 the job uses).
  Dependabot ignores its majors.
- publish.yml packs the tarball, prints its SHA-256 to the job summary,
  verifies its contents, attests it with actions/attest-build-provenance
  (next to the SBOM), attaches tarball + checksum to the release and
  publishes THAT packed file so the attested bytes are the published bytes.
- ci.yml: every job rebuilds and compares the SHA-256 of dist/cli.cjs
  (verified byte-stable locally), audits at --audit-level=moderate (clean),
  runs the new start-up guard, and a dependency-free commitlint job checks
  Conventional Commits on pull requests (commit subjects + PR title).
- tests/integration/startup-budget.test.ts: the bundle reaches the worker
  only through the lazy import() and its hoisted externals are Node
  built-ins plus the engine (the ≈10 ms top-level engine require stays
  tracked as ROADMAP A-11); start-up overhead over bare Node <= 250 ms
  (min of 3, relative budget).
- docs: SECURITY supply-chain paragraph, README, CONTRIBUTING CI table,
  commit-convention pattern, CHANGELOG.
… noUncheckedIndexedAccess

Review finding Q4-P1-3 and audit A-44 (ROADMAP → Released).

- eslint.config.js: tseslint strictTypeChecked over src/ (type-aware:
  no-floating-promises, no-misused-promises, no-unnecessary-condition, …)
  with three justified relaxations (restrict-template-expressions allows
  numbers/booleans, no-confusing-void-expression ignores arrow shorthands,
  require-await off for the `async (args) => Promise<void>` command
  contract); tests extend disableTypeChecked and keep the strict set.
- tsconfig.json: noUncheckedIndexedAccess (one test fixture adjusted).
- 17 findings fixed without behaviour change: unbound-method thunks,
  unnecessary assertions/conditions, JSON.stringify for manifest
  versions, Error causes on stream failures, a shared streamIsTty()
  (`isTTY` is typed boolean but undefined on a pipe), two documented
  eslint-disable lines (the inflator's mutated `finished`, the
  future-engine-code fallback in ZIP_TO_CLI).
…s, verifyEntry skipped reason

Review finding Q3 (layering exploration): the two compensations the CLI
carries for engine inconsistencies are now filed as human-submittable
drafts under .github/drafts/, in the format of the existing two, each
with an engine-only reproduction, the 1.0.0 source pointers, the
compensation to delete once fixed, the related API asks
(analyzeDeterminism, write-side unixMode), a non-goals check and the
compliance report. All four drafts pass `govern verify-issue`.
ROADMAP and CHANGELOG reference them.
… 1201 tests, coverage 96.32 / 92.52 / 97.93 / 96.91, tarball 120.0 kB)
…ATE, untrack the four engine drafts

The engine repository ignores agent issue drafts (only README/TEMPLATE are
tracked) and pdfnative-cli versions none: a draft is an intermediate state
addressed to a human, not a deliverable, and it goes stale the moment the
engine fixes the issue. This repository had committed four of them.

- .gitignore: `.github/drafts/*` with README.md and TEMPLATE.md exceptions
- .github/drafts/README.md: states the rule, the upstream target and the
  template workflow; TEMPLATE.md carries the sections the compliance report
  and `govern verify-issue` expect (the template itself passes the verifier)
- the four upstream-*.md drafts are removed from the index (they stay on the
  release machine until a human files them in Nizoka/zipnative)
- ROADMAP, CHANGELOG, PR draft and AGENT_RULES no longer point at ignored
  files; they describe the four engine asks instead
- governance-sync test pins the ignore rule, the README and the template
  headings to the policy's draft_location
@Nizoka Nizoka self-assigned this Sep 5, 2026
@Nizoka Nizoka added the enhancement New feature or request label Sep 5, 2026
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

…e length check advisory

The job scanned BASE..HEAD on refs/pull/N/merge, so GitHub's synthetic
"Merge X into Y" commit failed the pattern on every PR. The range now ends
at pull_request.head.sha with --no-merges. A subject over 100 characters
(commitlint's default, not a Conventional Commits rule) is a warning; the
format stays blocking, for the PR title too since a squash merge makes it
the commit subject. The PR draft carries a conforming title.
@Nizoka Nizoka changed the title v1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0 feat: zipnative-cli 1.0.0 — the agent-grade ZIP CLI on zipnative 1.0.0 Sep 5, 2026
The default pull_request types (opened, synchronize, reopened) do not
include `edited`, so a title fix never re-ran commitlint, and a manual
"Re-run" replays the original payload with the old title. All jobs run on
the event: a skipped job would count as a passing required check.
@Nizoka
Nizoka merged commit dd43f98 into main Sep 5, 2026
10 checks passed
@Nizoka
Nizoka deleted the release/v1.0.0 branch September 5, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants