Intuitive, runnable BDD over a v4 "Suluk" contract — a non-technical author (PM / BA / QA) writes Gherkin user-stories and journeys against a step vocabulary generated from the contract, and a bidirectional gap report tells everyone exactly what the contract can and cannot yet back.
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative. See ADR C038.
contract ──generateVocabulary──▶ step palette ──▶ humans author .feature stories
▲ │
└── dev fills the gap ◀── bidirectional gap report ◀── bindFeatures
generateVocabulary(doc) projects the contract into a deterministic Gherkin step palette:
x-suluk-access (authenticated → "Given I am a signed-in user")checkout → "When I checkout", getCredits → "When I view credits")x-suluk-store ("Then my credits refreshes"), per-unit
x-suluk-cost ("Then I am charged credits").feature files against that palette (the prose lives in a sidecar, never
in the contract — the D1 wall).bindFeatures(vocab, features) binds each step exact-or-UNBOUND, with outcome (Then) steps resolved
relative to the scenario's When-subject, and reports the gaps both ways.Binding never uses scoring, lemmatization, or embeddings — those would make the decision non-deterministic. String similarity appears only in the presentational "did you mean?" suggestion on an already-unbound step.
emitRunnableSuite(vocab, features) lowers bound scenarios to a self-contained bun:test suite driven through
@suluk/sdk's generated client — the same client your frontend ships on. A green scenario exercises the real
frontend data-path: typed dispatch, input validation, the auth interceptor, response decode, and the C037 store
invalidation/refetch. Honest boundary: it tests client + contract + wire + the store data layer — not rendered
UI, layout, or visual behavior (there is no DOM in a bun:test). That last mile is @suluk/visual + a browser.
import { generateVocabulary, parseFeature, bindFeatures, renderGapReport, renderPhrasebook } from "@suluk/journeys";
import { apiDocument } from "./contract"; // your v4 contract
const vocab = generateVocabulary(apiDocument());
console.log(renderPhrasebook(vocab)); // the palette an author picks from
const feature = parseFeature(await Bun.file("./billing.feature").text());
const report = bindFeatures(vocab, [feature], {
aliases: { "given i am a logged in user": "given i am a signed-in user" }, // author-owned, no dev
});
console.log(renderGapReport(report));
Step identity is op.name@path-uri (the by-name handle), not the @suluk/sdk client accessor — resolveOps
mutates the accessor in place during collision resolution, so accessor-keyed identity would churn when a sibling
operation is added. (Witnessed on toolfactory's api/billing/subscription, which holds both getSubscription and
cancelSubscription.)
A non-technical author can write stories in their own words and hand them to a scaffolder (a more technical author — not a developer) who maps that free prose onto the runnable vocabulary. Neither role writes code.
detectUndefined(vocab, features, defs) is the scaffolder's worklist (Cucumber-style undefined-step detection,
resolved by mapping not coding). It splits each not-yet-runnable step into "the scaffolder can define this"
(an operation/step exists → alias or decompose) vs "escalate to a developer" (NEEDS-CONTRACT — no operation
backs it). renderScaffold(...) prints it with paste-ready stubs.Definitions artifact (author/scaffolder-owned data) turns prose into runnable steps three ways:
"prose": "When I checkout" (one canonical step)"I sign up and buy credits": ["Given I am a signed-in user", "When I checkout", "Then it succeeds"]When I complete the "top up" journeyconst defs = {
steps: { "when i sign up and buy credits": ["Given I am a signed-in user", "When I checkout", "Then it succeeds"] },
journeys: { "top up": ["Given I am a signed-in user", "When I checkout", "Then it succeeds"] },
};
const report = bindFeatures(vocab, [story], { definitions: defs }); // composed + decomposed, all bound
const todo = detectUndefined(vocab, [story], { definitions: defs }); // what still needs defining / escalation
Composition lets authors build bigger journeys out of existing ones (complete the "…" journey), and outcome
steps bind to the most-recent action, so multi-step journeys bind each Then to its own When. The only thing
that ever needs a developer is genuinely new backend capability (NEEDS-CONTRACT) — composition and mapping do not.
A semantic reuse search — "find an existing flow to reuse / modify / rebuild" — is designed (a deterministic faceted handle-index inside this package; the reuse verdict is set algebra over contract-handle overlap; an embedding overlay is a walled-off, gated sibling). It is deferred until a real corpus exists. See ADR C038.
0.1.0, ceiling 0.45 — originated, projection-model-native, spike-witnessed on the real toolfactory contract; the
open question is whether the constrained-vocabulary-plus-alias UX feels intuitive to a real non-technical author.