@dzhechkov/harness-core 0.3.148 → 0.3.150
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 +32 -8
- package/README.md +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/recall-domain-boost.d.ts +96 -0
- package/dist/recall-domain-boost.d.ts.map +1 -0
- package/dist/recall-domain-boost.js +152 -0
- package/dist/recall-domain-boost.js.map +1 -0
- package/dist/reqe.d.ts +106 -0
- package/dist/reqe.d.ts.map +1 -0
- package/dist/reqe.js +197 -0
- package/dist/reqe.js.map +1 -0
- package/package.json +4 -4
- package/sbom.json +67 -7
- package/src/index.ts +24 -0
- package/src/recall-domain-boost.ts +172 -0
- package/src/reqe.ts +243 -0
package/dist/reqe.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* re-QE debt — the pure half of `dz reqe` (backlog 6b40e667, goal honest-quality).
|
|
3
|
+
*
|
|
4
|
+
* The cross-model-QE guard ("the model that writes code must not self-review") is an ADR-named
|
|
5
|
+
* safety property of the feature-adr pipeline. The usage-adaptive override consciously SUSPENDS it
|
|
6
|
+
* (FR-2.9): at >= threshold usage every remaining stage — including Step-8 QE — switches to Codex,
|
|
7
|
+
* so coder and reviewer become the SAME family. The rule doc said "run an independent re-QE after
|
|
8
|
+
* limits reset" — a human instruction on the weakest detection layer. This module turns it into a
|
|
9
|
+
* DEBT with a lifecycle:
|
|
10
|
+
*
|
|
11
|
+
* emit — the workflow records features/<slug>/.fa-state/reqe-due.json when Step-8 actually ran
|
|
12
|
+
* same-family under the override (not on every switch — a switch before Step 8 that
|
|
13
|
+
* still got cross-family QE creates no debt);
|
|
14
|
+
* list — `dz reqe` scans the debts; `dz usage` surfaces the count so the moment limits free up
|
|
15
|
+
* is the moment the debt is visible;
|
|
16
|
+
* brief — `dz reqe --slug <s>` prints a ready cross-family review brief (the OTHER family than
|
|
17
|
+
* the coder);
|
|
18
|
+
* settle — `dz reqe --slug <s> --done --report <file>` clears the debt FAIL-CLOSED: only against
|
|
19
|
+
* an existing, non-trivial report that names a grade; the settlement is appended to
|
|
20
|
+
* 08_qe_report.md so the artifact trail closes.
|
|
21
|
+
*
|
|
22
|
+
* HONEST SCOPE: nothing here re-runs QE automatically (no background spend — the human decides);
|
|
23
|
+
* the same-family CLAUDE belt fallback (codex unavailable) is out of scope by design — it is
|
|
24
|
+
* already logged loudly at run time and is not a limit-pressure artifact; runs from before this
|
|
25
|
+
* feature carry no marker and are UNDETERMINABLE, not debt-free.
|
|
26
|
+
*/
|
|
27
|
+
export const REQE_SCHEMA = 'reqe-due-1';
|
|
28
|
+
export const REQE_SCOPE = 'scope: debt is emitted only when Step-8 QE actually ran on the coder’s own family under the ' +
|
|
29
|
+
'usage override; settling requires a graded cross-family report (fail-closed); nothing re-runs QE ' +
|
|
30
|
+
'automatically.';
|
|
31
|
+
/** Family classification shared with the workflow's acFamOf (codex/gpt/openai markers ⇒ openai).
|
|
32
|
+
* DELIBERATELY binary over the workflow's own CONTROLLED vocabulary (coderUsed ∈ claude | codex |
|
|
33
|
+
* codex-fallback; qeReviewerUsed ∈ claude | codex) — this is never fed arbitrary model ids, so the
|
|
34
|
+
* claude default is the correct reading of "not a codex marker", not a fail-open (Codex QE #11,
|
|
35
|
+
* accepted with this documentation). */
|
|
36
|
+
export function modelFamily(spec) {
|
|
37
|
+
return /codex|gpt|openai/i.test(String(spec ?? '')) ? 'openai' : 'claude';
|
|
38
|
+
}
|
|
39
|
+
/** Emit iff the QE stage label carries the workflow's ' (usage-switched)' marker AND the reviewer
|
|
40
|
+
* family equals the coder family. Marker-only (cross-family survived the switch) or same-family
|
|
41
|
+
* WITHOUT the marker (the codex-unavailable Claude belt — degraded loudly at run time, not a
|
|
42
|
+
* limit-pressure artifact) both create NO debt. */
|
|
43
|
+
export function shouldEmitReqeDebt(input) {
|
|
44
|
+
const switched = /\(usage-switched\)/.test(String(input.qeModelLabel ?? ''));
|
|
45
|
+
const coderFam = modelFamily(input.coderUsed);
|
|
46
|
+
const qeFam = modelFamily(input.qeReviewerUsed);
|
|
47
|
+
if (switched && coderFam === qeFam) {
|
|
48
|
+
return {
|
|
49
|
+
emit: true,
|
|
50
|
+
reason: 'usage-switched self-review: Step-8 QE ran on the coder’s own family (' + coderFam +
|
|
51
|
+
') under the limit override — the cross-model guard was suspended (FR-2.9)',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (switched)
|
|
55
|
+
return { emit: false, reason: 'usage-switched but QE stayed cross-family (' + qeFam + ' vs coder ' + coderFam + ')' };
|
|
56
|
+
if (coderFam === qeFam)
|
|
57
|
+
return { emit: false, reason: 'same-family without the usage override (belt degrade — logged loudly at run time; out of re-QE-debt scope)' };
|
|
58
|
+
return { emit: false, reason: 'cross-family QE ran' };
|
|
59
|
+
}
|
|
60
|
+
/** Build the debt record (the workflow serializes this; emittedAt is stamped by the writer agent's
|
|
61
|
+
* shell `date`, so the sandbox needs no Date). */
|
|
62
|
+
export function buildReqeDebt(input) {
|
|
63
|
+
return {
|
|
64
|
+
schema: REQE_SCHEMA,
|
|
65
|
+
slug: input.slug,
|
|
66
|
+
coderFamily: modelFamily(input.coderUsed),
|
|
67
|
+
qeFamily: modelFamily(input.qeReviewerUsed),
|
|
68
|
+
qeGrade: input.qeGrade == null || String(input.qeGrade).trim() === '' ? null : String(input.qeGrade).trim(),
|
|
69
|
+
reason: input.reason,
|
|
70
|
+
emittedAt: input.emittedAt ?? null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Parse + validate a debt file's text. null = not a valid debt (the caller reports it as
|
|
74
|
+
* malformed — a corrupt debt file is NAMED, never silently dropped). */
|
|
75
|
+
export function parseReqeDebt(text) {
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = JSON.parse(String(text ?? ''));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const d = raw;
|
|
84
|
+
if (!d || typeof d !== 'object')
|
|
85
|
+
return null;
|
|
86
|
+
if (d.schema !== REQE_SCHEMA)
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof d.slug !== 'string' || d.slug.trim() === '')
|
|
89
|
+
return null;
|
|
90
|
+
if (d.coderFamily !== 'claude' && d.coderFamily !== 'openai')
|
|
91
|
+
return null;
|
|
92
|
+
if (d.qeFamily !== 'claude' && d.qeFamily !== 'openai')
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof d.reason !== 'string' || d.reason.trim() === '')
|
|
95
|
+
return null;
|
|
96
|
+
return {
|
|
97
|
+
schema: REQE_SCHEMA,
|
|
98
|
+
slug: d.slug,
|
|
99
|
+
coderFamily: d.coderFamily,
|
|
100
|
+
qeFamily: d.qeFamily,
|
|
101
|
+
qeGrade: typeof d.qeGrade === 'string' && d.qeGrade.trim() !== '' ? d.qeGrade.trim() : null,
|
|
102
|
+
reason: d.reason,
|
|
103
|
+
emittedAt: typeof d.emittedAt === 'string' && d.emittedAt.trim() !== '' ? d.emittedAt : null,
|
|
104
|
+
runStamp: typeof d.runStamp === 'string' && d.runStamp.trim() !== '' ? d.runStamp : null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** The ready-to-run cross-family review brief. Review family = the OTHER family than the CODER
|
|
108
|
+
* (reviewing with the other-than-reviewer family would let a codex-coded, codex-reviewed run be
|
|
109
|
+
* "re-reviewed" by codex again). */
|
|
110
|
+
export function buildReqeBrief(debt, artifactsDir) {
|
|
111
|
+
const reviewFamily = debt.coderFamily === 'openai' ? 'claude' : 'openai';
|
|
112
|
+
const files = [
|
|
113
|
+
artifactsDir + '/07_code_changes/change_manifest.md',
|
|
114
|
+
artifactsDir + '/08_qe_report.md',
|
|
115
|
+
artifactsDir + '/03_adr/',
|
|
116
|
+
];
|
|
117
|
+
const instructions = [
|
|
118
|
+
'Independent re-QE for "' + debt.slug + '": the recorded Step-8 review ran on the coder’s own family (' + debt.coderFamily + ') under the usage override' + (debt.qeGrade ? ' and graded ' + debt.qeGrade : '') + '.',
|
|
119
|
+
'Review with the ' + reviewFamily.toUpperCase() + ' family (the OTHER family than the coder — the suspended guard, restored).',
|
|
120
|
+
'Read: ' + files.join(' , ') + ' plus every file the change manifest lists.',
|
|
121
|
+
'Adversarially verify: correctness, edge cases, the ADR-named load-bearing property HAS a test, and whether the same-family review missed anything.',
|
|
122
|
+
'Output: GRADE A-F + numbered findings with file:line and severity; write the report to ' + artifactsDir + '/08b_reqe_report.md.',
|
|
123
|
+
'Then settle the debt: dz reqe --slug ' + debt.slug + ' --done --report ' + artifactsDir + '/08b_reqe_report.md',
|
|
124
|
+
];
|
|
125
|
+
const codexCmdTemplate = reviewFamily === 'openai'
|
|
126
|
+
? 'codex exec -m <probed-id> -c model_reasoning_effort="high" --sandbox read-only "<the brief above>" < /dev/null # probe the id first: ids are account-specific'
|
|
127
|
+
: null;
|
|
128
|
+
return {
|
|
129
|
+
reviewFamily,
|
|
130
|
+
header: 're-QE brief for ' + debt.slug + ' (' + (debt.emittedAt ?? 'emitted: unknown') + ')',
|
|
131
|
+
instructions,
|
|
132
|
+
codexCmdTemplate,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Extract the verdict grade from a report, or null. LINE-ANCHORED and range-proof (Codex QE #7):
|
|
136
|
+
* the boilerplate phrase `GRADE A-F` must not read as grade A, so a letter followed by a dash and
|
|
137
|
+
* another grade letter is rejected; and the grade must head its line (a quoted "do not assign
|
|
138
|
+
* GRADE A" mid-paragraph is not a verdict). Conflicting distinct grades ⇒ null (ambiguous). */
|
|
139
|
+
export function extractReportGrade(text) {
|
|
140
|
+
// the lookahead rejects RANGES in punctuation form (A-F, A/F) AND word form (A through F,
|
|
141
|
+
// A to F) — Codex QE round-2 #6: 'GRADE A through F' must not read as grade A
|
|
142
|
+
const matches = [...String(text ?? '').matchAll(/^\s*(?:\*{0,2}#{0,4}\s*)?GRADE\s*[:=—–-]?\s*([A-F])\b(?!\s*(?:[-–—/]|through|to|thru)\s*[A-F]\b)/gim)];
|
|
143
|
+
const distinct = new Set(matches.map((m) => (m[1] ?? '').toUpperCase()).filter((g) => g !== ''));
|
|
144
|
+
if (distinct.size !== 1)
|
|
145
|
+
return null;
|
|
146
|
+
return [...distinct][0] ?? null;
|
|
147
|
+
}
|
|
148
|
+
/** FAIL-CLOSED settlement validation: the report must be non-trivial (>= 200 chars of substance)
|
|
149
|
+
* and must NAME exactly one line-anchored grade. A settlement that cannot cite its evidence is
|
|
150
|
+
* refused — clearing a debt against an empty file would re-open the exact hole this feature closes.
|
|
151
|
+
* HONEST LIMIT (documented, not hidden): the validator proves the settlement is PROCEDURALLY sound
|
|
152
|
+
* (a distinct, graded report exists); it cannot prove which model authored the text — attribution
|
|
153
|
+
* stays with the human running the brief. */
|
|
154
|
+
export function settleReqeDebt(debt, reportText, reportPath) {
|
|
155
|
+
const text = String(reportText ?? '');
|
|
156
|
+
if (text.trim().length < 200) {
|
|
157
|
+
return { ok: false, error: 'report too small to be a review (< 200 chars of substance) — refusing to settle', grade: null, epilogue: null };
|
|
158
|
+
}
|
|
159
|
+
const grade = extractReportGrade(text);
|
|
160
|
+
if (grade === null) {
|
|
161
|
+
return { ok: false, error: 'report names no unambiguous line-anchored GRADE (A-F) — text without exactly one verdict grade is not a verdict; refusing to settle', grade: null, epilogue: null };
|
|
162
|
+
}
|
|
163
|
+
const reviewFamily = debt.coderFamily === 'openai' ? 'claude' : 'openai';
|
|
164
|
+
const epilogue = [
|
|
165
|
+
'',
|
|
166
|
+
'---',
|
|
167
|
+
'',
|
|
168
|
+
'## re-QE settlement (cross-model debt cleared)',
|
|
169
|
+
'',
|
|
170
|
+
'The original Step-8 review ran on the coder’s own family (' + debt.coderFamily + ') under the',
|
|
171
|
+
'usage override (' + debt.reason + '). An independent ' + reviewFamily.toUpperCase() + '-family re-QE was performed:',
|
|
172
|
+
'',
|
|
173
|
+
'- report: `' + reportPath + '`',
|
|
174
|
+
'- re-QE grade: **' + grade + '**' + (debt.qeGrade ? ' (same-family grade on record: ' + debt.qeGrade + ')' : ''),
|
|
175
|
+
'- settled via `dz reqe --done` (fail-closed: an existing graded report is required).',
|
|
176
|
+
'',
|
|
177
|
+
].join('\n');
|
|
178
|
+
return { ok: true, error: null, grade, epilogue };
|
|
179
|
+
}
|
|
180
|
+
/** Render the debt list for `dz reqe` / the `dz usage` surfacing line. */
|
|
181
|
+
export function renderReqeList(debts, malformed) {
|
|
182
|
+
const lines = [];
|
|
183
|
+
if (debts.length === 0 && malformed === 0) {
|
|
184
|
+
lines.push('dz reqe: no re-QE debts — every recorded run kept cross-model QE (or none used the usage override).');
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
lines.push('dz reqe — ' + debts.length + ' unsettled re-QE debt(s):');
|
|
188
|
+
for (const d of debts) {
|
|
189
|
+
lines.push(' ' + d.slug + ' coder=' + d.coderFamily + ' qe=' + d.qeFamily + (d.qeGrade ? ' grade=' + d.qeGrade : '') + (d.emittedAt ? ' ' + d.emittedAt : '') + ' → dz reqe --slug ' + d.slug);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (malformed > 0)
|
|
193
|
+
lines.push(' ' + malformed + ' malformed debt file(s) skipped (named, never silent).');
|
|
194
|
+
lines.push(REQE_SCOPE);
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=reqe.js.map
|
package/dist/reqe.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reqe.js","sourceRoot":"","sources":["../src/reqe.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.150",
|
|
4
4
|
"description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"@dzhechkov/adapter-openclaude": "^0.1.0",
|
|
33
33
|
"@dzhechkov/adapter-opencode": "^0.2.0",
|
|
34
34
|
"yaml": "^2.0.0",
|
|
35
|
-
"@dzhechkov/adapter-
|
|
35
|
+
"@dzhechkov/adapter-agents-md": "0.1.1",
|
|
36
36
|
"@dzhechkov/adapter-cursor": "0.1.1",
|
|
37
|
-
"@dzhechkov/adapter-windsurf": "0.1.1",
|
|
38
37
|
"@dzhechkov/adapter-gemini": "0.1.1",
|
|
39
|
-
"@dzhechkov/adapter-
|
|
38
|
+
"@dzhechkov/adapter-copilot": "0.1.1",
|
|
39
|
+
"@dzhechkov/adapter-windsurf": "0.1.1",
|
|
40
40
|
"@dzhechkov/core": "0.2.14",
|
|
41
41
|
"@dzhechkov/memory": "0.2.9"
|
|
42
42
|
},
|
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "6d047e89ebd5532a54d5200fa86660b3a8f1e96e128a5a4042d718d2cacae2ec"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -1295,7 +1295,7 @@
|
|
|
1295
1295
|
"hashes": [
|
|
1296
1296
|
{
|
|
1297
1297
|
"alg": "SHA-256",
|
|
1298
|
-
"content": "
|
|
1298
|
+
"content": "1651978c5ce1fc510541d6ae3cc09f9cd74dde1e31d460d01b16f637dd80c8d2"
|
|
1299
1299
|
}
|
|
1300
1300
|
]
|
|
1301
1301
|
},
|
|
@@ -1305,7 +1305,7 @@
|
|
|
1305
1305
|
"hashes": [
|
|
1306
1306
|
{
|
|
1307
1307
|
"alg": "SHA-256",
|
|
1308
|
-
"content": "
|
|
1308
|
+
"content": "a8098db1327beb284a22ac8b2410d030578e55a0648c8c8fbc7934dfa966ef85"
|
|
1309
1309
|
}
|
|
1310
1310
|
]
|
|
1311
1311
|
},
|
|
@@ -1315,7 +1315,7 @@
|
|
|
1315
1315
|
"hashes": [
|
|
1316
1316
|
{
|
|
1317
1317
|
"alg": "SHA-256",
|
|
1318
|
-
"content": "
|
|
1318
|
+
"content": "46032dd7deeb29b7a71d13d068620bd197a9a4d503e2c74089cbff0b9314859e"
|
|
1319
1319
|
}
|
|
1320
1320
|
]
|
|
1321
1321
|
},
|
|
@@ -1325,7 +1325,7 @@
|
|
|
1325
1325
|
"hashes": [
|
|
1326
1326
|
{
|
|
1327
1327
|
"alg": "SHA-256",
|
|
1328
|
-
"content": "
|
|
1328
|
+
"content": "ad9870713ae8589d93e031048a9af4cea0371a35b53194870a672d02e66ccaaa"
|
|
1329
1329
|
}
|
|
1330
1330
|
]
|
|
1331
1331
|
},
|
|
@@ -2009,6 +2009,46 @@
|
|
|
2009
2009
|
}
|
|
2010
2010
|
]
|
|
2011
2011
|
},
|
|
2012
|
+
{
|
|
2013
|
+
"type": "file",
|
|
2014
|
+
"name": "dist/reqe.d.ts",
|
|
2015
|
+
"hashes": [
|
|
2016
|
+
{
|
|
2017
|
+
"alg": "SHA-256",
|
|
2018
|
+
"content": "115fa0797ad0e98a452f9987c15b038e22b18486043c8890c65cc32c9dc51af9"
|
|
2019
|
+
}
|
|
2020
|
+
]
|
|
2021
|
+
},
|
|
2022
|
+
{
|
|
2023
|
+
"type": "file",
|
|
2024
|
+
"name": "dist/reqe.d.ts.map",
|
|
2025
|
+
"hashes": [
|
|
2026
|
+
{
|
|
2027
|
+
"alg": "SHA-256",
|
|
2028
|
+
"content": "75996953748647f0cbe548c73daa5cb7cd1f8906f2df474926c31b9e5fa223f9"
|
|
2029
|
+
}
|
|
2030
|
+
]
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
"type": "file",
|
|
2034
|
+
"name": "dist/reqe.js",
|
|
2035
|
+
"hashes": [
|
|
2036
|
+
{
|
|
2037
|
+
"alg": "SHA-256",
|
|
2038
|
+
"content": "229014a8fa37faeb716d7f0deb22758a133ce50479b849b072eb6a285e78db40"
|
|
2039
|
+
}
|
|
2040
|
+
]
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
"type": "file",
|
|
2044
|
+
"name": "dist/reqe.js.map",
|
|
2045
|
+
"hashes": [
|
|
2046
|
+
{
|
|
2047
|
+
"alg": "SHA-256",
|
|
2048
|
+
"content": "c28182b280cf2c016a2b1d2ef15a3a335ff25993220767ae9fc7c74af76ba022"
|
|
2049
|
+
}
|
|
2050
|
+
]
|
|
2051
|
+
},
|
|
2012
2052
|
{
|
|
2013
2053
|
"type": "file",
|
|
2014
2054
|
"name": "dist/risk-scoring.d.ts",
|
|
@@ -2775,7 +2815,7 @@
|
|
|
2775
2815
|
"hashes": [
|
|
2776
2816
|
{
|
|
2777
2817
|
"alg": "SHA-256",
|
|
2778
|
-
"content": "
|
|
2818
|
+
"content": "76997cf27ff9c8901df35b45fbdb1c91f201396f070dab0579289b8aa9ac851c"
|
|
2779
2819
|
}
|
|
2780
2820
|
]
|
|
2781
2821
|
},
|
|
@@ -3095,7 +3135,7 @@
|
|
|
3095
3135
|
"hashes": [
|
|
3096
3136
|
{
|
|
3097
3137
|
"alg": "SHA-256",
|
|
3098
|
-
"content": "
|
|
3138
|
+
"content": "5eb07fb5e21e7be1e78a11243d6f86b9c84461eec22a954102549b5e237b7ab2"
|
|
3099
3139
|
}
|
|
3100
3140
|
]
|
|
3101
3141
|
},
|
|
@@ -3259,6 +3299,16 @@
|
|
|
3259
3299
|
}
|
|
3260
3300
|
]
|
|
3261
3301
|
},
|
|
3302
|
+
{
|
|
3303
|
+
"type": "file",
|
|
3304
|
+
"name": "src/reqe.ts",
|
|
3305
|
+
"hashes": [
|
|
3306
|
+
{
|
|
3307
|
+
"alg": "SHA-256",
|
|
3308
|
+
"content": "67988858ff527e5b465aa46b341aa710465e8cb0f72038be30f2419e0ff12cc3"
|
|
3309
|
+
}
|
|
3310
|
+
]
|
|
3311
|
+
},
|
|
3262
3312
|
{
|
|
3263
3313
|
"type": "file",
|
|
3264
3314
|
"name": "src/risk-scoring.ts",
|
|
@@ -4109,6 +4159,16 @@
|
|
|
4109
4159
|
}
|
|
4110
4160
|
]
|
|
4111
4161
|
},
|
|
4162
|
+
{
|
|
4163
|
+
"type": "file",
|
|
4164
|
+
"name": "test/reqe.test.ts",
|
|
4165
|
+
"hashes": [
|
|
4166
|
+
{
|
|
4167
|
+
"alg": "SHA-256",
|
|
4168
|
+
"content": "c5bb25b04caa8e9c14111ab838a135f7e2564b9fd1b7624520827bc95600661e"
|
|
4169
|
+
}
|
|
4170
|
+
]
|
|
4171
|
+
},
|
|
4112
4172
|
{
|
|
4113
4173
|
"type": "file",
|
|
4114
4174
|
"name": "test/routing-outcomes.test.ts",
|
package/src/index.ts
CHANGED
|
@@ -155,6 +155,30 @@ export {
|
|
|
155
155
|
checkpointAppendCmd,
|
|
156
156
|
} from './feature-adr-checkpoints.js';
|
|
157
157
|
export type { CheckpointStage, CheckpointEntry, ResumeMode, ResumeDecision, ParsedCheckpointRead } from './feature-adr-checkpoints.js';
|
|
158
|
+
export {
|
|
159
|
+
DOMAIN_LIFT_EXACT,
|
|
160
|
+
DOMAIN_LIFT_RELATED,
|
|
161
|
+
normalizeDomain,
|
|
162
|
+
domainMatch,
|
|
163
|
+
applyDomainBoost,
|
|
164
|
+
countDisplacedByCut,
|
|
165
|
+
renderDomainBoostNote,
|
|
166
|
+
renderDomainCutNote,
|
|
167
|
+
} from './recall-domain-boost.js';
|
|
168
|
+
export type { DomainMatch, DomainBoostResult } from './recall-domain-boost.js';
|
|
169
|
+
export {
|
|
170
|
+
REQE_SCHEMA,
|
|
171
|
+
REQE_SCOPE,
|
|
172
|
+
modelFamily,
|
|
173
|
+
shouldEmitReqeDebt,
|
|
174
|
+
buildReqeDebt,
|
|
175
|
+
parseReqeDebt,
|
|
176
|
+
buildReqeBrief,
|
|
177
|
+
extractReportGrade,
|
|
178
|
+
settleReqeDebt,
|
|
179
|
+
renderReqeList,
|
|
180
|
+
} from './reqe.js';
|
|
181
|
+
export type { ModelFamily, ReqeEmitDecision, ReqeDebt, ReqeBrief, ReqeSettlement } from './reqe.js';
|
|
158
182
|
export {
|
|
159
183
|
detectQueryLang,
|
|
160
184
|
relevanceFloorFor,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-aware recall re-ranking — the pure half of `dz recall --domain <name>`.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. `dz teach --domain <name>` has always recorded a domain, and
|
|
5
|
+
* `dz recall --all --stats` prints the per-domain histogram — but the RANKING path
|
|
6
|
+
* (`recallHybrid`: lexical FTS5 + optional vector leg merged by RRF) never looked at
|
|
7
|
+
* the field. So a store shared by different KINDS of work returns them interleaved:
|
|
8
|
+
* medical-research lessons dilute a coding recall and vice versa, and the dilution
|
|
9
|
+
* gets worse as either side grows.
|
|
10
|
+
*
|
|
11
|
+
* WHY A BOOST AND NOT A FILTER. Cross-pollination here is real, not theoretical:
|
|
12
|
+
* "a reviewer's evidence needs the same execute-don't-describe discipline as your
|
|
13
|
+
* own claims" was learned reviewing code and applies verbatim to medical sources;
|
|
14
|
+
* the medical report's core insight ("was the source ever opened?") is the same
|
|
15
|
+
* principle as our claim-check. A hard filter would cut exactly the transfers that
|
|
16
|
+
* make one store worth more than two. So: matching-domain hits move UP, foreign
|
|
17
|
+
* ones stay in the list.
|
|
18
|
+
*
|
|
19
|
+
* THE PROMISE, NARROWLY: this reorders. It NEVER drops a hit, never invents one,
|
|
20
|
+
* and never changes how many are returned. If the boost is wrong, the cost is
|
|
21
|
+
* ordering; it can't hide a lesson from you.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { RecallHit } from './patterns.js';
|
|
25
|
+
|
|
26
|
+
/** How many places a same-domain lesson may climb. STATED AS POSITIONS, not as a
|
|
27
|
+
* score multiplier: "moves up at most two places" is a sentence a reader can check
|
|
28
|
+
* against the output, while "index × 0.55" is an opaque constant whose behaviour
|
|
29
|
+
* changes with position (it could not lift index 2 past index 1 at all, but did
|
|
30
|
+
* lift index 3 past index 2 — an accident, not a design). A bounded lift keeps
|
|
31
|
+
* lexical relevance dominant: a domain tag is a hint about relevance, not evidence
|
|
32
|
+
* of it, so it breaks near-ties instead of overruling the ranking. */
|
|
33
|
+
export const DOMAIN_LIFT_EXACT = 2;
|
|
34
|
+
|
|
35
|
+
/** A prefix/suffix relative (`health-research` against `health`) climbs less. */
|
|
36
|
+
export const DOMAIN_LIFT_RELATED = 1;
|
|
37
|
+
|
|
38
|
+
export type DomainMatch = 'exact' | 'related' | 'none';
|
|
39
|
+
|
|
40
|
+
/** Normalize a domain tag for comparison: case- and separator-insensitive, so
|
|
41
|
+
* `Health-Research`, `health_research` and `health research` are one domain. */
|
|
42
|
+
export function normalizeDomain(domain: string | null | undefined): string {
|
|
43
|
+
return String(domain ?? '')
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[\s_]+/g, '-')
|
|
46
|
+
.replace(/^-+|-+$/g, '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** How a hit's domain relates to the requested one. `related` covers the common
|
|
50
|
+
* hierarchy shapes (`health` ↔ `health-research`) without a taxonomy: a taxonomy
|
|
51
|
+
* nobody maintains drifts, and a wrong taxonomy is worse than none. */
|
|
52
|
+
export function domainMatch(hitDomain: string | null | undefined, wanted: string | null | undefined): DomainMatch {
|
|
53
|
+
const a = normalizeDomain(hitDomain);
|
|
54
|
+
const b = normalizeDomain(wanted);
|
|
55
|
+
if (a === '' || b === '') return 'none';
|
|
56
|
+
if (a === b) return 'exact';
|
|
57
|
+
if (a.startsWith(`${b}-`) || b.startsWith(`${a}-`)) return 'related';
|
|
58
|
+
return 'none';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DomainBoostResult {
|
|
62
|
+
readonly hits: readonly RecallHit[];
|
|
63
|
+
/** How many hits MATCHED the domain (exactly / relatedly). */
|
|
64
|
+
readonly exact: number;
|
|
65
|
+
readonly related: number;
|
|
66
|
+
/** How many hits actually CHANGED position. Counted separately because the note
|
|
67
|
+
* used to print the match counts as "lifted" — an exact match already at rank 0,
|
|
68
|
+
* or a list that is entirely one domain, matches without moving, and calling that
|
|
69
|
+
* "lifted" tells the reader the ranking changed when it did not. */
|
|
70
|
+
readonly moved: number;
|
|
71
|
+
/** True when NOTHING in the result matched: the caller says so out loud rather
|
|
72
|
+
* than implying the ranking is domain-aware when it had nothing to work with. */
|
|
73
|
+
readonly noMatches: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Re-rank hits so same-domain lessons surface first, keeping every hit.
|
|
78
|
+
*
|
|
79
|
+
* Effective position = `index - lift`. The original relevance order dominates; the
|
|
80
|
+
* domain only breaks near-ties, and the lift is BOUNDED so a tail match can never
|
|
81
|
+
* teleport to the top. Ties keep the incoming order (stable sort), so the function
|
|
82
|
+
* is deterministic — the same input always yields the same output, which is what
|
|
83
|
+
* makes it testable at all.
|
|
84
|
+
*/
|
|
85
|
+
export function applyDomainBoost(hits: readonly RecallHit[], wanted: string | null | undefined): DomainBoostResult {
|
|
86
|
+
const target = normalizeDomain(wanted);
|
|
87
|
+
if (target === '' || hits.length === 0) {
|
|
88
|
+
return { hits, exact: 0, related: 0, moved: 0, noMatches: true };
|
|
89
|
+
}
|
|
90
|
+
let exact = 0;
|
|
91
|
+
let related = 0;
|
|
92
|
+
const scored = hits.map((hit, index) => {
|
|
93
|
+
const match = domainMatch(hit.pattern.domain, target);
|
|
94
|
+
if (match === 'exact') exact += 1;
|
|
95
|
+
else if (match === 'related') related += 1;
|
|
96
|
+
const lift = match === 'exact' ? DOMAIN_LIFT_EXACT : match === 'related' ? DOMAIN_LIFT_RELATED : 0;
|
|
97
|
+
return { hit, index, lift, effective: index - lift };
|
|
98
|
+
});
|
|
99
|
+
// Tie-break: at equal effective position the LIFTED hit goes first, then the
|
|
100
|
+
// original order. Without this the foreign hit it landed level with won the tie
|
|
101
|
+
// by having the lower original index, so a lift of N moved the hit only N-1
|
|
102
|
+
// places — the code quietly delivered less than the constant promised. A test
|
|
103
|
+
// asserting the documented bound caught it; the fix is here, not in the test.
|
|
104
|
+
scored.sort((a, b) => (a.effective - b.effective) || (b.lift - a.lift) || (a.index - b.index));
|
|
105
|
+
const reordered = scored.map((s) => s.hit);
|
|
106
|
+
let moved = 0;
|
|
107
|
+
scored.forEach((s, newIndex) => { if (s.index !== newIndex) moved += 1; });
|
|
108
|
+
return { hits: reordered, exact, related, moved, noMatches: exact === 0 && related === 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* How many hits the CUT hid that an unboosted recall would have shown.
|
|
113
|
+
*
|
|
114
|
+
* WHY THIS EXISTS. `applyDomainBoost` never drops a hit — but the CLI still cuts the
|
|
115
|
+
* list at `--limit`, and a promotion INTO the top N necessarily pushes something out
|
|
116
|
+
* of it. Cross-model review found the resulting sentence to be a lie by omission: the
|
|
117
|
+
* note said "foreign-domain lessons kept" while `foreign-B`, visible without
|
|
118
|
+
* `--domain`, had vanished from the printed output. Both statements were true of
|
|
119
|
+
* different lists, which is exactly how an honest tool ends up misleading its reader.
|
|
120
|
+
*
|
|
121
|
+
* The fix is not to stop promoting — that would be the filter we refused to build. It
|
|
122
|
+
* is to COUNT what fell past the cut and say so, because the reader can act on that
|
|
123
|
+
* (raise `--limit`) only if they know it happened.
|
|
124
|
+
*
|
|
125
|
+
* Compares by identity: both lists hold the same hit objects.
|
|
126
|
+
*/
|
|
127
|
+
export function countDisplacedByCut(
|
|
128
|
+
original: readonly RecallHit[],
|
|
129
|
+
boosted: readonly RecallHit[],
|
|
130
|
+
limit: number,
|
|
131
|
+
): number {
|
|
132
|
+
if (!Number.isFinite(limit) || limit <= 0) return 0;
|
|
133
|
+
const shownAfter = new Set(boosted.slice(0, limit));
|
|
134
|
+
let displaced = 0;
|
|
135
|
+
for (const hit of original.slice(0, limit)) {
|
|
136
|
+
if (!shownAfter.has(hit)) displaced += 1;
|
|
137
|
+
}
|
|
138
|
+
return displaced;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The line that reports the cut. Empty when nothing was displaced, so the common
|
|
142
|
+
* case stays quiet. */
|
|
143
|
+
export function renderDomainCutNote(displaced: number, limit: number): string {
|
|
144
|
+
if (displaced <= 0) return '';
|
|
145
|
+
const plural = displaced === 1 ? 'lesson' : 'lessons';
|
|
146
|
+
return ` ${displaced} lower-ranked ${plural} fell past the --limit ${limit} cut to make room — raise --limit to see them (the boost promotes; the cut is what hides)`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One honest line about what the boost did — including the case where it did
|
|
150
|
+
* nothing, which a silent reorder would hide.
|
|
151
|
+
*
|
|
152
|
+
* The two tail phrases below are a WIRE CONTRACT: `learning_bridge.py` in the
|
|
153
|
+
* `goap-research-ed25519` skill detects whether the installed `dz` supports
|
|
154
|
+
* `--domain` by looking for them, because an older CLI ignores the flag and exits 0
|
|
155
|
+
* (so an exit code cannot tell the versions apart). A test pins both strings — if you
|
|
156
|
+
* reword them, update the bridge in the same change or you switch that loop into
|
|
157
|
+
* permanent degraded mode without a single test turning red. */
|
|
158
|
+
export function renderDomainBoostNote(result: DomainBoostResult, wanted: string): string {
|
|
159
|
+
if (result.noMatches) {
|
|
160
|
+
return ` domain "${wanted}": no lesson in this result carries it — order unchanged, nothing was hidden`;
|
|
161
|
+
}
|
|
162
|
+
const parts = [`${result.exact} exact`];
|
|
163
|
+
if (result.related > 0) parts.push(`${result.related} related`);
|
|
164
|
+
const effect = result.moved === 0
|
|
165
|
+
? 'the order was already correct — nothing moved'
|
|
166
|
+
: `${result.moved} changed position`;
|
|
167
|
+
// The counts describe the CANDIDATES the boost ranked, which is a longer list than
|
|
168
|
+
// the one printed (the caller over-fetches, then cuts at --limit). Saying "3 changed
|
|
169
|
+
// position" above two printed lines reads as an arithmetic error unless the note
|
|
170
|
+
// names the list it is talking about — so it does.
|
|
171
|
+
return ` domain "${wanted}": among ${result.hits.length} candidate(s) — ${parts.join(', ')} match(es), ${effect}; foreign-domain lessons kept (a boost, not a filter)`;
|
|
172
|
+
}
|