qfai 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +327 -245
  2. package/assets/init/.qfai/README.md +2 -1
  3. package/assets/init/.qfai/assistant/README.md +1 -1
  4. package/assets/init/.qfai/assistant/prompts/README.md +1 -1
  5. package/assets/init/.qfai/assistant/prompts/qfai-configure.md +197 -0
  6. package/assets/init/.qfai/assistant/prompts/qfai-verify.md +1 -1
  7. package/assets/init/.qfai/assistant/steering/README.md +6 -0
  8. package/assets/init/.qfai/assistant/steering/manifest.md +43 -0
  9. package/assets/init/.qfai/contracts/db/README.md +10 -3
  10. package/assets/init/.qfai/samples/guardrails/delta_with_guardrails.md +19 -0
  11. package/assets/init/.qfai/specs/README.md +4 -0
  12. package/assets/init/root/.claude/commands/qfai-configure.md +14 -0
  13. package/assets/init/root/.claude/commands/qfai-discuss.md +14 -0
  14. package/assets/init/root/.claude/commands/qfai-implement.md +14 -0
  15. package/assets/init/root/.claude/commands/qfai-require.md +14 -0
  16. package/assets/init/root/.claude/commands/qfai-scenario-test.md +14 -0
  17. package/assets/init/root/.claude/commands/qfai-spec.md +14 -0
  18. package/assets/init/root/.claude/commands/qfai-unit-test.md +14 -0
  19. package/assets/init/root/.claude/commands/qfai-verify.md +14 -0
  20. package/assets/init/root/.codex/README.md +16 -0
  21. package/assets/init/root/.codex/skills/qfai-configure/SKILL.md +18 -0
  22. package/assets/init/root/.codex/skills/qfai-discuss/SKILL.md +18 -0
  23. package/assets/init/root/.codex/skills/qfai-implement/SKILL.md +18 -0
  24. package/assets/init/root/.codex/skills/qfai-require/SKILL.md +18 -0
  25. package/assets/init/root/.codex/skills/qfai-scenario-test/SKILL.md +18 -0
  26. package/assets/init/root/.codex/skills/qfai-spec/SKILL.md +18 -0
  27. package/assets/init/root/.codex/skills/qfai-unit-test/SKILL.md +18 -0
  28. package/assets/init/root/.codex/skills/qfai-verify/SKILL.md +18 -0
  29. package/assets/init/root/.github/copilot-instructions.md +14 -0
  30. package/assets/init/root/.github/prompts/qfai-configure.prompt.md +17 -0
  31. package/assets/init/root/.github/prompts/qfai-discuss.prompt.md +17 -0
  32. package/assets/init/root/.github/prompts/qfai-implement.prompt.md +17 -0
  33. package/assets/init/root/.github/prompts/qfai-require.prompt.md +17 -0
  34. package/assets/init/root/.github/prompts/qfai-scenario-test.prompt.md +17 -0
  35. package/assets/init/root/.github/prompts/qfai-spec.prompt.md +17 -0
  36. package/assets/init/root/.github/prompts/qfai-unit-test.prompt.md +17 -0
  37. package/assets/init/root/.github/prompts/qfai-verify.prompt.md +17 -0
  38. package/assets/init/root/.github/workflows/qfai.yml +0 -2
  39. package/assets/init/root/qfai.config.yaml +1 -8
  40. package/dist/cli/index.cjs +880 -196
  41. package/dist/cli/index.cjs.map +1 -1
  42. package/dist/cli/index.mjs +866 -182
  43. package/dist/cli/index.mjs.map +1 -1
  44. package/dist/index.cjs +731 -221
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +91 -1
  47. package/dist/index.d.ts +91 -1
  48. package/dist/index.mjs +719 -216
  49. package/dist/index.mjs.map +1 -1
  50. package/package.json +1 -1
  51. package/assets/init/.qfai/assistant/prompts/qfai-pr.md +0 -209
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/cli/commands/doctor.ts
4
4
  import { mkdir, writeFile } from "fs/promises";
5
- import path11 from "path";
5
+ import path12 from "path";
6
6
 
7
7
  // src/core/doctor.ts
8
8
  import { access as access4 } from "fs/promises";
9
- import path10 from "path";
9
+ import path11 from "path";
10
10
 
11
11
  // src/core/config.ts
12
12
  import { access, readFile } from "fs/promises";
@@ -24,15 +24,7 @@ var defaultConfig = {
24
24
  validation: {
25
25
  failOn: "error",
26
26
  require: {
27
- specSections: [
28
- "\u80CC\u666F",
29
- "\u30B9\u30B3\u30FC\u30D7",
30
- "\u975E\u30B4\u30FC\u30EB",
31
- "\u7528\u8A9E",
32
- "\u524D\u63D0",
33
- "\u6C7A\u5B9A\u4E8B\u9805",
34
- "\u696D\u52D9\u30EB\u30FC\u30EB"
35
- ]
27
+ specSections: []
36
28
  },
37
29
  traceability: {
38
30
  brMustHaveSc: true,
@@ -1056,8 +1048,8 @@ import { readFile as readFile4 } from "fs/promises";
1056
1048
  import path9 from "path";
1057
1049
  import { fileURLToPath as fileURLToPath2 } from "url";
1058
1050
  async function resolveToolVersion() {
1059
- if ("1.0.6".length > 0) {
1060
- return "1.0.6";
1051
+ if ("1.1.0".length > 0) {
1052
+ return "1.1.0";
1061
1053
  }
1062
1054
  try {
1063
1055
  const packagePath = resolvePackageJsonPath();
@@ -1075,6 +1067,460 @@ function resolvePackageJsonPath() {
1075
1067
  return path9.resolve(path9.dirname(basePath), "../../package.json");
1076
1068
  }
1077
1069
 
1070
+ // src/core/decisionGuardrails.ts
1071
+ import { readFile as readFile5, stat } from "fs/promises";
1072
+ import path10 from "path";
1073
+
1074
+ // src/core/parse/markdown.ts
1075
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1076
+ function parseHeadings(md) {
1077
+ const lines = md.split(/\r?\n/);
1078
+ const headings = [];
1079
+ for (let i = 0; i < lines.length; i++) {
1080
+ const line = lines[i] ?? "";
1081
+ const match = line.match(HEADING_RE);
1082
+ if (!match) continue;
1083
+ const levelToken = match[1];
1084
+ const title = match[2];
1085
+ if (!levelToken || !title) continue;
1086
+ headings.push({
1087
+ level: levelToken.length,
1088
+ title: title.trim(),
1089
+ line: i + 1
1090
+ });
1091
+ }
1092
+ return headings;
1093
+ }
1094
+ function extractH2Sections(md) {
1095
+ const lines = md.split(/\r?\n/);
1096
+ const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1097
+ const sections = /* @__PURE__ */ new Map();
1098
+ for (let i = 0; i < headings.length; i++) {
1099
+ const current = headings[i];
1100
+ if (!current) continue;
1101
+ const next = headings[i + 1];
1102
+ const startLine = current.line + 1;
1103
+ const endLine = (next?.line ?? lines.length + 1) - 1;
1104
+ const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1105
+ sections.set(current.title.trim(), {
1106
+ title: current.title.trim(),
1107
+ startLine,
1108
+ endLine,
1109
+ body
1110
+ });
1111
+ }
1112
+ return sections;
1113
+ }
1114
+
1115
+ // src/core/decisionGuardrails.ts
1116
+ var DEFAULT_DECISION_GUARDRAILS_GLOBS = [".qfai/specs/**/delta.md"];
1117
+ var DEFAULT_GUARDRAILS_IGNORE_GLOBS = [
1118
+ "**/node_modules/**",
1119
+ "**/.git/**",
1120
+ "**/dist/**",
1121
+ "**/build/**",
1122
+ "**/.pnpm/**",
1123
+ "**/tmp/**",
1124
+ "**/.mcp-tools/**"
1125
+ ];
1126
+ var SECTION_TITLE = "decision guardrails";
1127
+ var ENTRY_START_RE = /^\s*[-*]\s+ID:\s*(.+?)\s*$/i;
1128
+ var FIELD_RE = /^\s{2,}([A-Za-z][A-Za-z0-9 _-]*):\s*(.*)$/;
1129
+ var CONTINUATION_RE = /^\s{4,}(.+)$/;
1130
+ var ID_FORMAT_RE = /^DG-\d{4}$/;
1131
+ var TYPE_ORDER = {
1132
+ "non-goal": 0,
1133
+ "not-now": 1,
1134
+ "trade-off": 2
1135
+ };
1136
+ async function loadDecisionGuardrails(root, options = {}) {
1137
+ const errors = [];
1138
+ const files = await scanDecisionGuardrailFiles(
1139
+ root,
1140
+ options.paths,
1141
+ errors,
1142
+ options.specsRoot
1143
+ );
1144
+ const entries = [];
1145
+ for (const filePath of files) {
1146
+ try {
1147
+ const content = await readFile5(filePath, "utf-8");
1148
+ const parsed = extractDecisionGuardrailsFromMarkdown(content, filePath);
1149
+ entries.push(...parsed);
1150
+ } catch (error2) {
1151
+ errors.push({ path: filePath, message: String(error2) });
1152
+ }
1153
+ }
1154
+ return { entries, errors, files };
1155
+ }
1156
+ function extractDecisionGuardrailsFromMarkdown(markdown, filePath) {
1157
+ const sections = extractH2Sections(markdown);
1158
+ const section = findDecisionGuardrailsSection(sections);
1159
+ if (!section) {
1160
+ return [];
1161
+ }
1162
+ const lines = section.body.split(/\r?\n/);
1163
+ const entries = [];
1164
+ let current = null;
1165
+ const flush = () => {
1166
+ if (!current) {
1167
+ return;
1168
+ }
1169
+ const entry = {
1170
+ keywords: current.keywords,
1171
+ source: { file: filePath, line: current.startLine },
1172
+ ...current.fields.id ? { id: current.fields.id } : {},
1173
+ ...current.fields.type ? { type: current.fields.type } : {},
1174
+ ...current.fields.guardrail ? { guardrail: current.fields.guardrail } : {},
1175
+ ...current.fields.rationale ? { rationale: current.fields.rationale } : {},
1176
+ ...current.fields.reconsider ? { reconsider: current.fields.reconsider } : {},
1177
+ ...current.fields.related ? { related: current.fields.related } : {},
1178
+ ...current.fields.title ? { title: current.fields.title } : {}
1179
+ };
1180
+ entries.push(entry);
1181
+ current = null;
1182
+ };
1183
+ for (let i = 0; i < lines.length; i += 1) {
1184
+ const rawLine = lines[i] ?? "";
1185
+ const lineNumber = section.startLine + i;
1186
+ const entryMatch = rawLine.match(ENTRY_START_RE);
1187
+ if (entryMatch) {
1188
+ flush();
1189
+ const id = entryMatch[1]?.trim() ?? "";
1190
+ current = {
1191
+ startLine: lineNumber,
1192
+ fields: { id },
1193
+ keywords: []
1194
+ };
1195
+ continue;
1196
+ }
1197
+ if (!current) {
1198
+ continue;
1199
+ }
1200
+ const fieldMatch = rawLine.match(FIELD_RE);
1201
+ if (fieldMatch) {
1202
+ const rawKey = fieldMatch[1] ?? "";
1203
+ const value = fieldMatch[2] ?? "";
1204
+ const key = normalizeFieldKey(rawKey);
1205
+ if (key) {
1206
+ if (key === "keywords") {
1207
+ current.keywords.push(
1208
+ ...value.split(",").map((item) => item.trim()).filter((item) => item.length > 0)
1209
+ );
1210
+ } else {
1211
+ const trimmed = value.trim();
1212
+ if (trimmed.length > 0) {
1213
+ const existing = current.fields[key];
1214
+ current.fields[key] = existing ? `${existing}
1215
+ ${trimmed}` : trimmed;
1216
+ }
1217
+ }
1218
+ current.lastKey = key;
1219
+ } else {
1220
+ delete current.lastKey;
1221
+ }
1222
+ continue;
1223
+ }
1224
+ const continuationMatch = rawLine.match(CONTINUATION_RE);
1225
+ if (continuationMatch && current.lastKey) {
1226
+ const value = continuationMatch[1]?.trim() ?? "";
1227
+ if (value.length > 0) {
1228
+ const existing = current.fields[current.lastKey];
1229
+ current.fields[current.lastKey] = existing ? `${existing}
1230
+ ${value}` : value;
1231
+ }
1232
+ }
1233
+ }
1234
+ flush();
1235
+ return entries;
1236
+ }
1237
+ function normalizeDecisionGuardrails(entries) {
1238
+ const items = [];
1239
+ for (const entry of entries) {
1240
+ const id = entry.id?.trim();
1241
+ const type = normalizeGuardrailType(entry.type);
1242
+ const guardrail = entry.guardrail?.trim();
1243
+ if (!id || !type || !guardrail) {
1244
+ continue;
1245
+ }
1246
+ const item = {
1247
+ id,
1248
+ type,
1249
+ guardrail,
1250
+ keywords: entry.keywords?.filter((word) => word.length > 0) ?? [],
1251
+ source: entry.source
1252
+ };
1253
+ const rationale = entry.rationale?.trim();
1254
+ if (rationale) {
1255
+ item.rationale = rationale;
1256
+ }
1257
+ const reconsider = entry.reconsider?.trim();
1258
+ if (reconsider) {
1259
+ item.reconsider = reconsider;
1260
+ }
1261
+ const related = entry.related?.trim();
1262
+ if (related) {
1263
+ item.related = related;
1264
+ }
1265
+ const title = entry.title?.trim();
1266
+ if (title) {
1267
+ item.title = title;
1268
+ }
1269
+ items.push(item);
1270
+ }
1271
+ return items;
1272
+ }
1273
+ function sortDecisionGuardrails(items) {
1274
+ return [...items].sort((a, b) => {
1275
+ const typeOrder = (TYPE_ORDER[a.type] ?? 999) - (TYPE_ORDER[b.type] ?? 999);
1276
+ if (typeOrder !== 0) {
1277
+ return typeOrder;
1278
+ }
1279
+ return a.id.localeCompare(b.id);
1280
+ });
1281
+ }
1282
+ function filterDecisionGuardrailsByKeyword(items, keyword) {
1283
+ const needle = keyword?.trim().toLowerCase();
1284
+ if (!needle) {
1285
+ return items;
1286
+ }
1287
+ return items.filter((item) => {
1288
+ const haystack = [
1289
+ item.title,
1290
+ item.guardrail,
1291
+ item.rationale,
1292
+ item.related,
1293
+ item.keywords.join(" ")
1294
+ ].filter((value) => Boolean(value)).map((value) => value.toLowerCase());
1295
+ return haystack.some((value) => value.includes(needle));
1296
+ });
1297
+ }
1298
+ function formatGuardrailsForLlm(items, max) {
1299
+ const limit = Math.max(0, Math.floor(max));
1300
+ const lines = ["# Decision Guardrails (extract)", ""];
1301
+ const slice = limit > 0 ? items.slice(0, limit) : [];
1302
+ if (slice.length === 0) {
1303
+ lines.push("- (none)");
1304
+ return lines.join("\n");
1305
+ }
1306
+ for (const item of slice) {
1307
+ lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
1308
+ if (item.rationale) {
1309
+ lines.push(` Rationale: ${item.rationale}`);
1310
+ }
1311
+ if (item.reconsider) {
1312
+ lines.push(` Reconsider: ${item.reconsider}`);
1313
+ }
1314
+ if (item.related) {
1315
+ lines.push(` Related: ${item.related}`);
1316
+ }
1317
+ }
1318
+ return lines.join("\n");
1319
+ }
1320
+ function checkDecisionGuardrails(entries) {
1321
+ const errors = [];
1322
+ const warnings = [];
1323
+ const idMap = /* @__PURE__ */ new Map();
1324
+ for (const entry of entries) {
1325
+ const file = entry.source.file;
1326
+ const line = entry.source.line;
1327
+ const id = entry.id?.trim();
1328
+ const typeRaw = entry.type?.trim();
1329
+ const guardrail = entry.guardrail?.trim();
1330
+ const rationale = entry.rationale?.trim();
1331
+ const reconsider = entry.reconsider?.trim();
1332
+ if (!id) {
1333
+ errors.push({
1334
+ severity: "error",
1335
+ code: "QFAI-GR-001",
1336
+ message: "ID is missing",
1337
+ file,
1338
+ line
1339
+ });
1340
+ } else {
1341
+ const list = idMap.get(id) ?? [];
1342
+ list.push(entry);
1343
+ idMap.set(id, list);
1344
+ if (!ID_FORMAT_RE.test(id)) {
1345
+ warnings.push({
1346
+ severity: "warning",
1347
+ code: "QFAI-GR-002",
1348
+ message: `ID format is not standard: ${id}`,
1349
+ file,
1350
+ line,
1351
+ id
1352
+ });
1353
+ }
1354
+ }
1355
+ if (!typeRaw) {
1356
+ errors.push({
1357
+ severity: "error",
1358
+ code: "QFAI-GR-003",
1359
+ message: "Type is missing",
1360
+ file,
1361
+ line,
1362
+ ...id ? { id } : {}
1363
+ });
1364
+ } else if (!normalizeGuardrailType(typeRaw)) {
1365
+ errors.push({
1366
+ severity: "error",
1367
+ code: "QFAI-GR-004",
1368
+ message: `Type is invalid: ${typeRaw}`,
1369
+ file,
1370
+ line,
1371
+ ...id ? { id } : {}
1372
+ });
1373
+ }
1374
+ if (!guardrail) {
1375
+ errors.push({
1376
+ severity: "error",
1377
+ code: "QFAI-GR-005",
1378
+ message: "Guardrail is missing",
1379
+ file,
1380
+ line,
1381
+ ...id ? { id } : {}
1382
+ });
1383
+ }
1384
+ if (!rationale) {
1385
+ warnings.push({
1386
+ severity: "warning",
1387
+ code: "QFAI-GR-006",
1388
+ message: "Rationale is missing",
1389
+ file,
1390
+ line,
1391
+ ...id ? { id } : {}
1392
+ });
1393
+ }
1394
+ if (!reconsider) {
1395
+ warnings.push({
1396
+ severity: "warning",
1397
+ code: "QFAI-GR-007",
1398
+ message: "Reconsider is missing",
1399
+ file,
1400
+ line,
1401
+ ...id ? { id } : {}
1402
+ });
1403
+ }
1404
+ }
1405
+ for (const [id, list] of idMap.entries()) {
1406
+ if (list.length > 1) {
1407
+ const locations = list.map((entry) => `${entry.source.file}:${entry.source.line}`).join(", ");
1408
+ const first = list[0];
1409
+ const file = first?.source.file ?? "";
1410
+ const line = first?.source.line;
1411
+ errors.push({
1412
+ severity: "error",
1413
+ code: "QFAI-GR-008",
1414
+ message: `ID is duplicated: ${id} (${locations})`,
1415
+ file,
1416
+ ...line !== void 0 ? { line } : {},
1417
+ id
1418
+ });
1419
+ }
1420
+ }
1421
+ return { errors, warnings };
1422
+ }
1423
+ function normalizeGuardrailType(raw) {
1424
+ if (!raw) {
1425
+ return null;
1426
+ }
1427
+ const normalized = raw.trim().toLowerCase().replace(/[_\s]+/g, "-");
1428
+ if (normalized === "non-goal") {
1429
+ return "non-goal";
1430
+ }
1431
+ if (normalized === "not-now") {
1432
+ return "not-now";
1433
+ }
1434
+ if (normalized === "trade-off") {
1435
+ return "trade-off";
1436
+ }
1437
+ return null;
1438
+ }
1439
+ function normalizeFieldKey(raw) {
1440
+ const normalized = raw.trim().toLowerCase().replace(/[_\s-]+/g, "");
1441
+ switch (normalized) {
1442
+ case "id":
1443
+ return "id";
1444
+ case "type":
1445
+ return "type";
1446
+ case "guardrail":
1447
+ return "guardrail";
1448
+ case "rationale":
1449
+ case "reason":
1450
+ return "rationale";
1451
+ case "reconsider":
1452
+ return "reconsider";
1453
+ case "related":
1454
+ return "related";
1455
+ case "keywords":
1456
+ case "keyword":
1457
+ return "keywords";
1458
+ case "title":
1459
+ case "heading":
1460
+ return "title";
1461
+ default:
1462
+ return null;
1463
+ }
1464
+ }
1465
+ async function scanDecisionGuardrailFiles(root, rawPaths, errors, specsRoot) {
1466
+ if (!rawPaths || rawPaths.length === 0) {
1467
+ const scanRoot = specsRoot ? path10.isAbsolute(specsRoot) ? specsRoot : path10.resolve(root, specsRoot) : root;
1468
+ const globs = specsRoot ? ["**/delta.md"] : DEFAULT_DECISION_GUARDRAILS_GLOBS;
1469
+ try {
1470
+ const result = await collectFilesByGlobs(scanRoot, {
1471
+ globs,
1472
+ ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
1473
+ });
1474
+ return result.files.sort((a, b) => a.localeCompare(b));
1475
+ } catch (error2) {
1476
+ errors.push({ path: scanRoot, message: String(error2) });
1477
+ return [];
1478
+ }
1479
+ }
1480
+ const files = /* @__PURE__ */ new Set();
1481
+ for (const rawPath of rawPaths) {
1482
+ const resolved = path10.isAbsolute(rawPath) ? rawPath : path10.resolve(root, rawPath);
1483
+ const stats = await safeStat(resolved);
1484
+ if (!stats) {
1485
+ errors.push({ path: resolved, message: "Path does not exist" });
1486
+ continue;
1487
+ }
1488
+ if (stats.isFile()) {
1489
+ files.add(resolved);
1490
+ continue;
1491
+ }
1492
+ if (stats.isDirectory()) {
1493
+ try {
1494
+ const result = await collectFilesByGlobs(resolved, {
1495
+ globs: ["**/delta.md"],
1496
+ ignore: DEFAULT_GUARDRAILS_IGNORE_GLOBS
1497
+ });
1498
+ result.files.forEach((file) => files.add(file));
1499
+ } catch (error2) {
1500
+ errors.push({ path: resolved, message: String(error2) });
1501
+ }
1502
+ continue;
1503
+ }
1504
+ errors.push({ path: resolved, message: "Unsupported path type" });
1505
+ }
1506
+ return Array.from(files).sort((a, b) => a.localeCompare(b));
1507
+ }
1508
+ async function safeStat(target) {
1509
+ try {
1510
+ return await stat(target);
1511
+ } catch {
1512
+ return null;
1513
+ }
1514
+ }
1515
+ function findDecisionGuardrailsSection(sections) {
1516
+ for (const [title, section] of sections.entries()) {
1517
+ if (title.trim().toLowerCase() === SECTION_TITLE) {
1518
+ return section;
1519
+ }
1520
+ }
1521
+ return null;
1522
+ }
1523
+
1078
1524
  // src/core/doctor.ts
1079
1525
  async function exists4(target) {
1080
1526
  try {
@@ -1098,7 +1544,7 @@ function normalizeGlobs2(values) {
1098
1544
  return values.map((glob) => glob.trim()).filter((glob) => glob.length > 0);
1099
1545
  }
1100
1546
  async function createDoctorData(options) {
1101
- const startDir = path10.resolve(options.startDir);
1547
+ const startDir = path11.resolve(options.startDir);
1102
1548
  const checks = [];
1103
1549
  const configPath = getConfigPath(startDir);
1104
1550
  const search = options.rootExplicit ? {
@@ -1160,9 +1606,9 @@ async function createDoctorData(options) {
1160
1606
  details: { path: toRelativePath(root, resolved) }
1161
1607
  });
1162
1608
  if (key === "promptsDir") {
1163
- const promptsLocalDir = path10.join(
1164
- path10.dirname(resolved),
1165
- `${path10.basename(resolved)}.local`
1609
+ const promptsLocalDir = path11.join(
1610
+ path11.dirname(resolved),
1611
+ `${path11.basename(resolved)}.local`
1166
1612
  );
1167
1613
  const found = await exists4(promptsLocalDir);
1168
1614
  addCheck(checks, {
@@ -1235,7 +1681,36 @@ async function createDoctorData(options) {
1235
1681
  message: missingFiles === 0 ? `All spec packs have required files (count=${entries.length})` : `Missing required files in spec packs (missingFiles=${missingFiles})`,
1236
1682
  details: { specPacks: entries.length, missingFiles }
1237
1683
  });
1238
- const validateJsonAbs = path10.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path10.resolve(root, config.output.validateJsonPath);
1684
+ const guardrailsLoad = await loadDecisionGuardrails(root, {
1685
+ specsRoot
1686
+ });
1687
+ const guardrailsItems = normalizeDecisionGuardrails(guardrailsLoad.entries);
1688
+ let guardrailsSeverity;
1689
+ let guardrailsMessage;
1690
+ if (guardrailsLoad.errors.length > 0) {
1691
+ guardrailsSeverity = "warning";
1692
+ guardrailsMessage = `Decision Guardrails scan failed (errors=${guardrailsLoad.errors.length})`;
1693
+ } else if (guardrailsItems.length === 0) {
1694
+ guardrailsSeverity = "info";
1695
+ guardrailsMessage = "Decision Guardrails not found (optional)";
1696
+ } else {
1697
+ guardrailsSeverity = "ok";
1698
+ guardrailsMessage = `Decision Guardrails detected (count=${guardrailsItems.length})`;
1699
+ }
1700
+ addCheck(checks, {
1701
+ id: "guardrails.present",
1702
+ severity: guardrailsSeverity,
1703
+ title: "Decision Guardrails",
1704
+ message: guardrailsMessage,
1705
+ details: {
1706
+ count: guardrailsItems.length,
1707
+ errors: guardrailsLoad.errors.map((item) => ({
1708
+ path: toRelativePath(root, item.path),
1709
+ message: item.message
1710
+ }))
1711
+ }
1712
+ });
1713
+ const validateJsonAbs = path11.isAbsolute(config.output.validateJsonPath) ? config.output.validateJsonPath : path11.resolve(root, config.output.validateJsonPath);
1239
1714
  const validateJsonExists = await exists4(validateJsonAbs);
1240
1715
  addCheck(checks, {
1241
1716
  id: "output.validateJson",
@@ -1245,8 +1720,8 @@ async function createDoctorData(options) {
1245
1720
  details: { path: toRelativePath(root, validateJsonAbs) }
1246
1721
  });
1247
1722
  const outDirAbs = resolvePath(root, config, "outDir");
1248
- const rel = path10.relative(outDirAbs, validateJsonAbs);
1249
- const inside = rel !== "" && !rel.startsWith("..") && !path10.isAbsolute(rel);
1723
+ const rel = path11.relative(outDirAbs, validateJsonAbs);
1724
+ const inside = rel !== "" && !rel.startsWith("..") && !path11.isAbsolute(rel);
1250
1725
  addCheck(checks, {
1251
1726
  id: "output.pathAlignment",
1252
1727
  severity: inside ? "ok" : "warning",
@@ -1369,12 +1844,12 @@ async function detectOutDirCollisions(root) {
1369
1844
  });
1370
1845
  const configPaths = configScan.files;
1371
1846
  const configRoots = Array.from(
1372
- new Set(configPaths.map((configPath) => path10.dirname(configPath)))
1847
+ new Set(configPaths.map((configPath) => path11.dirname(configPath)))
1373
1848
  ).sort((a, b) => a.localeCompare(b));
1374
1849
  const outDirToRoots = /* @__PURE__ */ new Map();
1375
1850
  for (const configRoot of configRoots) {
1376
1851
  const { config } = await loadConfig(configRoot);
1377
- const outDir = path10.normalize(resolvePath(configRoot, config, "outDir"));
1852
+ const outDir = path11.normalize(resolvePath(configRoot, config, "outDir"));
1378
1853
  const roots = outDirToRoots.get(outDir) ?? /* @__PURE__ */ new Set();
1379
1854
  roots.add(configRoot);
1380
1855
  outDirToRoots.set(outDir, roots);
@@ -1400,20 +1875,20 @@ async function detectOutDirCollisions(root) {
1400
1875
  };
1401
1876
  }
1402
1877
  async function findMonorepoRoot(startDir) {
1403
- let current = path10.resolve(startDir);
1878
+ let current = path11.resolve(startDir);
1404
1879
  while (true) {
1405
- const gitPath = path10.join(current, ".git");
1406
- const workspacePath = path10.join(current, "pnpm-workspace.yaml");
1880
+ const gitPath = path11.join(current, ".git");
1881
+ const workspacePath = path11.join(current, "pnpm-workspace.yaml");
1407
1882
  if (await exists4(gitPath) || await exists4(workspacePath)) {
1408
1883
  return current;
1409
1884
  }
1410
- const parent = path10.dirname(current);
1885
+ const parent = path11.dirname(current);
1411
1886
  if (parent === current) {
1412
1887
  break;
1413
1888
  }
1414
1889
  current = parent;
1415
1890
  }
1416
- return path10.resolve(startDir);
1891
+ return path11.resolve(startDir);
1417
1892
  }
1418
1893
 
1419
1894
  // src/cli/lib/logger.ts
@@ -1455,8 +1930,8 @@ async function runDoctor(options) {
1455
1930
  const output = options.format === "json" ? formatDoctorJson(data) : formatDoctorText(data);
1456
1931
  const exitCode = shouldFailDoctor(data.summary, options.failOn) ? 1 : 0;
1457
1932
  if (options.outPath) {
1458
- const outAbs = path11.isAbsolute(options.outPath) ? options.outPath : path11.resolve(process.cwd(), options.outPath);
1459
- await mkdir(path11.dirname(outAbs), { recursive: true });
1933
+ const outAbs = path12.isAbsolute(options.outPath) ? options.outPath : path12.resolve(process.cwd(), options.outPath);
1934
+ await mkdir(path12.dirname(outAbs), { recursive: true });
1460
1935
  await writeFile(outAbs, `${output}
1461
1936
  `, "utf-8");
1462
1937
  info(`doctor: wrote ${outAbs}`);
@@ -1475,12 +1950,77 @@ function shouldFailDoctor(summary, failOn) {
1475
1950
  return summary.warning + summary.error > 0;
1476
1951
  }
1477
1952
 
1478
- // src/cli/commands/init.ts
1953
+ // src/cli/commands/guardrails.ts
1479
1954
  import path13 from "path";
1955
+ var DEFAULT_EXTRACT_MAX = 20;
1956
+ async function runGuardrails(options) {
1957
+ if (!options.action) {
1958
+ error("guardrails: action is required (list|extract|check)");
1959
+ return 2;
1960
+ }
1961
+ const root = path13.resolve(options.root);
1962
+ const { entries, errors } = await loadDecisionGuardrails(root, {
1963
+ paths: options.paths
1964
+ });
1965
+ if (errors.length > 0) {
1966
+ errors.forEach((item) => {
1967
+ error(`guardrails: ${item.path}: ${item.message}`);
1968
+ });
1969
+ return 2;
1970
+ }
1971
+ if (options.action === "check") {
1972
+ return runGuardrailsCheck(entries, root);
1973
+ }
1974
+ const items = sortDecisionGuardrails(normalizeDecisionGuardrails(entries));
1975
+ const filtered = filterDecisionGuardrailsByKeyword(items, options.keyword);
1976
+ if (options.action === "extract") {
1977
+ const max = options.max !== void 0 ? options.max : DEFAULT_EXTRACT_MAX;
1978
+ if (!Number.isFinite(max) || max < 0) {
1979
+ error("guardrails: --max must be a non-negative number");
1980
+ return 2;
1981
+ }
1982
+ info(formatGuardrailsForLlm(filtered, max));
1983
+ return 0;
1984
+ }
1985
+ info(formatGuardrailsList(filtered, root));
1986
+ return 0;
1987
+ }
1988
+ function formatGuardrailsList(items, root) {
1989
+ const lines = ["# Decision Guardrails (list)", ""];
1990
+ if (items.length === 0) {
1991
+ lines.push("- (none)");
1992
+ return lines.join("\n");
1993
+ }
1994
+ for (const item of items) {
1995
+ const relPath = toRelativePath(root, item.source.file);
1996
+ const location = `${relPath}:${item.source.line}`;
1997
+ lines.push(`- [${item.id}][${item.type}] ${item.guardrail} (${location})`);
1998
+ }
1999
+ return lines.join("\n");
2000
+ }
2001
+ function runGuardrailsCheck(entries, root) {
2002
+ const result = checkDecisionGuardrails(entries);
2003
+ const lines = [
2004
+ `guardrails check: error=${result.errors.length} warning=${result.warnings.length}`
2005
+ ];
2006
+ const formatIssue = (issue7) => {
2007
+ const relPath = toRelativePath(root, issue7.file);
2008
+ const line = issue7.line ? `:${issue7.line}` : "";
2009
+ const id = issue7.id ? ` id=${issue7.id}` : "";
2010
+ return `[${issue7.severity}] ${issue7.code} ${issue7.message} (${relPath}${line})${id}`;
2011
+ };
2012
+ result.errors.forEach((issue7) => lines.push(formatIssue(issue7)));
2013
+ result.warnings.forEach((issue7) => lines.push(formatIssue(issue7)));
2014
+ info(lines.join("\n"));
2015
+ return result.errors.length > 0 ? 1 : 0;
2016
+ }
2017
+
2018
+ // src/cli/commands/init.ts
2019
+ import path15 from "path";
1480
2020
 
1481
2021
  // src/cli/lib/fs.ts
1482
2022
  import { access as access5, copyFile, mkdir as mkdir2, readdir as readdir3 } from "fs/promises";
1483
- import path12 from "path";
2023
+ import path14 from "path";
1484
2024
  async function copyTemplateTree(sourceRoot, destRoot, options) {
1485
2025
  const files = await collectTemplateFiles(sourceRoot);
1486
2026
  return copyFiles(files, sourceRoot, destRoot, options);
@@ -1488,7 +2028,7 @@ async function copyTemplateTree(sourceRoot, destRoot, options) {
1488
2028
  async function copyTemplatePaths(sourceRoot, destRoot, relativePaths, options) {
1489
2029
  const allFiles = [];
1490
2030
  for (const relPath of relativePaths) {
1491
- const fullPath = path12.join(sourceRoot, relPath);
2031
+ const fullPath = path14.join(sourceRoot, relPath);
1492
2032
  const files = await collectTemplateFiles(fullPath);
1493
2033
  allFiles.push(...files);
1494
2034
  }
@@ -1498,13 +2038,13 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1498
2038
  const copied = [];
1499
2039
  const skipped = [];
1500
2040
  const conflicts = [];
1501
- const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
1502
- const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path12.sep);
2041
+ const protectPrefixes = (options.protect ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path14.sep);
2042
+ const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p + path14.sep);
1503
2043
  const isProtectedRelative = (relative) => {
1504
2044
  if (protectPrefixes.length === 0) {
1505
2045
  return false;
1506
2046
  }
1507
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
2047
+ const normalized = relative.replace(/[\\/]+/g, path14.sep);
1508
2048
  return protectPrefixes.some(
1509
2049
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1510
2050
  );
@@ -1513,7 +2053,7 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1513
2053
  if (excludePrefixes.length === 0) {
1514
2054
  return false;
1515
2055
  }
1516
- const normalized = relative.replace(/[\\/]+/g, path12.sep);
2056
+ const normalized = relative.replace(/[\\/]+/g, path14.sep);
1517
2057
  return excludePrefixes.some(
1518
2058
  (prefix) => normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)
1519
2059
  );
@@ -1521,14 +2061,14 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1521
2061
  const conflictPolicy = options.conflictPolicy ?? "error";
1522
2062
  if (!options.force && conflictPolicy === "error") {
1523
2063
  for (const file of files) {
1524
- const relative = path12.relative(sourceRoot, file);
2064
+ const relative = path14.relative(sourceRoot, file);
1525
2065
  if (isExcludedRelative(relative)) {
1526
2066
  continue;
1527
2067
  }
1528
2068
  if (isProtectedRelative(relative)) {
1529
2069
  continue;
1530
2070
  }
1531
- const dest = path12.join(destRoot, relative);
2071
+ const dest = path14.join(destRoot, relative);
1532
2072
  if (!await shouldWrite(dest, options.force)) {
1533
2073
  conflicts.push(dest);
1534
2074
  }
@@ -1538,18 +2078,18 @@ async function copyFiles(files, sourceRoot, destRoot, options) {
1538
2078
  }
1539
2079
  }
1540
2080
  for (const file of files) {
1541
- const relative = path12.relative(sourceRoot, file);
2081
+ const relative = path14.relative(sourceRoot, file);
1542
2082
  if (isExcludedRelative(relative)) {
1543
2083
  continue;
1544
2084
  }
1545
- const dest = path12.join(destRoot, relative);
2085
+ const dest = path14.join(destRoot, relative);
1546
2086
  const forceForThisFile = isProtectedRelative(relative) ? false : options.force;
1547
2087
  if (!await shouldWrite(dest, forceForThisFile)) {
1548
2088
  skipped.push(dest);
1549
2089
  continue;
1550
2090
  }
1551
2091
  if (!options.dryRun) {
1552
- await mkdir2(path12.dirname(dest), { recursive: true });
2092
+ await mkdir2(path14.dirname(dest), { recursive: true });
1553
2093
  await copyFile(file, dest);
1554
2094
  }
1555
2095
  copied.push(dest);
@@ -1573,7 +2113,7 @@ async function collectTemplateFiles(root) {
1573
2113
  }
1574
2114
  const items = await readdir3(root, { withFileTypes: true });
1575
2115
  for (const item of items) {
1576
- const fullPath = path12.join(root, item.name);
2116
+ const fullPath = path14.join(root, item.name);
1577
2117
  if (item.isDirectory()) {
1578
2118
  const nested = await collectTemplateFiles(fullPath);
1579
2119
  entries.push(...nested);
@@ -1603,10 +2143,10 @@ async function exists5(target) {
1603
2143
  // src/cli/commands/init.ts
1604
2144
  async function runInit(options) {
1605
2145
  const assetsRoot = getInitAssetsDir();
1606
- const rootAssets = path13.join(assetsRoot, "root");
1607
- const qfaiAssets = path13.join(assetsRoot, ".qfai");
1608
- const destRoot = path13.resolve(options.dir);
1609
- const destQfai = path13.join(destRoot, ".qfai");
2146
+ const rootAssets = path15.join(assetsRoot, "root");
2147
+ const qfaiAssets = path15.join(assetsRoot, ".qfai");
2148
+ const destRoot = path15.resolve(options.dir);
2149
+ const destQfai = path15.join(destRoot, ".qfai");
1610
2150
  if (options.force) {
1611
2151
  info(
1612
2152
  "NOTE: --force \u306F .qfai/assistant/prompts/** \u306E\u307F\u4E0A\u66F8\u304D\u3057\u307E\u3059\uFF08prompts.local \u306F\u4FDD\u8B77\u3055\u308C\u3001specs/contracts \u7B49\u306F\u4E0A\u66F8\u304D\u3057\u307E\u305B\u3093\uFF09\u3002"
@@ -1639,22 +2179,28 @@ async function runInit(options) {
1639
2179
  [...rootResult.copied, ...qfaiResult.copied, ...promptsResult.copied],
1640
2180
  [...rootResult.skipped, ...qfaiResult.skipped, ...promptsResult.skipped],
1641
2181
  options.dryRun,
1642
- "init"
2182
+ "init",
2183
+ destRoot
1643
2184
  );
1644
2185
  }
1645
- function report(copied, skipped, dryRun, label) {
2186
+ function report(copied, skipped, dryRun, label, baseDir) {
1646
2187
  info(`qfai ${label}: ${dryRun ? "dry-run" : "done"}`);
1647
2188
  if (copied.length > 0) {
1648
2189
  info(` created: ${copied.length}`);
1649
2190
  }
1650
2191
  if (skipped.length > 0) {
1651
2192
  info(` skipped: ${skipped.length}`);
2193
+ info(" skipped paths:");
2194
+ for (const skippedPath of skipped) {
2195
+ const relative = path15.relative(baseDir, skippedPath);
2196
+ info(` - ${relative}`);
2197
+ }
1652
2198
  }
1653
2199
  }
1654
2200
 
1655
2201
  // src/cli/commands/report.ts
1656
- import { mkdir as mkdir3, readFile as readFile13, writeFile as writeFile2 } from "fs/promises";
1657
- import path21 from "path";
2202
+ import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
2203
+ import path23 from "path";
1658
2204
 
1659
2205
  // src/core/normalize.ts
1660
2206
  function normalizeIssuePaths(root, issues) {
@@ -1694,12 +2240,12 @@ function normalizeValidationResult(root, result) {
1694
2240
  }
1695
2241
 
1696
2242
  // src/core/report.ts
1697
- import { readFile as readFile12 } from "fs/promises";
1698
- import path20 from "path";
2243
+ import { readFile as readFile13 } from "fs/promises";
2244
+ import path22 from "path";
1699
2245
 
1700
2246
  // src/core/contractIndex.ts
1701
- import { readFile as readFile5 } from "fs/promises";
1702
- import path14 from "path";
2247
+ import { readFile as readFile6 } from "fs/promises";
2248
+ import path16 from "path";
1703
2249
 
1704
2250
  // src/core/contractsDecl.ts
1705
2251
  var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
@@ -1721,9 +2267,9 @@ function stripContractDeclarationLines(text) {
1721
2267
  // src/core/contractIndex.ts
1722
2268
  async function buildContractIndex(root, config) {
1723
2269
  const contractsRoot = resolvePath(root, config, "contractsDir");
1724
- const uiRoot = path14.join(contractsRoot, "ui");
1725
- const apiRoot = path14.join(contractsRoot, "api");
1726
- const dbRoot = path14.join(contractsRoot, "db");
2270
+ const uiRoot = path16.join(contractsRoot, "ui");
2271
+ const apiRoot = path16.join(contractsRoot, "api");
2272
+ const dbRoot = path16.join(contractsRoot, "db");
1727
2273
  const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
1728
2274
  collectUiContractFiles(uiRoot),
1729
2275
  collectThemaContractFiles(uiRoot),
@@ -1743,7 +2289,7 @@ async function buildContractIndex(root, config) {
1743
2289
  }
1744
2290
  async function indexContractFiles(files, index) {
1745
2291
  for (const file of files) {
1746
- const text = await readFile5(file, "utf-8");
2292
+ const text = await readFile6(file, "utf-8");
1747
2293
  extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
1748
2294
  }
1749
2295
  }
@@ -1868,53 +2414,11 @@ function unique3(values) {
1868
2414
  return Array.from(new Set(values));
1869
2415
  }
1870
2416
 
1871
- // src/core/parse/markdown.ts
1872
- var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
1873
- function parseHeadings(md) {
1874
- const lines = md.split(/\r?\n/);
1875
- const headings = [];
1876
- for (let i = 0; i < lines.length; i++) {
1877
- const line = lines[i] ?? "";
1878
- const match = line.match(HEADING_RE);
1879
- if (!match) continue;
1880
- const levelToken = match[1];
1881
- const title = match[2];
1882
- if (!levelToken || !title) continue;
1883
- headings.push({
1884
- level: levelToken.length,
1885
- title: title.trim(),
1886
- line: i + 1
1887
- });
1888
- }
1889
- return headings;
1890
- }
1891
- function extractH2Sections(md) {
1892
- const lines = md.split(/\r?\n/);
1893
- const headings = parseHeadings(md).filter((heading) => heading.level === 2);
1894
- const sections = /* @__PURE__ */ new Map();
1895
- for (let i = 0; i < headings.length; i++) {
1896
- const current = headings[i];
1897
- if (!current) continue;
1898
- const next = headings[i + 1];
1899
- const startLine = current.line + 1;
1900
- const endLine = (next?.line ?? lines.length + 1) - 1;
1901
- const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
1902
- sections.set(current.title.trim(), {
1903
- title: current.title.trim(),
1904
- startLine,
1905
- endLine,
1906
- body
1907
- });
1908
- }
1909
- return sections;
1910
- }
1911
-
1912
2417
  // src/core/parse/spec.ts
1913
2418
  var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
1914
2419
  var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
1915
2420
  var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
1916
2421
  var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
1917
- var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
1918
2422
  var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
1919
2423
  function parseSpec(md, file) {
1920
2424
  const headings = parseHeadings(md);
@@ -1922,15 +2426,13 @@ function parseSpec(md, file) {
1922
2426
  const specId = h1?.title.match(SPEC_ID_RE)?.[0];
1923
2427
  const sections = extractH2Sections(md);
1924
2428
  const sectionNames = new Set(Array.from(sections.keys()));
1925
- const brSection = sections.get(BR_SECTION_TITLE);
1926
- const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
1927
- const startLine = brSection?.startLine ?? 1;
2429
+ const lines = md.split(/\r?\n/);
1928
2430
  const brs = [];
1929
2431
  const brsWithoutPriority = [];
1930
2432
  const brsWithInvalidPriority = [];
1931
- for (let i = 0; i < brLines.length; i++) {
1932
- const lineText = brLines[i] ?? "";
1933
- const lineNumber = startLine + i;
2433
+ for (let i = 0; i < lines.length; i++) {
2434
+ const lineText = lines[i] ?? "";
2435
+ const lineNumber = i + 1;
1934
2436
  const validMatch = lineText.match(BR_LINE_RE);
1935
2437
  if (validMatch) {
1936
2438
  const id = validMatch[1];
@@ -1988,14 +2490,14 @@ function parseSpec(md, file) {
1988
2490
  }
1989
2491
 
1990
2492
  // src/core/validators/contracts.ts
1991
- import { access as access6, readFile as readFile6 } from "fs/promises";
1992
- import path16 from "path";
2493
+ import { access as access6, readFile as readFile7 } from "fs/promises";
2494
+ import path18 from "path";
1993
2495
 
1994
2496
  // src/core/contracts.ts
1995
- import path15 from "path";
2497
+ import path17 from "path";
1996
2498
  import { parse as parseYaml2 } from "yaml";
1997
2499
  function parseStructuredContract(file, text) {
1998
- const ext = path15.extname(file).toLowerCase();
2500
+ const ext = path17.extname(file).toLowerCase();
1999
2501
  if (ext === ".json") {
2000
2502
  return JSON.parse(text);
2001
2503
  }
@@ -2017,14 +2519,14 @@ async function validateContracts(root, config) {
2017
2519
  const issues = [];
2018
2520
  const contractIndex = await buildContractIndex(root, config);
2019
2521
  const contractsRoot = resolvePath(root, config, "contractsDir");
2020
- const uiRoot = path16.join(contractsRoot, "ui");
2522
+ const uiRoot = path18.join(contractsRoot, "ui");
2021
2523
  const themaIds = new Set(
2022
2524
  Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
2023
2525
  );
2024
2526
  issues.push(...await validateUiContracts(uiRoot, themaIds));
2025
2527
  issues.push(...await validateThemaContracts(uiRoot));
2026
- issues.push(...await validateApiContracts(path16.join(contractsRoot, "api")));
2027
- issues.push(...await validateDbContracts(path16.join(contractsRoot, "db")));
2528
+ issues.push(...await validateApiContracts(path18.join(contractsRoot, "api")));
2529
+ issues.push(...await validateDbContracts(path18.join(contractsRoot, "db")));
2028
2530
  issues.push(...validateDuplicateContractIds(contractIndex));
2029
2531
  return issues;
2030
2532
  }
@@ -2043,7 +2545,7 @@ async function validateUiContracts(uiRoot, themaIds) {
2043
2545
  }
2044
2546
  const issues = [];
2045
2547
  for (const file of files) {
2046
- const text = await readFile6(file, "utf-8");
2548
+ const text = await readFile7(file, "utf-8");
2047
2549
  const declaredIds = extractDeclaredContractIds(text);
2048
2550
  issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
2049
2551
  let doc = null;
@@ -2097,7 +2599,7 @@ async function validateThemaContracts(uiRoot) {
2097
2599
  }
2098
2600
  const issues = [];
2099
2601
  for (const file of files) {
2100
- const text = await readFile6(file, "utf-8");
2602
+ const text = await readFile7(file, "utf-8");
2101
2603
  const invalidIds = extractInvalidIds(text, [
2102
2604
  "SPEC",
2103
2605
  "BR",
@@ -2231,7 +2733,7 @@ async function validateApiContracts(apiRoot) {
2231
2733
  }
2232
2734
  const issues = [];
2233
2735
  for (const file of files) {
2234
- const text = await readFile6(file, "utf-8");
2736
+ const text = await readFile7(file, "utf-8");
2235
2737
  const invalidIds = extractInvalidIds(text, [
2236
2738
  "SPEC",
2237
2739
  "BR",
@@ -2300,7 +2802,7 @@ async function validateDbContracts(dbRoot) {
2300
2802
  }
2301
2803
  const issues = [];
2302
2804
  for (const file of files) {
2303
- const text = await readFile6(file, "utf-8");
2805
+ const text = await readFile7(file, "utf-8");
2304
2806
  const invalidIds = extractInvalidIds(text, [
2305
2807
  "SPEC",
2306
2808
  "BR",
@@ -2497,9 +2999,9 @@ async function validateUiAssets(assets, file, uiRoot) {
2497
2999
  );
2498
3000
  return issues;
2499
3001
  }
2500
- const packDir = path16.resolve(uiRoot, packValue);
2501
- const packRelative = path16.relative(uiRoot, packDir);
2502
- if (packRelative.startsWith("..") || path16.isAbsolute(packRelative)) {
3002
+ const packDir = path18.resolve(uiRoot, packValue);
3003
+ const packRelative = path18.relative(uiRoot, packDir);
3004
+ if (packRelative.startsWith("..") || path18.isAbsolute(packRelative)) {
2503
3005
  issues.push(
2504
3006
  issue(
2505
3007
  "QFAI-ASSET-001",
@@ -2525,7 +3027,7 @@ async function validateUiAssets(assets, file, uiRoot) {
2525
3027
  );
2526
3028
  return issues;
2527
3029
  }
2528
- const assetsYamlPath = path16.join(packDir, "assets.yaml");
3030
+ const assetsYamlPath = path18.join(packDir, "assets.yaml");
2529
3031
  if (!await exists6(assetsYamlPath)) {
2530
3032
  issues.push(
2531
3033
  issue(
@@ -2540,7 +3042,7 @@ async function validateUiAssets(assets, file, uiRoot) {
2540
3042
  }
2541
3043
  let manifest;
2542
3044
  try {
2543
- const manifestText = await readFile6(assetsYamlPath, "utf-8");
3045
+ const manifestText = await readFile7(assetsYamlPath, "utf-8");
2544
3046
  manifest = parseStructuredContract(assetsYamlPath, manifestText);
2545
3047
  } catch (error2) {
2546
3048
  issues.push(
@@ -2613,9 +3115,9 @@ async function validateUiAssets(assets, file, uiRoot) {
2613
3115
  );
2614
3116
  continue;
2615
3117
  }
2616
- const assetPath = path16.resolve(packDir, entry.path);
2617
- const assetRelative = path16.relative(packDir, assetPath);
2618
- if (assetRelative.startsWith("..") || path16.isAbsolute(assetRelative)) {
3118
+ const assetPath = path18.resolve(packDir, entry.path);
3119
+ const assetRelative = path18.relative(packDir, assetPath);
3120
+ if (assetRelative.startsWith("..") || path18.isAbsolute(assetRelative)) {
2619
3121
  issues.push(
2620
3122
  issue(
2621
3123
  "QFAI-ASSET-004",
@@ -2656,7 +3158,7 @@ function shouldIgnoreInvalidId(value, doc) {
2656
3158
  return false;
2657
3159
  }
2658
3160
  const normalized = packValue.replace(/\\/g, "/");
2659
- const basename = path16.posix.basename(normalized);
3161
+ const basename = path18.posix.basename(normalized);
2660
3162
  if (!basename) {
2661
3163
  return false;
2662
3164
  }
@@ -2666,7 +3168,7 @@ function isSafeRelativePath(value) {
2666
3168
  if (!value) {
2667
3169
  return false;
2668
3170
  }
2669
- if (path16.isAbsolute(value)) {
3171
+ if (path18.isAbsolute(value)) {
2670
3172
  return false;
2671
3173
  }
2672
3174
  const normalized = value.replace(/\\/g, "/");
@@ -2716,8 +3218,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
2716
3218
  }
2717
3219
 
2718
3220
  // src/core/validators/delta.ts
2719
- import { readFile as readFile7 } from "fs/promises";
2720
- import path17 from "path";
3221
+ import { readFile as readFile8 } from "fs/promises";
3222
+ import path19 from "path";
2721
3223
  async function validateDeltas(root, config) {
2722
3224
  const specsRoot = resolvePath(root, config, "specsDir");
2723
3225
  const packs = await collectSpecPackDirs(specsRoot);
@@ -2726,9 +3228,9 @@ async function validateDeltas(root, config) {
2726
3228
  }
2727
3229
  const issues = [];
2728
3230
  for (const pack of packs) {
2729
- const deltaPath = path17.join(pack, "delta.md");
3231
+ const deltaPath = path19.join(pack, "delta.md");
2730
3232
  try {
2731
- await readFile7(deltaPath, "utf-8");
3233
+ await readFile8(deltaPath, "utf-8");
2732
3234
  } catch (error2) {
2733
3235
  if (isMissingFileError2(error2)) {
2734
3236
  issues.push(
@@ -2779,8 +3281,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
2779
3281
  }
2780
3282
 
2781
3283
  // src/core/validators/ids.ts
2782
- import { readFile as readFile8 } from "fs/promises";
2783
- import path18 from "path";
3284
+ import { readFile as readFile9 } from "fs/promises";
3285
+ import path20 from "path";
2784
3286
  var SC_TAG_RE3 = /^SC-\d{4}$/;
2785
3287
  async function validateDefinedIds(root, config) {
2786
3288
  const issues = [];
@@ -2815,7 +3317,7 @@ async function validateDefinedIds(root, config) {
2815
3317
  }
2816
3318
  async function collectSpecDefinitionIds(files, out) {
2817
3319
  for (const file of files) {
2818
- const text = await readFile8(file, "utf-8");
3320
+ const text = await readFile9(file, "utf-8");
2819
3321
  const parsed = parseSpec(text, file);
2820
3322
  if (parsed.specId) {
2821
3323
  recordId(out, parsed.specId, file);
@@ -2825,7 +3327,7 @@ async function collectSpecDefinitionIds(files, out) {
2825
3327
  }
2826
3328
  async function collectScenarioDefinitionIds(files, out) {
2827
3329
  for (const file of files) {
2828
- const text = await readFile8(file, "utf-8");
3330
+ const text = await readFile9(file, "utf-8");
2829
3331
  const { document, errors } = parseScenarioDocument(text, file);
2830
3332
  if (!document || errors.length > 0) {
2831
3333
  continue;
@@ -2846,7 +3348,7 @@ function recordId(out, id, file) {
2846
3348
  }
2847
3349
  function formatFileList(files, root) {
2848
3350
  return files.map((file) => {
2849
- const relative = path18.relative(root, file);
3351
+ const relative = path20.relative(root, file);
2850
3352
  return relative.length > 0 ? relative : file;
2851
3353
  }).join(", ");
2852
3354
  }
@@ -2904,8 +3406,8 @@ async function validatePromptsIntegrity(root, config) {
2904
3406
  }
2905
3407
 
2906
3408
  // src/core/validators/scenario.ts
2907
- import { access as access7, readFile as readFile9 } from "fs/promises";
2908
- import path19 from "path";
3409
+ import { access as access7, readFile as readFile10 } from "fs/promises";
3410
+ import path21 from "path";
2909
3411
  var GIVEN_PATTERN = /\bGiven\b/;
2910
3412
  var WHEN_PATTERN = /\bWhen\b/;
2911
3413
  var THEN_PATTERN = /\bThen\b/;
@@ -2928,7 +3430,7 @@ async function validateScenarios(root, config) {
2928
3430
  }
2929
3431
  const issues = [];
2930
3432
  for (const entry of entries) {
2931
- const legacyScenarioPath = path19.join(entry.dir, "scenario.md");
3433
+ const legacyScenarioPath = path21.join(entry.dir, "scenario.md");
2932
3434
  if (await fileExists(legacyScenarioPath)) {
2933
3435
  issues.push(
2934
3436
  issue4(
@@ -2942,7 +3444,7 @@ async function validateScenarios(root, config) {
2942
3444
  }
2943
3445
  let text;
2944
3446
  try {
2945
- text = await readFile9(entry.scenarioPath, "utf-8");
3447
+ text = await readFile10(entry.scenarioPath, "utf-8");
2946
3448
  } catch (error2) {
2947
3449
  if (isMissingFileError3(error2)) {
2948
3450
  issues.push(
@@ -3125,7 +3627,7 @@ async function fileExists(target) {
3125
3627
  }
3126
3628
 
3127
3629
  // src/core/validators/spec.ts
3128
- import { readFile as readFile10 } from "fs/promises";
3630
+ import { readFile as readFile11 } from "fs/promises";
3129
3631
  async function validateSpecs(root, config) {
3130
3632
  const specsRoot = resolvePath(root, config, "specsDir");
3131
3633
  const entries = await collectSpecEntries(specsRoot);
@@ -3146,7 +3648,7 @@ async function validateSpecs(root, config) {
3146
3648
  for (const entry of entries) {
3147
3649
  let text;
3148
3650
  try {
3149
- text = await readFile10(entry.specPath, "utf-8");
3651
+ text = await readFile11(entry.specPath, "utf-8");
3150
3652
  } catch (error2) {
3151
3653
  if (isMissingFileError4(error2)) {
3152
3654
  issues.push(
@@ -3300,7 +3802,7 @@ function isMissingFileError4(error2) {
3300
3802
  }
3301
3803
 
3302
3804
  // src/core/validators/traceability.ts
3303
- import { readFile as readFile11 } from "fs/promises";
3805
+ import { readFile as readFile12 } from "fs/promises";
3304
3806
  var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
3305
3807
  var BR_TAG_RE2 = /^BR-\d{4}$/;
3306
3808
  async function validateTraceability(root, config) {
@@ -3320,7 +3822,7 @@ async function validateTraceability(root, config) {
3320
3822
  const contractIndex = await buildContractIndex(root, config);
3321
3823
  const contractIds = contractIndex.ids;
3322
3824
  for (const file of specFiles) {
3323
- const text = await readFile11(file, "utf-8");
3825
+ const text = await readFile12(file, "utf-8");
3324
3826
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
3325
3827
  const parsed = parseSpec(text, file);
3326
3828
  if (parsed.specId) {
@@ -3393,7 +3895,7 @@ async function validateTraceability(root, config) {
3393
3895
  }
3394
3896
  }
3395
3897
  for (const file of scenarioFiles) {
3396
- const text = await readFile11(file, "utf-8");
3898
+ const text = await readFile12(file, "utf-8");
3397
3899
  extractAllIds(text).forEach((id) => upstreamIds.add(id));
3398
3900
  const scenarioContractRefs = parseContractRefs(text, {
3399
3901
  allowCommentPrefix: true
@@ -3715,7 +4217,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
3715
4217
  const pattern = buildIdPattern(Array.from(upstreamIds));
3716
4218
  let found = false;
3717
4219
  for (const file of targetFiles) {
3718
- const text = await readFile11(file, "utf-8");
4220
+ const text = await readFile12(file, "utf-8");
3719
4221
  if (pattern.test(text)) {
3720
4222
  found = true;
3721
4223
  break;
@@ -3814,16 +4316,17 @@ var ID_PREFIXES2 = [
3814
4316
  "DB",
3815
4317
  "THEMA"
3816
4318
  ];
4319
+ var REPORT_GUARDRAILS_MAX = 20;
3817
4320
  async function createReportData(root, validation, configResult) {
3818
- const resolvedRoot = path20.resolve(root);
4321
+ const resolvedRoot = path22.resolve(root);
3819
4322
  const resolved = configResult ?? await loadConfig(resolvedRoot);
3820
4323
  const config = resolved.config;
3821
4324
  const configPath = resolved.configPath;
3822
4325
  const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
3823
4326
  const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
3824
- const apiRoot = path20.join(contractsRoot, "api");
3825
- const uiRoot = path20.join(contractsRoot, "ui");
3826
- const dbRoot = path20.join(contractsRoot, "db");
4327
+ const apiRoot = path22.join(contractsRoot, "api");
4328
+ const uiRoot = path22.join(contractsRoot, "ui");
4329
+ const dbRoot = path22.join(contractsRoot, "db");
3827
4330
  const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
3828
4331
  const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
3829
4332
  const specFiles = await collectSpecFiles(specsRoot);
@@ -3882,6 +4385,27 @@ async function createReportData(root, validation, configResult) {
3882
4385
  const scSourceRecord = mapToSortedRecord(
3883
4386
  normalizeScSources(resolvedRoot, scSources)
3884
4387
  );
4388
+ const guardrailsLoad = await loadDecisionGuardrails(resolvedRoot, {
4389
+ specsRoot
4390
+ });
4391
+ const guardrailsAll = sortDecisionGuardrails(
4392
+ normalizeDecisionGuardrails(guardrailsLoad.entries)
4393
+ );
4394
+ const guardrailsDisplay = guardrailsAll.slice(0, REPORT_GUARDRAILS_MAX);
4395
+ const guardrailsByType = { nonGoal: 0, notNow: 0, tradeOff: 0 };
4396
+ for (const item of guardrailsAll) {
4397
+ if (item.type === "non-goal") {
4398
+ guardrailsByType.nonGoal += 1;
4399
+ } else if (item.type === "not-now") {
4400
+ guardrailsByType.notNow += 1;
4401
+ } else if (item.type === "trade-off") {
4402
+ guardrailsByType.tradeOff += 1;
4403
+ }
4404
+ }
4405
+ const guardrailsErrors = guardrailsLoad.errors.map((item) => ({
4406
+ path: toRelativePath(resolvedRoot, item.path),
4407
+ message: item.message
4408
+ }));
3885
4409
  const version = await resolveToolVersion();
3886
4410
  const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
3887
4411
  const displayConfigPath = toRelativePath(resolvedRoot, configPath);
@@ -3929,6 +4453,34 @@ async function createReportData(root, validation, configResult) {
3929
4453
  specToContracts: specToContractsRecord
3930
4454
  }
3931
4455
  },
4456
+ guardrails: {
4457
+ total: guardrailsAll.length,
4458
+ max: REPORT_GUARDRAILS_MAX,
4459
+ truncated: guardrailsAll.length > guardrailsDisplay.length,
4460
+ byType: guardrailsByType,
4461
+ items: guardrailsDisplay.map((item) => {
4462
+ const entry = {
4463
+ id: item.id,
4464
+ type: item.type,
4465
+ guardrail: item.guardrail,
4466
+ source: {
4467
+ file: toRelativePath(resolvedRoot, item.source.file),
4468
+ line: item.source.line
4469
+ }
4470
+ };
4471
+ if (item.rationale) {
4472
+ entry.rationale = item.rationale;
4473
+ }
4474
+ if (item.reconsider) {
4475
+ entry.reconsider = item.reconsider;
4476
+ }
4477
+ if (item.related) {
4478
+ entry.related = item.related;
4479
+ }
4480
+ return entry;
4481
+ }),
4482
+ scanErrors: guardrailsErrors
4483
+ },
3932
4484
  issues: normalizedValidation.issues
3933
4485
  };
3934
4486
  }
@@ -4024,6 +4576,7 @@ function formatReportMarkdown(data, options = {}) {
4024
4576
  lines.push("");
4025
4577
  lines.push("- [Compatibility Issues](#compatibility-issues)");
4026
4578
  lines.push("- [Change Issues](#change-issues)");
4579
+ lines.push("- [Decision Guardrails](#decision-guardrails)");
4027
4580
  lines.push("- [IDs](#ids)");
4028
4581
  lines.push("- [Traceability](#traceability)");
4029
4582
  lines.push("");
@@ -4115,6 +4668,49 @@ function formatReportMarkdown(data, options = {}) {
4115
4668
  lines.push("### Issues");
4116
4669
  lines.push("");
4117
4670
  lines.push(...formatIssueCards(issuesByCategory.change));
4671
+ lines.push("## Decision Guardrails");
4672
+ lines.push("");
4673
+ lines.push(`- total: ${data.guardrails.total}`);
4674
+ lines.push(
4675
+ `- types: non-goal ${data.guardrails.byType.nonGoal} / not-now ${data.guardrails.byType.notNow} / trade-off ${data.guardrails.byType.tradeOff}`
4676
+ );
4677
+ if (data.guardrails.truncated) {
4678
+ lines.push(`- truncated: true (max=${data.guardrails.max})`);
4679
+ }
4680
+ if (data.guardrails.scanErrors.length > 0) {
4681
+ lines.push(`- scanErrors: ${data.guardrails.scanErrors.length}`);
4682
+ }
4683
+ lines.push("");
4684
+ if (data.guardrails.items.length === 0) {
4685
+ lines.push("- (none)");
4686
+ } else {
4687
+ for (const item of data.guardrails.items) {
4688
+ lines.push(`- [${item.id}][${item.type}] ${item.guardrail}`);
4689
+ lines.push(
4690
+ ` - source: ${formatPathWithLine(item.source.file, { line: item.source.line }, baseUrl)}`
4691
+ );
4692
+ if (item.rationale) {
4693
+ lines.push(` - Rationale: ${item.rationale}`);
4694
+ }
4695
+ if (item.reconsider) {
4696
+ lines.push(` - Reconsider: ${item.reconsider}`);
4697
+ }
4698
+ if (item.related) {
4699
+ lines.push(` - Related: ${item.related}`);
4700
+ }
4701
+ }
4702
+ }
4703
+ if (data.guardrails.scanErrors.length > 0) {
4704
+ lines.push("");
4705
+ lines.push("### Scan errors");
4706
+ lines.push("");
4707
+ for (const errorItem of data.guardrails.scanErrors) {
4708
+ lines.push(
4709
+ `- ${formatPathLink(errorItem.path, baseUrl)}: ${errorItem.message}`
4710
+ );
4711
+ }
4712
+ }
4713
+ lines.push("");
4118
4714
  lines.push("## IDs");
4119
4715
  lines.push("");
4120
4716
  lines.push(formatIdLine("SPEC", data.ids.spec));
@@ -4305,7 +4901,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
4305
4901
  idToSpecs.set(contractId, /* @__PURE__ */ new Set());
4306
4902
  }
4307
4903
  for (const file of specFiles) {
4308
- const text = await readFile12(file, "utf-8");
4904
+ const text = await readFile13(file, "utf-8");
4309
4905
  const parsed = parseSpec(text, file);
4310
4906
  const specKey = parsed.specId;
4311
4907
  if (!specKey) {
@@ -4347,7 +4943,7 @@ async function collectIds(files) {
4347
4943
  THEMA: /* @__PURE__ */ new Set()
4348
4944
  };
4349
4945
  for (const file of files) {
4350
- const text = await readFile12(file, "utf-8");
4946
+ const text = await readFile13(file, "utf-8");
4351
4947
  for (const prefix of ID_PREFIXES2) {
4352
4948
  const ids = extractIds(text, prefix);
4353
4949
  ids.forEach((id) => result[prefix].add(id));
@@ -4366,7 +4962,7 @@ async function collectIds(files) {
4366
4962
  async function collectUpstreamIds(files) {
4367
4963
  const ids = /* @__PURE__ */ new Set();
4368
4964
  for (const file of files) {
4369
- const text = await readFile12(file, "utf-8");
4965
+ const text = await readFile13(file, "utf-8");
4370
4966
  extractAllIds(text).forEach((id) => ids.add(id));
4371
4967
  }
4372
4968
  return ids;
@@ -4387,7 +4983,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
4387
4983
  }
4388
4984
  const pattern = buildIdPattern2(Array.from(upstreamIds));
4389
4985
  for (const file of targetFiles) {
4390
- const text = await readFile12(file, "utf-8");
4986
+ const text = await readFile13(file, "utf-8");
4391
4987
  if (pattern.test(text)) {
4392
4988
  return true;
4393
4989
  }
@@ -4524,7 +5120,7 @@ function warnIfTruncated(scan, context) {
4524
5120
 
4525
5121
  // src/cli/commands/report.ts
4526
5122
  async function runReport(options) {
4527
- const root = path21.resolve(options.root);
5123
+ const root = path23.resolve(options.root);
4528
5124
  const configResult = await loadConfig(root);
4529
5125
  let validation;
4530
5126
  if (options.runValidate) {
@@ -4541,7 +5137,7 @@ async function runReport(options) {
4541
5137
  validation = normalized;
4542
5138
  } else {
4543
5139
  const input = options.inputPath ?? configResult.config.output.validateJsonPath;
4544
- const inputPath = path21.isAbsolute(input) ? input : path21.resolve(root, input);
5140
+ const inputPath = path23.isAbsolute(input) ? input : path23.resolve(root, input);
4545
5141
  try {
4546
5142
  validation = await readValidationResult(inputPath);
4547
5143
  } catch (err) {
@@ -4568,10 +5164,10 @@ async function runReport(options) {
4568
5164
  warnIfTruncated(data.traceability.testFiles, "report");
4569
5165
  const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
4570
5166
  const outRoot = resolvePath(root, configResult.config, "outDir");
4571
- const defaultOut = options.format === "json" ? path21.join(outRoot, "report.json") : path21.join(outRoot, "report.md");
5167
+ const defaultOut = options.format === "json" ? path23.join(outRoot, "report.json") : path23.join(outRoot, "report.md");
4572
5168
  const out = options.outPath ?? defaultOut;
4573
- const outPath = path21.isAbsolute(out) ? out : path21.resolve(root, out);
4574
- await mkdir3(path21.dirname(outPath), { recursive: true });
5169
+ const outPath = path23.isAbsolute(out) ? out : path23.resolve(root, out);
5170
+ await mkdir3(path23.dirname(outPath), { recursive: true });
4575
5171
  await writeFile2(outPath, `${output}
4576
5172
  `, "utf-8");
4577
5173
  info(
@@ -4580,7 +5176,7 @@ async function runReport(options) {
4580
5176
  info(`wrote report: ${outPath}`);
4581
5177
  }
4582
5178
  async function readValidationResult(inputPath) {
4583
- const raw = await readFile13(inputPath, "utf-8");
5179
+ const raw = await readFile14(inputPath, "utf-8");
4584
5180
  const parsed = JSON.parse(raw);
4585
5181
  if (!isValidationResult(parsed)) {
4586
5182
  throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
@@ -4636,15 +5232,15 @@ function isMissingFileError5(error2) {
4636
5232
  return record2.code === "ENOENT";
4637
5233
  }
4638
5234
  async function writeValidationResult(root, outputPath, result) {
4639
- const abs = path21.isAbsolute(outputPath) ? outputPath : path21.resolve(root, outputPath);
4640
- await mkdir3(path21.dirname(abs), { recursive: true });
5235
+ const abs = path23.isAbsolute(outputPath) ? outputPath : path23.resolve(root, outputPath);
5236
+ await mkdir3(path23.dirname(abs), { recursive: true });
4641
5237
  await writeFile2(abs, `${JSON.stringify(result, null, 2)}
4642
5238
  `, "utf-8");
4643
5239
  }
4644
5240
 
4645
5241
  // src/cli/commands/validate.ts
4646
5242
  import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
4647
- import path22 from "path";
5243
+ import path24 from "path";
4648
5244
 
4649
5245
  // src/cli/lib/failOn.ts
4650
5246
  function shouldFail(result, failOn) {
@@ -4659,7 +5255,7 @@ function shouldFail(result, failOn) {
4659
5255
 
4660
5256
  // src/cli/commands/validate.ts
4661
5257
  async function runValidate(options) {
4662
- const root = path22.resolve(options.root);
5258
+ const root = path24.resolve(options.root);
4663
5259
  const configResult = await loadConfig(root);
4664
5260
  const result = await validateProject(root, configResult);
4665
5261
  const normalized = normalizeValidationResult(root, result);
@@ -4784,12 +5380,12 @@ function issueKey(issue7) {
4784
5380
  }
4785
5381
  async function emitJson(result, root, jsonPath) {
4786
5382
  const abs = resolveJsonPath(root, jsonPath);
4787
- await mkdir4(path22.dirname(abs), { recursive: true });
5383
+ await mkdir4(path24.dirname(abs), { recursive: true });
4788
5384
  await writeFile3(abs, `${JSON.stringify(result, null, 2)}
4789
5385
  `, "utf-8");
4790
5386
  }
4791
5387
  function resolveJsonPath(root, jsonPath) {
4792
- return path22.isAbsolute(jsonPath) ? jsonPath : path22.resolve(root, jsonPath);
5388
+ return path24.isAbsolute(jsonPath) ? jsonPath : path24.resolve(root, jsonPath);
4793
5389
  }
4794
5390
  var GITHUB_ANNOTATION_LIMIT = 100;
4795
5391
 
@@ -4807,7 +5403,9 @@ function parseArgs(argv, cwd) {
4807
5403
  doctorFormat: "text",
4808
5404
  validateFormat: "text",
4809
5405
  strict: false,
4810
- help: false
5406
+ guardrailsPaths: [],
5407
+ help: false,
5408
+ invalidExitCode: 1
4811
5409
  };
4812
5410
  const args = [...argv];
4813
5411
  let command = args.shift() ?? null;
@@ -4816,6 +5414,25 @@ function parseArgs(argv, cwd) {
4816
5414
  options.help = true;
4817
5415
  command = null;
4818
5416
  }
5417
+ const markInvalid = () => {
5418
+ invalid = true;
5419
+ options.help = true;
5420
+ if (command === "guardrails") {
5421
+ options.invalidExitCode = 2;
5422
+ }
5423
+ };
5424
+ if (command === "guardrails") {
5425
+ const candidate = args[0];
5426
+ if (candidate && !candidate.startsWith("--")) {
5427
+ const action = normalizeGuardrailsAction(candidate);
5428
+ if (action) {
5429
+ options.guardrailsAction = action;
5430
+ } else {
5431
+ markInvalid();
5432
+ }
5433
+ args.shift();
5434
+ }
5435
+ }
4819
5436
  for (let i = 0; i < args.length; i += 1) {
4820
5437
  const arg = args[i];
4821
5438
  switch (arg) {
@@ -4823,8 +5440,7 @@ function parseArgs(argv, cwd) {
4823
5440
  {
4824
5441
  const next = readOptionValue(args, i);
4825
5442
  if (next === null) {
4826
- invalid = true;
4827
- options.help = true;
5443
+ markInvalid();
4828
5444
  break;
4829
5445
  }
4830
5446
  options.root = next;
@@ -4836,8 +5452,7 @@ function parseArgs(argv, cwd) {
4836
5452
  {
4837
5453
  const next = readOptionValue(args, i);
4838
5454
  if (next === null) {
4839
- invalid = true;
4840
- options.help = true;
5455
+ markInvalid();
4841
5456
  break;
4842
5457
  }
4843
5458
  options.dir = next;
@@ -4856,8 +5471,7 @@ function parseArgs(argv, cwd) {
4856
5471
  case "--format": {
4857
5472
  const next = readOptionValue(args, i);
4858
5473
  if (next === null) {
4859
- invalid = true;
4860
- options.help = true;
5474
+ markInvalid();
4861
5475
  break;
4862
5476
  }
4863
5477
  applyFormatOption(command, next, options);
@@ -4870,8 +5484,7 @@ function parseArgs(argv, cwd) {
4870
5484
  case "--fail-on": {
4871
5485
  const next = readOptionValue(args, i);
4872
5486
  if (next === null) {
4873
- invalid = true;
4874
- options.help = true;
5487
+ markInvalid();
4875
5488
  break;
4876
5489
  }
4877
5490
  if (next === "never" || next === "warning" || next === "error") {
@@ -4883,8 +5496,7 @@ function parseArgs(argv, cwd) {
4883
5496
  case "--out": {
4884
5497
  const next = readOptionValue(args, i);
4885
5498
  if (next === null) {
4886
- invalid = true;
4887
- options.help = true;
5499
+ markInvalid();
4888
5500
  break;
4889
5501
  }
4890
5502
  if (command === "doctor") {
@@ -4898,8 +5510,7 @@ function parseArgs(argv, cwd) {
4898
5510
  case "--in": {
4899
5511
  const next = readOptionValue(args, i);
4900
5512
  if (next === null) {
4901
- invalid = true;
4902
- options.help = true;
5513
+ markInvalid();
4903
5514
  break;
4904
5515
  }
4905
5516
  options.reportIn = next;
@@ -4912,14 +5523,57 @@ function parseArgs(argv, cwd) {
4912
5523
  case "--base-url": {
4913
5524
  const next = readOptionValue(args, i);
4914
5525
  if (next === null) {
4915
- invalid = true;
4916
- options.help = true;
5526
+ markInvalid();
4917
5527
  break;
4918
5528
  }
4919
5529
  options.reportBaseUrl = next;
4920
5530
  i += 1;
4921
5531
  break;
4922
5532
  }
5533
+ case "--path": {
5534
+ if (command !== "guardrails") {
5535
+ break;
5536
+ }
5537
+ const next = readOptionValue(args, i);
5538
+ if (next === null) {
5539
+ markInvalid();
5540
+ break;
5541
+ }
5542
+ options.guardrailsPaths.push(next);
5543
+ i += 1;
5544
+ break;
5545
+ }
5546
+ case "--max": {
5547
+ if (command !== "guardrails") {
5548
+ break;
5549
+ }
5550
+ const next = readOptionValue(args, i);
5551
+ if (next === null) {
5552
+ markInvalid();
5553
+ break;
5554
+ }
5555
+ const parsed = Number.parseInt(next, 10);
5556
+ if (Number.isNaN(parsed)) {
5557
+ markInvalid();
5558
+ break;
5559
+ }
5560
+ options.guardrailsMax = parsed;
5561
+ i += 1;
5562
+ break;
5563
+ }
5564
+ case "--keyword": {
5565
+ if (command !== "guardrails") {
5566
+ break;
5567
+ }
5568
+ const next = readOptionValue(args, i);
5569
+ if (next === null) {
5570
+ markInvalid();
5571
+ break;
5572
+ }
5573
+ options.guardrailsKeyword = next;
5574
+ i += 1;
5575
+ break;
5576
+ }
4923
5577
  case "--help":
4924
5578
  case "-h":
4925
5579
  options.help = true;
@@ -4928,6 +5582,9 @@ function parseArgs(argv, cwd) {
4928
5582
  break;
4929
5583
  }
4930
5584
  }
5585
+ if (command === "guardrails" && !options.help && !options.guardrailsAction) {
5586
+ markInvalid();
5587
+ }
4931
5588
  return { command, invalid, options };
4932
5589
  }
4933
5590
  function readOptionValue(args, index) {
@@ -4966,6 +5623,16 @@ function applyFormatOption(command, value, options) {
4966
5623
  options.validateFormat = value;
4967
5624
  }
4968
5625
  }
5626
+ function normalizeGuardrailsAction(value) {
5627
+ switch (value) {
5628
+ case "list":
5629
+ case "extract":
5630
+ case "check":
5631
+ return value;
5632
+ default:
5633
+ return null;
5634
+ }
5635
+ }
4969
5636
 
4970
5637
  // src/cli/main.ts
4971
5638
  async function run(argv, cwd) {
@@ -4973,7 +5640,7 @@ async function run(argv, cwd) {
4973
5640
  if (!command || options.help) {
4974
5641
  info(usage());
4975
5642
  if (invalid) {
4976
- process.exitCode = 1;
5643
+ process.exitCode = options.invalidExitCode;
4977
5644
  }
4978
5645
  return;
4979
5646
  }
@@ -5022,6 +5689,19 @@ async function run(argv, cwd) {
5022
5689
  process.exitCode = exitCode;
5023
5690
  }
5024
5691
  return;
5692
+ case "guardrails":
5693
+ {
5694
+ const resolvedRoot = await resolveRoot(options);
5695
+ const exitCode = await runGuardrails({
5696
+ root: resolvedRoot,
5697
+ ...options.guardrailsAction ? { action: options.guardrailsAction } : {},
5698
+ paths: options.guardrailsPaths,
5699
+ ...options.guardrailsMax !== void 0 ? { max: options.guardrailsMax } : {},
5700
+ ...options.guardrailsKeyword !== void 0 ? { keyword: options.guardrailsKeyword } : {}
5701
+ });
5702
+ process.exitCode = exitCode;
5703
+ }
5704
+ return;
5025
5705
  default:
5026
5706
  error(`Unknown command: ${command}`);
5027
5707
  info(usage());
@@ -5036,6 +5716,7 @@ Commands:
5036
5716
  validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
5037
5717
  report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
5038
5718
  doctor \u8A2D\u5B9A/\u30D1\u30B9/\u51FA\u529B\u524D\u63D0\u306E\u8A3A\u65AD
5719
+ guardrails Decision Guardrails \u306E\u62BD\u51FA/\u691C\u67FB\uFF08list|extract|check\uFF09
5039
5720
 
5040
5721
  Options:
5041
5722
  --root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
@@ -5053,6 +5734,9 @@ Options:
5053
5734
  --in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
5054
5735
  --run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
5055
5736
  --base-url <url> report: \u30D1\u30B9\u3092\u30EA\u30F3\u30AF\u5316\u3059\u308B\u57FA\u6E96URL
5737
+ --path <path> guardrails: \u5BFE\u8C61\u30D5\u30A1\u30A4\u30EB/\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\uFF08\u8907\u6570\u6307\u5B9A\u53EF\uFF09
5738
+ --max <number> guardrails extract: \u6700\u5927\u4EF6\u6570
5739
+ --keyword <text> guardrails list/extract: \u30AD\u30FC\u30EF\u30FC\u30C9\u30D5\u30A3\u30EB\u30BF
5056
5740
  -h, --help \u30D8\u30EB\u30D7\u8868\u793A
5057
5741
  `;
5058
5742
  }