@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
package/src/loop-trace.ts CHANGED
@@ -89,7 +89,19 @@ export interface TraceRunClosed {
89
89
  counts: { dispatched: number; settled: number };
90
90
  }
91
91
 
92
- export type TraceEvent = TraceDispatchEvent | TraceSettleEvent | TraceRunOpened | TraceRunClosed;
92
+ /** A deliberate work-dropping receipt. Its presence is required for overflow:'truncate' to grade. */
93
+ export interface TraceFanoutTruncatedEvent {
94
+ v: 1;
95
+ runId: string;
96
+ seq: number;
97
+ event: 'fanout-truncated';
98
+ stage: string;
99
+ registrySize: number;
100
+ dispatched: number;
101
+ reason: string;
102
+ }
103
+
104
+ export type TraceEvent = TraceDispatchEvent | TraceSettleEvent | TraceRunOpened | TraceRunClosed | TraceFanoutTruncatedEvent;
93
105
 
94
106
  export interface TraceState {
95
107
  runId: string;
@@ -148,6 +160,20 @@ export function traceValidateEvent(e: unknown): string | null {
148
160
  return 'unknown event kind';
149
161
  }
150
162
 
163
+ /** Reader extension kept outside the generated emitter slice: the two locked feature-adr workflow
164
+ * copies embed the v1 emitter byte-for-byte and cannot be rewritten during this parallel Step 7. */
165
+ function traceValidateReaderEvent(e: unknown): string | null {
166
+ const base = traceValidateEvent(e);
167
+ if (base !== 'unknown event kind') return base;
168
+ const ev = e as Record<string, unknown>;
169
+ if (ev['event'] !== 'fanout-truncated') return base;
170
+ if (typeof ev['stage'] !== 'string' || !TRACE_KEY_RE.test(ev['stage'])) return 'fanout-truncated stage fails its VO regex';
171
+ if (typeof ev['registrySize'] !== 'number' || !Number.isInteger(ev['registrySize']) || ev['registrySize'] < 0) return 'fanout-truncated registrySize must be a non-negative integer';
172
+ if (typeof ev['dispatched'] !== 'number' || !Number.isInteger(ev['dispatched']) || ev['dispatched'] < 0 || ev['dispatched'] > ev['registrySize']) return 'fanout-truncated dispatched must be an integer within registrySize';
173
+ if (typeof ev['reason'] !== 'string' || ev['reason'].trim() === '') return 'fanout-truncated reason required';
174
+ return null;
175
+ }
176
+
151
177
  /**
152
178
  * Open a trace state and buffer the run.opened frame. Throws on an invalid runId (fail-closed).
153
179
  *
@@ -204,6 +230,20 @@ export function traceOnSettle(state: TraceState, e: { invocationId: string; outc
204
230
  return seq;
205
231
  }
206
232
 
233
+ /** Emit the trace half of the loud truncation receipt. This is not a dispatch and does not alter
234
+ * run.closed invocation counts. */
235
+ export function traceOnFanoutTruncated(
236
+ state: TraceState,
237
+ e: { stage: string; registrySize: number; dispatched: number; reason: string },
238
+ ): number {
239
+ const seq = ++state.seq;
240
+ const event: TraceFanoutTruncatedEvent = { v: 1, runId: state.runId, seq, event: 'fanout-truncated', ...e };
241
+ const err = traceValidateReaderEvent(event);
242
+ if (err !== null) throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');
243
+ state.buffer.push(JSON.stringify(event));
244
+ return seq;
245
+ }
246
+
207
247
  /** Buffer the run.closed frame (a trace without it parses as incomplete). */
208
248
  export function traceClose(state: TraceState): void {
209
249
  const closed: TraceRunClosed = {
@@ -378,7 +418,7 @@ export function parseTrace(text: string): TraceRun {
378
418
  run.parseErrors.push('unparseable line: ' + t.slice(0, 120));
379
419
  continue;
380
420
  }
381
- const err = traceValidateEvent(e);
421
+ const err = traceValidateReaderEvent(e);
382
422
  if (err !== null) {
383
423
  run.parseErrors.push('invalid event (' + err + '): ' + t.slice(0, 120));
384
424
  continue;
@@ -646,6 +686,53 @@ export function runInvariants(projection: TraceProjection, run: TraceRun): Invar
646
686
  if (peak > bound) out.push({ id: idC, status: 'fail', message: `observed scheduling concurrency ${peak} exceeds maxFanout ${bound}` });
647
687
  else out.push({ id: idC, status: 'pass', message: `peak scheduled concurrency ${peak} <= maxFanout ${bound}` });
648
688
  }
689
+
690
+ // ADR-001: coverage is matched to registry POSITIONS, never inferred from equal totals. A
691
+ // duplicate-valued registry therefore owns two independent expected slots. Retries do not
692
+ // manufacture coverage: only attempt 1 of the first member stage witnesses branch admission.
693
+ const idD = 'region-dispatch-completeness:' + region.fanout;
694
+ if ((projection as { kind: string }).kind === 'trace-projection/1' || !Array.isArray(region.registry) || typeof region.registrySize !== 'number' || region.overflow === undefined) {
695
+ out.push({ id: idD, status: 'inconclusive', message: 'trace-projection/1 region has no positional registry evidence — dispatch completeness cannot be proved' });
696
+ } else {
697
+ const registry = region.dedup === true
698
+ ? region.registry.filter((key, index, all) => all.indexOf(key) === index)
699
+ : region.registry;
700
+ const expected = region.overflow === 'truncate' ? registry.slice(0, Math.max(0, region.maxFanout)) : registry;
701
+ const witness = region.members[0];
702
+ const observed = witness === undefined
703
+ ? []
704
+ : run.events.filter((e): e is TraceDispatchEvent => e.event === 'dispatched' && e.stepId === witness && e.attempt === 1).map((e) => e.itemKey);
705
+ const unmatched = new Set(expected.map((_key, index) => index));
706
+ const extras: (string | null)[] = [];
707
+ for (const key of observed) {
708
+ const position = [...unmatched].find((index) => expected[index] === key);
709
+ if (position === undefined) extras.push(key);
710
+ else unmatched.delete(position);
711
+ }
712
+ const missing = [...unmatched].map((index) => `#${index + 1}:${expected[index]}`);
713
+ const receipt = run.events.find((e): e is TraceFanoutTruncatedEvent =>
714
+ e.event === 'fanout-truncated' && e.stage === region.fanout,
715
+ );
716
+ const badReceipt = region.overflow === 'truncate' && (
717
+ receipt === undefined ||
718
+ receipt.registrySize !== region.registrySize ||
719
+ receipt.dispatched !== expected.length ||
720
+ receipt.reason.trim() === ''
721
+ );
722
+ if (missing.length > 0 || extras.length > 0 || badReceipt) {
723
+ const parts: string[] = [];
724
+ if (missing.length > 0) parts.push('missing registry position(s) ' + missing.join(', '));
725
+ if (extras.length > 0) parts.push('unexpected dispatch key(s) ' + extras.map((x) => String(x)).join(', '));
726
+ if (badReceipt) parts.push('missing or mismatched fanout-truncated trace receipt');
727
+ out.push({
728
+ id: idD,
729
+ status: run.incomplete && region.overflow === 'window' && missing.length > 0 && extras.length === 0 ? 'inconclusive' : 'fail',
730
+ message: parts.join('; ') + (run.incomplete ? ' (trace incomplete)' : ''),
731
+ });
732
+ } else {
733
+ out.push({ id: idD, status: 'pass', message: `every one of ${expected.length} expected registry position(s) has exactly one first-attempt dispatch${region.overflow === 'truncate' ? ' and a matching truncation receipt' : ''}` });
734
+ }
735
+ }
649
736
  // Join coverage: the join itself is a structural pseudo-step (no trace event of its own), so
650
737
  // its trace-visible witnesses are (a) every dispatched member settles (a dispatched branch is
651
738
  // never skippable) and (b) every post-region dispatching step (`after`) dispatches AFTER every
@@ -833,6 +920,8 @@ export function assembleTimeline(input: {
833
920
  rows.push({ seq: e.seq, kind: 'trace', label: `settle ${e.invocationId}`, detail: `outcome=${e.outcome}`, wallTime: e.wallTime ?? null });
834
921
  } else if (e.event === 'run.opened') {
835
922
  rows.push({ seq: e.seq, kind: 'trace', label: 'run.opened', detail: `plan=${e.planDigest.slice(0, 12)} exec-fp=${e.execFp.slice(0, 12)}`, wallTime: null });
923
+ } else if (e.event === 'fanout-truncated') {
924
+ rows.push({ seq: e.seq, kind: 'trace', label: `fanout-truncated ${e.stage}`, detail: `${e.dispatched} of ${e.registrySize} — ${e.reason}`, wallTime: null });
836
925
  } else {
837
926
  rows.push({ seq: e.seq, kind: 'trace', label: 'run.closed', detail: JSON.stringify(e.counts), wallTime: null });
838
927
  }
@@ -92,7 +92,9 @@ export function harvestStageOutcomes(records: readonly unknown[]): Harvest {
92
92
  if (!isRecord(modelsUsed)) { skipped.noModels += 1; continue; }
93
93
  const grade = extractGrade(result);
94
94
  if (grade === null) { skipped.noGrade += 1; continue; }
95
- const runId = typeof source['runId'] === 'string' ? source['runId'] : JSON.stringify(modelsUsed).slice(0, 40);
95
+ // Full canonical JSON is intentional: the former 40-character prefix merged distinct records
96
+ // whose stage names shared a long prefix, manufacturing idempotency across different runs.
97
+ const runId = typeof source['runId'] === 'string' ? source['runId'] : `models:${JSON.stringify(modelsUsed)}`;
96
98
  const ts = typeof source['timestamp'] === 'string' ? source['timestamp'] : null;
97
99
  const tier = typeof result['tier'] === 'string' && result['tier'] !== '' ? result['tier'] : 'unknown';
98
100
  let contributed = false;
@@ -138,6 +140,8 @@ export interface Recommendation {
138
140
  readonly rule: string;
139
141
  readonly skipped: Harvest['skipped'];
140
142
  readonly crossFamilyNote: string;
143
+ readonly unfed: { readonly count: number; readonly runIds: string[] };
144
+ readonly freshness: 'current' | 'stale' | 'unfed';
141
145
  };
142
146
  }
143
147
 
@@ -154,7 +158,7 @@ const CROSS_NOTE =
154
158
  'the qe pick is computed with the family FORCED to the cross of the code pick — a same-family qe recommendation is unrepresentable (ADR-001 D3)';
155
159
 
156
160
  /** Recommend per stage over the harvested samples (optionally one tier's slice). */
157
- export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number } = {}): Recommendation {
161
+ export function recommendModels(harvest: Harvest, opts: { tier?: string; qualityBar?: number; minSamples?: number; alreadyFed?: readonly string[] } = {}): Recommendation {
158
162
  const slice = opts.tier === undefined ? harvest.samples : harvest.samples.filter((s) => s.tier === opts.tier);
159
163
  const tierLabel = opts.tier ?? 'all';
160
164
  const byStage = new Map<string, HarvestSample[]>();
@@ -197,9 +201,21 @@ export function recommendModels(harvest: Harvest, opts: { tier?: string; quality
197
201
  }
198
202
  }
199
203
  if (!stages.includes('code')) perStage.unshift(code);
204
+ const pending = unfedRuns(harvest.samples, opts.alreadyFed ?? []);
205
+ const harvestRunIds = [...new Set(harvest.samples.map((s) => s.runId))];
206
+ const fed = new Set(opts.alreadyFed ?? []);
207
+ const fedInHarvest = harvestRunIds.filter((runId) => fed.has(runId)).length;
200
208
  return {
201
209
  perStage,
202
- basis: { runsUsed: harvest.runsUsed, window: harvest.window, rule: harvest.rule, skipped: harvest.skipped, crossFamilyNote: CROSS_NOTE },
210
+ basis: {
211
+ runsUsed: harvest.runsUsed,
212
+ window: harvest.window,
213
+ rule: harvest.rule,
214
+ skipped: harvest.skipped,
215
+ crossFamilyNote: CROSS_NOTE,
216
+ unfed: { count: pending.length, runIds: pending },
217
+ freshness: pending.length === 0 ? 'current' : fedInHarvest === 0 ? 'unfed' : 'stale',
218
+ },
203
219
  };
204
220
  }
205
221
 
@@ -213,15 +229,30 @@ export interface FeedPlan {
213
229
  readonly fedAfter: string[];
214
230
  }
215
231
 
232
+ /** One shared definition of the telemetry/store gap, used by recommendation, apply and guard. */
233
+ export function unfedRuns(samples: readonly HarvestSample[], alreadyFed: readonly string[]): string[] {
234
+ const fed = new Set(alreadyFed);
235
+ return [...new Set(samples.map((s) => s.runId))].filter((runId) => !fed.has(runId)).sort();
236
+ }
237
+
216
238
  /** A run feeds ONCE. Double-feeding the same telemetry manufactures confidence the data does not
217
239
  * contain — the second `--apply` must feed 0 and say which runs it skipped. */
218
240
  export function planFeed(samples: readonly HarvestSample[], alreadyFed: readonly string[]): FeedPlan {
219
241
  const fed = new Set(alreadyFed);
220
242
  const toFeed: HarvestSample[] = [];
221
243
  const skippedRuns = new Set<string>();
244
+ const seenSamples = new Set<string>();
222
245
  for (const s of samples) {
223
246
  if (fed.has(s.runId)) skippedRuns.add(s.runId);
224
- else toFeed.push(s);
247
+ else {
248
+ // A multi-stage run legitimately feeds one sample per stage. Only a repeated sample from a
249
+ // duplicated record is suppressed within this harvest.
250
+ const sampleKey = JSON.stringify([s.runId, s.stage]);
251
+ if (!seenSamples.has(sampleKey)) {
252
+ seenSamples.add(sampleKey);
253
+ toFeed.push(s);
254
+ }
255
+ }
225
256
  }
226
257
  const fedAfter = [...new Set([...alreadyFed, ...toFeed.map((s) => s.runId)])].sort();
227
258
  return { toFeed, skippedRuns: [...skippedRuns].sort(), fedAfter };
package/src/qe-bridge.ts CHANGED
@@ -392,6 +392,15 @@ export function buildBridgePrompt(
392
392
  '4. Judge only what the extracts below actually show; if the evidence is insufficient to judge',
393
393
  ' something, say so explicitly rather than assuming it is fine.',
394
394
  '',
395
+ 'You have been given NO tools and NO filesystem access — this is deliberate isolation, not an',
396
+ 'accident, and there is nothing else to inspect. Do not attempt to run a shell command, read a',
397
+ 'file, or explore a directory: no such action can succeed here, and any text that looks like a',
398
+ 'tool invocation will simply sit in your own output unexecuted. The extracts below are the',
399
+ 'COMPLETE evidence you will ever receive for this review. If they are insufficient to judge a',
400
+ 'specific point, write that as a finding (e.g. "cannot verify X from the given extracts") and',
401
+ 'continue to a grade regardless — never stop your review to attempt verification that cannot',
402
+ 'happen in this environment.',
403
+ '',
395
404
  'Output format — ALL THREE parts are required, in this order, at the END of your answer:',
396
405
  ' (a) your review prose, containing exactly ONE line that reads: GRADE: <A-F>',
397
406
  ' (b) a fenced code block labelled ' + BRIDGE_FENCE_LABEL + ' whose body is JSON:',