@dzhechkov/harness-core 0.8.36 → 0.8.37
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 +195 -75
- package/README.md +235 -8
- package/dist/agentdb-index.d.ts +87 -7
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +416 -57
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +19 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +187 -36
- package/dist/apply-leg.js.map +1 -1
- package/dist/codex-rollouts.d.ts +118 -0
- package/dist/codex-rollouts.d.ts.map +1 -0
- package/dist/codex-rollouts.js +297 -0
- package/dist/codex-rollouts.js.map +1 -0
- package/dist/cost-ledger.d.ts +56 -4
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +176 -20
- package/dist/cost-ledger.js.map +1 -1
- package/dist/cross-family-control.d.ts +345 -0
- package/dist/cross-family-control.d.ts.map +1 -0
- package/dist/cross-family-control.js +802 -0
- package/dist/cross-family-control.js.map +1 -0
- package/dist/debt-ratchet.d.ts +53 -0
- package/dist/debt-ratchet.d.ts.map +1 -0
- package/dist/debt-ratchet.js +107 -0
- package/dist/debt-ratchet.js.map +1 -0
- package/dist/embedding-config.d.ts +42 -0
- package/dist/embedding-config.d.ts.map +1 -1
- package/dist/embedding-config.js +106 -10
- package/dist/embedding-config.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +6 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +29 -0
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-decision-recall.d.ts +2 -2
- package/dist/feature-adr-decision-recall.d.ts.map +1 -1
- package/dist/feature-adr-decision-recall.js +5 -3
- package/dist/feature-adr-decision-recall.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +96 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -0
- package/dist/feature-adr-envelope.js +183 -0
- package/dist/feature-adr-envelope.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +64 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +122 -2
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/feature-adr-stage-canon.d.ts +79 -0
- package/dist/feature-adr-stage-canon.d.ts.map +1 -0
- package/dist/feature-adr-stage-canon.js +117 -0
- package/dist/feature-adr-stage-canon.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +4 -4
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +51 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +295 -0
- package/dist/mutation-gate.js.map +1 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +4 -2
- package/dist/qe-bridge.js.map +1 -1
- package/dist/qe-findings.d.ts +107 -0
- package/dist/qe-findings.d.ts.map +1 -0
- package/dist/qe-findings.js +417 -0
- package/dist/qe-findings.js.map +1 -0
- package/dist/recap.d.ts +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +4 -2
- package/dist/recap.js.map +1 -1
- package/dist/round.d.ts +74 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +112 -4
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +60 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +244 -2
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +44 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +78 -5
- package/dist/score.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +374 -74
- package/src/agentdb-index.ts +423 -60
- package/src/apply-leg.ts +187 -36
- package/src/codex-rollouts.ts +374 -0
- package/src/cost-ledger.ts +232 -24
- package/src/cross-family-control.ts +960 -0
- package/src/debt-ratchet.ts +143 -0
- package/src/embedding-config.ts +131 -10
- package/src/feature-adr-checkpoints.ts +29 -0
- package/src/feature-adr-decision-recall.ts +6 -4
- package/src/feature-adr-envelope.ts +242 -0
- package/src/feature-adr-routing.ts +139 -2
- package/src/feature-adr-stage-canon.ts +141 -0
- package/src/index.ts +60 -6
- package/src/loop-blobs.generated.ts +4 -4
- package/src/mutation-gate.ts +316 -0
- package/src/qe-bridge.ts +4 -2
- package/src/qe-findings.ts +463 -0
- package/src/recap.ts +10 -3
- package/src/round.ts +165 -6
- package/src/run-records.ts +282 -2
- package/src/score.ts +115 -6
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared debt-ratchet verdict — one place, called by every "N exceeds ceiling C" gate.
|
|
3
|
+
*
|
|
4
|
+
* WHY (backlog `19e671ebfea26fc9`, feature debt-ceiling-diff): a count-only ceiling knows how MANY
|
|
5
|
+
* debt items exist but not WHICH ones. Once the set crosses the ceiling, printing "every current
|
|
6
|
+
* finding" drowns the one new offender in a legacy list of 100+ — the check technically fires, but
|
|
7
|
+
* a human (or an agent) reading the failure cannot see what actually changed. Pinning the set
|
|
8
|
+
* alongside the count lets the verdict print a DIFFERENCE: `added` (new, not in the pinned set) and
|
|
9
|
+
* `resolved` (pinned, no longer present) — the legacy tail stops being printed at all.
|
|
10
|
+
*
|
|
11
|
+
* The three-way verdict:
|
|
12
|
+
* - current.length > ceiling → ok:false, names only the newly added ids/findings.
|
|
13
|
+
* If `added` is empty despite being over ceiling, the pinned set and the ceiling number have
|
|
14
|
+
* drifted apart (caller error, not a real debt increase) — the message says so explicitly
|
|
15
|
+
* rather than silently printing an empty new: list.
|
|
16
|
+
* - current.length < ceiling → ok:true, suggests lowering the ceiling, names what resolved.
|
|
17
|
+
* - current.length === ceiling → ok:true UNLESS the membership itself changed (one debt swapped
|
|
18
|
+
* for another at the same count) — that specific case is ok:false with a `swapped:` message,
|
|
19
|
+
* because a same-count substitution is exactly the failure mode a raw-number ratchet cannot see:
|
|
20
|
+
* a new debt item can hide behind a legacy one that happened to be fixed in the same window.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface DebtRatchetArgs {
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly current: readonly string[];
|
|
26
|
+
readonly pinned: readonly string[];
|
|
27
|
+
readonly ceiling: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DebtRatchetVerdict {
|
|
31
|
+
readonly ok: boolean;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
readonly added: string[];
|
|
34
|
+
readonly resolved: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function debtRatchetVerdict(args: DebtRatchetArgs): DebtRatchetVerdict {
|
|
38
|
+
const { label, current, pinned, ceiling } = args;
|
|
39
|
+
const pinnedSet = new Set(pinned);
|
|
40
|
+
const currentSet = new Set(current);
|
|
41
|
+
const added = current.filter((id) => !pinnedSet.has(id));
|
|
42
|
+
const resolved = pinned.filter((id) => !currentSet.has(id));
|
|
43
|
+
const n = current.length;
|
|
44
|
+
|
|
45
|
+
// Lead delta after Codex r1 (MEDIUM): a "set" with duplicates is not a set — the count, the +K and
|
|
46
|
+
// the suggested ceiling would all be wrong. Loud, never normalised silently.
|
|
47
|
+
if (currentSet.size !== n) {
|
|
48
|
+
const dupes = current.filter((id, i) => current.indexOf(id) !== i);
|
|
49
|
+
return { ok: false, message: `${label}: duplicate ids in the current set (${[...new Set(dupes)].join(', ')}) — the count ${n} is not a set size`, added, resolved };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Lead delta after Codex r1 (HIGH): ANY new debt is a failure, whatever the count does — two legacy
|
|
53
|
+
// items resolved plus one new one is 2 < 3 by count and would otherwise pass silently, hiding the
|
|
54
|
+
// very thing this ratchet exists to name. The count-only ceiling never hid this any better; the
|
|
55
|
+
// pinned set finally makes it visible.
|
|
56
|
+
if (n <= ceiling && added.length > 0) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `${label} ${n}; ceiling ${ceiling}, but NEW debt appeared: new: ${added.join(', ')}; resolved: ${resolved.length > 0 ? resolved.join(', ') : '(none)'}`,
|
|
60
|
+
added,
|
|
61
|
+
resolved,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (n > ceiling) {
|
|
66
|
+
const newPart = added.length > 0 ? added.join(', ') : '(none — set/ceiling inconsistent)';
|
|
67
|
+
const resolvedPart = resolved.length > 0 ? resolved.join(', ') : '(none)';
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
message: `${label} ${n} exceeds ceiling ${ceiling} (+${n - ceiling}): new: ${newPart}; resolved: ${resolvedPart}`,
|
|
71
|
+
added,
|
|
72
|
+
resolved,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (n < ceiling) {
|
|
77
|
+
const resolvedPart = resolved.length > 0 ? resolved.join(', ') : '(none)';
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
message: `${label} ${n}; ceiling can be lowered to ${n}; resolved: ${resolvedPart}`,
|
|
81
|
+
added,
|
|
82
|
+
resolved,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// n === ceiling with no new ids: a swap (one resolved, one added) is already caught above by the
|
|
87
|
+
// any-new-debt rule, so the only way to land here is an unchanged set — a clean pass.
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
message: `${label} ${n}; ceiling ${ceiling}`,
|
|
92
|
+
added,
|
|
93
|
+
resolved,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PinnedCeiling {
|
|
98
|
+
readonly count: number;
|
|
99
|
+
readonly pinned: string[];
|
|
100
|
+
readonly measuredAt: string;
|
|
101
|
+
readonly reproducer: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads a debt-ceiling json file and requires the pinned set to agree with the declared count —
|
|
106
|
+
* a ceiling whose set and number disagree is not trustworthy data, so this fails loud rather than
|
|
107
|
+
* trusting the number alone (the exact defect this feature exists to close).
|
|
108
|
+
*/
|
|
109
|
+
export function ceilingUnreadableMessage(file: string): string {
|
|
110
|
+
return `Cannot read ${file}; measure the debt and create this ceiling file by hand.`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* PURE: the caller reads the file (tests own their I/O — the core-boundary ratchet counts src files
|
|
115
|
+
* that touch node:fs, and a debt-ceiling parser has no business being one); `file` is only used to
|
|
116
|
+
* name the offending file in error messages.
|
|
117
|
+
*/
|
|
118
|
+
export function parsePinnedCeiling(
|
|
119
|
+
source: string,
|
|
120
|
+
file: string,
|
|
121
|
+
countField: 'unobserved' | 'uncommented',
|
|
122
|
+
setField: 'ids' | 'findings',
|
|
123
|
+
): PinnedCeiling {
|
|
124
|
+
const parsed = JSON.parse(source) as Record<string, unknown>;
|
|
125
|
+
const count = parsed[countField];
|
|
126
|
+
const set = parsed[setField];
|
|
127
|
+
const measuredAt = parsed.measuredAt;
|
|
128
|
+
const reproducer = parsed.reproducer;
|
|
129
|
+
if (
|
|
130
|
+
!Number.isSafeInteger(count) || (count as number) < 0
|
|
131
|
+
|| typeof measuredAt !== 'string' || typeof reproducer !== 'string'
|
|
132
|
+
|| !Array.isArray(set) || !set.every((item) => typeof item === 'string')
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(`Invalid debt ceiling: ${file}`);
|
|
135
|
+
}
|
|
136
|
+
if (set.length !== count) {
|
|
137
|
+
throw new Error(`Invalid debt ceiling: ${file}: ${setField}.length ${set.length} ≠ ${countField} ${count}`);
|
|
138
|
+
}
|
|
139
|
+
if (new Set(set as string[]).size !== set.length) {
|
|
140
|
+
throw new Error(`Invalid debt ceiling: ${file}: ${setField} contains duplicates — a pinned set must be a set`);
|
|
141
|
+
}
|
|
142
|
+
return { count: count as number, pinned: set as string[], measuredAt, reproducer };
|
|
143
|
+
}
|
package/src/embedding-config.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export type EmbedModelSource = 'env' | 'config' | 'default';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* `embed-daemon-memory` (ADR-001 D2): dtype is a property of the STORE, not a global runtime
|
|
8
|
+
* setting. `fp32` is the full-precision default (existing behaviour, unchanged); `q8` is the
|
|
9
|
+
* quantized variant (`{ dtype: 'q8' }` at `pipeline()` construction, `model_quantized.onnx`) —
|
|
10
|
+
* roughly half the RSS of fp32 at a measured cosine parity >= 0.990 (see the harness-core README's
|
|
11
|
+
* `memory.embed.dtype` section for the numbers). `memory.embed.dtype` in config selects the dtype
|
|
12
|
+
* for a NEW index/reindex; the STORE's manifest is what a query is actually embedded with
|
|
13
|
+
* ({@link guardEmbedSpace}) — the two are deliberately allowed to disagree only long enough for the
|
|
14
|
+
* guard to demand a reindex, never silently.
|
|
15
|
+
*/
|
|
16
|
+
export type EmbedDtype = 'fp32' | 'q8';
|
|
17
|
+
export const KNOWN_EMBED_DTYPES: readonly EmbedDtype[] = ['fp32', 'q8'];
|
|
18
|
+
const DEFAULT_EMBED_DTYPE: EmbedDtype = 'fp32';
|
|
19
|
+
|
|
6
20
|
export interface EmbedModelConfig {
|
|
7
21
|
readonly model: string;
|
|
8
22
|
readonly dim: 384;
|
|
9
23
|
readonly source: EmbedModelSource;
|
|
24
|
+
readonly dtype: EmbedDtype;
|
|
10
25
|
}
|
|
11
26
|
|
|
12
27
|
export interface EmbedManifest {
|
|
@@ -14,6 +29,24 @@ export interface EmbedManifest {
|
|
|
14
29
|
readonly dim: 384;
|
|
15
30
|
readonly version: number;
|
|
16
31
|
readonly engine?: string;
|
|
32
|
+
/** Absent on a manifest written before this feature — reads as `'fp32'` everywhere it is compared
|
|
33
|
+
* ({@link guardEmbedSpace}), matching the pre-existing fp32-only behaviour exactly. */
|
|
34
|
+
readonly dtype?: EmbedDtype;
|
|
35
|
+
/**
|
|
36
|
+
* Fix round 1 (Codex #4): the RAW `dtype` string off disk when it is PRESENT but not one of
|
|
37
|
+
* {@link KNOWN_EMBED_DTYPES} — a corrupted manifest (`"Q8"`) or one written by a future version
|
|
38
|
+
* (`"int8"`). Distinct from an ABSENT field (legacy pre-feature manifest, safe to read as `'fp32'`):
|
|
39
|
+
* a present-but-unknown value must never be silently folded into the same "absent" bucket, because
|
|
40
|
+
* the underlying vectors may genuinely not be fp32 — {@link guardEmbedSpace} and
|
|
41
|
+
* {@link resolveStoreEmbedDtype} both refuse instead of guessing when this is set.
|
|
42
|
+
*/
|
|
43
|
+
readonly dtypeError?: string;
|
|
44
|
+
/** Lead delta after Codex r2 (HIGH): the manifest file EXISTS but could not be read/parsed
|
|
45
|
+
* (truncated by a concurrent writer, hand-edited into invalid JSON). It used to read as "absent"
|
|
46
|
+
* and fall through to legacy fp32 — the same silent cross-space risk as an unknown dtype. Only
|
|
47
|
+
* ENOENT means absent; a read/parse failure is carried here and guardEmbedSpace refuses. The
|
|
48
|
+
* remedy stays reachable: reindex stamps a NEW manifest before it re-indexes. */
|
|
49
|
+
readonly readError?: string;
|
|
17
50
|
}
|
|
18
51
|
|
|
19
52
|
export const DEFAULT_EMBED_MODEL = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2';
|
|
@@ -50,8 +83,10 @@ export const KNOWN_EMBED_DIMS: Readonly<Record<string, 384>> = {
|
|
|
50
83
|
*/
|
|
51
84
|
|
|
52
85
|
export function resolveEmbedModel(projectRoot: string): EmbedModelConfig | { error: string } {
|
|
86
|
+
const dtypeResult = resolveEmbedDtype(projectRoot);
|
|
87
|
+
if ('error' in dtypeResult) return dtypeResult;
|
|
53
88
|
const env = process.env['DZ_EMBED_MODEL'];
|
|
54
|
-
if (env !== undefined && env.trim() !== '') return modelConfig(env.trim(), 'env');
|
|
89
|
+
if (env !== undefined && env.trim() !== '') return modelConfig(env.trim(), 'env', dtypeResult.dtype);
|
|
55
90
|
const cfgPath = join(projectRoot, '.dz', 'config.json');
|
|
56
91
|
if (existsSync(cfgPath)) {
|
|
57
92
|
try {
|
|
@@ -60,20 +95,45 @@ export function resolveEmbedModel(projectRoot: string): EmbedModelConfig | { err
|
|
|
60
95
|
const agentdb = memory?.['agentdb'] as Record<string, unknown> | undefined;
|
|
61
96
|
const embed = memory?.['embed'] as Record<string, unknown> | undefined;
|
|
62
97
|
const configured = agentdb?.['embeddingModel'] ?? embed?.['model'];
|
|
63
|
-
if (typeof configured === 'string' && configured.trim() !== '') return modelConfig(configured.trim(), 'config');
|
|
98
|
+
if (typeof configured === 'string' && configured.trim() !== '') return modelConfig(configured.trim(), 'config', dtypeResult.dtype);
|
|
64
99
|
} catch {
|
|
65
100
|
/* corrupt config falls back to the default, matching the existing config-read discipline */
|
|
66
101
|
}
|
|
67
102
|
}
|
|
68
|
-
return modelConfig(DEFAULT_EMBED_MODEL, 'default');
|
|
103
|
+
return modelConfig(DEFAULT_EMBED_MODEL, 'default', dtypeResult.dtype);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* FR-3/AC-2 (`embed-daemon-memory`): `memory.embed.dtype` read INDEPENDENTLY of which model source
|
|
108
|
+
* won above — a dtype override must apply the same way whether the model itself came from env,
|
|
109
|
+
* config, or the default. An unset value (or a config file that predates this feature) is `'fp32'`,
|
|
110
|
+
* matching every store written before this feature existed. An unrecognized string is a hard
|
|
111
|
+
* `{error}` naming the allowed values, never a silent fp32 fallback — a typo in the dtype must not
|
|
112
|
+
* quietly build the wrong-shaped index.
|
|
113
|
+
*/
|
|
114
|
+
function resolveEmbedDtype(projectRoot: string): { dtype: EmbedDtype } | { error: string } {
|
|
115
|
+
const cfgPath = join(projectRoot, '.dz', 'config.json');
|
|
116
|
+
if (!existsSync(cfgPath)) return { dtype: DEFAULT_EMBED_DTYPE };
|
|
117
|
+
try {
|
|
118
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8')) as Record<string, unknown>;
|
|
119
|
+
const memory = cfg['memory'] as Record<string, unknown> | undefined;
|
|
120
|
+
const embed = memory?.['embed'] as Record<string, unknown> | undefined;
|
|
121
|
+
const raw = embed?.['dtype'];
|
|
122
|
+
if (raw === undefined) return { dtype: DEFAULT_EMBED_DTYPE };
|
|
123
|
+
if (typeof raw === 'string' && (KNOWN_EMBED_DTYPES as readonly string[]).includes(raw)) return { dtype: raw as EmbedDtype };
|
|
124
|
+
return { error: `unsupported embedding dtype '${String(raw)}' (known: ${KNOWN_EMBED_DTYPES.join(', ')})` };
|
|
125
|
+
} catch {
|
|
126
|
+
// corrupt config falls back to the default, matching resolveEmbedModel's own discipline
|
|
127
|
+
return { dtype: DEFAULT_EMBED_DTYPE };
|
|
128
|
+
}
|
|
69
129
|
}
|
|
70
130
|
|
|
71
|
-
function modelConfig(model: string, source: EmbedModelSource): EmbedModelConfig | { error: string } {
|
|
131
|
+
function modelConfig(model: string, source: EmbedModelSource, dtype: EmbedDtype): EmbedModelConfig | { error: string } {
|
|
72
132
|
const dim = KNOWN_EMBED_DIMS[model];
|
|
73
133
|
if (dim === undefined) {
|
|
74
134
|
return { error: `unsupported embedding model '${model}' (known 384-dim models: ${Object.keys(KNOWN_EMBED_DIMS).join(', ')})` };
|
|
75
135
|
}
|
|
76
|
-
return { model, dim, source };
|
|
136
|
+
return { model, dim, source, dtype };
|
|
77
137
|
}
|
|
78
138
|
|
|
79
139
|
export function embedManifestPath(storePath: string): string {
|
|
@@ -86,13 +146,41 @@ export function readEmbedManifest(storePath: string): EmbedManifest | undefined
|
|
|
86
146
|
|
|
87
147
|
function readManifestFile(p: string): EmbedManifest | undefined {
|
|
88
148
|
if (!existsSync(p)) return undefined;
|
|
149
|
+
// `readError` (below) is scoped to a REGULAR FILE whose bytes cannot be parsed — the concurrent-
|
|
150
|
+
// writer/hand-edit case Codex r2 named. A non-file at the sidecar path (a directory) is a different
|
|
151
|
+
// pathology and stays "absent": the store-generation AM-3 fixture plants exactly that directory so
|
|
152
|
+
// the manifest WRITE fails loudly after a real commit — refusing here would hide that contract.
|
|
153
|
+
let isFile = false;
|
|
154
|
+
try {
|
|
155
|
+
isFile = statSync(p).isFile();
|
|
156
|
+
} catch {
|
|
157
|
+
isFile = false;
|
|
158
|
+
}
|
|
159
|
+
if (!isFile) return undefined;
|
|
89
160
|
try {
|
|
90
161
|
const m = JSON.parse(readFileSync(p, 'utf-8')) as Partial<EmbedManifest>;
|
|
91
162
|
if (typeof m.model !== 'string' || m.model === '') return undefined;
|
|
92
163
|
if (m.dim !== DEFAULT_EMBED_DIM) return undefined;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
164
|
+
// T1 + fix round 1 (Codex #4): only the two known dtype strings are trusted off disk as a real
|
|
165
|
+
// dtype. An ABSENT field reads as `undefined` (legacy pre-feature manifest — guardEmbedSpace
|
|
166
|
+
// treats it as fp32, the safe pre-existing default). A field that IS PRESENT but names neither
|
|
167
|
+
// known value is NEVER folded into that same "absent" bucket — it used to be (T1's original cut),
|
|
168
|
+
// which let a corrupted (`"Q8"`) or future-version (`"int8"`) dtype masquerade as legacy-fp32 and
|
|
169
|
+
// search would silently compare vectors from two different spaces. It is carried instead as
|
|
170
|
+
// `dtypeError` (the raw string), which guardEmbedSpace/resolveStoreEmbedDtype turn into a hard
|
|
171
|
+
// refusal rather than a guess.
|
|
172
|
+
const dtype = m.dtype === 'fp32' || m.dtype === 'q8' ? m.dtype : undefined;
|
|
173
|
+
const dtypeError = m.dtype !== undefined && dtype === undefined ? String(m.dtype) : undefined;
|
|
174
|
+
return {
|
|
175
|
+
model: m.model,
|
|
176
|
+
dim: DEFAULT_EMBED_DIM,
|
|
177
|
+
version: typeof m.version === 'number' ? m.version : 1,
|
|
178
|
+
...(typeof m.engine === 'string' ? { engine: m.engine } : {}),
|
|
179
|
+
...(dtype !== undefined ? { dtype } : {}),
|
|
180
|
+
...(dtypeError !== undefined ? { dtypeError } : {}),
|
|
181
|
+
};
|
|
182
|
+
} catch (err) {
|
|
183
|
+
return { model: '', dim: DEFAULT_EMBED_DIM, version: 1, readError: err instanceof Error ? err.message : String(err) };
|
|
96
184
|
}
|
|
97
185
|
}
|
|
98
186
|
|
|
@@ -111,9 +199,20 @@ export function legacyEmbedManifest(): EmbedManifest {
|
|
|
111
199
|
}
|
|
112
200
|
|
|
113
201
|
export function currentEmbedManifest(configured: EmbedModelConfig, version = 1, engine?: string): EmbedManifest {
|
|
114
|
-
return { model: configured.model, dim: configured.dim, version, ...(engine !== undefined ? { engine } : {}) };
|
|
202
|
+
return { model: configured.model, dim: configured.dim, version, dtype: configured.dtype, ...(engine !== undefined ? { engine } : {}) };
|
|
115
203
|
}
|
|
116
204
|
|
|
205
|
+
/**
|
|
206
|
+
* D2 (`embed-daemon-memory`): the model/dim check is unchanged; a SEPARATE dtype check is added
|
|
207
|
+
* after it. Absent manifest dtype reads as `'fp32'` (matching every store written before this
|
|
208
|
+
* feature) before the comparison — so an existing fp32 store configured for fp32 never trips this,
|
|
209
|
+
* and only a genuine fp32<->q8 disagreement (or a q8 store re-configured to fp32) is refused.
|
|
210
|
+
*
|
|
211
|
+
* Fix round 1 (Codex #4): a manifest `dtype` that is PRESENT but unrecognized ({@link
|
|
212
|
+
* EmbedManifest.dtypeError}) is checked BEFORE the fp32-fallback comparison above — it must never
|
|
213
|
+
* be silently treated as the safe legacy-absent case, because the store's real vectors may not be
|
|
214
|
+
* fp32 at all.
|
|
215
|
+
*/
|
|
117
216
|
export function guardEmbedSpace(args: {
|
|
118
217
|
storePath: string;
|
|
119
218
|
configured: EmbedModelConfig;
|
|
@@ -122,6 +221,13 @@ export function guardEmbedSpace(args: {
|
|
|
122
221
|
}): { ok: true; manifest: EmbedManifest } | { ok: false; error: string; manifest: EmbedManifest } {
|
|
123
222
|
const manifest = readStoreManifest(args.storePath)
|
|
124
223
|
?? (args.hasRows ? legacyEmbedManifest() : currentEmbedManifest(args.configured));
|
|
224
|
+
if (manifest.readError !== undefined) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
manifest,
|
|
228
|
+
error: `embedding manifest unreadable (${manifest.readError}); run ${args.reindexHint}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
125
231
|
if (manifest.model !== args.configured.model || manifest.dim !== args.configured.dim) {
|
|
126
232
|
return {
|
|
127
233
|
ok: false,
|
|
@@ -129,6 +235,21 @@ export function guardEmbedSpace(args: {
|
|
|
129
235
|
error: `embedding model mismatch: index built with ${manifest.model}/${manifest.dim}, configured ${args.configured.model}/${args.configured.dim}; run ${args.reindexHint}`,
|
|
130
236
|
};
|
|
131
237
|
}
|
|
238
|
+
if (manifest.dtypeError !== undefined) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
manifest,
|
|
242
|
+
error: `unknown embedding dtype "${manifest.dtypeError}" in manifest; run ${args.reindexHint}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const manifestDtype = manifest.dtype ?? DEFAULT_EMBED_DTYPE;
|
|
246
|
+
if (manifestDtype !== args.configured.dtype) {
|
|
247
|
+
return {
|
|
248
|
+
ok: false,
|
|
249
|
+
manifest,
|
|
250
|
+
error: `embedding dtype mismatch: index built with ${manifestDtype}, configured ${args.configured.dtype}; run ${args.reindexHint}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
132
253
|
return { ok: true, manifest };
|
|
133
254
|
}
|
|
134
255
|
|
|
@@ -491,6 +491,11 @@ export interface TrainingPair {
|
|
|
491
491
|
truncated: TrainingPairTruncation | null;
|
|
492
492
|
captureMode: 'capture' | 'backfill';
|
|
493
493
|
resumed: boolean;
|
|
494
|
+
/** experiment-envelope FR-3(б): the envelope built once after the Step-0 router, carried
|
|
495
|
+
* alongside `budgetMode` (not instead of it — `budgetMode` is a narrower legacy summary).
|
|
496
|
+
* Normalized via `validateExperimentEnvelope`: an invalid or absent value becomes `null`, never
|
|
497
|
+
* a malformed value smuggled into the dataset. */
|
|
498
|
+
envelope: unknown | null;
|
|
494
499
|
}
|
|
495
500
|
|
|
496
501
|
/** Per-stage JSONL path, relative to the repo root. ONE file per stage. */
|
|
@@ -649,6 +654,28 @@ function normalizeTrainingPairBudget(raw: unknown): TrainingPairBudget | null {
|
|
|
649
654
|
}
|
|
650
655
|
}
|
|
651
656
|
|
|
657
|
+
/**
|
|
658
|
+
* experiment-envelope FR-3(б): a LIGHTWEIGHT structural check, not the full field-by-field
|
|
659
|
+
* validator (`validateExperimentEnvelope` in `feature-adr-envelope.ts`) — this module is
|
|
660
|
+
* DELIBERATELY import-free (the workflow sandbox mirrors it inline, same discipline as
|
|
661
|
+
* `TP_PROFILE_MARKER_START`/`tpRedact` above), so importing the full validator here would break
|
|
662
|
+
* that mirroring. This check catches gross malformation (not an object, wrong schema, missing
|
|
663
|
+
* the four nested sections) — the AUTHORITATIVE per-field validation gate lives at the
|
|
664
|
+
* run-records/round layer, where an invalid envelope actually refuses a write. Absent or
|
|
665
|
+
* malformed here degrades HONESTLY to `null`, never a fabricated or partially-checked value.
|
|
666
|
+
*/
|
|
667
|
+
function normalizeTrainingPairEnvelope(raw: unknown): unknown | null {
|
|
668
|
+
if (raw === null || raw === undefined || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
669
|
+
const v = raw as Record<string, unknown>;
|
|
670
|
+
if (v.schema !== 1) return null;
|
|
671
|
+
if (typeof v.runId !== 'string' || v.runId.trim() === '') return null;
|
|
672
|
+
if (v.arms === null || typeof v.arms !== 'object') return null;
|
|
673
|
+
if (v.chosen === null || typeof v.chosen !== 'object') return null;
|
|
674
|
+
if (v.policy === null || typeof v.policy !== 'object') return null;
|
|
675
|
+
if (v.evaluator === null || typeof v.evaluator !== 'object') return null;
|
|
676
|
+
return raw;
|
|
677
|
+
}
|
|
678
|
+
|
|
652
679
|
/** Assemble one SFT-ready training pair. Deterministic (ts passed in). Applies the oversize
|
|
653
680
|
* guard: when input+output exceed TRAINPAIR_MAX_IO_CHARS combined, each over-budget side is
|
|
654
681
|
* truncated with a marker naming the cut char count + the fnv1a64 of its FULL text (the
|
|
@@ -667,6 +694,7 @@ export function buildTrainingPair(opts: {
|
|
|
667
694
|
budgetMode?: unknown;
|
|
668
695
|
captureMode?: unknown;
|
|
669
696
|
resumed?: unknown;
|
|
697
|
+
envelope?: unknown;
|
|
670
698
|
}): TrainingPair {
|
|
671
699
|
// Operator-profile redaction FIRST — before the oversize guard, so the truncation hashes are
|
|
672
700
|
// hashes of the redacted text and the full-text fnv1a64 never fingerprints personal data.
|
|
@@ -709,6 +737,7 @@ export function buildTrainingPair(opts: {
|
|
|
709
737
|
truncated,
|
|
710
738
|
captureMode: opts.captureMode === 'backfill' ? 'backfill' : 'capture',
|
|
711
739
|
resumed: opts.resumed === true,
|
|
740
|
+
envelope: normalizeTrainingPairEnvelope(opts.envelope),
|
|
712
741
|
};
|
|
713
742
|
}
|
|
714
743
|
|
|
@@ -8,8 +8,8 @@ const MAX_QUERY_CHARS = 512;
|
|
|
8
8
|
const MAX_PATTERN_CHARS = 800;
|
|
9
9
|
const MAX_EVIDENCE_CHARS = 800;
|
|
10
10
|
|
|
11
|
-
export type DecisionRecallKind = 'adr-alternative-selection' | 'plan-route-selection';
|
|
12
|
-
export type DecisionRecallStage = 'step-3' | 'step-6';
|
|
11
|
+
export type DecisionRecallKind = 'adr-alternative-selection' | 'plan-route-selection' | 'code-implementation';
|
|
12
|
+
export type DecisionRecallStage = 'step-3' | 'step-6' | 'step-7';
|
|
13
13
|
export type DecisionRecallOutcomeName =
|
|
14
14
|
| 'success'
|
|
15
15
|
| 'empty'
|
|
@@ -63,6 +63,8 @@ function oneLine(value: unknown, cap: number): string {
|
|
|
63
63
|
function decisionShape(kind: DecisionRecallKind): { stage: DecisionRecallStage; banditContext: string } {
|
|
64
64
|
return kind === 'adr-alternative-selection'
|
|
65
65
|
? { stage: 'step-3', banditContext: 'feature-adr-decision-adr-alternative' }
|
|
66
|
+
: kind === 'code-implementation'
|
|
67
|
+
? { stage: 'step-7', banditContext: 'feature-adr-decision-code-implementation' }
|
|
66
68
|
: { stage: 'step-6', banditContext: 'feature-adr-decision-plan-route' };
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -391,8 +393,8 @@ function validBase(value: Record<string, unknown>): boolean {
|
|
|
391
393
|
return typeof value.slug === 'string' && value.slug !== ''
|
|
392
394
|
&& typeof value.logicalDecisionId === 'string' && /^decision:[0-9a-f]{16}$/.test(value.logicalDecisionId)
|
|
393
395
|
&& typeof value.attemptId === 'string' && value.attemptId.startsWith(`${value.logicalDecisionId}:`)
|
|
394
|
-
&& (value.stage === 'step-3' || value.stage === 'step-6')
|
|
395
|
-
&& (value.decisionKind === 'adr-alternative-selection' || value.decisionKind === 'plan-route-selection')
|
|
396
|
+
&& (value.stage === 'step-3' || value.stage === 'step-6' || value.stage === 'step-7')
|
|
397
|
+
&& (value.decisionKind === 'adr-alternative-selection' || value.decisionKind === 'plan-route-selection' || value.decisionKind === 'code-implementation')
|
|
396
398
|
&& typeof value.ts === 'string' && value.ts !== '';
|
|
397
399
|
}
|
|
398
400
|
|