@dzhechkov/harness-core 0.8.38 → 0.8.40
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 +281 -161
- package/README.md +145 -2
- package/dist/agentdb-index.d.ts +3 -3
- package/dist/agentdb-index.js +5 -5
- package/dist/agentdb-index.js.map +1 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +4 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog.d.ts +2 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +3 -2
- package/dist/backlog.js.map +1 -1
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +9 -2
- package/dist/brain.js.map +1 -1
- package/dist/bto-optimize.d.ts.map +1 -1
- package/dist/bto-optimize.js +9 -12
- package/dist/bto-optimize.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +50 -14
- package/dist/claim-check.js.map +1 -1
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +48 -2
- package/dist/cmd-usage.js.map +1 -1
- package/dist/compounding.d.ts +66 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +76 -9
- package/dist/compounding.js.map +1 -1
- package/dist/doctor-instrument.d.ts +65 -0
- package/dist/doctor-instrument.d.ts.map +1 -0
- package/dist/doctor-instrument.js +91 -0
- package/dist/doctor-instrument.js.map +1 -0
- package/dist/experiment-assign.d.ts +110 -0
- package/dist/experiment-assign.d.ts.map +1 -0
- package/dist/experiment-assign.js +229 -0
- package/dist/experiment-assign.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +7 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +20 -5
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +12 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -1
- package/dist/feature-adr-envelope.js +12 -1
- package/dist/feature-adr-envelope.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +4 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +4 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/feature-tier.d.ts.map +1 -1
- package/dist/feature-tier.js +13 -1
- package/dist/feature-tier.js.map +1 -1
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +15 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger-cost-fill.d.ts +58 -0
- package/dist/ledger-cost-fill.d.ts.map +1 -0
- package/dist/ledger-cost-fill.js +78 -0
- package/dist/ledger-cost-fill.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +59 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +65 -1
- package/dist/mutation-gate.js.map +1 -1
- package/dist/name-check.d.ts +20 -1
- package/dist/name-check.d.ts.map +1 -1
- package/dist/name-check.js +42 -1
- package/dist/name-check.js.map +1 -1
- package/dist/no-stubs.d.ts +10 -0
- package/dist/no-stubs.d.ts.map +1 -1
- package/dist/no-stubs.js +13 -9
- package/dist/no-stubs.js.map +1 -1
- package/dist/operations.d.ts +13 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +88 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +24 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +49 -9
- package/dist/patterns.js.map +1 -1
- package/dist/publish-source-scope.d.ts +32 -0
- package/dist/publish-source-scope.d.ts.map +1 -0
- package/dist/publish-source-scope.js +53 -0
- package/dist/publish-source-scope.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +32 -21
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +4 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +21 -11
- package/dist/qe-bridge.js.map +1 -1
- package/dist/rake-analyzer.d.ts +10 -3
- package/dist/rake-analyzer.d.ts.map +1 -1
- package/dist/rake-analyzer.js +84 -22
- package/dist/rake-analyzer.js.map +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +8 -4
- package/dist/recap.js.map +1 -1
- package/dist/registry.d.ts +58 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +62 -5
- package/dist/registry.js.map +1 -1
- package/dist/release.d.ts +20 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +48 -4
- package/dist/release.js.map +1 -1
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +11 -1
- package/dist/reqe.js.map +1 -1
- package/dist/round-exec.d.ts +10 -0
- package/dist/round-exec.d.ts.map +1 -1
- package/dist/round-exec.js +3 -2
- package/dist/round-exec.js.map +1 -1
- package/dist/round.d.ts +19 -0
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +1 -0
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +18 -4
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +61 -7
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +8 -1
- package/dist/score.js.map +1 -1
- package/dist/sign.d.ts +23 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +8 -5
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +59 -7
- package/dist/skills-verify.js.map +1 -1
- package/dist/store-guard-prune.d.ts +22 -0
- package/dist/store-guard-prune.d.ts.map +1 -0
- package/dist/store-guard-prune.js +54 -0
- package/dist/store-guard-prune.js.map +1 -0
- package/dist/sweep-failure-classify.d.ts +45 -0
- package/dist/sweep-failure-classify.d.ts.map +1 -0
- package/dist/sweep-failure-classify.js +90 -0
- package/dist/sweep-failure-classify.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +14 -2
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +14 -2
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +460 -160
- package/src/agentdb-index.ts +5 -5
- package/src/amendment-trace.ts +4 -1
- package/src/backlog.ts +3 -2
- package/src/brain.ts +8 -2
- package/src/bto-optimize.ts +10 -8
- package/src/claim-check.ts +48 -12
- package/src/cmd-usage.ts +52 -2
- package/src/compounding.ts +112 -9
- package/src/doctor-instrument.ts +153 -0
- package/src/experiment-assign.ts +276 -0
- package/src/feature-adr-checkpoints.ts +20 -5
- package/src/feature-adr-envelope.ts +24 -1
- package/src/feature-adr-routing.ts +4 -0
- package/src/feature-tier.ts +14 -1
- package/src/guard.ts +24 -0
- package/src/index.ts +22 -5
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +113 -1
- package/src/name-check.ts +43 -1
- package/src/no-stubs.ts +14 -5
- package/src/operations.ts +85 -3
- package/src/patterns.ts +49 -8
- package/src/publish-source-scope.ts +53 -0
- package/src/publish.ts +38 -16
- package/src/qe-bridge.ts +22 -11
- package/src/rake-analyzer.ts +75 -20
- package/src/rake-signatures.json +98 -0
- package/src/recap.ts +8 -4
- package/src/registry.ts +91 -1
- package/src/release.ts +54 -4
- package/src/reqe.ts +11 -1
- package/src/round-exec.ts +13 -2
- package/src/round.ts +20 -0
- package/src/run-records.ts +78 -10
- package/src/score.ts +8 -1
- package/src/sign.ts +53 -0
- package/src/skills-verify.ts +69 -9
- package/src/store-guard-prune.ts +81 -0
- package/src/sweep-failure-classify.ts +88 -0
- package/src/workflow-run-dispatch.ts +14 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, blocked arm assignment for a prospective ablation (ADR-001, ablation-c-start).
|
|
3
|
+
*
|
|
4
|
+
* WHY. The owner's question — does the Step-8 QE mode change task speed — is only answerable
|
|
5
|
+
* causally if the variant a task receives is decided by chance, BEFORE the work starts, and tied to
|
|
6
|
+
* the task's own identity so a repeated query for the same task never reassigns it. This module is
|
|
7
|
+
* that decision: a PURE function of `(experiment, stratum, seed, index)`. No filesystem, no clock,
|
|
8
|
+
* no network — the caller (the cli) owns the journal, the lock, and the "did work already start?"
|
|
9
|
+
* check; this module only computes.
|
|
10
|
+
*
|
|
11
|
+
* ALGORITHM — block randomization, one pair per arm-combination per block. `mulberry32` is the
|
|
12
|
+
* repo's ONLY pseudo-random generator (`compounding.ts` — "no second RNG in this repo"); reused
|
|
13
|
+
* here rather than adding a second stream. Block size is `arms.length * 2` — two occurrences of
|
|
14
|
+
* every arm per block, so a completed block is always perfectly balanced and `propensity` (the
|
|
15
|
+
* arm's actual share of its block) is a fixed `1 / arms.length` — never guessed, never assumed
|
|
16
|
+
* 0.5 by default (NFR-4). The block's own permutation is seeded from
|
|
17
|
+
* `fnv1a(JSON.stringify([experiment, stratum, seed, block]))` (a JSON-tuple, not a delimiter join —
|
|
18
|
+
* the same reason `workOrderDigestInput` in epoch-replay.ts uses tuples: a delimiter in `experiment`
|
|
19
|
+
* or `stratum` could otherwise make two different keys hash identically) — so different strata (and
|
|
20
|
+
* different experiments) never share a stream, and the SAME `(experiment, stratum, seed, index)`
|
|
21
|
+
* always rederives the SAME arm (A1 idempotency, A2 block balance, A3 propensity).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { fnv1a } from './feature-adr-checkpoints.js';
|
|
25
|
+
import { mulberry32 } from './compounding.js';
|
|
26
|
+
|
|
27
|
+
export interface AssignArmInput {
|
|
28
|
+
readonly experiment: string;
|
|
29
|
+
readonly stratum: string;
|
|
30
|
+
/** Pre-registered seed — fixed once, before any assignment is made. */
|
|
31
|
+
readonly seed: number;
|
|
32
|
+
/** 0-based ordinal of this task within its `(experiment, stratum)` sequence. */
|
|
33
|
+
readonly index: number;
|
|
34
|
+
/** The arms on offer, e.g. `['direct', 'reference']`. At least 2, all non-empty, all unique. */
|
|
35
|
+
readonly arms: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type AssignArmResult =
|
|
39
|
+
| { readonly ok: true; readonly arm: string; readonly propensity: number; readonly block: number; readonly position: number }
|
|
40
|
+
| { readonly ok: false; readonly reason: string };
|
|
41
|
+
|
|
42
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
43
|
+
return typeof v === 'string' && v.trim() !== '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fisher-Yates, driven by the given deterministic RNG. Does not mutate its input. */
|
|
47
|
+
function shuffle<T>(items: readonly T[], rand: () => number): T[] {
|
|
48
|
+
const out = items.slice();
|
|
49
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
50
|
+
const j = Math.floor(rand() * (i + 1));
|
|
51
|
+
const tmp = out[i]!;
|
|
52
|
+
out[i] = out[j]!;
|
|
53
|
+
out[j] = tmp;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Deterministically assign one arm to task ordinal `index` within `(experiment, stratum)`. Refuses
|
|
60
|
+
* (never throws, never guesses) on any malformed input — empty strings, a non-integer seed/index, a
|
|
61
|
+
* negative index, fewer than two arms, duplicate or blank arm names.
|
|
62
|
+
*/
|
|
63
|
+
export function assignArm(input: AssignArmInput): AssignArmResult {
|
|
64
|
+
if (!isNonEmptyString(input?.experiment)) return { ok: false, reason: 'experiment: expected a non-empty string' };
|
|
65
|
+
if (!isNonEmptyString(input?.stratum)) return { ok: false, reason: 'stratum: expected a non-empty string' };
|
|
66
|
+
if (!Number.isInteger(input?.seed) || input.seed < 0) return { ok: false, reason: 'seed: expected a non-negative integer' };
|
|
67
|
+
if (!Number.isInteger(input?.index) || input.index < 0) return { ok: false, reason: 'index: expected a non-negative integer' };
|
|
68
|
+
if (!Array.isArray(input?.arms) || input.arms.length < 2) {
|
|
69
|
+
return { ok: false, reason: 'arms: expected an array of at least 2 arm names' };
|
|
70
|
+
}
|
|
71
|
+
if (!input.arms.every(isNonEmptyString)) return { ok: false, reason: 'arms: every arm name must be a non-empty string' };
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
for (const a of input.arms) {
|
|
74
|
+
if (seen.has(a)) return { ok: false, reason: `arms: duplicate arm name ${JSON.stringify(a)}` };
|
|
75
|
+
seen.add(a);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const blockSize = input.arms.length * 2; // two copies of every arm — always a perfect block
|
|
79
|
+
const block = Math.floor(input.index / input.arms.length / 2); // == Math.floor(index / blockSize)
|
|
80
|
+
const position = input.index % blockSize;
|
|
81
|
+
|
|
82
|
+
const key = JSON.stringify({ experiment: input.experiment, stratum: input.stratum, seed: input.seed, block });
|
|
83
|
+
const rand = mulberry32(parseInt(fnv1a(key), 16) >>> 0);
|
|
84
|
+
// Base card set for this block: every arm twice, in the input's own order — then permuted by the
|
|
85
|
+
// block-seeded stream, so the CONTENT of every block is fixed by construction (perfect balance)
|
|
86
|
+
// and only the ORDER is randomized.
|
|
87
|
+
const cards: string[] = [];
|
|
88
|
+
for (const a of input.arms) { cards.push(a); cards.push(a); }
|
|
89
|
+
const permuted = shuffle(cards, rand);
|
|
90
|
+
const arm = permuted[position]!;
|
|
91
|
+
const propensity = 2 / blockSize; // == 1 / arms.length — the arm's fixed, actual share of its block
|
|
92
|
+
|
|
93
|
+
return { ok: true, arm, propensity, block, position };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AssignmentRecord {
|
|
97
|
+
readonly ts: string;
|
|
98
|
+
readonly experiment: string;
|
|
99
|
+
readonly taskId: string;
|
|
100
|
+
readonly stratum: string;
|
|
101
|
+
readonly seed: number;
|
|
102
|
+
readonly index: number;
|
|
103
|
+
readonly block: number;
|
|
104
|
+
readonly position: number;
|
|
105
|
+
readonly arm: string;
|
|
106
|
+
readonly propensity: number;
|
|
107
|
+
readonly assignedAt: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ReadAssignmentsResult {
|
|
111
|
+
readonly records: readonly AssignmentRecord[];
|
|
112
|
+
/** Non-blank lines that did not parse as a valid assignment record. Never silently dropped: a
|
|
113
|
+
* caller checking "is this task assigned?" must be able to tell "no" from "the journal is
|
|
114
|
+
* unreadable here" (A5). */
|
|
115
|
+
readonly malformedLines: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** fix-round-1 (Codex r1 HIGH #7): `new Date().toISOString()` is the ONLY producer of `ts`/
|
|
119
|
+
* `assignedAt` (see the cli's `experimentJournalPath` writer) — a record whose timestamp does not
|
|
120
|
+
* match that exact shape did not come from this code path, so it is corruption, not merely an
|
|
121
|
+
* unusual value. */
|
|
122
|
+
const ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
123
|
+
|
|
124
|
+
function isValidIsoTimestamp(v: unknown): v is string {
|
|
125
|
+
// Lead delta after Codex r2 (MEDIUM): shape + `Date.parse` accepts impossible calendar dates —
|
|
126
|
+
// `2026-02-30T00:00:00.000Z` parses (it rolls into March) and passed. Round-tripping through
|
|
127
|
+
// `toISOString()` is the exact check: only a real instant re-serializes to the same string.
|
|
128
|
+
if (typeof v !== 'string' || !ISO_TIMESTAMP_RE.test(v)) return false;
|
|
129
|
+
const ms = Date.parse(v);
|
|
130
|
+
return Number.isFinite(ms) && new Date(ms).toISOString() === v;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* PURELY STRUCTURAL: every field is present and the right TYPE. This is `readAssignments`'s gate —
|
|
135
|
+
* it decides whether a journal LINE parses at all (A5's `malformedLines`). It deliberately does NOT
|
|
136
|
+
* validate timestamp FORMAT, propensity RANGE, or arm MEMBERSHIP: a line that is well-typed but
|
|
137
|
+
* semantically wrong (a hand-edited `position:99`, `propensity:2`, a flipped `arm`) is exactly the
|
|
138
|
+
* "syntactically valid corruption" `verifyAssignmentRecord` below exists to catch — folding those
|
|
139
|
+
* checks in here would make a single semantically-tampered record poison `readAssignments` for
|
|
140
|
+
* every caller, including ones (like `status`'s per-task duration math) that need to name WHICH
|
|
141
|
+
* task is bad rather than refuse the whole read (fix-round-1 HIGH #7/#8 — see `verifyAssignmentRecord`).
|
|
142
|
+
*/
|
|
143
|
+
function isValidAssignmentRecord(v: unknown): v is AssignmentRecord {
|
|
144
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
145
|
+
const r = v as Record<string, unknown>;
|
|
146
|
+
return (
|
|
147
|
+
isNonEmptyString(r['ts']) &&
|
|
148
|
+
isNonEmptyString(r['experiment']) &&
|
|
149
|
+
isNonEmptyString(r['taskId']) &&
|
|
150
|
+
isNonEmptyString(r['stratum']) &&
|
|
151
|
+
Number.isInteger(r['seed']) && (r['seed'] as number) >= 0 &&
|
|
152
|
+
Number.isInteger(r['index']) && (r['index'] as number) >= 0 &&
|
|
153
|
+
Number.isInteger(r['block']) && (r['block'] as number) >= 0 &&
|
|
154
|
+
Number.isInteger(r['position']) && (r['position'] as number) >= 0 &&
|
|
155
|
+
isNonEmptyString(r['arm']) &&
|
|
156
|
+
typeof r['propensity'] === 'number' && Number.isFinite(r['propensity'] as number) &&
|
|
157
|
+
isNonEmptyString(r['assignedAt'])
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type VerifyAssignmentResult =
|
|
162
|
+
| { readonly ok: true }
|
|
163
|
+
| {
|
|
164
|
+
readonly ok: false;
|
|
165
|
+
readonly reason: string;
|
|
166
|
+
/** Lead delta after Codex r2: `null` when the record failed on its SEED — there is nothing to
|
|
167
|
+
* expect, because the tuple it would be derived from is itself the thing under suspicion. */
|
|
168
|
+
readonly expected: { readonly arm: string; readonly block: number; readonly position: number; readonly propensity: number } | null;
|
|
169
|
+
readonly actual: { readonly arm: string; readonly block: number; readonly position: number; readonly propensity: number };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* fix-round-1 (Codex r1 HIGH #7) — "syntactically valid journal corruption is accepted and returned
|
|
174
|
+
* as the task's assignment". A record that passes `isValidAssignmentRecord`'s SHAPE checks can still
|
|
175
|
+
* have been hand-edited to a different arm/block/position/propensity while keeping every field the
|
|
176
|
+
* right TYPE (e.g. flipping `arm:"direct"` to `arm:"reference"`, or `position:99`). The only way to
|
|
177
|
+
* catch that is to REDERIVE the assignment from the record's own tuple
|
|
178
|
+
* `(experiment, stratum, seed, index)` via the SAME pure `assignArm` that produced it, and compare —
|
|
179
|
+
* never trust the stored arm/block/position/propensity on their own. `arms` is the experiment's own
|
|
180
|
+
* registered arm set (from its `dz experiment init` config), passed in by the caller — this module
|
|
181
|
+
* stays pure and fs-free.
|
|
182
|
+
*/
|
|
183
|
+
export function verifyAssignmentRecord(record: AssignmentRecord, arms: readonly string[], pinnedSeed?: number): VerifyAssignmentResult {
|
|
184
|
+
// Lead delta after Codex r2 (BLOCKER): rederiving from `record.seed` proves the record is
|
|
185
|
+
// internally CONSISTENT, never that it is AUTHENTIC — a row carrying any seed at all passes,
|
|
186
|
+
// because the check compares the row with itself. The experiment's pinned seed (from its
|
|
187
|
+
// `dz experiment init` config, the one value fixed before the first assignment) is the trusted
|
|
188
|
+
// side of the comparison; a row that disagrees with it is tampered, however well-formed.
|
|
189
|
+
if (pinnedSeed !== undefined && record.seed !== pinnedSeed) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)} carries seed ${record.seed}, but experiment ${JSON.stringify(record.experiment)} is pinned to seed ${pinnedSeed} — a record's own seed can never authenticate it`,
|
|
193
|
+
expected: null,
|
|
194
|
+
actual: { arm: record.arm, block: record.block, position: record.position, propensity: record.propensity },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const recomputed = assignArm({ experiment: record.experiment, stratum: record.stratum, seed: record.seed, index: record.index, arms });
|
|
198
|
+
const actual = { arm: record.arm, block: record.block, position: record.position, propensity: record.propensity };
|
|
199
|
+
const expected = recomputed.ok
|
|
200
|
+
? { arm: recomputed.arm, block: recomputed.block, position: recomputed.position, propensity: recomputed.propensity }
|
|
201
|
+
: { arm: '(tuple refused)', block: -1, position: -1, propensity: -1 };
|
|
202
|
+
|
|
203
|
+
if (!recomputed.ok) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)}'s stored tuple (experiment=${JSON.stringify(record.experiment)}, stratum=${JSON.stringify(record.stratum)}, seed=${record.seed}, index=${record.index}) no longer rederives a valid assignment (${recomputed.reason}) — the stored record does not match what assignArm would produce from its own tuple`,
|
|
207
|
+
expected,
|
|
208
|
+
actual,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (expected.arm !== actual.arm || expected.block !== actual.block || expected.position !== actual.position || expected.propensity !== actual.propensity) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)}'s stored assignment does not match what assignArm rederives from its own tuple (experiment=${JSON.stringify(record.experiment)}, stratum=${JSON.stringify(record.stratum)}, seed=${record.seed}, index=${record.index}) — the journal line was modified after it was written`,
|
|
215
|
+
expected,
|
|
216
|
+
actual,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// fix-round-1 HIGH #7 (named explicitly in the brief, beyond what recompute-and-compare already
|
|
220
|
+
// implies): timestamp FORMAT, propensity RANGE, and arm MEMBERSHIP. Recompute already makes a
|
|
221
|
+
// WRONG arm/propensity/block/position fail above; these three checks catch what recompute cannot
|
|
222
|
+
// — `assignArm` never produces a `ts`/`assignedAt` at all (they are wall-clock, not derived from
|
|
223
|
+
// the tuple), so a hand-edited "not-a-date" needs its own check.
|
|
224
|
+
if (!isValidIsoTimestamp(record.ts) || !isValidIsoTimestamp(record.assignedAt)) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)}'s ts/assignedAt is not a valid ISO-8601 UTC timestamp (ts=${JSON.stringify(record.ts)}, assignedAt=${JSON.stringify(record.assignedAt)})`,
|
|
228
|
+
expected,
|
|
229
|
+
actual,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (!(record.propensity >= 0 && record.propensity <= 1)) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)}'s propensity ${record.propensity} is outside the valid [0,1] range`,
|
|
236
|
+
expected,
|
|
237
|
+
actual,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (!arms.includes(record.arm)) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: `assignment-tampered: task ${JSON.stringify(record.taskId)}'s arm ${JSON.stringify(record.arm)} is not one of this experiment's registered arms (${arms.join(', ')})`,
|
|
244
|
+
expected,
|
|
245
|
+
actual,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return { ok: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Parse a JSONL assignments journal (one record per line) into records, WITHOUT touching the
|
|
253
|
+
* filesystem — the caller reads the file, this only parses its text (NFR-1: the core stays
|
|
254
|
+
* `node:fs`-free). A line that is blank is skipped silently (append-only files end in a trailing
|
|
255
|
+
* newline); a line that is non-blank but does not parse as JSON, or parses to something missing a
|
|
256
|
+
* required field, counts toward `malformedLines` and is otherwise ignored — corruption is named, not
|
|
257
|
+
* folded into "not assigned" (A5).
|
|
258
|
+
*/
|
|
259
|
+
export function readAssignments(text: string): ReadAssignmentsResult {
|
|
260
|
+
const records: AssignmentRecord[] = [];
|
|
261
|
+
let malformedLines = 0;
|
|
262
|
+
for (const raw of String(text ?? '').split('\n')) {
|
|
263
|
+
const line = raw.trim();
|
|
264
|
+
if (line === '') continue;
|
|
265
|
+
let parsed: unknown;
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(line);
|
|
268
|
+
} catch {
|
|
269
|
+
malformedLines++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (isValidAssignmentRecord(parsed)) records.push(parsed);
|
|
273
|
+
else malformedLines++;
|
|
274
|
+
}
|
|
275
|
+
return { records, malformedLines };
|
|
276
|
+
}
|
|
@@ -267,11 +267,11 @@ export function decideCheckpointResume(opts: {
|
|
|
267
267
|
/** Serialize one checkpoint line, or null when the result is null/oversize/unserializable —
|
|
268
268
|
* the caller logs the skip loudly; a missing checkpoint only costs a re-run, never corrupts.
|
|
269
269
|
* A null result is never persisted (Codex QE #8: it would later parse as a resumable entry). */
|
|
270
|
-
export function serializeCheckpoint(stage: string, inputHash: string, result: unknown): string | null {
|
|
270
|
+
export function serializeCheckpoint(stage: string, inputHash: string, result: unknown, ts?: string): string | null {
|
|
271
271
|
if (result === null || result === undefined) return null;
|
|
272
272
|
let line: string;
|
|
273
273
|
try {
|
|
274
|
-
line = JSON.stringify({ stage, inputHash, result });
|
|
274
|
+
line = JSON.stringify({ stage, inputHash, result, ...(ts === undefined ? {} : { ts }) });
|
|
275
275
|
} catch {
|
|
276
276
|
return null;
|
|
277
277
|
}
|
|
@@ -289,6 +289,9 @@ export interface ParsedCheckpointRead {
|
|
|
289
289
|
* command's stdout. */
|
|
290
290
|
export const CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';
|
|
291
291
|
|
|
292
|
+
/** A writer-produced UTC instant. A malformed supplied value is unmeasured, not a bad record. */
|
|
293
|
+
const CHECKPOINT_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
294
|
+
|
|
292
295
|
/** Parse the read-back agent's stdout: JSONL entries (LAST occurrence of a stage wins — a re-run
|
|
293
296
|
* overwrites by append), then the sentinel ON ITS OWN LINE, then one artifact path per line
|
|
294
297
|
* (relative to the feature dir). The sentinel match is LINE-ANCHORED: JSON.stringify never emits
|
|
@@ -308,7 +311,11 @@ export function parseCheckpointRead(text: string): ParsedCheckpointRead {
|
|
|
308
311
|
try {
|
|
309
312
|
const e = JSON.parse(t) as CheckpointEntry;
|
|
310
313
|
if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {
|
|
311
|
-
|
|
314
|
+
const { ts, ...entry } = e;
|
|
315
|
+
// Narrow to `string` before the spread: under exactOptionalPropertyTypes an optional
|
|
316
|
+
// `ts?: string` refuses `string | undefined`, and an unmeasured record must carry NO key
|
|
317
|
+
// rather than an explicit undefined.
|
|
318
|
+
out.entries[e.stage] = typeof ts === 'string' && CHECKPOINT_TIMESTAMP.test(ts) ? { ...entry, ts } : entry;
|
|
312
319
|
} else {
|
|
313
320
|
// last-wins holds for BAD records too: a stage-identifiable null/invalid record ERASES the
|
|
314
321
|
// older entry for that stage instead of silently reactivating it (Codex QE r2 #6).
|
|
@@ -345,11 +352,19 @@ export function checkpointReadCmd(fdirAbs: string): string {
|
|
|
345
352
|
|
|
346
353
|
/** The one Bash command the write agent runs: mkdir the state dir, then append ONE line. The line
|
|
347
354
|
* is single-quote-escaped as a whole — JSON.stringify output never contains literal newlines, so
|
|
348
|
-
* printf '%s\n' emits exactly one record.
|
|
355
|
+
* printf '%s\n' emits exactly one record.
|
|
356
|
+
*
|
|
357
|
+
* The record's `ts` is stamped SHELL-SIDE (`date -u`), the same idiom the decision-recall ledger
|
|
358
|
+
* uses, because the workflow sandbox forbids `Date` and this function must stay pure. A line that
|
|
359
|
+
* is not a JSON object falls back to the plain append: an unstamped record beats a lost one, and
|
|
360
|
+
* the return type stays `string` so no caller grows a new failure path. */
|
|
349
361
|
export function checkpointAppendCmd(fdirAbs: string, line: string): string {
|
|
350
362
|
const dir = shellQuote(fdirAbs + '/.fa-state');
|
|
351
363
|
const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');
|
|
352
|
-
|
|
364
|
+
const plain = 'mkdir -p ' + dir + " && printf '%s\\n' " + shellQuote(line) + ' >> ' + file;
|
|
365
|
+
if (typeof line !== 'string' || !line.startsWith('{')) return plain;
|
|
366
|
+
return 'ts=$(date -u +%Y-%m-%dT%H:%M:%SZ); mkdir -p ' + dir
|
|
367
|
+
+ ' && { printf \'{"ts":"%s",\' "$ts"; printf \'%s\\n\' ' + shellQuote(line.slice(1)) + '; } >> ' + file;
|
|
353
368
|
}
|
|
354
369
|
|
|
355
370
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -34,6 +34,18 @@ export interface ExperimentEnvelopeChosen {
|
|
|
34
34
|
* (usage-override, session-inherited fallback) is recorded HERE explicitly — arms stay the set that
|
|
35
35
|
* was actually offered; the winner is never appended to them after the fact. */
|
|
36
36
|
readonly overrides?: Readonly<Record<string, string>>;
|
|
37
|
+
/**
|
|
38
|
+
* ablation-c-start (ADR-001, T3; fix-round-1 BLOCKER #2): the pre-registered ablation-C arm
|
|
39
|
+
* (`direct` | `reference`) for THIS run, filled only when `args.experiment`/`args.taskId` were
|
|
40
|
+
* given AND the workflow successfully RESOLVED an existing assignment for that task via
|
|
41
|
+
* `dz experiment resolve` — never taken from a caller-supplied arm option (that was the BLOCKER
|
|
42
|
+
* fix-round-1 found: a caller could set the arm to anything, with zero journal entry). Deliberately
|
|
43
|
+
* a SEPARATE field from `mode` — `mode` already means "same-family vs cross-family reviewer" (a
|
|
44
|
+
* different axis) — so setting `qeMode` never redefines what `mode` has always meant. Absent (not
|
|
45
|
+
* `null`) when no arm was resolved, so `JSON.stringify` drops the key and an unflagged run's
|
|
46
|
+
* envelope stays byte-identical to before this feature (NFR-2).
|
|
47
|
+
*/
|
|
48
|
+
readonly qeMode?: string;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
export interface ExperimentEnvelopePolicy {
|
|
@@ -104,7 +116,12 @@ export function buildExperimentEnvelope(input: BuildExperimentEnvelopeInput): Ex
|
|
|
104
116
|
treeSha: input.treeSha,
|
|
105
117
|
treeShaReason: input.treeSha === null ? (input.treeShaReason ?? 'unavailable') : null,
|
|
106
118
|
arms: { mode: [...input.arms.mode], stages: { ...input.arms.stages } },
|
|
107
|
-
chosen: {
|
|
119
|
+
chosen: {
|
|
120
|
+
mode: input.chosen.mode,
|
|
121
|
+
stages: { ...input.chosen.stages },
|
|
122
|
+
overrides: { ...(input.chosen.overrides ?? {}) },
|
|
123
|
+
...(input.chosen.qeMode !== undefined ? { qeMode: input.chosen.qeMode } : {}),
|
|
124
|
+
},
|
|
108
125
|
policy: { ...input.policy },
|
|
109
126
|
evaluator: { ...input.evaluator },
|
|
110
127
|
};
|
|
@@ -217,6 +234,12 @@ export function validateExperimentEnvelope(value: unknown): { ok: true } | { ok:
|
|
|
217
234
|
return { ok: false, reason: `chosen.stages.${stage}: "${String(spec)}" is not a member of arms.stages.${stage} (${offered.join('|')}) and not declared in chosen.overrides` };
|
|
218
235
|
}
|
|
219
236
|
}
|
|
237
|
+
// ablation-c-start (ADR-001, T3): qeMode is OPTIONAL — absent on every run this feature does not
|
|
238
|
+
// touch — but when present must be a non-empty string, same shape rule as every other envelope
|
|
239
|
+
// field (never a silently-accepted empty label).
|
|
240
|
+
if (chosen.qeMode !== undefined && !isNonEmptyString(chosen.qeMode)) {
|
|
241
|
+
return { ok: false, reason: 'chosen.qeMode: expected a non-empty string when present' };
|
|
242
|
+
}
|
|
220
243
|
|
|
221
244
|
if (!isPlainObject(v.policy)) return { ok: false, reason: 'policy: expected an object' };
|
|
222
245
|
const policy = v.policy;
|
|
@@ -1675,6 +1675,10 @@ export function codexDispatchMode(stage: string): CodexDispatch {
|
|
|
1675
1675
|
* SCOPE — `codexReviewCommand` (the diff defines it) and `scopedQePrompt` ("read ONLY these files").
|
|
1676
1676
|
* This constant is retained as a sanity bound on an absurd payload, and is no longer claimed as the
|
|
1677
1677
|
* thing that prevents a stall.
|
|
1678
|
+
*
|
|
1679
|
+
* THIS IS THE ONLY DEFINITION. `workflow-run-dispatch.ts` re-exports it rather than declaring its
|
|
1680
|
+
* own twin (it used to, and nothing compared the two). The direction is forced: this module is
|
|
1681
|
+
* lifted verbatim into the workflow sandbox by `scripts/gen-loop-blobs.mjs`, so it may not import.
|
|
1678
1682
|
*/
|
|
1679
1683
|
export const CODEX_EXEC_PROMPT_CEILING_CHARS = 24_000;
|
|
1680
1684
|
|
package/src/feature-tier.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import type { FeatureTier } from './guard-volume.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Начало строки, на котором тир ещё считается ОБЪЯВЛЕННЫМ, а не упомянутым в прозе: необязательный
|
|
5
|
+
* маркер списка (`-`, `*`, `+`, `1.`), необязательный заголовок и необязательное выделение.
|
|
6
|
+
*
|
|
7
|
+
* Маркер списка добавлен 2026-09-21 по измерению: `00_complexity_assessment.md` фичи
|
|
8
|
+
* `amendment-seams` несёт строку `- **Tier: S** — …`, и парсер её НЕ видел. Следствие было не
|
|
9
|
+
* косметическим: `dz contract-check --slug amendment-seams` отвечал NOT-ESTABLISHED с диагнозом
|
|
10
|
+
* «required ADR directory cannot be resolved», хотя в самом приборе есть верная ветка «тиру S
|
|
11
|
+
* каталог ADR не требуется» — она просто никогда не исполнялась, потому что тир читался как
|
|
12
|
+
* неизвестный. То есть вердикт называл не ту причину и отправлял чинить не то (бэклог 5d436aa6).
|
|
13
|
+
*/
|
|
14
|
+
const TIER_LINE_START = /^\s*(?:[-*+]\s+|\d+[.)]\s+)?(?:##+\s*)?(?:\*\*)?(?:Tier|Тир)(?=$|[\s:*])/iu;
|
|
15
|
+
|
|
3
16
|
function tiersAfterMarkers(line: string): FeatureTier[] {
|
|
4
|
-
if (
|
|
17
|
+
if (!TIER_LINE_START.test(line)) return [];
|
|
5
18
|
const tiers: FeatureTier[] = [];
|
|
6
19
|
for (const marker of line.matchAll(/Tier|Тир/giu)) {
|
|
7
20
|
const markerIndex = marker.index ?? 0;
|
package/src/guard.ts
CHANGED
|
@@ -1338,6 +1338,18 @@ export interface GuardAuditRecord {
|
|
|
1338
1338
|
readonly observations?: readonly GuardObservation[];
|
|
1339
1339
|
/** set when the operator overrode a block with `--force <reason>` — the override is logged, never silent. */
|
|
1340
1340
|
readonly override?: { readonly forced: true; readonly reason: string };
|
|
1341
|
+
/**
|
|
1342
|
+
* Ids of every rule this run EVALUATED — `checked` plus `notEstablished`, because both mean the
|
|
1343
|
+
* rule was active for the op and got its turn.
|
|
1344
|
+
*
|
|
1345
|
+
* Why the log needs it (backlog 1bee49dd): a guard rule's zero-firing state is its HEALTHY state,
|
|
1346
|
+
* so `violations[]` cannot tell a working safety net from a dead rule. MEASURED 2026-09-21 on
|
|
1347
|
+
* `.dz/guard-audit.jsonl`: 1854 rows, 26 default rules, 6 of which never appear in any violations
|
|
1348
|
+
* array — and 4 of those 6 are not allowlisted, so the moment the report's history floor is met
|
|
1349
|
+
* they would be named dead for doing their job. The evaluation set was already computed in
|
|
1350
|
+
* `GuardResult`; only the record dropped it.
|
|
1351
|
+
*/
|
|
1352
|
+
readonly evaluated?: readonly string[];
|
|
1341
1353
|
}
|
|
1342
1354
|
|
|
1343
1355
|
/** Build the audit record for a guard evaluation (+ an optional forced-override reason). Pure. */
|
|
@@ -1350,9 +1362,21 @@ export function auditRecord(result: GuardResult, ts: string, override?: { reason
|
|
|
1350
1362
|
...(Array.isArray(result.notes) && result.notes.length > 0 ? { notes: result.notes } : {}),
|
|
1351
1363
|
...(Array.isArray(result.observations) && result.observations.length > 0 ? { observations: result.observations } : {}),
|
|
1352
1364
|
...(override && typeof override.reason === 'string' ? { override: { forced: true, reason: override.reason } } : {}),
|
|
1365
|
+
...(evaluatedRuleIds(result).length > 0 ? { evaluated: evaluatedRuleIds(result) } : {}),
|
|
1353
1366
|
};
|
|
1354
1367
|
}
|
|
1355
1368
|
|
|
1369
|
+
/**
|
|
1370
|
+
* Every rule that got its turn this run, sorted and de-duplicated. A rule with no input still RAN —
|
|
1371
|
+
* calling that "not evaluated" would reintroduce the very conflation this field exists to remove.
|
|
1372
|
+
*/
|
|
1373
|
+
export function evaluatedRuleIds(result: GuardResult): string[] {
|
|
1374
|
+
const ids = new Set<string>();
|
|
1375
|
+
for (const id of Array.isArray(result.checked) ? result.checked : []) if (typeof id === 'string' && id !== '') ids.add(id);
|
|
1376
|
+
for (const id of Array.isArray(result.notEstablished) ? result.notEstablished : []) if (typeof id === 'string' && id !== '') ids.add(id);
|
|
1377
|
+
return [...ids].sort();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1356
1380
|
/** The exit-code contract: a block is non-zero unless forced; a warn/pass is zero. */
|
|
1357
1381
|
export function guardExitCode(result: GuardResult, forced: boolean): number {
|
|
1358
1382
|
return result.verdict === 'block' && !forced ? 1 : 0;
|
package/src/index.ts
CHANGED
|
@@ -78,6 +78,8 @@ export {
|
|
|
78
78
|
} from './parity.js';
|
|
79
79
|
export type { RuntimeCapability, FeatureForm, ParityFeature, ParityCell, ParityReportCell, ParityMatrixRow, CapabilityEvidence, UnbackedCapability, ProbedRuntimeVersions } from './parity.js';
|
|
80
80
|
export * from './operations.js';
|
|
81
|
+
export { checkInstrumentFreshness, checkRankingState } from './doctor-instrument.js';
|
|
82
|
+
export type { InstrumentFreshness, InstrumentCheckInput, InstrumentCheckResult, RankingStateCheckInput, RankingStateCheckResult } from './doctor-instrument.js';
|
|
81
83
|
// workflows.ts: the ADR-005 templates are RETIRED (feature loop-designer, AM-6) — the module is a
|
|
82
84
|
// deprecation shim (empty WORKFLOW_NAMES). BREAKING for external harness-core consumers of
|
|
83
85
|
// WorkflowTemplate/WORKFLOWS/getWorkflow — deliberately channeled through the 0.x MINOR bump and
|
|
@@ -116,6 +118,7 @@ export * from './trace-bundle.js';
|
|
|
116
118
|
export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
|
|
117
119
|
export type { LoopBlob } from './loop-blobs.generated.js';
|
|
118
120
|
export * from './sign.js';
|
|
121
|
+
export * from './publish-source-scope.js';
|
|
119
122
|
export * from './swarm-brief.js';
|
|
120
123
|
export * from './skill-schema.js';
|
|
121
124
|
export { createSkill } from './create-skill.js';
|
|
@@ -136,7 +139,7 @@ export type { RepoBoundaryIo } from './repo-boundary.js';
|
|
|
136
139
|
export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
|
|
137
140
|
export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
|
|
138
141
|
export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
139
|
-
export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs, discoverSkillCarryingDirs, discoverVerifiablePackDirs } from './registry.js';
|
|
142
|
+
export { buildRegistry, buildShowcaseRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs, discoverSkillCarryingDirs, discoverVerifiablePackDirs, packScope, verifiedScopeNote } from './registry.js';
|
|
140
143
|
export { tokenize, stemToken, stems } from './stem.js';
|
|
141
144
|
// Package skill-layout resolution (feature dz-install-npx-init) — the ONE seam that knows where an
|
|
142
145
|
// npm package keeps its skills (flat / templates/.claude/skills / skills). `cmdInstall` calls it;
|
|
@@ -144,7 +147,7 @@ export { tokenize, stemToken, stems } from './stem.js';
|
|
|
144
147
|
export * from './package-skill-layouts.js';
|
|
145
148
|
export { recommend } from './recommend.js';
|
|
146
149
|
export { pretrain } from './pretrain.js';
|
|
147
|
-
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, recordLessonForms, loadStorePatternsSync, loadStoreRecords, findExactLesson, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
150
|
+
export { loadPatterns, loadSessions, computePatternBoost, readLearningConfig, readMemoryLearningConfig, BOOST_CAP, recordPattern, recordLessonForms, loadStorePatternsSync, loadStoreRecords, findExactLesson, patternToRecord, recordToPattern, patternRecordId, patternIdentityOf, dreamRecordId, isMirrorableLearning, consolidateSessions, recallPatterns, pruneNoisePatterns, removePatternsByIds, snapshotStore, readReinforcementState, encodeReinforcementState, reinforcePattern, resolveReinforceTarget, updateReinforcementState, storeStats, lessonDeltaReport, lessonDeltaMap, readQuarantineState, encodeQuarantineState, promotePatterns, quarantineExpiryCandidates, pruneQuarantinePatterns } from './patterns.js';
|
|
148
151
|
export { normalizeLessonForms, validateClassTemplate, lessonPairIdOf, mergeLessonFormHits, mergeLessonMatchedForms } from './lesson-generalization.js';
|
|
149
152
|
export { withStoreLock, withStoreLockSync, storeLockPath, StoreLockTimeoutError, StoreLockCompromisedError, STALE_LOCK_MS, LOCK_TIMEOUT_MS } from './store-lock.js';
|
|
150
153
|
export type { StoreLockOptions } from './store-lock.js';
|
|
@@ -304,6 +307,13 @@ export type {
|
|
|
304
307
|
StoreMarkWriteOptions,
|
|
305
308
|
StoreResetReceipt,
|
|
306
309
|
} from './store-guard.js';
|
|
310
|
+
export { planStoreGuardPrune } from './store-guard-prune.js';
|
|
311
|
+
export type {
|
|
312
|
+
StoreGuardPruneBucket,
|
|
313
|
+
StoreGuardPruneEntry,
|
|
314
|
+
StoreGuardPrunePlan,
|
|
315
|
+
StoreGuardPruneDeps,
|
|
316
|
+
} from './store-guard-prune.js';
|
|
307
317
|
export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStateDir, featureAdrStatePath, writeFeatureAdrStateDetailed, renderFeatureAdrPhaseLine } from './statusline.js';
|
|
308
318
|
export type { StatuslineData, StatuslineStoreHealth, FeatureAdrState, WriteFeatureAdrStateInput, WriteFeatureAdrStateResult } from './statusline.js';
|
|
309
319
|
export {
|
|
@@ -501,6 +511,7 @@ export {
|
|
|
501
511
|
} from './amendment-trace.js';
|
|
502
512
|
export {
|
|
503
513
|
RECORD_MAX_LINE_CHARS,
|
|
514
|
+
INCOMPLETE_REASON_CODES,
|
|
504
515
|
decideRecordWrite,
|
|
505
516
|
decideReadBack,
|
|
506
517
|
recordVerdictLine,
|
|
@@ -528,7 +539,7 @@ export type {
|
|
|
528
539
|
} from './feature-adr-envelope.js';
|
|
529
540
|
export { decidePublishSigning, decidePostSigningVerification, decideSignableSet, publishSigningLine, signableSetLine } from './publish-signing.js';
|
|
530
541
|
export type { PublishSigningVerdict, PublishSigningDecision, SignableSetDecision } from './publish-signing.js';
|
|
531
|
-
export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec, TaskIdLookup } from './run-records.js';
|
|
542
|
+
export type { RecordKind, RecordVerdict, RecordDecision, LedgerEnrichInput, LedgerPriceEntry, ParsedModelSpec, TaskIdLookup, IncompleteReasonCode } from './run-records.js';
|
|
532
543
|
export type { AmendmentAmbiguity, AmendmentRow, AmendmentVerdict, AmendmentResolution, AmendmentOutcome, AmendmentDecision, PlanCoverageGap } from './amendment-trace.js';
|
|
533
544
|
// contract-checklist (ADR-001): pure extraction, canonical rendering, typed report parsing, and
|
|
534
545
|
// exact per-item verification. Filesystem discovery/containment stays in harness-cli.
|
|
@@ -755,7 +766,7 @@ export type {
|
|
|
755
766
|
ChainDefectAges,
|
|
756
767
|
ChainedJournal,
|
|
757
768
|
} from './event-chain.js';
|
|
758
|
-
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion, planReadmeVersionSync } from './publish.js';
|
|
769
|
+
export { decideProvenance, environmentCanMintProvenance, publishArgv, matchesPublishFilter, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion, planReadmeVersionSync } from './publish.js';
|
|
759
770
|
export type { ReadmeVersionSyncPlan, ReadmeSyncRewrite } from './publish.js';
|
|
760
771
|
export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
|
|
761
772
|
export * from './course-staleness.js';
|
|
@@ -773,6 +784,7 @@ export {
|
|
|
773
784
|
planReleaseGates,
|
|
774
785
|
classifyGateExecutions,
|
|
775
786
|
buildFailureIssue,
|
|
787
|
+
shouldRetryGhWithoutToken,
|
|
776
788
|
buildReleaseNotes,
|
|
777
789
|
releaseTagName,
|
|
778
790
|
firstOutputLine,
|
|
@@ -872,7 +884,7 @@ export type {
|
|
|
872
884
|
McpSeverity,
|
|
873
885
|
McpCapability,
|
|
874
886
|
} from './mcp-scan.js';
|
|
875
|
-
export type { RegistryEntry, Registry } from './registry.js';
|
|
887
|
+
export type { RegistryEntry, Registry, ShowcaseRegistry, ShowcaseSkill } from './registry.js';
|
|
876
888
|
export type { BenchmarkCheck, BenchmarkScore, BenchmarkReport, CompareResult } from './benchmark.js';
|
|
877
889
|
export {
|
|
878
890
|
specToOpts,
|
|
@@ -1148,6 +1160,11 @@ export * from './compounding.js';
|
|
|
1148
1160
|
// Advisory command-invocation telemetry + deadwood report (feature dz-deadwood).
|
|
1149
1161
|
export * from './cmd-usage.js';
|
|
1150
1162
|
|
|
1163
|
+
// Deterministic, blocked arm assignment for a prospective ablation (feature ablation-c-start,
|
|
1164
|
+
// ADR-001): pure — no fs, no clock, reuses `mulberry32` (the repo's only PRNG).
|
|
1165
|
+
export { assignArm, readAssignments, verifyAssignmentRecord } from './experiment-assign.js';
|
|
1166
|
+
export type { AssignArmInput, AssignArmResult, AssignmentRecord, ReadAssignmentsResult, VerifyAssignmentResult } from './experiment-assign.js';
|
|
1167
|
+
|
|
1151
1168
|
// Cold-vs-warm EPOCH RUNNER (feature epoch-replay, scout idea #4) — the RESULT leg to compounding's
|
|
1152
1169
|
// readiness leg. Orchestrates + scores; never calls a model. SUPPORTED requires two DISJOINT Wilson
|
|
1153
1170
|
// intervals; INCONCLUSIVE is a first-class honest outcome.
|
|
@@ -52,11 +52,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
52
52
|
"checkpoints": {
|
|
53
53
|
name: "checkpoints",
|
|
54
54
|
version: "1.2.0",
|
|
55
|
-
contentHash: "
|
|
55
|
+
contentHash: "b602357649042d6b8aa68b5ca5593f7f5c483fdb36a287742e7b27bda8a2a6c7",
|
|
56
56
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
57
57
|
requires: [],
|
|
58
58
|
exports: ["CHECKPOINT_STAGES","STAGE_ARTIFACTS","CHECKPOINT_MAX_RESULT_CHARS","CKPT_SCHEMA_VERSION","fnv1a","fnv1a64","checkpointInputHash","resumeMode","decideCheckpointResume","serializeCheckpoint","CHECKPOINT_LS_SENTINEL","parseCheckpointRead","shellQuote","checkpointReadCmd","checkpointAppendCmd","DESIGN_SUBSTAGES","designStageKey","decideDesignFanResume","parseArtifactProbe","ROUTER_CONTRACT_TOKEN"],
|
|
59
|
-
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: '00_complexity_assessment.md',\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-3';\nconst ROUTER_CONTRACT_TOKEN = 'router-writes-00-v1';\nfunction fnv1a(str) {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\nfunction fnv1a64(str) {\n return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);\n}\nfunction checkpointInputHash(stage, parts) {\n return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));\n}\nfunction resumeMode(raw) {\n return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';\n}\nconst DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];\nfunction designStageKey(sub) {\n return 'design:' + sub;\n}\nfunction decideDesignFanResume(opts) {\n const missingSubstages = [];\n opts.required.forEach((sub, i) => {\n const r = opts.results[i];\n if (r === null || r === undefined)\n missingSubstages.push(sub);\n });\n const missingArtifacts = [];\n let probeMissing = false;\n if (opts.artifacts.length > 0) {\n if (opts.postRunListing === null)\n probeMissing = true;\n else\n for (const rel of opts.artifacts)\n if (!opts.postRunListing.has(rel))\n missingArtifacts.push(rel);\n }\n const reason = missingSubstages.length > 0 ? 'substage-missing'\n : probeMissing ? 'probe-not-established'\n : missingArtifacts.length > 0 ? 'artifact-missing'\n : 'ok';\n return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };\n}\nfunction decideCheckpointResume(opts) {\n if (opts.mode === 'never')\n return { resume: false, reason: 'mode-never' };\n if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {\n return { resume: false, reason: 'no-checkpoint' };\n }\n if (opts.entry.inputHash !== opts.inputHash)\n return { resume: false, reason: 'stale-input' };\n if (opts.mode === 'force')\n return { resume: true, reason: 'resumed-force' };\n const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);\n for (const rel of required) {\n if (!opts.listing.has(rel))\n return { resume: false, reason: 'artifact-missing' };\n }\n return { resume: true, reason: 'resumed' };\n}\nfunction serializeCheckpoint(stage, inputHash, result) {\n if (result === null || result === undefined)\n return null;\n let line;\n try {\n line = JSON.stringify({ stage, inputHash, result });\n }\n catch {\n return null;\n }\n if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS)\n return null;\n return line;\n}\nconst CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';\nfunction parseCheckpointRead(text) {\n const out = { entries: {}, listing: new Set(), malformedLines: 0 };\n const raw = String(text ?? '');\n const lines = raw.split('\\n');\n const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);\n const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);\n const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);\n for (const line of body) {\n const t = line.trim();\n if (t === '')\n continue;\n try {\n const e = JSON.parse(t);\n if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {\n out.entries[e.stage] =
|
|
59
|
+
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: '00_complexity_assessment.md',\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-3';\nconst ROUTER_CONTRACT_TOKEN = 'router-writes-00-v1';\nfunction fnv1a(str) {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\nfunction fnv1a64(str) {\n return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);\n}\nfunction checkpointInputHash(stage, parts) {\n return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));\n}\nfunction resumeMode(raw) {\n return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';\n}\nconst DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];\nfunction designStageKey(sub) {\n return 'design:' + sub;\n}\nfunction decideDesignFanResume(opts) {\n const missingSubstages = [];\n opts.required.forEach((sub, i) => {\n const r = opts.results[i];\n if (r === null || r === undefined)\n missingSubstages.push(sub);\n });\n const missingArtifacts = [];\n let probeMissing = false;\n if (opts.artifacts.length > 0) {\n if (opts.postRunListing === null)\n probeMissing = true;\n else\n for (const rel of opts.artifacts)\n if (!opts.postRunListing.has(rel))\n missingArtifacts.push(rel);\n }\n const reason = missingSubstages.length > 0 ? 'substage-missing'\n : probeMissing ? 'probe-not-established'\n : missingArtifacts.length > 0 ? 'artifact-missing'\n : 'ok';\n return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };\n}\nfunction decideCheckpointResume(opts) {\n if (opts.mode === 'never')\n return { resume: false, reason: 'mode-never' };\n if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {\n return { resume: false, reason: 'no-checkpoint' };\n }\n if (opts.entry.inputHash !== opts.inputHash)\n return { resume: false, reason: 'stale-input' };\n if (opts.mode === 'force')\n return { resume: true, reason: 'resumed-force' };\n const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);\n for (const rel of required) {\n if (!opts.listing.has(rel))\n return { resume: false, reason: 'artifact-missing' };\n }\n return { resume: true, reason: 'resumed' };\n}\nfunction serializeCheckpoint(stage, inputHash, result, ts) {\n if (result === null || result === undefined)\n return null;\n let line;\n try {\n line = JSON.stringify({ stage, inputHash, result, ...(ts === undefined ? {} : { ts }) });\n }\n catch {\n return null;\n }\n if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS)\n return null;\n return line;\n}\nconst CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';\nconst CHECKPOINT_TIMESTAMP = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$/;\nfunction parseCheckpointRead(text) {\n const out = { entries: {}, listing: new Set(), malformedLines: 0 };\n const raw = String(text ?? '');\n const lines = raw.split('\\n');\n const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);\n const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);\n const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);\n for (const line of body) {\n const t = line.trim();\n if (t === '')\n continue;\n try {\n const e = JSON.parse(t);\n if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {\n const { ts, ...entry } = e;\n out.entries[e.stage] = typeof ts === 'string' && CHECKPOINT_TIMESTAMP.test(ts) ? { ...entry, ts } : entry;\n }\n else {\n if (e && typeof e === 'object' && typeof e.stage === 'string')\n delete out.entries[e.stage];\n out.malformedLines++;\n }\n }\n catch {\n out.malformedLines++;\n }\n }\n for (const line of ls) {\n const t = line.trim();\n if (t !== '')\n out.listing.add(t);\n }\n return out;\n}\nfunction shellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction checkpointReadCmd(fdirAbs) {\n const q = shellQuote(fdirAbs);\n return ('cat ' + q + '/.fa-state/checkpoints.jsonl 2>/dev/null || true; ' +\n \"echo '\" + CHECKPOINT_LS_SENTINEL + \"'; \" +\n 'cd ' + q + ' 2>/dev/null && find . -maxdepth 2 -type f 2>/dev/null | sed \"s|^\\\\./||\" || true');\n}\nfunction checkpointAppendCmd(fdirAbs, line) {\n const dir = shellQuote(fdirAbs + '/.fa-state');\n const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');\n const plain = 'mkdir -p ' + dir + \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + file;\n if (typeof line !== 'string' || !line.startsWith('{'))\n return plain;\n return 'ts=$(date -u +%Y-%m-%dT%H:%M:%SZ); mkdir -p ' + dir\n + ' && { printf \\'{\"ts\":\"%s\",\\' \"$ts\"; printf \\'%s\\\\n\\' ' + shellQuote(line.slice(1)) + '; } >> ' + file;\n}\nfunction parseArtifactProbe(opts) {\n if (opts.stdout === null || opts.stdout === undefined)\n return null;\n const known = new Map();\n for (const rel of opts.required)\n known.set('HAVE:' + rel, rel);\n const found = new Set();\n let sentinels = 0;\n for (const raw of String(opts.stdout).split('\\n')) {\n const line = raw.trim();\n if (line === '')\n continue;\n if (line === opts.sentinel) {\n sentinels++;\n continue;\n }\n if (sentinels > 0)\n return null;\n const rel = known.get(line);\n if (rel === undefined)\n return null;\n found.add(rel);\n }\n if (sentinels !== 1)\n return null;\n return found;\n}",
|
|
60
60
|
},
|
|
61
61
|
"training-pairs": {
|
|
62
62
|
name: "training-pairs",
|