trajectories 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +201 -0
- package/README.md +274 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +8542 -0
- package/dist/convert.d.ts +31 -0
- package/dist/convert.js +431 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8402 -0
- package/dist/log-baseline.d.ts +26 -0
- package/dist/log-baseline.js +104 -0
- package/dist/log-content.d.ts +16 -0
- package/dist/log-content.js +80 -0
- package/dist/log-io.d.ts +10 -0
- package/dist/log-io.js +121 -0
- package/dist/log-read.d.ts +24 -0
- package/dist/log-read.js +117 -0
- package/dist/log-reconcile.d.ts +5 -0
- package/dist/log-reconcile.js +38 -0
- package/dist/log-records.d.ts +35 -0
- package/dist/log-records.js +85 -0
- package/dist/log-recovery.d.ts +18 -0
- package/dist/log-recovery.js +123 -0
- package/dist/log-stage.d.ts +14 -0
- package/dist/log-stage.js +138 -0
- package/dist/log-state.d.ts +89 -0
- package/dist/log-state.js +92 -0
- package/dist/log-switch.d.ts +3 -0
- package/dist/log-switch.js +98 -0
- package/dist/log-types.d.ts +129 -0
- package/dist/log-types.js +8 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.js +62 -0
- package/dist/migrate.d.ts +29 -0
- package/dist/migrate.js +173 -0
- package/dist/normalization-runtime.d.ts +1 -0
- package/dist/normalization-runtime.js +3150 -0
- package/dist/portable.d.ts +9 -0
- package/dist/portable.js +26 -0
- package/dist/readers/canonical.d.ts +13 -0
- package/dist/readers/canonical.js +54 -0
- package/dist/readers/cline.d.ts +10 -0
- package/dist/readers/cline.js +107 -0
- package/dist/readers/cursor.d.ts +20 -0
- package/dist/readers/cursor.js +374 -0
- package/dist/readers/deepagents.d.ts +41 -0
- package/dist/readers/deepagents.js +163 -0
- package/dist/readers/fx.d.ts +11 -0
- package/dist/readers/fx.js +573 -0
- package/dist/readers/github-copilot.d.ts +8 -0
- package/dist/readers/github-copilot.js +216 -0
- package/dist/readers/grok-build.d.ts +12 -0
- package/dist/readers/grok-build.js +391 -0
- package/dist/readers/guards.d.ts +4 -0
- package/dist/readers/guards.js +121 -0
- package/dist/readers/pi-compaction.d.ts +7 -0
- package/dist/readers/pi-compaction.js +112 -0
- package/dist/readers/pi-context.d.ts +13 -0
- package/dist/readers/pi-context.js +64 -0
- package/dist/store.d.ts +25 -0
- package/dist/store.js +218 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/dist/writers/claude-code.d.ts +5 -0
- package/dist/writers/claude-code.js +51 -0
- package/dist/writers/cline.d.ts +3 -0
- package/dist/writers/cline.js +77 -0
- package/dist/writers/codex.d.ts +3 -0
- package/dist/writers/codex.js +32 -0
- package/dist/writers/cursor.d.ts +3 -0
- package/dist/writers/cursor.js +197 -0
- package/dist/writers/deepagents.d.ts +3 -0
- package/dist/writers/deepagents.js +116 -0
- package/dist/writers/entropy.d.ts +5 -0
- package/dist/writers/entropy.js +33 -0
- package/dist/writers/fx.d.ts +3 -0
- package/dist/writers/fx.js +135 -0
- package/dist/writers/github-copilot.d.ts +3 -0
- package/dist/writers/github-copilot.js +71 -0
- package/dist/writers/grok-build.d.ts +3 -0
- package/dist/writers/grok-build.js +43 -0
- package/dist/writers/helpers.d.ts +17 -0
- package/dist/writers/helpers.js +75 -0
- package/dist/writers/omp.d.ts +3 -0
- package/dist/writers/omp.js +7 -0
- package/dist/writers/opencode.d.ts +3 -0
- package/dist/writers/opencode.js +65 -0
- package/dist/writers/pi.d.ts +3 -0
- package/dist/writers/pi.js +45 -0
- package/docs/cline-native-format.md +43 -0
- package/docs/copilot-native-format.md +78 -0
- package/docs/cursor-native-format.md +103 -0
- package/docs/deepagents-native-format.md +98 -0
- package/docs/evidence-api.md +170 -0
- package/docs/fx-native-format.md +87 -0
- package/docs/harness-parity-bootstrap.md +78 -0
- package/docs/harness-support-parity.md +107 -0
- package/docs/inspirations.md +103 -0
- package/docs/ledger-migration.md +141 -0
- package/docs/pi-compaction-context.md +75 -0
- package/docs/pi-provider-context.md +76 -0
- package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
- package/docs/research/ledger/README.md +96 -0
- package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
- package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
- package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
- package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
- package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
- package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
- package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
- package/docs/research/ledger/docs/inspirations.md +320 -0
- package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
- package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
- package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
- package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
- package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
- package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
- package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
- package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
- package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
- package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
- package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
- package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
- package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
- package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
- package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
- package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
- package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
- package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
- package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
- package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
- package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
- package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
- package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
- package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
- package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
- package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
- package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
- package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
- package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
- package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
- package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
- package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
- package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
- package/docs/research/ledger/manifest.json +540 -0
- package/legacy/NOTICE.md +50 -0
- package/legacy/bin/ledger-core.ts +7 -0
- package/legacy/bin/ledger.ts +7 -0
- package/legacy/bin/traj-bridge.ts +7 -0
- package/legacy/scripts/import-claude-history.ts +130 -0
- package/legacy/scripts/native-history-smoke.ts +52 -0
- package/legacy/scripts/observe-daemon.ts +570 -0
- package/legacy/scripts/retitle-sessions.md +19 -0
- package/legacy/scripts/verify-runtime-rollout.ts +151 -0
- package/legacy/src/canonical.ts +108 -0
- package/legacy/src/cli.ts +1293 -0
- package/legacy/src/db.ts +2254 -0
- package/legacy/src/diagnostics.ts +87 -0
- package/legacy/src/discovery.ts +56 -0
- package/legacy/src/embeddings.ts +382 -0
- package/legacy/src/filter.ts +602 -0
- package/legacy/src/gemini_embeddings.ts +103 -0
- package/legacy/src/identity.ts +9 -0
- package/legacy/src/index.ts +10 -0
- package/legacy/src/ingest.ts +791 -0
- package/legacy/src/managed.ts +178 -0
- package/legacy/src/native/common.ts +68 -0
- package/legacy/src/native/cursor.ts +647 -0
- package/legacy/src/native/grok.ts +540 -0
- package/legacy/src/native/kimi.ts +706 -0
- package/legacy/src/native/opencode.ts +470 -0
- package/legacy/src/native/sqlite-ro.ts +281 -0
- package/legacy/src/native.ts +97 -0
- package/legacy/src/normalizer.ts +457 -0
- package/legacy/src/parentage.ts +562 -0
- package/legacy/src/pressure.ts +162 -0
- package/legacy/src/private-storage.ts +74 -0
- package/legacy/src/reader.ts +1255 -0
- package/legacy/src/scanner.ts +591 -0
- package/legacy/src/schema-gate.ts +263 -0
- package/legacy/src/schema.ts +829 -0
- package/legacy/src/semantic-top-k.ts +71 -0
- package/legacy/src/session-hash.ts +49 -0
- package/legacy/src/store-status.ts +218 -0
- package/legacy/src/sync-preparation.ts +45 -0
- package/legacy/src/sync-truth.ts +843 -0
- package/legacy/src/sync.ts +1219 -0
- package/legacy/src/types.ts +436 -0
- package/legacy/src/vocab.txt +30522 -0
- package/legacy/src/watch.ts +673 -0
- package/legacy/src/writer-lease.ts +310 -0
- package/ops/README.md +120 -0
- package/ops/install-cli.sh +63 -0
- package/ops/install-release.sh +49 -0
- package/ops/linux/install-systemd-user.sh +103 -0
- package/ops/macos/install-launchd.sh +72 -0
- package/ops/sync-settings.sh +37 -0
- package/package.json +19 -5
- package/skills/trajectories/SKILL.md +138 -0
- package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
- package/skills/trajectories/scripts/trajectories_search.sh +26 -0
- package/index.js +0 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# C8: remote truthfulness primitives, 2026-09-16
|
|
2
|
+
|
|
3
|
+
## Outcome
|
|
4
|
+
|
|
5
|
+
The failure semantics a sync surface must obey now exist as code with an owner:
|
|
6
|
+
`packages/core/src/sync-truth.ts` — a typed outcome vocabulary, a pure state
|
|
7
|
+
machine, the dialect data for transfer error lines, and the stamp/disclosure
|
|
8
|
+
contract — plus `packages/core/tests/sync-truth.test.ts` (25 cases, 435
|
|
9
|
+
assertions). No transport ships, and none is needed to use any of it.
|
|
10
|
+
|
|
11
|
+
The sentence this cycle exists to make impossible is *"it didn't answer, so it
|
|
12
|
+
must be empty."* Five rules do that work, and each one is enforced by a shape
|
|
13
|
+
rather than by a comment:
|
|
14
|
+
|
|
15
|
+
1. **`unreachable` and `empty` are different kinds, and the difference is
|
|
16
|
+
structural.** `empty` is produced only by a probe that exited 0 and found
|
|
17
|
+
none of the whitelist present; a probe that failed (spawn failure, non-zero
|
|
18
|
+
exit, signal) settles `unreachable` *before* any content claim is
|
|
19
|
+
constructible. The mirror action states the same fact mechanically:
|
|
20
|
+
`keep-untouched` is exactly the no-reached-probe case, so a driver cannot be
|
|
21
|
+
handed a mirror deletion it did not earn with a probe that reached.
|
|
22
|
+
2. **`partial` is not `ok`.** Exit 24 (a source vanished while the transfer read
|
|
23
|
+
it) settles `partial`: the mirror keeps what transferred — Wake's rule — and
|
|
24
|
+
the stamp action is `record-error`, so `last_sync_at` goes on naming the last
|
|
25
|
+
sync that *completed*. This is the cycle's one deliberate tightening of Wake,
|
|
26
|
+
which stamps a clean success for 24 (`remote.rs#L407-L419`,
|
|
27
|
+
`db.rs#L864-L877`); "partial ≠ complete" is only enforceable if the persisted
|
|
28
|
+
record says it.
|
|
29
|
+
3. **The probe decides what the transfer may touch.** A transfer directive
|
|
30
|
+
carries exactly the probe-confirmed present set, and absent paths are purged
|
|
31
|
+
between the two steps. The full whitelist never reaches a transfer — the
|
|
32
|
+
hazard Wake measured on this host (openrsync truncating its file list on a
|
|
33
|
+
missing source's parent, exit 23 only; any sender-side I/O error suppressing
|
|
34
|
+
the delete phase, `remote.rs#L13-L22`).
|
|
35
|
+
4. **Dialects are data.** GNU and openrsync live as rows in `RSYNC_DIALECTS`
|
|
36
|
+
(error-line shape, vanished-source shape, summary lines); `rsyncStderrShape`
|
|
37
|
+
is the single dialect-agnostic algorithm over that table, and a shape the
|
|
38
|
+
table does not know is not evidence either way.
|
|
39
|
+
5. **An empty source is a hypothesis only a successful probe may confirm, and an
|
|
40
|
+
unreachable source is not a fact about content at all.** This is the whole
|
|
41
|
+
vocabulary's ordering: reach first, then content.
|
|
42
|
+
|
|
43
|
+
Everything in the module is pure except `readSyncStamps`, which reads a table
|
|
44
|
+
this build deliberately does not create (decision 6 below). Nothing spawns a
|
|
45
|
+
process, nothing touches a filesystem, no CLI verb, no store write, no
|
|
46
|
+
scheduler, and no new dependency; the only import is a `Database` *type*.
|
|
47
|
+
|
|
48
|
+
### Evidence, and what kind it is
|
|
49
|
+
|
|
50
|
+
**OBSERVED** (in this tree's output, this cycle):
|
|
51
|
+
|
|
52
|
+
- the case suite and the sweep — `bun test tests/sync-truth.test.ts`: 25 pass /
|
|
53
|
+
0 fail, 435 assertions;
|
|
54
|
+
- the whole suite with this cycle's additions: 626 pass / 0 fail, 4,742
|
|
55
|
+
assertions, 43 files;
|
|
56
|
+
- `bun run typecheck` and `bun run build`: clean;
|
|
57
|
+
- the api contract test that pins the exported key list: 19 pass / 0 fail.
|
|
58
|
+
|
|
59
|
+
**INFERRED** (from Wake's source, or from the shapes here — not run):
|
|
60
|
+
|
|
61
|
+
- the dialect table's coverage limit (an unknown error shape is invisible to
|
|
62
|
+
`rsyncStderrShape`; no unknown-dialect rsync was executed here);
|
|
63
|
+
- the claim that a future transport driver can satisfy the state machine's
|
|
64
|
+
contract — no driver exists yet, and the two `INFERRED` items in *Still open*
|
|
65
|
+
are exactly where that could bite.
|
|
66
|
+
|
|
67
|
+
**Not done at all:** no ssh or rsync process was spawned, no transport was
|
|
68
|
+
written, no filesystem mirror was created or deleted, no store — production or
|
|
69
|
+
temporary — was opened by any shipped path, and no timing or scale measurement
|
|
70
|
+
was taken. Nothing in this note should be read as one.
|
|
71
|
+
|
|
72
|
+
## The vocabulary: kind, claim, mirror, stamp
|
|
73
|
+
|
|
74
|
+
| kind | what it claims | mirror (`cache`) | stamp |
|
|
75
|
+
| --- | --- | --- | --- |
|
|
76
|
+
| `ok` | reached; every probe-confirmed path transferred | `purge(landed)` — possibly empty | `update-stamp` |
|
|
77
|
+
| `empty` | reached; holds none of the whitelist | `purge(landed)` — the round's whole absent set when there was one, its purge having succeeded | `update-stamp` |
|
|
78
|
+
| `partial` | reached; a subset transferred, missing exactly the vanished sources | `purge(landed)` | `record-error` |
|
|
79
|
+
| `unreachable` | **no contact in the round that settled**; that round wrote nothing | `keep-untouched` when no round reached; else `purge(landed)` | `record-error` |
|
|
80
|
+
| `refused` | the attempt stopped short of a mirror state this store can vouch for (bad name, missing tool, failed mirror deletion, non-vanished transfer error) | `keep-untouched` when nothing reached; else `purge(landed)` — possibly empty when the licensed deletion failed | `record-error` |
|
|
81
|
+
|
|
82
|
+
`landed` is the union of the absent sets of purges that **reported success**:
|
|
83
|
+
the record names mirror entries that are really gone. A purge that failed joins
|
|
84
|
+
nothing — the paths it was licensed to delete stay visible as the outcome's
|
|
85
|
+
`absent` (a reached round's fact, deleted or not), and the account names the
|
|
86
|
+
filesystem failure. A mirror deletion that cannot land is `refused`, never a
|
|
87
|
+
quiet success: the remote no longer holds those sessions, and reporting a clean
|
|
88
|
+
sync would leave them in the cache and the index.
|
|
89
|
+
|
|
90
|
+
The reasons are a closed set (`host_name_invalid`, `probe_tool_unavailable`,
|
|
91
|
+
`probe_failed`, `source_empty`, `transferred`, `sources_vanished`,
|
|
92
|
+
`mirror_purge_failed`, `transfer_tool_unavailable`, `transfer_failed`), because a
|
|
93
|
+
disclosure has to name why, and free text cannot be counted or translated.
|
|
94
|
+
|
|
95
|
+
Two invariants are asserted over every scripted run, not merely documented:
|
|
96
|
+
|
|
97
|
+
- **Reach licenses the mirror.** `cache.kind === "keep-untouched"` exactly when
|
|
98
|
+
no probe in the attempt reached the source; once one did (detected
|
|
99
|
+
independently, by the presence of a `purge`/`transfer` directive), the action
|
|
100
|
+
is `purge` with the deletions that landed, and `present ∪ absent` partitions
|
|
101
|
+
the whitelist.
|
|
102
|
+
- **Completeness owns the stamp.** `update-stamp` exactly for `ok`/`empty`;
|
|
103
|
+
`record-error` for everything else, with a non-null account (the reason code is
|
|
104
|
+
the account of last resort, never an empty error).
|
|
105
|
+
|
|
106
|
+
## The state machine, step by step
|
|
107
|
+
|
|
108
|
+
`beginSyncAttempt(source, paths)` → a name that cannot be addressed settles
|
|
109
|
+
`refused/host_name_invalid` with **zero directives** (nothing runs); a valid one
|
|
110
|
+
asks for `probe`. `advanceSyncAttempt(state, observation)` is the only
|
|
111
|
+
transition, and an observation that does not match the stage the attempt waits
|
|
112
|
+
on **throws** — a driver bug must not be absorbed into an outcome the store then
|
|
113
|
+
believes.
|
|
114
|
+
|
|
115
|
+
| stage | observation | next |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `probe` | spawn failure | settle `unreachable`/`probe_tool_unavailable`, mirror untouched |
|
|
118
|
+
| `probe` | exit ≠ 0 (or signal) | settle `unreachable`/`probe_failed`, detail = `<tool> exited with N: <3-line tail>` |
|
|
119
|
+
| `probe` | exit 0, some paths absent | `purge` directive with the absent set (`remote.rs#L345-L349`) |
|
|
120
|
+
| `probe` | exit 0, nothing present | settle `empty`/`source_empty` via the purge, `update-stamp` |
|
|
121
|
+
| `probe` | exit 0, all present | `transfer` directive with the present set |
|
|
122
|
+
| `purge` | removed, present non-empty | `transfer` directive |
|
|
123
|
+
| `purge` | removed, present empty | settle `empty`/`source_empty` |
|
|
124
|
+
| `purge` | failed | settle `refused`/`mirror_purge_failed`, with nothing joining the landed set and the licensed paths left on `absent` (a remote deletion that cannot propagate must not report as a clean sync, `remote.rs#L361-L387`) |
|
|
125
|
+
| `transfer` | exit 0 | settle `ok`/`transferred` |
|
|
126
|
+
| `transfer` | spawn failure | settle `refused`/`transfer_tool_unavailable` |
|
|
127
|
+
| `transfer` | vanished sources, code 23, round 1 | **one** re-probe: back to `probe`, round 2 (`remote.rs#L355-L359`) |
|
|
128
|
+
| `transfer` | vanished sources otherwise | settle `partial`/`sources_vanished` |
|
|
129
|
+
| `transfer` | anything else | settle `refused`/`transfer_failed` |
|
|
130
|
+
|
|
131
|
+
The round's facts belong to the round: the retry clears `present`/`absent` but
|
|
132
|
+
keeps `reached` and the deletions that already landed, so a round-2 probe that
|
|
133
|
+
cannot reach settles `unreachable` with the first round's landed deletions named
|
|
134
|
+
and no content claim at all.
|
|
135
|
+
|
|
136
|
+
## The dialects, and where this stops short
|
|
137
|
+
|
|
138
|
+
GNU and openrsync, pinned from Wake's own fixtures (`remote.rs#L478-L496`):
|
|
139
|
+
|
|
140
|
+
| dialect | error line | vanished source | summaries (never errors) |
|
|
141
|
+
| --- | --- | --- | --- |
|
|
142
|
+
| GNU | `rsync: [sender] … failed|error …` | `… failed: No such file or directory (2)` | `some files/attrs were not transferred`; `some files vanished before they could be transferred` |
|
|
143
|
+
| openrsync | `rsync(pid): error: …` | `…: (l)stat: No such file or directory` | — |
|
|
144
|
+
|
|
145
|
+
The summary exclusion is load-bearing, not tidiness: GNU's exit-23 summary
|
|
146
|
+
contains "errors", so without it a vanished transfer would read as an
|
|
147
|
+
unaccounted error and be refused (`remote.rs#L421-L431`).
|
|
148
|
+
|
|
149
|
+
The exit-code rule is **evidence agreement**, and it differs from Wake in exactly
|
|
150
|
+
one place:
|
|
151
|
+
|
|
152
|
+
- `0` → complete. Warnings on stderr do not change that.
|
|
153
|
+
- `24` → vanished sources, unless an error line contradicts the code's own claim.
|
|
154
|
+
Wake accepts 24 unconditionally; the tightening here is that a 24 *with* a
|
|
155
|
+
non-vanished error line is refused, because a partial-transfer claim the
|
|
156
|
+
evidence contradicts is precisely what a truthfulness primitive exists to
|
|
157
|
+
refuse.
|
|
158
|
+
- `23` → vanished sources only on unanimous vanished-source evidence *and at
|
|
159
|
+
least one error line*. No lines at all is "shape unknown" and stays a failure
|
|
160
|
+
— Wake's own refusal, kept.
|
|
161
|
+
- Anything else → failure.
|
|
162
|
+
|
|
163
|
+
Known limit, recorded rather than hidden: only lines the dialect table
|
|
164
|
+
recognizes as errors count as evidence. A permission error printed in a shape
|
|
165
|
+
the table does not know is invisible to the classifier — for exit 23 that still
|
|
166
|
+
lands on `failed`, but for exit 24 it would be read as vanished. Adding the shape
|
|
167
|
+
is one row in `RSYNC_DIALECTS` and nothing else; `INFERRED` (from the table's
|
|
168
|
+
construction; no unknown-dialect rsync was run here).
|
|
169
|
+
|
|
170
|
+
## Ported mechanics (Wake MIT, `iAmCorey/Wake@d9f702b6df0e85` v0.6.5 — mechanics with attribution, no code copied)
|
|
171
|
+
|
|
172
|
+
| Ledger | Wake pin | What was kept |
|
|
173
|
+
| --- | --- | --- |
|
|
174
|
+
| probe → purge absent → transfer order; the transfer directive carries only probe-confirmed paths | `crates/wake-core/src/remote.rs#L13-L22` (measured hazard), `#L333-L354` | the order, and the reason: a missing source can silently truncate a transfer's file list (exit 23 only) and any sender I/O error suppresses the delete phase |
|
|
175
|
+
| probe stdout filtered to exact whitelist lines, in whitelist order | `remote.rs#L264-L275`, `#L277-L281`, test `#L594-L604` | MOTD/rc echo/parents/children/absolute paths cannot become sources; a duplicated line is still one path |
|
|
176
|
+
| probe failure fails the whole attempt, mirror untouched | `remote.rs#L333-L354` | "cannot treat cannot-connect as the remote is empty" — here it is the `unreachable` kind plus `keep-untouched` |
|
|
177
|
+
| exit 0 only for the probe; tail-of-stderr account | `remote.rs#L388-L394`, `#L452-L471` | last three lines joined, capped at 500 chars, signal death spelled out |
|
|
178
|
+
| one re-probe when sources vanished between probe and transfer | `remote.rs#L355-L359` | exactly one; the second round's facts settle as-is |
|
|
179
|
+
| vanished-source detection across two dialects; summary lines excluded; an evidence-free 23 not reclassified | `remote.rs#L421-L431`, `#L478-L496` | the shapes, moved into a data table; the unanimity rule |
|
|
180
|
+
| a mirror deletion that fails is a sync failure | `remote.rs#L361-L387`, `#L499-L521` | absence is success, an error is `refused/mirror_purge_failed` |
|
|
181
|
+
| disposable mirror: orphan purge re-asserted after every round, because an in-flight transfer cannot be cancelled and would write a removed tree back | `remote.rs#L202-L222` | `orphanedMirrors(configured, present)` as a pure name-set verdict (the caller owns the root and the deletion) |
|
|
182
|
+
| host names validated before a session key or an argv | `remote.rs#L194-L200`, test `#L546-L558` | the charset, the leading-alphanumeric rule, and the `:` refusal — decomposed into reasons (`colon` because this store keys sessions as `source:native-id`) |
|
|
183
|
+
| `last_sync_at` advances only on a completed sync; a failure records the error and leaves the stamp | `crates/wake-core/src/db.rs#L116-L122`, `#L864-L877` | the pair, and the "None = never successfully synced" reading |
|
|
184
|
+
|
|
185
|
+
**Adapted:** Wake's per-host row becomes a per-source one (`source` is this
|
|
186
|
+
store's axis everywhere else, and a remote is a source of a particular kind);
|
|
187
|
+
Wake's two soft-success paths (24, and a second all-ENOENT 23 returning `Err`)
|
|
188
|
+
become one `partial` kind with one `record-error` stamp, so the record
|
|
189
|
+
distinguishes "incomplete" from "failed" without either of them claiming
|
|
190
|
+
completeness; Wake's `RsyncError::SourcesVanished(String)` enum distinction
|
|
191
|
+
becomes the typed `TransferResult`.
|
|
192
|
+
|
|
193
|
+
**Not ported:** the transport and everything around it — `probe_args`' `sh -c`
|
|
194
|
+
loop, `rsync_args`, `SSH_OPTS`, `REMOTE_LAYOUTS` (eighteen agents' remote paths),
|
|
195
|
+
`--exclude` credential guards, `sync_hosts`' parallel thread scope, the mirror
|
|
196
|
+
size accounting for the settings screen, and `cache_bytes`. Those are the cycle
|
|
197
|
+
that ships a surface.
|
|
198
|
+
|
|
199
|
+
## Persistence: the stamp contract, and why no schema version moved
|
|
200
|
+
|
|
201
|
+
`SYNC_STAMP_DDL` is schema-ready and **deliberately not executed by this cycle**.
|
|
202
|
+
The decision, stated as the trade it is:
|
|
203
|
+
|
|
204
|
+
| Option | Cost | Verdict |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| Take `SCHEMA_VERSION` 16 now, add the table to `CREATE_TABLES_SQL`, enter the ladder from 13/14/15 | Migrates every store this build touches for a table no code path can populate; makes every older installed build refuse those stores (`SCHEMA_MISMATCH`); grows PR #52's ladder obligation from "accept 15" to "accept 15 and 16" on an unaccepted branch; freezes a row shape before the transport that owns it exists | **rejected** |
|
|
207
|
+
| Ship the contract — DDL as data, row type, `stampedSyncRow`, `readSyncStamps`, disclosure — and let the writer cycle take the version | The table exists only in tests this cycle; the future writer's diff is one line in `CREATE_TABLES_SQL` plus its ladder entry, with no rework of the vocabulary | **taken** |
|
|
208
|
+
|
|
209
|
+
Boring wins here because a schema version is a claim about shape, and a version
|
|
210
|
+
spent on an empty table buys nothing while charging every store and every
|
|
211
|
+
neighbouring branch. The obligation is recorded, not dropped: **the cycle that
|
|
212
|
+
ships a writer claims 16 and enters the ladder from every live shape**, the way
|
|
213
|
+
C7's step entered from both 13 and 14 — and #52's ladder then owes the same
|
|
214
|
+
two-sided treatment for 15 *and* 16.
|
|
215
|
+
|
|
216
|
+
What the contract does pin, with CHECKs, so a row cannot lie:
|
|
217
|
+
|
|
218
|
+
- `last_sync_at` is null or a time; a **complete** outcome must carry one and
|
|
219
|
+
must clear `last_sync_error`;
|
|
220
|
+
- an **incomplete** outcome must carry `last_sync_error` (the reason code is the
|
|
221
|
+
account of last resort — `stampedSyncRow` never writes a failure with no
|
|
222
|
+
account) and must not advance `last_sync_at`;
|
|
223
|
+
- `last_outcome` is the closed kind set, so the store can still tell
|
|
224
|
+
`unreachable` from `empty` a week later;
|
|
225
|
+
- `updated_at` stamps the *attempt*, complete or not, so staleness has two
|
|
226
|
+
readable ends: when the last attempt ran, and when the last sync completed.
|
|
227
|
+
|
|
228
|
+
`readSyncStamps` returns `null` for a store without the table — the same
|
|
229
|
+
"unknown is not empty" distinction `StoreStatus.coverage` draws, and the same
|
|
230
|
+
`sqlite_master` probe `TrajectoryDb.getSourceCoverageSummary` uses.
|
|
231
|
+
|
|
232
|
+
## Coverage honesty
|
|
233
|
+
|
|
234
|
+
`discloseSyncState(stamps | null)` is the shape a consuming surface renders,
|
|
235
|
+
modelled on the existing coverage envelope (`coverage: {degraded, total,
|
|
236
|
+
oldest_updated_at}` — `SourceCoverageSummary`, the `sync --robot` summary and
|
|
237
|
+
the v2 search envelope):
|
|
238
|
+
|
|
239
|
+
| state | meaning |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `unknown` | the store holds no stamp table: sync state is not knowable here — never "clean", never "never synced" |
|
|
242
|
+
| `never_synced` | the table exists and is empty: no attempt has run (a fact about attempts, not about content) |
|
|
243
|
+
| `recorded` | per-source entries, `total`, `incomplete` (last attempt did not complete — the `degraded` analog), and `oldest_synced_at` (the staleness floor over *completed* syncs) |
|
|
244
|
+
|
|
245
|
+
Each per-source entry carries a `mirror` claim ceiling: `current` (the last
|
|
246
|
+
attempt completed), `incomplete` (`partial`: the mirror misses exactly what
|
|
247
|
+
vanished), or `not-claimed` (`unreachable`/`refused`: this store says nothing
|
|
248
|
+
about what the mirror holds).
|
|
249
|
+
|
|
250
|
+
**No status line and no CLI surface ships with this cycle**, and that is the
|
|
251
|
+
honest half of "ship the disclosure if the table ships": the table does not, so a
|
|
252
|
+
rendered line would report an unknown as a fact. The types and their derivation
|
|
253
|
+
are tested; the verb that prints them belongs to the cycle that can write them.
|
|
254
|
+
|
|
255
|
+
## Decisions
|
|
256
|
+
|
|
257
|
+
1. **A closed kind set whose members are claims, not severities.** `ok`,
|
|
258
|
+
`empty`, `partial`, `unreachable`, `refused` — each one names what a
|
|
259
|
+
consumer may and may not conclude. Alternatives considered and rejected:
|
|
260
|
+
a boolean `ok`, because it cannot separate "no contact" from "empty";
|
|
261
|
+
separate `missing`/`error`/`timeout` kinds, because they are all "no contact"
|
|
262
|
+
to every consumer that matters (the distinction lives in the reason, where it
|
|
263
|
+
can be counted without widening the claim vocabulary).
|
|
264
|
+
2. **`refused` is one kind, not three.** Invalid name, missing tool, failed
|
|
265
|
+
mirror deletion, and a non-vanished transfer error are all "the attempt did
|
|
266
|
+
not establish a mirror state this store can vouch for"; the *reason* carries
|
|
267
|
+
the distinction. This keeps the disclosure table small and the reasons
|
|
268
|
+
countable.
|
|
269
|
+
3. **A reached probe is the mirror's only write authority, and the record names
|
|
270
|
+
deletions that landed.** Wake's own comment says it ("cannot treat
|
|
271
|
+
cannot-connect as the remote is empty"); here it is a typed cache action, so
|
|
272
|
+
`keep-untouched` cannot be confused with `purge([])`, and a purge that fails
|
|
273
|
+
contributes nothing to the deletion record (`deleted`) — the licensed set is
|
|
274
|
+
still a fact, and it is the outcome's own `absent`. Two facts, two fields,
|
|
275
|
+
each labeled for what it is.
|
|
276
|
+
4. **The transfer directive is the probe's output, by construction.** Not a flag,
|
|
277
|
+
not a caller's responsibility: the state machine produces the directive from
|
|
278
|
+
the probe it just classified.
|
|
279
|
+
5. **Partial records an error.** Wake's 24 is a soft success; here it is
|
|
280
|
+
`partial` + `record-error`. The cost is a visible account after a partial
|
|
281
|
+
round (a busy remote source can produce one repeatedly); the alternative is a
|
|
282
|
+
store that shows a clean timestamp over a mirror that is missing files.
|
|
283
|
+
6. **The stamp table is not created this cycle** (see the table above).
|
|
284
|
+
7. **`orphanedMirrors` decides names, not paths.** The caller owns the mirror
|
|
285
|
+
root and the deletion; the verdict is set difference against the configured
|
|
286
|
+
names, re-asserted after every round. Wake's ordering (purge after the round,
|
|
287
|
+
not only on removal) is the ported semantics — the function is idempotent and
|
|
288
|
+
cheap, and the contract is that a driver runs it after each attempt settles.
|
|
289
|
+
8. **No status verb, no CLI flag, no env var.** Nothing in this cycle is
|
|
290
|
+
reachable from a command line, by design: a verb would need a transport to
|
|
291
|
+
have something to report.
|
|
292
|
+
9. **The primitives are exported** (`index.ts` → `export * from
|
|
293
|
+
"./sync-truth.js"`), because the module's whole purpose is to be the one
|
|
294
|
+
owner of a vocabulary three future surfaces would otherwise invent three
|
|
295
|
+
times. `api.test.ts`'s exact export list and `docs/api-surface.md` §2 are
|
|
296
|
+
updated with them.
|
|
297
|
+
|
|
298
|
+
## The ADR-0005 boundary: what this cycle deliberately does not decide
|
|
299
|
+
|
|
300
|
+
ADR 0005 (push ingestion tiers) is **proposed and unaccepted** (`docs/decisions/`
|
|
301
|
+
does not carry it; it lives as PR #13's draft), and issue #14 stays blocked until
|
|
302
|
+
it is accepted or amended. This cycle ships **no transport and no sync feature**,
|
|
303
|
+
and the boundary is explicit:
|
|
304
|
+
|
|
305
|
+
- **No transport.** Nothing spawns ssh, rsync, or any other process. The
|
|
306
|
+
classifiers take reports; the machine returns directives; a driver that does
|
|
307
|
+
not exist yet runs them.
|
|
308
|
+
- **No push-vs-pull decision.** The vocabulary is deliberately transport-neutral:
|
|
309
|
+
the probe/transfer/purge steps describe a *mirror*, and the outcome/stamp/
|
|
310
|
+
disclosure vocabulary would serve a push tier unchanged — but nothing here
|
|
311
|
+
presumes either, and nothing here depends on ADR 0005's tier-B fidelity rule
|
|
312
|
+
or its "never a synchronous dependency" constraint.
|
|
313
|
+
- **No whitelist.** Which paths a source contributes, what is excluded, and the
|
|
314
|
+
rule that credentials are never in the list (Wake's contract test,
|
|
315
|
+
`remote.rs#L560-L572`) belong to the surface that knows the layouts.
|
|
316
|
+
- **No scheduling, no lease, no concurrency.** A future sync that writes the
|
|
317
|
+
store is a writer like any other and must take the writer lease; that
|
|
318
|
+
interaction, and whether a mirror pass runs beside a scan, are the transport
|
|
319
|
+
cycle's decisions.
|
|
320
|
+
- **No layout or cache root.** `orphanedMirrors` compares names; where mirrors
|
|
321
|
+
live, how they are laid out, and whether they count toward the store's
|
|
322
|
+
disclosed size (Wake folds `remotes/` into its storage line) are open.
|
|
323
|
+
- **No schema version.** See decision 6 — the ladder obligation is recorded for
|
|
324
|
+
the writer cycle, and the ladder itself is not touched here.
|
|
325
|
+
|
|
326
|
+
## Files
|
|
327
|
+
|
|
328
|
+
- `packages/core/src/sync-truth.ts` (new, 843 lines with comments) — the
|
|
329
|
+
vocabulary, the classifiers, the machine, `orphanedMirrors`, the stamp
|
|
330
|
+
contract and DDL, and the disclosure.
|
|
331
|
+
- `packages/core/tests/sync-truth.test.ts` (new, 622 lines, 25 cases) — names,
|
|
332
|
+
probe classification, dialects, the machine's every transition, the exhaustive
|
|
333
|
+
invariant sweep, the mirror lifecycle, the DDL's refusals, the stamp's
|
|
334
|
+
carry-forward rule, and the disclosure.
|
|
335
|
+
- `packages/core/src/index.ts` — exports the primitives.
|
|
336
|
+
- `packages/core/tests/api.test.ts` — the exact exported-key list.
|
|
337
|
+
- `docs/api-surface.md` — §2.1/§2.3/§2.4 (the new surface), §3.1 (the table that
|
|
338
|
+
is not created, and the obligation), §4 (the ADR entry).
|
|
339
|
+
- `docs/research/2026-09-16-wake-port-program.md` — C8's row marked done.
|
|
340
|
+
|
|
341
|
+
## Still open (recorded, not hidden)
|
|
342
|
+
|
|
343
|
+
- **The writer cycle's ladder obligation.** Claiming 16 later means entering
|
|
344
|
+
from 13, 14 and 15 (a store at 15 without `sync_stamps` is exactly the shape
|
|
345
|
+
the ladder must accept), and PR #52's branch then owes 15 *and* 16 the same
|
|
346
|
+
way. Recorded here because this cycle chose not to spend the version.
|
|
347
|
+
- **A configured name that fails validation.** `orphanedMirrors` decides by
|
|
348
|
+
configured-name membership, which is Wake's rule; a configured-but-invalid
|
|
349
|
+
name therefore still "claims" its mirror. A driver should drop such a name from
|
|
350
|
+
the configured set (or purge explicitly), and the next cycle should decide
|
|
351
|
+
whether the store refuses to keep it at all.
|
|
352
|
+
- **Dialect coverage is two shapes.** See the limit above; the escape hatch is
|
|
353
|
+
one row.
|
|
354
|
+
- **`partial`'s account may repeat.** A source that is being written during
|
|
355
|
+
every sync will report a vanished source every round; the stamp keeps the
|
|
356
|
+
error until a round completes. That is intended, and it is the cost decision 5
|
|
357
|
+
accepted.
|
|
358
|
+
- **Nothing consumes the primitives yet.** The stamp table has no writer, the
|
|
359
|
+
disclosure has no renderer, `orphanedMirrors` has no caller, and the mirror
|
|
360
|
+
has no location.
|
|
361
|
+
|
|
362
|
+
## The roadmap row
|
|
363
|
+
|
|
364
|
+
> **C8** Remote truthfulness primitives (unreachable ≠ empty, partial ≠
|
|
365
|
+
> complete, per-path probe, exit-24 handling, disposable cache lifecycle) —
|
|
366
|
+
> *#14/ADR-0005 blocked on the decision, but the failure-semantics vocabulary has
|
|
367
|
+
> no owner; port it as tested primitives without shipping a transport.*
|
|
368
|
+
> *Open-source case:* every multi-machine user hits this; honest sync failure is
|
|
369
|
+
> a moat vs "it silently showed nothing" — Wake's `remote.rs` is the pinned
|
|
370
|
+
> authority. (`docs/research/2026-09-16-wake-port-program.md`)
|
|
371
|
+
|
|
372
|
+
Ported: the probe-before-transfer order, exact-whitelist probe filtering, the
|
|
373
|
+
probe-fails-everything rule, the dialect-pinned vanished-source rule, the single
|
|
374
|
+
tolerated re-probe, the failed-mirror-deletion-is-a-failure rule, the disposable
|
|
375
|
+
mirror's orphan purge, host-name validation, and the `last_sync_at` /
|
|
376
|
+
`last_sync_error` pair. Adapted: the outcomes became a closed typed vocabulary
|
|
377
|
+
with explicit cache and stamp actions, `partial` replaced Wake's two soft-success
|
|
378
|
+
paths, and the dialects became a data table. Not ported: everything that talks to
|
|
379
|
+
another machine.
|
|
380
|
+
|
|
381
|
+
## Verification
|
|
382
|
+
|
|
383
|
+
- `cd packages/core && bun test` — **626 pass / 0 fail**, 4,742 assertions, 43
|
|
384
|
+
files (the tree before this cycle was 601 pass / 42 files; this cycle adds one
|
|
385
|
+
file and 25 cases).
|
|
386
|
+
- `bun run typecheck` — clean (`tsc --noEmit`).
|
|
387
|
+
- `bun run build` — clean (85 modules, 0.55 MB bundle, then `tsc --noEmit`).
|
|
388
|
+
- `bun test tests/sync-truth.test.ts` — 25 pass / 0 fail, 435 assertions.
|
|
389
|
+
- `bun test tests/api.test.ts` — 19 pass / 0 fail, including the exact
|
|
390
|
+
exported-key list this cycle extended.
|