harness-everything 0.47.3 → 0.47.5
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/behavioral-evals/README.md +27 -0
- package/behavioral-evals/case-validator.js +22 -1
- package/behavioral-evals/cases/baseline-profile-lookup-json-only.yaml +23 -0
- package/behavioral-evals/cases/baseline-tdd-workflow.yaml +4 -0
- package/behavioral-evals/run.js +257 -6
- package/behavioral-evals/transcript-parser.js +43 -0
- package/benchmarks/issue290-pilot/metrics.test.py +60 -2
- package/ci/mechanism-2s-tdd-quality.test.js +149 -6
- package/ci/test-behavioral-case-validation.js +362 -3
- package/code-review/SKILL.md +1 -1
- package/code-review/references/review-playbook.md +9 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +1 -1
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +9 -0
- package/plugins/harness-everything/skills/tdd/SKILL.md +1 -1
- package/plugins/harness-everything/skills/tdd/fixtures/quality-scenarios.json +10 -2
- package/plugins/harness-everything/skills/tdd/references/quality-model.md +2 -0
- package/plugins/harness-everything/skills/tdd/schemas/quality-evidence.schema.json +21 -0
- package/plugins/harness-everything/skills/tdd/schemas/quality-report.schema.json +2 -0
- package/plugins/harness-everything/skills/tdd/scripts/quality-gate.js +165 -1
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +1 -1
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +1 -0
- package/sbom.cdx.json +7 -7
- package/tdd/SKILL.md +1 -1
- package/tdd/fixtures/quality-scenarios.json +10 -2
- package/tdd/references/quality-model.md +2 -0
- package/tdd/schemas/quality-evidence.schema.json +21 -0
- package/tdd/schemas/quality-report.schema.json +2 -0
- package/tdd/scripts/quality-gate.js +165 -1
- package/zoom-out/SKILL.md +1 -1
- package/zoom-out/references/circuit-breaker.md +1 -0
|
@@ -5,8 +5,8 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { parseSimpleYaml } = require('../behavioral-evals/run');
|
|
8
|
-
const { grade, parseTranscriptEvents } = require('../behavioral-evals/run');
|
|
9
|
-
const { validateCase, validateDirectory, negativeControls } = require('../behavioral-evals/case-validator');
|
|
8
|
+
const { grade, parseTranscriptEvents, checkJsonOnlyOutput, finalAssistantText } = require('../behavioral-evals/run');
|
|
9
|
+
const { validateCase, validateDirectory, validateExpectation, negativeControls } = require('../behavioral-evals/case-validator');
|
|
10
10
|
const { canonicalRubric, extractTrace } = require('./ab-test-harness');
|
|
11
11
|
|
|
12
12
|
const casesDir = path.join(__dirname, '..', 'behavioral-evals', 'cases');
|
|
@@ -85,4 +85,363 @@ try {
|
|
|
85
85
|
fs.rmSync(transcriptDir, { recursive: true, force: true });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// final_output_json_only: pure checker, validator schema, shipped #308 case, and
|
|
89
|
+
// end-to-end grading over the final assistant message.
|
|
90
|
+
const SELECTION_KEYS = ['requestedProfile', 'effectiveProfile', 'profileAlias', 'assignedRole', 'runtimeModel', 'runtimeEffort'];
|
|
91
|
+
const SELECTION = { requestedProfile: 'fable', effectiveProfile: 'sonnet', profileAlias: 'fable-on-sonnet', assignedRole: 'worker', runtimeModel: 'claude-haiku-5-5', runtimeEffort: 'low' };
|
|
92
|
+
const LABEL = '<!-- harness-label {"judgement":"done","routing":"direct"} -->';
|
|
93
|
+
const RAW = JSON.stringify(SELECTION);
|
|
94
|
+
const FENCED = '```json\n' + JSON.stringify(SELECTION, null, 2) + '\n```';
|
|
95
|
+
const PASS_CASES = [
|
|
96
|
+
['raw JSON object', RAW],
|
|
97
|
+
['fenced json block', FENCED],
|
|
98
|
+
['untagged fence', '```\n' + RAW + '\n```'],
|
|
99
|
+
['raw JSON followed by harness-label line', RAW + '\n' + LABEL],
|
|
100
|
+
['fenced JSON followed by harness-label line', FENCED + '\n' + LABEL],
|
|
101
|
+
];
|
|
102
|
+
for (const [name, text] of PASS_CASES) {
|
|
103
|
+
const verdict = checkJsonOnlyOutput(text, SELECTION_KEYS);
|
|
104
|
+
assert.strictEqual(verdict.pass, true, `${name} should pass: ${verdict.reason}`);
|
|
105
|
+
}
|
|
106
|
+
const FAIL_CASES = [
|
|
107
|
+
// The #308 pilot: correct fields, explanatory prose outside the JSON.
|
|
108
|
+
['prose before JSON (#308)', `"Fable on sonnect" means the fable profile runs on sonnet.\n\n${FENCED}`, /prose before/],
|
|
109
|
+
['prose after fence', `${FENCED}\nThat is the selection record.`, /outside the single JSON code fence/],
|
|
110
|
+
['prose after raw JSON', `${RAW}\nThat is the selection record.`, /not a single valid JSON value/],
|
|
111
|
+
['two fenced blocks', `${FENCED}\n${FENCED}`, /more than one code fence/],
|
|
112
|
+
['missing required key', JSON.stringify({ ...SELECTION, runtimeEffort: undefined }), /missing required JSON keys: runtimeEffort/],
|
|
113
|
+
['non-object JSON (array)', JSON.stringify([SELECTION]), /must be an object/],
|
|
114
|
+
['non-object JSON (scalar)', '"fable"', /must be an object/],
|
|
115
|
+
['empty final text', '', /empty/],
|
|
116
|
+
['label-only final text', LABEL, /empty/],
|
|
117
|
+
['missing final text', undefined, /missing/],
|
|
118
|
+
];
|
|
119
|
+
for (const [name, text, expected] of FAIL_CASES) {
|
|
120
|
+
const verdict = checkJsonOnlyOutput(text, SELECTION_KEYS);
|
|
121
|
+
assert.strictEqual(verdict.pass, false, `${name} should fail`);
|
|
122
|
+
assert.match(verdict.reason, expected, `${name} should give a clear reason`);
|
|
123
|
+
}
|
|
124
|
+
assert.strictEqual(checkJsonOnlyOutput(RAW).pass, true, 'required_keys defaults to no key requirement');
|
|
125
|
+
|
|
126
|
+
for (const requiredKeys of ['requestedProfile', [''], [' '], [1], ['ok', null]]) {
|
|
127
|
+
const errors = validateExpectation({ type: 'final_output_json_only', required_keys: requiredKeys }, 0);
|
|
128
|
+
assert.ok(errors.some(error => /required_keys must be an array of non-empty strings/.test(error)), `required_keys ${JSON.stringify(requiredKeys)} should be rejected`);
|
|
129
|
+
}
|
|
130
|
+
assert.deepStrictEqual(validateExpectation({ type: 'final_output_json_only' }, 0), [], 'final_output_json_only without required_keys is valid');
|
|
131
|
+
assert.deepStrictEqual(validateExpectation({ type: 'final_output_json_only', required_keys: SELECTION_KEYS }, 0), [], 'final_output_json_only with string keys is valid');
|
|
132
|
+
|
|
133
|
+
const profileCasePath = path.join(casesDir, 'baseline-profile-lookup-json-only.yaml');
|
|
134
|
+
const profileCase = parseSimpleYaml(fs.readFileSync(profileCasePath, 'utf8'));
|
|
135
|
+
assert.deepStrictEqual(validateCase(profileCase, { fileName: 'baseline-profile-lookup-json-only.yaml' }), [], 'profile-lookup JSON-only case should validate');
|
|
136
|
+
assert.deepStrictEqual(profileCase.expectations.map(expectation => expectation.type), ['final_output_json_only'], 'profile-lookup case should assert JSON-only output');
|
|
137
|
+
assert.deepStrictEqual(profileCase.expectations[0].required_keys, SELECTION_KEYS, 'profile-lookup case should require the six selection keys');
|
|
138
|
+
assert.match(profileCase.prompt, /return only a small JSON selection record with requestedProfile, effectiveProfile, profileAlias, assignedRole, runtimeModel, runtimeEffort\. No orchestration or delegation\./, 'profile-lookup prompt should match the #308 wording');
|
|
139
|
+
|
|
140
|
+
const jsonOnlyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'behavioral-json-only-'));
|
|
141
|
+
try {
|
|
142
|
+
const writeTranscript = (name, finalText) => {
|
|
143
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
144
|
+
const events = [
|
|
145
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-1', state: { status: 'completed', input: { file_path: 'README.md' } } } },
|
|
146
|
+
{ type: 'text', part: { type: 'text', text: 'Checking the profile table first.' } },
|
|
147
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-2', state: { status: 'completed', input: { file_path: 'profiles.md' } } } },
|
|
148
|
+
{ type: 'text', part: { type: 'text', text: finalText } },
|
|
149
|
+
];
|
|
150
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
151
|
+
return transcriptPath;
|
|
152
|
+
};
|
|
153
|
+
const jsonOnlyExpectation = { expectations: [{ type: 'final_output_json_only', required_keys: SELECTION_KEYS }] };
|
|
154
|
+
let transcriptCount = 0;
|
|
155
|
+
const gradeFinal = finalText => grade(jsonOnlyExpectation, jsonOnlyDir, writeTranscript(`final-${transcriptCount++}.jsonl`, finalText));
|
|
156
|
+
assert.strictEqual(gradeFinal(RAW).passed, true, 'raw JSON final message should pass end to end');
|
|
157
|
+
assert.strictEqual(gradeFinal(`${FENCED}\n${LABEL}`).passed, true, 'fenced JSON with harness-label should pass end to end');
|
|
158
|
+
const prose = gradeFinal(`Fable on sonnect means a profile.\n${FENCED}`);
|
|
159
|
+
assert.strictEqual(prose.passed, false, 'prose before JSON must fail end to end');
|
|
160
|
+
assert.strictEqual(prose.results[0].status, 'fail');
|
|
161
|
+
assert.match(prose.results[0].reason, /prose before/);
|
|
162
|
+
// Prose from an earlier turn (before the last tool call) is not the final message.
|
|
163
|
+
const earlierProse = writeTranscript('earlier-prose.jsonl', RAW);
|
|
164
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, earlierProse).passed, true, 'only the final assistant message is graded');
|
|
165
|
+
|
|
166
|
+
// Multi-part final messages: the final turn may stream several text parts.
|
|
167
|
+
const writeParts = (name, finalParts) => {
|
|
168
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
169
|
+
const events = [
|
|
170
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-1', state: { status: 'completed', input: { file_path: 'profiles.md' } } } },
|
|
171
|
+
...finalParts.map(text => ({ type: 'text', part: { type: 'text', text } })),
|
|
172
|
+
];
|
|
173
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
174
|
+
return transcriptPath;
|
|
175
|
+
};
|
|
176
|
+
const proseThenJson = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('prose-part-json-part.jsonl', ['"Fable on sonnect" means the fable profile on sonnet.', FENCED]));
|
|
177
|
+
assert.strictEqual(proseThenJson.passed, false, 'prose part + JSON part must fail (#308 false pass)');
|
|
178
|
+
assert.match(proseThenJson.results[0].reason, /prose before/);
|
|
179
|
+
const jsonThenProse = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('json-part-prose-part.jsonl', [FENCED, 'That is the selection record.']));
|
|
180
|
+
assert.strictEqual(jsonThenProse.passed, false, 'JSON part + prose part must fail');
|
|
181
|
+
const splitBody = JSON.stringify(SELECTION, null, 2);
|
|
182
|
+
const cut = splitBody.indexOf('"assignedRole"');
|
|
183
|
+
const splitJson = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('split-json.jsonl', ['```json\n' + splitBody.slice(0, cut), splitBody.slice(cut) + '\n```']));
|
|
184
|
+
assert.strictEqual(splitJson.passed, true, `JSON split across text parts should pass: ${splitJson.results[0].reason}`);
|
|
185
|
+
|
|
186
|
+
// Transport chunks cut inside a JSON string value, a key, and the fence opener.
|
|
187
|
+
// Exact concatenation must rebuild the message; a '\n' join would break it.
|
|
188
|
+
const valueCut = RAW.indexOf('fable') + 2;
|
|
189
|
+
const keyCut = RAW.indexOf('requestedProfile') + 5;
|
|
190
|
+
const splitValueParts = [RAW.slice(0, valueCut), RAW.slice(valueCut)];
|
|
191
|
+
const splitValuePath = writeParts('split-inside-value.jsonl', splitValueParts);
|
|
192
|
+
assert.strictEqual(finalAssistantText(parseTranscriptEvents(splitValuePath).events), RAW, 'split-inside-value parts must concatenate to the exact raw JSON');
|
|
193
|
+
assert.throws(() => JSON.parse(RAW.slice(0, valueCut) + '\n' + RAW.slice(valueCut)), 'a newline join inside a JSON string must not parse (the #308 reviewer case)');
|
|
194
|
+
const POSITIVE_SPLITS = [
|
|
195
|
+
['split inside a JSON string value', splitValueParts],
|
|
196
|
+
['split inside a JSON key', [RAW.slice(0, keyCut), RAW.slice(keyCut)]],
|
|
197
|
+
['split inside the fence opener', ['```js', 'on\n' + JSON.stringify(SELECTION, null, 2) + '\n```']],
|
|
198
|
+
['fenced JSON block followed by a harness-label part', [FENCED, LABEL]],
|
|
199
|
+
];
|
|
200
|
+
POSITIVE_SPLITS.forEach(([name, parts], index) => {
|
|
201
|
+
const result = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts(`positive-split-${index}.jsonl`, parts));
|
|
202
|
+
assert.strictEqual(result.passed, true, `${name} should pass end to end: ${result.results[0].reason}`);
|
|
203
|
+
});
|
|
204
|
+
const NEGATIVE_SPLITS = [
|
|
205
|
+
['prose chunk glued to JSON start', ['Here:', RAW], /prose before/],
|
|
206
|
+
['prose after a split JSON', [RAW.slice(0, 10), RAW.slice(10), ' done'], /not a single valid JSON value/],
|
|
207
|
+
];
|
|
208
|
+
NEGATIVE_SPLITS.forEach(([name, parts, expected], index) => {
|
|
209
|
+
const result = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts(`negative-split-${index}.jsonl`, parts));
|
|
210
|
+
assert.strictEqual(result.passed, false, `${name} must fail end to end`);
|
|
211
|
+
assert.match(result.results[0].reason, expected, `${name} should give a clear reason`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Claude stream-json: assistant content parts plus a terminal `result` that
|
|
215
|
+
// repeats them. The repeat must not be double-counted, and must not mask prose.
|
|
216
|
+
const writeClaude = (name, contentTexts, resultText) => {
|
|
217
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
218
|
+
const lines = [
|
|
219
|
+
{ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: 'profiles.md' } }] } },
|
|
220
|
+
{ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: 'ok' }] } },
|
|
221
|
+
{ type: 'assistant', message: { content: contentTexts.map(text => ({ type: 'text', text })) } },
|
|
222
|
+
{ type: 'result', subtype: 'success', result: resultText },
|
|
223
|
+
];
|
|
224
|
+
fs.writeFileSync(transcriptPath, lines.map(line => JSON.stringify(line)).join('\n'));
|
|
225
|
+
return transcriptPath;
|
|
226
|
+
};
|
|
227
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, writeClaude('claude-result-repeat.jsonl', [FENCED], FENCED)).passed, true, 'Claude result repeat must not double the JSON');
|
|
228
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, writeClaude('claude-prose-parts.jsonl', ['Here is the record.', FENCED], FENCED)).passed, false, 'Claude prose content part must fail even when result holds only JSON');
|
|
229
|
+
} finally {
|
|
230
|
+
fs.rmSync(jsonOnlyDir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skill-core completion: passing project tests is artifact evidence only; the
|
|
234
|
+
// TDD case also requires an executed quality gate command.
|
|
235
|
+
const tddCase = parseSimpleYaml(fs.readFileSync(path.join(casesDir, 'baseline-tdd-workflow.yaml'), 'utf8'));
|
|
236
|
+
const qualityGate = tddCase.expectations.find(expectation => expectation.type === 'tdd_quality_gate');
|
|
237
|
+
assert.ok(qualityGate, 'baseline TDD case must require a passed TDD quality gate');
|
|
238
|
+
// The gate is graded by independently re-evaluating retained evidence, so the
|
|
239
|
+
// workspace holds a real passing evidence file and a failing variant.
|
|
240
|
+
const PASS_EVIDENCE = fs.readFileSync(path.join(__dirname, '..', 'tdd', 'fixtures', 'pass-evidence.json'), 'utf8');
|
|
241
|
+
const FAIL_EVIDENCE = (() => {
|
|
242
|
+
const evidence = JSON.parse(PASS_EVIDENCE);
|
|
243
|
+
evidence.requirements = evidence.requirements.map(requirement => ({ ...requirement, sourceConformance: 'FAIL' }));
|
|
244
|
+
return JSON.stringify(evidence);
|
|
245
|
+
})();
|
|
246
|
+
const gateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'behavioral-tdd-quality-gate-'));
|
|
247
|
+
try {
|
|
248
|
+
fs.writeFileSync(path.join(gateDir, 'evidence.json'), PASS_EVIDENCE);
|
|
249
|
+
fs.writeFileSync(path.join(gateDir, 'failing-evidence.json'), FAIL_EVIDENCE);
|
|
250
|
+
fs.writeFileSync(path.join(gateDir, 'broken-evidence.json'), '{ not json');
|
|
251
|
+
// A string command defaults to the gate's own success line when it is a
|
|
252
|
+
// quality-gate invocation, so existing positives stay meaningful. An object
|
|
253
|
+
// command states the observed output and exit code explicitly.
|
|
254
|
+
const QUALITY_GATE_COMMAND_RE = /quality-gate\.js|tdd:quality|test:tdd-quality/;
|
|
255
|
+
const gradeCommands = (name, commands) => {
|
|
256
|
+
const transcriptPath = path.join(gateDir, `${name}.jsonl`);
|
|
257
|
+
const events = commands.map((entry, index) => {
|
|
258
|
+
const { command, output, exit } = typeof entry === 'string'
|
|
259
|
+
? { command: entry, output: QUALITY_GATE_COMMAND_RE.test(entry) ? 'TDD Quality Gate: PASS' : '', exit: undefined }
|
|
260
|
+
: entry;
|
|
261
|
+
const state = { status: 'completed', input: { command } };
|
|
262
|
+
if (output !== undefined) state.output = output;
|
|
263
|
+
if (exit !== undefined) state.metadata = { exit };
|
|
264
|
+
return { type: 'tool_use', part: { type: 'tool', tool: 'Bash', callID: `${name}-${index}`, state } };
|
|
265
|
+
});
|
|
266
|
+
events.push({ type: 'text', part: { type: 'text', text: 'done' } });
|
|
267
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
268
|
+
return grade({ expectations: [qualityGate] }, gateDir, transcriptPath);
|
|
269
|
+
};
|
|
270
|
+
const testsOnly = gradeCommands('tests-only', ['npm test']);
|
|
271
|
+
assert.strictEqual(testsOnly.passed, false, 'passing project tests without the quality gate must fail the TDD case');
|
|
272
|
+
assert.strictEqual(testsOnly.results[0].status, 'fail', 'missing quality gate must be a definitive fail, not inconclusive evidence');
|
|
273
|
+
assert.strictEqual(gradeCommands('npm-gate', ['npm test', 'npm run tdd:quality -- evidence.json']).passed, true, 'npm run tdd:quality with evidence must satisfy the gate');
|
|
274
|
+
assert.strictEqual(gradeCommands('node-gate', ['npm test', 'node tdd/scripts/quality-gate.js evidence.json']).passed, true, 'node quality-gate.js with evidence must satisfy the gate');
|
|
275
|
+
assert.strictEqual(gradeCommands('installed-skill-gate', ['npm test', 'node .claude/skills/tdd/scripts/quality-gate.js evidence.json']).passed, true, 'quality gate under the installed skill dir must satisfy the gate');
|
|
276
|
+
assert.strictEqual(gradeCommands('absolute-gate', ['npm test', 'node /tmp/ws/.harness/tdd/scripts/quality-gate.js evidence.json']).passed, true, 'absolute quality-gate path must satisfy the gate');
|
|
277
|
+
for (const [name, commands] of [
|
|
278
|
+
['echo-mention', ['echo npm run tdd:quality']],
|
|
279
|
+
['compound-gate', ['npm run tdd:quality -- evidence.json && rm -rf build']],
|
|
280
|
+
['similar-name', ['npm run tdd:quality-notes']],
|
|
281
|
+
]) {
|
|
282
|
+
assert.strictEqual(gradeCommands(name, commands).passed, false, `${name} must not count as an executed quality gate`);
|
|
283
|
+
}
|
|
284
|
+
// A completed tool call is not completion evidence: the gate outcome is
|
|
285
|
+
// re-evaluated from the retained evidence, independent of transcript claims.
|
|
286
|
+
const gateFailures = [
|
|
287
|
+
['nonzero-exit', ['npm test', 'npm run tdd:quality -- failing-evidence.json'], /re-evaluation of failing-evidence\.json is FAIL/],
|
|
288
|
+
['missing-evidence', ['npm test', 'npm run tdd:quality -- absent.json'], /not retained/],
|
|
289
|
+
['invalid-evidence', ['npm test', 'node tdd/scripts/quality-gate.js broken-evidence.json'], /not valid JSON/],
|
|
290
|
+
['no-evidence-arg', ['npm test', 'npm run tdd:quality'], /without an evidence file/],
|
|
291
|
+
['self-test-alias', ['npm test', 'npm run test:tdd-quality'], /repository self-test/],
|
|
292
|
+
['escaping-evidence', ['npm test', 'npm run tdd:quality -- ../evidence.json'], /outside the workspace/],
|
|
293
|
+
['missing-output-report', ['npm test', 'npm run tdd:quality -- evidence.json --output reports/quality.json'], /quality report reports\/quality\.json was not retained/],
|
|
294
|
+
['last-run-counts', ['npm run tdd:quality -- evidence.json', 'npm run tdd:quality -- failing-evidence.json'], /FAIL/],
|
|
295
|
+
];
|
|
296
|
+
for (const [name, commands, reason] of gateFailures) {
|
|
297
|
+
const graded = gradeCommands(name, commands);
|
|
298
|
+
assert.strictEqual(graded.passed, false, `${name} must not satisfy the quality gate`);
|
|
299
|
+
assert.strictEqual(graded.results[0].status, 'fail', `${name} must be a definitive fail`);
|
|
300
|
+
assert.match(graded.results[0].reason, reason, `${name} should explain why`);
|
|
301
|
+
}
|
|
302
|
+
// OpenCode reports state.status "completed" even when the gate exits 1.
|
|
303
|
+
const opencodeExit1 = path.join(gateDir, 'opencode-exit1.jsonl');
|
|
304
|
+
fs.writeFileSync(opencodeExit1, [
|
|
305
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'bash', callID: 'gate', state: { status: 'completed', input: { command: 'npm run tdd:quality -- failing-evidence.json' }, output: 'TDD Quality Gate: FAIL\nexit code 1' } } },
|
|
306
|
+
{ type: 'text', part: { type: 'text', text: 'Quality gate done.' } },
|
|
307
|
+
].map(event => JSON.stringify(event)).join('\n'));
|
|
308
|
+
assert.strictEqual(grade({ expectations: [qualityGate] }, gateDir, opencodeExit1).passed, false, 'completed OpenCode event with exit 1 must fail');
|
|
309
|
+
// Fixing evidence and re-running is allowed: the last completed run counts.
|
|
310
|
+
assert.strictEqual(gradeCommands('rerun-after-fix', ['npm run tdd:quality -- failing-evidence.json', 'npm run tdd:quality -- evidence.json']).passed, true, 'a passing re-run after a failing run should satisfy the gate');
|
|
311
|
+
// Observed command outcome: the transcript must show the gate's own success
|
|
312
|
+
// line and no error or non-zero exit before the evidence is trusted.
|
|
313
|
+
const { evaluateEvidence } = require('../tdd/scripts/quality-gate.js');
|
|
314
|
+
const observedGate = 'npm run tdd:quality -- evidence.json';
|
|
315
|
+
const observedFailures = [
|
|
316
|
+
['failed-command-pass-evidence', { command: observedGate, output: 'npm ERR! Missing script: "tdd:quality"', exit: 1 }, /quality gate command failed/],
|
|
317
|
+
['nonzero-exit-no-error-text', { command: observedGate, output: 'TDD Quality Gate: PASS', exit: 1 }, /exited with code 1/],
|
|
318
|
+
['gate-printed-fail-evidence-now-pass', { command: observedGate, output: 'TDD Quality Gate: FAIL' }, /printed TDD Quality Gate: FAIL/],
|
|
319
|
+
];
|
|
320
|
+
for (const [name, command, reason] of observedFailures) {
|
|
321
|
+
const graded = gradeCommands(name, [command]);
|
|
322
|
+
assert.strictEqual(graded.passed, false, `${name} must not satisfy the quality gate`);
|
|
323
|
+
assert.strictEqual(graded.results[0].status, 'fail', `${name} must be a definitive fail`);
|
|
324
|
+
assert.match(graded.results[0].reason, reason, `${name} should explain why`);
|
|
325
|
+
}
|
|
326
|
+
const unobservable = gradeCommands('unobservable-outcome', [{ command: observedGate, output: '' }]);
|
|
327
|
+
assert.strictEqual(unobservable.results[0].status, 'inconclusive', 'a gate run with no observable output or exit code must be inconclusive');
|
|
328
|
+
assert.strictEqual(unobservable.passed, false, 'an inconclusive gate run must not pass');
|
|
329
|
+
assert.match(unobservable.results[0].reason, /not observable in the transcript/, 'inconclusive reason should say the outcome is not observable');
|
|
330
|
+
// Definitive workspace failures win over an unobservable outcome.
|
|
331
|
+
const missingNoOutput = gradeCommands('missing-evidence-no-output', [{ command: 'npm run tdd:quality -- nope.json', output: '' }]);
|
|
332
|
+
assert.strictEqual(missingNoOutput.results[0].status, 'fail', 'missing evidence must be a definitive fail even with no observable output');
|
|
333
|
+
assert.match(missingNoOutput.results[0].reason, /not retained/, 'missing evidence should be named');
|
|
334
|
+
// Only the error signal is line-anchored: prose mentioning npm errors passes,
|
|
335
|
+
// while a real npm error line still fails.
|
|
336
|
+
assert.strictEqual(gradeCommands('prose-npm-error', [{ command: observedGate, output: 'TDD Quality Gate: PASS\n- note: avoid npm error handling', exit: 0 }]).passed, true, 'prose mentioning npm error must not fail a passing gate');
|
|
337
|
+
const npmErrorLine = gradeCommands('npm-error-line', [{ command: observedGate, output: 'npm error Missing script: "tdd:quality"' }]);
|
|
338
|
+
assert.strictEqual(npmErrorLine.results[0].status, 'fail', 'an npm error line must be a definitive fail');
|
|
339
|
+
assert.match(npmErrorLine.results[0].reason, /npm error/, 'npm error line should be named');
|
|
340
|
+
// npm ERR!/error lines after a PASS line must fail even without exit metadata;
|
|
341
|
+
// the boundary after the token must match whitespace or end of line.
|
|
342
|
+
const npmTrailer = (name, output) => gradeCommands(name, [{ command: observedGate, output }]);
|
|
343
|
+
for (const [name, output] of [
|
|
344
|
+
['npm-err-bang-eacces', 'TDD Quality Gate: PASS\nnpm ERR! code EACCES'],
|
|
345
|
+
['npm-err-bang-eresolve', 'TDD Quality Gate: PASS\nnpm ERR! code ERESOLVE'],
|
|
346
|
+
['npm-error-elifecycle', 'TDD Quality Gate: PASS\nnpm error code ELIFECYCLE'],
|
|
347
|
+
['npm-err-bang-eol', 'TDD Quality Gate: PASS\nnpm ERR!'],
|
|
348
|
+
]) {
|
|
349
|
+
const graded = npmTrailer(name, output);
|
|
350
|
+
assert.strictEqual(graded.results[0].status, 'fail', `${name} must be a definitive fail`);
|
|
351
|
+
assert.strictEqual(graded.passed, false, `${name} must not satisfy the quality gate`);
|
|
352
|
+
assert.match(graded.results[0].reason, /npm error/i, `${name} should name the npm error`);
|
|
353
|
+
}
|
|
354
|
+
assert.strictEqual(npmTrailer('npm-errors-word', 'TDD Quality Gate: PASS\nnpm ERRORS are documented').passed, true, 'a word that only starts with ERR must not be an npm error');
|
|
355
|
+
const { GATE_COMMAND_ERROR_RE } = require('../behavioral-evals/run');
|
|
356
|
+
assert.strictEqual(GATE_COMMAND_ERROR_RE.test('npm ERR! code EACCES'), true, 'npm ERR! line must match the error regex');
|
|
357
|
+
assert.strictEqual(GATE_COMMAND_ERROR_RE.test('npm ERRORS'), false, 'npm ERRORS must not match the error regex');
|
|
358
|
+
|
|
359
|
+
// Claude stream-json: is_error on the tool_result is the host's failure
|
|
360
|
+
// signal; the call is recorded as failed, so it never counts as executed.
|
|
361
|
+
const claudeGate = (name, toolResult) => {
|
|
362
|
+
const transcriptPath = path.join(gateDir, `${name}.jsonl`);
|
|
363
|
+
fs.writeFileSync(transcriptPath, [
|
|
364
|
+
{ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'bash-1', name: 'Bash', input: { command: observedGate } }] } },
|
|
365
|
+
{ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'bash-1', ...toolResult }] } },
|
|
366
|
+
{ type: 'result', subtype: 'success', result: 'done' },
|
|
367
|
+
].map(event => JSON.stringify(event)).join('\n'));
|
|
368
|
+
return grade({ expectations: [qualityGate] }, gateDir, transcriptPath);
|
|
369
|
+
};
|
|
370
|
+
const claudeError = claudeGate('claude-error', { is_error: true, content: 'npm ERR! Missing script' });
|
|
371
|
+
assert.strictEqual(claudeError.passed, false, 'Claude tool_result with is_error must not satisfy the gate');
|
|
372
|
+
assert.strictEqual(claudeError.results[0].status, 'fail', 'Claude tool_result error must be a definitive fail');
|
|
373
|
+
assert.match(claudeError.results[0].reason, /last quality gate run \(bash-1\) failed/, 'Claude errored gate call should be the failing last run');
|
|
374
|
+
assert.strictEqual(claudeGate('claude-success', { is_error: false, content: [{ type: 'text', text: 'TDD Quality Gate: PASS' }] }).passed, true, 'Claude tool_result carrying the gate success line should pass');
|
|
375
|
+
// Runs are graded in transcript order: the last matching attempt decides, so an
|
|
376
|
+
// earlier PASS never hides a later failed or unresolved run.
|
|
377
|
+
const claudeRuns = (name, runs) => {
|
|
378
|
+
const transcriptPath = path.join(gateDir, `${name}.jsonl`);
|
|
379
|
+
const events = [];
|
|
380
|
+
runs.forEach(({ command, result }, index) => {
|
|
381
|
+
const id = `${name}-bash-${index}`;
|
|
382
|
+
events.push({ type: 'assistant', message: { content: [{ type: 'tool_use', id, name: 'Bash', input: { command } }] } });
|
|
383
|
+
if (result) events.push({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: id, ...result }] } });
|
|
384
|
+
});
|
|
385
|
+
events.push({ type: 'assistant', message: { content: [{ type: 'text', text: 'done' }] } });
|
|
386
|
+
events.push({ type: 'result', subtype: 'success', result: 'done' });
|
|
387
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
388
|
+
return grade({ expectations: [qualityGate] }, gateDir, transcriptPath);
|
|
389
|
+
};
|
|
390
|
+
const gatePass = { is_error: false, content: [{ type: 'text', text: 'TDD Quality Gate: PASS' }] };
|
|
391
|
+
const gateFail = { is_error: true, content: 'TDD Quality Gate: FAIL' };
|
|
392
|
+
const lastRunError = claudeRuns('claude-pass-then-error', [
|
|
393
|
+
{ command: observedGate, result: gatePass },
|
|
394
|
+
{ command: observedGate, result: gateFail },
|
|
395
|
+
]);
|
|
396
|
+
assert.strictEqual(lastRunError.results[0].status, 'fail', 'a failed last run must be a definitive fail even after an earlier PASS');
|
|
397
|
+
assert.strictEqual(lastRunError.passed, false, 'an earlier PASS must not satisfy the gate after a failed last run');
|
|
398
|
+
assert.match(lastRunError.results[0].reason, /last quality gate run/, 'the failed last run should be named');
|
|
399
|
+
const lastRunUnresolved = claudeRuns('claude-pass-then-unresolved', [
|
|
400
|
+
{ command: observedGate, result: gatePass },
|
|
401
|
+
{ command: observedGate },
|
|
402
|
+
]);
|
|
403
|
+
assert.strictEqual(lastRunUnresolved.results[0].status, 'inconclusive', 'an unresolved last run must be inconclusive, not a pass');
|
|
404
|
+
assert.strictEqual(lastRunUnresolved.passed, false, 'an unresolved last run must not pass the gate');
|
|
405
|
+
assert.match(lastRunUnresolved.results[0].reason, /no observable result/, 'the unresolved last run should be named');
|
|
406
|
+
const otherEvidenceError = claudeRuns('claude-pass-then-other-evidence', [
|
|
407
|
+
{ command: observedGate, result: gatePass },
|
|
408
|
+
{ command: 'node tdd/scripts/quality-gate.js failing-evidence.json', result: gateFail },
|
|
409
|
+
]);
|
|
410
|
+
assert.strictEqual(otherEvidenceError.results[0].status, 'fail', 'a failed run against other evidence must fail the gate');
|
|
411
|
+
assert.match(otherEvidenceError.results[0].reason, /last quality gate run/, 'the failed last run against other evidence should be named');
|
|
412
|
+
assert.strictEqual(claudeRuns('claude-fail-then-retry', [
|
|
413
|
+
{ command: observedGate, result: gateFail },
|
|
414
|
+
{ command: observedGate, result: gatePass },
|
|
415
|
+
]).passed, true, 'a failed run followed by a successful retry must pass');
|
|
416
|
+
// Retained --output report: a full report must be present and must equal the
|
|
417
|
+
// fresh re-evaluation over every top-level key.
|
|
418
|
+
const realReport = evaluateEvidence(JSON.parse(PASS_EVIDENCE));
|
|
419
|
+
assert.strictEqual(realReport.result, 'PASS', 'the pass fixture must re-evaluate to PASS');
|
|
420
|
+
fs.mkdirSync(path.join(gateDir, 'reports'), { recursive: true });
|
|
421
|
+
const reportPath = path.join(gateDir, 'reports', 'quality.json');
|
|
422
|
+
fs.writeFileSync(reportPath, JSON.stringify({ result: 'PASS' }));
|
|
423
|
+
const markerOnly = gradeCommands('marker-output-report', ['npm run tdd:quality -- evidence.json --output reports/quality.json']);
|
|
424
|
+
assert.strictEqual(markerOnly.passed, false, 'a {"result":"PASS"} marker must not satisfy a retained report');
|
|
425
|
+
assert.match(markerOnly.results[0].reason, /missing required keys: .*requirements/, 'a marker-only retained report should name the missing keys');
|
|
426
|
+
fs.writeFileSync(reportPath, JSON.stringify(realReport, null, 2));
|
|
427
|
+
assert.strictEqual(gradeCommands('retained-output-report', ['npm run tdd:quality -- evidence.json --output reports/quality.json']).passed, true, 'retained --output report equal to the re-evaluation should pass');
|
|
428
|
+
fs.writeFileSync(reportPath, JSON.stringify({ ...realReport, score: realReport.score - 1 }, null, 2));
|
|
429
|
+
const staleScore = gradeCommands('stale-output-report', ['npm run tdd:quality -- evidence.json --output reports/quality.json']);
|
|
430
|
+
assert.strictEqual(staleScore.passed, false, 'a retained report with a stale score must fail');
|
|
431
|
+
assert.match(staleScore.results[0].reason, /differs from re-evaluation at "score"/, 'a stale retained report should name the differing key');
|
|
432
|
+
// Without --output the gate prints its report to stdout; that printed report
|
|
433
|
+
// must equal the fresh re-evaluation of the evidence the command names.
|
|
434
|
+
const printedText = report => `TDD Quality Gate: PASS\n${JSON.stringify(report, null, 2)}`;
|
|
435
|
+
assert.strictEqual(gradeCommands('stdout-report-equal', [{ command: observedGate, output: printedText(realReport) }]).passed, true, 'a printed report equal to the re-evaluation should pass');
|
|
436
|
+
const otherEvidence = JSON.parse(PASS_EVIDENCE);
|
|
437
|
+
otherEvidence.requirements[0].requirementId = 'REQ-OTHER';
|
|
438
|
+
fs.writeFileSync(path.join(gateDir, 'other-evidence.json'), JSON.stringify(otherEvidence));
|
|
439
|
+
const otherReport = evaluateEvidence(otherEvidence);
|
|
440
|
+
const stdoutMismatch = gradeCommands('stdout-report-other', [{ command: observedGate, output: printedText(otherReport) }]);
|
|
441
|
+
assert.strictEqual(stdoutMismatch.passed, false, 'a printed report from different evidence must fail');
|
|
442
|
+
assert.match(stdoutMismatch.results[0].reason, /gate output report differs from re-evaluation at "[a-zA-Z]+"/, 'a mismatched printed report should name the differing key');
|
|
443
|
+
} finally {
|
|
444
|
+
fs.rmSync(gateDir, { recursive: true, force: true });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
console.log(`Behavioral case validation passed (${fs.readdirSync(casesDir).filter(name => name.endsWith('.yaml')).length} cases + negative controls + ordered execution evidence + json-only final output + tdd quality gate).`);
|
package/code-review/SKILL.md
CHANGED
|
@@ -19,6 +19,15 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
|
|
|
19
19
|
- [ ] Follow each important diagnostic/fact through every consumer and ancestor. Check whether required facts are computed before derived authority, indexes, or output.
|
|
20
20
|
- [ ] Compare new rules with previously accepted rules and controls. Separate display severity from safety prerequisites.
|
|
21
21
|
|
|
22
|
+
## Check evidence a gate trusts
|
|
23
|
+
|
|
24
|
+
Apply these when the change grades, gates or labels evidence. Authors SHOULD run them as a self-review before push.
|
|
25
|
+
|
|
26
|
+
- [ ] **Claimed vs observed.** Separate what the subject reports about itself (a tool's `completed` status, a result marker, an agent-supplied expected value, a status label) from what the checker observes independently (exit code, output, a recomputed result, the quoted source). A self-report alone never passes; when nothing independent is observable, the result is inconclusive.
|
|
27
|
+
- [ ] **Select, then judge.** When the last, first or any item of a sequence decides (last run, final message, latest attempt), select from every attempt in order before filtering by outcome. Test both orders: failure then success, and success then failure or no result.
|
|
28
|
+
- [ ] **One witness per rule.** Give each detection rule a negative case that triggers only that rule; a case that also trips an exit code or another pattern cannot show the rule works. Probe positive cases at structural boundaries (inside a token, string or quote), not only at safe separators.
|
|
29
|
+
- [ ] **Comparable subjects, honest labels.** A comparison or conflict needs both sides to describe the same target. A status name claims only what was checked: a consistent quote is not a validated diagnosis.
|
|
30
|
+
|
|
22
31
|
## Check tests and output
|
|
23
32
|
|
|
24
33
|
- [ ] Ask whether each test still passes if the behavior is disabled, bypassed, or broken (mutation thought experiment). Confirm the test is included in CI.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.47.
|
|
4
|
+
"version": "0.47.5",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -19,6 +19,15 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
|
|
|
19
19
|
- [ ] Follow each important diagnostic/fact through every consumer and ancestor. Check whether required facts are computed before derived authority, indexes, or output.
|
|
20
20
|
- [ ] Compare new rules with previously accepted rules and controls. Separate display severity from safety prerequisites.
|
|
21
21
|
|
|
22
|
+
## Check evidence a gate trusts
|
|
23
|
+
|
|
24
|
+
Apply these when the change grades, gates or labels evidence. Authors SHOULD run them as a self-review before push.
|
|
25
|
+
|
|
26
|
+
- [ ] **Claimed vs observed.** Separate what the subject reports about itself (a tool's `completed` status, a result marker, an agent-supplied expected value, a status label) from what the checker observes independently (exit code, output, a recomputed result, the quoted source). A self-report alone never passes; when nothing independent is observable, the result is inconclusive.
|
|
27
|
+
- [ ] **Select, then judge.** When the last, first or any item of a sequence decides (last run, final message, latest attempt), select from every attempt in order before filtering by outcome. Test both orders: failure then success, and success then failure or no result.
|
|
28
|
+
- [ ] **One witness per rule.** Give each detection rule a negative case that triggers only that rule; a case that also trips an exit code or another pattern cannot show the rule works. Probe positive cases at structural boundaries (inside a token, string or quote), not only at safe separators.
|
|
29
|
+
- [ ] **Comparable subjects, honest labels.** A comparison or conflict needs both sides to describe the same target. A status name claims only what was checked: a consistent quote is not a validated diagnosis.
|
|
30
|
+
|
|
22
31
|
## Check tests and output
|
|
23
32
|
|
|
24
33
|
- [ ] Ask whether each test still passes if the behavior is disabled, bypassed, or broken (mutation thought experiment). Confirm the test is included in CI.
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
{ "name": "weighted integration failure", "mutation": "the 0.15 positive dimension scores zero", "expectedScore": 85, "expectedResult": "FAIL" },
|
|
9
9
|
{ "name": "weighted unit failure", "mutation": "unit determinism is N/A and the 0.15 positive dimension scores zero", "expectedScore": 83.33, "expectedResult": "FAIL" },
|
|
10
10
|
{ "name": "NON_CONFORMANT", "mutation": "sourceConformance = NON_CONFORMANT", "expectedResult": "FAIL" },
|
|
11
|
-
{ "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT and
|
|
12
|
-
{ "name": "
|
|
11
|
+
{ "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT, sourceNotes is present, and sourceEvidence quotes the defective assertion against the observed value; sourceEvidenceStatus is REVIEWER_REQUIRED, never VERIFIED, because a single-source mismatch does not prove the source is wrong", "expectedResult": "PASS" },
|
|
12
|
+
{ "name": "SOURCE_DEFECT without sourceEvidence", "mutation": "sourceValidity = SOURCE_DEFECT with sourceNotes but no sourceEvidence", "expectedResult": "FAIL" },
|
|
13
|
+
{ "name": "SOURCE_CONFLICT", "mutation": "sourceValidity = SOURCE_CONFLICT with a traced additional source and sourceEvidence quoting both sources", "expectedResult": "FAIL" },
|
|
14
|
+
{ "name": "SOURCE_CONFLICT without sourceEvidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources but no sourceEvidence", "expectedResult": "FAIL" },
|
|
15
|
+
{ "name": "SOURCE_CONFLICT node-versus-value", "mutation": "sourceValidity = SOURCE_CONFLICT citing only expect(list.head.value).toEqual(1) from one spec, observed value is an Element object", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
16
|
+
{ "name": "SOURCE_CONFLICT agreeing expectations", "mutation": "sourceValidity = SOURCE_CONFLICT with two sources expecting the same value and a failing observed value; the mismatch is implementation behaviour", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
17
|
+
{ "name": "SOURCE_CONFLICT two-source evidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting disagreeing expectations for the same subject list.head and a failing observed value; one quote (toBeInstanceOf) is not mechanically checkable, so sourceEvidenceStatus is REVIEWER_REQUIRED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
|
|
18
|
+
{ "name": "SOURCE_CONFLICT relabelled expected", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting the identical assertion expect(list.head).toEqual(1) but expected values 1 and 3; each expected must match its quoted literal, so the second entry is rejected", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
19
|
+
{ "name": "SOURCE_CONFLICT verified literal disagreement", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting expect(list.head.value).toBe(1) and expect(list.head.value).toBe('1') with matching expected values 1 and '1' and a failing observed value; sourceEvidenceStatus is VERIFIED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
|
|
20
|
+
{ "name": "SOURCE_CONFLICT unrelated assertions", "mutation": "sourceValidity = SOURCE_CONFLICT quoting expect(foo).toEqual(1) from one source and expect(bar).toEqual(3) from another; unequal expectations about different subjects", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
13
21
|
{ "name": "malformed input", "mutation": "requirements is not an array", "expectedResult": "FAIL" },
|
|
14
22
|
{ "name": "unexpected input", "mutation": "one unexpectedInput check is FAIL", "expectedResult": "FAIL" },
|
|
15
23
|
{ "name": "error handling", "mutation": "one errorHandling check is SKIPPED", "expectedResult": "FAIL" },
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This model turns each source requirement into auditable evidence. The authoritative source remains the contract even when it appears wrong. Record a suspected documentation problem as `SOURCE_DEFECT`; record disagreement between traced sources as `SOURCE_CONFLICT`. Never reinterpret either into a passing implementation. If no authoritative source exists, use `INFERRED` and state the inference basis.
|
|
4
4
|
|
|
5
|
+
Before declaring either outcome, `sourceEvidence` quotes the exact conflicting assertions, names each declared source by path, and compares each `expected` value with the `observed` value from the run. A `SOURCE_CONFLICT` needs at least two distinct sources whose expected values disagree about the same `subject` — the contract target each quoted assertion constrains, written as it appears in the quote (`list.head` is not `list.head.value`). Unequal expectations about unrelated targets are not a conflict. If the quoted expectations agree and the run still fails, the mismatch is in the implementation: record `NON_CONFORMANT`, not `SOURCE_CONFLICT`. Each `expected` must match a mechanically readable literal in the quoted assertion (`toEqual`, `toStrictEqual`, `toBe`, or `assert.*` with a JSON or single-quoted literal); otherwise the claim is marked `REVIEWER_REQUIRED` and the gate does not treat it as an established disagreement. `VERIFIED` applies only to a `SOURCE_CONFLICT` whose two quoted literals disagree. A `SOURCE_DEFECT` is always `REVIEWER_REQUIRED`, because a mismatch with the observed value does not prove the source is wrong; the implementation may be the one at fault.
|
|
6
|
+
|
|
5
7
|
## Profile contract
|
|
6
8
|
|
|
7
9
|
Every requirement declares exactly one `testProfile`: `unit` or `integration`. The selected profile defines which case classes are applicable; the common scoring and source rules do not make every test use identical evidence.
|
|
@@ -149,6 +149,18 @@
|
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
},
|
|
152
|
+
"sourceEvidence": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"additionalProperties": false,
|
|
155
|
+
"required": ["source", "assertion", "expected", "observed"],
|
|
156
|
+
"properties": {
|
|
157
|
+
"source": { "type": "string", "minLength": 1 },
|
|
158
|
+
"subject": { "type": "string", "minLength": 1 },
|
|
159
|
+
"assertion": { "type": "string", "minLength": 1 },
|
|
160
|
+
"expected": {},
|
|
161
|
+
"observed": {}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
152
164
|
"requirement": {
|
|
153
165
|
"type": "object",
|
|
154
166
|
"additionalProperties": false,
|
|
@@ -161,6 +173,7 @@
|
|
|
161
173
|
"sourceConformance": { "enum": ["PASS", "NON_CONFORMANT"] },
|
|
162
174
|
"sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT"] },
|
|
163
175
|
"sourceNotes": { "type": "string", "minLength": 1 },
|
|
176
|
+
"sourceEvidence": { "type": "array", "items": { "$ref": "#/$defs/sourceEvidence" } },
|
|
164
177
|
"skippedTests": { "type": "array" },
|
|
165
178
|
"flakyTests": { "type": "array" },
|
|
166
179
|
"naDeclarations": { "type": "array" },
|
|
@@ -186,6 +199,14 @@
|
|
|
186
199
|
}
|
|
187
200
|
},
|
|
188
201
|
"allOf": [
|
|
202
|
+
{
|
|
203
|
+
"if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "enum": ["SOURCE_DEFECT", "SOURCE_CONFLICT"] } } },
|
|
204
|
+
"then": { "required": ["sourceEvidence"], "properties": { "sourceEvidence": { "minItems": 1 } } }
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "const": "SOURCE_CONFLICT" } } },
|
|
208
|
+
"then": { "properties": { "sourceEvidence": { "items": { "required": ["subject"] } } } }
|
|
209
|
+
},
|
|
189
210
|
{
|
|
190
211
|
"if": {
|
|
191
212
|
"required": ["testProfile"],
|
|
@@ -96,6 +96,8 @@
|
|
|
96
96
|
"sourceConformance": { "enum": ["PASS", "NON_CONFORMANT", null] },
|
|
97
97
|
"sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT", null] },
|
|
98
98
|
"sourceNotes": { "type": "string" },
|
|
99
|
+
"sourceEvidence": { "type": "array" },
|
|
100
|
+
"sourceEvidenceStatus": { "enum": ["VERIFIED", "REVIEWER_REQUIRED"], "description": "VERIFIED only for a SOURCE_CONFLICT whose two quoted literals disagree; SOURCE_DEFECT is always REVIEWER_REQUIRED." },
|
|
99
101
|
"dimensions": { "$ref": "#/$defs/dimensions" },
|
|
100
102
|
"skippedTests": { "type": "array" },
|
|
101
103
|
"flakyTests": { "type": "array" },
|