@telora/mcp-products 0.22.159 → 0.22.165

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.
@@ -30,3 +30,4 @@ export { registerEndpointTools, handleEndpointTool } from "./endpointHandlers.js
30
30
  export { registerServiceDefinitionTools, handleServiceDefinitionTool } from "./serviceDefinitionHandlers.js";
31
31
  export { registerStarterRepoTools, handleStarterRepoTool } from "./starterRepoHandlers.js";
32
32
  export { registerCodeObservationTools, handleCodeObservationCreate } from "./codeObservationHandlers.js";
33
+ export { registerMigrationClaimTools, handleMigrationClaim } from "./migrationClaimHandlers.js";
@@ -33,4 +33,5 @@ export { registerEndpointTools, handleEndpointTool } from "./endpointHandlers.js
33
33
  export { registerServiceDefinitionTools, handleServiceDefinitionTool } from "./serviceDefinitionHandlers.js";
34
34
  export { registerStarterRepoTools, handleStarterRepoTool } from "./starterRepoHandlers.js";
35
35
  export { registerCodeObservationTools, handleCodeObservationCreate } from "./codeObservationHandlers.js";
36
+ export { registerMigrationClaimTools, handleMigrationClaim } from "./migrationClaimHandlers.js";
36
37
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,uDAAuD;AACvD,8EAA8E;AAE9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,+BAA+B,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,sCAAsC,EACtC,gCAAgC,GACjC,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACtG,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7G,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC3F,OAAO,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,uDAAuD;AACvD,8EAA8E;AAE9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,+BAA+B,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAChF,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,8BAA8B,EAAE,MAAM,mBAAmB,CAAC;AACnH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,sCAAsC,EACtC,gCAAgC,GACjC,MAAM,wCAAwC,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACtG,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7G,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAC3F,OAAO,EAAE,4BAA4B,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AACzG,OAAO,EAAE,2BAA2B,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type ObservationOutcome } from "./migrationObservation.js";
3
+ import { successResult, validationError, type GetCreds, type ToolProfile } from "../shared.js";
4
+ export interface MigrationClaimParams {
5
+ productId?: string;
6
+ focusId?: string;
7
+ tag?: string;
8
+ agentSessionId?: string;
9
+ }
10
+ export declare function handleMigrationClaim(params: MigrationClaimParams, callApi: (body: Record<string, unknown>) => Promise<unknown>,
11
+ /**
12
+ * What THIS repository holds right now. Sent with the claim so the grant is
13
+ * made against a view taken at grant time, not only against the snapshot the
14
+ * daemon published earlier -- see migrationObservation.ts. Injected for tests.
15
+ */
16
+ observe?: () => ObservationOutcome): Promise<ReturnType<typeof successResult> | ReturnType<typeof validationError>>;
17
+ export declare function registerMigrationClaimTools(server: McpServer, getCreds: GetCreds, _profile?: ToolProfile): void;
@@ -0,0 +1,217 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Migration-number claim tool (consolidated):
3
+ // telora_product_migration (claim | list)
4
+ //
5
+ // THE RULE THIS TOOL EXISTS TO MAKE POSSIBLE: a migration number is claimed
6
+ // once, through Telora, BEFORE the file is named. Never read from your own
7
+ // worktree.
8
+ //
9
+ // Why: two focuses armed concurrently on one product each work in their own
10
+ // worktree, cut from the same base. A team that picks the next number by
11
+ // reading the highest one it can see picks the same number its sibling just
12
+ // picked. Measured 2026-09-22 on Tinker Tailor: from base 0351, two armed
13
+ // focuses both authored 0352, 0353, 0354 and 0355. Git sees no conflict (the
14
+ // files have different names), the merge admits it, and the breakage appears
15
+ // only when the migrations are applied in order -- after which repairing it is
16
+ // a renumber of every file plus the journal tail plus every reference.
17
+ //
18
+ // The number is granted from the union of what integration holds, what every
19
+ // live focus branch holds, and every outstanding claim -- none of which a
20
+ // single worktree can see. The grant is atomic at the database, so two teams
21
+ // claiming in the same instant cannot receive one number.
22
+ // ---------------------------------------------------------------------------
23
+ import { z } from "zod";
24
+ import { observeLocalMigrations } from "./migrationObservation.js";
25
+ import { callProductApi, successResult, validationError, wrapHandler, LIST_INDEX_NOTE, } from "../shared.js";
26
+ export async function handleMigrationClaim(params, callApi,
27
+ /**
28
+ * What THIS repository holds right now. Sent with the claim so the grant is
29
+ * made against a view taken at grant time, not only against the snapshot the
30
+ * daemon published earlier -- see migrationObservation.ts. Injected for tests.
31
+ */
32
+ observe = observeLocalMigrations) {
33
+ if (!params.productId)
34
+ return validationError("productId required for claim");
35
+ const contested = [];
36
+ for (let attempt = 1; attempt <= MAX_CLAIM_ATTEMPTS; attempt += 1) {
37
+ // WHAT THIS REPOSITORY HOLDS -- and, when it cannot say, saying so instead
38
+ // of saying nothing.
39
+ //
40
+ // Ordinals are additive: they only ever join the taken set, so they can
41
+ // raise the granted number and can never make two teams share one. The
42
+ // report of what could NOT be read is not additive evidence at all -- it
43
+ // tells the allocator the view is partial -- so it is sent whole, even when
44
+ // the refs that WERE read happened to hold nothing.
45
+ //
46
+ // An INCOMPLETE reading is refused HERE rather than sent. There is nothing
47
+ // useful to transmit: the fields would describe a subset of the repository
48
+ // with no way for the handler to know what is missing, and the handler's
49
+ // own fallback -- grant against the daemon's older leased snapshot -- is
50
+ // exactly the window this reader exists to close. A refusal is retryable; a
51
+ // number granted twice is not.
52
+ const observed = observe();
53
+ if (observed.kind === "incomplete")
54
+ return refuseIncomplete(observed.reason);
55
+ const body = {
56
+ action: "migration_claim_create",
57
+ productId: params.productId,
58
+ };
59
+ if (observed.kind === "observed") {
60
+ const { observation } = observed;
61
+ body.observedHeld = observation.observedHeld;
62
+ body.observedRefs = observation.observedRefs;
63
+ body.observedAt = observation.observedAt;
64
+ body.observedUnreadable = observation.observedUnreadable;
65
+ body.observedTips = observation.observedTips;
66
+ }
67
+ else {
68
+ // GENUINELY NOTHING TO OBSERVE -- no repository, or a product that
69
+ // declares no numbering. Declared explicitly so the handler can tell a
70
+ // deliberate omission from a claimant that simply sent no fields; the two
71
+ // used to be the same wire message.
72
+ body.observationScope = observed.reason;
73
+ }
74
+ if (params.focusId !== undefined)
75
+ body.focusId = params.focusId;
76
+ if (params.tag !== undefined)
77
+ body.tag = params.tag;
78
+ if (params.agentSessionId !== undefined)
79
+ body.agentSessionId = params.agentSessionId;
80
+ const result = await callApi(body);
81
+ const ordinal = String(result.claim.ordinal);
82
+ // ─── THE BARRIER ─────────────────────────────────────────────────────────
83
+ //
84
+ // THE WINDOW THIS CLOSES. Everything above bounds the reading; nothing
85
+ // above makes it hold THROUGH the insert. The scan ends, the request
86
+ // crosses the network, the row is inserted -- and in those hundreds of
87
+ // milliseconds a sibling worktree can commit a migration it never claimed.
88
+ // The number just granted is then one a branch already holds, and no party
89
+ // to the grant could have known: the handler has no git, and this process
90
+ // had already stopped looking.
91
+ //
92
+ // So it looks AGAIN, after the grant, and asks the one question that
93
+ // matters: is the number I was given still free on every branch? A grant
94
+ // that fails that check is not returned. The next attempt re-reads, and
95
+ // that reading now CONTAINS the sibling's ordinal, so the allocator is
96
+ // asked for a number above it.
97
+ //
98
+ // This does not make the grant atomic against the repository -- nothing on
99
+ // either side of the boundary can be, because a commit that does not exist
100
+ // yet is unobservable. What it does is make the unobservable interval
101
+ // bounded and CHECKED: a number that lost the race is detected and
102
+ // discarded rather than handed to a team that writes a file at it.
103
+ //
104
+ // THE LOSING CLAIM IS LEFT STANDING, deliberately. Its ordinal really is
105
+ // taken -- by the sibling's unclaimed file -- so releasing it would offer
106
+ // that number to a third team. It stays until the lifecycle releases it
107
+ // with its focus, and the file that took it is named to the team that wrote
108
+ // it by the unclaimed-migration record.
109
+ if (observed.kind !== "observed")
110
+ return granted(result, params);
111
+ const after = observe();
112
+ if (after.kind !== "observed") {
113
+ // LOSS OF VERIFICATION, NOT PROOF OF FREEDOM.
114
+ //
115
+ // The pre-grant reading was complete, so this process WAS in a declared
116
+ // repository moments ago. A second reading that now answers `incomplete`
117
+ // -- or `not_applicable`, which after a complete one means a transient
118
+ // `rev-parse` failure or a declaration that just disappeared -- has not
119
+ // established that the number still is free. It has established that
120
+ // nothing can currently say.
121
+ //
122
+ // Returning the ordinal there is the barrier declaring success on the
123
+ // strength of its own failure. The claim ROW stays (its ordinal may well
124
+ // be taken, and releasing it would offer that number to a third team),
125
+ // but the caller is refused and retries.
126
+ const reason = after.kind === "incomplete"
127
+ ? after.reason
128
+ : `the repository stopped being readable mid-claim (${after.reason})`;
129
+ return refuseIncomplete(`${reason} -- a number was granted and could NOT be confirmed still free`);
130
+ }
131
+ if (!after.observation.observedHeld.includes(ordinal)) {
132
+ return granted(result, params);
133
+ }
134
+ contested.push(ordinal);
135
+ }
136
+ return validationError(`Cannot claim a migration number: ${contested.length} grant(s) `
137
+ + `(${contested.join(", ")}) were each taken by a branch between the reading they were `
138
+ + `made against and the moment they were granted. That is a sibling writing migrations `
139
+ + `without claiming them -- the rule this tool exists to enforce -- rather than ordinary `
140
+ + `contention. Find the unclaimed files on the other branches before retrying.`);
141
+ }
142
+ /** How many times a grant may lose the scan-to-insert race before refusing. */
143
+ const MAX_CLAIM_ATTEMPTS = 3;
144
+ /** The refusal for a reading that could not be completed. */
145
+ function refuseIncomplete(reason) {
146
+ return validationError(`Cannot claim a migration number: this repository could not be read completely `
147
+ + `(${reason}). The claim is not attempted, because granting against a partial view of `
148
+ + `the branches is how two teams receive one number. Retry; if it persists, check the `
149
+ + `repository rather than numbering by hand.`);
150
+ }
151
+ /** A grant that survived the barrier. */
152
+ function granted(result, params) {
153
+ return successResult({
154
+ claim: result.claim,
155
+ // Stated back rather than assumed: the number is the one to write, and the
156
+ // tag recorded here is the INTENT -- the file may carry a different one,
157
+ // and the confirm pass records what the file actually carries.
158
+ use: `${String(result.claim.ordinal)}${params.tag ? `_${params.tag}` : ""}`,
159
+ });
160
+ }
161
+ export function registerMigrationClaimTools(server, getCreds, _profile = 'full') {
162
+ server.tool("telora_product_migration", "CLAIM A MIGRATION NUMBER BEFORE YOU NAME THE FILE. Never pick a migration " +
163
+ "number by reading your own worktree: two focuses armed at once on one " +
164
+ "product are cut from the same base, so 'the next free number' computed " +
165
+ "locally is the same number your sibling just computed. Git sees no conflict " +
166
+ "(the filenames differ), the merge admits it, and it surfaces only when the " +
167
+ "migrations run in order. This tool grants a number free across integration, " +
168
+ "every live focus branch, and every outstanding claim -- atomically, so two " +
169
+ "teams claiming in the same instant never receive one number. Pass the tag " +
170
+ "you intend to use; the file may end up named differently and that is fine " +
171
+ "(the daemon confirms the claim with the tag the file actually carries). " +
172
+ "REFUSALS are never a guessed number: migration_numbering_unavailable means " +
173
+ "the product declares no numbering (telora.migrations.json) or its " +
174
+ "declaration is broken; migration_held_set_unavailable means no daemon has " +
175
+ "reported what the branches hold, or a branch could not be read, so granting " +
176
+ "would be guessing. Fix the cause -- do not fall back to numbering by hand. " +
177
+ "Actions: claim a number (productId, usually with focusId + tag), or list " +
178
+ "what a product or focus holds." +
179
+ LIST_INDEX_NOTE, {
180
+ action: z.enum(["claim", "list"]).describe("Action to perform"),
181
+ productId: z.string().uuid().optional().describe("Product UUID the migration belongs to (required for claim and list)"),
182
+ migrationClaimId: z.string().uuid().optional().describe("Migration-claim UUID (self id; reserved for future get/by-id reads)"),
183
+ focusId: z.string().uuid().optional().describe("Focus UUID the claim is made for -- the claim follows this branch, and is released if the focus ends without the file. Omit only for a claim made outside a focus."),
184
+ tag: z.string().max(200).optional().describe("The migration name you intend to use (the part after the number). Recorded as intent; the file may carry a different tag and the daemon confirms with the real one."),
185
+ agentSessionId: z.string().uuid().optional().describe("Provenance: the session making the claim"),
186
+ includeReleased: z.boolean().optional().describe("List: include released claims (the history). Default: live claims only."),
187
+ limit: z.number().int().min(1).max(500).optional().describe("Max results (1-500, default 50)"),
188
+ offset: z.number().int().min(0).optional().describe("Number of results to skip (default 0)"),
189
+ }, wrapHandler(async (params) => {
190
+ switch (params.action) {
191
+ case "claim": {
192
+ return handleMigrationClaim(params, (body) => callProductApi(getCreds(), body));
193
+ }
194
+ case "list": {
195
+ if (!params.productId)
196
+ return validationError("productId required for list");
197
+ const body = {
198
+ action: "migration_claim_list",
199
+ productId: params.productId,
200
+ };
201
+ if (params.focusId !== undefined)
202
+ body.focusId = params.focusId;
203
+ if (params.includeReleased !== undefined)
204
+ body.includeReleased = params.includeReleased;
205
+ if (params.limit !== undefined)
206
+ body.limit = params.limit;
207
+ if (params.offset !== undefined)
208
+ body.offset = params.offset;
209
+ const result = await callProductApi(getCreds(), body);
210
+ return successResult({ items: result.claims, totalCount: result.totalCount });
211
+ }
212
+ default:
213
+ return validationError(`Unknown action: ${params.action}`);
214
+ }
215
+ }));
216
+ }
217
+ //# sourceMappingURL=migrationClaimHandlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrationClaimHandlers.js","sourceRoot":"","sources":["../../src/handlers/migrationClaimHandlers.ts"],"names":[],"mappings":"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"}
@@ -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 {};