gitnexus 1.6.10-rc.218 → 1.6.10-rc.219
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/group.js +124 -11
- package/dist/core/group/bridge-db.d.ts +173 -0
- package/dist/core/group/bridge-db.js +345 -4
- package/dist/core/group/completeness.d.ts +112 -0
- package/dist/core/group/completeness.js +60 -0
- package/dist/core/group/cross-impact.d.ts +25 -1
- package/dist/core/group/cross-impact.js +98 -19
- package/dist/core/group/cross-trace.d.ts +32 -11
- package/dist/core/group/cross-trace.js +86 -4
- package/dist/core/group/group-lock.d.ts +31 -0
- package/dist/core/group/group-lock.js +166 -0
- package/dist/core/group/service.js +184 -14
- package/dist/core/group/storage.d.ts +3 -0
- package/dist/core/group/storage.js +5 -1
- package/dist/core/group/sync.d.ts +40 -0
- package/dist/core/group/sync.js +0 -0
- package/dist/core/group/types.d.ts +135 -2
- package/dist/core/group/types.js +16 -1
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +1 -1
- package/dist/mcp/resources.js +19 -2
- package/dist/mcp/tools.js +10 -4
- package/dist/storage/index-lock.d.ts +19 -0
- package/dist/storage/index-lock.js +8 -2
- package/dist/storage/repo-manager.d.ts +25 -1
- package/dist/storage/repo-manager.js +126 -6
- package/package.json +1 -1
- package/scripts/cross-platform-shard.ts +10 -0
- package/scripts/cross-platform-tests.ts +34 -0
package/dist/cli/group.js
CHANGED
|
@@ -100,7 +100,24 @@ export function registerGroupCommands(program) {
|
|
|
100
100
|
console.log(' Repo index / contracts staleness:');
|
|
101
101
|
for (const [repoPath, row] of Object.entries(st.repos || {})) {
|
|
102
102
|
if (row.missing) {
|
|
103
|
-
|
|
103
|
+
// Two different facts with two different remedies: a repo the
|
|
104
|
+
// registry never heard of is fixed by indexing it, while an entry
|
|
105
|
+
// the resolver choked on is fixed by repairing the registry.
|
|
106
|
+
// Printing "no entry in the registry" for the second one states a
|
|
107
|
+
// cause that was never measured, and points at the wrong repair.
|
|
108
|
+
if (row.unresolvable) {
|
|
109
|
+
// The reason can be multi-line — an ambiguous registry names
|
|
110
|
+
// every colliding clone. Fold it onto this row's line rather
|
|
111
|
+
// than truncating it: those paths are what the operator acts on,
|
|
112
|
+
// and a table row that swallows half its own explanation is the
|
|
113
|
+
// failure this label exists to stop.
|
|
114
|
+
const why = (row.unresolvableReason ?? 'the registry entry could not be resolved')
|
|
115
|
+
.replace(/\s+/g, ' ')
|
|
116
|
+
.trim();
|
|
117
|
+
console.log(` ${repoPath.padEnd(25)} UNRESOLVABLE (${why})`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
console.log(` ${repoPath.padEnd(25)} MISSING (no entry in the registry)`);
|
|
104
121
|
continue;
|
|
105
122
|
}
|
|
106
123
|
const idx = row.indexStale
|
|
@@ -109,6 +126,25 @@ export function registerGroupCommands(program) {
|
|
|
109
126
|
const ctr = row.contractsStale ? ' CONTRACTS_STALE' : '';
|
|
110
127
|
console.log(` ${repoPath.padEnd(25)} ${idx}${ctr}`);
|
|
111
128
|
}
|
|
129
|
+
// `undefined` and `[]` are different answers here: a registry written
|
|
130
|
+
// before this was tracked has no opinion, while an empty array is a
|
|
131
|
+
// measurement. Printing nothing for both would let an unmeasured sync
|
|
132
|
+
// read as evidence that every index opened cleanly.
|
|
133
|
+
//
|
|
134
|
+
// `undefined` covers two ways of not knowing — the field is absent, or
|
|
135
|
+
// it held something that was not a list of repo paths and `getStatus`
|
|
136
|
+
// declined to guess. Naming only the first would make a corrupt
|
|
137
|
+
// registry read as a merely old one, which is the same shape of wrong
|
|
138
|
+
// answer this command exists to stop giving.
|
|
139
|
+
const unreadable = st.unreadableRepos;
|
|
140
|
+
if (unreadable === undefined) {
|
|
141
|
+
console.log(`\n Last sync unreadable repos: not recorded` +
|
|
142
|
+
`\n (the registry predates this field, or its value could not be read)` +
|
|
143
|
+
`\n Re-run \`gitnexus group sync\` to record it.`);
|
|
144
|
+
}
|
|
145
|
+
else if (unreadable.length > 0) {
|
|
146
|
+
console.log(`\n Last sync unreadable repos: ${unreadable.join(', ')}`);
|
|
147
|
+
}
|
|
112
148
|
if ((st.missingRepos || []).length > 0) {
|
|
113
149
|
console.log(`\n Last sync missing repos: ${st.missingRepos.join(', ')}`);
|
|
114
150
|
}
|
|
@@ -129,25 +165,85 @@ export function registerGroupCommands(program) {
|
|
|
129
165
|
const { getGroupDir, getDefaultGitnexusDir } = await import('../core/group/storage.js');
|
|
130
166
|
const { loadGroupConfig } = await import('../core/group/config-parser.js');
|
|
131
167
|
const { syncGroup } = await import('../core/group/sync.js');
|
|
168
|
+
const { GroupSyncLockError } = await import('../core/group/group-lock.js');
|
|
132
169
|
const groupDir = getGroupDir(getDefaultGitnexusDir(), name);
|
|
133
170
|
const config = await loadGroupConfig(groupDir);
|
|
134
171
|
console.log(`Syncing group "${name}" (${Object.keys(config.repos).length} repos)...\n`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
172
|
+
let result;
|
|
173
|
+
try {
|
|
174
|
+
result = await syncGroup(config, {
|
|
175
|
+
groupDir,
|
|
176
|
+
allowStale: Boolean(opts.allowStale),
|
|
177
|
+
verbose: Boolean(opts.verbose),
|
|
178
|
+
skipEmbeddings: Boolean(opts.skipEmbeddings),
|
|
179
|
+
exactOnly: Boolean(opts.exactOnly),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
// A sync that could not take the group's lock did NOT run and wrote
|
|
184
|
+
// nothing (R9 fails closed). That is an operator-actionable outcome, not
|
|
185
|
+
// a crash, so report it as a failed command rather than letting it
|
|
186
|
+
// surface as an unhandled rejection with a stack trace — commander's
|
|
187
|
+
// async actions have no error handler, so an uncaught throw here would
|
|
188
|
+
// print exactly that.
|
|
189
|
+
if (!(err instanceof GroupSyncLockError))
|
|
190
|
+
throw err;
|
|
191
|
+
logger.error(`⚠️ Did not sync group "${name}": ${err.message}`);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
142
195
|
if (opts.json) {
|
|
143
196
|
console.log(JSON.stringify(result, null, 2));
|
|
144
197
|
}
|
|
145
198
|
else {
|
|
199
|
+
// Repos we could not read are the most likely explanation for a small
|
|
200
|
+
// or empty contract count, so they are reported before the counts —
|
|
201
|
+
// otherwise a run that read nothing looks exactly like a clean run.
|
|
202
|
+
if (result.unreadableRepos.length > 0) {
|
|
203
|
+
// No "re-run with GITNEXUS_LOG_LEVEL=warn" hint: the default level is
|
|
204
|
+
// `info`, and pino emits `warn` (40) at `info` (30), so the reason was
|
|
205
|
+
// already printed by this same run — raising the level to `warn` would
|
|
206
|
+
// only suppress the surrounding `info` output.
|
|
207
|
+
console.log(`\n ⚠️ Could not extract contracts from: ${result.unreadableRepos.join(', ')}` +
|
|
208
|
+
`\n None of their contracts are included in this sync (the warning above says why),` +
|
|
209
|
+
`\n or check \`gitnexus doctor\` in the affected repo.`);
|
|
210
|
+
}
|
|
211
|
+
if (result.missingRepos.length > 0) {
|
|
212
|
+
console.log(`\n ⚠️ Not found in the registry: ${result.missingRepos.join(', ')}` +
|
|
213
|
+
`\n Index them with \`gitnexus analyze\`, or remove them from group.yaml.`);
|
|
214
|
+
}
|
|
146
215
|
console.log(`\nMatching cascade:`);
|
|
147
216
|
const exactLinks = result.crossLinks.filter((l) => l.matchType === 'exact');
|
|
148
217
|
console.log(` exact: ${exactLinks.length} cross-links (confidence 1.0)`);
|
|
149
218
|
console.log(` unmatched: ${result.unmatched.length} contracts`);
|
|
150
|
-
|
|
219
|
+
// Driven by what actually happened to the file. This line used to be
|
|
220
|
+
// unconditional, so a run that deliberately preserved the previous
|
|
221
|
+
// registry still announced `Wrote contracts.json (0 contracts, 0
|
|
222
|
+
// cross-links)` — a confident false statement about persisted state, on
|
|
223
|
+
// the exact path this command exists to make legible.
|
|
224
|
+
// Exhaustive by construction: a `Record` keyed on the union means a
|
|
225
|
+
// new outcome fails the build here instead of printing nothing, which
|
|
226
|
+
// is what previously pushed a distinct state into `preserved` and made
|
|
227
|
+
// this summary false on one of the two branches it then covered.
|
|
228
|
+
const OUTCOME_LINE = {
|
|
229
|
+
written: `\nWrote contracts.json (${result.contracts.length} contracts, ` +
|
|
230
|
+
`${result.crossLinks.length} cross-links)`,
|
|
231
|
+
preserved: `\nKept the previous contracts.json — no repo in this group could be read.` +
|
|
232
|
+
`\n Its contracts and cross-links are unchanged; only the unreadable/missing` +
|
|
233
|
+
`\n repo lists were refreshed to describe THIS run. Fix the repos above and re-run.`,
|
|
234
|
+
superseded: `\nDid NOT touch contracts.json — no repo in this group could be read, and another` +
|
|
235
|
+
`\n sync replaced the file while this one waited for the group lock. That sync's` +
|
|
236
|
+
`\n result stands and this run's repo lists were NOT recorded: they describe a` +
|
|
237
|
+
`\n group state older than what is on disk. Fix the repos above and re-run.`,
|
|
238
|
+
'no-prior-registry': `\nDid NOT write contracts.json — no repo in this group could be read,` +
|
|
239
|
+
`\n and there is no previous contracts.json to fall back on. Fix the repos` +
|
|
240
|
+
`\n above and re-run.`,
|
|
241
|
+
// Nothing to say: the caller asked for no write.
|
|
242
|
+
'not-attempted': null,
|
|
243
|
+
};
|
|
244
|
+
const line = OUTCOME_LINE[result.registryOutcome];
|
|
245
|
+
if (line)
|
|
246
|
+
console.log(line);
|
|
151
247
|
}
|
|
152
248
|
});
|
|
153
249
|
group
|
|
@@ -306,9 +402,15 @@ export function registerGroupCommands(program) {
|
|
|
306
402
|
process.exitCode = 1;
|
|
307
403
|
return;
|
|
308
404
|
}
|
|
309
|
-
const { contracts, crossLinks } = raw;
|
|
405
|
+
const { contracts, crossLinks, truncated, unreadableRepos, missingRepos } = raw;
|
|
310
406
|
if (opts.json) {
|
|
311
|
-
|
|
407
|
+
// The whole payload, not a re-serialized subset. Destructuring the two
|
|
408
|
+
// fields this command happens to print and rebuilding an object from
|
|
409
|
+
// them dropped everything else the service returned — which is how the
|
|
410
|
+
// completeness fields were invisible here while the MCP tool carried
|
|
411
|
+
// them. Printing `raw` means a field added to the service reaches
|
|
412
|
+
// `--json` without a matching edit in this file.
|
|
413
|
+
console.log(JSON.stringify(raw, null, 2));
|
|
312
414
|
}
|
|
313
415
|
else {
|
|
314
416
|
console.log(`Contracts (${contracts.length}):`);
|
|
@@ -319,6 +421,17 @@ export function registerGroupCommands(program) {
|
|
|
319
421
|
for (const l of crossLinks) {
|
|
320
422
|
console.log(` ${l.from.repo} -> ${l.to.repo} [${l.matchType}, conf=${l.confidence}] ${l.contractId}`);
|
|
321
423
|
}
|
|
424
|
+
if (truncated) {
|
|
425
|
+
// Counts above are a floor, not a census. Name the repos when the
|
|
426
|
+
// registry recorded them, and say so plainly when it did not — a
|
|
427
|
+
// listing that cannot say what it is missing is still incomplete.
|
|
428
|
+
const absent = [...(unreadableRepos ?? []), ...(missingRepos ?? [])];
|
|
429
|
+
console.log(absent.length > 0
|
|
430
|
+
? `\n⚠️ This listing is incomplete: the last sync could not account for ${absent.join(', ')}.` +
|
|
431
|
+
`\n Contracts from those repos are absent, so the counts above are a lower bound.`
|
|
432
|
+
: `\n⚠️ This listing is incomplete: the last sync did not record which repos it could` +
|
|
433
|
+
`\n read, so the counts above are a lower bound. Re-run group sync.`);
|
|
434
|
+
}
|
|
322
435
|
}
|
|
323
436
|
}
|
|
324
437
|
finally {
|
|
@@ -111,12 +111,138 @@ export declare function queryBridge<T>(handle: BridgeHandle, cypher: string, par
|
|
|
111
111
|
*/
|
|
112
112
|
export declare function closeBridgeDb(handle: BridgeHandle): Promise<void>;
|
|
113
113
|
export declare function writeBridgeMeta(groupDir: string, meta: BridgeMeta): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Does `meta` still describe the `bridge.lbug` sitting next to it?
|
|
116
|
+
*
|
|
117
|
+
* `writeBridge` stamps the database's size and mtime into the metadata it
|
|
118
|
+
* writes, so a metadata file left over from an earlier sync cannot match a
|
|
119
|
+
* database that was replaced after it. Callers whose answer depends on the
|
|
120
|
+
* metadata being true of THIS database (cross-repo impact reads completeness
|
|
121
|
+
* from it) must not treat a mismatch as fact.
|
|
122
|
+
*
|
|
123
|
+
* When BOTH halves of the stamp are absent the metadata predates stamping, and
|
|
124
|
+
* it is judged on the write order of the two files instead — see
|
|
125
|
+
* {@link unstampedMetaPairsByWriteOrder}. Failing every unstamped metadata
|
|
126
|
+
* closed would mark all pre-existing bridges as incomplete until re-synced,
|
|
127
|
+
* trading a narrow window for a repo-wide regression; accepting them all hands
|
|
128
|
+
* back "verified" for the very window this pairing exists to catch.
|
|
129
|
+
*
|
|
130
|
+
* A stamp is a PAIR, so exactly one half present is rejected rather than waved
|
|
131
|
+
* through. That is not the legacy shape: something wrote a stamp and did not
|
|
132
|
+
* finish, which is the very condition stamping was added to detect. Joining the
|
|
133
|
+
* two `undefined` checks with `||` returned "verified" for precisely the shape
|
|
134
|
+
* that most deserves suspicion.
|
|
135
|
+
*
|
|
136
|
+
* Returns `false` when the database itself cannot be stat'd, on either path,
|
|
137
|
+
* since metadata describing a file that is not there describes nothing.
|
|
138
|
+
*
|
|
139
|
+
* The checks are ORDERED by how strong their evidence is, strongest first, and
|
|
140
|
+
* each later one is reached only because every earlier one had nothing to say.
|
|
141
|
+
* `provenanceUnknown` therefore comes first: a metadata file whose own writer
|
|
142
|
+
* says it cannot vouch for the database beside it has settled the question, and
|
|
143
|
+
* neither the stamp nor the write-order heuristic may overturn that.
|
|
144
|
+
*
|
|
145
|
+
* The marker is not decoration. `refreshPreservedBridgeMeta` rewrites this file
|
|
146
|
+
* atomically without touching the database, which leaves `meta.mtime` newer —
|
|
147
|
+
* the write order a paired write produces, and the one the unstamped branch
|
|
148
|
+
* ACCEPTS. Reading the marker after that branch (or not at all) hands back
|
|
149
|
+
* "verified" for a pair the same code path had just found broken.
|
|
150
|
+
*/
|
|
151
|
+
export declare function bridgeMetaMatchesFile(groupDir: string, meta: BridgeMeta): Promise<boolean>;
|
|
152
|
+
/**
|
|
153
|
+
* Read `meta.json`, validating the SHAPE of what it holds.
|
|
154
|
+
*
|
|
155
|
+
* The read and the parse have always been guarded — an absent or unparseable
|
|
156
|
+
* file answers `version: 0`, which every caller already treats as "no
|
|
157
|
+
* provenance". What was not guarded is a file that parses into something that
|
|
158
|
+
* is not this shape: `runGroupImpact` spread both repo lists directly into a
|
|
159
|
+
* `Set`, so a non-iterable there threw a TypeError out of the whole cross-repo
|
|
160
|
+
* query, from a point where the bridge lease had been taken and not yet
|
|
161
|
+
* released. A malformed file is a reason to answer "provenance unknown", never
|
|
162
|
+
* a reason to crash the question.
|
|
163
|
+
*/
|
|
114
164
|
export declare function readBridgeMeta(groupDir: string): Promise<BridgeMeta>;
|
|
165
|
+
/**
|
|
166
|
+
* What a refresh did to `meta.json`.
|
|
167
|
+
*
|
|
168
|
+
* - `restamped` — the pair still matched, so the lists were refreshed
|
|
169
|
+
* and the stamp re-taken from the database on disk.
|
|
170
|
+
* - `provenance-unknown` — the pair did NOT match (or there is no database to
|
|
171
|
+
* match), so the lists were refreshed and the metadata
|
|
172
|
+
* marked as unable to vouch for the file beside it.
|
|
173
|
+
* - `no-bridge` — neither `meta.json` nor `bridge.lbug` exists, so
|
|
174
|
+
* there is no pair to keep honest and nothing written.
|
|
175
|
+
*/
|
|
176
|
+
export type PreservedBridgeMetaOutcome = 'restamped' | 'provenance-unknown' | 'no-bridge';
|
|
177
|
+
/**
|
|
178
|
+
* Bring `meta.json`'s diagnostic lists up to date with a sync that PRESERVED
|
|
179
|
+
* the bridge instead of rebuilding it, without ever making the metadata claim
|
|
180
|
+
* more about the database than it did before.
|
|
181
|
+
*
|
|
182
|
+
* `syncGroup`'s total-failure path keeps the previous run's contracts and
|
|
183
|
+
* deliberately leaves `bridge.lbug` alone — the contracts that bridge holds are
|
|
184
|
+
* the ones being preserved. But `runGroupImpact` reads completeness from
|
|
185
|
+
* `meta.json`, not from `contracts.json`, so leaving the metadata alone too left
|
|
186
|
+
* the two files telling different stories: the registry said "this sync could
|
|
187
|
+
* not read svc/users" while a cross-repo query answered "complete, nothing
|
|
188
|
+
* depends on this" (R4/R6).
|
|
189
|
+
*
|
|
190
|
+
* The refresh is the whole difficulty. It rewrites `meta.json` atomically, so
|
|
191
|
+
* the file's mtime becomes now while the database's stays old — which is the
|
|
192
|
+
* write order a paired write produces, and precisely what
|
|
193
|
+
* `unstampedMetaPairsByWriteOrder` accepts. Three rules follow, and each of them
|
|
194
|
+
* is load-bearing:
|
|
195
|
+
*
|
|
196
|
+
* 1. Ask `bridgeMetaMatchesFile` FIRST, on the file as it stands. After the
|
|
197
|
+
* write the question is unanswerable, because the write is what destroys
|
|
198
|
+
* the evidence.
|
|
199
|
+
* 2. Re-stamp only when that answer was yes. Re-stamping a pair that already
|
|
200
|
+
* failed would MANUFACTURE the provenance the failure just denied — the
|
|
201
|
+
* same metadata/database mis-pairing stamping exists to prevent (KTD6).
|
|
202
|
+
* 3. When it was no, record `provenanceUnknown` explicitly and carry the
|
|
203
|
+
* existing stamp fields through verbatim. Writing "no stamp" instead is
|
|
204
|
+
* worse, not better: an unstamped file is judged on the two file times,
|
|
205
|
+
* and this write has just put them in the accepting order.
|
|
206
|
+
*
|
|
207
|
+
* Nothing here opens, reads, or writes the database. The only `stat` of it
|
|
208
|
+
* happens on the branch where the pair was just verified.
|
|
209
|
+
*
|
|
210
|
+
* NOT SPLIT into locked/unlocked halves the way {@link writeBridge} is, and
|
|
211
|
+
* deliberately. Its one caller is `syncGroup`'s preserve branch, which is
|
|
212
|
+
* already inside `withGroupSyncLock` — so this write is ALREADY serialized
|
|
213
|
+
* against every other sync of the group, and taking the lock here would be the
|
|
214
|
+
* second acquisition of a non-reentrant primitive that the split exists to
|
|
215
|
+
* avoid. An acquiring wrapper would therefore have zero production callers,
|
|
216
|
+
* and no test calls this function at all: it would be dead code standing in for
|
|
217
|
+
* a guarantee the caller already provides. If a caller outside the critical
|
|
218
|
+
* section ever appears, it needs the same treatment `writeBridge` got — a
|
|
219
|
+
* wrapper, not a lock moved down here.
|
|
220
|
+
*/
|
|
221
|
+
export declare function refreshPreservedBridgeMeta(groupDir: string, diagnostics: {
|
|
222
|
+
missingRepos: string[];
|
|
223
|
+
unreadableRepos: string[];
|
|
224
|
+
}): Promise<PreservedBridgeMetaOutcome>;
|
|
115
225
|
export interface WriteBridgeInput {
|
|
116
226
|
contracts: StoredContract[];
|
|
117
227
|
crossLinks: CrossLink[];
|
|
118
228
|
repoSnapshots: Record<string, RepoSnapshot>;
|
|
119
229
|
missingRepos: string[];
|
|
230
|
+
/**
|
|
231
|
+
* Repos this sync could not extract from — see
|
|
232
|
+
* `ContractRegistry.unreadableRepos` for the full definition, which this
|
|
233
|
+
* field carries unchanged.
|
|
234
|
+
*
|
|
235
|
+
* Deliberately not restated here. The narrower wording this once had ("whose
|
|
236
|
+
* index could not be opened") described one of the two causes and silently
|
|
237
|
+
* excluded the other, an extractor that threw partway through — so the same
|
|
238
|
+
* field meant one thing on the registry, another on the bridge input, and a
|
|
239
|
+
* third on the result. One definition, referenced twice, cannot drift.
|
|
240
|
+
*
|
|
241
|
+
* Recorded in meta.json so cross-repo impact can tell "nothing depends on
|
|
242
|
+
* this" from "we could not look": the bridge built here is missing every
|
|
243
|
+
* contract those repos own.
|
|
244
|
+
*/
|
|
245
|
+
unreadableRepos?: string[];
|
|
120
246
|
}
|
|
121
247
|
/**
|
|
122
248
|
* Non-fatal issues encountered during writeBridge. Callers can log these to
|
|
@@ -138,6 +264,53 @@ export interface WriteBridgeReport {
|
|
|
138
264
|
message: string;
|
|
139
265
|
}>;
|
|
140
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Rebuild `bridge.lbug` and its `meta.json`, ASSUMING THE CALLER ALREADY HOLDS
|
|
269
|
+
* THE GROUP SYNC LOCK for `groupDir` (R9).
|
|
270
|
+
*
|
|
271
|
+
* PRECONDITION — the group lock is held. There is exactly one production call
|
|
272
|
+
* site, `syncGroup` in sync.ts, and it is already inside
|
|
273
|
+
* `withGroupSyncLock(groupDir, …)` when it gets here. Enforced by this comment
|
|
274
|
+
* rather than by a type, matching `registerRepoUnlocked` / `withRegistryLock`
|
|
275
|
+
* in repo-manager.ts, which splits the same shape for the same reason.
|
|
276
|
+
*
|
|
277
|
+
* WHY THE SPLIT EXISTS AT ALL. The swap this function performs — old database
|
|
278
|
+
* aside, temp database into place, then `meta.json` written as a SECOND
|
|
279
|
+
* operation — is the write two concurrent syncs can interleave into a pairing
|
|
280
|
+
* that never existed: one sync's metadata beside the other's database. That
|
|
281
|
+
* needs mutual exclusion. But taking the lock HERE would be a second
|
|
282
|
+
* acquisition of a non-reentrant primitive inside a region that already holds
|
|
283
|
+
* it, and it would hang every single sync on the happy path, not some rare
|
|
284
|
+
* interleave. So the exclusion is the caller's, and this function only states
|
|
285
|
+
* the precondition. {@link writeBridge} is the acquiring wrapper for callers
|
|
286
|
+
* who are not already inside that region.
|
|
287
|
+
*
|
|
288
|
+
* SCOPE — writer-writer only. The reader-side promotion of a leftover
|
|
289
|
+
* `bridge.lbug.bak` runs on ordinary reads, outside anybody's critical section;
|
|
290
|
+
* `bridgeMetaMatchesFile` remains the reader's defense there and is not
|
|
291
|
+
* replaced by this lock.
|
|
292
|
+
*/
|
|
293
|
+
export declare function writeBridgeUnlocked(groupDir: string, input: WriteBridgeInput): Promise<WriteBridgeReport>;
|
|
294
|
+
/**
|
|
295
|
+
* Rebuild `bridge.lbug` and its `meta.json` as the only writer of `groupDir`.
|
|
296
|
+
*
|
|
297
|
+
* The acquiring half of the split described on {@link writeBridgeUnlocked}: for
|
|
298
|
+
* callers that are NOT already inside the group's critical section, this takes
|
|
299
|
+
* the group sync lock around the whole swap and releases it afterwards. Two
|
|
300
|
+
* concurrent calls therefore run one after the other, so the `meta.json` left
|
|
301
|
+
* on disk is stamped for the `bridge.lbug` left on disk instead of for the
|
|
302
|
+
* loser's, which is the pairing the swap-plus-metadata sequence would otherwise
|
|
303
|
+
* let them interleave into.
|
|
304
|
+
*
|
|
305
|
+
* NOT used by `syncGroup`, and it must not be: that path already holds this
|
|
306
|
+
* lock, and `acquireIndexLock` is not reentrant, so routing it here would make
|
|
307
|
+
* every ordinary sync wait out the full `GROUP_SYNC_LOCK_TIMEOUT_MS` ceiling
|
|
308
|
+
* against itself. It calls {@link writeBridgeUnlocked} directly.
|
|
309
|
+
*
|
|
310
|
+
* Fails closed exactly as `withGroupSyncLock` does: if the lock cannot be
|
|
311
|
+
* acquired, a `GroupSyncLockError` is thrown and NOTHING is written —
|
|
312
|
+
* `bridge.lbug` and `meta.json` are left as they were.
|
|
313
|
+
*/
|
|
141
314
|
export declare function writeBridge(groupDir: string, input: WriteBridgeInput): Promise<WriteBridgeReport>;
|
|
142
315
|
export declare function openBridgeDbReadOnly(groupDir: string): Promise<BridgeHandle | null>;
|
|
143
316
|
export declare function bridgeExists(groupDir: string): Promise<boolean>;
|