valent-pipeline 0.19.67 → 0.19.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valent-pipeline",
3
- "version": "0.19.67",
3
+ "version": "0.19.68",
4
4
  "description": "v3 multi-agent AI pipeline for software development lifecycle",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,7 @@ import { isAbsolute, join } from 'path';
3
3
  import { checkSpecConformance } from '../lib/spec-conformance.js';
4
4
  import { evidenceDir, latestRun } from '../lib/evidence.js';
5
5
  import { parseJunit } from '../lib/junit.js';
6
+ import { detectManifestCaseDrift } from '../lib/spec-manifest.js';
6
7
 
7
8
  const DEFAULT_TEST_RE = /\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/;
8
9
  const IGNORE_DIRS = new Set([
@@ -79,6 +80,12 @@ export async function checkSpecConformanceCmd(options) {
79
80
  process.exit(2);
80
81
  }
81
82
 
83
+ const drift = detectManifestCaseDrift(manifest);
84
+ if (drift) {
85
+ console.error(`spec-conformance: ${drift}`);
86
+ process.exit(1);
87
+ }
88
+
82
89
  if (!Array.isArray(manifest?.cases) || manifest.cases.length === 0) {
83
90
  console.log(`spec-conformance: manifest has no cases — skipping (no-op pass).`);
84
91
  process.exit(0);
@@ -11,6 +11,7 @@ import { join, isAbsolute, dirname } from 'path';
11
11
  import { traceCheck, traceMatrix } from '../lib/trace.js';
12
12
  import { evidenceDir, resolveRunForAssert, requiredCaseIdsForRun, resolveRoot } from '../lib/evidence.js';
13
13
  import { parseJunit } from '../lib/junit.js';
14
+ import { detectManifestCaseDrift } from '../lib/spec-manifest.js';
14
15
 
15
16
  /** Load an exact evidence run by id (for `--run`), or null if absent/unreadable. */
16
17
  function loadRunById(evDir, runId) {
@@ -48,6 +49,8 @@ function loadInputs(options) {
48
49
  if (!existsSync(specPath)) return { noop: `no spec manifest at ${specPath} — skipping (no-op pass).` };
49
50
  const reqsManifest = readJson(reqsPath, 'reqs manifest');
50
51
  const specManifest = readJson(specPath, 'spec manifest');
52
+ const drift = detectManifestCaseDrift(specManifest);
53
+ if (drift) return { error: drift };
51
54
  const cases = Array.isArray(specManifest?.cases) ? specManifest.cases : [];
52
55
  if (cases.length > 0 && !cases.some((c) => Array.isArray(c.ac) && c.ac.length > 0)) {
53
56
  return { noop: 'spec manifest predates the per-case `ac` field — skipping (no-op pass). Re-run QA-A to gain mechanical AC coverage.' };
@@ -93,6 +96,10 @@ export async function traceCheckCmd(options) {
93
96
  process.exit(2);
94
97
  }
95
98
  const inputs = loadInputs(options);
99
+ if (inputs.error) {
100
+ console.error(`trace-check: ${inputs.error}`);
101
+ process.exit(1);
102
+ }
96
103
  if (inputs.noop) {
97
104
  console.log(`trace-check: ${inputs.noop}`);
98
105
  process.exit(0);
@@ -108,6 +115,10 @@ export async function traceMatrixCmd(options) {
108
115
  process.exit(2);
109
116
  }
110
117
  const inputs = loadInputs(options);
118
+ if (inputs.error) {
119
+ console.error(`trace-matrix: ${inputs.error}`);
120
+ process.exit(1);
121
+ }
111
122
  if (inputs.noop) {
112
123
  console.log(`trace-matrix: ${inputs.noop}`);
113
124
  process.exit(0);
@@ -0,0 +1,30 @@
1
+ // Shared guard against qa-test-spec.manifest.json schema drift (M-223).
2
+ //
3
+ // The canonical manifest carries its spec'd cases under `cases` (pipeline/schemas/qa-spec-manifest.schema.json,
4
+ // `required: ["schema","story","cases"]`). QA-A is an LLM and can drift the key — e.g. emit `testCases` instead
5
+ // of `cases`. The downstream readers (`trace check`, `check-spec-conformance`) then silently see ZERO cases:
6
+ // trace check reports every AC uncovered (a phantom `uncoveredAcs` coverage gap that reads as a real defect),
7
+ // and spec-conformance takes its empty-manifest no-op PASS (masking the drift entirely). Both cost a confused
8
+ // diagnosis + a re-run (3.9, 2026-06-28). The schema existed but was never enforced at ingestion.
9
+ //
10
+ // This detects the specific, recoverable drift — `cases` absent/empty WHILE a case-like array (objects with a
11
+ // string `id`) sits under another top-level key — and returns an actionable message so the gate fails LOUD with
12
+ // the real reason instead of a phantom coverage gap or a vacuous pass. It deliberately does NOT fire for a
13
+ // genuinely empty/absent `cases` (a pre-Phase manifest or a story with no spec'd cases yet) — that stays a
14
+ // legitimate no-op, so this adds a clear failure ONLY where the manifest is populated-but-misnamed. Returns the
15
+ // message string, or null when there is no drift.
16
+ export function detectManifestCaseDrift(manifest) {
17
+ if (!manifest || typeof manifest !== 'object') return null;
18
+ if (Array.isArray(manifest.cases) && manifest.cases.length > 0) return null; // canonical + populated
19
+
20
+ const caseLike = (arr) => Array.isArray(arr) && arr.length > 0
21
+ && arr.every((c) => c && typeof c === 'object' && typeof c.id === 'string' && c.id.length > 0);
22
+ const aliases = Object.keys(manifest).filter((k) => k !== 'cases' && caseLike(manifest[k]));
23
+ if (!aliases.length) return null; // genuinely empty/absent — legitimate no-op, not drift
24
+
25
+ const k = aliases[0];
26
+ const n = manifest[k].length;
27
+ return `no populated \`cases\` array but found ${n} case-like entr${n === 1 ? 'y' : 'ies'} under \`${k}\` `
28
+ + `— qa-test-spec.manifest.json schema drift (the canonical key is \`cases\`). Re-author the manifest with `
29
+ + `the cases under \`cases\` (see pipeline/schemas/qa-spec-manifest.schema.json).`;
30
+ }