zefiro 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  banner,
4
+ buildV4Graph,
4
5
  error,
6
+ extractStateVariables,
5
7
  header,
6
8
  info,
7
9
  isDAGValid,
@@ -14,7 +16,7 @@ import {
14
16
  validateQAMapV3,
15
17
  verbose,
16
18
  warn
17
- } from "./cli-fc97msmj.js";
19
+ } from "./cli-ychk5szm.js";
18
20
  import {
19
21
  CONFIG_DIR,
20
22
  getPackageRoot,
@@ -7637,7 +7639,7 @@ async function ensureApiKey(provider) {
7637
7639
  }
7638
7640
  }
7639
7641
  function registerAnalyze(program2) {
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) => {
7642
+ 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, 3, or 4)", "3").action(async (inputArg, opts) => {
7641
7643
  const schemaVersion = parseInt(opts?.schemaVersion ?? "3", 10);
7642
7644
  const ctx = await resolveCommandContext(program2);
7643
7645
  const root = ctx.paths.projectRoot;
@@ -7674,6 +7676,10 @@ function registerAnalyze(program2) {
7674
7676
  if (!hasKey)
7675
7677
  return;
7676
7678
  const spinner = createSpinner();
7679
+ if (schemaVersion === 4) {
7680
+ await runV4Analysis(ctx, ast, outputPath, spinner, opts);
7681
+ return;
7682
+ }
7677
7683
  if (schemaVersion === 3) {
7678
7684
  await runV3Analysis(ctx, ast, outputPath, prevQAMap, affected, spinner, opts);
7679
7685
  return;
@@ -7986,6 +7992,107 @@ async function runV3Analysis(ctx, ast, outputPath, _prevQAMap, _affected, spinne
7986
7992
  writeFile(outputPath, JSON.stringify(normalizedPayload, null, 2));
7987
7993
  success(`V3 QA map written to ${outputPath}`);
7988
7994
  }
7995
+ async function runV4Analysis(ctx, ast, outputPath, spinner, opts) {
7996
+ const { join: join5 } = await import("node:path");
7997
+ spinner.start("V4 Stage 1: Extracting state variables...");
7998
+ const scanDir = ast.files?.[0]?.path ? join5(process.cwd(), ast.files[0].path).replace(ast.files[0].path, "") : process.cwd();
7999
+ const filePaths = ast.files.map((f) => join5(scanDir, f.path));
8000
+ const stateVariables = extractStateVariables(filePaths, ast.routes, scanDir);
8001
+ spinner.stop();
8002
+ const highConf = stateVariables.filter((v) => v.extractionConfidence === "high").length;
8003
+ success(`Extracted ${stateVariables.length} state variables (${highConf} high confidence)`);
8004
+ spinner.start("V4 Stage 2: Building formal model (structural graph + state space + transitions)...");
8005
+ const graphResult = buildV4Graph(ast, stateVariables);
8006
+ spinner.stop();
8007
+ success(`Structural graph: ${graphResult.structuralGraph.nodes.length} nodes, ${graphResult.structuralGraph.edges.length} edges
8008
+ State space: ${graphResult.stateSpace.stats.totalVariables} vars, ${graphResult.stateSpace.stats.totalValidConfigurations} valid configs
8009
+ Transition system: ${graphResult.transitionSystem.stats.reachableNodes} reachable nodes, ${graphResult.transitionSystem.stats.totalTransitions} transitions`);
8010
+ spinner.start("V4 Stage 3: Generating semantic projection (feature labels)...");
8011
+ const featureAgent = loadAgent("feature-analyzer-agent-v4", ctx.config);
8012
+ const projectionInput = {
8013
+ structuralGraph: {
8014
+ nodes: graphResult.structuralGraph.nodes.map((n) => ({
8015
+ id: n.id,
8016
+ routePath: n.routePath,
8017
+ routeGroup: n.routeGroup,
8018
+ components: n.components,
8019
+ guards: n.guards,
8020
+ forms: n.forms
8021
+ })),
8022
+ edges: graphResult.structuralGraph.edges.map((e) => ({
8023
+ id: e.id,
8024
+ fromNodeId: e.fromNodeId,
8025
+ toNodeId: e.toNodeId,
8026
+ type: e.type
8027
+ })),
8028
+ entryPoints: graphResult.structuralGraph.entryPoints
8029
+ },
8030
+ stateVariables: stateVariables.map((v) => ({
8031
+ id: v.id,
8032
+ name: v.name,
8033
+ category: v.category,
8034
+ domain: v.domain,
8035
+ affectsRoutes: v.affectsRoutes,
8036
+ extractionConfidence: v.extractionConfidence
8037
+ })),
8038
+ transitionSystemStats: graphResult.transitionSystem.stats,
8039
+ ast: { routes: ast.routes, stats: ast.stats }
8040
+ };
8041
+ const featureResponse = await callLLM({
8042
+ provider: ctx.provider,
8043
+ model: ctx.model ?? featureAgent.config.model,
8044
+ systemPrompt: featureAgent.systemPrompt,
8045
+ userMessage: JSON.stringify(projectionInput),
8046
+ maxTokens: featureAgent.config.maxTokens,
8047
+ temperature: featureAgent.config.temperature,
8048
+ jsonMode: true
8049
+ });
8050
+ spinner.stop();
8051
+ const projection = JSON.parse(extractJSON(featureResponse.content));
8052
+ const features = (projection.features ?? []).map((f) => ({
8053
+ id: f.id,
8054
+ name: f.name,
8055
+ description: f.description ?? "",
8056
+ subgraph: {
8057
+ routeIds: f.subgraph?.routeIds ?? [],
8058
+ transitionIds: f.subgraph?.transitionIds ?? [],
8059
+ stateVariableIds: f.subgraph?.stateVariableIds ?? []
8060
+ },
8061
+ complexity: (f.subgraph?.routeIds?.length ?? 0) + (f.subgraph?.transitionIds?.length ?? 0),
8062
+ criticalPaths: [],
8063
+ routes: f.routes ?? [],
8064
+ sourceFiles: f.sourceFiles ?? []
8065
+ }));
8066
+ const baseComponents = projection.components ?? [];
8067
+ success(`Semantic projection: ${features.length} features`);
8068
+ const v4Payload = {
8069
+ schemaVersion: 4,
8070
+ structuralGraph: graphResult.structuralGraph,
8071
+ stateSpace: graphResult.stateSpace,
8072
+ transitionSystem: graphResult.transitionSystem,
8073
+ semanticProjection: {
8074
+ labels: [],
8075
+ features
8076
+ },
8077
+ components: baseComponents,
8078
+ git: ast.git ? {
8079
+ commitSha: ast.git.commitSha,
8080
+ shortSha: ast.git.shortSha,
8081
+ branch: ast.git.branch,
8082
+ tags: ast.git.tags,
8083
+ isDirty: ast.git.isDirty
8084
+ } : undefined,
8085
+ metadata: ast.git ? { git: ast.git } : undefined,
8086
+ compat: projection.sections ? {
8087
+ sections: projection.sections,
8088
+ features: projection.features,
8089
+ workflows: [],
8090
+ scenarios: []
8091
+ } : undefined
8092
+ };
8093
+ writeFile(outputPath, JSON.stringify(v4Payload, null, 2));
8094
+ success(`V4 QA map written to ${outputPath}`);
8095
+ }
7989
8096
 
7990
8097
  // src/commands/auth.ts
7991
8098
  function registerAuth(program2) {
@@ -8301,8 +8408,15 @@ import { join as join7 } from "node:path";
8301
8408
 
8302
8409
  // src/scanner/push.ts
8303
8410
  async function pushToApi(payload, apiUrl, apiKey) {
8304
- const isV3 = payload.schemaVersion === 3;
8305
- const url = isV3 ? apiUrl.replace("/v2/push", "/v3/push") : apiUrl.replace("/v3/push", "/v2/push");
8411
+ const schemaVersion = payload.schemaVersion;
8412
+ let url;
8413
+ if (schemaVersion === 4) {
8414
+ url = apiUrl.replace(/\/v[23]\/push/, "/v4/push");
8415
+ } else if (schemaVersion === 3) {
8416
+ url = apiUrl.replace("/v2/push", "/v3/push");
8417
+ } else {
8418
+ url = apiUrl.replace("/v3/push", "/v2/push");
8419
+ }
8306
8420
  const response = await fetch(url, {
8307
8421
  method: "POST",
8308
8422
  headers: {
@@ -8336,7 +8450,9 @@ function registerPush(program2) {
8336
8450
  process.exit(1);
8337
8451
  }
8338
8452
  const raw = JSON.parse(readFile(inputPath));
8339
- const isV3 = raw.schemaVersion === 3;
8453
+ const schemaVersion = raw.schemaVersion ?? 2;
8454
+ const isV4 = schemaVersion === 4;
8455
+ const isV3 = schemaVersion === 3;
8340
8456
  const payload = raw;
8341
8457
  if (opts?.commitSha) {
8342
8458
  payload.commitSha = opts.commitSha;
@@ -8354,9 +8470,18 @@ function registerPush(program2) {
8354
8470
  payload.commitSha = gitMeta.commitSha;
8355
8471
  }
8356
8472
  }
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";
8473
+ if (isV4) {
8474
+ const v4 = raw;
8475
+ const nodeCount = v4.structuralGraph?.nodes?.length ?? 0;
8476
+ const edgeCount = v4.structuralGraph?.edges?.length ?? 0;
8477
+ const stateVarCount = v4.stateSpace?.variables?.length ?? 0;
8478
+ const featureCount = v4.semanticProjection?.features?.length ?? 0;
8479
+ info(`Pushing (V4): ${nodeCount} nodes, ${edgeCount} edges, ${stateVarCount} state vars, ${featureCount} features`);
8480
+ } else {
8481
+ const sectionCount = isV3 ? payload.sections.length : 0;
8482
+ info(`Pushing (V${isV3 ? 3 : 2}): ${sectionCount ? sectionCount + " sections, " : ""}${payload.features.length} features, ${payload.workflows.length} workflows, ${payload.scenarios.length} scenarios`);
8483
+ }
8484
+ const pushEndpoint = isV4 ? "/api/cli/push-v4" : isV3 ? "/api/cli/push-v3" : "/api/cli/push";
8360
8485
  try {
8361
8486
  const { AuthClient } = await import("./index-fbvbnntc.js");
8362
8487
  const client = AuthClient.fromSession();
@@ -8367,7 +8492,11 @@ function registerPush(program2) {
8367
8492
  spinner2.stop();
8368
8493
  success("Push successful!");
8369
8494
  info(` Version: ${result2.version} (schema v${result2.schemaVersion})`);
8370
- info(` Auto-linked scenarios: ${result2.stats.autoLinkedScenarios}`);
8495
+ if (result2.schemaVersion === 4) {
8496
+ info(` Nodes: ${result2.stats.structuralNodes ?? 0}, Transitions: ${result2.stats.transitions ?? 0}, Features: ${result2.stats.features ?? 0}`);
8497
+ } else {
8498
+ info(` Auto-linked scenarios: ${result2.stats.autoLinkedScenarios}`);
8499
+ }
8371
8500
  return;
8372
8501
  }
8373
8502
  } catch {}
@@ -8387,7 +8516,11 @@ function registerPush(program2) {
8387
8516
  spinner.stop();
8388
8517
  success("Push successful!");
8389
8518
  info(` Version: ${result.version} (schema v${result.schemaVersion})`);
8390
- info(` Auto-linked scenarios: ${result.stats.autoLinkedScenarios}`);
8519
+ if (result.schemaVersion === 4) {
8520
+ info(` Nodes: ${result.stats.structuralNodes ?? 0}, Transitions: ${result.stats.transitions ?? 0}, Features: ${result.stats.features ?? 0}`);
8521
+ } else {
8522
+ info(` Auto-linked scenarios: ${result.stats.autoLinkedScenarios}`);
8523
+ }
8391
8524
  });
8392
8525
  }
8393
8526
 
@@ -12509,11 +12642,20 @@ function registerScan(program2) {
12509
12642
  info(` Diff: first scan`);
12510
12643
  }
12511
12644
  writeFile(outputPath, JSON.stringify(ast, null, 2));
12645
+ const filePaths = ast.files.map((f) => join8(scanConfig.scanDir, f.path));
12646
+ const stateVars = extractStateVariables(filePaths, ast.routes, scanConfig.scanDir);
12647
+ if (stateVars.length > 0) {
12648
+ const stateVarsPath = outputPath.replace("ast-scan.json", "state-variables.json");
12649
+ writeFile(stateVarsPath, JSON.stringify(stateVars, null, 2));
12650
+ }
12512
12651
  success(`AST written to ${outputPath}`);
12513
12652
  info(` Files: ${ast.stats.totalFiles} (${ast.stats.totalLines} lines)`);
12514
12653
  info(` Routes: ${ast.routes.length}`);
12515
12654
  info(` Components: ${ast.components.length}`);
12516
12655
  info(` Hooks: ${ast.hooks.length}`);
12656
+ if (stateVars.length > 0) {
12657
+ info(` State variables: ${stateVars.length} (${stateVars.filter((v) => v.extractionConfidence === "high").length} high confidence)`);
12658
+ }
12517
12659
  if (ast.git) {
12518
12660
  info(` Git: ${ast.git.branch}@${ast.git.shortSha}${ast.git.isDirty ? " (dirty)" : ""}`);
12519
12661
  }
package/dist/mcp.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ buildV4Graph,
4
+ extractStateVariables,
3
5
  runStage1,
4
6
  validateQAMap,
5
7
  validateQAMapV3
6
- } from "./cli-fc97msmj.js";
8
+ } from "./cli-ychk5szm.js";
7
9
  import {
8
10
  $ZodObject,
9
11
  $ZodType,
@@ -14990,11 +14992,61 @@ async function scanCodebase(root) {
14990
14992
 
14991
14993
  // src/mcp.ts
14992
14994
  import { execSync } from "node:child_process";
14995
+ function validateQAMapV4(payload) {
14996
+ const errors = [];
14997
+ const warnings = [];
14998
+ if (!payload || typeof payload !== "object") {
14999
+ return { valid: false, errors: ["Payload must be a non-null object"], warnings };
15000
+ }
15001
+ const p = payload;
15002
+ if (p.schemaVersion !== 4) {
15003
+ errors.push("schemaVersion must be 4");
15004
+ }
15005
+ const sg = p.structuralGraph;
15006
+ if (!sg || typeof sg !== "object") {
15007
+ errors.push("structuralGraph is required");
15008
+ } else {
15009
+ if (!Array.isArray(sg.nodes))
15010
+ errors.push("structuralGraph.nodes must be an array");
15011
+ if (!Array.isArray(sg.edges))
15012
+ errors.push("structuralGraph.edges must be an array");
15013
+ }
15014
+ const ss = p.stateSpace;
15015
+ if (!ss || typeof ss !== "object") {
15016
+ errors.push("stateSpace is required");
15017
+ } else {
15018
+ if (!Array.isArray(ss.variables))
15019
+ errors.push("stateSpace.variables must be an array");
15020
+ if (!Array.isArray(ss.constraints))
15021
+ errors.push("stateSpace.constraints must be an array");
15022
+ if (!Array.isArray(ss.configurations))
15023
+ errors.push("stateSpace.configurations must be an array");
15024
+ }
15025
+ const ts = p.transitionSystem;
15026
+ if (!ts || typeof ts !== "object") {
15027
+ errors.push("transitionSystem is required");
15028
+ } else {
15029
+ if (!Array.isArray(ts.nodes))
15030
+ errors.push("transitionSystem.nodes must be an array");
15031
+ if (!Array.isArray(ts.transitions))
15032
+ errors.push("transitionSystem.transitions must be an array");
15033
+ }
15034
+ const sp = p.semanticProjection;
15035
+ if (sp && typeof sp === "object") {
15036
+ if (!Array.isArray(sp.features))
15037
+ warnings.push("semanticProjection.features should be an array");
15038
+ if (!Array.isArray(sp.labels))
15039
+ warnings.push("semanticProjection.labels should be an array");
15040
+ } else {
15041
+ warnings.push("semanticProjection is missing — AI labeling not yet applied");
15042
+ }
15043
+ return { valid: errors.length === 0, errors, warnings };
15044
+ }
14993
15045
  var SERVER_INSTRUCTIONS = `
14994
15046
  # Zefiro — Scanner & QA Map Guide
14995
15047
 
14996
15048
  You have access to Zefiro, an AI-powered codebase scanner and QA map generator.
14997
- Supports two schema versions: V2 (flat/linear) and V3 (hierarchical/DAG). **Default to V3.**
15049
+ Supports three schema versions: V2 (flat/linear), V3 (hierarchical/DAG), and V4 (formal model). **Default to V3 for AI analysis, V4 for formal graph-based analysis.**
14998
15050
 
14999
15051
  ## Core Workflow
15000
15052
 
@@ -15010,7 +15062,9 @@ Supports two schema versions: V2 (flat/linear) and V3 (hierarchical/DAG). **Defa
15010
15062
  - \`zefiro_scan_codebase\` — scan test files, configs, fixtures, path aliases
15011
15063
  - \`zefiro_scan_ast\` — deep AST scan: routes, components, hooks, imports, navigation patterns
15012
15064
  - \`zefiro_scan_ast_detail\` — drill into a specific AST category (routes, components, hooks, dependencies, files, navigationPatterns)
15013
- - \`zefiro_build_qa_map\` — validate + write QA map payload (V2 or V3, auto-detected)
15065
+ - \`zefiro_extract_state_variables\` — V4: extract state variables (auth, roles, flags, permissions) from scanned AST
15066
+ - \`zefiro_build_v4_graph\` — V4: build formal model (structural graph + state space + transition system) from AST + state variables
15067
+ - \`zefiro_build_qa_map\` — validate + write QA map payload (V2, V3, or V4 auto-detected)
15014
15068
  - \`zefiro_read_qa_map\` — read existing QA map
15015
15069
 
15016
15070
  ## Scanner V3 Enhancements
@@ -15208,7 +15262,7 @@ V2 payloads do NOT include a schemaVersion field. Flat features, linear workflow
15208
15262
  ### V2 ConditionalBranch: \`{ condition, outcome, type: "validation"|"permission"|"error"|"business-logic" }\`
15209
15263
  ### V2 Scenario: \`{ id, workflowId, featureId, name, description, category, preconditions: string[], steps: ScenarioStep[], expectedOutcome, componentIds: string[], workflowStepIds: string[], priority }\`
15210
15264
  `.trim();
15211
- var server = new McpServer({ name: "zefiro", version: "0.4.0" }, { instructions: SERVER_INSTRUCTIONS });
15265
+ var server = new McpServer({ name: "zefiro", version: "0.6.1" }, { instructions: SERVER_INSTRUCTIONS });
15212
15266
  server.registerTool("zefiro_scan_codebase", {
15213
15267
  title: "Scan Codebase",
15214
15268
  description: "Scan a project directory for test files, configs, fixtures, path aliases, and sample test content.",
@@ -15272,16 +15326,17 @@ server.registerTool("zefiro_scan_ast_detail", {
15272
15326
  });
15273
15327
  server.registerTool("zefiro_build_qa_map", {
15274
15328
  title: "Build QA Map",
15275
- description: "Validate and optionally write a QA map payload (V2 or V3, auto-detected by schemaVersion field). Use dryRun: true to validate without writing.",
15329
+ description: "Validate and optionally write a QA map payload (V2, V3, or V4, auto-detected by schemaVersion field). Use dryRun: true to validate without writing.",
15276
15330
  inputSchema: exports_external.object({
15277
- payload: exports_external.any().describe("QAMapV2Payload or QAMapV3Payload JSON object. V3 must include schemaVersion: 3."),
15331
+ payload: exports_external.any().describe("QAMapV2Payload, QAMapV3Payload, or QAMapV4Payload JSON object. V3 must include schemaVersion: 3, V4 must include schemaVersion: 4."),
15278
15332
  output: exports_external.string().optional().describe(`Output file path (defaults to ${CONFIG_DIR}/qa-map.json)`),
15279
15333
  dryRun: exports_external.boolean().optional().describe("Validate only, do not write (default: false)")
15280
15334
  })
15281
15335
  }, async ({ payload, output, dryRun }) => {
15282
15336
  try {
15337
+ const isV4 = payload && typeof payload === "object" && payload.schemaVersion === 4;
15283
15338
  const isV3 = payload && typeof payload === "object" && payload.schemaVersion === 3;
15284
- const validation = isV3 ? validateQAMapV3(payload) : validateQAMap(payload);
15339
+ const validation = isV4 ? validateQAMapV4(payload) : isV3 ? validateQAMapV3(payload) : validateQAMap(payload);
15285
15340
  if (validation.valid && payload && typeof payload === "object") {
15286
15341
  try {
15287
15342
  const sha = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
@@ -15298,7 +15353,15 @@ server.registerTool("zefiro_build_qa_map", {
15298
15353
  written = true;
15299
15354
  }
15300
15355
  const p = payload;
15301
- const stats = validation.valid ? {
15356
+ const stats = validation.valid ? isV4 ? {
15357
+ structuralNodes: Array.isArray(p?.structuralGraph?.nodes) ? p.structuralGraph.nodes.length : 0,
15358
+ structuralEdges: Array.isArray(p?.structuralGraph?.edges) ? p.structuralGraph.edges.length : 0,
15359
+ stateVariables: Array.isArray(p?.stateSpace?.variables) ? p.stateSpace.variables.length : 0,
15360
+ stateConfigurations: Array.isArray(p?.stateSpace?.configurations) ? p.stateSpace.configurations.length : 0,
15361
+ transitionNodes: Array.isArray(p?.transitionSystem?.nodes) ? p.transitionSystem.nodes.length : 0,
15362
+ transitions: Array.isArray(p?.transitionSystem?.transitions) ? p.transitionSystem.transitions.length : 0,
15363
+ features: Array.isArray(p?.semanticProjection?.features) ? p.semanticProjection.features.length : 0
15364
+ } : {
15302
15365
  sections: isV3 && Array.isArray(p?.sections) ? p.sections.length : undefined,
15303
15366
  features: Array.isArray(p?.features) ? p.features.length : 0,
15304
15367
  workflows: Array.isArray(p?.workflows) ? p.workflows.length : 0,
@@ -15308,13 +15371,96 @@ server.registerTool("zefiro_build_qa_map", {
15308
15371
  return {
15309
15372
  content: [{
15310
15373
  type: "text",
15311
- text: JSON.stringify({ valid: validation.valid, schemaVersion: isV3 ? 3 : 2, errors: validation.errors, warnings: validation.warnings, written, outputPath: written ? outputPath : null, stats }, null, 2)
15374
+ text: JSON.stringify({ valid: validation.valid, schemaVersion: isV4 ? 4 : isV3 ? 3 : 2, errors: validation.errors, warnings: validation.warnings, written, outputPath: written ? outputPath : null, stats }, null, 2)
15312
15375
  }]
15313
15376
  };
15314
15377
  } catch (err) {
15315
15378
  return { content: [{ type: "text", text: `Error building QA map: ${err.message}` }], isError: true };
15316
15379
  }
15317
15380
  });
15381
+ server.registerTool("zefiro_extract_state_variables", {
15382
+ title: "Extract State Variables (V4)",
15383
+ description: "Extract state variables from the scanned codebase. Requires a prior zefiro_scan_ast call. " + "Returns state variables (auth guards, roles, feature flags, permissions, UI state, URL params) with confidence levels.",
15384
+ inputSchema: exports_external.object({})
15385
+ }, async () => {
15386
+ try {
15387
+ const cwd = process.cwd();
15388
+ const astPath = join2(cwd, CONFIG_DIR, "ast-scan.json");
15389
+ if (!existsSync2(astPath)) {
15390
+ return { content: [{ type: "text", text: "Error: No AST scan found. Run zefiro_scan_ast first." }], isError: true };
15391
+ }
15392
+ const ast = JSON.parse(readFileSync2(astPath, "utf-8"));
15393
+ const scanDir = ast.files.length > 0 ? join2(cwd, ast.scanDir ?? "src") : join2(cwd, "src");
15394
+ const filePaths = ast.files.map((f) => join2(scanDir, f.path));
15395
+ const stateVars = extractStateVariables(filePaths, ast.routes, scanDir);
15396
+ const stateVarsPath = join2(cwd, CONFIG_DIR, "state-variables.json");
15397
+ const { mkdirSync, writeFileSync } = await import("node:fs");
15398
+ mkdirSync(join2(cwd, CONFIG_DIR), { recursive: true });
15399
+ writeFileSync(stateVarsPath, JSON.stringify(stateVars, null, 2));
15400
+ const summary = {
15401
+ total: stateVars.length,
15402
+ byConfidence: {
15403
+ high: stateVars.filter((v) => v.extractionConfidence === "high").length,
15404
+ medium: stateVars.filter((v) => v.extractionConfidence === "medium").length,
15405
+ low: stateVars.filter((v) => v.extractionConfidence === "low").length
15406
+ },
15407
+ byCategory: stateVars.reduce((acc, v) => {
15408
+ const cat = v.id.split(":")[1]?.split("-")[0] ?? "unknown";
15409
+ acc[cat] = (acc[cat] ?? 0) + 1;
15410
+ return acc;
15411
+ }, {}),
15412
+ variables: stateVars,
15413
+ savedTo: stateVarsPath
15414
+ };
15415
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
15416
+ } catch (err) {
15417
+ return { content: [{ type: "text", text: `Error extracting state variables: ${err.message}` }], isError: true };
15418
+ }
15419
+ });
15420
+ server.registerTool("zefiro_build_v4_graph", {
15421
+ title: "Build V4 Graph",
15422
+ description: "Build the formal V4 model (structural graph + state space + transition system) from the AST scan and state variables. " + "Requires prior zefiro_scan_ast and zefiro_extract_state_variables calls. Returns graph statistics and saves the result.",
15423
+ inputSchema: exports_external.object({})
15424
+ }, async () => {
15425
+ try {
15426
+ const cwd = process.cwd();
15427
+ const astPath = join2(cwd, CONFIG_DIR, "ast-scan.json");
15428
+ const stateVarsPath = join2(cwd, CONFIG_DIR, "state-variables.json");
15429
+ if (!existsSync2(astPath)) {
15430
+ return { content: [{ type: "text", text: "Error: No AST scan found. Run zefiro_scan_ast first." }], isError: true };
15431
+ }
15432
+ if (!existsSync2(stateVarsPath)) {
15433
+ return { content: [{ type: "text", text: "Error: No state variables found. Run zefiro_extract_state_variables first." }], isError: true };
15434
+ }
15435
+ const ast = JSON.parse(readFileSync2(astPath, "utf-8"));
15436
+ const stateVars = JSON.parse(readFileSync2(stateVarsPath, "utf-8"));
15437
+ const result = buildV4Graph(ast, stateVars);
15438
+ const graphPath = join2(cwd, CONFIG_DIR, "v4-graph.json");
15439
+ const { mkdirSync, writeFileSync } = await import("node:fs");
15440
+ mkdirSync(join2(cwd, CONFIG_DIR), { recursive: true });
15441
+ writeFileSync(graphPath, JSON.stringify(result, null, 2));
15442
+ const summary = {
15443
+ structuralGraph: {
15444
+ nodes: result.structuralGraph.nodes.length,
15445
+ edges: result.structuralGraph.edges.length
15446
+ },
15447
+ stateSpace: {
15448
+ variables: result.stateSpace.variables.length,
15449
+ constraints: result.stateSpace.constraints.length,
15450
+ configurations: result.stateSpace.configurations.length
15451
+ },
15452
+ transitionSystem: {
15453
+ nodes: result.transitionSystem.nodes.length,
15454
+ transitions: result.transitionSystem.transitions.length,
15455
+ entryPoints: result.transitionSystem.nodes.filter((n) => n.isEntryPoint).length
15456
+ },
15457
+ savedTo: graphPath
15458
+ };
15459
+ return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
15460
+ } catch (err) {
15461
+ return { content: [{ type: "text", text: `Error building V4 graph: ${err.message}` }], isError: true };
15462
+ }
15463
+ });
15318
15464
  server.registerTool("zefiro_read_qa_map", {
15319
15465
  title: "Read QA Map",
15320
15466
  description: "Read an existing QA map file.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zefiro",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "zefiro": "./dist/cli.js",