trajectories 0.0.0 → 0.1.2

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.
Files changed (205) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/README.md +288 -2
  4. package/dist/cli.js +30210 -0
  5. package/dist/convert.d.ts +31 -0
  6. package/dist/convert.js +431 -0
  7. package/dist/deepagents_checkpoint.py +362 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.js +8402 -0
  10. package/dist/log-baseline.d.ts +26 -0
  11. package/dist/log-baseline.js +104 -0
  12. package/dist/log-content.d.ts +16 -0
  13. package/dist/log-content.js +80 -0
  14. package/dist/log-io.d.ts +10 -0
  15. package/dist/log-io.js +121 -0
  16. package/dist/log-read.d.ts +24 -0
  17. package/dist/log-read.js +117 -0
  18. package/dist/log-reconcile.d.ts +5 -0
  19. package/dist/log-reconcile.js +38 -0
  20. package/dist/log-records.d.ts +35 -0
  21. package/dist/log-records.js +85 -0
  22. package/dist/log-recovery.d.ts +18 -0
  23. package/dist/log-recovery.js +123 -0
  24. package/dist/log-stage.d.ts +14 -0
  25. package/dist/log-stage.js +138 -0
  26. package/dist/log-state.d.ts +89 -0
  27. package/dist/log-state.js +92 -0
  28. package/dist/log-switch.d.ts +3 -0
  29. package/dist/log-switch.js +98 -0
  30. package/dist/log-types.d.ts +129 -0
  31. package/dist/log-types.js +8 -0
  32. package/dist/log.d.ts +8 -0
  33. package/dist/log.js +62 -0
  34. package/dist/migrate.d.ts +29 -0
  35. package/dist/migrate.js +173 -0
  36. package/dist/normalization-runtime.d.ts +1 -0
  37. package/dist/normalization-runtime.js +3150 -0
  38. package/dist/portable.d.ts +9 -0
  39. package/dist/portable.js +26 -0
  40. package/dist/readers/canonical.d.ts +13 -0
  41. package/dist/readers/canonical.js +54 -0
  42. package/dist/readers/cline.d.ts +10 -0
  43. package/dist/readers/cline.js +107 -0
  44. package/dist/readers/cursor.d.ts +20 -0
  45. package/dist/readers/cursor.js +374 -0
  46. package/dist/readers/deepagents.d.ts +41 -0
  47. package/dist/readers/deepagents.js +163 -0
  48. package/dist/readers/fx.d.ts +11 -0
  49. package/dist/readers/fx.js +573 -0
  50. package/dist/readers/github-copilot.d.ts +8 -0
  51. package/dist/readers/github-copilot.js +216 -0
  52. package/dist/readers/grok-build.d.ts +12 -0
  53. package/dist/readers/grok-build.js +391 -0
  54. package/dist/readers/guards.d.ts +4 -0
  55. package/dist/readers/guards.js +121 -0
  56. package/dist/readers/pi-compaction.d.ts +7 -0
  57. package/dist/readers/pi-compaction.js +112 -0
  58. package/dist/readers/pi-context.d.ts +13 -0
  59. package/dist/readers/pi-context.js +64 -0
  60. package/dist/store.d.ts +25 -0
  61. package/dist/store.js +218 -0
  62. package/dist/types.d.ts +91 -0
  63. package/dist/types.js +1 -0
  64. package/dist/writers/claude-code.d.ts +5 -0
  65. package/dist/writers/claude-code.js +51 -0
  66. package/dist/writers/cline.d.ts +3 -0
  67. package/dist/writers/cline.js +77 -0
  68. package/dist/writers/codex.d.ts +3 -0
  69. package/dist/writers/codex.js +32 -0
  70. package/dist/writers/cursor.d.ts +3 -0
  71. package/dist/writers/cursor.js +197 -0
  72. package/dist/writers/deepagents.d.ts +3 -0
  73. package/dist/writers/deepagents.js +116 -0
  74. package/dist/writers/entropy.d.ts +5 -0
  75. package/dist/writers/entropy.js +33 -0
  76. package/dist/writers/fx.d.ts +3 -0
  77. package/dist/writers/fx.js +135 -0
  78. package/dist/writers/github-copilot.d.ts +3 -0
  79. package/dist/writers/github-copilot.js +71 -0
  80. package/dist/writers/grok-build.d.ts +3 -0
  81. package/dist/writers/grok-build.js +43 -0
  82. package/dist/writers/helpers.d.ts +17 -0
  83. package/dist/writers/helpers.js +75 -0
  84. package/dist/writers/omp.d.ts +3 -0
  85. package/dist/writers/omp.js +7 -0
  86. package/dist/writers/opencode.d.ts +3 -0
  87. package/dist/writers/opencode.js +65 -0
  88. package/dist/writers/pi.d.ts +3 -0
  89. package/dist/writers/pi.js +45 -0
  90. package/docs/cline-native-format.md +43 -0
  91. package/docs/copilot-native-format.md +78 -0
  92. package/docs/cursor-native-format.md +103 -0
  93. package/docs/deepagents-native-format.md +98 -0
  94. package/docs/evidence-api.md +170 -0
  95. package/docs/fx-native-format.md +87 -0
  96. package/docs/harness-parity-bootstrap.md +78 -0
  97. package/docs/harness-support-parity.md +107 -0
  98. package/docs/inspirations.md +103 -0
  99. package/docs/ledger-migration.md +141 -0
  100. package/docs/pi-compaction-context.md +75 -0
  101. package/docs/pi-provider-context.md +76 -0
  102. package/docs/research/2026-10-06-huihua-and-oar.md +176 -0
  103. package/docs/research/ledger/README.md +96 -0
  104. package/docs/research/ledger/confer/2026-09-13-round-10/README.md +61 -0
  105. package/docs/research/ledger/confer/2026-09-13-round-2/cass.md +72 -0
  106. package/docs/research/ledger/confer/2026-09-13-round-3/README.md +70 -0
  107. package/docs/research/ledger/confer/2026-09-13-round-5/README.md +61 -0
  108. package/docs/research/ledger/confer/2026-09-13-round-6/README.md +68 -0
  109. package/docs/research/ledger/confer/2026-09-13-round-7/README.md +64 -0
  110. package/docs/research/ledger/confer/2026-09-13-round-9/README.md +59 -0
  111. package/docs/research/ledger/confer/2026-09-14-wake/wake.md +146 -0
  112. package/docs/research/ledger/docs/inspirations.md +320 -0
  113. package/docs/research/ledger/docs/pr2-pr6-integration.md +53 -0
  114. package/docs/research/ledger/docs/research/2026-09-07-confer-inspirations.md +163 -0
  115. package/docs/research/ledger/docs/research/2026-09-07-letta-continuous-service.md +104 -0
  116. package/docs/research/ledger/docs/research/2026-09-08-claude-history-retrieval.md +117 -0
  117. package/docs/research/ledger/docs/research/2026-09-09-agentsview-native-discovery.md +87 -0
  118. package/docs/research/ledger/docs/research/2026-09-09-daemon-observation.md +231 -0
  119. package/docs/research/ledger/docs/research/2026-09-09-discovery-pagination.md +114 -0
  120. package/docs/research/ledger/docs/research/2026-09-09-harness-transcript-contract.md +145 -0
  121. package/docs/research/ledger/docs/research/2026-09-09-native-history-discovery.md +180 -0
  122. package/docs/research/ledger/docs/research/2026-09-09-native-history-implementation.md +137 -0
  123. package/docs/research/ledger/docs/research/2026-09-09-other-inspirations.md +279 -0
  124. package/docs/research/ledger/docs/research/2026-09-09-trajectory-and-letta-code.md +128 -0
  125. package/docs/research/ledger/docs/research/2026-09-13-diagnostics-coverage.md +116 -0
  126. package/docs/research/ledger/docs/research/2026-09-13-fts-rebuild.md +83 -0
  127. package/docs/research/ledger/docs/research/2026-09-13-read-path-split.md +74 -0
  128. package/docs/research/ledger/docs/research/2026-09-13-runtime-rollout.md +102 -0
  129. package/docs/research/ledger/docs/research/2026-09-13-scan-outcomes.md +80 -0
  130. package/docs/research/ledger/docs/research/2026-09-13-search-quality.md +105 -0
  131. package/docs/research/ledger/docs/research/2026-09-13-source-coverage.md +64 -0
  132. package/docs/research/ledger/docs/research/2026-09-13-sync-wall-budget.md +82 -0
  133. package/docs/research/ledger/docs/research/2026-09-13-typed-read-gate.md +81 -0
  134. package/docs/research/ledger/docs/research/2026-09-13-writer-lease.md +84 -0
  135. package/docs/research/ledger/docs/research/2026-09-16-c1-copy-fallback.md +227 -0
  136. package/docs/research/ledger/docs/research/2026-09-16-c10-kimi.md +414 -0
  137. package/docs/research/ledger/docs/research/2026-09-16-c2-derive-counter.md +216 -0
  138. package/docs/research/ledger/docs/research/2026-09-16-c3-echo-filter.md +305 -0
  139. package/docs/research/ledger/docs/research/2026-09-16-c4-title-index.md +349 -0
  140. package/docs/research/ledger/docs/research/2026-09-16-c5-trigram.md +268 -0
  141. package/docs/research/ledger/docs/research/2026-09-16-c6-recency-prior.md +438 -0
  142. package/docs/research/ledger/docs/research/2026-09-16-c7-tombstones.md +467 -0
  143. package/docs/research/ledger/docs/research/2026-09-16-c8-remote-truth.md +390 -0
  144. package/docs/research/ledger/docs/research/2026-09-16-c9-grok.md +380 -0
  145. package/docs/research/ledger/docs/research/2026-09-16-wake-port-program.md +38 -0
  146. package/docs/research/ledger/docs/research/2026-09-17-cursor-blobstore.md +157 -0
  147. package/docs/research/ledger/manifest.json +540 -0
  148. package/legacy/NOTICE.md +50 -0
  149. package/legacy/bin/ledger-core.ts +7 -0
  150. package/legacy/bin/ledger.ts +7 -0
  151. package/legacy/bin/traj-bridge.ts +7 -0
  152. package/legacy/scripts/import-claude-history.ts +130 -0
  153. package/legacy/scripts/native-history-smoke.ts +52 -0
  154. package/legacy/scripts/observe-daemon.ts +570 -0
  155. package/legacy/scripts/retitle-sessions.md +19 -0
  156. package/legacy/scripts/verify-runtime-rollout.ts +151 -0
  157. package/legacy/src/canonical.ts +108 -0
  158. package/legacy/src/cli.ts +1293 -0
  159. package/legacy/src/db.ts +2254 -0
  160. package/legacy/src/diagnostics.ts +87 -0
  161. package/legacy/src/discovery.ts +56 -0
  162. package/legacy/src/embeddings.ts +382 -0
  163. package/legacy/src/filter.ts +602 -0
  164. package/legacy/src/gemini_embeddings.ts +103 -0
  165. package/legacy/src/identity.ts +9 -0
  166. package/legacy/src/index.ts +10 -0
  167. package/legacy/src/ingest.ts +791 -0
  168. package/legacy/src/managed.ts +178 -0
  169. package/legacy/src/native/common.ts +68 -0
  170. package/legacy/src/native/cursor.ts +647 -0
  171. package/legacy/src/native/grok.ts +540 -0
  172. package/legacy/src/native/kimi.ts +706 -0
  173. package/legacy/src/native/opencode.ts +470 -0
  174. package/legacy/src/native/sqlite-ro.ts +281 -0
  175. package/legacy/src/native.ts +97 -0
  176. package/legacy/src/normalizer.ts +457 -0
  177. package/legacy/src/parentage.ts +562 -0
  178. package/legacy/src/pressure.ts +162 -0
  179. package/legacy/src/private-storage.ts +74 -0
  180. package/legacy/src/reader.ts +1255 -0
  181. package/legacy/src/scanner.ts +591 -0
  182. package/legacy/src/schema-gate.ts +263 -0
  183. package/legacy/src/schema.ts +829 -0
  184. package/legacy/src/semantic-top-k.ts +71 -0
  185. package/legacy/src/session-hash.ts +49 -0
  186. package/legacy/src/store-status.ts +218 -0
  187. package/legacy/src/sync-preparation.ts +45 -0
  188. package/legacy/src/sync-truth.ts +843 -0
  189. package/legacy/src/sync.ts +1233 -0
  190. package/legacy/src/types.ts +436 -0
  191. package/legacy/src/vocab.txt +30522 -0
  192. package/legacy/src/watch.ts +673 -0
  193. package/legacy/src/writer-lease.ts +310 -0
  194. package/ops/README.md +150 -0
  195. package/ops/install-cli.sh +63 -0
  196. package/ops/install-native.sh +35 -0
  197. package/ops/install-release.sh +49 -0
  198. package/ops/linux/install-systemd-user.sh +103 -0
  199. package/ops/macos/install-launchd.sh +72 -0
  200. package/ops/sync-settings.sh +37 -0
  201. package/package.json +21 -5
  202. package/skills/trajectories/SKILL.md +138 -0
  203. package/skills/trajectories/scripts/trajectories_recent.sh +26 -0
  204. package/skills/trajectories/scripts/trajectories_search.sh +26 -0
  205. 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.