@rotifer/playground 0.26.3 → 0.28.0
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 +37 -0
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/dist/cloud/capability-candidates.d.ts +87 -0
- package/dist/cloud/capability-candidates.d.ts.map +1 -0
- package/dist/cloud/capability-candidates.js +244 -0
- package/dist/cloud/capability-candidates.js.map +1 -0
- package/dist/commands/agent-create.d.ts +8 -0
- package/dist/commands/agent-create.d.ts.map +1 -1
- package/dist/commands/agent-create.js +5 -2
- package/dist/commands/agent-create.js.map +1 -1
- package/dist/commands/agent-run.d.ts.map +1 -1
- package/dist/commands/agent-run.js +23 -13
- package/dist/commands/agent-run.js.map +1 -1
- package/dist/commands/genome-import.d.ts +122 -0
- package/dist/commands/genome-import.d.ts.map +1 -0
- package/dist/commands/genome-import.js +632 -0
- package/dist/commands/genome-import.js.map +1 -0
- package/dist/commands/genome.d.ts +213 -0
- package/dist/commands/genome.d.ts.map +1 -0
- package/dist/commands/genome.js +606 -0
- package/dist/commands/genome.js.map +1 -0
- package/dist/commands/install.d.ts +54 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +148 -75
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/network.d.ts.map +1 -1
- package/dist/commands/network.js +12 -15
- package/dist/commands/network.js.map +1 -1
- package/dist/commands/publish.js +2 -2
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/vg.js +2 -2
- package/dist/commands/vg.js.map +1 -1
- package/dist/commands/wrap.d.ts.map +1 -1
- package/dist/commands/wrap.js +15 -3
- package/dist/commands/wrap.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/publish/pre-publish-check.d.ts +15 -0
- package/dist/publish/pre-publish-check.d.ts.map +1 -1
- package/dist/publish/pre-publish-check.js +17 -2
- package/dist/publish/pre-publish-check.js.map +1 -1
- package/dist/scanner/index.d.ts +1 -1
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +3 -1
- package/dist/scanner/index.js.map +1 -1
- package/dist/scanner/literals.d.ts +2 -0
- package/dist/scanner/literals.d.ts.map +1 -0
- package/dist/scanner/literals.js +293 -0
- package/dist/scanner/literals.js.map +1 -0
- package/dist/scanner/rules.d.ts +14 -0
- package/dist/scanner/rules.d.ts.map +1 -1
- package/dist/scanner/rules.js +30 -1
- package/dist/scanner/rules.js.map +1 -1
- package/dist/scanner/scanner.d.ts.map +1 -1
- package/dist/scanner/scanner.js +8 -1
- package/dist/scanner/scanner.js.map +1 -1
- package/dist/scanner/types.d.ts +7 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/utils/binding.d.ts +20 -2
- package/dist/utils/binding.d.ts.map +1 -1
- package/dist/utils/binding.js +22 -0
- package/dist/utils/binding.js.map +1 -1
- package/dist/utils/p2p-daemon.d.ts.map +1 -1
- package/dist/utils/p2p-daemon.js +6 -2
- package/dist/utils/p2p-daemon.js.map +1 -1
- package/dist/utils/rust-phenotype.d.ts +24 -0
- package/dist/utils/rust-phenotype.d.ts.map +1 -0
- package/dist/utils/rust-phenotype.js +59 -0
- package/dist/utils/rust-phenotype.js.map +1 -0
- package/genes/genesis-file-read/phenotype.json +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.genomeImportCommand = exports.GenomeImportError = exports.DEFAULT_MAX_EVAL_AGE_DAYS = exports.DEFAULT_MIN_VG = void 0;
|
|
37
|
+
exports.compositionPlan = compositionPlan;
|
|
38
|
+
exports.parseBlueprint = parseBlueprint;
|
|
39
|
+
exports.locatePinned = locatePinned;
|
|
40
|
+
exports.effectiveMinVg = effectiveMinVg;
|
|
41
|
+
exports.resolveRole = resolveRole;
|
|
42
|
+
exports.importGenome = importGenome;
|
|
43
|
+
exports.runGenomeImport = runGenomeImport;
|
|
44
|
+
const commander_1 = require("commander");
|
|
45
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
|
+
const node_fs_1 = require("node:fs");
|
|
47
|
+
const node_path_1 = require("node:path");
|
|
48
|
+
const promises_1 = require("node:readline/promises");
|
|
49
|
+
const display = __importStar(require("../utils/display.js"));
|
|
50
|
+
const config_js_1 = require("../utils/config.js");
|
|
51
|
+
const project_root_js_1 = require("../utils/project-root.js");
|
|
52
|
+
const binding_js_1 = require("../utils/binding.js");
|
|
53
|
+
const validate_gene_name_js_1 = require("../utils/validate-gene-name.js");
|
|
54
|
+
const client_js_1 = require("../cloud/client.js");
|
|
55
|
+
const capability_candidates_js_1 = require("../cloud/capability-candidates.js");
|
|
56
|
+
const install_js_1 = require("./install.js");
|
|
57
|
+
const agent_create_js_1 = require("./agent-create.js");
|
|
58
|
+
const genome_js_1 = require("./genome.js");
|
|
59
|
+
/** Recommended V_min of §5.1: the floor every capability selection must clear. */
|
|
60
|
+
exports.DEFAULT_MIN_VG = 0.7;
|
|
61
|
+
exports.DEFAULT_MAX_EVAL_AGE_DAYS = 90;
|
|
62
|
+
const PAR_MERGES = new Set(["first", "concat", "merge"]);
|
|
63
|
+
class GenomeImportError extends Error {
|
|
64
|
+
hint;
|
|
65
|
+
constructor(message, hint) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.hint = hint;
|
|
68
|
+
this.name = "GenomeImportError";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.GenomeImportError = GenomeImportError;
|
|
72
|
+
const OPS_FROM_SPEC = Object.fromEntries(Object.entries(genome_js_1.COMPARE_OPS).map(([local, spec]) => [spec, local]));
|
|
73
|
+
function outsideSubset(why) {
|
|
74
|
+
return new GenomeImportError(`Composition is outside the v0.9.2 executable subset: ${why}`, "v0.9.2 imports Gene, Seq of genes, Par of genes (no deadline), and Cond/TryCatch over exactly two genes.");
|
|
75
|
+
}
|
|
76
|
+
function isObject(v) {
|
|
77
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
78
|
+
}
|
|
79
|
+
/** The single key of an externally tagged AlgebraExpr, and its body. */
|
|
80
|
+
function tagOf(expr) {
|
|
81
|
+
if (!isObject(expr))
|
|
82
|
+
throw outsideSubset("an expression is not an object");
|
|
83
|
+
const keys = Object.keys(expr);
|
|
84
|
+
if (keys.length !== 1)
|
|
85
|
+
throw outsideSubset(`an expression has ${keys.length} tags`);
|
|
86
|
+
const body = expr[keys[0]];
|
|
87
|
+
if (!isObject(body))
|
|
88
|
+
throw outsideSubset(`'${keys[0]}' has no body`);
|
|
89
|
+
return [keys[0], body];
|
|
90
|
+
}
|
|
91
|
+
/** A `Gene` leaf's role, checked against the snapshot's gene list. */
|
|
92
|
+
function leafRole(expr, genes) {
|
|
93
|
+
const [tag, body] = tagOf(expr);
|
|
94
|
+
if (tag === "RemoteGene")
|
|
95
|
+
throw outsideSubset("RemoteGene is not supported");
|
|
96
|
+
if (tag !== "Gene")
|
|
97
|
+
throw outsideSubset(`'${tag}' appears where only a single gene may`);
|
|
98
|
+
const ref = body.geneRef;
|
|
99
|
+
if (!isObject(ref) || typeof ref.role !== "string" || typeof ref.geneId !== "string") {
|
|
100
|
+
throw outsideSubset("a Gene leaf has no geneRef { geneId, role }");
|
|
101
|
+
}
|
|
102
|
+
const listed = genes.find((g) => g.role === ref.role);
|
|
103
|
+
if (!listed || listed.geneId !== ref.geneId) {
|
|
104
|
+
throw new GenomeImportError(`Composition leaf '${ref.role}' does not match snapshot.genes`);
|
|
105
|
+
}
|
|
106
|
+
return ref.role;
|
|
107
|
+
}
|
|
108
|
+
function compositionPlan(blueprint) {
|
|
109
|
+
const genes = blueprint.snapshot.genes;
|
|
110
|
+
const order = genes.map((g) => g.role);
|
|
111
|
+
const ext = blueprint.bindingExtensions?.[genome_js_1.BINDING_NAME] ?? {};
|
|
112
|
+
const expr = blueprint.snapshot.composition;
|
|
113
|
+
// §4.3: a genome is an ordered composition; with no expression it runs in order.
|
|
114
|
+
if (expr === undefined) {
|
|
115
|
+
return genes.length === 1 ? { type: "Single", roles: order } : { type: "Seq", roles: order };
|
|
116
|
+
}
|
|
117
|
+
const [tag, body] = tagOf(expr);
|
|
118
|
+
let plan;
|
|
119
|
+
switch (tag) {
|
|
120
|
+
case "Gene":
|
|
121
|
+
plan = { type: "Single", roles: [leafRole(expr, genes)] };
|
|
122
|
+
break;
|
|
123
|
+
case "Seq": {
|
|
124
|
+
if (!Array.isArray(body.steps) || body.steps.length === 0)
|
|
125
|
+
throw outsideSubset("Seq has no steps");
|
|
126
|
+
plan = { type: "Seq", roles: body.steps.map((s) => leafRole(s, genes)) };
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "Par": {
|
|
130
|
+
// `agent run` executes Par flat and n-ary; export writes it right-nested.
|
|
131
|
+
// Flatten Par(a, Par(b, c)) back to [a, b, c]; anything else nested is refused.
|
|
132
|
+
const roles = [];
|
|
133
|
+
let node = body;
|
|
134
|
+
for (;;) {
|
|
135
|
+
if (node.deadline !== undefined && node.deadline !== null)
|
|
136
|
+
throw outsideSubset("Par with a deadline");
|
|
137
|
+
roles.push(leafRole(node.left, genes));
|
|
138
|
+
const [rightTag, rightBody] = tagOf(node.right);
|
|
139
|
+
if (rightTag === "Par") {
|
|
140
|
+
node = rightBody;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
roles.push(leafRole(node.right, genes));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
const merge = ext.parMerge ?? "first";
|
|
147
|
+
if (typeof merge !== "string" || !PAR_MERGES.has(merge)) {
|
|
148
|
+
throw new GenomeImportError(`bindingExtensions["${genome_js_1.BINDING_NAME}"].parMerge '${String(merge)}' is not first, concat or merge`);
|
|
149
|
+
}
|
|
150
|
+
plan = { type: "Par", roles, parMerge: merge };
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "Cond": {
|
|
154
|
+
const p = body.predicate;
|
|
155
|
+
if (!isObject(p) || typeof p.field !== "string" || typeof p.operator !== "string" || !("value" in p)) {
|
|
156
|
+
throw outsideSubset("Cond predicate must be { field, operator, value }");
|
|
157
|
+
}
|
|
158
|
+
// agent run substitutes "type" for an empty field and "primary" for a null
|
|
159
|
+
// value; accepting them would run a different branch than the one declared.
|
|
160
|
+
if (p.field === "")
|
|
161
|
+
throw outsideSubset("Cond predicate needs a non-empty field");
|
|
162
|
+
if (p.value === null)
|
|
163
|
+
throw outsideSubset("Cond predicate value must not be null");
|
|
164
|
+
const op = OPS_FROM_SPEC[p.operator];
|
|
165
|
+
if (!op)
|
|
166
|
+
throw outsideSubset(`unknown CompareOp '${p.operator}'`);
|
|
167
|
+
plan = {
|
|
168
|
+
type: "Cond",
|
|
169
|
+
roles: [leafRole(body.ifTrue, genes), leafRole(body.ifFalse, genes)],
|
|
170
|
+
predicate: { field: p.field, op, value: p.value },
|
|
171
|
+
};
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case "TryCatch":
|
|
175
|
+
plan = { type: "Try", roles: [leafRole(body.body, genes), leafRole(body.handler, genes)] };
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
throw outsideSubset(`'${tag}' is not a §15.2 operator v0.9.2 supports`);
|
|
179
|
+
}
|
|
180
|
+
if (plan.roles.join("\u0000") !== order.join("\u0000")) {
|
|
181
|
+
throw outsideSubset("the composition must use every gene of snapshot.genes once, in order");
|
|
182
|
+
}
|
|
183
|
+
return plan;
|
|
184
|
+
}
|
|
185
|
+
function checkMinVg(value, where) {
|
|
186
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < genome_js_1.MIN_VG_FLOOR || value >= 1) {
|
|
187
|
+
throw new GenomeImportError(`${where} must be in [${genome_js_1.MIN_VG_FLOOR}, 1.0)`, "§5.1 does not allow a V(g) threshold below 0.5.");
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
/** Parse a Blueprint, keep only what v0.9.2 can rebuild faithfully, and run the credential scan. */
|
|
192
|
+
function parseBlueprint(text) {
|
|
193
|
+
let raw;
|
|
194
|
+
try {
|
|
195
|
+
raw = JSON.parse(text);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
// The parser's own message quotes the input around the error, which may be
|
|
199
|
+
// a key; report only where it broke.
|
|
200
|
+
const position = /position (\d+)/.exec(err.message)?.[1];
|
|
201
|
+
throw new GenomeImportError(`Not a JSON Blueprint: the file is not valid JSON${position ? ` (at position ${position})` : ""}`);
|
|
202
|
+
}
|
|
203
|
+
if (!isObject(raw))
|
|
204
|
+
throw new GenomeImportError("Not a Blueprint: the top level is not an object");
|
|
205
|
+
// The credential scan runs before any structural check: structural errors
|
|
206
|
+
// name roles, keys and values from the file, and none of those may be a
|
|
207
|
+
// secret by the time they are printed. Findings carry masked pointers only.
|
|
208
|
+
const findings = (0, genome_js_1.scanBlueprint)(raw, { isLeafIdCheckedStructurally: true });
|
|
209
|
+
if (findings.length > 0) {
|
|
210
|
+
const where = findings.map((f) => `${f.pointer || "/"} [${f.layer}] ${f.rule}`).join("; ");
|
|
211
|
+
throw new GenomeImportError(`The Blueprint carries what looks like a credential, so it is not imported: ${where}`);
|
|
212
|
+
}
|
|
213
|
+
const format = raw.blueprintFormat;
|
|
214
|
+
if (typeof format !== "number" || !Number.isInteger(format) || format < 1) {
|
|
215
|
+
throw new GenomeImportError("Not a Blueprint: blueprintFormat is missing or not a positive integer");
|
|
216
|
+
}
|
|
217
|
+
if (format > genome_js_1.BLUEPRINT_FORMAT) {
|
|
218
|
+
throw new GenomeImportError(`Blueprint format ${format} is newer than this CLI understands (${genome_js_1.BLUEPRINT_FORMAT})`, "Update the CLI: rotifer self-update");
|
|
219
|
+
}
|
|
220
|
+
const snapshot = raw.snapshot;
|
|
221
|
+
if (!isObject(snapshot) || !Array.isArray(snapshot.genes) || snapshot.genes.length === 0) {
|
|
222
|
+
throw new GenomeImportError("Blueprint has no snapshot.genes");
|
|
223
|
+
}
|
|
224
|
+
const roles = new Set();
|
|
225
|
+
for (const g of snapshot.genes) {
|
|
226
|
+
if (!isObject(g) || typeof g.geneId !== "string" || typeof g.role !== "string" || g.role === "") {
|
|
227
|
+
throw new GenomeImportError("Every snapshot.genes entry needs a geneId and a role");
|
|
228
|
+
}
|
|
229
|
+
if (!genome_js_1.GENE_ID_V2.test(g.geneId))
|
|
230
|
+
throw new GenomeImportError(`Gene '${g.role}' has no v2 gene ID`);
|
|
231
|
+
if (g.optional === true)
|
|
232
|
+
throw outsideSubset(`gene '${g.role}' is optional, which agent run has no notion of`);
|
|
233
|
+
if (roles.has(g.role))
|
|
234
|
+
throw new GenomeImportError(`Role '${g.role}' appears twice in snapshot.genes`);
|
|
235
|
+
roles.add(g.role);
|
|
236
|
+
}
|
|
237
|
+
if (raw.capabilities !== undefined) {
|
|
238
|
+
if (!isObject(raw.capabilities))
|
|
239
|
+
throw new GenomeImportError("capabilities must be an object");
|
|
240
|
+
for (const [role, q] of Object.entries(raw.capabilities)) {
|
|
241
|
+
if (!roles.has(role))
|
|
242
|
+
throw new GenomeImportError(`capabilities has a role '${role}' that snapshot.genes lacks`);
|
|
243
|
+
if (!isObject(q) || typeof q.domain !== "string" || q.domain === "") {
|
|
244
|
+
throw new GenomeImportError(`capabilities['${role}'] has no domain`);
|
|
245
|
+
}
|
|
246
|
+
if (q.minFitness !== undefined && (typeof q.minFitness !== "number" || q.minFitness < 0 || q.minFitness > 1)) {
|
|
247
|
+
throw new GenomeImportError(`capabilities['${role}'].minFitness must be between 0 and 1`);
|
|
248
|
+
}
|
|
249
|
+
if (q.excludeGeneIds !== undefined) {
|
|
250
|
+
if (!Array.isArray(q.excludeGeneIds) || !q.excludeGeneIds.every((id) => typeof id === "string" && genome_js_1.GENE_ID_V2.test(id))) {
|
|
251
|
+
throw new GenomeImportError(`capabilities['${role}'].excludeGeneIds must be v2 gene IDs`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (raw.resolutionHints !== undefined) {
|
|
257
|
+
if (!isObject(raw.resolutionHints))
|
|
258
|
+
throw new GenomeImportError("resolutionHints must be an object");
|
|
259
|
+
for (const [role, h] of Object.entries(raw.resolutionHints)) {
|
|
260
|
+
if (!roles.has(role))
|
|
261
|
+
throw new GenomeImportError(`resolutionHints has a role '${role}' that snapshot.genes lacks`);
|
|
262
|
+
const isValid = isObject(h) &&
|
|
263
|
+
["owner", "name", "version", "fidelity"].every((k) => typeof h[k] === "string" && h[k] !== "") &&
|
|
264
|
+
(h.cloudId === undefined || typeof h.cloudId === "string");
|
|
265
|
+
if (!isValid)
|
|
266
|
+
throw new GenomeImportError(`resolutionHints['${role}'] needs owner, name, version and fidelity`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const blueprint = raw;
|
|
270
|
+
const ext = blueprint.bindingExtensions?.[genome_js_1.BINDING_NAME];
|
|
271
|
+
const suggestedMinVg = ext?.minVg === undefined ? undefined : checkMinVg(ext.minVg, `bindingExtensions["${genome_js_1.BINDING_NAME}"].minVg`);
|
|
272
|
+
const plan = compositionPlan(blueprint);
|
|
273
|
+
return { blueprint, plan, suggestedMinVg };
|
|
274
|
+
}
|
|
275
|
+
function isWrapped(fidelity) {
|
|
276
|
+
return fidelity.toLowerCase() === "wrapped";
|
|
277
|
+
}
|
|
278
|
+
/** Lock mode: the exact author + name + version from the hints. */
|
|
279
|
+
async function locatePinned(role, hint) {
|
|
280
|
+
if (!hint) {
|
|
281
|
+
throw new GenomeImportError(`Role '${role}' has no resolutionHints, so it cannot be located by gene ID`, "The author's gene was never published; ask them to publish it, or import with capability mode.");
|
|
282
|
+
}
|
|
283
|
+
if (isWrapped(hint.fidelity)) {
|
|
284
|
+
throw new GenomeImportError(`Role '${role}' is a Wrapped gene: v0.9.2 does not support installing Wrapped genes from the Registry`, "Drop --pinned so capability mode can look for a Native/Hybrid substitute.");
|
|
285
|
+
}
|
|
286
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
287
|
+
if (hint.cloudId) {
|
|
288
|
+
try {
|
|
289
|
+
const row = await (0, client_js_1.getGene)(hint.cloudId);
|
|
290
|
+
if (same(row.owner, hint.owner) && row.name === hint.name && row.version === hint.version) {
|
|
291
|
+
return { cloudId: row.id };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// Unpublished or unknown id: fall through to the version list.
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const versions = await (0, client_js_1.listGeneVersions)(hint.owner, hint.name);
|
|
299
|
+
const match = versions.find((v) => v.version === hint.version);
|
|
300
|
+
if (!match) {
|
|
301
|
+
throw new GenomeImportError(`Role '${role}': ${hint.owner}/${hint.name}@${hint.version} is not in the Registry (unpublished, or never published)`);
|
|
302
|
+
}
|
|
303
|
+
return { cloudId: match.id };
|
|
304
|
+
}
|
|
305
|
+
/** Strictest of the author's suggestion, the local flag, and the §5.1 default. */
|
|
306
|
+
function effectiveMinVg(suggested, local) {
|
|
307
|
+
return Math.max(exports.DEFAULT_MIN_VG, suggested ?? 0, local ?? 0);
|
|
308
|
+
}
|
|
309
|
+
async function resolveRole(role, ctx) {
|
|
310
|
+
const { blueprint, options } = ctx;
|
|
311
|
+
const ref = blueprint.snapshot.genes.find((g) => g.role === role);
|
|
312
|
+
const hint = blueprint.resolutionHints?.[role];
|
|
313
|
+
const query = blueprint.capabilities?.[role];
|
|
314
|
+
const pinned = async (mode, extra = {}) => {
|
|
315
|
+
const { cloudId } = await locatePinned(role, hint);
|
|
316
|
+
return {
|
|
317
|
+
role,
|
|
318
|
+
mode,
|
|
319
|
+
originalGeneId: ref.geneId,
|
|
320
|
+
cloudId,
|
|
321
|
+
owner: hint.owner,
|
|
322
|
+
name: hint.name,
|
|
323
|
+
version: hint.version,
|
|
324
|
+
fidelity: hint.fidelity,
|
|
325
|
+
selfHealUpgrade: false,
|
|
326
|
+
crossAuthor: false,
|
|
327
|
+
...extra,
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
if (options.isPinned || !query)
|
|
331
|
+
return pinned("pinned");
|
|
332
|
+
const policy = {
|
|
333
|
+
minFitness: query.minFitness ?? genome_js_1.DEFAULT_MIN_FITNESS,
|
|
334
|
+
minVg: effectiveMinVg(ctx.suggestedMinVg, options.minVg),
|
|
335
|
+
maxEvalAgeDays: options.maxEvalAgeDays ?? exports.DEFAULT_MAX_EVAL_AGE_DAYS,
|
|
336
|
+
now: options.now ?? new Date(),
|
|
337
|
+
};
|
|
338
|
+
const { candidates, admitted } = await (0, capability_candidates_js_1.resolveCapabilityCandidates)(query, policy);
|
|
339
|
+
if (admitted.length === 0) {
|
|
340
|
+
return pinned("fallback", { fallback: "no-admissible-candidate", candidates });
|
|
341
|
+
}
|
|
342
|
+
const best = admitted[0];
|
|
343
|
+
// With no hint there is no original author to compare against, so any pick is treated as a substitution.
|
|
344
|
+
const isCrossAuthor = !hint || best.owner.toLowerCase() !== hint.owner.toLowerCase();
|
|
345
|
+
if (isCrossAuthor && !options.isConfirmed) {
|
|
346
|
+
const question = `Role '${role}': replace ${hint ? `${hint.owner}/${hint.name}@${hint.version}` : "the original gene"} with ${best.owner}/${best.name}@${best.version} (another author)?`;
|
|
347
|
+
const isAccepted = options.confirm ? await options.confirm(question) : false;
|
|
348
|
+
if (!isAccepted) {
|
|
349
|
+
return pinned("fallback", {
|
|
350
|
+
fallback: "cross-author-not-confirmed",
|
|
351
|
+
candidates,
|
|
352
|
+
rejectedSubstitutions: [
|
|
353
|
+
{
|
|
354
|
+
cloudId: best.cloudId,
|
|
355
|
+
owner: best.owner,
|
|
356
|
+
name: best.name,
|
|
357
|
+
version: best.version,
|
|
358
|
+
reason: options.confirm ? "declined" : "cross-author substitute needs confirmation; re-run with --yes to accept",
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
role,
|
|
366
|
+
mode: "capability",
|
|
367
|
+
originalGeneId: ref.geneId,
|
|
368
|
+
cloudId: best.cloudId,
|
|
369
|
+
owner: best.owner,
|
|
370
|
+
name: best.name,
|
|
371
|
+
version: best.version,
|
|
372
|
+
fidelity: best.fidelity,
|
|
373
|
+
...(best.geneId ? { candidateGeneId: best.geneId } : {}),
|
|
374
|
+
selfHealUpgrade: !!hint && !isCrossAuthor && best.name === hint.name && best.version !== hint.version,
|
|
375
|
+
crossAuthor: isCrossAuthor,
|
|
376
|
+
evidence: best.evidence,
|
|
377
|
+
candidates,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Install every role into a staging directory, recompute each v2 gene ID, and
|
|
382
|
+
* check it: lock-mode roles must reproduce the Blueprint's geneId exactly;
|
|
383
|
+
* capability picks must be the content that was admitted. Nothing touches genes/ until every
|
|
384
|
+
* role has passed.
|
|
385
|
+
*/
|
|
386
|
+
async function installAndVerify(resolutions, genesRoot, stagingRoot, isVerbose) {
|
|
387
|
+
const staged = [];
|
|
388
|
+
for (const r of resolutions) {
|
|
389
|
+
(0, validate_gene_name_js_1.validateGeneName)(r.name);
|
|
390
|
+
const local = (0, node_path_1.join)(genesRoot, r.name);
|
|
391
|
+
const isLockMode = r.mode !== "capability";
|
|
392
|
+
if (isLockMode && (0, node_fs_1.existsSync)((0, node_path_1.join)(local, "phenotype.json"))) {
|
|
393
|
+
const localId = (0, binding_js_1.geneIdV2)(genesRoot, r.name);
|
|
394
|
+
if ("id" in localId && localId.id === r.originalGeneId) {
|
|
395
|
+
staged.push({ role: r.role, name: r.name, geneId: localId.id, stagedDir: null, contentHashCheck: null });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (isVerbose)
|
|
400
|
+
display.info(`Installing ${r.owner}/${r.name}@${r.version} (${r.cloudId})`);
|
|
401
|
+
const { gene, contentHashCheck } = await (0, install_js_1.installGeneFromCloud)(r.cloudId, { genesDir: stagingRoot });
|
|
402
|
+
const hashWarning = (0, install_js_1.contentHashWarning)(gene.name, contentHashCheck);
|
|
403
|
+
if (hashWarning && isVerbose)
|
|
404
|
+
display.warn(hashWarning);
|
|
405
|
+
if (gene.name !== r.name) {
|
|
406
|
+
throw new GenomeImportError(`Registry row ${r.cloudId} is '${gene.name}', expected '${r.name}'`);
|
|
407
|
+
}
|
|
408
|
+
const id = (0, binding_js_1.geneIdV2)(stagingRoot, gene.name);
|
|
409
|
+
if (!("id" in id)) {
|
|
410
|
+
throw new GenomeImportError(`Installed '${gene.name}' has no v2 gene ID: ${id.reason}`);
|
|
411
|
+
}
|
|
412
|
+
if (isLockMode && id.id !== r.originalGeneId) {
|
|
413
|
+
throw new GenomeImportError(`geneId mismatch for role '${r.role}': the Blueprint pins ${r.originalGeneId}, ` +
|
|
414
|
+
`the installed ${r.owner}/${r.name}@${r.version} is ${id.id}`, "The gene's content differs from what the Blueprint was made from; nothing was imported.");
|
|
415
|
+
}
|
|
416
|
+
// Exclusion was applied at admission; what was installed must be what was admitted.
|
|
417
|
+
if (!isLockMode && id.id !== r.candidateGeneId) {
|
|
418
|
+
throw new GenomeImportError(`Role '${r.role}': installed ${r.owner}/${r.name}@${r.version} is ${id.id}, not the ${r.candidateGeneId} admitted`, "The Registry row changed between admission and install; nothing was imported.");
|
|
419
|
+
}
|
|
420
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(local, "phenotype.json"))) {
|
|
421
|
+
const localId = (0, binding_js_1.geneIdV2)(genesRoot, r.name);
|
|
422
|
+
if (!("id" in localId) || localId.id !== id.id) {
|
|
423
|
+
throw new GenomeImportError(`A different gene named '${r.name}' already exists in this workspace`, "Import into another workspace, or move the local gene aside first.");
|
|
424
|
+
}
|
|
425
|
+
staged.push({ role: r.role, name: r.name, geneId: id.id, stagedDir: null, contentHashCheck });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
staged.push({ role: r.role, name: r.name, geneId: id.id, stagedDir: (0, node_path_1.join)(stagingRoot, gene.name), contentHashCheck });
|
|
429
|
+
}
|
|
430
|
+
return staged;
|
|
431
|
+
}
|
|
432
|
+
/** sha256 of the composition-relevant part of the Blueprint: same genome, same id. */
|
|
433
|
+
function genomeIdOf(blueprint) {
|
|
434
|
+
const material = JSON.stringify({ genes: blueprint.snapshot.genes, composition: blueprint.snapshot.composition ?? null });
|
|
435
|
+
return (0, node_crypto_1.createHash)("sha256").update(material).digest("hex");
|
|
436
|
+
}
|
|
437
|
+
async function importGenome(file, options, ctx) {
|
|
438
|
+
if (options.minVg !== undefined)
|
|
439
|
+
checkMinVg(options.minVg, "--min-vg");
|
|
440
|
+
if (options.maxEvalAgeDays !== undefined && !(Number.isFinite(options.maxEvalAgeDays) && options.maxEvalAgeDays > 0)) {
|
|
441
|
+
throw new GenomeImportError("--max-eval-age must be a positive number of days");
|
|
442
|
+
}
|
|
443
|
+
let text;
|
|
444
|
+
try {
|
|
445
|
+
text = (0, node_fs_1.readFileSync)(file, "utf-8");
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
throw new GenomeImportError(`Cannot read ${file}: ${err.message}`);
|
|
449
|
+
}
|
|
450
|
+
const { blueprint, plan, suggestedMinVg } = parseBlueprint(text);
|
|
451
|
+
const agentName = options.name ?? blueprint.metadata?.name;
|
|
452
|
+
if (typeof agentName !== "string" || agentName === "") {
|
|
453
|
+
throw new GenomeImportError("The Blueprint has no metadata.name", "Name the agent: --name <agent>");
|
|
454
|
+
}
|
|
455
|
+
const agentsDir = (0, node_path_1.join)(ctx.root, ".rotifer", "agents");
|
|
456
|
+
if ((0, node_fs_1.existsSync)(agentsDir)) {
|
|
457
|
+
const isTaken = (0, node_fs_1.readdirSync)(agentsDir)
|
|
458
|
+
.filter((f) => f.endsWith(".json"))
|
|
459
|
+
.some((f) => JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(agentsDir, f), "utf-8")).name === agentName);
|
|
460
|
+
if (isTaken)
|
|
461
|
+
throw new GenomeImportError(`An agent named '${agentName}' already exists`, "Pick another: --name <agent>");
|
|
462
|
+
}
|
|
463
|
+
const resolutions = [];
|
|
464
|
+
for (const role of plan.roles) {
|
|
465
|
+
try {
|
|
466
|
+
resolutions.push(await resolveRole(role, { blueprint, suggestedMinVg, options }));
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
if (err instanceof capability_candidates_js_1.EvidenceError) {
|
|
470
|
+
throw new GenomeImportError(`Role '${role}': ${err.message}`, "Capability evidence could not be read; nothing was imported.");
|
|
471
|
+
}
|
|
472
|
+
throw err;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const names = resolutions.map((r) => r.name);
|
|
476
|
+
if (new Set(names).size !== names.length) {
|
|
477
|
+
throw new GenomeImportError("Two roles resolved to the same gene name; the agent cannot hold both");
|
|
478
|
+
}
|
|
479
|
+
const resolvedAt = (options.now ?? new Date()).toISOString();
|
|
480
|
+
if (options.isDryRun) {
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
dryRun: true,
|
|
484
|
+
agent: null,
|
|
485
|
+
composition: plan.type,
|
|
486
|
+
executionSnapshot: null,
|
|
487
|
+
// Nothing is installed in a dry run, so no installed geneId is known yet.
|
|
488
|
+
roles: resolutions.map((r) => ({ ...r, geneId: null })),
|
|
489
|
+
schemaWarnings: [],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const genesRoot = (0, node_path_1.join)(ctx.root, ctx.genesDir);
|
|
493
|
+
(0, node_fs_1.mkdirSync)(genesRoot, { recursive: true });
|
|
494
|
+
const stagingRoot = (0, node_path_1.join)(ctx.root, ".rotifer", `import-staging-${(0, node_crypto_1.randomUUID)()}`);
|
|
495
|
+
(0, node_fs_1.mkdirSync)(stagingRoot, { recursive: true });
|
|
496
|
+
const schemaWarnings = [];
|
|
497
|
+
let staged;
|
|
498
|
+
try {
|
|
499
|
+
staged = await installAndVerify(resolutions, genesRoot, stagingRoot, !display.isJsonMode());
|
|
500
|
+
for (const s of staged) {
|
|
501
|
+
if (s.stagedDir)
|
|
502
|
+
(0, node_fs_1.cpSync)(s.stagedDir, (0, node_path_1.join)(genesRoot, s.name), { recursive: true });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
// The staging directory is this command's own scratch space.
|
|
507
|
+
(0, node_fs_1.rmSync)(stagingRoot, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
const agent = (0, agent_create_js_1.createAgentCore)({
|
|
510
|
+
root: ctx.root,
|
|
511
|
+
genesDir: ctx.genesDir,
|
|
512
|
+
agentName,
|
|
513
|
+
genome: staged.map((s) => s.name),
|
|
514
|
+
compositionType: plan.type,
|
|
515
|
+
parMerge: plan.parMerge,
|
|
516
|
+
predicate: plan.predicate,
|
|
517
|
+
strategy: "genome-import",
|
|
518
|
+
onSchemaWarnings: (w) => schemaWarnings.push(...w),
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
dryRun: false,
|
|
523
|
+
agent: { id: agent.id, name: agent.name },
|
|
524
|
+
composition: plan.type,
|
|
525
|
+
executionSnapshot: {
|
|
526
|
+
genomeId: genomeIdOf(blueprint),
|
|
527
|
+
resolvedDeps: Object.fromEntries(staged.map((s) => [s.role, s.geneId])),
|
|
528
|
+
resolvedAt,
|
|
529
|
+
status: "ACTIVE",
|
|
530
|
+
},
|
|
531
|
+
roles: resolutions.map((r, i) => ({ ...r, geneId: staged[i].geneId, contentHashCheck: staged[i].contentHashCheck })),
|
|
532
|
+
schemaWarnings,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function parseNumber(flag, raw) {
|
|
536
|
+
if (raw === undefined)
|
|
537
|
+
return undefined;
|
|
538
|
+
const n = Number(raw);
|
|
539
|
+
if (raw.trim() === "" || !Number.isFinite(n))
|
|
540
|
+
throw new GenomeImportError(`${flag} must be a number, got '${raw}'`);
|
|
541
|
+
return n;
|
|
542
|
+
}
|
|
543
|
+
function renderHuman(r) {
|
|
544
|
+
display.header(r.dryRun ? "Genome Import (dry run)" : "Genome Import");
|
|
545
|
+
display.keyValue("Composition", r.composition);
|
|
546
|
+
for (const role of r.roles) {
|
|
547
|
+
const mode = role.mode === "fallback" ? `fallback (${role.fallback})` : role.mode;
|
|
548
|
+
display.keyValue(` ${role.role}`, `${mode} → ${role.owner}/${role.name}@${role.version} ${role.cloudId}`);
|
|
549
|
+
display.keyValue(" original geneId", role.originalGeneId);
|
|
550
|
+
if (role.geneId)
|
|
551
|
+
display.keyValue(" installed geneId", role.geneId);
|
|
552
|
+
if (role.selfHealUpgrade)
|
|
553
|
+
display.keyValue(" self-heal upgrade", "yes");
|
|
554
|
+
if (role.crossAuthor)
|
|
555
|
+
display.keyValue(" cross-author", "yes");
|
|
556
|
+
if (role.evidence) {
|
|
557
|
+
const e = role.evidence;
|
|
558
|
+
display.keyValue(" evidence", `tier=${e.tier} fitness=${e.fitness_value} safety=${e.safety_score} (submitter-declared) ` +
|
|
559
|
+
`method=${e.evaluation_method} n=${e.evaluation_n} last=${e.last_evaluated}`);
|
|
560
|
+
}
|
|
561
|
+
for (const c of role.candidates ?? []) {
|
|
562
|
+
if (c.admitted)
|
|
563
|
+
continue;
|
|
564
|
+
display.hint(` rejected ${c.owner}/${c.name}@${c.version}: ${c.rejections.map((x) => x.detail).join("; ")}`);
|
|
565
|
+
}
|
|
566
|
+
for (const s of role.rejectedSubstitutions ?? []) {
|
|
567
|
+
display.warn(` substitute not taken ${s.owner}/${s.name}@${s.version}: ${s.reason}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const w of r.schemaWarnings)
|
|
571
|
+
display.warn(`Schema: ${w}`);
|
|
572
|
+
if (r.agent)
|
|
573
|
+
display.success(`Agent '${r.agent.name}' created (${r.agent.id})`);
|
|
574
|
+
else
|
|
575
|
+
display.hint("Dry run: nothing installed, no agent created.");
|
|
576
|
+
}
|
|
577
|
+
async function runGenomeImport(file, cli, ctx) {
|
|
578
|
+
try {
|
|
579
|
+
const isInteractive = !display.isJsonMode() && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
580
|
+
const confirm = isInteractive
|
|
581
|
+
? async (question) => {
|
|
582
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
583
|
+
try {
|
|
584
|
+
return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
585
|
+
}
|
|
586
|
+
finally {
|
|
587
|
+
rl.close();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
: undefined;
|
|
591
|
+
const result = await importGenome((0, node_path_1.resolve)(file), {
|
|
592
|
+
isPinned: cli.pinned,
|
|
593
|
+
isDryRun: cli.dryRun,
|
|
594
|
+
isConfirmed: cli.yes,
|
|
595
|
+
name: cli.name,
|
|
596
|
+
minVg: parseNumber("--min-vg", cli.minVg),
|
|
597
|
+
maxEvalAgeDays: parseNumber("--max-eval-age", cli.maxEvalAge),
|
|
598
|
+
confirm,
|
|
599
|
+
}, ctx);
|
|
600
|
+
display.renderResult(result, renderHuman);
|
|
601
|
+
return 0;
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
if (!(err instanceof GenomeImportError)) {
|
|
605
|
+
const message = err?.message ?? String(err);
|
|
606
|
+
err = new GenomeImportError(message);
|
|
607
|
+
}
|
|
608
|
+
const e = err;
|
|
609
|
+
if (display.isJsonMode()) {
|
|
610
|
+
process.stdout.write(JSON.stringify({ ok: false, error: e.message, hint: e.hint }, null, 2) + "\n");
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
display.error(e.message, e.hint);
|
|
614
|
+
}
|
|
615
|
+
return 1;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
exports.genomeImportCommand = new commander_1.Command("import")
|
|
619
|
+
.description("Rebuild an agent from a Genome Blueprint (.rgenome)")
|
|
620
|
+
.argument("<file>", "Blueprint file")
|
|
621
|
+
.option("--pinned", "install exactly the genes the Blueprint pins; skip capability matching")
|
|
622
|
+
.option("--dry-run", "resolve every role and print the plan; install nothing")
|
|
623
|
+
.option("--yes", "accept substitutes from other authors without asking")
|
|
624
|
+
.option("--name <agent>", "name for the new agent (default: the Blueprint's)")
|
|
625
|
+
.option("--min-vg <n>", `V(g) threshold for capability picks, in [${genome_js_1.MIN_VG_FLOOR}, 1.0) (default ${exports.DEFAULT_MIN_VG}; the strictest of this, the Blueprint's and the default wins)`)
|
|
626
|
+
.option("--max-eval-age <days>", `ignore Arena evidence older than this (default ${exports.DEFAULT_MAX_EVAL_AGE_DAYS})`)
|
|
627
|
+
.action(async (file, cli) => {
|
|
628
|
+
const root = (0, project_root_js_1.requireProjectRoot)();
|
|
629
|
+
const config = (0, config_js_1.loadConfig)(root);
|
|
630
|
+
process.exitCode = await runGenomeImport(file, cli, { root, genesDir: config.genes_dir });
|
|
631
|
+
});
|
|
632
|
+
//# sourceMappingURL=genome-import.js.map
|