Changelog0.2.0

Fixed

Fixed in fragcap 0.2.0.

Found by automated review of pull request 13, before the slice merged. All five are in code this slice introduced, so nothing shipped with them; they are recorded because the reasoning is worth keeping and because three of them were invisible to the tests as written.

  • A retained wildcard bind stopped matching. Retention was keyed by endpoint and looked up the packet's own local address, while a socket bound to 0.0.0.0:30000 was retained under the wildcard. Every flow that resolved through the section 8.4 wildcard allowance while live therefore became unattributable the instant its socket closed, which is the whole class of UDP game sockets and the tail of every one of their flows. Retention now resolves through the same matcher as the live path.
  • Retention kept one socket per local endpoint. Several sockets can occupy one: a server holds a row per client on a single port, and a reused port is two sockets in sequence. The map overwrote, so only whichever row the platform reported last survived, the others' tails were lost, and the creation-time ordering of FR-008a did not reach retention at all. Retention is now keyed by socket identity rather than by endpoint.
  • Retained attributions lost their image name, or gained the wrong one. Every refresh re-resolved every retained process identifier. A process that had exited was no longer in an enumeration, so a name once known was dropped; worse, an identifier the platform had reused resolved to a different process, attaching its name to a connection it never opened. That is a confidently wrong report of the kind constitution P-9 exists to prevent. A retained record now carries the name it was captured with and is never re-resolved.
  • A refresh could erase a request made against the index it had just published. The index was published before the schedule was marked refreshed, leaving a window in which a capture thread could read the new index, find an endpoint it still did not carry, and record a request that the mark then cleared. Because recording it also consumed the rate-limit window, nothing could re-arm for two hundred milliseconds and a short-lived flow would stay unattributed until the next periodic refresh. The order is reversed: an extra table read is cheap, a missed one loses attribution.
  • Two capture threads could both claim one rate-limit window. The trigger loaded and then stored, so two callers could pass the same check before either wrote. The window is now claimed with a compare-and-exchange.

The first three share a cause worth naming. Retention was written as a lookup of its own rather than as the live path with a different fidelity and an expiry, and every one of the divergences followed from that. It is now the same code path, which is what the specification meant by a grace period all along.

  • The getting-started guide no longer tells a first-timer to run a profile that does not exist (issue #43). No profiles ship bundled, so the guide now has the reader obtain one before the first capture, either by scaffolding from an installed Steam title with fragcap steam profile <APP_ID> or by authoring one, and states plainly that the eso used in the example is illustrative rather than a shipped profile. This is the docs-site counterpart of the same correction made to the README.

Withdraws a security claim from specification 0.1.1-draft. That revision asserted that a focal title passes a live session credential on its client's command line and described the mechanism as documented. It was inference from a parameter name, unsupported by the capture. An entropy scan of 3,694 command lines found no credential in either focal title. The command line handling rules in section 10.2 stand, rescoped to the operator-identifying data that was actually measured.

  • fragcap run --roles no longer panics. RunArgs.roles was declared with a value_parser returning Vec<String> over an Option<Vec<String>> field; clap derives the element type from the Vec and panicked at access time on the type mismatch, so any run --roles invocation aborted. No test exercised it, so it had gone unnoticed. This slice's extcap-versus-run parity test surfaced it. Both run and extcap now split the comma-separated roles with clap's value_delimiter, and the parity test covers --roles.

2026-08-11: shell wrappers (slice S18 sub-slice B). Pinned-artifact and design decisions.

  • Pinned artifacts changed, recorded here. This slice adds scripts/Invoke-FragCap.ps1 and scripts/fragcap.sh (both under the pinned scripts/**) and a wrappers step to .github/workflows/ci.yml (a pinned workflow). The step runs cargo run --package xtask -- wrappers on both the ubuntu and windows legs, after the licensing step.
  • The un-vendored Bash standard was resolved by authoring, not vendoring. The ShruggieTech PowerShell standard is vendored with a compliance checker; the Bash standard and a Bash checker are not on disk, a gap the foundation doc flagged as "must be resolved before S18." The operator chose to proceed: fragcap.sh is authored to the real ShruggieTech Bash standard, and a Bash structural checker is authored in xtask (a pure function over the file bytes, unit-tested against known-bad input like lint.rs). skills-lock.json is unchanged; vendoring the shruggie-bash skill itself remains a separate operator tooling task. The PowerShell wrapper reuses the vendored Test-ScriptCompliance.ps1 (its POSIX twin, so only bash is needed to run it).
  • A shell script's shebang forced a lint refinement. #!/usr/bin/env bash must be a Bash script's first line, which conflicts with the SPDX-first-line rule. xtask/lint.rs now accepts a first-line shebang and requires the SPDX identifier on the second line instead. xtask is not a pinned artifact.
  • The gate runs bash with relative paths from the repository root. An absolute drive-letter path (A:\...) is not one WSL bash can resolve; a relative path under current_dir(root) resolves under native bash, Git Bash, and WSL bash alike. The PowerShell runtime checks are best-effort: they run when pwsh is present and are skipped (not failed) when it is not, since the vendored checker's POSIX twin already validates the PowerShell script's structure with bash alone.
  • The wrappers' runtime behavior is tier 2. The elevation self-relaunch, real driver and interface detection, live capture, and WSL2 interop against a native binary do not run in continuous integration, exactly as live capture has not since S09. Continuous integration verifies the compliance checkers, the syntax validity, the help paths, and the templating and pass-through through --dry-run.
  • Review hardening (Codex review of PR #35). The PowerShell wrapper reconstructs the elevated child from its bound parameters (an elevated $args drops values already bound), waits on the child and propagates its exit code, resolves fragcap.exe beside the wrapper in the release archive before the PATH fallback, and reports the capture driver's version. The Bash wrapper heads the invocation with the resolved binary in every case (not only when a WSL path is translated), rejects a missing option value with exit 2, and prepares the output directory before capture. The wrappers gate now requires pwsh and adds a real PowerShell parse (a missing parser is exit 2, never a false pass), the Bash checker rejects invalid UTF-8 and verifies the section 18.3 fixtures including safe_run, and the ci.yml step is scoped to the ubuntu leg, which carries both bash and pwsh (the windows leg carries no bash on PATH).

Dated 2026-08-11. Sub-slice S18c-2 of roadmap slice S18 (the documentation website). Records the pinned-artifact changes, which the constitution requires be made only with a dated decision.

  • .github/workflows/docs.yml is rewritten from the skeleton to a real build and deploy. The skeleton dispatched manually and exited 1 by design, because a workflow that runs automatically and fails trains readers to ignore it. There is now a site to build: on a pull request it builds the static export and asserts the .nojekyll and CNAME markers without deploying; on the default branch it builds and deploys to GitHub Pages through upload-pages-artifact and deploy-pages, with pages: write and id-token: write and a github-pages environment. Hosting stays GitHub Pages behind Cloudflare DNS (the runbook is operator-run, documented, and uses no Cloudflare token in continuous integration), unchanged from the sub-slice S18c-1 decision. The build step goes through the same cargo run --package xtask -- docs build entry point local development uses, rather than calling pnpm build directly and reimplementing the marker assertions; that keeps any setup or assertion later added to the xtask command from being bypassed by the artifact that gets deployed (raised in review), at the cost of installing the Rust toolchain in the workflow.

  • .github/workflows/links.yml is rewritten to a weekly schedule. External link liveness is a property of the outside world, not of a change, so it runs on a Monday cron rather than per commit; workflow_dispatch is kept for an on-demand run. It runs bash scripts/lint-docs.sh link, which exits 2 (could not run) when curl is absent and 1 on a dead link.

  • Node and pnpm are pinned in docs.yml: Node 24, pnpm 9.15. These match the toolchain the committed pnpm-lock.yaml and every local build were produced with, so a Pages build cannot diverge from what was verified locally. pnpm install --frozen-lockfile fails rather than resolving a different graph.

  • ci.yml is unchanged. Its docs check step landed with sub-slice S18c-1 and already gates the glossary linter on every push; the website build is not a continuous-integration gate but a deploy pipeline, so it lives in docs.yml.

  • The glossary is rendered into the content tree at build time, not committed. docs/glossary/ is the single source; site/scripts/prebuild.mjs renders it into site/content/docs/glossary/, which is gitignored and excluded from the conventions linter (it is linted at its source). A committed second copy would be a drift surface for no benefit.

  • Static search uses the framework's built-in engine, no custom tokenizer. The default engine already indexes by heading and splits queries on underscores and hyphens, which is what FR-009 asks for; this was verified against path_regex and 5-tuple rather than assumed. A hand-rolled tokenizer would add a maintenance surface to reproduce behavior that already holds.

  • The conventions linter's SPDX rule now covers the site source faces. SOURCE_EXT in xtask/src/lint.rs gains ts, tsx, mjs, and css, so the TypeScript, TSX, ES module, and CSS files carry the Apache-2.0 SPDX identifier on their first line like every other source file (CONVENTIONS.md), enforced mechanically rather than by convention. Raised in review, where the new site tree had been left outside the rule. Content files (Markdown, MDX, JSON) are not source and stay exempt. The three first-party brand CSS token files gain the same header.

  • site/tsconfig.json stays under the encoding checks. An earlier revision excluded it alongside the generated next-env.d.ts on the assumption Next rewrites it with CRLF; Next 16 does not (the committed file already carries the options it wants), and .gitattributes normalizes it to LF, so excluding a tracked configuration file would only have hidden a real line-ending violation. Raised in review. Only the generated, gitignored next-env.d.ts remains excluded.

  • docs.yml and links.yml are watched to completion once before being reported as passing, like platform.yml. Neither has run against a real site before this slice.

2026-08-11: documentation site foundation (slice S18 sub-slice C, part 1). Pinned-artifact, specification, and design decisions.

  • Pinned artifacts changed, recorded here. This slice adds scripts/lint-docs.sh (under the pinned scripts/**) and a "Documentation check" step to .github/workflows/ci.yml (a pinned workflow), scoped to the ubuntu leg because it needs bash, mirroring the wrapper gate. The site build and deploy workflows (docs.yml, links.yml) are not touched here; they deploy the site and land with the site in sub-slice S18c-2.
  • S18c is delivered in two sub-slices under one roadmap id (operator decision, 2026-08-11). Part 1 (this change) is the deterministic, fully-verifiable foundation: the glossary split, the section 4.4 amendment, the documentation linter, and cargo xtask docs, all green under cargo xtask ci. Part 2 (S18c-2) is the Fumadocs on Next.js site and the brand application, which need a network install and a static build. This keeps a verified, mergeable checkpoint out of a large slice; the single v0.2.0 release waits for S18c-2.
  • Specification section 4.4 amended in-slice, against the usual defer-to-release convention. Section 22.4 binds the glossary split to "one page per category from section 4.4," and the authored glossary already carried an eighth category ("Command Line and Diagnostics", eight entries) not in section 4.4's seven. Splitting into eight pages while the spec said seven would be an internal contradiction the analyze gate must not ship, so section 4.4 gains the eighth category here (operator decision). Section 22.4 references section 4.4 without a hardcoded count, so no numeric edit was needed there.
  • Hosting stays GitHub Pages; Cloudflare serves DNS only (operator decision). Sections 22.1 and 23.2 name GitHub Pages and reject a vendor hosting account. The domain living on Cloudflare is a DNS fact; no Cloudflare credential enters continuous integration and wrangler is not a dependency. The Cloudflare DNS records and the GitHub Pages settings are an operator runbook applied by hand after merge, out of scope for the code slice.
  • The completeness check requires a prose blurb or detail and rejects empty sections, but does not mandate references on every entry. Specification section 4.6 lists a references section among entry completeness, but the authored glossary carries references only where a primary source exists (14 of 125 entries): much of the glossary is fragcap's own internal vocabulary (for example "Sink thread") for which no primary source exists, and fabricating one would violate P-9. The linter therefore requires a prose blurb or detail on every entry (not merely a metadata marker), and validates that a references section or matters callout, where present, is not empty, rather than mandating references on every entry. Tightened from an earlier "one non-blank line" rule in response to the Codex review.
  • The Undefined Term Rule is enforced for glossary references, not by a free-text scan. The linter's check verifies that every glossary reference (a Markdown link into the glossary) in the canonical documents of section 4.2 names a defined term, in addition to the glossary's own cross-link graph. A full undefined-term scan over all prose would need a marked-term list the project does not maintain and would false-positive on ordinary English; the enforced mechanism is the glossary reference the documents actually use to mark a term. The canonical-document scan was added in response to the Codex review.
  • Glossary cross-links are repository-relative sibling paths. CONVENTIONS.md requires relative links between repository documents so they resolve on disk and on GitHub. The split emits <category>.md#<anchor> sibling links rather than the site-absolute /docs/glossary/... form (which resolved outside the repository when a source page is read on GitHub). The site build maps these to routes in sub-slice S18c-2. Corrected in response to the Codex review.
  • The linter stays non-executable and is invoked through bash. It is committed mode 100644, like scripts/fragcap.sh, and every documented invocation uses bash scripts/lint-docs.sh, which is how continuous integration and the wrappers gate call the repository's shell scripts. Adding an executable bit would diverge from the sibling wrapper.

2026-08-10. The brand session resolved Q-7 and Q-8, and the approved kit is vendored into the repository.

  • Q-7 is Geist Mono. The monospace face is a selection requirement, not a style choice, because users read packet payloads in it. Geist Mono keeps 0 O, 1 l I, 8 B, 5 S, and 2 Z distinguishable at the interface size, and its family relationship to Geist keeps technical specimens from feeling detached from surrounding documentation. Any future replacement is evaluated against the real-format specimen in brand/specimens/, not a decorative alphabet.
  • Q-8 is an independent ShruggieTech sub-brand. fragcap shares Space Grotesk, Geist, Geist Mono, dark-first discipline, and the parent's exact #FF5300 orange, but not ShruggieTech green, the shruggie mark, or marketing layouts. The visible relationship is the endorsement "A ShruggieTech project" in Geist Mono, uppercase, subordinate, and outside the logo's clear space. There is no combined parent-product logo. This satisfies the section 23.3 "instrument, not weapon" posture, which is part of the security posture rather than decoration.
  • The kit is vendored under brand/, not embedded in a crate. The assets are not part of any published crate, so the per-crate license discipline is unaffected. The fonts remain under the SIL Open Font License 1.1, with the license texts carried in brand/fonts/licenses/; the logos and the custom wordmark are the project's brand marks, usable under the treatment rules in brand/README.md rather than under the code's Apache-2.0 grant.
  • brand is added to the linter's excluded-directory list (xtask/src/lint.rs), matching the vendored-content rationale already used for the skills directories. The binary fonts and images are skipped by content sniffing, but the PDF guide is text-like in its first bytes and the vector art is machine-generated, so the whole directory is treated as vendored content re-imported wholesale rather than edited in place. Text files were normalized to LF on import so the exclusion is a policy choice rather than a workaround for dirty bytes.

2026-08-08 The ci workflow gains a publication licensing step, and its header note is corrected to record that the check matrix has now run and passed while neutrality and msrv have not. The previous note claimed the workflow had never executed because there was no remote, which stopped being true when slice S01 integrated through pull request #1.

2026-08-08 The eight crate names are reserved on crates.io at 0.1.0, the version already declared in the workspace manifest, rather than at a 0.0.0 placeholder. The facade crate cannot be published without its six dependencies already in the registry, so reserving the headline name means publishing the whole graph; each crate's README states that the release is a skeleton so the listing does not overclaim.

2026-08-08 release.toml is added, and configured to move the version number and nothing else: no tag, no push, no publish. A release tool that commits, tags, pushes, and publishes in one step cannot coexist with an integration workflow where nobody pushes to main directly. Splitting the steps keeps the tag and the publish as separate authorized acts rather than side effects of a version bump.

2026-08-08 The release workflow stops exiting non-zero and becomes real. Its jobs run in the order specification section 24.4 states: build artifacts, generate checksums, create the release with notes from the changelog, then publish to the registry. Publication is last because it is the only step that cannot be undone, so a failure leaves a release that was never created rather than crate versions with nothing to download.

2026-08-08 Publication is gated on the crates-io environment, which requires a human to approve the run. That makes the constitution's rule against publishing without explicit authorization mechanical rather than remembered. Registry credentials reach the job as CARGO_REGISTRY_TOKEN from that environment, scoped to publish-update on fragcap-*.

2026-08-08 Publication order lives in cargo xtask publish rather than in the workflow, as section 24.4 requires. crates.io rejects a crate whose dependencies are not already in the registry, so the order is load-bearing; encoding it in Rust means a unit test asserts it against the dependency graph, and a reordering that would break a release fails a check instead.

2026-08-08 cargo xtask publish treats an already-published version as a skip rather than an error, which makes a run resumable. Uploading eight crates is eight network operations, and an interruption after the third would otherwise leave a release permanently half published, because rerunning would fail on the first crate and stop. Detection matches cargo's message text, which is unpleasant but is what cargo offers: there is no skip-existing flag and no distinct exit code, and querying the registry over HTTP would put a network client into a crate that deliberately has no external dependencies.

2026-08-08 Release artifacts are knowingly incomplete against specification section 24.5, which specifies an archive carrying the binary, both shell wrappers, the bundled profiles, the license, and the notice. The wrappers arrive with slice S18 and the profiles with S05, so those directories are empty and the archive cannot yet be complete. Rather than ship a partial archive that looks finished, the packaging step names the absent components, writes them to INCOMPLETE.txt inside the archive, and warns in the log. The record is generated from what is actually on disk, so it retires itself when the components land.

  • 2026-08-12 release.toml and scripts/** are pinned artifacts, changed here to add the release-preparation script pair (scripts/cut-release.sh, scripts/New-Release.ps1) and to record the sequence they consolidate. The script wraps cargo release rather than replacing it, so the version bump stays the tool release.toml configures; it stops before the tag push and the crates-io environment approval, so both authorizations the constitution requires are still separate human acts rather than side effects of a version bump.
  • 2026-08-12 The changelog assembler and the release-notes derivation live in cargo xtask (Rust), not in the shell scripts, per the house rule that a wrapper which parses text is a missing capability in Rust. The transform has a canonical section order and a merge rule that are worth a unit test, and a reimplementation in two shell dialects would be two untested copies of the same logic. The scripts stay thin orchestrators over git, cargo, and the task runner.
  • 2026-08-12 The release scripts regenerate goldens by targeting the two regenerating test binaries (cargo test -p fragcap --test goldens and cargo test -p fragcap-cli --test cli_run) rather than a workspace-wide test run. This corrects the first cut of this release, which failed at golden regeneration: a workspace run also executes the corpus and extcap conservation checks, which read the same goldens and refuse to regenerate on principle, so running them in the same pass compared new output against a not-yet-rewritten golden and failed. scripts/** is a pinned artifact, so the corrected test selection is recorded here as a dated decision.

2026-08-10 The release-versioning scheme is codified, and there is one first public release. v0.1.0 is the crates.io namespace-reservation stub already published and carries no functionality. The first public release is v0.2.0, and it comprises the whole roadmap: all eighteen slices, S01 through S18. It, and the crates.io publication of the functional crates, happen only after every slice is complete and operational; there is no earlier functional release. This reverses an earlier same-day decision that split the roadmap across two functional releases (v0.2.0 at the end of S14, v0.3.0 for S15 through S18); the split is retired. The specification is corrected to match: section 3.3 success criteria are no longer partitioned (v0.2.0 is complete when SC-1 through SC-7 hold), section 27.3's release table collapses to a single functional release, section 28 is retitled "Roadmap Beyond v0.2.0", and the scope prose in the specification, its outline, and the plan documents is updated. The workspace manifest stays at 0.1.0; the bump to 0.2.0 is a release-time cargo release minor action taken only once every slice is complete, so no code artifact changes here.

2026-08-10 The 0.2.0 version bump is necessary but not sufficient at release time, and the release runbook must say so. The pcapng USER_APPL and the JSON Lines VERSION are both concat!("fragcap/", env!("CARGO_PKG_VERSION")), so cargo release minor changes them from fragcap/0.1.0 to fragcap/0.2.0. That moves the value embedded in every committed golden and the two assertions that pin it (crates/fragcap-sink/src/pcapng/mod.rs and crates/fragcap-sink/src/json/mod.rs), so the release commit must also regenerate the golden corpus and update those assertions, or cargo xtask ci fails on the release branch. Recorded now so the obligation is not discovered during the release.

Dated 2026-08-11. The 2026-08-11 landing-page and brand review (issues #39 through #43), a website-only change ahead of the v0.2.0 release.

  • Specification section 23.1 is amended to permit a value proposition. The section previously held the landing page to exactly one sentence of definition with no capability statements, on the premise that the audience arrives already knowing they need a capture tool. That premise is retired: the page should still reach a technically competent visitor who does not yet know that attribution is the hard part. The amended section leads with the problem, allows a small number of capability statements each linking into the documentation, and retains the prohibitions on testimonials, feature grids, and calls to action, with the section 23.3 voice as the guardrail. The corresponding slice spec (specs/023-docsite/spec.md, FR-004, FR-005, SC-006, and User Story 1) is updated so the two do not contradict, including the getting-started ordering, which now names obtaining a profile between verify and capture (issue #43).

  • The disclaimer and the wordmark are wired without touching pinned CI. The disclaimer is single-sourced from README.md by extending the existing site/scripts/prebuild.mjs render step, which already generates the glossary content tree; the generated module is gitignored and excluded from the conventions linter, matching the glossary precedent. No workflow, release configuration, toolchain pin, or repository-root script changed. prebuild.mjs is a site build script under site/scripts/, not the constitution's pinned repository-root scripts/; the extension is recorded here regardless, since it is a build-affecting change.