zefiro 0.3.7 → 0.5.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/agents/1.feature-analyzer-agent-v3.md +138 -0
- package/agents/2.scenario-planner-agent-v3.md +106 -0
- package/agents/workflow-agent-v3.md +374 -0
- package/dist/cli-fc97msmj.js +191627 -0
- package/dist/cli-pe555th6.js +191211 -0
- package/dist/cli.js +253 -6
- package/dist/mcp.js +163 -286
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -4,14 +4,17 @@ import {
|
|
|
4
4
|
error,
|
|
5
5
|
header,
|
|
6
6
|
info,
|
|
7
|
+
isDAGValid,
|
|
8
|
+
linearToDAG,
|
|
7
9
|
require_picocolors,
|
|
8
10
|
require_src,
|
|
9
11
|
runStage1,
|
|
10
12
|
setVerbose,
|
|
11
13
|
success,
|
|
14
|
+
validateQAMapV3,
|
|
12
15
|
verbose,
|
|
13
16
|
warn
|
|
14
|
-
} from "./cli-
|
|
17
|
+
} from "./cli-fc97msmj.js";
|
|
15
18
|
import {
|
|
16
19
|
CONFIG_DIR,
|
|
17
20
|
getPackageRoot,
|
|
@@ -7476,6 +7479,9 @@ function generateComponentId(filePath, componentName) {
|
|
|
7476
7479
|
const hash = stableHash([filePath, componentName]);
|
|
7477
7480
|
return `comp:${slugify(componentName)}-${hash}`;
|
|
7478
7481
|
}
|
|
7482
|
+
function generateSectionId(name) {
|
|
7483
|
+
return `sec:${slugify(name)}`;
|
|
7484
|
+
}
|
|
7479
7485
|
function normalizeIds(qaMap) {
|
|
7480
7486
|
const featureIdMap = new Map;
|
|
7481
7487
|
const workflowIdMap = new Map;
|
|
@@ -7528,6 +7534,76 @@ function normalizeIds(qaMap) {
|
|
|
7528
7534
|
scenarios: newScenarios
|
|
7529
7535
|
};
|
|
7530
7536
|
}
|
|
7537
|
+
function normalizeIdsV3(qaMap) {
|
|
7538
|
+
const sectionIdMap = new Map;
|
|
7539
|
+
const featureIdMap = new Map;
|
|
7540
|
+
const workflowIdMap = new Map;
|
|
7541
|
+
const componentIdMap = new Map;
|
|
7542
|
+
for (const section of qaMap.sections) {
|
|
7543
|
+
const newId = generateSectionId(section.name);
|
|
7544
|
+
sectionIdMap.set(section.id, newId);
|
|
7545
|
+
}
|
|
7546
|
+
for (const feature of qaMap.features) {
|
|
7547
|
+
const newId = generateFeatureId(feature.name, feature.routes);
|
|
7548
|
+
featureIdMap.set(feature.id, newId);
|
|
7549
|
+
}
|
|
7550
|
+
for (const component of qaMap.components) {
|
|
7551
|
+
const primaryFile = component.sourceFiles[0] ?? "";
|
|
7552
|
+
const newId = generateComponentId(primaryFile, component.name);
|
|
7553
|
+
componentIdMap.set(component.id, newId);
|
|
7554
|
+
}
|
|
7555
|
+
for (const workflow of qaMap.workflows) {
|
|
7556
|
+
const newFeatureId = featureIdMap.get(workflow.featureId) ?? workflow.featureId;
|
|
7557
|
+
const newId = generateWorkflowId(newFeatureId, workflow.name);
|
|
7558
|
+
workflowIdMap.set(workflow.id, newId);
|
|
7559
|
+
}
|
|
7560
|
+
const newSections = qaMap.sections.map((s) => ({
|
|
7561
|
+
...s,
|
|
7562
|
+
id: sectionIdMap.get(s.id) ?? s.id,
|
|
7563
|
+
featureIds: s.featureIds.map((fId) => featureIdMap.get(fId) ?? fId)
|
|
7564
|
+
}));
|
|
7565
|
+
const newFeatures = qaMap.features.map((f) => ({
|
|
7566
|
+
...f,
|
|
7567
|
+
id: featureIdMap.get(f.id) ?? f.id,
|
|
7568
|
+
sectionId: sectionIdMap.get(f.sectionId) ?? f.sectionId,
|
|
7569
|
+
parentFeatureId: f.parentFeatureId ? featureIdMap.get(f.parentFeatureId) ?? f.parentFeatureId : undefined,
|
|
7570
|
+
subFeatureIds: f.subFeatureIds.map((id) => featureIdMap.get(id) ?? id),
|
|
7571
|
+
workflowIds: f.workflowIds.map((wId) => workflowIdMap.get(wId) ?? wId),
|
|
7572
|
+
entryPoints: f.entryPoints.map((ep) => ({
|
|
7573
|
+
...ep,
|
|
7574
|
+
sourceFeatureId: ep.sourceFeatureId ? featureIdMap.get(ep.sourceFeatureId) ?? ep.sourceFeatureId : undefined
|
|
7575
|
+
}))
|
|
7576
|
+
}));
|
|
7577
|
+
const newWorkflows = qaMap.workflows.map((w) => ({
|
|
7578
|
+
...w,
|
|
7579
|
+
id: workflowIdMap.get(w.id) ?? w.id,
|
|
7580
|
+
featureId: featureIdMap.get(w.featureId) ?? w.featureId,
|
|
7581
|
+
componentIds: w.componentIds.map((cId) => componentIdMap.get(cId) ?? cId),
|
|
7582
|
+
steps: w.steps.map((step) => ({
|
|
7583
|
+
...step,
|
|
7584
|
+
componentIds: step.componentIds.map((cId) => componentIdMap.get(cId) ?? cId)
|
|
7585
|
+
}))
|
|
7586
|
+
}));
|
|
7587
|
+
const newComponents = qaMap.components.map((c) => ({
|
|
7588
|
+
...c,
|
|
7589
|
+
id: componentIdMap.get(c.id) ?? c.id,
|
|
7590
|
+
referencedByWorkflows: c.referencedByWorkflows.map((wId) => workflowIdMap.get(wId) ?? wId)
|
|
7591
|
+
}));
|
|
7592
|
+
const newScenarios = qaMap.scenarios.map((s) => ({
|
|
7593
|
+
...s,
|
|
7594
|
+
featureId: featureIdMap.get(s.featureId) ?? s.featureId,
|
|
7595
|
+
workflowId: workflowIdMap.get(s.workflowId) ?? s.workflowId,
|
|
7596
|
+
componentIds: s.componentIds.map((cId) => componentIdMap.get(cId) ?? cId)
|
|
7597
|
+
}));
|
|
7598
|
+
return {
|
|
7599
|
+
...qaMap,
|
|
7600
|
+
sections: newSections,
|
|
7601
|
+
features: newFeatures,
|
|
7602
|
+
workflows: newWorkflows,
|
|
7603
|
+
components: newComponents,
|
|
7604
|
+
scenarios: newScenarios
|
|
7605
|
+
};
|
|
7606
|
+
}
|
|
7531
7607
|
|
|
7532
7608
|
// src/commands/analyze.ts
|
|
7533
7609
|
var ENV_KEY = {
|
|
@@ -7561,7 +7637,8 @@ async function ensureApiKey(provider) {
|
|
|
7561
7637
|
}
|
|
7562
7638
|
}
|
|
7563
7639
|
function registerAnalyze(program2) {
|
|
7564
|
-
program2.command("analyze [input]").description("Analyze AST scan with AI to generate QA map (features, workflows, scenarios)").option("--output <file>", "Write QA map to specific file").option("--skip-scenarios", "Only run feature analysis (skip scenario generation)").action(async (inputArg, opts) => {
|
|
7640
|
+
program2.command("analyze [input]").description("Analyze AST scan with AI to generate QA map (features, workflows, scenarios)").option("--output <file>", "Write QA map to specific file").option("--skip-scenarios", "Only run feature analysis (skip scenario generation)").option("--schema-version <version>", "Output schema version (2 or 3)", "3").action(async (inputArg, opts) => {
|
|
7641
|
+
const schemaVersion = parseInt(opts?.schemaVersion ?? "3", 10);
|
|
7565
7642
|
const ctx = await resolveCommandContext(program2);
|
|
7566
7643
|
const root = ctx.paths.projectRoot;
|
|
7567
7644
|
let inputPath;
|
|
@@ -7597,6 +7674,10 @@ function registerAnalyze(program2) {
|
|
|
7597
7674
|
if (!hasKey)
|
|
7598
7675
|
return;
|
|
7599
7676
|
const spinner = createSpinner();
|
|
7677
|
+
if (schemaVersion === 3) {
|
|
7678
|
+
await runV3Analysis(ctx, ast, outputPath, prevQAMap, affected, spinner, opts);
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7600
7681
|
spinner.start("Stage 2a: Identifying features...");
|
|
7601
7682
|
const featureAgent = loadAgent("feature-analyzer-agent", ctx.config);
|
|
7602
7683
|
const featureResponse = await callLLM({
|
|
@@ -7745,6 +7826,166 @@ function registerAnalyze(program2) {
|
|
|
7745
7826
|
success(`QA map written to ${outputPath}`);
|
|
7746
7827
|
});
|
|
7747
7828
|
}
|
|
7829
|
+
async function runV3Analysis(ctx, ast, outputPath, _prevQAMap, _affected, spinner, opts) {
|
|
7830
|
+
spinner.start("Stage 2a: Identifying sections and features (V3)...");
|
|
7831
|
+
const featureAgent = loadAgent("feature-analyzer-agent-v3", ctx.config);
|
|
7832
|
+
const featureResponse = await callLLM({
|
|
7833
|
+
provider: ctx.provider,
|
|
7834
|
+
model: ctx.model ?? featureAgent.config.model,
|
|
7835
|
+
systemPrompt: featureAgent.systemPrompt,
|
|
7836
|
+
userMessage: JSON.stringify({ ast }),
|
|
7837
|
+
maxTokens: featureAgent.config.maxTokens,
|
|
7838
|
+
temperature: featureAgent.config.temperature,
|
|
7839
|
+
jsonMode: true
|
|
7840
|
+
});
|
|
7841
|
+
spinner.stop();
|
|
7842
|
+
const featureMap = JSON.parse(extractJSON(featureResponse.content));
|
|
7843
|
+
if (!featureMap?.features || !featureMap?.sections) {
|
|
7844
|
+
error("Feature analysis failed: invalid response (missing features or sections)");
|
|
7845
|
+
process.exit(1);
|
|
7846
|
+
}
|
|
7847
|
+
const sections = featureMap.sections;
|
|
7848
|
+
const features = featureMap.features;
|
|
7849
|
+
const baseComponents = featureMap.components ?? [];
|
|
7850
|
+
success(`Identified ${sections.length} sections, ${features.length} features, ${baseComponents.length} components`);
|
|
7851
|
+
spinner.start(`Stage 2b: Mapping DAG workflows for ${features.length} feature(s)...`);
|
|
7852
|
+
const workflowTasks = features.map((feature) => {
|
|
7853
|
+
const featureComponents = baseComponents.filter((c) => c.sourceFiles.some((sf) => feature.sourceFiles.includes(sf)));
|
|
7854
|
+
const featureRoutes = ast.routes.filter((r) => feature.routes.some((fr) => r.path.startsWith(fr.split("/").slice(0, 3).join("/"))));
|
|
7855
|
+
const featureForms = (ast.forms ?? []).filter((f) => feature.sourceFiles.includes(f.filePath));
|
|
7856
|
+
return {
|
|
7857
|
+
agentName: "workflow-agent-v3",
|
|
7858
|
+
userMessage: JSON.stringify({ feature, components: featureComponents, routes: featureRoutes, forms: featureForms }),
|
|
7859
|
+
config: ctx.config,
|
|
7860
|
+
provider: ctx.provider,
|
|
7861
|
+
model: ctx.model
|
|
7862
|
+
};
|
|
7863
|
+
});
|
|
7864
|
+
const workflowResults = await runAgentsInParallel(workflowTasks, 4);
|
|
7865
|
+
spinner.stop();
|
|
7866
|
+
let allWorkflows = workflowResults.flatMap((r) => r.workflows ?? []);
|
|
7867
|
+
spinner.start("Validating workflow DAGs...");
|
|
7868
|
+
const validatedWorkflows = [];
|
|
7869
|
+
for (const wf of allWorkflows) {
|
|
7870
|
+
const dagResult = isDAGValid(wf.steps, wf.edges, wf.entryStepIds);
|
|
7871
|
+
if (dagResult.valid) {
|
|
7872
|
+
validatedWorkflows.push(wf);
|
|
7873
|
+
} else {
|
|
7874
|
+
warn(`Workflow "${wf.name}" has invalid DAG: ${dagResult.errors.join("; ")}. Falling back to linear chain.`);
|
|
7875
|
+
const linear = linearToDAG(wf.steps);
|
|
7876
|
+
validatedWorkflows.push({
|
|
7877
|
+
...wf,
|
|
7878
|
+
steps: linear.steps,
|
|
7879
|
+
edges: linear.edges,
|
|
7880
|
+
entryStepIds: linear.entryStepIds
|
|
7881
|
+
});
|
|
7882
|
+
}
|
|
7883
|
+
}
|
|
7884
|
+
spinner.stop();
|
|
7885
|
+
allWorkflows = validatedWorkflows;
|
|
7886
|
+
success(`Mapped ${allWorkflows.length} workflows with DAG structure`);
|
|
7887
|
+
const workflowsByFeature = new Map;
|
|
7888
|
+
for (const wf of allWorkflows) {
|
|
7889
|
+
const ids = workflowsByFeature.get(wf.featureId) ?? [];
|
|
7890
|
+
ids.push(wf.id);
|
|
7891
|
+
workflowsByFeature.set(wf.featureId, ids);
|
|
7892
|
+
}
|
|
7893
|
+
for (const feature of features) {
|
|
7894
|
+
const wfIds = workflowsByFeature.get(feature.id);
|
|
7895
|
+
if (wfIds)
|
|
7896
|
+
feature.workflowIds = wfIds;
|
|
7897
|
+
}
|
|
7898
|
+
const formComponents = baseComponents.filter((c) => c.type === "form");
|
|
7899
|
+
if (formComponents.length > 0 && (ast.forms ?? []).length > 0) {
|
|
7900
|
+
spinner.start(`Stage 2c: Enriching ${formComponents.length} form component(s)...`);
|
|
7901
|
+
const inputTasks = features.map((feature) => {
|
|
7902
|
+
const featureFormComponents = formComponents.filter((c) => c.sourceFiles.some((sf) => feature.sourceFiles.includes(sf)));
|
|
7903
|
+
const featureForms = (ast.forms ?? []).filter((f) => feature.sourceFiles.includes(f.filePath));
|
|
7904
|
+
if (featureFormComponents.length === 0)
|
|
7905
|
+
return null;
|
|
7906
|
+
return {
|
|
7907
|
+
agentName: "input-agent",
|
|
7908
|
+
userMessage: JSON.stringify({ featureId: feature.id, components: featureFormComponents, forms: featureForms }),
|
|
7909
|
+
config: ctx.config,
|
|
7910
|
+
provider: ctx.provider,
|
|
7911
|
+
model: ctx.model
|
|
7912
|
+
};
|
|
7913
|
+
}).filter((t) => t !== null);
|
|
7914
|
+
if (inputTasks.length > 0) {
|
|
7915
|
+
try {
|
|
7916
|
+
const inputResults = await runAgentsInParallel(inputTasks, 4);
|
|
7917
|
+
for (const result of inputResults) {
|
|
7918
|
+
for (const enriched of result.components ?? []) {
|
|
7919
|
+
const idx = baseComponents.findIndex((c) => c.id === enriched.id);
|
|
7920
|
+
if (idx !== -1) {
|
|
7921
|
+
baseComponents[idx] = { ...baseComponents[idx], ...enriched };
|
|
7922
|
+
}
|
|
7923
|
+
}
|
|
7924
|
+
}
|
|
7925
|
+
} catch {
|
|
7926
|
+
warn("Input enrichment failed (non-critical), continuing with base components");
|
|
7927
|
+
}
|
|
7928
|
+
}
|
|
7929
|
+
spinner.stop();
|
|
7930
|
+
}
|
|
7931
|
+
if (opts?.skipScenarios) {
|
|
7932
|
+
const finalPayload2 = {
|
|
7933
|
+
schemaVersion: 3,
|
|
7934
|
+
sections,
|
|
7935
|
+
features,
|
|
7936
|
+
workflows: allWorkflows,
|
|
7937
|
+
components: baseComponents,
|
|
7938
|
+
scenarios: [],
|
|
7939
|
+
metadata: ast.git ? { git: ast.git } : undefined
|
|
7940
|
+
};
|
|
7941
|
+
const normalizedPayload2 = normalizeIdsV3(finalPayload2);
|
|
7942
|
+
writeFile(outputPath, JSON.stringify(normalizedPayload2, null, 2));
|
|
7943
|
+
success(`V3 QA map written to ${outputPath}`);
|
|
7944
|
+
return;
|
|
7945
|
+
}
|
|
7946
|
+
spinner.start(`Stage 3: Generating scenarios for ${features.length} feature(s)...`);
|
|
7947
|
+
const scenarioTasks = features.map((feature) => {
|
|
7948
|
+
const featureWorkflows = allWorkflows.filter((w) => w.featureId === feature.id);
|
|
7949
|
+
const featureComponents = baseComponents.filter((c) => c.referencedByWorkflows.some((wfId) => featureWorkflows.map((w) => w.id).includes(wfId)));
|
|
7950
|
+
return {
|
|
7951
|
+
agentName: "scenario-planner-agent-v3",
|
|
7952
|
+
userMessage: JSON.stringify({
|
|
7953
|
+
features: [feature],
|
|
7954
|
+
workflows: featureWorkflows,
|
|
7955
|
+
components: featureComponents
|
|
7956
|
+
}),
|
|
7957
|
+
config: ctx.config,
|
|
7958
|
+
provider: ctx.provider,
|
|
7959
|
+
model: ctx.model
|
|
7960
|
+
};
|
|
7961
|
+
});
|
|
7962
|
+
const scenarioResults = await runAgentsInParallel(scenarioTasks, 4);
|
|
7963
|
+
spinner.stop();
|
|
7964
|
+
const allScenarios = scenarioResults.flatMap((r) => r.scenarios ?? []);
|
|
7965
|
+
success(`Generated ${allScenarios.length} scenarios`);
|
|
7966
|
+
const finalPayload = {
|
|
7967
|
+
schemaVersion: 3,
|
|
7968
|
+
sections,
|
|
7969
|
+
features,
|
|
7970
|
+
workflows: allWorkflows,
|
|
7971
|
+
components: baseComponents,
|
|
7972
|
+
scenarios: allScenarios,
|
|
7973
|
+
metadata: ast.git ? { git: ast.git } : undefined
|
|
7974
|
+
};
|
|
7975
|
+
const normalizedPayload = normalizeIdsV3(finalPayload);
|
|
7976
|
+
const validation = validateQAMapV3(normalizedPayload);
|
|
7977
|
+
if (!validation.valid) {
|
|
7978
|
+
warn("V3 QA map has validation issues:");
|
|
7979
|
+
for (const err of validation.errors)
|
|
7980
|
+
warn(` - ${err}`);
|
|
7981
|
+
}
|
|
7982
|
+
if (validation.warnings.length > 0) {
|
|
7983
|
+
for (const w of validation.warnings)
|
|
7984
|
+
info(` ⚠ ${w}`);
|
|
7985
|
+
}
|
|
7986
|
+
writeFile(outputPath, JSON.stringify(normalizedPayload, null, 2));
|
|
7987
|
+
success(`V3 QA map written to ${outputPath}`);
|
|
7988
|
+
}
|
|
7748
7989
|
|
|
7749
7990
|
// src/commands/auth.ts
|
|
7750
7991
|
function registerAuth(program2) {
|
|
@@ -8060,7 +8301,9 @@ import { join as join7 } from "node:path";
|
|
|
8060
8301
|
|
|
8061
8302
|
// src/scanner/push.ts
|
|
8062
8303
|
async function pushToApi(payload, apiUrl, apiKey) {
|
|
8063
|
-
const
|
|
8304
|
+
const isV3 = payload.schemaVersion === 3;
|
|
8305
|
+
const url = isV3 ? apiUrl.replace("/v2/push", "/v3/push") : apiUrl.replace("/v3/push", "/v2/push");
|
|
8306
|
+
const response = await fetch(url, {
|
|
8064
8307
|
method: "POST",
|
|
8065
8308
|
headers: {
|
|
8066
8309
|
"Content-Type": "application/json",
|
|
@@ -8092,7 +8335,9 @@ function registerPush(program2) {
|
|
|
8092
8335
|
error(`QA map not found: ${inputPath}`);
|
|
8093
8336
|
process.exit(1);
|
|
8094
8337
|
}
|
|
8095
|
-
const
|
|
8338
|
+
const raw = JSON.parse(readFile(inputPath));
|
|
8339
|
+
const isV3 = raw.schemaVersion === 3;
|
|
8340
|
+
const payload = raw;
|
|
8096
8341
|
if (opts?.commitSha) {
|
|
8097
8342
|
payload.commitSha = opts.commitSha;
|
|
8098
8343
|
}
|
|
@@ -8109,14 +8354,16 @@ function registerPush(program2) {
|
|
|
8109
8354
|
payload.commitSha = gitMeta.commitSha;
|
|
8110
8355
|
}
|
|
8111
8356
|
}
|
|
8112
|
-
|
|
8357
|
+
const sectionCount = isV3 ? payload.sections.length : 0;
|
|
8358
|
+
info(`Pushing (V${isV3 ? 3 : 2}): ${sectionCount ? sectionCount + " sections, " : ""}${payload.features.length} features, ${payload.workflows.length} workflows, ${payload.scenarios.length} scenarios`);
|
|
8359
|
+
const pushEndpoint = isV3 ? "/api/cli/push-v3" : "/api/cli/push";
|
|
8113
8360
|
try {
|
|
8114
8361
|
const { AuthClient } = await import("./index-fbvbnntc.js");
|
|
8115
8362
|
const client = AuthClient.fromSession();
|
|
8116
8363
|
if (client) {
|
|
8117
8364
|
const spinner2 = createSpinner();
|
|
8118
8365
|
spinner2.start("Pushing via authenticated session...");
|
|
8119
|
-
const result2 = await client.post(
|
|
8366
|
+
const result2 = await client.post(pushEndpoint, payload);
|
|
8120
8367
|
spinner2.stop();
|
|
8121
8368
|
success("Push successful!");
|
|
8122
8369
|
info(` Version: ${result2.version} (schema v${result2.schemaVersion})`);
|