superlab 0.1.11 → 0.1.13

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.
@@ -0,0 +1,129 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const PLACEHOLDER_VALUES = new Set(["", "tbd", "none", "待补充", "无"]);
5
+
6
+ function contextFile(targetDir, name) {
7
+ return path.join(targetDir, ".lab", "context", name);
8
+ }
9
+
10
+ function readFileIfExists(filePath) {
11
+ if (!fs.existsSync(filePath)) {
12
+ return "";
13
+ }
14
+ return fs.readFileSync(filePath, "utf8");
15
+ }
16
+
17
+ function extractValue(text, labels) {
18
+ for (const label of labels) {
19
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
20
+ const regex = new RegExp(`^\\s*-\\s*${escaped}:[ \\t]*([^\\n\\r]+?)[ \\t]*$`, "im");
21
+ const match = text.match(regex);
22
+ if (match && match[1]) {
23
+ return match[1].trim();
24
+ }
25
+ }
26
+ return "";
27
+ }
28
+
29
+ function isMeaningful(value) {
30
+ return !PLACEHOLDER_VALUES.has((value || "").trim().toLowerCase());
31
+ }
32
+
33
+ function normalizeAllowedStages(rawValue) {
34
+ return (rawValue || "")
35
+ .split(",")
36
+ .map((value) => value.trim().toLowerCase())
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function normalizeScalar(value) {
41
+ return (value || "").trim().toLowerCase();
42
+ }
43
+
44
+ function parseInteger(value, fallback = null) {
45
+ const normalized = (value || "").trim();
46
+ if (!normalized) {
47
+ return fallback;
48
+ }
49
+ const parsed = Number.parseInt(normalized, 10);
50
+ return Number.isFinite(parsed) ? parsed : fallback;
51
+ }
52
+
53
+ function parseDurationMs(value, fallbackMs) {
54
+ const normalized = (value || "").trim().toLowerCase();
55
+ if (!normalized) {
56
+ return fallbackMs;
57
+ }
58
+ const match = normalized.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
59
+ if (!match) {
60
+ return fallbackMs;
61
+ }
62
+ const amount = Number.parseFloat(match[1]);
63
+ const unit = match[2] || "s";
64
+ const multipliers = {
65
+ ms: 1,
66
+ s: 1000,
67
+ m: 60_000,
68
+ h: 3_600_000,
69
+ };
70
+ return Math.max(1, Math.round(amount * multipliers[unit]));
71
+ }
72
+
73
+ function sleep(ms) {
74
+ return new Promise((resolve) => setTimeout(resolve, ms));
75
+ }
76
+
77
+ function normalizeList(rawValue) {
78
+ return (rawValue || "")
79
+ .split(",")
80
+ .map((value) => value.trim())
81
+ .filter(Boolean);
82
+ }
83
+
84
+ function readWorkflowLanguage(targetDir) {
85
+ const configPath = path.join(targetDir, ".lab", "config", "workflow.json");
86
+ if (!fs.existsSync(configPath)) {
87
+ return "en";
88
+ }
89
+ try {
90
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
91
+ return config.workflow_language === "zh" ? "zh" : "en";
92
+ } catch {
93
+ return "en";
94
+ }
95
+ }
96
+
97
+ function readWorkflowConfig(targetDir) {
98
+ const configPath = path.join(targetDir, ".lab", "config", "workflow.json");
99
+ try {
100
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
101
+ } catch {
102
+ return {};
103
+ }
104
+ }
105
+
106
+ function resolveProjectPath(targetDir, configuredPath, fallbackRelativePath) {
107
+ if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
108
+ return path.join(targetDir, fallbackRelativePath);
109
+ }
110
+ return path.isAbsolute(configuredPath)
111
+ ? configuredPath
112
+ : path.resolve(targetDir, configuredPath);
113
+ }
114
+
115
+ module.exports = {
116
+ contextFile,
117
+ extractValue,
118
+ isMeaningful,
119
+ normalizeAllowedStages,
120
+ normalizeList,
121
+ normalizeScalar,
122
+ parseDurationMs,
123
+ parseInteger,
124
+ readFileIfExists,
125
+ readWorkflowConfig,
126
+ readWorkflowLanguage,
127
+ resolveProjectPath,
128
+ sleep,
129
+ };
@@ -0,0 +1,387 @@
1
+ const fs = require("node:fs");
2
+ const crypto = require("node:crypto");
3
+ const path = require("node:path");
4
+ const {
5
+ contextFile,
6
+ extractValue,
7
+ isMeaningful,
8
+ normalizeList,
9
+ parseInteger,
10
+ readFileIfExists,
11
+ readWorkflowConfig,
12
+ resolveProjectPath,
13
+ } = require("./auto_common.cjs");
14
+
15
+ const ALLOWED_AUTO_STAGES = new Set(["run", "iterate", "review", "report", "write"]);
16
+ const LOOP_AUTO_STAGES = new Set(["run", "iterate", "review"]);
17
+ const FINAL_AUTO_STAGES = new Set(["report", "write"]);
18
+ const AUTO_LEVEL_STAGE_ENVELOPES = {
19
+ l1: new Set(["run", "review", "report"]),
20
+ l2: new Set(["run", "iterate", "review", "report"]),
21
+ l3: new Set(["run", "iterate", "review", "report", "write"]),
22
+ };
23
+ const VALID_APPROVAL_STATUSES = new Set(["draft", "approved"]);
24
+ const VALID_TERMINAL_GOAL_TYPES = new Set(["rounds", "metric-threshold", "task-completion"]);
25
+ const FROZEN_CORE_ALIASES = {
26
+ mission: [path.join(".lab", "context", "mission.md")],
27
+ framing: [
28
+ path.join(".lab", "context", "terminology-lock.md"),
29
+ path.join(".lab", "writing", "framing.md"),
30
+ ],
31
+ claims: [path.join(".lab", "context", "terminology-lock.md")],
32
+ "terminology-lock": [path.join(".lab", "context", "terminology-lock.md")],
33
+ };
34
+ const REVIEW_CONTEXT_FILES = [
35
+ path.join(".lab", "context", "decisions.md"),
36
+ path.join(".lab", "context", "state.md"),
37
+ path.join(".lab", "context", "open-questions.md"),
38
+ path.join(".lab", "context", "evidence-index.md"),
39
+ ];
40
+ const PROMOTION_CANONICAL_FILES = [
41
+ path.join(".lab", "context", "data-decisions.md"),
42
+ path.join(".lab", "context", "decisions.md"),
43
+ path.join(".lab", "context", "state.md"),
44
+ path.join(".lab", "context", "session-brief.md"),
45
+ ];
46
+
47
+ function resolveFrozenCoreEntries(rawValue) {
48
+ const normalized = normalizeList(rawValue);
49
+ const paths = new Set();
50
+ const invalidEntries = [];
51
+
52
+ for (const entry of normalized) {
53
+ const key = entry.trim().toLowerCase();
54
+ if (FROZEN_CORE_ALIASES[key]) {
55
+ for (const aliasedPath of FROZEN_CORE_ALIASES[key]) {
56
+ paths.add(aliasedPath);
57
+ }
58
+ continue;
59
+ }
60
+
61
+ if (entry.includes("/") || entry.includes(path.sep) || entry.endsWith(".md")) {
62
+ paths.add(entry);
63
+ continue;
64
+ }
65
+
66
+ invalidEntries.push(entry);
67
+ }
68
+
69
+ return {
70
+ entries: normalized,
71
+ relativePaths: Array.from(paths),
72
+ invalidEntries,
73
+ };
74
+ }
75
+
76
+ function hashPathState(filePath) {
77
+ if (!fs.existsSync(filePath)) {
78
+ return "__missing__";
79
+ }
80
+
81
+ const stat = fs.statSync(filePath);
82
+ if (stat.isDirectory()) {
83
+ const entries = fs.readdirSync(filePath)
84
+ .sort((left, right) => left.localeCompare(right))
85
+ .map((entry) => {
86
+ const childPath = path.join(filePath, entry);
87
+ return `${entry}:${hashPathState(childPath)}`;
88
+ })
89
+ .join("|");
90
+ return crypto.createHash("sha256").update(entries).digest("hex");
91
+ }
92
+
93
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
94
+ }
95
+
96
+ function snapshotFrozenCore(targetDir, rawValue) {
97
+ const { relativePaths } = resolveFrozenCoreEntries(rawValue);
98
+ const snapshot = new Map();
99
+
100
+ for (const relativePath of relativePaths) {
101
+ const absolutePath = path.resolve(targetDir, relativePath);
102
+ snapshot.set(absolutePath, hashPathState(absolutePath));
103
+ }
104
+
105
+ return snapshot;
106
+ }
107
+
108
+ function detectFrozenCoreChanges(snapshot) {
109
+ const changedPaths = [];
110
+ for (const [absolutePath, initialHash] of snapshot.entries()) {
111
+ const currentHash = hashPathState(absolutePath);
112
+ if (currentHash !== initialHash) {
113
+ changedPaths.push(absolutePath);
114
+ }
115
+ }
116
+ return changedPaths;
117
+ }
118
+
119
+ function validateAutoMode(mode, status = null, evalProtocol = null) {
120
+ const issues = [];
121
+ const activated =
122
+ isMeaningful(mode.objective) ||
123
+ (status && ["running", "stopped", "failed", "completed"].includes((status.status || "").toLowerCase()));
124
+
125
+ if (!activated) {
126
+ return issues;
127
+ }
128
+
129
+ const requiredFields = [
130
+ ["objective", mode.objective],
131
+ ["autonomy level", mode.autonomyLevel],
132
+ ["approval status", mode.approvalStatus],
133
+ ["allowed stages", mode.allowedStages.join(", ")],
134
+ ["success criteria", mode.successCriteria],
135
+ ["terminal goal type", mode.terminalGoalType],
136
+ ["terminal goal target", mode.terminalGoalTarget],
137
+ ["required terminal artifact", mode.requiredTerminalArtifact],
138
+ ["max iterations", mode.maxIterations],
139
+ ["max wall-clock time", mode.maxWallClockTime],
140
+ ["max failures", mode.maxFailures],
141
+ ["poll interval", mode.pollInterval],
142
+ ["stop check command", mode.stopCheckCommand],
143
+ ["promotion policy", mode.promotionPolicy],
144
+ ["promotion check command", mode.promotionCheckCommand],
145
+ ["promotion command", mode.promotionCommand],
146
+ ["frozen core", mode.frozenCore],
147
+ ["exploration envelope", mode.explorationEnvelope],
148
+ ["stop conditions", mode.stopConditions],
149
+ ];
150
+
151
+ const missing = requiredFields
152
+ .filter(([, value]) => !isMeaningful(value))
153
+ .map(([name]) => name);
154
+ if (missing.length > 0) {
155
+ issues.push(`missing auto mode fields: ${missing.join(", ")}`);
156
+ }
157
+
158
+ const invalidStages = mode.allowedStages.filter((stage) => !ALLOWED_AUTO_STAGES.has(stage));
159
+ if (invalidStages.length > 0) {
160
+ issues.push(`unsupported auto stages: ${invalidStages.join(", ")}`);
161
+ }
162
+
163
+ if (isMeaningful(mode.autonomyLevel) && !AUTO_LEVEL_STAGE_ENVELOPES[mode.autonomyLevel]) {
164
+ issues.push(`invalid autonomy level: ${mode.autonomyLevel}`);
165
+ }
166
+
167
+ if (isMeaningful(mode.approvalStatus) && !VALID_APPROVAL_STATUSES.has(mode.approvalStatus)) {
168
+ issues.push(`invalid approval status: ${mode.approvalStatus}`);
169
+ }
170
+
171
+ if (isMeaningful(mode.terminalGoalType) && !VALID_TERMINAL_GOAL_TYPES.has(mode.terminalGoalType)) {
172
+ issues.push(`invalid terminal goal type: ${mode.terminalGoalType}`);
173
+ }
174
+
175
+ const allowedEnvelope = AUTO_LEVEL_STAGE_ENVELOPES[mode.autonomyLevel];
176
+ if (allowedEnvelope) {
177
+ const outOfEnvelopeStages = mode.allowedStages.filter((stage) => !allowedEnvelope.has(stage));
178
+ if (outOfEnvelopeStages.length > 0) {
179
+ issues.push(
180
+ `autonomy level ${mode.autonomyLevel} does not allow stages: ${outOfEnvelopeStages.join(", ")}`
181
+ );
182
+ }
183
+ }
184
+
185
+ const ladderStageCoverage = new Map();
186
+ if (evalProtocol && Array.isArray(evalProtocol.experimentRungs)) {
187
+ for (const rung of evalProtocol.experimentRungs) {
188
+ const hasExplicitCommand = isMeaningful(rung.command);
189
+ ladderStageCoverage.set(rung.stage, (ladderStageCoverage.get(rung.stage) || false) || hasExplicitCommand);
190
+ }
191
+ }
192
+
193
+ const missingCommands = mode.allowedStages.filter(
194
+ (stage) => !isMeaningful(mode.stageCommands?.[stage]) && !ladderStageCoverage.get(stage)
195
+ );
196
+ if (missingCommands.length > 0) {
197
+ issues.push(`missing auto commands for stages: ${missingCommands.join(", ")}`);
198
+ }
199
+
200
+ if (mode.terminalGoalType === "metric-threshold" && !isMeaningful(mode.successCheckCommand)) {
201
+ issues.push("missing auto mode fields: success check command");
202
+ }
203
+
204
+ if (mode.terminalGoalType === "rounds") {
205
+ const roundsTarget = parseInteger(mode.terminalGoalTarget, null);
206
+ if (!Number.isInteger(roundsTarget) || roundsTarget <= 0) {
207
+ issues.push(`terminal goal target must be a positive integer for rounds: ${mode.terminalGoalTarget}`);
208
+ }
209
+ }
210
+
211
+ const invalidFrozenCoreEntries = resolveFrozenCoreEntries(mode.frozenCore).invalidEntries;
212
+ if (invalidFrozenCoreEntries.length > 0) {
213
+ issues.push(`unsupported frozen core entries: ${invalidFrozenCoreEntries.join(", ")}`);
214
+ }
215
+
216
+ return issues;
217
+ }
218
+
219
+ function validateAutoStatus(status, mode = null) {
220
+ const issues = [];
221
+ const normalizedStatus = (status.status || "").trim().toLowerCase();
222
+ if (!normalizedStatus || normalizedStatus === "idle") {
223
+ return issues;
224
+ }
225
+
226
+ if (!["running", "stopped", "failed", "completed"].includes(normalizedStatus)) {
227
+ issues.push(`invalid auto status: ${status.status}`);
228
+ return issues;
229
+ }
230
+
231
+ if (normalizedStatus === "running") {
232
+ if (!isMeaningful(status.currentStage)) {
233
+ issues.push("auto status is missing current stage");
234
+ }
235
+ if (!isMeaningful(status.startedAt)) {
236
+ issues.push("auto status is missing started at");
237
+ }
238
+ if (!isMeaningful(status.lastHeartbeat)) {
239
+ issues.push("auto status is missing last heartbeat");
240
+ }
241
+ }
242
+
243
+ if (mode && mode.allowedStages.length > 0 && isMeaningful(status.currentStage)) {
244
+ const currentStage = status.currentStage.trim().toLowerCase();
245
+ if (!mode.allowedStages.includes(currentStage)) {
246
+ issues.push(`auto status stage is outside allowed stages: ${status.currentStage}`);
247
+ }
248
+ }
249
+
250
+ if (
251
+ mode &&
252
+ ["running", "completed"].includes(normalizedStatus) &&
253
+ mode.approvalStatus !== "approved"
254
+ ) {
255
+ issues.push(`auto mode must be approved before it can be ${normalizedStatus}`);
256
+ }
257
+
258
+ return issues;
259
+ }
260
+
261
+ function snapshotPaths(targetDir, relativePaths) {
262
+ const snapshot = new Map();
263
+ for (const relativePath of relativePaths) {
264
+ const absolutePath = path.resolve(targetDir, relativePath);
265
+ snapshot.set(absolutePath, hashPathState(absolutePath));
266
+ }
267
+ return snapshot;
268
+ }
269
+
270
+ function changedSnapshotPaths(snapshot) {
271
+ const changed = [];
272
+ for (const [absolutePath, previousHash] of snapshot.entries()) {
273
+ if (hashPathState(absolutePath) !== previousHash) {
274
+ changed.push(absolutePath);
275
+ }
276
+ }
277
+ return changed;
278
+ }
279
+
280
+ function stageContractSnapshot(targetDir, stage) {
281
+ const workflowConfig = readWorkflowConfig(targetDir);
282
+ const resultsRoot = resolveProjectPath(targetDir, workflowConfig.results_root, "results");
283
+ const deliverablesRoot = resolveProjectPath(targetDir, workflowConfig.deliverables_root, path.join("docs", "research"));
284
+ const trackedPathsByStage = {
285
+ run: [resultsRoot],
286
+ iterate: [resultsRoot],
287
+ review: REVIEW_CONTEXT_FILES.map((relativePath) => path.resolve(targetDir, relativePath)),
288
+ report: [path.join(deliverablesRoot, "report.md")],
289
+ write: [
290
+ path.join(deliverablesRoot, "paper", "main.tex"),
291
+ path.join(deliverablesRoot, "paper", "sections"),
292
+ ],
293
+ };
294
+
295
+ const absolutePaths = trackedPathsByStage[stage] || [];
296
+ const snapshot = new Map();
297
+ for (const absolutePath of absolutePaths) {
298
+ snapshot.set(absolutePath, hashPathState(absolutePath));
299
+ }
300
+ return {
301
+ stage,
302
+ absolutePaths,
303
+ snapshot,
304
+ };
305
+ }
306
+
307
+ function verifyStageContract({ stage, snapshot }) {
308
+ const changedPaths = [];
309
+ for (const [absolutePath, previousHash] of snapshot.entries()) {
310
+ if (hashPathState(absolutePath) !== previousHash) {
311
+ changedPaths.push(absolutePath);
312
+ }
313
+ }
314
+
315
+ if (stage === "review") {
316
+ if (changedPaths.length === 0) {
317
+ throw new Error(
318
+ "review stage did not update canonical review context (.lab/context/decisions.md, state.md, open-questions.md, or evidence-index.md)"
319
+ );
320
+ }
321
+ return;
322
+ }
323
+
324
+ if (stage === "report") {
325
+ if (changedPaths.length === 0) {
326
+ throw new Error("report stage did not produce the deliverable report.md under deliverables_root");
327
+ }
328
+ return;
329
+ }
330
+
331
+ if (stage === "write") {
332
+ if (changedPaths.length === 0) {
333
+ throw new Error("write stage did not produce LaTeX output under deliverables_root/paper");
334
+ }
335
+ return;
336
+ }
337
+
338
+ if ((stage === "run" || stage === "iterate") && changedPaths.length === 0) {
339
+ throw new Error(`${stage} stage did not produce persistent outputs under results_root`);
340
+ }
341
+ }
342
+
343
+ function verifyPromotionWriteback(targetDir, snapshot) {
344
+ const changedPaths = changedSnapshotPaths(snapshot);
345
+ if (changedPaths.length !== PROMOTION_CANONICAL_FILES.length) {
346
+ throw new Error(
347
+ `promotion did not update canonical context: ${PROMOTION_CANONICAL_FILES.filter(
348
+ (relativePath) => !changedPaths.includes(path.resolve(targetDir, relativePath))
349
+ ).join(", ")}`
350
+ );
351
+ }
352
+ }
353
+
354
+ function splitAutoStages(allowedStages) {
355
+ const loopStages = allowedStages.filter((stage) => LOOP_AUTO_STAGES.has(stage));
356
+ const finalStages = allowedStages.filter((stage) => FINAL_AUTO_STAGES.has(stage));
357
+ return { loopStages, finalStages };
358
+ }
359
+
360
+ function resolveStageCommand(mode, stage, commandOverride = "") {
361
+ return isMeaningful(commandOverride) ? commandOverride : mode.stageCommands[stage];
362
+ }
363
+
364
+ module.exports = {
365
+ ALLOWED_AUTO_STAGES,
366
+ AUTO_LEVEL_STAGE_ENVELOPES,
367
+ FINAL_AUTO_STAGES,
368
+ FROZEN_CORE_ALIASES,
369
+ LOOP_AUTO_STAGES,
370
+ PROMOTION_CANONICAL_FILES,
371
+ REVIEW_CONTEXT_FILES,
372
+ VALID_APPROVAL_STATUSES,
373
+ VALID_TERMINAL_GOAL_TYPES,
374
+ changedSnapshotPaths,
375
+ detectFrozenCoreChanges,
376
+ hashPathState,
377
+ resolveFrozenCoreEntries,
378
+ resolveStageCommand,
379
+ snapshotFrozenCore,
380
+ snapshotPaths,
381
+ splitAutoStages,
382
+ stageContractSnapshot,
383
+ validateAutoMode,
384
+ validateAutoStatus,
385
+ verifyPromotionWriteback,
386
+ verifyStageContract,
387
+ };