@dzhechkov/harness-core 0.8.2 → 0.8.6

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 (81) hide show
  1. package/.dz-manifest.json +146 -58
  2. package/README.md +72 -2
  3. package/dist/cmd-usage.d.ts +148 -0
  4. package/dist/cmd-usage.d.ts.map +1 -0
  5. package/dist/cmd-usage.js +548 -0
  6. package/dist/cmd-usage.js.map +1 -0
  7. package/dist/compounding.d.ts +4 -0
  8. package/dist/compounding.d.ts.map +1 -1
  9. package/dist/compounding.js +6 -0
  10. package/dist/compounding.js.map +1 -1
  11. package/dist/contract-checklist.d.ts +123 -0
  12. package/dist/contract-checklist.d.ts.map +1 -0
  13. package/dist/contract-checklist.js +700 -0
  14. package/dist/contract-checklist.js.map +1 -0
  15. package/dist/feature-adr-checkpoints.d.ts +11 -2
  16. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  17. package/dist/feature-adr-checkpoints.js +37 -2
  18. package/dist/feature-adr-checkpoints.js.map +1 -1
  19. package/dist/feature-adr-routing.d.ts +58 -23
  20. package/dist/feature-adr-routing.d.ts.map +1 -1
  21. package/dist/feature-adr-routing.js +208 -59
  22. package/dist/feature-adr-routing.js.map +1 -1
  23. package/dist/guard.d.ts +25 -0
  24. package/dist/guard.d.ts.map +1 -1
  25. package/dist/guard.js +59 -1
  26. package/dist/guard.js.map +1 -1
  27. package/dist/index.d.ts +9 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/loop-blobs.generated.js +8 -8
  32. package/dist/loop-blobs.generated.js.map +1 -1
  33. package/dist/loop-plan.d.ts +13 -1
  34. package/dist/loop-plan.d.ts.map +1 -1
  35. package/dist/loop-plan.js +15 -1
  36. package/dist/loop-plan.js.map +1 -1
  37. package/dist/loop-render.d.ts.map +1 -1
  38. package/dist/loop-render.js +51 -6
  39. package/dist/loop-render.js.map +1 -1
  40. package/dist/loop-trace.d.ts +20 -1
  41. package/dist/loop-trace.d.ts.map +1 -1
  42. package/dist/loop-trace.js +83 -1
  43. package/dist/loop-trace.js.map +1 -1
  44. package/dist/model-recommender.d.ts +8 -0
  45. package/dist/model-recommender.d.ts.map +1 -1
  46. package/dist/model-recommender.js +31 -4
  47. package/dist/model-recommender.js.map +1 -1
  48. package/dist/qe-bridge.d.ts.map +1 -1
  49. package/dist/qe-bridge.js +9 -0
  50. package/dist/qe-bridge.js.map +1 -1
  51. package/dist/restart-advisor.d.ts +103 -0
  52. package/dist/restart-advisor.d.ts.map +1 -0
  53. package/dist/restart-advisor.js +445 -0
  54. package/dist/restart-advisor.js.map +1 -0
  55. package/dist/slop-lint.d.ts +128 -0
  56. package/dist/slop-lint.d.ts.map +1 -0
  57. package/dist/slop-lint.js +607 -0
  58. package/dist/slop-lint.js.map +1 -0
  59. package/dist/workflow-run.d.ts.map +1 -1
  60. package/dist/workflow-run.js +18 -12
  61. package/dist/workflow-run.js.map +1 -1
  62. package/package.json +19 -15
  63. package/sbom.json +277 -57
  64. package/src/cmd-usage.ts +720 -0
  65. package/src/compounding.ts +13 -0
  66. package/src/contract-checklist.ts +973 -0
  67. package/src/deadwood-allowlist.json +80 -0
  68. package/src/feature-adr-checkpoints.ts +38 -2
  69. package/src/feature-adr-routing.ts +238 -55
  70. package/src/guard.ts +79 -1
  71. package/src/index.ts +81 -1
  72. package/src/loop-blobs.generated.ts +8 -8
  73. package/src/loop-plan.ts +36 -3
  74. package/src/loop-render.ts +50 -6
  75. package/src/loop-trace.ts +91 -2
  76. package/src/model-recommender.ts +35 -4
  77. package/src/qe-bridge.ts +9 -0
  78. package/src/restart-advisor.ts +579 -0
  79. package/src/slop-lint.ts +762 -0
  80. package/src/slop-markers.json +71 -0
  81. package/src/workflow-run.ts +18 -11
@@ -0,0 +1,700 @@
1
+ /**
2
+ * Pure contract-checklist policy for feature-ADR artifacts.
3
+ *
4
+ * Markdown/JSON text and injected evidence reads go in; typed decisions come out. Filesystem
5
+ * discovery, realpath confinement, rendering to a terminal, and process exits belong to the CLI.
6
+ */
7
+ export const CONTRACT_CHECKLIST_SCHEMA = 'contract-checklist/1';
8
+ export const CONTRACT_VERDICT_SCHEMA = 'contract-checklist-verdict/1';
9
+ const REQUIREMENTS_HEADING = '## Acceptance criteria';
10
+ const CONFIRMATION_HEADING = '## Confirmation';
11
+ const VERDICT_HEADING = '## Contract checklist';
12
+ const REQUIREMENTS_FORMAT_LINE = 'Format: Every acceptance criterion below is exactly one physical line matching `^AC-([1-9][0-9]*): (\\S.*)$`; identifiers are contiguous from `AC-1`, and only the literal H2 `## Acceptance criteria` establishes this source section.';
13
+ const ADR_BASENAME = /^([0-9]{3})-[a-z][a-z0-9]*(?:-[a-z0-9]+)*\.md$/;
14
+ const ACCEPTANCE_ROW = /^AC-([1-9][0-9]*): (\S.*)$/;
15
+ const LOAD_PROPERTY = /^- Load-bearing property:(?: (.*))?$/;
16
+ const REQUIRED_CHECK = /^- Required automated check:(?: (.*))?$/;
17
+ function linesOf(text) {
18
+ return text.replace(/\r\n?/g, '\n').split('\n');
19
+ }
20
+ function h2Indexes(lines, heading) {
21
+ const indexes = [];
22
+ for (let index = 0; index < lines.length; index++) {
23
+ if (lines[index] === heading)
24
+ indexes.push(index);
25
+ }
26
+ return indexes;
27
+ }
28
+ function h2End(lines, start) {
29
+ for (let index = start + 1; index < lines.length; index++) {
30
+ if (/^## (?!#)\S/.test(lines[index] ?? ''))
31
+ return index;
32
+ }
33
+ return lines.length;
34
+ }
35
+ function diagnostic(code, message, fields = {}) {
36
+ return { code, message, ...fields };
37
+ }
38
+ function acceptanceItems(artifact, diagnostics) {
39
+ const lines = linesOf(artifact.text);
40
+ const headings = h2Indexes(lines, REQUIREMENTS_HEADING);
41
+ if (headings.length !== 1) {
42
+ diagnostics.push(diagnostic(headings.length === 0 ? 'requirements-section-missing' : 'requirements-section-duplicate', `expected exactly one ${REQUIREMENTS_HEADING} section; found ${headings.length}`, { artifact: artifact.path, section: REQUIREMENTS_HEADING, observed: headings.length }));
43
+ return [];
44
+ }
45
+ const start = headings[0];
46
+ const end = h2End(lines, start);
47
+ const out = [];
48
+ for (let index = start + 1; index < end; index++) {
49
+ const line = lines[index] ?? '';
50
+ if (line.trim() === '' || line === REQUIREMENTS_FORMAT_LINE)
51
+ continue;
52
+ const match = ACCEPTANCE_ROW.exec(line);
53
+ if (!match) {
54
+ diagnostics.push(diagnostic('acceptance-row-malformed', `non-canonical acceptance content at line ${index + 1}`, { artifact: artifact.path, section: REQUIREMENTS_HEADING, observed: line }));
55
+ continue;
56
+ }
57
+ const number = Number(match[1]);
58
+ out.push({
59
+ sourceId: `AC-${number}`,
60
+ sourceKind: 'acceptance-criterion',
61
+ statement: match[2],
62
+ sourcePath: artifact.path,
63
+ sourceLine: index + 1,
64
+ });
65
+ }
66
+ return out;
67
+ }
68
+ function validateAcceptanceIds(items, artifact, diagnostics) {
69
+ if (items.length === 0) {
70
+ diagnostics.push(diagnostic('acceptance-items-empty', 'the Acceptance criteria section contains zero canonical rows', { artifact: artifact.path, section: REQUIREMENTS_HEADING, observed: 0 }));
71
+ return;
72
+ }
73
+ const seen = new Set();
74
+ for (let index = 0; index < items.length; index++) {
75
+ const item = items[index];
76
+ if (seen.has(item.sourceId)) {
77
+ diagnostics.push(diagnostic('acceptance-id-duplicate', `duplicate acceptance identity ${item.sourceId}`, { artifact: artifact.path, section: REQUIREMENTS_HEADING, sourceId: item.sourceId }));
78
+ }
79
+ seen.add(item.sourceId);
80
+ const expected = `AC-${index + 1}`;
81
+ if (item.sourceId !== expected) {
82
+ diagnostics.push(diagnostic('acceptance-id-noncontiguous', `expected ${expected} at contract position ${index + 1}; found ${item.sourceId}`, { artifact: artifact.path, section: REQUIREMENTS_HEADING, sourceId: item.sourceId, observed: expected }));
83
+ }
84
+ }
85
+ }
86
+ function fieldContinuation(lines, fieldIndex, end) {
87
+ const nextIndex = fieldIndex + 1;
88
+ if (nextIndex >= end)
89
+ return null;
90
+ const next = lines[nextIndex] ?? '';
91
+ if (next.trim() === '' || /^- \S/.test(next))
92
+ return null;
93
+ return { line: nextIndex + 1, text: next };
94
+ }
95
+ function confirmationItem(artifact, diagnostics) {
96
+ const basename = artifact.path.replace(/\\/g, '/').split('/').at(-1) ?? '';
97
+ const filename = ADR_BASENAME.exec(basename);
98
+ if (!filename) {
99
+ diagnostics.push(diagnostic('adr-filename-noncanonical', `direct ADR Markdown file ${basename} is not a canonical NNN-lowercase-kebab filename`, { artifact: artifact.path, observed: basename }));
100
+ return null;
101
+ }
102
+ const lines = linesOf(artifact.text);
103
+ const headings = h2Indexes(lines, CONFIRMATION_HEADING);
104
+ if (headings.length !== 1) {
105
+ diagnostics.push(diagnostic(headings.length === 0 ? 'confirmation-section-missing' : 'confirmation-section-duplicate', `expected exactly one ${CONFIRMATION_HEADING} section; found ${headings.length}`, { artifact: artifact.path, section: CONFIRMATION_HEADING, observed: headings.length }));
106
+ return null;
107
+ }
108
+ const start = headings[0];
109
+ const end = h2End(lines, start);
110
+ const properties = [];
111
+ const checks = [];
112
+ for (let index = start + 1; index < end; index++) {
113
+ const line = lines[index] ?? '';
114
+ const property = LOAD_PROPERTY.exec(line);
115
+ const check = REQUIRED_CHECK.exec(line);
116
+ if (line.startsWith('- Load-bearing property:'))
117
+ properties.push({ index, value: property?.[1] ?? '' });
118
+ if (line.startsWith('- Required automated check:'))
119
+ checks.push({ index, value: check?.[1] ?? '' });
120
+ }
121
+ const propertyValid = properties.length === 1 && /^\S.*$/.test(properties[0]?.value ?? '');
122
+ const checkValid = checks.length === 1 && /^\S.*$/.test(checks[0]?.value ?? '');
123
+ if (!propertyValid) {
124
+ const observed = properties.length === 1 ? 0 : properties.length;
125
+ diagnostics.push(diagnostic('confirmation-property-count', `expected one non-empty Load-bearing property field; found ${observed}`, { artifact: artifact.path, section: CONFIRMATION_HEADING, observed }));
126
+ }
127
+ if (!checkValid) {
128
+ const observed = checks.length === 1 ? 0 : checks.length;
129
+ diagnostics.push(diagnostic('confirmation-check-count', `expected one non-empty Required automated check field; found ${observed}`, { artifact: artifact.path, section: CONFIRMATION_HEADING, observed }));
130
+ }
131
+ if (!propertyValid || !checkValid)
132
+ return null;
133
+ const property = properties[0];
134
+ const check = checks[0];
135
+ let wrapped = false;
136
+ for (const [label, field] of [['Load-bearing property', property], ['Required automated check', check]]) {
137
+ const continuation = fieldContinuation(lines, field.index, end);
138
+ if (continuation !== null) {
139
+ wrapped = true;
140
+ diagnostics.push(diagnostic('confirmation-field-wrapped', `${label} must occupy one physical line; continuation found at line ${continuation.line}`, {
141
+ artifact: artifact.path,
142
+ section: CONFIRMATION_HEADING,
143
+ sourceId: `ADR-${filename[1]}-CONFIRMATION`,
144
+ observed: continuation.text,
145
+ }));
146
+ }
147
+ }
148
+ if (wrapped)
149
+ return null;
150
+ return {
151
+ sourceId: `ADR-${filename[1]}-CONFIRMATION`,
152
+ sourceKind: 'adr-confirmation',
153
+ statement: property.value,
154
+ requiredAutomatedCheck: check.value,
155
+ sourcePath: artifact.path,
156
+ sourceLine: property.index + 1,
157
+ };
158
+ }
159
+ export function extractContractChecklist(input) {
160
+ const diagnostics = [];
161
+ const requirements = acceptanceItems(input.requirements, diagnostics);
162
+ validateAcceptanceIds(requirements, input.requirements, diagnostics);
163
+ if (input.adrs.length === 0) {
164
+ diagnostics.push(diagnostic('adr-input-empty', 'at least one direct canonical ADR Markdown file is required', { section: CONFIRMATION_HEADING, observed: 0 }));
165
+ }
166
+ const adrBasename = (path) => path.replace(/\\/g, '/').split('/').at(-1) ?? '';
167
+ const sortedAdrs = [...input.adrs].sort((a, b) => {
168
+ const aName = adrBasename(a.path);
169
+ const bName = adrBasename(b.path);
170
+ if (aName !== bName)
171
+ return aName < bName ? -1 : 1;
172
+ return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
173
+ });
174
+ const confirmations = [];
175
+ for (const artifact of sortedAdrs) {
176
+ const item = confirmationItem(artifact, diagnostics);
177
+ if (item !== null)
178
+ confirmations.push(item);
179
+ }
180
+ const pending = [...requirements, ...confirmations];
181
+ const seen = new Set();
182
+ for (const item of pending) {
183
+ if (seen.has(item.sourceId)) {
184
+ diagnostics.push(diagnostic('contract-source-id-duplicate', `duplicate emitted source identity ${item.sourceId}`, { artifact: item.sourcePath, sourceId: item.sourceId }));
185
+ }
186
+ seen.add(item.sourceId);
187
+ }
188
+ if (pending.length === 0) {
189
+ diagnostics.push(diagnostic('contract-empty', 'the combined contract contains zero items', { observed: 0 }));
190
+ }
191
+ if (diagnostics.length > 0)
192
+ return { ok: false, diagnostics };
193
+ const items = pending.map((item, index) => ({ id: `CC-${index + 1}`, ...item }));
194
+ return {
195
+ ok: true,
196
+ checklist: { schema: CONTRACT_CHECKLIST_SCHEMA, items },
197
+ diagnostics: [],
198
+ };
199
+ }
200
+ export function renderContractChecklist(checklist) {
201
+ return `\`\`\`contract-checklist\n${JSON.stringify(checklist, null, 2)}\n\`\`\``;
202
+ }
203
+ class JsonDecodeError extends Error {
204
+ code;
205
+ constructor(code, message) {
206
+ super(message);
207
+ this.code = code;
208
+ }
209
+ }
210
+ /** Strict JSON decoder whose object parser sees duplicate members before a value can overwrite one. */
211
+ class DuplicateSafeJsonDecoder {
212
+ text;
213
+ index = 0;
214
+ constructor(text) {
215
+ this.text = text;
216
+ }
217
+ decode() {
218
+ const value = this.value();
219
+ this.space();
220
+ if (this.index !== this.text.length)
221
+ throw new JsonDecodeError('json-trailing-content', `unexpected content at byte ${this.index}`);
222
+ return value;
223
+ }
224
+ space() {
225
+ while (/[\t\n\r ]/.test(this.text[this.index] ?? ''))
226
+ this.index++;
227
+ }
228
+ value() {
229
+ this.space();
230
+ const char = this.text[this.index];
231
+ if (char === '{')
232
+ return this.object();
233
+ if (char === '[')
234
+ return this.array();
235
+ if (char === '"')
236
+ return this.string();
237
+ if (char === '-' || (char !== undefined && /[0-9]/.test(char)))
238
+ return this.number();
239
+ for (const [token, value] of [['true', true], ['false', false], ['null', null]]) {
240
+ if (this.text.startsWith(token, this.index)) {
241
+ this.index += token.length;
242
+ return value;
243
+ }
244
+ }
245
+ throw new JsonDecodeError('json-invalid', `expected a JSON value at byte ${this.index}`);
246
+ }
247
+ object() {
248
+ this.index++;
249
+ // A null prototype makes decoded member names inert data. Assigning `__proto__` into `{}`
250
+ // invokes the legacy prototype setter and would hide that member from Object.keys/closed-schema
251
+ // validation instead of reporting it as unknown.
252
+ const value = Object.create(null);
253
+ const keys = new Set();
254
+ this.space();
255
+ if (this.text[this.index] === '}') {
256
+ this.index++;
257
+ return value;
258
+ }
259
+ while (true) {
260
+ this.space();
261
+ if (this.text[this.index] !== '"')
262
+ throw new JsonDecodeError('json-invalid', `expected an object key at byte ${this.index}`);
263
+ const key = this.string();
264
+ if (keys.has(key))
265
+ throw new JsonDecodeError('json-duplicate-member', `duplicate JSON member ${JSON.stringify(key)}`);
266
+ keys.add(key);
267
+ this.space();
268
+ if (this.text[this.index] !== ':')
269
+ throw new JsonDecodeError('json-invalid', `expected ':' after ${JSON.stringify(key)}`);
270
+ this.index++;
271
+ value[key] = this.value();
272
+ this.space();
273
+ const delimiter = this.text[this.index];
274
+ if (delimiter === '}') {
275
+ this.index++;
276
+ return value;
277
+ }
278
+ if (delimiter !== ',')
279
+ throw new JsonDecodeError('json-invalid', `expected ',' or '}' at byte ${this.index}`);
280
+ this.index++;
281
+ }
282
+ }
283
+ array() {
284
+ this.index++;
285
+ const value = [];
286
+ this.space();
287
+ if (this.text[this.index] === ']') {
288
+ this.index++;
289
+ return value;
290
+ }
291
+ while (true) {
292
+ value.push(this.value());
293
+ this.space();
294
+ const delimiter = this.text[this.index];
295
+ if (delimiter === ']') {
296
+ this.index++;
297
+ return value;
298
+ }
299
+ if (delimiter !== ',')
300
+ throw new JsonDecodeError('json-invalid', `expected ',' or ']' at byte ${this.index}`);
301
+ this.index++;
302
+ }
303
+ }
304
+ string() {
305
+ const start = this.index;
306
+ this.index++;
307
+ let escaped = false;
308
+ while (this.index < this.text.length) {
309
+ const char = this.text[this.index];
310
+ if (!escaped && char === '"') {
311
+ this.index++;
312
+ try {
313
+ return JSON.parse(this.text.slice(start, this.index));
314
+ }
315
+ catch {
316
+ throw new JsonDecodeError('json-invalid-string', `invalid JSON string at byte ${start}`);
317
+ }
318
+ }
319
+ if (!escaped && char.charCodeAt(0) < 0x20)
320
+ throw new JsonDecodeError('json-invalid-string', `control character at byte ${this.index}`);
321
+ if (!escaped && char === '\\')
322
+ escaped = true;
323
+ else
324
+ escaped = false;
325
+ this.index++;
326
+ }
327
+ throw new JsonDecodeError('json-invalid-string', `unterminated JSON string at byte ${start}`);
328
+ }
329
+ number() {
330
+ const match = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(this.text.slice(this.index));
331
+ if (!match || match.index !== 0)
332
+ throw new JsonDecodeError('json-invalid-number', `invalid number at byte ${this.index}`);
333
+ this.index += match[0].length;
334
+ const value = Number(match[0]);
335
+ if (!Number.isFinite(value))
336
+ throw new JsonDecodeError('json-invalid-number', `non-finite number at byte ${this.index}`);
337
+ return value;
338
+ }
339
+ }
340
+ function isRecord(value) {
341
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
342
+ }
343
+ function exactKeys(value, allowed, required, location, diagnostics, fields = {}) {
344
+ const allowedSet = new Set(allowed);
345
+ for (const key of Object.keys(value)) {
346
+ if (!allowedSet.has(key))
347
+ diagnostics.push(diagnostic('verdict-unknown-member', `${location} contains unknown member ${key}`, { ...fields, observed: key }));
348
+ }
349
+ for (const key of required) {
350
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
351
+ diagnostics.push(diagnostic('verdict-member-missing', `${location} is missing required member ${key}`, { ...fields, observed: key }));
352
+ }
353
+ }
354
+ }
355
+ function nonEmptyString(value) {
356
+ return typeof value === 'string' && value.trim() !== '';
357
+ }
358
+ function validateEvidence(value, location, diagnostics, contractId) {
359
+ const fields = contractId === undefined ? {} : { contractId };
360
+ if (!isRecord(value)) {
361
+ diagnostics.push(diagnostic('verdict-evidence-type', `${location}.evidence must be an object`, fields));
362
+ return null;
363
+ }
364
+ exactKeys(value, ['artifact', 'quote', 'observedOutcome'], ['artifact', 'quote', 'observedOutcome'], `${location}.evidence`, diagnostics, fields);
365
+ if (!nonEmptyString(value['artifact']))
366
+ diagnostics.push(diagnostic('verdict-artifact-type', `${location}.evidence.artifact must be a non-empty string`, fields));
367
+ if (!nonEmptyString(value['quote']))
368
+ diagnostics.push(diagnostic('verdict-quote-type', `${location}.evidence.quote must be a non-empty string`, fields));
369
+ const observed = value['observedOutcome'];
370
+ if (observed !== 'pass' && observed !== 'fail' && observed !== 'not-testable') {
371
+ diagnostics.push(diagnostic('verdict-observed-outcome', `${location}.evidence.observedOutcome is not canonical`, { ...fields, observed: String(observed) }));
372
+ }
373
+ if (!nonEmptyString(value['artifact']) || !nonEmptyString(value['quote'])
374
+ || (observed !== 'pass' && observed !== 'fail' && observed !== 'not-testable'))
375
+ return null;
376
+ return { artifact: value['artifact'], quote: value['quote'], observedOutcome: observed };
377
+ }
378
+ function validateVerdictItem(value, index, diagnostics) {
379
+ const location = `items[${index}]`;
380
+ if (!isRecord(value)) {
381
+ diagnostics.push(diagnostic('verdict-row-type', `${location} must be an object`, { observed: Array.isArray(value) ? 'array' : typeof value }));
382
+ return null;
383
+ }
384
+ const contractId = typeof value['id'] === 'string' ? value['id'] : undefined;
385
+ const fields = contractId === undefined ? {} : { contractId };
386
+ exactKeys(value, ['id', 'verdict', 'evidence', 'reason'], ['id', 'verdict', 'evidence'], location, diagnostics, fields);
387
+ if (!nonEmptyString(value['id']))
388
+ diagnostics.push(diagnostic('verdict-id-type', `${location}.id must be a non-empty string`, fields));
389
+ const verdict = value['verdict'];
390
+ if (verdict !== 'met' && verdict !== 'unmet' && verdict !== 'not-testable') {
391
+ diagnostics.push(diagnostic('verdict-status-invalid', `${location}.verdict is not canonical`, { ...fields, observed: String(verdict) }));
392
+ }
393
+ const evidence = validateEvidence(value['evidence'], location, diagnostics, contractId);
394
+ const reason = value['reason'];
395
+ if ((verdict === 'unmet' || verdict === 'not-testable') && !nonEmptyString(reason)) {
396
+ diagnostics.push(diagnostic('verdict-reason-required', `${location}.reason must be non-empty for ${verdict}`, fields));
397
+ }
398
+ if (reason !== undefined && !nonEmptyString(reason)) {
399
+ diagnostics.push(diagnostic('verdict-reason-type', `${location}.reason must be a non-empty string when present`, fields));
400
+ }
401
+ if (!nonEmptyString(value['id']) || (verdict !== 'met' && verdict !== 'unmet' && verdict !== 'not-testable') || evidence === null)
402
+ return null;
403
+ if ((verdict === 'unmet' || verdict === 'not-testable') && !nonEmptyString(reason))
404
+ return null;
405
+ return {
406
+ id: value['id'],
407
+ verdict,
408
+ evidence,
409
+ ...(nonEmptyString(reason) ? { reason } : {}),
410
+ };
411
+ }
412
+ function validateVerdictPayload(value) {
413
+ const diagnostics = [];
414
+ if (!isRecord(value)) {
415
+ return { ok: false, diagnostics: [diagnostic('verdict-payload-type', 'the verdict payload must be a JSON object')] };
416
+ }
417
+ exactKeys(value, ['schema', 'overallGrade', 'items'], ['schema', 'overallGrade', 'items'], 'payload', diagnostics);
418
+ if (value['schema'] !== CONTRACT_VERDICT_SCHEMA) {
419
+ diagnostics.push(diagnostic('verdict-schema-unsupported', `unsupported verdict schema ${String(value['schema'])}`, { observed: String(value['schema']) }));
420
+ }
421
+ const grade = value['overallGrade'];
422
+ if (grade !== 'A' && grade !== 'B' && grade !== 'C' && grade !== 'D') {
423
+ diagnostics.push(diagnostic('verdict-grade-invalid', 'overallGrade must be A, B, C, or D', { observed: String(grade) }));
424
+ }
425
+ const rawItems = value['items'];
426
+ if (!Array.isArray(rawItems)) {
427
+ diagnostics.push(diagnostic('verdict-items-type', 'payload.items must be an array'));
428
+ }
429
+ const items = [];
430
+ if (Array.isArray(rawItems)) {
431
+ for (let index = 0; index < rawItems.length; index++) {
432
+ const item = validateVerdictItem(rawItems[index], index, diagnostics);
433
+ if (item !== null)
434
+ items.push(item);
435
+ }
436
+ }
437
+ if (diagnostics.length > 0 || value['schema'] !== CONTRACT_VERDICT_SCHEMA
438
+ || (grade !== 'A' && grade !== 'B' && grade !== 'C' && grade !== 'D') || !Array.isArray(rawItems)) {
439
+ return { ok: false, diagnostics };
440
+ }
441
+ return { ok: true, report: { schema: CONTRACT_VERDICT_SCHEMA, overallGrade: grade, items } };
442
+ }
443
+ function parseHumanGrade(text) {
444
+ const lines = linesOf(text);
445
+ const grades = [];
446
+ for (let index = 0; index < lines.length; index++) {
447
+ const heading = /^## Grade: \*\*([A-D])\*\*$/.exec(lines[index] ?? '');
448
+ if (heading)
449
+ grades.push(heading[1]);
450
+ if (lines[index] === '## Grade') {
451
+ const end = h2End(lines, index);
452
+ for (let cursor = index + 1; cursor < end; cursor++) {
453
+ const field = /^\*\*Grade: ([A-D])\*\*$/.exec(lines[cursor] ?? '');
454
+ if (field)
455
+ grades.push(field[1]);
456
+ }
457
+ }
458
+ }
459
+ if (grades.length !== 1) {
460
+ return {
461
+ ok: false,
462
+ diagnostic: diagnostic('report-grade-ambiguous', `expected exactly one human Grade A-D value; found ${grades.length}`, { section: '## Grade', observed: grades }),
463
+ };
464
+ }
465
+ return { ok: true, grade: grades[0] };
466
+ }
467
+ export function parseContractVerdictReport(text) {
468
+ const lines = linesOf(text);
469
+ const headings = h2Indexes(lines, VERDICT_HEADING);
470
+ if (headings.length === 0) {
471
+ return {
472
+ ok: false,
473
+ established: false,
474
+ diagnostics: [diagnostic('verdict-section-missing', `missing canonical ${VERDICT_HEADING} section`, { section: VERDICT_HEADING })],
475
+ };
476
+ }
477
+ if (headings.length !== 1) {
478
+ return {
479
+ ok: false,
480
+ established: true,
481
+ diagnostics: [diagnostic('verdict-section-duplicate', `expected exactly one ${VERDICT_HEADING} section; found ${headings.length}`, { section: VERDICT_HEADING, observed: headings.length })],
482
+ };
483
+ }
484
+ const start = headings[0];
485
+ const section = lines.slice(start + 1, h2End(lines, start)).join('\n').trim();
486
+ const fence = /^```json\n([\s\S]*)\n```$/.exec(section);
487
+ if (!fence) {
488
+ return {
489
+ ok: false,
490
+ established: true,
491
+ diagnostics: [diagnostic('verdict-fence-invalid', 'the Contract checklist section must contain exactly one fenced json object and no other content', { section: VERDICT_HEADING })],
492
+ };
493
+ }
494
+ let decoded;
495
+ try {
496
+ decoded = new DuplicateSafeJsonDecoder(fence[1]).decode();
497
+ }
498
+ catch (error) {
499
+ const jsonError = error instanceof JsonDecodeError ? error : new JsonDecodeError('json-invalid', String(error));
500
+ return {
501
+ ok: false,
502
+ established: true,
503
+ diagnostics: [diagnostic(jsonError.code, jsonError.message, { section: VERDICT_HEADING })],
504
+ };
505
+ }
506
+ const payload = validateVerdictPayload(decoded);
507
+ if (!payload.ok) {
508
+ return {
509
+ ok: false,
510
+ established: true,
511
+ diagnostics: payload.diagnostics.map((entry) => ({ ...entry, section: entry.section ?? VERDICT_HEADING })),
512
+ };
513
+ }
514
+ const human = parseHumanGrade(text);
515
+ if (!human.ok)
516
+ return { ok: false, established: true, diagnostics: [human.diagnostic] };
517
+ if (human.grade !== payload.report.overallGrade) {
518
+ return {
519
+ ok: false,
520
+ established: true,
521
+ diagnostics: [diagnostic('report-grade-mismatch', `human Grade ${human.grade} disagrees with payload overallGrade ${payload.report.overallGrade}`, { section: '## Grade', observed: [human.grade, payload.report.overallGrade] })],
522
+ };
523
+ }
524
+ return { ok: true, report: payload.report, humanGrade: human.grade, diagnostics: [] };
525
+ }
526
+ function contractIdNumber(id) {
527
+ const match = /^CC-([1-9][0-9]*)$/.exec(id);
528
+ return match ? Number(match[1]) : null;
529
+ }
530
+ function quoteOccurrences(text, quote) {
531
+ let count = 0;
532
+ let index = 0;
533
+ while (index <= text.length - quote.length) {
534
+ const found = text.indexOf(quote, index);
535
+ if (found < 0)
536
+ break;
537
+ count++;
538
+ index = found + 1;
539
+ }
540
+ return count;
541
+ }
542
+ function safeEvidenceArtifact(artifact) {
543
+ if (artifact.startsWith('/') || artifact.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(artifact))
544
+ return 'absolute paths are forbidden';
545
+ if (artifact.includes('\\'))
546
+ return 'backslash path separators are forbidden';
547
+ const parts = artifact.split('/');
548
+ if (parts.some((part) => part === '' || part === '.' || part === '..'))
549
+ return 'empty, dot, and traversal path segments are forbidden';
550
+ return null;
551
+ }
552
+ function baseCounts(checklist, report) {
553
+ // `verifyContractVerdicts` is a public runtime boundary even though TypeScript callers receive a
554
+ // typed signature. Count defensively before the closed-schema validator reports malformed rows.
555
+ const rows = isRecord(report) && Array.isArray(report['items']) ? report['items'] : [];
556
+ const hasVerdict = (value, verdict) => isRecord(value) && value['verdict'] === verdict;
557
+ return {
558
+ contractItems: checklist.items.length,
559
+ verdictItems: rows.length,
560
+ met: rows.filter((item) => hasVerdict(item, 'met')).length,
561
+ unmet: rows.filter((item) => hasVerdict(item, 'unmet')).length,
562
+ notTestable: rows.filter((item) => hasVerdict(item, 'not-testable')).length,
563
+ missing: 0,
564
+ duplicate: 0,
565
+ orphan: 0,
566
+ invalidEvidence: 0,
567
+ gradeConflicts: 0,
568
+ };
569
+ }
570
+ function failedVerification(checklist, report, diagnostics, counts, items) {
571
+ return {
572
+ outcome: 'fail',
573
+ exitCode: 1,
574
+ overallGrade: isRecord(report)
575
+ && (report['overallGrade'] === 'A' || report['overallGrade'] === 'B'
576
+ || report['overallGrade'] === 'C' || report['overallGrade'] === 'D')
577
+ ? report['overallGrade']
578
+ : null,
579
+ items: items ?? checklist.items.map((item) => ({
580
+ id: item.id,
581
+ verdict: null,
582
+ evidence: 'not-checked',
583
+ diagnostics: diagnostics.filter((entry) => entry.contractId === item.id),
584
+ })),
585
+ counts,
586
+ diagnostics,
587
+ };
588
+ }
589
+ export function verifyContractVerdicts(checklist, report, evidence) {
590
+ const runtime = validateVerdictPayload(report);
591
+ let counts = baseCounts(checklist, report);
592
+ if (!runtime.ok)
593
+ return failedVerification(checklist, report, runtime.diagnostics, counts);
594
+ report = runtime.report;
595
+ counts = baseCounts(checklist, report);
596
+ const diagnostics = [];
597
+ if (checklist.schema !== CONTRACT_CHECKLIST_SCHEMA || checklist.items.length === 0) {
598
+ diagnostics.push(diagnostic(checklist.schema !== CONTRACT_CHECKLIST_SCHEMA ? 'contract-schema-unsupported' : 'contract-empty', checklist.schema !== CONTRACT_CHECKLIST_SCHEMA ? `unsupported contract schema ${String(checklist.schema)}` : 'the contract contains zero items'));
599
+ return failedVerification(checklist, report, diagnostics, counts);
600
+ }
601
+ const expectedIds = checklist.items.map((item) => item.id);
602
+ const reportedIds = report.items.map((item) => item.id);
603
+ const frequencies = new Map();
604
+ for (const id of reportedIds)
605
+ frequencies.set(id, (frequencies.get(id) ?? 0) + 1);
606
+ let duplicate = 0;
607
+ for (const [id, frequency] of frequencies) {
608
+ if (frequency > 1) {
609
+ duplicate += frequency - 1;
610
+ diagnostics.push(diagnostic('verdict-id-duplicate', `verdict id ${id} occurs ${frequency} times`, { contractId: id, observed: frequency }));
611
+ }
612
+ }
613
+ const reportedSet = new Set(reportedIds);
614
+ const expectedSet = new Set(expectedIds);
615
+ const missing = expectedIds.filter((id) => !reportedSet.has(id));
616
+ const orphan = [...reportedSet].filter((id) => !expectedSet.has(id));
617
+ for (const id of missing)
618
+ diagnostics.push(diagnostic('verdict-id-missing', `contract id ${id} has no verdict`, { contractId: id }));
619
+ for (const id of orphan)
620
+ diagnostics.push(diagnostic(contractIdNumber(id) === null ? 'verdict-id-malformed' : 'verdict-id-orphan', `reported verdict id ${id} is not an expected contract id`, { contractId: id }));
621
+ if (missing.length === 0 && orphan.length === 0 && duplicate === 0
622
+ && expectedIds.some((id, index) => reportedIds[index] !== id)) {
623
+ diagnostics.push(diagnostic('verdict-id-order', 'verdict ids contain the expected set but not in contract order', { observed: reportedIds }));
624
+ }
625
+ counts = { ...counts, missing: missing.length, duplicate, orphan: orphan.length };
626
+ if (diagnostics.length > 0)
627
+ return failedVerification(checklist, report, diagnostics, counts);
628
+ const itemResults = [];
629
+ let invalidEvidence = 0;
630
+ for (let index = 0; index < checklist.items.length; index++) {
631
+ const contract = checklist.items[index];
632
+ const verdict = report.items[index];
633
+ const itemDiagnostics = [];
634
+ const pathProblem = safeEvidenceArtifact(verdict.evidence.artifact);
635
+ if (pathProblem !== null) {
636
+ itemDiagnostics.push(diagnostic('evidence-path-unsafe', `${verdict.evidence.artifact}: ${pathProblem}`, { artifact: verdict.evidence.artifact, contractId: contract.id }));
637
+ }
638
+ else if (evidence.reportArtifact !== undefined && verdict.evidence.artifact === evidence.reportArtifact) {
639
+ itemDiagnostics.push(diagnostic('evidence-self-citation', `${contract.id} cites the QE verdict payload itself`, { artifact: verdict.evidence.artifact, contractId: contract.id }));
640
+ }
641
+ else {
642
+ const read = evidence.read(verdict.evidence.artifact);
643
+ if (!read.ok) {
644
+ itemDiagnostics.push(diagnostic(read.code, read.detail, { artifact: verdict.evidence.artifact, contractId: contract.id }));
645
+ }
646
+ else {
647
+ const matches = quoteOccurrences(read.text, verdict.evidence.quote);
648
+ if (matches !== 1) {
649
+ itemDiagnostics.push(diagnostic('evidence-quote-count', `${contract.id} evidence quote occurs ${matches} times; expected exactly once`, { artifact: verdict.evidence.artifact, contractId: contract.id, observed: matches }));
650
+ }
651
+ }
652
+ }
653
+ const expectedOutcome = {
654
+ met: 'pass',
655
+ unmet: 'fail',
656
+ 'not-testable': 'not-testable',
657
+ };
658
+ if (verdict.evidence.observedOutcome !== expectedOutcome[verdict.verdict]) {
659
+ itemDiagnostics.push(diagnostic('evidence-outcome-polarity', `${contract.id} verdict ${verdict.verdict} requires observedOutcome ${expectedOutcome[verdict.verdict]}`, {
660
+ artifact: verdict.evidence.artifact,
661
+ contractId: contract.id,
662
+ observed: verdict.evidence.observedOutcome,
663
+ }));
664
+ }
665
+ if (verdict.verdict === 'unmet') {
666
+ itemDiagnostics.push(diagnostic('contract-item-unmet', `${contract.id} is unmet: ${verdict.reason ?? 'no reason recorded'}`, { contractId: contract.id }));
667
+ }
668
+ else if (verdict.verdict === 'not-testable') {
669
+ itemDiagnostics.push(diagnostic('contract-item-not-testable', `${contract.id} is not-testable: ${verdict.reason ?? 'no reason recorded'}`, { contractId: contract.id }));
670
+ }
671
+ const evidenceErrors = itemDiagnostics.filter((entry) => entry.code.startsWith('evidence-')).length;
672
+ if (evidenceErrors > 0)
673
+ invalidEvidence++;
674
+ diagnostics.push(...itemDiagnostics);
675
+ itemResults.push({
676
+ id: contract.id,
677
+ verdict: verdict.verdict,
678
+ evidence: evidenceErrors > 0 ? 'invalid' : 'valid',
679
+ ...(verdict.reason !== undefined ? { reason: verdict.reason } : {}),
680
+ diagnostics: itemDiagnostics,
681
+ });
682
+ }
683
+ const conflicts = report.items.filter((item) => item.verdict === 'unmet').map((item) => item.id);
684
+ const gradeConflicts = (report.overallGrade === 'A' || report.overallGrade === 'B') ? conflicts : [];
685
+ if (gradeConflicts.length > 0) {
686
+ diagnostics.push(diagnostic('grade-unmet-conflict', `overall grade ${report.overallGrade} cannot coexist with unmet items: ${gradeConflicts.join(', ')}`, { observed: gradeConflicts }));
687
+ }
688
+ counts = { ...counts, invalidEvidence, gradeConflicts: gradeConflicts.length };
689
+ if (diagnostics.length > 0)
690
+ return failedVerification(checklist, report, diagnostics, counts, itemResults);
691
+ return {
692
+ outcome: 'pass',
693
+ exitCode: 0,
694
+ overallGrade: report.overallGrade,
695
+ items: itemResults,
696
+ counts,
697
+ diagnostics: [],
698
+ };
699
+ }
700
+ //# sourceMappingURL=contract-checklist.js.map