qfai 1.0.7 → 1.1.1
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 +71 -10
- package/assets/init/.qfai/README.md +1 -1
- package/assets/init/.qfai/assistant/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/README.md +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-configure.md +33 -8
- package/assets/init/.qfai/assistant/prompts/qfai-verify.md +1 -1
- package/assets/init/.qfai/assistant/steering/README.md +6 -0
- package/assets/init/.qfai/assistant/steering/manifest.md +45 -0
- package/assets/init/.qfai/contracts/db/README.md +10 -3
- package/assets/init/.qfai/samples/guardrails/delta_with_guardrails.md +19 -0
- package/assets/init/.qfai/specs/README.md +6 -0
- package/assets/init/root/.github/workflows/qfai.yml +0 -2
- package/assets/init/root/qfai.config.yaml +1 -8
- package/dist/cli/index.cjs +873 -195
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +859 -181
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.cjs +731 -221
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.mjs +719 -216
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/assets/init/.qfai/assistant/prompts/qfai-pr.md +0 -209
package/dist/cli/index.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/commands/doctor.ts
|
|
4
4
|
import { mkdir, writeFile } from "fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path12 from "path";
|
|
6
6
|
|
|
7
7
|
// src/core/doctor.ts
|
|
8
8
|
import { access as access4 } from "fs/promises";
|
|
9
|
-
import
|
|
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.
|
|
1060
|
-
return "1.
|
|
1051
|
+
if ("1.1.1".length > 0) {
|
|
1052
|
+
return "1.1.1";
|
|
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 =
|
|
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 =
|
|
1164
|
-
|
|
1165
|
-
`${
|
|
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
|
|
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 =
|
|
1249
|
-
const inside = rel !== "" && !rel.startsWith("..") && !
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
1878
|
+
let current = path11.resolve(startDir);
|
|
1404
1879
|
while (true) {
|
|
1405
|
-
const gitPath =
|
|
1406
|
-
const workspacePath =
|
|
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 =
|
|
1885
|
+
const parent = path11.dirname(current);
|
|
1411
1886
|
if (parent === current) {
|
|
1412
1887
|
break;
|
|
1413
1888
|
}
|
|
1414
1889
|
current = parent;
|
|
1415
1890
|
}
|
|
1416
|
-
return
|
|
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 =
|
|
1459
|
-
await mkdir(
|
|
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/
|
|
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
|
|
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 =
|
|
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 +
|
|
1502
|
-
const excludePrefixes = (options.exclude ?? []).map((p) => p.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "")).filter((p) => p.length > 0).map((p) => p +
|
|
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,
|
|
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,
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2081
|
+
const relative = path14.relative(sourceRoot, file);
|
|
1542
2082
|
if (isExcludedRelative(relative)) {
|
|
1543
2083
|
continue;
|
|
1544
2084
|
}
|
|
1545
|
-
const dest =
|
|
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(
|
|
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 =
|
|
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 =
|
|
1607
|
-
const qfaiAssets =
|
|
1608
|
-
const destRoot =
|
|
1609
|
-
const destQfai =
|
|
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"
|
|
@@ -1652,15 +2192,15 @@ function report(copied, skipped, dryRun, label, baseDir) {
|
|
|
1652
2192
|
info(` skipped: ${skipped.length}`);
|
|
1653
2193
|
info(" skipped paths:");
|
|
1654
2194
|
for (const skippedPath of skipped) {
|
|
1655
|
-
const relative =
|
|
2195
|
+
const relative = path15.relative(baseDir, skippedPath);
|
|
1656
2196
|
info(` - ${relative}`);
|
|
1657
2197
|
}
|
|
1658
2198
|
}
|
|
1659
2199
|
}
|
|
1660
2200
|
|
|
1661
2201
|
// src/cli/commands/report.ts
|
|
1662
|
-
import { mkdir as mkdir3, readFile as
|
|
1663
|
-
import
|
|
2202
|
+
import { mkdir as mkdir3, readFile as readFile14, writeFile as writeFile2 } from "fs/promises";
|
|
2203
|
+
import path23 from "path";
|
|
1664
2204
|
|
|
1665
2205
|
// src/core/normalize.ts
|
|
1666
2206
|
function normalizeIssuePaths(root, issues) {
|
|
@@ -1700,12 +2240,12 @@ function normalizeValidationResult(root, result) {
|
|
|
1700
2240
|
}
|
|
1701
2241
|
|
|
1702
2242
|
// src/core/report.ts
|
|
1703
|
-
import { readFile as
|
|
1704
|
-
import
|
|
2243
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2244
|
+
import path22 from "path";
|
|
1705
2245
|
|
|
1706
2246
|
// src/core/contractIndex.ts
|
|
1707
|
-
import { readFile as
|
|
1708
|
-
import
|
|
2247
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
2248
|
+
import path16 from "path";
|
|
1709
2249
|
|
|
1710
2250
|
// src/core/contractsDecl.ts
|
|
1711
2251
|
var CONTRACT_DECLARATION_RE = /^\s*(?:#|\/\/|--|\/\*+|\*+)?\s*QFAI-CONTRACT-ID:\s*((?:API|UI|DB)-\d{4}|THEMA-\d{3})\s*(?:\*\/)?\s*$/gm;
|
|
@@ -1727,9 +2267,9 @@ function stripContractDeclarationLines(text) {
|
|
|
1727
2267
|
// src/core/contractIndex.ts
|
|
1728
2268
|
async function buildContractIndex(root, config) {
|
|
1729
2269
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
1730
|
-
const uiRoot =
|
|
1731
|
-
const apiRoot =
|
|
1732
|
-
const dbRoot =
|
|
2270
|
+
const uiRoot = path16.join(contractsRoot, "ui");
|
|
2271
|
+
const apiRoot = path16.join(contractsRoot, "api");
|
|
2272
|
+
const dbRoot = path16.join(contractsRoot, "db");
|
|
1733
2273
|
const [uiFiles, themaFiles, apiFiles, dbFiles] = await Promise.all([
|
|
1734
2274
|
collectUiContractFiles(uiRoot),
|
|
1735
2275
|
collectThemaContractFiles(uiRoot),
|
|
@@ -1749,7 +2289,7 @@ async function buildContractIndex(root, config) {
|
|
|
1749
2289
|
}
|
|
1750
2290
|
async function indexContractFiles(files, index) {
|
|
1751
2291
|
for (const file of files) {
|
|
1752
|
-
const text = await
|
|
2292
|
+
const text = await readFile6(file, "utf-8");
|
|
1753
2293
|
extractDeclaredContractIds(text).forEach((id) => record(index, id, file));
|
|
1754
2294
|
}
|
|
1755
2295
|
}
|
|
@@ -1874,53 +2414,11 @@ function unique3(values) {
|
|
|
1874
2414
|
return Array.from(new Set(values));
|
|
1875
2415
|
}
|
|
1876
2416
|
|
|
1877
|
-
// src/core/parse/markdown.ts
|
|
1878
|
-
var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
|
|
1879
|
-
function parseHeadings(md) {
|
|
1880
|
-
const lines = md.split(/\r?\n/);
|
|
1881
|
-
const headings = [];
|
|
1882
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1883
|
-
const line = lines[i] ?? "";
|
|
1884
|
-
const match = line.match(HEADING_RE);
|
|
1885
|
-
if (!match) continue;
|
|
1886
|
-
const levelToken = match[1];
|
|
1887
|
-
const title = match[2];
|
|
1888
|
-
if (!levelToken || !title) continue;
|
|
1889
|
-
headings.push({
|
|
1890
|
-
level: levelToken.length,
|
|
1891
|
-
title: title.trim(),
|
|
1892
|
-
line: i + 1
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
return headings;
|
|
1896
|
-
}
|
|
1897
|
-
function extractH2Sections(md) {
|
|
1898
|
-
const lines = md.split(/\r?\n/);
|
|
1899
|
-
const headings = parseHeadings(md).filter((heading) => heading.level === 2);
|
|
1900
|
-
const sections = /* @__PURE__ */ new Map();
|
|
1901
|
-
for (let i = 0; i < headings.length; i++) {
|
|
1902
|
-
const current = headings[i];
|
|
1903
|
-
if (!current) continue;
|
|
1904
|
-
const next = headings[i + 1];
|
|
1905
|
-
const startLine = current.line + 1;
|
|
1906
|
-
const endLine = (next?.line ?? lines.length + 1) - 1;
|
|
1907
|
-
const body = startLine <= endLine ? lines.slice(startLine - 1, endLine).join("\n") : "";
|
|
1908
|
-
sections.set(current.title.trim(), {
|
|
1909
|
-
title: current.title.trim(),
|
|
1910
|
-
startLine,
|
|
1911
|
-
endLine,
|
|
1912
|
-
body
|
|
1913
|
-
});
|
|
1914
|
-
}
|
|
1915
|
-
return sections;
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
2417
|
// src/core/parse/spec.ts
|
|
1919
2418
|
var SPEC_ID_RE = /\bSPEC-\d{4}\b/;
|
|
1920
2419
|
var BR_LINE_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[0-3])\]\s*(.+)$/;
|
|
1921
2420
|
var BR_LINE_ANY_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\]\[(P[^\]]+)\]\s*(.+)$/;
|
|
1922
2421
|
var BR_LINE_NO_PRIORITY_RE = /^\s*(?:[-*]\s*)?\[(BR-\d{4})\](?!\s*\[P)\s*(.*\S.*)$/;
|
|
1923
|
-
var BR_SECTION_TITLE = "\u696D\u52D9\u30EB\u30FC\u30EB";
|
|
1924
2422
|
var VALID_PRIORITIES = /* @__PURE__ */ new Set(["P0", "P1", "P2", "P3"]);
|
|
1925
2423
|
function parseSpec(md, file) {
|
|
1926
2424
|
const headings = parseHeadings(md);
|
|
@@ -1928,15 +2426,13 @@ function parseSpec(md, file) {
|
|
|
1928
2426
|
const specId = h1?.title.match(SPEC_ID_RE)?.[0];
|
|
1929
2427
|
const sections = extractH2Sections(md);
|
|
1930
2428
|
const sectionNames = new Set(Array.from(sections.keys()));
|
|
1931
|
-
const
|
|
1932
|
-
const brLines = brSection ? brSection.body.split(/\r?\n/) : [];
|
|
1933
|
-
const startLine = brSection?.startLine ?? 1;
|
|
2429
|
+
const lines = md.split(/\r?\n/);
|
|
1934
2430
|
const brs = [];
|
|
1935
2431
|
const brsWithoutPriority = [];
|
|
1936
2432
|
const brsWithInvalidPriority = [];
|
|
1937
|
-
for (let i = 0; i <
|
|
1938
|
-
const lineText =
|
|
1939
|
-
const lineNumber =
|
|
2433
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2434
|
+
const lineText = lines[i] ?? "";
|
|
2435
|
+
const lineNumber = i + 1;
|
|
1940
2436
|
const validMatch = lineText.match(BR_LINE_RE);
|
|
1941
2437
|
if (validMatch) {
|
|
1942
2438
|
const id = validMatch[1];
|
|
@@ -1994,14 +2490,14 @@ function parseSpec(md, file) {
|
|
|
1994
2490
|
}
|
|
1995
2491
|
|
|
1996
2492
|
// src/core/validators/contracts.ts
|
|
1997
|
-
import { access as access6, readFile as
|
|
1998
|
-
import
|
|
2493
|
+
import { access as access6, readFile as readFile7 } from "fs/promises";
|
|
2494
|
+
import path18 from "path";
|
|
1999
2495
|
|
|
2000
2496
|
// src/core/contracts.ts
|
|
2001
|
-
import
|
|
2497
|
+
import path17 from "path";
|
|
2002
2498
|
import { parse as parseYaml2 } from "yaml";
|
|
2003
2499
|
function parseStructuredContract(file, text) {
|
|
2004
|
-
const ext =
|
|
2500
|
+
const ext = path17.extname(file).toLowerCase();
|
|
2005
2501
|
if (ext === ".json") {
|
|
2006
2502
|
return JSON.parse(text);
|
|
2007
2503
|
}
|
|
@@ -2023,14 +2519,14 @@ async function validateContracts(root, config) {
|
|
|
2023
2519
|
const issues = [];
|
|
2024
2520
|
const contractIndex = await buildContractIndex(root, config);
|
|
2025
2521
|
const contractsRoot = resolvePath(root, config, "contractsDir");
|
|
2026
|
-
const uiRoot =
|
|
2522
|
+
const uiRoot = path18.join(contractsRoot, "ui");
|
|
2027
2523
|
const themaIds = new Set(
|
|
2028
2524
|
Array.from(contractIndex.ids).filter((id) => id.startsWith("THEMA-"))
|
|
2029
2525
|
);
|
|
2030
2526
|
issues.push(...await validateUiContracts(uiRoot, themaIds));
|
|
2031
2527
|
issues.push(...await validateThemaContracts(uiRoot));
|
|
2032
|
-
issues.push(...await validateApiContracts(
|
|
2033
|
-
issues.push(...await validateDbContracts(
|
|
2528
|
+
issues.push(...await validateApiContracts(path18.join(contractsRoot, "api")));
|
|
2529
|
+
issues.push(...await validateDbContracts(path18.join(contractsRoot, "db")));
|
|
2034
2530
|
issues.push(...validateDuplicateContractIds(contractIndex));
|
|
2035
2531
|
return issues;
|
|
2036
2532
|
}
|
|
@@ -2049,7 +2545,7 @@ async function validateUiContracts(uiRoot, themaIds) {
|
|
|
2049
2545
|
}
|
|
2050
2546
|
const issues = [];
|
|
2051
2547
|
for (const file of files) {
|
|
2052
|
-
const text = await
|
|
2548
|
+
const text = await readFile7(file, "utf-8");
|
|
2053
2549
|
const declaredIds = extractDeclaredContractIds(text);
|
|
2054
2550
|
issues.push(...validateDeclaredContractIds(declaredIds, file, "UI"));
|
|
2055
2551
|
let doc = null;
|
|
@@ -2103,7 +2599,7 @@ async function validateThemaContracts(uiRoot) {
|
|
|
2103
2599
|
}
|
|
2104
2600
|
const issues = [];
|
|
2105
2601
|
for (const file of files) {
|
|
2106
|
-
const text = await
|
|
2602
|
+
const text = await readFile7(file, "utf-8");
|
|
2107
2603
|
const invalidIds = extractInvalidIds(text, [
|
|
2108
2604
|
"SPEC",
|
|
2109
2605
|
"BR",
|
|
@@ -2237,7 +2733,7 @@ async function validateApiContracts(apiRoot) {
|
|
|
2237
2733
|
}
|
|
2238
2734
|
const issues = [];
|
|
2239
2735
|
for (const file of files) {
|
|
2240
|
-
const text = await
|
|
2736
|
+
const text = await readFile7(file, "utf-8");
|
|
2241
2737
|
const invalidIds = extractInvalidIds(text, [
|
|
2242
2738
|
"SPEC",
|
|
2243
2739
|
"BR",
|
|
@@ -2306,7 +2802,7 @@ async function validateDbContracts(dbRoot) {
|
|
|
2306
2802
|
}
|
|
2307
2803
|
const issues = [];
|
|
2308
2804
|
for (const file of files) {
|
|
2309
|
-
const text = await
|
|
2805
|
+
const text = await readFile7(file, "utf-8");
|
|
2310
2806
|
const invalidIds = extractInvalidIds(text, [
|
|
2311
2807
|
"SPEC",
|
|
2312
2808
|
"BR",
|
|
@@ -2503,9 +2999,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2503
2999
|
);
|
|
2504
3000
|
return issues;
|
|
2505
3001
|
}
|
|
2506
|
-
const packDir =
|
|
2507
|
-
const packRelative =
|
|
2508
|
-
if (packRelative.startsWith("..") ||
|
|
3002
|
+
const packDir = path18.resolve(uiRoot, packValue);
|
|
3003
|
+
const packRelative = path18.relative(uiRoot, packDir);
|
|
3004
|
+
if (packRelative.startsWith("..") || path18.isAbsolute(packRelative)) {
|
|
2509
3005
|
issues.push(
|
|
2510
3006
|
issue(
|
|
2511
3007
|
"QFAI-ASSET-001",
|
|
@@ -2531,7 +3027,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2531
3027
|
);
|
|
2532
3028
|
return issues;
|
|
2533
3029
|
}
|
|
2534
|
-
const assetsYamlPath =
|
|
3030
|
+
const assetsYamlPath = path18.join(packDir, "assets.yaml");
|
|
2535
3031
|
if (!await exists6(assetsYamlPath)) {
|
|
2536
3032
|
issues.push(
|
|
2537
3033
|
issue(
|
|
@@ -2546,7 +3042,7 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2546
3042
|
}
|
|
2547
3043
|
let manifest;
|
|
2548
3044
|
try {
|
|
2549
|
-
const manifestText = await
|
|
3045
|
+
const manifestText = await readFile7(assetsYamlPath, "utf-8");
|
|
2550
3046
|
manifest = parseStructuredContract(assetsYamlPath, manifestText);
|
|
2551
3047
|
} catch (error2) {
|
|
2552
3048
|
issues.push(
|
|
@@ -2619,9 +3115,9 @@ async function validateUiAssets(assets, file, uiRoot) {
|
|
|
2619
3115
|
);
|
|
2620
3116
|
continue;
|
|
2621
3117
|
}
|
|
2622
|
-
const assetPath =
|
|
2623
|
-
const assetRelative =
|
|
2624
|
-
if (assetRelative.startsWith("..") ||
|
|
3118
|
+
const assetPath = path18.resolve(packDir, entry.path);
|
|
3119
|
+
const assetRelative = path18.relative(packDir, assetPath);
|
|
3120
|
+
if (assetRelative.startsWith("..") || path18.isAbsolute(assetRelative)) {
|
|
2625
3121
|
issues.push(
|
|
2626
3122
|
issue(
|
|
2627
3123
|
"QFAI-ASSET-004",
|
|
@@ -2662,7 +3158,7 @@ function shouldIgnoreInvalidId(value, doc) {
|
|
|
2662
3158
|
return false;
|
|
2663
3159
|
}
|
|
2664
3160
|
const normalized = packValue.replace(/\\/g, "/");
|
|
2665
|
-
const basename =
|
|
3161
|
+
const basename = path18.posix.basename(normalized);
|
|
2666
3162
|
if (!basename) {
|
|
2667
3163
|
return false;
|
|
2668
3164
|
}
|
|
@@ -2672,7 +3168,7 @@ function isSafeRelativePath(value) {
|
|
|
2672
3168
|
if (!value) {
|
|
2673
3169
|
return false;
|
|
2674
3170
|
}
|
|
2675
|
-
if (
|
|
3171
|
+
if (path18.isAbsolute(value)) {
|
|
2676
3172
|
return false;
|
|
2677
3173
|
}
|
|
2678
3174
|
const normalized = value.replace(/\\/g, "/");
|
|
@@ -2722,8 +3218,8 @@ function issue(code, message, severity, file, rule, refs, category = "compatibil
|
|
|
2722
3218
|
}
|
|
2723
3219
|
|
|
2724
3220
|
// src/core/validators/delta.ts
|
|
2725
|
-
import { readFile as
|
|
2726
|
-
import
|
|
3221
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3222
|
+
import path19 from "path";
|
|
2727
3223
|
async function validateDeltas(root, config) {
|
|
2728
3224
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
2729
3225
|
const packs = await collectSpecPackDirs(specsRoot);
|
|
@@ -2732,9 +3228,9 @@ async function validateDeltas(root, config) {
|
|
|
2732
3228
|
}
|
|
2733
3229
|
const issues = [];
|
|
2734
3230
|
for (const pack of packs) {
|
|
2735
|
-
const deltaPath =
|
|
3231
|
+
const deltaPath = path19.join(pack, "delta.md");
|
|
2736
3232
|
try {
|
|
2737
|
-
await
|
|
3233
|
+
await readFile8(deltaPath, "utf-8");
|
|
2738
3234
|
} catch (error2) {
|
|
2739
3235
|
if (isMissingFileError2(error2)) {
|
|
2740
3236
|
issues.push(
|
|
@@ -2785,8 +3281,8 @@ function issue2(code, message, severity, file, rule, refs, category = "change",
|
|
|
2785
3281
|
}
|
|
2786
3282
|
|
|
2787
3283
|
// src/core/validators/ids.ts
|
|
2788
|
-
import { readFile as
|
|
2789
|
-
import
|
|
3284
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3285
|
+
import path20 from "path";
|
|
2790
3286
|
var SC_TAG_RE3 = /^SC-\d{4}$/;
|
|
2791
3287
|
async function validateDefinedIds(root, config) {
|
|
2792
3288
|
const issues = [];
|
|
@@ -2821,7 +3317,7 @@ async function validateDefinedIds(root, config) {
|
|
|
2821
3317
|
}
|
|
2822
3318
|
async function collectSpecDefinitionIds(files, out) {
|
|
2823
3319
|
for (const file of files) {
|
|
2824
|
-
const text = await
|
|
3320
|
+
const text = await readFile9(file, "utf-8");
|
|
2825
3321
|
const parsed = parseSpec(text, file);
|
|
2826
3322
|
if (parsed.specId) {
|
|
2827
3323
|
recordId(out, parsed.specId, file);
|
|
@@ -2831,7 +3327,7 @@ async function collectSpecDefinitionIds(files, out) {
|
|
|
2831
3327
|
}
|
|
2832
3328
|
async function collectScenarioDefinitionIds(files, out) {
|
|
2833
3329
|
for (const file of files) {
|
|
2834
|
-
const text = await
|
|
3330
|
+
const text = await readFile9(file, "utf-8");
|
|
2835
3331
|
const { document, errors } = parseScenarioDocument(text, file);
|
|
2836
3332
|
if (!document || errors.length > 0) {
|
|
2837
3333
|
continue;
|
|
@@ -2852,7 +3348,7 @@ function recordId(out, id, file) {
|
|
|
2852
3348
|
}
|
|
2853
3349
|
function formatFileList(files, root) {
|
|
2854
3350
|
return files.map((file) => {
|
|
2855
|
-
const relative =
|
|
3351
|
+
const relative = path20.relative(root, file);
|
|
2856
3352
|
return relative.length > 0 ? relative : file;
|
|
2857
3353
|
}).join(", ");
|
|
2858
3354
|
}
|
|
@@ -2910,8 +3406,8 @@ async function validatePromptsIntegrity(root, config) {
|
|
|
2910
3406
|
}
|
|
2911
3407
|
|
|
2912
3408
|
// src/core/validators/scenario.ts
|
|
2913
|
-
import { access as access7, readFile as
|
|
2914
|
-
import
|
|
3409
|
+
import { access as access7, readFile as readFile10 } from "fs/promises";
|
|
3410
|
+
import path21 from "path";
|
|
2915
3411
|
var GIVEN_PATTERN = /\bGiven\b/;
|
|
2916
3412
|
var WHEN_PATTERN = /\bWhen\b/;
|
|
2917
3413
|
var THEN_PATTERN = /\bThen\b/;
|
|
@@ -2934,7 +3430,7 @@ async function validateScenarios(root, config) {
|
|
|
2934
3430
|
}
|
|
2935
3431
|
const issues = [];
|
|
2936
3432
|
for (const entry of entries) {
|
|
2937
|
-
const legacyScenarioPath =
|
|
3433
|
+
const legacyScenarioPath = path21.join(entry.dir, "scenario.md");
|
|
2938
3434
|
if (await fileExists(legacyScenarioPath)) {
|
|
2939
3435
|
issues.push(
|
|
2940
3436
|
issue4(
|
|
@@ -2948,7 +3444,7 @@ async function validateScenarios(root, config) {
|
|
|
2948
3444
|
}
|
|
2949
3445
|
let text;
|
|
2950
3446
|
try {
|
|
2951
|
-
text = await
|
|
3447
|
+
text = await readFile10(entry.scenarioPath, "utf-8");
|
|
2952
3448
|
} catch (error2) {
|
|
2953
3449
|
if (isMissingFileError3(error2)) {
|
|
2954
3450
|
issues.push(
|
|
@@ -3131,7 +3627,7 @@ async function fileExists(target) {
|
|
|
3131
3627
|
}
|
|
3132
3628
|
|
|
3133
3629
|
// src/core/validators/spec.ts
|
|
3134
|
-
import { readFile as
|
|
3630
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3135
3631
|
async function validateSpecs(root, config) {
|
|
3136
3632
|
const specsRoot = resolvePath(root, config, "specsDir");
|
|
3137
3633
|
const entries = await collectSpecEntries(specsRoot);
|
|
@@ -3152,7 +3648,7 @@ async function validateSpecs(root, config) {
|
|
|
3152
3648
|
for (const entry of entries) {
|
|
3153
3649
|
let text;
|
|
3154
3650
|
try {
|
|
3155
|
-
text = await
|
|
3651
|
+
text = await readFile11(entry.specPath, "utf-8");
|
|
3156
3652
|
} catch (error2) {
|
|
3157
3653
|
if (isMissingFileError4(error2)) {
|
|
3158
3654
|
issues.push(
|
|
@@ -3306,7 +3802,7 @@ function isMissingFileError4(error2) {
|
|
|
3306
3802
|
}
|
|
3307
3803
|
|
|
3308
3804
|
// src/core/validators/traceability.ts
|
|
3309
|
-
import { readFile as
|
|
3805
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3310
3806
|
var SPEC_TAG_RE3 = /^SPEC-\d{4}$/;
|
|
3311
3807
|
var BR_TAG_RE2 = /^BR-\d{4}$/;
|
|
3312
3808
|
async function validateTraceability(root, config) {
|
|
@@ -3326,7 +3822,7 @@ async function validateTraceability(root, config) {
|
|
|
3326
3822
|
const contractIndex = await buildContractIndex(root, config);
|
|
3327
3823
|
const contractIds = contractIndex.ids;
|
|
3328
3824
|
for (const file of specFiles) {
|
|
3329
|
-
const text = await
|
|
3825
|
+
const text = await readFile12(file, "utf-8");
|
|
3330
3826
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3331
3827
|
const parsed = parseSpec(text, file);
|
|
3332
3828
|
if (parsed.specId) {
|
|
@@ -3399,7 +3895,7 @@ async function validateTraceability(root, config) {
|
|
|
3399
3895
|
}
|
|
3400
3896
|
}
|
|
3401
3897
|
for (const file of scenarioFiles) {
|
|
3402
|
-
const text = await
|
|
3898
|
+
const text = await readFile12(file, "utf-8");
|
|
3403
3899
|
extractAllIds(text).forEach((id) => upstreamIds.add(id));
|
|
3404
3900
|
const scenarioContractRefs = parseContractRefs(text, {
|
|
3405
3901
|
allowCommentPrefix: true
|
|
@@ -3721,7 +4217,7 @@ async function validateCodeReferences(upstreamIds, srcRoot, testsRoot) {
|
|
|
3721
4217
|
const pattern = buildIdPattern(Array.from(upstreamIds));
|
|
3722
4218
|
let found = false;
|
|
3723
4219
|
for (const file of targetFiles) {
|
|
3724
|
-
const text = await
|
|
4220
|
+
const text = await readFile12(file, "utf-8");
|
|
3725
4221
|
if (pattern.test(text)) {
|
|
3726
4222
|
found = true;
|
|
3727
4223
|
break;
|
|
@@ -3820,16 +4316,17 @@ var ID_PREFIXES2 = [
|
|
|
3820
4316
|
"DB",
|
|
3821
4317
|
"THEMA"
|
|
3822
4318
|
];
|
|
4319
|
+
var REPORT_GUARDRAILS_MAX = 20;
|
|
3823
4320
|
async function createReportData(root, validation, configResult) {
|
|
3824
|
-
const resolvedRoot =
|
|
4321
|
+
const resolvedRoot = path22.resolve(root);
|
|
3825
4322
|
const resolved = configResult ?? await loadConfig(resolvedRoot);
|
|
3826
4323
|
const config = resolved.config;
|
|
3827
4324
|
const configPath = resolved.configPath;
|
|
3828
4325
|
const specsRoot = resolvePath(resolvedRoot, config, "specsDir");
|
|
3829
4326
|
const contractsRoot = resolvePath(resolvedRoot, config, "contractsDir");
|
|
3830
|
-
const apiRoot =
|
|
3831
|
-
const uiRoot =
|
|
3832
|
-
const dbRoot =
|
|
4327
|
+
const apiRoot = path22.join(contractsRoot, "api");
|
|
4328
|
+
const uiRoot = path22.join(contractsRoot, "ui");
|
|
4329
|
+
const dbRoot = path22.join(contractsRoot, "db");
|
|
3833
4330
|
const srcRoot = resolvePath(resolvedRoot, config, "srcDir");
|
|
3834
4331
|
const testsRoot = resolvePath(resolvedRoot, config, "testsDir");
|
|
3835
4332
|
const specFiles = await collectSpecFiles(specsRoot);
|
|
@@ -3888,6 +4385,27 @@ async function createReportData(root, validation, configResult) {
|
|
|
3888
4385
|
const scSourceRecord = mapToSortedRecord(
|
|
3889
4386
|
normalizeScSources(resolvedRoot, scSources)
|
|
3890
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
|
+
}));
|
|
3891
4409
|
const version = await resolveToolVersion();
|
|
3892
4410
|
const displayRoot = toRelativePath(resolvedRoot, resolvedRoot);
|
|
3893
4411
|
const displayConfigPath = toRelativePath(resolvedRoot, configPath);
|
|
@@ -3935,6 +4453,34 @@ async function createReportData(root, validation, configResult) {
|
|
|
3935
4453
|
specToContracts: specToContractsRecord
|
|
3936
4454
|
}
|
|
3937
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
|
+
},
|
|
3938
4484
|
issues: normalizedValidation.issues
|
|
3939
4485
|
};
|
|
3940
4486
|
}
|
|
@@ -4030,6 +4576,7 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
4030
4576
|
lines.push("");
|
|
4031
4577
|
lines.push("- [Compatibility Issues](#compatibility-issues)");
|
|
4032
4578
|
lines.push("- [Change Issues](#change-issues)");
|
|
4579
|
+
lines.push("- [Decision Guardrails](#decision-guardrails)");
|
|
4033
4580
|
lines.push("- [IDs](#ids)");
|
|
4034
4581
|
lines.push("- [Traceability](#traceability)");
|
|
4035
4582
|
lines.push("");
|
|
@@ -4121,6 +4668,49 @@ function formatReportMarkdown(data, options = {}) {
|
|
|
4121
4668
|
lines.push("### Issues");
|
|
4122
4669
|
lines.push("");
|
|
4123
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("");
|
|
4124
4714
|
lines.push("## IDs");
|
|
4125
4715
|
lines.push("");
|
|
4126
4716
|
lines.push(formatIdLine("SPEC", data.ids.spec));
|
|
@@ -4311,7 +4901,7 @@ async function collectSpecContractRefs(specFiles, contractIdList) {
|
|
|
4311
4901
|
idToSpecs.set(contractId, /* @__PURE__ */ new Set());
|
|
4312
4902
|
}
|
|
4313
4903
|
for (const file of specFiles) {
|
|
4314
|
-
const text = await
|
|
4904
|
+
const text = await readFile13(file, "utf-8");
|
|
4315
4905
|
const parsed = parseSpec(text, file);
|
|
4316
4906
|
const specKey = parsed.specId;
|
|
4317
4907
|
if (!specKey) {
|
|
@@ -4353,7 +4943,7 @@ async function collectIds(files) {
|
|
|
4353
4943
|
THEMA: /* @__PURE__ */ new Set()
|
|
4354
4944
|
};
|
|
4355
4945
|
for (const file of files) {
|
|
4356
|
-
const text = await
|
|
4946
|
+
const text = await readFile13(file, "utf-8");
|
|
4357
4947
|
for (const prefix of ID_PREFIXES2) {
|
|
4358
4948
|
const ids = extractIds(text, prefix);
|
|
4359
4949
|
ids.forEach((id) => result[prefix].add(id));
|
|
@@ -4372,7 +4962,7 @@ async function collectIds(files) {
|
|
|
4372
4962
|
async function collectUpstreamIds(files) {
|
|
4373
4963
|
const ids = /* @__PURE__ */ new Set();
|
|
4374
4964
|
for (const file of files) {
|
|
4375
|
-
const text = await
|
|
4965
|
+
const text = await readFile13(file, "utf-8");
|
|
4376
4966
|
extractAllIds(text).forEach((id) => ids.add(id));
|
|
4377
4967
|
}
|
|
4378
4968
|
return ids;
|
|
@@ -4393,7 +4983,7 @@ async function evaluateTraceability(upstreamIds, srcRoot, testsRoot) {
|
|
|
4393
4983
|
}
|
|
4394
4984
|
const pattern = buildIdPattern2(Array.from(upstreamIds));
|
|
4395
4985
|
for (const file of targetFiles) {
|
|
4396
|
-
const text = await
|
|
4986
|
+
const text = await readFile13(file, "utf-8");
|
|
4397
4987
|
if (pattern.test(text)) {
|
|
4398
4988
|
return true;
|
|
4399
4989
|
}
|
|
@@ -4530,7 +5120,7 @@ function warnIfTruncated(scan, context) {
|
|
|
4530
5120
|
|
|
4531
5121
|
// src/cli/commands/report.ts
|
|
4532
5122
|
async function runReport(options) {
|
|
4533
|
-
const root =
|
|
5123
|
+
const root = path23.resolve(options.root);
|
|
4534
5124
|
const configResult = await loadConfig(root);
|
|
4535
5125
|
let validation;
|
|
4536
5126
|
if (options.runValidate) {
|
|
@@ -4547,7 +5137,7 @@ async function runReport(options) {
|
|
|
4547
5137
|
validation = normalized;
|
|
4548
5138
|
} else {
|
|
4549
5139
|
const input = options.inputPath ?? configResult.config.output.validateJsonPath;
|
|
4550
|
-
const inputPath =
|
|
5140
|
+
const inputPath = path23.isAbsolute(input) ? input : path23.resolve(root, input);
|
|
4551
5141
|
try {
|
|
4552
5142
|
validation = await readValidationResult(inputPath);
|
|
4553
5143
|
} catch (err) {
|
|
@@ -4574,10 +5164,10 @@ async function runReport(options) {
|
|
|
4574
5164
|
warnIfTruncated(data.traceability.testFiles, "report");
|
|
4575
5165
|
const output = options.format === "json" ? formatReportJson(data) : options.baseUrl ? formatReportMarkdown(data, { baseUrl: options.baseUrl }) : formatReportMarkdown(data);
|
|
4576
5166
|
const outRoot = resolvePath(root, configResult.config, "outDir");
|
|
4577
|
-
const defaultOut = options.format === "json" ?
|
|
5167
|
+
const defaultOut = options.format === "json" ? path23.join(outRoot, "report.json") : path23.join(outRoot, "report.md");
|
|
4578
5168
|
const out = options.outPath ?? defaultOut;
|
|
4579
|
-
const outPath =
|
|
4580
|
-
await mkdir3(
|
|
5169
|
+
const outPath = path23.isAbsolute(out) ? out : path23.resolve(root, out);
|
|
5170
|
+
await mkdir3(path23.dirname(outPath), { recursive: true });
|
|
4581
5171
|
await writeFile2(outPath, `${output}
|
|
4582
5172
|
`, "utf-8");
|
|
4583
5173
|
info(
|
|
@@ -4586,7 +5176,7 @@ async function runReport(options) {
|
|
|
4586
5176
|
info(`wrote report: ${outPath}`);
|
|
4587
5177
|
}
|
|
4588
5178
|
async function readValidationResult(inputPath) {
|
|
4589
|
-
const raw = await
|
|
5179
|
+
const raw = await readFile14(inputPath, "utf-8");
|
|
4590
5180
|
const parsed = JSON.parse(raw);
|
|
4591
5181
|
if (!isValidationResult(parsed)) {
|
|
4592
5182
|
throw new Error(`validate.json \u306E\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059: ${inputPath}`);
|
|
@@ -4642,15 +5232,15 @@ function isMissingFileError5(error2) {
|
|
|
4642
5232
|
return record2.code === "ENOENT";
|
|
4643
5233
|
}
|
|
4644
5234
|
async function writeValidationResult(root, outputPath, result) {
|
|
4645
|
-
const abs =
|
|
4646
|
-
await mkdir3(
|
|
5235
|
+
const abs = path23.isAbsolute(outputPath) ? outputPath : path23.resolve(root, outputPath);
|
|
5236
|
+
await mkdir3(path23.dirname(abs), { recursive: true });
|
|
4647
5237
|
await writeFile2(abs, `${JSON.stringify(result, null, 2)}
|
|
4648
5238
|
`, "utf-8");
|
|
4649
5239
|
}
|
|
4650
5240
|
|
|
4651
5241
|
// src/cli/commands/validate.ts
|
|
4652
5242
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
4653
|
-
import
|
|
5243
|
+
import path24 from "path";
|
|
4654
5244
|
|
|
4655
5245
|
// src/cli/lib/failOn.ts
|
|
4656
5246
|
function shouldFail(result, failOn) {
|
|
@@ -4665,7 +5255,7 @@ function shouldFail(result, failOn) {
|
|
|
4665
5255
|
|
|
4666
5256
|
// src/cli/commands/validate.ts
|
|
4667
5257
|
async function runValidate(options) {
|
|
4668
|
-
const root =
|
|
5258
|
+
const root = path24.resolve(options.root);
|
|
4669
5259
|
const configResult = await loadConfig(root);
|
|
4670
5260
|
const result = await validateProject(root, configResult);
|
|
4671
5261
|
const normalized = normalizeValidationResult(root, result);
|
|
@@ -4790,12 +5380,12 @@ function issueKey(issue7) {
|
|
|
4790
5380
|
}
|
|
4791
5381
|
async function emitJson(result, root, jsonPath) {
|
|
4792
5382
|
const abs = resolveJsonPath(root, jsonPath);
|
|
4793
|
-
await mkdir4(
|
|
5383
|
+
await mkdir4(path24.dirname(abs), { recursive: true });
|
|
4794
5384
|
await writeFile3(abs, `${JSON.stringify(result, null, 2)}
|
|
4795
5385
|
`, "utf-8");
|
|
4796
5386
|
}
|
|
4797
5387
|
function resolveJsonPath(root, jsonPath) {
|
|
4798
|
-
return
|
|
5388
|
+
return path24.isAbsolute(jsonPath) ? jsonPath : path24.resolve(root, jsonPath);
|
|
4799
5389
|
}
|
|
4800
5390
|
var GITHUB_ANNOTATION_LIMIT = 100;
|
|
4801
5391
|
|
|
@@ -4813,7 +5403,9 @@ function parseArgs(argv, cwd) {
|
|
|
4813
5403
|
doctorFormat: "text",
|
|
4814
5404
|
validateFormat: "text",
|
|
4815
5405
|
strict: false,
|
|
4816
|
-
|
|
5406
|
+
guardrailsPaths: [],
|
|
5407
|
+
help: false,
|
|
5408
|
+
invalidExitCode: 1
|
|
4817
5409
|
};
|
|
4818
5410
|
const args = [...argv];
|
|
4819
5411
|
let command = args.shift() ?? null;
|
|
@@ -4822,6 +5414,25 @@ function parseArgs(argv, cwd) {
|
|
|
4822
5414
|
options.help = true;
|
|
4823
5415
|
command = null;
|
|
4824
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
|
+
}
|
|
4825
5436
|
for (let i = 0; i < args.length; i += 1) {
|
|
4826
5437
|
const arg = args[i];
|
|
4827
5438
|
switch (arg) {
|
|
@@ -4829,8 +5440,7 @@ function parseArgs(argv, cwd) {
|
|
|
4829
5440
|
{
|
|
4830
5441
|
const next = readOptionValue(args, i);
|
|
4831
5442
|
if (next === null) {
|
|
4832
|
-
|
|
4833
|
-
options.help = true;
|
|
5443
|
+
markInvalid();
|
|
4834
5444
|
break;
|
|
4835
5445
|
}
|
|
4836
5446
|
options.root = next;
|
|
@@ -4842,8 +5452,7 @@ function parseArgs(argv, cwd) {
|
|
|
4842
5452
|
{
|
|
4843
5453
|
const next = readOptionValue(args, i);
|
|
4844
5454
|
if (next === null) {
|
|
4845
|
-
|
|
4846
|
-
options.help = true;
|
|
5455
|
+
markInvalid();
|
|
4847
5456
|
break;
|
|
4848
5457
|
}
|
|
4849
5458
|
options.dir = next;
|
|
@@ -4862,8 +5471,7 @@ function parseArgs(argv, cwd) {
|
|
|
4862
5471
|
case "--format": {
|
|
4863
5472
|
const next = readOptionValue(args, i);
|
|
4864
5473
|
if (next === null) {
|
|
4865
|
-
|
|
4866
|
-
options.help = true;
|
|
5474
|
+
markInvalid();
|
|
4867
5475
|
break;
|
|
4868
5476
|
}
|
|
4869
5477
|
applyFormatOption(command, next, options);
|
|
@@ -4876,8 +5484,7 @@ function parseArgs(argv, cwd) {
|
|
|
4876
5484
|
case "--fail-on": {
|
|
4877
5485
|
const next = readOptionValue(args, i);
|
|
4878
5486
|
if (next === null) {
|
|
4879
|
-
|
|
4880
|
-
options.help = true;
|
|
5487
|
+
markInvalid();
|
|
4881
5488
|
break;
|
|
4882
5489
|
}
|
|
4883
5490
|
if (next === "never" || next === "warning" || next === "error") {
|
|
@@ -4889,8 +5496,7 @@ function parseArgs(argv, cwd) {
|
|
|
4889
5496
|
case "--out": {
|
|
4890
5497
|
const next = readOptionValue(args, i);
|
|
4891
5498
|
if (next === null) {
|
|
4892
|
-
|
|
4893
|
-
options.help = true;
|
|
5499
|
+
markInvalid();
|
|
4894
5500
|
break;
|
|
4895
5501
|
}
|
|
4896
5502
|
if (command === "doctor") {
|
|
@@ -4904,8 +5510,7 @@ function parseArgs(argv, cwd) {
|
|
|
4904
5510
|
case "--in": {
|
|
4905
5511
|
const next = readOptionValue(args, i);
|
|
4906
5512
|
if (next === null) {
|
|
4907
|
-
|
|
4908
|
-
options.help = true;
|
|
5513
|
+
markInvalid();
|
|
4909
5514
|
break;
|
|
4910
5515
|
}
|
|
4911
5516
|
options.reportIn = next;
|
|
@@ -4918,14 +5523,57 @@ function parseArgs(argv, cwd) {
|
|
|
4918
5523
|
case "--base-url": {
|
|
4919
5524
|
const next = readOptionValue(args, i);
|
|
4920
5525
|
if (next === null) {
|
|
4921
|
-
|
|
4922
|
-
options.help = true;
|
|
5526
|
+
markInvalid();
|
|
4923
5527
|
break;
|
|
4924
5528
|
}
|
|
4925
5529
|
options.reportBaseUrl = next;
|
|
4926
5530
|
i += 1;
|
|
4927
5531
|
break;
|
|
4928
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
|
+
}
|
|
4929
5577
|
case "--help":
|
|
4930
5578
|
case "-h":
|
|
4931
5579
|
options.help = true;
|
|
@@ -4934,6 +5582,9 @@ function parseArgs(argv, cwd) {
|
|
|
4934
5582
|
break;
|
|
4935
5583
|
}
|
|
4936
5584
|
}
|
|
5585
|
+
if (command === "guardrails" && !options.help && !options.guardrailsAction) {
|
|
5586
|
+
markInvalid();
|
|
5587
|
+
}
|
|
4937
5588
|
return { command, invalid, options };
|
|
4938
5589
|
}
|
|
4939
5590
|
function readOptionValue(args, index) {
|
|
@@ -4972,6 +5623,16 @@ function applyFormatOption(command, value, options) {
|
|
|
4972
5623
|
options.validateFormat = value;
|
|
4973
5624
|
}
|
|
4974
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
|
+
}
|
|
4975
5636
|
|
|
4976
5637
|
// src/cli/main.ts
|
|
4977
5638
|
async function run(argv, cwd) {
|
|
@@ -4979,7 +5640,7 @@ async function run(argv, cwd) {
|
|
|
4979
5640
|
if (!command || options.help) {
|
|
4980
5641
|
info(usage());
|
|
4981
5642
|
if (invalid) {
|
|
4982
|
-
process.exitCode =
|
|
5643
|
+
process.exitCode = options.invalidExitCode;
|
|
4983
5644
|
}
|
|
4984
5645
|
return;
|
|
4985
5646
|
}
|
|
@@ -5028,6 +5689,19 @@ async function run(argv, cwd) {
|
|
|
5028
5689
|
process.exitCode = exitCode;
|
|
5029
5690
|
}
|
|
5030
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;
|
|
5031
5705
|
default:
|
|
5032
5706
|
error(`Unknown command: ${command}`);
|
|
5033
5707
|
info(usage());
|
|
@@ -5042,6 +5716,7 @@ Commands:
|
|
|
5042
5716
|
validate \u4ED5\u69D8/\u5951\u7D04/\u53C2\u7167\u306E\u691C\u67FB
|
|
5043
5717
|
report \u691C\u8A3C\u7D50\u679C\u3068\u96C6\u8A08\u3092\u51FA\u529B
|
|
5044
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
|
|
5045
5720
|
|
|
5046
5721
|
Options:
|
|
5047
5722
|
--root <path> \u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA
|
|
@@ -5059,6 +5734,9 @@ Options:
|
|
|
5059
5734
|
--in <path> report: validate.json \u306E\u5165\u529B\u5148\uFF08config\u3088\u308A\u512A\u5148\uFF09
|
|
5060
5735
|
--run-validate report: validate \u3092\u5B9F\u884C\u3057\u3066\u304B\u3089 report \u3092\u751F\u6210
|
|
5061
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
|
|
5062
5740
|
-h, --help \u30D8\u30EB\u30D7\u8868\u793A
|
|
5063
5741
|
`;
|
|
5064
5742
|
}
|