@telora/mcp-products 0.22.159 → 0.22.166

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.
@@ -0,0 +1,181 @@
1
+ /** What the claimant's own repository holds, right now. */
2
+ export interface MigrationObservation {
3
+ /** Every ordinal any local branch holds, deduplicated. */
4
+ observedHeld: string[];
5
+ /** How many refs were read -- evidence the observation is not vacuous. */
6
+ observedRefs: number;
7
+ /** When it was taken (epoch ms). */
8
+ observedAt: number;
9
+ /**
10
+ * Refs this observation could NOT read.
11
+ *
12
+ * NAMED RATHER THAN DROPPED. A ref that could not be read holds an unknown
13
+ * set of ordinals, and "unknown" silently folded in as "none" is the exact
14
+ * shape of fail-open the daemon's own reader refuses. The handler treats a
15
+ * non-empty list as a partial view and refuses the grant, retryably --
16
+ * symmetric with `migration_held_set_unavailable` on the daemon's side. A
17
+ * caller with NO repository at all reports no observation and is unaffected;
18
+ * this is only about a repository that exists and could not be fully read.
19
+ */
20
+ observedUnreadable: string[];
21
+ /**
22
+ * Every ref that was read, with the commit it pointed at. Recorded on the
23
+ * granted row, so "which commits did the allocator look at?" is answered by
24
+ * the claim rather than reconstructed afterwards.
25
+ */
26
+ observedTips: Record<string, string>;
27
+ }
28
+ /** A repo's migration declaration, as much of it as this reader needs. */
29
+ interface Declaration {
30
+ dir: string;
31
+ filePattern: string;
32
+ scheme: MigrationScheme;
33
+ journal?: {
34
+ path?: string;
35
+ };
36
+ }
37
+ /** The two numbering schemes; an unrecognised value is malformed, never defaulted. */
38
+ type MigrationScheme = "sequential" | "timestamp";
39
+ /** The ordinals a declaration's pattern reads out of a list of paths. */
40
+ export declare function ordinalsFromPaths(paths: readonly string[], declaration: Declaration): string[];
41
+ /**
42
+ * The width the declaration's own ordinal group fixes, when it fixes one.
43
+ *
44
+ * WHY THE DECLARATION AND NOT THE FILES. A journal entry is a bare index --
45
+ * `2`, not `0002` -- so placing it needs a width, and reading that width from
46
+ * the matching migration FILES fails in exactly the case that matters: a
47
+ * sibling branch that has advanced its journal but has no file this pattern
48
+ * matches, or a ref carrying journal entries and no migration file at all.
49
+ * The idx was then dropped, and against a stale heartbeat the allocator could
50
+ * grant the very number the journal already holds. The declaration fixes the
51
+ * width independently of what any ref happens to contain.
52
+ *
53
+ * Only a pattern that PINS the width answers -- `\d{4}` or `[0-9]{4}`. A
54
+ * variable-width group (`\d+`) fixes nothing and returns null, which is the
55
+ * honest answer rather than a guess.
56
+ */
57
+ export declare function declaredOrdinalWidth(filePattern: string): number | null;
58
+ /**
59
+ * What reading a declared journal produced.
60
+ *
61
+ * A JOURNAL THAT CANNOT BE READ IS NOT AN EMPTY JOURNAL. This used to answer
62
+ * `[]` for unparseable text, a missing `entries` array, and an index it could
63
+ * not place -- indistinguishable from a journal that genuinely registers
64
+ * nothing. Against a heartbeat taken before a journal-only advance, that drops
65
+ * the held idx and lets the allocator grant the number again: the exact
66
+ * fail-open the observation exists to close, arriving through the one input
67
+ * that has no file to corroborate it.
68
+ */
69
+ export type JournalReading = {
70
+ ok: true;
71
+ ordinals: string[];
72
+ } | {
73
+ ok: false;
74
+ reason: "unparseable" | "no_entries" | "unplaceable_entry";
75
+ };
76
+ /** The idx values a journal registers, normalised to the width in use. */
77
+ export declare function readJournalOrdinals(text: string, width: number | null): JournalReading;
78
+ /**
79
+ * What reading a file produced.
80
+ *
81
+ * ABSENT AND UNREADABLE ARE DIFFERENT ANSWERS, and collapsing them to `null` is
82
+ * how a transient failure came to mean "this product opted out". A missing
83
+ * declaration is a product that does not use the allocator; a declaration that
84
+ * exists and could not be read is a repository this process cannot currently
85
+ * describe, and granting against the daemon's older snapshot alone is exactly
86
+ * what the claimant's reading exists to prevent.
87
+ */
88
+ export type FileReading = {
89
+ kind: "read";
90
+ text: string;
91
+ } | {
92
+ kind: "absent";
93
+ } | {
94
+ kind: "unreadable";
95
+ reason: string;
96
+ };
97
+ /**
98
+ * VALIDATE TO THE CANONICAL CONTRACT.
99
+ *
100
+ * The rules here are the daemon's, transcribed:
101
+ * `packages/daemon/src/migrations/migration-manifest.ts` is the single
102
+ * definition, and `migration-manifest-parity.test.ts` pins that this reader
103
+ * accepts and rejects the same declarations it does.
104
+ *
105
+ * WHY TRANSCRIBED RATHER THAN IMPORTED. This package is published and installed
106
+ * on daemon hosts by itself; it cannot depend on `@telora/daemon`. The parity
107
+ * test is what keeps a transcription from drifting into a second, laxer
108
+ * contract.
109
+ *
110
+ * WHY IT MATTERS THAT THEY AGREE. A claimant that accepts a declaration the
111
+ * daemon calls unreadable reports a COMPLETE observation of a repository it
112
+ * cannot actually read -- an empty one, because a pattern that does not compile
113
+ * matches nothing -- and a focus-scoped claim then grants against the daemon's
114
+ * older published snapshot with an empty "current" reading vouching for it.
115
+ * Every present-but-invalid declaration must reach the same verdict on both
116
+ * sides: not usable.
117
+ */
118
+ export declare function validateDeclaration(parsed: unknown): {
119
+ ok: true;
120
+ declaration: Declaration;
121
+ } | {
122
+ ok: false;
123
+ problem: string;
124
+ };
125
+ /** Injected seams, so the reader is testable over a real temp repo or a fake. */
126
+ export interface ObservationDeps {
127
+ cwd: () => string;
128
+ runGit: (args: string[], cwd: string) => string | null;
129
+ readFile: (path: string) => FileReading;
130
+ now: () => number;
131
+ }
132
+ export declare const defaultObservationDeps: ObservationDeps;
133
+ /**
134
+ * What observing this process's repository produced.
135
+ *
136
+ * THE THREE OUTCOMES ARE NOT INTERCHANGEABLE, and collapsing them to
137
+ * "observation or null" is what let a failed read be served as though the
138
+ * caller simply had no repository. The edge then treated the absence as an
139
+ * empty VALID report and granted against the daemon's older leased snapshot
140
+ * alone -- which is precisely the window the observation exists to close.
141
+ *
142
+ * - `not_applicable` -- there is genuinely nothing to observe: no repository,
143
+ * or a product that declares no numbering. The grant proceeds on the
144
+ * daemon's reading exactly as it did before this reader existed. This is
145
+ * the ONLY silent path, and it is silent on purpose: refusing here would
146
+ * block claiming for every caller outside a checkout.
147
+ * - `incomplete` -- there IS a declared repository and it could not be read
148
+ * whole. Nothing is sent and the claim is not attempted: a partial view
149
+ * folded in as "nothing" is how a held number reads as free.
150
+ * - `observed` -- a complete reading, taken over a ref set that did not move
151
+ * while it was being taken.
152
+ */
153
+ export type ObservationOutcome = {
154
+ kind: "observed";
155
+ observation: MigrationObservation;
156
+ } | {
157
+ kind: "not_applicable";
158
+ reason: "no_repository" | "no_declaration";
159
+ } | {
160
+ kind: "incomplete";
161
+ reason: string;
162
+ };
163
+ /**
164
+ * Observe every local branch's migration ordinals, right now.
165
+ *
166
+ * READ BY COMMIT, NOT BY NAME. The tips are captured once, up front, and every
167
+ * tree and journal below is read through the captured SHA. Reading through the
168
+ * mutable branch NAME meant a branch that moved mid-scan was reported at its
169
+ * OLD tip while its NEW tree had been folded in -- `granted_against_tips` then
170
+ * recorded provenance that was simply false, naming commits the allocator had
171
+ * not looked at.
172
+ *
173
+ * AND THE SET IS RE-CHECKED AFTERWARDS. Reading each ref by SHA makes the
174
+ * individual reads coherent; it does not make the SET of them a single moment.
175
+ * A sibling that commits, or a branch created, between the first ref and the
176
+ * last is invisible to a scan that never looks again -- so the enumeration is
177
+ * repeated at the end, and any difference makes the whole reading `incomplete`
178
+ * rather than a snapshot of no particular instant.
179
+ */
180
+ export declare function observeLocalMigrations(deps?: ObservationDeps): ObservationOutcome;
181
+ export {};
@@ -0,0 +1,446 @@
1
+ // ---------------------------------------------------------------------------
2
+ // WHAT THE CLAIMANT CAN SEE, AT THE MOMENT IT CLAIMS.
3
+ //
4
+ // THE HOLE THIS CLOSES. The allocator grants against a held set the daemon
5
+ // PUBLISHED -- a snapshot of branch tips taken at some earlier instant. The
6
+ // grant is atomic against other CLAIMS (the partial unique index sees to
7
+ // that), and a lease bounds how old the snapshot may be, but neither makes the
8
+ // snapshot CURRENT. Between the reading and the grant a sibling branch can
9
+ // acquire a migration nobody claimed, and the handler -- which has no git --
10
+ // cannot know. It then grants that very number to somebody else.
11
+ //
12
+ // WHO CAN KNOW. The claimant. Every focus team works in a worktree of the SAME
13
+ // repository, sharing one object store, so a sibling branch's commit is
14
+ // visible from here the instant it is made -- which is exactly how the daemon
15
+ // reads those branches in the first place. The party asking for a number is
16
+ // the one party that both holds the repo and is present at grant time.
17
+ //
18
+ // SO THE CLAIMANT REPORTS WHAT IT SEES, and the handler folds that into the
19
+ // numbers already taken. This is SAFE BY CONSTRUCTION, and the direction
20
+ // matters: an observation can only ever ADD to what is taken, so a wrong or
21
+ // stale one can make a granted number higher than it needed to be and can
22
+ // never make two teams share one. It does not decide; the index still decides.
23
+ //
24
+ // FAILURE IS SILENT AND HARMLESS. No repo, no declaration, no git, a pattern
25
+ // that does not parse -- any of them yields no observation, and the grant
26
+ // proceeds exactly as it did before. A refusal here would block claiming for
27
+ // every caller that runs outside a checkout, which is a worse failure than the
28
+ // one being narrowed.
29
+ // ---------------------------------------------------------------------------
30
+ import { execFileSync } from "node:child_process";
31
+ import { readFileSync } from "node:fs";
32
+ const MIGRATION_SCHEMES = ["sequential", "timestamp"];
33
+ const MAX_OBSERVED_ORDINALS = 5000;
34
+ const GIT_TIMEOUT_MS = 10_000;
35
+ /** Run git in `cwd`, or null when it fails for any reason. */
36
+ function git(args, cwd) {
37
+ try {
38
+ return execFileSync("git", args, {
39
+ cwd,
40
+ encoding: "utf-8",
41
+ timeout: GIT_TIMEOUT_MS,
42
+ maxBuffer: 16 * 1024 * 1024,
43
+ stdio: ["ignore", "pipe", "ignore"],
44
+ });
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ /** The ordinals a declaration's pattern reads out of a list of paths. */
51
+ export function ordinalsFromPaths(paths, declaration) {
52
+ let pattern;
53
+ try {
54
+ pattern = new RegExp(declaration.filePattern);
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ const ordinals = [];
60
+ for (const path of paths) {
61
+ const basename = path.slice(path.lastIndexOf("/") + 1);
62
+ const ordinal = pattern.exec(basename)?.groups?.ordinal;
63
+ if (ordinal)
64
+ ordinals.push(ordinal);
65
+ }
66
+ return ordinals;
67
+ }
68
+ /**
69
+ * The width the declaration's own ordinal group fixes, when it fixes one.
70
+ *
71
+ * WHY THE DECLARATION AND NOT THE FILES. A journal entry is a bare index --
72
+ * `2`, not `0002` -- so placing it needs a width, and reading that width from
73
+ * the matching migration FILES fails in exactly the case that matters: a
74
+ * sibling branch that has advanced its journal but has no file this pattern
75
+ * matches, or a ref carrying journal entries and no migration file at all.
76
+ * The idx was then dropped, and against a stale heartbeat the allocator could
77
+ * grant the very number the journal already holds. The declaration fixes the
78
+ * width independently of what any ref happens to contain.
79
+ *
80
+ * Only a pattern that PINS the width answers -- `\d{4}` or `[0-9]{4}`. A
81
+ * variable-width group (`\d+`) fixes nothing and returns null, which is the
82
+ * honest answer rather than a guess.
83
+ */
84
+ export function declaredOrdinalWidth(filePattern) {
85
+ const group = /\(\?<ordinal>((?:\\.|[^()\\])*)\)/.exec(filePattern)?.[1];
86
+ if (group === undefined)
87
+ return null;
88
+ const fixed = /^(?:\\d|\[0-9\])\{(\d+)\}$/.exec(group);
89
+ return fixed ? Number(fixed[1]) : null;
90
+ }
91
+ /** The idx values a journal registers, normalised to the width in use. */
92
+ export function readJournalOrdinals(text, width) {
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(text);
96
+ }
97
+ catch {
98
+ return { ok: false, reason: "unparseable" };
99
+ }
100
+ const entries = parsed?.entries;
101
+ if (!Array.isArray(entries))
102
+ return { ok: false, reason: "no_entries" };
103
+ const ordinals = [];
104
+ for (const entry of entries) {
105
+ const idx = String(entry?.idx ?? "").trim();
106
+ // An entry this reader cannot place is reported, never dropped. Dropping
107
+ // was safe only under the assumption that the daemon's reading covers it --
108
+ // and a journal-only advance on a branch the heartbeat predates is exactly
109
+ // the case the daemon's reading does NOT cover.
110
+ if (!/^\d+$/.test(idx) || width === null || idx.length > width) {
111
+ return { ok: false, reason: "unplaceable_entry" };
112
+ }
113
+ ordinals.push(idx.padStart(width, "0"));
114
+ }
115
+ return { ok: true, ordinals };
116
+ }
117
+ /**
118
+ * A repo-relative path safe to hand to git as a pathspec.
119
+ *
120
+ * Mirrors `requireRepoRelativePath` in the canonical reader.
121
+ */
122
+ function repoRelativePath(raw, key) {
123
+ if (typeof raw !== "string") {
124
+ return {
125
+ ok: false,
126
+ problem: raw === undefined
127
+ ? `missing required "${key}" string (check for a typo in the key)`
128
+ : `"${key}" must be a string (got ${typeof raw})`,
129
+ };
130
+ }
131
+ const value = raw.trim();
132
+ if (value === "")
133
+ return { ok: false, problem: `"${key}" must not be empty` };
134
+ if (value.startsWith("/")) {
135
+ return { ok: false, problem: `"${key}" must be repo-relative, not absolute (got ${JSON.stringify(value)})` };
136
+ }
137
+ if (value.split("/").includes("..")) {
138
+ return { ok: false, problem: `"${key}" must not escape the repo with ".." (got ${JSON.stringify(value)})` };
139
+ }
140
+ return { ok: true, value: value.replace(/\/+$/, "") };
141
+ }
142
+ /**
143
+ * VALIDATE TO THE CANONICAL CONTRACT.
144
+ *
145
+ * The rules here are the daemon's, transcribed:
146
+ * `packages/daemon/src/migrations/migration-manifest.ts` is the single
147
+ * definition, and `migration-manifest-parity.test.ts` pins that this reader
148
+ * accepts and rejects the same declarations it does.
149
+ *
150
+ * WHY TRANSCRIBED RATHER THAN IMPORTED. This package is published and installed
151
+ * on daemon hosts by itself; it cannot depend on `@telora/daemon`. The parity
152
+ * test is what keeps a transcription from drifting into a second, laxer
153
+ * contract.
154
+ *
155
+ * WHY IT MATTERS THAT THEY AGREE. A claimant that accepts a declaration the
156
+ * daemon calls unreadable reports a COMPLETE observation of a repository it
157
+ * cannot actually read -- an empty one, because a pattern that does not compile
158
+ * matches nothing -- and a focus-scoped claim then grants against the daemon's
159
+ * older published snapshot with an empty "current" reading vouching for it.
160
+ * Every present-but-invalid declaration must reach the same verdict on both
161
+ * sides: not usable.
162
+ */
163
+ export function validateDeclaration(parsed) {
164
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
165
+ return { ok: false, problem: `declaration must be a JSON object (got ${Array.isArray(parsed) ? "an array" : typeof parsed})` };
166
+ }
167
+ const raw = parsed;
168
+ const dir = repoRelativePath(raw.dir, "dir");
169
+ if (!dir.ok)
170
+ return dir;
171
+ // The pattern must COMPILE and capture both groups. A claim is recorded by
172
+ // number AND tag, and the confirm pass matches the file that lands by both.
173
+ const pattern = raw.filePattern;
174
+ if (typeof pattern !== "string") {
175
+ return {
176
+ ok: false,
177
+ problem: pattern === undefined
178
+ ? `missing required "filePattern" string (check for a typo in the key)`
179
+ : `"filePattern" must be a string (got ${typeof pattern})`,
180
+ };
181
+ }
182
+ if (pattern.trim() === "")
183
+ return { ok: false, problem: `"filePattern" must not be empty` };
184
+ let compiled;
185
+ try {
186
+ compiled = new RegExp(pattern);
187
+ }
188
+ catch (err) {
189
+ return {
190
+ ok: false,
191
+ problem: `"filePattern" is not a valid regular expression (${String(err?.message ?? err)})`,
192
+ };
193
+ }
194
+ for (const group of ["ordinal", "tag"]) {
195
+ if (!compiled.source.includes(`(?<${group}>`)) {
196
+ return { ok: false, problem: `"filePattern" must carry a named capture group (?<${group}>...)` };
197
+ }
198
+ }
199
+ // The scheme is required and never inferred: guessing it from the ordinal's
200
+ // width reads a 14-digit sequential scheme as a timestamp, silently.
201
+ const scheme = raw.scheme;
202
+ if (typeof scheme !== "string") {
203
+ return {
204
+ ok: false,
205
+ problem: scheme === undefined
206
+ ? `missing required "scheme" string -- one of ${MIGRATION_SCHEMES.join(" | ")}`
207
+ : `"scheme" must be a string (got ${typeof scheme})`,
208
+ };
209
+ }
210
+ const matched = MIGRATION_SCHEMES.find((value) => value === scheme.trim());
211
+ if (matched === undefined) {
212
+ return {
213
+ ok: false,
214
+ problem: `"scheme" must be one of ${MIGRATION_SCHEMES.join(" | ")} (got ${JSON.stringify(scheme)})`,
215
+ };
216
+ }
217
+ // The journal is optional; present-but-wrong is malformed like every other field.
218
+ let journal;
219
+ if (raw.journal !== undefined) {
220
+ const value = raw.journal;
221
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
222
+ return {
223
+ ok: false,
224
+ problem: `"journal" must be an object with a "path" (got ${Array.isArray(value) ? "an array" : typeof value})`,
225
+ };
226
+ }
227
+ const journalPath = repoRelativePath(value.path, "journal.path");
228
+ if (!journalPath.ok)
229
+ return journalPath;
230
+ journal = { path: journalPath.value };
231
+ }
232
+ return {
233
+ ok: true,
234
+ declaration: {
235
+ dir: dir.value,
236
+ filePattern: pattern,
237
+ scheme: matched,
238
+ ...(journal ? { journal } : {}),
239
+ },
240
+ };
241
+ }
242
+ /**
243
+ * Read the repo's declaration.
244
+ *
245
+ * MALFORMED IS NOT MISSING. Unparseable JSON, and any shape the canonical
246
+ * contract rejects, are a declaration that EXISTS and cannot be used -- the
247
+ * same loud-on-malformed posture the daemon's own manifest reader takes. Read
248
+ * as "absent", any of them silently opts the product out of the allocator at
249
+ * the exact moment it is being asked for a number.
250
+ */
251
+ function readDeclaration(repoRoot, readFile) {
252
+ const reading = readFile(`${repoRoot}/telora.migrations.json`);
253
+ if (reading.kind === "absent")
254
+ return { kind: "absent" };
255
+ if (reading.kind === "unreadable")
256
+ return { kind: "unreadable", reason: reading.reason };
257
+ let parsed;
258
+ try {
259
+ parsed = JSON.parse(reading.text);
260
+ }
261
+ catch (err) {
262
+ return { kind: "unreadable", reason: `declaration is not valid JSON: ${String(err?.message ?? err)}` };
263
+ }
264
+ const validated = validateDeclaration(parsed);
265
+ if (!validated.ok)
266
+ return { kind: "unreadable", reason: validated.problem };
267
+ return { kind: "declared", declaration: validated.declaration };
268
+ }
269
+ export const defaultObservationDeps = {
270
+ cwd: () => process.cwd(),
271
+ runGit: git,
272
+ // Imported at the top of the module, NOT `require`d here. This package is
273
+ // NodeNext ESM: `require` is not defined in it, so the call threw, the catch
274
+ // turned that into "no declaration", and every production claim sent no
275
+ // observation at all -- silently, because the same null means "this caller is
276
+ // not in a repo". The tests injected their own readFile and never ran this.
277
+ readFile: (path) => {
278
+ try {
279
+ return { kind: "read", text: readFileSync(path, "utf-8") };
280
+ }
281
+ catch (err) {
282
+ // ENOENT is an ANSWER -- this product declares no numbering. Anything
283
+ // else (a permission error, an I/O error, a directory where a file was
284
+ // expected) is the read not happening, and must not read as an opt-out.
285
+ const code = err?.code;
286
+ if (code === "ENOENT" || code === "ENOTDIR")
287
+ return { kind: "absent" };
288
+ return { kind: "unreadable", reason: String(err?.message ?? err) };
289
+ }
290
+ },
291
+ now: () => Date.now(),
292
+ };
293
+ /** Enumerate local branches with the commit each points at, right now. */
294
+ function enumerateRefs(deps, root) {
295
+ const output = deps.runGit(["for-each-ref", "--format=%(refname:short) %(objectname)", "refs/heads"], root);
296
+ if (output === null)
297
+ return null;
298
+ const refs = new Map();
299
+ for (const line of output.split("\n")) {
300
+ const [name, tip] = line.trim().split(/\s+/);
301
+ if (name)
302
+ refs.set(name, tip ?? "");
303
+ }
304
+ return refs;
305
+ }
306
+ /** Whether two ref enumerations describe the same branches at the same commits. */
307
+ function sameRefs(a, b) {
308
+ if (a.size !== b.size)
309
+ return false;
310
+ for (const [name, tip] of a)
311
+ if (b.get(name) !== tip)
312
+ return false;
313
+ return true;
314
+ }
315
+ /**
316
+ * Observe every local branch's migration ordinals, right now.
317
+ *
318
+ * READ BY COMMIT, NOT BY NAME. The tips are captured once, up front, and every
319
+ * tree and journal below is read through the captured SHA. Reading through the
320
+ * mutable branch NAME meant a branch that moved mid-scan was reported at its
321
+ * OLD tip while its NEW tree had been folded in -- `granted_against_tips` then
322
+ * recorded provenance that was simply false, naming commits the allocator had
323
+ * not looked at.
324
+ *
325
+ * AND THE SET IS RE-CHECKED AFTERWARDS. Reading each ref by SHA makes the
326
+ * individual reads coherent; it does not make the SET of them a single moment.
327
+ * A sibling that commits, or a branch created, between the first ref and the
328
+ * last is invisible to a scan that never looks again -- so the enumeration is
329
+ * repeated at the end, and any difference makes the whole reading `incomplete`
330
+ * rather than a snapshot of no particular instant.
331
+ */
332
+ export function observeLocalMigrations(deps = defaultObservationDeps) {
333
+ const cwd = deps.cwd();
334
+ const root = deps.runGit(["rev-parse", "--show-toplevel"], cwd)?.trim();
335
+ if (!root)
336
+ return { kind: "not_applicable", reason: "no_repository" };
337
+ const declared = readDeclaration(root, deps.readFile);
338
+ if (declared.kind === "unreadable") {
339
+ // A declaration that EXISTS and cannot be used. Answering "no_declaration"
340
+ // here told the handler this product had opted out, and the grant was made
341
+ // against the daemon's older snapshot alone.
342
+ return { kind: "incomplete", reason: `migration declaration unreadable: ${declared.reason}` };
343
+ }
344
+ if (declared.kind === "absent")
345
+ return { kind: "not_applicable", reason: "no_declaration" };
346
+ const declaration = declared.declaration;
347
+ // Name and tip in ONE call: the tips are recorded on the granted row, so the
348
+ // commits the allocator looked at are readable from the claim afterwards.
349
+ const refs = enumerateRefs(deps, root);
350
+ if (refs === null) {
351
+ // A DECLARED repository whose branches could not be enumerated. This used
352
+ // to answer null -- the same answer as "not in a repository" -- so the
353
+ // claim went out with no observation and the grant was made blind.
354
+ return { kind: "incomplete", reason: "could not enumerate local branches" };
355
+ }
356
+ if (refs.size === 0) {
357
+ return { kind: "incomplete", reason: "a declared repository reported no local branches" };
358
+ }
359
+ const held = new Set();
360
+ const journalTexts = [];
361
+ const observedTips = {};
362
+ const observedUnreadable = [];
363
+ const journalPath = declaration.journal?.path;
364
+ for (const [name, tip] of refs) {
365
+ // By SHA. The name is carried only for reporting.
366
+ const listed = deps.runGit(["ls-tree", "-r", "--name-only", tip, "--", declaration.dir], root);
367
+ if (listed === null) {
368
+ // NAMED, not skipped. What this ref holds is unknown, and unknown folded
369
+ // in as "nothing" is how a held number reads as free.
370
+ observedUnreadable.push(name);
371
+ continue;
372
+ }
373
+ observedTips[name] = tip;
374
+ for (const ordinal of ordinalsFromPaths(listed.split("\n"), declaration)) {
375
+ held.add(ordinal);
376
+ }
377
+ if (!journalPath)
378
+ continue;
379
+ // ABSENT vs UNREADABLE, asked separately. `git show` returns nothing for
380
+ // both "no such path" and "the object cannot be read", and most refs
381
+ // legitimately have no journal -- so the tree is asked whether the path
382
+ // EXISTS on this commit before its contents are held against anyone.
383
+ const present = deps.runGit(["ls-tree", tip, "--", journalPath], root);
384
+ if (present === null) {
385
+ observedUnreadable.push(`${name}:${journalPath}`);
386
+ continue;
387
+ }
388
+ if (present.trim() === "")
389
+ continue; // genuinely absent
390
+ const text = deps.runGit(["show", `${tip}:${journalPath}`], root);
391
+ if (text === null) {
392
+ // The ref SAYS it has one and it could not be read. Reading that as an
393
+ // empty journal drops whatever indexes it registers.
394
+ observedUnreadable.push(`${name}:${journalPath}`);
395
+ continue;
396
+ }
397
+ journalTexts.push({ ref: name, text });
398
+ }
399
+ // The journal's bare indexes are normalised to the DECLARED width, falling
400
+ // back to the width the files themselves use when the pattern does not fix
401
+ // one. A bare 352 and a padded 0352 are two widths in one set, and the
402
+ // candidate rule refuses a product whose ordinals are not one fixed width.
403
+ const fileWidths = [...new Set([...held].map((o) => o.length))];
404
+ const width = declaredOrdinalWidth(declaration.filePattern)
405
+ ?? (fileWidths.length === 1 ? (fileWidths[0] ?? null) : null);
406
+ for (const { ref, text } of journalTexts) {
407
+ const reading = readJournalOrdinals(text, width);
408
+ if (!reading.ok) {
409
+ // Malformed, or carrying an index this reader cannot place. Either way it
410
+ // is a declared journal whose contents are not established.
411
+ observedUnreadable.push(`${ref}:${journalPath ?? "journal"}`);
412
+ continue;
413
+ }
414
+ for (const ordinal of reading.ordinals)
415
+ held.add(ordinal);
416
+ }
417
+ // THE SET, RE-CHECKED. Every read above is coherent with its own commit; that
418
+ // says nothing about whether the branches moved underneath the scan as a
419
+ // whole.
420
+ const after = enumerateRefs(deps, root);
421
+ if (after === null) {
422
+ return { kind: "incomplete", reason: "could not re-read local branches after the scan" };
423
+ }
424
+ if (!sameRefs(refs, after)) {
425
+ return { kind: "incomplete", reason: "local branches moved while they were being read" };
426
+ }
427
+ // Bounded from the HIGH end. At one fixed width lexicographic order is
428
+ // numeric order, and it is the newest ordinals that decide the next number --
429
+ // keeping the oldest 5,000 and discarding the rest would drop exactly the
430
+ // evidence a sequential grant depends on.
431
+ const sorted = [...held].sort();
432
+ const observedHeld = sorted.length > MAX_OBSERVED_ORDINALS
433
+ ? sorted.slice(-MAX_OBSERVED_ORDINALS)
434
+ : sorted;
435
+ return {
436
+ kind: "observed",
437
+ observation: {
438
+ observedHeld,
439
+ observedRefs: Object.keys(observedTips).length,
440
+ observedAt: deps.now(),
441
+ observedUnreadable,
442
+ observedTips,
443
+ },
444
+ };
445
+ }
446
+ //# sourceMappingURL=migrationObservation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrationObservation.js","sourceRoot":"","sources":["../../src/handlers/migrationObservation.ts"],"names":[],"mappings":"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"}
@@ -26,7 +26,7 @@ import { callProductApi, successResult, validationError, wrapHandler, } from "..
26
26
  // write that omits that pass leaves it untouched.
27
27
  // '' clears (inherit the builtin stage / backend default).
28
28
  export const SELECTABLE_PASS_MODEL_IDS = new Set([
29
- "claude-fable-5-1", "claude-opus-5", "claude-opus-4-8", "claude-sonnet-5",
29
+ "claude-fable-5-1", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-sonnet-5",
30
30
  "claude-haiku-4-5-20251001", "gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
31
31
  ]);
32
32
  export function isSelectablePassModelId(id) {