@dzhechkov/harness-core 0.8.30 → 0.8.31
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 +233 -93
- package/README.md +269 -0
- package/dist/agentdb-index.d.ts +45 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +345 -85
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +170 -0
- package/dist/agentdb-reindex-marker.d.ts.map +1 -0
- package/dist/agentdb-reindex-marker.js +293 -0
- package/dist/agentdb-reindex-marker.js.map +1 -0
- package/dist/agentdb-snapshot-rotation.d.ts +169 -0
- package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
- package/dist/agentdb-snapshot-rotation.js +322 -0
- package/dist/agentdb-snapshot-rotation.js.map +1 -0
- package/dist/agentdb-snapshot.d.ts +126 -0
- package/dist/agentdb-snapshot.d.ts.map +1 -0
- package/dist/agentdb-snapshot.js +195 -0
- package/dist/agentdb-snapshot.js.map +1 -0
- package/dist/apply-leg.d.ts +202 -0
- package/dist/apply-leg.d.ts.map +1 -0
- package/dist/apply-leg.js +975 -0
- package/dist/apply-leg.js.map +1 -0
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +57 -1
- package/dist/book-kb.js.map +1 -1
- package/dist/brain.d.ts +3 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +1 -1
- package/dist/brain.js.map +1 -1
- package/dist/cadence.d.ts +30 -1
- package/dist/cadence.d.ts.map +1 -1
- package/dist/cadence.js +66 -2
- package/dist/cadence.js.map +1 -1
- package/dist/guard.d.ts +14 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +36 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-probe-worker.d.ts.map +1 -1
- package/dist/integration-probe-worker.js +4 -6
- package/dist/integration-probe-worker.js.map +1 -1
- package/dist/integrations-verify.d.ts.map +1 -1
- package/dist/integrations-verify.js +3 -1
- package/dist/integrations-verify.js.map +1 -1
- package/dist/managed-hooks.d.ts +11 -2
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +33 -7
- package/dist/managed-hooks.js.map +1 -1
- package/dist/mutation-gate.d.ts +35 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +49 -9
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +23 -4
- package/dist/patterns.js.map +1 -1
- package/dist/repository-origin.d.ts +1 -1
- package/dist/repository-origin.d.ts.map +1 -1
- package/dist/repository-origin.js +7 -1
- package/dist/repository-origin.js.map +1 -1
- package/dist/round-exec.d.ts +41 -0
- package/dist/round-exec.d.ts.map +1 -0
- package/dist/round-exec.js +45 -0
- package/dist/round-exec.js.map +1 -0
- package/dist/round.d.ts +109 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +148 -0
- package/dist/round.js.map +1 -0
- package/dist/run-records.d.ts +8 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +69 -3
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +166 -3
- package/dist/setup.js.map +1 -1
- package/dist/sqlite-read-helpers.d.ts +60 -0
- package/dist/sqlite-read-helpers.d.ts.map +1 -0
- package/dist/sqlite-read-helpers.js +118 -0
- package/dist/sqlite-read-helpers.js.map +1 -0
- package/dist/statusline.d.ts +20 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +40 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +30 -0
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +189 -0
- package/dist/store-counts.js.map +1 -1
- package/dist/usage.d.ts +55 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +184 -33
- package/dist/usage.js.map +1 -1
- package/dist/vector-tier.d.ts +18 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +26 -2
- package/dist/vector-tier.js.map +1 -1
- package/package.json +6 -6
- package/sbom.json +442 -92
- package/src/agentdb-index.ts +315 -32
- package/src/agentdb-reindex-marker.ts +330 -0
- package/src/agentdb-snapshot-rotation.ts +393 -0
- package/src/agentdb-snapshot.ts +222 -0
- package/src/apply-leg.ts +1046 -0
- package/src/book-kb.ts +64 -1
- package/src/brain.ts +11 -2
- package/src/cadence.ts +87 -2
- package/src/guard.ts +46 -0
- package/src/index.ts +58 -5
- package/src/integration-probe-worker.ts +4 -6
- package/src/integrations-verify.ts +3 -1
- package/src/managed-hooks.ts +32 -7
- package/src/mutation-gate.ts +87 -8
- package/src/operations.ts +89 -0
- package/src/patterns.ts +23 -4
- package/src/repository-origin.ts +7 -1
- package/src/round-exec.ts +85 -0
- package/src/round.ts +228 -0
- package/src/run-records.ts +77 -2
- package/src/setup.ts +198 -3
- package/src/sqlite-read-helpers.ts +128 -0
- package/src/statusline.ts +57 -9
- package/src/store-counts.ts +245 -1
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
package/src/mutation-gate.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface MutationRegistryEntry {
|
|
|
30
30
|
readonly file: string;
|
|
31
31
|
/** exact text surgery: `find` must occur EXACTLY ONCE in the file (rule 1). */
|
|
32
32
|
readonly mutation: { readonly find: string; readonly replace: string };
|
|
33
|
+
/** test files that discriminate this entry; a named registry self-check must remain runnable. */
|
|
34
|
+
readonly tests?: readonly string[];
|
|
33
35
|
/** how many tests MUST go red under the mutation (default 1). */
|
|
34
36
|
readonly minFailing?: number;
|
|
35
37
|
/** how many actually did at the time of writing — makes a coverage DROP visible. */
|
|
@@ -42,6 +44,34 @@ export interface MutationRegistryEntry {
|
|
|
42
44
|
readonly maxFailing?: number;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/** Registry integrity tests add the same unrelated failure to every ordinary mutant run. */
|
|
48
|
+
export const REGISTRY_SELFCHECK_TESTS = [
|
|
49
|
+
'test/mutation-registry-freshness.test.ts',
|
|
50
|
+
'test/mutation-registry-anchors.test.ts',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export interface MutationTestCommand {
|
|
54
|
+
readonly testCommand: string;
|
|
55
|
+
readonly excluded: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Build the mutant-only command; baseline commands remain unchanged in the executor. */
|
|
59
|
+
export function buildMutationTestCommand(
|
|
60
|
+
testCommand: string,
|
|
61
|
+
entry: Pick<MutationRegistryEntry, 'tests'>,
|
|
62
|
+
): MutationTestCommand {
|
|
63
|
+
const entryTests = new Set(entry.tests ?? []);
|
|
64
|
+
const commandTokens = testCommand.split(/\s+/);
|
|
65
|
+
const excluded = REGISTRY_SELFCHECK_TESTS.filter(
|
|
66
|
+
(testFile) => commandTokens.includes(testFile) && !entryTests.has(testFile),
|
|
67
|
+
);
|
|
68
|
+
const excludedSet = new Set<string>(excluded);
|
|
69
|
+
return {
|
|
70
|
+
testCommand: commandTokens.filter((token) => !excludedSet.has(token)).join(' '),
|
|
71
|
+
excluded,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
export interface MutationRegistry {
|
|
46
76
|
/** optional suite command override for the whole registry (default `npm test`). */
|
|
47
77
|
readonly testCommand?: string;
|
|
@@ -112,6 +142,24 @@ export interface MutationObservation {
|
|
|
112
142
|
readonly rebaselineFailureReason?: string;
|
|
113
143
|
/** parsed failing files from a RED restored-tree run; absent when no red rebaseline ran. */
|
|
114
144
|
readonly rebaselineAttribution?: BaselineAttribution;
|
|
145
|
+
/** bounded stdout+stderr tail supplied by the executor for a RED restored-tree run. */
|
|
146
|
+
readonly rebaselineOutputTail?: string;
|
|
147
|
+
/**
|
|
148
|
+
* path to the FULL stdout+stderr the executor saved for a RED baseline/rebaseline run (gate
|
|
149
|
+
* stability, 2026-09-12) — the bounded tail above is a diagnostic teaser; a multi-entry gate run
|
|
150
|
+
* that goes INCONCLUSIVE/OVER_FAILING needs the whole log to attribute the redness, and the tail
|
|
151
|
+
* alone was measured to hand back three lines of an unrelated neighbour's stderr. Absent when the
|
|
152
|
+
* run was green (nothing written) or the executor could not write the file.
|
|
153
|
+
*/
|
|
154
|
+
readonly outputPath?: string;
|
|
155
|
+
/**
|
|
156
|
+
* set (to the caught error's message) when the executor tried and FAILED to save the full RED
|
|
157
|
+
* output — EACCES/ENOSPC/EROFS/ENOTDIR and the like (fix-round-1, HIGH/MEDIUM review findings,
|
|
158
|
+
* 2026-09-12). Mutually exclusive with `outputPath`: a red run either saved (path) or did not
|
|
159
|
+
* (error), never both, and a green run has neither. Absent/undefined = not attempted (green) or
|
|
160
|
+
* the save succeeded.
|
|
161
|
+
*/
|
|
162
|
+
readonly outputError?: string;
|
|
115
163
|
/** bounded log proving an internal runner failure received at most one retry. */
|
|
116
164
|
readonly internalAttemptLog?: string;
|
|
117
165
|
}
|
|
@@ -137,6 +185,12 @@ export interface MutationEntryResult {
|
|
|
137
185
|
* of letting the reader assume it covers every entry.
|
|
138
186
|
*/
|
|
139
187
|
readonly dropComparable: boolean;
|
|
188
|
+
/** full bounded restored-tree output tail for machine-readable reports. */
|
|
189
|
+
readonly rebaselineOutputTail?: string;
|
|
190
|
+
/** path to the full saved output for a RED baseline/rebaseline run; see MutationObservation. */
|
|
191
|
+
readonly outputPath?: string;
|
|
192
|
+
/** the save FAILED for a RED run; see MutationObservation.outputError. Mutually exclusive with `outputPath`. */
|
|
193
|
+
readonly outputError?: string;
|
|
140
194
|
/** human sentence for the report line — names the undefended property on a green suite. */
|
|
141
195
|
readonly detail: string;
|
|
142
196
|
}
|
|
@@ -371,6 +425,9 @@ export function parseMutationRegistry(text: string): ParsedRegistry {
|
|
|
371
425
|
property: o['property'].trim(),
|
|
372
426
|
file,
|
|
373
427
|
mutation: { find: mut.find, replace: mut.replace },
|
|
428
|
+
...(Array.isArray(o['tests']) && o['tests'].every((test) => typeof test === 'string')
|
|
429
|
+
? { tests: o['tests'] as string[] }
|
|
430
|
+
: {}),
|
|
374
431
|
minFailing,
|
|
375
432
|
...(observed !== undefined ? { observed } : {}),
|
|
376
433
|
...(maxFailing !== undefined ? { maxFailing } : {}),
|
|
@@ -717,6 +774,19 @@ export interface BaselineResult {
|
|
|
717
774
|
readonly reason?: BaselineFailureReason;
|
|
718
775
|
}
|
|
719
776
|
|
|
777
|
+
/**
|
|
778
|
+
* `; full output: <path>` when the executor saved the full red-run output, `; full output NOT
|
|
779
|
+
* saved: <error>` when it tried and failed (fix-round-1 MEDIUM finding — an EACCES/ENOSPC/EROFS
|
|
780
|
+
* during the save must not vanish silently), else '' — byte-identical to the pre-gate-stability
|
|
781
|
+
* text when neither is known (NFR-1). `outputPath` takes precedence if somehow both are set (the
|
|
782
|
+
* callers never set both).
|
|
783
|
+
*/
|
|
784
|
+
function outputPathSuffix(outputPath?: string, outputError?: string): string {
|
|
785
|
+
if (outputPath !== undefined) return `; full output: ${outputPath}`;
|
|
786
|
+
if (outputError !== undefined) return `; full output NOT saved: ${outputError}`;
|
|
787
|
+
return '';
|
|
788
|
+
}
|
|
789
|
+
|
|
720
790
|
/**
|
|
721
791
|
* A RED baseline in the scratch copy is a SETUP error, never a mutation result: every subsequent
|
|
722
792
|
* "red under mutation" would be noise, and every "green" a lie about an unrunnable copy.
|
|
@@ -725,14 +795,17 @@ export function classifyBaseline(
|
|
|
725
795
|
exitCode: number | null,
|
|
726
796
|
runFailureReason?: string,
|
|
727
797
|
attribution?: BaselineAttribution,
|
|
798
|
+
outputPath?: string,
|
|
799
|
+
outputError?: string,
|
|
728
800
|
): BaselineResult {
|
|
729
801
|
if (exitCode === 0) return { ok: true, detail: 'baseline suite green in the scratch copy' };
|
|
802
|
+
const pathSuffix = outputPathSuffix(outputPath, outputError);
|
|
730
803
|
if (exitCode === null) {
|
|
731
804
|
const internal = runFailureReason?.startsWith('runner-internal-error:') === true;
|
|
732
805
|
return {
|
|
733
806
|
ok: false,
|
|
734
807
|
reason: internal ? 'runner-internal-error' : 'runner-no-exit',
|
|
735
|
-
detail: `baseline INCONCLUSIVE — suite produced no exit code (${runFailureReason ?? 'unknown timeout/spawn failure'}) — the copy is not runnable; do not read this as a mutation result`,
|
|
808
|
+
detail: `baseline INCONCLUSIVE — suite produced no exit code (${runFailureReason ?? 'unknown timeout/spawn failure'}) — the copy is not runnable; do not read this as a mutation result${pathSuffix}`,
|
|
736
809
|
};
|
|
737
810
|
}
|
|
738
811
|
const infrastructure = attribution?.infrastructureFailure;
|
|
@@ -740,14 +813,14 @@ export function classifyBaseline(
|
|
|
740
813
|
return {
|
|
741
814
|
ok: false,
|
|
742
815
|
...(infrastructure.reason !== undefined ? { reason: infrastructure.reason } : {}),
|
|
743
|
-
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — runner infrastructure failure: ${infrastructure.reason} — ${infrastructure.evidence}; the broken copy cannot prove anything`,
|
|
816
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — runner infrastructure failure: ${infrastructure.reason} — ${infrastructure.evidence}; the broken copy cannot prove anything${pathSuffix}`,
|
|
744
817
|
};
|
|
745
818
|
}
|
|
746
819
|
if (attribution === undefined || attribution.parsedFrom === 'unparseable') {
|
|
747
820
|
return {
|
|
748
821
|
ok: false,
|
|
749
822
|
reason: 'baseline-red-files-unparseable',
|
|
750
|
-
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — failing files: unparseable from runner output — the broken copy cannot prove anything`,
|
|
823
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — failing files: unparseable from runner output — the broken copy cannot prove anything${pathSuffix}`,
|
|
751
824
|
};
|
|
752
825
|
}
|
|
753
826
|
const failing = `failing files: ${attribution.failingFiles.join(', ')}`;
|
|
@@ -755,13 +828,13 @@ export function classifyBaseline(
|
|
|
755
828
|
return {
|
|
756
829
|
ok: false,
|
|
757
830
|
reason: 'extraneous-red-in-allowlist',
|
|
758
|
-
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — extraneous red in the testCommand allowlist; the registry entries themselves are not disproven`,
|
|
831
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — extraneous red in the testCommand allowlist; the registry entries themselves are not disproven${pathSuffix}`,
|
|
759
832
|
};
|
|
760
833
|
}
|
|
761
834
|
return {
|
|
762
835
|
ok: false,
|
|
763
836
|
reason: 'baseline-red-covered-files',
|
|
764
|
-
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — broken-copy baseline redness touches registry-covered files; fix the copy before evaluating mutations`,
|
|
837
|
+
detail: `baseline suite RED (exit ${exitCode}) in the UNMUTATED scratch copy — ${failing} — broken-copy baseline redness touches registry-covered files; fix the copy before evaluating mutations${pathSuffix}`,
|
|
765
838
|
};
|
|
766
839
|
}
|
|
767
840
|
|
|
@@ -814,6 +887,9 @@ function classifyMutationOutcomeWithoutAttemptLog(obs: MutationObservation): Mut
|
|
|
814
887
|
failingCount: obs.failingCount,
|
|
815
888
|
// an `observed` anchor is the ONLY thing that makes a drop detectable at all (round-7 honesty)
|
|
816
889
|
dropComparable: e.observed !== undefined,
|
|
890
|
+
...(obs.rebaselineOutputTail !== undefined ? { rebaselineOutputTail: obs.rebaselineOutputTail } : {}),
|
|
891
|
+
...(obs.outputPath !== undefined ? { outputPath: obs.outputPath } : {}),
|
|
892
|
+
...(obs.outputError !== undefined ? { outputError: obs.outputError } : {}),
|
|
817
893
|
};
|
|
818
894
|
|
|
819
895
|
if (obs.occurrences !== 1) {
|
|
@@ -864,7 +940,7 @@ function classifyMutationOutcomeWithoutAttemptLog(obs: MutationObservation): Mut
|
|
|
864
940
|
applied: true,
|
|
865
941
|
verdict: 'INCONCLUSIVE',
|
|
866
942
|
drop: false,
|
|
867
|
-
detail: `suite produced NO exit code under the mutation (${obs.runFailureReason ?? 'unknown timeout / spawn failure'}) — inconclusive is a FAILURE, never a pass`,
|
|
943
|
+
detail: `suite produced NO exit code under the mutation (${obs.runFailureReason ?? 'unknown timeout / spawn failure'}) — inconclusive is a FAILURE, never a pass${outputPathSuffix(obs.outputPath, obs.outputError)}`,
|
|
868
944
|
};
|
|
869
945
|
}
|
|
870
946
|
|
|
@@ -898,7 +974,7 @@ function classifyMutationOutcomeWithoutAttemptLog(obs: MutationObservation): Mut
|
|
|
898
974
|
applied: true,
|
|
899
975
|
verdict: 'INCONCLUSIVE',
|
|
900
976
|
drop: false,
|
|
901
|
-
detail: `suite red but the runner output is UNRECOGNISED (${obs.outputUnrecognised.slice(0, 220)}) — this tool has no classifier for the output shape, so file-load redness cannot be told from assertion redness; INCONCLUSIVE is a FAILURE, never a pass (run the gate with a node --test or vitest test command, or extend classifyRunFailure)`,
|
|
977
|
+
detail: `suite red but the runner output is UNRECOGNISED (${obs.outputUnrecognised.slice(0, 220)}) — this tool has no classifier for the output shape, so file-load redness cannot be told from assertion redness; INCONCLUSIVE is a FAILURE, never a pass (run the gate with a node --test or vitest test command, or extend classifyRunFailure)${outputPathSuffix(obs.outputPath, obs.outputError)}`,
|
|
902
978
|
};
|
|
903
979
|
}
|
|
904
980
|
|
|
@@ -912,12 +988,15 @@ function classifyMutationOutcomeWithoutAttemptLog(obs: MutationObservation): Mut
|
|
|
912
988
|
: obs.rebaselineAttribution === undefined || obs.rebaselineAttribution.parsedFrom === 'unparseable'
|
|
913
989
|
? 'failing files: unparseable from runner output'
|
|
914
990
|
: `failing files: ${obs.rebaselineAttribution.failingFiles.join(', ')}`;
|
|
991
|
+
const outputTailDetail = obs.rebaselineOutputTail === undefined
|
|
992
|
+
? ''
|
|
993
|
+
: `; output tail (first 3 lines):\n${obs.rebaselineOutputTail.split(/\r?\n/).slice(0, 3).join('\n')}`;
|
|
915
994
|
return {
|
|
916
995
|
...base,
|
|
917
996
|
applied: true,
|
|
918
997
|
verdict: 'INCONCLUSIVE',
|
|
919
998
|
drop: false,
|
|
920
|
-
detail: `suite red under the mutation BUT the restored baseline did not reproduce green (${obs.rebaselineExitCode === null ? `no exit code: ${obs.rebaselineFailureReason ?? 'unknown timeout / spawn failure'}` : `exit ${obs.rebaselineExitCode}`}) — mutation did not revert / flaky restored-tree route; ${failing}; the redness is not attributable to the protection`,
|
|
999
|
+
detail: `suite red under the mutation BUT the restored baseline did not reproduce green (${obs.rebaselineExitCode === null ? `no exit code: ${obs.rebaselineFailureReason ?? 'unknown timeout / spawn failure'}` : `exit ${obs.rebaselineExitCode}`}) — mutation did not revert / flaky restored-tree route; ${failing}${outputTailDetail}; the redness is not attributable to the protection${outputPathSuffix(obs.outputPath, obs.outputError)}`,
|
|
921
1000
|
};
|
|
922
1001
|
}
|
|
923
1002
|
|
package/src/operations.ts
CHANGED
|
@@ -1360,6 +1360,58 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
1360
1360
|
}
|
|
1361
1361
|
} catch { /* doctor never throws on a diagnostic */ }
|
|
1362
1362
|
|
|
1363
|
+
// APPLY-LEG INSTALLED (feature `setup-installs-apply-leg`, ADR-001 Decision 3). Deliberately
|
|
1364
|
+
// OUTSIDE the `existsSync(writerPath)` guard below, for the same reason the Codex apply-leg block
|
|
1365
|
+
// above is: MEASURED 2026-09-12, `dz parity` printed a static "✓ automatic apply-leg" for
|
|
1366
|
+
// `learning-apply` regardless of what the project actually had installed — a capability read off
|
|
1367
|
+
// a table, not off the project. `applyLegStatus` is the ONE measurement; this check and
|
|
1368
|
+
// `dz parity`'s `learning-apply` cell both read it, so they cannot disagree.
|
|
1369
|
+
try {
|
|
1370
|
+
const { applyLegStatus, applyLegReasonMessage } = await import('./apply-leg.js');
|
|
1371
|
+
let configuredBackend: 'agentdb' | 'jsonl' | 'unknown' = 'unknown';
|
|
1372
|
+
try {
|
|
1373
|
+
const cfg = JSON.parse(readFileSync(join(root, '.dz', 'config.json'), 'utf-8')) as { memory?: { backend?: string } };
|
|
1374
|
+
if (cfg.memory?.backend === 'agentdb') configuredBackend = 'agentdb';
|
|
1375
|
+
else if (cfg.memory?.backend === 'jsonl') configuredBackend = 'jsonl';
|
|
1376
|
+
} catch { /* no .dz/config.json yet — nothing to check */ }
|
|
1377
|
+
|
|
1378
|
+
if (configuredBackend === 'agentdb') {
|
|
1379
|
+
// `applyLegStatus` is the ONE measurement, `installed` already folds in version + structural
|
|
1380
|
+
// hook wiring (fix round 1, HIGH findings 1/2) — a stale-versioned or unreadable helper is
|
|
1381
|
+
// `installed:false` with a `reason`, never a separate ad-hoc re-check of the same facts here.
|
|
1382
|
+
const status = applyLegStatus(root);
|
|
1383
|
+
if (!status.installed) {
|
|
1384
|
+
if (status.reason === 'stale-version') {
|
|
1385
|
+
// Distinct row name from "apply-leg installed", exactly as `AGENTDB_WRITER_VERSION`'s own
|
|
1386
|
+
// staleness check does — `dz parity` reads the SAME `applyLegReasonMessage` for its
|
|
1387
|
+
// `learning-apply` remedy note, so the two instruments cannot disagree about WHY.
|
|
1388
|
+
checks.push({ name: 'apply-leg version', ok: false, detail: applyLegReasonMessage(status) });
|
|
1389
|
+
} else if (status.reason === 'unreadable') {
|
|
1390
|
+
// Q3 (fix round 1): an EXISTING-but-unreadable helper used to be swallowed by this whole
|
|
1391
|
+
// block's outer catch (silence) — now a named, non-OK row, not folded into "not installed".
|
|
1392
|
+
checks.push({ name: 'apply-leg unreadable', ok: false, detail: applyLegReasonMessage(status) });
|
|
1393
|
+
} else {
|
|
1394
|
+
checks.push({
|
|
1395
|
+
name: 'apply-leg installed',
|
|
1396
|
+
ok: false,
|
|
1397
|
+
detail: 'NOT INSTALLED — run: dz setup --target claude-code --memory agentdb',
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
// Liveness (embed.sock) stays the existing check below, gated on the writer file — installed
|
|
1402
|
+
// implies the recall hook + settings entries exist; whether the daemon is CURRENTLY alive is
|
|
1403
|
+
// a separate, already-covered fact.
|
|
1404
|
+
} else if (configuredBackend === 'jsonl') {
|
|
1405
|
+
checks.push({
|
|
1406
|
+
name: 'apply-leg',
|
|
1407
|
+
ok: true,
|
|
1408
|
+
detail: 'apply-leg unavailable on jsonl backend (needs --memory agentdb)',
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
// configuredBackend === 'unknown' (no .dz/config.json yet): nothing to check — a project that
|
|
1412
|
+
// never ran `dz setup` gets no apply-leg opinion, same as every other doctor check here.
|
|
1413
|
+
} catch { /* doctor never throws on a diagnostic */ }
|
|
1414
|
+
|
|
1363
1415
|
const writerPath = join(root, '.dz', 'agentdb-writer.mjs');
|
|
1364
1416
|
if (existsSync(writerPath)) {
|
|
1365
1417
|
const { writerVersionOf, AGENTDB_WRITER_VERSION } = await import('./setup.js');
|
|
@@ -1517,6 +1569,43 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
1517
1569
|
}
|
|
1518
1570
|
} catch { /* advisory only — the vector tier must never fail doctor */ }
|
|
1519
1571
|
|
|
1572
|
+
// Mirror writer state (issue #10 defect 6, AM-9, feature setup-installs-apply-leg): the SAME
|
|
1573
|
+
// reason `dz vector status` and `dz teach` both read (`mirrorWriterReason`) — one function, so a
|
|
1574
|
+
// "legacy-shape" config (a top-level `backend` key instead of `memory.backend`) cannot be silent
|
|
1575
|
+
// in doctor while `dz teach` is silent too. Informational only (`ok:true`): a mirror that is OFF
|
|
1576
|
+
// by explicit config (`engine-off`) or simply unconfigured (`no-config`) is not a doctor FAILURE,
|
|
1577
|
+
// it is a fact worth naming once per run rather than never.
|
|
1578
|
+
try {
|
|
1579
|
+
const { mirrorWriterReason: reasonFn, mirrorWriterExplanation: explainFn } = await import('./vector-tier.js');
|
|
1580
|
+
const mirror = reasonFn(root);
|
|
1581
|
+
// 'no-config' is the honest quiet baseline (a project that never ran `dz setup` at all) — every
|
|
1582
|
+
// OTHER state means `.dz/config.json` exists and has an opinion worth naming, legacy-shape
|
|
1583
|
+
// included, so THAT is the line this check exists to stop being silent about.
|
|
1584
|
+
if (mirror.state !== 'no-config') {
|
|
1585
|
+
checks.push({
|
|
1586
|
+
name: 'mirror writer',
|
|
1587
|
+
ok: true,
|
|
1588
|
+
detail: `${mirror.enabled ? 'ON' : 'OFF'} (${explainFn(mirror.state)})`,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
} catch { /* advisory only — the vector tier must never fail doctor */ }
|
|
1592
|
+
|
|
1593
|
+
// Quarantine labels have their own identity parity: equal aggregate counts can still describe
|
|
1594
|
+
// disjoint dzIds. Informational only; the lexical store is authoritative and reindex projects it.
|
|
1595
|
+
try {
|
|
1596
|
+
const { countLearningStoreRowsReadonly } = await import('./store-counts.js');
|
|
1597
|
+
const parity = countLearningStoreRowsReadonly(root).quarantineTierParity;
|
|
1598
|
+
if (parity !== undefined) {
|
|
1599
|
+
const drift = parity.lexicalOnly + parity.mirrorOnly;
|
|
1600
|
+
checks.push({
|
|
1601
|
+
name: 'quarantine tier parity',
|
|
1602
|
+
ok: true,
|
|
1603
|
+
detail: `quarantine tiers: both ${parity.both} · lexical-only ${parity.lexicalOnly} · mirror-only ${parity.mirrorOnly}` +
|
|
1604
|
+
(drift > 0 ? ' — run dz vector reindex to project' : ''),
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
} catch { /* advisory only — unreadable identities leave the line absent */ }
|
|
1608
|
+
|
|
1520
1609
|
// 10. AQE store integrity (observability item 3/5). `.agentic-qe/integrity-log.jsonl` is the
|
|
1521
1610
|
// best-attested log in this repo — a UserPromptSubmit hook runs a REAL `PRAGMA quick_check`,
|
|
1522
1611
|
// stats the file and scans /proc for the process holding it, so unlike almost every other store
|
package/src/patterns.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { VectorEntry } from './vector-tier.js';
|
|
|
32
32
|
import { rankLessonsByDelta, type LessonHistory } from './safla-delta.js';
|
|
33
33
|
import { withStoreLock, withStoreLockSync, StoreLockTimeoutError, StoreLockCompromisedError } from './store-lock.js';
|
|
34
34
|
import { describeNativeDep, exerciseSqliteOpen, probeNativeDep } from './native-dep-probe.js';
|
|
35
|
+
import { classifySqliteReadFailure, warnOnce } from './sqlite-read-helpers.js';
|
|
35
36
|
import {
|
|
36
37
|
lessonPairIdOf,
|
|
37
38
|
mergeLessonFormHits,
|
|
@@ -619,9 +620,18 @@ function loadStoreRecordsSync(projectRoot: string): MemoryRecord[] {
|
|
|
619
620
|
// SQLite tier (only if the db file exists — avoid creating one on a pure read)
|
|
620
621
|
if (sqliteBackend !== 'json' && existsSync(sqlitePath(projectRoot))) {
|
|
621
622
|
try {
|
|
622
|
-
const db = SqliteBackend.
|
|
623
|
+
const db = SqliteBackend.openReadOnly(sqlitePath(projectRoot));
|
|
623
624
|
try { for (const r of db.allSync()) add(r); } finally { db.close(); }
|
|
624
|
-
} catch {
|
|
625
|
+
} catch (err) {
|
|
626
|
+
// FR-2 (readonly-residuals): native-unavailable stays a silent JSON fallback (unchanged
|
|
627
|
+
// behaviour); store-unreadable (a corrupt/broken store) warns once instead of silently
|
|
628
|
+
// looking like "fewer lessons".
|
|
629
|
+
if (classifySqliteReadFailure(err) === 'store-unreadable') {
|
|
630
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
631
|
+
const path = sqlitePath(projectRoot);
|
|
632
|
+
warnOnce(path, `dz: ${path} unreadable (${cause}) — falling back to the JSON store`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
625
635
|
}
|
|
626
636
|
// JSON store (the deterministic fallback; also the source during migration)
|
|
627
637
|
try {
|
|
@@ -1266,7 +1276,7 @@ export function recallPatterns(
|
|
|
1266
1276
|
const { sqliteBackend } = readLearningConfig(projectRoot);
|
|
1267
1277
|
if (sqliteBackend !== 'json' && existsSync(sqlitePath(projectRoot))) {
|
|
1268
1278
|
try {
|
|
1269
|
-
const db = SqliteBackend.
|
|
1279
|
+
const db = SqliteBackend.openReadOnly(sqlitePath(projectRoot));
|
|
1270
1280
|
try {
|
|
1271
1281
|
if (!classFormIndexPresent(projectRoot)) {
|
|
1272
1282
|
return sinkQuarantined(
|
|
@@ -1283,7 +1293,16 @@ export function recallPatterns(
|
|
|
1283
1293
|
} finally {
|
|
1284
1294
|
db.close();
|
|
1285
1295
|
}
|
|
1286
|
-
} catch {
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
// FR-2 (readonly-residuals): same classify-then-warn-once as loadStoreRecordsSync — a
|
|
1298
|
+
// native-unavailable failure keeps the silent JSON fallback; a store-unreadable failure
|
|
1299
|
+
// warns once instead of silently looking like "fewer lessons".
|
|
1300
|
+
if (classifySqliteReadFailure(err) === 'store-unreadable') {
|
|
1301
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
const path = sqlitePath(projectRoot);
|
|
1303
|
+
warnOnce(path, `dz: ${path} unreadable (${cause}) — falling back to the JSON store`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1287
1306
|
}
|
|
1288
1307
|
try {
|
|
1289
1308
|
const backend = JsonFileBackend.openSync(storePath(projectRoot));
|
package/src/repository-origin.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
// Points at the public mirror repo (git+https://github.com/djd1m/dz-harness.git), not the private
|
|
2
|
+
// dz-harness-hub monorepo. Decision + rationale: docs/public-mirror-plan.md (2026-08-29) and
|
|
3
|
+
// feature-adr npm-readme-install-first (2026-09-13, FR-5, owner approval "возвращать" 07:05 UTC).
|
|
4
|
+
// A prior "hygiene" pass (278debac, 2026-09-10) flipped this to the private origin because the
|
|
5
|
+
// decision lived only in prose; package-repository-field.test.ts now cites this same trail so a
|
|
6
|
+
// future pass reads the reason before reverting it again (teach:a05c61bb).
|
|
7
|
+
export const REPOSITORY_ORIGIN = 'git+https://github.com/djd1m/dz-harness.git';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Pure parsing and classification for one `dz round exec` subprocess receipt. */
|
|
2
|
+
|
|
3
|
+
export type RoundExecOutcome =
|
|
4
|
+
| 'done'
|
|
5
|
+
| 'timeout'
|
|
6
|
+
| 'session-limit'
|
|
7
|
+
| 'model-refused'
|
|
8
|
+
| 'failed'
|
|
9
|
+
| 'empty';
|
|
10
|
+
|
|
11
|
+
export interface RoundExecLedgerRow {
|
|
12
|
+
readonly stage: 'round-exec';
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
readonly round: number;
|
|
15
|
+
readonly coder: string;
|
|
16
|
+
readonly minutes: number;
|
|
17
|
+
readonly tokens: number | null;
|
|
18
|
+
readonly agents: 1;
|
|
19
|
+
readonly outcome: RoundExecOutcome;
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly bytes: number;
|
|
22
|
+
readonly startedAt: string;
|
|
23
|
+
readonly endedAt: string;
|
|
24
|
+
readonly log: string;
|
|
25
|
+
readonly brief: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseCodexTokens(logText: string): number | null {
|
|
29
|
+
const matches = [...logText.matchAll(/^tokens used[ \t]*\r?\n([0-9][0-9, \t]*)[ \t]*$/gmi)];
|
|
30
|
+
const raw = matches.at(-1)?.[1];
|
|
31
|
+
if (raw === undefined) return null;
|
|
32
|
+
const parsed = Number(raw.replace(/[ ,\t]/g, ''));
|
|
33
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function classifyRoundExecOutcome(input: {
|
|
37
|
+
readonly exitCode: number | null;
|
|
38
|
+
readonly timedOut: boolean;
|
|
39
|
+
readonly bytes: number;
|
|
40
|
+
readonly tail: string;
|
|
41
|
+
}): RoundExecOutcome {
|
|
42
|
+
if (input.timedOut) return 'timeout';
|
|
43
|
+
if (/rate limit|usage limit|limit reached/i.test(input.tail)) return 'session-limit';
|
|
44
|
+
if (/HTTP 400|not supported when using Codex/i.test(input.tail)) return 'model-refused';
|
|
45
|
+
if (input.exitCode === 0 && input.bytes > 0) {
|
|
46
|
+
const marker = input.tail.lastIndexOf('\ncodex\n');
|
|
47
|
+
const finalLine = marker < 0 ? '' : input.tail.slice(marker + '\ncodex\n'.length).split(/\r?\n/, 1)[0]?.trim() ?? '';
|
|
48
|
+
if (finalLine !== '') return 'done';
|
|
49
|
+
}
|
|
50
|
+
if (input.exitCode === 0 && input.bytes === 0) return 'empty';
|
|
51
|
+
return 'failed';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildRoundExecRow(input: {
|
|
55
|
+
readonly slug: string;
|
|
56
|
+
readonly round: number;
|
|
57
|
+
readonly model: string;
|
|
58
|
+
readonly effort: string;
|
|
59
|
+
readonly minutes: number;
|
|
60
|
+
readonly tokens: number | null;
|
|
61
|
+
readonly outcome: RoundExecOutcome;
|
|
62
|
+
readonly exitCode: number | null;
|
|
63
|
+
readonly bytes: number;
|
|
64
|
+
readonly startedAt: string;
|
|
65
|
+
readonly endedAt: string;
|
|
66
|
+
readonly log: string;
|
|
67
|
+
readonly brief: string;
|
|
68
|
+
}): RoundExecLedgerRow {
|
|
69
|
+
return {
|
|
70
|
+
stage: 'round-exec',
|
|
71
|
+
slug: input.slug,
|
|
72
|
+
round: input.round,
|
|
73
|
+
coder: `codex:${input.model}:${input.effort}`,
|
|
74
|
+
minutes: input.minutes,
|
|
75
|
+
tokens: input.tokens,
|
|
76
|
+
agents: 1,
|
|
77
|
+
outcome: input.outcome,
|
|
78
|
+
exitCode: input.exitCode,
|
|
79
|
+
bytes: input.bytes,
|
|
80
|
+
startedAt: input.startedAt,
|
|
81
|
+
endedAt: input.endedAt,
|
|
82
|
+
log: input.log,
|
|
83
|
+
brief: input.brief,
|
|
84
|
+
};
|
|
85
|
+
}
|