@archal/cli 0.7.7 → 0.7.9

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/index.js CHANGED
@@ -2982,10 +2982,11 @@ var LlmApiError = class extends Error {
2982
2982
  };
2983
2983
  var RETRYABLE_STATUS_CODES2 = /* @__PURE__ */ new Set([429, 500, 502, 503, 529]);
2984
2984
  function detectProvider(model) {
2985
- if (model.startsWith("gemini-")) return "gemini";
2986
- if (model.startsWith("claude-")) return "anthropic";
2987
- if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o2-") || model.startsWith("o3-") || model.startsWith("o4-")) return "openai";
2988
- if (model.startsWith("llama") || model.startsWith("mixtral") || model.startsWith("mistral") || model.startsWith("deepseek") || model.startsWith("qwen") || model.startsWith("codestral") || model.startsWith("command")) return "openai-compatible";
2985
+ const normalized = model.toLowerCase();
2986
+ if (normalized.startsWith("gemini-")) return "gemini";
2987
+ if (normalized.startsWith("claude-") || normalized.startsWith("sonnet-") || normalized.startsWith("haiku-") || normalized.startsWith("opus-")) return "anthropic";
2988
+ if (normalized.startsWith("gpt-") || normalized.startsWith("o1-") || normalized.startsWith("o2-") || normalized.startsWith("o3-") || normalized.startsWith("o4-")) return "openai";
2989
+ if (normalized.startsWith("llama") || normalized.startsWith("mixtral") || normalized.startsWith("mistral") || normalized.startsWith("deepseek") || normalized.startsWith("qwen") || normalized.startsWith("codestral") || normalized.startsWith("command")) return "openai-compatible";
2989
2990
  return "openai-compatible";
2990
2991
  }
2991
2992
  var PROVIDER_ENV_VARS = {
@@ -3011,8 +3012,18 @@ function validateKeyForProvider(key, provider) {
3011
3012
  return void 0;
3012
3013
  }
3013
3014
  function resolveProviderApiKey(explicitKey, provider) {
3014
- if (explicitKey) return explicitKey;
3015
- return process.env[PROVIDER_ENV_VARS[provider]] ?? "";
3015
+ const normalizedExplicit = explicitKey.trim();
3016
+ const providerEnvKey = process.env[PROVIDER_ENV_VARS[provider]]?.trim() ?? "";
3017
+ if (!normalizedExplicit) return providerEnvKey;
3018
+ const mismatch = validateKeyForProvider(normalizedExplicit, provider);
3019
+ if (mismatch && providerEnvKey) {
3020
+ debug("Configured API key appears mismatched for provider; falling back to provider env key", {
3021
+ provider,
3022
+ providerEnvVar: PROVIDER_ENV_VARS[provider]
3023
+ });
3024
+ return providerEnvKey;
3025
+ }
3026
+ return normalizedExplicit;
3016
3027
  }
3017
3028
  var REQUEST_TIMEOUT_MS3 = 6e4;
3018
3029
  var MAX_RETRIES2 = 3;
@@ -7933,7 +7944,11 @@ function mergeCollectionSchema(entitySchema, overrides) {
7933
7944
  if (override.default !== void 0) def.default = override.default;
7934
7945
  }
7935
7946
  if (isRequired) {
7936
- delete def.default;
7947
+ if (nullableSet.has(field)) {
7948
+ def.default = null;
7949
+ } else {
7950
+ delete def.default;
7951
+ }
7937
7952
  delete def.required;
7938
7953
  }
7939
7954
  merged[field] = def;
@@ -7951,6 +7966,9 @@ function buildSchemaFromOverrides(overrides) {
7951
7966
  const type = nullableSet.has(field) && !baseType.includes("null") && !baseType.includes("[]") ? `${baseType}|null` : baseType;
7952
7967
  schema[field] = {
7953
7968
  type,
7969
+ // If field is both required AND nullable, give it a null default so
7970
+ // the LLM can omit it without triggering a hard validation error.
7971
+ ...nullableSet.has(field) && { default: null },
7954
7972
  ...override?.aliases && { aliases: override.aliases },
7955
7973
  ...override?.description && { description: override.description },
7956
7974
  ...override?.fk && { fk: override.fk },
@@ -8063,6 +8081,8 @@ function validateSeedAgainstSchema(twinName, seed, baseEntityCounts) {
8063
8081
  errors.push(
8064
8082
  `${entityLabel}: wrong field name "${usedAlias}" - use "${fieldName}" instead`
8065
8083
  );
8084
+ } else if (def.type.includes("null")) {
8085
+ entity[fieldName] = null;
8066
8086
  } else {
8067
8087
  errors.push(
8068
8088
  `${entityLabel}: missing required field "${fieldName}" (${def.type})`
@@ -8537,7 +8557,6 @@ var KIND_COLLECTION_HINTS = {
8537
8557
  event: ["events"],
8538
8558
  email: ["gmail_messages", "messages"]
8539
8559
  };
8540
- var STRICT_QUOTE_TWINS = /* @__PURE__ */ new Set(["slack", "google-workspace"]);
8541
8560
  var ENTITY_KEY_ALIASES = {
8542
8561
  "repo.owner": ["ownerLogin", "owner_login", "login", "owner.login", "owner.name"],
8543
8562
  "issue.key": ["identifier"],
@@ -8696,13 +8715,20 @@ function validateSeedCoverage(intent, mergedSeed) {
8696
8715
  const entityIssues = [];
8697
8716
  const quoteErrors = [];
8698
8717
  const quoteWarnings = [];
8718
+ const CORE_ENTITY_KEYS = /* @__PURE__ */ new Set(["owner", "name", "fullName", "channel_name", "key", "identifier", "number"]);
8719
+ const entityWarnings = [];
8699
8720
  for (const entity of intent.entities) {
8700
8721
  if (typeof entity.value === "boolean") continue;
8701
8722
  if (!valueExistsInCollections(mergedSeed, entity.kind, entity.key, entity.value)) {
8702
- entityIssues.push({
8723
+ const issue = {
8703
8724
  type: "missing_entity",
8704
8725
  message: `Expected ${entity.kind}.${entity.key}=${String(entity.value)} to exist`
8705
- });
8726
+ };
8727
+ if (CORE_ENTITY_KEYS.has(entity.key)) {
8728
+ entityIssues.push(issue);
8729
+ } else {
8730
+ entityWarnings.push(issue);
8731
+ }
8706
8732
  }
8707
8733
  }
8708
8734
  for (const quote of intent.quotedStrings) {
@@ -8715,18 +8741,14 @@ function validateSeedCoverage(intent, mergedSeed) {
8715
8741
  type: "missing_quote",
8716
8742
  message: `Expected quoted text to exist: "${quote}"`
8717
8743
  };
8718
- if (STRICT_QUOTE_TWINS.has(intent.twinName)) {
8719
- quoteErrors.push(issue);
8720
- } else {
8721
- quoteWarnings.push(issue);
8722
- }
8744
+ quoteWarnings.push(issue);
8723
8745
  }
8724
8746
  }
8725
8747
  const errors = [...entityIssues, ...quoteErrors];
8726
8748
  return {
8727
8749
  valid: errors.length === 0,
8728
8750
  issues: errors,
8729
- warnings: quoteWarnings
8751
+ warnings: [...quoteWarnings, ...entityWarnings]
8730
8752
  };
8731
8753
  }
8732
8754
 
@@ -9039,14 +9061,928 @@ function cacheNegativeSeed(twinName, baseSeedName, setupText, missingSlots, scop
9039
9061
  }
9040
9062
  }
9041
9063
 
9064
+ // src/runner/seed-blueprint.ts
9065
+ var BLUEPRINT_SYSTEM_PROMPT = `You are a precise data extraction tool. Given a scenario setup description, extract a structured JSON blueprint describing what test data needs to be created.
9066
+
9067
+ Output ONLY valid JSON matching this schema:
9068
+ {
9069
+ "identities": [
9070
+ { "collection": "<collection_name>", "fields": { "<field>": "<value>", ... } }
9071
+ ],
9072
+ "collections": [
9073
+ {
9074
+ "name": "<collection_name>",
9075
+ "totalCount": <number>,
9076
+ "groups": [
9077
+ {
9078
+ "count": <number>,
9079
+ "properties": { "<property>": <value>, ... }
9080
+ }
9081
+ ],
9082
+ "contentHint": "<optional description of what content should look like>"
9083
+ }
9084
+ ]
9085
+ }
9086
+
9087
+ Rules:
9088
+ - "identities" are named entities that MUST exist exactly (repos, channels, users with specific names)
9089
+ - "collections" describe bulk entities with count distributions
9090
+ - Group counts MUST sum to totalCount exactly
9091
+ - Use these property names for temporal attributes:
9092
+ - "stale": true/false (not updated in 90+ days)
9093
+ - "recentlyActive": true/false (updated within 30 days)
9094
+ - Use "state" for entity state: "open", "closed", "merged", etc.
9095
+ - Use "labels" for label arrays: ["bug", "keep-open"]
9096
+ - Use "assigned" for assignment: true/false or assignee name
9097
+ - Use "hasComments": true/false for whether entity has comments
9098
+ - Use "priority" for priority: "P0", "P1", "P2", "critical", "high", "medium", "low"
9099
+ - Use "private" for visibility: true/false
9100
+ - Keep it simple \u2014 only include what the setup text explicitly states
9101
+ - If the setup says "N of those" or "N of the X", that's a subset of the previous group
9102
+ - Do NOT include fields not mentioned in the setup text`;
9103
+ async function extractBlueprint(setupText, twinName, availableCollections, config) {
9104
+ const userPrompt = `Twin: ${twinName}
9105
+ Available collections: ${availableCollections.join(", ")}
9106
+
9107
+ Setup text:
9108
+ ${setupText}
9109
+
9110
+ Extract the seed blueprint as JSON.`;
9111
+ try {
9112
+ const provider = detectProvider(config.model);
9113
+ const apiKey = resolveProviderApiKey(config.apiKey, provider);
9114
+ const responseText = await callLlm({
9115
+ provider,
9116
+ model: config.model,
9117
+ apiKey,
9118
+ systemPrompt: BLUEPRINT_SYSTEM_PROMPT,
9119
+ userPrompt,
9120
+ maxTokens: 4096,
9121
+ baseUrl: config.baseUrl,
9122
+ providerMode: config.providerMode,
9123
+ intent: "seed-generate",
9124
+ responseFormat: "json"
9125
+ });
9126
+ if (!responseText) {
9127
+ warn("Blueprint extraction returned no text");
9128
+ return null;
9129
+ }
9130
+ const parsed = parseBlueprint(responseText, twinName);
9131
+ if (!parsed) return null;
9132
+ for (const col of parsed.collections) {
9133
+ const groupSum = col.groups.reduce((sum, g) => sum + g.count, 0);
9134
+ if (groupSum !== col.totalCount) {
9135
+ debug(`Blueprint group count mismatch for ${col.name}: groups sum to ${groupSum}, totalCount is ${col.totalCount}. Adjusting.`);
9136
+ col.totalCount = groupSum;
9137
+ }
9138
+ }
9139
+ return parsed;
9140
+ } catch (err) {
9141
+ const msg = err instanceof Error ? err.message : String(err);
9142
+ warn(`Blueprint extraction failed: ${msg}`);
9143
+ return null;
9144
+ }
9145
+ }
9146
+ function parseBlueprint(text, twinName) {
9147
+ try {
9148
+ let cleaned = text.trim();
9149
+ if (cleaned.startsWith("```")) {
9150
+ cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
9151
+ }
9152
+ const raw = JSON.parse(cleaned);
9153
+ if (!raw || typeof raw !== "object") return null;
9154
+ const identities = [];
9155
+ for (const id of raw.identities ?? []) {
9156
+ if (id?.collection && typeof id.fields === "object") {
9157
+ identities.push({ collection: String(id.collection), fields: id.fields });
9158
+ }
9159
+ }
9160
+ const collections = [];
9161
+ for (const col of raw.collections ?? []) {
9162
+ if (!col?.name || typeof col.totalCount !== "number") continue;
9163
+ const groups = [];
9164
+ for (const g of col.groups ?? []) {
9165
+ if (typeof g?.count === "number" && g.count > 0) {
9166
+ groups.push({ count: g.count, properties: g.properties ?? {} });
9167
+ }
9168
+ }
9169
+ if (groups.length === 0) {
9170
+ groups.push({ count: col.totalCount, properties: {} });
9171
+ }
9172
+ collections.push({
9173
+ name: String(col.name),
9174
+ totalCount: col.totalCount,
9175
+ groups,
9176
+ contentHint: col.contentHint
9177
+ });
9178
+ }
9179
+ return { twin: twinName, identities, collections };
9180
+ } catch {
9181
+ warn("Failed to parse blueprint JSON");
9182
+ return null;
9183
+ }
9184
+ }
9185
+
9186
+ // src/runner/seed-builder.ts
9187
+ var MS_PER_DAY = 864e5;
9188
+ function daysAgo(days, base) {
9189
+ const now = base ?? /* @__PURE__ */ new Date();
9190
+ return new Date(now.getTime() - days * MS_PER_DAY).toISOString();
9191
+ }
9192
+ function recentDate(withinDays = 7, base) {
9193
+ const now = base ?? /* @__PURE__ */ new Date();
9194
+ const offset = Math.floor(Math.random() * withinDays) * MS_PER_DAY;
9195
+ return new Date(now.getTime() - offset).toISOString();
9196
+ }
9197
+ function staleDate(minDays = 91, maxDays = 300, base) {
9198
+ const now = base ?? /* @__PURE__ */ new Date();
9199
+ const range = maxDays - minDays;
9200
+ const offset = (minDays + Math.floor(Math.random() * range)) * MS_PER_DAY;
9201
+ return new Date(now.getTime() - offset).toISOString();
9202
+ }
9203
+ var ISSUE_TITLES = [
9204
+ "Fix login redirect loop on Safari",
9205
+ "Add dark mode support to settings page",
9206
+ "Rate limiter returns 500 instead of 429",
9207
+ "Memory leak in WebSocket connection manager",
9208
+ "Dropdown menus do not close on mobile",
9209
+ "Search results do not highlight matching terms",
9210
+ "File upload fails silently for large files",
9211
+ "Add CSV export to analytics dashboard",
9212
+ "Implement bulk actions for notifications",
9213
+ "GraphQL query returns stale cached data",
9214
+ "Table component lacks keyboard navigation",
9215
+ "Create onboarding tutorial for new members",
9216
+ "API response times degrade for large date ranges",
9217
+ "Add two-factor authentication for admin accounts",
9218
+ "Deprecated crypto API warnings in test suite",
9219
+ "Evaluate replacing Moment.js with date-fns",
9220
+ "Broken tooltip positioning on Safari 17",
9221
+ "Login page shows blank screen on slow 3G",
9222
+ "Intermittent 502 errors on reports endpoint",
9223
+ "Tracking: migrate auth from cookies to JWT",
9224
+ "Roadmap: accessibility audit and WCAG compliance",
9225
+ "Upgrade Node.js runtime from 18 to 20 LTS",
9226
+ "Refactor database connection pool configuration",
9227
+ "Add retry logic for third-party API calls",
9228
+ "Improve error messages for form validation",
9229
+ "Pagination breaks when filtering by status",
9230
+ "Email notification templates are not responsive",
9231
+ "CI pipeline takes 45 minutes on large PRs",
9232
+ "Localization support for date and number formats",
9233
+ "Dashboard widgets do not resize on mobile"
9234
+ ];
9235
+ var ISSUE_BODIES = [
9236
+ "This has been reported multiple times by users. Needs investigation and a fix.",
9237
+ "No one has started working on this yet. Low priority but would improve UX.",
9238
+ "This is affecting production traffic. We need to address this soon.",
9239
+ "Users have requested this feature multiple times. Would be a nice enhancement.",
9240
+ "This is a long-running tracking issue. Should remain open for visibility.",
9241
+ "Repro steps: 1) Open the app 2) Navigate to settings 3) Observe the issue.",
9242
+ "This blocks other work. Please prioritize.",
9243
+ "Nice to have. Not urgent but would reduce tech debt."
9244
+ ];
9245
+ var PR_TITLES = [
9246
+ "Fix null check in JWT validation",
9247
+ "Add pagination to search results",
9248
+ "Refactor database connection pooling",
9249
+ "Update dependencies to latest versions",
9250
+ "Improve error handling in API middleware",
9251
+ "Add unit tests for auth module",
9252
+ "Fix race condition in WebSocket handler",
9253
+ "Migrate CSS to custom properties for theming"
9254
+ ];
9255
+ var CHANNEL_PURPOSES = [
9256
+ "General discussion",
9257
+ "Engineering team updates",
9258
+ "Product announcements",
9259
+ "Customer support escalations",
9260
+ "Random fun stuff",
9261
+ "Incident response",
9262
+ "Design feedback",
9263
+ "Deploy notifications"
9264
+ ];
9265
+ var MESSAGE_TEXTS = [
9266
+ "Hey team, just a heads up about the deploy today.",
9267
+ "Can someone review my PR? It's been open for a while.",
9268
+ "The CI pipeline is green again after the fix.",
9269
+ "Meeting notes from today's standup are in the doc.",
9270
+ "Has anyone seen this error before? Getting a 502 intermittently.",
9271
+ "Thanks for the quick turnaround on that bug fix!",
9272
+ "Reminder: retro is at 3pm today.",
9273
+ "I pushed a hotfix for the login issue. Please verify."
9274
+ ];
9275
+ function generateNodeId(prefix, id) {
9276
+ return `${prefix}_kgDOB${String(id).padStart(5, "0")}`;
9277
+ }
9278
+ function generateSha() {
9279
+ const hex = "0123456789abcdef";
9280
+ let sha = "";
9281
+ for (let i = 0; i < 40; i++) sha += hex[Math.floor(Math.random() * 16)];
9282
+ return sha;
9283
+ }
9284
+ function generateSlackId(prefix, index) {
9285
+ const base = "ABCDEFG0123456789";
9286
+ let id = prefix;
9287
+ let n = index + 100;
9288
+ for (let i = 0; i < 8; i++) {
9289
+ id += base[n % base.length];
9290
+ n = Math.floor(n / base.length) + i + 1;
9291
+ }
9292
+ return id;
9293
+ }
9294
+ function generateSlackTs(baseTs, index) {
9295
+ return `${baseTs + index * 60}.${String(100001 + index * 100).padStart(6, "0")}`;
9296
+ }
9297
+ function generateUuid() {
9298
+ const hex = "0123456789abcdef";
9299
+ const parts = [8, 4, 4, 4, 12];
9300
+ return parts.map((len) => {
9301
+ let s = "";
9302
+ for (let i = 0; i < len; i++) s += hex[Math.floor(Math.random() * 16)];
9303
+ return s;
9304
+ }).join("-");
9305
+ }
9306
+ function generateStripeId(prefix, index) {
9307
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
9308
+ let id = prefix;
9309
+ let n = index + 42;
9310
+ for (let i = 0; i < 14; i++) {
9311
+ id += chars[n % chars.length];
9312
+ n = Math.floor(n / chars.length) + i + 7;
9313
+ }
9314
+ return id;
9315
+ }
9316
+ var DEFAULT_REACTIONS = {
9317
+ totalCount: 0,
9318
+ plusOne: 0,
9319
+ minusOne: 0,
9320
+ laugh: 0,
9321
+ hooray: 0,
9322
+ confused: 0,
9323
+ heart: 0,
9324
+ rocket: 0,
9325
+ eyes: 0
9326
+ };
9327
+ function resolveTemporalProperties(props, base) {
9328
+ const now = base ?? /* @__PURE__ */ new Date();
9329
+ if (props["stale"] === true) {
9330
+ const created2 = daysAgo(200 + Math.floor(Math.random() * 200), now);
9331
+ const updated2 = staleDate(91, 300, now);
9332
+ return { createdAt: created2, updatedAt: updated2 };
9333
+ }
9334
+ if (props["recentlyActive"] === true) {
9335
+ const created2 = daysAgo(30 + Math.floor(Math.random() * 100), now);
9336
+ const updated2 = recentDate(30, now);
9337
+ return { createdAt: created2, updatedAt: updated2 };
9338
+ }
9339
+ const created = daysAgo(14 + Math.floor(Math.random() * 60), now);
9340
+ const updated = recentDate(14, now);
9341
+ return { createdAt: created, updatedAt: updated };
9342
+ }
9343
+ function resolveLabels(props, availableLabels) {
9344
+ const labels = props["labels"];
9345
+ if (Array.isArray(labels)) return labels.map(String);
9346
+ return [];
9347
+ }
9348
+ function buildSeedFromBlueprint(blueprint, baseSeed) {
9349
+ const seed = structuredClone(baseSeed);
9350
+ const warnings = [];
9351
+ const now = /* @__PURE__ */ new Date();
9352
+ const existingLabels = /* @__PURE__ */ new Set();
9353
+ for (const label of seed["labels"] ?? []) {
9354
+ if (typeof label["name"] === "string") existingLabels.add(label["name"]);
9355
+ }
9356
+ for (const identity of blueprint.identities) {
9357
+ processIdentity(identity, seed, warnings);
9358
+ }
9359
+ for (const spec of blueprint.collections) {
9360
+ processCollection(spec, seed, blueprint.twin, existingLabels, warnings, now);
9361
+ }
9362
+ return { seed, warnings };
9363
+ }
9364
+ function processIdentity(identity, seed, warnings) {
9365
+ const collection = identity.collection;
9366
+ if (!seed[collection]) {
9367
+ seed[collection] = [];
9368
+ }
9369
+ const existing = seed[collection].find((entity2) => {
9370
+ for (const [key, value] of Object.entries(identity.fields)) {
9371
+ if (entity2[key] !== value) return false;
9372
+ }
9373
+ return true;
9374
+ });
9375
+ if (existing) {
9376
+ debug(`Identity already exists in ${collection}: ${JSON.stringify(identity.fields)}`);
9377
+ return;
9378
+ }
9379
+ const nextId = getMaxId(seed[collection]) + 1;
9380
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9381
+ const entity = {
9382
+ id: nextId,
9383
+ ...identity.fields,
9384
+ createdAt: now,
9385
+ updatedAt: now
9386
+ };
9387
+ fillDefaults(entity, collection);
9388
+ seed[collection].push(entity);
9389
+ debug(`Added identity to ${collection}: ${JSON.stringify(identity.fields)}`);
9390
+ }
9391
+ function processCollection(spec, seed, twinName, existingLabels, warnings, now) {
9392
+ const collection = spec.name;
9393
+ if (!seed[collection]) {
9394
+ seed[collection] = [];
9395
+ }
9396
+ const existingCount = seed[collection].length;
9397
+ if (existingCount > 0) {
9398
+ debug(`Clearing ${existingCount} existing ${collection} entities (blueprint replaces)`);
9399
+ seed[collection] = [];
9400
+ }
9401
+ const referencedLabels = /* @__PURE__ */ new Set();
9402
+ for (const group of spec.groups) {
9403
+ const labels = group.properties["labels"];
9404
+ if (Array.isArray(labels)) {
9405
+ for (const l of labels) {
9406
+ const name = String(l);
9407
+ if (!existingLabels.has(name)) referencedLabels.add(name);
9408
+ }
9409
+ }
9410
+ }
9411
+ if (referencedLabels.size > 0 && seed["labels"]) {
9412
+ const labelColors = ["d73a4a", "a2eeef", "0e8a16", "fbca04", "d876e3", "e4e669", "f9d0c4", "bfdadc"];
9413
+ let labelId = getMaxId(seed["labels"]) + 1;
9414
+ const repoId = findFirstRepoId(seed);
9415
+ for (const name of referencedLabels) {
9416
+ const color = labelColors[labelId % labelColors.length];
9417
+ seed["labels"].push({
9418
+ id: labelId,
9419
+ repoId: repoId ?? 1,
9420
+ nodeId: generateNodeId("LA_kwDOBlab", labelId),
9421
+ name,
9422
+ description: `Label: ${name}`,
9423
+ color,
9424
+ isDefault: false,
9425
+ createdAt: daysAgo(60, now),
9426
+ updatedAt: daysAgo(60, now)
9427
+ });
9428
+ existingLabels.add(name);
9429
+ labelId++;
9430
+ }
9431
+ }
9432
+ let entityIndex = 0;
9433
+ for (const group of spec.groups) {
9434
+ for (let i = 0; i < group.count; i++) {
9435
+ const entity = buildEntity(
9436
+ collection,
9437
+ twinName,
9438
+ group.properties,
9439
+ seed,
9440
+ entityIndex,
9441
+ spec.contentHint,
9442
+ now
9443
+ );
9444
+ seed[collection].push(entity);
9445
+ entityIndex++;
9446
+ }
9447
+ }
9448
+ debug(`Built ${entityIndex} entities for ${collection}`);
9449
+ }
9450
+ function buildEntity(collection, twinName, properties, seed, index, contentHint, now) {
9451
+ const nextId = getMaxId(seed[collection]) + 1;
9452
+ const temporal = resolveTemporalProperties(properties, now);
9453
+ if (twinName === "github") return buildGitHubEntity(collection, nextId, properties, seed, index, temporal, contentHint);
9454
+ if (twinName === "slack") return buildSlackEntity(collection, nextId, properties, seed, index, temporal, contentHint);
9455
+ if (twinName === "linear") return buildLinearEntity(collection, nextId, properties, seed, index, temporal, contentHint);
9456
+ if (twinName === "stripe") return buildStripeEntity(collection, nextId, properties, seed, index, temporal, contentHint);
9457
+ if (twinName === "jira") return buildJiraEntity(collection, nextId, properties, seed, index, temporal, contentHint);
9458
+ return {
9459
+ id: nextId,
9460
+ ...properties,
9461
+ createdAt: temporal.createdAt,
9462
+ updatedAt: temporal.updatedAt
9463
+ };
9464
+ }
9465
+ function buildGitHubEntity(collection, id, props, seed, index, temporal, contentHint) {
9466
+ const repoId = findFirstRepoId(seed) ?? 1;
9467
+ const owner = findRepoOwner(seed) ?? "acme";
9468
+ switch (collection) {
9469
+ case "issues": {
9470
+ const state = String(props["state"] ?? "open");
9471
+ const number = getMaxIssueNumber(seed) + 1;
9472
+ const labels = resolveLabels(props, []);
9473
+ const title = ISSUE_TITLES[index % ISSUE_TITLES.length];
9474
+ const body = ISSUE_BODIES[index % ISSUE_BODIES.length];
9475
+ const assigned = props["assigned"] === true;
9476
+ return {
9477
+ id,
9478
+ repoId,
9479
+ nodeId: generateNodeId("I_kwDOBiss", id),
9480
+ number,
9481
+ title: contentHint ? `${title} (${contentHint})` : title,
9482
+ body,
9483
+ state,
9484
+ stateReason: state === "closed" ? "completed" : null,
9485
+ locked: false,
9486
+ assignees: assigned ? [owner] : [],
9487
+ labels,
9488
+ milestone: null,
9489
+ authorLogin: owner,
9490
+ closedAt: state === "closed" ? temporal.updatedAt : null,
9491
+ closedBy: state === "closed" ? owner : null,
9492
+ htmlUrl: `https://github.com/${owner}/webapp/issues/${number}`,
9493
+ isPullRequest: false,
9494
+ reactions: { ...DEFAULT_REACTIONS },
9495
+ createdAt: temporal.createdAt,
9496
+ updatedAt: temporal.updatedAt
9497
+ };
9498
+ }
9499
+ case "pullRequests": {
9500
+ const state = String(props["state"] ?? "open");
9501
+ const number = getMaxPRNumber(seed) + 1;
9502
+ const title = PR_TITLES[index % PR_TITLES.length];
9503
+ const sha = generateSha();
9504
+ const baseSha = generateSha();
9505
+ return {
9506
+ id,
9507
+ repoId,
9508
+ nodeId: generateNodeId("PR_kwDOBpr", id),
9509
+ number,
9510
+ title,
9511
+ body: `This PR addresses ${title.toLowerCase()}.`,
9512
+ state,
9513
+ merged: props["merged"] === true || state === "closed",
9514
+ draft: props["draft"] === true,
9515
+ headRef: `feature/fix-${index}`,
9516
+ headSha: sha,
9517
+ baseRef: "main",
9518
+ baseSha,
9519
+ authorLogin: owner,
9520
+ labels: resolveLabels(props, []),
9521
+ assignees: [],
9522
+ requestedReviewers: [],
9523
+ milestone: null,
9524
+ additions: 10 + index * 5,
9525
+ deletions: 3 + index * 2,
9526
+ changedFiles: 1 + index % 5,
9527
+ commits: 1 + index % 3,
9528
+ comments: 0,
9529
+ reviewComments: 0,
9530
+ maintainerCanModify: true,
9531
+ mergeable: state === "open",
9532
+ mergedAt: props["merged"] === true ? temporal.updatedAt : null,
9533
+ mergedBy: props["merged"] === true ? owner : null,
9534
+ mergeCommitSha: props["merged"] === true ? generateSha() : null,
9535
+ closedAt: state === "closed" ? temporal.updatedAt : null,
9536
+ autoMerge: null,
9537
+ htmlUrl: `https://github.com/${owner}/webapp/pull/${number}`,
9538
+ diffUrl: `https://github.com/${owner}/webapp/pull/${number}.diff`,
9539
+ patchUrl: `https://github.com/${owner}/webapp/pull/${number}.patch`,
9540
+ createdAt: temporal.createdAt,
9541
+ updatedAt: temporal.updatedAt
9542
+ };
9543
+ }
9544
+ case "comments": {
9545
+ const issueNumber = resolveIssueNumberForComment(seed, index);
9546
+ return {
9547
+ id,
9548
+ repoId,
9549
+ nodeId: generateNodeId("IC_kwDOBcom", id),
9550
+ issueNumber,
9551
+ body: props["body"] ? String(props["body"]) : `Comment on issue #${issueNumber}`,
9552
+ authorLogin: owner,
9553
+ authorAssociation: "MEMBER",
9554
+ htmlUrl: `https://github.com/${owner}/webapp/issues/${issueNumber}#issuecomment-${id}`,
9555
+ reactions: { ...DEFAULT_REACTIONS },
9556
+ createdAt: temporal.createdAt,
9557
+ updatedAt: temporal.updatedAt
9558
+ };
9559
+ }
9560
+ case "labels": {
9561
+ const colors = ["d73a4a", "a2eeef", "0e8a16", "fbca04", "d876e3"];
9562
+ return {
9563
+ id,
9564
+ repoId,
9565
+ nodeId: generateNodeId("LA_kwDOBlab", id),
9566
+ name: props["name"] ? String(props["name"]) : `label-${id}`,
9567
+ description: props["description"] ? String(props["description"]) : null,
9568
+ color: colors[id % colors.length],
9569
+ isDefault: false,
9570
+ createdAt: temporal.createdAt,
9571
+ updatedAt: temporal.updatedAt
9572
+ };
9573
+ }
9574
+ default:
9575
+ return {
9576
+ id,
9577
+ repoId,
9578
+ ...props,
9579
+ createdAt: temporal.createdAt,
9580
+ updatedAt: temporal.updatedAt
9581
+ };
9582
+ }
9583
+ }
9584
+ function buildSlackEntity(collection, id, props, seed, index, temporal, contentHint) {
9585
+ const teamId = findSlackTeamId(seed) ?? "T0001TEAM";
9586
+ switch (collection) {
9587
+ case "channels": {
9588
+ const channelId = generateSlackId("C", index);
9589
+ const name = props["name"] ? String(props["name"]) : `channel-${index + 1}`;
9590
+ const userIds = (seed["users"] ?? []).map((u) => u["user_id"]).filter(Boolean);
9591
+ return {
9592
+ id,
9593
+ channel_id: channelId,
9594
+ name,
9595
+ is_channel: true,
9596
+ is_group: false,
9597
+ is_im: false,
9598
+ is_mpim: false,
9599
+ is_private: props["private"] === true,
9600
+ is_archived: false,
9601
+ is_general: name === "general",
9602
+ creator: userIds[0] ?? "U0001AAAA",
9603
+ topic: { value: "", creator: "", last_set: 0 },
9604
+ purpose: { value: CHANNEL_PURPOSES[index % CHANNEL_PURPOSES.length], creator: "", last_set: 0 },
9605
+ members: userIds,
9606
+ num_members: userIds.length,
9607
+ created: Math.floor(new Date(temporal.createdAt).getTime() / 1e3),
9608
+ updated: Math.floor(new Date(temporal.updatedAt).getTime() / 1e3),
9609
+ createdAt: temporal.createdAt,
9610
+ updatedAt: temporal.updatedAt
9611
+ };
9612
+ }
9613
+ case "messages": {
9614
+ const channels = seed["channels"] ?? [];
9615
+ const channelId = channels.length > 0 ? String(channels[index % channels.length]["channel_id"] ?? "C0001AAAA") : "C0001AAAA";
9616
+ const users = seed["users"] ?? [];
9617
+ const userId = users.length > 0 ? String(users[index % users.length]["user_id"] ?? "U0001AAAA") : "U0001AAAA";
9618
+ const baseTs = Math.floor(new Date(temporal.createdAt).getTime() / 1e3);
9619
+ const ts = generateSlackTs(baseTs, index);
9620
+ return {
9621
+ id,
9622
+ ts,
9623
+ channel_id: channelId,
9624
+ user_id: userId,
9625
+ text: props["text"] ? String(props["text"]) : MESSAGE_TEXTS[index % MESSAGE_TEXTS.length],
9626
+ type: "message",
9627
+ subtype: null,
9628
+ thread_ts: props["isReply"] === true ? generateSlackTs(baseTs, 0) : null,
9629
+ reply_count: 0,
9630
+ reply_users: [],
9631
+ reply_users_count: 0,
9632
+ latest_reply: null,
9633
+ createdAt: temporal.createdAt,
9634
+ updatedAt: temporal.updatedAt
9635
+ };
9636
+ }
9637
+ case "users": {
9638
+ const userId = generateSlackId("U", index);
9639
+ const name = props["name"] ? String(props["name"]) : `user${index + 1}`;
9640
+ return {
9641
+ id,
9642
+ user_id: userId,
9643
+ team_id: teamId,
9644
+ name,
9645
+ real_name: props["real_name"] ? String(props["real_name"]) : `User ${index + 1}`,
9646
+ display_name: name,
9647
+ email: `${name}@example.com`,
9648
+ is_admin: props["is_admin"] === true,
9649
+ is_owner: false,
9650
+ is_bot: props["is_bot"] === true,
9651
+ is_restricted: false,
9652
+ is_ultra_restricted: false,
9653
+ deleted: false,
9654
+ color: "4bbe2e",
9655
+ timezone: "America/Los_Angeles",
9656
+ tz_label: "Pacific Daylight Time",
9657
+ tz_offset: -25200,
9658
+ is_email_confirmed: true,
9659
+ who_can_share_contact_card: "EVERYONE",
9660
+ createdAt: temporal.createdAt,
9661
+ updatedAt: temporal.updatedAt
9662
+ };
9663
+ }
9664
+ default:
9665
+ return {
9666
+ id,
9667
+ ...props,
9668
+ createdAt: temporal.createdAt,
9669
+ updatedAt: temporal.updatedAt
9670
+ };
9671
+ }
9672
+ }
9673
+ function buildLinearEntity(collection, id, props, seed, index, temporal, contentHint) {
9674
+ switch (collection) {
9675
+ case "issues": {
9676
+ const teamId = findFirstId(seed, "teams") ?? 1;
9677
+ const team = (seed["teams"] ?? []).find((t) => t["id"] === teamId);
9678
+ const teamKey = team ? String(team["key"] ?? "ENG") : "ENG";
9679
+ const number = index + 1;
9680
+ const stateId = resolveLinearStateId(seed, props);
9681
+ const priority = typeof props["priority"] === "number" ? props["priority"] : 3;
9682
+ const priorityMap = {
9683
+ 0: "No priority",
9684
+ 1: "Urgent",
9685
+ 2: "High",
9686
+ 3: "Medium",
9687
+ 4: "Low"
9688
+ };
9689
+ return {
9690
+ id,
9691
+ linearId: generateUuid(),
9692
+ teamId,
9693
+ number,
9694
+ identifier: `${teamKey}-${number}`,
9695
+ title: ISSUE_TITLES[index % ISSUE_TITLES.length],
9696
+ description: null,
9697
+ descriptionData: null,
9698
+ priority,
9699
+ priorityLabel: priorityMap[priority] ?? "Medium",
9700
+ stateId,
9701
+ assigneeId: null,
9702
+ creatorId: null,
9703
+ projectId: null,
9704
+ cycleId: null,
9705
+ parentId: null,
9706
+ labelIds: [],
9707
+ estimate: null,
9708
+ dueDate: null,
9709
+ subIssueSortOrder: null,
9710
+ sortOrder: index * -100,
9711
+ snoozedUntilAt: null,
9712
+ startedAt: null,
9713
+ completedAt: null,
9714
+ canceledAt: null,
9715
+ archivedAt: null,
9716
+ trashed: false,
9717
+ createdAt: temporal.createdAt,
9718
+ updatedAt: temporal.updatedAt
9719
+ };
9720
+ }
9721
+ default:
9722
+ return {
9723
+ id,
9724
+ linearId: generateUuid(),
9725
+ ...props,
9726
+ createdAt: temporal.createdAt,
9727
+ updatedAt: temporal.updatedAt
9728
+ };
9729
+ }
9730
+ }
9731
+ function buildStripeEntity(collection, id, props, seed, index, temporal, contentHint) {
9732
+ switch (collection) {
9733
+ case "paymentIntents": {
9734
+ const piId = generateStripeId("pi_", index);
9735
+ const amount = typeof props["amount"] === "number" ? props["amount"] : 1e3 + index * 500;
9736
+ const status = String(props["status"] ?? "succeeded");
9737
+ return {
9738
+ id,
9739
+ paymentIntentId: piId,
9740
+ amount,
9741
+ currency: String(props["currency"] ?? "usd"),
9742
+ status,
9743
+ customerId: props["customerId"] ? String(props["customerId"]) : null,
9744
+ description: props["description"] ? String(props["description"]) : null,
9745
+ paymentMethodId: null,
9746
+ captureMethod: "automatic",
9747
+ confirmationMethod: "automatic",
9748
+ canceledAt: null,
9749
+ cancellationReason: null,
9750
+ latestChargeId: null,
9751
+ metadata: {},
9752
+ livemode: false,
9753
+ created: Math.floor(new Date(temporal.createdAt).getTime() / 1e3),
9754
+ createdAt: temporal.createdAt,
9755
+ updatedAt: temporal.updatedAt
9756
+ };
9757
+ }
9758
+ case "customers": {
9759
+ const custId = generateStripeId("cus_", index);
9760
+ return {
9761
+ id,
9762
+ customerId: custId,
9763
+ name: props["name"] ? String(props["name"]) : `Customer ${index + 1}`,
9764
+ email: props["email"] ? String(props["email"]) : `customer${index + 1}@example.com`,
9765
+ phone: null,
9766
+ description: null,
9767
+ currency: null,
9768
+ defaultPaymentMethod: null,
9769
+ address: null,
9770
+ shipping: null,
9771
+ balance: 0,
9772
+ delinquent: false,
9773
+ metadata: {},
9774
+ livemode: false,
9775
+ created: Math.floor(new Date(temporal.createdAt).getTime() / 1e3),
9776
+ createdAt: temporal.createdAt,
9777
+ updatedAt: temporal.updatedAt
9778
+ };
9779
+ }
9780
+ default:
9781
+ return {
9782
+ id,
9783
+ ...props,
9784
+ created: Math.floor(new Date(temporal.createdAt).getTime() / 1e3),
9785
+ createdAt: temporal.createdAt,
9786
+ updatedAt: temporal.updatedAt
9787
+ };
9788
+ }
9789
+ }
9790
+ function buildJiraEntity(collection, id, props, seed, index, temporal, contentHint) {
9791
+ switch (collection) {
9792
+ case "issues": {
9793
+ const projectId = findFirstId(seed, "projects") ?? 1;
9794
+ const project = (seed["projects"] ?? []).find((p) => p["id"] === projectId);
9795
+ const projectKey = project ? String(project["key"] ?? "PROJ") : "PROJ";
9796
+ const number = index + 1;
9797
+ return {
9798
+ id,
9799
+ key: `${projectKey}-${number}`,
9800
+ projectId,
9801
+ issueTypeId: findFirstId(seed, "issueTypes") ?? 1,
9802
+ summary: ISSUE_TITLES[index % ISSUE_TITLES.length],
9803
+ description: null,
9804
+ statusId: findFirstId(seed, "statuses") ?? 1,
9805
+ priorityId: findFirstId(seed, "priorities") ?? 1,
9806
+ reporterAccountId: findFirstAccountId(seed),
9807
+ assigneeAccountId: props["assigned"] === true ? findFirstAccountId(seed) : null,
9808
+ parentKey: null,
9809
+ storyPoints: null,
9810
+ resolution: null,
9811
+ resolutionDate: null,
9812
+ labels: resolveLabels(props, []),
9813
+ components: [],
9814
+ fixVersions: [],
9815
+ createdAt: temporal.createdAt,
9816
+ updatedAt: temporal.updatedAt
9817
+ };
9818
+ }
9819
+ default:
9820
+ return {
9821
+ id,
9822
+ ...props,
9823
+ createdAt: temporal.createdAt,
9824
+ updatedAt: temporal.updatedAt
9825
+ };
9826
+ }
9827
+ }
9828
+ function findFirstRepoId(seed) {
9829
+ const repos = seed["repos"];
9830
+ return repos?.[0]?.["id"];
9831
+ }
9832
+ function findRepoOwner(seed) {
9833
+ const repos = seed["repos"];
9834
+ return repos?.[0]?.["owner"];
9835
+ }
9836
+ function findFirstId(seed, collection) {
9837
+ const entities = seed[collection];
9838
+ return entities?.[0]?.["id"];
9839
+ }
9840
+ function findFirstAccountId(seed) {
9841
+ const users = seed["users"];
9842
+ return String(users?.[0]?.["accountId"] ?? "account-1");
9843
+ }
9844
+ function findSlackTeamId(seed) {
9845
+ const workspaces = seed["workspaces"];
9846
+ if (workspaces?.[0]) return String(workspaces[0]["team_id"]);
9847
+ const users = seed["users"];
9848
+ if (users?.[0]) return String(users[0]["team_id"] ?? "T0001TEAM");
9849
+ return void 0;
9850
+ }
9851
+ function getMaxIssueNumber(seed) {
9852
+ let max = 0;
9853
+ for (const issue of seed["issues"] ?? []) {
9854
+ const n = issue["number"];
9855
+ if (typeof n === "number" && n > max) max = n;
9856
+ }
9857
+ return max;
9858
+ }
9859
+ function getMaxPRNumber(seed) {
9860
+ let max = 0;
9861
+ for (const pr of seed["pullRequests"] ?? []) {
9862
+ const n = pr["number"];
9863
+ if (typeof n === "number" && n > max) max = n;
9864
+ }
9865
+ return Math.max(max, getMaxIssueNumber(seed));
9866
+ }
9867
+ function resolveIssueNumberForComment(seed, index) {
9868
+ const issues = seed["issues"] ?? [];
9869
+ if (issues.length === 0) return 1;
9870
+ const issue = issues[index % issues.length];
9871
+ return issue["number"] ?? 1;
9872
+ }
9873
+ function resolveLinearStateId(seed, props) {
9874
+ const states = seed["workflowStates"] ?? [];
9875
+ if (states.length === 0) return 1;
9876
+ const state = props["state"];
9877
+ if (state === "completed" || state === "closed" || state === "done") {
9878
+ const found = states.find((s) => s["type"] === "completed");
9879
+ if (found) return found["id"];
9880
+ }
9881
+ if (state === "cancelled" || state === "canceled") {
9882
+ const found = states.find((s) => s["type"] === "cancelled");
9883
+ if (found) return found["id"];
9884
+ }
9885
+ if (state === "in_progress" || state === "started") {
9886
+ const found = states.find((s) => s["type"] === "started");
9887
+ if (found) return found["id"];
9888
+ }
9889
+ if (state === "backlog") {
9890
+ const found = states.find((s) => s["type"] === "backlog");
9891
+ if (found) return found["id"];
9892
+ }
9893
+ const unstarted = states.find((s) => s["type"] === "unstarted");
9894
+ return unstarted?.["id"] ?? states[0]["id"];
9895
+ }
9896
+ function fillDefaults(entity, collection) {
9897
+ switch (collection) {
9898
+ case "repos":
9899
+ entity["fullName"] = entity["fullName"] ?? `${entity["owner"] ?? "org"}/${entity["name"] ?? "repo"}`;
9900
+ entity["private"] = entity["private"] ?? false;
9901
+ entity["fork"] = entity["fork"] ?? false;
9902
+ entity["defaultBranch"] = entity["defaultBranch"] ?? "main";
9903
+ entity["archived"] = entity["archived"] ?? false;
9904
+ entity["disabled"] = entity["disabled"] ?? false;
9905
+ entity["visibility"] = entity["visibility"] ?? "public";
9906
+ entity["hasIssues"] = entity["hasIssues"] ?? true;
9907
+ entity["topics"] = entity["topics"] ?? [];
9908
+ entity["openIssuesCount"] = entity["openIssuesCount"] ?? 0;
9909
+ entity["stargazersCount"] = entity["stargazersCount"] ?? 0;
9910
+ entity["forksCount"] = entity["forksCount"] ?? 0;
9911
+ entity["watchersCount"] = entity["watchersCount"] ?? 0;
9912
+ break;
9913
+ case "users":
9914
+ entity["type"] = entity["type"] ?? "User";
9915
+ entity["siteAdmin"] = entity["siteAdmin"] ?? false;
9916
+ entity["publicRepos"] = entity["publicRepos"] ?? 0;
9917
+ entity["followers"] = entity["followers"] ?? 0;
9918
+ entity["following"] = entity["following"] ?? 0;
9919
+ break;
9920
+ }
9921
+ }
9922
+
9042
9923
  // src/runner/dynamic-seed-generator.ts
9924
+ function extractCountRequirements(setupText) {
9925
+ const requirements = [];
9926
+ const seen = /* @__PURE__ */ new Set();
9927
+ const NON_ENTITY = /* @__PURE__ */ new Set([
9928
+ "minutes",
9929
+ "minute",
9930
+ "hours",
9931
+ "hour",
9932
+ "days",
9933
+ "day",
9934
+ "weeks",
9935
+ "week",
9936
+ "months",
9937
+ "month",
9938
+ "years",
9939
+ "year",
9940
+ "seconds",
9941
+ "second",
9942
+ "ms",
9943
+ "am",
9944
+ "pm",
9945
+ "st",
9946
+ "nd",
9947
+ "rd",
9948
+ "th",
9949
+ "usd",
9950
+ "eur",
9951
+ "gbp",
9952
+ "percent",
9953
+ "kb",
9954
+ "mb",
9955
+ "gb",
9956
+ "tb"
9957
+ ]);
9958
+ const patterns = [
9959
+ /\b(\d+)\s+([\w\s]+?)(?:\s+(?:that|which|are|with|in|labeled|assigned|have|has)\b)/gi,
9960
+ /\b(\d+)\s+([\w\s]+?)(?:[.,;:)]|$)/gm
9961
+ ];
9962
+ for (const pattern of patterns) {
9963
+ for (const match of setupText.matchAll(pattern)) {
9964
+ const count = parseInt(match[1], 10);
9965
+ const subject = match[2].trim().toLowerCase();
9966
+ if (!subject || count <= 0 || count > 200) continue;
9967
+ const firstWord = subject.split(/\s+/)[0] ?? "";
9968
+ if (NON_ENTITY.has(firstWord)) continue;
9969
+ if (/^(?:of|and|or|the|those|these|them)\b/.test(subject)) continue;
9970
+ const key = `${count}:${subject}`;
9971
+ if (seen.has(key)) continue;
9972
+ seen.add(key);
9973
+ requirements.push(`- ${subject}: exactly ${count}`);
9974
+ }
9975
+ }
9976
+ return requirements;
9977
+ }
9043
9978
  var DynamicSeedError = class extends Error {
9044
9979
  twinName;
9045
9980
  validationErrors;
9046
9981
  constructor(twinName, validationErrors) {
9047
9982
  const details = validationErrors.length > 0 ? `:
9048
9983
  ${validationErrors.map((e) => ` - ${e}`).join("\n")}` : ".";
9049
- super(`Dynamic seed generation failed for twin "${twinName}"${details}`);
9984
+ const hint = "\n\nHint: Try `--static-seed` to skip dynamic generation and use pre-built seeds instead.";
9985
+ super(`Dynamic seed generation failed for twin "${twinName}"${details}${hint}`);
9050
9986
  this.name = "DynamicSeedError";
9051
9987
  this.twinName = twinName;
9052
9988
  this.validationErrors = validationErrors;
@@ -9280,6 +10216,14 @@ ${context.expectedBehavior}
9280
10216
  prompt += context.successCriteria.map((criterion) => `- ${criterion}`).join("\n");
9281
10217
  prompt += "\n\n";
9282
10218
  }
10219
+ const countReqs = extractCountRequirements(setupDescription);
10220
+ if (countReqs.length > 0) {
10221
+ prompt += `## MANDATORY Count Requirements
10222
+ The final seed state MUST have these exact counts. Do NOT deviate:
10223
+ `;
10224
+ prompt += countReqs.join("\n");
10225
+ prompt += "\n\n";
10226
+ }
9283
10227
  prompt += `## Setup Description
9284
10228
  This is a multi-service scenario. Generate ONLY the ${twinName} seed data. Ignore setup details that belong to other services. Faithfully reproduce EVERY detail relevant to ${twinName}. Specific names, messages, amounts, and entities mentioned MUST exist in the generated data.
9285
10229
 
@@ -9682,6 +10626,87 @@ function parseSeedPatchResponse(text, twinName) {
9682
10626
  });
9683
10627
  return null;
9684
10628
  }
10629
+ async function tryBlueprintPath(twinName, baseSeedData, setupDescription, availableCollections, config, intent) {
10630
+ try {
10631
+ const blueprintConfig = {
10632
+ apiKey: config.apiKey,
10633
+ model: config.model,
10634
+ baseUrl: config.baseUrl,
10635
+ providerMode: config.providerMode
10636
+ };
10637
+ const blueprint = await extractBlueprint(
10638
+ setupDescription,
10639
+ twinName,
10640
+ availableCollections,
10641
+ blueprintConfig
10642
+ );
10643
+ if (!blueprint) {
10644
+ debug("Blueprint extraction returned null");
10645
+ return null;
10646
+ }
10647
+ if (blueprint.collections.length === 0 && blueprint.identities.length === 0) {
10648
+ debug("Blueprint extraction returned empty blueprint");
10649
+ return null;
10650
+ }
10651
+ debug("Blueprint extracted", {
10652
+ identities: String(blueprint.identities.length),
10653
+ collections: blueprint.collections.map((c) => `${c.name}:${c.totalCount}`).join(", ")
10654
+ });
10655
+ const { seed: builtSeed, warnings } = buildSeedFromBlueprint(blueprint, baseSeedData);
10656
+ if (warnings.length > 0) {
10657
+ debug("Blueprint builder warnings", { warnings: warnings.join("; ") });
10658
+ }
10659
+ let finalSeed = normalizeSeedData(builtSeed, twinName);
10660
+ finalSeed = autoFillMissingFKs(finalSeed, twinName);
10661
+ const relValidation = validateSeedRelationships(finalSeed, twinName);
10662
+ if (!relValidation.valid) {
10663
+ warn("Blueprint seed failed relationship validation", {
10664
+ errors: relValidation.errors.slice(0, 5).join("; ")
10665
+ });
10666
+ return null;
10667
+ }
10668
+ if (intent) {
10669
+ const coverage = validateSeedCoverage(intent, finalSeed);
10670
+ if (!coverage.valid) {
10671
+ warn("Blueprint seed failed coverage validation", {
10672
+ errors: coverage.issues.map((i) => i.message).join("; ")
10673
+ });
10674
+ return null;
10675
+ }
10676
+ }
10677
+ const flatForVerify = {};
10678
+ flatForVerify[twinName] = finalSeed;
10679
+ const countMismatches = verifySeedCounts(setupDescription, flatForVerify);
10680
+ if (countMismatches.length > 0) {
10681
+ debug("Blueprint seed has count mismatches (acceptable)", {
10682
+ mismatches: countMismatches.map((m) => `${m.subject}: ${m.expected} vs ${m.actual}`).join("; ")
10683
+ });
10684
+ }
10685
+ const syntheticPatch = {
10686
+ add: {}
10687
+ };
10688
+ for (const [collection, entities] of Object.entries(finalSeed)) {
10689
+ const baseCount = baseSeedData[collection]?.length ?? 0;
10690
+ if (entities.length > baseCount) {
10691
+ syntheticPatch.add[collection] = entities.slice(baseCount).map((e) => {
10692
+ const { id: _id, createdAt: _ca, updatedAt: _ua, ...rest } = e;
10693
+ return rest;
10694
+ });
10695
+ }
10696
+ }
10697
+ return {
10698
+ seed: finalSeed,
10699
+ patch: syntheticPatch,
10700
+ fromCache: false,
10701
+ source: "llm"
10702
+ // Blueprint still uses LLM for extraction
10703
+ };
10704
+ } catch (err) {
10705
+ const msg = err instanceof Error ? err.message : String(err);
10706
+ warn(`Blueprint path failed: ${msg}`);
10707
+ return null;
10708
+ }
10709
+ }
9685
10710
  async function generateDynamicSeed(twinName, baseSeedName, baseSeedData, setupDescription, config, intent, context) {
9686
10711
  const cacheScope = {
9687
10712
  baseSeedData,
@@ -9703,6 +10728,28 @@ async function generateDynamicSeed(twinName, baseSeedName, baseSeedData, setupDe
9703
10728
  "No API key configured for seed generation. Set ARCHAL_TOKEN or configure a provider API key."
9704
10729
  ]);
9705
10730
  }
10731
+ progress(`Generating dynamic seed for ${twinName}...`);
10732
+ const availableCollections = Object.keys(baseSeedData);
10733
+ const blueprintResult = await tryBlueprintPath(
10734
+ twinName,
10735
+ baseSeedData,
10736
+ setupDescription,
10737
+ availableCollections,
10738
+ config,
10739
+ intent
10740
+ );
10741
+ if (blueprintResult) {
10742
+ info("Dynamic seed generated via blueprint", { twin: twinName });
10743
+ if (!config.noCache) {
10744
+ const cacheContext = buildSeedCacheContext(twinName, intent, context);
10745
+ cacheSeed(twinName, baseSeedName, setupDescription, blueprintResult.seed, blueprintResult.patch, {
10746
+ baseSeedData,
10747
+ cacheContext
10748
+ });
10749
+ }
10750
+ return blueprintResult;
10751
+ }
10752
+ debug("Blueprint path failed or produced invalid seed, falling back to full LLM generation");
9706
10753
  const userPrompt = buildSeedGenerationPrompt(
9707
10754
  twinName,
9708
10755
  baseSeedData,
@@ -9710,7 +10757,6 @@ async function generateDynamicSeed(twinName, baseSeedName, baseSeedData, setupDe
9710
10757
  intent,
9711
10758
  context
9712
10759
  );
9713
- progress(`Generating dynamic seed for ${twinName}...`);
9714
10760
  let patch = null;
9715
10761
  let mergedSeed = null;
9716
10762
  let lastErrors = [];
@@ -9860,6 +10906,22 @@ Fix these issues:
9860
10906
  continue;
9861
10907
  }
9862
10908
  }
10909
+ if (mergedSeed && setupDescription && validationAttempts < MAX_ATTEMPTS - 1) {
10910
+ const flatForVerify = {};
10911
+ flatForVerify[twinName] = mergedSeed;
10912
+ const countMismatches = verifySeedCounts(setupDescription, flatForVerify);
10913
+ if (countMismatches.length > 0) {
10914
+ const countErrors = countMismatches.map(
10915
+ (m) => `Count mismatch: "${m.subject}" should be ${m.expected} but got ${m.actual}`
10916
+ );
10917
+ warn(`Seed count mismatch (attempt ${attempt + 1})`, {
10918
+ errors: countErrors.join("; ")
10919
+ });
10920
+ lastErrors = countErrors;
10921
+ validationAttempts++;
10922
+ continue;
10923
+ }
10924
+ }
9863
10925
  break;
9864
10926
  } catch (err) {
9865
10927
  const message = err instanceof Error ? err.message : String(err);
@@ -12132,7 +13194,10 @@ function createRunCommand() {
12132
13194
  ).addOption(new Option("--openclaw-url <url>", "Deprecated alias for --engine-endpoint").hideHelp()).addOption(new Option("--openclaw-token <token>", "Deprecated alias for --engine-token").hideHelp()).addOption(new Option("--openclaw-agent <id>", "Deprecated alias for --engine-model").hideHelp()).addOption(new Option("--openclaw-twin-urls <path>", "Deprecated alias for --engine-twin-urls").hideHelp()).addOption(new Option("--openclaw-timeout <seconds>", "Deprecated alias for --engine-timeout").hideHelp()).option("--api-base-urls <path>", "Path to JSON mapping service names to clone API base URLs for raw API code routing").option("--api-proxy-url <url>", "Proxy URL for raw API code routing metadata").option("--preflight-only", "Run environment/config preflight checks only and exit").option("--seed-cache", "Enable seed cache for dynamic generation (off by default)").option("--static-seed", "Use seed files as-is without LLM mutation (uses --seed name or auto-selected per twin)").option("--no-failure-analysis", "Skip LLM failure analysis on imperfect scores").option(
12133
13195
  "--allow-ambiguous-seed",
12134
13196
  "Allow dynamic seed generation when setup is underspecified"
12135
- ).option("--tag <tag>", "Only run if scenario has this tag (exit 0 if not)").option("-q, --quiet", "Suppress non-error output").option("-v, --verbose", "Enable debug logging").action(async (scenarioArg, opts) => {
13197
+ ).option("--tag <tag>", "Only run if scenario has this tag (exit 0 if not)").option("-q, --quiet", "Suppress non-error output").option("-v, --verbose", "Enable debug logging").action(async (scenarioArg, opts, command) => {
13198
+ const parentOpts = command.parent?.opts() ?? {};
13199
+ if (parentOpts.quiet) opts.quiet = true;
13200
+ if (parentOpts.verbose) opts.verbose = true;
12136
13201
  if (opts.quiet) {
12137
13202
  configureLogger({ quiet: true });
12138
13203
  }
@@ -12405,9 +13470,16 @@ function createRunCommand() {
12405
13470
  }
12406
13471
  if (opts.apiKey?.trim()) {
12407
13472
  warnIfKeyLooksInvalid(opts.apiKey.trim(), "--api-key");
12408
- process.env["ARCHAL_ENGINE_API_KEY"] = opts.apiKey.trim();
13473
+ const key = opts.apiKey.trim();
13474
+ process.env["ARCHAL_ENGINE_API_KEY"] = key;
13475
+ if (key.startsWith("AIza")) {
13476
+ process.env["GEMINI_API_KEY"] = process.env["GEMINI_API_KEY"] || key;
13477
+ } else if (key.startsWith("sk-ant-")) {
13478
+ process.env["ANTHROPIC_API_KEY"] = process.env["ANTHROPIC_API_KEY"] || key;
13479
+ } else if (key.startsWith("sk-")) {
13480
+ process.env["OPENAI_API_KEY"] = process.env["OPENAI_API_KEY"] || key;
13481
+ }
12409
13482
  if (!opts.engineModel && !process.env["ARCHAL_ENGINE_MODEL"] && !opts.model?.trim()) {
12410
- const key = opts.apiKey.trim();
12411
13483
  if (key.startsWith("AIza")) {
12412
13484
  opts.engineModel = "gemini-2.0-flash";
12413
13485
  } else if (key.startsWith("sk-ant-")) {
@@ -12578,13 +13650,13 @@ function createRunCommand() {
12578
13650
  let statusReadySinceMs = null;
12579
13651
  const isRetryablePollFailure = (result) => result.offline || typeof result.status === "number" && result.status >= 500;
12580
13652
  const sleepForPollInterval = async () => new Promise((resolve12) => setTimeout(resolve12, SESSION_POLL_INTERVAL_MS));
12581
- process.stderr.write("Starting cloud session...\n");
13653
+ if (!opts.quiet) process.stderr.write("Starting cloud session...\n");
12582
13654
  let pollCount = 0;
12583
13655
  while (Date.now() < readyDeadline) {
12584
13656
  pollCount++;
12585
13657
  if (pollCount % 4 === 0) {
12586
13658
  const elapsedSec = Math.round((Date.now() - (readyDeadline - SESSION_READY_TIMEOUT_MS)) / 1e3);
12587
- process.stderr.write(` Still waiting for session to be ready (${elapsedSec}s)...
13659
+ if (!opts.quiet) process.stderr.write(` Still waiting for session to be ready (${elapsedSec}s)...
12588
13660
  `);
12589
13661
  }
12590
13662
  const freshCreds = getCredentials();
@@ -12655,7 +13727,7 @@ function createRunCommand() {
12655
13727
  }
12656
13728
  if (sessionReady) {
12657
13729
  const warmupSec = Math.round((Date.now() - (readyDeadline - SESSION_READY_TIMEOUT_MS)) / 1e3);
12658
- process.stderr.write(`Cloud session ready (${warmupSec}s).
13730
+ if (!opts.quiet) process.stderr.write(`Cloud session ready (${warmupSec}s).
12659
13731
  `);
12660
13732
  }
12661
13733
  if (!sessionReady && !runFailureMessage) {
@@ -13018,12 +14090,33 @@ function buildEvidenceArtifacts(report) {
13018
14090
  runIndex: run.runIndex,
13019
14091
  steps: buildAgentTraceSteps(run)
13020
14092
  })).filter((run) => run.steps.length > 0);
14093
+ const evaluations = reportRuns.flatMap(
14094
+ (run) => (run.evaluations ?? []).map((ev) => ({
14095
+ runIndex: run.runIndex,
14096
+ criterionId: ev.criterionId,
14097
+ status: ev.status,
14098
+ result: ev.status,
14099
+ confidence: ev.confidence,
14100
+ explanation: ev.explanation
14101
+ }))
14102
+ );
14103
+ const latestEvaluationByCriterion = /* @__PURE__ */ new Map();
14104
+ for (const evaluation of evaluations) {
14105
+ const current = latestEvaluationByCriterion.get(evaluation.criterionId);
14106
+ if (!current || evaluation.runIndex >= current.runIndex) {
14107
+ latestEvaluationByCriterion.set(evaluation.criterionId, evaluation);
14108
+ }
14109
+ }
13021
14110
  const criteria = Object.entries(report.criterionDescriptions ?? {}).map(
13022
- ([id, description]) => ({
13023
- id,
13024
- label: description,
13025
- kind: report.criterionTypes?.[id] ?? null
13026
- })
14111
+ ([id, description]) => {
14112
+ const latest = latestEvaluationByCriterion.get(id);
14113
+ return {
14114
+ id,
14115
+ label: description,
14116
+ kind: report.criterionTypes?.[id] ?? null,
14117
+ result: latest?.result ?? null
14118
+ };
14119
+ }
13027
14120
  );
13028
14121
  const runs = reportRuns.map((run) => ({
13029
14122
  runIndex: run.runIndex,
@@ -13033,6 +14126,7 @@ function buildEvidenceArtifacts(report) {
13033
14126
  evaluations: (run.evaluations ?? []).map((ev) => ({
13034
14127
  criterionId: ev.criterionId,
13035
14128
  status: ev.status,
14129
+ result: ev.status,
13036
14130
  confidence: ev.confidence,
13037
14131
  explanation: ev.explanation
13038
14132
  }))
@@ -13041,6 +14135,7 @@ function buildEvidenceArtifacts(report) {
13041
14135
  satisfaction: report.satisfactionScore,
13042
14136
  scores: reportRuns.map((r) => r.overallScore),
13043
14137
  criteria,
14138
+ evaluations,
13044
14139
  runs,
13045
14140
  traceEntries,
13046
14141
  thinkingTraceEntries,
@@ -15118,9 +16213,10 @@ function findBundledScenarios() {
15118
16213
  return results;
15119
16214
  }
15120
16215
  function detectProviderName(model) {
15121
- if (model.startsWith("gemini-")) return "Gemini";
15122
- if (model.startsWith("claude-")) return "Anthropic";
15123
- if (model.startsWith("gpt-") || model.startsWith("o1-") || model.startsWith("o3-") || model.startsWith("o4-")) return "OpenAI";
16216
+ const normalized = model.toLowerCase();
16217
+ if (normalized.startsWith("gemini-")) return "Gemini";
16218
+ if (normalized.startsWith("claude-") || normalized.startsWith("sonnet-") || normalized.startsWith("haiku-") || normalized.startsWith("opus-")) return "Anthropic";
16219
+ if (normalized.startsWith("gpt-") || normalized.startsWith("o1-") || normalized.startsWith("o3-") || normalized.startsWith("o4-")) return "OpenAI";
15124
16220
  return "OpenAI-compatible";
15125
16221
  }
15126
16222
  function resolveEngineApiKey(explicitKey) {
@@ -15189,6 +16285,13 @@ Set one via:
15189
16285
  process.exit(1);
15190
16286
  }
15191
16287
  process.env["ARCHAL_ENGINE_API_KEY"] = engineApiKey;
16288
+ if (engineApiKey.startsWith("AIza")) {
16289
+ process.env["GEMINI_API_KEY"] = process.env["GEMINI_API_KEY"] || engineApiKey;
16290
+ } else if (engineApiKey.startsWith("sk-ant-")) {
16291
+ process.env["ANTHROPIC_API_KEY"] = process.env["ANTHROPIC_API_KEY"] || engineApiKey;
16292
+ } else if (engineApiKey.startsWith("sk-")) {
16293
+ process.env["OPENAI_API_KEY"] = process.env["OPENAI_API_KEY"] || engineApiKey;
16294
+ }
15192
16295
  const runs = parseInt(opts.runs, 10);
15193
16296
  if (Number.isNaN(runs) || runs <= 0) {
15194
16297
  process.stderr.write("Error: --runs must be a positive integer\n");