browser-broker 0.1.0

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 (123) hide show
  1. package/.env.example +173 -0
  2. package/LICENSE +21 -0
  3. package/README.md +374 -0
  4. package/RELEASES.md +97 -0
  5. package/dist/package.json +58 -0
  6. package/dist/src/adapter/conformance/case.js +1 -0
  7. package/dist/src/adapter/conformance/cases.js +429 -0
  8. package/dist/src/adapter/conformance/discovery.js +156 -0
  9. package/dist/src/adapter/conformance/driver.js +1 -0
  10. package/dist/src/adapter/conformance/drivers.js +36 -0
  11. package/dist/src/adapter/conformance/run.js +224 -0
  12. package/dist/src/adapter/conformance/service-subject.js +165 -0
  13. package/dist/src/adapter/contract.js +24 -0
  14. package/dist/src/adapter/operations.js +114 -0
  15. package/dist/src/adapter/service-seam.js +1 -0
  16. package/dist/src/artifacts/names.js +229 -0
  17. package/dist/src/artifacts/store.js +174 -0
  18. package/dist/src/bin/broker-tool.js +63 -0
  19. package/dist/src/bin/broker.js +111 -0
  20. package/dist/src/browser/adoption.js +143 -0
  21. package/dist/src/browser/automation-probe.js +113 -0
  22. package/dist/src/browser/conformance/case.js +1 -0
  23. package/dist/src/browser/conformance/cases.js +192 -0
  24. package/dist/src/browser/conformance/run.js +102 -0
  25. package/dist/src/browser/conformance/subjects.js +19 -0
  26. package/dist/src/browser/discovery.js +226 -0
  27. package/dist/src/browser/driver.js +195 -0
  28. package/dist/src/browser/fake.js +585 -0
  29. package/dist/src/browser/launch.js +504 -0
  30. package/dist/src/browser/real.js +1425 -0
  31. package/dist/src/browser/setup.js +161 -0
  32. package/dist/src/capture/accounting.js +59 -0
  33. package/dist/src/capture/image.js +112 -0
  34. package/dist/src/capture/ladder.js +72 -0
  35. package/dist/src/capture/legibility.js +195 -0
  36. package/dist/src/capture/pipeline.js +153 -0
  37. package/dist/src/capture/tiers.js +166 -0
  38. package/dist/src/cli/adapter.js +233 -0
  39. package/dist/src/cli/commands.js +270 -0
  40. package/dist/src/cli/conformance-driver.js +119 -0
  41. package/dist/src/cli/diffs.js +122 -0
  42. package/dist/src/cli/image.js +274 -0
  43. package/dist/src/cli/index.js +895 -0
  44. package/dist/src/cli/login-command.js +401 -0
  45. package/dist/src/cli/operations-commands.js +186 -0
  46. package/dist/src/cli/reconcile-command.js +137 -0
  47. package/dist/src/cli/sign-in.js +134 -0
  48. package/dist/src/cli/telemetry.js +222 -0
  49. package/dist/src/config/environment.js +446 -0
  50. package/dist/src/diff/artifact-path.js +77 -0
  51. package/dist/src/diff/crops.js +102 -0
  52. package/dist/src/diff/geometry.js +122 -0
  53. package/dist/src/diff/image.js +132 -0
  54. package/dist/src/diff/mask.js +46 -0
  55. package/dist/src/diff/regions.js +263 -0
  56. package/dist/src/diff/settings.js +135 -0
  57. package/dist/src/doctor/checks.js +588 -0
  58. package/dist/src/doctor/report.js +152 -0
  59. package/dist/src/doctor/session.js +161 -0
  60. package/dist/src/errors.js +36 -0
  61. package/dist/src/feedback/read.js +119 -0
  62. package/dist/src/feedback/record.js +199 -0
  63. package/dist/src/operations/addresses.js +175 -0
  64. package/dist/src/operations/derive.js +109 -0
  65. package/dist/src/operations/ledger.js +194 -0
  66. package/dist/src/operations/status.js +197 -0
  67. package/dist/src/operations/telemetry.js +280 -0
  68. package/dist/src/report/document.js +419 -0
  69. package/dist/src/report/escape.js +68 -0
  70. package/dist/src/report/snapshot.js +97 -0
  71. package/dist/src/service/arbitration.js +537 -0
  72. package/dist/src/service/artifacts.js +85 -0
  73. package/dist/src/service/bridge.js +577 -0
  74. package/dist/src/service/broker.js +120 -0
  75. package/dist/src/service/browser-session.js +269 -0
  76. package/dist/src/service/capacity.js +62 -0
  77. package/dist/src/service/capture-seam.js +83 -0
  78. package/dist/src/service/capture-store.js +91 -0
  79. package/dist/src/service/comparison-store.js +101 -0
  80. package/dist/src/service/comparison.js +173 -0
  81. package/dist/src/service/events.js +93 -0
  82. package/dist/src/service/keys.js +68 -0
  83. package/dist/src/service/leases.js +147 -0
  84. package/dist/src/service/nudge.js +66 -0
  85. package/dist/src/service/operations/claim.js +692 -0
  86. package/dist/src/service/operations/give-back.js +131 -0
  87. package/dist/src/service/operations/pages.js +771 -0
  88. package/dist/src/service/operations/sign-in.js +915 -0
  89. package/dist/src/service/operations/status.js +62 -0
  90. package/dist/src/service/ownership.js +93 -0
  91. package/dist/src/service/pages.js +616 -0
  92. package/dist/src/service/pending-seeds.js +20 -0
  93. package/dist/src/service/queue.js +233 -0
  94. package/dist/src/service/reconcile.js +220 -0
  95. package/dist/src/service/refusals.js +262 -0
  96. package/dist/src/service/runtime.js +131 -0
  97. package/dist/src/service/signin-recovery.js +148 -0
  98. package/dist/src/service/storage-seed.js +239 -0
  99. package/dist/src/service/tabs.js +123 -0
  100. package/dist/src/store/budget.js +99 -0
  101. package/dist/src/store/location.js +42 -0
  102. package/dist/src/store/network-path.js +182 -0
  103. package/dist/src/store/network-volume.js +92 -0
  104. package/dist/src/store/open.js +226 -0
  105. package/dist/src/store/schema/step-001-initial.js +523 -0
  106. package/dist/src/store/schema/step-002-tab-budget.js +53 -0
  107. package/dist/src/store/schema/step-003-queue-order.js +110 -0
  108. package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
  109. package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
  110. package/dist/src/store/schema/step-006-signin-events.js +104 -0
  111. package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
  112. package/dist/src/store/schema/step-008-signin-owner.js +76 -0
  113. package/dist/src/store/schema/step-009-named-browsers.js +138 -0
  114. package/dist/src/store/schema/step-010-signin-request.js +135 -0
  115. package/dist/src/store/schema/step.js +172 -0
  116. package/dist/src/store/schema/steps.js +58 -0
  117. package/dist/src/store/transaction.js +37 -0
  118. package/dist/src/tool/adapter.js +90 -0
  119. package/dist/src/tool/conformance-driver.js +184 -0
  120. package/dist/src/tool/protocol.js +310 -0
  121. package/dist/src/tool/session.js +351 -0
  122. package/dist/src/tool/tools.js +310 -0
  123. package/package.json +58 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Step ten: a sign-in can be **asked for by a caller**, and one asked for
3
+ * that way has a deadline and a lease behind it.
4
+ *
5
+ * ── The gap this closes, stated as the thing that happened ──────────────
6
+ *
7
+ * §1.2 records **25 sessions that hand-seeded authentication tokens into an
8
+ * isolated browser while the signed-in browser sat unused**. That measurement
9
+ * is usually read as a browser-choice problem, and `DECISIONS.md` §13i reads
10
+ * it that way when it makes `browser` optional. **It has a second half.** A
11
+ * caller that picked the right browser and still landed on a login page had
12
+ * no call available to it that asks a person to sign in: `broker login` is a
13
+ * command a person types, and the ten tools contain nothing that moves a
14
+ * browser into `signing-in`. So the only moves left to such a caller were to
15
+ * abandon the task or to fabricate a session — and fabricating a session is
16
+ * precisely what those 25 sessions did.
17
+ *
18
+ * ── Why the existing two columns do not cover this ──────────────────────
19
+ *
20
+ * Step eight added `signin_owner_pid` so that a sign-in whose *command*
21
+ * died is recoverable. That column answers **"is the person's command still
22
+ * running"**, and it is exactly right for `broker login`, where a person is
23
+ * sitting in front of a process that stays up for the duration.
24
+ *
25
+ * **A requested sign-in has no such process.** The caller that asked is a
26
+ * connected session which returns from the call immediately and goes back to
27
+ * polling; nothing on the machine is blocked on the person, so there is no
28
+ * identifier whose death means *nobody is signing in any more*. Recording the
29
+ * requesting service's own identifier would be worse than recording nothing,
30
+ * because that process stays alive for the whole session: the row would look
31
+ * permanently live, and the recovery step eight exists to provide would never
32
+ * fire for the one path that most needs it.
33
+ *
34
+ * So the two ways in are recorded differently on purpose, and the columns are
35
+ * separate rather than one column meaning two things:
36
+ *
37
+ * | Began by | Held by | Ends when |
38
+ * |---|---|---|
39
+ * | `broker login` | a process (`signin_owner_pid`) | the person closes the window, or the process dies |
40
+ * | a caller asking | a deadline (`signin_deadline`) and the asking lease (`signin_claim_id`) | the person confirms, or the deadline passes |
41
+ *
42
+ * ── Why a deadline at all, when §5.5.1 says a sign-in has no expiry ─────
43
+ *
44
+ * §5.5.1's *"a person takes as long as they take"* is a statement about a
45
+ * person **who is already at the keyboard**, and it is right for the command:
46
+ * a timeout there would end a sign-in that was going fine while somebody was
47
+ * halfway through a password, and the process holding it is evidence enough
48
+ * that somebody is still there.
49
+ *
50
+ * A requested sign-in has no such evidence, and the failure it makes possible
51
+ * is new. The request is relayed by an agent to a person **who may not be at
52
+ * the keyboard at all** — who may not read the message for an hour, or ever.
53
+ * Left unbounded, one such request holds the browser in `signing-in` against
54
+ * every other caller indefinitely, which is the same unrecoverable state step
55
+ * eight was written to remove, arriving through a door step eight cannot
56
+ * watch. **The deadline is the evidence-substitute**: no process to ask about,
57
+ * so a number instead.
58
+ *
59
+ * ── Why the deadline is a stored column rather than a computed one ──────
60
+ *
61
+ * §1.12's rule is that a value is stored when it cannot be derived from what
62
+ * is already there. `browsers.updated_at` moves on every write to the row, so
63
+ * *"when did this sign-in start"* is not recoverable from it after any other
64
+ * update, and a deadline computed from a moving column is a deadline that
65
+ * moves. The alternative — reading the last `browser_signin_began` ledger row
66
+ * — makes an operational decision depend on scanning an append-only log whose
67
+ * whole design is that it is written and never read back for control flow
68
+ * (§1.6). So the moment is written down once, at the instant it is decided.
69
+ *
70
+ * ── Why the asking lease is recorded ────────────────────────────────────
71
+ *
72
+ * Two reasons, and the second is the load-bearing one.
73
+ *
74
+ * 1. **So the requester is not refused by its own request.** §5.5.1's first
75
+ * step refuses a sign-in while any live lease holds a tab on that browser,
76
+ * and the requesting caller *is* such a lease — it is holding the very tab
77
+ * sitting on the login page. Without knowing which lease asked, the
78
+ * operation cannot tell the caller it is serving from the callers it is
79
+ * protecting, and the request refuses itself every time.
80
+ * 2. **So the exemption is exactly one lease wide.** Recording the identifier
81
+ * means the refusal still fires for every *other* live lease, which is the
82
+ * property §5.5.1 calls "why signing in is a service operation": a person
83
+ * driving a window by hand underneath somebody else's work corrupts it.
84
+ * An exemption expressed as a flag — *"this sign-in was requested, so skip
85
+ * the check"* — would be an exemption for all of them.
86
+ *
87
+ * ── Nullable, because the older path sets neither ───────────────────────
88
+ *
89
+ * Both columns are null for a `broker login` sign-in, and that is the shape
90
+ * rather than a gap: a null deadline means **this sign-in is held by a
91
+ * process, ask step eight's column about it**. The two are not made mutually
92
+ * exclusive by a check constraint, and the reason is worth recording rather
93
+ * than being discovered as an omission: a constraint spanning them would
94
+ * have to be rebuilt to change, and the invariant it would express — that a
95
+ * sign-in has exactly one kind of owner — is one the operations enforce by
96
+ * refusing a second sign-in at all. There is no path that writes both.
97
+ *
98
+ * ── A new step, and no rebuild ──────────────────────────────────────────
99
+ *
100
+ * Two `ALTER TABLE ... ADD COLUMN` statements, which SQLite performs in place.
101
+ * Step nine had to rebuild because it dropped a check; nothing here changes a
102
+ * constraint, so nothing here needs the table copied. Adding a step rather
103
+ * than editing step nine is the rule in `steps.ts`: a step that has run
104
+ * somewhere is history.
105
+ */
106
+ const ADDITIONS = [
107
+ // When this sign-in stops being somebody's and starts being nobody's.
108
+ // ISO-8601 in the same spelling every other instant in this store uses, so
109
+ // the comparison against `strftime('%Y-%m-%dT%H:%M:%fZ', 'now')` that the
110
+ // sweep already runs against `claims.expires_at` compares like with like.
111
+ `ALTER TABLE browsers ADD COLUMN signin_deadline TEXT`,
112
+ // Which lease asked. **Deliberately not a foreign key**, and this is the one
113
+ // decision in the step that is not obvious.
114
+ //
115
+ // `claims` rows are never deleted — a lease ends by moving to a terminal
116
+ // state (§2.2) — so a reference would not dangle. What a foreign key would
117
+ // do is make the *order* of two writes load-bearing: clearing this column
118
+ // and ending the claim would have to happen in an order the constraint
119
+ // permits, on a path whose whole job is to run during a sweep that is
120
+ // already updating claims. The column is read for exactly one comparison —
121
+ // is this the lease that asked — and a value naming a claim absent from the
122
+ // table gives that comparison the same answer as a null would.
123
+ `ALTER TABLE browsers ADD COLUMN signin_claim_id TEXT`,
124
+ ];
125
+ /** Every statement this step runs, in order. */
126
+ export const STEP_TEN_SQL = ADDITIONS;
127
+ export const stepTen = {
128
+ version: 10,
129
+ summary: 'A sign-in can be requested by a caller: a deadline and the asking lease (§5.5.2, DECISIONS.md §13j).',
130
+ apply: (db) => {
131
+ for (const statement of STEP_TEN_SQL) {
132
+ db.exec(statement);
133
+ }
134
+ },
135
+ };
@@ -0,0 +1,172 @@
1
+ import { StartupRefusal } from "../../errors.js";
2
+ import { immediate } from "../transaction.js";
3
+ import { EXPECTED_VERSION, STEPS } from "./steps.js";
4
+ /**
5
+ * Step a store from whatever version it is at to the version this build
6
+ * expects.
7
+ *
8
+ * `SCHEMA.md` §1.2d: the schema is a version stepper the service applies to
9
+ * its own store **on every spawn**, rather than a migration tool somebody
10
+ * runs as a deployment step. There is no deployment moment at which "run the
11
+ * migrations" could be a separate act: a caller that has just upgraded and a
12
+ * caller that has not may both spawn within the same minute, so the check
13
+ * belongs on every spawn or it belongs nowhere. A store already at the right
14
+ * version is left untouched.
15
+ *
16
+ * The steps between the two versions are applied **in order, in one
17
+ * transaction**.
18
+ */
19
+ /**
20
+ * `startup.schema_stepped` (§7.2): **a store at a version this build does not
21
+ * understand is a refusal, not an attempted downgrade.** Two callers on
22
+ * different builds against one store is an ordinary situation here, and
23
+ * guessing is how one of them corrupts it.
24
+ *
25
+ * This refusal lands before any step exists, deliberately. A stepper that
26
+ * only knows how to step *up* is the one somebody later extends with a
27
+ * well-meaning downgrade path; landing the refusal first makes that a change
28
+ * to existing behaviour rather than a gap somebody fills.
29
+ */
30
+ export function readStoreVersion(db) {
31
+ const row = db.pragma('user_version', { simple: true });
32
+ return typeof row === 'number' ? row : 0;
33
+ }
34
+ export async function stepSchema(db, steps = STEPS, expected = EXPECTED_VERSION) {
35
+ // ── The version is read OUTSIDE the transaction only to decide whether to
36
+ // open one, and is read AGAIN INSIDE it before anything is applied ────
37
+ //
38
+ // **This read decides nothing on its own.** It exists so that the ordinary
39
+ // case — a store already at the right version, which is every spawn after
40
+ // the first — costs a pragma rather than a transaction. Every decision that
41
+ // matters is taken again below, inside the transaction, against a version
42
+ // re-read there.
43
+ //
44
+ // A reader tempted to trust this value and delete the re-read should know
45
+ // what that costs: it is the defect this function was changed to fix.
46
+ const observed = readStoreVersion(db);
47
+ if (observed > expected) {
48
+ throw new StartupRefusal('startup.schema_stepped', `The store is at schema version ${String(observed)} and this build understands version ${String(expected)}. A store newer than the build is refused rather than downgraded: two callers on different builds against one store is ordinary here, and guessing is how one of them corrupts it. Upgrade this installation.`);
49
+ }
50
+ if (observed === expected) {
51
+ return { from: observed, to: expected, applied: [] };
52
+ }
53
+ // ── Foreign keys are suspended for the duration of the steps ────────────
54
+ //
55
+ // **This is SQLite's documented table-rebuild procedure, and it is required
56
+ // rather than convenient.** A step that changes a check constraint cannot
57
+ // alter it in place: the table has to be rebuilt, which means dropping a
58
+ // table that another table references — and that violates the reference
59
+ // *even when the rebuild is going to restore every row*.
60
+ //
61
+ // The failure this prevents is total and was reproduced on this path before
62
+ // the guard existed: `feedback.last_event_id` references `events (id)`, so
63
+ // rebuilding `events` failed with a foreign-key violation on **any store
64
+ // that had feedback naming an event**. `prepareStore` rethrows and there is
65
+ // no long-lived process, so every spawn failed — an installation
66
+ // permanently unable to start, and specifically the installations used
67
+ // enough to have collected feedback.
68
+ //
69
+ // **Why it is here rather than at the four call sites.** The pragma is a
70
+ // **no-op inside a transaction** — SQLite silently ignores it and it reads
71
+ // back unchanged — so it has to be set before `BEGIN`, which is inside this
72
+ // function. Putting it in the callers would mean four places to get right
73
+ // and a fifth caller that misses it; this function owns the transaction, so
74
+ // it owns the pragma that has to wrap it.
75
+ //
76
+ // **What is given up, stated plainly:** during the steps the database does
77
+ // not enforce references, so a step that orphaned a row would not be caught
78
+ // as it ran. That is why the integrity check below is not optional — it is
79
+ // the enforcement, re-applied in one pass before the version is stamped.
80
+ const foreignKeysWereOn = db.pragma('foreign_keys', { simple: true }) === 1;
81
+ if (foreignKeysWereOn) {
82
+ db.pragma('foreign_keys = OFF');
83
+ }
84
+ try {
85
+ // **Awaited, not returned.** `immediate` is asynchronous, so returning its
86
+ // promise would run the `finally` below — restoring the pragma — before
87
+ // the transaction had even begun, which puts enforcement back on for the
88
+ // rebuild it was suspended for. The await is what makes the restore
89
+ // happen after the steps rather than during them.
90
+ return await immediate(db, () => {
91
+ // ══════════════════════════════════════════════════════════════════
92
+ // THE VERSION IS READ AGAIN HERE, AND THIS READ IS THE ONE THAT COUNTS
93
+ // ══════════════════════════════════════════════════════════════════
94
+ //
95
+ // **The hazard this closes.** Reading the version once, outside the
96
+ // transaction, and deriving the steps from it lets two processes spawning
97
+ // simultaneously against an empty store both read zero, both decide step
98
+ // one is pending, and the loser run a `CREATE TABLE` the winner has
99
+ // already committed — `table browsers already exists`.
100
+ //
101
+ // That is not an exotic race here. The service is **spawned per session
102
+ // and exits with it**, so two callers starting at once is the ordinary
103
+ // case, and it is exactly what a fresh install hits: the first two
104
+ // agents to reach for a browser on a machine that has never run this.
105
+ //
106
+ // **Why re-reading inside is sufficient.** `immediate` issues
107
+ // `BEGIN IMMEDIATE`, which declares intent to write at the moment it
108
+ // opens, so the store serialises the two writers itself rather than
109
+ // letting both proceed and discovering the conflict at the end (§1.0a).
110
+ // The loser therefore does not enter this callback until the winner has
111
+ // committed, and the version it reads here is the version the winner
112
+ // stamped. It finds nothing pending and applies nothing.
113
+ //
114
+ // This is the same read-then-write window every arbitration path closes
115
+ // the same way. The stepper is not an arbitration path, but it has the
116
+ // identical shape of problem and takes the identical answer.
117
+ const from = readStoreVersion(db);
118
+ // The winner of the race stamped the expected version while this
119
+ // process waited to enter. §1.2d's promise — a store already at the
120
+ // right version is left untouched — now holds for the first two spawns
121
+ // as well as for every later one.
122
+ if (from === expected) {
123
+ return { value: { from, to: expected, applied: [] } };
124
+ }
125
+ // Re-checked inside as well, and not merely for symmetry: between the
126
+ // read above and this one another process may have stepped the store
127
+ // **past** what this build understands. Refusing here rather than
128
+ // stepping onto a store a newer build owns is the same refusal §7.2
129
+ // makes outside, applied to the version that is actually current.
130
+ if (from > expected) {
131
+ throw new StartupRefusal('startup.schema_stepped', `The store is at schema version ${String(from)} and this build understands version ${String(expected)}. A store newer than the build is refused rather than downgraded: two callers on different builds against one store is ordinary here, and guessing is how one of them corrupts it. Upgrade this installation.`);
132
+ }
133
+ // Derived from the version read **inside** the transaction, so the
134
+ // steps applied are the ones actually still pending rather than the
135
+ // ones that were pending when this process first looked.
136
+ const pending = steps
137
+ .filter((step) => step.version > from)
138
+ .sort((a, b) => a.version - b.version);
139
+ const applied = [];
140
+ for (const step of pending) {
141
+ step.apply(db);
142
+ applied.push(step.version);
143
+ }
144
+ // **The enforcement that was suspended, re-applied as a check before
145
+ // anything is committed.** `foreign_key_check` reports every row whose
146
+ // reference does not resolve, and it is read *inside* the transaction
147
+ // so a step that broke a reference rolls back rather than committing a
148
+ // store whose integrity nothing will re-examine.
149
+ //
150
+ // This is the assertion that makes suspending the pragma safe rather
151
+ // than merely quiet: without it, turning enforcement off would trade a
152
+ // loud failure for a silent one.
153
+ const violations = db.pragma('foreign_key_check');
154
+ if (violations.length > 0) {
155
+ throw new StartupRefusal('startup.schema_stepped', `Stepping the schema from version ${String(from)} to ${String(expected)} left ${String(violations.length)} row(s) naming something that does not exist. The store has been left at version ${String(from)} rather than committed in that state.`);
156
+ }
157
+ // `user_version` takes no parameter binding, and the value is a number
158
+ // this module computed rather than anything a caller supplied.
159
+ db.pragma(`user_version = ${String(expected)}`);
160
+ return { value: { from, to: expected, applied } };
161
+ });
162
+ }
163
+ finally {
164
+ // Restored on the way out whatever happened, including a step that threw:
165
+ // a caller that opened the store with references enforced gets a handle
166
+ // with them enforced, and a failed upgrade does not silently leave the
167
+ // connection weaker than the caller asked for.
168
+ if (foreignKeysWereOn) {
169
+ db.pragma('foreign_keys = ON');
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,58 @@
1
+ import { stepOne } from "./step-001-initial.js";
2
+ import { stepEight } from "./step-008-signin-owner.js";
3
+ import { stepFive } from "./step-005-storage-seed-event.js";
4
+ import { stepNine } from "./step-009-named-browsers.js";
5
+ import { stepSix } from "./step-006-signin-events.js";
6
+ import { stepTen } from "./step-010-signin-request.js";
7
+ import { stepSeven } from "./step-007-signin-without-process.js";
8
+ import { stepFour } from "./step-004-tab-never-opened.js";
9
+ import { stepThree } from "./step-003-queue-order.js";
10
+ import { stepTwo } from "./step-002-tab-budget.js";
11
+ export const STEPS = [
12
+ stepOne,
13
+ stepTwo,
14
+ stepThree,
15
+ stepFour,
16
+ stepFive,
17
+ stepSix,
18
+ stepSeven,
19
+ stepEight,
20
+ stepNine,
21
+ stepTen,
22
+ ];
23
+ /**
24
+ * The version a store must be at for this build to use it.
25
+ *
26
+ * **Stamped, not counted.** A store's version is written into the file and is
27
+ * then compared against by every later build, so it is a fact about what has
28
+ * been applied rather than a fact about this array's shape. Deriving it from
29
+ * the length couples the two: a step deleted or a placeholder appended by
30
+ * somebody who has not read the rule above moves the number every installed
31
+ * store is compared against, and the refusal that protects a store from a
32
+ * build it does not understand starts firing — or stops firing — for a reason
33
+ * nobody wrote down.
34
+ *
35
+ * So the number is written here and the check below is what keeps it honest.
36
+ */
37
+ export const EXPECTED_VERSION = 10;
38
+ /**
39
+ * The list is consistent with the version above, asserted where the list is
40
+ * defined rather than in a test.
41
+ *
42
+ * A test would prove this on a machine running the tests. This proves it on
43
+ * every spawn, which is the only moment that matters for a store somebody is
44
+ * about to open — and it is the moment the mistake is cheapest to see, because
45
+ * nothing has been written yet.
46
+ */
47
+ function assertStepsAreWellFormed(steps, expected) {
48
+ steps.forEach((step, index) => {
49
+ if (step.version !== index + 1) {
50
+ throw new Error(`Schema step ${String(index + 1)} declares version ${String(step.version)}. Steps are one-based and contiguous, in order, and a version is never reused.`);
51
+ }
52
+ });
53
+ const last = steps.at(-1)?.version ?? 0;
54
+ if (last !== expected) {
55
+ throw new Error(`The last schema step is version ${String(last)} and this build expects version ${String(expected)}. A step was added without stamping the version, or the version was stamped without the step.`);
56
+ }
57
+ }
58
+ assertStepsAreWellFormed(STEPS, EXPECTED_VERSION);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The statement that opens the transaction. Exported so a test can assert the
3
+ * mode by naming the literal rather than by trusting this comment.
4
+ */
5
+ export const BEGIN_STATEMENT = 'BEGIN IMMEDIATE';
6
+ /**
7
+ * Run `fn` inside an immediate transaction: commit on success, roll back and
8
+ * rethrow on a throw.
9
+ *
10
+ * After-commit actions run once the transaction has ended, and a failure in
11
+ * one does not undo the commit — the work is done and the store says so. They
12
+ * are best effort by design (§2.4b), not by oversight.
13
+ */
14
+ export async function immediate(db, fn) {
15
+ db.prepare(BEGIN_STATEMENT).run();
16
+ let result;
17
+ try {
18
+ result = await fn({ db });
19
+ }
20
+ catch (error) {
21
+ // Rolling back is the point of this branch: the caller must not be able
22
+ // to leave a partial write behind by throwing.
23
+ db.prepare('ROLLBACK').run();
24
+ throw error;
25
+ }
26
+ db.prepare('COMMIT').run();
27
+ for (const action of result.afterCommit ?? []) {
28
+ try {
29
+ await action();
30
+ }
31
+ catch {
32
+ // Best effort, outside the transaction, after the commit. A tab that
33
+ // will not close is not a reason to report failed work that succeeded.
34
+ }
35
+ }
36
+ return result.value;
37
+ }
@@ -0,0 +1,90 @@
1
+ import { TOOLS_BY_NAME, TOOL_OPERATIONS } from "./tools.js";
2
+ /**
3
+ * The tool surface as an adapter: a thin shell over one service call.
4
+ *
5
+ * ── This is the primary route ───────────────────────────────────────────
6
+ *
7
+ * `MILESTONES.md` #27: the service is **spawned by its caller**, serves that
8
+ * session, and exits with it. No port, no daemon, no container. This is the
9
+ * route real callers use, and the command line exists mainly as the second
10
+ * route that makes the parity claim assertable (§5.1).
11
+ *
12
+ * ── What this file may and may not do ───────────────────────────────────
13
+ *
14
+ * It resolves a tool call into arguments, calls **one** service operation,
15
+ * and shapes the outcome for the wire. It reaches no database and no guard:
16
+ * it is handed a {@link BrokerService} rather than finding one, so there is no
17
+ * store handle in scope to be tempted by. Every rule that decides whether an
18
+ * operation is allowed lives behind that seam, or it holds on this route and
19
+ * not the others (`CLAUDE.md`).
20
+ *
21
+ * **Stated honestly, because the seam cannot enforce it:** nothing structural
22
+ * stops a future edit adding a check here. `db.import_isolated` (§7.3) is a
23
+ * build rule precisely because a type cannot express "and you did not
24
+ * reimplement this". What this file does is keep the correct path the easy
25
+ * one — one call in, one outcome out, no branch between them that could
26
+ * decide anything.
27
+ */
28
+ /**
29
+ * Every operation this route offers — all ten, from the tool table.
30
+ *
31
+ * Read from the table rather than written out again, so the tools a caller
32
+ * can see and the operations the parity suite drives cannot drift.
33
+ */
34
+ export const TOOL_SURFACE_OPERATIONS = TOOL_OPERATIONS;
35
+ /**
36
+ * Operations this route does not offer, with the reason each is absent.
37
+ *
38
+ * **Empty, and that is the claim.** §3.1 lists twelve tools and §5.3 lists twelve
39
+ * commands for them, so every operation is on both routes. The array exists
40
+ * so that a later row removing a tool has somewhere to write down why — and
41
+ * so the runner's waiver rule has something to check rather than an absence
42
+ * to interpret.
43
+ *
44
+ * Note what a waiver could *not* buy here: this route declares `readOnly:
45
+ * false`, and the runner refuses a write waiver from a route that is not
46
+ * read-only. Declaring it read-only to buy waivers would be the loophole, so
47
+ * the declaration is made honestly.
48
+ */
49
+ export const TOOL_OPERATION_WAIVERS = [];
50
+ /**
51
+ * The tool-surface adapter.
52
+ *
53
+ * Its `id` is `tool-stdio`, which is also the value §1.6 names for the
54
+ * ledger's `adapter` column — the column that turns "the same rules apply on
55
+ * every route" from a claim into a query. One spelling, used by the registry,
56
+ * the ledger and the conformance report alike.
57
+ */
58
+ export const toolStdioAdapter = {
59
+ id: 'tool-stdio',
60
+ description: 'The tool surface over standard input and output. Spawned by its caller, serves that session, exits with it.',
61
+ readOnly: false,
62
+ operations: TOOL_SURFACE_OPERATIONS,
63
+ waivers: TOOL_OPERATION_WAIVERS,
64
+ invoke: async (service, operation, input) => {
65
+ // The route's own vocabulary is a tool-call object: a name and an
66
+ // arguments record. Anything else is a caller reaching past the
67
+ // transport, so it is refused rather than coerced.
68
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
69
+ throw new TypeError('the tool-surface adapter takes a tool-call object');
70
+ }
71
+ const call = input;
72
+ if (typeof call.name !== 'string') {
73
+ throw new TypeError('a tool call names the tool it is calling');
74
+ }
75
+ const tool = TOOLS_BY_NAME.get(call.name);
76
+ if (tool === undefined) {
77
+ throw new TypeError(`no tool named "${call.name}"`);
78
+ }
79
+ // A tool maps to exactly one operation, so a call naming a tool whose
80
+ // operation is not the one asked for is a wiring bug in the caller rather
81
+ // than something to reconcile silently.
82
+ if (tool.operation !== operation) {
83
+ throw new TypeError(`${call.name} performs "${tool.operation}", not "${String(operation)}"`);
84
+ }
85
+ const args = call.arguments === null || typeof call.arguments !== 'object' || Array.isArray(call.arguments)
86
+ ? {}
87
+ : call.arguments;
88
+ return service.perform({ operation, adapter: 'tool-stdio', arguments: args });
89
+ },
90
+ };