@mcoda/mswarm 0.1.89 → 0.1.91
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/codali-executor.d.ts +107 -6
- package/dist/codali-executor.d.ts.map +1 -1
- package/dist/codali-executor.js +500 -26
- package/dist/codali-executor.js.map +1 -1
- package/dist/runtime.d.ts +6 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +18 -5
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +18 -3
- package/dist/server.js.map +1 -1
- package/dist/vendor/codali/cli/DatasetCommand.d.ts +38 -0
- package/dist/vendor/codali/cli/DatasetCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/DatasetCommand.js +959 -0
- package/dist/vendor/codali/cli/EvalCommand.d.ts +3 -0
- package/dist/vendor/codali/cli/EvalCommand.d.ts.map +1 -1
- package/dist/vendor/codali/cli/EvalCommand.js +55 -1
- package/dist/vendor/codali/cli/ImprovementCommand.d.ts +97 -0
- package/dist/vendor/codali/cli/ImprovementCommand.d.ts.map +1 -0
- package/dist/vendor/codali/cli/ImprovementCommand.js +1879 -0
- package/dist/vendor/codali/cli.d.ts.map +1 -1
- package/dist/vendor/codali/cli.js +15 -0
- package/dist/vendor/codali/dataset-cli.d.ts +3 -0
- package/dist/vendor/codali/dataset-cli.d.ts.map +1 -0
- package/dist/vendor/codali/dataset-cli.js +30 -0
- package/dist/vendor/codali/eval/CodaliGatewayLiveHarness.d.ts +90 -1
- package/dist/vendor/codali/eval/CodaliGatewayLiveHarness.d.ts.map +1 -1
- package/dist/vendor/codali/eval/CodaliGatewayLiveHarness.js +443 -9
- package/dist/vendor/codali/eval/GatewayDatasetEval.d.ts +70 -0
- package/dist/vendor/codali/eval/GatewayDatasetEval.d.ts.map +1 -0
- package/dist/vendor/codali/eval/GatewayDatasetEval.js +477 -0
- package/dist/vendor/codali/eval/GatewayEvalSuite.d.ts +52 -0
- package/dist/vendor/codali/eval/GatewayEvalSuite.d.ts.map +1 -1
- package/dist/vendor/codali/eval/GatewayEvalSuite.js +135 -1
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts +4 -0
- package/dist/vendor/codali/gateway/CodaliGateway.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/CodaliGateway.js +37 -1
- package/dist/vendor/codali/gateway/CodaliGatewaySchemas.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/CodaliGatewaySchemas.js +0 -4
- package/dist/vendor/codali/gateway/CodaliGatewayStore.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/CodaliGatewayStore.js +1 -0
- package/dist/vendor/codali/gateway/GatewayPlanner.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/GatewayPlanner.js +355 -41
- package/dist/vendor/codali/gateway/GatewayStateMachine.d.ts +1 -0
- package/dist/vendor/codali/gateway/GatewayStateMachine.d.ts.map +1 -1
- package/dist/vendor/codali/gateway/GatewayStateMachine.js +221 -5
- package/dist/vendor/codali/improvement/CandidateReleaseBuilder.d.ts +201 -0
- package/dist/vendor/codali/improvement/CandidateReleaseBuilder.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/CandidateReleaseBuilder.js +1175 -0
- package/dist/vendor/codali/improvement/DatasetEligibilityGate.d.ts +75 -0
- package/dist/vendor/codali/improvement/DatasetEligibilityGate.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/DatasetEligibilityGate.js +497 -0
- package/dist/vendor/codali/improvement/DatasetExportManifestReader.d.ts +88 -0
- package/dist/vendor/codali/improvement/DatasetExportManifestReader.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/DatasetExportManifestReader.js +333 -0
- package/dist/vendor/codali/improvement/DocdexRetrievalCandidateBuilder.d.ts +192 -0
- package/dist/vendor/codali/improvement/DocdexRetrievalCandidateBuilder.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/DocdexRetrievalCandidateBuilder.js +625 -0
- package/dist/vendor/codali/improvement/EvalReplayCandidateBuilder.d.ts +173 -0
- package/dist/vendor/codali/improvement/EvalReplayCandidateBuilder.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/EvalReplayCandidateBuilder.js +363 -0
- package/dist/vendor/codali/improvement/FineTuneJobPlanner.d.ts +281 -0
- package/dist/vendor/codali/improvement/FineTuneJobPlanner.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/FineTuneJobPlanner.js +823 -0
- package/dist/vendor/codali/improvement/ImprovementEvalRunner.d.ts +50 -0
- package/dist/vendor/codali/improvement/ImprovementEvalRunner.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/ImprovementEvalRunner.js +648 -0
- package/dist/vendor/codali/improvement/ImprovementPolicy.d.ts +272 -0
- package/dist/vendor/codali/improvement/ImprovementPolicy.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/ImprovementPolicy.js +1450 -0
- package/dist/vendor/codali/improvement/ModelRouterCandidateBuilder.d.ts +186 -0
- package/dist/vendor/codali/improvement/ModelRouterCandidateBuilder.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/ModelRouterCandidateBuilder.js +880 -0
- package/dist/vendor/codali/improvement/OperatorInspector.d.ts +182 -0
- package/dist/vendor/codali/improvement/OperatorInspector.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/OperatorInspector.js +604 -0
- package/dist/vendor/codali/improvement/ProductionGovernance.d.ts +91 -0
- package/dist/vendor/codali/improvement/ProductionGovernance.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/ProductionGovernance.js +329 -0
- package/dist/vendor/codali/improvement/PromptSchemaToolMetadataCandidateBuilder.d.ts +152 -0
- package/dist/vendor/codali/improvement/PromptSchemaToolMetadataCandidateBuilder.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/PromptSchemaToolMetadataCandidateBuilder.js +480 -0
- package/dist/vendor/codali/improvement/PublishOrchestrator.d.ts +113 -0
- package/dist/vendor/codali/improvement/PublishOrchestrator.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/PublishOrchestrator.js +590 -0
- package/dist/vendor/codali/improvement/ReleaseOutcomeReporter.d.ts +133 -0
- package/dist/vendor/codali/improvement/ReleaseOutcomeReporter.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/ReleaseOutcomeReporter.js +501 -0
- package/dist/vendor/codali/improvement/StorageServiceImprovementClient.d.ts +71 -0
- package/dist/vendor/codali/improvement/StorageServiceImprovementClient.d.ts.map +1 -0
- package/dist/vendor/codali/improvement/StorageServiceImprovementClient.js +245 -0
- package/dist/vendor/codali/index.d.ts +43 -0
- package/dist/vendor/codali/index.d.ts.map +1 -1
- package/dist/vendor/codali/index.js +21 -0
- package/dist/vendor/codali/runtime/CodaliRuntime.d.ts +0 -2
- package/dist/vendor/codali/runtime/CodaliRuntime.d.ts.map +1 -1
- package/dist/vendor/codali/runtime/CodaliRuntime.js +0 -5
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts +138 -0
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.d.ts.map +1 -0
- package/dist/vendor/codali/storage/CodaliDatasetPrivacyEngine.js +417 -0
- package/dist/vendor/codali/storage/CodaliFeedbackReviewIngestion.d.ts +50 -0
- package/dist/vendor/codali/storage/CodaliFeedbackReviewIngestion.d.ts.map +1 -0
- package/dist/vendor/codali/storage/CodaliFeedbackReviewIngestion.js +141 -0
- package/dist/vendor/codali/storage/CodaliStorageContracts.d.ts +337 -0
- package/dist/vendor/codali/storage/CodaliStorageContracts.d.ts.map +1 -0
- package/dist/vendor/codali/storage/CodaliStorageContracts.js +1555 -0
- package/dist/vendor/codali/storage/DatasetExportJob.d.ts +51 -0
- package/dist/vendor/codali/storage/DatasetExportJob.d.ts.map +1 -0
- package/dist/vendor/codali/storage/DatasetExportJob.js +358 -0
- package/dist/vendor/codali/storage/DatasetReviewQueue.d.ts +98 -0
- package/dist/vendor/codali/storage/DatasetReviewQueue.d.ts.map +1 -0
- package/dist/vendor/codali/storage/DatasetReviewQueue.js +426 -0
- package/dist/vendor/codali/storage/GatewayDatasetStore.d.ts +233 -0
- package/dist/vendor/codali/storage/GatewayDatasetStore.d.ts.map +1 -0
- package/dist/vendor/codali/storage/GatewayDatasetStore.js +2070 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1175 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstat, mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { CODALI_IMPROVEMENT_CONTRACT_SCHEMA_VERSION, } from "./ImprovementPolicy.js";
|
|
7
|
+
import { CODALI_PATCH_CANDIDATE_SCHEMA_VERSION, CODALI_PATCH_PROPOSAL_ARTIFACTS, buildCodaliPatchCandidateBundle, } from "./PromptSchemaToolMetadataCandidateBuilder.js";
|
|
8
|
+
export const CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION = "codali.improvement.candidate_release.v1";
|
|
9
|
+
export const CODALI_CANDIDATE_RELEASE_PLAN_SCHEMA_VERSION = "codali.improvement.release_plan.v1";
|
|
10
|
+
export const DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS = [
|
|
11
|
+
".codali/improvement/",
|
|
12
|
+
];
|
|
13
|
+
export const DEFAULT_CODALI_CANDIDATE_RELEASE_DIRECTORIES = [
|
|
14
|
+
".codali/improvement/candidates",
|
|
15
|
+
];
|
|
16
|
+
export class CodaliCandidateReleaseBuilderError extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(`${code}: ${message}`);
|
|
19
|
+
this.name = "CodaliCandidateReleaseBuilderError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const stableJson = (value) => {
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return `[${value.map((item) => stableJson(item)).join(",")}]`;
|
|
26
|
+
}
|
|
27
|
+
if (value && typeof value === "object") {
|
|
28
|
+
return `{${Object.entries(value)
|
|
29
|
+
.filter(([, item]) => item !== undefined)
|
|
30
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
31
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
|
|
32
|
+
.join(",")}}`;
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
};
|
|
36
|
+
const hashJson = (value) => createHash("sha256").update(stableJson(value)).digest("hex");
|
|
37
|
+
const hashContent = (value) => createHash("sha256").update(value).digest("hex");
|
|
38
|
+
const stableId = (prefix, value) => `${prefix}-${hashJson(value).slice(0, 16)}`;
|
|
39
|
+
const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
40
|
+
const stringArray = (value) => Array.isArray(value)
|
|
41
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
42
|
+
: [];
|
|
43
|
+
const uniqueSortedStrings = (values) => Array.from(new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))).sort();
|
|
44
|
+
const nestedRecord = (record, pathSegments) => {
|
|
45
|
+
let current = record;
|
|
46
|
+
for (const segment of pathSegments) {
|
|
47
|
+
if (!isRecord(current))
|
|
48
|
+
return undefined;
|
|
49
|
+
current = current[segment];
|
|
50
|
+
}
|
|
51
|
+
return isRecord(current) ? current : undefined;
|
|
52
|
+
};
|
|
53
|
+
const readJsonRecord = async (filePath) => {
|
|
54
|
+
const raw = await readFile(filePath, "utf8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!isRecord(parsed)) {
|
|
57
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_JSON_RECORD_REQUIRED", `Expected JSON object at ${filePath}.`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
};
|
|
61
|
+
const formatJsonRecord = (record) => `${JSON.stringify(record, null, 2)}\n`;
|
|
62
|
+
const semverParts = (version) => {
|
|
63
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u);
|
|
64
|
+
if (!match) {
|
|
65
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_INVALID_VERSION", `Package version must be semver-compatible: ${version}`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
major: Number(match[1]),
|
|
69
|
+
minor: Number(match[2]),
|
|
70
|
+
patch: Number(match[3]),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const bumpSemver = (version, bump) => {
|
|
74
|
+
const parts = semverParts(version);
|
|
75
|
+
if (bump === "major")
|
|
76
|
+
return `${parts.major + 1}.0.0`;
|
|
77
|
+
if (bump === "minor")
|
|
78
|
+
return `${parts.major}.${parts.minor + 1}.0`;
|
|
79
|
+
return `${parts.major}.${parts.minor}.${parts.patch + 1}`;
|
|
80
|
+
};
|
|
81
|
+
const artifactClassForProposal = (artifact) => {
|
|
82
|
+
if (artifact === "prompt")
|
|
83
|
+
return "prompt_patch";
|
|
84
|
+
if (artifact === "schema")
|
|
85
|
+
return "schema_patch";
|
|
86
|
+
if (artifact === "tool-metadata")
|
|
87
|
+
return "tool_metadata_patch";
|
|
88
|
+
if (artifact === "eval")
|
|
89
|
+
return "eval_replay_fixture";
|
|
90
|
+
if (artifact === "model-router")
|
|
91
|
+
return "model_router";
|
|
92
|
+
if (artifact === "docdex-retrieval")
|
|
93
|
+
return "docdex_retrieval";
|
|
94
|
+
return artifact.trim().toLowerCase().replace(/[^a-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "");
|
|
95
|
+
};
|
|
96
|
+
const semverBumpForArtifactClasses = (changedArtifactClasses) => {
|
|
97
|
+
if (changedArtifactClasses.some((item) => item.includes("breaking") || item === "major")) {
|
|
98
|
+
return "major";
|
|
99
|
+
}
|
|
100
|
+
if (changedArtifactClasses.some((item) => item === "schema_patch" ||
|
|
101
|
+
item === "tool_metadata_patch" ||
|
|
102
|
+
item.includes("contract"))) {
|
|
103
|
+
return "minor";
|
|
104
|
+
}
|
|
105
|
+
return "patch";
|
|
106
|
+
};
|
|
107
|
+
const packageManifestCandidates = async (repoRoot) => {
|
|
108
|
+
const candidates = ["package.json"];
|
|
109
|
+
const packagesRoot = path.join(repoRoot, "packages");
|
|
110
|
+
try {
|
|
111
|
+
const entries = await readdir(packagesRoot, { withFileTypes: true });
|
|
112
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
candidates.push(toPosixPath(path.join("packages", entry.name, "package.json")));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (!isNotFoundError(error))
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return candidates;
|
|
123
|
+
};
|
|
124
|
+
const readPackageManifests = async (repoRoot) => {
|
|
125
|
+
const manifests = [];
|
|
126
|
+
for (const relativePath of await packageManifestCandidates(repoRoot)) {
|
|
127
|
+
const absolutePath = path.join(repoRoot, relativePath);
|
|
128
|
+
try {
|
|
129
|
+
const raw = await readJsonRecord(absolutePath);
|
|
130
|
+
const packageName = typeof raw.name === "string" ? raw.name : undefined;
|
|
131
|
+
const version = typeof raw.version === "string" ? raw.version : undefined;
|
|
132
|
+
if (!packageName || !version)
|
|
133
|
+
continue;
|
|
134
|
+
manifests.push({
|
|
135
|
+
packageName,
|
|
136
|
+
relativePath,
|
|
137
|
+
version,
|
|
138
|
+
privatePackage: raw.private === true,
|
|
139
|
+
raw,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (!isNotFoundError(error))
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return manifests.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
148
|
+
};
|
|
149
|
+
const releasePleaseManifestTarget = async (input) => {
|
|
150
|
+
const relativePath = ".release-please-manifest.json";
|
|
151
|
+
const absolutePath = path.join(input.repoRoot, relativePath);
|
|
152
|
+
try {
|
|
153
|
+
const raw = await readJsonRecord(absolutePath);
|
|
154
|
+
const updated = Object.fromEntries(Object.entries(raw).map(([key, value]) => [
|
|
155
|
+
key,
|
|
156
|
+
typeof value === "string" ? input.plannedVersion : value,
|
|
157
|
+
]));
|
|
158
|
+
const content = formatJsonRecord(updated);
|
|
159
|
+
return {
|
|
160
|
+
relativePath,
|
|
161
|
+
content,
|
|
162
|
+
artifact: generatedArtifactForContent({
|
|
163
|
+
artifactKind: "release_manifest",
|
|
164
|
+
relativePath,
|
|
165
|
+
content,
|
|
166
|
+
sourceExportIds: input.sourceExportIds,
|
|
167
|
+
storageManifestVersion: input.storageManifestVersion,
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (isNotFoundError(error))
|
|
173
|
+
return undefined;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const generatedArtifactForContent = (input) => {
|
|
178
|
+
const contentHash = hashContent(input.content);
|
|
179
|
+
return {
|
|
180
|
+
artifactId: `${input.artifactKind}-${contentHash.slice(0, 16)}`,
|
|
181
|
+
artifactKind: input.artifactKind,
|
|
182
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
183
|
+
sourceExportIds: input.sourceExportIds,
|
|
184
|
+
sourceSchemaVersions: {
|
|
185
|
+
candidateRelease: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
186
|
+
patchCandidate: CODALI_PATCH_CANDIDATE_SCHEMA_VERSION,
|
|
187
|
+
storageManifest: input.storageManifestVersion,
|
|
188
|
+
},
|
|
189
|
+
relativePath: input.relativePath,
|
|
190
|
+
contentHash,
|
|
191
|
+
byteSize: Buffer.byteLength(input.content, "utf8"),
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const packageVersionTargetsFor = async (input) => {
|
|
195
|
+
const manifests = await readPackageManifests(input.repoRoot);
|
|
196
|
+
if (!manifests.length) {
|
|
197
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_PACKAGE_MANIFESTS_MISSING", "No package.json manifests with name and version were found.");
|
|
198
|
+
}
|
|
199
|
+
const rootVersion = manifests.find((manifest) => manifest.relativePath === "package.json")?.version ??
|
|
200
|
+
manifests[0]?.version;
|
|
201
|
+
if (!rootVersion) {
|
|
202
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_ROOT_VERSION_MISSING", "Unable to determine the current workspace version.");
|
|
203
|
+
}
|
|
204
|
+
const plannedVersion = bumpSemver(rootVersion, input.semverBump);
|
|
205
|
+
const targets = manifests.map((manifest) => ({
|
|
206
|
+
packageName: manifest.packageName,
|
|
207
|
+
relativePath: manifest.relativePath,
|
|
208
|
+
currentVersion: manifest.version,
|
|
209
|
+
plannedVersion,
|
|
210
|
+
privatePackage: manifest.privatePackage,
|
|
211
|
+
}));
|
|
212
|
+
const writeTargets = manifests.map((manifest) => {
|
|
213
|
+
const content = formatJsonRecord({
|
|
214
|
+
...manifest.raw,
|
|
215
|
+
version: plannedVersion,
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
relativePath: manifest.relativePath,
|
|
219
|
+
content,
|
|
220
|
+
artifact: generatedArtifactForContent({
|
|
221
|
+
artifactKind: "package_version",
|
|
222
|
+
relativePath: manifest.relativePath,
|
|
223
|
+
content,
|
|
224
|
+
sourceExportIds: input.sourceExportIds,
|
|
225
|
+
storageManifestVersion: input.storageManifestVersion,
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
const releasePleaseTarget = await releasePleaseManifestTarget({
|
|
230
|
+
repoRoot: input.repoRoot,
|
|
231
|
+
plannedVersion,
|
|
232
|
+
sourceExportIds: input.sourceExportIds,
|
|
233
|
+
storageManifestVersion: input.storageManifestVersion,
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
currentVersion: rootVersion,
|
|
237
|
+
plannedVersion,
|
|
238
|
+
targets,
|
|
239
|
+
writeTargets: releasePleaseTarget
|
|
240
|
+
? [...writeTargets, releasePleaseTarget]
|
|
241
|
+
: writeTargets,
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
const sanitizeBranchSegment = (value) => {
|
|
245
|
+
const normalized = value
|
|
246
|
+
.trim()
|
|
247
|
+
.toLowerCase()
|
|
248
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
249
|
+
.replace(/^-+|-+$/g, "")
|
|
250
|
+
.slice(0, 64);
|
|
251
|
+
return normalized || "run";
|
|
252
|
+
};
|
|
253
|
+
const dateFromInspection = (inspection, fallback) => {
|
|
254
|
+
const candidate = inspection.manifest.createdAt;
|
|
255
|
+
const parsed = candidate ? new Date(candidate) : undefined;
|
|
256
|
+
if (parsed && Number.isFinite(parsed.getTime())) {
|
|
257
|
+
return parsed.toISOString().slice(0, 10);
|
|
258
|
+
}
|
|
259
|
+
return fallback().toISOString().slice(0, 10);
|
|
260
|
+
};
|
|
261
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
262
|
+
const normalizeRepoRelativePath = (repoRoot, candidatePath) => {
|
|
263
|
+
if (!candidatePath.trim()) {
|
|
264
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_EMPTY_PATH", "Candidate write path cannot be empty.");
|
|
265
|
+
}
|
|
266
|
+
const resolved = path.isAbsolute(candidatePath)
|
|
267
|
+
? path.resolve(candidatePath)
|
|
268
|
+
: path.resolve(repoRoot, candidatePath);
|
|
269
|
+
const relative = toPosixPath(path.relative(repoRoot, resolved));
|
|
270
|
+
if (!relative ||
|
|
271
|
+
relative === ".." ||
|
|
272
|
+
relative.startsWith("../") ||
|
|
273
|
+
path.isAbsolute(relative)) {
|
|
274
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_PATH_OUTSIDE_REPO", `Candidate write path must stay inside the repo: ${candidatePath}`);
|
|
275
|
+
}
|
|
276
|
+
if (relative === ".git" || relative.startsWith(".git/")) {
|
|
277
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_GIT_PATH_BLOCKED", "Candidate writer cannot target .git paths.");
|
|
278
|
+
}
|
|
279
|
+
return relative;
|
|
280
|
+
};
|
|
281
|
+
const normalizeApprovedPath = (repoRoot, candidatePath) => {
|
|
282
|
+
const directory = candidatePath.endsWith("/") || candidatePath.endsWith(path.sep);
|
|
283
|
+
const relative = normalizeRepoRelativePath(repoRoot, candidatePath);
|
|
284
|
+
return {
|
|
285
|
+
path: directory && !relative.endsWith("/") ? `${relative}/` : relative,
|
|
286
|
+
directory,
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
const isApprovedRelativePath = (relativePath, approvedPaths) => approvedPaths.some((approved) => approved.directory
|
|
290
|
+
? relativePath.startsWith(approved.path)
|
|
291
|
+
: relativePath === approved.path);
|
|
292
|
+
const parseGitStatus = (stdout) => stdout
|
|
293
|
+
.split(/\r?\n/u)
|
|
294
|
+
.map((line) => line.trimEnd())
|
|
295
|
+
.filter(Boolean)
|
|
296
|
+
.map((line) => {
|
|
297
|
+
const status = line.slice(0, 2).trim() || line.slice(0, 2);
|
|
298
|
+
const rawPath = line.slice(3).trim();
|
|
299
|
+
const renamedPath = rawPath.includes(" -> ")
|
|
300
|
+
? rawPath.slice(rawPath.lastIndexOf(" -> ") + 4)
|
|
301
|
+
: rawPath;
|
|
302
|
+
return {
|
|
303
|
+
status,
|
|
304
|
+
path: toPosixPath(renamedPath.replace(/^"|"$/g, "")),
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
const defaultCommandRunner = (command, args, options) => {
|
|
308
|
+
const result = spawnSync(command, args, {
|
|
309
|
+
cwd: options.cwd,
|
|
310
|
+
encoding: "utf8",
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
314
|
+
stdout: result.stdout ?? "",
|
|
315
|
+
stderr: result.stderr ?? "",
|
|
316
|
+
error: result.error ? String(result.error) : undefined,
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
const isNotFoundError = (error) => Boolean(error &&
|
|
320
|
+
typeof error === "object" &&
|
|
321
|
+
"code" in error &&
|
|
322
|
+
error.code === "ENOENT");
|
|
323
|
+
const pathIsInsideOrEqual = (rootPath, candidatePath) => {
|
|
324
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
325
|
+
return !relative || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
326
|
+
};
|
|
327
|
+
const findExistingWritePath = async (absolutePath) => {
|
|
328
|
+
let current = absolutePath;
|
|
329
|
+
for (;;) {
|
|
330
|
+
try {
|
|
331
|
+
await lstat(current);
|
|
332
|
+
return current;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (!isNotFoundError(error)) {
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
const parent = path.dirname(current);
|
|
339
|
+
if (parent === current) {
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
current = parent;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const assertWriteTargetsInsideRepo = async (input) => {
|
|
347
|
+
const repoRealPath = await realpath(input.repoRoot);
|
|
348
|
+
for (const target of input.targets) {
|
|
349
|
+
const absolute = path.join(input.repoRoot, target.relativePath);
|
|
350
|
+
const existingPath = await findExistingWritePath(absolute);
|
|
351
|
+
const existingRealPath = await realpath(existingPath);
|
|
352
|
+
if (!pathIsInsideOrEqual(repoRealPath, existingRealPath)) {
|
|
353
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_PATH_OUTSIDE_REPO", `Candidate write path must not resolve outside the repo: ${target.relativePath}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const readDirtyEntries = (repoRoot, commandRunner) => {
|
|
358
|
+
const result = commandRunner("git", ["status", "--porcelain"], { cwd: repoRoot });
|
|
359
|
+
if (result.exitCode !== 0 || result.error) {
|
|
360
|
+
return {
|
|
361
|
+
entries: [],
|
|
362
|
+
warning: result.error ?? result.stderr ?? "git status unavailable",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return { entries: parseGitStatus(result.stdout) };
|
|
366
|
+
};
|
|
367
|
+
const dirtySummaryFor = (entries, targetPaths, warning) => {
|
|
368
|
+
if (warning) {
|
|
369
|
+
return {
|
|
370
|
+
status: "unavailable",
|
|
371
|
+
dirtyFileCount: 0,
|
|
372
|
+
targetDirtyFiles: [],
|
|
373
|
+
unrelatedDirtyFileCount: 0,
|
|
374
|
+
unrelatedDirtyFilesSample: [],
|
|
375
|
+
warning,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const targetSet = new Set(targetPaths);
|
|
379
|
+
const dirtyPaths = Array.from(new Set(entries.map((entry) => entry.path))).sort();
|
|
380
|
+
const targetDirtyFiles = dirtyPaths.filter((item) => targetSet.has(item));
|
|
381
|
+
const unrelatedDirtyFiles = dirtyPaths.filter((item) => !targetSet.has(item));
|
|
382
|
+
return {
|
|
383
|
+
status: dirtyPaths.length ? "dirty" : "clean",
|
|
384
|
+
dirtyFileCount: dirtyPaths.length,
|
|
385
|
+
targetDirtyFiles,
|
|
386
|
+
unrelatedDirtyFileCount: unrelatedDirtyFiles.length,
|
|
387
|
+
unrelatedDirtyFilesSample: unrelatedDirtyFiles.slice(0, 20),
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
const unifiedDiffForNewFile = (relativePath, content) => {
|
|
391
|
+
const lines = content.endsWith("\n")
|
|
392
|
+
? content.slice(0, -1).split("\n")
|
|
393
|
+
: content.split("\n");
|
|
394
|
+
return [
|
|
395
|
+
`diff --git a/${relativePath} b/${relativePath}`,
|
|
396
|
+
"new file mode 100644",
|
|
397
|
+
"index 0000000..0000000",
|
|
398
|
+
"--- /dev/null",
|
|
399
|
+
`+++ b/${relativePath}`,
|
|
400
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
401
|
+
...lines.map((line) => `+${line}`),
|
|
402
|
+
"",
|
|
403
|
+
].join("\n");
|
|
404
|
+
};
|
|
405
|
+
const releaseStatusFor = (dryRun, blockedReasons) => {
|
|
406
|
+
if (blockedReasons.length > 0)
|
|
407
|
+
return "blocked";
|
|
408
|
+
return dryRun ? "planned" : "created";
|
|
409
|
+
};
|
|
410
|
+
const buildGeneratedArtifactPayload = (input) => ({
|
|
411
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
412
|
+
artifactKind: "candidate_release",
|
|
413
|
+
generatedBy: "codali improve build-release",
|
|
414
|
+
generatedAt: input.createdAt,
|
|
415
|
+
candidateId: input.candidateId,
|
|
416
|
+
sourceExportIds: input.sourceExportIds,
|
|
417
|
+
sourceSchemaVersions: {
|
|
418
|
+
candidateRelease: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
419
|
+
patchCandidate: CODALI_PATCH_CANDIDATE_SCHEMA_VERSION,
|
|
420
|
+
storageManifest: input.inspection.manifest.schemaVersion,
|
|
421
|
+
},
|
|
422
|
+
workspace: {
|
|
423
|
+
workspaceId: input.workspaceId,
|
|
424
|
+
branchName: input.branchName,
|
|
425
|
+
discardable: true,
|
|
426
|
+
},
|
|
427
|
+
sourceManifest: {
|
|
428
|
+
exportId: input.inspection.exportId,
|
|
429
|
+
manifestId: input.inspection.manifest.manifestId,
|
|
430
|
+
manifestPath: input.inspection.manifestPath,
|
|
431
|
+
exportKind: input.inspection.manifest.exportKind,
|
|
432
|
+
checksum: input.inspection.manifest.checksum,
|
|
433
|
+
recordCount: input.inspection.manifest.recordCount,
|
|
434
|
+
privacySummary: privacySummaryFor(input.inspection.manifest.privacySummary),
|
|
435
|
+
},
|
|
436
|
+
changedArtifactClasses: input.changedArtifactClasses,
|
|
437
|
+
proposals: input.proposals,
|
|
438
|
+
});
|
|
439
|
+
const privacySummaryFor = (value) => {
|
|
440
|
+
const record = isRecord(value) ? value : undefined;
|
|
441
|
+
if (!record)
|
|
442
|
+
return { status: "not_provided" };
|
|
443
|
+
const output = {
|
|
444
|
+
status: "provided",
|
|
445
|
+
};
|
|
446
|
+
if (typeof record.recordCount === "number")
|
|
447
|
+
output.recordCount = record.recordCount;
|
|
448
|
+
if (typeof record.containsCustomerData === "boolean") {
|
|
449
|
+
output.containsCustomerData = record.containsCustomerData;
|
|
450
|
+
}
|
|
451
|
+
if (typeof record.containsSecrets === "boolean")
|
|
452
|
+
output.containsSecrets = record.containsSecrets;
|
|
453
|
+
if (typeof record.exportAllowedCount === "number") {
|
|
454
|
+
output.exportAllowedCount = record.exportAllowedCount;
|
|
455
|
+
}
|
|
456
|
+
if (typeof record.trainingAllowedCount === "number") {
|
|
457
|
+
output.trainingAllowedCount = record.trainingAllowedCount;
|
|
458
|
+
}
|
|
459
|
+
const policyTags = stringArray(record.policyTags);
|
|
460
|
+
if (policyTags.length)
|
|
461
|
+
output.policyTags = policyTags;
|
|
462
|
+
return output;
|
|
463
|
+
};
|
|
464
|
+
const evalDeltasFor = (value) => {
|
|
465
|
+
const record = isRecord(value) ? value : undefined;
|
|
466
|
+
const evalDeltas = nestedRecord(record, ["evalDeltas"]) ??
|
|
467
|
+
nestedRecord(record, ["metadata", "evalDeltas"]) ??
|
|
468
|
+
nestedRecord(record, ["release", "metadata", "evalDeltas"]);
|
|
469
|
+
if (!evalDeltas) {
|
|
470
|
+
return {
|
|
471
|
+
status: "not_provided",
|
|
472
|
+
summary: "No eval delta evidence was attached to this release candidate.",
|
|
473
|
+
metrics: {},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const metrics = Object.fromEntries(Object.entries(evalDeltas)
|
|
477
|
+
.filter(([, item]) => typeof item === "number" && Number.isFinite(item)));
|
|
478
|
+
return {
|
|
479
|
+
status: Object.keys(metrics).length ? "provided" : "not_provided",
|
|
480
|
+
summary: Object.keys(metrics).length
|
|
481
|
+
? "Eval delta metrics are present as numeric aggregate values."
|
|
482
|
+
: "Eval delta evidence was present but did not include numeric aggregate metrics.",
|
|
483
|
+
metrics,
|
|
484
|
+
};
|
|
485
|
+
};
|
|
486
|
+
const changedArtifactClassesFor = (proposals) => uniqueSortedStrings(proposals.map((proposal) => artifactClassForProposal(proposal.artifact)));
|
|
487
|
+
const changedArtifactClassesFromRecord = (record) => {
|
|
488
|
+
const proposals = record?.proposals ??
|
|
489
|
+
nestedRecord(record, ["release", "metadata"])?.proposalSummary ??
|
|
490
|
+
nestedRecord(record, ["metadata"])?.proposalSummary;
|
|
491
|
+
const artifacts = [];
|
|
492
|
+
if (Array.isArray(proposals)) {
|
|
493
|
+
for (const proposal of proposals) {
|
|
494
|
+
if (isRecord(proposal) && typeof proposal.artifact === "string") {
|
|
495
|
+
artifacts.push(artifactClassForProposal(proposal.artifact));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
artifacts.push(...stringArray(record?.changedArtifactClasses));
|
|
500
|
+
return uniqueSortedStrings(artifacts);
|
|
501
|
+
};
|
|
502
|
+
const sourceExportIdsFromRecord = (record) => uniqueSortedStrings([
|
|
503
|
+
...stringArray(record?.sourceExportIds),
|
|
504
|
+
...stringArray(nestedRecord(record, ["metadata"])?.sourceExportIds),
|
|
505
|
+
...stringArray(nestedRecord(record, ["release", "metadata"])?.sourceExportIds),
|
|
506
|
+
typeof nestedRecord(record, ["sourceManifest"])?.exportId === "string"
|
|
507
|
+
? nestedRecord(record, ["sourceManifest"])?.exportId
|
|
508
|
+
: undefined,
|
|
509
|
+
typeof nestedRecord(record, ["sourceManifest"])?.manifestId === "string"
|
|
510
|
+
? nestedRecord(record, ["sourceManifest"])?.manifestId
|
|
511
|
+
: undefined,
|
|
512
|
+
]);
|
|
513
|
+
const privacySummaryFromRecord = (record) => {
|
|
514
|
+
const sourceManifest = nestedRecord(record, ["sourceManifest"]);
|
|
515
|
+
const candidate = record?.privacySummary ??
|
|
516
|
+
sourceManifest?.privacySummary ??
|
|
517
|
+
nestedRecord(record, ["metadata"])?.privacySummary ??
|
|
518
|
+
nestedRecord(record, ["release", "metadata"])?.privacySummary;
|
|
519
|
+
return privacySummaryFor(candidate);
|
|
520
|
+
};
|
|
521
|
+
const buildReleasePlan = (input) => {
|
|
522
|
+
const futureTag = `v${input.plannedVersion}`;
|
|
523
|
+
const storageServiceReleaseId = stableId("storage-service-release", {
|
|
524
|
+
candidateId: input.candidateId,
|
|
525
|
+
releaseId: input.releaseId,
|
|
526
|
+
futureTag,
|
|
527
|
+
sourceExportIds: input.sourceExportIds,
|
|
528
|
+
});
|
|
529
|
+
const packageVersionsMatch = input.packageVersionTargets.every((target) => target.plannedVersion === input.plannedVersion);
|
|
530
|
+
const rollbackCommands = [
|
|
531
|
+
`git tag -d ${futureTag}`,
|
|
532
|
+
`git branch -D ${input.branchName}`,
|
|
533
|
+
`git restore ${input.packageVersionTargets.map((target) => target.relativePath).join(" ")}`,
|
|
534
|
+
];
|
|
535
|
+
const gates = [
|
|
536
|
+
{
|
|
537
|
+
gateId: "package_versions_match_future_tag",
|
|
538
|
+
status: packageVersionsMatch ? "passed" : "blocked",
|
|
539
|
+
reasons: packageVersionsMatch ? [] : ["planned_package_version_mismatch"],
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
gateId: "storage_service_release_id_present",
|
|
543
|
+
status: storageServiceReleaseId ? "passed" : "blocked",
|
|
544
|
+
reasons: storageServiceReleaseId ? [] : ["storage_service_release_id_missing"],
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
gateId: "raw_customer_data_absent",
|
|
548
|
+
status: "passed",
|
|
549
|
+
reasons: [],
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
gateId: "dry_run_no_file_changes",
|
|
553
|
+
status: input.dryRun ? "passed" : "skipped",
|
|
554
|
+
reasons: input.dryRun ? [] : ["non_dry_run_writes_guarded_by_write_plan"],
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
gateId: "write_plan_clear",
|
|
558
|
+
status: input.blockedReasons.length ? "blocked" : "passed",
|
|
559
|
+
reasons: input.blockedReasons,
|
|
560
|
+
},
|
|
561
|
+
];
|
|
562
|
+
const changelog = {
|
|
563
|
+
sourceExportIds: input.sourceExportIds,
|
|
564
|
+
changedArtifactClasses: input.changedArtifactClasses,
|
|
565
|
+
evalDeltas: input.evalDeltas,
|
|
566
|
+
privacySummary: input.privacySummary,
|
|
567
|
+
rollback: rollbackCommands,
|
|
568
|
+
rawCustomerDataIncluded: false,
|
|
569
|
+
notes: [
|
|
570
|
+
`Release ${futureTag} is planned from candidate ${input.candidateId}.`,
|
|
571
|
+
`Source export ids: ${input.sourceExportIds.length ? input.sourceExportIds.join(", ") : "not_provided"}.`,
|
|
572
|
+
`Changed artifact classes: ${input.changedArtifactClasses.length ? input.changedArtifactClasses.join(", ") : "not_provided"}.`,
|
|
573
|
+
`Eval deltas: ${input.evalDeltas.summary}`,
|
|
574
|
+
input.privacySummary.status === "provided"
|
|
575
|
+
? `Privacy summary: records=${input.privacySummary.recordCount ?? "unknown"} customerData=${String(input.privacySummary.containsCustomerData ?? "unknown")} secrets=${String(input.privacySummary.containsSecrets ?? "unknown")}.`
|
|
576
|
+
: "Privacy summary: not_provided.",
|
|
577
|
+
`Rollback: delete ${futureTag}, delete branch ${input.branchName}, and restore planned package version files.`,
|
|
578
|
+
],
|
|
579
|
+
};
|
|
580
|
+
return {
|
|
581
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_PLAN_SCHEMA_VERSION,
|
|
582
|
+
dryRun: input.dryRun,
|
|
583
|
+
candidateId: input.candidateId,
|
|
584
|
+
releaseId: input.releaseId,
|
|
585
|
+
storageServiceReleaseId,
|
|
586
|
+
semverBump: input.semverBump,
|
|
587
|
+
version: input.plannedVersion,
|
|
588
|
+
futureTag,
|
|
589
|
+
branch: {
|
|
590
|
+
name: input.branchName,
|
|
591
|
+
discardable: true,
|
|
592
|
+
},
|
|
593
|
+
commit: {
|
|
594
|
+
message: `chore(release): plan ${futureTag}`,
|
|
595
|
+
files: input.changedFiles,
|
|
596
|
+
rawCustomerDataIncluded: false,
|
|
597
|
+
},
|
|
598
|
+
tag: {
|
|
599
|
+
name: futureTag,
|
|
600
|
+
message: `mcoda ${futureTag}`,
|
|
601
|
+
matchesPackageVersions: packageVersionsMatch,
|
|
602
|
+
rawCustomerDataIncluded: false,
|
|
603
|
+
},
|
|
604
|
+
gates,
|
|
605
|
+
rollback: {
|
|
606
|
+
commands: rollbackCommands,
|
|
607
|
+
packageVersions: input.packageVersionTargets,
|
|
608
|
+
},
|
|
609
|
+
packageVersions: {
|
|
610
|
+
currentVersion: input.currentVersion,
|
|
611
|
+
plannedVersion: input.plannedVersion,
|
|
612
|
+
futureTag,
|
|
613
|
+
matchesFutureTag: packageVersionsMatch && futureTag === `v${input.plannedVersion}`,
|
|
614
|
+
targets: input.packageVersionTargets,
|
|
615
|
+
},
|
|
616
|
+
changelog,
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
const pathExists = async (candidatePath) => {
|
|
620
|
+
try {
|
|
621
|
+
await lstat(candidatePath);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (isNotFoundError(error))
|
|
626
|
+
return false;
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const candidateDirectPath = async (repoRoot, candidateId, explicitPath) => {
|
|
631
|
+
const value = explicitPath ?? candidateId;
|
|
632
|
+
if (!value.trim())
|
|
633
|
+
return undefined;
|
|
634
|
+
const resolved = value.startsWith("file://")
|
|
635
|
+
? fileURLToPath(value)
|
|
636
|
+
: path.isAbsolute(value)
|
|
637
|
+
? path.resolve(value)
|
|
638
|
+
: path.resolve(repoRoot, value);
|
|
639
|
+
return (await pathExists(resolved)) ? resolved : undefined;
|
|
640
|
+
};
|
|
641
|
+
const candidateRecordMatches = (candidateId, record) => {
|
|
642
|
+
if (record.candidateId === candidateId)
|
|
643
|
+
return true;
|
|
644
|
+
if (nestedRecord(record, ["release"])?.candidateId === candidateId)
|
|
645
|
+
return true;
|
|
646
|
+
if (nestedRecord(record, ["releasePlan"])?.candidateId === candidateId)
|
|
647
|
+
return true;
|
|
648
|
+
if (nestedRecord(record, ["metadata", "releasePlan"])?.candidateId === candidateId)
|
|
649
|
+
return true;
|
|
650
|
+
if (nestedRecord(record, ["workspace"])?.workspaceId === candidateId)
|
|
651
|
+
return true;
|
|
652
|
+
if (nestedRecord(record, ["sourceManifest"])?.manifestId === candidateId)
|
|
653
|
+
return true;
|
|
654
|
+
if (nestedRecord(record, ["sourceManifest"])?.exportId === candidateId)
|
|
655
|
+
return true;
|
|
656
|
+
return false;
|
|
657
|
+
};
|
|
658
|
+
const findCandidateReleaseRecord = async (input) => {
|
|
659
|
+
const directPath = await candidateDirectPath(input.repoRoot, input.candidateId, input.candidatePath);
|
|
660
|
+
if (directPath) {
|
|
661
|
+
return {
|
|
662
|
+
path: directPath,
|
|
663
|
+
record: await readJsonRecord(directPath),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const directories = input.candidateDirectories?.length
|
|
667
|
+
? input.candidateDirectories
|
|
668
|
+
: DEFAULT_CODALI_CANDIDATE_RELEASE_DIRECTORIES;
|
|
669
|
+
let inspected = 0;
|
|
670
|
+
for (const directory of directories) {
|
|
671
|
+
const root = path.isAbsolute(directory)
|
|
672
|
+
? path.resolve(directory)
|
|
673
|
+
: path.resolve(input.repoRoot, directory);
|
|
674
|
+
if (!(await pathExists(root)))
|
|
675
|
+
continue;
|
|
676
|
+
const stack = [root];
|
|
677
|
+
while (stack.length) {
|
|
678
|
+
const current = stack.pop();
|
|
679
|
+
if (!current)
|
|
680
|
+
continue;
|
|
681
|
+
const entries = (await readdir(current, { withFileTypes: true }))
|
|
682
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
const entryPath = path.join(current, entry.name);
|
|
685
|
+
if (entry.isDirectory()) {
|
|
686
|
+
stack.push(entryPath);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!entry.isFile() || !entry.name.endsWith(".json"))
|
|
690
|
+
continue;
|
|
691
|
+
inspected += 1;
|
|
692
|
+
if (inspected > 5000) {
|
|
693
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_SEARCH_LIMIT", "Candidate release search inspected more than 5000 JSON files.");
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
const record = await readJsonRecord(entryPath);
|
|
697
|
+
if (candidateRecordMatches(input.candidateId, record)) {
|
|
698
|
+
return { path: entryPath, record };
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// Ignore unrelated JSON files in local object stores.
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return undefined;
|
|
708
|
+
};
|
|
709
|
+
const buildWritePlan = (input) => {
|
|
710
|
+
const targetSummaries = input.targets.map((target) => ({
|
|
711
|
+
relativePath: target.relativePath,
|
|
712
|
+
approved: isApprovedRelativePath(target.relativePath, input.normalizedApprovedPaths),
|
|
713
|
+
dirty: input.dirtySummary.targetDirtyFiles.includes(target.relativePath),
|
|
714
|
+
contentHash: target.artifact.contentHash,
|
|
715
|
+
byteSize: target.artifact.byteSize,
|
|
716
|
+
}));
|
|
717
|
+
const blockedReasons = Array.from(new Set([
|
|
718
|
+
...targetSummaries
|
|
719
|
+
.filter((target) => !target.approved)
|
|
720
|
+
.map(() => "target_path_not_approved"),
|
|
721
|
+
...targetSummaries
|
|
722
|
+
.filter((target) => target.dirty)
|
|
723
|
+
.map(() => "target_file_dirty"),
|
|
724
|
+
!input.dryRun && input.dirtySummary.unrelatedDirtyFileCount > 0
|
|
725
|
+
? "unrelated_dirty_worktree"
|
|
726
|
+
: undefined,
|
|
727
|
+
!input.dryRun && input.dirtySummary.status === "unavailable"
|
|
728
|
+
? "git_status_unavailable"
|
|
729
|
+
: undefined,
|
|
730
|
+
].filter((item) => Boolean(item))));
|
|
731
|
+
return {
|
|
732
|
+
status: blockedReasons.length > 0
|
|
733
|
+
? "blocked"
|
|
734
|
+
: input.dryRun
|
|
735
|
+
? "dry_run"
|
|
736
|
+
: "ready",
|
|
737
|
+
dryRun: input.dryRun,
|
|
738
|
+
repoRoot: input.repoRoot,
|
|
739
|
+
approvedPaths: [...input.approvedPaths],
|
|
740
|
+
targets: targetSummaries,
|
|
741
|
+
blockedReasons,
|
|
742
|
+
};
|
|
743
|
+
};
|
|
744
|
+
const applyWritePlan = async (input) => {
|
|
745
|
+
await assertWriteTargetsInsideRepo({
|
|
746
|
+
repoRoot: input.repoRoot,
|
|
747
|
+
targets: input.targets,
|
|
748
|
+
});
|
|
749
|
+
const branch = input.commandRunner("git", ["switch", "-c", input.branchName], {
|
|
750
|
+
cwd: input.repoRoot,
|
|
751
|
+
});
|
|
752
|
+
if (branch.exitCode !== 0 || branch.error) {
|
|
753
|
+
throw new CodaliCandidateReleaseBuilderError("CODALI_CANDIDATE_RELEASE_BRANCH_CREATE_FAILED", branch.error ?? branch.stderr ?? "git switch failed");
|
|
754
|
+
}
|
|
755
|
+
const written = [];
|
|
756
|
+
for (const target of input.targets) {
|
|
757
|
+
const absolute = path.join(input.repoRoot, target.relativePath);
|
|
758
|
+
await mkdir(path.dirname(absolute), { recursive: true });
|
|
759
|
+
await writeFile(absolute, target.content, "utf8");
|
|
760
|
+
written.push(target.relativePath);
|
|
761
|
+
}
|
|
762
|
+
return written;
|
|
763
|
+
};
|
|
764
|
+
export const buildCodaliCandidateRelease = async (input) => {
|
|
765
|
+
const dryRun = input.dryRun ?? true;
|
|
766
|
+
const repoRoot = path.resolve(input.repoRoot ?? process.cwd());
|
|
767
|
+
const now = input.now ?? (() => new Date());
|
|
768
|
+
const artifacts = input.artifacts?.length
|
|
769
|
+
? [...input.artifacts]
|
|
770
|
+
: [...CODALI_PATCH_PROPOSAL_ARTIFACTS];
|
|
771
|
+
const uniqueArtifacts = Array.from(new Set(artifacts)).sort();
|
|
772
|
+
const candidateDate = sanitizeBranchSegment(input.candidateDate ?? dateFromInspection(input.inspection, now));
|
|
773
|
+
const runId = sanitizeBranchSegment(input.runId ?? stableId("run", {
|
|
774
|
+
manifestId: input.inspection.manifest.manifestId,
|
|
775
|
+
checksum: input.inspection.manifest.checksum,
|
|
776
|
+
artifacts: uniqueArtifacts,
|
|
777
|
+
}));
|
|
778
|
+
const workspaceId = `${candidateDate}-${runId}`;
|
|
779
|
+
const branchName = `codali/auto-improve/${workspaceId}`;
|
|
780
|
+
const createdAt = input.inspection.manifest.createdAt ?? now().toISOString();
|
|
781
|
+
const sourceExportIds = Array.from(new Set([
|
|
782
|
+
input.inspection.exportId,
|
|
783
|
+
input.inspection.manifest.manifestId,
|
|
784
|
+
].filter(Boolean))).sort();
|
|
785
|
+
const proposals = uniqueArtifacts.map((artifact) => buildCodaliPatchCandidateBundle({
|
|
786
|
+
inspection: input.inspection,
|
|
787
|
+
artifact,
|
|
788
|
+
}));
|
|
789
|
+
const changedArtifactClasses = changedArtifactClassesFor(proposals);
|
|
790
|
+
const semverBump = semverBumpForArtifactClasses(changedArtifactClasses);
|
|
791
|
+
const defaultTarget = `.codali/improvement/candidates/${workspaceId}/candidate-release.json`;
|
|
792
|
+
const targetPath = normalizeRepoRelativePath(repoRoot, input.outputPath ?? defaultTarget);
|
|
793
|
+
const candidateId = input.candidateId ?? stableId("candidate-release", {
|
|
794
|
+
workspaceId,
|
|
795
|
+
targetPath,
|
|
796
|
+
sourceExportIds,
|
|
797
|
+
changedArtifactClasses,
|
|
798
|
+
});
|
|
799
|
+
const packagePlan = await packageVersionTargetsFor({
|
|
800
|
+
repoRoot,
|
|
801
|
+
semverBump,
|
|
802
|
+
sourceExportIds,
|
|
803
|
+
storageManifestVersion: input.inspection.manifest.schemaVersion,
|
|
804
|
+
});
|
|
805
|
+
const approvedPaths = input.approvedPaths?.length
|
|
806
|
+
? [...input.approvedPaths]
|
|
807
|
+
: [...DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS];
|
|
808
|
+
const effectiveApprovedPaths = dryRun
|
|
809
|
+
? approvedPaths
|
|
810
|
+
: uniqueSortedStrings([
|
|
811
|
+
...approvedPaths,
|
|
812
|
+
...packagePlan.writeTargets.map((target) => target.relativePath),
|
|
813
|
+
]);
|
|
814
|
+
const normalizedApprovedPaths = effectiveApprovedPaths.map((approvedPath) => normalizeApprovedPath(repoRoot, approvedPath));
|
|
815
|
+
const artifactPayload = buildGeneratedArtifactPayload({
|
|
816
|
+
inspection: input.inspection,
|
|
817
|
+
proposals,
|
|
818
|
+
workspaceId,
|
|
819
|
+
branchName,
|
|
820
|
+
candidateId,
|
|
821
|
+
createdAt,
|
|
822
|
+
sourceExportIds,
|
|
823
|
+
changedArtifactClasses,
|
|
824
|
+
});
|
|
825
|
+
const artifactContent = `${JSON.stringify(artifactPayload, null, 2)}\n`;
|
|
826
|
+
const contentHash = hashContent(artifactContent);
|
|
827
|
+
const generatedArtifact = {
|
|
828
|
+
artifactId: `candidate-release-artifact-${contentHash.slice(0, 16)}`,
|
|
829
|
+
artifactKind: "candidate_release",
|
|
830
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
831
|
+
sourceExportIds,
|
|
832
|
+
sourceSchemaVersions: {
|
|
833
|
+
candidateRelease: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
834
|
+
patchCandidate: CODALI_PATCH_CANDIDATE_SCHEMA_VERSION,
|
|
835
|
+
storageManifest: input.inspection.manifest.schemaVersion,
|
|
836
|
+
},
|
|
837
|
+
relativePath: targetPath,
|
|
838
|
+
contentHash,
|
|
839
|
+
byteSize: Buffer.byteLength(artifactContent, "utf8"),
|
|
840
|
+
};
|
|
841
|
+
const targets = [{
|
|
842
|
+
relativePath: targetPath,
|
|
843
|
+
content: artifactContent,
|
|
844
|
+
artifact: generatedArtifact,
|
|
845
|
+
}];
|
|
846
|
+
const writeTargets = dryRun
|
|
847
|
+
? targets
|
|
848
|
+
: [...targets, ...packagePlan.writeTargets];
|
|
849
|
+
const dirtyRead = input.dirtyEntries
|
|
850
|
+
? { entries: [...input.dirtyEntries] }
|
|
851
|
+
: readDirtyEntries(repoRoot, input.commandRunner ?? defaultCommandRunner);
|
|
852
|
+
const dirtySummary = dirtySummaryFor(dirtyRead.entries, writeTargets.map((target) => target.relativePath), dirtyRead.warning);
|
|
853
|
+
const writePlan = buildWritePlan({
|
|
854
|
+
dryRun,
|
|
855
|
+
repoRoot,
|
|
856
|
+
approvedPaths: effectiveApprovedPaths,
|
|
857
|
+
normalizedApprovedPaths,
|
|
858
|
+
targets: writeTargets,
|
|
859
|
+
dirtySummary,
|
|
860
|
+
});
|
|
861
|
+
const patchOutput = targets
|
|
862
|
+
.map((target) => unifiedDiffForNewFile(target.relativePath, target.content))
|
|
863
|
+
.join("\n");
|
|
864
|
+
let effectiveWritePlan = writePlan;
|
|
865
|
+
const commandRunner = input.commandRunner ?? defaultCommandRunner;
|
|
866
|
+
if (!dryRun && writePlan.status === "ready") {
|
|
867
|
+
const written = await applyWritePlan({
|
|
868
|
+
repoRoot,
|
|
869
|
+
branchName,
|
|
870
|
+
targets: writeTargets,
|
|
871
|
+
commandRunner,
|
|
872
|
+
});
|
|
873
|
+
effectiveWritePlan = {
|
|
874
|
+
...writePlan,
|
|
875
|
+
status: "written",
|
|
876
|
+
targets: writePlan.targets.map((target) => ({
|
|
877
|
+
...target,
|
|
878
|
+
dirty: !written.includes(target.relativePath) && target.dirty,
|
|
879
|
+
})),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const blockedReasons = [...effectiveWritePlan.blockedReasons];
|
|
883
|
+
const releaseStatus = releaseStatusFor(dryRun, blockedReasons);
|
|
884
|
+
const allGeneratedArtifacts = [
|
|
885
|
+
generatedArtifact,
|
|
886
|
+
...packagePlan.writeTargets.map((target) => target.artifact),
|
|
887
|
+
];
|
|
888
|
+
const releaseId = stableId("release", {
|
|
889
|
+
candidateId,
|
|
890
|
+
branchName,
|
|
891
|
+
releaseStatus,
|
|
892
|
+
artifactIds: allGeneratedArtifacts.map((artifact) => artifact.artifactId),
|
|
893
|
+
});
|
|
894
|
+
const candidateWorkspace = {
|
|
895
|
+
workspaceId,
|
|
896
|
+
branchName,
|
|
897
|
+
branchPrefix: "codali/auto-improve",
|
|
898
|
+
candidateDate,
|
|
899
|
+
runId,
|
|
900
|
+
discardable: true,
|
|
901
|
+
dryRunOnly: dryRun,
|
|
902
|
+
defaultApprovedPaths: DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS,
|
|
903
|
+
approvedPaths: effectiveApprovedPaths,
|
|
904
|
+
targetDirectory: path.posix.dirname(targetPath),
|
|
905
|
+
patchOutput,
|
|
906
|
+
discardInstructions: [
|
|
907
|
+
`git switch -`,
|
|
908
|
+
`git branch -D ${branchName}`,
|
|
909
|
+
],
|
|
910
|
+
};
|
|
911
|
+
const releaseLevel = input.releaseLevel ?? 2;
|
|
912
|
+
const scope = input.scope ?? {
|
|
913
|
+
tenantHash: "local_tenant",
|
|
914
|
+
productId: "local_product",
|
|
915
|
+
};
|
|
916
|
+
const releasePlan = buildReleasePlan({
|
|
917
|
+
dryRun,
|
|
918
|
+
candidateId,
|
|
919
|
+
releaseId,
|
|
920
|
+
branchName,
|
|
921
|
+
sourceExportIds,
|
|
922
|
+
changedArtifactClasses,
|
|
923
|
+
semverBump,
|
|
924
|
+
currentVersion: packagePlan.currentVersion,
|
|
925
|
+
plannedVersion: packagePlan.plannedVersion,
|
|
926
|
+
packageVersionTargets: packagePlan.targets,
|
|
927
|
+
changedFiles: [
|
|
928
|
+
targetPath,
|
|
929
|
+
...packagePlan.targets.map((target) => target.relativePath),
|
|
930
|
+
],
|
|
931
|
+
writePlan: effectiveWritePlan,
|
|
932
|
+
blockedReasons,
|
|
933
|
+
privacySummary: privacySummaryFor(input.inspection.manifest.privacySummary),
|
|
934
|
+
evalDeltas: evalDeltasFor(undefined),
|
|
935
|
+
});
|
|
936
|
+
const release = {
|
|
937
|
+
schemaVersion: CODALI_IMPROVEMENT_CONTRACT_SCHEMA_VERSION,
|
|
938
|
+
releaseId,
|
|
939
|
+
candidateId,
|
|
940
|
+
scope,
|
|
941
|
+
releaseLevel,
|
|
942
|
+
status: releaseStatus,
|
|
943
|
+
artifactIds: allGeneratedArtifacts.map((artifact) => artifact.artifactId),
|
|
944
|
+
createdAt,
|
|
945
|
+
tagName: releasePlan.futureTag,
|
|
946
|
+
packageName: "mcoda",
|
|
947
|
+
version: releasePlan.version,
|
|
948
|
+
...(blockedReasons.length ? { blockedReasons } : {}),
|
|
949
|
+
metadata: {
|
|
950
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
951
|
+
source: "codali improve build-release",
|
|
952
|
+
sourceExportIds,
|
|
953
|
+
sourceSchemaVersions: generatedArtifact.sourceSchemaVersions,
|
|
954
|
+
candidateWorkspace,
|
|
955
|
+
writePlan: effectiveWritePlan,
|
|
956
|
+
dirtyWorktree: dirtySummary,
|
|
957
|
+
generatedArtifacts: allGeneratedArtifacts,
|
|
958
|
+
patchOutput,
|
|
959
|
+
releasePlan,
|
|
960
|
+
proposalSummary: proposals.map((proposal) => ({
|
|
961
|
+
artifact: proposal.artifact,
|
|
962
|
+
schemaVersion: proposal.schemaVersion,
|
|
963
|
+
sourceExportId: proposal.source.exportId,
|
|
964
|
+
manifestId: proposal.source.manifestId,
|
|
965
|
+
candidateIds: proposal.candidates.map((candidate) => candidate.candidateId),
|
|
966
|
+
candidateStatuses: proposal.candidates.map((candidate) => candidate.status),
|
|
967
|
+
blockedReasons: proposal.candidates.flatMap((candidate) => candidate.blockedReasons),
|
|
968
|
+
operationCount: proposal.patchPlan.operations.length,
|
|
969
|
+
})),
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
return {
|
|
973
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
974
|
+
dryRun,
|
|
975
|
+
release,
|
|
976
|
+
releasePlan,
|
|
977
|
+
candidateWorkspace,
|
|
978
|
+
writePlan: effectiveWritePlan,
|
|
979
|
+
dirtyWorktree: dirtySummary,
|
|
980
|
+
generatedArtifacts: allGeneratedArtifacts,
|
|
981
|
+
proposals,
|
|
982
|
+
patchOutput,
|
|
983
|
+
blockedReasons,
|
|
984
|
+
};
|
|
985
|
+
};
|
|
986
|
+
const releaseLevelFromRecord = (record, fallback) => {
|
|
987
|
+
const value = nestedRecord(record, ["release"])?.releaseLevel;
|
|
988
|
+
return value === 0 || value === 1 || value === 2 || value === 3 || value === 4
|
|
989
|
+
? value
|
|
990
|
+
: fallback;
|
|
991
|
+
};
|
|
992
|
+
const branchNameFromRecord = (input) => {
|
|
993
|
+
const workspaceBranch = nestedRecord(input.record, ["workspace"])?.branchName;
|
|
994
|
+
const metadataBranch = nestedRecord(input.record, ["metadata", "candidateWorkspace"])?.branchName ??
|
|
995
|
+
nestedRecord(input.record, ["release", "metadata", "candidateWorkspace"])?.branchName;
|
|
996
|
+
if (typeof workspaceBranch === "string" && workspaceBranch.trim())
|
|
997
|
+
return workspaceBranch;
|
|
998
|
+
if (typeof metadataBranch === "string" && metadataBranch.trim())
|
|
999
|
+
return metadataBranch;
|
|
1000
|
+
return `codali/auto-improve/${input.candidateDate}-${input.runId}`;
|
|
1001
|
+
};
|
|
1002
|
+
const storageManifestVersionFromRecord = (record) => {
|
|
1003
|
+
const sourceSchemaVersions = nestedRecord(record, ["sourceSchemaVersions"]) ??
|
|
1004
|
+
nestedRecord(record, ["metadata", "sourceSchemaVersions"]) ??
|
|
1005
|
+
nestedRecord(record, ["release", "metadata", "sourceSchemaVersions"]);
|
|
1006
|
+
const sourceManifest = nestedRecord(record, ["sourceManifest"]);
|
|
1007
|
+
return typeof sourceSchemaVersions?.storageManifest === "string"
|
|
1008
|
+
? sourceSchemaVersions.storageManifest
|
|
1009
|
+
: typeof sourceManifest?.schemaVersion === "string"
|
|
1010
|
+
? sourceManifest.schemaVersion
|
|
1011
|
+
: "not_provided";
|
|
1012
|
+
};
|
|
1013
|
+
export const buildCodaliCandidateReleasePlan = async (input) => {
|
|
1014
|
+
const dryRun = input.dryRun ?? true;
|
|
1015
|
+
const repoRoot = path.resolve(input.repoRoot ?? process.cwd());
|
|
1016
|
+
const now = input.now ?? (() => new Date());
|
|
1017
|
+
const found = await findCandidateReleaseRecord({
|
|
1018
|
+
repoRoot,
|
|
1019
|
+
candidateId: input.candidateId,
|
|
1020
|
+
candidatePath: input.candidatePath,
|
|
1021
|
+
candidateDirectories: input.candidateDirectories,
|
|
1022
|
+
});
|
|
1023
|
+
const record = found?.record;
|
|
1024
|
+
const candidateDate = sanitizeBranchSegment(input.candidateDate ?? now().toISOString().slice(0, 10));
|
|
1025
|
+
const runId = sanitizeBranchSegment(input.runId ?? input.candidateId);
|
|
1026
|
+
const branchName = branchNameFromRecord({ record, candidateDate, runId });
|
|
1027
|
+
const sourceExportIds = sourceExportIdsFromRecord(record);
|
|
1028
|
+
const changedArtifactClasses = changedArtifactClassesFromRecord(record);
|
|
1029
|
+
const semverBump = semverBumpForArtifactClasses(changedArtifactClasses);
|
|
1030
|
+
const packagePlan = await packageVersionTargetsFor({
|
|
1031
|
+
repoRoot,
|
|
1032
|
+
semverBump,
|
|
1033
|
+
sourceExportIds,
|
|
1034
|
+
storageManifestVersion: storageManifestVersionFromRecord(record),
|
|
1035
|
+
});
|
|
1036
|
+
const writeTargets = dryRun || !found ? [] : packagePlan.writeTargets;
|
|
1037
|
+
const approvedPaths = dryRun
|
|
1038
|
+
? [...DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS]
|
|
1039
|
+
: uniqueSortedStrings([
|
|
1040
|
+
...DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS,
|
|
1041
|
+
...packagePlan.writeTargets.map((target) => target.relativePath),
|
|
1042
|
+
]);
|
|
1043
|
+
const normalizedApprovedPaths = approvedPaths.map((approvedPath) => normalizeApprovedPath(repoRoot, approvedPath));
|
|
1044
|
+
const dirtyRead = input.dirtyEntries
|
|
1045
|
+
? { entries: [...input.dirtyEntries] }
|
|
1046
|
+
: readDirtyEntries(repoRoot, input.commandRunner ?? defaultCommandRunner);
|
|
1047
|
+
const dirtySummary = dirtySummaryFor(dirtyRead.entries, writeTargets.map((target) => target.relativePath), dirtyRead.warning);
|
|
1048
|
+
const writePlan = buildWritePlan({
|
|
1049
|
+
dryRun,
|
|
1050
|
+
repoRoot,
|
|
1051
|
+
approvedPaths,
|
|
1052
|
+
normalizedApprovedPaths,
|
|
1053
|
+
targets: writeTargets,
|
|
1054
|
+
dirtySummary,
|
|
1055
|
+
});
|
|
1056
|
+
const missingReasons = found ? [] : ["candidate_artifact_not_found"];
|
|
1057
|
+
let effectiveWritePlan = missingReasons.length
|
|
1058
|
+
? {
|
|
1059
|
+
...writePlan,
|
|
1060
|
+
status: "blocked",
|
|
1061
|
+
blockedReasons: [...writePlan.blockedReasons, ...missingReasons],
|
|
1062
|
+
}
|
|
1063
|
+
: writePlan;
|
|
1064
|
+
const commandRunner = input.commandRunner ?? defaultCommandRunner;
|
|
1065
|
+
if (!dryRun && effectiveWritePlan.status === "ready") {
|
|
1066
|
+
const written = await applyWritePlan({
|
|
1067
|
+
repoRoot,
|
|
1068
|
+
branchName,
|
|
1069
|
+
targets: writeTargets,
|
|
1070
|
+
commandRunner,
|
|
1071
|
+
});
|
|
1072
|
+
effectiveWritePlan = {
|
|
1073
|
+
...effectiveWritePlan,
|
|
1074
|
+
status: "written",
|
|
1075
|
+
targets: effectiveWritePlan.targets.map((target) => ({
|
|
1076
|
+
...target,
|
|
1077
|
+
dirty: !written.includes(target.relativePath) && target.dirty,
|
|
1078
|
+
})),
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
const blockedReasons = [...effectiveWritePlan.blockedReasons];
|
|
1082
|
+
const releaseStatus = releaseStatusFor(dryRun, blockedReasons);
|
|
1083
|
+
const releaseId = stableId("release", {
|
|
1084
|
+
candidateId: input.candidateId,
|
|
1085
|
+
branchName,
|
|
1086
|
+
releaseStatus,
|
|
1087
|
+
sourceExportIds,
|
|
1088
|
+
plannedVersion: packagePlan.plannedVersion,
|
|
1089
|
+
});
|
|
1090
|
+
const relativeCandidatePath = found
|
|
1091
|
+
? toPosixPath(path.relative(repoRoot, found.path))
|
|
1092
|
+
: undefined;
|
|
1093
|
+
const releasePlan = buildReleasePlan({
|
|
1094
|
+
dryRun,
|
|
1095
|
+
candidateId: input.candidateId,
|
|
1096
|
+
releaseId,
|
|
1097
|
+
branchName,
|
|
1098
|
+
sourceExportIds,
|
|
1099
|
+
changedArtifactClasses,
|
|
1100
|
+
semverBump,
|
|
1101
|
+
currentVersion: packagePlan.currentVersion,
|
|
1102
|
+
plannedVersion: packagePlan.plannedVersion,
|
|
1103
|
+
packageVersionTargets: packagePlan.targets,
|
|
1104
|
+
changedFiles: packagePlan.targets.map((target) => target.relativePath),
|
|
1105
|
+
writePlan: effectiveWritePlan,
|
|
1106
|
+
blockedReasons,
|
|
1107
|
+
privacySummary: privacySummaryFromRecord(record),
|
|
1108
|
+
evalDeltas: evalDeltasFor(record),
|
|
1109
|
+
});
|
|
1110
|
+
const candidateWorkspace = {
|
|
1111
|
+
workspaceId: `${candidateDate}-${runId}`,
|
|
1112
|
+
branchName,
|
|
1113
|
+
branchPrefix: "codali/auto-improve",
|
|
1114
|
+
candidateDate,
|
|
1115
|
+
runId,
|
|
1116
|
+
discardable: true,
|
|
1117
|
+
dryRunOnly: dryRun,
|
|
1118
|
+
defaultApprovedPaths: DEFAULT_CODALI_CANDIDATE_RELEASE_APPROVED_PATHS,
|
|
1119
|
+
approvedPaths,
|
|
1120
|
+
targetDirectory: relativeCandidatePath
|
|
1121
|
+
? path.posix.dirname(relativeCandidatePath)
|
|
1122
|
+
: DEFAULT_CODALI_CANDIDATE_RELEASE_DIRECTORIES[0],
|
|
1123
|
+
patchOutput: "",
|
|
1124
|
+
discardInstructions: [
|
|
1125
|
+
`git switch -`,
|
|
1126
|
+
`git branch -D ${branchName}`,
|
|
1127
|
+
],
|
|
1128
|
+
};
|
|
1129
|
+
const releaseLevel = releaseLevelFromRecord(record, input.releaseLevel ?? 2);
|
|
1130
|
+
const scope = input.scope ?? {
|
|
1131
|
+
tenantHash: "local_tenant",
|
|
1132
|
+
productId: "local_product",
|
|
1133
|
+
};
|
|
1134
|
+
const generatedArtifacts = packagePlan.writeTargets.map((target) => target.artifact);
|
|
1135
|
+
const release = {
|
|
1136
|
+
schemaVersion: CODALI_IMPROVEMENT_CONTRACT_SCHEMA_VERSION,
|
|
1137
|
+
releaseId,
|
|
1138
|
+
candidateId: input.candidateId,
|
|
1139
|
+
scope,
|
|
1140
|
+
releaseLevel,
|
|
1141
|
+
status: releaseStatus,
|
|
1142
|
+
artifactIds: generatedArtifacts.map((artifact) => artifact.artifactId),
|
|
1143
|
+
createdAt: now().toISOString(),
|
|
1144
|
+
tagName: releasePlan.futureTag,
|
|
1145
|
+
packageName: "mcoda",
|
|
1146
|
+
version: releasePlan.version,
|
|
1147
|
+
...(blockedReasons.length ? { blockedReasons } : {}),
|
|
1148
|
+
metadata: {
|
|
1149
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
1150
|
+
source: "codali improve build-release --candidate",
|
|
1151
|
+
candidateSource: found ? "candidate_file" : "missing",
|
|
1152
|
+
...(relativeCandidatePath ? { candidatePath: relativeCandidatePath } : {}),
|
|
1153
|
+
sourceExportIds,
|
|
1154
|
+
candidateWorkspace,
|
|
1155
|
+
writePlan: effectiveWritePlan,
|
|
1156
|
+
dirtyWorktree: dirtySummary,
|
|
1157
|
+
generatedArtifacts,
|
|
1158
|
+
releasePlan,
|
|
1159
|
+
changedArtifactClasses,
|
|
1160
|
+
},
|
|
1161
|
+
};
|
|
1162
|
+
return {
|
|
1163
|
+
schemaVersion: CODALI_CANDIDATE_RELEASE_SCHEMA_VERSION,
|
|
1164
|
+
dryRun,
|
|
1165
|
+
release,
|
|
1166
|
+
releasePlan,
|
|
1167
|
+
candidateWorkspace,
|
|
1168
|
+
writePlan: effectiveWritePlan,
|
|
1169
|
+
dirtyWorktree: dirtySummary,
|
|
1170
|
+
generatedArtifacts,
|
|
1171
|
+
proposals: [],
|
|
1172
|
+
patchOutput: "",
|
|
1173
|
+
blockedReasons,
|
|
1174
|
+
};
|
|
1175
|
+
};
|