@functional-systems/srcx 0.1.0-dev.4 → 0.1.0-dev.40

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 (66) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/DESIGN.md +378 -58
  4. package/README.md +628 -35
  5. package/RELEASING.md +15 -3
  6. package/VALIDATION.md +1120 -16
  7. package/dist/artifact.d.ts +31 -0
  8. package/dist/artifact.js +237 -0
  9. package/dist/artifact.js.map +1 -0
  10. package/dist/build.d.ts +86 -3
  11. package/dist/build.js +233 -71
  12. package/dist/build.js.map +1 -1
  13. package/dist/chunk.d.ts +9 -3
  14. package/dist/chunk.js +341 -14
  15. package/dist/chunk.js.map +1 -1
  16. package/dist/cli.js +130 -21
  17. package/dist/cli.js.map +1 -1
  18. package/dist/file-policy.d.ts +20 -0
  19. package/dist/file-policy.js +103 -0
  20. package/dist/file-policy.js.map +1 -0
  21. package/dist/implementation.d.ts +11 -0
  22. package/dist/implementation.js +191 -0
  23. package/dist/implementation.js.map +1 -0
  24. package/dist/publish.d.ts +18 -0
  25. package/dist/publish.js +224 -46
  26. package/dist/publish.js.map +1 -1
  27. package/dist/releases.js +21 -3
  28. package/dist/releases.js.map +1 -1
  29. package/dist/remote.d.ts +1 -0
  30. package/dist/remote.js +50 -9
  31. package/dist/remote.js.map +1 -1
  32. package/dist/repository.d.ts +4 -1
  33. package/dist/repository.js +26 -11
  34. package/dist/repository.js.map +1 -1
  35. package/dist/rerank.d.ts +16 -0
  36. package/dist/rerank.js +87 -0
  37. package/dist/rerank.js.map +1 -0
  38. package/dist/search.d.ts +18 -3
  39. package/dist/search.js +143 -23
  40. package/dist/search.js.map +1 -1
  41. package/dist/skills.d.ts +24 -0
  42. package/dist/skills.js +155 -0
  43. package/dist/skills.js.map +1 -0
  44. package/package.json +17 -6
  45. package/runtime/grammars/README.md +50 -0
  46. package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
  47. package/runtime/grammars/tree-sitter-bash.wasm +0 -0
  48. package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
  49. package/runtime/grammars/tree-sitter-sql.wasm +0 -0
  50. package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
  51. package/runtime/qwen.py +132 -0
  52. package/runtime/requirements.txt +34 -0
  53. package/skills/srcx-search/SKILL.md +103 -0
  54. package/src/artifact.ts +303 -0
  55. package/src/build.ts +313 -84
  56. package/src/chunk.ts +360 -12
  57. package/src/cli.ts +263 -23
  58. package/src/file-policy.ts +166 -0
  59. package/src/implementation.ts +253 -0
  60. package/src/publish.ts +321 -47
  61. package/src/releases.ts +36 -3
  62. package/src/remote.ts +70 -13
  63. package/src/repository.ts +40 -11
  64. package/src/rerank.ts +131 -0
  65. package/src/search.ts +229 -28
  66. package/src/skills.ts +205 -0
@@ -0,0 +1,191 @@
1
+ import { CHUNKER, detectLanguage, tokens, withSyntaxTree } from "./chunk.js";
2
+ import { invariant, lineAt } from "./common.js";
3
+ const children = (n) => n.namedChildren.filter((c) => !!c);
4
+ /** Only imported typing overload aliases used exclusively as decorators are safe here. */
5
+ function overloadAliases(root) {
6
+ const aliases = new Set(), imports = new Set();
7
+ for (const n of children(root)) {
8
+ if (n.type !== "import_from_statement" ||
9
+ !["typing", "typing_extensions"].includes(n.childForFieldName("module_name")?.text ?? ""))
10
+ continue;
11
+ for (const name of n.childrenForFieldName("name")) {
12
+ if (!name)
13
+ continue;
14
+ if (name.type === "dotted_name" && name.text === "overload") {
15
+ aliases.add("overload");
16
+ imports.add(name.id);
17
+ }
18
+ else if (name.type === "aliased_import" &&
19
+ name.childForFieldName("name")?.text === "overload") {
20
+ const alias = name.childForFieldName("alias");
21
+ if (alias) {
22
+ aliases.add(alias.text);
23
+ imports.add(name.id);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ function visit(n) {
29
+ if (imports.has(n.id))
30
+ return;
31
+ if (n.type === "wildcard_import")
32
+ aliases.clear();
33
+ if (n.type === "identifier" &&
34
+ aliases.has(n.text) &&
35
+ !(n.parent?.type === "decorator" && n.parent.text === `@${n.text}`))
36
+ aliases.delete(n.text);
37
+ for (const child of children(n))
38
+ visit(child);
39
+ }
40
+ visit(root);
41
+ return aliases;
42
+ }
43
+ function definition(n, python, aliases) {
44
+ const d = n.childForFieldName(python ? "definition" : "declaration") ?? n;
45
+ const name = d.childForFieldName("name");
46
+ if (!name ||
47
+ ![
48
+ "identifier",
49
+ "property_identifier",
50
+ "private_property_identifier",
51
+ ].includes(name.type))
52
+ return;
53
+ if (python) {
54
+ if (d.type !== "function_definition")
55
+ return;
56
+ const decorators = children(n).filter((c) => c.type === "decorator");
57
+ const declaration = decorators.some((c) => aliases.has(c.text.slice(1)));
58
+ const body = d.childForFieldName("body");
59
+ // Stubs without an overload marker do not supply an implementation.
60
+ const executable = body &&
61
+ children(body).some((c) => c.type !== "comment" &&
62
+ c.type !== "pass_statement" &&
63
+ !(c.type === "expression_statement" &&
64
+ ["ellipsis", "string"].includes(c.firstNamedChild?.type ?? "")));
65
+ if (!declaration && !executable)
66
+ return;
67
+ // Other decorators may change dispatch (property, singledispatch, etc.).
68
+ if (decorators.some((c) => !aliases.has(c.text.slice(1)) &&
69
+ !["@staticmethod", "@classmethod"].includes(c.text)))
70
+ return;
71
+ const modifier = decorators
72
+ .filter((c) => ["@staticmethod", "@classmethod"].includes(c.text))
73
+ .map((c) => c.text)
74
+ .sort()
75
+ .join(":");
76
+ const async = d.children.some((c) => c?.type === "async");
77
+ return {
78
+ node: n,
79
+ name: name.text,
80
+ key: `${name.text}:${modifier}:${async}`,
81
+ declaration,
82
+ };
83
+ }
84
+ if (n !== d && n.type !== "export_statement")
85
+ return;
86
+ if (![
87
+ "function_signature",
88
+ "function_declaration",
89
+ "method_signature",
90
+ "method_definition",
91
+ ].includes(d.type))
92
+ return;
93
+ const prefix = d.text.slice(0, name.startIndex - d.startIndex);
94
+ if (/\b(abstract|get|set|declare)\b/.test(prefix))
95
+ return;
96
+ const method = d.type.startsWith("method");
97
+ const declaration = d.type.endsWith("signature");
98
+ if (!declaration && !d.childForFieldName("body"))
99
+ return;
100
+ return {
101
+ node: n,
102
+ name: name.text,
103
+ key: `${method}:${/\bstatic\b/.test(prefix)}:${name.text}`,
104
+ declaration,
105
+ };
106
+ }
107
+ /** Follow only a contiguous, unambiguous overload group in the same AST container. */
108
+ export async function implementationSpan(source, path, span) {
109
+ const raw = Buffer.from(source), lang = detectLanguage(path);
110
+ invariant(Number.isInteger(span.startByte) &&
111
+ Number.isInteger(span.endByte) &&
112
+ span.startByte >= 0 &&
113
+ span.endByte > span.startByte &&
114
+ span.endByte <= raw.length, "Invalid implementation source range.");
115
+ if (!["python", "typescript", "tsx"].includes(lang) ||
116
+ /\.(pyi|d\.ts|d\.tsx)$/i.test(path))
117
+ return { status: "unsupported" };
118
+ const start = raw.subarray(0, span.startByte).toString("utf8").length;
119
+ const end = raw.subarray(0, span.endByte).toString("utf8").length;
120
+ return withSyntaxTree(source, lang, (root) => {
121
+ if (root.hasError)
122
+ return { status: "parse-error" };
123
+ const python = lang === "python", aliases = python ? overloadAliases(root) : new Set();
124
+ const targets = [];
125
+ function visit(container) {
126
+ const siblings = children(container).filter((n) => n.type !== "comment");
127
+ const definitions = siblings.map((n) => definition(n, python, aliases));
128
+ for (let i = 0; i < definitions.length; i++) {
129
+ const d = definitions[i];
130
+ // A class/container hit is not a declaration hit. Require the selected
131
+ // span to end before the implementation, and contain a whole declaration.
132
+ if (!d?.declaration ||
133
+ d.node.startIndex < start ||
134
+ d.node.endIndex > end)
135
+ continue;
136
+ let first = i;
137
+ while (first > 0 &&
138
+ definitions[first - 1]?.declaration &&
139
+ definitions[first - 1]?.key === d.key)
140
+ first--;
141
+ let next = first;
142
+ while (definitions[next]?.declaration &&
143
+ definitions[next]?.key === d.key)
144
+ next++;
145
+ const target = definitions[next];
146
+ if (!target ||
147
+ target.declaration ||
148
+ target.key !== d.key ||
149
+ target.node.startIndex < end)
150
+ continue;
151
+ const matches = siblings.filter((n) => {
152
+ const node = n.childForFieldName(python ? "definition" : "declaration") ?? n;
153
+ // TypeScript namespaces augment a function without adding another body.
154
+ // Keep other same-name declarations in the ambiguity check.
155
+ if (!python && node.type === "internal_module")
156
+ return false;
157
+ return node.childForFieldName("name")?.text === d.name;
158
+ });
159
+ if (matches.length !== next - first + 1)
160
+ continue;
161
+ // Reject unrelated code included in the selected chunk, but allow trivia
162
+ // between declarations (the chunker can attach comments/whitespace).
163
+ const selected = siblings.filter((n) => n.startIndex < end && n.endIndex > start);
164
+ if (selected.some((n) => !definitions.slice(first, next).some((x) => x?.node.id === n.id)))
165
+ continue;
166
+ if (!targets.some((t) => t.node.id === target.node.id))
167
+ targets.push(target);
168
+ }
169
+ for (const child of siblings)
170
+ visit(child);
171
+ }
172
+ visit(root);
173
+ if (targets.length !== 1)
174
+ return { status: "not-linked" };
175
+ const target = targets[0];
176
+ const startLine = lineAt(raw, Buffer.byteLength(source.slice(0, target.node.startIndex)));
177
+ const endLine = lineAt(raw, Buffer.byteLength(source.slice(0, target.node.endIndex)) - 1);
178
+ const text = (source.match(/[^\n]*\n|[^\n]+$/g) ?? [])
179
+ .slice(startLine - 1, endLine)
180
+ .join("");
181
+ if (tokens(text) > CHUNKER.maxTokens)
182
+ return { status: "too-large" };
183
+ return {
184
+ status: "resolved",
185
+ startLine,
186
+ endLine,
187
+ symbol: target.name,
188
+ };
189
+ });
190
+ }
191
+ //# sourceMappingURL=implementation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"implementation.js","sourceRoot":"","sources":["../src/implementation.ts"],"names":[],"mappings":"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"}
package/dist/publish.d.ts CHANGED
@@ -25,6 +25,8 @@ export type Journal = {
25
25
  attemptId: string;
26
26
  writer: string;
27
27
  source: string;
28
+ gitRef?: string;
29
+ reuseWriter?: boolean;
28
30
  baseline?: Published;
29
31
  phase: "branch" | "writing" | "waiting" | "publishing";
30
32
  batches: {
@@ -37,6 +39,22 @@ export type Journal = {
37
39
  snapshotId: string;
38
40
  };
39
41
  };
42
+ /** The applied snapshot may differ from the shared canonical commit Tag. */
43
+ export type TrackedBranch = Doc & {
44
+ gitRef: string;
45
+ writer: string;
46
+ applied?: Published;
47
+ publishedTagName?: string;
48
+ pending?: {
49
+ attemptId: string;
50
+ buildId: string;
51
+ commitOid: string;
52
+ };
53
+ };
54
+ export declare function gitBranchName(ref: string): string;
55
+ export declare const gitBranchId: (ref: string) => string;
56
+ export declare const lastBuildPath: (state: string, ref?: string) => string;
57
+ export declare function trackedBranch(store: CollectionStore, binding: Binding, ref: string): Promise<TrackedBranch | undefined>;
40
58
  export declare function versionTag(oid: string): string;
41
59
  export declare function versionId(oid: string): string;
42
60
  export declare function exclusive<T>(directory: string, work: () => Promise<T>): Promise<T>;
package/dist/publish.js CHANGED
@@ -2,8 +2,27 @@ import { mkdir, rm, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { atomic, canonical, hash, invariant, optionalJson, sleep, } from "./common.js";
5
- import { corpusMetadata, records, validateBuild } from "./build.js";
5
+ import { corpusMetadata, loadBuild, records, recordHash, supportedPreset, validateBuild, } from "./build.js";
6
6
  import { branch, tag, one } from "./remote.js";
7
+ export function gitBranchName(ref) {
8
+ invariant(ref.startsWith("refs/heads/") && ref.length > 11, "Expected a full Git branch ref.");
9
+ return `git-${hash(ref).slice(0, 40)}`;
10
+ }
11
+ export const gitBranchId = (ref) => `branch-${hash(ref)}`;
12
+ export const lastBuildPath = (state, ref) => join(state, ref ? `last-build-${hash(ref)}.json` : "last-build.json");
13
+ export async function trackedBranch(store, binding, ref) {
14
+ const doc = await one(store, branch("main"), gitBranchId(ref), true);
15
+ if (!doc)
16
+ return undefined;
17
+ invariant(doc.kind === "manifest" &&
18
+ doc.role === "git-branch" &&
19
+ doc.gitRef === ref &&
20
+ doc.writer === gitBranchName(ref) &&
21
+ doc.repoId === binding.repoId &&
22
+ doc.indexId === binding.indexId &&
23
+ doc.configHash === binding.configHash, "Tracked branch identity mismatch.");
24
+ return doc;
25
+ }
7
26
  export function versionTag(oid) {
8
27
  return `ver-${hash(oid).slice(0, 40)}`;
9
28
  }
@@ -60,8 +79,13 @@ export async function validateCandidate(store, name, b, binding, attemptId) {
60
79
  for (const d of corpusMetadata(b, binding.repoId, binding.indexId, attemptId))
61
80
  expected.set(d.id, hash(d));
62
81
  for await (const d of store.list(tag(name))) {
63
- if (expected.get(d.id) !== hash(d))
82
+ try {
83
+ if (expected.get(d.id) !== recordHash(d, b.preset))
84
+ return false;
85
+ }
86
+ catch {
64
87
  return false;
88
+ }
65
89
  expected.delete(d.id);
66
90
  }
67
91
  if (expected.size)
@@ -70,14 +94,11 @@ export async function validateCandidate(store, name, b, binding, attemptId) {
70
94
  let query = {
71
95
  queryString: { query: "kind:chunk" },
72
96
  };
73
- for await (const d of records(b.directory)) {
97
+ for await (const d of records(b)) {
74
98
  if (d.kind !== "chunk")
75
99
  continue;
76
- // Keep a complete surface token: the standard analyzer may retain dots
77
- // (e.g. code.ts), so extracting an alphanumeric substring is not a valid probe.
78
- const term = String(d.symbol ?? d.searchText).match(/\S+/u)?.[0];
79
- if (!term)
80
- continue;
100
+ // Probe an indexed chunk ID: a valid source may contain only stop words
101
+ // or punctuation, so text-query matches cannot establish readiness.
81
102
  query = {
82
103
  bool: [
83
104
  {
@@ -90,16 +111,8 @@ export async function validateCandidate(store, name, b, binding, attemptId) {
90
111
  },
91
112
  {
92
113
  queryString: {
93
- query: d.fileId,
94
- defaultField: "fileId",
95
- skipSyntax: true,
96
- },
97
- occur: "filter",
98
- },
99
- {
100
- queryString: {
101
- query: term,
102
- defaultField: "searchText",
114
+ query: d.id,
115
+ defaultField: "id",
103
116
  skipSyntax: true,
104
117
  },
105
118
  occur: "must",
@@ -110,7 +123,8 @@ export async function validateCandidate(store, name, b, binding, attemptId) {
110
123
  }
111
124
  const found = await store.query(name, query, 1);
112
125
  if (!found.length ||
113
- found.some((h) => h.doc.kind !== "chunk" || b.recordHashes[h.doc.id] !== hash(h.doc)))
126
+ found.some((h) => h.doc.kind !== "chunk" ||
127
+ b.recordHashes[h.doc.id] !== recordHash(h.doc, b.preset)))
114
128
  return false;
115
129
  }
116
130
  return true;
@@ -156,35 +170,133 @@ export async function published(store, binding) {
156
170
  return result.sort((a, b) => String(a.commitTime).localeCompare(String(b.commitTime)) ||
157
171
  a.commitOid.localeCompare(b.commitOid));
158
172
  }
173
+ /** A local artifact is only a cache for this exact, previously published baseline. */
174
+ async function baselineHashes(store, binding, preset, baseline, state, gitRef, local) {
175
+ const pin = (await store.tags()).find((t) => t.name === baseline.tagName);
176
+ const root = await one(store, tag(baseline.tagName), "__manifest__");
177
+ invariant(pin?.snapshotId === baseline.snapshotId &&
178
+ root?.repoId === binding.repoId &&
179
+ root.indexId === binding.indexId &&
180
+ root.configHash === binding.configHash &&
181
+ root.buildId === baseline.buildId &&
182
+ root.commitOid === baseline.commitOid &&
183
+ root.attemptId === baseline.attemptId &&
184
+ root.recordsHash === baseline.recordsHash &&
185
+ root.inventoryHash === baseline.inventoryHash, "Baseline Tag is missing or changed.");
186
+ if (!local) {
187
+ const last = await optionalJson(lastBuildPath(state, gitRef));
188
+ if (last) {
189
+ try {
190
+ local = await loadBuild(last.artifact);
191
+ }
192
+ catch (error) {
193
+ if (error.code !== "ENOENT")
194
+ throw error;
195
+ }
196
+ }
197
+ }
198
+ if (local &&
199
+ local.repoKey === binding.repoKey &&
200
+ local.configHash === binding.configHash &&
201
+ local.buildId === baseline.buildId &&
202
+ local.commitOid === baseline.commitOid &&
203
+ local.recordsHash === baseline.recordsHash &&
204
+ local.inventoryHash === baseline.inventoryHash) {
205
+ try {
206
+ await validateBuild(local);
207
+ const hashes = new Map(Object.entries(local.recordHashes));
208
+ for (const doc of corpusMetadata(local, binding.repoId, binding.indexId, baseline.attemptId))
209
+ hashes.set(doc.id, hash(doc));
210
+ return hashes;
211
+ }
212
+ catch (error) {
213
+ if (error.code !== "ENOENT")
214
+ throw error;
215
+ }
216
+ }
217
+ // Retain recovery without local files, including explicit same-artifact retries.
218
+ const hashes = new Map();
219
+ for await (const doc of store.list(tag(baseline.tagName)))
220
+ hashes.set(doc.id, recordHash(doc, preset));
221
+ return hashes;
222
+ }
159
223
  /** Caller holds the per-collection lock for the complete import. */
160
224
  export async function publish(args) {
161
225
  const { store, binding, build: b, state } = args;
162
226
  invariant(b.repoKey === binding.repoKey && b.configHash === binding.configHash, "Build does not belong to this index.");
163
- invariant(!b.preset.embedding, "CLI publication currently supports only the embedding=none preset.");
227
+ invariant(!b.preset.embedding || supportedPreset(b.preset), "Publication requires a supported lexical or managed embedding preset.");
164
228
  await validateBuild(b);
165
229
  const journalPath = join(state, "pending.json");
166
230
  let j = await optionalJson(journalPath);
167
231
  if (j) {
168
232
  invariant(j.buildId === b.buildId &&
169
233
  j.recordsHash === b.recordsHash &&
170
- j.inventoryHash === b.inventoryHash, "Another build is pending. Resume the artifact recorded in pending.json before importing a new commit.");
234
+ j.inventoryHash === b.inventoryHash &&
235
+ (j.gitRef === b.branchRef ||
236
+ (j.reuseWriter === undefined && j.writer.startsWith("work-"))), "Another build is pending. Resume the artifact recorded in pending.json before importing a new commit.");
171
237
  invariant(args.resume, "Pending import found; use --resume with the same artifact.");
172
238
  }
173
239
  const existing = (await published(store, binding)).find((v) => v.commitOid === b.commitOid);
240
+ const tracked = b.branchRef
241
+ ? await trackedBranch(store, binding, b.branchRef)
242
+ : undefined;
243
+ if (tracked?.pending)
244
+ invariant(j?.attemptId === tracked.pending.attemptId, "Git branch has a pending import; resume with its original retry journal.");
245
+ if (j?.gitRef && j.phase !== "branch")
246
+ invariant(tracked?.pending?.attemptId === j.attemptId ||
247
+ (j.phase === "publishing" &&
248
+ !tracked?.pending &&
249
+ tracked?.applied?.attemptId === j.attemptId), "Tracked branch import ownership changed; refusing to replay this journal.");
174
250
  if (existing) {
175
251
  invariant(await validateCandidate(store, existing.tagName, b, binding, existing.attemptId), "Existing published version differs from this build.");
176
- await atomic(join(state, "last-build.json"), {
177
- artifact: b.directory,
178
- version: existing,
179
- });
180
- if (j)
252
+ // A shared commit Tag does not mean this Git branch has applied that commit.
253
+ if (!j && (!b.branchRef || tracked?.applied?.commitOid === b.commitOid)) {
254
+ if (tracked?.applied) {
255
+ const writer = (await store.branches()).find((x) => x.name === tracked.writer);
256
+ invariant(writer?.snapshotId === tracked.applied.snapshotId, "Writer baseline has changed; refusing to update tracked branch.");
257
+ invariant(await validateCandidate(store, tracked.applied.tagName, b, binding, tracked.applied.attemptId), "Tracked branch baseline validation failed.");
258
+ }
259
+ await atomic(lastBuildPath(state, b.branchRef), {
260
+ artifact: b.directory,
261
+ version: existing,
262
+ });
263
+ return existing;
264
+ }
265
+ // Legacy manual imports can finish bookkeeping after publication on retry.
266
+ if (j && !j.gitRef) {
267
+ await atomic(lastBuildPath(state), {
268
+ artifact: b.directory,
269
+ version: existing,
270
+ });
181
271
  await rm(journalPath);
182
- return existing;
272
+ return existing;
273
+ }
183
274
  }
184
275
  if (!j) {
185
276
  let source = "checkpoint-empty";
186
277
  let baseline;
187
- if (args.baseline) {
278
+ if (b.branchRef) {
279
+ const writer = (await store.branches()).find((x) => x.name === gitBranchName(b.branchRef));
280
+ if (tracked?.applied) {
281
+ baseline = tracked.applied;
282
+ invariant(writer?.snapshotId === baseline.snapshotId, "Writer baseline has changed; refusing to update tracked branch.");
283
+ const pin = (await store.tags()).find((t) => t.name === baseline.tagName);
284
+ const root = await one(store, tag(baseline.tagName), "__manifest__");
285
+ invariant(pin?.snapshotId === baseline.snapshotId &&
286
+ root?.repoId === binding.repoId &&
287
+ root.indexId === binding.indexId &&
288
+ root.configHash === binding.configHash &&
289
+ root.buildId === baseline.buildId &&
290
+ root.attemptId === baseline.attemptId &&
291
+ root.recordsHash === baseline.recordsHash &&
292
+ root.inventoryHash === baseline.inventoryHash &&
293
+ root.commitOid === baseline.commitOid, "Tracked branch baseline Tag is missing or changed.");
294
+ }
295
+ else {
296
+ invariant(!writer && !tracked, "Unowned or unfinished tracked branch; retain its retry journal.");
297
+ }
298
+ }
299
+ else if (args.baseline?.version.writer.startsWith("work-")) {
188
300
  const prev = args.baseline;
189
301
  await validateBuild(prev.build);
190
302
  invariant(prev.build.repoKey === b.repoKey &&
@@ -202,7 +314,9 @@ export async function publish(args) {
202
314
  inventoryHash: b.inventoryHash,
203
315
  artifact: b.directory,
204
316
  attemptId,
205
- writer: `work-${attemptId}`,
317
+ writer: b.branchRef ? gitBranchName(b.branchRef) : `work-${attemptId}`,
318
+ gitRef: b.branchRef,
319
+ reuseWriter: !!tracked?.applied,
206
320
  source,
207
321
  baseline,
208
322
  phase: "branch",
@@ -212,12 +326,39 @@ export async function publish(args) {
212
326
  }
213
327
  const save = () => atomic(journalPath, j);
214
328
  if (j.phase === "branch") {
329
+ if (j.gitRef) {
330
+ // Record intent before any corpus mutation, including before branch creation.
331
+ // A fresh local state must not treat a lagging, partially written head as clean.
332
+ const current = await trackedBranch(store, binding, j.gitRef);
333
+ invariant((!current?.pending || current.pending.attemptId === j.attemptId) &&
334
+ current?.applied?.snapshotId === j.baseline?.snapshotId, "Git branch ownership or baseline changed before import.");
335
+ await store.upsert("main", [
336
+ {
337
+ ...current,
338
+ id: gitBranchId(j.gitRef),
339
+ kind: "manifest",
340
+ role: "git-branch",
341
+ schemaVersion: 1,
342
+ repoId: binding.repoId,
343
+ indexId: binding.indexId,
344
+ configHash: binding.configHash,
345
+ gitRef: j.gitRef,
346
+ writer: j.writer,
347
+ pending: {
348
+ attemptId: j.attemptId,
349
+ buildId: j.buildId,
350
+ commitOid: b.commitOid,
351
+ },
352
+ },
353
+ ]);
354
+ }
215
355
  const branches = await store.branches();
216
356
  const found = branches.find((x) => x.name === j.writer);
217
357
  if (found)
218
- invariant(found.parent === j.source &&
358
+ invariant((j.reuseWriter || found.parent === j.source) &&
219
359
  found.snapshotId === (j.baseline?.snapshotId ?? null), "Partially created writer has an unexpected baseline.");
220
360
  else {
361
+ invariant(!j.reuseWriter, "Tracked writer is missing; refusing to recreate a nonempty baseline.");
221
362
  const source = branches.find((x) => x.name === j.source);
222
363
  invariant(source &&
223
364
  (j.baseline
@@ -232,18 +373,14 @@ export async function publish(args) {
232
373
  const marker = metadata.at(-1);
233
374
  if (j.phase === "writing") {
234
375
  // Reconcile from the immutable old Tag, never from a half-written writer.
235
- const oldIds = new Set();
236
- const oldHashes = new Map();
237
- if (j.baseline)
238
- for await (const d of store.list(tag(j.baseline.tagName))) {
239
- oldIds.add(d.id);
240
- oldHashes.set(d.id, hash(d));
241
- }
376
+ const oldHashes = j.baseline
377
+ ? await baselineHashes(store, binding, b.preset, j.baseline, state, j.gitRef, args.baseline?.build)
378
+ : new Map();
242
379
  const newIds = new Set([
243
380
  ...Object.keys(b.recordHashes),
244
381
  ...metadata.map((d) => d.id),
245
382
  ]);
246
- const obsolete = [...oldIds].filter((id) => !newIds.has(id));
383
+ const obsolete = [...oldHashes.keys()].filter((id) => !newIds.has(id));
247
384
  const mutate = async (operation, payload, send) => {
248
385
  const receipt = {
249
386
  operation,
@@ -261,11 +398,12 @@ export async function publish(args) {
261
398
  await mutate("delete", ids, () => store.delete(j.writer, ids));
262
399
  }
263
400
  async function* changed() {
264
- for await (const d of records(b.directory))
401
+ for await (const d of records(b))
265
402
  if (oldHashes.get(d.id) !== hash(d))
266
403
  yield d;
267
404
  for (const d of metadata.slice(0, -1))
268
- yield d;
405
+ if (oldHashes.get(d.id) !== hash(d))
406
+ yield d;
269
407
  }
270
408
  for await (const docs of batches(changed()))
271
409
  await mutate("upsert", docs, () => store.upsert(j.writer, docs));
@@ -280,6 +418,7 @@ export async function publish(args) {
280
418
  const deadline = Date.now() + (args.timeoutMs ?? 120_000);
281
419
  const pause = args.pollMs ?? 1000;
282
420
  let rejectedSnapshot;
421
+ let validatedHere = false;
283
422
  while (j.phase === "waiting") {
284
423
  invariant(Date.now() < deadline, "Index validation timed out; build remains unpublished. Resume this artifact with --resume.");
285
424
  const visible = await one(store, branch(j.writer), "__manifest__", false);
@@ -301,16 +440,24 @@ export async function publish(args) {
301
440
  continue;
302
441
  }
303
442
  j.candidate = candidate;
443
+ validatedHere = true;
304
444
  j.phase = "publishing";
305
445
  await save();
306
446
  }
307
447
  invariant(j.candidate, "Missing validated candidate.");
308
- invariant(await validateCandidate(store, j.candidate.name, b, binding, j.attemptId), "Validated candidate is missing or changed.");
448
+ // Never persist this shortcut: a resumed journal must revalidate the full corpus.
449
+ if (!validatedHere)
450
+ invariant(await validateCandidate(store, j.candidate.name, b, binding, j.attemptId), "Validated candidate is missing or changed.");
451
+ const pins = await store.tags();
452
+ invariant(pins.find((t) => t.name === j.candidate.name)?.snapshotId ===
453
+ j.candidate.snapshotId, "Validated candidate Tag is missing or changed.");
309
454
  const name = versionTag(b.commitOid);
310
- const found = (await store.tags()).find((t) => t.name === name);
455
+ const found = pins.find((t) => t.name === name);
311
456
  const final = found ?? (await store.tag(name, tag(j.candidate.name)));
312
- invariant(final.snapshotId === j.candidate.snapshotId, "Published Tag name already points to a different snapshot.");
313
- const result = {
457
+ invariant(existing
458
+ ? final.snapshotId === existing.snapshotId
459
+ : final.snapshotId === j.candidate.snapshotId, "Published Tag name already points to a different snapshot.");
460
+ const result = existing ?? {
314
461
  id: versionId(b.commitOid),
315
462
  kind: "manifest",
316
463
  role: "published",
@@ -330,8 +477,39 @@ export async function publish(args) {
330
477
  counts: b.counts,
331
478
  validation: { inventory: true, content: true, query: true },
332
479
  };
333
- await store.upsert("main", [result]);
334
- await atomic(join(state, "last-build.json"), {
480
+ if (!existing)
481
+ await store.upsert("main", [result]);
482
+ if (j.gitRef) {
483
+ const current = await trackedBranch(store, binding, j.gitRef);
484
+ invariant(current?.pending?.attemptId === j.attemptId ||
485
+ (!current?.pending && current?.applied?.attemptId === j.attemptId), "Tracked branch publication ownership changed.");
486
+ // Pin the exact writer baseline even when another branch published this commit first.
487
+ const applied = {
488
+ ...result,
489
+ writer: j.writer,
490
+ snapshotId: j.candidate.snapshotId,
491
+ tagName: final.snapshotId === j.candidate.snapshotId
492
+ ? final.name
493
+ : j.candidate.name,
494
+ attemptId: j.attemptId,
495
+ };
496
+ await store.upsert("main", [
497
+ {
498
+ id: gitBranchId(j.gitRef),
499
+ kind: "manifest",
500
+ role: "git-branch",
501
+ schemaVersion: 1,
502
+ repoId: binding.repoId,
503
+ indexId: binding.indexId,
504
+ configHash: binding.configHash,
505
+ gitRef: j.gitRef,
506
+ writer: j.writer,
507
+ applied,
508
+ publishedTagName: result.tagName,
509
+ },
510
+ ]);
511
+ }
512
+ await atomic(lastBuildPath(state, j.gitRef), {
335
513
  artifact: b.directory,
336
514
  version: result,
337
515
  });