qfai 1.7.6 → 1.7.7

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.
@@ -465,6 +465,15 @@ function normalizeUiux(raw, configPath, issues) {
465
465
  );
466
466
  }
467
467
  }
468
+ if (raw.phase1ReleaseDate !== void 0) {
469
+ if (typeof raw.phase1ReleaseDate === "string" && !isNaN(new Date(raw.phase1ReleaseDate).getTime())) {
470
+ result.phase1ReleaseDate = raw.phase1ReleaseDate;
471
+ } else {
472
+ issues.push(
473
+ configIssue(configPath, "uiux.phase1ReleaseDate \u306F\u6709\u52B9\u306A\u65E5\u4ED8\u6587\u5B57\u5217\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
474
+ );
475
+ }
476
+ }
468
477
  if (raw.warning_as_error_override !== void 0) {
469
478
  if (Array.isArray(raw.warning_as_error_override) && raw.warning_as_error_override.every((v) => typeof v === "string")) {
470
479
  result.warning_as_error_override = raw.warning_as_error_override;
@@ -489,6 +498,12 @@ function normalizeUiux(raw, configPath, issues) {
489
498
  result.audit = audit;
490
499
  }
491
500
  }
501
+ if (raw.migration !== void 0) {
502
+ const migration = normalizeUiuxMigration(raw.migration, configPath, issues);
503
+ if (migration) {
504
+ result.migration = migration;
505
+ }
506
+ }
492
507
  return Object.keys(result).length > 0 ? result : void 0;
493
508
  }
494
509
  function normalizeUiuxAudit(raw, configPath, issues) {
@@ -548,6 +563,23 @@ function normalizeUiuxAudit(raw, configPath, issues) {
548
563
  }
549
564
  return Object.keys(result).length > 0 ? result : void 0;
550
565
  }
566
+ function normalizeUiuxMigration(raw, configPath, issues) {
567
+ if (!isRecord(raw)) {
568
+ issues.push(configIssue(configPath, "uiux.migration \u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002"));
569
+ return void 0;
570
+ }
571
+ const result = {};
572
+ if (raw.strict !== void 0) {
573
+ if (typeof raw.strict === "boolean") {
574
+ result.strict = raw.strict;
575
+ } else {
576
+ issues.push(
577
+ configIssue(configPath, "uiux.migration.strict \u306F\u30D6\u30FC\u30EB\u5024\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002")
578
+ );
579
+ }
580
+ }
581
+ return Object.keys(result).length > 0 ? result : void 0;
582
+ }
551
583
  function normalizeRenderEvidence(raw, configPath, issues) {
552
584
  if (!isRecord(raw)) {
553
585
  issues.push(
@@ -1684,8 +1716,8 @@ import { readFile as readFile5 } from "fs/promises";
1684
1716
  import path8 from "path";
1685
1717
  import { fileURLToPath as fileURLToPath2 } from "url";
1686
1718
  async function resolveToolVersion() {
1687
- if ("1.7.6".length > 0) {
1688
- return "1.7.6";
1719
+ if ("1.7.7".length > 0) {
1720
+ return "1.7.7";
1689
1721
  }
1690
1722
  try {
1691
1723
  const packagePath = resolvePackageJsonPath();
@@ -3428,8 +3460,8 @@ function buildCopilotInstructions() {
3428
3460
  }
3429
3461
 
3430
3462
  // src/cli/commands/prototyping.ts
3431
- import { mkdir as mkdir4, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
3432
- import path17 from "path";
3463
+ import { mkdir as mkdir4, readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
3464
+ import path20 from "path";
3433
3465
 
3434
3466
  // src/core/prototyping/uiFidelityAutogen.ts
3435
3467
  import { readFile as readFile8 } from "fs/promises";
@@ -4068,256 +4100,715 @@ var RUNTIME_HEAVY_CHECKS = [
4068
4100
  ];
4069
4101
  var FULL_HARNESS_OBLIGATIONS = [...STATIC_OBLIGATIONS, ...RUNTIME_HEAVY_CHECKS];
4070
4102
 
4071
- // src/cli/commands/prototyping.ts
4072
- var ENV_AUTOGEN = "QFAI_PROTOTYPE_FIDELITY_AUTOGEN";
4073
- var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
4074
- async function runPrototyping(options) {
4075
- const { config } = await loadConfig(options.root);
4076
- const renderOptions = mergeRenderOptions(options, config.uiux?.renderEvidence);
4077
- const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
4078
- if (!autogenEnabled) {
4079
- if (options.autogenOnly) {
4080
- error(
4081
- `prototyping: --autogen-only \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u3059\u304C --autogen-ui-fidelity / ${ENV_AUTOGEN}=1 \u304C\u3042\u308A\u307E\u305B\u3093\u3002`
4082
- );
4083
- return 2;
4084
- }
4085
- info(
4086
- `prototyping: --autogen-ui-fidelity or ${ENV_AUTOGEN}=1 \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u4F55\u3082\u5B9F\u884C\u3057\u307E\u305B\u3093\u3002`
4087
- );
4088
- const evidencePath2 = resolveEvidencePath(options.root, options.evidenceOut);
4089
- const toolVersion2 = await resolveToolVersion();
4090
- let existingEvidence2 = {};
4091
- try {
4092
- const raw = await readFile9(evidencePath2, "utf-8");
4093
- const parsed = JSON.parse(raw);
4094
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4095
- existingEvidence2 = parsed;
4096
- }
4097
- } catch {
4098
- }
4099
- const skippedEvidence = emitUiFidelity({
4100
- evidence: existingEvidence2,
4101
- toolVersion: toolVersion2,
4102
- command: "qfai prototyping",
4103
- baseUrl: "",
4104
- status: "skipped",
4105
- reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
4106
- });
4107
- const renderBundle2 = await maybeWriteRenderBundle(
4108
- toolVersion2,
4109
- "qfai prototyping --render-evidence",
4110
- renderOptions,
4111
- false
4112
- );
4113
- await writeEvidence(
4114
- evidencePath2,
4115
- applyRenderEvidence(
4116
- skippedEvidence,
4117
- await resolveAttachedRenderEvidence(renderBundle2, renderOptions, false)
4118
- )
4119
- );
4120
- return 0;
4121
- }
4122
- const baseUrl = resolveBaseUrl(renderOptions);
4123
- if (!baseUrl) {
4124
- error(`prototyping: --base-url \u307E\u305F\u306F QFAI_PROTOTYPE_BASE_URL \u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002`);
4125
- return 1;
4126
- }
4127
- const toolVersion = await resolveToolVersion();
4128
- const evidencePath = resolveEvidencePath(options.root, options.evidenceOut);
4129
- info(`prototyping: autogen uiFidelity \u3092\u5B9F\u884C\u3057\u307E\u3059 (baseUrl=${baseUrl})`);
4130
- let existingEvidence = {};
4103
+ // src/core/prototyping/discussionReader.ts
4104
+ import { readFile as readFile9 } from "fs/promises";
4105
+ import path17 from "path";
4106
+ import { parse as parseYaml3 } from "yaml";
4107
+ var VALID_MODES = /* @__PURE__ */ new Set(["low-cost", "standard", "full-harness"]);
4108
+ var SIDECAR_FILENAME = "prototyping.yaml";
4109
+ async function readDiscussionRecommendation(discussionPackDir) {
4110
+ const filePath = path17.join(discussionPackDir, SIDECAR_FILENAME);
4111
+ let raw;
4131
4112
  try {
4132
- const raw = await readFile9(evidencePath, "utf-8");
4133
- const parsed = JSON.parse(raw);
4134
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4135
- existingEvidence = parsed;
4136
- }
4113
+ raw = await readFile9(filePath, "utf-8");
4137
4114
  } catch {
4115
+ return { recommended_mode: null, rationale: null };
4138
4116
  }
4139
- const routeHints = extractRouteHintsFromEvidence(existingEvidence);
4140
- let result;
4117
+ let doc;
4141
4118
  try {
4142
- result = await autogenerateUiFidelity(options.root, config, baseUrl, routeHints);
4143
- } catch (err) {
4144
- const reason = err instanceof Error ? err.message : String(err);
4145
- warn(`prototyping: autogen failed - ${reason}`);
4146
- const failedEvidence = emitUiFidelity({
4147
- evidence: existingEvidence,
4148
- toolVersion,
4149
- command: "qfai prototyping --autogen-ui-fidelity",
4150
- baseUrl,
4151
- status: "failed",
4152
- reason
4153
- });
4154
- const renderBundle2 = await maybeWriteRenderBundle(
4155
- toolVersion,
4156
- "qfai prototyping --render-evidence",
4157
- renderOptions,
4158
- true
4159
- );
4160
- await writeEvidence(
4161
- evidencePath,
4162
- applyRenderEvidence(
4163
- failedEvidence,
4164
- await resolveAttachedRenderEvidence(renderBundle2, renderOptions, true)
4165
- )
4166
- );
4167
- info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4168
- return options.autogenOnly ? 1 : 0;
4169
- }
4170
- const hasScreens = result.screens.length > 0;
4171
- const allRoutesFailed = result.crawled.every((r) => r.status === "failed");
4172
- if (!hasScreens || allRoutesFailed) {
4173
- const uiContractsPath = path17.join(resolvePath(options.root, config, "contractsDir"), "ui");
4174
- const reason = !hasScreens ? `no screens found in ${uiContractsPath}` : "all route crawls failed";
4175
- warn(`prototyping: autogen produced no usable data - ${reason}`);
4176
- const failedEvidence = emitUiFidelity({
4177
- evidence: existingEvidence,
4178
- toolVersion,
4179
- command: "qfai prototyping --autogen-ui-fidelity",
4180
- baseUrl,
4181
- status: "failed",
4182
- crawled: result.crawled,
4183
- reason
4184
- });
4185
- const renderBundle2 = await maybeWriteRenderBundle(
4186
- toolVersion,
4187
- "qfai prototyping --render-evidence",
4188
- renderOptions,
4189
- true
4190
- );
4191
- await writeEvidence(
4192
- evidencePath,
4193
- applyRenderEvidence(
4194
- failedEvidence,
4195
- await resolveAttachedRenderEvidence(renderBundle2, renderOptions, true)
4196
- )
4197
- );
4198
- info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4199
- return options.autogenOnly ? 1 : 0;
4200
- }
4201
- const successEvidence = emitUiFidelity({
4202
- evidence: existingEvidence,
4203
- toolVersion,
4204
- command: "qfai prototyping --autogen-ui-fidelity",
4205
- baseUrl,
4206
- status: "success",
4207
- screens: result.screens,
4208
- crawled: result.crawled
4209
- });
4210
- const renderBundle = await maybeWriteRenderBundle(
4211
- toolVersion,
4212
- "qfai prototyping --render-evidence",
4213
- renderOptions,
4214
- true
4215
- );
4216
- await writeEvidence(
4217
- evidencePath,
4218
- applyRenderEvidence(
4219
- successEvidence,
4220
- await resolveAttachedRenderEvidence(renderBundle, renderOptions, true)
4221
- )
4222
- );
4223
- const routeOkCount = result.crawled.filter((r) => r.status === "ok").length;
4224
- const routeFailCount = result.crawled.filter((r) => r.status === "failed").length;
4225
- const avgCoverage = result.screens.length > 0 ? (result.screens.reduce((sum, s) => sum + s.coverage, 0) / result.screens.length).toFixed(2) : "N/A";
4226
- info(
4227
- `prototyping: autogen success - ${result.screens.length} screens, routes ok=${routeOkCount} fail=${routeFailCount}, avg coverage=${avgCoverage}`
4228
- );
4229
- info(`prototyping: wrote evidence to ${evidencePath}`);
4230
- return 0;
4231
- }
4232
- function resolveBaseUrl(options) {
4233
- if (options.baseUrl) {
4234
- return options.baseUrl;
4235
- }
4236
- const envUrl = process.env.QFAI_PROTOTYPE_BASE_URL;
4237
- if (envUrl && envUrl.trim().length > 0) {
4238
- return envUrl.trim();
4119
+ doc = parseYaml3(raw);
4120
+ } catch {
4121
+ return { recommended_mode: null, rationale: null };
4239
4122
  }
4240
- return null;
4241
- }
4242
- function resolveEvidencePath(root, explicit) {
4243
- if (explicit) {
4244
- return path17.isAbsolute(explicit) ? explicit : path17.resolve(root, explicit);
4123
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
4124
+ return { recommended_mode: null, rationale: null };
4245
4125
  }
4246
- return path17.resolve(root, DEFAULT_EVIDENCE_PATH);
4247
- }
4248
- async function writeEvidence(filePath, evidence) {
4249
- await mkdir4(path17.dirname(filePath), { recursive: true });
4250
- await writeFile3(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4251
- }
4252
- async function writeRenderBundle(filePath, bundle) {
4253
- await mkdir4(path17.dirname(filePath), { recursive: true });
4254
- await writeFile3(filePath, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
4255
- }
4256
- async function maybeWriteRenderBundle(toolVersion, command, options, autogenEnabled) {
4257
- if (!options.renderEvidence) {
4258
- return void 0;
4126
+ const record2 = doc;
4127
+ const prototyping = record2.prototyping;
4128
+ if (!prototyping || typeof prototyping !== "object" || Array.isArray(prototyping)) {
4129
+ return { recommended_mode: null, rationale: null };
4259
4130
  }
4260
- const renderBundle = await buildRenderBundle(toolVersion, command, options, autogenEnabled);
4261
- await writeRenderBundle(renderBundle.path, renderBundle.bundle);
4262
- return renderBundle;
4131
+ const section = prototyping;
4132
+ const rawMode = typeof section.recommended_mode === "string" ? section.recommended_mode.trim() : null;
4133
+ const rationale = typeof section.rationale === "string" ? section.rationale.trim() : null;
4134
+ const recommended_mode = rawMode && VALID_MODES.has(rawMode) ? rawMode : null;
4135
+ return { recommended_mode, rationale };
4263
4136
  }
4264
- function applyRenderEvidence(evidence, renderEvidence) {
4265
- if (!renderEvidence) {
4266
- return evidence;
4137
+
4138
+ // src/core/prototyping/precedenceResolver.ts
4139
+ var VALID_MODES2 = /* @__PURE__ */ new Set([
4140
+ "default",
4141
+ "standard",
4142
+ "low-cost",
4143
+ "full-harness"
4144
+ ]);
4145
+ function isPrototypingMode(value) {
4146
+ return VALID_MODES2.has(value);
4147
+ }
4148
+ var SYSTEM_DEFAULT_MODE = "standard";
4149
+ function resolvePrecedence(input) {
4150
+ const { cliMode, discussion } = input;
4151
+ const rawRecommended = discussion?.recommended_mode ?? null;
4152
+ const recommended_mode = rawRecommended && isPrototypingMode(rawRecommended) ? rawRecommended : null;
4153
+ if (cliMode) {
4154
+ return {
4155
+ effective_mode: cliMode,
4156
+ mode_source: "cli-override",
4157
+ recommended_mode,
4158
+ rationale: "CLI override"
4159
+ };
4267
4160
  }
4268
- return {
4269
- ...evidence,
4270
- renderEvidence
4271
- };
4272
- }
4273
- async function resolveAttachedRenderEvidence(renderBundle, options, autogenEnabled) {
4274
- if (!options.renderEvidence) {
4275
- return void 0;
4161
+ if (recommended_mode) {
4162
+ return {
4163
+ effective_mode: recommended_mode,
4164
+ mode_source: "discussion-recommendation",
4165
+ recommended_mode,
4166
+ rationale: discussion?.rationale ?? "discussion recommendation"
4167
+ };
4276
4168
  }
4277
- return renderBundle?.renderEvidence ?? await buildRenderEvidenceRecord(
4278
- options,
4279
- autogenEnabled,
4280
- resolveRenderOutPath(options.root, options.renderOut)
4281
- );
4282
- }
4283
- async function buildRenderBundle(toolVersion, command, options, autogenEnabled) {
4284
- const outPath = resolveRenderOutPath(options.root, options.renderOut);
4285
- const renderEvidence = await buildRenderEvidenceRecord(options, autogenEnabled, outPath);
4286
4169
  return {
4287
- path: outPath,
4288
- bundle: {
4289
- meta: {
4290
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4291
- toolVersion,
4292
- commands: [command]
4293
- },
4294
- renderEvidence
4295
- },
4296
- renderEvidence
4170
+ effective_mode: SYSTEM_DEFAULT_MODE,
4171
+ mode_source: "default",
4172
+ recommended_mode: null,
4173
+ rationale: "system default"
4297
4174
  };
4298
4175
  }
4299
- function mergeRenderOptions(options, configRenderEvidence) {
4300
- const cliViewportsSpecified = Array.isArray(options.renderViewports);
4301
- const mergedViewports = cliViewportsSpecified ? normalizeRenderViewports(options.renderViewports) : normalizeRenderViewports(configRenderEvidence?.viewports);
4302
- const renderOut = options.renderOut ?? configRenderEvidence?.out;
4303
- const baseUrl = options.baseUrl ?? configRenderEvidence?.baseUrl;
4176
+
4177
+ // src/core/prototyping/modeLogger.ts
4178
+ var EVIDENCE_EXPECTATIONS = {
4179
+ "low-cost": "L1/L2",
4180
+ standard: "L2/L3",
4181
+ "full-harness": "L3/L4/L5"
4182
+ };
4183
+ function formatModeLog(resolution) {
4304
4184
  return {
4305
- ...options,
4306
- renderEvidence: options.renderEvidence || configRenderEvidence?.enabled === true,
4307
- renderViewports: mergedViewports,
4308
- renderFailOpen: options.renderFailOpen ?? configRenderEvidence?.failOpen === true,
4309
- ...renderOut ? { renderOut } : {},
4310
- ...baseUrl ? { baseUrl } : {}
4185
+ mode_source: resolution.mode_source,
4186
+ recommended_mode: resolution.recommended_mode,
4187
+ effective_mode: resolution.effective_mode,
4188
+ rationale: resolution.rationale,
4189
+ evidence_expectations: EVIDENCE_EXPECTATIONS[resolution.effective_mode] ?? "L2/L3"
4311
4190
  };
4312
4191
  }
4313
- function resolveRenderOutPath(root, explicit) {
4314
- if (explicit) {
4315
- return path17.isAbsolute(explicit) ? explicit : path17.resolve(root, explicit);
4316
- }
4317
- return path17.resolve(root, ".qfai/evidence/render.json");
4318
- }
4319
- async function buildRenderEvidenceRecord(options, autogenEnabled, outputPath) {
4320
- const viewports = normalizeRenderViewports(options.renderViewports);
4192
+
4193
+ // src/core/discussionPack.ts
4194
+ import { readFile as readFile10, stat as stat2 } from "fs/promises";
4195
+ import path19 from "path";
4196
+
4197
+ // src/core/packLocator.ts
4198
+ import { readdir as readdir5 } from "fs/promises";
4199
+ import path18 from "path";
4200
+ var PACK_RULES = {
4201
+ discussion: {
4202
+ prefix: "discussion",
4203
+ legacyPattern: /^discussion-\d{4}$/i,
4204
+ parkedLegacyPattern: /^discussion-legacy-[a-z0-9][a-z0-9-]*$/i
4205
+ }
4206
+ };
4207
+ var PACK_TIMESTAMP_RE = /^\d{17}$/;
4208
+ function parsePackTimestamp(kind, name) {
4209
+ const rule = PACK_RULES[kind];
4210
+ const prefix = `${rule.prefix}-`;
4211
+ if (!name.startsWith(prefix)) {
4212
+ return null;
4213
+ }
4214
+ const suffix = name.slice(prefix.length);
4215
+ if (!PACK_TIMESTAMP_RE.test(suffix)) {
4216
+ return null;
4217
+ }
4218
+ return suffix;
4219
+ }
4220
+ function validatePackName(kind, name) {
4221
+ const rule = PACK_RULES[kind];
4222
+ const normalizedPrefix = `${rule.prefix}-`;
4223
+ const isPrefixed = name.toLowerCase().startsWith(normalizedPrefix);
4224
+ const timestamp = parsePackTimestamp(kind, name);
4225
+ const isCanonical = timestamp !== null;
4226
+ const isLegacy = !isCanonical && rule.legacyPattern.test(name);
4227
+ const isParkedLegacy = !isCanonical && rule.parkedLegacyPattern.test(name);
4228
+ const isDangerous = isPrefixed && !isCanonical && !isLegacy && !isParkedLegacy;
4229
+ const status = isCanonical ? "canonical" : isLegacy ? "legacy" : isDangerous ? "dangerous" : "other";
4230
+ return {
4231
+ kind,
4232
+ name,
4233
+ status,
4234
+ isCanonical,
4235
+ isLegacy,
4236
+ isDangerous,
4237
+ isPrefixed,
4238
+ timestamp
4239
+ };
4240
+ }
4241
+ async function findPacks(rootDir, kind) {
4242
+ try {
4243
+ const entries = await readdir5(rootDir, { withFileTypes: true });
4244
+ const packs = [];
4245
+ for (const entry of entries) {
4246
+ if (!entry.isDirectory()) {
4247
+ continue;
4248
+ }
4249
+ const validation = validatePackName(kind, entry.name);
4250
+ if (validation.status === "other") {
4251
+ continue;
4252
+ }
4253
+ packs.push({
4254
+ kind,
4255
+ rootDir,
4256
+ path: path18.join(rootDir, entry.name),
4257
+ name: entry.name,
4258
+ status: validation.status,
4259
+ isCanonical: validation.isCanonical,
4260
+ isLegacy: validation.isLegacy,
4261
+ isDangerous: validation.isDangerous,
4262
+ timestamp: validation.timestamp
4263
+ });
4264
+ }
4265
+ return packs.sort((left, right) => left.name.localeCompare(right.name));
4266
+ } catch {
4267
+ return [];
4268
+ }
4269
+ }
4270
+ function latestPack(packs) {
4271
+ let latest = null;
4272
+ for (const candidate of packs) {
4273
+ if (!candidate.isCanonical || !candidate.timestamp) {
4274
+ continue;
4275
+ }
4276
+ if (!latest || isTimestampGreater(candidate.timestamp, latest.timestamp)) {
4277
+ latest = candidate;
4278
+ }
4279
+ }
4280
+ return latest;
4281
+ }
4282
+ async function findLatestPack(rootDir, kind) {
4283
+ const packs = await findPacks(rootDir, kind);
4284
+ return latestPack(packs);
4285
+ }
4286
+ function isTimestampGreater(left, right) {
4287
+ if (!right) {
4288
+ return true;
4289
+ }
4290
+ return BigInt(left) > BigInt(right);
4291
+ }
4292
+
4293
+ // src/core/discussionPack.ts
4294
+ var REQUIRED_DISCUSSION_PACK_FILES = [
4295
+ "01_Context.md",
4296
+ "02_Inception-Deck.md",
4297
+ "03_Story-Workshop.md",
4298
+ "04_Sources.md",
4299
+ "05_Scope.md",
4300
+ "06_REQ.md",
4301
+ "07_NFR.md",
4302
+ "08_Glossary.md",
4303
+ "09_Constraints.md",
4304
+ "10_Policy.md",
4305
+ "11_OQ-Register.md",
4306
+ "12_OQ-Resolution-Log.md",
4307
+ "13_Deferred.md",
4308
+ "14_Review-Request.md",
4309
+ "99_delta.md"
4310
+ ];
4311
+ var PLACEHOLDER_LINE_RE = /^(?:[-*]\s*)?(?:tbd|todo|none|n\/a|placeholder|\(placeholder\)|to be defined|to be updated|<[^>]+>)\.?$/i;
4312
+ async function inspectLatestDiscussionPack(discussionRoot) {
4313
+ const packs = await findPacks(discussionRoot, "discussion");
4314
+ const legacyPackNames = packs.filter((pack) => pack.isLegacy).map((pack) => pack.name).sort((left, right) => left.localeCompare(right));
4315
+ const dangerousPackNames = packs.filter((pack) => pack.isDangerous).map((pack) => pack.name).sort((left, right) => left.localeCompare(right));
4316
+ const latest = latestPack(packs);
4317
+ const latestPackDir = latest?.path ?? null;
4318
+ const latestPackName = latest?.name ?? null;
4319
+ if (!latestPackDir) {
4320
+ return {
4321
+ discussionRoot,
4322
+ latestPackDir: null,
4323
+ latestPackName,
4324
+ legacyPackNames,
4325
+ dangerousPackNames,
4326
+ missingFiles: [...REQUIRED_DISCUSSION_PACK_FILES],
4327
+ incompleteFiles: [],
4328
+ blockingOqIds: [],
4329
+ deferredWithoutDetails: []
4330
+ };
4331
+ }
4332
+ const missingFiles = [];
4333
+ const incompleteFiles = [];
4334
+ let blockingOqIds = [];
4335
+ let deferredWithoutDetails = [];
4336
+ for (const fileName of REQUIRED_DISCUSSION_PACK_FILES) {
4337
+ const target = path19.join(latestPackDir, fileName);
4338
+ const content = await readSafe(target);
4339
+ if (content === null) {
4340
+ missingFiles.push(fileName);
4341
+ continue;
4342
+ }
4343
+ if (isDiscussionPackFileIncomplete(content)) {
4344
+ incompleteFiles.push(fileName);
4345
+ }
4346
+ if (fileName === "11_OQ-Register.md") {
4347
+ blockingOqIds = extractBlockingOqIds(content);
4348
+ }
4349
+ }
4350
+ const oqRegisterContent = await readSafe(path19.join(latestPackDir, "11_OQ-Register.md"));
4351
+ const deferredContent = await readSafe(path19.join(latestPackDir, "13_Deferred.md"));
4352
+ if (oqRegisterContent !== null && deferredContent !== null) {
4353
+ deferredWithoutDetails = extractDeferredWithoutDetails(oqRegisterContent, deferredContent);
4354
+ }
4355
+ return {
4356
+ discussionRoot,
4357
+ latestPackDir,
4358
+ latestPackName,
4359
+ legacyPackNames,
4360
+ dangerousPackNames,
4361
+ missingFiles,
4362
+ incompleteFiles,
4363
+ blockingOqIds,
4364
+ deferredWithoutDetails
4365
+ };
4366
+ }
4367
+ async function findLatestDiscussionPackDir(discussionRoot) {
4368
+ const packs = await findPacks(discussionRoot, "discussion");
4369
+ return latestPack(packs)?.path ?? null;
4370
+ }
4371
+ function isDiscussionPackFileIncomplete(text) {
4372
+ const normalized = text.replace(/\r\n/g, "\n").trim();
4373
+ if (normalized.length < 100) {
4374
+ return true;
4375
+ }
4376
+ const contentLines = normalized.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !line.startsWith("#")).filter((line) => !/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/.test(line));
4377
+ if (contentLines.length === 0) {
4378
+ return true;
4379
+ }
4380
+ if (contentLines.every((line) => isPlaceholderLine(line))) {
4381
+ return true;
4382
+ }
4383
+ return false;
4384
+ }
4385
+ function isPlaceholderLine(line) {
4386
+ const plain = line.replace(/[`*_~]/g, "").replace(/\|/g, " ").replace(/\s+/g, " ").trim();
4387
+ return PLACEHOLDER_LINE_RE.test(plain);
4388
+ }
4389
+ function extractBlockingOqIds(text) {
4390
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
4391
+ const oqStates = /* @__PURE__ */ new Map();
4392
+ let currentId = null;
4393
+ for (const line of lines) {
4394
+ const idMatch = /\b(OQ-\d+)\b/i.exec(line);
4395
+ if (idMatch?.[1]) {
4396
+ currentId = idMatch[1].toUpperCase();
4397
+ }
4398
+ if (currentId !== null) {
4399
+ const state = oqStates.get(currentId) ?? { disposition: null };
4400
+ const disposition = /^\s*-\s*Disposition\s*:\s*([^\s#]+)/i.exec(line)?.[1] ?? null;
4401
+ if (disposition) {
4402
+ state.disposition = disposition.toLowerCase();
4403
+ }
4404
+ oqStates.set(currentId, state);
4405
+ }
4406
+ if (line.trim().startsWith("|")) {
4407
+ const cells = parseTableCells(line);
4408
+ for (const cell of cells) {
4409
+ const oqMatch = /\b(OQ-\d+)\b/i.exec(cell);
4410
+ if (oqMatch?.[1]) {
4411
+ currentId = oqMatch[1].toUpperCase();
4412
+ const existingState = oqStates.get(currentId) ?? { disposition: null };
4413
+ oqStates.set(currentId, existingState);
4414
+ }
4415
+ }
4416
+ }
4417
+ }
4418
+ const tableOqs = extractOqTableRows(text);
4419
+ for (const row of tableOqs) {
4420
+ if (row.id && row.disposition) {
4421
+ const existing = oqStates.get(row.id) ?? { disposition: null };
4422
+ existing.disposition = row.disposition;
4423
+ oqStates.set(row.id, existing);
4424
+ }
4425
+ }
4426
+ const blocking = Array.from(oqStates.entries()).filter(([, state]) => state.disposition === "open").map(([id]) => id).sort((left, right) => left.localeCompare(right));
4427
+ return blocking;
4428
+ }
4429
+ function extractOqTableRows(text) {
4430
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
4431
+ const results = [];
4432
+ let headerIndex = -1;
4433
+ let oqIdCol = -1;
4434
+ let dispositionCol = -1;
4435
+ for (let i = 0; i < lines.length - 1; i++) {
4436
+ const line = lines[i] ?? "";
4437
+ if (!line.includes("|")) continue;
4438
+ const cells = parseTableCells(line);
4439
+ const normalizedCells = cells.map((c) => c.toLowerCase().replace(/[^a-z0-9-]/g, ""));
4440
+ oqIdCol = normalizedCells.findIndex((c) => c === "oq-id" || c === "oqid");
4441
+ dispositionCol = normalizedCells.findIndex((c) => c === "disposition");
4442
+ if (oqIdCol >= 0 && dispositionCol >= 0) {
4443
+ const sepLine = lines[i + 1] ?? "";
4444
+ const sepCells = parseTableCells(sepLine);
4445
+ if (sepCells.length === cells.length && sepCells.every((c) => /^:?-{3,}:?$/.test(c.trim()))) {
4446
+ headerIndex = i;
4447
+ break;
4448
+ }
4449
+ }
4450
+ }
4451
+ if (headerIndex < 0) return results;
4452
+ for (let i = headerIndex + 2; i < lines.length; i++) {
4453
+ const line = lines[i] ?? "";
4454
+ if (!line.trim().startsWith("|")) break;
4455
+ const cells = parseTableCells(line);
4456
+ const oqIdRaw = cells[oqIdCol] ?? "";
4457
+ const dispositionRaw = cells[dispositionCol] ?? "";
4458
+ const oqMatch = /\b(OQ-\d+)\b/i.exec(oqIdRaw);
4459
+ if (oqMatch?.[1]) {
4460
+ results.push({
4461
+ id: oqMatch[1].toUpperCase(),
4462
+ disposition: dispositionRaw.trim().toLowerCase()
4463
+ });
4464
+ }
4465
+ }
4466
+ return results;
4467
+ }
4468
+ function extractDeferredWithoutDetails(oqRegisterText, deferredText) {
4469
+ const registerRows = extractOqTableRows(oqRegisterText);
4470
+ const deferredIds = registerRows.filter((row) => row.disposition === "deferred").map((row) => row.id);
4471
+ if (deferredIds.length === 0) return [];
4472
+ const deferredDetailSet = extractAllOqIds(deferredText);
4473
+ return deferredIds.filter((id) => !deferredDetailSet.has(id));
4474
+ }
4475
+ function extractAllOqIds(text) {
4476
+ const ids = /* @__PURE__ */ new Set();
4477
+ const re = /\b(OQ-\d+)\b/gi;
4478
+ let match;
4479
+ while ((match = re.exec(text)) !== null) {
4480
+ if (match[1]) {
4481
+ ids.add(match[1].toUpperCase());
4482
+ }
4483
+ }
4484
+ return ids;
4485
+ }
4486
+ function parseTableCells(line) {
4487
+ const trimmed = line.trim();
4488
+ if (!trimmed.startsWith("|")) return [];
4489
+ const normalized = trimmed.replace(/^\|/, "").replace(/\|$/, "");
4490
+ return normalized.split("|").map((cell) => cell.trim());
4491
+ }
4492
+ async function readSafe(filePath) {
4493
+ try {
4494
+ const stats = await stat2(filePath);
4495
+ if (!stats.isFile()) {
4496
+ return null;
4497
+ }
4498
+ return await readFile10(filePath, "utf-8");
4499
+ } catch {
4500
+ return null;
4501
+ }
4502
+ }
4503
+
4504
+ // src/cli/commands/prototyping.ts
4505
+ var ENV_AUTOGEN = "QFAI_PROTOTYPE_FIDELITY_AUTOGEN";
4506
+ var DEFAULT_EVIDENCE_PATH = ".qfai/evidence/prototyping.json";
4507
+ async function runPrototyping(options) {
4508
+ if (options.modeInvalid) {
4509
+ error(
4510
+ `QFAI-PROTO-010: invalid mode "${options.modeInvalid}". Valid modes: low-cost, standard, full-harness`
4511
+ );
4512
+ return 1;
4513
+ }
4514
+ const { config } = await loadConfig(options.root);
4515
+ const discussionDir = path20.join(options.root, config.paths.discussionDir);
4516
+ const packDir = await findLatestDiscussionPackDir(discussionDir);
4517
+ const recommendation = packDir ? await readDiscussionRecommendation(packDir) : null;
4518
+ const modeResolution = resolvePrecedence({
4519
+ cliMode: options.mode ?? null,
4520
+ discussion: recommendation
4521
+ });
4522
+ const modeLog = formatModeLog(modeResolution);
4523
+ info(`prototyping: mode resolution \u2014 ${JSON.stringify(modeLog)}`);
4524
+ if (modeResolution.effective_mode === "full-harness") {
4525
+ info(
4526
+ "prototyping: full-harness mode requires /qfai-prototyping-full-harness. Exiting without starting loop."
4527
+ );
4528
+ return 0;
4529
+ }
4530
+ if (modeResolution.effective_mode === "low-cost") {
4531
+ info("prototyping: low-cost mode \u2014 skipping runtime evidence (static obligations only).");
4532
+ const evidencePath2 = resolveEvidencePath(options.root, options.evidenceOut);
4533
+ const toolVersion2 = await resolveToolVersion();
4534
+ let existingEvidence2 = {};
4535
+ try {
4536
+ const raw = await readFile11(evidencePath2, "utf-8");
4537
+ const parsed = JSON.parse(raw);
4538
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4539
+ existingEvidence2 = parsed;
4540
+ }
4541
+ } catch {
4542
+ }
4543
+ const skippedEvidence = emitUiFidelity({
4544
+ evidence: existingEvidence2,
4545
+ toolVersion: toolVersion2,
4546
+ command: "qfai prototyping",
4547
+ baseUrl: "",
4548
+ status: "skipped",
4549
+ reason: "low-cost mode: runtime evidence skipped"
4550
+ });
4551
+ const renderOptions2 = mergeRenderOptions(options, config.uiux?.renderEvidence);
4552
+ const renderBundle2 = await maybeWriteRenderBundle(
4553
+ toolVersion2,
4554
+ "qfai prototyping --render-evidence",
4555
+ renderOptions2,
4556
+ false
4557
+ );
4558
+ await writeEvidence(
4559
+ evidencePath2,
4560
+ applyRenderEvidence(
4561
+ skippedEvidence,
4562
+ await resolveAttachedRenderEvidence(renderBundle2, renderOptions2, false)
4563
+ )
4564
+ );
4565
+ return 0;
4566
+ }
4567
+ const renderOptions = mergeRenderOptions(options, config.uiux?.renderEvidence);
4568
+ const autogenEnabled = options.autogenUiFidelity || process.env[ENV_AUTOGEN] === "1";
4569
+ if (!autogenEnabled) {
4570
+ if (options.autogenOnly) {
4571
+ error(
4572
+ `prototyping: --autogen-only \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u3059\u304C --autogen-ui-fidelity / ${ENV_AUTOGEN}=1 \u304C\u3042\u308A\u307E\u305B\u3093\u3002`
4573
+ );
4574
+ return 2;
4575
+ }
4576
+ info(
4577
+ `prototyping: --autogen-ui-fidelity or ${ENV_AUTOGEN}=1 \u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u4F55\u3082\u5B9F\u884C\u3057\u307E\u305B\u3093\u3002`
4578
+ );
4579
+ const evidencePath2 = resolveEvidencePath(options.root, options.evidenceOut);
4580
+ const toolVersion2 = await resolveToolVersion();
4581
+ let existingEvidence2 = {};
4582
+ try {
4583
+ const raw = await readFile11(evidencePath2, "utf-8");
4584
+ const parsed = JSON.parse(raw);
4585
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4586
+ existingEvidence2 = parsed;
4587
+ }
4588
+ } catch {
4589
+ }
4590
+ const skippedEvidence = emitUiFidelity({
4591
+ evidence: existingEvidence2,
4592
+ toolVersion: toolVersion2,
4593
+ command: "qfai prototyping",
4594
+ baseUrl: "",
4595
+ status: "skipped",
4596
+ reason: "autogen not enabled (--autogen-ui-fidelity or env not set)"
4597
+ });
4598
+ const renderBundle2 = await maybeWriteRenderBundle(
4599
+ toolVersion2,
4600
+ "qfai prototyping --render-evidence",
4601
+ renderOptions,
4602
+ false
4603
+ );
4604
+ await writeEvidence(
4605
+ evidencePath2,
4606
+ applyRenderEvidence(
4607
+ skippedEvidence,
4608
+ await resolveAttachedRenderEvidence(renderBundle2, renderOptions, false)
4609
+ )
4610
+ );
4611
+ return 0;
4612
+ }
4613
+ const baseUrl = resolveBaseUrl(renderOptions);
4614
+ if (!baseUrl) {
4615
+ error(`prototyping: --base-url \u307E\u305F\u306F QFAI_PROTOTYPE_BASE_URL \u306E\u6307\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002`);
4616
+ return 1;
4617
+ }
4618
+ const toolVersion = await resolveToolVersion();
4619
+ const evidencePath = resolveEvidencePath(options.root, options.evidenceOut);
4620
+ info(`prototyping: autogen uiFidelity \u3092\u5B9F\u884C\u3057\u307E\u3059 (baseUrl=${baseUrl})`);
4621
+ let existingEvidence = {};
4622
+ try {
4623
+ const raw = await readFile11(evidencePath, "utf-8");
4624
+ const parsed = JSON.parse(raw);
4625
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
4626
+ existingEvidence = parsed;
4627
+ }
4628
+ } catch {
4629
+ }
4630
+ const routeHints = extractRouteHintsFromEvidence(existingEvidence);
4631
+ let result;
4632
+ try {
4633
+ result = await autogenerateUiFidelity(options.root, config, baseUrl, routeHints);
4634
+ } catch (err) {
4635
+ const reason = err instanceof Error ? err.message : String(err);
4636
+ warn(`prototyping: autogen failed - ${reason}`);
4637
+ const failedEvidence = emitUiFidelity({
4638
+ evidence: existingEvidence,
4639
+ toolVersion,
4640
+ command: "qfai prototyping --autogen-ui-fidelity",
4641
+ baseUrl,
4642
+ status: "failed",
4643
+ reason
4644
+ });
4645
+ const renderBundle2 = await maybeWriteRenderBundle(
4646
+ toolVersion,
4647
+ "qfai prototyping --render-evidence",
4648
+ renderOptions,
4649
+ true
4650
+ );
4651
+ await writeEvidence(
4652
+ evidencePath,
4653
+ applyRenderEvidence(
4654
+ failedEvidence,
4655
+ await resolveAttachedRenderEvidence(renderBundle2, renderOptions, true)
4656
+ )
4657
+ );
4658
+ info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4659
+ return options.autogenOnly ? 1 : 0;
4660
+ }
4661
+ const hasScreens = result.screens.length > 0;
4662
+ const allRoutesFailed = result.crawled.every((r) => r.status === "failed");
4663
+ if (!hasScreens || allRoutesFailed) {
4664
+ const uiContractsPath = path20.join(resolvePath(options.root, config, "contractsDir"), "ui");
4665
+ const reason = !hasScreens ? `no screens found in ${uiContractsPath}` : "all route crawls failed";
4666
+ warn(`prototyping: autogen produced no usable data - ${reason}`);
4667
+ const failedEvidence = emitUiFidelity({
4668
+ evidence: existingEvidence,
4669
+ toolVersion,
4670
+ command: "qfai prototyping --autogen-ui-fidelity",
4671
+ baseUrl,
4672
+ status: "failed",
4673
+ crawled: result.crawled,
4674
+ reason
4675
+ });
4676
+ const renderBundle2 = await maybeWriteRenderBundle(
4677
+ toolVersion,
4678
+ "qfai prototyping --render-evidence",
4679
+ renderOptions,
4680
+ true
4681
+ );
4682
+ await writeEvidence(
4683
+ evidencePath,
4684
+ applyRenderEvidence(
4685
+ failedEvidence,
4686
+ await resolveAttachedRenderEvidence(renderBundle2, renderOptions, true)
4687
+ )
4688
+ );
4689
+ info(`prototyping: wrote evidence with status=failed to ${evidencePath}`);
4690
+ return options.autogenOnly ? 1 : 0;
4691
+ }
4692
+ const successEvidence = emitUiFidelity({
4693
+ evidence: existingEvidence,
4694
+ toolVersion,
4695
+ command: "qfai prototyping --autogen-ui-fidelity",
4696
+ baseUrl,
4697
+ status: "success",
4698
+ screens: result.screens,
4699
+ crawled: result.crawled
4700
+ });
4701
+ const renderBundle = await maybeWriteRenderBundle(
4702
+ toolVersion,
4703
+ "qfai prototyping --render-evidence",
4704
+ renderOptions,
4705
+ true
4706
+ );
4707
+ await writeEvidence(
4708
+ evidencePath,
4709
+ applyRenderEvidence(
4710
+ successEvidence,
4711
+ await resolveAttachedRenderEvidence(renderBundle, renderOptions, true)
4712
+ )
4713
+ );
4714
+ const routeOkCount = result.crawled.filter((r) => r.status === "ok").length;
4715
+ const routeFailCount = result.crawled.filter((r) => r.status === "failed").length;
4716
+ const avgCoverage = result.screens.length > 0 ? (result.screens.reduce((sum, s) => sum + s.coverage, 0) / result.screens.length).toFixed(2) : "N/A";
4717
+ info(
4718
+ `prototyping: autogen success - ${result.screens.length} screens, routes ok=${routeOkCount} fail=${routeFailCount}, avg coverage=${avgCoverage}`
4719
+ );
4720
+ info(`prototyping: wrote evidence to ${evidencePath}`);
4721
+ return 0;
4722
+ }
4723
+ function resolveBaseUrl(options) {
4724
+ if (options.baseUrl) {
4725
+ return options.baseUrl;
4726
+ }
4727
+ const envUrl = process.env.QFAI_PROTOTYPE_BASE_URL;
4728
+ if (envUrl && envUrl.trim().length > 0) {
4729
+ return envUrl.trim();
4730
+ }
4731
+ return null;
4732
+ }
4733
+ function resolveEvidencePath(root, explicit) {
4734
+ if (explicit) {
4735
+ return path20.isAbsolute(explicit) ? explicit : path20.resolve(root, explicit);
4736
+ }
4737
+ return path20.resolve(root, DEFAULT_EVIDENCE_PATH);
4738
+ }
4739
+ async function writeEvidence(filePath, evidence) {
4740
+ await mkdir4(path20.dirname(filePath), { recursive: true });
4741
+ await writeFile3(filePath, JSON.stringify(evidence, null, 2) + "\n", "utf-8");
4742
+ }
4743
+ async function writeRenderBundle(filePath, bundle) {
4744
+ await mkdir4(path20.dirname(filePath), { recursive: true });
4745
+ await writeFile3(filePath, JSON.stringify(bundle, null, 2) + "\n", "utf-8");
4746
+ }
4747
+ async function maybeWriteRenderBundle(toolVersion, command, options, autogenEnabled) {
4748
+ if (!options.renderEvidence) {
4749
+ return void 0;
4750
+ }
4751
+ const renderBundle = await buildRenderBundle(toolVersion, command, options, autogenEnabled);
4752
+ await writeRenderBundle(renderBundle.path, renderBundle.bundle);
4753
+ return renderBundle;
4754
+ }
4755
+ function applyRenderEvidence(evidence, renderEvidence) {
4756
+ if (!renderEvidence) {
4757
+ return evidence;
4758
+ }
4759
+ return {
4760
+ ...evidence,
4761
+ renderEvidence
4762
+ };
4763
+ }
4764
+ async function resolveAttachedRenderEvidence(renderBundle, options, autogenEnabled) {
4765
+ if (!options.renderEvidence) {
4766
+ return void 0;
4767
+ }
4768
+ return renderBundle?.renderEvidence ?? await buildRenderEvidenceRecord(
4769
+ options,
4770
+ autogenEnabled,
4771
+ resolveRenderOutPath(options.root, options.renderOut)
4772
+ );
4773
+ }
4774
+ async function buildRenderBundle(toolVersion, command, options, autogenEnabled) {
4775
+ const outPath = resolveRenderOutPath(options.root, options.renderOut);
4776
+ const renderEvidence = await buildRenderEvidenceRecord(options, autogenEnabled, outPath);
4777
+ return {
4778
+ path: outPath,
4779
+ bundle: {
4780
+ meta: {
4781
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4782
+ toolVersion,
4783
+ commands: [command]
4784
+ },
4785
+ renderEvidence
4786
+ },
4787
+ renderEvidence
4788
+ };
4789
+ }
4790
+ function mergeRenderOptions(options, configRenderEvidence) {
4791
+ const cliViewportsSpecified = Array.isArray(options.renderViewports);
4792
+ const mergedViewports = cliViewportsSpecified ? normalizeRenderViewports(options.renderViewports) : normalizeRenderViewports(configRenderEvidence?.viewports);
4793
+ const renderOut = options.renderOut ?? configRenderEvidence?.out;
4794
+ const baseUrl = options.baseUrl ?? configRenderEvidence?.baseUrl;
4795
+ return {
4796
+ ...options,
4797
+ renderEvidence: options.renderEvidence || configRenderEvidence?.enabled === true,
4798
+ renderViewports: mergedViewports,
4799
+ renderFailOpen: options.renderFailOpen ?? configRenderEvidence?.failOpen === true,
4800
+ ...renderOut ? { renderOut } : {},
4801
+ ...baseUrl ? { baseUrl } : {}
4802
+ };
4803
+ }
4804
+ function resolveRenderOutPath(root, explicit) {
4805
+ if (explicit) {
4806
+ return path20.isAbsolute(explicit) ? explicit : path20.resolve(root, explicit);
4807
+ }
4808
+ return path20.resolve(root, ".qfai/evidence/render.json");
4809
+ }
4810
+ async function buildRenderEvidenceRecord(options, autogenEnabled, outputPath) {
4811
+ const viewports = normalizeRenderViewports(options.renderViewports);
4321
4812
  if (!autogenEnabled) {
4322
4813
  return {
4323
4814
  status: "skipped",
@@ -4401,8 +4892,8 @@ function extractRouteHintsFromEvidence(evidence) {
4401
4892
  }
4402
4893
 
4403
4894
  // src/cli/commands/report.ts
4404
- import { mkdir as mkdir8, readFile as readFile45, writeFile as writeFile7 } from "fs/promises";
4405
- import path61 from "path";
4895
+ import { mkdir as mkdir8, readFile as readFile46, writeFile as writeFile7 } from "fs/promises";
4896
+ import path64 from "path";
4406
4897
 
4407
4898
  // src/core/normalize.ts
4408
4899
  function normalizeIssuePaths(root, issues) {
@@ -4497,17 +4988,17 @@ async function createPhaseGuardResult(phase, blockedIssue) {
4497
4988
  }
4498
4989
 
4499
4990
  // src/core/report.ts
4500
- import { readFile as readFile43 } from "fs/promises";
4501
- import path59 from "path";
4991
+ import { readFile as readFile44 } from "fs/promises";
4992
+ import path62 from "path";
4502
4993
 
4503
4994
  // src/core/contractIndex.ts
4504
- import { readFile as readFile10 } from "fs/promises";
4505
- import path18 from "path";
4995
+ import { readFile as readFile12 } from "fs/promises";
4996
+ import path21 from "path";
4506
4997
  async function buildContractIndex(root, config) {
4507
4998
  const contractsRoot = resolvePath(root, config, "contractsDir");
4508
- const uiRoot = path18.join(contractsRoot, "ui");
4509
- const apiRoot = path18.join(contractsRoot, "api");
4510
- const dbRoot = path18.join(contractsRoot, "db");
4999
+ const uiRoot = path21.join(contractsRoot, "ui");
5000
+ const apiRoot = path21.join(contractsRoot, "api");
5001
+ const dbRoot = path21.join(contractsRoot, "db");
4511
5002
  const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
4512
5003
  collectUiContractFiles(uiRoot),
4513
5004
  collectThemaContractFiles(),
@@ -4527,7 +5018,7 @@ async function buildContractIndex(root, config) {
4527
5018
  }
4528
5019
  async function indexContractFiles(files, index) {
4529
5020
  for (const file of files) {
4530
- const text = await readFile10(file, "utf-8");
5021
+ const text = await readFile12(file, "utf-8");
4531
5022
  extractDeclaredContractIds(text).forEach((id) => {
4532
5023
  record(index, id, file);
4533
5024
  });
@@ -4920,7 +5411,7 @@ function parseSpec(md, file) {
4920
5411
  }
4921
5412
 
4922
5413
  // src/core/deltaV1.ts
4923
- import { parse as parseYaml3 } from "yaml";
5414
+ import { parse as parseYaml4 } from "yaml";
4924
5415
  var REQUIRED_DELTA_META_KEYS = [
4925
5416
  "id",
4926
5417
  "date",
@@ -5075,7 +5566,7 @@ function parseYamlMeta(block) {
5075
5566
  return { value: null, error: null };
5076
5567
  }
5077
5568
  try {
5078
- const parsed = parseYaml3(sanitizeMetaYaml(block));
5569
+ const parsed = parseYaml4(sanitizeMetaYaml(block));
5079
5570
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5080
5571
  return {
5081
5572
  value: null,
@@ -5109,7 +5600,7 @@ function parseVerificationPlan(body) {
5109
5600
  return { planHeadingLine: planHeading.line, parseError: null, items: [] };
5110
5601
  }
5111
5602
  try {
5112
- const parsed = parseYaml3(yamlSource);
5603
+ const parsed = parseYaml4(yamlSource);
5113
5604
  if (!Array.isArray(parsed)) {
5114
5605
  return {
5115
5606
  planHeadingLine: planHeading.line,
@@ -5196,9 +5687,9 @@ function asTagArray(value) {
5196
5687
  }
5197
5688
 
5198
5689
  // src/core/waivers.ts
5199
- import { access as access9, readFile as readFile12 } from "fs/promises";
5200
- import path19 from "path";
5201
- import { parse as parseYaml4 } from "yaml";
5690
+ import { access as access9, readFile as readFile14 } from "fs/promises";
5691
+ import path22 from "path";
5692
+ import { parse as parseYaml5 } from "yaml";
5202
5693
 
5203
5694
  // src/core/regex.ts
5204
5695
  function escapeRegExp(value) {
@@ -5206,7 +5697,7 @@ function escapeRegExp(value) {
5206
5697
  }
5207
5698
 
5208
5699
  // src/core/validators/utils.ts
5209
- import { access as access8, readFile as readFile11 } from "fs/promises";
5700
+ import { access as access8, readFile as readFile13 } from "fs/promises";
5210
5701
  function issue(code, message, severity, file, rule, refs, category = "compatibility", suggested_action, details) {
5211
5702
  const issue2 = {
5212
5703
  code,
@@ -5239,9 +5730,9 @@ async function exists8(filePath) {
5239
5730
  return false;
5240
5731
  }
5241
5732
  }
5242
- async function readSafe(filePath) {
5733
+ async function readSafe2(filePath) {
5243
5734
  try {
5244
- return await readFile11(filePath, "utf-8");
5735
+ return await readFile13(filePath, "utf-8");
5245
5736
  } catch {
5246
5737
  return "";
5247
5738
  }
@@ -5329,12 +5820,12 @@ function cloneGlobal(pattern) {
5329
5820
  }
5330
5821
 
5331
5822
  // src/core/waivers.ts
5332
- var WAIVER_FILE = path19.join(".qfai", "waivers.yml");
5333
- var UNSUPPORTED_WAIVER_FILE = path19.join(".qfai", "waivers.yaml");
5823
+ var WAIVER_FILE = path22.join(".qfai", "waivers.yml");
5824
+ var UNSUPPORTED_WAIVER_FILE = path22.join(".qfai", "waivers.yaml");
5334
5825
  var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
5335
5826
  var RULE_ID_RE = /^[A-Z]+-\d{3}$/;
5336
5827
  async function applyWaivers(root, findings) {
5337
- const resolvedRoot = path19.resolve(root);
5828
+ const resolvedRoot = path22.resolve(root);
5338
5829
  const ruleSeverityIndex = buildRuleSeverityIndex(findings);
5339
5830
  const loaded = await loadWaivers(resolvedRoot, ruleSeverityIndex);
5340
5831
  const applied = applyWaiversToFindings(resolvedRoot, findings, loaded.applicableWaivers);
@@ -5347,8 +5838,8 @@ async function applyWaivers(root, findings) {
5347
5838
  };
5348
5839
  }
5349
5840
  async function loadWaivers(root, ruleSeverityIndex) {
5350
- const waiverPath = path19.join(root, WAIVER_FILE);
5351
- const unsupportedPath = path19.join(root, UNSUPPORTED_WAIVER_FILE);
5841
+ const waiverPath = path22.join(root, WAIVER_FILE);
5842
+ const unsupportedPath = path22.join(root, UNSUPPORTED_WAIVER_FILE);
5352
5843
  const validationIssues = [];
5353
5844
  if (await exists9(unsupportedPath)) {
5354
5845
  validationIssues.push(
@@ -5372,7 +5863,7 @@ async function loadWaivers(root, ruleSeverityIndex) {
5372
5863
  }
5373
5864
  let rawText;
5374
5865
  try {
5375
- rawText = await readFile12(waiverPath, "utf-8");
5866
+ rawText = await readFile14(waiverPath, "utf-8");
5376
5867
  } catch (error2) {
5377
5868
  validationIssues.push(
5378
5869
  issue(
@@ -5393,7 +5884,7 @@ async function loadWaivers(root, ruleSeverityIndex) {
5393
5884
  }
5394
5885
  let parsed;
5395
5886
  try {
5396
- parsed = parseYaml4(rawText);
5887
+ parsed = parseYaml5(rawText);
5397
5888
  } catch (error2) {
5398
5889
  validationIssues.push(
5399
5890
  issue(
@@ -6065,8 +6556,8 @@ var STATIC_RULE_SEVERITY = {
6065
6556
  };
6066
6557
 
6067
6558
  // src/core/validators/contracts.ts
6068
- import { readFile as readFile13 } from "fs/promises";
6069
- import path20 from "path";
6559
+ import { readFile as readFile15 } from "fs/promises";
6560
+ import path23 from "path";
6070
6561
  var SQL_DANGEROUS_PATTERNS = [
6071
6562
  { pattern: /\bDROP\s+TABLE\b/i, label: "DROP TABLE" },
6072
6563
  { pattern: /\bDROP\s+DATABASE\b/i, label: "DROP DATABASE" },
@@ -6079,9 +6570,9 @@ var SQL_DANGEROUS_PATTERNS = [
6079
6570
  async function validateContracts(root, config) {
6080
6571
  const issues = [];
6081
6572
  const contractsRoot = resolvePath(root, config, "contractsDir");
6082
- const uiRoot = path20.join(contractsRoot, "ui");
6083
- const apiRoot = path20.join(contractsRoot, "api");
6084
- const dbRoot = path20.join(contractsRoot, "db");
6573
+ const uiRoot = path23.join(contractsRoot, "ui");
6574
+ const apiRoot = path23.join(contractsRoot, "api");
6575
+ const dbRoot = path23.join(contractsRoot, "db");
6085
6576
  const [uiFiles, apiFiles, dbFiles] = await Promise.all([
6086
6577
  collectUiContractFiles(uiRoot),
6087
6578
  collectApiContractFiles(apiRoot),
@@ -6135,7 +6626,7 @@ async function validateContracts(root, config) {
6135
6626
  }
6136
6627
  async function validateContractFile(file, kind) {
6137
6628
  const issues = [];
6138
- const text = await readFile13(file, "utf-8");
6629
+ const text = await readFile15(file, "utf-8");
6139
6630
  const declaredIds = extractDeclaredContractIds(text);
6140
6631
  issues.push(...validateDeclaredContractIds(declaredIds, file, kind));
6141
6632
  if (kind === "DB") {
@@ -6256,121 +6747,23 @@ function formatError5(error2) {
6256
6747
  }
6257
6748
 
6258
6749
  // src/core/validators/discussMermaid.ts
6259
- import { readFile as readFile14 } from "fs/promises";
6260
- import path22 from "path";
6261
-
6262
- // src/core/packLocator.ts
6263
- import { readdir as readdir5 } from "fs/promises";
6264
- import path21 from "path";
6265
- var PACK_RULES = {
6266
- discussion: {
6267
- prefix: "discussion",
6268
- legacyPattern: /^discussion-\d{4}$/i,
6269
- parkedLegacyPattern: /^discussion-legacy-[a-z0-9][a-z0-9-]*$/i
6270
- }
6271
- };
6272
- var PACK_TIMESTAMP_RE = /^\d{17}$/;
6273
- function parsePackTimestamp(kind, name) {
6274
- const rule = PACK_RULES[kind];
6275
- const prefix = `${rule.prefix}-`;
6276
- if (!name.startsWith(prefix)) {
6277
- return null;
6278
- }
6279
- const suffix = name.slice(prefix.length);
6280
- if (!PACK_TIMESTAMP_RE.test(suffix)) {
6281
- return null;
6282
- }
6283
- return suffix;
6284
- }
6285
- function validatePackName(kind, name) {
6286
- const rule = PACK_RULES[kind];
6287
- const normalizedPrefix = `${rule.prefix}-`;
6288
- const isPrefixed = name.toLowerCase().startsWith(normalizedPrefix);
6289
- const timestamp = parsePackTimestamp(kind, name);
6290
- const isCanonical = timestamp !== null;
6291
- const isLegacy = !isCanonical && rule.legacyPattern.test(name);
6292
- const isParkedLegacy = !isCanonical && rule.parkedLegacyPattern.test(name);
6293
- const isDangerous = isPrefixed && !isCanonical && !isLegacy && !isParkedLegacy;
6294
- const status = isCanonical ? "canonical" : isLegacy ? "legacy" : isDangerous ? "dangerous" : "other";
6295
- return {
6296
- kind,
6297
- name,
6298
- status,
6299
- isCanonical,
6300
- isLegacy,
6301
- isDangerous,
6302
- isPrefixed,
6303
- timestamp
6304
- };
6305
- }
6306
- async function findPacks(rootDir, kind) {
6307
- try {
6308
- const entries = await readdir5(rootDir, { withFileTypes: true });
6309
- const packs = [];
6310
- for (const entry of entries) {
6311
- if (!entry.isDirectory()) {
6312
- continue;
6313
- }
6314
- const validation = validatePackName(kind, entry.name);
6315
- if (validation.status === "other") {
6316
- continue;
6317
- }
6318
- packs.push({
6319
- kind,
6320
- rootDir,
6321
- path: path21.join(rootDir, entry.name),
6322
- name: entry.name,
6323
- status: validation.status,
6324
- isCanonical: validation.isCanonical,
6325
- isLegacy: validation.isLegacy,
6326
- isDangerous: validation.isDangerous,
6327
- timestamp: validation.timestamp
6328
- });
6329
- }
6330
- return packs.sort((left, right) => left.name.localeCompare(right.name));
6331
- } catch {
6332
- return [];
6333
- }
6334
- }
6335
- function latestPack(packs) {
6336
- let latest = null;
6337
- for (const candidate of packs) {
6338
- if (!candidate.isCanonical || !candidate.timestamp) {
6339
- continue;
6340
- }
6341
- if (!latest || isTimestampGreater(candidate.timestamp, latest.timestamp)) {
6342
- latest = candidate;
6343
- }
6344
- }
6345
- return latest;
6346
- }
6347
- async function findLatestPack(rootDir, kind) {
6348
- const packs = await findPacks(rootDir, kind);
6349
- return latestPack(packs);
6350
- }
6351
- function isTimestampGreater(left, right) {
6352
- if (!right) {
6353
- return true;
6354
- }
6355
- return BigInt(left) > BigInt(right);
6356
- }
6357
-
6358
- // src/core/validators/discussMermaid.ts
6750
+ import { readFile as readFile16 } from "fs/promises";
6751
+ import path24 from "path";
6359
6752
  var DISCUSSION_PACK_FLOW_FILE = "03_Story-Workshop.md";
6360
6753
  var MERMAID_START_RE = /^\s*(`{3,}|~{3,})\s*mermaid\b/i;
6361
6754
  var FLOW_OR_SEQUENCE_RE = /\b(?:sequenceDiagram|flowchart)\b/;
6362
6755
  async function validateDiscussionMermaid(root) {
6363
- const discussionRootDir = path22.join(root, ".qfai", "discussion");
6756
+ const discussionRootDir = path24.join(root, ".qfai", "discussion");
6364
6757
  const discussionPackMarkdownFiles = await collectFiles(discussionRootDir, {
6365
6758
  extensions: [".md"]
6366
6759
  });
6367
6760
  const discussionPackFiles = [];
6368
6761
  for (const file of discussionPackMarkdownFiles) {
6369
- const fileName = path22.basename(file);
6762
+ const fileName = path24.basename(file);
6370
6763
  if (fileName !== DISCUSSION_PACK_FLOW_FILE) {
6371
6764
  continue;
6372
6765
  }
6373
- const discussionDirName = path22.basename(path22.dirname(file));
6766
+ const discussionDirName = path24.basename(path24.dirname(file));
6374
6767
  const nameValidation = validatePackName("discussion", discussionDirName);
6375
6768
  if (nameValidation.isCanonical) {
6376
6769
  discussionPackFiles.push({ file, kind: "current" });
@@ -6400,7 +6793,7 @@ async function validateDiscussionMermaid(root) {
6400
6793
  );
6401
6794
  }
6402
6795
  for (const { file } of discussionPackFiles) {
6403
- const text = await readFile14(file, "utf-8");
6796
+ const text = await readFile16(file, "utf-8");
6404
6797
  if (containsMermaidFlowDiagram(text)) {
6405
6798
  continue;
6406
6799
  }
@@ -6450,17 +6843,17 @@ function containsMermaidFlowDiagram(text) {
6450
6843
  }
6451
6844
 
6452
6845
  // src/core/validators/assistantAssets.ts
6453
- import { access as access10, readFile as readFile15 } from "fs/promises";
6454
- import path23 from "path";
6846
+ import { access as access10, readFile as readFile17 } from "fs/promises";
6847
+ import path25 from "path";
6455
6848
  var DRIFT_PROTOCOL_MARKER = "[DRIFT-PROTOCOL:MANDATORY]";
6456
6849
  var REVIEWER_GATE_HEADING_PATTERN = /^###\s+Reviewer Gate\b.*$/im;
6457
6850
  var ANY_MARKDOWN_HEADING_PATTERN = /^\s*#{1,6}\s+/m;
6458
6851
  async function validateAssistantAssets(root, config) {
6459
6852
  const skillsDir = resolvePath(root, config, "skillsDir");
6460
- const assistantDir = path23.dirname(skillsDir);
6461
- const skillsLocalDir = path23.join(path23.dirname(skillsDir), `${path23.basename(skillsDir)}.local`);
6462
- const driftProtocolPath = path23.join(assistantDir, "instructions", "drift-protocol.md");
6463
- const testLayersPath = path23.join(assistantDir, "steering", "test-layers.md");
6853
+ const assistantDir = path25.dirname(skillsDir);
6854
+ const skillsLocalDir = path25.join(path25.dirname(skillsDir), `${path25.basename(skillsDir)}.local`);
6855
+ const driftProtocolPath = path25.join(assistantDir, "instructions", "drift-protocol.md");
6856
+ const testLayersPath = path25.join(assistantDir, "steering", "test-layers.md");
6464
6857
  const issues = [];
6465
6858
  if (!await exists10(driftProtocolPath)) {
6466
6859
  issues.push(
@@ -6486,7 +6879,7 @@ async function validateAssistantAssets(root, config) {
6486
6879
  }
6487
6880
  const skillFiles = await collectSkillFiles([skillsDir, skillsLocalDir]);
6488
6881
  for (const skillFile of skillFiles) {
6489
- const content = await readFile15(skillFile, "utf-8");
6882
+ const content = await readFile17(skillFile, "utf-8");
6490
6883
  if (!content.includes(DRIFT_PROTOCOL_MARKER)) {
6491
6884
  issues.push(
6492
6885
  issue(
@@ -6528,7 +6921,7 @@ async function validateAssistantAssets(root, config) {
6528
6921
  }
6529
6922
  async function collectSkillFiles(dirs) {
6530
6923
  const files = await Promise.all(dirs.map((dir) => collectFiles(dir)));
6531
- return files.flat().filter((filePath) => path23.basename(filePath) === "SKILL.md").sort((a, b) => a.localeCompare(b));
6924
+ return files.flat().filter((filePath) => path25.basename(filePath) === "SKILL.md").sort((a, b) => a.localeCompare(b));
6532
6925
  }
6533
6926
  function extractReviewerGateSection(content) {
6534
6927
  const headingMatch = REVIEWER_GATE_HEADING_PATTERN.exec(content);
@@ -6600,8 +6993,8 @@ async function validateSkillsIntegrity(root, config) {
6600
6993
  }
6601
6994
 
6602
6995
  // src/core/validators/ids.ts
6603
- import { readFile as readFile16 } from "fs/promises";
6604
- import path24 from "path";
6996
+ import { readFile as readFile18 } from "fs/promises";
6997
+ import path26 from "path";
6605
6998
  var SC_TAG_RE2 = /^SC-\d{4}-\d{4}$/;
6606
6999
  var AC_ID_RE2 = /\bAC-[A-Za-z0-9_-]+\b/g;
6607
7000
  var CASE_ID_RE = /\b(?:CASE|TC)-[A-Za-z0-9_-]+\b/g;
@@ -6657,7 +7050,7 @@ async function validateDefinedIds(root, config) {
6657
7050
  return issues;
6658
7051
  }
6659
7052
  async function collectLayeredSharedCapabilityIds(file, out) {
6660
- const text = await readSafe2(file);
7053
+ const text = await readSafe3(file);
6661
7054
  if (!text) {
6662
7055
  return;
6663
7056
  }
@@ -6670,7 +7063,7 @@ async function collectLayeredSharedCapabilityIds(file, out) {
6670
7063
  }
6671
7064
  }
6672
7065
  async function collectLayeredDefinitionIds(file, definitionRe, out) {
6673
- const text = await readSafe2(file);
7066
+ const text = await readSafe3(file);
6674
7067
  if (!text) {
6675
7068
  return;
6676
7069
  }
@@ -6684,7 +7077,7 @@ async function collectLayeredDefinitionIds(file, definitionRe, out) {
6684
7077
  }
6685
7078
  async function collectCaseCatalogueDefinitionIds(files, out) {
6686
7079
  for (const file of files) {
6687
- const text = await readSafe2(file);
7080
+ const text = await readSafe3(file);
6688
7081
  if (!text) {
6689
7082
  continue;
6690
7083
  }
@@ -6696,7 +7089,7 @@ async function collectCaseCatalogueDefinitionIds(files, out) {
6696
7089
  }
6697
7090
  async function collectSpecDefinitionIds(files, out) {
6698
7091
  for (const file of files) {
6699
- const text = await readSafe2(file);
7092
+ const text = await readSafe3(file);
6700
7093
  if (!text) {
6701
7094
  continue;
6702
7095
  }
@@ -6715,16 +7108,16 @@ async function collectSpecDefinitionIds(files, out) {
6715
7108
  }
6716
7109
  }
6717
7110
  }
6718
- async function readSafe2(file) {
7111
+ async function readSafe3(file) {
6719
7112
  try {
6720
- return await readFile16(file, "utf-8");
7113
+ return await readFile18(file, "utf-8");
6721
7114
  } catch {
6722
7115
  return "";
6723
7116
  }
6724
7117
  }
6725
7118
  async function collectScenarioDefinitionIds(files, out) {
6726
7119
  for (const file of files) {
6727
- const text = await readSafe2(file);
7120
+ const text = await readSafe3(file);
6728
7121
  if (!text) {
6729
7122
  continue;
6730
7123
  }
@@ -6748,23 +7141,23 @@ function recordId(out, id, file) {
6748
7141
  }
6749
7142
  function formatFileList(files, root) {
6750
7143
  return files.map((file) => {
6751
- const relative = path24.relative(root, file);
7144
+ const relative = path26.relative(root, file);
6752
7145
  return relative.length > 0 ? relative : file;
6753
7146
  }).join(", ");
6754
7147
  }
6755
7148
 
6756
7149
  // src/core/validators/reviewArtifacts.ts
6757
- import { readFile as readFile17, readdir as readdir6, stat as stat2 } from "fs/promises";
6758
- import path25 from "path";
7150
+ import { readFile as readFile19, readdir as readdir6, stat as stat3 } from "fs/promises";
7151
+ import path27 from "path";
6759
7152
  var REVIEW_PACK_DIR_RE = /^review-(\d{17})$/i;
6760
7153
  var REVIEWER_FILE_RE = /^R\d+_.+\.md$/i;
6761
7154
  var ALLOWED_TARGET_KINDS = /* @__PURE__ */ new Set(["spec", "discussion"]);
6762
7155
  var ALLOWED_ROSTER_STATUS = /* @__PURE__ */ new Set(["PASS", "FAIL", "NA"]);
6763
7156
  var ALLOWED_OVERALL_STATUS = /* @__PURE__ */ new Set(["PASS", "FAIL"]);
6764
7157
  async function validateReviewArtifacts(root) {
6765
- const reviewRoot = path25.join(root, ".qfai", "review");
7158
+ const reviewRoot = path27.join(root, ".qfai", "review");
6766
7159
  const issues = [];
6767
- const gitignorePath = path25.join(reviewRoot, ".gitignore");
7160
+ const gitignorePath = path27.join(reviewRoot, ".gitignore");
6768
7161
  if (!await isFile(gitignorePath)) {
6769
7162
  issues.push(
6770
7163
  issue(
@@ -6802,8 +7195,8 @@ async function validateReviewArtifacts(root) {
6802
7195
  }
6803
7196
  async function validateReviewPack(reviewPackDir) {
6804
7197
  const issues = [];
6805
- const reviewRequestPath = path25.join(reviewPackDir, "review_request.md");
6806
- const summaryPath = path25.join(reviewPackDir, "summary.json");
7198
+ const reviewRequestPath = path27.join(reviewPackDir, "review_request.md");
7199
+ const summaryPath = path27.join(reviewPackDir, "summary.json");
6807
7200
  if (!await isFile(reviewRequestPath)) {
6808
7201
  issues.push(
6809
7202
  issue(
@@ -6846,7 +7239,7 @@ async function validateReviewPack(reviewPackDir) {
6846
7239
  async function validateSummarySchema(summaryPath) {
6847
7240
  let parsed;
6848
7241
  try {
6849
- parsed = JSON.parse(await readFile17(summaryPath, "utf-8"));
7242
+ parsed = JSON.parse(await readFile19(summaryPath, "utf-8"));
6850
7243
  } catch (error2) {
6851
7244
  return [
6852
7245
  issue(
@@ -6935,7 +7328,7 @@ async function listReviewPackDirs(reviewRoot) {
6935
7328
  } catch {
6936
7329
  return [];
6937
7330
  }
6938
- return entries.map((name) => path25.join(reviewRoot, name));
7331
+ return entries.map((name) => path27.join(reviewRoot, name));
6939
7332
  }
6940
7333
  async function listReviewerFiles(reviewPackDir) {
6941
7334
  try {
@@ -6947,7 +7340,7 @@ async function listReviewerFiles(reviewPackDir) {
6947
7340
  }
6948
7341
  async function isFile(target) {
6949
7342
  try {
6950
- const stats = await stat2(target);
7343
+ const stats = await stat3(target);
6951
7344
  return stats.isFile();
6952
7345
  } catch {
6953
7346
  return false;
@@ -6983,8 +7376,8 @@ function formatError6(error2) {
6983
7376
  }
6984
7377
 
6985
7378
  // src/core/validators/specPack.ts
6986
- import { readFile as readFile18 } from "fs/promises";
6987
- import path26 from "path";
7379
+ import { readFile as readFile20 } from "fs/promises";
7380
+ import path28 from "path";
6988
7381
  var LEDGER_REQUIRED_COLUMNS = [
6989
7382
  "trace_id",
6990
7383
  "obj_id",
@@ -7271,7 +7664,7 @@ async function validateLayeredSpecEntry(entry) {
7271
7664
  );
7272
7665
  }
7273
7666
  }
7274
- const capabilitiesText = await readSafe3(entry.capabilityPath);
7667
+ const capabilitiesText = await readSafe4(entry.capabilityPath);
7275
7668
  issues.push(
7276
7669
  ...validateLayeredIdFormat(
7277
7670
  entry.capabilityPath,
@@ -7284,11 +7677,11 @@ async function validateLayeredSpecEntry(entry) {
7284
7677
  return issues;
7285
7678
  }
7286
7679
  const [userStoriesText, acceptanceCriteriaText, businessRulesText, examplesText, testCasesText] = await Promise.all([
7287
- readSafe3(entry.userStoriesPath),
7288
- readSafe3(entry.acceptanceCriteriaPath),
7289
- readSafe3(entry.businessRulesPath),
7290
- readSafe3(entry.examplesPath),
7291
- readSafe3(entry.testCasesPath)
7680
+ readSafe4(entry.userStoriesPath),
7681
+ readSafe4(entry.acceptanceCriteriaPath),
7682
+ readSafe4(entry.businessRulesPath),
7683
+ readSafe4(entry.examplesPath),
7684
+ readSafe4(entry.testCasesPath)
7292
7685
  ]);
7293
7686
  issues.push(
7294
7687
  ...validateLayeredIdFormat(
@@ -7402,7 +7795,7 @@ function validateLayeredNamespace(entry, filePath, ids, prefix) {
7402
7795
  "specPack.layered.namespace",
7403
7796
  mismatched,
7404
7797
  "compatibility",
7405
- `${path26.basename(filePath)} \u306E ${prefix} ID \u3092 spec-${entry.specNumber} \u306B\u5408\u308F\u305B\u3066\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
7798
+ `${path28.basename(filePath)} \u306E ${prefix} ID \u3092 spec-${entry.specNumber} \u306B\u5408\u308F\u305B\u3066\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
7406
7799
  )
7407
7800
  ];
7408
7801
  }
@@ -7410,7 +7803,7 @@ async function collectExistingLayeredDeltaFiles(entry) {
7410
7803
  const candidates = Array.from(new Set(entry.deltaCandidates));
7411
7804
  const existing = [];
7412
7805
  for (const candidate of candidates) {
7413
- if (!/(?:^|_)delta\.md$/i.test(path26.basename(candidate))) {
7806
+ if (!/(?:^|_)delta\.md$/i.test(path28.basename(candidate))) {
7414
7807
  continue;
7415
7808
  }
7416
7809
  if (await fileExists(candidate)) {
@@ -7421,7 +7814,7 @@ async function collectExistingLayeredDeltaFiles(entry) {
7421
7814
  }
7422
7815
  async function fileExists(target) {
7423
7816
  try {
7424
- await readFile18(target, "utf-8");
7817
+ await readFile20(target, "utf-8");
7425
7818
  return true;
7426
7819
  } catch {
7427
7820
  return false;
@@ -7582,7 +7975,7 @@ async function validateTraceabilityLedger(entry, contractIds) {
7582
7975
  const issues = [];
7583
7976
  let ledgerText;
7584
7977
  try {
7585
- ledgerText = await readFile18(entry.traceabilityLedgerPath, "utf-8");
7978
+ ledgerText = await readFile20(entry.traceabilityLedgerPath, "utf-8");
7586
7979
  } catch {
7587
7980
  return issues;
7588
7981
  }
@@ -7873,14 +8266,14 @@ async function collectDefinitions(entry) {
7873
8266
  examplesText,
7874
8267
  tcText
7875
8268
  ] = await Promise.all([
7876
- readSafe3(entry.objectivePath),
7877
- readSafe3(entry.initiativePath),
7878
- readSafe3(entry.capabilityPath),
7879
- readSafe3(entry.flowPath),
7880
- readSafe3(entry.userStoriesPath),
7881
- readSafe3(entry.acceptanceCriteriaPath),
7882
- readSafe3(entry.examplesPath),
7883
- readSafe3(entry.testCasesPath)
8269
+ readSafe4(entry.objectivePath),
8270
+ readSafe4(entry.initiativePath),
8271
+ readSafe4(entry.capabilityPath),
8272
+ readSafe4(entry.flowPath),
8273
+ readSafe4(entry.userStoriesPath),
8274
+ readSafe4(entry.acceptanceCriteriaPath),
8275
+ readSafe4(entry.examplesPath),
8276
+ readSafe4(entry.testCasesPath)
7884
8277
  ]);
7885
8278
  const examples = parseExamplesFeature(examplesText, entry.examplesPath);
7886
8279
  const exIds = /* @__PURE__ */ new Set();
@@ -7974,16 +8367,16 @@ async function loadExistingRequiredTexts(entry, missingFiles) {
7974
8367
  if (!fullPath) {
7975
8368
  continue;
7976
8369
  }
7977
- texts[fileName] = await readSafe3(fullPath);
8370
+ texts[fileName] = await readSafe4(fullPath);
7978
8371
  }
7979
8372
  return texts;
7980
8373
  }
7981
8374
  async function loadLayerPolicy(root, config) {
7982
8375
  const skillsDir = resolvePath(root, config, "skillsDir");
7983
- const assistantRoot = path26.dirname(skillsDir);
7984
- const policyPath = path26.join(assistantRoot, "steering", "test-layers.md");
8376
+ const assistantRoot = path28.dirname(skillsDir);
8377
+ const policyPath = path28.join(assistantRoot, "steering", "test-layers.md");
7985
8378
  try {
7986
- const policyText = await readFile18(policyPath, "utf-8");
8379
+ const policyText = await readFile20(policyPath, "utf-8");
7987
8380
  return {
7988
8381
  tags: resolveAllowedLayerTagsFromPolicy(policyText),
7989
8382
  issues: []
@@ -8089,17 +8482,17 @@ function extractMarkdownSection(text, heading) {
8089
8482
  }
8090
8483
  return lines.slice(start, end).join("\n");
8091
8484
  }
8092
- async function readSafe3(filePath) {
8485
+ async function readSafe4(filePath) {
8093
8486
  try {
8094
- return await readFile18(filePath, "utf-8");
8487
+ return await readFile20(filePath, "utf-8");
8095
8488
  } catch {
8096
8489
  return "";
8097
8490
  }
8098
8491
  }
8099
8492
 
8100
8493
  // src/core/validators/traceability.ts
8101
- import { access as access11, readFile as readFile19 } from "fs/promises";
8102
- import path27 from "path";
8494
+ import { access as access11, readFile as readFile21 } from "fs/promises";
8495
+ import path29 from "path";
8103
8496
  var SC_TAG_RE3 = /^SC-\d{4}-\d{4}$/;
8104
8497
  var SC_TAG_RE_GLOBAL = /\bSC-\d{4}-\d{4}\b/g;
8105
8498
  var SPEC_TAG_RE = /^SPEC-\d{4}$/;
@@ -8116,10 +8509,10 @@ async function validateTraceability(root, config, phase) {
8116
8509
  if (layeredEntries.length === 0) {
8117
8510
  return issues;
8118
8511
  }
8119
- const policiesDir = layeredEntries[0]?.sharedDir ?? path27.join(specsRoot, "_policies");
8120
- const capabilitiesPath = path27.join(policiesDir, "03_Capabilities.md");
8512
+ const policiesDir = layeredEntries[0]?.sharedDir ?? path29.join(specsRoot, "_policies");
8513
+ const capabilitiesPath = path29.join(policiesDir, "03_Capabilities.md");
8121
8514
  const capabilitiesExists = await exists11(capabilitiesPath);
8122
- const capabilitiesText = await readSafe4(capabilitiesPath);
8515
+ const capabilitiesText = await readSafe5(capabilitiesPath);
8123
8516
  const capIds = new Set(extractIds(capabilitiesText, "CAP"));
8124
8517
  const capSpecNumbers = new Set(
8125
8518
  Array.from(capIds).map((capId) => capId.match(/^CAP-(\d{4})$/)?.[1]).filter((value) => Boolean(value))
@@ -8198,8 +8591,8 @@ async function validatePoliciesLayerDownRef(policiesDir) {
8198
8591
  "04_Business-flow.md"
8199
8592
  ];
8200
8593
  for (const fileName of targets) {
8201
- const targetPath = path27.join(policiesDir, fileName);
8202
- const text = await readSafe4(targetPath);
8594
+ const targetPath = path29.join(policiesDir, fileName);
8595
+ const text = await readSafe5(targetPath);
8203
8596
  if (text.length === 0) {
8204
8597
  continue;
8205
8598
  }
@@ -8221,11 +8614,11 @@ async function validatePoliciesLayerDownRef(policiesDir) {
8221
8614
  return issues;
8222
8615
  }
8223
8616
  async function collectLayeredEdgeData(entry, issues) {
8224
- const userStoriesText = await readSafe4(entry.userStoriesPath);
8225
- const acceptanceCriteriaText = await readSafe4(entry.acceptanceCriteriaPath);
8226
- const businessRulesText = await readSafe4(entry.businessRulesPath);
8227
- const examplesText = await readSafe4(entry.examplesPath);
8228
- const testCasesText = await readSafe4(entry.testCasesPath);
8617
+ const userStoriesText = await readSafe5(entry.userStoriesPath);
8618
+ const acceptanceCriteriaText = await readSafe5(entry.acceptanceCriteriaPath);
8619
+ const businessRulesText = await readSafe5(entry.businessRulesPath);
8620
+ const examplesText = await readSafe5(entry.examplesPath);
8621
+ const testCasesText = await readSafe5(entry.testCasesPath);
8229
8622
  const usIds = extractTableDefinitionIds(userStoriesText, /^US-\d{4}-\d{4}$/);
8230
8623
  const { definitions: acIds, refsByDefinition: acToUs } = extractTableDefinitionRefs(
8231
8624
  acceptanceCriteriaText,
@@ -8655,9 +9048,9 @@ function cloneGlobal2(re) {
8655
9048
  const flags = re.flags.includes("g") ? re.flags : `${re.flags}g`;
8656
9049
  return new RegExp(re.source, flags);
8657
9050
  }
8658
- async function readSafe4(target) {
9051
+ async function readSafe5(target) {
8659
9052
  try {
8660
- return await readFile19(target, "utf-8");
9053
+ return await readFile21(target, "utf-8");
8661
9054
  } catch {
8662
9055
  return "";
8663
9056
  }
@@ -8673,11 +9066,11 @@ async function exists11(target) {
8673
9066
 
8674
9067
  // src/core/validators/atddCodeTraceability.ts
8675
9068
  import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
8676
- import path29 from "path";
9069
+ import path31 from "path";
8677
9070
 
8678
9071
  // src/core/atddTraceability.ts
8679
- import { readFile as readFile20 } from "fs/promises";
8680
- import path28 from "path";
9072
+ import { readFile as readFile22 } from "fs/promises";
9073
+ import path30 from "path";
8681
9074
  var US_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):US-(\d{4}(?:-\d{4})?)\b/g;
8682
9075
  var TC_TEST_ANNOTATION_RE = /\bQFAI:SPEC-(\d{4}):TC-(\d{4}(?:-\d{4})?)\b/g;
8683
9076
  var API_TEST_ANNOTATION_RE = /\bQFAI:CON-API-(\d+)\b/g;
@@ -8688,15 +9081,15 @@ var TEST_FILE_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts,feature,md,markdown}";
8688
9081
  async function evaluateAtddCodeTraceability(root, config) {
8689
9082
  const specsRoot = resolvePath(root, config, "specsDir");
8690
9083
  const contractsRoot = resolvePath(root, config, "contractsDir");
8691
- const contractsApiRoot = path28.join(contractsRoot, "api");
9084
+ const contractsApiRoot = path30.join(contractsRoot, "api");
8692
9085
  const [specRefs, apiContractIds] = await Promise.all([
8693
9086
  collectSpecRefs(specsRoot),
8694
9087
  collectApiContractIds(contractsApiRoot)
8695
9088
  ]);
8696
9089
  const testsRoot = resolvePath(root, config, "testsDir");
8697
- const e2eRoot = path28.join(testsRoot, "e2e");
8698
- const apiRoot = path28.join(testsRoot, "api");
8699
- const integrationRoot = path28.join(testsRoot, "integration");
9090
+ const e2eRoot = path30.join(testsRoot, "e2e");
9091
+ const apiRoot = path30.join(testsRoot, "api");
9092
+ const integrationRoot = path30.join(testsRoot, "integration");
8700
9093
  const scanGlobs = buildAtddTestGlobs(root, testsRoot);
8701
9094
  const scanResult = await collectTestFiles(root, scanGlobs);
8702
9095
  const usRefs = /* @__PURE__ */ new Map();
@@ -8717,7 +9110,7 @@ async function evaluateAtddCodeTraceability(root, config) {
8717
9110
  if (!kind) {
8718
9111
  continue;
8719
9112
  }
8720
- const text = await readSafe5(file);
9113
+ const text = await readSafe6(file);
8721
9114
  const usAnnotations = extractSpecScopedAnnotations(text, US_TEST_ANNOTATION_RE);
8722
9115
  const tcAnnotations = extractSpecScopedAnnotations(text, TC_TEST_ANNOTATION_RE);
8723
9116
  const apiAnnotations = extractApiContractAnnotations(text);
@@ -8798,8 +9191,8 @@ async function collectSpecRefs(specsRoot) {
8798
9191
  const tc = /* @__PURE__ */ new Map();
8799
9192
  for (const entry of entries) {
8800
9193
  const [usText, tcText] = await Promise.all([
8801
- readSafe5(entry.userStoriesPath),
8802
- readSafe5(entry.testCasesPath)
9194
+ readSafe6(entry.userStoriesPath),
9195
+ readSafe6(entry.testCasesPath)
8803
9196
  ]);
8804
9197
  const usIds = collectShortIds(usText, "US");
8805
9198
  const tcIds = collectShortIds(tcText, "TC");
@@ -8816,7 +9209,7 @@ async function collectApiContractIds(apiRoot) {
8816
9209
  const files = await collectApiContractFiles(apiRoot);
8817
9210
  const ids = /* @__PURE__ */ new Set();
8818
9211
  for (const file of files) {
8819
- const text = await readSafe5(file);
9212
+ const text = await readSafe6(file);
8820
9213
  const declared = extractDeclaredContractIds(text);
8821
9214
  for (const id of declared) {
8822
9215
  const normalized = id.toUpperCase();
@@ -8841,8 +9234,8 @@ function collectShortIds(text, prefix) {
8841
9234
  return ids;
8842
9235
  }
8843
9236
  function buildAtddTestGlobs(root, testsRoot) {
8844
- const relativeTestsRoot = path28.relative(root, testsRoot);
8845
- const isInsideRoot = relativeTestsRoot.length === 0 || !relativeTestsRoot.startsWith("..") && !path28.isAbsolute(relativeTestsRoot);
9237
+ const relativeTestsRoot = path30.relative(root, testsRoot);
9238
+ const isInsideRoot = relativeTestsRoot.length === 0 || !relativeTestsRoot.startsWith("..") && !path30.isAbsolute(relativeTestsRoot);
8846
9239
  const base = isInsideRoot ? toPosixPath2(relativeTestsRoot.length === 0 ? "." : relativeTestsRoot) : toPosixPath2(testsRoot);
8847
9240
  const normalizedBase = base.replace(/\/+$/, "");
8848
9241
  return [
@@ -8871,11 +9264,11 @@ function resolveTestKind(filePath, roots) {
8871
9264
  return null;
8872
9265
  }
8873
9266
  function isWithinPath(base, target) {
8874
- const relative = path28.relative(base, target);
9267
+ const relative = path30.relative(base, target);
8875
9268
  if (relative === "") {
8876
9269
  return true;
8877
9270
  }
8878
- return !relative.startsWith("..") && !path28.isAbsolute(relative);
9271
+ return !relative.startsWith("..") && !path30.isAbsolute(relative);
8879
9272
  }
8880
9273
  function extractSpecScopedAnnotations(text, pattern) {
8881
9274
  const refs = [];
@@ -8909,22 +9302,22 @@ function extractApiContractAnnotations(text) {
8909
9302
  function recordSpecRef(refs, specNumber, id, file) {
8910
9303
  const bySpec = refs.get(specNumber) ?? /* @__PURE__ */ new Map();
8911
9304
  const files = bySpec.get(id) ?? /* @__PURE__ */ new Set();
8912
- files.add(path28.normalize(file));
9305
+ files.add(path30.normalize(file));
8913
9306
  bySpec.set(id, files);
8914
9307
  refs.set(specNumber, bySpec);
8915
9308
  }
8916
9309
  function recordApiRef(refs, id, file) {
8917
9310
  const files = refs.get(id) ?? /* @__PURE__ */ new Set();
8918
- files.add(path28.normalize(file));
9311
+ files.add(path30.normalize(file));
8919
9312
  refs.set(id, files);
8920
9313
  }
8921
9314
  function recordForbidden(refs, file, id) {
8922
- const files = refs.get(path28.normalize(file)) ?? /* @__PURE__ */ new Set();
9315
+ const files = refs.get(path30.normalize(file)) ?? /* @__PURE__ */ new Set();
8923
9316
  files.add(id);
8924
- refs.set(path28.normalize(file), files);
9317
+ refs.set(path30.normalize(file), files);
8925
9318
  }
8926
9319
  function pushUnknown(unknown, dedup, file, token, kind) {
8927
- const normalizedFile = path28.normalize(file);
9320
+ const normalizedFile = path30.normalize(file);
8928
9321
  const key = `${kind}|${normalizedFile}|${token}`;
8929
9322
  if (dedup.has(key)) {
8930
9323
  return;
@@ -9007,9 +9400,9 @@ function cloneGlobal3(pattern) {
9007
9400
  function toPosixPath2(value) {
9008
9401
  return value.replace(/\\/g, "/");
9009
9402
  }
9010
- async function readSafe5(target) {
9403
+ async function readSafe6(target) {
9011
9404
  try {
9012
- return await readFile20(target, "utf-8");
9405
+ return await readFile22(target, "utf-8");
9013
9406
  } catch {
9014
9407
  return "";
9015
9408
  }
@@ -9098,7 +9491,7 @@ async function validateAtddCodeTraceability(root, config) {
9098
9491
  "QFAI-ATDD-901",
9099
9492
  `atdd-traceability report \u306E\u51FA\u529B\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${formatError7(error2)}`,
9100
9493
  "warning",
9101
- path29.join(resolvePath(root, config, "outDir"), "atdd-traceability"),
9494
+ path31.join(resolvePath(root, config, "outDir"), "atdd-traceability"),
9102
9495
  "atddCodeTraceability.report"
9103
9496
  )
9104
9497
  );
@@ -9129,352 +9522,137 @@ function buildUnknownIssues(unknown) {
9129
9522
  "error",
9130
9523
  entry.file,
9131
9524
  "atddCodeTraceability.unknown.us",
9132
- refs,
9133
- "change",
9134
- "spec \u5074\u306B US \u3092\u5B9A\u7FA9\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9135
- );
9136
- }
9137
- if (entry.kind === "tc") {
9138
- return issue(
9139
- "QFAI-ATDD-102",
9140
- `\u672A\u5B9A\u7FA9\u306E TC \u53C2\u7167\u3092\u691C\u51FA\u3057\u307E\u3057\u305F: ${refs.join(", ")}`,
9141
- "error",
9142
- entry.file,
9143
- "atddCodeTraceability.unknown.tc",
9144
- refs,
9145
- "change",
9146
- "spec \u5074\u306B TC \u3092\u5B9A\u7FA9\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9147
- );
9148
- }
9149
- return issue(
9150
- "QFAI-ATDD-103",
9151
- `\u672A\u5B9A\u7FA9\u306E CON-API \u53C2\u7167\u3092\u691C\u51FA\u3057\u307E\u3057\u305F: ${refs.join(", ")}`,
9152
- "error",
9153
- entry.file,
9154
- "atddCodeTraceability.unknown.conApi",
9155
- refs,
9156
- "change",
9157
- "contracts/api \u306B CON-API \u3092\u5BA3\u8A00\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9158
- );
9159
- }).sort((left, right) => {
9160
- if (left.code !== right.code) {
9161
- return left.code.localeCompare(right.code);
9162
- }
9163
- return (left.file ?? "").localeCompare(right.file ?? "");
9164
- });
9165
- }
9166
- async function writeAtddTraceabilityReport(root, config, result) {
9167
- const outputDir = path29.join(resolvePath(root, config, "outDir"), "atdd-traceability");
9168
- await mkdir5(outputDir, { recursive: true });
9169
- const summary = {
9170
- missing: {
9171
- us: result.missing.us,
9172
- tc: result.missing.tc,
9173
- conApi: result.missing.conApi
9174
- },
9175
- unknown: result.unknown.map((entry) => ({
9176
- file: entry.file,
9177
- token: entry.token
9178
- })),
9179
- forbidden: {
9180
- tcInApi: result.forbidden.tcInApi,
9181
- tcInE2e: result.forbidden.tcInE2e
9182
- },
9183
- scan: {
9184
- matchedFileCount: result.scan.matchedFileCount,
9185
- truncated: result.scan.truncated,
9186
- limit: result.scan.limit,
9187
- globs: result.scan.globs
9188
- }
9189
- };
9190
- await writeFile4(
9191
- path29.join(outputDir, "summary.json"),
9192
- `${JSON.stringify(summary, null, 2)}
9193
- `,
9194
- "utf-8"
9195
- );
9196
- await writeFile4(path29.join(outputDir, "summary.md"), buildSummaryMarkdown(summary), "utf-8");
9197
- }
9198
- function buildSummaryMarkdown(summary) {
9199
- const lines = [];
9200
- lines.push("# ATDD Traceability Summary");
9201
- lines.push("");
9202
- lines.push("## Missing Coverage");
9203
- lines.push("");
9204
- lines.push("- US -> E2E");
9205
- lines.push(...toList(summary.missing.us));
9206
- lines.push("- TC -> Integration");
9207
- lines.push(...toList(summary.missing.tc));
9208
- lines.push("- CON-API -> API");
9209
- lines.push(...toList(summary.missing.conApi));
9210
- lines.push("");
9211
- lines.push("## Unknown References");
9212
- lines.push("");
9213
- if (summary.unknown.length === 0) {
9214
- lines.push("- \u306A\u3057");
9215
- } else {
9216
- for (const item of summary.unknown) {
9217
- lines.push(`- ${item.token} (${item.file})`);
9218
- }
9219
- }
9220
- lines.push("");
9221
- lines.push("## Forbidden References");
9222
- lines.push("");
9223
- lines.push("- TC in tests/api/**");
9224
- lines.push(...toFileIdList(summary.forbidden.tcInApi));
9225
- lines.push("- TC in tests/e2e/**");
9226
- lines.push(...toFileIdList(summary.forbidden.tcInE2e));
9227
- lines.push("");
9228
- lines.push("## Scan");
9229
- lines.push("");
9230
- lines.push(`- matchedFileCount: ${summary.scan.matchedFileCount}`);
9231
- lines.push(`- truncated: ${summary.scan.truncated ? "true" : "false"}`);
9232
- lines.push(`- limit: ${summary.scan.limit}`);
9233
- lines.push("- globs:");
9234
- for (const glob of summary.scan.globs) {
9235
- lines.push(` - ${glob}`);
9236
- }
9237
- lines.push("");
9238
- return `${lines.join("\n")}
9239
- `;
9240
- }
9241
- function toList(items) {
9242
- if (items.length === 0) {
9243
- return [" - \u306A\u3057"];
9244
- }
9245
- return items.map((item) => ` - ${item}`);
9246
- }
9247
- function toFileIdList(items) {
9248
- if (items.length === 0) {
9249
- return [" - \u306A\u3057"];
9250
- }
9251
- return items.map((item) => ` - ${item.file}: ${item.ids.join(", ")}`);
9252
- }
9253
- function formatError7(error2) {
9254
- if (error2 instanceof Error) {
9255
- return error2.message;
9256
- }
9257
- return String(error2);
9258
- }
9259
-
9260
- // src/core/validators/discussionPack.ts
9261
- import { readFile as readFile22, stat as stat4 } from "fs/promises";
9262
- import path31 from "path";
9263
-
9264
- // src/core/discussionPack.ts
9265
- import { readFile as readFile21, stat as stat3 } from "fs/promises";
9266
- import path30 from "path";
9267
- var REQUIRED_DISCUSSION_PACK_FILES = [
9268
- "01_Context.md",
9269
- "02_Inception-Deck.md",
9270
- "03_Story-Workshop.md",
9271
- "04_Sources.md",
9272
- "05_Scope.md",
9273
- "06_REQ.md",
9274
- "07_NFR.md",
9275
- "08_Glossary.md",
9276
- "09_Constraints.md",
9277
- "10_Policy.md",
9278
- "11_OQ-Register.md",
9279
- "12_OQ-Resolution-Log.md",
9280
- "13_Deferred.md",
9281
- "14_Review-Request.md",
9282
- "99_delta.md"
9283
- ];
9284
- var PLACEHOLDER_LINE_RE = /^(?:[-*]\s*)?(?:tbd|todo|none|n\/a|placeholder|\(placeholder\)|to be defined|to be updated|<[^>]+>)\.?$/i;
9285
- async function inspectLatestDiscussionPack(discussionRoot) {
9286
- const packs = await findPacks(discussionRoot, "discussion");
9287
- const legacyPackNames = packs.filter((pack) => pack.isLegacy).map((pack) => pack.name).sort((left, right) => left.localeCompare(right));
9288
- const dangerousPackNames = packs.filter((pack) => pack.isDangerous).map((pack) => pack.name).sort((left, right) => left.localeCompare(right));
9289
- const latest = latestPack(packs);
9290
- const latestPackDir = latest?.path ?? null;
9291
- const latestPackName = latest?.name ?? null;
9292
- if (!latestPackDir) {
9293
- return {
9294
- discussionRoot,
9295
- latestPackDir: null,
9296
- latestPackName,
9297
- legacyPackNames,
9298
- dangerousPackNames,
9299
- missingFiles: [...REQUIRED_DISCUSSION_PACK_FILES],
9300
- incompleteFiles: [],
9301
- blockingOqIds: [],
9302
- deferredWithoutDetails: []
9303
- };
9304
- }
9305
- const missingFiles = [];
9306
- const incompleteFiles = [];
9307
- let blockingOqIds = [];
9308
- let deferredWithoutDetails = [];
9309
- for (const fileName of REQUIRED_DISCUSSION_PACK_FILES) {
9310
- const target = path30.join(latestPackDir, fileName);
9311
- const content = await readSafe6(target);
9312
- if (content === null) {
9313
- missingFiles.push(fileName);
9314
- continue;
9315
- }
9316
- if (isDiscussionPackFileIncomplete(content)) {
9317
- incompleteFiles.push(fileName);
9318
- }
9319
- if (fileName === "11_OQ-Register.md") {
9320
- blockingOqIds = extractBlockingOqIds(content);
9321
- }
9322
- }
9323
- const oqRegisterContent = await readSafe6(path30.join(latestPackDir, "11_OQ-Register.md"));
9324
- const deferredContent = await readSafe6(path30.join(latestPackDir, "13_Deferred.md"));
9325
- if (oqRegisterContent !== null && deferredContent !== null) {
9326
- deferredWithoutDetails = extractDeferredWithoutDetails(oqRegisterContent, deferredContent);
9327
- }
9328
- return {
9329
- discussionRoot,
9330
- latestPackDir,
9331
- latestPackName,
9332
- legacyPackNames,
9333
- dangerousPackNames,
9334
- missingFiles,
9335
- incompleteFiles,
9336
- blockingOqIds,
9337
- deferredWithoutDetails
9338
- };
9339
- }
9340
- async function findLatestDiscussionPackDir(discussionRoot) {
9341
- const packs = await findPacks(discussionRoot, "discussion");
9342
- return latestPack(packs)?.path ?? null;
9343
- }
9344
- function isDiscussionPackFileIncomplete(text) {
9345
- const normalized = text.replace(/\r\n/g, "\n").trim();
9346
- if (normalized.length < 100) {
9347
- return true;
9348
- }
9349
- const contentLines = normalized.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).filter((line) => !line.startsWith("#")).filter((line) => !/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?$/.test(line));
9350
- if (contentLines.length === 0) {
9351
- return true;
9352
- }
9353
- if (contentLines.every((line) => isPlaceholderLine(line))) {
9354
- return true;
9355
- }
9356
- return false;
9357
- }
9358
- function isPlaceholderLine(line) {
9359
- const plain = line.replace(/[`*_~]/g, "").replace(/\|/g, " ").replace(/\s+/g, " ").trim();
9360
- return PLACEHOLDER_LINE_RE.test(plain);
9361
- }
9362
- function extractBlockingOqIds(text) {
9363
- const lines = text.replace(/\r\n/g, "\n").split("\n");
9364
- const oqStates = /* @__PURE__ */ new Map();
9365
- let currentId = null;
9366
- for (const line of lines) {
9367
- const idMatch = /\b(OQ-\d+)\b/i.exec(line);
9368
- if (idMatch?.[1]) {
9369
- currentId = idMatch[1].toUpperCase();
9370
- }
9371
- if (currentId !== null) {
9372
- const state = oqStates.get(currentId) ?? { disposition: null };
9373
- const disposition = /^\s*-\s*Disposition\s*:\s*([^\s#]+)/i.exec(line)?.[1] ?? null;
9374
- if (disposition) {
9375
- state.disposition = disposition.toLowerCase();
9376
- }
9377
- oqStates.set(currentId, state);
9525
+ refs,
9526
+ "change",
9527
+ "spec \u5074\u306B US \u3092\u5B9A\u7FA9\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9528
+ );
9378
9529
  }
9379
- if (line.trim().startsWith("|")) {
9380
- const cells = parseTableCells(line);
9381
- for (const cell of cells) {
9382
- const oqMatch = /\b(OQ-\d+)\b/i.exec(cell);
9383
- if (oqMatch?.[1]) {
9384
- currentId = oqMatch[1].toUpperCase();
9385
- const existingState = oqStates.get(currentId) ?? { disposition: null };
9386
- oqStates.set(currentId, existingState);
9387
- }
9388
- }
9530
+ if (entry.kind === "tc") {
9531
+ return issue(
9532
+ "QFAI-ATDD-102",
9533
+ `\u672A\u5B9A\u7FA9\u306E TC \u53C2\u7167\u3092\u691C\u51FA\u3057\u307E\u3057\u305F: ${refs.join(", ")}`,
9534
+ "error",
9535
+ entry.file,
9536
+ "atddCodeTraceability.unknown.tc",
9537
+ refs,
9538
+ "change",
9539
+ "spec \u5074\u306B TC \u3092\u5B9A\u7FA9\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9540
+ );
9389
9541
  }
9390
- }
9391
- const tableOqs = extractOqTableRows(text);
9392
- for (const row of tableOqs) {
9393
- if (row.id && row.disposition) {
9394
- const existing = oqStates.get(row.id) ?? { disposition: null };
9395
- existing.disposition = row.disposition;
9396
- oqStates.set(row.id, existing);
9542
+ return issue(
9543
+ "QFAI-ATDD-103",
9544
+ `\u672A\u5B9A\u7FA9\u306E CON-API \u53C2\u7167\u3092\u691C\u51FA\u3057\u307E\u3057\u305F: ${refs.join(", ")}`,
9545
+ "error",
9546
+ entry.file,
9547
+ "atddCodeTraceability.unknown.conApi",
9548
+ refs,
9549
+ "change",
9550
+ "contracts/api \u306B CON-API \u3092\u5BA3\u8A00\u3059\u308B\u304B\u3001\u30C6\u30B9\u30C8\u6CE8\u91C8\u3092\u6B63\u3057\u3044 ID \u3078\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
9551
+ );
9552
+ }).sort((left, right) => {
9553
+ if (left.code !== right.code) {
9554
+ return left.code.localeCompare(right.code);
9397
9555
  }
9398
- }
9399
- const blocking = Array.from(oqStates.entries()).filter(([, state]) => state.disposition === "open").map(([id]) => id).sort((left, right) => left.localeCompare(right));
9400
- return blocking;
9556
+ return (left.file ?? "").localeCompare(right.file ?? "");
9557
+ });
9401
9558
  }
9402
- function extractOqTableRows(text) {
9403
- const lines = text.replace(/\r\n/g, "\n").split("\n");
9404
- const results = [];
9405
- let headerIndex = -1;
9406
- let oqIdCol = -1;
9407
- let dispositionCol = -1;
9408
- for (let i = 0; i < lines.length - 1; i++) {
9409
- const line = lines[i] ?? "";
9410
- if (!line.includes("|")) continue;
9411
- const cells = parseTableCells(line);
9412
- const normalizedCells = cells.map((c) => c.toLowerCase().replace(/[^a-z0-9-]/g, ""));
9413
- oqIdCol = normalizedCells.findIndex((c) => c === "oq-id" || c === "oqid");
9414
- dispositionCol = normalizedCells.findIndex((c) => c === "disposition");
9415
- if (oqIdCol >= 0 && dispositionCol >= 0) {
9416
- const sepLine = lines[i + 1] ?? "";
9417
- const sepCells = parseTableCells(sepLine);
9418
- if (sepCells.length === cells.length && sepCells.every((c) => /^:?-{3,}:?$/.test(c.trim()))) {
9419
- headerIndex = i;
9420
- break;
9421
- }
9559
+ async function writeAtddTraceabilityReport(root, config, result) {
9560
+ const outputDir = path31.join(resolvePath(root, config, "outDir"), "atdd-traceability");
9561
+ await mkdir5(outputDir, { recursive: true });
9562
+ const summary = {
9563
+ missing: {
9564
+ us: result.missing.us,
9565
+ tc: result.missing.tc,
9566
+ conApi: result.missing.conApi
9567
+ },
9568
+ unknown: result.unknown.map((entry) => ({
9569
+ file: entry.file,
9570
+ token: entry.token
9571
+ })),
9572
+ forbidden: {
9573
+ tcInApi: result.forbidden.tcInApi,
9574
+ tcInE2e: result.forbidden.tcInE2e
9575
+ },
9576
+ scan: {
9577
+ matchedFileCount: result.scan.matchedFileCount,
9578
+ truncated: result.scan.truncated,
9579
+ limit: result.scan.limit,
9580
+ globs: result.scan.globs
9422
9581
  }
9423
- }
9424
- if (headerIndex < 0) return results;
9425
- for (let i = headerIndex + 2; i < lines.length; i++) {
9426
- const line = lines[i] ?? "";
9427
- if (!line.trim().startsWith("|")) break;
9428
- const cells = parseTableCells(line);
9429
- const oqIdRaw = cells[oqIdCol] ?? "";
9430
- const dispositionRaw = cells[dispositionCol] ?? "";
9431
- const oqMatch = /\b(OQ-\d+)\b/i.exec(oqIdRaw);
9432
- if (oqMatch?.[1]) {
9433
- results.push({
9434
- id: oqMatch[1].toUpperCase(),
9435
- disposition: dispositionRaw.trim().toLowerCase()
9436
- });
9582
+ };
9583
+ await writeFile4(
9584
+ path31.join(outputDir, "summary.json"),
9585
+ `${JSON.stringify(summary, null, 2)}
9586
+ `,
9587
+ "utf-8"
9588
+ );
9589
+ await writeFile4(path31.join(outputDir, "summary.md"), buildSummaryMarkdown(summary), "utf-8");
9590
+ }
9591
+ function buildSummaryMarkdown(summary) {
9592
+ const lines = [];
9593
+ lines.push("# ATDD Traceability Summary");
9594
+ lines.push("");
9595
+ lines.push("## Missing Coverage");
9596
+ lines.push("");
9597
+ lines.push("- US -> E2E");
9598
+ lines.push(...toList(summary.missing.us));
9599
+ lines.push("- TC -> Integration");
9600
+ lines.push(...toList(summary.missing.tc));
9601
+ lines.push("- CON-API -> API");
9602
+ lines.push(...toList(summary.missing.conApi));
9603
+ lines.push("");
9604
+ lines.push("## Unknown References");
9605
+ lines.push("");
9606
+ if (summary.unknown.length === 0) {
9607
+ lines.push("- \u306A\u3057");
9608
+ } else {
9609
+ for (const item of summary.unknown) {
9610
+ lines.push(`- ${item.token} (${item.file})`);
9437
9611
  }
9438
9612
  }
9439
- return results;
9440
- }
9441
- function extractDeferredWithoutDetails(oqRegisterText, deferredText) {
9442
- const registerRows = extractOqTableRows(oqRegisterText);
9443
- const deferredIds = registerRows.filter((row) => row.disposition === "deferred").map((row) => row.id);
9444
- if (deferredIds.length === 0) return [];
9445
- const deferredDetailSet = extractAllOqIds(deferredText);
9446
- return deferredIds.filter((id) => !deferredDetailSet.has(id));
9613
+ lines.push("");
9614
+ lines.push("## Forbidden References");
9615
+ lines.push("");
9616
+ lines.push("- TC in tests/api/**");
9617
+ lines.push(...toFileIdList(summary.forbidden.tcInApi));
9618
+ lines.push("- TC in tests/e2e/**");
9619
+ lines.push(...toFileIdList(summary.forbidden.tcInE2e));
9620
+ lines.push("");
9621
+ lines.push("## Scan");
9622
+ lines.push("");
9623
+ lines.push(`- matchedFileCount: ${summary.scan.matchedFileCount}`);
9624
+ lines.push(`- truncated: ${summary.scan.truncated ? "true" : "false"}`);
9625
+ lines.push(`- limit: ${summary.scan.limit}`);
9626
+ lines.push("- globs:");
9627
+ for (const glob of summary.scan.globs) {
9628
+ lines.push(` - ${glob}`);
9629
+ }
9630
+ lines.push("");
9631
+ return `${lines.join("\n")}
9632
+ `;
9447
9633
  }
9448
- function extractAllOqIds(text) {
9449
- const ids = /* @__PURE__ */ new Set();
9450
- const re = /\b(OQ-\d+)\b/gi;
9451
- let match;
9452
- while ((match = re.exec(text)) !== null) {
9453
- if (match[1]) {
9454
- ids.add(match[1].toUpperCase());
9455
- }
9634
+ function toList(items) {
9635
+ if (items.length === 0) {
9636
+ return [" - \u306A\u3057"];
9456
9637
  }
9457
- return ids;
9638
+ return items.map((item) => ` - ${item}`);
9458
9639
  }
9459
- function parseTableCells(line) {
9460
- const trimmed = line.trim();
9461
- if (!trimmed.startsWith("|")) return [];
9462
- const normalized = trimmed.replace(/^\|/, "").replace(/\|$/, "");
9463
- return normalized.split("|").map((cell) => cell.trim());
9640
+ function toFileIdList(items) {
9641
+ if (items.length === 0) {
9642
+ return [" - \u306A\u3057"];
9643
+ }
9644
+ return items.map((item) => ` - ${item.file}: ${item.ids.join(", ")}`);
9464
9645
  }
9465
- async function readSafe6(filePath) {
9466
- try {
9467
- const stats = await stat3(filePath);
9468
- if (!stats.isFile()) {
9469
- return null;
9470
- }
9471
- return await readFile21(filePath, "utf-8");
9472
- } catch {
9473
- return null;
9646
+ function formatError7(error2) {
9647
+ if (error2 instanceof Error) {
9648
+ return error2.message;
9474
9649
  }
9650
+ return String(error2);
9475
9651
  }
9476
9652
 
9477
9653
  // src/core/validators/discussionPack.ts
9654
+ import { readFile as readFile23, stat as stat4 } from "fs/promises";
9655
+ import path32 from "path";
9478
9656
  async function validateDiscussionPackReadiness(root, config) {
9479
9657
  const discussionRoot = resolvePath(root, config, "discussionDir");
9480
9658
  const readiness = await inspectLatestDiscussionPack(discussionRoot);
@@ -9566,7 +9744,7 @@ async function validateDiscussionPackReadiness(root, config) {
9566
9744
  );
9567
9745
  }
9568
9746
  if (readiness.blockingOqIds.length > 0) {
9569
- const oqPath = path31.join(readiness.latestPackDir, "11_OQ-Register.md");
9747
+ const oqPath = path32.join(readiness.latestPackDir, "11_OQ-Register.md");
9570
9748
  issues.push(
9571
9749
  issue(
9572
9750
  "QFAI-DPACK-004",
@@ -9581,7 +9759,7 @@ async function validateDiscussionPackReadiness(root, config) {
9581
9759
  );
9582
9760
  }
9583
9761
  if (readiness.deferredWithoutDetails.length > 0) {
9584
- const deferredPath = path31.join(readiness.latestPackDir, "13_Deferred.md");
9762
+ const deferredPath = path32.join(readiness.latestPackDir, "13_Deferred.md");
9585
9763
  issues.push(
9586
9764
  issue(
9587
9765
  "QFAI-DPACK-007",
@@ -9595,7 +9773,7 @@ async function validateDiscussionPackReadiness(root, config) {
9595
9773
  )
9596
9774
  );
9597
9775
  }
9598
- const storyWorkshopPath = path31.join(readiness.latestPackDir, "03_Story-Workshop.md");
9776
+ const storyWorkshopPath = path32.join(readiness.latestPackDir, "03_Story-Workshop.md");
9599
9777
  const storyWorkshopText = await readSafe7(storyWorkshopPath);
9600
9778
  if (storyWorkshopText !== null && !containsMermaidBlock(storyWorkshopText)) {
9601
9779
  issues.push(
@@ -9622,28 +9800,28 @@ async function readSafe7(filePath) {
9622
9800
  if (!stats.isFile()) {
9623
9801
  return null;
9624
9802
  }
9625
- return await readFile22(filePath, "utf-8");
9803
+ return await readFile23(filePath, "utf-8");
9626
9804
  } catch {
9627
9805
  return null;
9628
9806
  }
9629
9807
  }
9630
9808
 
9631
9809
  // src/core/validators/discussionVisuals.ts
9632
- import { readFile as readFile23 } from "fs/promises";
9633
- import path32 from "path";
9810
+ import { readFile as readFile24 } from "fs/promises";
9811
+ import path33 from "path";
9634
9812
  var MERMAID_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*mermaid\b/im;
9635
9813
  var SCREEN_MOCK_HEADING_RE = /^\s*#{1,6}\s*screen mock(?:\s*[-\u2014]+\s*fallback)?\s*\(html\+css\)\s*$/im;
9636
9814
  var HTML_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*html\b/im;
9637
9815
  var CSS_FENCE_RE = /^\s*(?:`{3,}|~{3,})\s*css\b/im;
9638
9816
  var UI_HINT_RE = /\b(?:ui|ux|screen|screens|page|pages|layout|wireframe|mock|form|button|frontend|navigation)\b|画面|モック|遷移|フォーム|ボタン/i;
9639
9817
  async function validateDiscussionVisuals(root) {
9640
- const discussionRoot = path32.join(root, ".qfai", "discussion");
9818
+ const discussionRoot = path33.join(root, ".qfai", "discussion");
9641
9819
  const latestPackDir = await findLatestDiscussionPackDir(discussionRoot);
9642
9820
  if (!latestPackDir) {
9643
9821
  return [];
9644
9822
  }
9645
9823
  const issues = [];
9646
- const inceptionPath = path32.join(latestPackDir, "02_Inception-Deck.md");
9824
+ const inceptionPath = path33.join(latestPackDir, "02_Inception-Deck.md");
9647
9825
  const inceptionText = await readSafe8(inceptionPath);
9648
9826
  if (inceptionText !== null && !MERMAID_FENCE_RE.test(inceptionText)) {
9649
9827
  issues.push(
@@ -9659,7 +9837,7 @@ async function validateDiscussionVisuals(root) {
9659
9837
  )
9660
9838
  );
9661
9839
  }
9662
- const storyWorkshopPath = path32.join(latestPackDir, "03_Story-Workshop.md");
9840
+ const storyWorkshopPath = path33.join(latestPackDir, "03_Story-Workshop.md");
9663
9841
  const storyWorkshopText = await readSafe8(storyWorkshopPath);
9664
9842
  const storyWorkshopPlainText = storyWorkshopText === null ? null : stripFencedCodeBlocks(storyWorkshopText);
9665
9843
  if (storyWorkshopText === null || storyWorkshopPlainText === null) {
@@ -9723,14 +9901,14 @@ function escapeRegExp2(value) {
9723
9901
  }
9724
9902
  async function readSafe8(filePath) {
9725
9903
  try {
9726
- return await readFile23(filePath, "utf-8");
9904
+ return await readFile24(filePath, "utf-8");
9727
9905
  } catch {
9728
9906
  return null;
9729
9907
  }
9730
9908
  }
9731
9909
 
9732
9910
  // src/core/validators/densityHints.ts
9733
- import { readFile as readFile24 } from "fs/promises";
9911
+ import { readFile as readFile25 } from "fs/promises";
9734
9912
  var BR_ID_RE = /\bBR-[A-Za-z0-9-]+\b/g;
9735
9913
  var SCENARIO_RE = /^\s*Scenario(?:\s+Outline)?\s*:/gim;
9736
9914
  var TC_OR_CASE_RE = /\b(?:TC|CASE)-[A-Za-z0-9-]+\b/g;
@@ -9829,7 +10007,7 @@ function isSeparatorRow(line) {
9829
10007
  }
9830
10008
  async function readSafe9(filePath) {
9831
10009
  try {
9832
- return await readFile24(filePath, "utf-8");
10010
+ return await readFile25(filePath, "utf-8");
9833
10011
  } catch {
9834
10012
  return "";
9835
10013
  }
@@ -9837,11 +10015,11 @@ async function readSafe9(filePath) {
9837
10015
 
9838
10016
  // src/core/validators/importLite.ts
9839
10017
  import { readdir as readdir7 } from "fs/promises";
9840
- import path33 from "path";
10018
+ import path34 from "path";
9841
10019
 
9842
10020
  // src/core/validators/layerCoverage.ts
9843
10021
  import { mkdir as mkdir6, writeFile as writeFile5 } from "fs/promises";
9844
- import path34 from "path";
10022
+ import path35 from "path";
9845
10023
  var ID_PATTERNS = {
9846
10024
  us: /^US-\d{4}(?:-\d{4})?$/,
9847
10025
  ac: /^AC-\d{4}(?:-\d{4})?$/,
@@ -9862,7 +10040,7 @@ async function validateLayerCoverage(root, config) {
9862
10040
  if (layeredEntries.length === 0) {
9863
10041
  return issues;
9864
10042
  }
9865
- const coverageRoot = path34.join(resolvePath(root, config, "outDir"), "specs-coverage");
10043
+ const coverageRoot = path35.join(resolvePath(root, config, "outDir"), "specs-coverage");
9866
10044
  for (const entry of layeredEntries) {
9867
10045
  if (isV1421LayeredEntry(entry)) {
9868
10046
  const coverageResult = await validateV1421Coverage(entry);
@@ -9897,7 +10075,7 @@ async function validateLayerCoverage(root, config) {
9897
10075
  }
9898
10076
  async function validatePlanArtifacts(specsRoot, entries) {
9899
10077
  const issues = [];
9900
- const deprecatedPlanPath = path34.join(specsRoot, "plan.md");
10078
+ const deprecatedPlanPath = path35.join(specsRoot, "plan.md");
9901
10079
  if (await exists8(deprecatedPlanPath)) {
9902
10080
  issues.push(
9903
10081
  issue(
@@ -9913,14 +10091,14 @@ async function validatePlanArtifacts(specsRoot, entries) {
9913
10091
  );
9914
10092
  }
9915
10093
  for (const entry of entries) {
9916
- const planFileName = path34.basename(entry.planPath).toLowerCase();
10094
+ const planFileName = path35.basename(entry.planPath).toLowerCase();
9917
10095
  if (planFileName !== "10_plan.md") {
9918
10096
  continue;
9919
10097
  }
9920
10098
  if (!await exists8(entry.planPath)) {
9921
10099
  continue;
9922
10100
  }
9923
- const text = await readSafe(entry.planPath);
10101
+ const text = await readSafe2(entry.planPath);
9924
10102
  const headings = extractHeadings(text);
9925
10103
  const forbiddenHeadingGroups = [
9926
10104
  {
@@ -9979,10 +10157,10 @@ async function validatePlanArtifacts(specsRoot, entries) {
9979
10157
  }
9980
10158
  async function validateV1421Coverage(entry) {
9981
10159
  const [acText, brText, exText, tcText] = await Promise.all([
9982
- readSafe(entry.acceptanceCriteriaPath),
9983
- readSafe(entry.businessRulesPath),
9984
- readSafe(entry.examplesPath),
9985
- readSafe(entry.testCasesPath)
10160
+ readSafe2(entry.acceptanceCriteriaPath),
10161
+ readSafe2(entry.businessRulesPath),
10162
+ readSafe2(entry.examplesPath),
10163
+ readSafe2(entry.testCasesPath)
9986
10164
  ]);
9987
10165
  const acIds = parseAcceptanceCriteriaIds2(acText);
9988
10166
  const brToAcRefs = parseDefinitionRefs(brText, "BR", V1421_REFS.ac, {
@@ -10295,7 +10473,7 @@ function buildSignalRows(prefix, counts, target) {
10295
10473
  }
10296
10474
  async function writeCoverageReport(coverageRoot, specNumber, snapshot) {
10297
10475
  await mkdir6(coverageRoot, { recursive: true });
10298
- const reportPath = path34.join(coverageRoot, `spec-${specNumber}.md`);
10476
+ const reportPath = path35.join(coverageRoot, `spec-${specNumber}.md`);
10299
10477
  const lines = [];
10300
10478
  lines.push(`# Spec Coverage (spec-${specNumber})`);
10301
10479
  lines.push("");
@@ -10340,7 +10518,7 @@ function isV1421LayeredEntry(entry) {
10340
10518
  if (entry.layeredStyle === "v1421") {
10341
10519
  return true;
10342
10520
  }
10343
- return path34.extname(entry.examplesPath).toLowerCase() === ".md";
10521
+ return path35.extname(entry.examplesPath).toLowerCase() === ".md";
10344
10522
  }
10345
10523
  function extractHeadings(text) {
10346
10524
  const headings = [];
@@ -10413,8 +10591,8 @@ async function validateUsToAcCoverage(userStoriesPath, acceptanceCriteriaPath) {
10413
10591
  if (!await exists8(userStoriesPath) || !await exists8(acceptanceCriteriaPath)) {
10414
10592
  return [];
10415
10593
  }
10416
- const usItems = collectMarkdownItems(await readSafe(userStoriesPath), "US");
10417
- const acItems = collectMarkdownItems(await readSafe(acceptanceCriteriaPath), "AC");
10594
+ const usItems = collectMarkdownItems(await readSafe2(userStoriesPath), "US");
10595
+ const acItems = collectMarkdownItems(await readSafe2(acceptanceCriteriaPath), "AC");
10418
10596
  const usIds = usItems.map((item) => item.id).filter((id) => ID_PATTERNS.us.test(id));
10419
10597
  if (usIds.length === 0) {
10420
10598
  return [];
@@ -10443,8 +10621,8 @@ async function validateAcToBrCoverage(acceptanceCriteriaPath, businessRulesPath)
10443
10621
  if (!await exists8(acceptanceCriteriaPath) || !await exists8(businessRulesPath)) {
10444
10622
  return [];
10445
10623
  }
10446
- const acItems = collectMarkdownItems(await readSafe(acceptanceCriteriaPath), "AC");
10447
- const brItems = collectMarkdownItems(await readSafe(businessRulesPath), "BR");
10624
+ const acItems = collectMarkdownItems(await readSafe2(acceptanceCriteriaPath), "AC");
10625
+ const brItems = collectMarkdownItems(await readSafe2(businessRulesPath), "BR");
10448
10626
  const acIds = acItems.map((item) => item.id).filter((id) => ID_PATTERNS.ac.test(id));
10449
10627
  if (acIds.length === 0) {
10450
10628
  return [];
@@ -10473,8 +10651,8 @@ async function validateBrToExCoverage(businessRulesPath, examplesPath) {
10473
10651
  if (!await exists8(businessRulesPath) || !await exists8(examplesPath)) {
10474
10652
  return [];
10475
10653
  }
10476
- const brItems = collectMarkdownItems(await readSafe(businessRulesPath), "BR");
10477
- const exItems = collectScenarioItems(await readSafe(examplesPath));
10654
+ const brItems = collectMarkdownItems(await readSafe2(businessRulesPath), "BR");
10655
+ const exItems = collectScenarioItems(await readSafe2(examplesPath));
10478
10656
  const brIds = brItems.map((item) => item.id).filter((id) => ID_PATTERNS.br.test(id));
10479
10657
  if (brIds.length === 0) {
10480
10658
  return [];
@@ -10503,8 +10681,8 @@ async function validateExToTcCoverage(examplesPath, testCasesPath) {
10503
10681
  if (!await exists8(examplesPath) || !await exists8(testCasesPath)) {
10504
10682
  return [];
10505
10683
  }
10506
- const exItems = collectScenarioItems(await readSafe(examplesPath));
10507
- const tcItems = collectMarkdownItems(await readSafe(testCasesPath), "TC");
10684
+ const exItems = collectScenarioItems(await readSafe2(examplesPath));
10685
+ const tcItems = collectMarkdownItems(await readSafe2(testCasesPath), "TC");
10508
10686
  const exIds = exItems.map((item) => item.exId.replace(/^@/, "")).filter((id) => ID_PATTERNS.ex.test(id));
10509
10687
  if (exIds.length === 0) {
10510
10688
  return [];
@@ -10532,7 +10710,7 @@ async function validateExToTcCoverage(examplesPath, testCasesPath) {
10532
10710
 
10533
10711
  // src/core/validators/layeredTraceability.ts
10534
10712
  import { readdir as readdir8 } from "fs/promises";
10535
- import path35 from "path";
10713
+ import path36 from "path";
10536
10714
  var POLICIES_FILES = [
10537
10715
  "01_Objective.md",
10538
10716
  "02_Initiative.md",
@@ -10581,7 +10759,7 @@ async function validateLayeredTraceability(root, config) {
10581
10759
  }
10582
10760
  const issues = [];
10583
10761
  if (layeredV1417Entries.length > 0) {
10584
- const policiesDir = layeredV1417Entries[0]?.sharedDir ?? path35.join(specsRoot, "_policies");
10762
+ const policiesDir = layeredV1417Entries[0]?.sharedDir ?? path36.join(specsRoot, "_policies");
10585
10763
  issues.push(...await validatePoliciesDownstreamReferences(policiesDir));
10586
10764
  for (const entry of layeredV1417Entries) {
10587
10765
  issues.push(...await validateSpecRootParent(entry));
@@ -10622,7 +10800,7 @@ async function validateLayeredTraceability(root, config) {
10622
10800
  }
10623
10801
  }
10624
10802
  if (layeredV1421Entries.length > 0) {
10625
- const policiesDir = layeredV1421Entries[0]?.sharedDir ?? path35.join(specsRoot, "_policies");
10803
+ const policiesDir = layeredV1421Entries[0]?.sharedDir ?? path36.join(specsRoot, "_policies");
10626
10804
  issues.push(...await validatePoliciesScopeForV1421(policiesDir));
10627
10805
  for (const entry of layeredV1421Entries) {
10628
10806
  issues.push(...await validateDownstreamRefsForV1421(entry));
@@ -10633,8 +10811,8 @@ async function validateLayeredTraceability(root, config) {
10633
10811
  async function validatePoliciesDownstreamReferences(policiesDir) {
10634
10812
  const issues = [];
10635
10813
  for (const fileName of POLICIES_FILES) {
10636
- const filePath = path35.join(policiesDir, fileName);
10637
- const text = await readSafe(filePath);
10814
+ const filePath = path36.join(policiesDir, fileName);
10815
+ const text = await readSafe2(filePath);
10638
10816
  if (text.trim().length === 0) {
10639
10817
  continue;
10640
10818
  }
@@ -10662,7 +10840,7 @@ async function validatePoliciesScopeForV1421(policiesDir) {
10662
10840
  }
10663
10841
  const issues = [];
10664
10842
  for (const filePath of files) {
10665
- const text = await readSafe(filePath);
10843
+ const text = await readSafe2(filePath);
10666
10844
  if (text.trim().length === 0) {
10667
10845
  continue;
10668
10846
  }
@@ -10693,7 +10871,7 @@ async function validateDownstreamRefsForV1421(entry) {
10693
10871
  ];
10694
10872
  const issues = [];
10695
10873
  for (const check of checks) {
10696
- const text = await readSafe(check.filePath);
10874
+ const text = await readSafe2(check.filePath);
10697
10875
  if (text.trim().length === 0) {
10698
10876
  continue;
10699
10877
  }
@@ -10706,7 +10884,7 @@ async function validateDownstreamRefsForV1421(entry) {
10706
10884
  issues.push(
10707
10885
  issue(
10708
10886
  "TRACE_DOWNSTREAM_REF",
10709
- `${path35.basename(check.filePath)} \u3067\u4E0B\u4F4D\u30EC\u30A4\u30E4\u30FC\u53C2\u7167\u306F\u7981\u6B62\u3067\u3059: ${refs.join(", ")}`,
10887
+ `${path36.basename(check.filePath)} \u3067\u4E0B\u4F4D\u30EC\u30A4\u30E4\u30FC\u53C2\u7167\u306F\u7981\u6B62\u3067\u3059: ${refs.join(", ")}`,
10710
10888
  "error",
10711
10889
  check.filePath,
10712
10890
  "layeredTraceability.downstream",
@@ -10736,14 +10914,14 @@ function resolveLayerFromId(id) {
10736
10914
  async function collectMarkdownFiles(policiesDir) {
10737
10915
  try {
10738
10916
  const entries = await readdir8(policiesDir, { withFileTypes: true });
10739
- return entries.filter((entry) => entry.isFile() && path35.extname(entry.name).toLowerCase() === ".md").map((entry) => path35.join(policiesDir, entry.name)).sort((left, right) => left.localeCompare(right));
10917
+ return entries.filter((entry) => entry.isFile() && path36.extname(entry.name).toLowerCase() === ".md").map((entry) => path36.join(policiesDir, entry.name)).sort((left, right) => left.localeCompare(right));
10740
10918
  } catch {
10741
10919
  return [];
10742
10920
  }
10743
10921
  }
10744
10922
  async function validateSpecRootParent(entry) {
10745
- const specPath = path35.join(entry.dir, "01_Spec.md");
10746
- const text = await readSafe(specPath);
10923
+ const specPath = path36.join(entry.dir, "01_Spec.md");
10924
+ const text = await readSafe2(specPath);
10747
10925
  if (text.trim().length > 0 && /\bCAP-\d{4}\b/.test(text)) {
10748
10926
  return [];
10749
10927
  }
@@ -10758,7 +10936,7 @@ async function validateSpecRootParent(entry) {
10758
10936
  ];
10759
10937
  }
10760
10938
  async function validateMarkdownParentFormat(filePath, prefix, parentFormat, parentPrefix) {
10761
- const text = await readSafe(filePath);
10939
+ const text = await readSafe2(filePath);
10762
10940
  const items = collectMarkdownItems(text, prefix);
10763
10941
  const issues = [];
10764
10942
  for (const item of items) {
@@ -10791,7 +10969,7 @@ async function validateMarkdownParentFormat(filePath, prefix, parentFormat, pare
10791
10969
  return issues;
10792
10970
  }
10793
10971
  async function validateExamplesParentFormat(filePath) {
10794
- const text = await readSafe(filePath);
10972
+ const text = await readSafe2(filePath);
10795
10973
  const scenarios = collectScenarioItems(text);
10796
10974
  const issues = [];
10797
10975
  for (const scenario of scenarios) {
@@ -10824,7 +11002,7 @@ async function validateExamplesParentFormat(filePath) {
10824
11002
  return issues;
10825
11003
  }
10826
11004
  async function validateForbiddenRefs(filePath, pattern, messagePrefix) {
10827
- const text = await readSafe(filePath);
11005
+ const text = await readSafe2(filePath);
10828
11006
  const refs = uniqueMatches(text, pattern);
10829
11007
  if (refs.length === 0) {
10830
11008
  return [];
@@ -10843,10 +11021,10 @@ async function validateForbiddenRefs(filePath, pattern, messagePrefix) {
10843
11021
 
10844
11022
  // src/core/validators/legacyStatusDir.ts
10845
11023
  import { readdir as readdir9, stat as stat5 } from "fs/promises";
10846
- import path36 from "path";
11024
+ import path37 from "path";
10847
11025
  var BASELINE_STATUS_ARTIFACTS = /* @__PURE__ */ new Set(["README.md", ".gitignore"]);
10848
11026
  async function validateLegacyStatusDir(root) {
10849
- const statusDir = path36.join(root, ".qfai", "status");
11027
+ const statusDir = path37.join(root, ".qfai", "status");
10850
11028
  if (!await isDirectory(statusDir)) {
10851
11029
  return [];
10852
11030
  }
@@ -10898,15 +11076,15 @@ async function isDirectory(target) {
10898
11076
  }
10899
11077
 
10900
11078
  // src/core/validators/mermaidEnforcement.ts
10901
- import { readFile as readFile25 } from "fs/promises";
10902
- import path37 from "path";
11079
+ import { readFile as readFile26 } from "fs/promises";
11080
+ import path38 from "path";
10903
11081
  var TARGETS = [
10904
11082
  { segments: [".qfai", "specs"], extensions: [".md", ".feature"] },
10905
11083
  { segments: [".qfai", "discussion"], extensions: [".md"] }
10906
11084
  ];
10907
11085
  var BUSINESS_FLOW_RELATIVE_CANDIDATES = [
10908
- path37.join(".qfai", "specs", "_policies", "04_Business-Flow.md"),
10909
- path37.join(".qfai", "specs", "_policies", "04_Business-flow.md")
11086
+ path38.join(".qfai", "specs", "_policies", "04_Business-Flow.md"),
11087
+ path38.join(".qfai", "specs", "_policies", "04_Business-flow.md")
10910
11088
  ];
10911
11089
  var MERMAID_DIRECTIVE_RE = /^\s*(?:sequenceDiagram|flowchart|erDiagram|classDiagram|stateDiagram(?:-v2)?|journey|gantt|graph\s+(?:TB|BT|RL|LR|TD))\b/i;
10912
11090
  var FLOW_OR_SEQUENCE_RE2 = /\b(?:sequenceDiagram|flowchart)\b/i;
@@ -10916,13 +11094,13 @@ async function validateMermaidEnforcement(root) {
10916
11094
  const issues = [];
10917
11095
  const scanResults = /* @__PURE__ */ new Map();
10918
11096
  for (const filePath of targetFiles) {
10919
- const text = await readFile25(filePath, "utf-8");
11097
+ const text = await readFile26(filePath, "utf-8");
10920
11098
  const result = scanMermaidUsage(filePath, text);
10921
11099
  scanResults.set(filePath, result);
10922
11100
  issues.push(...result.issues);
10923
11101
  }
10924
11102
  if (businessFlowPath) {
10925
- const businessFlowScan = scanResults.get(businessFlowPath) ?? scanMermaidUsage(businessFlowPath, await readFile25(businessFlowPath, "utf-8"));
11103
+ const businessFlowScan = scanResults.get(businessFlowPath) ?? scanMermaidUsage(businessFlowPath, await readFile26(businessFlowPath, "utf-8"));
10926
11104
  if (businessFlowScan.mermaidFenceCount === 0) {
10927
11105
  issues.push(
10928
11106
  issue(
@@ -10950,7 +11128,7 @@ async function validateMermaidEnforcement(root) {
10950
11128
  )
10951
11129
  );
10952
11130
  }
10953
- if (path37.basename(businessFlowPath) === "04_Business-flow.md") {
11131
+ if (path38.basename(businessFlowPath) === "04_Business-flow.md") {
10954
11132
  issues.push(
10955
11133
  issue(
10956
11134
  "QFAI-MMD-005",
@@ -10971,14 +11149,14 @@ async function validateMermaidEnforcement(root) {
10971
11149
  async function collectTargetFiles(root) {
10972
11150
  const files = [];
10973
11151
  for (const target of TARGETS) {
10974
- const targetDir = path37.join(root, ...target.segments);
11152
+ const targetDir = path38.join(root, ...target.segments);
10975
11153
  files.push(
10976
11154
  ...await collectFiles(targetDir, {
10977
11155
  extensions: [...target.extensions]
10978
11156
  })
10979
11157
  );
10980
11158
  }
10981
- return Array.from(new Set(files)).filter((filePath) => path37.basename(filePath).toLowerCase() !== "readme.md").sort((a, b) => a.localeCompare(b));
11159
+ return Array.from(new Set(files)).filter((filePath) => path38.basename(filePath).toLowerCase() !== "readme.md").sort((a, b) => a.localeCompare(b));
10982
11160
  }
10983
11161
  function scanMermaidUsage(filePath, text) {
10984
11162
  const lines = text.replace(/\r\n/g, "\n").split("\n");
@@ -11068,13 +11246,13 @@ function parseFenceStart(line) {
11068
11246
  };
11069
11247
  }
11070
11248
  async function collectDeprecatedBusinessFlowFeatureWarnings(root) {
11071
- const policiesDir = path37.join(root, ".qfai", "specs", "_policies");
11249
+ const policiesDir = path38.join(root, ".qfai", "specs", "_policies");
11072
11250
  const featureFiles = await collectFiles(policiesDir, {
11073
11251
  extensions: [".feature"]
11074
11252
  });
11075
11253
  const issues = [];
11076
11254
  for (const filePath of featureFiles) {
11077
- if (!/business-flow/i.test(path37.basename(filePath))) {
11255
+ if (!/business-flow/i.test(path38.basename(filePath))) {
11078
11256
  continue;
11079
11257
  }
11080
11258
  issues.push(
@@ -11094,7 +11272,7 @@ async function collectDeprecatedBusinessFlowFeatureWarnings(root) {
11094
11272
  }
11095
11273
  async function resolveBusinessFlowPath(root) {
11096
11274
  for (const relativePath of BUSINESS_FLOW_RELATIVE_CANDIDATES) {
11097
- const target = path37.join(root, relativePath);
11275
+ const target = path38.join(root, relativePath);
11098
11276
  if (await exists8(target)) {
11099
11277
  return target;
11100
11278
  }
@@ -11120,7 +11298,7 @@ async function validateContractReferences(root, config) {
11120
11298
  const issues = [];
11121
11299
  const severity = config.validation.traceability.unknownContractIdSeverity;
11122
11300
  for (const filePath of Array.from(contractIndexFiles).sort((a, b) => a.localeCompare(b))) {
11123
- const text = await readSafe(filePath);
11301
+ const text = await readSafe2(filePath);
11124
11302
  if (text.trim().length === 0) {
11125
11303
  continue;
11126
11304
  }
@@ -11232,7 +11410,7 @@ function normalizeHeaderKey(column) {
11232
11410
  }
11233
11411
 
11234
11412
  // src/core/validators/orphanProhibition.ts
11235
- import path38 from "path";
11413
+ import path39 from "path";
11236
11414
  async function validateOrphanProhibition(root, config) {
11237
11415
  const specsRoot = resolvePath(root, config, "specsDir");
11238
11416
  const entries = await collectSpecEntries(specsRoot);
@@ -11243,16 +11421,16 @@ async function validateOrphanProhibition(root, config) {
11243
11421
  return [];
11244
11422
  }
11245
11423
  const issues = [];
11246
- const policiesDir = layeredEntries[0]?.sharedDir ?? path38.join(specsRoot, "_policies");
11424
+ const policiesDir = layeredEntries[0]?.sharedDir ?? path39.join(specsRoot, "_policies");
11247
11425
  const capIds = new Set(
11248
- uniqueMatches(await readSafe(path38.join(policiesDir, "03_Capabilities.md")), /\bCAP-\d{4}\b/g)
11426
+ uniqueMatches(await readSafe2(path39.join(policiesDir, "03_Capabilities.md")), /\bCAP-\d{4}\b/g)
11249
11427
  );
11250
11428
  for (const entry of layeredEntries) {
11251
- const usItems = collectMarkdownItems(await readSafe(entry.userStoriesPath), "US");
11252
- const acItems = collectMarkdownItems(await readSafe(entry.acceptanceCriteriaPath), "AC");
11253
- const brItems = collectMarkdownItems(await readSafe(entry.businessRulesPath), "BR");
11254
- const exItems = collectScenarioItems(await readSafe(entry.examplesPath));
11255
- const tcItems = collectMarkdownItems(await readSafe(entry.testCasesPath), "TC");
11429
+ const usItems = collectMarkdownItems(await readSafe2(entry.userStoriesPath), "US");
11430
+ const acItems = collectMarkdownItems(await readSafe2(entry.acceptanceCriteriaPath), "AC");
11431
+ const brItems = collectMarkdownItems(await readSafe2(entry.businessRulesPath), "BR");
11432
+ const exItems = collectScenarioItems(await readSafe2(entry.examplesPath));
11433
+ const tcItems = collectMarkdownItems(await readSafe2(entry.testCasesPath), "TC");
11256
11434
  const usIds = new Set(usItems.map((item) => item.id));
11257
11435
  const acIds = new Set(acItems.map((item) => item.id));
11258
11436
  const brIds = new Set(brItems.map((item) => item.id));
@@ -11395,8 +11573,8 @@ function validateExParentExists(filePath, exItems, acIds, brIds) {
11395
11573
  }
11396
11574
 
11397
11575
  // src/core/validators/prototypingEvidence.ts
11398
- import { access as access12, readFile as readFile26 } from "fs/promises";
11399
- import path39 from "path";
11576
+ import { access as access12, readFile as readFile27 } from "fs/promises";
11577
+ import path40 from "path";
11400
11578
  var EVIDENCE_MARKDOWN_FILE = "prototyping.md";
11401
11579
  var EVIDENCE_JSON_FILE = "prototyping.json";
11402
11580
  async function validatePrototypingEvidence(root, config) {
@@ -11405,10 +11583,10 @@ async function validatePrototypingEvidence(root, config) {
11405
11583
  if (specEntries.length === 0) {
11406
11584
  return [];
11407
11585
  }
11408
- const qfaiRoot = path39.dirname(specsRoot);
11409
- const evidenceRoot = path39.join(qfaiRoot, "evidence");
11410
- const evidenceMarkdownPath = path39.join(evidenceRoot, EVIDENCE_MARKDOWN_FILE);
11411
- const evidenceJsonPath = path39.join(evidenceRoot, EVIDENCE_JSON_FILE);
11586
+ const qfaiRoot = path40.dirname(specsRoot);
11587
+ const evidenceRoot = path40.join(qfaiRoot, "evidence");
11588
+ const evidenceMarkdownPath = path40.join(evidenceRoot, EVIDENCE_MARKDOWN_FILE);
11589
+ const evidenceJsonPath = path40.join(evidenceRoot, EVIDENCE_JSON_FILE);
11412
11590
  const [markdownRaw, jsonRaw] = await Promise.all([
11413
11591
  readSafe10(evidenceMarkdownPath),
11414
11592
  readSafe10(evidenceJsonPath)
@@ -11585,7 +11763,7 @@ function extractSpecRefs(rows) {
11585
11763
  }
11586
11764
  async function readSafe10(filePath) {
11587
11765
  try {
11588
- return await readFile26(filePath, "utf-8");
11766
+ return await readFile27(filePath, "utf-8");
11589
11767
  } catch {
11590
11768
  return null;
11591
11769
  }
@@ -11741,7 +11919,7 @@ async function validateUiFidelity(root, config, evidenceJsonPath, evidence) {
11741
11919
  for (const screen of uiFidelity.screens) {
11742
11920
  const contractFiles = contractIndex.idToFiles.get(screen.uiContractId);
11743
11921
  const contractFile = contractFiles ? Array.from(contractFiles).sort((left, right) => left.localeCompare(right))[0] : void 0;
11744
- const contractRefFile = contractFile ? toPosixPath3(path39.relative(root, contractFile)) : void 0;
11922
+ const contractRefFile = contractFile ? toPosixPath3(path40.relative(root, contractFile)) : void 0;
11745
11923
  if (!contractFiles || contractFiles.size === 0) {
11746
11924
  mismatches.push({
11747
11925
  contractId: screen.uiContractId,
@@ -12550,7 +12728,7 @@ async function collectMissingRenderArtifacts(root, render) {
12550
12728
  { label: "htmlPath", target: render.htmlPath }
12551
12729
  ];
12552
12730
  for (const candidate of candidates) {
12553
- const resolved = path39.isAbsolute(candidate.target) ? candidate.target : path39.resolve(root, candidate.target);
12731
+ const resolved = path40.isAbsolute(candidate.target) ? candidate.target : path40.resolve(root, candidate.target);
12554
12732
  try {
12555
12733
  await access12(resolved);
12556
12734
  } catch {
@@ -12573,12 +12751,12 @@ function formatError9(error2) {
12573
12751
  }
12574
12752
 
12575
12753
  // src/core/validators/requireIndex.ts
12576
- import { readFile as readFile27 } from "fs/promises";
12577
- import path40 from "path";
12754
+ import { readFile as readFile28 } from "fs/promises";
12755
+ import path41 from "path";
12578
12756
 
12579
12757
  // src/core/validators/repositoryHygiene.ts
12580
12758
  import { readdir as readdir10, stat as stat6 } from "fs/promises";
12581
- import path41 from "path";
12759
+ import path42 from "path";
12582
12760
  var LEGACY_DIR_RULES = [
12583
12761
  { legacy: "discussions", canonical: "discussion" },
12584
12762
  { legacy: "discuss", canonical: "discussion" },
@@ -12589,11 +12767,11 @@ var LEGACY_DIR_RULES = [
12589
12767
  ];
12590
12768
  var SUSPICIOUS_TEMPLATE_NAME_RE = /^(?:_?templates?|_?sample(?:s)?|sample-template)$/i;
12591
12769
  async function validateRepositoryHygiene(root, config) {
12592
- const qfaiRoot = path41.join(root, ".qfai");
12770
+ const qfaiRoot = path42.join(root, ".qfai");
12593
12771
  const specsRoot = resolvePath(root, config, "specsDir");
12594
12772
  const issues = [];
12595
12773
  for (const rule of LEGACY_DIR_RULES) {
12596
- const legacyPath = path41.join(qfaiRoot, rule.legacy);
12774
+ const legacyPath = path42.join(qfaiRoot, rule.legacy);
12597
12775
  if (!await isDirectory2(legacyPath)) {
12598
12776
  continue;
12599
12777
  }
@@ -12648,10 +12826,10 @@ async function collectSuspiciousTemplatePaths(root) {
12648
12826
  continue;
12649
12827
  }
12650
12828
  for (const entry of entries) {
12651
- const absolute = path41.join(current, entry.name);
12829
+ const absolute = path42.join(current, entry.name);
12652
12830
  if (entry.isDirectory()) {
12653
12831
  if (SUSPICIOUS_TEMPLATE_NAME_RE.test(entry.name)) {
12654
- matches.push(toPosix(path41.relative(root, absolute)));
12832
+ matches.push(toPosix(path42.relative(root, absolute)));
12655
12833
  }
12656
12834
  queue.push(absolute);
12657
12835
  continue;
@@ -12659,9 +12837,9 @@ async function collectSuspiciousTemplatePaths(root) {
12659
12837
  if (!entry.isFile()) {
12660
12838
  continue;
12661
12839
  }
12662
- const baseName = path41.parse(entry.name).name;
12840
+ const baseName = path42.parse(entry.name).name;
12663
12841
  if (SUSPICIOUS_TEMPLATE_NAME_RE.test(baseName)) {
12664
- matches.push(toPosix(path41.relative(root, absolute)));
12842
+ matches.push(toPosix(path42.relative(root, absolute)));
12665
12843
  }
12666
12844
  }
12667
12845
  }
@@ -12679,7 +12857,7 @@ function toPosix(value) {
12679
12857
  }
12680
12858
 
12681
12859
  // src/core/validators/specSplitByCapability.ts
12682
- import path42 from "path";
12860
+ import path43 from "path";
12683
12861
  var CAP_ID_RE2 = /\bCAP-\d{4}\b/g;
12684
12862
  async function validateSpecSplitByCapability(root, config) {
12685
12863
  const specsRoot = resolvePath(root, config, "specsDir");
@@ -12690,9 +12868,9 @@ async function validateSpecSplitByCapability(root, config) {
12690
12868
  if (layeredEntries.length === 0) {
12691
12869
  return [];
12692
12870
  }
12693
- const policiesDir = layeredEntries[0]?.sharedDir ?? path42.join(specsRoot, "_policies");
12694
- const capabilitiesPath = path42.join(policiesDir, "03_Capabilities.md");
12695
- const capabilityText = await readSafe(capabilitiesPath);
12871
+ const policiesDir = layeredEntries[0]?.sharedDir ?? path43.join(specsRoot, "_policies");
12872
+ const capabilitiesPath = path43.join(policiesDir, "03_Capabilities.md");
12873
+ const capabilityText = await readSafe2(capabilitiesPath);
12696
12874
  const issues = [];
12697
12875
  if (!await exists8(capabilitiesPath)) {
12698
12876
  issues.push(
@@ -12731,7 +12909,7 @@ async function validateSpecSplitByCapability(root, config) {
12731
12909
  );
12732
12910
  }
12733
12911
  const actualSpecIds = new Set(
12734
- layeredEntries.map((entry) => path42.basename(entry.dir).toLowerCase())
12912
+ layeredEntries.map((entry) => path43.basename(entry.dir).toLowerCase())
12735
12913
  );
12736
12914
  const expectedSpecIds = capIds.map((_, index) => `spec-${to4(index + 1)}`);
12737
12915
  const missingSpecIds = expectedSpecIds.filter((specId) => !actualSpecIds.has(specId));
@@ -12768,12 +12946,12 @@ async function validateSpecSplitByCapability(root, config) {
12768
12946
  continue;
12769
12947
  }
12770
12948
  const specId = `spec-${to4(index + 1)}`;
12771
- const entry = layeredEntries.find((value) => path42.basename(value.dir).toLowerCase() === specId);
12949
+ const entry = layeredEntries.find((value) => path43.basename(value.dir).toLowerCase() === specId);
12772
12950
  if (!entry) {
12773
12951
  continue;
12774
12952
  }
12775
- const specFilePath = path42.join(entry.dir, "01_Spec.md");
12776
- const specText = await readSafe(specFilePath);
12953
+ const specFilePath = path43.join(entry.dir, "01_Spec.md");
12954
+ const specText = await readSafe2(specFilePath);
12777
12955
  if (specText.trim().length === 0 || !specText.includes(capId)) {
12778
12956
  issues.push(
12779
12957
  issue(
@@ -12791,8 +12969,8 @@ async function validateSpecSplitByCapability(root, config) {
12791
12969
  }
12792
12970
 
12793
12971
  // src/core/validators/statusInSpecs.ts
12794
- import { readFile as readFile28 } from "fs/promises";
12795
- import path43 from "path";
12972
+ import { readFile as readFile29 } from "fs/promises";
12973
+ import path44 from "path";
12796
12974
  var STRONG_PATTERNS = [
12797
12975
  { label: "release_candidate:", pattern: /\brelease_candidate\s*:/i },
12798
12976
  { label: "status:", pattern: /^\s*(?:-\s*)?status\s*:/im },
@@ -12849,23 +13027,23 @@ function hasMatch(text, pattern) {
12849
13027
  }
12850
13028
  async function readSafe11(filePath) {
12851
13029
  try {
12852
- return await readFile28(filePath, "utf-8");
13030
+ return await readFile29(filePath, "utf-8");
12853
13031
  } catch {
12854
13032
  return "";
12855
13033
  }
12856
13034
  }
12857
13035
  function isOpenQuestionsFile(filePath) {
12858
- return /open-questions\.md$/i.test(path43.basename(filePath));
13036
+ return /open-questions\.md$/i.test(path44.basename(filePath));
12859
13037
  }
12860
13038
 
12861
13039
  // src/core/validators/designToken.ts
12862
- import { readFile as readFile29 } from "fs/promises";
12863
- import path44 from "path";
13040
+ import { readFile as readFile30 } from "fs/promises";
13041
+ import path45 from "path";
12864
13042
  import fg2 from "fast-glob";
12865
- import { parse as parseYaml6 } from "yaml";
13043
+ import { parse as parseYaml7 } from "yaml";
12866
13044
 
12867
13045
  // src/core/parse/designToken.ts
12868
- import { parse as parseYaml5 } from "yaml";
13046
+ import { parse as parseYaml6 } from "yaml";
12869
13047
  var REF_PATTERN = /\{([^}]+)\}/g;
12870
13048
  var MAX_RESOLVE_DEPTH = 10;
12871
13049
  function parseDesignToken(yamlContent) {
@@ -12878,7 +13056,7 @@ function parseDesignToken(yamlContent) {
12878
13056
  };
12879
13057
  let parsed;
12880
13058
  try {
12881
- parsed = parseYaml5(yamlContent);
13059
+ parsed = parseYaml6(yamlContent);
12882
13060
  } catch (error2) {
12883
13061
  const msg = error2 instanceof Error ? error2.message : String(error2);
12884
13062
  result.errors.push({ message: `YAML parse error: ${msg}` });
@@ -12907,7 +13085,7 @@ function collectLayer(layer, layerName, target, errors) {
12907
13085
  }
12908
13086
  function flattenTokens(obj, prefix, target, errors) {
12909
13087
  for (const [key, value] of Object.entries(obj)) {
12910
- const path64 = `${prefix}.${key}`;
13088
+ const path67 = `${prefix}.${key}`;
12911
13089
  if (value && typeof value === "object" && !Array.isArray(value)) {
12912
13090
  const record2 = value;
12913
13091
  if ("$value" in record2) {
@@ -12923,9 +13101,9 @@ function flattenTokens(obj, prefix, target, errors) {
12923
13101
  if (typeof record2.platform === "string") {
12924
13102
  token.platform = record2.platform;
12925
13103
  }
12926
- target.set(path64, token);
13104
+ target.set(path67, token);
12927
13105
  } else {
12928
- flattenTokens(record2, path64, target, errors);
13106
+ flattenTokens(record2, path67, target, errors);
12929
13107
  }
12930
13108
  }
12931
13109
  }
@@ -12935,44 +13113,44 @@ function resolveAllReferences(result) {
12935
13113
  for (const [key, val] of result.primitives) allTokens.set(key, val);
12936
13114
  for (const [key, val] of result.semantics) allTokens.set(key, val);
12937
13115
  for (const [key, val] of result.components) allTokens.set(key, val);
12938
- for (const [path64] of allTokens) {
12939
- resolveTokenRef(path64, allTokens, /* @__PURE__ */ new Set(), 0, result);
13116
+ for (const [path67] of allTokens) {
13117
+ resolveTokenRef(path67, allTokens, /* @__PURE__ */ new Set(), 0, result);
12940
13118
  }
12941
13119
  }
12942
- function resolveTokenRef(path64, allTokens, visited, depth, result) {
12943
- if (result.resolved.has(path64)) {
12944
- return result.resolved.get(path64);
13120
+ function resolveTokenRef(path67, allTokens, visited, depth, result) {
13121
+ if (result.resolved.has(path67)) {
13122
+ return result.resolved.get(path67);
12945
13123
  }
12946
13124
  if (depth > MAX_RESOLVE_DEPTH) {
12947
13125
  result.errors.push({
12948
- message: `Max reference depth exceeded at: ${path64}`,
12949
- path: path64
13126
+ message: `Max reference depth exceeded at: ${path67}`,
13127
+ path: path67
12950
13128
  });
12951
13129
  return void 0;
12952
13130
  }
12953
- if (visited.has(path64)) {
13131
+ if (visited.has(path67)) {
12954
13132
  result.errors.push({
12955
- message: `Circular reference detected: ${path64}`,
12956
- path: path64
13133
+ message: `Circular reference detected: ${path67}`,
13134
+ path: path67
12957
13135
  });
12958
13136
  return void 0;
12959
13137
  }
12960
- const token = allTokens.get(path64);
13138
+ const token = allTokens.get(path67);
12961
13139
  if (!token) {
12962
13140
  return void 0;
12963
13141
  }
12964
13142
  if (typeof token.$value !== "string") {
12965
13143
  const rawValue2 = stringifyTokenValue(token.$value);
12966
- result.resolved.set(path64, rawValue2);
13144
+ result.resolved.set(path67, rawValue2);
12967
13145
  return rawValue2;
12968
13146
  }
12969
13147
  const rawValue = stringifyTokenValue(token.$value);
12970
13148
  const refs = [...rawValue.matchAll(REF_PATTERN)];
12971
13149
  if (refs.length === 0) {
12972
- result.resolved.set(path64, rawValue);
13150
+ result.resolved.set(path67, rawValue);
12973
13151
  return rawValue;
12974
13152
  }
12975
- visited.add(path64);
13153
+ visited.add(path67);
12976
13154
  let resolved = rawValue;
12977
13155
  for (const ref of refs) {
12978
13156
  const refPath = ref[1];
@@ -12980,8 +13158,8 @@ function resolveTokenRef(path64, allTokens, visited, depth, result) {
12980
13158
  const refToken = allTokens.get(refPath);
12981
13159
  if (!refToken) {
12982
13160
  result.errors.push({
12983
- message: `Unresolved token reference: {${refPath}} at ${path64}`,
12984
- path: path64
13161
+ message: `Unresolved token reference: {${refPath}} at ${path67}`,
13162
+ path: path67
12985
13163
  });
12986
13164
  continue;
12987
13165
  }
@@ -12990,7 +13168,7 @@ function resolveTokenRef(path64, allTokens, visited, depth, result) {
12990
13168
  resolved = resolved.split(`{${refPath}}`).join(refValue);
12991
13169
  }
12992
13170
  }
12993
- result.resolved.set(path64, resolved);
13171
+ result.resolved.set(path67, resolved);
12994
13172
  return resolved;
12995
13173
  }
12996
13174
  function stringifyTokenValue(value) {
@@ -13045,8 +13223,8 @@ var VALID_TYPES = [
13045
13223
  var VALID_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
13046
13224
  async function validateDesignToken(root, config) {
13047
13225
  const configuredDir = config.uiux?.designTokensDir;
13048
- const designDir = configuredDir ? path44.resolve(root, configuredDir) : path44.join(root, config.paths.contractsDir, "design");
13049
- const pattern = path44.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
13226
+ const designDir = configuredDir ? path45.resolve(root, configuredDir) : path45.join(root, config.paths.contractsDir, "design");
13227
+ const pattern = path45.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
13050
13228
  const files = await fg2(pattern, {
13051
13229
  absolute: true,
13052
13230
  ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
@@ -13056,11 +13234,11 @@ async function validateDesignToken(root, config) {
13056
13234
  }
13057
13235
  const issues = [];
13058
13236
  for (const filePath of files) {
13059
- const rel = path44.relative(root, filePath).replace(/\\/g, "/");
13237
+ const rel = path45.relative(root, filePath).replace(/\\/g, "/");
13060
13238
  let hasRootObjectError = false;
13061
13239
  let content;
13062
13240
  try {
13063
- content = await readFile29(filePath, "utf-8");
13241
+ content = await readFile30(filePath, "utf-8");
13064
13242
  } catch {
13065
13243
  issues.push(
13066
13244
  issue(
@@ -13074,7 +13252,7 @@ async function validateDesignToken(root, config) {
13074
13252
  continue;
13075
13253
  }
13076
13254
  try {
13077
- const parsed = parseYaml6(content);
13255
+ const parsed = parseYaml7(content);
13078
13256
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
13079
13257
  hasRootObjectError = true;
13080
13258
  issues.push(
@@ -13223,8 +13401,8 @@ function normalizePlatform(value) {
13223
13401
  }
13224
13402
 
13225
13403
  // src/core/validators/htmlMock.ts
13226
- import { readFile as readFile30 } from "fs/promises";
13227
- import path45 from "path";
13404
+ import { readFile as readFile31 } from "fs/promises";
13405
+ import path46 from "path";
13228
13406
  import fg3 from "fast-glob";
13229
13407
 
13230
13408
  // src/core/uiux/contrastRatio.ts
@@ -13604,19 +13782,19 @@ async function validateHtmlMock(root, platform, config) {
13604
13782
  const startTime = performance.now();
13605
13783
  const budget = config.uiux?.htmlMockTimeout ?? 2e3;
13606
13784
  const patterns = [
13607
- path45.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
13608
- path45.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
13785
+ path46.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
13786
+ path46.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
13609
13787
  ];
13610
13788
  const files = await fg3(patterns, { absolute: true });
13611
13789
  const mockBlocks = [];
13612
13790
  for (const filePath of files) {
13613
13791
  let content;
13614
13792
  try {
13615
- content = await readFile30(filePath, "utf-8");
13793
+ content = await readFile31(filePath, "utf-8");
13616
13794
  } catch {
13617
13795
  continue;
13618
13796
  }
13619
- const rel = path45.relative(root, filePath).replace(/\\/g, "/");
13797
+ const rel = path46.relative(root, filePath).replace(/\\/g, "/");
13620
13798
  for (const block of collectHtmlMockBlocks(content)) {
13621
13799
  mockBlocks.push({ file: rel, html: block.html, rawBlock: block.rawBlock });
13622
13800
  }
@@ -13782,8 +13960,8 @@ async function validateHtmlMock(root, platform, config) {
13782
13960
  }
13783
13961
 
13784
13962
  // src/core/validators/mermaidScreenFlow.ts
13785
- import { readFile as readFile31 } from "fs/promises";
13786
- import path46 from "path";
13963
+ import { readFile as readFile32 } from "fs/promises";
13964
+ import path47 from "path";
13787
13965
  import fg4 from "fast-glob";
13788
13966
 
13789
13967
  // src/core/validators/mermaidUtils.ts
@@ -13834,18 +14012,18 @@ var FLOWCHART_RE = /^\s*flowchart\s+(TD|LR|TB|RL|BT)\b/;
13834
14012
  async function validateMermaidScreenFlow(root, config) {
13835
14013
  const issues = [];
13836
14014
  const patterns = [
13837
- path46.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
13838
- path46.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
14015
+ path47.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
14016
+ path47.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
13839
14017
  ];
13840
14018
  const files = await fg4(patterns, { absolute: true });
13841
14019
  for (const filePath of files) {
13842
14020
  let content;
13843
14021
  try {
13844
- content = await readFile31(filePath, "utf-8");
14022
+ content = await readFile32(filePath, "utf-8");
13845
14023
  } catch {
13846
14024
  continue;
13847
14025
  }
13848
- const rel = path46.relative(root, filePath).replace(/\\/g, "/");
14026
+ const rel = path47.relative(root, filePath).replace(/\\/g, "/");
13849
14027
  const blocks = extractFencedCodeBlocks(content);
13850
14028
  for (const block of blocks) {
13851
14029
  if (block.language !== "mermaid") continue;
@@ -13943,10 +14121,10 @@ function parseTransitions(content) {
13943
14121
  }
13944
14122
 
13945
14123
  // src/core/validators/bpApDb.ts
13946
- import { readFile as readFile32 } from "fs/promises";
13947
- import path47 from "path";
14124
+ import { readFile as readFile33 } from "fs/promises";
14125
+ import path48 from "path";
13948
14126
  import fg5 from "fast-glob";
13949
- import { parse as parseYaml7 } from "yaml";
14127
+ import { parse as parseYaml8 } from "yaml";
13950
14128
  var BP_ID_RE = /^BP-\d{4}$/;
13951
14129
  var AP_ID_RE = /^AP-\d{4}$/;
13952
14130
  var VALID_SEVERITIES = ["critical", "major", "minor"];
@@ -13981,9 +14159,9 @@ var AP_REQUIRED_FIELDS = [
13981
14159
  ];
13982
14160
  async function validateBpApDb(root, config) {
13983
14161
  const issues = [];
13984
- const designDir = path47.join(root, config.paths.contractsDir, "design");
13985
- const bpPattern = path47.posix.join(designDir.replace(/\\/g, "/"), "best-practices*.yaml");
13986
- const apPattern = path47.posix.join(designDir.replace(/\\/g, "/"), "anti-patterns*.yaml");
14162
+ const designDir = path48.join(root, config.paths.contractsDir, "design");
14163
+ const bpPattern = path48.posix.join(designDir.replace(/\\/g, "/"), "best-practices*.yaml");
14164
+ const apPattern = path48.posix.join(designDir.replace(/\\/g, "/"), "anti-patterns*.yaml");
13987
14165
  const globOptions = {
13988
14166
  absolute: true,
13989
14167
  ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
@@ -13996,14 +14174,14 @@ async function validateBpApDb(root, config) {
13996
14174
  const seenBpIds = /* @__PURE__ */ new Set();
13997
14175
  const seenApIds = /* @__PURE__ */ new Set();
13998
14176
  for (const filePath of bpFiles) {
13999
- const rel = path47.relative(root, filePath).replace(/\\/g, "/");
14177
+ const rel = path48.relative(root, filePath).replace(/\\/g, "/");
14000
14178
  const entries = await parseRuleFile(filePath, rel, issues);
14001
14179
  for (const entry of entries) {
14002
14180
  validateBpEntry(entry, rel, seenBpIds, issues);
14003
14181
  }
14004
14182
  }
14005
14183
  for (const filePath of apFiles) {
14006
- const rel = path47.relative(root, filePath).replace(/\\/g, "/");
14184
+ const rel = path48.relative(root, filePath).replace(/\\/g, "/");
14007
14185
  const entries = await parseRuleFile(filePath, rel, issues);
14008
14186
  for (const entry of entries) {
14009
14187
  validateApEntry(entry, rel, seenApIds, issues);
@@ -14014,7 +14192,7 @@ async function validateBpApDb(root, config) {
14014
14192
  async function parseRuleFile(filePath, rel, issues) {
14015
14193
  let content;
14016
14194
  try {
14017
- content = await readFile32(filePath, "utf-8");
14195
+ content = await readFile33(filePath, "utf-8");
14018
14196
  } catch {
14019
14197
  issues.push(
14020
14198
  issue("QFAI-BPAP-001", `BP/AP file unreadable: ${rel}`, "error", rel, "bpApDb.readFile")
@@ -14023,7 +14201,7 @@ async function parseRuleFile(filePath, rel, issues) {
14023
14201
  }
14024
14202
  let parsed;
14025
14203
  try {
14026
- parsed = parseYaml7(content);
14204
+ parsed = parseYaml8(content);
14027
14205
  } catch (error2) {
14028
14206
  issues.push(
14029
14207
  issue(
@@ -14178,8 +14356,8 @@ function toSafeString(value) {
14178
14356
  }
14179
14357
 
14180
14358
  // src/core/validators/platformDetection.ts
14181
- import { readFile as readFile33 } from "fs/promises";
14182
- import path48 from "path";
14359
+ import { readFile as readFile34 } from "fs/promises";
14360
+ import path49 from "path";
14183
14361
  var KNOWN_PLATFORMS = ["web", "windows", "mobile-ios", "mobile-android", "cross-platform"];
14184
14362
  async function detectPlatform(root, config, cliPlatform) {
14185
14363
  const issues = [];
@@ -14222,13 +14400,13 @@ async function detectPlatform(root, config, cliPlatform) {
14222
14400
  return { platform: "web", source: "fallback", issues };
14223
14401
  }
14224
14402
  async function inferPlatform(root, issues) {
14225
- if (await exists8(path48.join(root, "pubspec.yaml"))) {
14226
- const hasAndroid = await exists8(path48.join(root, "android"));
14227
- const hasIos = await exists8(path48.join(root, "ios"));
14228
- const hasWeb = await exists8(path48.join(root, "web"));
14229
- const hasWindows = await exists8(path48.join(root, "windows"));
14230
- const hasMacos = await exists8(path48.join(root, "macos"));
14231
- const hasLinux = await exists8(path48.join(root, "linux"));
14403
+ if (await exists8(path49.join(root, "pubspec.yaml"))) {
14404
+ const hasAndroid = await exists8(path49.join(root, "android"));
14405
+ const hasIos = await exists8(path49.join(root, "ios"));
14406
+ const hasWeb = await exists8(path49.join(root, "web"));
14407
+ const hasWindows = await exists8(path49.join(root, "windows"));
14408
+ const hasMacos = await exists8(path49.join(root, "macos"));
14409
+ const hasLinux = await exists8(path49.join(root, "linux"));
14232
14410
  const mobileTargets = [hasAndroid, hasIos].filter(Boolean).length;
14233
14411
  const desktopTargets = [hasWeb, hasWindows, hasMacos, hasLinux].filter(Boolean).length;
14234
14412
  if (mobileTargets + desktopTargets > 1) {
@@ -14248,10 +14426,10 @@ async function inferPlatform(root, issues) {
14248
14426
  }
14249
14427
  return null;
14250
14428
  }
14251
- const pkgJsonPath = path48.join(root, "package.json");
14429
+ const pkgJsonPath = path49.join(root, "package.json");
14252
14430
  if (await exists8(pkgJsonPath)) {
14253
14431
  try {
14254
- const raw = await readFile33(pkgJsonPath, "utf-8");
14432
+ const raw = await readFile34(pkgJsonPath, "utf-8");
14255
14433
  const pkg = JSON.parse(raw);
14256
14434
  const deps = {
14257
14435
  ...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
@@ -14270,8 +14448,8 @@ async function inferPlatform(root, issues) {
14270
14448
  return "cross-platform";
14271
14449
  }
14272
14450
  if ("react-native" in deps) {
14273
- const hasAndroid = await exists8(path48.join(root, "android"));
14274
- const hasIos = await exists8(path48.join(root, "ios"));
14451
+ const hasAndroid = await exists8(path49.join(root, "android"));
14452
+ const hasIos = await exists8(path49.join(root, "ios"));
14275
14453
  if (hasAndroid && hasIos) {
14276
14454
  return "cross-platform";
14277
14455
  }
@@ -14293,15 +14471,15 @@ function normalizePlatformInput(platform) {
14293
14471
  }
14294
14472
 
14295
14473
  // src/core/validators/uiDefinitionConsistency.ts
14296
- import { readFile as readFile34 } from "fs/promises";
14297
- import path49 from "path";
14474
+ import { readFile as readFile35 } from "fs/promises";
14475
+ import path50 from "path";
14298
14476
  import fg6 from "fast-glob";
14299
- import { parse as parseYaml8 } from "yaml";
14477
+ import { parse as parseYaml9 } from "yaml";
14300
14478
  async function validateUiDefinitionConsistency(root, config) {
14301
14479
  const issues = [];
14302
14480
  const configuredDir = config.uiux?.designTokensDir;
14303
- const designDir = configuredDir ? path49.resolve(root, configuredDir) : path49.join(root, config.paths.contractsDir, "design");
14304
- const tokenPattern = path49.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
14481
+ const designDir = configuredDir ? path50.resolve(root, configuredDir) : path50.join(root, config.paths.contractsDir, "design");
14482
+ const tokenPattern = path50.posix.join(designDir.replace(/\\/g, "/"), "design-tokens*.yaml");
14305
14483
  const tokenFiles = await fg6(tokenPattern, {
14306
14484
  absolute: true,
14307
14485
  ignore: ["**/*.schema.yaml", "**/*.schema.yml"]
@@ -14309,7 +14487,7 @@ async function validateUiDefinitionConsistency(root, config) {
14309
14487
  const resolvedTokens = /* @__PURE__ */ new Map();
14310
14488
  for (const tokenFile of tokenFiles) {
14311
14489
  try {
14312
- const content = await readFile34(tokenFile, "utf-8");
14490
+ const content = await readFile35(tokenFile, "utf-8");
14313
14491
  const result = parseDesignToken(content);
14314
14492
  for (const [key, val] of result.resolved) {
14315
14493
  resolvedTokens.set(key, val);
@@ -14317,14 +14495,14 @@ async function validateUiDefinitionConsistency(root, config) {
14317
14495
  } catch {
14318
14496
  }
14319
14497
  }
14320
- const uiContractDir = path49.join(root, config.paths.contractsDir, "ui");
14321
- const uiPattern = path49.posix.join(uiContractDir.replace(/\\/g, "/"), "**/*.yaml");
14498
+ const uiContractDir = path50.join(root, config.paths.contractsDir, "ui");
14499
+ const uiPattern = path50.posix.join(uiContractDir.replace(/\\/g, "/"), "**/*.yaml");
14322
14500
  const uiFiles = await fg6(uiPattern, { absolute: true });
14323
14501
  const contractScreenIds = /* @__PURE__ */ new Set();
14324
14502
  for (const uiFile of uiFiles) {
14325
14503
  try {
14326
- const content = await readFile34(uiFile, "utf-8");
14327
- const parsed = parseYaml8(content);
14504
+ const content = await readFile35(uiFile, "utf-8");
14505
+ const parsed = parseYaml9(content);
14328
14506
  if (parsed && typeof parsed === "object") {
14329
14507
  const screens = parsed.screens;
14330
14508
  if (Array.isArray(screens)) {
@@ -14339,15 +14517,15 @@ async function validateUiDefinitionConsistency(root, config) {
14339
14517
  }
14340
14518
  }
14341
14519
  const mdPatterns = [
14342
- path49.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
14343
- path49.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
14520
+ path50.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md"),
14521
+ path50.posix.join(root.replace(/\\/g, "/"), config.paths.specsDir, "**/*.md")
14344
14522
  ];
14345
14523
  const mdFiles = await fg6(mdPatterns, { absolute: true });
14346
14524
  const mockScreenIds = /* @__PURE__ */ new Set();
14347
14525
  for (const mdFile of mdFiles) {
14348
14526
  try {
14349
- const content = await readFile34(mdFile, "utf-8");
14350
- const rel = path49.relative(root, mdFile).replace(/\\/g, "/");
14527
+ const content = await readFile35(mdFile, "utf-8");
14528
+ const rel = path50.relative(root, mdFile).replace(/\\/g, "/");
14351
14529
  const htmlBlocks = collectHtmlMockBlocks(content);
14352
14530
  if (resolvedTokens.size > 0) {
14353
14531
  for (const htmlBlock of htmlBlocks) {
@@ -14404,8 +14582,8 @@ async function validateUiDefinitionConsistency(root, config) {
14404
14582
  }
14405
14583
 
14406
14584
  // src/core/validators/researchSummary.ts
14407
- import { readFile as readFile35 } from "fs/promises";
14408
- import path50 from "path";
14585
+ import { readFile as readFile36 } from "fs/promises";
14586
+ import path51 from "path";
14409
14587
  import fg7 from "fast-glob";
14410
14588
  var RESEARCH_SUMMARY_HEADING_RE = /^#{1,3}\s+Research\s+Summary/im;
14411
14589
  var SOURCE_ENTRY_RE = /^\s*-\s*id:\s*(\S+)/gm;
@@ -14413,17 +14591,17 @@ var REFLECTION_APPLY_RE = /action:\s*apply/i;
14413
14591
  var FULL_DATE_RE = /^\s+published:\s*["']?(\d{4}-\d{2}-\d{2})["']?/m;
14414
14592
  async function validateResearchSummary(root, config) {
14415
14593
  const issues = [];
14416
- const pattern = path50.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
14594
+ const pattern = path51.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
14417
14595
  const files = await fg7(pattern, { absolute: true });
14418
14596
  for (const filePath of files) {
14419
14597
  let content;
14420
14598
  try {
14421
- content = await readFile35(filePath, "utf-8");
14599
+ content = await readFile36(filePath, "utf-8");
14422
14600
  } catch {
14423
14601
  continue;
14424
14602
  }
14425
14603
  if (!RESEARCH_SUMMARY_HEADING_RE.test(content)) continue;
14426
- const rel = path50.relative(root, filePath).replace(/\\/g, "/");
14604
+ const rel = path51.relative(root, filePath).replace(/\\/g, "/");
14427
14605
  const section = extractResearchSummarySection(content);
14428
14606
  if (!section) continue;
14429
14607
  const sourceEntries = extractSourceEntries(section);
@@ -14654,9 +14832,9 @@ function resolveFreshnessReferenceNow() {
14654
14832
  }
14655
14833
 
14656
14834
  // src/core/validators/agentDefinition.ts
14657
- import { readFile as readFile36 } from "fs/promises";
14658
- import path51 from "path";
14659
- import { parse as parseYaml9 } from "yaml";
14835
+ import { readFile as readFile37 } from "fs/promises";
14836
+ import path52 from "path";
14837
+ import { parse as parseYaml10 } from "yaml";
14660
14838
  var REQUIRED_AGENTS = [
14661
14839
  "uiux-expert.md",
14662
14840
  "design-expert.md",
@@ -14675,13 +14853,13 @@ var REQUIRED_SECTIONS = [
14675
14853
  var REQUIRED_PHASES = ["discussion", "SDD", "prototyping", "ATDD"];
14676
14854
  async function validateAgentDefinition(root, _config) {
14677
14855
  const issues = [];
14678
- const agentsDir = path51.join(root, ".qfai", "assistant", "agents");
14856
+ const agentsDir = path52.join(root, ".qfai", "assistant", "agents");
14679
14857
  if (!await exists8(agentsDir)) {
14680
14858
  return [];
14681
14859
  }
14682
14860
  let anyAgentExists = false;
14683
14861
  for (const agentFile of REQUIRED_AGENTS) {
14684
- if (await exists8(path51.join(agentsDir, agentFile))) {
14862
+ if (await exists8(path52.join(agentsDir, agentFile))) {
14685
14863
  anyAgentExists = true;
14686
14864
  break;
14687
14865
  }
@@ -14690,7 +14868,7 @@ async function validateAgentDefinition(root, _config) {
14690
14868
  return [];
14691
14869
  }
14692
14870
  for (const agentFile of REQUIRED_AGENTS) {
14693
- const filePath = path51.join(agentsDir, agentFile);
14871
+ const filePath = path52.join(agentsDir, agentFile);
14694
14872
  const rel = `.qfai/assistant/agents/${agentFile}`;
14695
14873
  if (!await exists8(filePath)) {
14696
14874
  issues.push(
@@ -14706,7 +14884,7 @@ async function validateAgentDefinition(root, _config) {
14706
14884
  }
14707
14885
  let content;
14708
14886
  try {
14709
- content = await readFile36(filePath, "utf-8");
14887
+ content = await readFile37(filePath, "utf-8");
14710
14888
  } catch {
14711
14889
  issues.push(
14712
14890
  issue(
@@ -14812,11 +14990,11 @@ async function validateAgentDefinition(root, _config) {
14812
14990
  }
14813
14991
  }
14814
14992
  }
14815
- const rosterPath = path51.join(root, ".qfai", "assistant", "steering", "review-roster.yml");
14993
+ const rosterPath = path52.join(root, ".qfai", "assistant", "steering", "review-roster.yml");
14816
14994
  if (await exists8(rosterPath)) {
14817
14995
  try {
14818
- const rosterContent = await readFile36(rosterPath, "utf-8");
14819
- const roster = parseYaml9(rosterContent);
14996
+ const rosterContent = await readFile37(rosterPath, "utf-8");
14997
+ const roster = parseYaml10(rosterContent);
14820
14998
  if (roster && typeof roster === "object") {
14821
14999
  const rosterObj = roster;
14822
15000
  const entries = Array.isArray(rosterObj.roster) ? rosterObj.roster : [];
@@ -14860,8 +15038,8 @@ function extractPhaseBody(phaseSection, phaseName) {
14860
15038
  }
14861
15039
 
14862
15040
  // src/core/validators/tddList.ts
14863
- import { readFile as readFile37, stat as stat7 } from "fs/promises";
14864
- import path52 from "path";
15041
+ import { readFile as readFile38, stat as stat7 } from "fs/promises";
15042
+ import path53 from "path";
14865
15043
  var REQUIRED_COLUMNS = [
14866
15044
  "TDD-ID",
14867
15045
  "TC-Refs",
@@ -14875,7 +15053,7 @@ var REQUIRED_COLUMNS = [
14875
15053
  var VALID_STATUSES = /* @__PURE__ */ new Set(["todo", "red", "green", "refactor", "done", "exception"]);
14876
15054
  var TEST_FILE_CHECK_STATUSES = /* @__PURE__ */ new Set(["green", "refactor", "done"]);
14877
15055
  var TDD_ID_FORMAT = /^TDD-\d{4}$/;
14878
- var TDD_LIST_REL_PATH = path52.join("tdd", "test-list.md");
15056
+ var TDD_LIST_REL_PATH = path53.join("tdd", "test-list.md");
14879
15057
  async function validateTddList(root, config) {
14880
15058
  const specsRoot = resolvePath(root, config, "specsDir");
14881
15059
  const entries = await collectSpecEntries(specsRoot);
@@ -14887,8 +15065,8 @@ async function validateTddList(root, config) {
14887
15065
  return issues;
14888
15066
  }
14889
15067
  async function validateSpecTddList(root, specDir, specNumber) {
14890
- const filePath = path52.join(specDir, TDD_LIST_REL_PATH);
14891
- const relPath = path52.relative(root, filePath).replace(/\\/g, "/");
15068
+ const filePath = path53.join(specDir, TDD_LIST_REL_PATH);
15069
+ const relPath = path53.relative(root, filePath).replace(/\\/g, "/");
14892
15070
  const issues = [];
14893
15071
  if (!await exists8(filePath)) {
14894
15072
  issues.push(
@@ -14902,7 +15080,7 @@ async function validateSpecTddList(root, specDir, specNumber) {
14902
15080
  );
14903
15081
  return issues;
14904
15082
  }
14905
- const content = await readSafe(filePath);
15083
+ const content = await readSafe2(filePath);
14906
15084
  const table = parseFirstMarkdownTable(content);
14907
15085
  if (!table) {
14908
15086
  issues.push(
@@ -15075,9 +15253,9 @@ async function validateSpecTddList(root, specDir, specNumber) {
15075
15253
  continue;
15076
15254
  }
15077
15255
  const normalized = testFile.replace(/\\/g, "/");
15078
- const resolved = path52.resolve(root, normalized);
15079
- const relative = path52.relative(root, resolved);
15080
- if (path52.isAbsolute(normalized) || path52.win32.isAbsolute(normalized) || relative === ".." || relative.startsWith(".." + path52.sep)) {
15256
+ const resolved = path53.resolve(root, normalized);
15257
+ const relative = path53.relative(root, resolved);
15258
+ if (path53.isAbsolute(normalized) || path53.win32.isAbsolute(normalized) || relative === ".." || relative.startsWith(".." + path53.sep)) {
15081
15259
  issues.push(
15082
15260
  issue(
15083
15261
  "TDDLIST_TEST_FILE_MISSING",
@@ -15140,11 +15318,11 @@ async function validateSpecTddList(root, specDir, specNumber) {
15140
15318
  }
15141
15319
  async function collectTestCaseIds(specDir) {
15142
15320
  const empty = { knownTcIds: /* @__PURE__ */ new Set(), unitComponentTcIds: /* @__PURE__ */ new Set() };
15143
- const testCasesPath = path52.join(specDir, "06_Test-Cases.md");
15321
+ const testCasesPath = path53.join(specDir, "06_Test-Cases.md");
15144
15322
  if (!await exists8(testCasesPath)) return empty;
15145
15323
  let content;
15146
15324
  try {
15147
- content = await readFile37(testCasesPath, "utf-8");
15325
+ content = await readFile38(testCasesPath, "utf-8");
15148
15326
  } catch {
15149
15327
  return empty;
15150
15328
  }
@@ -15170,8 +15348,8 @@ async function collectTestCaseIds(specDir) {
15170
15348
  }
15171
15349
 
15172
15350
  // src/core/validators/ddpValidation.ts
15173
- import { readFile as readFile38 } from "fs/promises";
15174
- import path53 from "path";
15351
+ import { readFile as readFile39 } from "fs/promises";
15352
+ import path54 from "path";
15175
15353
  import { fileURLToPath as fileURLToPath3 } from "url";
15176
15354
  import fg8 from "fast-glob";
15177
15355
  var DDP_HEADING_RE = /^#{1,3}\s+Design\s+Direction\s+Pack/im;
@@ -15189,9 +15367,9 @@ var _bannedPatterns = null;
15189
15367
  async function loadBannedPatterns() {
15190
15368
  if (_bannedPatterns !== null) return _bannedPatterns;
15191
15369
  try {
15192
- const thisDir = path53.dirname(fileURLToPath3(import.meta.url));
15193
- const filePath = path53.join(thisDir, "ddpBannedPatterns.txt");
15194
- const content = await readFile38(filePath, "utf-8");
15370
+ const thisDir = path54.dirname(fileURLToPath3(import.meta.url));
15371
+ const filePath = path54.join(thisDir, "ddpBannedPatterns.txt");
15372
+ const content = await readFile39(filePath, "utf-8");
15195
15373
  _bannedPatterns = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
15196
15374
  } catch {
15197
15375
  _bannedPatterns = [];
@@ -15200,15 +15378,15 @@ async function loadBannedPatterns() {
15200
15378
  }
15201
15379
  async function validateDdpFields(root, config) {
15202
15380
  const issues = [];
15203
- const pattern = path53.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
15381
+ const pattern = path54.posix.join(root.replace(/\\/g, "/"), config.paths.discussionDir, "**/*.md");
15204
15382
  const files = await fg8(pattern, { absolute: true });
15205
15383
  let ddpFound = false;
15206
15384
  let isUiBearing2 = false;
15207
15385
  for (const filePath of files) {
15208
- const basename = path53.basename(filePath);
15386
+ const basename = path54.basename(filePath);
15209
15387
  if (basename === "03_Story-Workshop.md") {
15210
15388
  try {
15211
- const storyContent = await readFile38(filePath, "utf-8");
15389
+ const storyContent = await readFile39(filePath, "utf-8");
15212
15390
  if (UI_BEARING_KEYWORDS_RE.test(storyContent)) {
15213
15391
  isUiBearing2 = true;
15214
15392
  }
@@ -15219,13 +15397,13 @@ async function validateDdpFields(root, config) {
15219
15397
  for (const filePath of files) {
15220
15398
  let content;
15221
15399
  try {
15222
- content = await readFile38(filePath, "utf-8");
15400
+ content = await readFile39(filePath, "utf-8");
15223
15401
  } catch {
15224
15402
  continue;
15225
15403
  }
15226
15404
  if (!DDP_HEADING_RE.test(content)) continue;
15227
15405
  ddpFound = true;
15228
- const rel = path53.relative(root, filePath).replace(/\\/g, "/");
15406
+ const rel = path54.relative(root, filePath).replace(/\\/g, "/");
15229
15407
  const section = extractDdpSection(content);
15230
15408
  if (!section) continue;
15231
15409
  for (const field of DDP_REQUIRED_FIELDS) {
@@ -15418,7 +15596,7 @@ var ANTI_PATTERN_CHECKS = [
15418
15596
  ];
15419
15597
  async function validateResearchTraceability(root, config, issues) {
15420
15598
  const requireResearchSummary = config.uiux?.requireResearchSummary === true;
15421
- const discussionPattern = path53.posix.join(
15599
+ const discussionPattern = path54.posix.join(
15422
15600
  root.replace(/\\/g, "/"),
15423
15601
  config.paths.discussionDir,
15424
15602
  "**/*.md"
@@ -15427,7 +15605,7 @@ async function validateResearchTraceability(root, config, issues) {
15427
15605
  let hasResearchSummary = false;
15428
15606
  for (const filePath of discussionFiles) {
15429
15607
  try {
15430
- const content = await readFile38(filePath, "utf-8");
15608
+ const content = await readFile39(filePath, "utf-8");
15431
15609
  if (/research_summary\s*:/m.test(content)) {
15432
15610
  hasResearchSummary = true;
15433
15611
  break;
@@ -15446,7 +15624,7 @@ async function validateResearchTraceability(root, config, issues) {
15446
15624
  )
15447
15625
  );
15448
15626
  }
15449
- const contractPattern = path53.posix.join(
15627
+ const contractPattern = path54.posix.join(
15450
15628
  root.replace(/\\/g, "/"),
15451
15629
  config.paths.contractsDir,
15452
15630
  "design",
@@ -15456,11 +15634,11 @@ async function validateResearchTraceability(root, config, issues) {
15456
15634
  for (const filePath of contractFiles) {
15457
15635
  let content;
15458
15636
  try {
15459
- content = await readFile38(filePath, "utf-8");
15637
+ content = await readFile39(filePath, "utf-8");
15460
15638
  } catch {
15461
15639
  continue;
15462
15640
  }
15463
- const rel = path53.relative(root, filePath).replace(/\\/g, "/");
15641
+ const rel = path54.relative(root, filePath).replace(/\\/g, "/");
15464
15642
  const ruleBlocks = content.split(/(?=^-\s+(?:rule|id)\s*:)/m).filter((b) => b.trim());
15465
15643
  for (const block of ruleBlocks) {
15466
15644
  const idMatch = /(?:^|\n)\s*(?:-\s+)?id\s*:\s*(\S+)/m.exec(block);
@@ -15483,11 +15661,11 @@ async function validateStoryWorkshopTemplates(root, config, issues, discussionFi
15483
15661
  for (const filePath of discussionFiles) {
15484
15662
  let content;
15485
15663
  try {
15486
- content = await readFile38(filePath, "utf-8");
15664
+ content = await readFile39(filePath, "utf-8");
15487
15665
  } catch {
15488
15666
  continue;
15489
15667
  }
15490
- const rel = path53.relative(root, filePath).replace(/\\/g, "/");
15668
+ const rel = path54.relative(root, filePath).replace(/\\/g, "/");
15491
15669
  if (/screen_type\s*:\s*list/im.test(content)) {
15492
15670
  for (const field of LIST_TEMPLATE_REQUIRED_FIELDS) {
15493
15671
  if (!hasNonEmptyField(content, field)) {
@@ -15587,7 +15765,7 @@ function validateQualityProfile(config, issues) {
15587
15765
  }
15588
15766
  }
15589
15767
  async function validateOptionComparison(root, config, issues) {
15590
- const contractPattern = path53.posix.join(
15768
+ const contractPattern = path54.posix.join(
15591
15769
  root.replace(/\\/g, "/"),
15592
15770
  config.paths.contractsDir,
15593
15771
  "design",
@@ -15597,11 +15775,11 @@ async function validateOptionComparison(root, config, issues) {
15597
15775
  for (const filePath of contractFiles) {
15598
15776
  let content;
15599
15777
  try {
15600
- content = await readFile38(filePath, "utf-8");
15778
+ content = await readFile39(filePath, "utf-8");
15601
15779
  } catch {
15602
15780
  continue;
15603
15781
  }
15604
- const rel = path53.relative(root, filePath).replace(/\\/g, "/");
15782
+ const rel = path54.relative(root, filePath).replace(/\\/g, "/");
15605
15783
  const optionMatches = content.match(/(?:^|\n)\s*-\s+option\s*:/gm) ?? [];
15606
15784
  const numberedOptions = content.match(/(?:^|\n)\s*option_\d+\s*:/gm) ?? [];
15607
15785
  const totalOptions = optionMatches.length + numberedOptions.length;
@@ -15636,7 +15814,7 @@ async function validateOptionComparison(root, config, issues) {
15636
15814
  }
15637
15815
  async function validateCompetitiveRefs(root, config, issues) {
15638
15816
  const minRefs = config.uiux?.competitive_refs_min ?? 3;
15639
- const contractPattern = path53.posix.join(
15817
+ const contractPattern = path54.posix.join(
15640
15818
  root.replace(/\\/g, "/"),
15641
15819
  config.paths.contractsDir,
15642
15820
  "design",
@@ -15646,11 +15824,11 @@ async function validateCompetitiveRefs(root, config, issues) {
15646
15824
  for (const filePath of contractFiles) {
15647
15825
  let content;
15648
15826
  try {
15649
- content = await readFile38(filePath, "utf-8");
15827
+ content = await readFile39(filePath, "utf-8");
15650
15828
  } catch {
15651
15829
  continue;
15652
15830
  }
15653
- const rel = path53.relative(root, filePath).replace(/\\/g, "/");
15831
+ const rel = path54.relative(root, filePath).replace(/\\/g, "/");
15654
15832
  const refsBlock = extractNestedBlock(content, "competitive_refs");
15655
15833
  if (refsBlock !== null) {
15656
15834
  const refItems = refsBlock.split(/\n/).filter((l) => /^\s*-\s/.test(l));
@@ -15800,7 +15978,7 @@ function collectListItems(section, field) {
15800
15978
  }
15801
15979
 
15802
15980
  // src/core/validators/navigationFlow.ts
15803
- import { readFile as readFile39 } from "fs/promises";
15981
+ import { readFile as readFile40 } from "fs/promises";
15804
15982
  var NODE_DEF_RE = /([A-Za-z_][\w-]*)\s*(?:\[.*?\]|\(.*?\)|\{.*?\})/g;
15805
15983
  var EDGE_RE = /([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?\s*(?:--+>|==+>|-.->|~~>)\s*(?:\|"?([^"|]*)"?\|)?\s*([A-Za-z_][\w-]*)(?:\s*(?:\[[^\]]*\]|\([^)]*\)|\{[^}]*\}))?(?:::\w+)?/g;
15806
15984
  var SUBGRAPH_RE = /^\s*subgraph\s+([\w-]+)/gm;
@@ -16079,7 +16257,7 @@ async function validateNavigationFlow(root, config) {
16079
16257
  const specFiles = allFiles.filter((f) => /[\\/]spec-\d{4}[\\/]/.test(f));
16080
16258
  const issues = [];
16081
16259
  for (const file of specFiles) {
16082
- const content = await readFile39(file, "utf-8");
16260
+ const content = await readFile40(file, "utf-8");
16083
16261
  const blocks = extractFencedCodeBlocks(content);
16084
16262
  const mermaidBlocks = blocks.filter((b) => b.language === "mermaid");
16085
16263
  const _flowchartBlocks = mermaidBlocks.filter((b) => hasFlowchartDeclaration(b.content));
@@ -16101,8 +16279,8 @@ async function validateNavigationFlow(root, config) {
16101
16279
  }
16102
16280
 
16103
16281
  // src/core/validators/renderCritique.ts
16104
- import path54 from "path";
16105
- import { readFile as readFile40 } from "fs/promises";
16282
+ import path55 from "path";
16283
+ import { readFile as readFile41 } from "fs/promises";
16106
16284
  import fg9 from "fast-glob";
16107
16285
  var RENDERED_KEYWORDS_RE = /\b(rendered|screenshot|html\b|preview|visual\s*review)/i;
16108
16286
  var DDP_REFERENCE_RE = /\b(ddp|design\s*direction\s*pack)\b/i;
@@ -16121,31 +16299,31 @@ var FOUR_STATE_CHECK_RE = /\bfour_state_check\s*:/i;
16121
16299
  var MAX_PRIMARY_STEPS_RE = /\bmax_primary_steps\s*:\s*(\d+)/i;
16122
16300
  async function validateRenderCritique(root, config) {
16123
16301
  const issues = [];
16124
- const discussionDir = path54.join(root, config.paths.discussionDir).replace(/\\/g, "/");
16125
- const discussionFiles = await fg9(path54.posix.join(discussionDir, "**/*.md"), { absolute: true });
16302
+ const discussionDir = path55.join(root, config.paths.discussionDir).replace(/\\/g, "/");
16303
+ const discussionFiles = await fg9(path55.posix.join(discussionDir, "**/*.md"), { absolute: true });
16126
16304
  let hasDdp = false;
16127
16305
  for (const df of discussionFiles) {
16128
- const content = await readSafe(df);
16306
+ const content = await readSafe2(df);
16129
16307
  if (/^#{1,3}\s+Design\s+Direction\s+Pack/im.test(content)) {
16130
16308
  hasDdp = true;
16131
16309
  break;
16132
16310
  }
16133
16311
  }
16134
16312
  if (!hasDdp) return issues;
16135
- const skillsDir = path54.join(root, config.paths.skillsDir).replace(/\\/g, "/");
16136
- const evidenceDir = path54.join(root, ".qfai", "evidence").replace(/\\/g, "/");
16313
+ const skillsDir = path55.join(root, config.paths.skillsDir).replace(/\\/g, "/");
16314
+ const evidenceDir = path55.join(root, ".qfai", "evidence").replace(/\\/g, "/");
16137
16315
  const renderEvidenceViewports = await collectRenderEvidenceViewports(root);
16138
- const skillPromptPattern = path54.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
16316
+ const skillPromptPattern = path55.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
16139
16317
  const skillFiles = await fg9(skillPromptPattern, { dot: true });
16140
- const evidencePattern = path54.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
16318
+ const evidencePattern = path55.posix.join(evidenceDir, "{prototyping*,critique-*}.md");
16141
16319
  const evidenceFiles = await fg9(evidencePattern, { dot: true });
16142
16320
  for (const sf of skillFiles) {
16143
- const content = await readSafe(sf);
16321
+ const content = await readSafe2(sf);
16144
16322
  if (content.length > 0 && !RENDERED_KEYWORDS_RE.test(content)) {
16145
16323
  issues.push(
16146
16324
  issue(
16147
16325
  "QFAI-CRIT-001",
16148
- `Skill prompt does not mention rendered/screenshot/HTML review: ${path54.relative(root, sf)}`,
16326
+ `Skill prompt does not mention rendered/screenshot/HTML review: ${path55.relative(root, sf)}`,
16149
16327
  "error",
16150
16328
  sf,
16151
16329
  "renderCritique.codeOnly",
@@ -16157,12 +16335,12 @@ async function validateRenderCritique(root, config) {
16157
16335
  }
16158
16336
  }
16159
16337
  for (const sf of skillFiles) {
16160
- const content = await readSafe(sf);
16338
+ const content = await readSafe2(sf);
16161
16339
  if (content.length > 0 && !DDP_REFERENCE_RE.test(content)) {
16162
16340
  issues.push(
16163
16341
  issue(
16164
16342
  "QFAI-CRIT-002",
16165
- `Downstream skill prompt missing DDP reference: ${path54.relative(root, sf)}`,
16343
+ `Downstream skill prompt missing DDP reference: ${path55.relative(root, sf)}`,
16166
16344
  "error",
16167
16345
  sf,
16168
16346
  "renderCritique.ddpMissing",
@@ -16203,12 +16381,12 @@ async function validateRenderCritique(root, config) {
16203
16381
  );
16204
16382
  }
16205
16383
  for (const sf of skillFiles) {
16206
- const content = await readSafe(sf);
16384
+ const content = await readSafe2(sf);
16207
16385
  if (content.length > 0 && !READ_ORDER_RE.test(content)) {
16208
16386
  issues.push(
16209
16387
  issue(
16210
16388
  "QFAI-CRIT-005",
16211
- `Read order not specified (DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow): ${path54.relative(root, sf)}`,
16389
+ `Read order not specified (DDP \u2192 Design Token \u2192 UI Contract \u2192 HTML Mock \u2192 Flow): ${path55.relative(root, sf)}`,
16212
16390
  "error",
16213
16391
  sf,
16214
16392
  "renderCritique.readOrder",
@@ -16220,7 +16398,7 @@ async function validateRenderCritique(root, config) {
16220
16398
  }
16221
16399
  }
16222
16400
  for (const ef of evidenceFiles) {
16223
- const content = await readSafe(ef);
16401
+ const content = await readSafe2(ef);
16224
16402
  if (content.length === 0) continue;
16225
16403
  const hasDate = EVIDENCE_DATE_RE.test(content);
16226
16404
  const hasViewport = EVIDENCE_VIEWPORT_RE.test(content);
@@ -16235,7 +16413,7 @@ async function validateRenderCritique(root, config) {
16235
16413
  issues.push(
16236
16414
  issue(
16237
16415
  "QFAI-CRIT-006",
16238
- `Critique evidence incomplete (missing: ${missing.join(", ")}): ${path54.relative(root, ef)}`,
16416
+ `Critique evidence incomplete (missing: ${missing.join(", ")}): ${path55.relative(root, ef)}`,
16239
16417
  "error",
16240
16418
  ef,
16241
16419
  "renderCritique.incompleteEvidence",
@@ -16345,14 +16523,14 @@ function hasViewportPass(content, viewport) {
16345
16523
  async function collectContent(files) {
16346
16524
  const contents = [];
16347
16525
  for (const f of files) {
16348
- contents.push(await readSafe(f));
16526
+ contents.push(await readSafe2(f));
16349
16527
  }
16350
16528
  return contents.join("\n---\n");
16351
16529
  }
16352
16530
  async function collectRenderEvidenceViewports(root) {
16353
- const prototypingJsonPath = path54.join(root, ".qfai", "evidence", "prototyping.json");
16531
+ const prototypingJsonPath = path55.join(root, ".qfai", "evidence", "prototyping.json");
16354
16532
  try {
16355
- const raw = await readFile40(prototypingJsonPath, "utf-8");
16533
+ const raw = await readFile41(prototypingJsonPath, "utf-8");
16356
16534
  const parsed = JSON.parse(raw);
16357
16535
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
16358
16536
  return /* @__PURE__ */ new Set();
@@ -16391,8 +16569,8 @@ async function collectRenderEvidenceViewports(root) {
16391
16569
  }
16392
16570
 
16393
16571
  // src/core/validators/designFidelity.ts
16394
- import { readFile as readFile41 } from "fs/promises";
16395
- import path55 from "path";
16572
+ import { readFile as readFile42 } from "fs/promises";
16573
+ import path56 from "path";
16396
16574
  import fg10 from "fast-glob";
16397
16575
  var SCORECARD_HEADING_RE = /^#{1,3}\s+Fidelity\s+Scorecard/im;
16398
16576
  var BASE_DIMENSIONS = ["hierarchy", "clarity", "accessibility", "responsive"];
@@ -16417,19 +16595,19 @@ async function validateDesignFidelity(root, config) {
16417
16595
  const evidenceDirs = [".qfai/evidence", ".qfai/review"];
16418
16596
  const allFiles = [];
16419
16597
  for (const dir of evidenceDirs) {
16420
- const pattern = path55.posix.join(root.replace(/\\/g, "/"), dir, "**/*.md");
16598
+ const pattern = path56.posix.join(root.replace(/\\/g, "/"), dir, "**/*.md");
16421
16599
  const files = await fg10(pattern, { absolute: true });
16422
16600
  allFiles.push(...files);
16423
16601
  }
16424
16602
  for (const filePath of allFiles) {
16425
16603
  let content;
16426
16604
  try {
16427
- content = await readFile41(filePath, "utf-8");
16605
+ content = await readFile42(filePath, "utf-8");
16428
16606
  } catch {
16429
16607
  continue;
16430
16608
  }
16431
16609
  if (!SCORECARD_HEADING_RE.test(content)) continue;
16432
- const rel = path55.relative(root, filePath).replace(/\\/g, "/");
16610
+ const rel = path56.relative(root, filePath).replace(/\\/g, "/");
16433
16611
  const section = extractScorecardSection(content);
16434
16612
  if (!section) continue;
16435
16613
  const dimensions = parseDimensions(section);
@@ -16721,7 +16899,7 @@ function hasAntiPatternMention(section, code) {
16721
16899
  }
16722
16900
 
16723
16901
  // src/core/validators/discussionDesignHardening.ts
16724
- import path56 from "path";
16902
+ import path57 from "path";
16725
16903
  var HTML_TAG_RE = /<(?:style|div|section|span|button|input|form|header|footer|nav|main|aside)\b/i;
16726
16904
  var MERMAID_SCREEN_FLOW_RE = /```mermaid[\s\S]*?(?:stateDiagram|flowchart|graph)[\s\S]*?(?:Screen|Page|View|Dashboard|Login|Settings|Home)\b/i;
16727
16905
  var DDS_HEADING = "## Design Direction Summary";
@@ -16736,10 +16914,17 @@ var DDS_SUBSECTIONS = [
16736
16914
  var REQUIRED_STATES = ["empty", "loading", "error", "populated"];
16737
16915
  var COMPETITIVE_REF_FIELDS = ["adopted_points", "rejected_points", "local_translation"];
16738
16916
  var PLACEHOLDER_RE = /^(?:tbd|todo|n\/a|na|xxx|\?\?\?|placeholder)$/i;
16917
+ var SURFACE_TYPE_RE = /\|\s*Surface Type\s*\|\s*(\S+)\s*\|/i;
16739
16918
  async function isUiBearing(packRoot) {
16740
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16741
- const content = await readSafe(storyPath);
16919
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
16920
+ const content = await readSafe2(storyPath);
16742
16921
  if (!content) return false;
16922
+ const surfaceMatch = SURFACE_TYPE_RE.exec(content);
16923
+ if (surfaceMatch?.[1]) {
16924
+ const surface = surfaceMatch[1].toLowerCase();
16925
+ if (surface === "non-ui") return false;
16926
+ if (["web-ui", "mobile-ui", "desktop-ui", "mixed"].includes(surface)) return true;
16927
+ }
16743
16928
  if (HTML_TAG_RE.test(content)) return true;
16744
16929
  if (MERMAID_SCREEN_FLOW_RE.test(content)) return true;
16745
16930
  return false;
@@ -16774,8 +16959,8 @@ function extractOptionNames(optionSection) {
16774
16959
  }
16775
16960
  async function validateDdsPresence(packRoot) {
16776
16961
  const issues = [];
16777
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16778
- const content = await readSafe(storyPath);
16962
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
16963
+ const content = await readSafe2(storyPath);
16779
16964
  const relPath = "03_Story-Workshop.md";
16780
16965
  if (!content || !content.includes(DDS_HEADING)) {
16781
16966
  issues.push(
@@ -16808,8 +16993,8 @@ async function validateDdsPresence(packRoot) {
16808
16993
  }
16809
16994
  async function validateOptionComparison2(packRoot) {
16810
16995
  const issues = [];
16811
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16812
- const content = await readSafe(storyPath);
16996
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
16997
+ const content = await readSafe2(storyPath);
16813
16998
  if (!content) return issues;
16814
16999
  const dds = extractDdsSection(content);
16815
17000
  if (dds === null) return issues;
@@ -16832,8 +17017,8 @@ async function validateOptionComparison2(packRoot) {
16832
17017
  }
16833
17018
  async function validateAnchorScreen(packRoot) {
16834
17019
  const issues = [];
16835
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16836
- const content = await readSafe(storyPath);
17020
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17021
+ const content = await readSafe2(storyPath);
16837
17022
  if (!content) return issues;
16838
17023
  const dds = extractDdsSection(content);
16839
17024
  if (dds === null) return issues;
@@ -16874,8 +17059,8 @@ async function validateAnchorScreen(packRoot) {
16874
17059
  }
16875
17060
  async function validateCompetitiveRefs2(packRoot) {
16876
17061
  const issues = [];
16877
- const sourcesPath = path56.join(packRoot, "04_Sources.md");
16878
- const content = await readSafe(sourcesPath);
17062
+ const sourcesPath = path57.join(packRoot, "04_Sources.md");
17063
+ const content = await readSafe2(sourcesPath);
16879
17064
  if (!content) return issues;
16880
17065
  const registryHeadingRe = /^##\s+Competitive Reference Registry\b.*$/m;
16881
17066
  const registryMatch = registryHeadingRe.exec(content);
@@ -16931,8 +17116,8 @@ function fieldGuidance(field) {
16931
17116
  }
16932
17117
  async function validateCtaHierarchy(packRoot) {
16933
17118
  const issues = [];
16934
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16935
- const content = await readSafe(storyPath);
17119
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17120
+ const content = await readSafe2(storyPath);
16936
17121
  if (!content) return issues;
16937
17122
  const dds = extractDdsSection(content);
16938
17123
  if (dds === null) return issues;
@@ -16969,8 +17154,8 @@ async function validateCtaHierarchy(packRoot) {
16969
17154
  }
16970
17155
  async function validateStateCoverage(packRoot) {
16971
17156
  const issues = [];
16972
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16973
- const content = await readSafe(storyPath);
17157
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17158
+ const content = await readSafe2(storyPath);
16974
17159
  if (!content) return issues;
16975
17160
  const dds = extractDdsSection(content);
16976
17161
  if (dds === null) return issues;
@@ -16994,8 +17179,8 @@ async function validateStateCoverage(packRoot) {
16994
17179
  }
16995
17180
  async function validateDesignAntiGoals(packRoot) {
16996
17181
  const issues = [];
16997
- const storyPath = path56.join(packRoot, "03_Story-Workshop.md");
16998
- const content = await readSafe(storyPath);
17182
+ const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17183
+ const content = await readSafe2(storyPath);
16999
17184
  if (!content) return issues;
17000
17185
  const dds = extractDdsSection(content);
17001
17186
  if (dds === null) return issues;
@@ -17031,7 +17216,7 @@ async function validateDesignAntiGoals(packRoot) {
17031
17216
  return issues;
17032
17217
  }
17033
17218
  async function validateDiscussionDesignHardening(root, config) {
17034
- const discussionDir = path56.join(root, config.paths.discussionDir);
17219
+ const discussionDir = path57.join(root, config.paths.discussionDir);
17035
17220
  const packRoot = await findLatestDiscussionPackDir(discussionDir);
17036
17221
  if (!packRoot) return [];
17037
17222
  const uiBearing = await isUiBearing(packRoot);
@@ -17049,7 +17234,7 @@ async function validateDiscussionDesignHardening(root, config) {
17049
17234
 
17050
17235
  // src/core/validators/designAudit.ts
17051
17236
  import { readdir as readdir11 } from "fs/promises";
17052
- import path57 from "path";
17237
+ import path58 from "path";
17053
17238
  var COSMETIC_CATEGORIES = ["generic-shell", "stock-imagery", "placeholder-copy"];
17054
17239
  function resolveAuditConfig(config) {
17055
17240
  const audit = config.uiux?.audit;
@@ -17131,7 +17316,7 @@ var RAW_COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)|
17131
17316
  async function checkTokenDrift(root, auditConfig, cfg) {
17132
17317
  const findings = [];
17133
17318
  const configuredDir = cfg.uiux?.designTokensDir;
17134
- const tokensDir = configuredDir ? path57.resolve(root, configuredDir) : path57.join(root, cfg.paths.contractsDir, "design");
17319
+ const tokensDir = configuredDir ? path58.resolve(root, configuredDir) : path58.join(root, cfg.paths.contractsDir, "design");
17135
17320
  let hasTokenFiles = false;
17136
17321
  try {
17137
17322
  const entries = await readdir11(tokensDir);
@@ -17140,7 +17325,7 @@ async function checkTokenDrift(root, auditConfig, cfg) {
17140
17325
  return findings;
17141
17326
  }
17142
17327
  if (!hasTokenFiles) return findings;
17143
- const contractsUiDir = path57.join(root, cfg.paths.contractsDir, "ui");
17328
+ const contractsUiDir = path58.join(root, cfg.paths.contractsDir, "ui");
17144
17329
  let htmlFiles = [];
17145
17330
  try {
17146
17331
  const entries = await readdir11(contractsUiDir);
@@ -17151,7 +17336,7 @@ async function checkTokenDrift(root, auditConfig, cfg) {
17151
17336
  let rawCount = 0;
17152
17337
  const sampleLiterals = [];
17153
17338
  for (const htmlFile of htmlFiles) {
17154
- const content = await readSafe(path57.join(contractsUiDir, htmlFile));
17339
+ const content = await readSafe2(path58.join(contractsUiDir, htmlFile));
17155
17340
  if (!content) continue;
17156
17341
  const matches = content.match(RAW_COLOR_RE);
17157
17342
  if (matches) {
@@ -17202,13 +17387,13 @@ function deduplicateFindings(issues, maxPerRule) {
17202
17387
  async function validateDesignAudit(root, config) {
17203
17388
  const auditConfig = resolveAuditConfig(config);
17204
17389
  if (!auditConfig.enabled) return [];
17205
- const discussionDir = path57.join(root, config.paths.discussionDir);
17390
+ const discussionDir = path58.join(root, config.paths.discussionDir);
17206
17391
  const packRoot = await findLatestDiscussionPackDir(discussionDir);
17207
17392
  if (!packRoot) return [];
17208
17393
  const uiBearing = await isUiBearing(packRoot);
17209
17394
  if (!uiBearing) return [];
17210
- const storyPath = path57.join(packRoot, "03_Story-Workshop.md");
17211
- const content = await readSafe(storyPath);
17395
+ const storyPath = path58.join(packRoot, "03_Story-Workshop.md");
17396
+ const content = await readSafe2(storyPath);
17212
17397
  if (!content) return [];
17213
17398
  const findings = [];
17214
17399
  findings.push(...checkCtaHierarchy(content, auditConfig, "03_Story-Workshop.md"));
@@ -17219,8 +17404,8 @@ async function validateDesignAudit(root, config) {
17219
17404
 
17220
17405
  // src/core/validators/designSlop.ts
17221
17406
  import { existsSync as existsSync2 } from "fs";
17222
- import { readFile as readFile42 } from "fs/promises";
17223
- import path58 from "path";
17407
+ import { readFile as readFile43 } from "fs/promises";
17408
+ import path59 from "path";
17224
17409
  import { fileURLToPath as fileURLToPath4 } from "url";
17225
17410
  function isValidSlopPattern(rule) {
17226
17411
  if (typeof rule !== "object" || rule === null) return false;
@@ -17228,7 +17413,7 @@ function isValidSlopPattern(rule) {
17228
17413
  return typeof r.id === "string" && typeof r.category === "string" && typeof r.tier === "number" && Array.isArray(r.scopes) && typeof r.match === "string" && typeof r.message === "string" && typeof r.guidance === "string";
17229
17414
  }
17230
17415
  async function loadSlopPatterns(jsonPath) {
17231
- const raw = await readFile42(jsonPath, "utf-8");
17416
+ const raw = await readFile43(jsonPath, "utf-8");
17232
17417
  const parsed = JSON.parse(raw);
17233
17418
  if (!Array.isArray(parsed)) return [];
17234
17419
  return parsed.filter((r) => isValidSlopPattern(r));
@@ -17236,11 +17421,11 @@ async function loadSlopPatterns(jsonPath) {
17236
17421
  function defaultPatternsPath() {
17237
17422
  const base = import.meta.url;
17238
17423
  const basePath = base.startsWith("file:") ? fileURLToPath4(base) : base;
17239
- const baseDir = path58.dirname(basePath);
17424
+ const baseDir = path59.dirname(basePath);
17240
17425
  const candidates = [
17241
- path58.join(baseDir, "designSlopPatterns.json"),
17242
- path58.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
17243
- path58.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
17426
+ path59.join(baseDir, "designSlopPatterns.json"),
17427
+ path59.resolve(baseDir, "../../../assets/validators/designSlopPatterns.json"),
17428
+ path59.resolve(baseDir, "../../assets/validators/designSlopPatterns.json")
17244
17429
  ];
17245
17430
  for (const c of candidates) {
17246
17431
  if (existsSync2(c)) return c;
@@ -17251,7 +17436,7 @@ async function validateDesignSlop(root, config) {
17251
17436
  const auditConfig = resolveAuditConfig(config);
17252
17437
  if (!auditConfig.enabled) return [];
17253
17438
  if (!auditConfig.slopDetection) return [];
17254
- const discussionDir = path58.join(root, config.paths.discussionDir);
17439
+ const discussionDir = path59.join(root, config.paths.discussionDir);
17255
17440
  const packRoot = await findLatestDiscussionPackDir(discussionDir);
17256
17441
  if (!packRoot) return [];
17257
17442
  const uiBearing = await isUiBearing(packRoot);
@@ -17272,8 +17457,8 @@ async function validateDesignSlop(root, config) {
17272
17457
  continue;
17273
17458
  }
17274
17459
  for (const scope of pattern.scopes) {
17275
- const filePath = path58.join(packRoot, scope);
17276
- const content = await readSafe(filePath);
17460
+ const filePath = path59.join(packRoot, scope);
17461
+ const content = await readSafe2(filePath);
17277
17462
  if (!content) continue;
17278
17463
  if (regex.test(content) && !seenRules.has(pattern.id)) {
17279
17464
  seenRules.add(pattern.id);
@@ -17293,6 +17478,460 @@ async function validateDesignSlop(root, config) {
17293
17478
  return findings.map((f) => findingToIssue(f, auditConfig.qualityProfile, "slop"));
17294
17479
  }
17295
17480
 
17481
+ // src/core/validators/uixValidators.ts
17482
+ import { readdir as readdir13 } from "fs/promises";
17483
+ import path61 from "path";
17484
+
17485
+ // src/core/validators/uixDetection.ts
17486
+ import { readdir as readdir12 } from "fs/promises";
17487
+ import path60 from "path";
17488
+ var UI_BEARING_SURFACES = /* @__PURE__ */ new Set(["web-ui", "mobile-ui", "desktop-ui", "mixed"]);
17489
+ var NON_UI_SURFACES = /* @__PURE__ */ new Set(["non-ui"]);
17490
+ function stripCodeBlocks(content) {
17491
+ let stripped = content.replace(/```[\s\S]*?```/g, "");
17492
+ stripped = stripped.replace(/`[^`]+`/g, "");
17493
+ return stripped;
17494
+ }
17495
+ var HTML_TAG_RE2 = /<(?:style|div|section|span|button|input|form|header|footer|nav|main|aside)\b/i;
17496
+ var MERMAID_SCREEN_FLOW_RE2 = /```mermaid[\s\S]*?(?:stateDiagram)[\s\S]*?(?:Screen|Page|View|Dashboard|Login|Settings|Home)\b/i;
17497
+ var SCREEN_CONTRACT_YAML_RE = /screens:\s*\n\s*-\s*route:/;
17498
+ async function isUiBearingSpec(root) {
17499
+ const specContent = await readSafe2(path60.join(root, "01_Spec.md"));
17500
+ const surfaceMatch = /^\s*-\s*surface:\s*(\S+)/im.exec(specContent);
17501
+ if (surfaceMatch?.[1]) {
17502
+ const surface = surfaceMatch[1].toLowerCase();
17503
+ if (UI_BEARING_SURFACES.has(surface)) return true;
17504
+ if (NON_UI_SURFACES.has(surface)) return false;
17505
+ }
17506
+ try {
17507
+ await readdir12(path60.join(root, "uiux"));
17508
+ return true;
17509
+ } catch {
17510
+ }
17511
+ const contractsContent = await readSafe2(path60.join(root, "uiux", "40_contracts.md"));
17512
+ if (contractsContent && SCREEN_CONTRACT_YAML_RE.test(contractsContent)) return true;
17513
+ const storyContent = await readSafe2(path60.join(root, "03_Story-Workshop.md"));
17514
+ if (storyContent) {
17515
+ const stripped = stripCodeBlocks(storyContent);
17516
+ if (HTML_TAG_RE2.test(stripped)) return true;
17517
+ if (MERMAID_SCREEN_FLOW_RE2.test(storyContent)) return true;
17518
+ }
17519
+ return false;
17520
+ }
17521
+
17522
+ // src/core/validators/uixValidators.ts
17523
+ function uixIssue(code, message, severity, file, suggestedAction) {
17524
+ return {
17525
+ code,
17526
+ severity,
17527
+ category: "compatibility",
17528
+ message,
17529
+ file,
17530
+ suggested_action: suggestedAction
17531
+ };
17532
+ }
17533
+ function parseSimpleYaml(content) {
17534
+ const result = {};
17535
+ const store = (rawKey, value) => {
17536
+ const key = rawKey.trim().toLowerCase().replace(/\s+/g, "_");
17537
+ if (key && value) result[key] = value;
17538
+ };
17539
+ for (const line of content.split("\n")) {
17540
+ const yamlMatch = /^\s*(\w[\w_]*):\s*(.*)$/.exec(line);
17541
+ if (yamlMatch?.[1] !== void 0 && yamlMatch[2] !== void 0) {
17542
+ store(yamlMatch[1], yamlMatch[2].trim());
17543
+ continue;
17544
+ }
17545
+ const bulletMatch = /^\s*-\s+([\w][\w\s]*?):\s+(.+)$/.exec(line);
17546
+ if (bulletMatch?.[1] !== void 0 && bulletMatch[2] !== void 0) {
17547
+ store(bulletMatch[1], bulletMatch[2].trim());
17548
+ continue;
17549
+ }
17550
+ const tableMatch = /^\s*\|\s*(\w[\w\s_]*?)\s*\|\s*(.+?)\s*\|\s*$/.exec(line);
17551
+ if (tableMatch?.[1] !== void 0 && tableMatch[2] !== void 0) {
17552
+ if (!/^[-:]+$/.test(tableMatch[2].trim())) {
17553
+ store(tableMatch[1], tableMatch[2].trim());
17554
+ }
17555
+ }
17556
+ }
17557
+ return result;
17558
+ }
17559
+ async function validateSidecarMissing(root, _config) {
17560
+ if (!await isUiBearingSpec(root)) return [];
17561
+ try {
17562
+ await readdir13(path61.join(root, "uiux"));
17563
+ return [];
17564
+ } catch {
17565
+ return [
17566
+ uixIssue(
17567
+ "UIX-VAL-SIDECAR-MISSING",
17568
+ "UI-bearing spec detected but uiux/ sidecar directory is missing.",
17569
+ "error",
17570
+ "uiux/",
17571
+ "Create the uiux/ directory and populate it with the sidecar template files."
17572
+ )
17573
+ ];
17574
+ }
17575
+ }
17576
+ var STRATEGY_REQUIRED_FIELDS_LEGACY = [
17577
+ "selection_required",
17578
+ "candidate_options",
17579
+ "chosen_option",
17580
+ "verification_expectations",
17581
+ "none_as_legitimate_outcome"
17582
+ ];
17583
+ var STRATEGY_REQUIRED_FIELDS_CURRENT = ["surface_type", "approach", "rationale"];
17584
+ var STRATEGY_MIN_LENGTH_FIELDS = ["rationale", "approach"];
17585
+ var STRATEGY_MIN_LENGTH = 20;
17586
+ async function validateStrategyCompleteness(root, _config) {
17587
+ if (!await isUiBearingSpec(root)) return [];
17588
+ const strategyPath = path61.join(root, "uiux", "10_strategy.md");
17589
+ const content = await readSafe2(strategyPath);
17590
+ if (!content) return [];
17591
+ const parsed = parseSimpleYaml(content);
17592
+ const issues = [];
17593
+ const relPath = "uiux/10_strategy.md";
17594
+ const isCurrentFormat = parsed["surface_type"] !== void 0;
17595
+ const requiredFields = isCurrentFormat ? STRATEGY_REQUIRED_FIELDS_CURRENT : STRATEGY_REQUIRED_FIELDS_LEGACY;
17596
+ for (const field of requiredFields) {
17597
+ const value = parsed[field];
17598
+ if (value === void 0 || value === "") {
17599
+ issues.push(
17600
+ uixIssue(
17601
+ "UIX-VAL-STRATEGY-INCOMPLETE",
17602
+ `Strategy field '${field}' is missing or empty in 10_strategy.md.`,
17603
+ "error",
17604
+ relPath,
17605
+ `Add the '${field}' field to uiux/10_strategy.md with a valid value.`
17606
+ )
17607
+ );
17608
+ }
17609
+ }
17610
+ for (const field of STRATEGY_MIN_LENGTH_FIELDS) {
17611
+ const value = parsed[field];
17612
+ if (value === void 0 || value.length < STRATEGY_MIN_LENGTH) {
17613
+ issues.push(
17614
+ uixIssue(
17615
+ "UIX-VAL-STRATEGY-INCOMPLETE",
17616
+ `Strategy field '${field}' must be at least ${STRATEGY_MIN_LENGTH} characters (current: ${value?.length ?? 0}).`,
17617
+ "error",
17618
+ relPath,
17619
+ `Expand the '${field}' field in uiux/10_strategy.md to at least ${STRATEGY_MIN_LENGTH} characters.`
17620
+ )
17621
+ );
17622
+ }
17623
+ }
17624
+ return issues;
17625
+ }
17626
+ async function validateScoringAxes(root, _config) {
17627
+ if (!await isUiBearingSpec(root)) return [];
17628
+ const singlePath = path61.join(root, "uiux", "20_eval_axes.md");
17629
+ let content = await readSafe2(singlePath);
17630
+ let relPath = "uiux/20_eval_axes.md";
17631
+ if (!content) {
17632
+ const splitFiles = [
17633
+ "20_eval_axis_usability.md",
17634
+ "21_eval_axis_consistency.md",
17635
+ "22_eval_axis_accessibility.md",
17636
+ "23_eval_axis_delight.md"
17637
+ ];
17638
+ const parts = [];
17639
+ for (const f of splitFiles) {
17640
+ const c = await readSafe2(path61.join(root, "uiux", f));
17641
+ if (c) parts.push(c);
17642
+ }
17643
+ if (parts.length > 0) {
17644
+ content = parts.join("\n");
17645
+ relPath = "uiux/20_eval_axis_*.md";
17646
+ }
17647
+ }
17648
+ if (!content) return [];
17649
+ const issues = [];
17650
+ const lines = content.split("\n");
17651
+ let inTrendSection = false;
17652
+ for (const line of lines) {
17653
+ if (/trend[_-]derived/i.test(line)) {
17654
+ inTrendSection = true;
17655
+ continue;
17656
+ }
17657
+ if (inTrendSection && /^#+\s/.test(line)) {
17658
+ inTrendSection = false;
17659
+ continue;
17660
+ }
17661
+ const isBulletRow = /^\s*-\s/.test(line);
17662
+ const isTableRow2 = /^\s*\|/.test(line) && !/^\s*\|[\s-:|]+\|[\s-:|]*$/.test(line);
17663
+ if (inTrendSection && (isBulletRow || isTableRow2)) {
17664
+ if (!/source_translation/i.test(line)) {
17665
+ issues.push(
17666
+ uixIssue(
17667
+ "UIX-VAL-SCORING-AXIS-INCOMPLETE",
17668
+ "Trend-derived scoring axis row is missing source_translation field.",
17669
+ "error",
17670
+ relPath,
17671
+ "Add source_translation to each trend-derived evaluation axis row."
17672
+ )
17673
+ );
17674
+ }
17675
+ }
17676
+ }
17677
+ return issues;
17678
+ }
17679
+ var AGGREGATE_REQUIRED_FIELDS = ["weights", "normalization", "threshold"];
17680
+ async function validateAggregateScoringRules(root, _config) {
17681
+ if (!await isUiBearingSpec(root)) return [];
17682
+ const aggregatePath = path61.join(root, "uiux", "21_aggregate_scoring.md");
17683
+ let content = await readSafe2(aggregatePath);
17684
+ let relPath = "uiux/21_aggregate_scoring.md";
17685
+ if (!content) {
17686
+ const delightContent = await readSafe2(path61.join(root, "uiux", "23_eval_axis_delight.md"));
17687
+ if (delightContent) {
17688
+ const lines = delightContent.split("\n");
17689
+ let inSection = false;
17690
+ const sectionLines = [];
17691
+ for (const line of lines) {
17692
+ if (/^#+\s*Aggregate\s+Scoring/i.test(line)) {
17693
+ inSection = true;
17694
+ continue;
17695
+ }
17696
+ if (inSection && /^#+\s/.test(line)) break;
17697
+ if (inSection) sectionLines.push(line);
17698
+ }
17699
+ if (sectionLines.length > 0) {
17700
+ content = sectionLines.join("\n");
17701
+ relPath = "uiux/23_eval_axis_delight.md";
17702
+ }
17703
+ }
17704
+ }
17705
+ if (!content) return [];
17706
+ const parsed = parseSimpleYaml(content);
17707
+ const issues = [];
17708
+ for (const field of AGGREGATE_REQUIRED_FIELDS) {
17709
+ const value = parsed[field] ?? parsed[`${field}s`];
17710
+ if (value === void 0 || value === "") {
17711
+ issues.push(
17712
+ uixIssue(
17713
+ "UIX-VAL-AGGREGATE-SCORING-INCOMPLETE",
17714
+ `Aggregate scoring field '${field}' is missing in ${relPath}.`,
17715
+ "error",
17716
+ relPath,
17717
+ `Add the '${field}' field to ${relPath}.`
17718
+ )
17719
+ );
17720
+ }
17721
+ }
17722
+ return issues;
17723
+ }
17724
+ async function validateOptionComparison3(root, _config) {
17725
+ if (!await isUiBearingSpec(root)) return [];
17726
+ const issues = [];
17727
+ const compPath = path61.join(root, "uiux", "30_comparison.md");
17728
+ const compContent = await readSafe2(compPath);
17729
+ if (compContent) {
17730
+ const headingOptions = compContent.match(/^##\s+Option\b/gim);
17731
+ const tableOptions = compContent.match(/\bOption\s+[A-Z]\b/gim);
17732
+ const uniqueTableOptions = tableOptions ? new Set(tableOptions.map((m) => m.trim().toUpperCase())).size : 0;
17733
+ const optionCount = Math.max(headingOptions?.length ?? 0, uniqueTableOptions);
17734
+ if (optionCount < 2) {
17735
+ issues.push(
17736
+ uixIssue(
17737
+ "UIX-VAL-COMPARISON-INSUFFICIENT",
17738
+ "30_comparison.md must contain at least 2 options for meaningful comparison.",
17739
+ "error",
17740
+ "uiux/30_comparison.md",
17741
+ "Add at least 2 '## Option' sections to uiux/30_comparison.md."
17742
+ )
17743
+ );
17744
+ }
17745
+ }
17746
+ const anchorPath = path61.join(root, "uiux", "31_anchor.md");
17747
+ const anchorContent = await readSafe2(anchorPath);
17748
+ if (anchorContent) {
17749
+ if (!/selected_anchor\s*:/i.test(anchorContent) && !/chosen\s*:/i.test(anchorContent) && !/source\s+option\s*:/i.test(anchorContent)) {
17750
+ issues.push(
17751
+ uixIssue(
17752
+ "UIX-VAL-ANCHOR-MISSING",
17753
+ "31_anchor.md is missing a selected anchor declaration.",
17754
+ "error",
17755
+ "uiux/31_anchor.md",
17756
+ "Add a 'selected_anchor:' field to uiux/31_anchor.md."
17757
+ )
17758
+ );
17759
+ }
17760
+ }
17761
+ return issues;
17762
+ }
17763
+ var SCREEN_CONTRACT_REQUIRED_FIELDS = [
17764
+ "route",
17765
+ "actor",
17766
+ "purpose",
17767
+ "primary_tasks",
17768
+ "required_states",
17769
+ "transitions",
17770
+ "observable_outcomes"
17771
+ ];
17772
+ async function validateScreenContracts(root, _config) {
17773
+ if (!await isUiBearingSpec(root)) return [];
17774
+ const contractsPath = path61.join(root, "uiux", "40_contracts.md");
17775
+ const content = await readSafe2(contractsPath);
17776
+ if (!content) return [];
17777
+ const parsed = parseSimpleYaml(content);
17778
+ const issues = [];
17779
+ const relPath = "uiux/40_contracts.md";
17780
+ const sectionHeadings = {
17781
+ primary_tasks: /^#{3,4}\s+Primary\s+Tasks/im,
17782
+ required_states: /^#{3,4}\s+Required\s+States/im,
17783
+ transitions: /^#{3,4}\s+Transitions/im,
17784
+ observable_outcomes: /^#{3,4}\s+Observable\s+Outcomes/im
17785
+ };
17786
+ for (const field of SCREEN_CONTRACT_REQUIRED_FIELDS) {
17787
+ const value = parsed[field];
17788
+ const hasValue = value !== void 0 && value !== "";
17789
+ const headingPattern = sectionHeadings[field];
17790
+ const hasHeading = headingPattern ? headingPattern.test(content) : false;
17791
+ if (!hasValue && !hasHeading) {
17792
+ issues.push(
17793
+ uixIssue(
17794
+ "UIX-VAL-SCREEN-CONTRACT-INCOMPLETE",
17795
+ `Screen contract field '${field}' is missing in 40_contracts.md.`,
17796
+ "error",
17797
+ relPath,
17798
+ `Add the '${field}' field to screen contracts in uiux/40_contracts.md.`
17799
+ )
17800
+ );
17801
+ }
17802
+ }
17803
+ return issues;
17804
+ }
17805
+ async function validateOqClosure(root, _config) {
17806
+ if (!await isUiBearingSpec(root)) return [];
17807
+ const rootOqPath = path61.join(root, "11_OQ-Register.md");
17808
+ const uiuxOqPath = path61.join(root, "uiux", "11_OQ-Register.md");
17809
+ const rootContent = await readSafe2(rootOqPath);
17810
+ const content = rootContent || await readSafe2(uiuxOqPath);
17811
+ if (!content) return [];
17812
+ const issues = [];
17813
+ const relPath = rootContent ? "11_OQ-Register.md" : "uiux/11_OQ-Register.md";
17814
+ const oqBlocks = content.split(/(?=^##\s+OQ-\d{4})/m);
17815
+ for (const block of oqBlocks) {
17816
+ const idMatch = /^##\s+(OQ-\d{4})/m.exec(block);
17817
+ if (!idMatch?.[1]) continue;
17818
+ const oqId = idMatch[1];
17819
+ const isOpen = /status\s*:\s*open/i.test(block);
17820
+ const isCritical = /severity\s*:\s*(?:critical|blocking)/i.test(block);
17821
+ if (isOpen && isCritical) {
17822
+ issues.push(
17823
+ uixIssue(
17824
+ "UIX-VAL-OQ-OPEN-CRITICAL",
17825
+ `Open critical OQ found: ${oqId}. Must be resolved before proceeding.`,
17826
+ "error",
17827
+ relPath,
17828
+ `Resolve or downgrade ${oqId} in uiux/11_OQ-Register.md before validation can pass.`
17829
+ )
17830
+ );
17831
+ }
17832
+ }
17833
+ const tableRowRegex = /^\s*\|\s*(OQ-\d{4})\s*\|/;
17834
+ for (const line of content.split("\n")) {
17835
+ const rowMatch = tableRowRegex.exec(line);
17836
+ if (!rowMatch?.[1]) continue;
17837
+ const oqId = rowMatch[1];
17838
+ const cols = line.split("|").map((c) => c.trim()).filter(Boolean);
17839
+ const disposition = cols[3]?.toLowerCase();
17840
+ if (disposition === "open") {
17841
+ const fullRow = line.toLowerCase();
17842
+ if (/critical|blocking/i.test(fullRow)) {
17843
+ issues.push(
17844
+ uixIssue(
17845
+ "UIX-VAL-OQ-OPEN-CRITICAL",
17846
+ `Open critical OQ found: ${oqId}. Must be resolved before proceeding.`,
17847
+ "error",
17848
+ relPath,
17849
+ `Resolve or downgrade ${oqId} in ${relPath} before validation can pass.`
17850
+ )
17851
+ );
17852
+ }
17853
+ }
17854
+ }
17855
+ return issues;
17856
+ }
17857
+ var CURRENT_SIDECAR_VERSION = "1.0.0";
17858
+ async function validateMigration(root, config) {
17859
+ if (!await isUiBearingSpec(root)) return [];
17860
+ const strict = config.uiux?.migration?.strict === true;
17861
+ const issues = [];
17862
+ try {
17863
+ await readdir13(path61.join(root, "uiux"));
17864
+ } catch {
17865
+ issues.push(
17866
+ uixIssue(
17867
+ "UIX-VAL-MIGRATION-SIDECAR-MISSING",
17868
+ "UI-bearing spec detected but uiux/ sidecar directory is missing. Migration required.",
17869
+ strict ? "error" : "warning",
17870
+ "uiux/",
17871
+ "Run the UIX sidecar migration to create the uiux/ directory structure."
17872
+ )
17873
+ );
17874
+ return issues;
17875
+ }
17876
+ const versionPath = path61.join(root, "uiux", ".sidecar-version");
17877
+ const versionContent = await readSafe2(versionPath);
17878
+ if (versionContent) {
17879
+ const version = versionContent.trim();
17880
+ if (version && version !== CURRENT_SIDECAR_VERSION) {
17881
+ issues.push(
17882
+ uixIssue(
17883
+ "UIX-VAL-MIGRATION-STALE-VERSION",
17884
+ `Sidecar template version '${version}' is outdated (current: ${CURRENT_SIDECAR_VERSION}).`,
17885
+ "warning",
17886
+ "uiux/.sidecar-version",
17887
+ `Upgrade sidecar template from ${version} to ${CURRENT_SIDECAR_VERSION}. Run the migration tool for upgrade steps.`
17888
+ )
17889
+ );
17890
+ }
17891
+ }
17892
+ return issues;
17893
+ }
17894
+ function applyPhase1Ratchet(issues, releaseDate, now = /* @__PURE__ */ new Date()) {
17895
+ const phase1EndMs = releaseDate.getTime() + 30 * 24 * 60 * 60 * 1e3;
17896
+ if (now.getTime() > phase1EndMs) return issues;
17897
+ return issues.map((iss) => {
17898
+ if (iss.code.startsWith("UIX-VAL-") && iss.severity === "error") {
17899
+ return { ...iss, severity: "warning" };
17900
+ }
17901
+ return iss;
17902
+ });
17903
+ }
17904
+ async function runAllUixValidators(root, config) {
17905
+ let effectiveRoot = root;
17906
+ const directSpec = await readSafe2(path61.join(root, "01_Spec.md"));
17907
+ if (!directSpec) {
17908
+ const discussionDir = path61.join(root, config.paths.discussionDir);
17909
+ const packRoot = await findLatestDiscussionPackDir(discussionDir);
17910
+ if (!packRoot) return [];
17911
+ effectiveRoot = packRoot;
17912
+ }
17913
+ const validators = [
17914
+ validateSidecarMissing,
17915
+ validateStrategyCompleteness,
17916
+ validateScoringAxes,
17917
+ validateAggregateScoringRules,
17918
+ validateOptionComparison3,
17919
+ validateScreenContracts,
17920
+ validateOqClosure,
17921
+ validateMigration
17922
+ ];
17923
+ const results = await Promise.all(validators.map((v) => v(effectiveRoot, config)));
17924
+ let issues = results.flat();
17925
+ const relDateStr = config.uiux?.phase1ReleaseDate;
17926
+ if (relDateStr) {
17927
+ const releaseDate = new Date(relDateStr);
17928
+ if (!isNaN(releaseDate.getTime())) {
17929
+ issues = applyPhase1Ratchet(issues, releaseDate);
17930
+ }
17931
+ }
17932
+ return issues;
17933
+ }
17934
+
17296
17935
  // src/core/validate.ts
17297
17936
  var UIUX_VALIDATION_BUDGET_MS = 2e3;
17298
17937
  async function validateProject(root, configResult, options = {}) {
@@ -17312,7 +17951,8 @@ async function validateProject(root, configResult, options = {}) {
17312
17951
  () => validateResearchSummary(root, config),
17313
17952
  () => validateAgentDefinition(root, config),
17314
17953
  () => validateDesignAudit(root, config),
17315
- () => validateDesignSlop(root, config)
17954
+ () => validateDesignSlop(root, config),
17955
+ () => runAllUixValidators(root, config)
17316
17956
  ];
17317
17957
  const uiuxIssueGroups = await Promise.all(uiuxValidators.map((validator) => validator()));
17318
17958
  const uiuxIssues = [...platformResult.issues, ...uiuxIssueGroups.flat()];
@@ -17399,15 +18039,15 @@ var REPORT_GUARDRAILS_MAX = 20;
17399
18039
  var REPORT_TEST_STRATEGY_SAMPLE_LIMIT = 20;
17400
18040
  var SC_TAG_RE4 = /^SC-\d{4}-\d{4}$/;
17401
18041
  async function createReportData(root, validation, configResult) {
17402
- const resolvedRoot = path59.resolve(root);
18042
+ const resolvedRoot = path62.resolve(root);
17403
18043
  const resolved = configResult ?? await loadConfig(resolvedRoot);
17404
18044
  const config = resolved.config;
17405
18045
  const configPath = resolved.configPath;
17406
18046
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
17407
18047
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
17408
- const apiRoot = path59.join(contractsRoot, "api");
17409
- const uiRoot = path59.join(contractsRoot, "ui");
17410
- const dbRoot = path59.join(contractsRoot, "db");
18048
+ const apiRoot = path62.join(contractsRoot, "api");
18049
+ const uiRoot = path62.join(contractsRoot, "ui");
18050
+ const dbRoot = path62.join(contractsRoot, "db");
17411
18051
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
17412
18052
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
17413
18053
  const specEntries = await collectSpecEntries(specsRoot);
@@ -18344,7 +18984,7 @@ async function collectChangeTypeSummary(specsRoot) {
18344
18984
  };
18345
18985
  const deltaFiles = await collectDeltaFiles(specsRoot);
18346
18986
  for (const deltaFile of deltaFiles) {
18347
- const text = await readFile43(deltaFile, "utf-8");
18987
+ const text = await readFile44(deltaFile, "utf-8");
18348
18988
  const parsed = parseDeltaV1(text);
18349
18989
  for (const entry of parsed.entries) {
18350
18990
  if (!entry.meta) {
@@ -18381,7 +19021,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
18381
19021
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
18382
19022
  }
18383
19023
  for (const file of specFiles) {
18384
- const text = await readFile43(file, "utf-8");
19024
+ const text = await readFile44(file, "utf-8");
18385
19025
  const parsed = parseSpec(text, file);
18386
19026
  const specKey = parsed.specId;
18387
19027
  if (!specKey) {
@@ -18418,7 +19058,7 @@ async function collectIds(files) {
18418
19058
  result[prefix] = /* @__PURE__ */ new Set();
18419
19059
  }
18420
19060
  for (const file of files) {
18421
- const text = await readFile43(file, "utf-8");
19061
+ const text = await readFile44(file, "utf-8");
18422
19062
  for (const prefix of ID_PREFIXES) {
18423
19063
  const ids = extractIds(text, prefix);
18424
19064
  ids.forEach((id) => result[prefix].add(id));
@@ -18433,7 +19073,7 @@ async function collectIds(files) {
18433
19073
  async function collectUpstreamIds(files) {
18434
19074
  const ids = /* @__PURE__ */ new Set();
18435
19075
  for (const file of files) {
18436
- const text = await readFile43(file, "utf-8");
19076
+ const text = await readFile44(file, "utf-8");
18437
19077
  extractAllIds(text).forEach((id) => ids.add(id));
18438
19078
  }
18439
19079
  return ids;
@@ -18454,7 +19094,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
18454
19094
  }
18455
19095
  const pattern = buildIdPattern(Array.from(upstreamIds));
18456
19096
  for (const file of targetFiles) {
18457
- const text = await readFile43(file, "utf-8");
19097
+ const text = await readFile44(file, "utf-8");
18458
19098
  if (pattern.test(text)) {
18459
19099
  return true;
18460
19100
  }
@@ -18573,7 +19213,7 @@ function normalizeScSources(root, sources) {
18573
19213
  async function countScenarios(scenarioFiles) {
18574
19214
  let total = 0;
18575
19215
  for (const file of scenarioFiles) {
18576
- const text = await readFile43(file, "utf-8");
19216
+ const text = await readFile44(file, "utf-8");
18577
19217
  const { document, errors } = parseScenarioDocument(text, file);
18578
19218
  if (!document || errors.length > 0) {
18579
19219
  continue;
@@ -18604,7 +19244,7 @@ async function collectTestStrategy(scenarioFiles, root, config, limit) {
18604
19244
  let totalScenarios = 0;
18605
19245
  let e2eCount = 0;
18606
19246
  for (const file of scenarioFiles) {
18607
- const text = await readFile43(file, "utf-8");
19247
+ const text = await readFile44(file, "utf-8");
18608
19248
  const { document, errors } = parseScenarioDocument(text, file);
18609
19249
  if (!document || errors.length > 0) {
18610
19250
  continue;
@@ -18692,10 +19332,10 @@ function buildHotspots(issues) {
18692
19332
  async function collectTddCoverage(entries) {
18693
19333
  const specs = [];
18694
19334
  for (const entry of entries) {
18695
- const testCasesPath = path59.join(entry.dir, "06_Test-Cases.md");
19335
+ const testCasesPath = path62.join(entry.dir, "06_Test-Cases.md");
18696
19336
  let tcContent;
18697
19337
  try {
18698
- tcContent = await readFile43(testCasesPath, "utf-8");
19338
+ tcContent = await readFile44(testCasesPath, "utf-8");
18699
19339
  } catch {
18700
19340
  continue;
18701
19341
  }
@@ -18727,10 +19367,10 @@ async function collectTddCoverage(entries) {
18727
19367
  });
18728
19368
  continue;
18729
19369
  }
18730
- const tddListPath = path59.join(entry.dir, "tdd", "test-list.md");
19370
+ const tddListPath = path62.join(entry.dir, "tdd", "test-list.md");
18731
19371
  let tddContent;
18732
19372
  try {
18733
- tddContent = await readFile43(tddListPath, "utf-8");
19373
+ tddContent = await readFile44(tddListPath, "utf-8");
18734
19374
  } catch {
18735
19375
  specs.push({
18736
19376
  specNumber: entry.specNumber,
@@ -18813,8 +19453,8 @@ async function collectTddCoverage(entries) {
18813
19453
  }
18814
19454
 
18815
19455
  // src/core/specPackReport.ts
18816
- import { mkdir as mkdir7, readFile as readFile44, writeFile as writeFile6 } from "fs/promises";
18817
- import path60 from "path";
19456
+ import { mkdir as mkdir7, readFile as readFile45, writeFile as writeFile6 } from "fs/promises";
19457
+ import path63 from "path";
18818
19458
  var REQUIRED_LEDGER_COLUMNS = [
18819
19459
  "trace_id",
18820
19460
  "obj_id",
@@ -18832,8 +19472,8 @@ async function writeSpecPackReports(root, config) {
18832
19472
  const entries = await collectSpecEntries(specsRoot);
18833
19473
  const contractIndex = await buildContractIndex(root, config);
18834
19474
  for (const entry of entries) {
18835
- const specName = path60.basename(entry.dir);
18836
- const outputDir = path60.join(outRoot, specName);
19475
+ const specName = path63.basename(entry.dir);
19476
+ const outputDir = path63.join(outRoot, specName);
18837
19477
  await mkdir7(outputDir, { recursive: true });
18838
19478
  const [acText, tcText, exText, ledgerText] = await Promise.all([
18839
19479
  readSafe12(entry.acceptanceCriteriaPath),
@@ -18860,13 +19500,13 @@ async function writeSpecPackReports(root, config) {
18860
19500
  });
18861
19501
  const graph = buildTraceabilityGraph(ledgerRows);
18862
19502
  await writeFile6(
18863
- path60.join(outputDir, "coverage.md"),
19503
+ path63.join(outputDir, "coverage.md"),
18864
19504
  `${formatCoverageMarkdown(specName, coverage)}
18865
19505
  `,
18866
19506
  "utf-8"
18867
19507
  );
18868
19508
  await writeFile6(
18869
- path60.join(outputDir, "traceability-graph.json"),
19509
+ path63.join(outputDir, "traceability-graph.json"),
18870
19510
  `${JSON.stringify(graph, null, 2)}
18871
19511
  `,
18872
19512
  "utf-8"
@@ -19041,7 +19681,7 @@ function getCell(row, indexByColumn, column) {
19041
19681
  }
19042
19682
  async function readSafe12(filePath) {
19043
19683
  try {
19044
- return await readFile44(filePath, "utf-8");
19684
+ return await readFile45(filePath, "utf-8");
19045
19685
  } catch {
19046
19686
  return "";
19047
19687
  }
@@ -19059,7 +19699,7 @@ function warnIfTruncated(scan, context) {
19059
19699
 
19060
19700
  // src/cli/commands/report.ts
19061
19701
  async function runReport(options) {
19062
- const root = path61.resolve(options.root);
19702
+ const root = path64.resolve(options.root);
19063
19703
  const configResult = await loadConfig(root);
19064
19704
  let validation;
19065
19705
  let blockedByPhaseGuard = false;
@@ -19075,7 +19715,7 @@ async function runReport(options) {
19075
19715
  validation = normalized;
19076
19716
  } else {
19077
19717
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
19078
- const inputPath = path61.isAbsolute(input) ? input : path61.resolve(root, input);
19718
+ const inputPath = path64.isAbsolute(input) ? input : path64.resolve(root, input);
19079
19719
  try {
19080
19720
  validation = await readValidationResult(inputPath);
19081
19721
  } catch (err) {
@@ -19102,10 +19742,10 @@ async function runReport(options) {
19102
19742
  warnIfTruncated(data.traceability.testFiles, "report");
19103
19743
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
19104
19744
  const outRoot = resolvePath(root, configResult.config, "outDir");
19105
- const defaultOut = options.format === "json" ? path61.join(outRoot, "report.json") : path61.join(outRoot, "report.md");
19745
+ const defaultOut = options.format === "json" ? path64.join(outRoot, "report.json") : path64.join(outRoot, "report.md");
19106
19746
  const out = options.outPath ?? defaultOut;
19107
- const outPath = path61.isAbsolute(out) ? out : path61.resolve(root, out);
19108
- await mkdir8(path61.dirname(outPath), { recursive: true });
19747
+ const outPath = path64.isAbsolute(out) ? out : path64.resolve(root, out);
19748
+ await mkdir8(path64.dirname(outPath), { recursive: true });
19109
19749
  await writeFile7(outPath, `${output}
19110
19750
  `, "utf-8");
19111
19751
  await writeSpecPackReports(root, configResult.config);
@@ -19121,7 +19761,7 @@ async function runReport(options) {
19121
19761
  info(`wrote report: ${outPath}`);
19122
19762
  }
19123
19763
  async function readValidationResult(inputPath) {
19124
- const raw = await readFile45(inputPath, "utf-8");
19764
+ const raw = await readFile46(inputPath, "utf-8");
19125
19765
  const parsed = JSON.parse(raw);
19126
19766
  if (!isValidationResult(parsed)) {
19127
19767
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -19181,21 +19821,21 @@ function isMissingFileError2(error2) {
19181
19821
  return record2.code === "ENOENT";
19182
19822
  }
19183
19823
  async function writeValidationResult(root, outputPath, result) {
19184
- const abs = path61.isAbsolute(outputPath) ? outputPath : path61.resolve(root, outputPath);
19185
- await mkdir8(path61.dirname(abs), { recursive: true });
19824
+ const abs = path64.isAbsolute(outputPath) ? outputPath : path64.resolve(root, outputPath);
19825
+ await mkdir8(path64.dirname(abs), { recursive: true });
19186
19826
  await writeFile7(abs, `${JSON.stringify(result, null, 2)}
19187
19827
  `, "utf-8");
19188
19828
  }
19189
19829
 
19190
19830
  // src/cli/commands/validate.ts
19191
19831
  import { mkdir as mkdir10, writeFile as writeFile9 } from "fs/promises";
19192
- import path63 from "path";
19832
+ import path66 from "path";
19193
19833
 
19194
19834
  // src/core/runLog.ts
19195
19835
  import { mkdir as mkdir9, writeFile as writeFile8 } from "fs/promises";
19196
- import path62 from "path";
19836
+ import path65 from "path";
19197
19837
  async function writeValidateRunLog(input) {
19198
- const root = path62.resolve(input.root);
19838
+ const root = path65.resolve(input.root);
19199
19839
  const outDir = resolvePath(root, input.config, "outDir");
19200
19840
  await mkdir9(outDir, { recursive: true });
19201
19841
  const { runId, reportDir } = await allocateRunReportDir(outDir, input.startedAt);
@@ -19242,10 +19882,10 @@ async function writeValidateRunLog(input) {
19242
19882
  errors,
19243
19883
  warnings
19244
19884
  });
19245
- await writeJson(path62.join(reportDir, "run.json"), runJson);
19246
- await writeJson(path62.join(reportDir, "validator.json"), validatorJson);
19247
- await writeJson(path62.join(reportDir, "traceability.json"), traceabilityJson);
19248
- await writeFile8(path62.join(reportDir, "summary.md"), `${summaryMd}
19885
+ await writeJson(path65.join(reportDir, "run.json"), runJson);
19886
+ await writeJson(path65.join(reportDir, "validator.json"), validatorJson);
19887
+ await writeJson(path65.join(reportDir, "traceability.json"), traceabilityJson);
19888
+ await writeFile8(path65.join(reportDir, "summary.md"), `${summaryMd}
19249
19889
  `, "utf-8");
19250
19890
  return {
19251
19891
  runId,
@@ -19369,7 +20009,7 @@ async function allocateRunReportDir(outDir, startedAt) {
19369
20009
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
19370
20010
  const candidateDate = new Date(startedAt.getTime() + attempt);
19371
20011
  const runId = `run-${formatTimestamp17(candidateDate)}`;
19372
- const reportDir = path62.join(outDir, runId);
20012
+ const reportDir = path65.join(outDir, runId);
19373
20013
  try {
19374
20014
  await mkdir9(reportDir);
19375
20015
  return { runId, reportDir };
@@ -19400,7 +20040,7 @@ function shouldFail(result, failOn) {
19400
20040
  // src/cli/commands/validate.ts
19401
20041
  async function runValidate(options) {
19402
20042
  const startedAt = /* @__PURE__ */ new Date();
19403
- const root = path63.resolve(options.root);
20043
+ const root = path66.resolve(options.root);
19404
20044
  const configResult = await loadConfig(root);
19405
20045
  const blockedIssue = buildCiRefinementIssue(options.phase);
19406
20046
  const blockedByPhaseGuard = blockedIssue !== null;
@@ -19556,12 +20196,12 @@ function issueKey(issue2) {
19556
20196
  }
19557
20197
  async function emitJson(result, root, jsonPath) {
19558
20198
  const abs = resolveJsonPath(root, jsonPath);
19559
- await mkdir10(path63.dirname(abs), { recursive: true });
20199
+ await mkdir10(path66.dirname(abs), { recursive: true });
19560
20200
  await writeFile9(abs, `${JSON.stringify(result, null, 2)}
19561
20201
  `, "utf-8");
19562
20202
  }
19563
20203
  function resolveJsonPath(root, jsonPath) {
19564
- return path63.isAbsolute(jsonPath) ? jsonPath : path63.resolve(root, jsonPath);
20204
+ return path66.isAbsolute(jsonPath) ? jsonPath : path66.resolve(root, jsonPath);
19565
20205
  }
19566
20206
  var GITHUB_ANNOTATION_LIMIT = 100;
19567
20207
  var ISSUE_EXPECTED_BY_CODE = {
@@ -19692,6 +20332,9 @@ function parseArgs(argv, cwd) {
19692
20332
  options.help = true;
19693
20333
  command = null;
19694
20334
  }
20335
+ if (command === "prototype") {
20336
+ command = "prototyping";
20337
+ }
19695
20338
  const markInvalid = () => {
19696
20339
  invalid = true;
19697
20340
  options.help = true;
@@ -19870,6 +20513,23 @@ function parseArgs(argv, cwd) {
19870
20513
  i += 1;
19871
20514
  break;
19872
20515
  }
20516
+ case "--mode": {
20517
+ if (command !== "prototyping") {
20518
+ break;
20519
+ }
20520
+ const next = readOptionValue(args, i);
20521
+ if (next === null) {
20522
+ markInvalid();
20523
+ break;
20524
+ }
20525
+ if (next === "low-cost" || next === "standard" || next === "full-harness") {
20526
+ options.prototypingMode = next;
20527
+ } else {
20528
+ options.prototypingModeInvalid = next;
20529
+ }
20530
+ i += 1;
20531
+ break;
20532
+ }
19873
20533
  case "--autogen-ui-fidelity":
19874
20534
  if (command === "prototyping") {
19875
20535
  options.prototypingAutogen = true;
@@ -20077,6 +20737,8 @@ async function run(argv, cwd) {
20077
20737
  autogenOnly: options.prototypingAutogenOnly,
20078
20738
  ...options.prototypingBaseUrl !== void 0 ? { baseUrl: options.prototypingBaseUrl } : {},
20079
20739
  ...options.prototypingEvidenceOut !== void 0 ? { evidenceOut: options.prototypingEvidenceOut } : {},
20740
+ ...options.prototypingMode !== void 0 ? { mode: options.prototypingMode } : {},
20741
+ ...options.prototypingModeInvalid !== void 0 ? { modeInvalid: options.prototypingModeInvalid } : {},
20080
20742
  renderEvidence: options.prototypingRenderEvidence,
20081
20743
  renderViewports: options.prototypingRenderViewports,
20082
20744
  ...options.prototypingRenderOut !== void 0 ? { renderOut: options.prototypingRenderOut } : {}
@@ -20122,6 +20784,7 @@ Options:
20122
20784
  --path <path> guardrails: \u5BFE\u8C61\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\uFF08\u8907\u6570\u6307\u5B9A\u53EF\uFF09
20123
20785
  --max <number> guardrails extract: \u6700\u5927\u4EF6\u6570
20124
20786
  --keyword <text> guardrails list/extract: \u30AD\u30FC\u30EF\u30FC\u30C9\u30D5\u30A3\u30EB\u30BF
20787
+ --mode <low-cost|standard|full-harness> prototyping: \u5B9F\u884C\u30E2\u30FC\u30C9 (full-harness \u306F /qfai-prototyping-full-harness \u3078\u30EB\u30FC\u30C6\u30A3\u30F3\u30B0)
20125
20788
  --autogen-ui-fidelity prototyping: uiFidelity \u81EA\u52D5\u751F\u6210\u3092\u6709\u52B9\u5316
20126
20789
  --autogen-only prototyping: \u81EA\u52D5\u751F\u6210\u306E\u307F\u5B9F\u884C\uFF08\u5931\u6557\u6642exit 1\uFF09
20127
20790
  --evidence-out <path> prototyping: \u51FA\u529B\u5148\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8 .qfai/evidence/prototyping.json\uFF09