Skip to content

ObjectFS v0.10.3

Choose a tag to compare

@github-actions github-actions released this 03 Aug 03:33
· 143 commits to main since this release
463860e

Part 4 of the v0.10.0 audit: say only what the code does, and bill accurately. The audit found that
documentation, cost figures, and repository metadata each asserted things no mechanism checked, so
most of what follows is a correction paired with the gate that fails if it recurs — five of them,
now running on every PR.

This release exists because of a numbering defect worth stating plainly, since it is the second
instance of the same one. v0.10.2 was tagged when the first of this milestone's twelve issues
closed, and the remaining eleven landed after it — so the tag published under that number holds the
packaging fix and none of the work below, exactly as v0.10.1 was tagged two hours before the
module-path fix it was cut for. A tag is not a promise that can be revised: it is already in the
GitHub releases list and cached by the Go module proxy, which resolves a version to one tree
forever. So the fix is a new number rather than a moved tag, and the rule that prevents a third
instance is to cut the tag from the merge commit that closes the milestone, not from the one that
opens it.

Added

  • A gate that fails when CHANGELOG.md's version headings and its link definitions disagree. Keep a Changelog puts each release in a bracketed heading and defines the link separately at the bottom of the file — two hand edits per release with nothing connecting them, and markdown fails silently in both directions. An undefined reference renders as the literal text [0.10.2] instead of a link; a definition with no section renders as nothing. Neither breaks a build, fails a lint, or produces a visibly wrong page, so the only witness is a reader noticing a heading stopped being clickable, which is not a thing readers report. Both halves had already broken: [0.10.2] was never defined at all, and [Unreleased] still compared from v0.10.1 — so the link that answers "what is on main but not released" spanned two releases and 52 entries of already-released work. internal/config/changelog_test.go checks three properties: every section has a definition and every definition has a section, [Unreleased] compares from the version constant, and each release's diff starts at the release immediately before it. The third exists because that failure is the one that renders and resolves — copying the previous definition and editing only the right-hand side produces a real GitHub diff covering more releases than the section it is attached to. All four failure modes were verified by mutation, which is also how the orphan-definition message got fixed: it printed the URL where the version belonged. This is the same defect the release itself is about, one file over — a fact restated in a second place with no mechanism to notice the two have drifted
  • A gate that fails when documentation names a Go symbol or a CLI flag that does not exist. internal/config/docs_symbols_test.go extracts pkg.Symbol references from fenced Go blocks — checked against the packages that same block imports, parsed with go/ast — and objectfs command lines from shell blocks, checked against the flags cmd/objectfs/main.go actually declares. This is the mechanism #182 asked for, and the point is that it fires at authoring time: correcting the nineteen files that issue cataloged only resets the clock, since a fenced code block is a string as far as the compiler, vet, and lint are concerned. It found eleven defects on its first run, listed under Fixed below, and a companion test compares the flag list against main.go so the two cannot drift apart silently. The admission rule — which references get checked — was chosen by measuring three candidates rather than guessed: checking every lowercase.Uppercase in every Go block gives 93 findings of which 3 are real (s3.Client is the AWS SDK, errors.Is is the standard library), file-scoped imports give 5 of which 3 are real, and block-scoped gives 3 of 3 with no false positives. Its one known blind spot is stated in the test rather than left to be discovered: a continuation block that uses a package imported by the block above it is not checked
  • A gate that fails when documentation links at a page that does not exist. internal/config/docs_links_test.go extracts every relative markdown link from every tracked markdown file and resolves it on disk — relative paths against the linking file's directory, root-absolute paths in docs-platform/ against VitePress's routing rule, where /guide/installation is served from guide/installation.md and /api/ from api/index.md. This is #208's mechanism, and it is a Go test rather than the link checker in CI that issue proposed for three reasons recorded in the file: it needs no network and no new tool, so pre-commit and CI check at identical fidelity; it sits with the gates a contributor already satisfies; and an exemption can carry its reason in code, the way docsExemptFromConfigSchema does. It found 45 dead links, not the 24 the issue catalogued — because #208 was written by walking docs/, and two whole classes live outside it: 13 links into SDK examples/ directories that have never existed, and 8 root-absolute VitePress routes. That gap is the finding, and it is the same shape as docs_test.go's nestedSectionNames: scoping a gate to where the defects were already known is how the next cluster stays invisible. A link target is a path, not a symbol, which is why the symbol gate above cannot see it — a link written as [tuning] followed by (./perf.md) is prose to the compiler, to vet, and to lint, and stays prose after the file is renamed. A third test asserts the walk's reach rather than its findings, and it earns its place: a mutation that made the link regexp match nothing left the resolving test passing on zero links and green, and only the reach test caught it
  • A gate that checks mkdocs.yml's nav against the tree, in both directions. TestMkDocsNavMatchesTheTree asserts that every nav entry has a file and that every page under docs/ is either in the nav or exempt with a stated reason. Both directions, because that is how the defect ran: 47 of 50 entries pointed at no file, and 14 of the 17 pages in the tree were missing from the nav. Checking only that entries resolve would have left the orphans, which is the half a reader loses — a page absent from the nav is a page nobody finds. A nav entry is a link target with a different syntax, and that syntax is why it went unchecked: the link gate walks markdown, and nav: is YAML. It is a line scan rather than a YAML parse for a stated reason — mkdocs.yml carries !!python/name: tags for the emoji and superfences extensions, so decoding it needs a custom resolver or unsafe mode, and the nav is a flat list of - Title: path.md lines that needs neither
  • docs/features/compression.md — what transparent compression costs, and when it saves nothing. The question it answers is the one #186 was filed for: project-level compression saves bandwidth and end-to-end latency, so what else does it buy? Less than you would expect. It names four costs, each measured rather than asserted: a compressed object is not readable by anything but ObjectFS (aws s3 cp and boto3 both write the raw zstd frame to disk with a successful exit status — no error to notice); a 4 KiB read of a compressed object transfers the whole stored object, which is 1,836× / 7,344× / 29,380× amplification at 16/64/256 MiB; enabling compression turns off parallel range reads for every object in the bucket, compressed or not; and on the three tiers with a 128 KB billable floor, compressing a 100 KB object to 40 KB changes the invoice by zero. Byte counts are presented as the result and wall-clock only as an aside, for a reason stated on the page — bytes are a property of the design, latency is a property of the day, and the audit's 15.6×/43×/216.5× and this page's 3.0×/5.0×/12.3× are the same defect measured on different days. Every figure is either linked to the AWS page that publishes it or carries its bucket, region, date, and payload, which is docs-platform/index.md's standard after its hardcoded chart was removed. Two of the numbers #186 itself specified are wrong, and the page states what AWS publishes instead: AWS applies no minimum billable object size to GLACIER, DEEP_ARCHIVE, or INTELLIGENT_TIERING. The archive classes' 40 KB is metadata added per object (32 KB at the archive rate, 8 KB at Standard), which points the opposite way from a floor — compression does reduce the bill there, it just cannot touch the surcharge, which is about 23× the payload for a 10 KB object on DEEP_ARCHIVE. Writing the page found three defects, filed as #228, #229, and #230
  • A gate that fails when .github/labels.yml and the repository's labels disagree — in both directions. internal/config/labels_test.go is the fifth mechanical gate, and it exists because the file is a hand-maintained description of state held on GitHub and nothing compared the two, so they had drifted by nine labels. Both directions, because only one is intuitive: all nine existed on GitHub and were absent from the file, none the other way, so a sync that creates labels from the file is green on every one of them — it has nothing to create. That is the failure mode #190's own acceptance criteria name, and the test for the gate is the one they specify: create a label on GitHub without touching the file and confirm the job notices. Verified by doing exactly that, with a throwaway zz-drift-probe. Colors and descriptions are compared too, not just names — a label the file describes differently from the label that exists is drift with a longer fuse, because the name still filters correctly and nothing looks wrong. #190 proposed a paths:-filtered sync job that runs when labels.yml changes; measurement is why this one runs unconditionally instead. Every drift this repository has had originated on GitHub — two labels created by hand in the web UI, one invented by gh issue create --label, one created by Dependabot, six defaults never deleted — and none of those events touches the file, so a job keyed on the file changing would have fired on zero of the nine. Confirmed with git log -S against the exact - name: form of each rather than assumed. A companion offline test holds .github/dependabot.yml's labels: blocks to the file, which is the seam that left 46 Dependabot PRs unmerged: automerge was named there, defined nowhere, and Dependabot drops a label it cannot find without reporting it
  • internal/awsrates — one table of AWS S3 list prices, with a test that checks it against the live AWS Pricing API. Every rate in it was read from that API rather than from a pricing page, and AWS_PROFILE=aws go test -tags=integration ./internal/awsrates/ re-reads 23 of them and fails on any difference, so a price change is something the suite reports rather than something a report quietly gets wrong. Two things are in the type rather than at the call sites, because both are where the errors below came from: per-request fields hold the cost of one call (AWS publishes per 1,000 or per 10,000), and GBFromBytes divides by 10⁹ (AWS bills decimal GB). The rates are us-east-1, first volume band, Standard retrieval speed on the Glacier classes — stated in the package doc, because a rate table that does not say which band it holds invites the comparison against a different one

Fixed

  • make -C sdks/c clean deleted a tracked file. The clean target removed libobjectfs.h along with the shared library, but only the library is gitignored — the cgo-generated header is checked in, because sdks/c/README.md lists it as the reference copy of the declarations for a reader who has not built anything. So building and then cleaning left git status reporting a deletion the contributor did not make. The committed header was verified byte-identical to a fresh generation before deciding which of the two to keep, since "checked-in artifact" and "stale checked-in artifact" call for opposite fixes. The same Makefile also still exported GOPRIVATE and GONOSUMDB for github.com/scttfrdmn/*, the last copy of a setting removed from CLAUDE.md as untrue — all three repositories are public — and the C SDK builds and passes its 15 tests without it

  • Configuring compression turned off parallel range reads for every object in the bucket, compressed or not. The gate selecting the fan-out asked b.compressor.Enabled(), which reports the local write configuration and says nothing about the object being read — so a mount with compression enabled read large objects serially even when they had never been compressed, were below min_size, had not compressed usefully, or had been written by another tool entirely. Declining the fan-out is right for a compressed object, since a zstd or gzip frame must be decoded from its start and there is no set of independent ranges to assemble; the defect was the scope. The compounding part is who paid: most research data does not compress, so the objects that gained nothing from compression were the same ones losing parallel reads because of it, and v0.10.0's headline feature was off for the whole bucket on any mount that enabled compression. This is audit finding C4 one line above C4's own fix, and it survived for the reason C4 did not — C4 moved bytes that did not need moving, which a byte-count assertion catches, while this merely declined an optimization: nothing failed, nothing was logged. The decision is now the object's. Where GetObject already needs a HEAD for the chunk arithmetic the encoding comes free with it; otherwise the fan-out is attempted and abandoned when a chunk's response carries a Content-Encoding, which keeps the cost on compressed objects — which already pay a whole-object fetch — rather than adding a HEAD to every large read

  • A compressed object read past the end of its stored body reported DATA_CORRUPTION instead of falling back. The stored body of a compressed object is a fraction of the size the caller is told, so a read at a high offset is a range no chunk can satisfy — every chunk gets a refusal and none ever sees a response header to learn the encoding from. The recorded finding for an unsatisfiable range is "the object shrank mid-read", which is the right diagnosis when the object is not encoded and the wrong one here. That ambiguity is now resolved with one HEAD, on a path that has already failed and only for the reads that hit it. Abandoning a fan-out also has to leave no mark on health: several chunks fail from one root cause, s3-reads degrades at a few consecutive errors, and a degraded component refuses reads at the top of GetObject — so one compressed object could have taken unrelated, perfectly readable objects offline. TestFanOutFallbackLeavesNoHealthErrors asserts ConsecutiveErrors directly rather than checking that a later read still succeeds, because RecordSuccess decrements the counter and the successful whole-object re-read that follows every fallback pushes it back down before the threshold — the weaker version passes even with the 416 misclassified as a service failure, which was checked by making that mutation

  • Three of the eight storage classes carried a minimum billable object size AWS does not publish, and two of the three numbers were real AWS figures used for the opposite purpose. StorageTierInfo.MinObjectSize held 40 KB for GLACIER and DEEP_ARCHIVE and 128 KB for INTELLIGENT_TIERING; AWS's storage class table lists min billable object size as NA for the two archive classes and None for Intelligent-Tiering. The archive classes' 40 KB is per-object metadata AWS bills in addition to the object, and Intelligent-Tiering's 128 KB is the size below which an object is not monitored, not auto-tiered, and not charged the automation fee. A minimum and an overhead are arithmetically opposite and so cannot share a field: a minimum replaces a smaller object's size, an overhead adds to it, and under a floor compressing a 30 KB object to 10 KB saves nothing while under an overhead it saves every byte it removes. So the direction of every small-object recommendation was wrong on the two cheapest tiers, and ValidateWrite — which refuses writes below the minimum, itself a policy S3 does not have (#154) — was rejecting writes on the strength of numbers that were not minimums. The two real figures now have fields named for what they are, PerObjectOverheadBytes and MonitoringEligibilityBytes, and the archive classes warn about the surcharge at write time instead of refusing, because packing small files into one archive is a remedy available before the write and not after. What kept this in place was that each wrong value carried a confident comment stating its reason — "40 KB minimum", "128 KB minimum for optimization" — and a number with a stated reason reads as a number somebody checked

  • The archive classes' 40 KB is billed at two rates, and pricing it at one understates the smaller portion 23-fold. 32 KB is charged at the archive class's own rate for the index Glacier maintains; 8 KB at the S3 Standard rate for the name and metadata S3 keeps so the object stays listable. Standard is $0.023/GB-month against Deep Archive's $0.00099, so a caller that sums the 40 KB and prices it once at the archive rate gets the cheap answer for the expensive part — on a 10 KB DEEP_ARCHIVE object that portion is 82% of the true total. ArchiveOverhead returns the split, and calculateObjectCost prices each part at its own rate

  • calculateObjectCost was 7.4% low on every figure it produced, and the helper written to prevent exactly that had no callers. It divided bytes by 2³⁰ to get GB in three places while AWS bills GB-months in decimal GB, and awsrates.GBFromBytes, added for this reason, was used nowhere outside its own package. The test could not see it: it passed 1024*1024*1024, called it "1GB", and asserted 1.0 * CostPerGBMonth — an expectation that holds under both the right divisor and the wrong one, because the test made the same choice the code did. A test that recomputes the implementation's formula agrees with it by construction. The seven subtests that replaced it assert hand-computed dollar literals, and their failure messages name the wrong-answer signatures, so $0.023 for a GiB reads as "something is dividing by 2³⁰ again" rather than as an unexplained mismatch. TestTierSizeThresholdsMatchWhatAWSPublishes pins all eight classes' thresholds with the AWS source URL in each failure message, in the shape of internal/awsrates/rates_aws_test.go — the rates can be re-read from the live Pricing API, but these thresholds are not published there, so a citation naming the page to open is the substitute

  • .github/scripts/sync-labels.sh reported success while syncing nothing, for its entire existence. It parsed labels.yml with a bash regex requiring - name: "..." — double quotes — and the file has used single quotes or bare scalars since it was written, so the loop matched 0 of 78 entries and the script then printed ✅ Label sync complete! and exited 0. #190 concluded from that symptom that labels.yml "is applied by nothing"; what was true is worse, and it is the reason the replacement is careful — the file was applied by something that reported success without doing anything, which is indistinguishable from a working sync in every log it produced. The fix is not a better regex: all three YAML scalar forms are legal, all three are present in this file, and a pattern written against one is blind to the other two. It now parses, reports drift by default and changes nothing without --apply, refuses to sync at all if the parse yields implausibly few labels, and edits labels that already exist rather than skipping them, since color and description drift is drift a create-only sync can never fix. Extras — on GitHub, absent from the file — are named with the command to remove each and are never deleted, because deleting a label removes it from every issue that carries it and that is not a thing to do as a side effect. The header of labels.yml also told the reader to run gh label sync, which is not a gh subcommand at all

  • Nine labels existed on GitHub and were absent from .github/labels.yml. area: sdk and area: ci-cd were created by hand in the web UI — area: ci-cd arriving with a null description and the default grey, which is what gh issue create --label produces when it invents a label it cannot find rather than failing. java was created by Dependabot itself, on the first maven PR after sdks/java gained an ecosystem entry, which is to say the drift #190 documented grew by one while that issue sat open. All three are now in the file with their provenance recorded, and the repository's 81 labels now match it exactly, descriptions and colors included — 78 of them had never been applied from the file at all, because of the sync script above

  • CLAUDE.md described a GitHub repository that does not exist in four places. It hand-transcribed the label taxonomy as two tables holding 8 type, 4 priority, and 12 area: labels against the 22 area: labels that exist — so the document a contributor consults to pick a label was missing ten of the choices, and would go stale again on the next label added. It listed six milestones, five of which are closed, and omitted every open one. Its project-board link used /orgs/scttfrdmn/projects, and scttfrdmn is a user account, so the URL 404s. And its "Private module — set these for go get" block with GOPRIVATE/GONOSUMDB describes a state that has not been true for some time: all three repositories are public, verified by fetching objectfs@v0.10.2 with both variables explicitly empty. Each is now a pointer to the authority rather than a copy of it — labels.yml for labels, the milestones URL for milestones — because a transcribed list is a claim with no way to be told it is stale, which is the same defect the four documentation gates were built to catch in prose

  • mkdocs.yml's navigation described a documentation site that was never written. Fifty entries, three of which resolved: the other 47 named pages under getting-started/, user-guides/, personas/, features/, operations/, architecture/, development/, and api/ that do not exist, plus ROADMAP.md, which is at the repository root and therefore outside docs_dir. mkdocs build fails on an entry with no file, so this configuration could never have produced a site — and nothing builds it, which is why it survived: mkdocs is not installed in this environment, no workflow runs it, and Pages is off on the repository. The nav now lists the pages that exist, grouped as the tree groups them, with the 47 recorded in a comment rather than recreated as stubs

  • Fourteen of the seventeen pages under docs/ were absent from the navigation, including the four most substantial — architecture/overview.md, features/read-ahead.md, features/multipart-uploads.md, and s3-acceleration.md. So the same file both described pages nobody had written and omitted almost all of the ones somebody had. All fourteen are now in the nav; docs/README.md is exempt with its reason, being build instructions for a contributor rather than a page about ObjectFS

  • Six directories under docs/ held a .gitkeep and nothing elseapi, development, getting-started, operations, personas, user-guides. Removed. An empty directory is worse here than no directory: three of the six were targets of the dead links above, and the directory existing is what made those links look plausible to whoever wrote them. Two prose pointers into them survived the link sweep because prose is not a link — one in OBJECTFS.md and one in docs/features/multipart-uploads.md, both written during that same sweep, which is a fair illustration of how the empty directory misleads

  • Both SDK READMEs pointed at seven example programs apiece, and neither examples/ directory has ever existed. Thirteen dead links, in the section a reader goes to after deciding they want to use the SDK — the point of maximum invested attention. The tempting repair is thirteen stub files, which turns the gate green and teaches nothing; #208 names it ("the docs equivalent of the echo \"Would update the Homebrew formula...\" job that v0.10.1 deleted"), so a third test now fails if an examples/ directory appears holding files too small to be working programs. Both sections now point at the inline examples above them, which do exist and do run. The Python README's monitoring pointer went to internal/metrics/doc.go after the obvious target turned out to be an empty directory — docs/operations/ is one of six directories in docs/ with no files in them

  • Eight links in docs-platform/ were VitePress routes with no page behind them: /guide/installation, /guide/troubleshooting, /guide/configuration, /guide/performance, /api/, and three more. These are invisible to a walk of docs/ and to any checker that treats /guide/installation as a filesystem path, which is why they outlived #208's audit. Resolved the two ways that issue permits — point at the page that covers the topic, or delete the link — and in one case the deletion is the finding: there is no installation guide because installation is two commands, so it belongs in the Quick Start rather than in a page of its own

  • Twenty-four relative links in docs/ resolved to files that were never written, and four of them named the same one. performance-tuning.md was linked from four pages — read-ahead, multipart uploads, memory monitoring, and S3 acceleration — which is the pattern worth noting: four authors each assumed the page existed because the others linked it. If it is ever written it has to cite measurements, per the rule added to CONTRIBUTING.md, and that is likely why it never was. ml-training.md was the most misleading of the set, because there is no model to train: the Predictor interface internal/cache accepts is never set on the mount path. docs/index.md lost the whole audience section — five persona links into an empty directory — replaced with the persona names as plain text, since the persona: labels do exist on GitHub and the intent was real

  • docs/features/read-ahead.md documented the way to read predictive-cache statistics, and there is no way to read them. The page called cache.GetPredictiveCache(), which exists under no name. The PredictiveCache a mount builds is wrapped inside MultiLevelCache.initializeLevels and stored as an opaque types.Cache in a level; types.Cache is six methods about bytes, GetLevelStats("L1") returns hits/misses/size, and no exported accessor reaches past either. So prediction accuracy, prefetch efficiency, and cache-hit improvement are computed on every read of every mount and then discarded at unmount. The page now says that, and shows the one way those numbers can be observed today — on a PredictiveCache the caller constructs, which watches the caller's own accesses and nothing the filesystem does. Filed as #223, where the recommendation is to export them through the Prometheus surface the rest of the cache telemetry already uses

  • The playground's Go example imported a package that does not exist and called an API that was never written. github.com/scttfrdmn/objectfs/pkg/client is not in the tree, and neither are the client.Mount, GetHealth, ListObjects, and Unmount calls beneath it; config.Config is config.Configuration. Replaced with the real embedding API — adapter.New, Start, Stop — which was compiled to check it before publishing, along with the caveat that makes it honest: these types are under internal/, so the example builds inside this module and nowhere else, and running the binary is currently the only supported way to use ObjectFS from another program

  • objectfs config validate, config diff, and config generate were documented as runnable, and there are no subcommands. OBJECTFS.md's configuration-validation section offered four commands of which one exists; each of the other three exits 1 with an argument error. They are now shown struck through with what each would have done, since a config-diff and a config-generate remain reasonable things to want, and the one real command — --dry-run, which loads a file, runs every validation rule, and never touches the mount point — is marked as the one that works. --profile high_latency was doubly wrong: there is no profile generator and no profile mechanism of any kind

  • docs-platform/index.md's Quick Start opened with curl -sSL https://get.objectfs.io | sh followed by objectfs mount — an install script for a domain this project does not serve, then a subcommand that does not exist. It now builds from source and mounts with two positional arguments, and notes that the mount runs in the foreground, which the original did not and which is the next thing a reader would have been confused by

  • OBJECTFS.md's table of contents listed eight sections that were never written, and omitted the one that exists. Entries 8 through 15 — Product Family & Roadmap, Monitoring & Observability, Security & Compliance, Operations & Maintenance, Development Guide, API Documentation, Troubleshooting & Support, Appendices — have no corresponding heading at any level; the document ends at Advanced Features, which the list did not mention. So half the contents of a 1,900-line document were links to nothing and its last section was unreachable from the top. The list now matches the headings, records what the eight were, and points at the documents that do cover that ground

  • CONTRIBUTING.md's eight table-of-contents links all resolved to nothing, because every heading they targeted begins with an emoji: ## 🤝 Code of Conduct anchors as #-code-of-conduct, not #code-of-conduct. Both files' broken links were found by markdownlint's MD051, which had been failing on them for as long as the rule has been enabled — the hook only reports files a commit touches, so a defect in a file nobody edits is a defect nobody sees

  • pip install objectfs installed an unrelated project. The Python SDK's README documented three pip install objectfs... commands; the SDK has never been published — no workflow here publishes either SDK — and the name is taken on PyPI by a "Simple Python VFS module" from 2015 by a different author. So the command did not fail, it succeeded with somebody else's code, which is the worse of the two outcomes and indistinguishable from success until an import fails. @objectfs/sdk on npm at least 404s. Both READMEs now install from this repository and say why

  • The Python and JavaScript SDKs declared themselves MIT, and this project is Apache 2.0. setup.py's classifier and package.json's license field, which are exactly the places a licence scanner or a dependency review looks — the Java SDK's pom.xml had it right, so the three shipped manifests disagreed with each other and two disagreed with LICENSE. Both now say Apache-2.0. The team@objectfs.io author address went with them: it is not a deliverable mailbox

  • docs-platform/guide/index.md was the last document claiming POSIX compliance, and checking the rest of the page found more: hot configuration reloading (SIGHUP is registered alongside SIGINT/SIGTERM and treated as shutdown, so sending it unmounts the filesystem), "Authentication and authorization with RBAC" (there is no auth code — zero hits for RBAC, Authorize, or Authenticate outside tests), load balancing and distributed clusters as shipping features (nothing outside internal/distributed imports it), S3 lifecycle management (no lifecycle API call exists anywhere in the tree), Kubernetes persistent volumes (no CSI driver, no chart, no manifest), and "Compliance and governance" (no audit log). Its architecture diagram drew the distributed coordinator as a solid component and omitted both layers of the v0.10.1 refactor; it now shows internal/fuseinternal/vfs and draws the coordinator dashed, because that is what it is

  • Dead-domain links removed from six files: a community forum at community.objectfs.io, a documentation site at docs.objectfs.io, and commercial support at mailto:support@objectfs.io — none resolves, so a reader with a problem was sent to three dead ends before reaching the issue tracker. mkdocs.yml's site_url pointed at objectfs.io, a parked registrar page: no site is published from either documentation tree and no Pages workflow exists, so it named a domain this project does not serve in every canonical link and sitemap entry a build would emit. All now point at the repository, which is where the documentation is

  • CONTRIBUTING.md told contributors to use LocalStack. This project does not use LocalStack and never has; CLAUDE.md, DEVELOPMENT.md, and the CI workflow all say to use real AWS or the in-process substrate emulator. The section now names internal/testaws as the default choice, real AWS behind -tags=integration for what an emulator cannot answer, and a hand-written mock as the last resort — with the reason, which is that a mock sits on the far side of a seam and agrees with its caller by construction, and that is how 32,680 lines of tests missed roughly 45 defects. Its "Running Tests" block also omitted -race from every command; it now shows what CI runs

  • The supported-operations table now names the errno for every unimplemented operation, and a test holds it to that. Six rows said only "not implemented", which tells a user nothing about what their tool will print or branch on. The errnos are not ObjectFS's to choose — with no method on DirectoryNode, the answer is go-fuse's default for the absent interface, and those defaults are neither uniform nor guessable: rename, symlink, link, mknod, and fallocate give ENOTSUP; getxattr and removexattr give ENOATTR, which is ENODATA on Linux; listxattr succeeds with an empty list; and unlink and rmdir default to success, which is why this package implements them only to refuse. mv had in fact been documented as ENOSYS on the strength of a reasonable guess, and it is ENOTSUP — a difference that matters, because ENOSYS means "this filesystem will never do this" and a caller may stop asking. internal/fuse/unimplemented_test.go now drives the real fuse.RawFileSystem (no mount, no macFUSE, no privileges) and asserts each one, so a go-fuse upgrade that changes a default is something the suite reports rather than something the README quietly gets wrong

  • The locking row said "not implemented", and locks are not refused — they are host-local. The mount does not set go-fuse's EnableLocks, so the kernel never negotiates CAP_POSIX_LOCKS/CAP_FLOCK_LOCKS and never asks ObjectFS to arbitrate; it tracks locks itself, on the mounting host. flock therefore succeeds and means nothing to any other mount of the same bucket, which is a worse failure than a refusal and a different one than the README described: two hosts will both believe they hold the same exclusive lock. Asserted alongside the errnos, in both directions — setting EnableLocks without implementing Getlk/Setlk/Setlkw would flip every locking caller to ENOTSUP, SQLite included

  • Every write was costed at a tenth of its price on the default configuration. internal/storage/s3 stored the Standard PUT rate as 0.0005, which is what AWS charges per 1,000 requests, in a field the code then divided by 1,000 again — so a PUT was reported at $0.0000005 against a real $0.000005. internal/cost had the same rate right, which is the more instructive half: the two packages disagreed by 10×, so what an operation cost depended on which package a caller reached for, and neither was flagged by anything. Verified against the live Pricing API, which also turned up two more the issue had not recorded: Glacier Instant Retrieval PUT was 4× low and its GET 5× low

  • Storage costs were 7.4% low, everywhere. internal/cost converted bytes to GB by dividing by 2³⁰, with a comment asserting the binary reading was correct. S3 quotes GB-months in decimal GB, so the correct divisor is 10⁹ and the ratio between them is exactly 1.0737. The comment is why it survived: it made a wrong unit look considered rather than mistaken, so a reader checking the line found a deliberate-looking choice and moved on. The tests could not catch it either — they passed 1024*1024*1024 bytes, called it "one GB", and asserted the per-GB rate came back, which is an expectation that holds under both the right divisor and the wrong one. The new tests state hand-computed dollar figures as literals instead

  • Glacier Flexible Retrieval's retrieval rate was 0.02/GB with the comment "Variable based on retrieval speed" — which is not a rate, it is a record that nobody had established which of the three retrieval speeds it was. AWS charges $0.01/GB for Standard retrieval; the table now says so, and the package doc names the speed each Glacier figure assumes

  • internal/cost/pricing_drift_test.go compared DefaultPrices against its own hand-written copy of the rates, on the stated grounds that importing internal/storage/s3 would create a cycle. There is no cycle — go list -deps confirms s3 does not import cost — so the literal was a third copy of the rate card, introduced by the test whose purpose was to catch there being more than one. It also had no entry for REDUCED_REDUNDANCY, so that tier went unchecked by the drift test that existed to check tiers. It now compares both tables directly against internal/awsrates, for every class the config loader accepts

Changed

  • make test now runs the race detector, which it did not, while CLAUDE.md and CONTRIBUTING.md both said every test in this project runs with it. The local gate was therefore weaker than the CI gate it stood in for — and this repository had sixteen concurrency bugs filed after a document declared it race-free, most of them found by the detector. test-race is kept as an alias so existing habits and scripts keep working
  • CONTRIBUTING.md states the rule for performance claims that no test can enforce. A throughput, latency, or speedup figure in documentation must cite the benchmark that produced it by file and function, the parameters it ran with, and a copy-pasteable command; without all three, say nothing about throughput. This is the procedural half of #182's gate, and it is a rule rather than a test because a number in prose cannot be compared to a benchmark automatically. The audit's most-repeated false claim was "4.6x throughput improvement" in 21 places across 9 files, uncheckable by construction, attributed to a congestion-control implementation with no caller on any mount path — which is why it survived nine files' worth of review. The four gates that are mechanical are listed in a table alongside it, so a contributor whose PR fails one knows where it lives
  • The five copies of the S3 rate card are now one. Rates lived in internal/cost/pricing.go, internal/cost/reporter.go, internal/storage/s3/tiers.go, internal/storage/s3/doc.go, and internal/analytics/model.go; all five now read from internal/awsrates. This is the shape fix rather than the value fix — correcting the 10× error without it would leave the arrangement that produced the error, and #209 named the consolidation as the prerequisite that makes wiring up the real Pricing API (#183) a single-site change instead of five. StorageTierInfo.CostPerGBMonth is now filled in at init and panics if a tier has no rate, because the alternative is a zero, and a cost report showing $0 reads as free storage rather than as a lookup that missed
  • internal/storage/s3's package doc no longer quotes a per-GB price for each tier, and the storage-class summary table has lost its Cost/GB column. A rate in a doc comment has no way to be told it is stale, so the only question is when it starts lying. What stays is the part that is S3 behavior rather than S3 price — minimum billable size, minimum storage duration, retrieval latency — which changes when AWS changes the product, not when AWS changes a number
  • internal/cost.Reporter.Report's doc said to "pass 0.023" for the ROI baseline, making it a sixth place the Standard rate was written down. There is now a StandardBaselinePerGB derived from the rate table, so savings-versus-Standard is measured against the same rate everything else is charged at. A baseline drifting from the live rate is the hardest kind of discrepancy to notice: cost figures stay right, savings figures go wrong, and only their difference is wrong
  • No document claims a throughput figure any more. "4.6x" appeared 24 times across 10 files — in the internal/storage/s3 package doc, in docs/index.md's performance table, in the ROADMAP's success criteria — attributed variously to BBR, to CargoShip, or to both. It is CargoShip's number for CargoShip's workload, restated as a property of ObjectFS; nothing here measured it and no benchmark here can produce it. The mechanism it was credited to is not even on the path: internal/network's BBR surface (NewBBRDialer, BestAvailableDialer, IsBBRAvailable) has no caller outside its own tests, and what runs is a best-effort TCP_CONGESTION socket option on Linux and a plain net.Dialer elsewhere. Each site now names the mechanism, or cites benchmarks/ and the parameters a figure would need. The 0.1.0 changelog entry keeps its figures under a banner saying they were never measured, for the same reason the withdrawn 0.10.0 entry is still here: a changelog records what was published
  • docs/index.md gained a Not yet wired up table, and the features in it left the feature list. Cost tracking, archive access, the REST API, detailed per-file metrics, ML tier prediction, the Redis cache, and multi-node coordination all have code and documentation pages and no path from a mount that reaches them. Verified by import graph rather than by reading the code, which is what caught the two that are subtler than "no importer": internal/analytics is imported by internal/cache, but the Predictor field is never set on the mount path, so the size heuristic always runs; internal/cache/redis is selected by cache.NewFromConfig, but nothing calls NewFromConfig — the adapter constructs NewMultiLevelCache directly. Listing them beats deleting the pages: the code exists and may be wired up, and a reader deserves to know which column a feature is in
  • docs/ARCHITECTURE_EVOLUTION.md, docs/PLATFORM_STRATEGY.md, and docs/CARGOSHIP_MODULARIZATION_REQUEST.md open with banners recording that they are 2025 design sketches and where the code diverged from them, in both directions. The multi-protocol path is unimplemented — no SMB, no NFS, no internal/protocols. The shared/aws-optimization module was never built; ObjectFS imports pkg/aws/s3 and pkg/aws/config from CargoShip directly. CargoShip now does export pkg/s3optimization, holding different components than the one requested, and ObjectFS imports none of it. The divergence is left visible rather than edited away — the Phase 1 layout proposed internal/filesystem + internal/protocols/fuse and what shipped is internal/vfs + internal/fuse, and saying so is more useful to a reader than a document that appears to have predicted itself
  • CLAUDE.md's architecture line still routed through cgofuse and skipped both layers of the v0.10.1 refactor. It now reads Kernel VFS → FUSE (go-fuse) → internal/fuse → internal/vfs → Adapter, which is what the code does
  • No document calls ObjectFS "POSIX-compliant". It appeared in OBJECTFS.md three times, in docs/VISION.md three times, in docs/DESIGN_PRINCIPLES.md as "✅ Full POSIX compliance", and in internal/adapter's package doc — against roughly ten of forty VFS operations, with no rename, no links, and no xattrs. Each site now says what is true: a POSIX interface, with the README's supported-operations table as the authority. OBJECTFS.md previously carried a banner noting the claim was wrong while the body went on making it three more times, which is the weaker fix — a reader who skips the banner reads the claim, so the sentences no longer make it
  • docs/VISION.md listed "Compliance (HIPAA, FISMA, SOC 2)" as a medium-term objective. Removed rather than reworded: ObjectFS holds no certification under any of the three, adds no authorization layer of its own, and writes no audit log. The heading did say these were aspirations, and that is a real defence — but a procurement reviewer scanning bullets does not read headings, and the same claim was removed from the documentation platform's landing page in v0.10.1 for that reason
  • docs-platform/guide/getting-started.md and docs-platform/playground/index.md had their shell commands corrected against objectfs --help. Between them they opened with curl -sSL https://get.objectfs.io | sh and went on to use objectfs mount, objectfs unmount, objectfs list-mounts, objectfs status, objectfs health, and objectfs metrics --watch — none of which exist; the binary has no subcommands and takes two positional arguments. Also gone: an apt repository, a Homebrew tap, an AUR package, "Windows with WSL2" as a prerequisite, and two flags (--enable-predictive-caching, --cost-optimization) that are not parsed. These pages are in a tree that cannot build (#214), and were corrected anyway because a markdown page is read on GitHub whether or not the site renders, and a wrong curl | sh is something a reader will type. Their SDK code blocks are deliberately left alone, with the reason stated in-page

Removed

  • Six of GitHub's nine default labels, which duplicated the type: family and had never been used: bug, enhancement, documentation, duplicate, invalid, and wontfix, each shadowed by type: bug, type: enhancement, type: documentation, resolution: duplicate, resolution: invalid, and resolution: wontfix. Confirmed at zero issues and zero PRs apiece before deleting; the seven issues carrying the bare enhancement label were relabelled type: enhancement first, since deleting a label removes it from everything that had it. good first issue, help wanted, and question are kept — they collide with nothing and .github/labels.yml declares all three. Project board #11, "ObjectFS v0.5.0 Development", is closed for a related reason: all 14 of its items were closed issues, nothing had touched it since February, and its title named a release five versions back. CLAUDE.md linked it as a place to track work
  • docs/performance-metrics.md — 618 lines describing internal/metrics/detailed.go, with nine verified defects: a zero-argument constructor for a function taking four arguments, a NewDetailedPerformanceMetricsWithOptions that never existed, three getters that do not exist, OpMkdir/OpRmdir/OpStatfs against the real OpMkDir/OpRmDir/OpStatFS, an undeclared CacheSourceNone, a four-argument RecordNetworkOperation, and two behaviours documented as working that are not. It was absent from the site nav with no inbound links. Replaced by a short section in the internal/metrics package doc — short so that it can stay true, which a separately-maintained 618-line description of one 600-line file cannot. Two of the nine were code defects rather than documentation defects, and are recorded there and filed: P50Latency/P95Latency/P99Latency are declared and never assigned, so anything serializing the struct publishes zeros as percentiles (which reads as a fast filesystem); and LatencyHistogram is indexed by int(latency.Milliseconds()) % 100, so 1 ms, 101 ms, and 201 ms share a bucket — a modulo, not a bucketing

Verify a download:

sha256sum -c objectfs-<platform>.tar.gz.sha256

Container image: ghcr.io/scttfrdmn/objectfs:0.10.3

Full changelog: https://github.com/scttfrdmn/objectfs/blob/v0.10.3/CHANGELOG.md