qatools-cli 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/qatools.js +210 -6
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qatools-cli",
3
- "version": "1.1.0",
4
- "description": "CLI for QA Tools - read test sessions, upload Claude analysis",
3
+ "version": "1.1.2",
4
+ "description": "CLI for QA Tools — requirements, epics, sprints, sessions, App Context sync, project agenda. The companion to qa-tools.com.",
5
5
  "bin": {
6
6
  "qatools": "./qatools.js"
7
7
  },
package/qatools.js CHANGED
@@ -127,7 +127,7 @@ async function authenticate(options) {
127
127
  program
128
128
  .name("qatools")
129
129
  .description("CLI for QA Tools")
130
- .version("1.0.0");
130
+ .version(require("./package.json").version);
131
131
 
132
132
  program
133
133
  .command("upload-analysis")
@@ -923,10 +923,11 @@ program
923
923
 
924
924
  program
925
925
  .command("requirement-update")
926
- .description("Update one or more requirements (by id, optionally from a JSON file)")
927
- .option("-f, --file <path>", "JSON file with updates: { '<requirementId>': { status, epicKey, ... } }")
926
+ .description("Update one or more requirements (by id, optionally from a JSON file). status=implemented/tested REQUIRES --evidence.")
927
+ .option("-f, --file <path>", "JSON file with updates: { '<requirementId>': { status, epicKey, evidence, ... } }")
928
928
  .option("-i, --id <id>", "Single requirement ID to update")
929
929
  .option("--status <status>", "New status (draft/approved/rejected/implemented/tested/superseded)")
930
+ .option("--evidence <text>", "REQUIRED when status=implemented or tested. Cite test output, file:line, expected vs actual, or 'no test applies because <reason>'.")
930
931
  .option("--epic <key>", "Set epicKey")
931
932
  .option("--sprint <id>", "Set sprintId (use empty string to clear)")
932
933
  .option("--type <type>", "Set type (functional/non-functional)")
@@ -943,8 +944,16 @@ program
943
944
  if (!projectId) { console.error("No project configured. Run: qatools setup, or pass --project <id>"); process.exit(1); }
944
945
 
945
946
  let updates = {};
947
+ let evidenceById = {}; // requirementId -> evidence text
946
948
  if (options.file) {
947
949
  updates = JSON.parse(fs.readFileSync(path.resolve(options.file), "utf8"));
950
+ // In file mode, evidence is per-update inline (e.g. { status: 'tested', evidence: '...' })
951
+ for (const [id, u] of Object.entries(updates)) {
952
+ if (u.evidence) {
953
+ evidenceById[id] = u.evidence;
954
+ delete u.evidence; // separate from updates so backend handles via dedicated arg
955
+ }
956
+ }
948
957
  } else if (options.id) {
949
958
  const u = {};
950
959
  if (options.status) u.status = options.status;
@@ -955,16 +964,48 @@ program
955
964
  if (options.description) u.description = options.description;
956
965
  if (options.ac) u.acceptanceCriteria = options.ac;
957
966
  updates[options.id] = u;
967
+ if (options.evidence) evidenceById[options.id] = options.evidence;
958
968
  } else {
959
969
  console.error("Provide --file or --id");
960
970
  process.exit(1);
961
971
  }
962
972
 
973
+ // Pre-flight: refuse status=implemented/tested without evidence (catch before round-trip)
974
+ const REQUIRES_EVIDENCE = new Set(['implemented', 'tested']);
975
+ const violations = [];
976
+ for (const [id, u] of Object.entries(updates)) {
977
+ if (u.status && REQUIRES_EVIDENCE.has(u.status)) {
978
+ const ev = (evidenceById[id] || '').trim();
979
+ if (ev.length < 10) violations.push({ id, status: u.status });
980
+ }
981
+ }
982
+ if (violations.length > 0) {
983
+ console.error("");
984
+ console.error("✗ Refusing to set status=implemented/tested without --evidence:");
985
+ console.error("");
986
+ for (const v of violations) {
987
+ console.error(` • ${v.id} → status=${v.status} (no evidence given)`);
988
+ }
989
+ console.error("");
990
+ console.error(" Evidence-driven status: cite a concrete verification before claiming done.");
991
+ console.error(" Examples:");
992
+ console.error(" --evidence \"Test AUTH-1.3 passed in session abc123, response was 302 to /login\"");
993
+ console.error(" --evidence \"Manual: input mass=10 kg, rate=5 kr/kg → got 50 kr (matches expected)\"");
994
+ console.error(" --evidence \"qa.service.ts:340 — checkProjectAccess now returns 403 for cross-project keys\"");
995
+ console.error(" --evidence \"No test applies — pure docs change in README.md, no executable behavior\"");
996
+ console.error("");
997
+ console.error(" Min 10 chars. If you cannot articulate the verification, use status=ready_for_test instead.");
998
+ console.error("");
999
+ process.exit(2);
1000
+ }
1001
+
963
1002
  const updateReq = httpsCallable(functions, "updateRequirement");
964
1003
  let ok = 0, failed = 0;
965
1004
  for (const [requirementId, fields] of Object.entries(updates)) {
966
1005
  try {
967
- await updateReq({ projectId, requirementId, updates: fields });
1006
+ const payload = { projectId, requirementId, updates: fields };
1007
+ if (evidenceById[requirementId]) payload.evidence = evidenceById[requirementId];
1008
+ await updateReq(payload);
968
1009
  ok++;
969
1010
  process.stdout.write(`\r Updated ${ok}, failed ${failed} / ${Object.keys(updates).length}`);
970
1011
  } catch (e) {
@@ -1093,10 +1134,11 @@ program
1093
1134
 
1094
1135
  program
1095
1136
  .command("backlog-update")
1096
- .description("Update one or more backlog items (JSON file or inline)")
1097
- .option("-f, --file <path>", "JSON file with updates: { '<id>': { status, comment, ... } }")
1137
+ .description("Update one or more backlog items. status=resolved REQUIRES --evidence (cite the verification).")
1138
+ .option("-f, --file <path>", "JSON file with updates: { '<id>': { status, evidence, comment, ... } }")
1098
1139
  .option("-i, --id <id>", "Single item ID to update")
1099
1140
  .option("--status <status>", "New status")
1141
+ .option("--evidence <text>", "REQUIRED when status=resolved (except for GAP). Cite the verification: test output, file:line, expected vs actual, or 'no test applies because <reason>'.")
1100
1142
  .option("--comment <text>", "Add comment")
1101
1143
  .option("--assigned <email>", "Assign to")
1102
1144
  .option("--sprint <id>", "Set sprint ID")
@@ -1106,6 +1148,8 @@ program
1106
1148
  .option("--tags <tags>", "Set tags (comma-separated, replaces existing)")
1107
1149
  .option("--description <text>", "Update description")
1108
1150
  .option("--author <name>", "Author name for comments (default: your email)", "Claude")
1151
+ .option("--skip-test-verification", "Allow setting status=resolved without going through ready_for_test (use only when no test applies). Evidence is still required.")
1152
+ .option("--project <id>", "Target project ID (overrides default; needed for status pre-check)")
1109
1153
  .option("-e, --email <email>", "Login email")
1110
1154
  .option("-p, --password <password>", "Login password")
1111
1155
  .action(async (options) => {
@@ -1121,6 +1165,7 @@ program
1121
1165
  } else if (options.id) {
1122
1166
  const u = {};
1123
1167
  if (options.status) u.status = options.status;
1168
+ if (options.evidence) u.evidence = options.evidence;
1124
1169
  if (options.comment) { u.comment = options.comment; u.authorName = options.author; }
1125
1170
  if (options.assigned) u.assignedTo = options.assigned;
1126
1171
  if (options.developer) u.developer = options.developer;
@@ -1135,6 +1180,105 @@ program
1135
1180
  process.exit(1);
1136
1181
  }
1137
1182
 
1183
+ // Pre-flight: refuse status=resolved without evidence (catch before round-trip)
1184
+ // Need to fetch items to know their type (GAP is exempt)
1185
+ const settingResolved = Object.entries(updates).filter(([, u]) => u.status === 'resolved');
1186
+ if (settingResolved.length > 0) {
1187
+ const creds = getCredentials();
1188
+ const preCheckProjectId = options.project || creds?.projectId;
1189
+ if (preCheckProjectId) {
1190
+ try {
1191
+ const listBacklog = httpsCallable(functions, "listBacklog");
1192
+ const listResult = await listBacklog({ projectId: preCheckProjectId });
1193
+ const allItems = listResult.data.items || [];
1194
+ const itemById = {};
1195
+ for (const it of allItems) itemById[it.id] = it;
1196
+ const evidenceViolations = [];
1197
+ for (const [id, u] of settingResolved) {
1198
+ const item = itemById[id];
1199
+ if (!item) continue;
1200
+ if (item.type === 'requirement_gap') continue; // GAP exempt from evidence
1201
+ const ev = (u.evidence || '').trim();
1202
+ if (ev.length < 10) {
1203
+ evidenceViolations.push({ id, type: item.type, currentStatus: item.status, description: (item.description || '').substring(0, 80) });
1204
+ }
1205
+ }
1206
+ if (evidenceViolations.length > 0) {
1207
+ console.error("");
1208
+ console.error("✗ Refusing to set status=resolved without --evidence:");
1209
+ console.error("");
1210
+ for (const v of evidenceViolations) {
1211
+ console.error(` • ${v.id} type=${v.type} current=${v.currentStatus}`);
1212
+ console.error(` "${v.description}"`);
1213
+ }
1214
+ console.error("");
1215
+ console.error(" Evidence-driven status: cite a concrete verification before claiming resolved.");
1216
+ console.error(" Examples:");
1217
+ console.error(" --evidence \"Test SPRINT-1.2 passed in session abc123, item appeared in sprint list\"");
1218
+ console.error(" --evidence \"Reproduced bug then re-ran: input=10, expected=50, actual=50 (file.ts:42)\"");
1219
+ console.error(" --evidence \"No test applies — pure docs change in CHANGELOG.md\"");
1220
+ console.error("");
1221
+ console.error(" Min 10 chars. If you cannot articulate the verification, use status=ready_for_test instead.");
1222
+ console.error("");
1223
+ process.exit(2);
1224
+ }
1225
+ } catch (e) {
1226
+ // Pre-check failed (network etc) — fall through; backend will enforce
1227
+ console.error(`Warning: pre-flight evidence check failed (${e.message}). Backend will enforce.`);
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ // Pre-flight: enforce backlog flow. AVV/DEF/IMP cannot jump straight to resolved.
1233
+ // GAP is exempt (its "fix" is writing a requirement, not testing code).
1234
+ if (!options.skipTestVerification) {
1235
+ const settingResolved = Object.entries(updates).filter(([, u]) => u.status === 'resolved');
1236
+ if (settingResolved.length > 0) {
1237
+ const creds = getCredentials();
1238
+ const projectId = options.project || creds?.projectId;
1239
+ if (projectId) {
1240
+ try {
1241
+ const listBacklog = httpsCallable(functions, "listBacklog");
1242
+ const listResult = await listBacklog({ projectId });
1243
+ const allItems = listResult.data.items || [];
1244
+ const itemById = {};
1245
+ for (const it of allItems) itemById[it.id] = it;
1246
+ const violations = [];
1247
+ for (const [id] of settingResolved) {
1248
+ const item = itemById[id];
1249
+ if (!item) continue;
1250
+ if (item.type === 'requirement_gap') continue; // GAP exempt
1251
+ if (item.status === 'ready_for_test') continue; // legitimate transition
1252
+ violations.push({ id, type: item.type, currentStatus: item.status, description: (item.description || '').substring(0, 80) });
1253
+ }
1254
+ if (violations.length > 0) {
1255
+ console.error("");
1256
+ console.error("✗ Refusing to set status=resolved on items that haven't been through ready_for_test:");
1257
+ console.error("");
1258
+ for (const v of violations) {
1259
+ console.error(` • ${v.id} type=${v.type} current=${v.currentStatus}`);
1260
+ console.error(` "${v.description}"`);
1261
+ }
1262
+ console.error("");
1263
+ console.error(" Backlog flow: open → in_progress → ready_for_test → resolved");
1264
+ console.error(" AVV/DEF/IMP must be verified by a test before resolved.");
1265
+ console.error("");
1266
+ console.error(" Fix:");
1267
+ console.error(" a) Set status=ready_for_test instead and let a tester verify it");
1268
+ console.error(" b) Run a test session that covers this item, then flip to resolved");
1269
+ console.error(" c) If no test applies (e.g. pure docs), pass --skip-test-verification");
1270
+ console.error("");
1271
+ process.exit(2);
1272
+ }
1273
+ } catch (e) {
1274
+ console.error(`Warning: pre-flight check failed (${e.message}). Continuing without verification — pass --skip-test-verification to suppress this warning.`);
1275
+ }
1276
+ } else {
1277
+ console.error("Warning: no projectId configured — cannot pre-check backlog flow. Pass --project <id> for verification.");
1278
+ }
1279
+ }
1280
+ }
1281
+
1138
1282
  const updateItems = httpsCallable(functions, "updateBacklogItems");
1139
1283
  try {
1140
1284
  const result = await updateItems({ updates });
@@ -1719,6 +1863,65 @@ Every claim, verdict, analysis, and recommendation MUST cite specific evidence:
1719
1863
 
1720
1864
  ## If you're unsure
1721
1865
  Say "I'm unsure" — that's better than guessing. Suggest what evidence would resolve the uncertainty.
1866
+ `,
1867
+ "qa-numerical-verification.md": `---
1868
+ description: How to verify algorithmic and numerical bug fixes. Read this when fixing a formula, calculation, or any code where the bug is "wrong number" rather than "wrong structure".
1869
+ ---
1870
+
1871
+ # Numerical & Algorithmic Verification
1872
+
1873
+ Most evidence-first examples deal with structural bugs (NullReferenceException, wrong redirect, race condition). Algorithmic bugs are different — the code runs without crashing, it just produces the wrong number.
1874
+
1875
+ This skill is for those bugs. The verification pattern is non-negotiable.
1876
+
1877
+ ## The pattern
1878
+
1879
+ 1. **Trivialize the input.** Pick values that make the formula readable. Set multipliers to 1, set inputs to round numbers (10, 100), set toggles to known states.
1880
+ 2. **Compute the expected value by hand.** Write down the formula, plug in the trivialized inputs, get the expected number. Show your work.
1881
+ 3. **Run the same input through the system.** Use whatever entry point the bug lives in — CLI, API, UI, test harness. Capture the actual output.
1882
+ 4. **Compare with tolerance.** For floats, exact equality is fragile. Use a tolerance (e.g. abs(actual - expected) < 0.001) and document it.
1883
+ 5. **Cite both numbers in evidence.** When you set status=tested, the --evidence flag must contain both the expected and actual value, plus the input that produced them.
1884
+
1885
+ ## What "evidence" looks like for numerical bugs
1886
+
1887
+ ❌ "Fixed the formula in calculation.ts"
1888
+ ❌ "User confirmed the value looks right now"
1889
+ ❌ "Tests pass" (which test? what value?)
1890
+
1891
+ ✅ "Input: mass=10 kg, rate=5 kr/kg, alpha=0. Expected = 10*5 = 50. Actual = 50.0 (calc.ts:142). Diff < 0.001."
1892
+ ✅ "Input: temp=20°C, time=3600s. Expected = 20*3600/1000 = 72 kWh. Actual = 72.0 (sim.py:88)."
1893
+ ✅ "Trivialized: set kr_per_kg=1 to inspect the structure. Expected = mass*1 = mass = 10. Actual = 10.0. Now bumped kr_per_kg=5: expected=50, actual=50. Original bug was (1+alpha)*prod*rate*T which gave 132 for the same input."
1894
+
1895
+ ## Why this matters
1896
+
1897
+ The difference between "I fixed it" and "I verified it" is the difference between a fix that holds and a fix that creates the next bug. For numerical code, "looks right" is not verification — the formula can be wrong by a constant factor and look fine until production.
1898
+
1899
+ ## Specific traps
1900
+
1901
+ ### 1. The dataset doesn't have a discriminating input
1902
+ If your test data has kr_per_kg = 0 for everything, multiplying by anything gives 0. The bug in the multiplication is invisible. **Fix: change the dataset or create a new one** with values that would produce different results between the buggy and fixed formula. Never skip verification with "the data doesn't show it".
1903
+
1904
+ ### 2. Off-by-constant errors
1905
+ The bug formula (1+alpha) * prod * rate * T happens to equal the correct mass * rate when alpha=0 AND prod*T = mass. If your test input satisfies both, both formulas pass. **Fix: pick inputs where the two formulas diverge.**
1906
+
1907
+ ### 3. Sign errors and unit mismatches
1908
+ mg/MJ vs kg/MWh differ by 10^9 / 3600 ≈ 277,777x. A "small refactor" can introduce this and pass type checks. **Fix: include unit conversion in the trivialized expected calc.**
1909
+
1910
+ ### 4. The user testing loop
1911
+ "I'll fix it and the user will test it" is not a fix workflow — it's a hand-off of responsibility. Every numerical fix should come with at least one number you computed and one number the system produced.
1912
+
1913
+ ## Status transitions
1914
+
1915
+ - After implementing the fix → status=ready_for_test, with --evidence describing what the fix does and what test should verify it
1916
+ - After running the verification → status=tested (req) or resolved (backlog), with --evidence containing the expected vs actual numbers
1917
+
1918
+ If the verification fails: do NOT mark tested. Go back to step 1, find the discrepancy, fix again.
1919
+
1920
+ ## Cross-reference
1921
+
1922
+ - qa-evidence-first.md — the core principle
1923
+ - qa-developer.md — general implementation guidance
1924
+ - The CLI will refuse status=implemented/tested without --evidence (min 10 chars). Use that as your forcing function.
1722
1925
  `
1723
1926
  };
1724
1927
 
@@ -1732,6 +1935,7 @@ Say "I'm unsure" — that's better than guessing. Suggest what evidence would re
1732
1935
  console.log(" ✓ qa-test-analyst.md");
1733
1936
  console.log(" ✓ qa-release-manager.md");
1734
1937
  console.log(" ✓ qa-evidence-first.md");
1938
+ console.log(" ✓ qa-numerical-verification.md");
1735
1939
 
1736
1940
  // 4. Generate app context skill from API
1737
1941
  try {