@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.
- package/CHANGELOG.md +179 -0
- package/CONTRIBUTING.md +4 -0
- package/DESIGN.md +378 -58
- package/README.md +628 -35
- package/RELEASING.md +15 -3
- package/VALIDATION.md +1120 -16
- package/dist/artifact.d.ts +31 -0
- package/dist/artifact.js +237 -0
- package/dist/artifact.js.map +1 -0
- package/dist/build.d.ts +86 -3
- package/dist/build.js +233 -71
- package/dist/build.js.map +1 -1
- package/dist/chunk.d.ts +9 -3
- package/dist/chunk.js +341 -14
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +130 -21
- package/dist/cli.js.map +1 -1
- package/dist/file-policy.d.ts +20 -0
- package/dist/file-policy.js +103 -0
- package/dist/file-policy.js.map +1 -0
- package/dist/implementation.d.ts +11 -0
- package/dist/implementation.js +191 -0
- package/dist/implementation.js.map +1 -0
- package/dist/publish.d.ts +18 -0
- package/dist/publish.js +224 -46
- package/dist/publish.js.map +1 -1
- package/dist/releases.js +21 -3
- package/dist/releases.js.map +1 -1
- package/dist/remote.d.ts +1 -0
- package/dist/remote.js +50 -9
- package/dist/remote.js.map +1 -1
- package/dist/repository.d.ts +4 -1
- package/dist/repository.js +26 -11
- package/dist/repository.js.map +1 -1
- package/dist/rerank.d.ts +16 -0
- package/dist/rerank.js +87 -0
- package/dist/rerank.js.map +1 -0
- package/dist/search.d.ts +18 -3
- package/dist/search.js +143 -23
- package/dist/search.js.map +1 -1
- package/dist/skills.d.ts +24 -0
- package/dist/skills.js +155 -0
- package/dist/skills.js.map +1 -0
- package/package.json +17 -6
- package/runtime/grammars/README.md +50 -0
- package/runtime/grammars/tree-sitter-bash.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-bash.wasm +0 -0
- package/runtime/grammars/tree-sitter-sql.LICENSE +21 -0
- package/runtime/grammars/tree-sitter-sql.wasm +0 -0
- package/runtime/grammars/tree-sitter-wasm.LICENSE +21 -0
- package/runtime/qwen.py +132 -0
- package/runtime/requirements.txt +34 -0
- package/skills/srcx-search/SKILL.md +103 -0
- package/src/artifact.ts +303 -0
- package/src/build.ts +313 -84
- package/src/chunk.ts +360 -12
- package/src/cli.ts +263 -23
- package/src/file-policy.ts +166 -0
- package/src/implementation.ts +253 -0
- package/src/publish.ts +321 -47
- package/src/releases.ts +36 -3
- package/src/remote.ts +70 -13
- package/src/repository.ts +40 -11
- package/src/rerank.ts +131 -0
- package/src/search.ts +229 -28
- 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
|
-
|
|
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
|
|
97
|
+
for await (const d of records(b)) {
|
|
74
98
|
if (d.kind !== "chunk")
|
|
75
99
|
continue;
|
|
76
|
-
//
|
|
77
|
-
//
|
|
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.
|
|
94
|
-
defaultField: "
|
|
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" ||
|
|
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, "
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
272
|
+
return existing;
|
|
273
|
+
}
|
|
183
274
|
}
|
|
184
275
|
if (!j) {
|
|
185
276
|
let source = "checkpoint-empty";
|
|
186
277
|
let baseline;
|
|
187
|
-
if (
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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 = [...
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
455
|
+
const found = pins.find((t) => t.name === name);
|
|
311
456
|
const final = found ?? (await store.tag(name, tag(j.candidate.name)));
|
|
312
|
-
invariant(
|
|
313
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
});
|