@hongmaple0820/scale-engine 0.27.0 → 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/README.en.md +74 -3
- package/README.md +75 -3
- package/dist/api/cli.js +376 -7
- package/dist/api/cli.js.map +1 -1
- package/dist/runtime/AiOsRuntime.d.ts +291 -0
- package/dist/runtime/AiOsRuntime.js +1031 -1
- package/dist/runtime/AiOsRuntime.js.map +1 -1
- package/dist/workflow/GovernanceTemplatePacks.js +10 -3
- package/dist/workflow/GovernanceTemplatePacks.js.map +1 -1
- package/dist/workflow/UpgradeManager.d.ts +4 -1
- package/dist/workflow/UpgradeManager.js +35 -0
- package/dist/workflow/UpgradeManager.js.map +1 -1
- package/docs/AI_ENGINEERING_OS_POSITIONING.md +141 -11
- package/docs/README.md +1 -1
- package/docs/start/README.md +2 -1
- package/docs/start/quickstart.md +6 -0
- package/docs/start/workflow-upgrade.md +17 -0
- package/docs/workflow/README.md +2 -1
- package/package.json +1 -1
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
3
|
import { buildContextPack, scanContextBudget, } from '../context/ContextBudget.js';
|
|
3
4
|
import { createGovernanceRoiReport, } from '../governance/GovernanceRoi.js';
|
|
4
5
|
import { evaluateProgressiveGovernance, } from '../governance/ProgressiveGovernance.js';
|
|
5
6
|
import { MemoryFabric, recallMemoryProviders, } from '../memory/index.js';
|
|
6
7
|
import { createSkillPlan, loadSkillRoutingPolicy, } from '../skills/routing/index.js';
|
|
8
|
+
import { runSafeCommand } from '../tools/SafeCommandRunner.js';
|
|
7
9
|
import { SCALE_ENGINE_VERSION } from '../version.js';
|
|
10
|
+
import { resolveVerificationTargets, } from '../workflow/VerificationProfile.js';
|
|
11
|
+
import { RuntimeEvidenceLedger } from './RuntimeEvidenceLedger.js';
|
|
8
12
|
export async function createAiOsPlan(input) {
|
|
9
13
|
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
10
14
|
const scaleDir = input.scaleDir ?? '.scale';
|
|
@@ -93,6 +97,1032 @@ export async function createAiOsPlan(input) {
|
|
|
93
97
|
recommendations: recommendations({ governance, context, memoryRecall, skillPlan }),
|
|
94
98
|
};
|
|
95
99
|
}
|
|
100
|
+
export async function createAiOsRun(input) {
|
|
101
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
102
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
103
|
+
const mode = input.mode ?? 'dry-run';
|
|
104
|
+
const plan = await createAiOsPlan({ ...input, projectDir, scaleDir });
|
|
105
|
+
const generatedAt = new Date().toISOString();
|
|
106
|
+
const runReportPath = resolveRunReportPath(projectDir, scaleDir, plan.task.taskId ?? `AIOS-RUN-${Date.now()}`);
|
|
107
|
+
const steps = buildRunSteps(plan);
|
|
108
|
+
const verification = await runGuardedVerification({
|
|
109
|
+
projectDir,
|
|
110
|
+
scaleDir,
|
|
111
|
+
plan,
|
|
112
|
+
steps,
|
|
113
|
+
commands: input.verificationCommands ?? [],
|
|
114
|
+
timeout: input.commandTimeoutMs,
|
|
115
|
+
allowShell: input.allowShell,
|
|
116
|
+
enabled: mode === 'guarded',
|
|
117
|
+
});
|
|
118
|
+
const failureCandidates = buildFailureLearningCandidates(plan, steps);
|
|
119
|
+
const evidence = summarizeRunEvidence(steps);
|
|
120
|
+
const status = steps.some(step => step.status === 'blocked') ? 'blocked' : 'ready';
|
|
121
|
+
const report = {
|
|
122
|
+
version: SCALE_ENGINE_VERSION,
|
|
123
|
+
generatedAt,
|
|
124
|
+
mode,
|
|
125
|
+
dryRun: mode === 'dry-run',
|
|
126
|
+
status,
|
|
127
|
+
plan,
|
|
128
|
+
steps,
|
|
129
|
+
evidence,
|
|
130
|
+
verification,
|
|
131
|
+
failureLearning: {
|
|
132
|
+
status: failureCandidates.length > 0 ? 'candidate-created' : 'idle',
|
|
133
|
+
candidates: failureCandidates,
|
|
134
|
+
},
|
|
135
|
+
artifacts: {
|
|
136
|
+
runReport: runReportPath,
|
|
137
|
+
},
|
|
138
|
+
nextActions: buildRunNextActions(steps, mode),
|
|
139
|
+
};
|
|
140
|
+
writeAiOsRunReport(runReportPath, report);
|
|
141
|
+
return report;
|
|
142
|
+
}
|
|
143
|
+
export function createAiOsDashboard(input = {}) {
|
|
144
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
145
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
146
|
+
const runsDir = resolveRunsDir(projectDir, scaleDir);
|
|
147
|
+
const warnings = [];
|
|
148
|
+
const reports = readAiOsRunReports(runsDir, warnings);
|
|
149
|
+
const latestRuns = reports
|
|
150
|
+
.sort((a, b) => Date.parse(b.generatedAt) - Date.parse(a.generatedAt))
|
|
151
|
+
.slice(0, input.limit ?? 10)
|
|
152
|
+
.map(toDashboardRunSummary);
|
|
153
|
+
const summary = {
|
|
154
|
+
totalRuns: reports.length,
|
|
155
|
+
readyRuns: reports.filter(report => report.status === 'ready').length,
|
|
156
|
+
blockedRuns: reports.filter(report => report.status === 'blocked').length,
|
|
157
|
+
dryRunRuns: reports.filter(report => report.mode === 'dry-run').length,
|
|
158
|
+
guardedRuns: reports.filter(report => report.mode === 'guarded').length,
|
|
159
|
+
verificationCommands: reports.reduce((sum, report) => sum + report.verification.commands.length, 0),
|
|
160
|
+
failedVerificationCommands: reports.reduce((sum, report) => sum + report.verification.commands.filter(command => command.status === 'failed').length, 0),
|
|
161
|
+
pendingEvidence: reports.reduce((sum, report) => sum + report.evidence.pending.length, 0),
|
|
162
|
+
failureLearningCandidates: reports.reduce((sum, report) => sum + report.failureLearning.candidates.length, 0),
|
|
163
|
+
};
|
|
164
|
+
const health = summarizeDashboardHealth(summary);
|
|
165
|
+
return {
|
|
166
|
+
version: SCALE_ENGINE_VERSION,
|
|
167
|
+
generatedAt: new Date().toISOString(),
|
|
168
|
+
runsDir,
|
|
169
|
+
summary,
|
|
170
|
+
health,
|
|
171
|
+
latestRuns,
|
|
172
|
+
recommendations: dashboardRecommendations(summary),
|
|
173
|
+
warnings,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function createAiOsBenchmark(input = {}) {
|
|
177
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
178
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
179
|
+
const scenarios = defaultBenchmarkScenarios(input.budget);
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const scenario of scenarios) {
|
|
182
|
+
const plan = await createAiOsPlan({
|
|
183
|
+
projectDir,
|
|
184
|
+
scaleDir,
|
|
185
|
+
taskId: `BENCH-${scenario.id}`,
|
|
186
|
+
task: scenario.task,
|
|
187
|
+
level: scenario.level,
|
|
188
|
+
files: scenario.files,
|
|
189
|
+
services: scenario.services,
|
|
190
|
+
budget: scenario.budget,
|
|
191
|
+
});
|
|
192
|
+
results.push({
|
|
193
|
+
id: scenario.id,
|
|
194
|
+
task: scenario.task,
|
|
195
|
+
level: scenario.level,
|
|
196
|
+
governanceMode: plan.governance.effectiveMode,
|
|
197
|
+
metrics: {
|
|
198
|
+
estimatedTokens: plan.context.totalEstimatedTokens,
|
|
199
|
+
budget: plan.context.task.budget,
|
|
200
|
+
estimatedTokenSavings: plan.context.compiler?.estimatedTokenSavings ?? 0,
|
|
201
|
+
memoryItems: plan.memory.items.length,
|
|
202
|
+
selectedProviders: plan.memory.selectedProviders,
|
|
203
|
+
skillSteps: plan.skillPlan.executionPlan.steps.length,
|
|
204
|
+
requiredSkillSteps: plan.skillPlan.executionPlan.steps.filter(step => step.required).length,
|
|
205
|
+
gates: plan.adaptiveWorkflow.gates.length,
|
|
206
|
+
roiModules: plan.roi.modules.length,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const summary = summarizeBenchmark(results);
|
|
211
|
+
const generatedAt = new Date().toISOString();
|
|
212
|
+
const benchmarkReport = resolveBenchmarkReportPath(projectDir, scaleDir);
|
|
213
|
+
const report = {
|
|
214
|
+
version: SCALE_ENGINE_VERSION,
|
|
215
|
+
generatedAt,
|
|
216
|
+
scenarios: results,
|
|
217
|
+
summary,
|
|
218
|
+
dashboard: createAiOsDashboard({ projectDir, scaleDir }),
|
|
219
|
+
artifacts: {
|
|
220
|
+
benchmarkReport,
|
|
221
|
+
},
|
|
222
|
+
recommendations: benchmarkRecommendations(summary),
|
|
223
|
+
};
|
|
224
|
+
writeAiOsBenchmarkReport(benchmarkReport, report);
|
|
225
|
+
return report;
|
|
226
|
+
}
|
|
227
|
+
export function createAiOsMigration(input = {}) {
|
|
228
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
229
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
230
|
+
const scaleRoot = isAbsolute(scaleDir) ? scaleDir : join(projectDir, scaleDir);
|
|
231
|
+
const requiredDirs = [
|
|
232
|
+
join(scaleRoot, 'ai-os'),
|
|
233
|
+
join(scaleRoot, 'ai-os', 'runs'),
|
|
234
|
+
join(scaleRoot, 'ai-os', 'benchmarks'),
|
|
235
|
+
join(scaleRoot, 'ai-os', 'migrations'),
|
|
236
|
+
];
|
|
237
|
+
const created = [];
|
|
238
|
+
const existing = [];
|
|
239
|
+
for (const dir of requiredDirs) {
|
|
240
|
+
if (existsSync(dir)) {
|
|
241
|
+
existing.push(normalizeProjectPath(projectDir, dir));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
created.push(normalizeProjectPath(projectDir, dir));
|
|
246
|
+
}
|
|
247
|
+
const migrationReport = join(scaleRoot, 'ai-os', 'migrations', 'migration.json');
|
|
248
|
+
const report = {
|
|
249
|
+
version: SCALE_ENGINE_VERSION,
|
|
250
|
+
generatedAt: new Date().toISOString(),
|
|
251
|
+
status: created.length > 0 ? 'migrated' : 'compatible',
|
|
252
|
+
scaleRoot,
|
|
253
|
+
created,
|
|
254
|
+
existing,
|
|
255
|
+
files: {
|
|
256
|
+
migrationReport,
|
|
257
|
+
},
|
|
258
|
+
warnings: [],
|
|
259
|
+
nextActions: created.length > 0
|
|
260
|
+
? ['Run `scale ai-os run --dry-run --json` to create the first AI OS runtime report.']
|
|
261
|
+
: ['AI OS runtime directories are compatible; continue with run, dashboard, or benchmark commands.'],
|
|
262
|
+
};
|
|
263
|
+
writeFileSync(migrationReport, JSON.stringify(report, null, 2), 'utf-8');
|
|
264
|
+
return report;
|
|
265
|
+
}
|
|
266
|
+
export function createAiOsDoctor(input = {}) {
|
|
267
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
268
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
269
|
+
const scaleRoot = resolveScaleRoot(projectDir, scaleDir);
|
|
270
|
+
const benchmarkMaxAgeHours = input.benchmarkMaxAgeHours ?? 24;
|
|
271
|
+
const lang = input.lang ?? 'en';
|
|
272
|
+
const warnings = [];
|
|
273
|
+
const dashboard = createAiOsDashboard({ projectDir, scaleDir });
|
|
274
|
+
const benchmark = inspectBenchmarkReport(projectDir, scaleDir, benchmarkMaxAgeHours, warnings);
|
|
275
|
+
const requiredDirs = [
|
|
276
|
+
join(scaleRoot, 'ai-os'),
|
|
277
|
+
join(scaleRoot, 'ai-os', 'runs'),
|
|
278
|
+
join(scaleRoot, 'ai-os', 'benchmarks'),
|
|
279
|
+
join(scaleRoot, 'ai-os', 'migrations'),
|
|
280
|
+
];
|
|
281
|
+
const missingDirs = requiredDirs.filter(dir => !existsSync(dir)).map(dir => normalizeProjectPath(projectDir, dir));
|
|
282
|
+
const checks = [
|
|
283
|
+
{
|
|
284
|
+
id: 'ai-os-runtime-dirs',
|
|
285
|
+
title: 'AI OS runtime directories',
|
|
286
|
+
status: missingDirs.length === 0 ? 'passed' : 'blocked',
|
|
287
|
+
summary: missingDirs.length === 0
|
|
288
|
+
? 'Required AI OS runtime directories exist.'
|
|
289
|
+
: `Missing AI OS runtime directories: ${missingDirs.join(', ')}.`,
|
|
290
|
+
evidence: missingDirs.length === 0 ? requiredDirs.map(dir => normalizeProjectPath(projectDir, dir)) : missingDirs,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'ai-os-run-history',
|
|
294
|
+
title: 'AI OS run history',
|
|
295
|
+
status: dashboard.summary.totalRuns > 0 ? 'passed' : 'warning',
|
|
296
|
+
summary: dashboard.summary.totalRuns > 0
|
|
297
|
+
? `${dashboard.summary.totalRuns} run report(s), ${dashboard.summary.guardedRuns} guarded.`
|
|
298
|
+
: 'No AI OS run reports found yet.',
|
|
299
|
+
evidence: dashboard.latestRuns.map(run => run.runReport),
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'ai-os-dashboard-health',
|
|
303
|
+
title: 'AI OS dashboard health',
|
|
304
|
+
status: dashboard.health.status === 'blocked'
|
|
305
|
+
? 'blocked'
|
|
306
|
+
: dashboard.health.status === 'healthy' ? 'passed' : 'warning',
|
|
307
|
+
summary: `${dashboard.health.status} (${dashboard.health.score}): ${dashboard.health.reasons.join('; ')}`,
|
|
308
|
+
evidence: dashboard.health.reasons,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'ai-os-benchmark',
|
|
312
|
+
title: 'AI OS benchmark evidence',
|
|
313
|
+
status: benchmark.status === 'fresh' ? 'passed' : benchmark.status === 'invalid' ? 'blocked' : 'warning',
|
|
314
|
+
summary: summarizeBenchmarkDoctor(benchmark),
|
|
315
|
+
evidence: [benchmark.reportPath],
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
const summary = {
|
|
319
|
+
totalChecks: checks.length,
|
|
320
|
+
passedChecks: checks.filter(check => check.status === 'passed').length,
|
|
321
|
+
warningChecks: checks.filter(check => check.status === 'warning').length,
|
|
322
|
+
blockedChecks: checks.filter(check => check.status === 'blocked').length,
|
|
323
|
+
};
|
|
324
|
+
const status = summary.blockedChecks > 0
|
|
325
|
+
? 'blocked'
|
|
326
|
+
: summary.warningChecks > 0 ? 'warning' : 'ready';
|
|
327
|
+
return {
|
|
328
|
+
version: SCALE_ENGINE_VERSION,
|
|
329
|
+
generatedAt: new Date().toISOString(),
|
|
330
|
+
status,
|
|
331
|
+
projectDir,
|
|
332
|
+
scaleRoot,
|
|
333
|
+
dashboard,
|
|
334
|
+
benchmark,
|
|
335
|
+
checks,
|
|
336
|
+
summary,
|
|
337
|
+
warnings: [...warnings, ...dashboard.warnings],
|
|
338
|
+
nextActions: aiOsDoctorNextActions({ status, checks, dashboard, benchmark, lang }),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
export async function createAiOsAdoption(input) {
|
|
342
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
343
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
344
|
+
const scaleRoot = resolveScaleRoot(projectDir, scaleDir);
|
|
345
|
+
const generatedAt = new Date().toISOString();
|
|
346
|
+
const migration = createAiOsMigration({ projectDir, scaleDir });
|
|
347
|
+
const run = await createAiOsRun({
|
|
348
|
+
...input,
|
|
349
|
+
projectDir,
|
|
350
|
+
scaleDir,
|
|
351
|
+
taskId: input.taskId ?? `AIOS-ADOPT-${Date.now()}`,
|
|
352
|
+
mode: 'dry-run',
|
|
353
|
+
});
|
|
354
|
+
const benchmark = await createAiOsBenchmark({
|
|
355
|
+
projectDir,
|
|
356
|
+
scaleDir,
|
|
357
|
+
budget: input.budget,
|
|
358
|
+
});
|
|
359
|
+
const doctor = createAiOsDoctor({
|
|
360
|
+
projectDir,
|
|
361
|
+
scaleDir,
|
|
362
|
+
benchmarkMaxAgeHours: input.benchmarkMaxAgeHours,
|
|
363
|
+
lang: input.lang,
|
|
364
|
+
});
|
|
365
|
+
const phases = [
|
|
366
|
+
{
|
|
367
|
+
id: 'migrate',
|
|
368
|
+
status: migration.status === 'migrated' || migration.status === 'compatible' ? 'passed' : 'blocked',
|
|
369
|
+
summary: migration.status === 'migrated'
|
|
370
|
+
? `Created ${migration.created.length} AI OS runtime path(s).`
|
|
371
|
+
: 'AI OS runtime paths were already compatible.',
|
|
372
|
+
evidence: [migration.files.migrationReport, ...migration.created, ...migration.existing],
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
id: 'first-run',
|
|
376
|
+
status: run.status === 'ready' ? 'passed' : 'blocked',
|
|
377
|
+
summary: `Created first ${run.mode} AI OS run report with ${run.steps.length} step(s).`,
|
|
378
|
+
evidence: [run.artifacts.runReport, ...run.evidence.produced],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'benchmark',
|
|
382
|
+
status: benchmark.summary.scenarios > 0 ? 'passed' : 'blocked',
|
|
383
|
+
summary: `Created AI OS benchmark report with ${benchmark.summary.scenarios} scenario(s).`,
|
|
384
|
+
evidence: [benchmark.artifacts.benchmarkReport],
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: 'doctor',
|
|
388
|
+
status: doctor.status === 'ready' ? 'passed' : doctor.status === 'warning' ? 'warning' : 'blocked',
|
|
389
|
+
summary: `AI OS doctor status is ${doctor.status}: ${doctor.summary.passedChecks}/${doctor.summary.totalChecks} checks passed.`,
|
|
390
|
+
evidence: doctor.checks.flatMap(check => check.evidence),
|
|
391
|
+
},
|
|
392
|
+
];
|
|
393
|
+
const status = phases.some(phase => phase.status === 'blocked')
|
|
394
|
+
? 'blocked'
|
|
395
|
+
: phases.some(phase => phase.status === 'warning') ? 'warning' : 'ready';
|
|
396
|
+
const adoptionReport = resolveAdoptionReportPath(projectDir, scaleDir);
|
|
397
|
+
const report = {
|
|
398
|
+
version: SCALE_ENGINE_VERSION,
|
|
399
|
+
generatedAt,
|
|
400
|
+
status,
|
|
401
|
+
projectDir,
|
|
402
|
+
scaleRoot,
|
|
403
|
+
phases,
|
|
404
|
+
migration,
|
|
405
|
+
run,
|
|
406
|
+
benchmark,
|
|
407
|
+
doctor,
|
|
408
|
+
artifacts: {
|
|
409
|
+
migrationReport: migration.files.migrationReport,
|
|
410
|
+
runReport: run.artifacts.runReport,
|
|
411
|
+
benchmarkReport: benchmark.artifacts.benchmarkReport,
|
|
412
|
+
adoptionReport,
|
|
413
|
+
},
|
|
414
|
+
warnings: [...migration.warnings, ...doctor.warnings],
|
|
415
|
+
nextActions: aiOsAdoptionNextActions(status, input.lang ?? 'en'),
|
|
416
|
+
};
|
|
417
|
+
writeFileSync(adoptionReport, JSON.stringify(report, null, 2), 'utf-8');
|
|
418
|
+
return report;
|
|
419
|
+
}
|
|
420
|
+
export function createAiOsStatus(input = {}) {
|
|
421
|
+
const projectDir = resolve(input.projectDir ?? process.cwd());
|
|
422
|
+
const scaleDir = input.scaleDir ?? '.scale';
|
|
423
|
+
const lang = input.lang ?? 'en';
|
|
424
|
+
const scaleRoot = resolveScaleRoot(projectDir, scaleDir);
|
|
425
|
+
const warnings = [];
|
|
426
|
+
const runsDir = resolveRunsDir(projectDir, scaleDir);
|
|
427
|
+
const runReports = readAiOsRunReports(runsDir, warnings);
|
|
428
|
+
const dashboard = createAiOsDashboard({ projectDir, scaleDir });
|
|
429
|
+
const doctor = createAiOsDoctor({
|
|
430
|
+
projectDir,
|
|
431
|
+
scaleDir,
|
|
432
|
+
benchmarkMaxAgeHours: input.benchmarkMaxAgeHours,
|
|
433
|
+
lang,
|
|
434
|
+
});
|
|
435
|
+
const requiredDirs = [
|
|
436
|
+
join(scaleRoot, 'ai-os'),
|
|
437
|
+
join(scaleRoot, 'ai-os', 'runs'),
|
|
438
|
+
join(scaleRoot, 'ai-os', 'benchmarks'),
|
|
439
|
+
join(scaleRoot, 'ai-os', 'migrations'),
|
|
440
|
+
];
|
|
441
|
+
const missingDirs = requiredDirs.filter(dir => !existsSync(dir)).map(dir => normalizeProjectPath(projectDir, dir));
|
|
442
|
+
const runEvidence = runReports.map(report => report.artifacts.runReport);
|
|
443
|
+
const verificationEvidence = runReports
|
|
444
|
+
.filter(report => report.verification.commands.length > 0)
|
|
445
|
+
.flatMap(report => [report.artifacts.runReport, ...report.verification.commands.map(command => command.evidenceId)]);
|
|
446
|
+
const verificationRecommendations = buildVerificationRecommendations(projectDir, scaleDir, lang);
|
|
447
|
+
const benchmarkReport = resolveBenchmarkReportPath(projectDir, scaleDir);
|
|
448
|
+
const adoptionReport = resolveAdoptionReportPath(projectDir, scaleDir);
|
|
449
|
+
const checks = [
|
|
450
|
+
{
|
|
451
|
+
id: 'runtime-dirs',
|
|
452
|
+
title: 'Runtime directories',
|
|
453
|
+
status: missingDirs.length === 0 ? 'ready' : 'blocked',
|
|
454
|
+
summary: missingDirs.length === 0
|
|
455
|
+
? 'AI OS runtime directories exist.'
|
|
456
|
+
: `${missingDirs.length} AI OS runtime director${missingDirs.length === 1 ? 'y is' : 'ies are'} missing.`,
|
|
457
|
+
evidence: missingDirs.length === 0 ? requiredDirs.map(dir => normalizeProjectPath(projectDir, dir)) : missingDirs,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: 'plan-evidence',
|
|
461
|
+
title: 'Plan evidence',
|
|
462
|
+
status: runReports.length > 0 ? 'ready' : 'blocked',
|
|
463
|
+
summary: runReports.length > 0
|
|
464
|
+
? `${runReports.length} run report(s) include embedded AI OS plans.`
|
|
465
|
+
: 'No AI OS plan evidence is persisted through a run report.',
|
|
466
|
+
evidence: runEvidence,
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: 'run-evidence',
|
|
470
|
+
title: 'Run evidence',
|
|
471
|
+
status: runReports.length > 0 ? 'ready' : 'blocked',
|
|
472
|
+
summary: runReports.length > 0
|
|
473
|
+
? `${runReports.length} AI OS run report(s) found.`
|
|
474
|
+
: 'No AI OS run reports found.',
|
|
475
|
+
evidence: runEvidence,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
id: 'verification-evidence',
|
|
479
|
+
title: 'Verification evidence',
|
|
480
|
+
status: verificationEvidence.length > 0 ? 'ready' : 'blocked',
|
|
481
|
+
summary: verificationEvidence.length > 0
|
|
482
|
+
? `${verificationEvidence.length} guarded verification evidence reference(s) found.`
|
|
483
|
+
: 'No guarded verification evidence found.',
|
|
484
|
+
evidence: verificationEvidence,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: 'dashboard-health',
|
|
488
|
+
title: 'Dashboard health',
|
|
489
|
+
status: dashboard.health.status === 'healthy'
|
|
490
|
+
? 'ready'
|
|
491
|
+
: dashboard.health.status === 'empty' || dashboard.health.status === 'blocked' ? 'blocked' : 'warning',
|
|
492
|
+
summary: `${dashboard.health.status} (${dashboard.health.score}): ${dashboard.health.reasons.join('; ')}`,
|
|
493
|
+
evidence: [runsDir],
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: 'benchmark-evidence',
|
|
497
|
+
title: 'Benchmark evidence',
|
|
498
|
+
status: doctor.benchmark.status === 'fresh' ? 'ready' : doctor.benchmark.status === 'stale' ? 'warning' : 'blocked',
|
|
499
|
+
summary: summarizeBenchmarkDoctor(doctor.benchmark),
|
|
500
|
+
evidence: [benchmarkReport],
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
id: 'adoption-evidence',
|
|
504
|
+
title: 'Adoption evidence',
|
|
505
|
+
status: existsSync(adoptionReport) ? 'ready' : 'blocked',
|
|
506
|
+
summary: existsSync(adoptionReport)
|
|
507
|
+
? 'AI OS adoption report exists.'
|
|
508
|
+
: 'No AI OS adoption report found.',
|
|
509
|
+
evidence: [adoptionReport],
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
const summary = {
|
|
513
|
+
total: checks.length,
|
|
514
|
+
ready: checks.filter(check => check.status === 'ready').length,
|
|
515
|
+
warning: checks.filter(check => check.status === 'warning').length,
|
|
516
|
+
blocked: checks.filter(check => check.status === 'blocked').length,
|
|
517
|
+
};
|
|
518
|
+
const status = summary.blocked > 0 ? 'blocked' : summary.warning > 0 ? 'warning' : 'ready';
|
|
519
|
+
return {
|
|
520
|
+
version: SCALE_ENGINE_VERSION,
|
|
521
|
+
generatedAt: new Date().toISOString(),
|
|
522
|
+
status,
|
|
523
|
+
projectDir,
|
|
524
|
+
scaleRoot,
|
|
525
|
+
checks,
|
|
526
|
+
summary,
|
|
527
|
+
dashboard,
|
|
528
|
+
doctor,
|
|
529
|
+
verificationRecommendations,
|
|
530
|
+
nextActions: aiOsStatusNextActions(status, checks, lang, verificationRecommendations),
|
|
531
|
+
warnings: [...warnings, ...doctor.warnings],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function buildRunSteps(plan) {
|
|
535
|
+
const steps = new Map();
|
|
536
|
+
const upsert = (step) => steps.set(step.id, step);
|
|
537
|
+
upsert({
|
|
538
|
+
id: 'runtime-plan',
|
|
539
|
+
kind: 'plan',
|
|
540
|
+
title: 'Create unified AI OS runtime plan',
|
|
541
|
+
status: 'passed',
|
|
542
|
+
required: true,
|
|
543
|
+
summary: `Governance mode ${plan.governance.effectiveMode}; ${plan.skillPlan.executionPlan.steps.length} skill step(s).`,
|
|
544
|
+
evidence: ['governance', 'context', 'memory', 'skillPlan', 'roi'],
|
|
545
|
+
});
|
|
546
|
+
upsert({
|
|
547
|
+
id: 'context-compiler',
|
|
548
|
+
kind: 'context',
|
|
549
|
+
title: 'Compile task context',
|
|
550
|
+
status: 'passed',
|
|
551
|
+
required: true,
|
|
552
|
+
summary: `${plan.context.totalEstimatedTokens}/${plan.context.task.budget} estimated tokens; saved ${plan.context.compiler?.estimatedTokenSavings ?? 0}.`,
|
|
553
|
+
evidence: ['context.compiler', 'context.includedSections', 'context.omittedSections'],
|
|
554
|
+
dependsOn: ['runtime-plan'],
|
|
555
|
+
});
|
|
556
|
+
upsert({
|
|
557
|
+
id: 'memory-provider-recall',
|
|
558
|
+
kind: 'memory',
|
|
559
|
+
title: 'Recall provider-backed memory',
|
|
560
|
+
status: 'passed',
|
|
561
|
+
required: true,
|
|
562
|
+
summary: `${plan.memory.items.length} recalled item(s); providers ${plan.memory.providerOrder.join(' -> ')}.`,
|
|
563
|
+
evidence: ['memory.providerOrder', 'memory.selectedProviders', 'memory.items'],
|
|
564
|
+
dependsOn: ['runtime-plan'],
|
|
565
|
+
});
|
|
566
|
+
for (const gate of plan.adaptiveWorkflow.gates) {
|
|
567
|
+
if (steps.has(gate))
|
|
568
|
+
continue;
|
|
569
|
+
upsert({
|
|
570
|
+
id: gate,
|
|
571
|
+
kind: gate === 'runtime-evidence' ? 'evidence' : 'gate',
|
|
572
|
+
title: `Satisfy ${gate} gate`,
|
|
573
|
+
status: 'planned',
|
|
574
|
+
required: true,
|
|
575
|
+
summary: `Required by ${plan.adaptiveWorkflow.strategy} in ${plan.adaptiveWorkflow.mode} mode.`,
|
|
576
|
+
evidence: [`gate.${gate}`],
|
|
577
|
+
dependsOn: ['runtime-plan'],
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
for (const skillStep of plan.skillPlan.executionPlan.steps) {
|
|
581
|
+
upsert({
|
|
582
|
+
id: `skill:${skillStep.id}`,
|
|
583
|
+
kind: 'skill',
|
|
584
|
+
title: `${skillStep.kind}: ${skillStep.id}`,
|
|
585
|
+
status: 'planned',
|
|
586
|
+
required: skillStep.required,
|
|
587
|
+
summary: `${skillStep.reason} Fallback: ${skillStep.fallback}.`,
|
|
588
|
+
evidence: [skillStep.evidenceRequired],
|
|
589
|
+
dependsOn: ['skill-evidence'],
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
upsert({
|
|
593
|
+
id: 'failure-learning',
|
|
594
|
+
kind: 'learning',
|
|
595
|
+
title: 'Prepare failure learning settlement',
|
|
596
|
+
status: 'planned',
|
|
597
|
+
required: false,
|
|
598
|
+
summary: 'Create lesson or rule candidates only when a gate, verification step, or evidence requirement fails.',
|
|
599
|
+
evidence: ['failureLearning.candidates'],
|
|
600
|
+
dependsOn: ['runtime-evidence'],
|
|
601
|
+
});
|
|
602
|
+
return [...steps.values()];
|
|
603
|
+
}
|
|
604
|
+
async function runGuardedVerification(options) {
|
|
605
|
+
if (!options.enabled || options.commands.length === 0) {
|
|
606
|
+
return { commands: [], allPassed: options.commands.length === 0 };
|
|
607
|
+
}
|
|
608
|
+
const ledger = new RuntimeEvidenceLedger({
|
|
609
|
+
projectDir: options.projectDir,
|
|
610
|
+
scaleDir: options.scaleDir,
|
|
611
|
+
});
|
|
612
|
+
const reports = [];
|
|
613
|
+
for (const [index, command] of options.commands.entries()) {
|
|
614
|
+
const stepId = `verify-command:${index + 1}`;
|
|
615
|
+
let result;
|
|
616
|
+
try {
|
|
617
|
+
result = await runSafeCommand(command, {
|
|
618
|
+
cwd: options.projectDir,
|
|
619
|
+
timeout: options.timeout ?? 120_000,
|
|
620
|
+
allowShell: options.allowShell,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
result = {
|
|
625
|
+
exitCode: 1,
|
|
626
|
+
stdout: '',
|
|
627
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
const passed = result.exitCode === 0;
|
|
631
|
+
const evidence = ledger.record({
|
|
632
|
+
taskId: options.plan.task.taskId,
|
|
633
|
+
kind: 'command',
|
|
634
|
+
title: `AI OS verification command ${index + 1}`,
|
|
635
|
+
status: passed ? 'passed' : 'failed',
|
|
636
|
+
command,
|
|
637
|
+
exitCode: result.exitCode,
|
|
638
|
+
summary: passed
|
|
639
|
+
? `Guarded verification command passed: ${command}`
|
|
640
|
+
: `Guarded verification command failed with exit code ${result.exitCode}: ${command}`,
|
|
641
|
+
metadata: {
|
|
642
|
+
aiOsRun: true,
|
|
643
|
+
stepId,
|
|
644
|
+
stdoutPreview: truncate(result.stdout),
|
|
645
|
+
stderrPreview: truncate(result.stderr),
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
reports.push({
|
|
649
|
+
command,
|
|
650
|
+
status: passed ? 'passed' : 'failed',
|
|
651
|
+
exitCode: result.exitCode,
|
|
652
|
+
stdout: result.stdout,
|
|
653
|
+
stderr: result.stderr,
|
|
654
|
+
evidenceId: evidence.id,
|
|
655
|
+
});
|
|
656
|
+
options.steps.push({
|
|
657
|
+
id: stepId,
|
|
658
|
+
kind: 'evidence',
|
|
659
|
+
title: `Run verification command ${index + 1}`,
|
|
660
|
+
status: passed ? 'passed' : 'blocked',
|
|
661
|
+
required: true,
|
|
662
|
+
summary: passed
|
|
663
|
+
? `Command passed and runtime evidence was recorded as ${evidence.id}.`
|
|
664
|
+
: `Command failed and runtime evidence was recorded as ${evidence.id}.`,
|
|
665
|
+
evidence: [evidence.id],
|
|
666
|
+
dependsOn: ['runtime-evidence'],
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const runtimeEvidenceStep = options.steps.find(step => step.id === 'runtime-evidence');
|
|
670
|
+
if (runtimeEvidenceStep) {
|
|
671
|
+
const allPassed = reports.every(report => report.status === 'passed');
|
|
672
|
+
runtimeEvidenceStep.status = allPassed ? 'passed' : 'blocked';
|
|
673
|
+
runtimeEvidenceStep.summary = allPassed
|
|
674
|
+
? `${reports.length} guarded verification command(s) passed and were recorded as runtime evidence.`
|
|
675
|
+
: `${reports.filter(report => report.status === 'failed').length}/${reports.length} guarded verification command(s) failed.`;
|
|
676
|
+
runtimeEvidenceStep.evidence = reports.map(report => report.evidenceId);
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
commands: reports,
|
|
680
|
+
allPassed: reports.every(report => report.status === 'passed'),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function summarizeRunEvidence(steps) {
|
|
684
|
+
const required = new Set();
|
|
685
|
+
const produced = new Set();
|
|
686
|
+
const pending = new Set();
|
|
687
|
+
for (const step of steps) {
|
|
688
|
+
if (step.required) {
|
|
689
|
+
for (const item of evidenceCategory(step))
|
|
690
|
+
required.add(item);
|
|
691
|
+
}
|
|
692
|
+
if (step.status === 'passed') {
|
|
693
|
+
for (const item of evidenceCategory(step))
|
|
694
|
+
produced.add(item);
|
|
695
|
+
}
|
|
696
|
+
else if (step.required && step.status === 'planned') {
|
|
697
|
+
for (const item of evidenceCategory(step))
|
|
698
|
+
pending.add(item);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
required: [...required],
|
|
703
|
+
produced: [...produced],
|
|
704
|
+
pending: [...pending],
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function evidenceCategory(step) {
|
|
708
|
+
if (step.id === 'runtime-plan')
|
|
709
|
+
return ['ai-os-plan'];
|
|
710
|
+
if (step.id === 'context-compiler')
|
|
711
|
+
return ['context-compiler'];
|
|
712
|
+
if (step.id === 'memory-provider-recall')
|
|
713
|
+
return ['memory-provider-recall'];
|
|
714
|
+
if (step.id === 'skill-evidence' || step.kind === 'skill')
|
|
715
|
+
return ['skill-routing-engine'];
|
|
716
|
+
if (step.id === 'runtime-evidence' || step.kind === 'evidence')
|
|
717
|
+
return ['runtime-evidence'];
|
|
718
|
+
if (step.kind === 'gate')
|
|
719
|
+
return [`gate:${step.id}`];
|
|
720
|
+
return [step.id];
|
|
721
|
+
}
|
|
722
|
+
function buildFailureLearningCandidates(plan, steps) {
|
|
723
|
+
const hasBlockedVerification = steps.some(step => step.status === 'blocked' && step.id.startsWith('verify-command:'));
|
|
724
|
+
const failed = steps.filter(step => step.status === 'blocked' && !(hasBlockedVerification && step.id === 'runtime-evidence'));
|
|
725
|
+
return failed.map(step => ({
|
|
726
|
+
id: `AIO-FLC-${safePathSegment(plan.task.taskId ?? step.id)}-${safePathSegment(step.id)}`,
|
|
727
|
+
source: 'failed-step',
|
|
728
|
+
title: `Failure learning candidate: ${step.title}`,
|
|
729
|
+
summary: step.summary,
|
|
730
|
+
recommendedAction: 'resolve-before-promotion',
|
|
731
|
+
evidenceRefs: step.evidence,
|
|
732
|
+
promotable: false,
|
|
733
|
+
}));
|
|
734
|
+
}
|
|
735
|
+
function buildRunNextActions(steps, mode) {
|
|
736
|
+
const actions = [];
|
|
737
|
+
for (const step of steps) {
|
|
738
|
+
if (step.status !== 'planned' || !step.required)
|
|
739
|
+
continue;
|
|
740
|
+
if (step.kind === 'skill')
|
|
741
|
+
actions.push(`Execute required skill step "${step.title}" and attach evidence: ${step.evidence.join(', ')}.`);
|
|
742
|
+
else if (step.kind === 'evidence')
|
|
743
|
+
actions.push(`Record runtime evidence for "${step.id}" before claiming completion.`);
|
|
744
|
+
else if (step.kind === 'gate')
|
|
745
|
+
actions.push(`Satisfy gate "${step.id}" before ship.`);
|
|
746
|
+
}
|
|
747
|
+
if (mode === 'dry-run')
|
|
748
|
+
actions.push('Re-run with guarded execution only after reviewing the dry-run report.');
|
|
749
|
+
return actions;
|
|
750
|
+
}
|
|
751
|
+
function resolveRunReportPath(projectDir, scaleDir, taskId) {
|
|
752
|
+
return join(resolveRunsDir(projectDir, scaleDir), `${safePathSegment(taskId)}.json`);
|
|
753
|
+
}
|
|
754
|
+
function writeAiOsRunReport(path, report) {
|
|
755
|
+
const dir = dirname(path);
|
|
756
|
+
if (dir && !existsSync(dir))
|
|
757
|
+
mkdirSync(dir, { recursive: true });
|
|
758
|
+
writeFileSync(path, JSON.stringify(report, null, 2), 'utf-8');
|
|
759
|
+
}
|
|
760
|
+
function safePathSegment(value) {
|
|
761
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 120) || 'ai-os-run';
|
|
762
|
+
}
|
|
763
|
+
function truncate(value, max = 1000) {
|
|
764
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
765
|
+
}
|
|
766
|
+
function normalizeProjectPath(projectDir, path) {
|
|
767
|
+
const normalizedProject = resolve(projectDir);
|
|
768
|
+
const normalizedPath = resolve(path);
|
|
769
|
+
if (normalizedPath.startsWith(normalizedProject)) {
|
|
770
|
+
return normalizedPath.slice(normalizedProject.length + 1).replace(/\\/g, '/');
|
|
771
|
+
}
|
|
772
|
+
return normalizedPath.replace(/\\/g, '/');
|
|
773
|
+
}
|
|
774
|
+
function resolveScaleRoot(projectDir, scaleDir) {
|
|
775
|
+
return isAbsolute(scaleDir) ? scaleDir : join(projectDir, scaleDir);
|
|
776
|
+
}
|
|
777
|
+
function resolveRunsDir(projectDir, scaleDir) {
|
|
778
|
+
return join(resolveScaleRoot(projectDir, scaleDir), 'ai-os', 'runs');
|
|
779
|
+
}
|
|
780
|
+
function resolveBenchmarkReportPath(projectDir, scaleDir) {
|
|
781
|
+
return join(resolveScaleRoot(projectDir, scaleDir), 'ai-os', 'benchmarks', 'latest.json');
|
|
782
|
+
}
|
|
783
|
+
function resolveAdoptionReportPath(projectDir, scaleDir) {
|
|
784
|
+
return join(resolveScaleRoot(projectDir, scaleDir), 'ai-os', 'adoption.json');
|
|
785
|
+
}
|
|
786
|
+
function aiOsAdoptionNextActions(status, lang) {
|
|
787
|
+
if (lang === 'zh') {
|
|
788
|
+
if (status === 'ready')
|
|
789
|
+
return ['AI OS runtime 接入完成;后续真实任务使用 `scale ai-os run --mode guarded`。'];
|
|
790
|
+
if (status === 'warning')
|
|
791
|
+
return ['AI OS runtime 已可用但仍有警告;先运行 `scale ai-os doctor --json --lang zh` 处理剩余项。'];
|
|
792
|
+
return ['AI OS runtime 接入被阻断;查看 adoption report 和 `scale ai-os doctor --json --lang zh` 的失败项。'];
|
|
793
|
+
}
|
|
794
|
+
if (status === 'ready')
|
|
795
|
+
return ['AI OS runtime adoption is complete; use `scale ai-os run --mode guarded` for governed work.'];
|
|
796
|
+
if (status === 'warning')
|
|
797
|
+
return ['AI OS runtime is usable with warnings; run `scale ai-os doctor --json --lang en` and resolve remaining items.'];
|
|
798
|
+
return ['AI OS runtime adoption is blocked; inspect the adoption report and `scale ai-os doctor --json --lang en` failures.'];
|
|
799
|
+
}
|
|
800
|
+
function aiOsStatusNextActions(status, checks, lang, verificationRecommendations) {
|
|
801
|
+
const blocked = new Set(checks.filter(check => check.status === 'blocked').map(check => check.id));
|
|
802
|
+
const firstVerificationCommand = verificationRecommendations[0]?.command ?? '<command>';
|
|
803
|
+
if (lang === 'zh') {
|
|
804
|
+
if (status === 'ready')
|
|
805
|
+
return ['AI OS 闭环已就绪,可使用 `scale ai-os run --mode guarded` 执行受治理任务。'];
|
|
806
|
+
if (blocked.has('runtime-dirs') || blocked.has('adoption-evidence')) {
|
|
807
|
+
return ['运行 `scale ai-os adopt --task "接入 AI OS runtime" --lang zh` 生成运行态、首份 dry-run、benchmark 和 doctor 报告。'];
|
|
808
|
+
}
|
|
809
|
+
if (blocked.has('verification-evidence'))
|
|
810
|
+
return ['运行 `scale ai-os run --mode guarded --verify "<command>"` 生成受治理验证证据。'];
|
|
811
|
+
if (blocked.has('benchmark-evidence'))
|
|
812
|
+
return ['运行 `scale ai-os benchmark --json` 生成闭环 benchmark 证据。'];
|
|
813
|
+
return ['查看 status checks,补齐 blocked 项后重新运行 `scale ai-os status --lang zh`。'];
|
|
814
|
+
}
|
|
815
|
+
if (status === 'ready')
|
|
816
|
+
return ['AI OS closed loop is ready for guarded project work.'];
|
|
817
|
+
if (blocked.has('runtime-dirs') || blocked.has('adoption-evidence')) {
|
|
818
|
+
return ['Run `scale ai-os adopt --task "Adopt AI OS runtime" --lang en` to create runtime state, first dry-run, benchmark, and doctor reports.'];
|
|
819
|
+
}
|
|
820
|
+
if (blocked.has('verification-evidence'))
|
|
821
|
+
return [`Run \`scale ai-os run --mode guarded --verify "${escapeCliDoubleQuoted(firstVerificationCommand)}"\` to produce governed verification evidence.`];
|
|
822
|
+
if (blocked.has('benchmark-evidence'))
|
|
823
|
+
return ['Run `scale ai-os benchmark --json` to produce closed-loop benchmark evidence.'];
|
|
824
|
+
return ['Inspect status checks, resolve blocked items, then rerun `scale ai-os status --lang en`.'];
|
|
825
|
+
}
|
|
826
|
+
const VERIFICATION_COMMAND_ORDER = ['build', 'lint', 'test', 'smoke', 'coverage'];
|
|
827
|
+
function buildVerificationRecommendations(projectDir, scaleDir, lang) {
|
|
828
|
+
const recommendations = [];
|
|
829
|
+
const seen = new Set();
|
|
830
|
+
const add = (recommendation) => {
|
|
831
|
+
const key = `${recommendation.command}\n${recommendation.service ?? ''}`;
|
|
832
|
+
if (seen.has(key))
|
|
833
|
+
return;
|
|
834
|
+
seen.add(key);
|
|
835
|
+
recommendations.push(recommendation);
|
|
836
|
+
};
|
|
837
|
+
try {
|
|
838
|
+
const resolved = resolveVerificationTargets({ projectDir, scaleDir, service: 'all' });
|
|
839
|
+
for (const target of resolved.targets) {
|
|
840
|
+
for (const name of VERIFICATION_COMMAND_ORDER) {
|
|
841
|
+
const command = target.config[name];
|
|
842
|
+
if (!command)
|
|
843
|
+
continue;
|
|
844
|
+
add({
|
|
845
|
+
command,
|
|
846
|
+
source: 'verification-profile',
|
|
847
|
+
reason: verificationRecommendationReason(name, lang),
|
|
848
|
+
profile: resolved.profileName,
|
|
849
|
+
service: target.service?.name,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// Best effort only. Status should not fail just because verification config is invalid.
|
|
856
|
+
}
|
|
857
|
+
if (recommendations.length > 0)
|
|
858
|
+
return recommendations;
|
|
859
|
+
for (const item of packageScriptVerificationCommands(projectDir)) {
|
|
860
|
+
add({
|
|
861
|
+
command: item.command,
|
|
862
|
+
source: 'package-script',
|
|
863
|
+
reason: verificationRecommendationReason(item.name, lang),
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
if (recommendations.length > 0)
|
|
867
|
+
return recommendations;
|
|
868
|
+
return [{
|
|
869
|
+
command: 'scale preflight --preflight-profile quick --json',
|
|
870
|
+
source: 'fallback',
|
|
871
|
+
reason: lang === 'zh'
|
|
872
|
+
? '未找到验证矩阵或 package script,先运行 SCALE 快速预检生成基础验证证据。'
|
|
873
|
+
: 'No verification matrix or package script was found; run SCALE quick preflight as baseline evidence.',
|
|
874
|
+
}];
|
|
875
|
+
}
|
|
876
|
+
function packageScriptVerificationCommands(projectDir) {
|
|
877
|
+
const packageJsonPath = join(projectDir, 'package.json');
|
|
878
|
+
if (!existsSync(packageJsonPath))
|
|
879
|
+
return [];
|
|
880
|
+
try {
|
|
881
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
882
|
+
const scripts = parsed.scripts ?? {};
|
|
883
|
+
const commands = [];
|
|
884
|
+
if (scripts.build)
|
|
885
|
+
commands.push({ name: 'build', command: 'npm run build' });
|
|
886
|
+
if (scripts.lint)
|
|
887
|
+
commands.push({ name: 'lint', command: 'npm run lint' });
|
|
888
|
+
if (scripts.test)
|
|
889
|
+
commands.push({ name: 'test', command: 'npm test' });
|
|
890
|
+
return commands;
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function verificationRecommendationReason(name, lang) {
|
|
897
|
+
if (lang === 'zh') {
|
|
898
|
+
if (name === 'build')
|
|
899
|
+
return '构建验证可以证明代码仍可编译并生成发布产物。';
|
|
900
|
+
if (name === 'lint')
|
|
901
|
+
return 'Lint 验证可以捕获工程规范和静态质量问题。';
|
|
902
|
+
if (name === 'test')
|
|
903
|
+
return '测试验证可以证明核心行为没有回归。';
|
|
904
|
+
if (name === 'smoke')
|
|
905
|
+
return '冒烟验证可以证明关键产品路径仍可用。';
|
|
906
|
+
return '覆盖率验证可以补充测试充分性证据。';
|
|
907
|
+
}
|
|
908
|
+
if (name === 'build')
|
|
909
|
+
return 'Build verification proves the code still compiles and produces releasable artifacts.';
|
|
910
|
+
if (name === 'lint')
|
|
911
|
+
return 'Lint verification catches engineering-standard and static-quality issues.';
|
|
912
|
+
if (name === 'test')
|
|
913
|
+
return 'Test verification proves core behavior did not regress.';
|
|
914
|
+
if (name === 'smoke')
|
|
915
|
+
return 'Smoke verification proves critical product paths still work.';
|
|
916
|
+
return 'Coverage verification adds evidence for test adequacy.';
|
|
917
|
+
}
|
|
918
|
+
function escapeCliDoubleQuoted(value) {
|
|
919
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
920
|
+
}
|
|
921
|
+
function inspectBenchmarkReport(projectDir, scaleDir, maxAgeHours, warnings) {
|
|
922
|
+
const reportPath = resolveBenchmarkReportPath(projectDir, scaleDir);
|
|
923
|
+
if (!existsSync(reportPath))
|
|
924
|
+
return { status: 'missing', reportPath };
|
|
925
|
+
try {
|
|
926
|
+
const parsed = JSON.parse(readFileSync(reportPath, 'utf-8'));
|
|
927
|
+
if (!parsed.generatedAt || !parsed.summary || typeof parsed.summary.scenarios !== 'number') {
|
|
928
|
+
warnings.push(`Invalid AI OS benchmark report: ${reportPath}`);
|
|
929
|
+
return { status: 'invalid', reportPath };
|
|
930
|
+
}
|
|
931
|
+
const generatedAtMs = Date.parse(parsed.generatedAt);
|
|
932
|
+
const ageHours = Number(((Date.now() - generatedAtMs) / 3_600_000).toFixed(2));
|
|
933
|
+
const fileAgeHours = Number(((Date.now() - statSync(reportPath).mtimeMs) / 3_600_000).toFixed(2));
|
|
934
|
+
const effectiveAgeHours = Number.isFinite(ageHours) ? ageHours : fileAgeHours;
|
|
935
|
+
return {
|
|
936
|
+
status: effectiveAgeHours <= maxAgeHours ? 'fresh' : 'stale',
|
|
937
|
+
reportPath,
|
|
938
|
+
generatedAt: parsed.generatedAt,
|
|
939
|
+
ageHours: effectiveAgeHours,
|
|
940
|
+
scenarios: parsed.summary.scenarios,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
warnings.push(`Unreadable AI OS benchmark report: ${reportPath} (${error instanceof Error ? error.message : String(error)})`);
|
|
945
|
+
return { status: 'invalid', reportPath };
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function summarizeBenchmarkDoctor(benchmark) {
|
|
949
|
+
if (benchmark.status === 'missing')
|
|
950
|
+
return 'No AI OS benchmark report found.';
|
|
951
|
+
if (benchmark.status === 'invalid')
|
|
952
|
+
return 'AI OS benchmark report is invalid or unreadable.';
|
|
953
|
+
const age = benchmark.ageHours === undefined ? 'unknown age' : `${benchmark.ageHours}h old`;
|
|
954
|
+
return `${benchmark.scenarios ?? 0} benchmark scenario(s); ${age}; status ${benchmark.status}.`;
|
|
955
|
+
}
|
|
956
|
+
function aiOsDoctorNextActions(input) {
|
|
957
|
+
if (input.lang === 'zh')
|
|
958
|
+
return aiOsDoctorNextActionsZh(input);
|
|
959
|
+
return aiOsDoctorNextActionsEn(input);
|
|
960
|
+
}
|
|
961
|
+
function aiOsDoctorNextActionsEn(input) {
|
|
962
|
+
const actions = [];
|
|
963
|
+
if (input.checks.some(check => check.id === 'ai-os-runtime-dirs' && check.status === 'blocked')) {
|
|
964
|
+
actions.push('Run `scale ai-os migrate --json` before using the AI OS beta runtime.');
|
|
965
|
+
}
|
|
966
|
+
if (input.dashboard.summary.totalRuns === 0) {
|
|
967
|
+
actions.push('Run `scale ai-os run --dry-run --json` to create the first AI OS run report.');
|
|
968
|
+
}
|
|
969
|
+
if (input.dashboard.summary.blockedRuns > 0) {
|
|
970
|
+
actions.push('Resolve blocked AI OS runs before claiming the project is ready.');
|
|
971
|
+
}
|
|
972
|
+
if (input.benchmark.status === 'missing' || input.benchmark.status === 'stale') {
|
|
973
|
+
actions.push('Run `scale ai-os benchmark --json` before release or milestone review.');
|
|
974
|
+
}
|
|
975
|
+
if (input.status === 'ready')
|
|
976
|
+
actions.push('AI OS beta runtime is ready for guarded project tasks.');
|
|
977
|
+
return actions;
|
|
978
|
+
}
|
|
979
|
+
function aiOsDoctorNextActionsZh(input) {
|
|
980
|
+
const actions = [];
|
|
981
|
+
if (input.checks.some(check => check.id === 'ai-os-runtime-dirs' && check.status === 'blocked')) {
|
|
982
|
+
actions.push('先运行 `scale ai-os migrate --json`,再接入 AI OS beta runtime。');
|
|
983
|
+
}
|
|
984
|
+
if (input.dashboard.summary.totalRuns === 0) {
|
|
985
|
+
actions.push('运行 `scale ai-os run --dry-run --json` 生成第一份 AI OS 运行报告。');
|
|
986
|
+
}
|
|
987
|
+
if (input.dashboard.summary.blockedRuns > 0) {
|
|
988
|
+
actions.push('先处理 blocked 的 AI OS run,再声明项目运行态就绪。');
|
|
989
|
+
}
|
|
990
|
+
if (input.benchmark.status === 'missing' || input.benchmark.status === 'stale') {
|
|
991
|
+
actions.push('发版或阶段验收前运行 `scale ai-os benchmark --json`。');
|
|
992
|
+
}
|
|
993
|
+
if (input.status === 'ready')
|
|
994
|
+
actions.push('AI OS beta runtime 已可用于 guarded 项目任务。');
|
|
995
|
+
return actions;
|
|
996
|
+
}
|
|
997
|
+
function writeAiOsBenchmarkReport(path, report) {
|
|
998
|
+
const dir = dirname(path);
|
|
999
|
+
if (dir && !existsSync(dir))
|
|
1000
|
+
mkdirSync(dir, { recursive: true });
|
|
1001
|
+
writeFileSync(path, JSON.stringify(report, null, 2), 'utf-8');
|
|
1002
|
+
}
|
|
1003
|
+
function readAiOsRunReports(runsDir, warnings) {
|
|
1004
|
+
if (!existsSync(runsDir))
|
|
1005
|
+
return [];
|
|
1006
|
+
return readdirSync(runsDir)
|
|
1007
|
+
.filter(file => file.endsWith('.json'))
|
|
1008
|
+
.flatMap(file => {
|
|
1009
|
+
const path = join(runsDir, file);
|
|
1010
|
+
try {
|
|
1011
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
1012
|
+
if (!parsed || !parsed.plan || !parsed.evidence || !parsed.verification) {
|
|
1013
|
+
warnings.push(`Ignored invalid AI OS run report: ${path}`);
|
|
1014
|
+
return [];
|
|
1015
|
+
}
|
|
1016
|
+
return [parsed];
|
|
1017
|
+
}
|
|
1018
|
+
catch (error) {
|
|
1019
|
+
warnings.push(`Ignored unreadable AI OS run report: ${path} (${error instanceof Error ? error.message : String(error)})`);
|
|
1020
|
+
return [];
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function toDashboardRunSummary(report) {
|
|
1025
|
+
return {
|
|
1026
|
+
taskId: report.plan.task.taskId,
|
|
1027
|
+
task: report.plan.task.task,
|
|
1028
|
+
mode: report.mode,
|
|
1029
|
+
status: report.status,
|
|
1030
|
+
generatedAt: report.generatedAt,
|
|
1031
|
+
runReport: report.artifacts.runReport,
|
|
1032
|
+
verificationCommands: report.verification.commands.length,
|
|
1033
|
+
failedVerificationCommands: report.verification.commands.filter(command => command.status === 'failed').length,
|
|
1034
|
+
pendingEvidence: report.evidence.pending.length,
|
|
1035
|
+
failureLearningCandidates: report.failureLearning.candidates.length,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function summarizeDashboardHealth(summary) {
|
|
1039
|
+
if (summary.totalRuns === 0) {
|
|
1040
|
+
return { status: 'empty', score: 0, reasons: ['No AI OS run reports found.'] };
|
|
1041
|
+
}
|
|
1042
|
+
const reasons = [];
|
|
1043
|
+
if (summary.blockedRuns > 0)
|
|
1044
|
+
reasons.push(`${summary.blockedRuns} blocked AI OS run(s).`);
|
|
1045
|
+
if (summary.failedVerificationCommands > 0)
|
|
1046
|
+
reasons.push(`${summary.failedVerificationCommands} failed guarded verification command(s).`);
|
|
1047
|
+
if (summary.failureLearningCandidates > 0)
|
|
1048
|
+
reasons.push(`${summary.failureLearningCandidates} failure learning candidate(s) need review.`);
|
|
1049
|
+
const score = Math.max(0, Math.round(((summary.readyRuns / summary.totalRuns) * 100) - (summary.failedVerificationCommands * 10) - (summary.failureLearningCandidates * 5)));
|
|
1050
|
+
if (summary.blockedRuns === summary.totalRuns)
|
|
1051
|
+
return { status: 'blocked', score, reasons };
|
|
1052
|
+
if (reasons.length > 0)
|
|
1053
|
+
return { status: 'attention', score, reasons };
|
|
1054
|
+
return { status: 'healthy', score: 100, reasons: ['All AI OS runs are ready.'] };
|
|
1055
|
+
}
|
|
1056
|
+
function dashboardRecommendations(summary) {
|
|
1057
|
+
const recommendations = [];
|
|
1058
|
+
if (summary.totalRuns === 0) {
|
|
1059
|
+
recommendations.push('Run `scale ai-os run --dry-run` to create the first AI OS execution report.');
|
|
1060
|
+
return recommendations;
|
|
1061
|
+
}
|
|
1062
|
+
if (summary.blockedRuns > 0)
|
|
1063
|
+
recommendations.push('Resolve blocked AI OS run reports before promoting lessons or shipping.');
|
|
1064
|
+
if (summary.failedVerificationCommands > 0)
|
|
1065
|
+
recommendations.push('Inspect failed guarded verification runtime evidence and fix the underlying command or code issue.');
|
|
1066
|
+
if (summary.failureLearningCandidates > 0)
|
|
1067
|
+
recommendations.push('Review failure learning candidates before turning them into durable rules.');
|
|
1068
|
+
if (summary.guardedRuns === 0)
|
|
1069
|
+
recommendations.push('Add guarded verification runs for at least one representative task to validate evidence flow.');
|
|
1070
|
+
return recommendations;
|
|
1071
|
+
}
|
|
1072
|
+
function defaultBenchmarkScenarios(budget = 8_000) {
|
|
1073
|
+
return [
|
|
1074
|
+
{
|
|
1075
|
+
id: 'docs-governance',
|
|
1076
|
+
task: 'Update bilingual governance documentation and keep README, docs map, and strategy aligned',
|
|
1077
|
+
level: 'M',
|
|
1078
|
+
files: ['README.md', 'README.en.md', 'docs/README.md', 'docs/AI_ENGINEERING_OS_POSITIONING.md'],
|
|
1079
|
+
services: ['docs'],
|
|
1080
|
+
budget,
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
id: 'security-code-change',
|
|
1084
|
+
task: 'Harden auth token handling and verify runtime evidence for a security-sensitive code change',
|
|
1085
|
+
level: 'L',
|
|
1086
|
+
files: ['src/auth/token.ts', 'src/runtime/AiOsRuntime.ts', 'tests/runtime/aiOsRuntime.test.ts'],
|
|
1087
|
+
services: ['runtime', 'security'],
|
|
1088
|
+
budget,
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
id: 'browser-ui-flow',
|
|
1092
|
+
task: 'Verify a browser callback UI flow with screenshots, runtime evidence, and guarded workflow gates',
|
|
1093
|
+
level: 'L',
|
|
1094
|
+
files: ['src/ui/callback.tsx', 'tests/api/aiOsCli.test.ts'],
|
|
1095
|
+
services: ['ui', 'browser'],
|
|
1096
|
+
budget,
|
|
1097
|
+
},
|
|
1098
|
+
];
|
|
1099
|
+
}
|
|
1100
|
+
function summarizeBenchmark(results) {
|
|
1101
|
+
const totalBudget = results.reduce((sum, result) => sum + result.metrics.budget, 0);
|
|
1102
|
+
const totalEstimatedTokens = results.reduce((sum, result) => sum + result.metrics.estimatedTokens, 0);
|
|
1103
|
+
return {
|
|
1104
|
+
scenarios: results.length,
|
|
1105
|
+
totalEstimatedTokens,
|
|
1106
|
+
totalBudget,
|
|
1107
|
+
totalEstimatedTokenSavings: results.reduce((sum, result) => sum + result.metrics.estimatedTokenSavings, 0),
|
|
1108
|
+
totalMemoryItems: results.reduce((sum, result) => sum + result.metrics.memoryItems, 0),
|
|
1109
|
+
totalSkillSteps: results.reduce((sum, result) => sum + result.metrics.skillSteps, 0),
|
|
1110
|
+
requiredSkillSteps: results.reduce((sum, result) => sum + result.metrics.requiredSkillSteps, 0),
|
|
1111
|
+
governanceModes: [...new Set(results.map(result => result.governanceMode))],
|
|
1112
|
+
averageTokenUtilization: totalBudget > 0 ? Number((totalEstimatedTokens / totalBudget).toFixed(4)) : 0,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function benchmarkRecommendations(summary) {
|
|
1116
|
+
const recommendations = ['Use benchmark deltas in release notes only after comparing the same scenario set across versions.'];
|
|
1117
|
+
if (summary.totalSkillSteps === 0)
|
|
1118
|
+
recommendations.push('Skill routing did not produce steps; inspect skill policy detection.');
|
|
1119
|
+
if (summary.averageTokenUtilization > 0.9)
|
|
1120
|
+
recommendations.push('Context utilization is high; lower budgets or improve relevance filtering before scaling.');
|
|
1121
|
+
if (!summary.governanceModes.includes('critical') && !summary.governanceModes.includes('expanded')) {
|
|
1122
|
+
recommendations.push('Add at least one high-risk benchmark scenario before claiming adaptive governance coverage.');
|
|
1123
|
+
}
|
|
1124
|
+
return recommendations;
|
|
1125
|
+
}
|
|
96
1126
|
function createAdaptiveWorkflow(governance, skillPlan) {
|
|
97
1127
|
const gates = new Set();
|
|
98
1128
|
gates.add('context-compiler');
|