@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.
- package/.dz-manifest.json +146 -58
- package/README.md +72 -2
- package/dist/cmd-usage.d.ts +148 -0
- package/dist/cmd-usage.d.ts.map +1 -0
- package/dist/cmd-usage.js +548 -0
- package/dist/cmd-usage.js.map +1 -0
- package/dist/compounding.d.ts +4 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +6 -0
- package/dist/compounding.js.map +1 -1
- package/dist/contract-checklist.d.ts +123 -0
- package/dist/contract-checklist.d.ts.map +1 -0
- package/dist/contract-checklist.js +700 -0
- package/dist/contract-checklist.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +11 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +37 -2
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +58 -23
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +208 -59
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +25 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +59 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +8 -8
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-plan.d.ts +13 -1
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +15 -1
- package/dist/loop-plan.js.map +1 -1
- package/dist/loop-render.d.ts.map +1 -1
- package/dist/loop-render.js +51 -6
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +20 -1
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +83 -1
- package/dist/loop-trace.js.map +1 -1
- package/dist/model-recommender.d.ts +8 -0
- package/dist/model-recommender.d.ts.map +1 -1
- package/dist/model-recommender.js +31 -4
- package/dist/model-recommender.js.map +1 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +9 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/restart-advisor.d.ts +103 -0
- package/dist/restart-advisor.d.ts.map +1 -0
- package/dist/restart-advisor.js +445 -0
- package/dist/restart-advisor.js.map +1 -0
- package/dist/slop-lint.d.ts +128 -0
- package/dist/slop-lint.d.ts.map +1 -0
- package/dist/slop-lint.js +607 -0
- package/dist/slop-lint.js.map +1 -0
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +18 -12
- package/dist/workflow-run.js.map +1 -1
- package/package.json +19 -15
- package/sbom.json +277 -57
- package/src/cmd-usage.ts +720 -0
- package/src/compounding.ts +13 -0
- package/src/contract-checklist.ts +973 -0
- package/src/deadwood-allowlist.json +80 -0
- package/src/feature-adr-checkpoints.ts +38 -2
- package/src/feature-adr-routing.ts +238 -55
- package/src/guard.ts +79 -1
- package/src/index.ts +81 -1
- package/src/loop-blobs.generated.ts +8 -8
- package/src/loop-plan.ts +36 -3
- package/src/loop-render.ts +50 -6
- package/src/loop-trace.ts +91 -2
- package/src/model-recommender.ts +35 -4
- package/src/qe-bridge.ts +9 -0
- package/src/restart-advisor.ts +579 -0
- package/src/slop-lint.ts +762 -0
- package/src/slop-markers.json +71 -0
- package/src/workflow-run.ts +18 -11
package/src/guard.ts
CHANGED
|
@@ -69,6 +69,29 @@ export interface GuardFacts {
|
|
|
69
69
|
readonly counts?: readonly { readonly label: string; readonly a: number; readonly b: number }[];
|
|
70
70
|
/** for store-bloat-cap: current learned-store size vs its cap. */
|
|
71
71
|
readonly store?: { readonly count: number; readonly cap: number };
|
|
72
|
+
/** Advisory freshness evidence for the auto-cost outcome store. Missing evidence fails open. */
|
|
73
|
+
readonly routingFreshness?: { readonly unfedRunIds: readonly string[] };
|
|
74
|
+
/**
|
|
75
|
+
* for marketplace-parity: the CLI's read-only regeneration result. The whole field being absent
|
|
76
|
+
* means evidence could not be gathered, so this advisory rule reports nothing.
|
|
77
|
+
*/
|
|
78
|
+
readonly marketplaceParity?: {
|
|
79
|
+
/** false ⇒ this repository has no .claude-plugin/ showcase and is out of scope. */
|
|
80
|
+
readonly applicable: boolean;
|
|
81
|
+
/** true ⇒ exactly one of plugin.json / marketplace.json is present. */
|
|
82
|
+
readonly onlyOnePresent?: boolean;
|
|
83
|
+
/** true ⇒ registry-derived composition differs from a fresh regeneration. */
|
|
84
|
+
readonly diverged?: boolean;
|
|
85
|
+
/** Operator-owned published version, carried only to render the exact repair command. */
|
|
86
|
+
readonly publishedVersion?: string;
|
|
87
|
+
/** Existing published manifests that could not be read or parsed. */
|
|
88
|
+
readonly manifestFailures?: readonly {
|
|
89
|
+
readonly file: 'plugin.json' | 'marketplace.json';
|
|
90
|
+
readonly error: string;
|
|
91
|
+
}[];
|
|
92
|
+
/** true ⇒ regeneration was attempted but could not complete. */
|
|
93
|
+
readonly regenerateFailed?: boolean;
|
|
94
|
+
};
|
|
72
95
|
/** for skills-registrable: per skill pack, dirs that would ship un-registrable (no depth-1 SKILL.md). */
|
|
73
96
|
readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
|
|
74
97
|
/** for readme-first: per publishable package, is a version bump staged without a README change? */
|
|
@@ -262,6 +285,8 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
262
285
|
{ id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
|
|
263
286
|
{ id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
|
|
264
287
|
{ id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
|
|
288
|
+
{ id: 'routing-store-stale', severity: 'soft', ops: ['publish'], description: 'harvested routing telemetry has been applied to the auto-cost outcome store' },
|
|
289
|
+
{ id: 'marketplace-parity', severity: 'soft', ops: ['publish'], description: 'the published .claude-plugin/ showcase composition matches a fresh regeneration from the live registry (version excluded — an operator field)' },
|
|
265
290
|
{ id: 'agents-md-policy-sync', severity: 'soft', ops: ['publish'], description: 'proves the AGENTS.md copy is in SYNC with its source — not that the runtime read or obeyed it; heal drift with dz agents-sync' },
|
|
266
291
|
{ id: 'lockfile-in-sync', severity: 'soft', ops: ['publish'], description: 'every workspace @dzhechkov/* dependency spec matches the specifier pnpm-lock.yaml records for that importer (a dep bump without a lockfile refresh breaks CI with ERR_PNPM_OUTDATED_LOCKFILE). SOFT-ONLY — a config cannot promote it to HARD' },
|
|
267
292
|
{ id: 'store-bloat-cap', severity: 'soft', ops: ['teach', 'consolidate'], description: 'the learned store is within its size cap' },
|
|
@@ -360,6 +385,59 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
360
385
|
}
|
|
361
386
|
return out;
|
|
362
387
|
},
|
|
388
|
+
'routing-store-stale': (f, _sev) => {
|
|
389
|
+
const ids = f.routingFreshness?.unfedRunIds;
|
|
390
|
+
if (!Array.isArray(ids) || ids.length === 0) return [];
|
|
391
|
+
const valid = [...new Set(ids.filter((id): id is string => typeof id === 'string' && id !== ''))].sort();
|
|
392
|
+
if (valid.length === 0) return [];
|
|
393
|
+
return [{
|
|
394
|
+
rule: 'routing-store-stale',
|
|
395
|
+
severity: 'soft',
|
|
396
|
+
detail: `${valid.length} harvested run(s) are not reflected in the auto-cost store: ${valid.slice(0, 5).join(', ')}${valid.length > 5 ? '…' : ''} — run dz routing recommend --apply`,
|
|
397
|
+
}];
|
|
398
|
+
},
|
|
399
|
+
'marketplace-parity': (f, _sev) => {
|
|
400
|
+
const fact = f.marketplaceParity;
|
|
401
|
+
if (fact === undefined || fact.applicable !== true) return [];
|
|
402
|
+
const hasPublishedVersion = typeof fact.publishedVersion === 'string' && fact.publishedVersion !== '';
|
|
403
|
+
const fix = `dz plugin --version ${hasPublishedVersion ? fact.publishedVersion : 'X.Y.Z'}`;
|
|
404
|
+
const fixHint = hasPublishedVersion ? '' : ' (substitute the published version for X.Y.Z)';
|
|
405
|
+
const manifestFailures = Array.isArray(fact.manifestFailures)
|
|
406
|
+
? fact.manifestFailures.filter((failure) => failure
|
|
407
|
+
&& (failure.file === 'plugin.json' || failure.file === 'marketplace.json')
|
|
408
|
+
&& typeof failure.error === 'string'
|
|
409
|
+
&& failure.error !== '')
|
|
410
|
+
: [];
|
|
411
|
+
if (manifestFailures.length > 0) {
|
|
412
|
+
return manifestFailures.map((failure) => ({
|
|
413
|
+
rule: 'marketplace-parity',
|
|
414
|
+
severity: 'soft',
|
|
415
|
+
detail: `.claude-plugin/${failure.file} exists but could not be read or parsed: ${failure.error} — run \`${fix}\`${fixHint} and commit the result`,
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
if (fact.onlyOnePresent === true) {
|
|
419
|
+
return [{
|
|
420
|
+
rule: 'marketplace-parity',
|
|
421
|
+
severity: 'soft',
|
|
422
|
+
detail: `.claude-plugin/ has only one of plugin.json / marketplace.json — a broken half-showcase; run \`${fix}\`${fixHint} and commit the result`,
|
|
423
|
+
}];
|
|
424
|
+
}
|
|
425
|
+
if (fact.regenerateFailed === true) {
|
|
426
|
+
return [{
|
|
427
|
+
rule: 'marketplace-parity',
|
|
428
|
+
severity: 'soft',
|
|
429
|
+
detail: 'could not verify .claude-plugin/ composition because fresh regeneration failed',
|
|
430
|
+
}];
|
|
431
|
+
}
|
|
432
|
+
if (fact.diverged === true) {
|
|
433
|
+
return [{
|
|
434
|
+
rule: 'marketplace-parity',
|
|
435
|
+
severity: 'soft',
|
|
436
|
+
detail: `.claude-plugin/ composition does not match a fresh regeneration from the live registry — run \`${fix}\`${fixHint} and commit the result`,
|
|
437
|
+
}];
|
|
438
|
+
}
|
|
439
|
+
return [];
|
|
440
|
+
},
|
|
363
441
|
'review-round': (f, sev) => {
|
|
364
442
|
// The publish gate had eleven rules and not one asked whether anyone but the author had read the
|
|
365
443
|
// code. MEASURED cost (health-advisor slice H): five rounds graded F, thirteen packages published
|
|
@@ -508,7 +586,7 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
508
586
|
* may not understand a file, and "I might be wrong" plus "block the publish" is the wrong pair. Disabling
|
|
509
587
|
* such a rule stays allowed — only the promotion is refused.
|
|
510
588
|
*/
|
|
511
|
-
export const SOFT_ONLY_RULES: readonly string[] = ['lockfile-in-sync', 'agents-md-policy-sync'];
|
|
589
|
+
export const SOFT_ONLY_RULES: readonly string[] = ['lockfile-in-sync', 'agents-md-policy-sync', 'routing-store-stale', 'marketplace-parity'];
|
|
512
590
|
|
|
513
591
|
/**
|
|
514
592
|
* A well-formed PROMOTED rule: an id the engine does not know, made enforceable by a template +
|
package/src/index.ts
CHANGED
|
@@ -224,6 +224,28 @@ export type { PretrainResult, DetectedTech } from './pretrain.js';
|
|
|
224
224
|
export type { RecommendationReport, SkillRecommendation } from './recommend.js';
|
|
225
225
|
export { claimCheck, summarize, decideClaimCheckText, severityCounts, isGated } from './claim-check.js';
|
|
226
226
|
export type { ClaimFinding, ClaimCheckResult, ClaimTextDecision, FailOn } from './claim-check.js';
|
|
227
|
+
export {
|
|
228
|
+
BUNDLED_SLOP_REGISTRY_URL,
|
|
229
|
+
DEFAULT_SLOP_CONFIG,
|
|
230
|
+
parseSlopRegistry,
|
|
231
|
+
slopLint,
|
|
232
|
+
validateSlopLintConfig,
|
|
233
|
+
} from './slop-lint.js';
|
|
234
|
+
export type {
|
|
235
|
+
SlopDiagnostic,
|
|
236
|
+
SlopEvidence,
|
|
237
|
+
SlopFinding,
|
|
238
|
+
SlopFindingMetrics,
|
|
239
|
+
SlopFindingThresholds,
|
|
240
|
+
SlopLanguage,
|
|
241
|
+
SlopLintConfig,
|
|
242
|
+
SlopLintResult,
|
|
243
|
+
SlopRegistry,
|
|
244
|
+
SlopRegistryEntry,
|
|
245
|
+
SlopRuleId,
|
|
246
|
+
SlopValidationError,
|
|
247
|
+
ValidationResult,
|
|
248
|
+
} from './slop-lint.js';
|
|
227
249
|
export { hookDecision, isFenced, isNewLine, ESCAPE_TEACHING } from './claim-check-hook-policy.js';
|
|
228
250
|
export type { HookDecision, HookDecisionOpts } from './claim-check-hook-policy.js';
|
|
229
251
|
export { step8ClaimGate } from './feature-adr-claim-gate.js';
|
|
@@ -261,7 +283,7 @@ export {
|
|
|
261
283
|
codeCheckpointPersistAllowed,
|
|
262
284
|
codeStageResultShapeValid,
|
|
263
285
|
} from './feature-adr-checkpoints.js';
|
|
264
|
-
export type { CheckpointStage, CheckpointEntry, ResumeMode, ResumeDecision, ParsedCheckpointRead, TrainingPairFamily, TrainingPair, TrainingPairEvaluation, TrainingPairProvenance, TrainingPairTruncation,
|
|
286
|
+
export type { CheckpointStage, CheckpointEntry, ResumeMode, ResumeDecision, ParsedCheckpointRead, TrainingPairFamily, TrainingPair, TrainingPairEvaluation, TrainingPairProvenance, TrainingPairBudget, TrainingPairTruncation,
|
|
265
287
|
CheckpointWriteVerdict,
|
|
266
288
|
} from './feature-adr-checkpoints.js';
|
|
267
289
|
|
|
@@ -290,6 +312,35 @@ export { decidePublishSigning, decidePostSigningVerification, decideSignableSet,
|
|
|
290
312
|
export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
|
|
291
313
|
export type { RecordKind, RecordVerdict, RecordDecision } from './run-records.js';
|
|
292
314
|
export type { AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
|
|
315
|
+
// contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
|
|
316
|
+
// exact per-item verification. Filesystem discovery/containment stays in harness-cli.
|
|
317
|
+
export {
|
|
318
|
+
extractContractChecklist,
|
|
319
|
+
renderContractChecklist,
|
|
320
|
+
parseContractVerdictReport,
|
|
321
|
+
verifyContractVerdicts,
|
|
322
|
+
} from './contract-checklist.js';
|
|
323
|
+
export type {
|
|
324
|
+
ContractSourceKind,
|
|
325
|
+
ContractVerdict,
|
|
326
|
+
ContractObservedOutcome,
|
|
327
|
+
ContractGrade,
|
|
328
|
+
ContractSourceArtifact,
|
|
329
|
+
ContractChecklistSource,
|
|
330
|
+
ContractItem,
|
|
331
|
+
ContractChecklist,
|
|
332
|
+
ContractDiagnostic,
|
|
333
|
+
ContractChecklistResult,
|
|
334
|
+
ContractVerdictEvidence,
|
|
335
|
+
ContractVerdictItem,
|
|
336
|
+
ContractVerdictReport,
|
|
337
|
+
ContractVerdictParseResult,
|
|
338
|
+
ContractEvidenceReadResult,
|
|
339
|
+
ContractEvidenceReader,
|
|
340
|
+
ContractItemVerification,
|
|
341
|
+
ContractVerificationCounts,
|
|
342
|
+
ContractVerification,
|
|
343
|
+
} from './contract-checklist.js';
|
|
293
344
|
export {
|
|
294
345
|
DOMAIN_LIFT_EXACT,
|
|
295
346
|
DOMAIN_LIFT_RELATED,
|
|
@@ -576,6 +627,7 @@ export {
|
|
|
576
627
|
resolveCoderSpec,
|
|
577
628
|
coderIsCodex,
|
|
578
629
|
resolveQeSpec,
|
|
630
|
+
resolveQeSpecForCoder,
|
|
579
631
|
crossFamilyQe,
|
|
580
632
|
decideModeBScope,
|
|
581
633
|
partitionReviewFindings,
|
|
@@ -792,6 +844,9 @@ export * from './skills-verify.js';
|
|
|
792
844
|
// finding, not a pass).
|
|
793
845
|
export * from './compounding.js';
|
|
794
846
|
|
|
847
|
+
// Advisory command-invocation telemetry + deadwood report (feature dz-deadwood).
|
|
848
|
+
export * from './cmd-usage.js';
|
|
849
|
+
|
|
795
850
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay, scout idea #4) — the RESULT leg to compounding's
|
|
796
851
|
// readiness leg. Orchestrates + scores; never calls a model. SUPPORTED requires two DISJOINT Wilson
|
|
797
852
|
// intervals; INCONCLUSIVE is a first-class honest outcome.
|
|
@@ -872,6 +927,31 @@ export { decideCadenceWindow, isoWeekOf, weeklyBuckets, guardRepeatDecay, buildC
|
|
|
872
927
|
export type { CadenceWindow, CadenceReport, CadenceWindowDecision } from './cadence.js';
|
|
873
928
|
export { readQeRounds, countQeRounds, QE_ROUNDS_DEFAULT_CEILING } from './qe-rounds.js';
|
|
874
929
|
export type { QeRound, QeFailedAttempt, QeRoundsReport, QeRoundsStatus } from './qe-rounds.js';
|
|
930
|
+
export {
|
|
931
|
+
adviseRestart,
|
|
932
|
+
decideRestartRecommendation,
|
|
933
|
+
parseCheckpointQeHistory,
|
|
934
|
+
parseTrainingPairQeHistory,
|
|
935
|
+
renderRestartDecisionLog,
|
|
936
|
+
RESTART_ADVISOR_SCHEMA,
|
|
937
|
+
RESTART_ADVISOR_MAX_DIAGNOSTICS,
|
|
938
|
+
RESTART_ADVISOR_MAX_EVIDENCE,
|
|
939
|
+
} from './restart-advisor.js';
|
|
940
|
+
export type {
|
|
941
|
+
ParsedRestartHistory,
|
|
942
|
+
RestartAdvice,
|
|
943
|
+
RestartAdvisorInput,
|
|
944
|
+
RestartAdvisorPolicy,
|
|
945
|
+
RestartDecision,
|
|
946
|
+
RestartDecisionInput,
|
|
947
|
+
RestartGrade,
|
|
948
|
+
RestartNormalizedRound,
|
|
949
|
+
RestartPolicyOrigin,
|
|
950
|
+
RestartReason,
|
|
951
|
+
RestartRecommendation,
|
|
952
|
+
RestartSource,
|
|
953
|
+
RestartThreshold,
|
|
954
|
+
} from './restart-advisor.js';
|
|
875
955
|
export { describeStoreLocation, storeLocationLine } from './store-location.js';
|
|
876
956
|
export type { StoreLocation, StoreOrigin } from './store-location.js';
|
|
877
957
|
export { mergeStoreHits, sameStore, globalStoreRoot, storeCountLabel } from './store-merge.js';
|
|
@@ -42,7 +42,7 @@ export const BLOB_COVERAGE_MANIFEST: { coveredWorkflows: string[] } = {
|
|
|
42
42
|
export const BLOBS: Record<string, LoopBlob> = {
|
|
43
43
|
"checkpoints": {
|
|
44
44
|
name: "checkpoints",
|
|
45
|
-
version: "1.
|
|
45
|
+
version: "1.2.0",
|
|
46
46
|
contentHash: "a44560c6036fd143b7a3f125fec00fa8b91c3c06ac5b9e1ecfb83d27a5211e9b",
|
|
47
47
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
48
48
|
requires: [],
|
|
@@ -51,30 +51,30 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
51
51
|
},
|
|
52
52
|
"training-pairs": {
|
|
53
53
|
name: "training-pairs",
|
|
54
|
-
version: "1.
|
|
55
|
-
contentHash: "
|
|
54
|
+
version: "1.2.0",
|
|
55
|
+
contentHash: "7c836995b72f0b8fc69074cf6bb7b8a58fac609e4f5c365c82b8745c655da39c",
|
|
56
56
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
57
57
|
requires: ["checkpoints"],
|
|
58
58
|
exports: ["TRAINPAIR_SCHEMA_VERSION","TRAINPAIR_MAX_IO_CHARS","trainingPairFamily","trainingPairPath","TRAINPAIR_PRIVACY_NOTE","buildTrainingPair","serializeTrainingPair","trainingPairAppendCmd","decideCaptureMode","captureFailureRecord","trainingPairBackfillCmd","TP_BACKFILL_OK","TP_BACKFILL_SKIP"],
|
|
59
|
-
code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-
|
|
59
|
+
code: "function decideCaptureMode(opts) {\n if (!opts.enabled)\n return 'skip-disabled';\n if (!Number.isInteger(opts.recordCount) || opts.recordCount <= 0)\n return 'skip-empty';\n return opts.resumed ? 'backfill' : 'capture';\n}\nfunction captureFailureRecord(stage, mode, reason, detail) {\n const normalizedStage = typeof stage === 'string' && stage.trim() !== '' ? stage : 'unknown';\n const normalizedMode = mode === 'capture' || mode === 'backfill' || mode === 'skip-disabled' || mode === 'skip-empty'\n ? mode\n : null;\n const normalizedReason = reason === 'threw' || reason === 'unserializable' || reason === 'unverified' || reason === 'backfill-unverified' || reason === 'empty-output'\n ? reason\n : 'threw';\n let normalizedDetail = null;\n if (detail !== null && detail !== undefined) {\n try {\n const text = String(detail);\n if (text !== '')\n normalizedDetail = text.length > 500 ? text.slice(0, 500) + '…' : text;\n }\n catch {\n normalizedDetail = null;\n }\n }\n return { stage: normalizedStage, mode: normalizedMode, reason: normalizedReason, detail: normalizedDetail };\n}\nconst TRAINPAIR_SCHEMA_VERSION = 'fa-trainpair-3';\nconst TRAINPAIR_MAX_IO_CHARS = 48000;\nfunction trainingPairFamily(spec) {\n return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'codex' : 'claude';\n}\nfunction trainingPairPath(slug, stage) {\n return '.dz/fa-training/' + slug + '/' + stage + '.jsonl';\n}\nconst TRAINPAIR_PRIVACY_NOTE = \"feature-adr TRAINING PAIRS (backlog 70e0f083): per-stage SFT records - STAGE INPUT (full prompt/context) -> STAGE OUTPUT (artifact/result) -> EVALUATION (QE grade + injected lessons) with model+family provenance; one JSONL file per stage per slug. PRIVACY: pairs may contain TARGET-REPO CODE and full prompts. This directory is NOT gitignored yet by explicit owner decision - review contents before sharing or publishing anything that embeds it. ts is the CAPTURE time. On a record with captureMode: 'backfill' that is the RECONSTRUCTION time, NOT the stage's observation time — the original stage's timing lives in that run's .fa-state checkpoint.\";\nconst TP_PROFILE_MARKER_START = '<!-- dz:profile:start -->';\nconst TP_PROFILE_MARKER_END = '<!-- dz:profile:end -->';\nconst TP_PROFILE_REDACTED = '[dz:profile REDACTED]';\nfunction redactProfileBlock(text) {\n if (typeof text !== 'string' || text === '')\n return typeof text === 'string' ? text : '';\n let out = '';\n let rest = text;\n for (;;) {\n const start = rest.indexOf(TP_PROFILE_MARKER_START);\n if (start === -1)\n return out + rest;\n out += rest.slice(0, start) + TP_PROFILE_REDACTED;\n const end = rest.indexOf(TP_PROFILE_MARKER_END, start + TP_PROFILE_MARKER_START.length);\n if (end === -1)\n return out;\n rest = rest.slice(end + TP_PROFILE_MARKER_END.length);\n }\n}\nfunction coerceText(v) {\n if (typeof v === 'string')\n return v;\n if (v === null || v === undefined)\n return '';\n try {\n const s = JSON.stringify(v);\n return typeof s === 'string' ? s : String(v);\n }\n catch {\n return String(v);\n }\n}\nfunction normalizeTrainingPairBudget(raw) {\n try {\n if (raw === undefined)\n return { primary: 'claude', claude: 'normal', codex: 'normal', preset: 'unset' };\n if (raw === null || typeof raw !== 'object')\n return null;\n const value = raw;\n const primary = value.primary;\n const claude = value.claude;\n const codex = value.codex;\n if (primary !== 'claude' && primary !== 'codex')\n return null;\n if (claude !== 'normal' && claude !== 'eco')\n return null;\n if (codex !== 'normal' && codex !== 'eco')\n return null;\n if (value.preset === 'unset')\n return { primary, claude, codex, preset: 'unset' };\n let preset = 'custom';\n if (claude === 'normal' && codex === 'normal')\n preset = 'normal';\n else if (claude === 'eco' && codex === 'eco')\n preset = 'eco';\n else if (claude === 'eco' && codex === 'normal')\n preset = 'hybrid';\n return { primary, claude, codex, preset };\n }\n catch {\n return null;\n }\n}\nfunction buildTrainingPair(opts) {\n let input = redactProfileBlock(coerceText(opts.input));\n let output = redactProfileBlock(coerceText(opts.output));\n let truncated = null;\n if (input.length + output.length > TRAINPAIR_MAX_IO_CHARS) {\n truncated = { inputChars: input.length, outputChars: output.length, inputHash: fnv1a64(input), outputHash: fnv1a64(output) };\n const half = Math.floor(TRAINPAIR_MAX_IO_CHARS / 2);\n let inKeep = input.length;\n let outKeep = output.length;\n if (outKeep <= half)\n inKeep = TRAINPAIR_MAX_IO_CHARS - outKeep;\n else if (inKeep <= half)\n outKeep = TRAINPAIR_MAX_IO_CHARS - inKeep;\n else {\n inKeep = half;\n outKeep = TRAINPAIR_MAX_IO_CHARS - half;\n }\n if (inKeep < input.length)\n input = input.slice(0, inKeep) + '\\n…[TRUNCATED ' + (truncated.inputChars - inKeep) + ' chars — full-text fnv1a64=' + truncated.inputHash + ']';\n if (outKeep < output.length)\n output = output.slice(0, outKeep) + '\\n…[TRUNCATED ' + (truncated.outputChars - outKeep) + ' chars — full-text fnv1a64=' + truncated.outputHash + ']';\n }\n const ev = opts.evaluation || {};\n const pv = opts.provenance || {};\n return {\n schema: TRAINPAIR_SCHEMA_VERSION,\n slug: opts.slug,\n stage: opts.stage,\n ts: opts.ts === undefined ? null : opts.ts,\n input,\n output,\n evaluation: {\n grade: typeof ev.grade === 'string' && ev.grade.trim() !== '' ? ev.grade : null,\n gradedBy: typeof ev.gradedBy === 'string' && ev.gradedBy !== '' ? ev.gradedBy : null,\n lessonsInjected: Array.isArray(ev.lessonsInjected) ? ev.lessonsInjected.filter((s) => typeof s === 'string' && s !== '') : [],\n },\n provenance: {\n model: typeof pv.model === 'string' && pv.model !== '' ? pv.model : 'unknown',\n family: pv.family === 'claude' || pv.family === 'codex' ? pv.family : trainingPairFamily(pv.model),\n role: typeof pv.role === 'string' && pv.role !== '' ? pv.role : 'unknown',\n tokens: typeof pv.tokens === 'number' && Number.isFinite(pv.tokens) ? pv.tokens : null,\n minutes: typeof pv.minutes === 'number' && Number.isFinite(pv.minutes) ? pv.minutes : null,\n },\n budgetMode: normalizeTrainingPairBudget(opts.budgetMode),\n truncated,\n captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',\n resumed: opts.resumed === true,\n };\n}\nfunction serializeTrainingPair(pair) {\n try {\n const line = JSON.stringify(pair);\n return typeof line === 'string' ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction trainingPairAppendCmd(repoAbs, slug, stage, line) {\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs));\n}\nconst TP_BACKFILL_OK = 'TP-BACKFILL-OK';\nconst TP_BACKFILL_SKIP = 'TP-BACKFILL-SKIP';\nconst TP_BACKFILL_DUP = 'TP-BACKFILL-DUP';\nfunction trainingPairBackfillCmd(repoAbs, slug, stage, lines, markKey) {\n if (typeof repoAbs !== 'string' || repoAbs === '')\n return null;\n if (typeof slug !== 'string' || slug === '')\n return null;\n if (typeof stage !== 'string' || stage === '')\n return null;\n if (!Array.isArray(lines) || lines.length === 0 || !lines.every(line => typeof line === 'string' && line !== ''))\n return null;\n const dirAbs = repoAbs + '/.dz/fa-training/' + slug;\n const readmeAbs = repoAbs + '/.dz/fa-training/README.md';\n const fileAbs = dirAbs + '/' + stage + '.jsonl';\n const markDir = repoAbs + '/.dz/fa-training/.backfill-marks';\n const markStage = stage.replace(/\\.\\./g, '_').replace(/\\//g, '_');\n const resolvedMarkKey = markKey === undefined ? fnv1a64(stage + '\\0' + lines.join('\\n')) : markKey;\n const markPath = markDir + '/' + markStage + '-' + resolvedMarkKey;\n const appends = lines\n .map(line => \"printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + shellQuote(fileAbs))\n .join(' && ');\n return ('mkdir -p ' + shellQuote(dirAbs) +\n ' && { [ -f ' + shellQuote(readmeAbs) + ' ] || printf \\'%s\\\\n\\' ' + shellQuote(TRAINPAIR_PRIVACY_NOTE) + ' > ' + shellQuote(readmeAbs) + '; }' +\n ' && mkdir -p ' + shellQuote(markDir) +\n ' && if mkdir ' + shellQuote(markPath) + ' 2>/dev/null; then ' +\n 'if [ -f ' + shellQuote(fileAbs) + ' ]; then echo ' + shellQuote(TP_BACKFILL_SKIP) +\n '; else { ' + appends + ' && echo ' + shellQuote(TP_BACKFILL_OK) + '; } || { rmdir ' + shellQuote(markPath) + ' 2>/dev/null; false; }; fi' +\n '; else echo ' + shellQuote(TP_BACKFILL_DUP) + '; fi');\n}",
|
|
60
60
|
},
|
|
61
61
|
"model-resolver": {
|
|
62
62
|
name: "model-resolver",
|
|
63
63
|
version: "1.0.0",
|
|
64
|
-
contentHash: "
|
|
64
|
+
contentHash: "fcf8c1dfc4b5364adb6e532565953ef30d94a45070b2b903411170965a1fb372",
|
|
65
65
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
|
|
66
66
|
requires: [],
|
|
67
67
|
exports: ["specToOpts","resolveStageModel","KNOWN_CODEX","mergeOpts","stageLabel","modelLabel"],
|
|
68
|
-
code: "const
|
|
68
|
+
code: "const STAGE_EFFORT = { override: {\n router: 'medium',\n requirements: 'medium',\n research: 'medium',\n adr: 'high',\n ideation: 'medium',\n ddd: 'high',\n architecture: 'high',\n plan: 'high',\n code: 'medium',\n qe: 'high',\n fleet: 'medium',\n } };\nfunction topCodexId(env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS.flagship;\n}\nconst KNOWN_CODEX = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1, 'gpt-5.6-luna': 1, 'gpt-5.6-terra': 1, 'gpt-5.6-sol': 1 };\nconst CODEX_TIERS = {\n flagship: 'gpt-5.6-sol',\n workhorse: 'gpt-5.6-terra',\n 'high-volume': 'gpt-5.6-luna',\n};\nfunction codexIdForTier(tier, env) {\n return env.CODEX_MODEL !== 'auto' ? env.CODEX_MODEL : CODEX_TIERS[tier];\n}\nconst BUDGET_PRESETS = {\n normal: { claude: 'normal', codex: 'normal' },\n eco: { claude: 'eco', codex: 'eco' },\n hybrid: { claude: 'eco', codex: 'normal' },\n};\nfunction resolveBudgetMode(raw) {\n if (raw === undefined)\n return BUDGET_PRESETS.normal;\n if (typeof raw === 'string') {\n const preset = BUDGET_PRESETS[raw];\n if (!preset)\n throw new RangeError('budget: unknown preset \"' + raw + '\" — valid: normal|eco|hybrid');\n return preset;\n }\n if (raw && typeof raw === 'object') {\n const value = raw;\n for (const key of Object.keys(value)) {\n if (key !== 'claude' && key !== 'codex') {\n throw new RangeError('budget: unknown family key \"' + key + '\" — valid: claude|codex');\n }\n }\n for (const key of ['claude', 'codex']) {\n const level = value[key];\n if (level !== undefined && level !== 'normal' && level !== 'eco') {\n throw new RangeError('budget.' + key + ': unknown level \"' + level + '\" — valid: normal|eco');\n }\n }\n return {\n claude: value.claude || 'normal',\n codex: value.codex || 'normal',\n };\n }\n throw new RangeError('budget: expected a preset name or {claude,codex} object, got ' + typeof raw);\n}\nconst CLAUDE_NAMES = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };\nconst VALID_REASONING = { none: 1, minimal: 1, low: 1, medium: 1, high: 1, xhigh: 1, max: 1 };\nconst DEFAULT_MODELS = {\n router: 'fable',\n requirements: 'sonnet',\n research: 'sonnet',\n adr: 'opus',\n ideation: 'sonnet',\n ddd: 'opus',\n architecture: 'opus',\n plan: 'sonnet',\n code: null,\n qe: null,\n fleet: 'sonnet',\n};\nconst ROUTING_TABLES = {\n claude: {\n claude: {\n normal: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'fable', ideation: 'sonnet', ddd: 'fable', architecture: 'fable', plan: 'opus', code: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', requirements: 'sonnet', research: 'sonnet', adr: 'opus', ideation: 'sonnet', ddd: 'opus', architecture: 'opus', plan: 'sonnet', code: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n codex: {\n claude: {\n normal: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n eco: { router: 'sonnet', qe: 'sonnet', fleet: 'sonnet' },\n },\n codex: { normal: {}, eco: {} },\n },\n};\nfunction codexCell(tier, effort, env) {\n return 'codex:' + codexIdForTier(tier, env) + ':' + effort;\n}\nfunction budgetTable(primary, mode, env) {\n const claudeHalf = ROUTING_TABLES[primary].claude[mode.claude];\n let codexHalf;\n if (primary === 'claude') {\n const qeSpec = mode.codex === 'normal'\n ? codexCell('flagship', 'high', env)\n : codexCell('workhorse', 'medium', env);\n codexHalf = { ...ROUTING_TABLES.claude.codex[mode.codex], qe: env.codexAvailable === false ? 'opus' : qeSpec };\n }\n else {\n const normal = mode.codex === 'normal';\n const design = codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'medium', env);\n codexHalf = {\n requirements: design,\n research: design,\n adr: design,\n ideation: design,\n ddd: design,\n architecture: design,\n plan: codexCell(normal ? 'flagship' : 'workhorse', normal ? 'high' : 'low', env),\n code: codexCell(normal ? 'flagship' : 'workhorse', 'medium', env),\n };\n }\n return { ...claudeHalf, ...codexHalf };\n}\nfunction specToOpts(spec, env) {\n const log = env.log || function () { };\n if (!spec)\n return {};\n const parts = String(spec).split(':');\n const head = parts[0] || '';\n if (head === 'codex') {\n let id = parts[1] || env.CODEX_MODEL;\n if (id !== 'auto' && !KNOWN_CODEX[id]) {\n log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);\n id = env.CODEX_MODEL;\n }\n let reasoning = parts[2] || 'high';\n if (!VALID_REASONING[reasoning]) {\n throw new RangeError('models: invalid reasoning \"' + reasoning + '\" — valid: ' + Object.keys(VALID_REASONING).join('|'));\n }\n return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };\n }\n if (CLAUDE_NAMES[head])\n return { model: head };\n log('models: unknown spec ' + spec + ' — session-inherited');\n return {};\n}\nfunction resolveCoderSpec(env) {\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return 'codex:' + env.CODEX_MODEL + ':high';\n return 'opus';\n}\nfunction coderIsCodex(env) {\n const codeSpec = env.MODELS.code;\n if (codeSpec !== undefined && codeSpec !== null)\n return String(codeSpec).split(':')[0] === 'codex';\n if (env.CODER === 'codex' || env.CODER === 'codex-fallback')\n return true;\n return env.primary === 'codex';\n}\nfunction resolveQeSpecForCoder(coderCodex, env) {\n if (coderCodex)\n return 'sonnet';\n const CODEX_AVAILABLE = env.codexAvailable !== false;\n if (!CODEX_AVAILABLE)\n return 'opus';\n const budget = resolveBudgetMode(env.budget);\n return budget.codex === 'eco'\n ? 'codex:' + codexIdForTier('workhorse', env) + ':medium'\n : 'codex:' + codexIdForTier('flagship', env) + ':high';\n}\nfunction resolveQeSpec(env) {\n return resolveQeSpecForCoder(coderIsCodex(env), env);\n}\nfunction routingRequested(env) {\n return (Object.keys(env.MODELS).length > 0 ||\n env.primary !== undefined ||\n env.budget !== undefined ||\n env.PLANNER === 'codex' ||\n env.CODER === 'codex' ||\n env.CODER === 'codex-fallback' ||\n env.QE_REVIEWER === 'codex' ||\n env.QE_REVIEWER === 'codex-fallback');\n}\nfunction resolveStageModel(stage, env) {\n if (env.usageOverride) {\n const r = (env.usageReasoning && env.usageReasoning[stage]) || STAGE_EFFORT.override[stage] || 'medium';\n const o = specToOpts('codex:' + topCodexId(env) + ':' + r, env);\n o._usageSwitched = true;\n return o;\n }\n let spec = env.MODELS[stage];\n if (spec === undefined) {\n if (!routingRequested(env))\n return {};\n if (stage === 'code' && (env.CODER === 'codex' || env.CODER === 'codex-fallback')) {\n return specToOpts(resolveCoderSpec(env), env);\n }\n if (stage === 'plan' && env.PLANNER === 'codex') {\n return specToOpts('codex:' + env.CODEX_MODEL + ':high', env);\n }\n if (stage === 'qe') {\n return specToOpts(resolveQeSpec(env), env);\n }\n const resolvedPrimary = env.primary || 'claude';\n const cell = budgetTable(resolvedPrimary, resolveBudgetMode(env.budget), env)[stage];\n spec = cell !== undefined ? cell : DEFAULT_MODELS[stage];\n }\n if (stage === 'code' && (spec === null || spec === undefined))\n return specToOpts(resolveCoderSpec(env), env);\n if (stage === 'qe' && (spec === null || spec === undefined))\n return specToOpts(resolveQeSpec(env), env);\n return specToOpts(spec, env);\n}\nfunction modelLabel(opts) {\n if (opts && opts.agentType === 'codex:codex-rescue') {\n const base = 'codex:' + opts.codexModel + ':' + opts._reasoning;\n return opts._usageSwitched ? base + ' (usage-switched)' : base;\n }\n if (opts && opts.model)\n return opts.model;\n return 'session';\n}\nfunction stageLabel(base, opts) {\n const m = modelLabel(opts);\n return m === 'session' ? base : base + ' · ' + m;\n}\nfunction mergeOpts(base, extra) {\n const out = {};\n for (const k in base)\n out[k] = base[k];\n for (const k in extra)\n out[k] = extra[k];\n return out;\n}",
|
|
69
69
|
},
|
|
70
70
|
"usage-probes": {
|
|
71
71
|
name: "usage-probes",
|
|
72
72
|
version: "1.0.0",
|
|
73
|
-
contentHash: "
|
|
73
|
+
contentHash: "a7d7fecc30202595c05cd31d5c36ee3bfa59f803f3e707e3c28c3ae372bc373b",
|
|
74
74
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-routing.ts",
|
|
75
75
|
requires: ["model-resolver"],
|
|
76
76
|
exports: ["decideUsageAction","OVERRIDE_REASONING","topCodexId"],
|
|
77
|
-
code: "
|
|
77
|
+
code: "const OVERRIDE_REASONING = Object.freeze(STAGE_EFFORT.override);\nfunction decideUsageAction(prevOverride, signal, threshold) {\n if (signal === null || signal === undefined) {\n if (prevOverride)\n return { override: true, action: 'keep' };\n return { override: true, action: 'fail-safe-switch' };\n }\n const s = signal.sessionPct;\n const w = signal.weeklyPct;\n const sKnown = typeof s === 'number' && isFinite(s) && s >= 0;\n const wKnown = typeof w === 'number' && isFinite(w) && w >= 0;\n if ((sKnown && s >= threshold) || (wKnown && w >= threshold)) {\n return { override: true, action: prevOverride ? 'keep' : 'switch' };\n }\n if (sKnown && wKnown) {\n return { override: false, action: prevOverride ? 'restore' : 'none' };\n }\n return { override: prevOverride, action: prevOverride ? 'keep' : 'none' };\n}",
|
|
78
78
|
},
|
|
79
79
|
"codex-dispatch": {
|
|
80
80
|
name: "codex-dispatch",
|
package/src/loop-plan.ts
CHANGED
|
@@ -150,6 +150,11 @@ export interface LoopFanout {
|
|
|
150
150
|
registry: string[];
|
|
151
151
|
/** Hard concurrency bound — REQUIRED, >= 1 (INV-2; unbounded fanout is unrepresentable-invalid). */
|
|
152
152
|
maxFanout: number;
|
|
153
|
+
/** Admission policy when registry length exceeds maxFanout. Absent means window: dispatch all,
|
|
154
|
+
* while maxFanout bounds only concurrent work. */
|
|
155
|
+
overflow?: 'window' | 'truncate';
|
|
156
|
+
/** Mandatory human-readable receipt when overflow:'truncate' deliberately drops work. */
|
|
157
|
+
truncateReason?: string;
|
|
153
158
|
dedup?: boolean;
|
|
154
159
|
reasonRequired?: boolean;
|
|
155
160
|
/** Per-item step chain (pipeline shape) — stepIds run in sequence per member. */
|
|
@@ -371,6 +376,8 @@ export const FIELD_DOMAINS: Record<string, FieldDomain> = {
|
|
|
371
376
|
'LoopFanout.stage': { t: 'string' },
|
|
372
377
|
'LoopFanout.registry': { t: 'string[]' },
|
|
373
378
|
'LoopFanout.maxFanout': { t: 'number' },
|
|
379
|
+
'LoopFanout.overflow': { t: 'enum', values: ['window', 'truncate'] },
|
|
380
|
+
'LoopFanout.truncateReason': { t: 'string' },
|
|
374
381
|
'LoopFanout.dedup': { t: 'boolean' },
|
|
375
382
|
'LoopFanout.reasonRequired': { t: 'boolean' },
|
|
376
383
|
'LoopFanout.chain': { t: 'string[]' },
|
|
@@ -992,6 +999,11 @@ export function validatePlan(plan: LoopPlan): Diagnostic[] {
|
|
|
992
999
|
if (!Array.isArray(f.registry) || f.registry.length === 0) {
|
|
993
1000
|
out.push({ invariant: 'INV-2', path: `$.fanouts[${f.stage}].registry`, message: 'a fanout requires a non-empty member registry' });
|
|
994
1001
|
}
|
|
1002
|
+
if (f.overflow !== undefined && f.overflow !== 'window' && f.overflow !== 'truncate') {
|
|
1003
|
+
out.push({ invariant: 'INV-2b', path: `$.fanouts[${f.stage}].overflow`, message: `overflow must be one of window|truncate (received ${JSON.stringify(f.overflow)})` });
|
|
1004
|
+
} else if (f.overflow === 'truncate' && (typeof f.truncateReason !== 'string' || f.truncateReason.trim() === '')) {
|
|
1005
|
+
out.push({ invariant: 'INV-2b', path: `$.fanouts[${f.stage}].overflow`, message: 'overflow:"truncate" requires a non-blank truncateReason — deliberate dropped work needs a receipt' });
|
|
1006
|
+
}
|
|
995
1007
|
});
|
|
996
1008
|
|
|
997
1009
|
// INV-3: every fanout (parallel region) names an explicit join with a closed-set joinPolicy.
|
|
@@ -1388,7 +1400,7 @@ export function toLintProjection(plan: LoopPlan): LintProjection {
|
|
|
1388
1400
|
}
|
|
1389
1401
|
|
|
1390
1402
|
export interface TraceProjection {
|
|
1391
|
-
kind: 'trace-projection/
|
|
1403
|
+
kind: 'trace-projection/2';
|
|
1392
1404
|
/** Expected happens-before edges: every dispatch of `step` must come after the settle of each
|
|
1393
1405
|
* dep. Only DISPATCHING step kinds (agent/gate) appear — fanout/join/pause are structural
|
|
1394
1406
|
* pseudo-steps that emit no trace events of their own (their ordering lives in `regions`). */
|
|
@@ -1396,7 +1408,20 @@ export interface TraceProjection {
|
|
|
1396
1408
|
/** Parallel regions with the runtime bound + concurrency shape the trace must exhibit.
|
|
1397
1409
|
* `after` = the dispatching steps that depend on the region (fanout or join) — the trace-visible
|
|
1398
1410
|
* witnesses of the barrier. */
|
|
1399
|
-
regions: {
|
|
1411
|
+
regions: {
|
|
1412
|
+
fanout: string;
|
|
1413
|
+
join: string;
|
|
1414
|
+
joinPolicy: string;
|
|
1415
|
+
maxFanout: number;
|
|
1416
|
+
members: string[];
|
|
1417
|
+
shape: ConcurrencyShape;
|
|
1418
|
+
after: string[];
|
|
1419
|
+
/** Projection/2 evidence used to prove positional dispatch coverage. */
|
|
1420
|
+
registry: string[];
|
|
1421
|
+
registrySize: number;
|
|
1422
|
+
overflow: 'window' | 'truncate';
|
|
1423
|
+
dedup: boolean;
|
|
1424
|
+
}[];
|
|
1400
1425
|
/** Steps expected to appear in the trace, keyed by stepId (iteration/attempt keys are runtime axes). */
|
|
1401
1426
|
expectedSteps: string[];
|
|
1402
1427
|
}
|
|
@@ -1432,10 +1457,14 @@ export function toTraceProjection(plan: LoopPlan): TraceProjection {
|
|
|
1432
1457
|
members: [...(f.chain ?? [])],
|
|
1433
1458
|
shape: shapes.get(f.stage) ?? 'barrier',
|
|
1434
1459
|
after,
|
|
1460
|
+
registry: [...f.registry],
|
|
1461
|
+
registrySize: f.registry.length,
|
|
1462
|
+
overflow: f.overflow ?? 'window',
|
|
1463
|
+
dedup: f.dedup === true,
|
|
1435
1464
|
};
|
|
1436
1465
|
});
|
|
1437
1466
|
return {
|
|
1438
|
-
kind: 'trace-projection/
|
|
1467
|
+
kind: 'trace-projection/2',
|
|
1439
1468
|
happensBefore,
|
|
1440
1469
|
regions,
|
|
1441
1470
|
expectedSteps: norm.steps.filter((s) => s.kind === 'agent' || s.kind === 'gate').map((s) => s.stepId),
|
|
@@ -1501,6 +1530,8 @@ export interface RunBoundary {
|
|
|
1501
1530
|
maxFanout: number;
|
|
1502
1531
|
registry: string[];
|
|
1503
1532
|
dedup: boolean;
|
|
1533
|
+
overflow: 'window' | 'truncate';
|
|
1534
|
+
truncateReason: string | null;
|
|
1504
1535
|
shape: ConcurrencyShape;
|
|
1505
1536
|
chain: RunStepSpec[];
|
|
1506
1537
|
};
|
|
@@ -1586,6 +1617,8 @@ export function toRunProjection(plan: LoopPlan): RunProjection {
|
|
|
1586
1617
|
maxFanout: typeof f?.maxFanout === 'number' ? f.maxFanout : 0,
|
|
1587
1618
|
registry: [...(f?.registry ?? [])],
|
|
1588
1619
|
dedup: f?.dedup === true,
|
|
1620
|
+
overflow: f?.overflow ?? 'window',
|
|
1621
|
+
truncateReason: typeof f?.truncateReason === 'string' ? f.truncateReason : null,
|
|
1589
1622
|
shape: s.concurrency ?? 'barrier',
|
|
1590
1623
|
chain: (f?.chain ?? []).map((c) => byId.get(c)).filter((c): c is LoopStep => c !== undefined).map(specOf),
|
|
1591
1624
|
},
|
package/src/loop-render.ts
CHANGED
|
@@ -431,9 +431,18 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
|
|
|
431
431
|
lines.push(G(`step:${id} kind=fanout shape=${shape} maxFanout=${f.maxFanout}`));
|
|
432
432
|
for (const ms of members) lines.push(...stepPromptAssembly(ms, plan));
|
|
433
433
|
lines.push(`const REGISTRY_${ident(id)} = ${JSON.stringify(f.registry)}`);
|
|
434
|
-
// dedup (enacted — QE round-3 B1): a declared dedup DEDUPLICATES the registry before
|
|
434
|
+
// dedup (enacted — QE round-3 B1): a declared dedup DEDUPLICATES the registry before admission.
|
|
435
435
|
const registryExpr = f.dedup === true ? `REGISTRY_${ident(id)}.filter(function (x, i) { return REGISTRY_${ident(id)}.indexOf(x) === i })` : `REGISTRY_${ident(id)}`;
|
|
436
|
-
|
|
436
|
+
if (f.overflow === 'truncate') {
|
|
437
|
+
const reason = f.truncateReason ?? '';
|
|
438
|
+
const commentReason = reason.replace(/\r?\n/g, ' ').replace(/\*\//g, '* /');
|
|
439
|
+
lines.push(`// ==================== FANOUT TRUNCATION DECLARED: ${commentReason} ====================`);
|
|
440
|
+
lines.push(`const MEMBERS_${ident(id)} = ${registryExpr}.slice(0, ${f.maxFanout}) // declared truncation (INV-2b): intentionally drops registry positions beyond maxFanout`);
|
|
441
|
+
lines.push(`console.error('[fanout-truncated] ${id}: ' + MEMBERS_${ident(id)}.length + ' of ' + REGISTRY_${ident(id)}.length + ' items — ' + ${jsString(reason)})`);
|
|
442
|
+
lines.push(`if (__hooks.onFanoutTruncated) { __hooks.onFanoutTruncated({ stage: ${jsString(id)}, registrySize: REGISTRY_${ident(id)}.length, dispatched: MEMBERS_${ident(id)}.length, reason: ${jsString(reason)} }) }`);
|
|
443
|
+
} else {
|
|
444
|
+
lines.push(`const MEMBERS_${ident(id)} = ${registryExpr} // full registry (INV-2b): every item dispatched; maxFanout bounds CONCURRENCY, not work`);
|
|
445
|
+
}
|
|
437
446
|
// ROUND-6 B3 SHAPE: the region's awaits (member dispatches via parallel + the join) ride ONE
|
|
438
447
|
// settle-routed try, mirroring the per-step shape. Member runStep failures settle themselves
|
|
439
448
|
// durably before rejecting; the outer catch is the structural belt for the region as a whole.
|
|
@@ -445,7 +454,7 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
|
|
|
445
454
|
// dispatch(B:item1) is allocated a seq before settle(A:item3). Predecessor by POSITION (G15),
|
|
446
455
|
// qualified by the branch OCCURRENCE (round 7 — duplicate registry values are distinct
|
|
447
456
|
// branches, so a chain predecessor is looked up per occurrence, never per value).
|
|
448
|
-
lines.push(` R_${ident(id)} = await
|
|
457
|
+
lines.push(` R_${ident(id)} = await __drainAllWindowed(MEMBERS_${ident(id)}.map((it, __ix) => async () => {`);
|
|
449
458
|
let prev: string | null = null;
|
|
450
459
|
for (let ci = 0; ci < members.length; ci++) {
|
|
451
460
|
const cs = members[ci] as LoopStep;
|
|
@@ -457,13 +466,13 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
|
|
|
457
466
|
prev = `v_${ident(cs.stepId)}`;
|
|
458
467
|
}
|
|
459
468
|
lines.push(` return ${prev ?? 'null'}`);
|
|
460
|
-
lines.push(` }))`);
|
|
469
|
+
lines.push(` }), ${f.maxFanout})`);
|
|
461
470
|
} else {
|
|
462
471
|
// BARRIER shape: all members dispatched, one join closes the region (exactly one member step —
|
|
463
472
|
// MEMBER-2; extra barrier chain entries used to be silently never dispatched).
|
|
464
473
|
const ms = members[0] as LoopStep;
|
|
465
474
|
const call = stepCallExpr(ms, memberDeps(ms), { chainCausedBy: null, inputExpr: null });
|
|
466
|
-
lines.push(` R_${ident(id)} = await
|
|
475
|
+
lines.push(` R_${ident(id)} = await __drainAllWindowed(MEMBERS_${ident(id)}.map((it, __ix) => () => ${call}), ${f.maxFanout})`);
|
|
467
476
|
}
|
|
468
477
|
lines.push(` J_${ident(id)} = await __joinSettled(${jsString(j.stage)}, ${jsString(fanoutStep.phase)}, R_${ident(id)}, { policy: ${jsString(j.joinPolicy)}, onInvalid: ${jsString(j.onInvalid ?? 'named-failure')}, region: ${jsString(id)} })`);
|
|
469
478
|
lines.push(`} catch (__stepErr) { await __settleStep({ stepId: ${jsString(id)}, phase: ${jsString(fanoutStep.phase)}, outcome: 'failed', error: __stepErr }) }`);
|
|
@@ -478,6 +487,7 @@ function renderFanout(plan: LoopPlan, fanoutStep: LoopStep): string {
|
|
|
478
487
|
* attach hooks INSIDE it, and when opted out the hook sites are no-ops). */
|
|
479
488
|
function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: LoopBlob[]): string {
|
|
480
489
|
const traceOn = plan.trace?.emit === true;
|
|
490
|
+
const hasDeclaredTruncation = (plan.fanouts ?? []).some((f) => f.overflow === 'truncate');
|
|
481
491
|
const ckptOn = plan.checkpointing?.enabled === true || plan.subsystems?.checkpoints === true;
|
|
482
492
|
// budget: declared per-step budgets PLUS the declared gate-redo allowance (QE round-3 B1 — a
|
|
483
493
|
// plan-declared redo must be affordable; an undeclared one still hits the guard loudly). The
|
|
@@ -505,7 +515,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
505
515
|
lines.push(`let __ledgerDone = false`);
|
|
506
516
|
if (traceOn) lines.push(`let __faLegWarned = false`);
|
|
507
517
|
lines.push(`function __spendBudget(stepId) { if (__budget.left <= 0) { throw new Error('loop budget exhausted before ' + stepId) } __budget.left-- }`);
|
|
508
|
-
lines.push(`const __hooks = { onDispatch: null, onSettle: null }`);
|
|
518
|
+
lines.push(`const __hooks = { onDispatch: null, onSettle: null${hasDeclaredTruncation ? ', onFanoutTruncated: null' : ''} }`);
|
|
509
519
|
lines.push(`const __settled = {}`);
|
|
510
520
|
lines.push(`// settle identity is PER-OCCURRENCE (round-7; Codex round-6 R2: with dedup:false and a`);
|
|
511
521
|
lines.push(`// duplicated registry value, two branches shared one (stepId,itemKey) slot — the second`);
|
|
@@ -650,12 +660,46 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
650
660
|
lines.push(` if (__primary !== null) { throw __primary.error } // drained FIRST, then the primary failure propagates`);
|
|
651
661
|
lines.push(` return __results`);
|
|
652
662
|
lines.push(`}`);
|
|
663
|
+
lines.push(`async function __drainAllWindowed(thunks, limit) {`);
|
|
664
|
+
lines.push(` const __limit = Math.max(1, Math.floor(Number(limit) || 1))`);
|
|
665
|
+
lines.push(` if (thunks.length <= __limit) { return __drainAll(thunks) }`);
|
|
666
|
+
lines.push(` const __out = new Array(thunks.length)`);
|
|
667
|
+
lines.push(` const __values = new Array(thunks.length)`);
|
|
668
|
+
lines.push(` let __next = 0`);
|
|
669
|
+
lines.push(` let __order = 0`);
|
|
670
|
+
lines.push(` const __workers = []`);
|
|
671
|
+
lines.push(` const __workerCount = Math.min(__limit, thunks.length)`);
|
|
672
|
+
lines.push(` for (let w = 0; w < __workerCount; w++) {`);
|
|
673
|
+
lines.push(` __workers.push(async function () {`);
|
|
674
|
+
lines.push(` while (__next < thunks.length) {`);
|
|
675
|
+
lines.push(` const i = __next++`);
|
|
676
|
+
lines.push(` try { const v = await thunks[i](); __values[i] = v; __out[i] = { ok: true, value: v, at: __order++ } }`);
|
|
677
|
+
lines.push(` catch (e) { __out[i] = { ok: false, error: e, at: __order++ } }`);
|
|
678
|
+
lines.push(` }`);
|
|
679
|
+
lines.push(` })`);
|
|
680
|
+
lines.push(` }`);
|
|
681
|
+
lines.push(` await parallel(__workers) // workers never reject ⇒ all registry positions drain to SETTLEMENT`);
|
|
682
|
+
lines.push(` let __primary = null`);
|
|
683
|
+
lines.push(` for (let i = 0; i < __out.length; i++) {`);
|
|
684
|
+
lines.push(` const o = __out[i]`);
|
|
685
|
+
lines.push(` if (o && o.ok !== true && (__primary === null || o.at < __primary.at)) { __primary = o }`);
|
|
686
|
+
lines.push(` }`);
|
|
687
|
+
lines.push(` if (__primary !== null) { throw __primary.error }`);
|
|
688
|
+
lines.push(` return __values`);
|
|
689
|
+
lines.push(`}`);
|
|
653
690
|
}
|
|
654
691
|
if (traceOn) {
|
|
655
692
|
lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
|
|
656
693
|
lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP, 'rendered-script')`);
|
|
657
694
|
lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
|
|
658
695
|
lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
|
|
696
|
+
if (hasDeclaredTruncation) {
|
|
697
|
+
lines.push(`__hooks.onFanoutTruncated = function (e) {`);
|
|
698
|
+
lines.push(` const ev = { v: 1, runId: __traceState.runId, seq: ++__traceState.seq, event: 'fanout-truncated', stage: e.stage, registrySize: e.registrySize, dispatched: e.dispatched, reason: e.reason }`);
|
|
699
|
+
lines.push(` __traceState.buffer.push(JSON.stringify(ev))`);
|
|
700
|
+
lines.push(` return ev.seq`);
|
|
701
|
+
lines.push(`}`);
|
|
702
|
+
}
|
|
659
703
|
lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
|
|
660
704
|
lines.push(` if (TRACE_FILE === null) { return }`);
|
|
661
705
|
lines.push(` // cmd must be let: the trace payload stays LEFT and must never be replaced by the fa-record panel leg; both ride the SAME writer agent.`);
|