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.
- package/README.md +10 -4
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/20_eval_axis_usability.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/21_eval_axis_consistency.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/22_eval_axis_accessibility.md +5 -0
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/23_eval_axis_delight.md +23 -1
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/30_comparison.md +20 -5
- package/assets/init/.qfai/assistant/skills/qfai-discussion/templates/uiux/40_contracts.md +36 -15
- package/assets/uix-rev/comparison-review.md +13 -0
- package/assets/uix-rev/contracts-review.md +18 -0
- package/assets/uix-rev/migration-review.md +17 -0
- package/assets/uix-rev/oq-review.md +13 -0
- package/assets/uix-rev/scoring-review.md +14 -0
- package/assets/uix-rev/strategy-review.md +22 -0
- package/dist/cli/index.cjs +1809 -1146
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +1789 -1126
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +995 -384
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -1
- package/dist/index.d.ts +107 -1
- package/dist/index.mjs +963 -357
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -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.
|
|
1688
|
-
return "1.7.
|
|
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
|
|
3432
|
-
import
|
|
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/
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4140
|
-
let result;
|
|
4117
|
+
let doc;
|
|
4141
4118
|
try {
|
|
4142
|
-
|
|
4143
|
-
} catch
|
|
4144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
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
|
|
4261
|
-
|
|
4262
|
-
|
|
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
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
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
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
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
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
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
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
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
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
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
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
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
|
|
4405
|
-
import
|
|
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
|
|
4501
|
-
import
|
|
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
|
|
4505
|
-
import
|
|
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 =
|
|
4509
|
-
const apiRoot =
|
|
4510
|
-
const dbRoot =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
5200
|
-
import
|
|
5201
|
-
import { parse as
|
|
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
|
|
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
|
|
5733
|
+
async function readSafe2(filePath) {
|
|
5243
5734
|
try {
|
|
5244
|
-
return await
|
|
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 =
|
|
5333
|
-
var UNSUPPORTED_WAIVER_FILE =
|
|
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 =
|
|
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 =
|
|
5351
|
-
const unsupportedPath =
|
|
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
|
|
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 =
|
|
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
|
|
6069
|
-
import
|
|
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 =
|
|
6083
|
-
const apiRoot =
|
|
6084
|
-
const dbRoot =
|
|
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
|
|
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
|
|
6260
|
-
import
|
|
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 =
|
|
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 =
|
|
6762
|
+
const fileName = path24.basename(file);
|
|
6370
6763
|
if (fileName !== DISCUSSION_PACK_FLOW_FILE) {
|
|
6371
6764
|
continue;
|
|
6372
6765
|
}
|
|
6373
|
-
const discussionDirName =
|
|
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
|
|
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
|
|
6454
|
-
import
|
|
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 =
|
|
6461
|
-
const skillsLocalDir =
|
|
6462
|
-
const driftProtocolPath =
|
|
6463
|
-
const testLayersPath =
|
|
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
|
|
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) =>
|
|
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
|
|
6604
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7111
|
+
async function readSafe3(file) {
|
|
6719
7112
|
try {
|
|
6720
|
-
return await
|
|
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
|
|
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 =
|
|
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
|
|
6758
|
-
import
|
|
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 =
|
|
7158
|
+
const reviewRoot = path27.join(root, ".qfai", "review");
|
|
6766
7159
|
const issues = [];
|
|
6767
|
-
const gitignorePath =
|
|
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 =
|
|
6806
|
-
const summaryPath =
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
6987
|
-
import
|
|
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
|
|
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
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
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
|
-
`${
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
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
|
|
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 =
|
|
7984
|
-
const policyPath =
|
|
8376
|
+
const assistantRoot = path28.dirname(skillsDir);
|
|
8377
|
+
const policyPath = path28.join(assistantRoot, "steering", "test-layers.md");
|
|
7985
8378
|
try {
|
|
7986
|
-
const policyText = await
|
|
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
|
|
8485
|
+
async function readSafe4(filePath) {
|
|
8093
8486
|
try {
|
|
8094
|
-
return await
|
|
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
|
|
8102
|
-
import
|
|
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 ??
|
|
8120
|
-
const capabilitiesPath =
|
|
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
|
|
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 =
|
|
8202
|
-
const text = await
|
|
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
|
|
8225
|
-
const acceptanceCriteriaText = await
|
|
8226
|
-
const businessRulesText = await
|
|
8227
|
-
const examplesText = await
|
|
8228
|
-
const testCasesText = await
|
|
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
|
|
9051
|
+
async function readSafe5(target) {
|
|
8659
9052
|
try {
|
|
8660
|
-
return await
|
|
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
|
|
9069
|
+
import path31 from "path";
|
|
8677
9070
|
|
|
8678
9071
|
// src/core/atddTraceability.ts
|
|
8679
|
-
import { readFile as
|
|
8680
|
-
import
|
|
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 =
|
|
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 =
|
|
8698
|
-
const apiRoot =
|
|
8699
|
-
const integrationRoot =
|
|
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
|
|
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
|
-
|
|
8802
|
-
|
|
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
|
|
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 =
|
|
8845
|
-
const isInsideRoot = relativeTestsRoot.length === 0 || !relativeTestsRoot.startsWith("..") && !
|
|
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 =
|
|
9267
|
+
const relative = path30.relative(base, target);
|
|
8875
9268
|
if (relative === "") {
|
|
8876
9269
|
return true;
|
|
8877
9270
|
}
|
|
8878
|
-
return !relative.startsWith("..") && !
|
|
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(
|
|
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(
|
|
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(
|
|
9315
|
+
const files = refs.get(path30.normalize(file)) ?? /* @__PURE__ */ new Set();
|
|
8923
9316
|
files.add(id);
|
|
8924
|
-
refs.set(
|
|
9317
|
+
refs.set(path30.normalize(file), files);
|
|
8925
9318
|
}
|
|
8926
9319
|
function pushUnknown(unknown, dedup, file, token, kind) {
|
|
8927
|
-
const normalizedFile =
|
|
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
|
|
9403
|
+
async function readSafe6(target) {
|
|
9011
9404
|
try {
|
|
9012
|
-
return await
|
|
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
|
-
|
|
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 (
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
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
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
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
|
-
|
|
9400
|
-
return blocking;
|
|
9556
|
+
return (left.file ?? "").localeCompare(right.file ?? "");
|
|
9557
|
+
});
|
|
9401
9558
|
}
|
|
9402
|
-
function
|
|
9403
|
-
const
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
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
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
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
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
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
|
|
9449
|
-
|
|
9450
|
-
|
|
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
|
|
9638
|
+
return items.map((item) => ` - ${item}`);
|
|
9458
9639
|
}
|
|
9459
|
-
function
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
return
|
|
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
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
9633
|
-
import
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
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 =
|
|
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
|
|
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
|
|
10417
|
-
const acItems = collectMarkdownItems(await
|
|
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
|
|
10447
|
-
const brItems = collectMarkdownItems(await
|
|
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
|
|
10477
|
-
const exItems = collectScenarioItems(await
|
|
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
|
|
10507
|
-
const tcItems = collectMarkdownItems(await
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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 =
|
|
10637
|
-
const text = await
|
|
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
|
|
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
|
|
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
|
-
`${
|
|
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() &&
|
|
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 =
|
|
10746
|
-
const text = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
10902
|
-
import
|
|
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
|
-
|
|
10909
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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 ??
|
|
11424
|
+
const policiesDir = layeredEntries[0]?.sharedDir ?? path39.join(specsRoot, "_policies");
|
|
11247
11425
|
const capIds = new Set(
|
|
11248
|
-
uniqueMatches(await
|
|
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
|
|
11252
|
-
const acItems = collectMarkdownItems(await
|
|
11253
|
-
const brItems = collectMarkdownItems(await
|
|
11254
|
-
const exItems = collectScenarioItems(await
|
|
11255
|
-
const tcItems = collectMarkdownItems(await
|
|
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
|
|
11399
|
-
import
|
|
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 =
|
|
11409
|
-
const evidenceRoot =
|
|
11410
|
-
const evidenceMarkdownPath =
|
|
11411
|
-
const evidenceJsonPath =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
12577
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
12840
|
+
const baseName = path42.parse(entry.name).name;
|
|
12663
12841
|
if (SUSPICIOUS_TEMPLATE_NAME_RE.test(baseName)) {
|
|
12664
|
-
matches.push(toPosix(
|
|
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
|
|
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 ??
|
|
12694
|
-
const capabilitiesPath =
|
|
12695
|
-
const capabilityText = await
|
|
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) =>
|
|
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) =>
|
|
12949
|
+
const entry = layeredEntries.find((value) => path43.basename(value.dir).toLowerCase() === specId);
|
|
12772
12950
|
if (!entry) {
|
|
12773
12951
|
continue;
|
|
12774
12952
|
}
|
|
12775
|
-
const specFilePath =
|
|
12776
|
-
const specText = await
|
|
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
|
|
12795
|
-
import
|
|
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
|
|
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(
|
|
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
|
|
12863
|
-
import
|
|
13040
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
13041
|
+
import path45 from "path";
|
|
12864
13042
|
import fg2 from "fast-glob";
|
|
12865
|
-
import { parse as
|
|
13043
|
+
import { parse as parseYaml7 } from "yaml";
|
|
12866
13044
|
|
|
12867
13045
|
// src/core/parse/designToken.ts
|
|
12868
|
-
import { parse as
|
|
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 =
|
|
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
|
|
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(
|
|
13104
|
+
target.set(path67, token);
|
|
12927
13105
|
} else {
|
|
12928
|
-
flattenTokens(record2,
|
|
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 [
|
|
12939
|
-
resolveTokenRef(
|
|
13116
|
+
for (const [path67] of allTokens) {
|
|
13117
|
+
resolveTokenRef(path67, allTokens, /* @__PURE__ */ new Set(), 0, result);
|
|
12940
13118
|
}
|
|
12941
13119
|
}
|
|
12942
|
-
function resolveTokenRef(
|
|
12943
|
-
if (result.resolved.has(
|
|
12944
|
-
return result.resolved.get(
|
|
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: ${
|
|
12949
|
-
path:
|
|
13126
|
+
message: `Max reference depth exceeded at: ${path67}`,
|
|
13127
|
+
path: path67
|
|
12950
13128
|
});
|
|
12951
13129
|
return void 0;
|
|
12952
13130
|
}
|
|
12953
|
-
if (visited.has(
|
|
13131
|
+
if (visited.has(path67)) {
|
|
12954
13132
|
result.errors.push({
|
|
12955
|
-
message: `Circular reference detected: ${
|
|
12956
|
-
path:
|
|
13133
|
+
message: `Circular reference detected: ${path67}`,
|
|
13134
|
+
path: path67
|
|
12957
13135
|
});
|
|
12958
13136
|
return void 0;
|
|
12959
13137
|
}
|
|
12960
|
-
const token = allTokens.get(
|
|
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(
|
|
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(
|
|
13150
|
+
result.resolved.set(path67, rawValue);
|
|
12973
13151
|
return rawValue;
|
|
12974
13152
|
}
|
|
12975
|
-
visited.add(
|
|
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 ${
|
|
12984
|
-
path:
|
|
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(
|
|
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 ?
|
|
13049
|
-
const pattern =
|
|
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 =
|
|
13237
|
+
const rel = path45.relative(root, filePath).replace(/\\/g, "/");
|
|
13060
13238
|
let hasRootObjectError = false;
|
|
13061
13239
|
let content;
|
|
13062
13240
|
try {
|
|
13063
|
-
content = await
|
|
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 =
|
|
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
|
|
13227
|
-
import
|
|
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
|
-
|
|
13608
|
-
|
|
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
|
|
13793
|
+
content = await readFile31(filePath, "utf-8");
|
|
13616
13794
|
} catch {
|
|
13617
13795
|
continue;
|
|
13618
13796
|
}
|
|
13619
|
-
const rel =
|
|
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
|
|
13786
|
-
import
|
|
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
|
-
|
|
13838
|
-
|
|
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
|
|
14022
|
+
content = await readFile32(filePath, "utf-8");
|
|
13845
14023
|
} catch {
|
|
13846
14024
|
continue;
|
|
13847
14025
|
}
|
|
13848
|
-
const rel =
|
|
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
|
|
13947
|
-
import
|
|
14124
|
+
import { readFile as readFile33 } from "fs/promises";
|
|
14125
|
+
import path48 from "path";
|
|
13948
14126
|
import fg5 from "fast-glob";
|
|
13949
|
-
import { parse as
|
|
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 =
|
|
13985
|
-
const bpPattern =
|
|
13986
|
-
const apPattern =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
14182
|
-
import
|
|
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(
|
|
14226
|
-
const hasAndroid = await exists8(
|
|
14227
|
-
const hasIos = await exists8(
|
|
14228
|
-
const hasWeb = await exists8(
|
|
14229
|
-
const hasWindows = await exists8(
|
|
14230
|
-
const hasMacos = await exists8(
|
|
14231
|
-
const hasLinux = await exists8(
|
|
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 =
|
|
14429
|
+
const pkgJsonPath = path49.join(root, "package.json");
|
|
14252
14430
|
if (await exists8(pkgJsonPath)) {
|
|
14253
14431
|
try {
|
|
14254
|
-
const raw = await
|
|
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(
|
|
14274
|
-
const hasIos = await exists8(
|
|
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
|
|
14297
|
-
import
|
|
14474
|
+
import { readFile as readFile35 } from "fs/promises";
|
|
14475
|
+
import path50 from "path";
|
|
14298
14476
|
import fg6 from "fast-glob";
|
|
14299
|
-
import { parse as
|
|
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 ?
|
|
14304
|
-
const tokenPattern =
|
|
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
|
|
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 =
|
|
14321
|
-
const uiPattern =
|
|
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
|
|
14327
|
-
const parsed =
|
|
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
|
-
|
|
14343
|
-
|
|
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
|
|
14350
|
-
const rel =
|
|
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
|
|
14408
|
-
import
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
14658
|
-
import
|
|
14659
|
-
import { parse as
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
14819
|
-
const roster =
|
|
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
|
|
14864
|
-
import
|
|
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 =
|
|
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 =
|
|
14891
|
-
const relPath =
|
|
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
|
|
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 =
|
|
15079
|
-
const relative =
|
|
15080
|
-
if (
|
|
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 =
|
|
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
|
|
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
|
|
15174
|
-
import
|
|
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 =
|
|
15193
|
-
const filePath =
|
|
15194
|
-
const content = await
|
|
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 =
|
|
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 =
|
|
15386
|
+
const basename = path54.basename(filePath);
|
|
15209
15387
|
if (basename === "03_Story-Workshop.md") {
|
|
15210
15388
|
try {
|
|
15211
|
-
const storyContent = await
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
15637
|
+
content = await readFile39(filePath, "utf-8");
|
|
15460
15638
|
} catch {
|
|
15461
15639
|
continue;
|
|
15462
15640
|
}
|
|
15463
|
-
const rel =
|
|
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
|
|
15664
|
+
content = await readFile39(filePath, "utf-8");
|
|
15487
15665
|
} catch {
|
|
15488
15666
|
continue;
|
|
15489
15667
|
}
|
|
15490
|
-
const rel =
|
|
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 =
|
|
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
|
|
15778
|
+
content = await readFile39(filePath, "utf-8");
|
|
15601
15779
|
} catch {
|
|
15602
15780
|
continue;
|
|
15603
15781
|
}
|
|
15604
|
-
const rel =
|
|
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 =
|
|
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
|
|
15827
|
+
content = await readFile39(filePath, "utf-8");
|
|
15650
15828
|
} catch {
|
|
15651
15829
|
continue;
|
|
15652
15830
|
}
|
|
15653
|
-
const rel =
|
|
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
|
|
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
|
|
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
|
|
16105
|
-
import { readFile as
|
|
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 =
|
|
16125
|
-
const discussionFiles = await fg9(
|
|
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
|
|
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 =
|
|
16136
|
-
const evidenceDir =
|
|
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 =
|
|
16316
|
+
const skillPromptPattern = path55.posix.join(skillsDir, "qfai-{prototyping,implement}*/SKILL.md");
|
|
16139
16317
|
const skillFiles = await fg9(skillPromptPattern, { dot: true });
|
|
16140
|
-
const evidencePattern =
|
|
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
|
|
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: ${
|
|
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
|
|
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: ${
|
|
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
|
|
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): ${
|
|
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
|
|
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(", ")}): ${
|
|
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
|
|
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 =
|
|
16531
|
+
const prototypingJsonPath = path55.join(root, ".qfai", "evidence", "prototyping.json");
|
|
16354
16532
|
try {
|
|
16355
|
-
const raw = await
|
|
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
|
|
16395
|
-
import
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
16741
|
-
const content = await
|
|
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 =
|
|
16778
|
-
const content = await
|
|
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 =
|
|
16812
|
-
const content = await
|
|
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 =
|
|
16836
|
-
const content = await
|
|
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 =
|
|
16878
|
-
const content = await
|
|
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 =
|
|
16935
|
-
const content = await
|
|
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 =
|
|
16973
|
-
const content = await
|
|
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 =
|
|
16998
|
-
const content = await
|
|
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 =
|
|
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
|
|
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 ?
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
17211
|
-
const content = await
|
|
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
|
|
17223
|
-
import
|
|
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
|
|
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 =
|
|
17424
|
+
const baseDir = path59.dirname(basePath);
|
|
17240
17425
|
const candidates = [
|
|
17241
|
-
|
|
17242
|
-
|
|
17243
|
-
|
|
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 =
|
|
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 =
|
|
17276
|
-
const content = await
|
|
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 =
|
|
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 =
|
|
17409
|
-
const uiRoot =
|
|
17410
|
-
const dbRoot =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
19335
|
+
const testCasesPath = path62.join(entry.dir, "06_Test-Cases.md");
|
|
18696
19336
|
let tcContent;
|
|
18697
19337
|
try {
|
|
18698
|
-
tcContent = await
|
|
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 =
|
|
19370
|
+
const tddListPath = path62.join(entry.dir, "tdd", "test-list.md");
|
|
18731
19371
|
let tddContent;
|
|
18732
19372
|
try {
|
|
18733
|
-
tddContent = await
|
|
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
|
|
18817
|
-
import
|
|
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 =
|
|
18836
|
-
const outputDir =
|
|
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
|
-
|
|
19503
|
+
path63.join(outputDir, "coverage.md"),
|
|
18864
19504
|
`${formatCoverageMarkdown(specName, coverage)}
|
|
18865
19505
|
`,
|
|
18866
19506
|
"utf-8"
|
|
18867
19507
|
);
|
|
18868
19508
|
await writeFile6(
|
|
18869
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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" ?
|
|
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 =
|
|
19108
|
-
await mkdir8(
|
|
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
|
|
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 =
|
|
19185
|
-
await mkdir8(
|
|
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
|
|
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
|
|
19836
|
+
import path65 from "path";
|
|
19197
19837
|
async function writeValidateRunLog(input) {
|
|
19198
|
-
const 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(
|
|
19246
|
-
await writeJson(
|
|
19247
|
-
await writeJson(
|
|
19248
|
-
await writeFile8(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|