Skip to main content

Module sync

Module sync 

Source
Expand description

External tracker sync: orchestration for tix export / tix import, the Tool selector, the CommandRunner spawn seam, and the pure mapping / conflict-resolution rules.

WP05 wires the CLI to glab (GitLab) and linear-cli (Linear) under contracts/external-tools.md. Tokens are resolved through token::resolve with env precedence, injected only as an env pair (never argv), and every error/report surface scrubs the secret as <token>.

The orchestration lives here so it is testable at module granularity by pointing PATH at fake tool scripts; export / import are pure in the sense that they take the store, tool, config and an output sink, and the only externally visible side effects are local issue files, tix.toml, the tool subprocesses, and what is written to out.

§One definition of “complete”, and what this module’s job actually is

A second local completion definition in this module is a C-004 defect. There used to be one: remote_status_is_done(status) == (status == "closed" || status == "Done"), a helper that looked like it decided whether an issue was complete and in fact decided whether a remote state word was one of the two this module happens to send. It had no production callers — WP02 removed the last one — and its only remaining referrer was its own unit test, which is the shape dead code takes when a test keeps it alive. Removed in WP07.

What this module legitimately owns is the translation: the remote trackers have a closed set of states (opened/closed for GitLab, a workflow-state label for Linear), so something here has to say which remote word means closed and which local column to send it as. That is a protocol concern and it is this module’s job.

What it does not own is the meaning of complete. That is crate::workflow::is_completed and nothing else — so local_to_remote_status asks the service whether a local column is complete, and remote_to_local_status picks the column the service calls complete via [completion_column].

The distinction matters because the two can disagree. A board whose category_map puts completed on shipped while done_column is done has a completion column that is not the one done_column names: comparing names exports shipped as open and closes done instead, so the tracker and the board disagree about the same issue in both directions.

Modules§

beads
Beads (bd) interchange — the tix → bd import row mapping, its JSONL encoding, and the bd export reader that backs the import leg.
glab
GitLab adapter: shells out to the glab CLI under contracts/external-tools.md (GitLab row). Every contract fabricate template’s command shape is reproduced; the only additions are --output json (needed to parse the create/list responses) and the token env injection.
linear
Linear adapter: shells out to the canonical linear-cli CLI (nesszer/linear-cli, the cargo:linear-cli crate pinned in mise.toml).
token
Token resolution with the contract’s precedence: env var, then tix.toml [export], then the CLI’s native auth.

Structs§

CommandOutcome
The captured result of a spawned external tool.
ProcessRunner
Production runner: std::process::Command with a synchronous output wait.
RemoteEpic
An epic reference a remote reported on one issue.
RemoteIssue
A remote issue parsed from the tracker’s JSON list output.
RemoteMappingDiagnostic
The record emitted when a remote tracker cannot hold a piece of local data — a relation (FR-005, C-005, data-model.md §6), a parent reference, a health value, a project association (WP08, mission first-class-projects-01M3BYBW), or an epic reference (WP08, mission epics-work-items-01M3BYW4).
RemoteProject
A project reference a remote reported on one issue.
SyncReport
What one export or import run did, and what it could not do.

Enums§

MappedEpic
What one local epic reference maps to on the wire.
MappedHealth
What one local health value maps to on the wire.
MappedInitiative
What one local initiative maps to on the wire.
MappedParent
What one parent reference maps to on the wire.
MappedProject
What one local project association maps to on the wire.
MappedRelation
The outcome of mapping one local relationship for one tracker: either a remote representation, or a diagnostic naming why there is none.
Tool
The supported external tracker, selected via --target gitlab|linear.

Traits§

CommandRunner
Spawn seam: production uses ProcessRunner; tests inject a FakeRunner that records argv/env without spawning anything.
TrackerAdapter
Common surface both tracker adapters implement so the orchestration is tool-agnostic.

Functions§

date_to_epoch_days
Parse YYYY-MM-DD into days since the Unix epoch.
days_from_civil
The inverse of model::civil_from_days (Howard Hinnant’s days_from_civil): calendar date → days since the Unix epoch.
export
tix export: mirror local issues to the remote tracker, recording the remote id back into the local gitlab_id/linear_id so a re-run updates instead of creating duplicates (idempotency, FR-Remote-sync).
import
tix import: pull open remote issues into local files.
import_with_adapter
import with the adapter injected — the CommandRunner pattern, so the orchestration under test is the real one and the remote side is a fake that returns canned RemoteIssues without spawning a process (WP07 T037).
is_remote_newer
The conflict rule: the remote wins when its updated_at is strictly later than the local issue’s updated (a YYYY-MM-DD interpreted as end of day). A remote timestamp that cannot be parsed counts as not newer, so the local issue is kept.
iso_to_epoch_secs
Parse an ISO-8601 remote timestamp into epoch seconds. Accepts YYYY-MM-DDTHH:MM:SS (the T may be a space) with optional fractional seconds and a Z or ±HH:MM zone.
local_to_remote_status
Map a local column to the remote state/label sent to the tracker (contracts/external-tools.md mapping table).
map_epic_from_remote
Map one remote epic reference back onto a local reference id, or say why not.
map_epic_to_remote
Map one local epic reference to its remote representation, or say why not.
map_health_to_remote
Map one local health value to its remote representation, or say why not (T022, T023).
map_initiative_to_remote
Map one local initiative to its remote representation, or say why not (T034, T035).
map_parent_to_remote
Map one local parent reference to its remote representation, or say why not.
map_project_from_remote
Map one remote project reference back onto a local association id, or say why not.
map_project_to_remote
Map one local project association to its remote representation, or say why not.
map_relation_from_remote
The inverse: read a remote relation back to a local kind and target.
map_relation_to_remote
Map one local relationship to its remote representation, or say why not.
remote_epics
Parse the epic references one remote issue reported.
remote_projects
Parse the project references one remote issue reported.
remote_to_local_status
Map a remote state/label back onto a local column. Closed/Done maps to the effective done column; anything else lands in the column whose name matches (case-insensitive), else the first column — per the contract’s “whose name matches, else into the first column”.
scrub_secret
Redact a resolved secret from text flowing through error/report surfaces.