ai-spec-dev 0.31.0 → 0.33.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/.claude/settings.local.json +5 -1
- package/RELEASE_LOG.md +155 -0
- package/cli/commands/config.ts +93 -0
- package/cli/commands/export.ts +66 -0
- package/cli/commands/init.ts +153 -0
- package/cli/commands/learn.ts +30 -0
- package/cli/commands/logs.ts +106 -0
- package/cli/commands/model.ts +156 -0
- package/cli/commands/restore.ts +22 -0
- package/cli/commands/review.ts +63 -0
- package/cli/commands/trend.ts +36 -0
- package/cli/commands/update.ts +178 -0
- package/cli/commands/workspace.ts +219 -0
- package/cli/index.ts +277 -0
- package/cli/utils.ts +83 -0
- package/core/dsl-feedback.ts +255 -0
- package/core/run-trend.ts +241 -0
- package/core/self-evaluator.ts +106 -2
- package/dist/cli/index.js +972 -449
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +972 -449
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +6 -3
- package/tests/dsl-extractor.test.ts +264 -0
- package/tests/dsl-feedback.test.ts +266 -0
- package/tests/dsl-validator.test.ts +283 -0
- package/tests/error-feedback.test.ts +292 -0
- package/tests/provider-utils.test.ts +173 -0
- package/tests/run-trend.test.ts +186 -0
- package/tests/self-evaluator.test.ts +339 -0
- package/tests/spec-assessor.test.ts +142 -0
- package/tests/task-generator.test.ts +230 -0
package/cli/index.ts
CHANGED
|
@@ -75,6 +75,15 @@ import { generateRunId, RunLogger, setActiveLogger } from "../core/run-logger";
|
|
|
75
75
|
import { RunSnapshot, setActiveSnapshot } from "../core/run-snapshot";
|
|
76
76
|
import { computePromptHash } from "../core/prompt-hasher";
|
|
77
77
|
import { runSelfEval, printSelfEval } from "../core/self-evaluator";
|
|
78
|
+
import { loadRunLogs, buildTrendReport, printTrendReport } from "../core/run-trend";
|
|
79
|
+
import {
|
|
80
|
+
assessDslRichness,
|
|
81
|
+
buildDslGapRefinementPrompt,
|
|
82
|
+
extractStructuralFindings,
|
|
83
|
+
buildStructuralAmendmentPrompt,
|
|
84
|
+
printDslGaps,
|
|
85
|
+
printStructuralFindings,
|
|
86
|
+
} from "../core/dsl-feedback";
|
|
78
87
|
|
|
79
88
|
// ─── Config File ──────────────────────────────────────────────────────────────
|
|
80
89
|
|
|
@@ -524,6 +533,60 @@ program
|
|
|
524
533
|
}
|
|
525
534
|
}
|
|
526
535
|
|
|
536
|
+
// ── Loop 1: DSL Gap Feedback ──────────────────────────────────────────────
|
|
537
|
+
// Runs only in interactive mode (not --auto / --fast / --skip-dsl).
|
|
538
|
+
// Checks for common completeness gaps in the freshly-extracted DSL and
|
|
539
|
+
// offers a targeted spec refinement AI call to fill them before codegen.
|
|
540
|
+
// Zero extra AI calls unless the user explicitly opts in.
|
|
541
|
+
if (extractedDsl && !opts.auto && !opts.fast && !opts.skipDsl) {
|
|
542
|
+
const dslGaps = assessDslRichness(extractedDsl);
|
|
543
|
+
|
|
544
|
+
if (dslGaps.length > 0) {
|
|
545
|
+
printDslGaps(dslGaps);
|
|
546
|
+
runLogger.stageStart("dsl_gap_feedback", { gapCount: dslGaps.length, gaps: dslGaps.map((g) => g.code) });
|
|
547
|
+
|
|
548
|
+
const refineChoice = await select({
|
|
549
|
+
message: "How would you like to proceed?",
|
|
550
|
+
choices: [
|
|
551
|
+
{ name: "🔧 Refine spec (AI fills the gaps, then re-extract DSL)", value: "refine" },
|
|
552
|
+
{ name: "⏭ Skip — proceed with the current DSL", value: "skip" },
|
|
553
|
+
],
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
if (refineChoice === "refine") {
|
|
557
|
+
console.log(chalk.blue(" Refining spec to fill DSL gaps..."));
|
|
558
|
+
try {
|
|
559
|
+
const refinedSpec = await specProvider.generate(
|
|
560
|
+
buildDslGapRefinementPrompt(finalSpec, dslGaps),
|
|
561
|
+
"You are a Senior Tech Lead doing a targeted spec revision. Output only the complete revised Markdown spec."
|
|
562
|
+
);
|
|
563
|
+
finalSpec = refinedSpec;
|
|
564
|
+
console.log(chalk.green(" ✔ Spec refined."));
|
|
565
|
+
|
|
566
|
+
// Re-extract DSL from the improved spec
|
|
567
|
+
console.log(chalk.blue(" Re-extracting DSL from refined spec..."));
|
|
568
|
+
const isFrontend2 = isFrontendDeps(context.dependencies);
|
|
569
|
+
const reExtractor = new DslExtractor(specProvider);
|
|
570
|
+
const reExtractedDsl = await reExtractor.extract(finalSpec, { auto: true, isFrontend: isFrontend2 });
|
|
571
|
+
if (reExtractedDsl) {
|
|
572
|
+
extractedDsl = reExtractedDsl;
|
|
573
|
+
console.log(chalk.green(` ✔ DSL re-extracted: ${extractedDsl.endpoints.length} endpoint(s), ${extractedDsl.models.length} model(s).`));
|
|
574
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined", endpoints: extractedDsl.endpoints.length, models: extractedDsl.models.length });
|
|
575
|
+
} else {
|
|
576
|
+
console.log(chalk.yellow(" ⚠ Re-extraction failed — keeping original DSL."));
|
|
577
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refined_but_reextract_failed" });
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
console.log(chalk.yellow(` ⚠ Spec refinement failed: ${(err as Error).message} — keeping original DSL.`));
|
|
581
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "refinement_error", error: (err as Error).message });
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
runLogger.stageEnd("dsl_gap_feedback", { action: "skipped" });
|
|
585
|
+
console.log(chalk.gray(" Continuing with current DSL."));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
527
590
|
// ── Step 4: Git Worktree ──────────────────────────────────────────────────
|
|
528
591
|
// Frontend projects (React / Vue / Next / React-Native) work directly on a
|
|
529
592
|
// feature branch — no worktree needed. node_modules is not copied into
|
|
@@ -674,6 +737,82 @@ program
|
|
|
674
737
|
await accumulateReviewKnowledge(specProvider, currentDir, reviewResult);
|
|
675
738
|
}
|
|
676
739
|
|
|
740
|
+
// ── Loop 2: Review → DSL Structural Feedback ─────────────────────────────
|
|
741
|
+
// Runs only in interactive mode when a review was produced.
|
|
742
|
+
// Classifies Pass 1 (architecture) findings as design-level vs implementation-
|
|
743
|
+
// level issues. Design issues belong in the Spec/DSL — not just in §9.
|
|
744
|
+
// If found: offer to amend the spec → re-extract DSL → overwrite saved DSL file.
|
|
745
|
+
// The user can then run `ai-spec update --codegen` to regenerate affected files.
|
|
746
|
+
if (reviewResult && !opts.skipReview && !opts.auto && extractedDsl && savedDslFile) {
|
|
747
|
+
const structuralFindings = extractStructuralFindings(reviewResult);
|
|
748
|
+
|
|
749
|
+
if (structuralFindings.length > 0) {
|
|
750
|
+
printStructuralFindings(structuralFindings);
|
|
751
|
+
runLogger.stageStart("review_dsl_feedback", { findingCount: structuralFindings.length, categories: structuralFindings.map((f) => f.category) });
|
|
752
|
+
|
|
753
|
+
const savedSpecContent = await fs.readFile(specFile, "utf-8");
|
|
754
|
+
|
|
755
|
+
const patchChoice = await select({
|
|
756
|
+
message: "These are design issues in the Spec/DSL. How would you like to handle them?",
|
|
757
|
+
choices: [
|
|
758
|
+
{ name: "🔧 Amend spec + update DSL (AI fixes the design issues, no regen yet)", value: "amend" },
|
|
759
|
+
{ name: "📝 Note in §9 only (already done — no DSL change)", value: "note" },
|
|
760
|
+
{ name: "⏭ Skip", value: "skip" },
|
|
761
|
+
],
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (patchChoice === "amend") {
|
|
765
|
+
console.log(chalk.blue(" Amending spec to address structural findings..."));
|
|
766
|
+
try {
|
|
767
|
+
const amendedSpec = await specProvider.generate(
|
|
768
|
+
buildStructuralAmendmentPrompt(savedSpecContent, structuralFindings),
|
|
769
|
+
"You are a Senior Tech Lead doing a targeted spec correction. Output only the complete revised Markdown spec."
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
// Snapshot spec + DSL before overwriting so `ai-spec restore <runId>` works
|
|
773
|
+
await runSnapshot.snapshotFile(specFile);
|
|
774
|
+
if (savedDslFile) await runSnapshot.snapshotFile(savedDslFile);
|
|
775
|
+
|
|
776
|
+
// Overwrite the saved spec file with the amendment
|
|
777
|
+
await fs.writeFile(specFile, amendedSpec, "utf-8");
|
|
778
|
+
console.log(chalk.green(` ✔ Spec updated: ${specFile}`));
|
|
779
|
+
|
|
780
|
+
// Re-extract DSL from the amended spec
|
|
781
|
+
console.log(chalk.blue(" Re-extracting DSL from amended spec..."));
|
|
782
|
+
const isFrontend3 = isFrontendDeps(context.dependencies);
|
|
783
|
+
const amendExtractor = new DslExtractor(specProvider);
|
|
784
|
+
const amendedDsl = await amendExtractor.extract(amendedSpec, { auto: true, isFrontend: isFrontend3 });
|
|
785
|
+
if (amendedDsl) {
|
|
786
|
+
// Overwrite saved DSL file
|
|
787
|
+
const dslWriter = new DslExtractor(specProvider);
|
|
788
|
+
const newDslPath = await dslWriter.saveDsl(amendedDsl, specFile);
|
|
789
|
+
extractedDsl = amendedDsl;
|
|
790
|
+
console.log(chalk.green(` ✔ DSL updated: ${newDslPath}`));
|
|
791
|
+
console.log(chalk.cyan(
|
|
792
|
+
`\n Next step: run ${chalk.white("ai-spec update --codegen")} to regenerate files affected by the DSL change.`
|
|
793
|
+
));
|
|
794
|
+
runLogger.stageEnd("review_dsl_feedback", {
|
|
795
|
+
action: "amended",
|
|
796
|
+
endpoints: amendedDsl.endpoints.length,
|
|
797
|
+
models: amendedDsl.models.length,
|
|
798
|
+
});
|
|
799
|
+
} else {
|
|
800
|
+
console.log(chalk.yellow(" ⚠ DSL re-extraction failed — spec was updated but DSL file unchanged."));
|
|
801
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amended_spec_only" });
|
|
802
|
+
}
|
|
803
|
+
} catch (err) {
|
|
804
|
+
console.log(chalk.yellow(` ⚠ Spec amendment failed: ${(err as Error).message}`));
|
|
805
|
+
runLogger.stageEnd("review_dsl_feedback", { action: "amendment_error", error: (err as Error).message });
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
runLogger.stageEnd("review_dsl_feedback", { action: patchChoice });
|
|
809
|
+
if (patchChoice === "note") {
|
|
810
|
+
console.log(chalk.gray(" Structural findings retained in §9. DSL unchanged."));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
677
816
|
// ── Step 10: Harness Self-Evaluation ──────────────────────────────────────
|
|
678
817
|
// Zero AI calls — deterministic scoring from file-system state + review text.
|
|
679
818
|
// Records harnessScore + promptHash in RunLog for cross-run trend analysis.
|
|
@@ -2253,4 +2392,142 @@ program
|
|
|
2253
2392
|
}
|
|
2254
2393
|
});
|
|
2255
2394
|
|
|
2395
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2396
|
+
// Command: trend
|
|
2397
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2398
|
+
|
|
2399
|
+
program
|
|
2400
|
+
.command("trend")
|
|
2401
|
+
.description("Show harness score trend across past create runs")
|
|
2402
|
+
.option("--last <n>", "Number of recent scored runs to show (default: 15)", "15")
|
|
2403
|
+
.option("--prompt <hash>", "Filter to a specific prompt hash (prefix match)")
|
|
2404
|
+
.option("--json", "Output raw JSON instead of formatted table")
|
|
2405
|
+
.action(async (opts: { last: string; prompt?: string; json?: boolean }) => {
|
|
2406
|
+
const currentDir = process.cwd();
|
|
2407
|
+
const last = parseInt(opts.last, 10) || 15;
|
|
2408
|
+
|
|
2409
|
+
const logs = await loadRunLogs(currentDir);
|
|
2410
|
+
if (logs.length === 0) {
|
|
2411
|
+
console.log(chalk.yellow(
|
|
2412
|
+
"\n No run logs found. Run `ai-spec create` at least once to start tracking.\n"
|
|
2413
|
+
));
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const report = buildTrendReport(logs, {
|
|
2418
|
+
last,
|
|
2419
|
+
promptFilter: opts.prompt,
|
|
2420
|
+
});
|
|
2421
|
+
|
|
2422
|
+
if (opts.json) {
|
|
2423
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
printTrendReport(report, currentDir);
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2431
|
+
// Command: logs
|
|
2432
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2433
|
+
|
|
2434
|
+
program
|
|
2435
|
+
.command("logs")
|
|
2436
|
+
.description("List recent run logs with stage timing")
|
|
2437
|
+
.argument("[runId]", "Show detailed stage breakdown for a specific run ID")
|
|
2438
|
+
.option("--last <n>", "Number of runs to list (default: 10)", "10")
|
|
2439
|
+
.action(async (runId: string | undefined, opts: { last: string }) => {
|
|
2440
|
+
const currentDir = process.cwd();
|
|
2441
|
+
const logDir = path.join(currentDir, ".ai-spec-logs");
|
|
2442
|
+
|
|
2443
|
+
if (!(await fs.pathExists(logDir))) {
|
|
2444
|
+
console.log(chalk.yellow("\n No run logs found (.ai-spec-logs/ does not exist).\n"));
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
if (runId) {
|
|
2449
|
+
// ── Detail view for a single run ──────────────────────────────────────
|
|
2450
|
+
const logPath = path.join(logDir, `${runId}.json`);
|
|
2451
|
+
if (!(await fs.pathExists(logPath))) {
|
|
2452
|
+
console.log(chalk.red(`\n Run not found: ${runId}\n`));
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
const log = await fs.readJson(logPath);
|
|
2456
|
+
|
|
2457
|
+
console.log(chalk.cyan(`\n─── Run: ${log.runId} ─────────────────────────────────`));
|
|
2458
|
+
console.log(chalk.gray(` Started : ${log.startedAt}`));
|
|
2459
|
+
if (log.endedAt) console.log(chalk.gray(` Ended : ${log.endedAt}`));
|
|
2460
|
+
if (log.totalDurationMs !== undefined)
|
|
2461
|
+
console.log(chalk.gray(` Duration: ${(log.totalDurationMs / 1000).toFixed(1)}s`));
|
|
2462
|
+
if (log.provider) console.log(chalk.gray(` Provider: ${log.provider} / ${log.model ?? "?"}`));
|
|
2463
|
+
if (log.promptHash) console.log(chalk.gray(` Prompt : ${log.promptHash}`));
|
|
2464
|
+
if (log.harnessScore !== undefined)
|
|
2465
|
+
console.log(chalk.white(` Score : ${log.harnessScore}/10`));
|
|
2466
|
+
if (log.filesWritten?.length)
|
|
2467
|
+
console.log(chalk.gray(` Files : ${log.filesWritten.length} written`));
|
|
2468
|
+
if (log.errors?.length)
|
|
2469
|
+
console.log(chalk.yellow(` Errors : ${log.errors.length}`));
|
|
2470
|
+
|
|
2471
|
+
if (log.entries?.length) {
|
|
2472
|
+
console.log(chalk.bold("\n Stages:\n"));
|
|
2473
|
+
const doneEvents = (log.entries as Array<{ event: string; data?: Record<string, unknown>; ts: string }>)
|
|
2474
|
+
.filter((e) => e.event.endsWith(":done") || e.event.endsWith(":failed"));
|
|
2475
|
+
|
|
2476
|
+
for (const entry of doneEvents) {
|
|
2477
|
+
const isOk = entry.event.endsWith(":done");
|
|
2478
|
+
const stage = entry.event.replace(/:done$|:failed$/, "");
|
|
2479
|
+
const dur = entry.data?.durationMs
|
|
2480
|
+
? chalk.gray(` ${(Number(entry.data.durationMs) / 1000).toFixed(1)}s`)
|
|
2481
|
+
: "";
|
|
2482
|
+
const mark = isOk ? chalk.green("✔") : chalk.red("✘");
|
|
2483
|
+
console.log(` ${mark} ${stage.padEnd(20)}${dur}`);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
console.log(chalk.cyan("─".repeat(52)));
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// ── List view ─────────────────────────────────────────────────────────────
|
|
2491
|
+
const logs = await loadRunLogs(currentDir);
|
|
2492
|
+
const last = parseInt(opts.last, 10) || 10;
|
|
2493
|
+
const shown = logs.slice(0, last);
|
|
2494
|
+
|
|
2495
|
+
if (shown.length === 0) {
|
|
2496
|
+
console.log(chalk.yellow("\n No run logs found.\n"));
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
console.log(chalk.cyan("\n─── Run Logs ────────────────────────────────────────────────"));
|
|
2501
|
+
console.log(chalk.gray(
|
|
2502
|
+
"\n " +
|
|
2503
|
+
"Run ID ".padEnd(26) +
|
|
2504
|
+
"Date " +
|
|
2505
|
+
"Score ".padStart(6) +
|
|
2506
|
+
" Files Dur\n"
|
|
2507
|
+
));
|
|
2508
|
+
|
|
2509
|
+
for (const log of shown) {
|
|
2510
|
+
const date = log.startedAt.slice(0, 10);
|
|
2511
|
+
const score = log.harnessScore !== undefined
|
|
2512
|
+
? (log.harnessScore >= 8 ? chalk.green : log.harnessScore >= 6 ? chalk.yellow : chalk.red)(
|
|
2513
|
+
log.harnessScore.toFixed(1).padStart(5)
|
|
2514
|
+
)
|
|
2515
|
+
: chalk.gray(" —");
|
|
2516
|
+
const files = String(log.filesWritten?.length ?? 0).padStart(5);
|
|
2517
|
+
const dur = log.totalDurationMs !== undefined
|
|
2518
|
+
? chalk.gray((log.totalDurationMs / 1000).toFixed(0) + "s")
|
|
2519
|
+
: chalk.gray("—");
|
|
2520
|
+
const errMark = (log.errors?.length ?? 0) > 0
|
|
2521
|
+
? chalk.yellow(` ⚠${log.errors.length}`)
|
|
2522
|
+
: "";
|
|
2523
|
+
|
|
2524
|
+
console.log(` ${chalk.white(log.runId.padEnd(25))} ${chalk.gray(date)} ${score} ${chalk.gray(files)} ${dur}${errMark}`);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
console.log(chalk.gray(`\n Showing ${shown.length} of ${logs.length} run(s) · logs: .ai-spec-logs/`));
|
|
2528
|
+
console.log(chalk.cyan("─".repeat(63)));
|
|
2529
|
+
console.log(chalk.gray(` Tip: ai-spec logs <runId> to see stage breakdown`));
|
|
2530
|
+
console.log(chalk.gray(` ai-spec trend to see score trend by prompt version\n`));
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2256
2533
|
program.parse();
|
package/cli/utils.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs-extra";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { input, select } from "@inquirer/prompts";
|
|
5
|
+
import { CodeGenMode } from "../core/code-generator";
|
|
6
|
+
import { ENV_KEY_MAP } from "../core/spec-generator";
|
|
7
|
+
import { getSavedKey, saveKey, KEY_STORE_FILE } from "../core/key-store";
|
|
8
|
+
|
|
9
|
+
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface AiSpecConfig {
|
|
12
|
+
provider?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
codegen?: CodeGenMode;
|
|
15
|
+
codegenProvider?: string;
|
|
16
|
+
codegenModel?: string;
|
|
17
|
+
/** Minimum overall spec score (1-10) required to pass Approval Gate. 0 = disabled (default). */
|
|
18
|
+
minSpecScore?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CONFIG_FILE = ".ai-spec.json";
|
|
22
|
+
|
|
23
|
+
export async function loadConfig(dir: string): Promise<AiSpecConfig> {
|
|
24
|
+
const p = path.join(dir, CONFIG_FILE);
|
|
25
|
+
if (await fs.pathExists(p)) {
|
|
26
|
+
return fs.readJson(p);
|
|
27
|
+
}
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── API Key Resolution ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export async function resolveApiKey(
|
|
34
|
+
providerName: string,
|
|
35
|
+
cliKey?: string
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
if (cliKey) return cliKey;
|
|
38
|
+
|
|
39
|
+
const envVar = ENV_KEY_MAP[providerName];
|
|
40
|
+
if (envVar && process.env[envVar]) return process.env[envVar]!;
|
|
41
|
+
|
|
42
|
+
const savedKey = await getSavedKey(providerName);
|
|
43
|
+
if (savedKey) {
|
|
44
|
+
const masked = savedKey.slice(0, 6) + "..." + savedKey.slice(-4);
|
|
45
|
+
const choice = await select({
|
|
46
|
+
message: `${providerName} API key (saved: ${masked}):`,
|
|
47
|
+
choices: [
|
|
48
|
+
{ name: "Use saved key", value: "reuse" },
|
|
49
|
+
{ name: "Enter a new key", value: "new" },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
if (choice === "reuse") return savedKey;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const newKey = await input({
|
|
56
|
+
message: `Enter your ${providerName} API key${envVar ? ` (or set ${envVar} env var)` : ""}:`,
|
|
57
|
+
validate: (v) => v.trim().length > 0 || "API key cannot be empty",
|
|
58
|
+
});
|
|
59
|
+
await saveKey(providerName, newKey.trim());
|
|
60
|
+
console.log(chalk.gray(` Key saved to ${KEY_STORE_FILE}`));
|
|
61
|
+
return newKey.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Banner ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function printBanner(opts: {
|
|
67
|
+
specProvider: string;
|
|
68
|
+
specModel: string;
|
|
69
|
+
codegenMode: string;
|
|
70
|
+
codegenProvider: string;
|
|
71
|
+
codegenModel: string;
|
|
72
|
+
}) {
|
|
73
|
+
console.log(chalk.blue("\n" + "─".repeat(52)));
|
|
74
|
+
console.log(chalk.bold(" ai-spec — AI-driven Development Orchestrator"));
|
|
75
|
+
console.log(chalk.blue("─".repeat(52)));
|
|
76
|
+
console.log(chalk.gray(` Spec : ${opts.specProvider} / ${opts.specModel}`));
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.gray(
|
|
79
|
+
` Codegen : ${opts.codegenMode} (${opts.codegenProvider} / ${opts.codegenModel})`
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
console.log(chalk.blue("─".repeat(52) + "\n"));
|
|
83
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsl-feedback.ts — Two pipeline feedback loops for ai-spec create
|
|
3
|
+
*
|
|
4
|
+
* Loop 1 (DSL → Spec): after DSL extraction, detect sparse/incomplete DSL
|
|
5
|
+
* and offer a targeted spec refinement pass before codegen starts.
|
|
6
|
+
*
|
|
7
|
+
* Loop 2 (Review → DSL): after 3-pass review, detect design-level findings
|
|
8
|
+
* (as opposed to implementation issues) and offer to amend the spec + DSL
|
|
9
|
+
* so the next update/regen starts from a corrected contract.
|
|
10
|
+
*
|
|
11
|
+
* Design constraints:
|
|
12
|
+
* - Both loops are SKIPPED in --auto / --fast / --skip-dsl modes.
|
|
13
|
+
* - Zero extra AI calls until the user explicitly opts in.
|
|
14
|
+
* - Non-blocking: user can always skip.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { SpecDSL } from "./dsl-types";
|
|
19
|
+
|
|
20
|
+
// ─── Loop 1 Types ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface DslGap {
|
|
23
|
+
/** Short machine key for RunLog serialisation */
|
|
24
|
+
code: "sparse_model" | "missing_errors" | "generic_endpoint_desc" | "no_models_no_endpoints";
|
|
25
|
+
/** Human-readable message shown to the user */
|
|
26
|
+
message: string;
|
|
27
|
+
/** Concrete suggestion injected into the refinement prompt */
|
|
28
|
+
hint: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Loop 1: DSL Richness Assessment ─────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Inspect a freshly-extracted DSL for common completeness gaps.
|
|
35
|
+
* Returns a list of DslGap objects (empty = DSL looks adequate).
|
|
36
|
+
*
|
|
37
|
+
* All checks are pure heuristics — zero AI calls.
|
|
38
|
+
*/
|
|
39
|
+
export function assessDslRichness(dsl: SpecDSL): DslGap[] {
|
|
40
|
+
const gaps: DslGap[] = [];
|
|
41
|
+
|
|
42
|
+
// ── No endpoints AND no models ────────────────────────────────────────────
|
|
43
|
+
if (dsl.endpoints.length === 0 && dsl.models.length === 0) {
|
|
44
|
+
gaps.push({
|
|
45
|
+
code: "no_models_no_endpoints",
|
|
46
|
+
message: "DSL has no endpoints and no models — spec may be too abstract for structured extraction",
|
|
47
|
+
hint: "Please add explicit API endpoint definitions (method, path, request/response) and any data models that this feature requires.",
|
|
48
|
+
});
|
|
49
|
+
return gaps; // no point checking the rest
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Endpoints with very generic / short descriptions ─────────────────────
|
|
53
|
+
const GENERIC_DESC_KEYWORDS = ["handles", "processes", "manages", "操作", "处理", "管理"];
|
|
54
|
+
const GENERIC_DESC_MIN_LEN = 15;
|
|
55
|
+
|
|
56
|
+
for (const ep of dsl.endpoints) {
|
|
57
|
+
const desc = (ep.description ?? "").trim();
|
|
58
|
+
const isGeneric =
|
|
59
|
+
desc.length < GENERIC_DESC_MIN_LEN ||
|
|
60
|
+
GENERIC_DESC_KEYWORDS.some((kw) => desc.toLowerCase().startsWith(kw));
|
|
61
|
+
|
|
62
|
+
if (isGeneric) {
|
|
63
|
+
gaps.push({
|
|
64
|
+
code: "generic_endpoint_desc",
|
|
65
|
+
message: `Endpoint ${ep.method} ${ep.path} has a vague description: "${desc}"`,
|
|
66
|
+
hint: `Clarify what ${ep.method} ${ep.path} does: what inputs are required, what the success response contains, and what business rule it enforces.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Endpoints with no error definitions (but spec text likely mentions them) ──
|
|
72
|
+
const endpointsWithoutErrors = dsl.endpoints.filter(
|
|
73
|
+
(ep) => !ep.errors || ep.errors.length === 0
|
|
74
|
+
);
|
|
75
|
+
if (endpointsWithoutErrors.length > 0 && dsl.endpoints.length >= 2) {
|
|
76
|
+
gaps.push({
|
|
77
|
+
code: "missing_errors",
|
|
78
|
+
message: `${endpointsWithoutErrors.length}/${dsl.endpoints.length} endpoints have no error definitions`,
|
|
79
|
+
hint: `For each endpoint, specify at least the main error cases: e.g. 400 validation errors, 401 auth failures, 404 not found, 409 conflict. Include an error code (e.g. INVALID_INPUT) and description for each.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Models with fewer than 2 fields ──────────────────────────────────────
|
|
84
|
+
for (const model of dsl.models) {
|
|
85
|
+
if (!model.fields || model.fields.length < 2) {
|
|
86
|
+
gaps.push({
|
|
87
|
+
code: "sparse_model",
|
|
88
|
+
message: `Model "${model.name}" has only ${model.fields?.length ?? 0} field(s) — likely incomplete`,
|
|
89
|
+
hint: `List all fields for "${model.name}" with their types and whether they are required. Include at minimum an id, created_at, and the core domain fields this model needs.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return gaps;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Loop 1: Targeted Spec Refinement Prompt ─────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a targeted AI refinement prompt that focuses the LLM on filling
|
|
101
|
+
* only the specific gaps detected by `assessDslRichness`.
|
|
102
|
+
*/
|
|
103
|
+
export function buildDslGapRefinementPrompt(spec: string, gaps: DslGap[]): string {
|
|
104
|
+
const gapList = gaps
|
|
105
|
+
.map((g, i) => `${i + 1}. [${g.code}] ${g.message}\n → ${g.hint}`)
|
|
106
|
+
.join("\n\n");
|
|
107
|
+
|
|
108
|
+
return `The following feature spec has been structurally analysed. The DSL extracted from it was found to be incomplete in these specific areas:
|
|
109
|
+
|
|
110
|
+
${gapList}
|
|
111
|
+
|
|
112
|
+
Your task: revise the spec below to address ONLY the gaps listed above.
|
|
113
|
+
- Do NOT change the overall feature scope or business logic.
|
|
114
|
+
- Do NOT rewrite sections that are already complete.
|
|
115
|
+
- Add missing error cases, clarify vague endpoint descriptions, complete sparse model field lists.
|
|
116
|
+
- Output ONLY the complete revised Markdown spec. No preamble, no explanation.
|
|
117
|
+
|
|
118
|
+
=== Current Spec ===
|
|
119
|
+
${spec}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Loop 2 Types ─────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export interface StructuralFinding {
|
|
125
|
+
/** Short label for display + RunLog */
|
|
126
|
+
category: "auth_design" | "model_design" | "api_contract" | "layer_violation" | "other_design";
|
|
127
|
+
description: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Loop 2: Review Structural Issue Classifier ───────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse a 3-pass review text to extract Pass 1 (architecture) findings
|
|
134
|
+
* that indicate design-level issues in the Spec/DSL — as opposed to
|
|
135
|
+
* implementation-level issues that belong in §9 knowledge.
|
|
136
|
+
*
|
|
137
|
+
* Returns an empty array if no structural issues are found or if the
|
|
138
|
+
* review score for Pass 1 is high (≥ 8), indicating overall approval.
|
|
139
|
+
*/
|
|
140
|
+
export function extractStructuralFindings(reviewText: string): StructuralFinding[] {
|
|
141
|
+
// Split by the separator used between passes ("─────...")
|
|
142
|
+
const parts = reviewText.split(/─{20,}/);
|
|
143
|
+
// Pass 1 is always the first section
|
|
144
|
+
const pass1Text = parts[0] ?? "";
|
|
145
|
+
|
|
146
|
+
// If Pass 1 scored well, treat as no structural issues
|
|
147
|
+
const pass1Score = extractPassScore(pass1Text);
|
|
148
|
+
if (pass1Score !== null && pass1Score >= 8) return [];
|
|
149
|
+
|
|
150
|
+
const findings: StructuralFinding[] = [];
|
|
151
|
+
|
|
152
|
+
// ── Auth / 认证 design issues ──────────────────────────────────────────
|
|
153
|
+
if (
|
|
154
|
+
/缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should/i.test(pass1Text)
|
|
155
|
+
) {
|
|
156
|
+
const match = pass1Text.match(/[^。\n]*(?:缺少认证|missing auth|auth.*false|未加认证|鉴权.*缺|endpoint.*public.*should)[^。\n]*/i);
|
|
157
|
+
findings.push({
|
|
158
|
+
category: "auth_design",
|
|
159
|
+
description: match ? match[0].trim() : "One or more endpoints may have incorrect authentication requirements",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── API contract / 接口设计 issues ────────────────────────────────────
|
|
164
|
+
if (
|
|
165
|
+
/接口设计.*问题|接口.*不合理|API design|response.*missing|request.*missing|接口.*缺少/i.test(pass1Text)
|
|
166
|
+
) {
|
|
167
|
+
const match = pass1Text.match(/[^。\n]*(?:接口设计.*问题|接口.*不合理|API design|response.*missing|接口.*缺少)[^。\n]*/i);
|
|
168
|
+
findings.push({
|
|
169
|
+
category: "api_contract",
|
|
170
|
+
description: match ? match[0].trim() : "API contract design may have issues",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Model / 数据模型 design issues ────────────────────────────────────
|
|
175
|
+
if (
|
|
176
|
+
/模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete|字段.*missing/i.test(pass1Text)
|
|
177
|
+
) {
|
|
178
|
+
const match = pass1Text.match(/[^。\n]*(?:模型.*缺少字段|model.*missing field|数据结构.*问题|schema.*incomplete)[^。\n]*/i);
|
|
179
|
+
findings.push({
|
|
180
|
+
category: "model_design",
|
|
181
|
+
description: match ? match[0].trim() : "Data model design may be incomplete",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Layer separation / 层级分离 violations ────────────────────────────
|
|
186
|
+
if (
|
|
187
|
+
/层级.*违反|layer.*violation|business logic.*controller|controller.*service.*混|分层.*问题/i.test(pass1Text)
|
|
188
|
+
) {
|
|
189
|
+
const match = pass1Text.match(/[^。\n]*(?:层级.*违反|layer.*violation|business logic.*controller|分层.*问题)[^。\n]*/i);
|
|
190
|
+
findings.push({
|
|
191
|
+
category: "layer_violation",
|
|
192
|
+
description: match ? match[0].trim() : "Layer separation may be violated in the generated code",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return findings;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Extract the numeric score from a single pass section. */
|
|
200
|
+
function extractPassScore(text: string): number | null {
|
|
201
|
+
const m = text.match(/Score:\s*(\d+(?:\.\d+)?)\s*\/\s*10/i);
|
|
202
|
+
return m ? parseFloat(m[1]) : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Loop 2: Spec Amendment Prompt ────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build a prompt asking the AI to produce a minimal spec amendment
|
|
209
|
+
* that addresses the structural findings from the review.
|
|
210
|
+
*
|
|
211
|
+
* The amendment is a targeted addition/correction — NOT a full rewrite.
|
|
212
|
+
*/
|
|
213
|
+
export function buildStructuralAmendmentPrompt(
|
|
214
|
+
spec: string,
|
|
215
|
+
findings: StructuralFinding[]
|
|
216
|
+
): string {
|
|
217
|
+
const findingList = findings
|
|
218
|
+
.map((f, i) => `${i + 1}. [${f.category}] ${f.description}`)
|
|
219
|
+
.join("\n");
|
|
220
|
+
|
|
221
|
+
return `A code review of the feature built from this spec found the following DESIGN-LEVEL issues.
|
|
222
|
+
These are problems in the spec/contract itself, not in the implementation.
|
|
223
|
+
|
|
224
|
+
=== Structural Findings ===
|
|
225
|
+
${findingList}
|
|
226
|
+
|
|
227
|
+
Your task:
|
|
228
|
+
- Revise the spec below to correct the design issues listed above.
|
|
229
|
+
- Do NOT change the feature scope, business logic, or sections unrelated to these findings.
|
|
230
|
+
- Be minimal: only change what is necessary to fix the design issues.
|
|
231
|
+
- Output ONLY the complete revised Markdown spec. No preamble, no explanation.
|
|
232
|
+
|
|
233
|
+
=== Current Spec ===
|
|
234
|
+
${spec}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── Display Helpers ──────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
export function printDslGaps(gaps: DslGap[]): void {
|
|
240
|
+
console.log(chalk.yellow("\n ⚠ DSL Completeness Check — gaps detected:"));
|
|
241
|
+
for (const gap of gaps) {
|
|
242
|
+
console.log(chalk.yellow(` · ${gap.message}`));
|
|
243
|
+
}
|
|
244
|
+
console.log(chalk.gray(" → A targeted spec refinement can fill these gaps before codegen."));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function printStructuralFindings(findings: StructuralFinding[]): void {
|
|
248
|
+
console.log(chalk.yellow("\n ⚠ Review — structural (design-level) issues found:"));
|
|
249
|
+
for (const f of findings) {
|
|
250
|
+
const label = chalk.gray(`[${f.category}]`);
|
|
251
|
+
console.log(` ${label} ${f.description}`);
|
|
252
|
+
}
|
|
253
|
+
console.log(chalk.gray(" → These are contract issues in the Spec/DSL, not just implementation problems."));
|
|
254
|
+
console.log(chalk.gray(" → Fixing the spec now means the next run generates correct code from the start."));
|
|
255
|
+
}
|