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.
- package/README.md +327 -245
- package/assets/init/.qfai/README.md +2 -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 +197 -0
- 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 +43 -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 +4 -0
- package/assets/init/root/.claude/commands/qfai-configure.md +14 -0
- package/assets/init/root/.claude/commands/qfai-discuss.md +14 -0
- package/assets/init/root/.claude/commands/qfai-implement.md +14 -0
- package/assets/init/root/.claude/commands/qfai-require.md +14 -0
- package/assets/init/root/.claude/commands/qfai-scenario-test.md +14 -0
- package/assets/init/root/.claude/commands/qfai-spec.md +14 -0
- package/assets/init/root/.claude/commands/qfai-unit-test.md +14 -0
- package/assets/init/root/.claude/commands/qfai-verify.md +14 -0
- package/assets/init/root/.codex/README.md +16 -0
- package/assets/init/root/.codex/skills/qfai-configure/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-discuss/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-implement/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-require/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-scenario-test/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-spec/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-unit-test/SKILL.md +18 -0
- package/assets/init/root/.codex/skills/qfai-verify/SKILL.md +18 -0
- package/assets/init/root/.github/copilot-instructions.md +14 -0
- package/assets/init/root/.github/prompts/qfai-configure.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-discuss.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-implement.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-require.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-scenario-test.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-spec.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-unit-test.prompt.md +17 -0
- package/assets/init/root/.github/prompts/qfai-verify.prompt.md +17 -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 +880 -196
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.mjs +866 -182
- 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.0
|
|
1060
|
-
return "1.0
|
|
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 =
|
|
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"
|
|
@@ -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
|
|
1657
|
-
import
|
|
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
|
|
1698
|
-
import
|
|
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
|
|
1702
|
-
import
|
|
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 =
|
|
1725
|
-
const apiRoot =
|
|
1726
|
-
const dbRoot =
|
|
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
|
|
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
|
|
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 <
|
|
1932
|
-
const lineText =
|
|
1933
|
-
const lineNumber =
|
|
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
|
|
1992
|
-
import
|
|
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
|
|
2497
|
+
import path17 from "path";
|
|
1996
2498
|
import { parse as parseYaml2 } from "yaml";
|
|
1997
2499
|
function parseStructuredContract(file, text) {
|
|
1998
|
-
const ext =
|
|
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 =
|
|
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(
|
|
2027
|
-
issues.push(...await validateDbContracts(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2501
|
-
const packRelative =
|
|
2502
|
-
if (packRelative.startsWith("..") ||
|
|
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 =
|
|
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
|
|
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 =
|
|
2617
|
-
const assetRelative =
|
|
2618
|
-
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)) {
|
|
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 =
|
|
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 (
|
|
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
|
|
2720
|
-
import
|
|
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 =
|
|
3231
|
+
const deltaPath = path19.join(pack, "delta.md");
|
|
2730
3232
|
try {
|
|
2731
|
-
await
|
|
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
|
|
2783
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2908
|
-
import
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
3825
|
-
const uiRoot =
|
|
3826
|
-
const dbRoot =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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" ?
|
|
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 =
|
|
4574
|
-
await mkdir3(
|
|
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
|
|
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 =
|
|
4640
|
-
await mkdir3(
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
}
|