@dzhechkov/harness-core 0.8.46 → 0.8.48
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 +200 -168
- package/README.md +130 -2
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +32 -2
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-snapshot.d.ts +14 -1
- package/dist/agentdb-snapshot.d.ts.map +1 -1
- package/dist/agentdb-snapshot.js +100 -4
- package/dist/agentdb-snapshot.js.map +1 -1
- package/dist/apply-leg.d.ts +9 -1
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +26 -4
- package/dist/apply-leg.js.map +1 -1
- package/dist/architecture.d.ts +0 -5
- package/dist/architecture.d.ts.map +1 -1
- package/dist/architecture.js +25 -2
- package/dist/architecture.js.map +1 -1
- package/dist/codex-rollouts.d.ts +35 -7
- package/dist/codex-rollouts.d.ts.map +1 -1
- package/dist/codex-rollouts.js +201 -113
- package/dist/codex-rollouts.js.map +1 -1
- package/dist/cost-ledger.d.ts +42 -3
- package/dist/cost-ledger.d.ts.map +1 -1
- package/dist/cost-ledger.js +478 -15
- package/dist/cost-ledger.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +16 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +21 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard.d.ts +7 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +48 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-plan.d.ts +4 -0
- package/dist/loop-plan.d.ts.map +1 -1
- package/dist/loop-plan.js +4 -0
- package/dist/loop-plan.js.map +1 -1
- package/dist/mutation-gate.d.ts +36 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +109 -19
- package/dist/mutation-gate.js.map +1 -1
- package/dist/npm-homepage.d.ts +64 -0
- package/dist/npm-homepage.d.ts.map +1 -0
- package/dist/npm-homepage.js +109 -0
- package/dist/npm-homepage.js.map +1 -0
- package/dist/operations.d.ts +6 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +150 -24
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts.map +1 -1
- package/dist/pack-inventory.js +6 -1
- package/dist/pack-inventory.js.map +1 -1
- package/dist/parity.d.ts.map +1 -1
- package/dist/parity.js +4 -1
- package/dist/parity.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +43 -1
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +269 -13
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +5 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +18 -1
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +63 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +82 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release-package-audit.d.ts +86 -0
- package/dist/release-package-audit.d.ts.map +1 -0
- package/dist/release-package-audit.js +272 -0
- package/dist/release-package-audit.js.map +1 -0
- package/dist/release.d.ts +8 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +58 -32
- package/dist/release.js.map +1 -1
- package/dist/round.d.ts +10 -1
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +5 -1
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +11 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +124 -20
- package/dist/run-records.js.map +1 -1
- package/dist/setup-memory-deps.d.ts +14 -0
- package/dist/setup-memory-deps.d.ts.map +1 -0
- package/dist/setup-memory-deps.js +210 -0
- package/dist/setup-memory-deps.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +379 -378
- package/dist/setup.js.map +1 -1
- package/dist/stage-usage.d.ts +248 -0
- package/dist/stage-usage.d.ts.map +1 -0
- package/dist/stage-usage.js +511 -0
- package/dist/stage-usage.js.map +1 -0
- package/dist/statusline.d.ts +23 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +167 -1
- package/dist/statusline.js.map +1 -1
- package/dist/workflow-run-dispatch.d.ts +36 -19
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +255 -102
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/dist/workflow-run.d.ts +19 -3
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +93 -2
- package/dist/workflow-run.js.map +1 -1
- package/package.json +3 -3
- package/sbom.json +303 -223
- package/src/agentdb-index.ts +31 -2
- package/src/agentdb-snapshot.ts +105 -4
- package/src/apply-leg.ts +26 -4
- package/src/architecture.ts +15 -2
- package/src/codex-rollouts.ts +155 -147
- package/src/cost-ledger.ts +308 -19
- package/src/feature-adr-routing.ts +22 -0
- package/src/guard.ts +52 -0
- package/src/index.ts +15 -3
- package/src/loop-plan.ts +8 -0
- package/src/mutation-gate.ts +153 -19
- package/src/npm-homepage.ts +142 -0
- package/src/operations.ts +177 -22
- package/src/pack-inventory.ts +6 -1
- package/src/parity.ts +4 -1
- package/src/publish-sibling-drift.ts +214 -11
- package/src/publish.ts +20 -1
- package/src/qe-bridge.ts +86 -1
- package/src/release-package-audit.ts +264 -0
- package/src/release.ts +55 -22
- package/src/round.ts +8 -0
- package/src/run-records.ts +101 -22
- package/src/setup-memory-deps.ts +166 -0
- package/src/setup.ts +95 -98
- package/src/stage-usage.ts +385 -0
- package/src/statusline.ts +149 -1
- package/src/workflow-run-dispatch.ts +197 -93
- package/src/workflow-run.ts +101 -5
- package/dist/ledger-cost-fill.d.ts +0 -58
- package/dist/ledger-cost-fill.d.ts.map +0 -1
- package/dist/ledger-cost-fill.js +0 -78
- package/dist/ledger-cost-fill.js.map +0 -1
- package/dist/retro.d.ts +0 -131
- package/dist/retro.d.ts.map +0 -1
- package/dist/retro.js +0 -207
- package/dist/retro.js.map +0 -1
- package/dist/sbom.d.ts +0 -42
- package/dist/sbom.d.ts.map +0 -1
- package/dist/sbom.js +0 -120
- package/dist/sbom.js.map +0 -1
package/src/run-records.ts
CHANGED
|
@@ -13,11 +13,28 @@
|
|
|
13
13
|
* Pure: payload in, verdict out. The CLI owns paths, the append, the read-back and the exit code.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { matchCodexRollouts } from './codex-rollouts.js';
|
|
17
18
|
import type { CodexRollout } from './codex-rollouts.js';
|
|
18
|
-
import { redactTrainingPayload } from './feature-adr-checkpoints.js';
|
|
19
|
+
import { fnv1a64, redactTrainingPayload } from './feature-adr-checkpoints.js';
|
|
19
20
|
import { validateExperimentEnvelope } from './feature-adr-envelope.js';
|
|
20
21
|
|
|
22
|
+
// Capture contract: ordered named fields, with absent distinct from explicit null.
|
|
23
|
+
// Kept private at producer/reader boundaries; both use this exact sha256 representation.
|
|
24
|
+
function capturedPayload(evidence: Record<string, unknown>): string {
|
|
25
|
+
const value = (v: unknown) => v === undefined ? { absent: true } : v;
|
|
26
|
+
const receipts = Array.isArray(evidence['receipts']) ? evidence['receipts'] : [];
|
|
27
|
+
return JSON.stringify([
|
|
28
|
+
...['schema', 'sessionId', 'turnId', 'sourcePath', 'matchBasis', 'capturedFrom', 'capturedTo', 'model', 'cwd', 'reportedTotalBasis', 'inputCacheSemantics'].map((k) => [k, value(evidence[k])]),
|
|
29
|
+
['owner', ...['runId', 'taskId', 'stage', 'attempt', 'role'].map((k) => [k, value((evidence['owner'] as Record<string, unknown> | undefined)?.[k])])],
|
|
30
|
+
receipts.map((raw) => {
|
|
31
|
+
const r = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {}; const t = r['totals'] as Record<string, unknown> | undefined;
|
|
32
|
+
return [...['key', 'responseId', 'turnId', 'turnIndex', 'timestamp', 'payloadDigest', 'source'].map((k) => [k, value(r[k])]),
|
|
33
|
+
['totals', ...['input', 'output', 'cachedInput', 'cachedWrite', 'reasoning', 'total'].map((k) => [k, value(t?.[k])])]];
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
/** Structural — a caller passes `cost-scoring.ts`'s `ModelPricing`; kept local so `run-records.ts`
|
|
22
39
|
* does not have to import `cost-scoring.ts` just to name a type.
|
|
23
40
|
*
|
|
@@ -86,6 +103,44 @@ export function parseModelSpec(spec: unknown): ParsedModelSpec | null {
|
|
|
86
103
|
return null;
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
/** Internal shared ledger authority resolver; not a package-public barrel API.
|
|
107
|
+
* Capture/source values are comparison targets and never participate in claimant resolution. */
|
|
108
|
+
export function resolveLedgerModelProvenance(row: Readonly<Record<string, unknown>>) {
|
|
109
|
+
const diagnostics: string[] = [];
|
|
110
|
+
const canonicalFamily = (v: string) => v === 'openai' ? 'codex' : v;
|
|
111
|
+
const direct = typeof row['model'] === 'string' && row['model'].length <= 128 && /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)?$/.test(row['model']) ? row['model'] : null;
|
|
112
|
+
if (row['model'] != null && direct === null) diagnostics.push('model-invalid-direct');
|
|
113
|
+
const family = typeof row['family'] === 'string' && SPEC_PART.test(row['family']) ? row['family'] : null;
|
|
114
|
+
if (row['family'] != null && family === null) diagnostics.push('model-invalid-family');
|
|
115
|
+
const specs: ParsedModelSpec[] = []; const familyHints: string[] = [];
|
|
116
|
+
for (const key of ['coder', 'reviewer']) {
|
|
117
|
+
const raw = row[key]; if (raw == null) continue;
|
|
118
|
+
if (raw === 'codex' || raw === 'claude') { familyHints.push(raw); continue; }
|
|
119
|
+
const spec = parseModelSpec(raw);
|
|
120
|
+
const parts = typeof raw === 'string' ? raw.trim().split(':') : [];
|
|
121
|
+
if (!spec || !SPEC_PART.test(spec.model) || (parts[0] === 'claude' && parts.length !== 2)) {
|
|
122
|
+
diagnostics.push('model-invalid-executor:' + key); continue;
|
|
123
|
+
}
|
|
124
|
+
specs.push(spec); familyHints.push(spec.family);
|
|
125
|
+
}
|
|
126
|
+
const models = new Set(specs.map((v) => v.model)); const families = new Set(familyHints);
|
|
127
|
+
if (models.size > 1 || families.size > 1) diagnostics.push('model-executor-conflict');
|
|
128
|
+
const unique = models.size === 1 ? specs[0]! : null;
|
|
129
|
+
if (direct !== null && unique && direct !== unique.model) diagnostics.push('model-direct-executor-conflict');
|
|
130
|
+
if (family !== null && [...families].some((v) => canonicalFamily(family) !== v)) diagnostics.push('model-family-conflict');
|
|
131
|
+
const declaredProvenance = row['modelProvenance'];
|
|
132
|
+
const validProvenance = new Set(['caller-recorded', 'executor-spec', 'provider-reported', 'dispatcher-reported', 'probed-request', 'not-recorded']);
|
|
133
|
+
if (declaredProvenance != null && (typeof declaredProvenance !== 'string' || !validProvenance.has(declaredProvenance))) diagnostics.push('model-invalid-provenance');
|
|
134
|
+
if (declaredProvenance === 'executor-spec' && (!unique || (direct !== null && direct !== unique.model))) diagnostics.push('model-provenance-conflict');
|
|
135
|
+
if (declaredProvenance != null && declaredProvenance !== 'executor-spec' && declaredProvenance !== 'not-recorded' && direct === null) diagnostics.push('model-provenance-conflict');
|
|
136
|
+
const conflict = diagnostics.some((d) => /invalid|conflict/.test(d));
|
|
137
|
+
const model = conflict ? null : direct ?? unique?.model ?? null;
|
|
138
|
+
if (model === null && !conflict) diagnostics.push('model-not-recorded');
|
|
139
|
+
const resolvedFamily = family ?? (families.size === 1 ? [...families][0]! : null);
|
|
140
|
+
return { model, family: resolvedFamily, modelProvenance: model === null ? 'not-recorded' : direct !== null
|
|
141
|
+
? typeof declaredProvenance === 'string' ? declaredProvenance : 'caller-recorded' : 'executor-spec', diagnostics };
|
|
142
|
+
}
|
|
143
|
+
|
|
89
144
|
/** measurement-integrity FR-5/FR-6: enrichment the WRITER supplies at write time — the rollout logs
|
|
90
145
|
* it already read (I/O lives in the CLI; this stays pure) and the price table snapshot. Absent
|
|
91
146
|
* entirely ⇒ zero behavior change from before this feature (NFR-1). */
|
|
@@ -93,6 +148,9 @@ export interface LedgerEnrichInput {
|
|
|
93
148
|
/** Parsed Codex rollout logs for the window the CLI read — usually every rollout from the days the
|
|
94
149
|
* window spans. Pure data; the CLI is the one that walked `~/.codex/sessions`. */
|
|
95
150
|
readonly rollouts?: readonly CodexRollout[];
|
|
151
|
+
readonly rolloutId?: string;
|
|
152
|
+
readonly turnId?: string;
|
|
153
|
+
readonly discoveryDiagnostics?: readonly string[];
|
|
96
154
|
/** The stage's own time window — usually [the previous ledger row's `ts`, this write's `ts`], or
|
|
97
155
|
* an explicit `--window-from/--window-to`. Omitted ⇒ no rollout match is even attempted. */
|
|
98
156
|
readonly window?: { readonly from: string; readonly to: string };
|
|
@@ -492,32 +550,29 @@ export function decideRecordWrite(input: {
|
|
|
492
550
|
// one that already has a token figure, is left untouched. The loose `/codex/i` check below only
|
|
493
551
|
// decides whether this row is WORTH TRYING at all.
|
|
494
552
|
const tokensIsNull = stamped['tokens'] === null;
|
|
495
|
-
const looksCodexFamily = isCodexFamily(stamped['coder']) || isCodexFamily(stamped['reviewer']);
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const codexModels = new Set(
|
|
505
|
-
[parseModelSpec(stamped['coder']), parseModelSpec(stamped['reviewer'])]
|
|
506
|
-
.filter((s): s is ParsedModelSpec => s !== null && s.family === 'codex')
|
|
507
|
-
.map((s) => s.model),
|
|
508
|
-
);
|
|
509
|
-
if (codexModels.size !== 1) {
|
|
553
|
+
const looksCodexFamily = isCodexFamily(stamped['coder']) || isCodexFamily(stamped['reviewer']) || stamped['family'] === 'codex' || stamped['family'] === 'openai';
|
|
554
|
+
if (enrich.discoveryDiagnostics?.length) {
|
|
555
|
+
stamped['usageDiagnostics'] = enrich.discoveryDiagnostics;
|
|
556
|
+
stamped['tokensSource'] = 'codex-rollout:source-discovery-unavailable';
|
|
557
|
+
} else if ((tokensIsNull || enrich.rolloutId !== undefined || enrich.turnId !== undefined) && looksCodexFamily) {
|
|
558
|
+
// Use the same authority contract as the readonly source comparison and normalized report.
|
|
559
|
+
// A direct model or unique agreeing executor specs supply authority; a source never does.
|
|
560
|
+
const identity = resolveLedgerModelProvenance(stamped);
|
|
561
|
+
if (identity.model === null || !['codex', 'openai'].includes(identity.family ?? '')) {
|
|
510
562
|
stamped['tokensSource'] = 'codex-rollout:no-model';
|
|
511
|
-
|
|
512
|
-
|
|
563
|
+
stamped['usageDiagnostics'] = [...(Array.isArray(stamped['usageDiagnostics']) ? stamped['usageDiagnostics'] : []), ...identity.diagnostics];
|
|
564
|
+
} else if (enrich.window !== undefined || enrich.rolloutId !== undefined || enrich.turnId !== undefined) {
|
|
565
|
+
const model = identity.model;
|
|
513
566
|
const match = matchCodexRollouts(enrich.rollouts ?? [], {
|
|
514
|
-
from: enrich.window.from,
|
|
515
|
-
to: enrich.window.to,
|
|
567
|
+
...(enrich.window !== undefined ? { from: enrich.window.from, to: enrich.window.to } : {}),
|
|
516
568
|
model,
|
|
569
|
+
...(enrich.rolloutId !== undefined ? { rolloutId: enrich.rolloutId } : {}),
|
|
570
|
+
...(enrich.turnId !== undefined ? { turnId: enrich.turnId } : {}),
|
|
517
571
|
...(enrich.cwd !== undefined ? { cwd: enrich.cwd } : {}),
|
|
518
572
|
});
|
|
519
573
|
if (match.status === 'one') {
|
|
520
|
-
stamped['tokens'] = match.rollout.totals.total;
|
|
574
|
+
if (tokensIsNull || stamped['tokens'] === undefined) stamped['tokens'] = match.rollout.totals.total;
|
|
575
|
+
else if (stamped['tokens'] !== match.rollout.totals.total) stamped['usageDiagnostics'] = ['caller-source-total-mismatch'];
|
|
521
576
|
const startMs = match.rollout.startedAt !== null ? Date.parse(match.rollout.startedAt) : NaN;
|
|
522
577
|
const endMs = match.rollout.endedAt !== null ? Date.parse(match.rollout.endedAt) : NaN;
|
|
523
578
|
// measurement-integrity fix-round-1/F6 (Codex r1 HIGH #6): fill-ONLY-null — an existing
|
|
@@ -528,9 +583,26 @@ export function decideRecordWrite(input: {
|
|
|
528
583
|
}
|
|
529
584
|
stamped['tokensSource'] = 'codex-rollout';
|
|
530
585
|
stamped['rolloutId'] = match.rollout.id;
|
|
586
|
+
const found = match.rollout;
|
|
587
|
+
for (const [key, value] of Object.entries({ tokensTotal: found.totals.total, tokensIn: found.totals.input,
|
|
588
|
+
tokensOut: found.totals.output, tokensCacheRead: found.totals.cachedInput, tokensCacheWrite: found.totals.cachedWrite ?? null,
|
|
589
|
+
tokensReasoning: found.totals.reasoning, reportedTotalBasis: 'raw-inclusive', inputCacheSemantics: 'includes-cache-read-write',
|
|
590
|
+
usageDiagnostics: found.diagnostics ?? [] })) if (stamped[key] === undefined || stamped[key] === null) stamped[key] = value;
|
|
591
|
+
const receipts = (found.receipts ?? []).map((receipt) => ({ ...receipt, key: JSON.stringify([found.id, receipt.key]) }));
|
|
592
|
+
const usageEvidence: Record<string, unknown> = { schema: 'codex-rollout-scope-1', sessionId: found.id, turnId: found.turnId ?? null,
|
|
593
|
+
sourcePath: found.sourcePath ?? null, matchBasis: enrich.rolloutId !== undefined || enrich.turnId !== undefined ? 'exact' : 'legacy-window',
|
|
594
|
+
capturedFrom: found.startedAt, capturedTo: found.endedAt, receipts, model: found.model, cwd: found.cwd,
|
|
595
|
+
owner: Object.fromEntries(['runId', 'taskId', 'stage', 'attempt', 'role'].map((key) => [key, stamped[key] ?? null])),
|
|
596
|
+
reportedTotalBasis: 'raw-inclusive', inputCacheSemantics: 'includes-cache-read-write',
|
|
597
|
+
payloadDigest: fnv1a64(JSON.stringify(receipts.map((r) => [r.key, r.payloadDigest]))) };
|
|
598
|
+
usageEvidence['captureSha256'] = createHash('sha256').update(capturedPayload(usageEvidence)).digest('hex');
|
|
599
|
+
if (JSON.stringify(usageEvidence).length <= 16000) {
|
|
600
|
+
if (stamped['usageEvidence'] === undefined || stamped['usageEvidence'] === null) stamped['usageEvidence'] = usageEvidence;
|
|
601
|
+
} else stamped['usageDiagnostics'] = [...(Array.isArray(stamped['usageDiagnostics']) ? stamped['usageDiagnostics'] : []), 'source-scope-over-record-limit'];
|
|
602
|
+
|
|
531
603
|
} else {
|
|
532
604
|
// `none` or `ambiguous` — NFR-3: an explicit status, never a guessed number.
|
|
533
|
-
stamped['tokensSource'] = `codex-rollout:${match.status}`;
|
|
605
|
+
stamped['tokensSource'] = `codex-rollout:${enrich.rolloutId !== undefined || enrich.turnId !== undefined ? 'exact-' : ''}${match.status}`;
|
|
534
606
|
}
|
|
535
607
|
} else {
|
|
536
608
|
// Eligible in principle (codex family, one reliable model, tokens null) but no window was
|
|
@@ -544,6 +616,7 @@ export function decideRecordWrite(input: {
|
|
|
544
616
|
// above (a Claude row gets priced too; only tokens enrichment is codex-specific).
|
|
545
617
|
if (enrich.prices !== undefined) {
|
|
546
618
|
const modelIds = new Set<string>();
|
|
619
|
+
if (typeof stamped['model'] === 'string' && stamped['model'].trim()) modelIds.add(stamped['model'].trim());
|
|
547
620
|
for (const v of [stamped['coder'], stamped['reviewer']]) {
|
|
548
621
|
if (typeof v === 'string' && v.trim() !== '') modelIds.add(v.trim());
|
|
549
622
|
}
|
|
@@ -566,7 +639,13 @@ export function decideRecordWrite(input: {
|
|
|
566
639
|
if (price === null) unknown.push(modelId);
|
|
567
640
|
else table[modelId] = { prompt: price.prompt, completion: price.completion, cachedInput: price.cachedInput, cacheCreation: price.cacheCreation };
|
|
568
641
|
}
|
|
642
|
+
const matches = Object.fromEntries([...modelIds].map((id) => {
|
|
643
|
+
const parsed = parseModelSpec(id); const normalized = parsed?.family === 'claude' ? 'claude-' + parsed.model : parsed?.model ?? id;
|
|
644
|
+
const key = Object.keys(enrich.prices ?? {}).filter((k) => normalized.toLowerCase().startsWith(k)).sort((a,b) => b.length-a.length)[0] ?? null;
|
|
645
|
+
return [id, { tableKey: key, kind: key !== null && key === normalized && !key.includes('claude') ? 'exact' : key !== null ? 'family-estimate' : 'unknown' }];
|
|
646
|
+
}));
|
|
569
647
|
const computedPrices = {
|
|
648
|
+
matches,
|
|
570
649
|
snapshotAt: input.timestamp ?? null,
|
|
571
650
|
table,
|
|
572
651
|
...(unknown.length > 0 ? { unknown } : {}),
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/** Exact consumer npm persistence and local native readiness. No ancestor/global installs. */
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, rmSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
|
+
|
|
7
|
+
export const MEMORY_DEFAULTS = { agentdb: '3.0.0-alpha.20', 'better-sqlite3': '11.10.0' } as const;
|
|
8
|
+
const names = Object.keys(MEMORY_DEFAULTS) as (keyof typeof MEMORY_DEFAULTS)[];
|
|
9
|
+
const sections = ['dependencies', 'devDependencies', 'optionalDependencies'] as const;
|
|
10
|
+
type Json = Record<string, any>;
|
|
11
|
+
export interface MemoryDependencyResult {
|
|
12
|
+
readonly ready: boolean;
|
|
13
|
+
readonly changedManifest: boolean;
|
|
14
|
+
readonly changedLock: boolean;
|
|
15
|
+
readonly detail: string;
|
|
16
|
+
}
|
|
17
|
+
const object = (value: unknown): value is Json => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
const same = (a: unknown, b: unknown) => isDeepStrictEqual(a, b);
|
|
19
|
+
const bytes = (path: string) => existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
20
|
+
function json(path: string): Json {
|
|
21
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
22
|
+
if (!object(value)) throw Error(`${path}: expected a JSON object`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function supportedMemoryVersion(name: keyof typeof MEMORY_DEFAULTS, value: unknown): value is string {
|
|
26
|
+
const number = '(?:0|[1-9][0-9]*)';
|
|
27
|
+
return typeof value === 'string' && new RegExp(name === 'agentdb'
|
|
28
|
+
? `^(?:3\\.${number}\\.${number}|3\\.0\\.0-alpha\\.${number})$`
|
|
29
|
+
: `^11\\.${number}\\.${number}$`).test(value);
|
|
30
|
+
}
|
|
31
|
+
function installed(root: string, name: string): string | null {
|
|
32
|
+
const path = join(root, 'node_modules', name, 'package.json');
|
|
33
|
+
if (!existsSync(path)) return null;
|
|
34
|
+
const pkg = json(path);
|
|
35
|
+
if (pkg['name'] !== name || typeof pkg['version'] !== 'string') throw Error(`${name}: malformed installed package metadata`);
|
|
36
|
+
return pkg['version'];
|
|
37
|
+
}
|
|
38
|
+
function lockVersion(lock: Json | null, name: string): string | null {
|
|
39
|
+
const item = lock?.['packages']?.[`node_modules/${name}`] ?? lock?.['dependencies']?.[name];
|
|
40
|
+
return item?.version ?? null;
|
|
41
|
+
}
|
|
42
|
+
function selectVersions(root: string, pkg: Json, lock: Json | null): Record<string, string> {
|
|
43
|
+
for (const section of [...sections, 'peerDependencies']) {
|
|
44
|
+
if (pkg[section] !== undefined && !object(pkg[section])) throw Error(`package.json: malformed ${section}`);
|
|
45
|
+
}
|
|
46
|
+
const versions: Record<string, string> = {};
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
if (Object.hasOwn(pkg['peerDependencies'] ?? {}, name)) throw Error(`${name}: peer declaration is outside exact persistence repair`);
|
|
49
|
+
const declared = sections.flatMap(section => Object.hasOwn(pkg[section] ?? {}, name) ? [pkg[section][name]] : []);
|
|
50
|
+
if (declared.some(value => !supportedMemoryVersion(name, value)) || new Set(declared).size > 1) throw Error(`${name}: conflicting or non-exact/unsupported saved declaration; original files preserved`);
|
|
51
|
+
if (declared.length) { versions[name] = declared[0]; continue; }
|
|
52
|
+
const local = installed(root, name), locked = lockVersion(lock, name);
|
|
53
|
+
if (local !== null) {
|
|
54
|
+
if (!supportedMemoryVersion(name, local)) throw Error(`${name}: unsupported installed-only version ${local}`);
|
|
55
|
+
if (locked !== null && locked !== local) throw Error(`${name}: installed-only and orphan lock versions conflict`);
|
|
56
|
+
versions[name] = local;
|
|
57
|
+
} else if (locked !== null) throw Error(`${name}: incomplete legacy lock-only state; original files preserved`);
|
|
58
|
+
else versions[name] = MEMORY_DEFAULTS[name];
|
|
59
|
+
}
|
|
60
|
+
return versions;
|
|
61
|
+
}
|
|
62
|
+
function persisted(pkg: Json, lock: Json | null, versions: Record<string, string>): boolean {
|
|
63
|
+
return names.every(name => pkg['devDependencies']?.[name] === versions[name]
|
|
64
|
+
&& !Object.hasOwn(pkg['dependencies'] ?? {}, name) && !Object.hasOwn(pkg['optionalDependencies'] ?? {}, name)
|
|
65
|
+
&& lockVersion(lock, name) === versions[name] && lock?.['packages']?.['']?.['devDependencies']?.[name] === versions[name]
|
|
66
|
+
&& !Object.hasOwn(lock?.['packages']?.['']?.['dependencies'] ?? {}, name)
|
|
67
|
+
&& !Object.hasOwn(lock?.['packages']?.['']?.['optionalDependencies'] ?? {}, name));
|
|
68
|
+
}
|
|
69
|
+
function protectedManifest(pkg: Json): Json {
|
|
70
|
+
const copy: Json = JSON.parse(JSON.stringify(pkg));
|
|
71
|
+
for (const section of sections) {
|
|
72
|
+
if (!object(copy[section])) continue;
|
|
73
|
+
for (const name of names) delete copy[section][name];
|
|
74
|
+
if (!Object.keys(copy[section]).length) delete copy[section];
|
|
75
|
+
}
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
78
|
+
function resolutions(lock: Json | null): Record<string, string> {
|
|
79
|
+
const result: Record<string, string> = {};
|
|
80
|
+
if (object(lock?.['packages'])) for (const [path, item] of Object.entries(lock['packages'])) {
|
|
81
|
+
if (path && !names.some(name => path === `node_modules/${name}`) && object(item) && typeof item['version'] === 'string') result[path] = item['version'];
|
|
82
|
+
}
|
|
83
|
+
else if (object(lock?.['dependencies'])) for (const [name, item] of Object.entries(lock['dependencies'])) {
|
|
84
|
+
if (!names.includes(name as keyof typeof MEMORY_DEFAULTS) && object(item) && typeof item['version'] === 'string') result[`node_modules/${name}`] = item['version'];
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function npm(root: string, args: string[]): void {
|
|
89
|
+
execFileSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, {
|
|
90
|
+
cwd: root, timeout: 300000, maxBuffer: 8 * 1024 * 1024, stdio: 'pipe', shell: process.platform === 'win32',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function probeMemoryNative(root: string): string | null {
|
|
94
|
+
try {
|
|
95
|
+
for (const name of names) if (installed(root, name) === null) return `${name}: local package missing`;
|
|
96
|
+
const script = `const {createRequire}=require('node:module'),fs=require('node:fs'),path=require('node:path');
|
|
97
|
+
const root=process.argv[1],r=createRequire(path.join(root,'package.json')),local=fs.realpathSync(path.join(root,'node_modules'));
|
|
98
|
+
const resolved=fs.realpathSync(r.resolve('better-sqlite3')); if(path.relative(local,resolved).startsWith('..'))throw Error('native module resolved outside project');
|
|
99
|
+
const Sqlite=r('better-sqlite3'),db=new Sqlite(':memory:');try{if(db.prepare('SELECT 42 AS answer').get().answer!==42)throw Error('native SELECT failed');}finally{db.close();}
|
|
100
|
+
process.stdout.write('native-sql-ready');`;
|
|
101
|
+
const output = execFileSync(process.execPath, ['-e', script, realpathSync(root)], { cwd: root, timeout: 60000, maxBuffer: 4 * 1024 * 1024, stdio: 'pipe', encoding: 'utf8' });
|
|
102
|
+
return output === 'native-sql-ready' ? null : 'native SQL readiness stdout not established';
|
|
103
|
+
} catch (error) { return `native SQLite unavailable: ${error instanceof Error ? error.message : String(error)}`; }
|
|
104
|
+
}
|
|
105
|
+
export function reconcileMemoryDependencies(projectRoot: string, repair = true): MemoryDependencyResult {
|
|
106
|
+
const pkgPath = join(projectRoot, 'package.json'), lockPath = join(projectRoot, 'package-lock.json');
|
|
107
|
+
const beforePkg = bytes(pkgPath), beforeLock = bytes(lockPath);
|
|
108
|
+
let stage = 'validate', repaired = false;
|
|
109
|
+
const result = (ready: boolean, detail: string): MemoryDependencyResult => ({ ready, changedManifest: bytes(pkgPath) !== beforePkg, changedLock: bytes(lockPath) !== beforeLock, detail });
|
|
110
|
+
try {
|
|
111
|
+
const pkg = beforePkg === null ? { name: 'dz-harness-project', private: true, version: '0.0.0' } : json(pkgPath);
|
|
112
|
+
const lock = beforeLock === null ? null : json(lockPath);
|
|
113
|
+
const versions = selectVersions(projectRoot, pkg, lock);
|
|
114
|
+
const localCurrent = names.every(name => installed(projectRoot, name) === versions[name]);
|
|
115
|
+
const savedCurrent = persisted(pkg, lock, versions);
|
|
116
|
+
if (!repair && (!savedCurrent || !localCurrent)) return result(false, 'saved/local dependency state is inconsistent; run setup to repair exact devDependencies');
|
|
117
|
+
const nativeBefore = localCurrent ? probeMemoryNative(projectRoot) : 'local package versions need repair';
|
|
118
|
+
if (!repair && nativeBefore) return result(false, nativeBefore);
|
|
119
|
+
if (!savedCurrent || !localCurrent) {
|
|
120
|
+
stage = 'npm persistence repair';
|
|
121
|
+
const protectedFields = protectedManifest(pkg), protectedLock = resolutions(lock);
|
|
122
|
+
try {
|
|
123
|
+
// npm can retain an existing optional declaration despite --save-dev. Selection and
|
|
124
|
+
// conflicts were validated above; normalize ONLY the two memory declarations first.
|
|
125
|
+
const canonical: Json = JSON.parse(JSON.stringify(pkg));
|
|
126
|
+
for (const section of sections) {
|
|
127
|
+
if (!object(canonical[section])) continue;
|
|
128
|
+
let removed = false;
|
|
129
|
+
for (const name of names) if (Object.hasOwn(canonical[section], name)) { delete canonical[section][name]; removed = true; }
|
|
130
|
+
if (removed && !Object.keys(canonical[section]).length) delete canonical[section];
|
|
131
|
+
}
|
|
132
|
+
canonical['devDependencies'] = { ...(canonical['devDependencies'] ?? {}), ...versions };
|
|
133
|
+
if (beforePkg === null || !same(canonical, pkg)) writeFileSync(pkgPath, JSON.stringify(canonical, null, 2) + '\n');
|
|
134
|
+
npm(projectRoot, ['install', ...names.map(name => `${name}@${versions[name]}`), '--save-dev', '--save-exact', '--no-audit', '--no-fund', '--loglevel=error']);
|
|
135
|
+
const after = json(pkgPath), afterLock = json(lockPath), afterResolutions = resolutions(afterLock);
|
|
136
|
+
if (!same(protectedManifest(after), protectedFields) || Object.entries(protectedLock).some(([path, version]) => afterResolutions[path] !== version)) {
|
|
137
|
+
throw Error('npm changed protected unrelated manifest fields/lock resolutions');
|
|
138
|
+
}
|
|
139
|
+
if (!persisted(after, afterLock, versions) || !names.every(name => installed(projectRoot, name) === versions[name])) throw Error('npm did not establish requested exact manifest/lock/local versions');
|
|
140
|
+
repaired = true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
// npm is not a whole-project transaction. Restore only protected manifest/lock files.
|
|
143
|
+
let changedProtected = false;
|
|
144
|
+
try {
|
|
145
|
+
const after = existsSync(pkgPath) ? json(pkgPath) : {};
|
|
146
|
+
const afterResolutions = resolutions(existsSync(lockPath) ? json(lockPath) : null);
|
|
147
|
+
changedProtected = !same(protectedManifest(after), protectedFields) || Object.entries(protectedLock).some(([path, version]) => afterResolutions[path] !== version);
|
|
148
|
+
} catch { changedProtected = true; }
|
|
149
|
+
if (changedProtected) {
|
|
150
|
+
if (beforePkg === null) rmSync(pkgPath, { force: true }); else writeFileSync(pkgPath, beforePkg);
|
|
151
|
+
if (beforeLock === null) rmSync(lockPath, { force: true }); else writeFileSync(lockPath, beforeLock);
|
|
152
|
+
throw Error(`protected manifest/lock restored after npm changed unrelated state; local modules may be incomplete: ${String(error)}`);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
} else if (repair && nativeBefore) {
|
|
157
|
+
stage = 'native rebuild'; npm(projectRoot, ['rebuild', 'better-sqlite3', '--no-audit', '--no-fund', '--loglevel=error']); repaired = true;
|
|
158
|
+
}
|
|
159
|
+
stage = 'native SQL probe';
|
|
160
|
+
const nativeError = probeMemoryNative(projectRoot);
|
|
161
|
+
if (nativeError) return result(false, `INCOMPLETE ${stage}: ${nativeError}; package.json ${bytes(pkgPath) === beforePkg ? 'unchanged' : 'changed'}, package-lock.json ${bytes(lockPath) === beforeLock ? 'unchanged' : 'changed'}`);
|
|
162
|
+
return result(true, `${repaired ? 'repaired exact devDependencies/local readiness' : 'already current exact devDependencies and native SQLite'}; package.json ${bytes(pkgPath) === beforePkg ? 'unchanged' : 'changed'}, package-lock.json ${bytes(lockPath) === beforeLock ? 'unchanged' : 'changed'}; ${names.map(name => `${name}@${versions[name]}`).join(', ')}; native SELECT 42 passed`);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return result(false, `INCOMPLETE ${stage}: ${error instanceof Error ? error.message : String(error)}; package.json ${bytes(pkgPath) === beforePkg ? 'unchanged' : 'changed'}, package-lock.json ${bytes(lockPath) === beforeLock ? 'unchanged' : 'changed'}`);
|
|
165
|
+
}
|
|
166
|
+
}
|