@vijaypjavvadi/bdd2pw 1.1.7 → 2.0.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.
@@ -1,24 +1,44 @@
1
1
  /**
2
- * Routes every LLM call through the `ai-governance` sidecar (default :8004).
3
- * No direct calls to provider SDKs — providers are plugins inside the sidecar.
2
+ * Wrapper for the ai-governance sidecar's `/sanitize` endpoint.
4
3
  *
5
- * Phase 4 work item.
4
+ * Per `E:\EB1A_Research\ai-governance\service\app.py`:
5
+ *
6
+ * POST /sanitize
7
+ * body: { kind: "code", content: string }
8
+ * 200: { kind: "code", sanitised: string, findings: [] }
9
+ *
10
+ * v2.0 (this file): we send the LLM PROMPT (the user-message content
11
+ * built in `prompt.ts`) as kind="code" — `code` re-uses the log-rule
12
+ * scrubber to redact secrets that might leak via test data in the
13
+ * .feature file.
14
+ *
15
+ * v1.x note: the original Phase 4 stub here imagined a unified
16
+ * "callViaGovernance" RPC that the sidecar would proxy to providers.
17
+ * That's NOT the actual sidecar API — the sidecar is a sanitiser, not
18
+ * an LLM proxy. bdd2pw owns the provider call; the sidecar just scrubs
19
+ * the payload first.
20
+ *
21
+ * Fail-closed: if the sidecar is unreachable or returns a non-2xx, we
22
+ * REFUSE to call the LLM. Better to degrade to TODO than leak unsanitised
23
+ * payloads. The caller can pass `skipGovernance: true` for unit tests
24
+ * (NOT for production).
6
25
  */
26
+ export interface SanitiseResult {
27
+ sanitised: string;
28
+ findings: Array<Record<string, unknown>>;
29
+ }
30
+ export declare class GovernanceUnreachableError extends Error {
31
+ constructor(url: string, cause?: unknown);
32
+ }
33
+ /** Legacy alias kept for backwards-compat with any external imports. */
7
34
  export declare class GovernanceError extends Error {
8
35
  readonly cause?: unknown | undefined;
9
36
  constructor(message: string, cause?: unknown | undefined);
10
37
  }
11
- export interface GovernanceCallInput {
12
- provider: "anthropic" | "openai" | "gemini";
13
- model?: string;
14
- prompt: string;
15
- context?: Record<string, unknown>;
16
- governanceUrl?: string;
17
- }
18
- export interface GovernanceCallResult {
19
- text: string;
20
- redactions: number;
21
- provider: string;
22
- model: string;
38
+ export declare class GovernanceClient {
39
+ private baseUrl;
40
+ constructor(baseUrl: string);
41
+ sanitiseCode(content: string): Promise<SanitiseResult>;
42
+ /** Probe `/health` — used at startup to fail-fast before the first scaffold. */
43
+ health(): Promise<boolean>;
23
44
  }
24
- export declare function callViaGovernance(_input: GovernanceCallInput): Promise<GovernanceCallResult>;
@@ -1,13 +1,43 @@
1
1
  "use strict";
2
2
  /**
3
- * Routes every LLM call through the `ai-governance` sidecar (default :8004).
4
- * No direct calls to provider SDKs — providers are plugins inside the sidecar.
3
+ * Wrapper for the ai-governance sidecar's `/sanitize` endpoint.
5
4
  *
6
- * Phase 4 work item.
5
+ * Per `E:\EB1A_Research\ai-governance\service\app.py`:
6
+ *
7
+ * POST /sanitize
8
+ * body: { kind: "code", content: string }
9
+ * 200: { kind: "code", sanitised: string, findings: [] }
10
+ *
11
+ * v2.0 (this file): we send the LLM PROMPT (the user-message content
12
+ * built in `prompt.ts`) as kind="code" — `code` re-uses the log-rule
13
+ * scrubber to redact secrets that might leak via test data in the
14
+ * .feature file.
15
+ *
16
+ * v1.x note: the original Phase 4 stub here imagined a unified
17
+ * "callViaGovernance" RPC that the sidecar would proxy to providers.
18
+ * That's NOT the actual sidecar API — the sidecar is a sanitiser, not
19
+ * an LLM proxy. bdd2pw owns the provider call; the sidecar just scrubs
20
+ * the payload first.
21
+ *
22
+ * Fail-closed: if the sidecar is unreachable or returns a non-2xx, we
23
+ * REFUSE to call the LLM. Better to degrade to TODO than leak unsanitised
24
+ * payloads. The caller can pass `skipGovernance: true` for unit tests
25
+ * (NOT for production).
7
26
  */
8
27
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.GovernanceError = void 0;
10
- exports.callViaGovernance = callViaGovernance;
28
+ exports.GovernanceClient = exports.GovernanceError = exports.GovernanceUnreachableError = void 0;
29
+ const undici_1 = require("undici");
30
+ class GovernanceUnreachableError extends Error {
31
+ constructor(url, cause) {
32
+ super(`ai-governance sidecar unreachable at ${url}. Start it with: ` +
33
+ `'uvicorn service.app:app --port 4900' from the ai-governance repo, ` +
34
+ `or pass --skip-governance for non-production runs (NOT recommended). ` +
35
+ (cause instanceof Error ? `Underlying error: ${cause.message}` : ""));
36
+ this.name = "GovernanceUnreachableError";
37
+ }
38
+ }
39
+ exports.GovernanceUnreachableError = GovernanceUnreachableError;
40
+ /** Legacy alias kept for backwards-compat with any external imports. */
11
41
  class GovernanceError extends Error {
12
42
  constructor(message, cause) {
13
43
  super(message);
@@ -16,7 +46,43 @@ class GovernanceError extends Error {
16
46
  }
17
47
  }
18
48
  exports.GovernanceError = GovernanceError;
19
- async function callViaGovernance(_input) {
20
- throw new GovernanceError("callViaGovernance() — Phase 4");
49
+ class GovernanceClient {
50
+ constructor(baseUrl) {
51
+ this.baseUrl = baseUrl.replace(/\/$/, "");
52
+ }
53
+ async sanitiseCode(content) {
54
+ const url = `${this.baseUrl}/sanitize`;
55
+ let res;
56
+ try {
57
+ res = await (0, undici_1.request)(url, {
58
+ method: "POST",
59
+ headers: { "content-type": "application/json" },
60
+ body: JSON.stringify({ kind: "code", content }),
61
+ });
62
+ }
63
+ catch (err) {
64
+ throw new GovernanceUnreachableError(url, err);
65
+ }
66
+ if (res.statusCode < 200 || res.statusCode >= 300) {
67
+ const body = await res.body.text().catch(() => "(no body)");
68
+ throw new Error(`ai-governance /sanitize returned ${res.statusCode}: ${body.slice(0, 200)}`);
69
+ }
70
+ const json = (await res.body.json());
71
+ return {
72
+ sanitised: json.sanitised ?? content,
73
+ findings: json.findings ?? [],
74
+ };
75
+ }
76
+ /** Probe `/health` — used at startup to fail-fast before the first scaffold. */
77
+ async health() {
78
+ try {
79
+ const res = await (0, undici_1.request)(`${this.baseUrl}/health`);
80
+ return res.statusCode === 200;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
21
86
  }
87
+ exports.GovernanceClient = GovernanceClient;
22
88
  //# sourceMappingURL=governanceClient.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"governanceClient.js","sourceRoot":"","sources":["../../src/llm/governanceClient.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAwBH,8CAIC;AA1BD,MAAa,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe,EAAkB,KAAe;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QAD4B,UAAK,GAAL,KAAK,CAAU;QAE1D,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AALD,0CAKC;AAiBM,KAAK,UAAU,iBAAiB,CACrC,MAA2B;IAE3B,MAAM,IAAI,eAAe,CAAC,+BAA+B,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"governanceClient.js","sourceRoot":"","sources":["../../src/llm/governanceClient.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,mCAAiC;AAOjC,MAAa,0BAA2B,SAAQ,KAAK;IACnD,YAAY,GAAW,EAAE,KAAe;QACtC,KAAK,CACH,wCAAwC,GAAG,mBAAmB;YAC5D,qEAAqE;YACrE,uEAAuE;YACvE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,qBAAqB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACvE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAVD,gEAUC;AAED,wEAAwE;AACxE,MAAa,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe,EAAkB,KAAe;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QAD4B,UAAK,GAAL,KAAK,CAAU;QAE1D,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AALD,0CAKC;AAED,MAAa,gBAAgB;IAE3B,YAAY,OAAe;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAe;QAChC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,WAAW,CAAC;QACvC,IAAI,GAAG,CAAC;QACR,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,IAAA,gBAAO,EAAC,GAAG,EAAE;gBACvB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;aAChD,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,0BAA0B,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;YAClD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC;YAC5D,MAAM,IAAI,KAAK,CACb,oCAAoC,GAAG,CAAC,UAAU,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAC5E,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAGlC,CAAC;QACF,OAAO;YACL,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,OAAO;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;SAC9B,CAAC;IACJ,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,MAAM;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAA,gBAAO,EAAC,GAAG,IAAI,CAAC,OAAO,SAAS,CAAC,CAAC;YACpD,OAAO,GAAG,CAAC,UAAU,KAAK,GAAG,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AA3CD,4CA2CC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Public API of the LLM module — v2.0.
3
+ *
4
+ * import { createLLMClient, matchStepWithLLM, CandidateRulesWriter }
5
+ * from "./llm";
6
+ *
7
+ * The bdd2pw orchestrator (src/index.ts) only needs to import these
8
+ * three names. Everything else is internal.
9
+ */
10
+ import type { LLMClient, LLMClientOptions } from "./types";
11
+ export { AnthropicLLMClient } from "./anthropicClient";
12
+ export { CandidateRulesWriter } from "./candidateRules";
13
+ export { GovernanceClient, GovernanceUnreachableError } from "./governanceClient";
14
+ export { matchStepWithLLM, matchStepsWithLLM, type MatchStepWithLLMContext, } from "./llmStepMatcher";
15
+ export type { CandidateRuleEntry, GenerateBindingInput, GenerateBindingResult, LLMClient, LLMClientOptions, LLMLogEvent, } from "./types";
16
+ /**
17
+ * Construct the right LLMClient for the given options. Today the only
18
+ * provider is "anthropic"; the factory exists so v2.1 (OpenAI/Gemini)
19
+ * can plug in without touching callers.
20
+ *
21
+ * `repoRoot` is used to derive the default cache path:
22
+ * `<repoRoot>/.bdd2pw/llm-cache.sqlite`.
23
+ *
24
+ * Returns undefined when LLM is disabled (no provider configured) — so
25
+ * callers can pass the result straight into matchStepWithLLM's `llm`
26
+ * field without an explicit null-check.
27
+ */
28
+ export declare function createLLMClient(opts: LLMClientOptions | undefined, repoRoot: string): LLMClient | undefined;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * Public API of the LLM module — v2.0.
4
+ *
5
+ * import { createLLMClient, matchStepWithLLM, CandidateRulesWriter }
6
+ * from "./llm";
7
+ *
8
+ * The bdd2pw orchestrator (src/index.ts) only needs to import these
9
+ * three names. Everything else is internal.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.matchStepsWithLLM = exports.matchStepWithLLM = exports.GovernanceUnreachableError = exports.GovernanceClient = exports.CandidateRulesWriter = exports.AnthropicLLMClient = void 0;
46
+ exports.createLLMClient = createLLMClient;
47
+ const path = __importStar(require("path"));
48
+ const anthropicClient_1 = require("./anthropicClient");
49
+ var anthropicClient_2 = require("./anthropicClient");
50
+ Object.defineProperty(exports, "AnthropicLLMClient", { enumerable: true, get: function () { return anthropicClient_2.AnthropicLLMClient; } });
51
+ var candidateRules_1 = require("./candidateRules");
52
+ Object.defineProperty(exports, "CandidateRulesWriter", { enumerable: true, get: function () { return candidateRules_1.CandidateRulesWriter; } });
53
+ var governanceClient_1 = require("./governanceClient");
54
+ Object.defineProperty(exports, "GovernanceClient", { enumerable: true, get: function () { return governanceClient_1.GovernanceClient; } });
55
+ Object.defineProperty(exports, "GovernanceUnreachableError", { enumerable: true, get: function () { return governanceClient_1.GovernanceUnreachableError; } });
56
+ var llmStepMatcher_1 = require("./llmStepMatcher");
57
+ Object.defineProperty(exports, "matchStepWithLLM", { enumerable: true, get: function () { return llmStepMatcher_1.matchStepWithLLM; } });
58
+ Object.defineProperty(exports, "matchStepsWithLLM", { enumerable: true, get: function () { return llmStepMatcher_1.matchStepsWithLLM; } });
59
+ /**
60
+ * Construct the right LLMClient for the given options. Today the only
61
+ * provider is "anthropic"; the factory exists so v2.1 (OpenAI/Gemini)
62
+ * can plug in without touching callers.
63
+ *
64
+ * `repoRoot` is used to derive the default cache path:
65
+ * `<repoRoot>/.bdd2pw/llm-cache.sqlite`.
66
+ *
67
+ * Returns undefined when LLM is disabled (no provider configured) — so
68
+ * callers can pass the result straight into matchStepWithLLM's `llm`
69
+ * field without an explicit null-check.
70
+ */
71
+ function createLLMClient(opts, repoRoot) {
72
+ if (!opts || !opts.provider)
73
+ return undefined;
74
+ const cachePathDefault = path.join(repoRoot, ".bdd2pw", "llm-cache.sqlite");
75
+ switch (opts.provider) {
76
+ case "anthropic":
77
+ return new anthropicClient_1.AnthropicLLMClient(opts, cachePathDefault);
78
+ default:
79
+ // v2.1 will add openai / gemini.
80
+ return undefined;
81
+ }
82
+ }
83
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/llm/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCH,0CAiBC;AAlDD,2CAA6B;AAC7B,uDAAuD;AAGvD,qDAAuD;AAA9C,qHAAA,kBAAkB,OAAA;AAC3B,mDAAwD;AAA/C,sHAAA,oBAAoB,OAAA;AAC7B,uDAAkF;AAAzE,oHAAA,gBAAgB,OAAA;AAAE,8HAAA,0BAA0B,OAAA;AACrD,mDAI0B;AAHxB,kHAAA,gBAAgB,OAAA;AAChB,mHAAA,iBAAiB,OAAA;AAYnB;;;;;;;;;;;GAWG;AACH,SAAgB,eAAe,CAC7B,IAAkC,EAClC,QAAgB;IAEhB,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAChC,QAAQ,EACR,SAAS,EACT,kBAAkB,CACnB,CAAC;IACF,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QACtB,KAAK,WAAW;YACd,OAAO,IAAI,oCAAkB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;QACxD;YACE,iCAAiC;YACjC,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Async overlay over the deterministic stepMatcher — when the rule table
3
+ * fails to match a step AND an LLM client is provided, fall back to the
4
+ * LLM. Every successful LLM-generated binding is appended to
5
+ * `<repo>/artefacts/candidate-rules.jsonl` for offline review.
6
+ *
7
+ * Why a wrapper, not modifying matchStep:
8
+ * - Keeps the rule-based matcher pure-sync, so unit tests stay fast.
9
+ * - LLM fallback is opt-in and orthogonal — easier to reason about
10
+ * and to disable in CI where determinism matters.
11
+ * - The wrapper is the natural integration point for the candidate-
12
+ * rules JSONL writer; the deterministic matcher doesn't need to
13
+ * know that pipeline exists.
14
+ */
15
+ import type { PageObjectIR, StepBinding, StepIR } from "../types";
16
+ import { CandidateRulesWriter } from "./candidateRules";
17
+ import type { LLMClient } from "./types";
18
+ export interface MatchStepWithLLMContext {
19
+ /** When provided AND the rule matcher returns a warning, ask the LLM. */
20
+ llm?: LLMClient;
21
+ /** Where to log every successful LLM binding for offline review. */
22
+ candidates?: CandidateRulesWriter;
23
+ /** Stable scaffold-run identifier — written into each candidate row. */
24
+ scaffoldId: string;
25
+ /** Target URL — passed to LLM for additional context. */
26
+ url?: string;
27
+ }
28
+ export declare function matchStepWithLLM(step: StepIR, pom: PageObjectIR, pageVar: string, ctx: MatchStepWithLLMContext): Promise<StepBinding>;
29
+ /**
30
+ * Convenience: match a list of steps with LLM overlay, sequentially. We
31
+ * deliberately don't parallelise — the cache means repeated step text
32
+ * within a feature returns instantly, but parallel calls would all miss
33
+ * the cache and pay for the same step twice.
34
+ */
35
+ export declare function matchStepsWithLLM(steps: StepIR[], pom: PageObjectIR, pageVar: string, ctx: MatchStepWithLLMContext): Promise<StepBinding[]>;
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * Async overlay over the deterministic stepMatcher — when the rule table
4
+ * fails to match a step AND an LLM client is provided, fall back to the
5
+ * LLM. Every successful LLM-generated binding is appended to
6
+ * `<repo>/artefacts/candidate-rules.jsonl` for offline review.
7
+ *
8
+ * Why a wrapper, not modifying matchStep:
9
+ * - Keeps the rule-based matcher pure-sync, so unit tests stay fast.
10
+ * - LLM fallback is opt-in and orthogonal — easier to reason about
11
+ * and to disable in CI where determinism matters.
12
+ * - The wrapper is the natural integration point for the candidate-
13
+ * rules JSONL writer; the deterministic matcher doesn't need to
14
+ * know that pipeline exists.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.matchStepWithLLM = matchStepWithLLM;
18
+ exports.matchStepsWithLLM = matchStepsWithLLM;
19
+ const stepMatcher_1 = require("../transformers/stepMatcher");
20
+ async function matchStepWithLLM(step, pom, pageVar, ctx) {
21
+ // 1) Always try rule-based first.
22
+ const ruleBased = (0, stepMatcher_1.matchStep)(step, pom, pageVar);
23
+ // 2) If the rule matcher produced a real binding (no warning), use it.
24
+ if (!ruleBased.warning)
25
+ return ruleBased;
26
+ // 3) No LLM client → return the warning binding (TODO behaviour).
27
+ if (!ctx.llm)
28
+ return ruleBased;
29
+ // 4) Budget exhausted → return the warning binding (caller already
30
+ // notified at startup; this just keeps scaffolding moving).
31
+ if (ctx.llm.budgetExhausted())
32
+ return ruleBased;
33
+ // 5) Try the LLM. Soft-fail back to rule-based on any error.
34
+ try {
35
+ const result = await ctx.llm.generateBinding({
36
+ step,
37
+ pom,
38
+ pageVar,
39
+ url: ctx.url,
40
+ scaffoldId: ctx.scaffoldId,
41
+ });
42
+ if (!result.binding) {
43
+ // Annotate the warning with the LLM error so BDD_REVIEW shows why
44
+ // the LLM didn't help. The step still falls through to TODO.
45
+ return {
46
+ ...ruleBased,
47
+ warning: result.error
48
+ ? `${ruleBased.warning} (LLM fallback also failed: ${result.error})`
49
+ : ruleBased.warning,
50
+ };
51
+ }
52
+ // Append to candidate-rules.jsonl for offline review. Best-effort.
53
+ if (ctx.candidates) {
54
+ await ctx.candidates.append({
55
+ ts: new Date().toISOString(),
56
+ scaffoldId: ctx.scaffoldId,
57
+ stepText: step.text,
58
+ stepKeyword: step.keyword,
59
+ binding: result.binding,
60
+ pomSignature: {
61
+ className: pom.className,
62
+ fieldNames: pom.fields.map((f) => f.fieldName),
63
+ methodNames: pom.methods.map((m) => m.name),
64
+ },
65
+ provider: "anthropic",
66
+ model: result.model ?? "(unknown)",
67
+ fromCache: result.fromCache,
68
+ });
69
+ }
70
+ return result.binding;
71
+ }
72
+ catch (err) {
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ return {
75
+ ...ruleBased,
76
+ warning: `${ruleBased.warning} (LLM fallback threw: ${message})`,
77
+ };
78
+ }
79
+ }
80
+ /**
81
+ * Convenience: match a list of steps with LLM overlay, sequentially. We
82
+ * deliberately don't parallelise — the cache means repeated step text
83
+ * within a feature returns instantly, but parallel calls would all miss
84
+ * the cache and pay for the same step twice.
85
+ */
86
+ async function matchStepsWithLLM(steps, pom, pageVar, ctx) {
87
+ const out = [];
88
+ for (const step of steps) {
89
+ out.push(await matchStepWithLLM(step, pom, pageVar, ctx));
90
+ }
91
+ return out;
92
+ }
93
+ //# sourceMappingURL=llmStepMatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llmStepMatcher.js","sourceRoot":"","sources":["../../src/llm/llmStepMatcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Build the Anthropic prompt used to generate a `StepBinding` from an
3
+ * unmatched Gherkin step.
4
+ *
5
+ * Design principles:
6
+ *
7
+ * 1. **JSON-only output.** The model is instructed to return ONE JSON
8
+ * object matching the StepBinding type — no prose, no Markdown
9
+ * fences. We then JSON.parse it; malformed output triggers a soft-fail.
10
+ *
11
+ * 2. **Specific not creative.** Show the model the existing rule
12
+ * taxonomy (briefly) so it tries to match the same shapes our
13
+ * deterministic rules produce. Otherwise the LLM invents novel
14
+ * locator chains that drift from the rest of the codebase.
15
+ *
16
+ * 3. **POM-aware.** Every prompt includes the current POM's field and
17
+ * method names. The LLM should prefer existing fields over
18
+ * synthesising raw locators.
19
+ *
20
+ * 4. **Temperature 0** (set at call site) — same input → same output,
21
+ * paired with caching for true determinism.
22
+ */
23
+ import type { GenerateBindingInput } from "./types";
24
+ /** System prompt — shared across all generations. */
25
+ export declare const SYSTEM_PROMPT = "You are a deterministic step-binding generator for bdd2pw, a Gherkin \u2192 Playwright TypeScript scaffolder.\n\nbdd2pw normally maps each Gherkin step to one of 30+ deterministic regex rules. When no rule matches, you produce the binding instead.\n\nYour output MUST be a single JSON object matching this TypeScript type:\n\ninterface StepBinding {\n step: { keyword: \"Given\"|\"When\"|\"Then\"|\"And\"|\"But\"; text: string };\n pomCall?: { page: string; method: string; args: string[] }; // for actions\n assertion?: { locator: string; matcher: string; expected?: string }; // for assertions\n customBody?: string; // pre-rendered TS for compound steps\n warning?: string; // populated only if you genuinely cannot map the step\n}\n\nRules:\n- Output exactly ONE JSON object. No prose, no Markdown, no backticks.\n- Echo the step in the \"step\" field unchanged.\n- Choose ONE of: pomCall, assertion, customBody. Never two.\n- Prefer existing POM fields over synthesizing locators. Only use page.locator(...) / page.getByText(...) / page.getByRole(...) when no POM field matches.\n- For assertions, set \"matcher\" to one of: toBeVisible, toBeHidden, toContainText, toHaveText, toHaveURL, toHaveAttribute, not.toBeVisible, not.toHaveURL.\n- For toContainText/toHaveText, \"expected\" must be a JS-quoted string like \"\\\"X\\\"\".\n- For toHaveURL, \"expected\" should be: new RegExp(\"...\") (a JS expression as a STRING).\n- For toHaveAttribute, \"expected\" should be: \"name\", \"value\" (two args, as a single string).\n- Always end synthesised locators with .first() to avoid Playwright strict-mode violations when text appears in multiple elements.\n- If the step has no quoted value AND no resolvable POM field, set \"warning\" to a clear single-sentence explanation of what's ambiguous and emit no pomCall/assertion. The user will hand-edit the spec.";
26
+ /**
27
+ * Build the user-side message for one step. Includes:
28
+ * - The step keyword + text.
29
+ * - The POM IR (className, field names with their locator API+args, methods).
30
+ * - The target URL (gives the model context about what page).
31
+ * - The pageVar variable name to use in pomCall.page.
32
+ */
33
+ export declare function buildUserPrompt(input: GenerateBindingInput): string;
34
+ /**
35
+ * Cache key — stable across runs given identical inputs. Determinism plus
36
+ * cost control: the same step text + POM signature returns the same binding
37
+ * forever (until the user explicitly clears the cache or upgrades bdd2pw).
38
+ */
39
+ export declare function cacheKey(input: GenerateBindingInput, model: string): string;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * Build the Anthropic prompt used to generate a `StepBinding` from an
4
+ * unmatched Gherkin step.
5
+ *
6
+ * Design principles:
7
+ *
8
+ * 1. **JSON-only output.** The model is instructed to return ONE JSON
9
+ * object matching the StepBinding type — no prose, no Markdown
10
+ * fences. We then JSON.parse it; malformed output triggers a soft-fail.
11
+ *
12
+ * 2. **Specific not creative.** Show the model the existing rule
13
+ * taxonomy (briefly) so it tries to match the same shapes our
14
+ * deterministic rules produce. Otherwise the LLM invents novel
15
+ * locator chains that drift from the rest of the codebase.
16
+ *
17
+ * 3. **POM-aware.** Every prompt includes the current POM's field and
18
+ * method names. The LLM should prefer existing fields over
19
+ * synthesising raw locators.
20
+ *
21
+ * 4. **Temperature 0** (set at call site) — same input → same output,
22
+ * paired with caching for true determinism.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.SYSTEM_PROMPT = void 0;
26
+ exports.buildUserPrompt = buildUserPrompt;
27
+ exports.cacheKey = cacheKey;
28
+ /** System prompt — shared across all generations. */
29
+ exports.SYSTEM_PROMPT = `You are a deterministic step-binding generator for bdd2pw, a Gherkin → Playwright TypeScript scaffolder.
30
+
31
+ bdd2pw normally maps each Gherkin step to one of 30+ deterministic regex rules. When no rule matches, you produce the binding instead.
32
+
33
+ Your output MUST be a single JSON object matching this TypeScript type:
34
+
35
+ interface StepBinding {
36
+ step: { keyword: "Given"|"When"|"Then"|"And"|"But"; text: string };
37
+ pomCall?: { page: string; method: string; args: string[] }; // for actions
38
+ assertion?: { locator: string; matcher: string; expected?: string }; // for assertions
39
+ customBody?: string; // pre-rendered TS for compound steps
40
+ warning?: string; // populated only if you genuinely cannot map the step
41
+ }
42
+
43
+ Rules:
44
+ - Output exactly ONE JSON object. No prose, no Markdown, no backticks.
45
+ - Echo the step in the "step" field unchanged.
46
+ - Choose ONE of: pomCall, assertion, customBody. Never two.
47
+ - Prefer existing POM fields over synthesizing locators. Only use page.locator(...) / page.getByText(...) / page.getByRole(...) when no POM field matches.
48
+ - For assertions, set "matcher" to one of: toBeVisible, toBeHidden, toContainText, toHaveText, toHaveURL, toHaveAttribute, not.toBeVisible, not.toHaveURL.
49
+ - For toContainText/toHaveText, "expected" must be a JS-quoted string like "\\"X\\"".
50
+ - For toHaveURL, "expected" should be: new RegExp("...") (a JS expression as a STRING).
51
+ - For toHaveAttribute, "expected" should be: "name", "value" (two args, as a single string).
52
+ - Always end synthesised locators with .first() to avoid Playwright strict-mode violations when text appears in multiple elements.
53
+ - If the step has no quoted value AND no resolvable POM field, set "warning" to a clear single-sentence explanation of what's ambiguous and emit no pomCall/assertion. The user will hand-edit the spec.`;
54
+ /**
55
+ * Build the user-side message for one step. Includes:
56
+ * - The step keyword + text.
57
+ * - The POM IR (className, field names with their locator API+args, methods).
58
+ * - The target URL (gives the model context about what page).
59
+ * - The pageVar variable name to use in pomCall.page.
60
+ */
61
+ function buildUserPrompt(input) {
62
+ const { step, pom, pageVar, url } = input;
63
+ const fieldsBlock = pom.fields.length
64
+ ? pom.fields
65
+ .map((f) => ` - ${f.fieldName}: ${pageVar}.${f.fieldName} (page.${f.api}(${f.args}))`)
66
+ .join("\n")
67
+ : " (no fields discovered yet)";
68
+ const methodsBlock = pom.methods.length
69
+ ? pom.methods
70
+ .map((m) => ` - ${m.name}(${m.params.map((p) => `${p.name}: ${p.type}`).join(", ")})`)
71
+ .join("\n")
72
+ : " (no methods)";
73
+ const stepArg = step.argument
74
+ ? typeof step.argument === "string"
75
+ ? `\nDoc string:\n${step.argument}`
76
+ : `\nData table:\n${JSON.stringify(step.argument)}`
77
+ : "";
78
+ return `Step that didn't match any rule:
79
+ ${step.keyword} ${step.text}${stepArg}
80
+
81
+ POM context:
82
+ className: ${pom.className}
83
+ pageVar: ${pageVar}
84
+ url: ${url ?? "(none)"}
85
+
86
+ POM fields:
87
+ ${fieldsBlock}
88
+
89
+ POM methods:
90
+ ${methodsBlock}
91
+
92
+ Return the JSON binding now.`;
93
+ }
94
+ /**
95
+ * Cache key — stable across runs given identical inputs. Determinism plus
96
+ * cost control: the same step text + POM signature returns the same binding
97
+ * forever (until the user explicitly clears the cache or upgrades bdd2pw).
98
+ */
99
+ function cacheKey(input, model) {
100
+ const fieldNames = input.pom.fields.map((f) => f.fieldName).sort().join(",");
101
+ const methodNames = input.pom.methods.map((m) => m.name).sort().join(",");
102
+ // Include the model so a model upgrade invalidates old cache entries.
103
+ return [
104
+ "v2",
105
+ model,
106
+ input.step.keyword,
107
+ input.step.text,
108
+ input.pom.className,
109
+ fieldNames,
110
+ methodNames,
111
+ ].join("|");
112
+ }
113
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/llm/prompt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAsCH,0CA0CC;AAOD,4BAaC;AAhGD,qDAAqD;AACxC,QAAA,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;yMAwB4K,CAAC;AAE1M;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,KAA2B;IACzD,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;IAE1C,MAAM,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM;QACnC,CAAC,CAAC,GAAG,CAAC,MAAM;aACP,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,IAAI,CAC7E;aACA,IAAI,CAAC,IAAI,CAAC;QACf,CAAC,CAAC,8BAA8B,CAAC;IAEnC,MAAM,YAAY,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM;QACrC,CAAC,CAAC,GAAG,CAAC,OAAO;aACR,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC7E;aACA,IAAI,CAAC,IAAI,CAAC;QACf,CAAC,CAAC,gBAAgB,CAAC;IAErB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ;QAC3B,CAAC,CAAC,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ;YACjC,CAAC,CAAC,kBAAkB,IAAI,CAAC,QAAQ,EAAE;YACnC,CAAC,CAAC,kBAAkB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;QACrD,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;IACL,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,GAAG,OAAO;;;eAGxB,GAAG,CAAC,SAAS;aACf,OAAO;SACX,GAAG,IAAI,QAAQ;;;EAGtB,WAAW;;;EAGX,YAAY;;6BAEe,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,SAAgB,QAAQ,CAAC,KAA2B,EAAE,KAAa;IACjE,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7E,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1E,sEAAsE;IACtE,OAAO;QACL,IAAI;QACJ,KAAK;QACL,KAAK,CAAC,IAAI,CAAC,OAAO;QAClB,KAAK,CAAC,IAAI,CAAC,IAAI;QACf,KAAK,CAAC,GAAG,CAAC,SAAS;QACnB,UAAU;QACV,WAAW;KACZ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}